Skip to content

fix(mobile): the session rail is the workspace's screen, not a drawer - #247

Open
pythonlearner1025 wants to merge 1 commit into
mainfrom
feat/mobile-rail-fullscreen
Open

fix(mobile): the session rail is the workspace's screen, not a drawer#247
pythonlearner1025 wants to merge 1 commit into
mainfrom
feat/mobile-rail-fullscreen

Conversation

@pythonlearner1025

Copy link
Copy Markdown
Member

What part this touches

The webApp draws a workspace as three columns. Column one is a 48px strip of
workspace tiles. Column two is the session rail. Column three is the work area.

Below 900px the shell changes shape. ShellNav's wrapper stops being
display: contents and becomes an off-canvas drawer that carries the strip and
the rail together. A hamburger in the pane chrome opens it. A scrim closes it.

Three dialogs sit above that shell: create workspace, workspace details, and the
shared modal screen. Each one is a fixed overlay with its own z-index.

This PR only changes what a phone sees. Desktop is untouched, and a test proves
it.

The bugs

One: the rail is a drawer, not a screen. It opens at
min(88vw, 348px) (strip-rail.css:539). On a 390px phone that is 343px, and
the rail itself gets 295px. A session title clips. The chat shows through on the
right, under a scrim.

Two: a workspace opens on the chat, not on the list. Every route change runs
setDrawerOpen(false) (CloudApp.tsx:286). So a phone lands on Lody's own home
screen and the session list is one tap away.

Three: create workspace opens underneath the rail.
.create-workspace-screen is z-index: 100. .shell-nav is z-index: 400. So
the dialog opens and the rail covers it. The member sees slivers of a dialog they
cannot reach. Five sibling handlers already close the drawer on a phone before
they open a dialog. onCreateWorkspace was missed.

Four: the create-workspace buttons are pinned and mis-aligned. The footer is
flex: none in a column flex dialog, so it eats 70px of every phone screen. The
base rule sets justify-content: flex-end; the mobile rule sets display: grid
and the buttons width: 100%. The justify-content survives, so the grid column
is max-content and the buttons hug the right edge at 186px instead of filling.

Five: the member row overlaps itself. .workspace-member-row is an
eight-column grid. At max-width: 620px it becomes three columns, and the five
remaining children auto-place into those three columns on implicit rows. The
state chip lands in a 28px column and overlaps the machine-type select. The
volume meter lands in the last 28px column and runs off the card.

The fix

  • The mobile nav fills the width. The scrim is deleted, because it now covers
    nothing. The X in the strip and the Escape key already close the rail.
  • routeShowsMobileRail(route) names one rule: the rail is the workspace's own
    screen, so it shows at the workspace root and nowhere else.
  • On a phone the effect opens the rail at that root and closes it everywhere
    else. On desktop it still only closes.
  • onCreateWorkspace closes the rail on a phone first, like its five siblings.
  • On a phone the create-workspace dialog scrolls as one page. The header sticks.
    The buttons end the form and fill the width.
  • The four machine controls in a member row share one wrapper. The wrapper is
    display: contents on desktop, so nothing there moves. On a phone it becomes
    a wrapped band on its own row.

The risk trade

The rail now covers Lody's home screen, which carries a composer that starts a
session from the first message. That composer costs one extra tap: New session,
or the X. The trade buys a phone layout where the list is the workspace and a
session is a page, which is what a phone user expects.

display: contents is the one tool that adds a wrapper without adding a box. The
alternative was to reorder the member row in JSX and rebuild the desktop grid,
which risks the change this PR promises not to make.

The rejected alternative for bug three: raise the dialog above z-index 400.
.webapp-modal-screen (z-index 300) has the same relationship, so raising one
modal leaves every other one wrong. Closing the rail keeps one rule.

Tests

