Skip to content

Commit 7757a02

Browse files
dorlugasigalCopilot
andcommitted
fix(mobile): stabilize bottom touchbar across iOS PWA orientation changes
- useMobileKeyboard: add width-based rotation detection to prevent visualViewport resize during rotation from being mistaken for keyboard events (iOS PWA orientationchange is unreliable) - TouchBar: simplify to fixed bottom: 0 + fixed height; drop env() lift (env(safe-area-inset-bottom) returns 0 post-rotation in iOS PWA) - TerminalApp: reserve only bar height in terminalArea; remove safe-bottom component from layout calcs - Add --safe-bottom CSS var (default 0) for cross-screen consistency Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b167254 commit 7757a02

8 files changed

Lines changed: 67 additions & 61 deletions

File tree

src/frontend/src/components/CopilotPane/CopilotPane.module.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -635,7 +635,7 @@
635635
.inputBar {
636636
flex-shrink: 0;
637637
padding: 8px 16px;
638-
padding-bottom: calc(8px + env(safe-area-inset-bottom, 0px));
638+
padding-bottom: calc(8px + var(--safe-bottom));
639639
background: var(--bg);
640640
}
641641

src/frontend/src/components/ResumeBrowser/ResumeBrowser.module.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@
124124
padding: 0 16px;
125125
padding-left: max(16px, env(safe-area-inset-left, 0));
126126
padding-right: max(16px, env(safe-area-inset-right, 0));
127-
padding-bottom: max(20px, env(safe-area-inset-bottom, 0));
127+
padding-bottom: max(20px, var(--safe-bottom));
128128
display: flex;
129129
flex-direction: column;
130130
gap: 8px;

src/frontend/src/components/SessionsHub/SessionsHub.module.css

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@
197197
display: flex;
198198
flex-direction: column;
199199
padding: 16px;
200-
padding-bottom: calc(80px + min(env(safe-area-inset-bottom, 0px), 2px));
200+
padding-bottom: calc(82px + var(--safe-bottom));
201201
gap: 12px;
202202
}
203203

@@ -242,11 +242,10 @@
242242

243243
.hubFooter {
244244
position: fixed;
245-
bottom: 0;
245+
bottom: var(--safe-bottom);
246246
left: 0;
247247
right: 0;
248-
padding: 8px calc(16px + env(safe-area-inset-left, 0px))
249-
calc(8px + min(env(safe-area-inset-bottom, 0px), 2px))
248+
padding: 4px calc(16px + env(safe-area-inset-left, 0px)) 4px
250249
calc(16px + env(safe-area-inset-right, 0px));
251250
background: var(--bg);
252251
display: flex;

src/frontend/src/components/SidePanel/SidePanel.module.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
overflow: hidden;
2525
padding-top: env(safe-area-inset-top, 0px);
2626
padding-left: env(safe-area-inset-left, 0px);
27-
padding-bottom: min(env(safe-area-inset-bottom, 0px), 2px);
27+
padding-bottom: var(--safe-bottom);
2828
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
2929
animation: slide-in-left 0.25s ease-out;
3030
}

src/frontend/src/components/TerminalApp/TerminalApp.module.css

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -180,11 +180,7 @@
180180
.terminalArea {
181181
position: absolute;
182182
top: calc(40px + env(safe-area-inset-top, 0px));
183-
/* TouchBar height (71px) + capped home-indicator gap + keyboard height.
184-
iOS Safari does not shrink the layout viewport when the keyboard opens,
185-
so we must explicitly offset by --keyboard-height (set by
186-
useMobileKeyboard via visualViewport). */
187-
bottom: calc(71px + min(env(safe-area-inset-bottom, 0px), 2px) + var(--keyboard-height, 0px));
183+
bottom: calc(75px + var(--keyboard-height, 0px));
188184
left: 0;
189185
right: 0;
190186
display: flex;
@@ -195,14 +191,13 @@
195191
/* When the keyboard is open, the touchbar's home-indicator padding is
196192
covered by the keyboard, so the terminal can sit directly on top of the
197193
71px touchbar core (above the keyboard). */
194+
/* Keyboard open: bar drops to sit on keyboard (no safe-area lift). */
198195
.layout[data-keyboard-open] .terminalArea {
199-
bottom: calc(71px + var(--keyboard-height, 0px));
196+
bottom: calc(75px + var(--keyboard-height, 0px));
200197
}
201198

202-
/* Copilot chat hides the TouchBar — reclaim its space (no keyboard offset
203-
needed because TerminalPane handles its own resize/scroll-into-view). */
204199
.layout[data-hide-touchbar] .terminalArea {
205-
bottom: calc(min(env(safe-area-inset-bottom, 0px), 2px) + var(--keyboard-height, 0px));
200+
bottom: var(--keyboard-height, 0px);
206201
}
207202

208203
.split {
@@ -269,14 +264,11 @@
269264
}
270265
.terminalArea {
271266
top: calc(32px + env(safe-area-inset-top, 0px));
272-
bottom: calc(44px + min(env(safe-area-inset-bottom, 0px), 2px) + var(--keyboard-height, 0px));
267+
bottom: calc(48px + var(--keyboard-height, 0px));
273268
}
274-
/* TouchBar hides when keyboard is open in landscape — sit directly on
275-
top of the keyboard, no touchbar offset. */
276269
.layout[data-keyboard-open] .terminalArea {
277270
bottom: var(--keyboard-height, 0px);
278271
}
279-
/* Copilot chat hides TouchBar — reclaim its space */
280272
.layout[data-hide-touchbar] .terminalArea {
281273
bottom: var(--keyboard-height, 0px);
282274
}

src/frontend/src/components/TouchBar/TouchBar.module.css

Lines changed: 11 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
left: 0;
99
right: 0;
1010
z-index: 50;
11-
/* Compact bar height + capped home-indicator gap (saves screen real estate). */
12-
height: calc(71px + min(env(safe-area-inset-bottom, 0px), 2px));
13-
padding: 3px 4px;
14-
padding-bottom: min(env(safe-area-inset-bottom, 0px), 2px);
11+
/* Compact bar — fixed height. iOS home-indicator strip below is left
12+
visible (env() is unreliable in PWA standalone post-rotation). */
13+
height: 75px;
14+
padding: 3px 4px 4px;
1515
transition: bottom 0.15s ease-out;
1616
background: linear-gradient(to bottom, var(--bg) 0%, var(--surface) 100%);
1717
display: flex;
@@ -24,29 +24,10 @@
2424
overscroll-behavior: none;
2525
}
2626

27-
/* Backfill the area below the TouchBar (the keyboard zone) with --bg so it
28-
blends with the dark iOS keyboard chrome. Without this the body bg can
29-
leak through gaps around the iOS accessory bar. */
30-
.touchBar::after {
31-
content: '';
32-
position: absolute;
33-
top: 100%;
34-
left: 0;
35-
right: 0;
36-
height: var(--keyboard-height, 0px);
37-
background: var(--bg);
38-
pointer-events: none;
39-
}
40-
41-
/* When the keyboard is open, drop the gradient and the keyboard-area
42-
backfill — the body bg already paints the area dark, and a plain
43-
transparent TouchBar lets the buttons (with their drop shadow) read
44-
as floating chips above the keyboard. */
27+
/* Keyboard open: bar stays put, no special handling needed. */
4528
:global([data-keyboard-open]) .touchBar {
4629
background: transparent;
47-
}
48-
:global([data-keyboard-open]) .touchBar::after {
49-
display: none;
30+
bottom: var(--keyboard-height, 0px);
5031
}
5132

5233
.row {
@@ -70,10 +51,10 @@
7051
justify-content: center;
7152
cursor: pointer;
7253
box-shadow:
73-
inset 0 1px 0 rgba(255, 255, 255, 0.06),
54+
inset 0 1px 0 rgba(255, 255, 255, 0.08),
7455
0 1px 0 var(--key-shadow),
75-
0 2px 6px rgba(0, 0, 0, 0.45),
76-
0 4px 12px rgba(0, 0, 0, 0.35);
56+
0 2px 3px rgba(0, 0, 0, 0.55),
57+
0 3px 6px rgba(0, 0, 0, 0.4);
7758
-webkit-tap-highlight-color: transparent;
7859
font-family: inherit;
7960
transition: background 0.08s;
@@ -214,9 +195,8 @@
214195
/* Landscape on short screens: single compact row */
215196
@media (orientation: landscape) and (max-height: 500px) {
216197
.touchBar {
217-
height: calc(44px + min(env(safe-area-inset-bottom, 0px), 2px));
218-
padding: 4px 4px;
219-
padding-bottom: min(env(safe-area-inset-bottom, 0px), 2px);
198+
height: 48px;
199+
padding: 4px;
220200
}
221201
.keyBtn {
222202
height: 28px;

src/frontend/src/hooks/useMobileKeyboard.ts

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,42 @@ export function useMobileKeyboard(): MobileKeyboardState {
2323
if (!vv) return;
2424

2525
baseHeightRef.current = vv.height;
26+
let baseWidth = vv.width;
27+
28+
// While a rotation is in flight, vv.height changes by hundreds of pixels
29+
// and would otherwise be mistaken for a keyboard opening. Suppress resize
30+
// events during this window and force a clean baseline afterward.
31+
let rotating = false;
32+
let rotationTimer: ReturnType<typeof setTimeout> | undefined;
33+
34+
function startRotation() {
35+
rotating = true;
36+
// Force a clean state immediately so any stale --keyboard-height clears.
37+
setState({ keyboardOpen: false, keyboardHeight: 0 });
38+
clearTimeout(rotationTimer);
39+
rotationTimer = setTimeout(() => {
40+
baseHeightRef.current = vv!.height;
41+
baseWidth = vv!.width;
42+
rotating = false;
43+
// Re-evaluate after rotation settles in case a real keyboard is open.
44+
const diff = baseHeightRef.current - vv!.height;
45+
const isOpen = diff > KEYBOARD_THRESHOLD;
46+
setState({
47+
keyboardOpen: isOpen,
48+
keyboardHeight: isOpen ? diff : 0,
49+
});
50+
}, 500);
51+
}
2652

2753
function onResize() {
54+
// Width change ⇒ this is a rotation, not a keyboard event. iOS PWA
55+
// doesn't reliably fire orientationchange, so detect it from the
56+
// viewport directly.
57+
if (vv!.width !== baseWidth) {
58+
startRotation();
59+
return;
60+
}
61+
if (rotating) return;
2862
const currentHeight = vv!.height;
2963
const diff = baseHeightRef.current - currentHeight;
3064
const isOpen = diff > KEYBOARD_THRESHOLD;
@@ -34,16 +68,8 @@ export function useMobileKeyboard(): MobileKeyboardState {
3468
});
3569
}
3670

37-
// On orientation change, the viewport height changes without a keyboard
38-
// event. Reset the baseline so the diff calculation stays correct.
39-
let orientationTimer: ReturnType<typeof setTimeout> | undefined;
4071
function onOrientationChange() {
41-
// Short delay — browsers need a frame to settle the new viewport size
42-
clearTimeout(orientationTimer);
43-
orientationTimer = setTimeout(() => {
44-
baseHeightRef.current = vv!.height;
45-
setState({ keyboardOpen: false, keyboardHeight: 0 });
46-
}, 200);
72+
startRotation();
4773
}
4874

4975
vv.addEventListener('resize', onResize);
@@ -56,7 +82,7 @@ export function useMobileKeyboard(): MobileKeyboardState {
5682
window.addEventListener('orientationchange', onOrientationChange);
5783

5884
return () => {
59-
clearTimeout(orientationTimer);
85+
clearTimeout(rotationTimer);
6086
vv.removeEventListener('resize', onResize);
6187
if (orientation) {
6288
orientation.removeEventListener('change', onOrientationChange);

src/frontend/src/styles/global.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@
66
padding: 0;
77
}
88

9+
:root {
10+
/* Bottom safe zone for iOS home-indicator. Default 0; useSafeArea()
11+
populates this with the cached env(safe-area-inset-bottom) value so
12+
the iOS PWA rotation bug (env() returning 0 post-rotation) can't
13+
collapse it. */
14+
--safe-bottom: 0px;
15+
--safe-top: 0px;
16+
}
17+
918
html,
1019
body {
1120
height: 100vh;

0 commit comments

Comments
 (0)