Skip to content

iPhone Duo support: fold-aware dialogs, and a fold is no longer mistaken for the keyboard - #407

Open
Ark0N wants to merge 1 commit into
masterfrom
feat/iphone-duo
Open

iPhone Duo support: fold-aware dialogs, and a fold is no longer mistaken for the keyboard#407
Ark0N wants to merge 1 commit into
masterfrom
feat/iphone-duo

Conversation

@Ark0N

@Ark0N Ark0N commented Sep 12, 2026

Copy link
Copy Markdown
Owner

Apple published Designing for iPhone Duo. It asks an app to do three things: adapt to both displays, stay continuous as the device opens and closes, and treat the band a partly-open display folds through as a reserved region that content avoids covering. This PR does those three things for Codeman's web UI, and fixes a bug the first one exposed that also affects hardware shipping today.

1. A fold is not the virtual keyboard (bug fix)

KeyboardHandler.handleViewportResize() (mobile-handlers.js) read any visual-viewport height drop over 150px as the keyboard appearing.

A virtual keyboard only ever takes height. Anything that changes the viewport WIDTH is the device changing shape. Closing an iPhone Duo moves the browser from the 626x890pt inner display to the 466x678pt outer one, a 212px height drop, so the app latched keyboardVisible with no keyboard on screen:

  • the keyboard accessory bar appeared,
  • main grew 84px of padding reserving room for a keyboard that was not there,
  • MobileDetection.updateAppHeight(), which bails out while the keyboard is up, stopped refreshing --app-height.

The latch was sticky. Clearing it needs the height back within 100px of a baseline that now belongs to a display the user is no longer looking at, so the state survived until the device was opened again.

This is not Duo-specific. Rotating any phone hits the same latch: 393x659 to 852x330 is a 329px drop with no keyboard anywhere. That has presumably been true for a long time and is fixed here too.

The fix is one distinction: a resize that changes the width re-baselines and is never read as a keyboard transition. The re-baseline (rather than just suppressing the branch) is what lets a keyboard opened after the fold still be detected, measured against the display it is actually on.

2. The hinge is a reserved region (new)

On the web, a fold is described by the CSS Viewport Segments media features and env() variables, which report two segments only while a foldable is actually bent. Codeman's centred overlays are all position: fixed; inset: 0 flex-centring boxes, so in book pose their dialog lands on the hinge, and in tabletop pose it lands on a horizontal one.

--fold-inline-end / --fold-block-end (styles.css) measure the strip to keep clear and are 0px on everything else, so the rules are inert by construction rather than behind a branch. Each overlay 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 response viewer, a bottom sheet, is capped to the bottom segment in tabletop pose only, since a vertical hinge through a full-width sheet is fine.

Two details worth a reviewer's eye:

  • Each rule re-states the overlay's own gutter, because a later padding-right longhand beats the earlier padding shorthand it composes with and would otherwise erase it. test/foldable-layout.test.ts reads both numbers out of the stylesheet and compares them, so changing one alone fails there.
  • The command palette needs the compound selector .modal.command-palette-modal, because mobile.css loads after styles.css and gives it a padding shorthand under 768px, exactly the width a folding phone lives at.

Physical sides, not logical ones: dialogs sit in the left segment (the top one in tabletop pose) in every language, because the HIG keeps Duo's side controls on the same physical edge in RTL, aligned with the hardware.

3. Device profiles

iPhone Duo (outer) 466x678 and iPhone Duo (inner) 626x890, both at DPR 3, derived from Apple's published pixel specs (1398x2034 and 1878x2670, at 460 and 430 ppi). No browser-chrome allowance is subtracted, unlike the other iOS entries, because the HIG says the system moves toolbars and tab bars to the side on this device, so the vertical allowance copied from other iPhones would be wrong in both axes. The registry's other foldable uses the full viewport for the same reason.

