fix: improve Android tablet keyboard-safe layout
Use dynamic viewport sizing and keyboard-aware spacing to keep new-game inputs and navigation reachable in PWA landscape mode on Android tablets. Refs #30. Made-with: Cursor
This commit is contained in:
@@ -33,6 +33,35 @@ export default function App() {
|
||||
const validationModal = useValidationModal();
|
||||
const completionModal = useCompletionModal();
|
||||
|
||||
// Keep viewport height stable on Android tablets when virtual keyboard opens.
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
|
||||
const updateViewportVars = () => {
|
||||
const viewport = window.visualViewport;
|
||||
const appHeight = viewport ? viewport.height : window.innerHeight;
|
||||
const keyboardOffset = viewport
|
||||
? Math.max(0, window.innerHeight - viewport.height - viewport.offsetTop)
|
||||
: 0;
|
||||
|
||||
root.style.setProperty('--app-height', `${Math.round(appHeight)}px`);
|
||||
root.style.setProperty('--keyboard-offset', `${Math.round(keyboardOffset)}px`);
|
||||
};
|
||||
|
||||
updateViewportVars();
|
||||
window.addEventListener('resize', updateViewportVars);
|
||||
window.addEventListener('orientationchange', updateViewportVars);
|
||||
window.visualViewport?.addEventListener('resize', updateViewportVars);
|
||||
window.visualViewport?.addEventListener('scroll', updateViewportVars);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateViewportVars);
|
||||
window.removeEventListener('orientationchange', updateViewportVars);
|
||||
window.visualViewport?.removeEventListener('resize', updateViewportVars);
|
||||
window.visualViewport?.removeEventListener('scroll', updateViewportVars);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Game lifecycle handlers
|
||||
const handleCreateGame = useCallback(async (gameData: any) => {
|
||||
try {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
min-height: var(--app-height);
|
||||
display: none;
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
.screen-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
min-height: var(--app-height);
|
||||
padding: var(--space-lg);
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
min-height: var(--app-height);
|
||||
display: none;
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
@@ -145,8 +145,10 @@
|
||||
|
||||
.form-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding: 0 var(--space-lg);
|
||||
padding-bottom: var(--space-md);
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -154,7 +156,12 @@
|
||||
|
||||
.form-footer {
|
||||
flex-shrink: 0;
|
||||
padding: var(--space-lg);
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 2;
|
||||
background: var(--color-surface);
|
||||
padding: var(--space-md) var(--space-lg) calc(var(--space-lg) + var(--keyboard-offset)) var(--space-lg);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
.progress-indicator {
|
||||
display: flex;
|
||||
@@ -210,7 +217,7 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: var(--space-xxl);
|
||||
margin-top: var(--space-lg);
|
||||
width: 100%;
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
@@ -111,8 +111,15 @@ export const Player1Step = ({ playerNameHistory, onNext, onCancel, initialValue
|
||||
}
|
||||
}}
|
||||
autoComplete="off"
|
||||
autoCapitalize="words"
|
||||
spellCheck={false}
|
||||
enterKeyHint="next"
|
||||
aria-label="Name Spieler 1"
|
||||
aria-describedby="player1-help"
|
||||
onFocus={(e) => {
|
||||
const target = e.currentTarget as HTMLInputElement;
|
||||
target.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||
}}
|
||||
style={{
|
||||
fontSize: UI_CONSTANTS.INPUT_FONT_SIZE,
|
||||
minHeight: UI_CONSTANTS.INPUT_MIN_HEIGHT,
|
||||
|
||||
@@ -70,7 +70,14 @@ export const Player2Step = ({ playerNameHistory, onNext, onCancel, initialValue
|
||||
setPlayer2(target.value);
|
||||
}}
|
||||
autoComplete="off"
|
||||
autoCapitalize="words"
|
||||
spellCheck={false}
|
||||
enterKeyHint="next"
|
||||
aria-label="Name Spieler 2"
|
||||
onFocus={(e) => {
|
||||
const target = e.currentTarget as HTMLInputElement;
|
||||
target.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||
}}
|
||||
style={{ fontSize: '1.2rem', minHeight: 48, marginTop: 12, marginBottom: 12, width: '100%', paddingRight: 44 }}
|
||||
ref={inputRef}
|
||||
/>
|
||||
|
||||
@@ -67,7 +67,14 @@ export const Player3Step = ({ playerNameHistory, onNext, onCancel, initialValue
|
||||
setPlayer3(target.value);
|
||||
}}
|
||||
autoComplete="off"
|
||||
autoCapitalize="words"
|
||||
spellCheck={false}
|
||||
enterKeyHint="done"
|
||||
aria-label="Name Spieler 3"
|
||||
onFocus={(e) => {
|
||||
const target = e.currentTarget as HTMLInputElement;
|
||||
target.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||
}}
|
||||
style={{ fontSize: '1.2rem', minHeight: 48, marginTop: 12, marginBottom: 12, width: '100%', paddingRight: 44 }}
|
||||
ref={inputRef}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.layout {
|
||||
height: 100vh;
|
||||
height: var(--app-height);
|
||||
overflow: hidden;
|
||||
background-color: var(--color-background);
|
||||
color: var(--color-text);
|
||||
|
||||
@@ -8,7 +8,7 @@ import App from "../components/App";
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content">
|
||||
<title>BSC Score - Pool Scoring App</title>
|
||||
<meta name="description" content="Professional pool/billiards scoring application for tournaments and casual games">
|
||||
|
||||
|
||||
+10
-7
@@ -3,17 +3,13 @@
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* Design system tokens */
|
||||
:root {
|
||||
--app-height: 100dvh;
|
||||
--keyboard-offset: 0px;
|
||||
/* Colors */
|
||||
--color-primary: #ff9800;
|
||||
--color-primary-hover: #ffa726;
|
||||
@@ -87,7 +83,7 @@
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -98,6 +94,13 @@ body {
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
min-height: var(--app-height);
|
||||
}
|
||||
|
||||
button, .btn, .button {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
/* Improved input styling for better tablet experience */
|
||||
|
||||
Reference in New Issue
Block a user