Repository navigation
fix(staged): fix mobile Safari layout, keyboard, dialog and menu issues - #1015
Merged
Merged
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
…em (07) Issue 07 of the mobile Safari audit: a menu presented as a bottom sheet (the ⋮ menus on repo cards and branches, the workspace-status chevron) sometimes closed the instant it opened. bits-ui opens a menu on the trigger's touch pointerup, before the tap's synthesized click is dispatched. The sheet mounts under the lifting finger, WebKit delivers that trailing click to the scrim now under it, and the scrim closed the menu on any click. The scrim now only dismisses on a click whose gesture began on it: createScrimDismiss arms on the scrim's own pointerdown, disarms on pointercancel, and fires once per armed click. Chosen over the note's alternatives (ignore clicks for ~350 ms after mount, or dismiss on pointerup) because it follows the gesture instead of a clock: a trailing click delayed by a busy main thread still cannot close the sheet, and a deliberate quick tap on the scrim right after opening still can. Dismissing on pointerup would close before the click and let it fall through to the page underneath, which is why the scrim owns the whole gesture. Overlay audit: the MenuSheet scrim is the only overlay that dismisses on its own click. Dialogs, alert dialogs and popovers dismiss through bits-ui's DismissibleLayer, which only acts on a pointerdown it captured while mounted (the same arming rule), and popovers and sub-menus open on click, so no trailing click follows them. Context-menu sheets and sub-menu sheets share the MenuSheet frame and get the fix; long-press opens already swallow their own click (longPress.ts). Verified on the iPhone 17 Pro simulator, iOS 27.2 (0B6E1FFA-E4B0-4B95-B63E-7C195593E403), Safari: 24 taps opening the repo card ⋮, branch ⋮ and workspace-status sheets each delivered their trailing click to the unarmed scrim and none closed a sheet; every one of the following scrim taps closed its sheet. Four long-press context-menu sheets opened and closed by scrim tap the same way. Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…d (01, 04) Issues 01 and 04 of the mobile Safari audit: focusing the chat composer in a full-screen dialog scrolled the whole page up by the keyboard's height (scrollY 347 in Safari, 413 as a home-screen app), taking the dialog header and transcript off screen until the user panned back. iOS does not shrink the layout viewport or 100dvh for the keyboard. It shrinks the visual viewport and scrolls the page to reveal the input, even with html/body overflow: hidden. The old inset, innerHeight - vv.height - vv.offsetTop, is 0 once Safari has scrolled (innerHeight shrinks with the visual viewport on iOS 27), so the dialog kept its full height in a layout viewport that was now scrolled away. Full-screen dialogs now size and place themselves from the visual viewport: top = vv.offsetTop, height = vv.height, via a pure computeViewportGeometry (unit tested) and a ref-counted watcher, visualViewport.svelte.ts, that replaces keyboardInset.svelte.ts. While a full-screen dialog is mounted, each visual viewport resize or scroll undoes the page scroll Safari added; the dialog already keeps the composer above the keyboard, so Safari has no reason to scroll it back. The reset runs on every change, not just the first resize after focus, because iOS fires resize and scroll in either order while the keyboard slides. A pinch-zoomed page (iOS ignores user-scalable=no) is left alone and the dialog falls back to 100dvh. The safe-area bottom padding drops while the keyboard is up, since the footer then sits on the keyboard, not the home indicator. The visual viewport is only followed while a text input has focus; with nothing to type into, the dialog keeps its old top: 0; height: 100dvh, and focus changes re-measure. iOS can leave the visual viewport stale: a keyboard raised by a system sheet (Add to Home Screen) and still up when Safari went to the background stayed subtracted from it (484 of 714 px) after Safari returned, with nothing covering the page, and a dialog sized from it ended 230 px above the toolbar. The page cannot have a keyboard over it without a focused text input, so that gate drops the stale reading without guessing at heuristics. The note suggested publishing --vv-top / --vv-height on :root. The geometry is instead reactive state applied as inline style on the dialog element only: every write to the root's style attribute restyles the whole document and fires isDark's MutationObserver, and the keyboard moves the viewport on every frame of its slide. The old module already deduplicated its root writes for the same reason. The watcher only runs while a full-screen (mobile) dialog is mounted, so desktop is unchanged. Verified on the iPhone 17 Pro simulator, iOS 27.2 (0B6E1FFA-E4B0-4B95-B63E-7C195593E403), note dialog chat composer with the software keyboard: in Safari the visual viewport is 384 px at offset 0, scrollY 0 and the dialog spans 0-384 with its header visible; six focus/dismiss cycles return to 714 px each time and closing the dialog with the keyboard up leaves scrollY 0. As a home-screen app: 416 px, scrollY 0, dialog 0-416, back to 812 px on dismiss. With Safari's visual viewport stuck at 484 px as above, a note dialog spans 0-714 and the composer keyboard still pins it to 0-384. Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Issue 05 of the mobile Safari audit: a full-screen dialog that focuses an input as it opens (Add Repository) appeared with its title and fields 386 px above the top of the screen. Two things put the input where Safari had to scroll a long way for it. Full-screen dialog content is a CSS grid whose auto rows stretch over the whole height, so a short form sat spread down the screen; and the explicit focus() calls ran while the dialog was still sliding up from below, so the input was mostly off screen at the moment iOS revealed it. Full-screen dialogs now pack their rows at the top (align-content: start). Chosen over switching the mobile dialog to flex-col because it leaves every caller's layout alone: flex-col dialogs already start at the top, grids with a 1fr row still fill, and only the stretched auto rows of the short form dialogs change. The focus calls the note lists (RepoSearchInput, NewProjectForm, RenameBranchDialog, MoveBranchDialog, NoteTransferDialog, ActionEditDialog, NewSessionModal, SessionChatPane) go through focusAfterDialogEnter, which waits for the enclosing dialog's own enter animation to finish (getAnimations().finished) instead of a fixed delay, so it tracks the real duration and a cancelled animation still focuses. It only waits inside a full-screen dialog (marked data-full-screen); desktop dialogs and non-dialog hosts focus immediately as before, so typing straight after opening a dialog keeps every keystroke. The pooled chat pane re-checks that it is still open when the deferred focus lands. Verified on the iPhone 17 Pro simulator, iOS 27.2 (0B6E1FFA-E4B0-4B95-B63E-7C195593E403), Safari: Add Repository opens with title, close button and search field on screen, the keyboard up and the dialog at 0-411 with scrollY 0; Rename Branch opens top-aligned with the name selected, the keyboard up and the dialog at 0-384. The deferred focus still raises the software keyboard. Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
In iOS Safari `100vh` is the viewport with the toolbar hidden, 754 px,
inside a 714 px `<body>` that is `overflow: hidden`. The 40 px overflow
can't be scrolled by the user, but it can be scrolled programmatically.
Closing a dialog returned focus to the workspace layer, which ran past
the body's bottom edge, and Safari scrolled `<body>` by 40 px to reveal
it. That left the TopBar and every hit target offset until a reload
(02). The same overflow kept the bottom 40 px of every pane under the
toolbar (08).
- `main` is `height: 100dvh`, with `100vh` as the fallback for webviews
without dynamic units. The note also offered `100%`. I chose `100dvh`
because `main` sits in `#app`, which has no definite height, so `100%`
would only work if the whole ancestor chain were sized as well. The
desktop window is unchanged because `dvh` equals `vh` there.
- On touch screens (`viewport.hasCoarsePointer`), dialog and alert
dialog focus restore uses `focus({ preventScroll: true })`
(restoreFocusWithoutScroll). It records the restore target the same
way bits-ui's focus scope does and only takes over when the caller
hasn't handled the close itself. With a mouse and keyboard, bits-ui's
restore still scrolls the trigger into view.
- guardDocumentScroll, installed from App.svelte, undoes any scroll of
the window (the scrolling element) or `<body>` as it happens. It
leaves two cases alone:
- a pinch-zoomed page;
- a text input that has focus outside a full-screen dialog, where
Safari's scroll is what keeps the input above the keyboard.
The guard does nothing wherever the document never scrolls, which
includes the desktop window.
isTextInput moves to textInput.ts and the zoom check becomes
isPinchZoomed, so visualViewport.svelte.ts and the guard share them.
Verified in Safari on the iPhone 17 Pro simulator, iOS 27.2
(0B6E1FFA-E4B0-4B95-B63E-7C195593E403):
- After closing a note dialog, `<body>` stays 714/714 and unscrolled,
and the TopBar stays in place.
- I recreated issue 02's 754 px `main` with a dist-only probe. Closing
the dialog then returned focus to the 42-754 layer with no `<body>`
scroll event.
- A forced `body.scrollTop = 40` was back to 0 within 300 ms.
- Settings scrolled to the bottom shows its last line above the
toolbar.
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Long-pressing a repo card on iOS selected the title and raised the Copy callout. The card had no long-press wiring, and only a reorderable card suppressed selection. A touch long press anywhere on the card now opens its ⋮ menu, which is a bottom sheet on phones. It uses the shared context-menu controller (createLongPress in longPress.ts) with the same 500 ms hold, movement threshold and suppression of the release click as project, session and timeline rows. On coarse pointers the card gets `user-select: none` and `-webkit-touch-callout: none`, as context-menu-trigger does. A `contextmenu` event opens the menu only on coarse pointers (Android Chrome raises one for a touch hold); a mouse right-click keeps the browser's default. A reorderable card is left alone, because there a touch hold starts the drag. The note offered two options: wrap the card in `ContextMenu.Trigger`, or use `createLongPress`. I chose `createLongPress` driving the existing DropdownMenu through `bind:open` because: - the card keeps one menu definition instead of a second copy of its items to keep in sync; - desktop gets no new right-click menu; - on phones the dropdown and the context menu render as the same MenuSheet. A long press can open the menu while the ⋮ trigger is scrolled out of the horizontal repos row. bits-ui's close focus on that trigger then scrolled the row sideways under the finger: the next tap meant for the neighbouring card's ⋮ hit its "Add project". On touch screens the trigger is now focused with `preventScroll`. Verified in Safari on the iPhone 17 Pro simulator, iOS 27.2 (0B6E1FFA-E4B0-4B95-B63E-7C195593E403), on the home screen's repos row: - Long-pressing a card's title opens its sheet, with no selection, no callout and no flash-close. - Long-pressing "Add project" opens the sheet without starting a new project. - A cloned repo's sheet loads its clone details (Copy Path). - After cancelling the sheet of a card whose ⋮ is off screen, the row stays put, and tapping the next card's ⋮ opens that card's own sheet. Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…e indicator (06) The viewport meta had no `viewport-fit=cover`, so every `env(safe-area-inset-*)` was 0. The safe-area padding already on the menu sheet and full-screen dialogs fell back to nothing, which left the sheets' Cancel row and the chat composer flush on the home indicator in the home-screen app. - index.html sets `viewport-fit=cover`. - `main` pads the right, bottom and left insets, so the panes end above the home indicator and clear the notch in landscape. - The TopBar pads the top inset itself, so its background fills the status bar wherever the page runs under it. The reset shell has no TopBar, so it pads the top inset directly. - Three other edge-anchored surfaces would otherwise run under the status bar or home indicator, so they measure from the safe area too: - DiffModal's full-screen "Changed files" list; - the session launcher; - toasts, via svelte-sonner's offsets (safeAreaOffset). - `apple-mobile-web-app-status-bar-style` is now `default` instead of `black-translucent`. With black-translucent and cover, iOS 27 draws the page under the status bar but keeps the layout viewport at the height below it (812 of 874 pt). The page then ends 62 pt above the bottom of the screen, a `bottom: 0` scrim leaves that strip undimmed, and the 34 px bottom inset lands above the real home indicator. With `default`, the page sits below the status bar and runs to the bottom of the screen, and the insets are top 0 / bottom 34, which puts the bottom inset on the home indicator. iOS tints the status bar to match the page in both themes. iOS reads this tag only when the app is added to the Home Screen, so an existing install keeps the old style until it is added again. The desktop window is unchanged. In a WKWebView hosted like Tauri's window (overlay title bar, full-size content view), every `env(safe-area-inset-*)` reads 0 with and without `viewport-fit=cover`, even though the view's own safe area is 32 pt. The TopBar stays 42 px tall. Safari in portrait also reads 0 for every inset and its layout is unchanged. Verified on the iPhone 17 Pro simulator, iOS 27.2 (0B6E1FFA-E4B0-4B95-B63E-7C195593E403). In the home-screen app with the clip's status bar style set to default (as a fresh Add to Home Screen now records it): - The page runs from below the status bar to the bottom of the screen, with insets 0/34. - The repo ⋮ sheet's Cancel row and the chat composer sit above the home indicator, and the sheet still survives the opening tap (07). - The status bar is tinted light or dark to match the theme. - The keyboard still fits the full-screen chat dialog to 416 px. In Safari, the insets read 0 and the layout is unchanged. Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
The rest of issue 08: popovers sized or placed from `innerHeight` or `100vh` still assumed the layout viewport was what was on screen. On iOS the keyboard covers the bottom of the layout viewport without shrinking it, so with the keyboard up the hashtag dropdown measured room below the caret down to `innerHeight` (714 - 328 = 386 px in Safari), opened below, and landed under the keyboard. - visibleViewportRect() gives the on-screen rectangle in client coordinates: the visual viewport, offset into the layout viewport, or the window where `visualViewport` is missing. Wherever the two agree, including the desktop window, it equals the window. - placeHashtagDropdown() picks a side of the caret against that rectangle, caps the list's height to the room on that side (with a 96 px minimum), and places it with `top` alone. Above the caret it uses `translate: 0 -100%` instead of `bottom`, so every position is in the caret's own coordinates and none is measured from the bottom of a layout viewport the keyboard doesn't shrink. - On device the dropdown also ran off the right edge. Its left edge followed the caret while its width was `min(420px, 100vw - 32px)`, so it ended at x 407 on a 402 px screen even with the caret at the start of the line. It now gets the same width inline and moves left just far enough to keep the 16 px side margin that `max-width` already implied. - ParentBranchCommitsHover clamps to visibleViewportRect() instead of `innerWidth`/`innerHeight`. - The remaining `100vh` heights (ParentBranchCommitsHover, AcpConfigPickerShell, NewSessionModal, DiagramViewerModal) use `dvh`. On desktop the side choice, the above/below positions and the width are unchanged. The height cap and the left shift only apply where the list used to overflow the window edge. Verified on the iPhone 17 Pro simulator, iOS 27.2 (0B6E1FFA-E4B0-4B95-B63E-7C195593E403), by typing `#` on the on-screen keyboard in the session composer: - Safari: the visible area is 384 px, the caret top is 301, and the list sits at 17-297 x 16-386. - Home-screen app: the visible area is 416 px, the caret top is 334, and the list sits at 49-329 x 16-386, with the caret mid-line after "Qwertyuiop ". - Tapping an item inserted its reference badge. - `<body>` stayed unscrolled throughout. Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Review #review:9f8db66d-f08f-4ca5-a842-2ff263cb1b3e of the seven mobile Safari fix commits a623787..c556063. Repo card long press (03). A touch that begins inside the card's ⋮ DropdownMenu.Trigger no longer starts the long press, and a coarse-pointer `contextmenu` there no longer opens the menu. bits-ui toggles a menu on the trigger's touch pointerup, so a hold on ⋮ that had already opened the sheet closed it again on release. Left to the trigger, the release opens it like a tap. Shared scroll predicate (02 vs 01/04). shouldKeepDocumentScroll is exported from documentScroll.ts and visualViewport.svelte.ts uses it in place of its own zoom check. Where the guard keeps Safari's page scroll (a pinch-zoomed page, or a focused input outside the full-screen dialog, such as IconPicker's search field portalled over ActionEditDialog), the full-screen dialog now also leaves the scroll alone and keeps the layout viewport's geometry, so it scrolls with the page and the input stays where Safari revealed it. Following the visual viewport while keeping the scroll would have carried the dialog, and the popover anchored in it, back over the input. Unit tests cover the outside-input case in both modules. focusAfterDialogEnter (05). Only waits for the enter animation on a coarse pointer, read directly from `(pointer: coarse)` because a dialog's form can ask for focus before anything has synced viewport.hasCoarsePointer; a narrow desktop window whose dialogs are full-screen focuses immediately again. The deferred focus also requires the dialog's `data-state` to still be `open`, since closing mid-slide cancels the enter animation with the dialog still connected. SessionChatPane's insertSlashCommand goes back to a plain `inputEl?.focus()`. Existing home-screen installs (06). I recreated an install made before viewport-fit=cover by restoring the web clip's original Info.plist (WebClipStatusBarStyle LegacyBlackTranslucent, the backup from the fix session) on the iPhone 17 Pro simulator, iOS 27.2 (0B6E1FFA-E4B0-4B95-B63E-7C195593E403), and launching the current build. The reviewer's concern is real: insets read 62/34, the layout viewport was 812 of 874 pt, drawn from the top of the screen, and with a ⋮ sheet open the scrim ended at 812 and left the bottom 62 pt undimmed, with the sheet's Cancel row and its 34 px inset 62 pt above the home indicator. viewportFit.ts (new, called from main.ts before mount) drops the `viewport-fit` key from the viewport meta when, in a home-screen app, `env(safe-area-inset-top)` is non-zero but the layout viewport (documentElement.clientHeight, which iOS leaves alone for the keyboard) is shorter than the window (outerHeight). Without cover such an install lays out exactly as it did before cover was added: page below the status bar to the bottom of the screen, insets 0, sheets on the fallback 8 px padding. A fresh install (status bar `default`) has no top inset and keeps cover; so would a black-translucent install wherever iOS lays it out to the bottom edge. The status-bar style itself is not readable from JS, and gating cover on display-mode alone would take it from the fresh installs that need it. The check could not be a one-shot at startup: iOS delivers the insets about 2 s after launch, well after load and the first frame, with a `resize`; so the guard listens to `resize` until it has dropped cover, reading one hidden probe kept outside <body>. Verified on the same simulator: - Black-translucent clip, home-screen app: the insets arrived at 17.65 s with a `resize`, cover was dropped, the next `resize` read insets 0/0, the TopBar sat at CSS 0-42 below the status bar, and a ⋮ sheet's scrim spanned CSS 0-812 = the full screen below the status bar. The sheet ended at the bottom of the screen with 8 px padding, as before cover existed. - Default clip (restored afterwards): cover kept, insets 0/34, TopBar below the status bar, as commit ecf2924 verified. - Repo card, home-screen app: holds of 0.63, 0.65, 0.98 and 1.3 s on ⋮ opened the sheet on release and it stayed open (the trailing click, when there was one, hit the unarmed scrim); holds on the title and on the card body opened it at ~500 ms and it stayed open; a plain tap on ⋮ still opens it. Verify counter: 7 mounts, 0 flashes. Android Chrome's `contextmenu` path on ⋮ is reasoned from the same toggle-on-pointerup, not tested. Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…ortFit Review #review:6d54fcb5-8360-4276-8866-ca4a86eef4be of 8e86046 warned that the viewport-fit=cover guard can only act once iOS delivers the safe-area insets, which the guard's comment put at about 2 s after launch. If so, every cold launch of an install added under black-translucent would draw the TopBar under the clock for a second or two, then shift it 62 px when cover was dropped. Measured on the iPhone 17 Pro simulator, iOS 27.2 (0B6E1FFA-E4B0-4B95-B63E-7C195593E403), it does not happen, so there is no behaviour change. The comment now gives the measured timing and names the race that remains. Setup: the Staged web clip's Info.plist swapped to the LegacyBlackTranslucent backup, the app terminated before each launch and opened from its icon. A dist-only probe, the first script in <head>, logged the env() insets, the viewport meta, the layout height and the TopBar's rect on every `resize` and animation frame that changed them, and sent each line to a local logger. Eight cold launches (bt-cold1..8): - The insets arrived in a `resize` 71-136 ms after the first script ran, 18-77 ms after `load`. The guard dropped cover in that same `resize`, and the next one, 26-63 ms later, laid the page out below the status bar with insets 0/0. - The TopBar first rendered 123-298 ms in. In seven launches its first frame was already its final position. In bt-cold6 it mounted in the same frame as the inset `resize`, already padded by the 62 px inset, so its content sat where it stayed when the layout switched one `resize` later. - bt-cold6's video, frame by frame: before the app's first frame the window shows a blank page under the status bar for about 80 ms of iOS's launch fade. The first frame with app content has the TopBar below the status bar. - Timed screenshots of bt-cold8, about 0.8 s apart from the tap: home screen, black launch screen, blank page, then the app below the status bar in every shot from 6.1 s to 14.5 s. The 2 s figure did not reproduce in any launch. The margin is small: in bt-cold6 the app rendered in the same frame as the insets. If startup ever renders earlier, the first frames would sit under the status bar and shift 62 px, and the comment says so. The fix would then be the review's suggestion: store the decision and apply it before mount. It is not added now because there is nothing to fix, and a stored flag must never reach a `default` install. For whoever picks it up, each web clip keeps its website data in its own folder (Library/WebClips/<id>.webclip/Storage, with LocalStorage, CacheStorage and IndexedDB), apart from Safari's. Isolation between clips was not exercised. Default clip restored afterwards, byte-identical to its backup: a cold launch kept cover with insets 0/34 and the TopBar at CSS 0-46, below the status bar. Evidence in /tmp/staged-audit/work/launch/: summary.txt (per-launch table), beacons.log (probe timelines), bt-cold1..7.mov, zoom-bt6-95-106.png (bt-cold6 around its first app frame), sheet-bt1.png, bt-cold8/ and sheet-bt8-timed.png (timed screenshots), launch-probe.js and the launch scripts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
matt2e
force-pushed
the
find-staged-web-weirdness
branch
from
November 2, 2026 08:46
f606da9 to
0f89d14
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the issues from the mobile Safari audit of Staged (issue numbers are in parentheses). Each fix was verified on the iPhone 17 Pro simulator (iOS 27.2), in Safari and as a home-screen app. The desktop/Tauri window is unchanged.
Changes
scrimDismiss.ts).visualViewport.svelte.ts. Full-screen dialogs now follow the visual viewport's position and size, so the page no longer scrolls up by the keyboard's height.focusAfterDialogEnter, which waits for the dialog's enter animation to finish, but only for full-screen dialogs on coarse pointers.mainuses100dvh. On touch screens, dialogs restore focus withpreventScroll.guardDocumentScrollundoes stray programmatic scrolls of the window or<body>. It leaves pinch-zoom and focused inputs outside full-screen dialogs alone.createLongPresscontroller. On coarse pointers the card no longer selects text or shows the callout. A hold that starts on the ⋮ trigger is left to the trigger.viewport-fit=cover, and the status bar style is nowdefault. The TopBar,main, the DiffModal file list, the session launcher and toasts now pad for the safe-area insets.viewportFit.tsdropscoverfor installs made under the oldblack-translucentstyle, so those installs lay out as they did before.visibleViewportRect()andplaceHashtagDropdown()choose a side and cap the height against the on-screen area instead ofinnerHeight. The dropdown no longer runs off the right edge. The remaining100vhheights now usedvh.Testing
🤖 Generated with Claude Code