Two new test files, three extended.

  • test/mobile-rail-route.test.tsrouteShowsMobileRail over every
    ChatAddress arm and every AppRoute arm. Landing is true. Session, shared
    session, landing terminal, session terminal, null, archive, home and settings
    are all false.
  • test/mobile-only-styles.test.ts — the zero-desktop-change gate. It parses the
    three stylesheets and asserts every changed declaration appears only inside a
    @media (max-width: N) block, that .workspace-member-machine declares
    display: contents outside every block, that the blueprint action row stays
    flex on a phone, and that the scrim is deleted rather than hidden.
  • test/shell-smoke.test.tsx — two cases on the real CloudApp: a mobile
    workspace landing opens the rail, and Create workspace closes it first.
  • test/shell-mobile-drawer.test.tsx — the drawer closes through the strip's own
    button, and renders no scrim.
  • test/WorkspaceDetailsDialog.test.tsx — the chip, the type select, the volume
    meter and the actions menu share one .workspace-member-machine band.

Every one fails when its own source change is reverted. Measured:

Test Reverted Failure
mobile-rail-route the landing predicate expected false to be true
mobile-only-styles the full-width rail .shell-nav must declare width: 100% inside a max-width query: expected false to be true
mobile-only-styles the scrim deletion strip-rail.css: the scrim must be deleted, not hidden: expected true to be false
mobile-only-styles the create-workspace CSS .create-workspace-dialog must declare overflow-y: auto inside a max-width query: expected false to be true
mobile-only-styles the member wrapper CSS the desktop member machine wrapper must use display: contents: expected false to be true
shell-mobile-drawer the scrim deletion expected <button class="shell-nav-scrim shell-nav-scrim--open"> to be null
shell-smoke the workspace-landing effect expected 'false' to be 'true'
shell-smoke the create-workspace close expected 'true' to be 'false'
WorkspaceDetailsDialog the JSX wrapper member machine controls must share one mobile band: expected null not to be null

Gates, all run on the final tree:

npm test -w @blitzos/webapp
  Test Files  122 passed | 12 skipped (134)
       Tests  1027 passed | 1 expected fail | 62 skipped (1090)

npm run lint:gate
  Lint gate passed: 42 anti-slop findings; 0 blitz-house findings.
  every per-rule count equal to baseline; 8 max-lines warnings, unchanged

typecheck  webapp, schema and control-plane clean.
           control-plane needs its bindings regenerated from
           wrangler.toml.example first; the local wrangler.toml is stale
           machine noise, not a finding.

git diff --stat -- tools/oxlint/anti-slop   empty

The 12 skipped files are the existing missing-Lody-bundle skip path.

What is NOT proven: pixel geometry. jsdom has no layout engine, so no test
measures a rendered width. The stylesheet gate proves placement and the DOM
tests prove structure. I checked the four phone layouts by hand against a 390 x
844 mock built from tokens.css.

Deploy

The webApp ships inside the control-plane Worker. A merge to main deploys it to
canary with no other step. No box image and no payload change: this is browser
code.

# canary, automatic on merge to main
npm run deploy -w packages/control-plane

Rollback: revert the commit and merge. Canary redeploys the previous bundle.
Members get it on the next page load.

🤖 Generated with Claude Code

https://claude.ai/code/session_019ZvBaFqU4mgX75HAbiYJYV

Below 900px the rail slid in at min(88vw, 348px) over the chat, behind a
scrim. It now fills the width, and a workspace opens on it. Picking a
session hides it, so the pick reads as a page change.

Four more phone defects go with it:

- Create workspace opened at z-index 100 under the rail's z-index 400.
  Its handler now closes the rail first, like its five siblings.
- The create-workspace footer was pinned and ate 70px of every screen.
  Its buttons also hugged the right edge, because the base rule's
  justify-content survived the mobile override. The dialog now scrolls as
  one page, the header sticks, and the buttons end the form.
- The member row is an eight-column grid that dropped to three columns at
  620px, so five children auto-placed into 28px columns and overlapped.
  The four machine controls now share one wrapper: display: contents on
  desktop, a wrapped band on a phone.
- The scrim can no longer cover anything, so it is deleted rather than
  hidden.

Desktop does not move. Every edit sits inside an existing mobile media
query or behind mobileWebApp, and test/mobile-only-styles.test.ts proves
it by parsing the stylesheets.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZvBaFqU4mgX75HAbiYJYV
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.

1 participant