fix(web): keep the Workspace rail clear of the OS status and nav bars - #3587
fix(web): keep the Workspace rail clear of the OS status and nav bars#3587btli wants to merge 20 commits into
Conversation
2033967 to
57c8698
Compare
|
Withdrawing — this isn't the right fix for the rail inset issue; a different approach is coming. |
|
Reopening after discussion — this approach is correct after all. |
|
Pushed 2 commits:
Verification: web suite green on this branch (4,715); Android green except one pre-existing failure ( Recommended merge order — this PR is one of seven fixed and verified together; the fully integrated reference (all cross-PR conflicts resolved, 5,003 web tests + 273/273 Android tests green) is Android track (suggest landing first — security fixes, and the later two resolve against it):
Web track (file-disjoint from the Android track; order within it matters): |
752db09 to
906a6f1
Compare
On a tablet or unfolded foldable the right Workspace rail rendered under the status bar, leaving its tab icons un-tappable, and ran under the gesture-nav bar at the bottom. The rail is `md:m-2` and only renders at md+, but every native-shell inset rule lives inside `@media (width < 48rem)` — so none of them reach it — and it is absent from the shared panel selector lists on both the CSS and Android-injected sides. Add the safe-area margins outside the width gate, and mirror the same declaration into the Android injected sheet so shells pointed at an older web build get the fix too. Uses --omnigent-safe-*, not --omnigent-inset-*: the latter folds in the native bottom-bar footprint and would double-count. Signed-off-by: Bryan Li <bryan.li@gmail.com>
The panel docks as a full-height rail at md+ but was missing from both inset selector lists, so it had no safe-area padding at any width. It is reachable only in debug mode, which is why it went unnoticed. Pre-existing gap, adjacent to the Workspace rail fix rather than part of it. Signed-off-by: Bryan Li <bryan.li@gmail.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>
A media-gated override appearing later in index.css with the same selector previously slipped past the ancestry check, which only inspected the first matching rule. Assert over every rule that sets the Workspace rail's margins so a later override at md+ fails the suite instead of silently winning the cascade. Signed-off-by: Bryan Li <bryan.li@gmail.com>
The safe-area assertion read only the first matching rule, so a second top-level rule could reintroduce the composite inset vars — double counting the native bottom bar — while the suite stayed green. Signed-off-by: Bryan Li <bryan.li@gmail.com>
Only the longhand properties were matched, so a shorthand `margin` declaration set both edges without ever being checked for an enclosing at-rule. Signed-off-by: Bryan Li <bryan.li@gmail.com>
Matching the exact selector text let an equivalently-spelled one through, and checking only the first rule let a later override zero the margins without naming a banned variable. Match on the rail's aria-label and require the margin rule to be unique. Signed-off-by: Bryan Li <bryan.li@gmail.com>
margin-block and its start/end forms set the same edges as the rule being guarded, so an override written that way was never matched. Signed-off-by: Bryan Li <bryan.li@gmail.com>
The rule scan keyed on one literal spelling, so the same selector written with single quotes, no quotes, or extra spacing introduced a margin the uniqueness and safe-area checks never saw. Signed-off-by: Bryan Li <bryan.li@gmail.com>
The scan matched any spelling of the rail's attribute selector, but the ancestry check then re-found one exact literal and skipped the rule when it differed — two sources of truth for the same question, and a rule whose nesting was never actually checked. Signed-off-by: Bryan Li <bryan.li@gmail.com>
…tors The ancestry walk built named at-rule records that no assertion ever read — every use compared them to an empty list. Brace depth answers the same question in a third of the lines and is stricter: it also rejects the rule being nested inside a plain rule, which the ancestor form let through. Also stops `scroll-margin-top` and friends registering as margin overrides. Signed-off-by: Bryan Li <bryan.li@gmail.com>
The rail now pads content clear of the OS bars on all four edges instead of shrinking with top/bottom margins, so its background and divider stay flush. Lateral insets are folded into --omnigent-safe-left and -right and the published insets include the display cutout, which systemBars() alone omits. The injected fallback sheet mirrors the same rules. Co-authored-by: Isaac Signed-off-by: Bryan Li <bryan.li@gmail.com>
The Workspace-rail and full-height-panel checks each spelled out the same
four padding expectations and the same stylesheet append/remove dance, and
three call sites repeated the selector-extraction slice. They now go
through withStyle, expectSafeAreaPadding, and selectorOf. Also records why
the injected Android inset CSS must avoid backticks and `${`.
Co-authored-by: Isaac
Signed-off-by: Bryan Li <bryan.li@gmail.com>
The injected fallback sheet flattened index.css's media split, so at md+ widths panels docked inside the already-padded Workspace rail (frameless FileViewer, right-docked drawers) were safe-area padded twice. Mirror the (width < 48rem) guard around the :is(...) panel rule and pin it with a structural test. Also decouple the CSS layout test from WorkspacePanel's prop surface: a stub aside carries the selector contract, a source assertion pins the component's aria-label, and brace-depth scanning shrinks to a blanked- source count. Co-authored-by: Isaac Signed-off-by: Bryan Li <bryan.li@gmail.com>
A local uv invocation rewrote the lock's package URLs to an internal proxy; restore the normalized PyPI form the lock check expects. Co-authored-by: Isaac Signed-off-by: Bryan Li <bryan.li@gmail.com>
The rail only exists at md+, outside the mobile drawer rules, so its safe-area padding has its own rule; this pins the injected-bridge -> data-android-native -> inset-fold -> computed-padding chain end to end with distinct per-edge insets. Co-authored-by: Isaac Signed-off-by: Bryan Li <bryan.li@gmail.com>
Execution-logs / file-viewer / terminals panels replace the Workspace rail in the same right-edge slot at md+, so they slid under the status bar and cutout the rail was just fixed to avoid. Pad their top, bottom and right edges (content sits to the left), exempting panels nested inside the already-padded rail, in both index.css and the injected fallback sheet. Share the CSS block-extraction across the test file. Co-authored-by: Isaac Signed-off-by: Bryan Li <bryan.li@gmail.com>
The injected sheet is interpolated into a JS template literal, so a backtick or dollar-brace in the CSS breaks the script at runtime while the style-tag tests keep passing. Co-authored-by: Isaac Signed-off-by: Bryan Li <bryan.li@gmail.com>
906a6f1 to
2455241
Compare
|
This PR is part of a stacked train rebased onto current main so the whole set merges conflict-free in order. Full order: #3985 → #4065 → #4060 → #4057 (web/Sidebar train) and #3587 → #3589 → #3800 (android train). The two trains are independent of each other. This PR is 1st in the android train (based directly on main) — mergeable now. Until its predecessors merge, this PR's diff temporarily includes their commits; it collapses to a clean, self-contained diff automatically as each predecessor lands. |
The chat header's right edge clears the inline workspace rail via --workspace-panel-offset. The +16 covered the rail's old outer margins; the rail is flush now, so the offset is the rail width itself. Co-authored-by: Isaac Signed-off-by: Bryan Li <bryan.li@gmail.com>
…bars Scoping the injected panel inset rule to phone widths also stripped the conversations sidebar's insets at md+, where it is a pinned column at the left screen edge — unfolded, its header rendered under the status bar. Pad its exposed edges (top/bottom/left) at md+ in index.css and mirror the rule in the injected fallback sheet. Android-only: iOS md+ keeps its current layout. Signed-off-by: Bryan Li <bryan.li@gmail.com>
bdb57cc to
a2cb59a
Compare
Related issue
Closes #3585
Summary
On a tablet or unfolded foldable (Pixel 10 Pro Fold), the right Workspace rail rendered underneath the OS status bar — its Files / Agents / Shells / Tasks tab icons sat in the occluded band and could not be tapped — and its bottom edge ran under the gesture-nav bar.
The rail is
md:m-2(8px on every edge) and, beinghidden md:flex, only exists at ≥768px. Every native-shell inset rule lives inside@media (width < 48rem)(index.css:522→731), so none of them can reach it; it is also absent from the shared panel selector lists on both the CSS side and the Android-injected side. Its top edge therefore landed at y = 16px, inside a 24–48dp status bar.:is([data-ios-native], [data-android-native]), so both native shells get it and browser/Electron are untouched.ensureInsetStyles) so shells pointed at a server running an older web build get the fix from an APK update. Same property, same value, so the two stylesheets are idempotent and cannot double-count.Uses
--omnigent-safe-*, not--omnigent-inset-*— the latter folds in the native bottom-bar footprint and would double-count against the bottom edge.ELI5: the rail is a floating card pinned 8px from the screen edges. On phones a separate set of rules pushes UI clear of the status bar, but those rules switch off at tablet width — exactly where this card starts existing. So it kept its 8px and slid under the clock and battery icons.
Test Plan
npx vitest run src/index.css.test.ts— 16 passed.--omnigent-safe-*rather than the inset vars, and that exactly one such rule exists.@media (width < 48rem),@media (max-width: 767px)or@supports; appending a later@media (min-width: 48rem)block that zeroes the margins; a second top-level rule reintroducing the inset vars; shorthandmarginand logicalmargin-block*overrides; and the same selector written single-quoted, unquoted or with extra spacing.pre-commit run --files <changed files>— passed.npm run lint— no new findings.Note the uniqueness assertion will deliberately fail if someone later adds a second margin rule for the rail. That is intended: this rule was silently inert on every tablet until now, so a future change to it should require a human to look rather than pass quietly.
Demo
Recorded on an Android emulator at 800×1280 @ 160dpi (an 800dp-wide viewport, i.e. the md+ tablet/unfolded-foldable layout) with the tall display-cutout overlay enabled, connected to a local server.
mainAPK)Before: the Workspace rail's Files / Agents / Shells tab icons render inside the status bar, sharing the row with the clock and battery; tapping them does nothing (the taps land on the system bar), while content lower in the rail still responds. After: the rail's card starts below the status bar and clear of the gesture-nav area, and the same taps switch tabs normally. Full-resolution MP4s alongside the GIFs on the asset branch.
The floating server pill visible near the top of the "after" capture still overlaps web chrome — that is the separate defect fixed by #3589 and is untouched by this PR.
Type of change
Test coverage
Coverage notes
The CSS contract is covered by the mutation-checked regression test above, and the rendered result is now verified on an emulator at md+ width (see Demo), including the tap behaviour before and after. Not yet verified on physical foldable hardware. The inset values themselves are unchanged and already exercised on phones — this change only adds a new consumer of them.
Deliberately out of scope, and worth a follow-up: the same
@media (width < 48rem)gate means that at iPad widths the chat header, conversation content, terminal view and composer are also uncovered, because iOS has no injected-sheet backstop the way Android does. This PR fixes the rail on both shells but does not make iPad correct in general.Changelog
The workspace rail on Android tablets and foldables no longer slides under the status bar, so its tab icons stay tappable