Merge pull request #407 from Ark0N/feat/iphone-duo

iPhone Duo support: fold-aware dialogs, and a fold is no longer mistaken for the keyboard
This commit is contained in:
Ark0N
2026-09-14 16:10:38 +02:00
committed by GitHub
10 changed files with 1012 additions and 15 deletions
+8
View File
@@ -398,6 +398,14 @@ Anatomy: `.set-shell` → `.set-shell-head` (title + `.set-head-actions`) + `.se
**Foldable settings identity**: responsive layout remains width-driven through `MobileDetection.getDeviceType()`, but the localStorage namespace/defaults use `MobileDetection.isHandheldDevice()` so an Android foldable keeps `codeman-app-settings-mobile` after unfolding past the desktop breakpoint. The stable handheld check prefers explicit phone/tablet/desktop UA tokens, then `navigator.userAgentData.mobile`; Android WebView is covered by the `Mobile` UA fallback. Do not switch per-device settings namespaces from instantaneous viewport width — a posture-triggered WebView reload would lose opt-in UI such as `showResponseViewer` and `extendedKeyboardBar`. Regression profile: `OPPO Find N5 (unfolded)` in `test/mobile/devices.ts`.
### Folding devices
**A fold is not a keyboard.** `KeyboardHandler.handleViewportResize()` (mobile-handlers.js) used to read any visual-viewport height drop over 150px as the keyboard appearing. A virtual keyboard only ever takes HEIGHT, so a resize that changes the WIDTH is the device changing shape (a rotation, or a foldable opening or closing) and is skipped by both detection branches. Closing an iPhone Duo (626→466pt wide, 890→678pt tall) otherwise latched `keyboardVisible` with no keyboard on screen: the accessory bar appeared, `main` grew 84px of dead padding, and `updateAppHeight()`, which bails while the keyboard is up, stopped refreshing `--app-height`. The latch was sticky because clearing it needed the height back within 100px of a baseline belonging to a display the user was no longer looking at. The shape branch re-baselines instead, which is also what lets a keyboard opened after the fold be detected, and `init()` seeds `lastViewportWidth` so the first resize of the page is not itself read as a shape change.
**With the keyboard up, the new baseline is `window.innerHeight`, never the shrunk visual height.** The page sets no `interactive-widget`, so the default resizes-visual mode holds on both engines: the keyboard shrinks only the visual viewport and the layout viewport stays the display's full height (`updateLayoutForKeyboard()` relies on the same fact). The first version baselined to the shrunk height, which made `heightDiff` 0, so the settle event the OS animation fires at the new width (or any later address-bar drift) satisfied the hide branch and ran `onKeyboardHide()` with the keyboard still on screen: accessory bar hidden, the toolbar lift dropped, `main`'s padding cleared. It could not recover, because no further 150px drop can re-arm the show branch against a baseline already sitting at the shrunk height. Reproduced against the real handler in the vm harness with both the fold flavour (626x590 → 466x378 → 466x378) and the rotation flavour (393x359 → 852x150 → 852x160); `test/viewport-shape-change.test.ts` models the two heights separately and pins both.
**The hinge is a reserved region, and the CSS is inert by construction.** The CSS Viewport Segments media features report two segments only while a foldable is actually bent; `--fold-inline-end` / `--fold-block-end` (end of styles.css) measure the strip to keep clear from the LEADING segment (`env(viewport-segment-right 0 0)` and `env(viewport-segment-bottom 0 0)`, physical sides in every text direction) and are `0px` on everything else. The seven centred overlays are all `position: fixed; inset: 0` flex boxes; each shrinks its CONTENT box with padding rather than the box itself, so the backdrop still covers the far side of the fold and still swallows taps there. The cascade traps, each measured in headless Chromium against styles.css + mobile.css in index.html link order: (1) a later `padding-right` longhand beats the earlier `padding` shorthand it composes with, so each fold rule re-states the overlay's own gutter; (2) a base gutter that a LATER `@media` block overrides needs its own fold restatement in that block on a ZERO base, since the unconditional rules at the end of the file otherwise put the gutter back (the phone path picker and path preview drop to `padding: 0` under 600px and came back at 16px and 18px on every phone); (3) a compound rule written to outrank a mobile.css shorthand must be scoped to the band where that shorthand applies, because outside it there is no gutter to compose with (the palette's `.modal.command-palette-modal` added 0.75rem at 393, 900 and 1400px and pushed the shell 6px off centre, and inside the band it lost its bottom gutter to the shorthand until it restated that side too); (4) mobile.css loads AFTER styles.css, so a same-specificity rule there wins under its own breakpoint (the response viewer's `max-height: 92dvh` under 600px beat the tabletop cap, which now has a twin at the end of mobile.css). `test/foldable-layout.test.ts` DERIVES the overlay list from the stylesheet, simulates the cascade across both files at every breakpoint with and without the fold rules, and requires the two results to differ by exactly the fold strip, so each of the four fails there instead of on hardware nobody has.
## Security layers
### Layer-by-layer detail