Both postures land in the tablet band (466 and 626 are each above the 430px phone cut and below 768). That is deliberate and matches the existing note on shouldUseMobileOverview(): layout follows width, while per-device settings identity follows form factor and must not move with the fold.

Testing

  • npm test: 6831 passed, 357 files. Two new files in the gate, 28 tests: test/viewport-shape-change.test.ts (the keyboard/shape logic, in a vm against the real KeyboardHandler) and test/foldable-layout.test.ts (postcss guard plus the handheld-identity check).
  • The new keyboard tests were confirmed to FAIL on master for the right reasons (5 of 10) before the fix was applied.
  • The CSS guard was confirmed to fail when a gutter is changed on one side only, and when a fold rule is deleted.
  • test/foldable-layout.test.ts DERIVES the overlay list from the stylesheet, so a new position: fixed; inset: 0 centred overlay added without a fold rule fails there rather than on hardware nobody has.
  • npm run test:mobile -- test/mobile/settings.test.ts (excluded from CI, run by hand): 19 passed, 1 failed. Master is 18 passed, 1 failed, the same pre-existing failure (subagentTrackingEnabled default), so the FAIL list is unchanged and the new Duo close test passes.
  • Browser-verified in Chromium against the real stylesheet, since viewport segments cannot be emulated: flat, a dialog stays centred at 313 of a 626pt viewport. In book pose (segments 0-305 and 321-626) it centres at 153 with its right edge at 293, clearing the hinge, while the backdrop still spans all 626px. The path picker keeps its own 16px gutter. In tabletop pose the offline overlay's 3-term calc resolves to 367px and the card moves from 399 to 226, inside the top segment. Flat it is 20px, unchanged.
  • typecheck, lint, format:check, check:public-assets, check:frontend-syntax all clean.

Notes

  • Feature detection only. Where a browser does not support viewport segments, every rule in section 2 resolves to the current behaviour with no branch and no JS.
  • No changeset, so the release flow stays with whoever runs COM.
  • Out of scope, and worth a separate look: the HIG puts toolbars and tab bars on the SIDE on the outer display and on the inner one in landscape, to preserve vertical space on a short, wide screen. Codeman already has that shape (the session sidebar and the vertical tab rail), so defaulting a Duo to it would follow the guidance, but it changes a user-facing default and deserves its own decision.

…oard)

Apple's "Designing for iPhone Duo" asks an app to adapt to both displays,
to stay continuous as the device opens and closes, and to treat the band a
partly-open display folds through as a reserved region. Three things here.

1. A visual-viewport resize that changes the WIDTH is the device changing
   shape (a rotation, or a foldable opening or closing) and is never the
   virtual keyboard, which only ever takes height. handleViewportResize()
   read any height drop over 150px as the keyboard appearing, so closing a
   Duo (890 to 678pt tall) latched keyboardVisible with no keyboard on
   screen: the accessory bar appeared, main grew 84px of dead padding, and
   updateAppHeight() stopped refreshing --app-height. The latch was sticky,
   because clearing it needs the height back within 100px of a baseline
   belonging to a display the user is no longer looking at. Rotating any
   phone hit the same latch. The shape branch re-baselines instead, which
   is also what lets a keyboard opened after the fold be detected.

2. The hinge is now a reserved region in CSS. --fold-inline-end and
   --fold-block-end measure the strip to keep clear from the Viewport
   Segments env() variables, and are 0px everywhere else, so the seven
   centred overlays are inert by construction off a foldable. 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.

3. iPhone Duo (outer) and iPhone Duo (inner) join the mobile device
   registry, derived from Apple's published pixel specs at 3x.

Verified in Chromium: flat, a dialog stays centred at 313 of a 626pt
viewport; in book pose it centres at 153 inside the 0-305 leading segment
with its right edge at 293, while the backdrop still spans all 626. The
3-term calc on the offline overlay resolves to 367px in tabletop pose and
20px flat.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants