Skip to content

feat(os): mobile responsive shell below 768px (NEXT.md 3.1) - #100

Merged
xcjs merged 2 commits into
mainfrom
feat/mobile-shell
Oct 4, 2026
Merged

xcjs merged 2 commits into
mainfrom
feat/mobile-shell

Conversation

@xcjs

@xcjs xcjs commented Oct 3, 2026

Copy link
Copy Markdown
Owner

Closes #99 (NEXT.md step 3.1).

What

Below the md breakpoint (768px) the desktop shell degrades to a phone
layout; at md and above nothing changes visually or behaviorally.

  • WindowFrame: below md each window renders as a stacked full-width
    card - stored x/y/width/height are overridden by max-md:! utilities
    (static, margins, h-[70vh]), so the stored geometry is ignored as the
    issue requires. Dragging is a no-op at phone widths (useMediaQuery
    guard in onPointerDown, max-md:cursor-default) - ADR 0004 "touch-
    optimized gestures" deferred, taps still work. Focusing a card scrolls
    it into view (the phone equivalent of raising it).
  • Taskbar: becomes a touch-sized bottom sheet (h-14, w-11/
    h-11 controls) with a horizontally scrollable control strip
    (overflow-x-auto, shrink-0 items); DOMinic label and dividers hide
    below md. Pinned as an absolute overlay with env(safe-area-inset-
    bottom) padding. Desktop taskbar markup is byte-identical in behavior.
  • app.vue root: scrolls vertically below md so stacked cards flow
    above the sheet; clock hidden at phone widths (taskbar overlay would
    overlap it).

Files

  • app/app.vue
  • app/features/os/components/WindowFrame.vue
  • app/features/os/components/Taskbar.vue
  • tests/e2e/mobile-shell.spec.ts (new)

Verification

  • typecheck exit 0; production build exit 0
  • full e2e suite 3/3 (core-loop x2 + new mobile-shell spec at 390x844):
    card width bounds, drag no-op, chat stacked above insertion order,
    focus scroll-into-view, sheet geometry, minimize/restore, reload
    persistence - all verified locally against the production build

Notes

  • app/features/os/** is this lane's; app/app.vue touches
    composition-root wiring only (launcher/clock repositioning declared in
    the issue); tests/e2e/** is quality lane - review request goes to the
    owner(s) of the other two paths.
  • git rebase-safe: no kernel stores changed; os.ts untouched.

WindowFrame degrades to stacked full-width cards (stored geometry
overridden below md, drag disabled via useMediaQuery, focused card
scrolls into view). Taskbar becomes a touch-sized bottom sheet with
a horizontally scrollable control strip, pinned as an overlay with
safe-area padding. Root scrolls instead of clipping so cards stack
above the sheet. Phone-viewport e2e covers stacking, drag no-op,
scroll-into-view, sheet geometry, minimize/restore, reload.

@m-vawter m-vawter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work. The stacked cards, the drag guard and the spec's coverage (width, drag, stacking, focus, minimize and restore, reload) all read well, and CI is green.

One thing breaks the sheet itself: it doesn't stay at the bottom once the cards scroll. With Agent Chat plus one app at 390×844, the page is 1290px tall. I measured the footer in a production build with this branch merged into main:

  • before scrolling: top 788, bottom 844, at the bottom of the screen;
  • after scrolling the root 446px: top 342, bottom 398, mid-screen over the cards.

The footer is absolute inside the root, and below md the root is now the scroll container, so the footer scrolls with the content. max-md:fixed pins it to the viewport, and the root's max-md:pb-16 already keeps the last card clear of it. With that one class (suggested inline), the sheet stays at 788–844 after the same scroll, and this spec and core-loop still pass.

For the spec (tests lane), all optional:

  1. A guard for this:
    await test.step('the sheet stays at the bottom while the cards scroll', async () => {
      await page.locator('footer').evaluate((f) => {
        const root = f.closest('.h-screen') as HTMLElement
        root.scrollTop = root.scrollHeight
      })
      const sheet = await page.locator('footer').boundingBox()
      expect(sheet!.y + sheet!.height).toBeCloseTo(844, 0)
    })
  2. The controls themselves. The "touch-sized controls" step measures the sheet, not the controls. In the same build, the sheet's buttons are 42px tall and the launcher pills 38px, a little under the 44px that h-11 aims for. If 44 is the target, assert it on the buttons.
  3. Small ones. The comment says the cards never overlap; chat.y + chat.height <= pomodoro.y would check it. And Playwright's boundingBox() is viewport-relative, so the "document-relative" comment can go once the sheet is fixed.

Requesting changes for the pinning only; the rest is optional.

Comment thread app/features/os/components/Taskbar.vue Outdated
class="flex min-w-0 items-center gap-1.5 px-3 text-xs text-slate-200 hover:bg-slate-700"
:aria-label="`Open ${app.title}`"
@click="props.onOpenApp(app.id)"
<footer class="absolute bottom-0 left-0 right-0 z-40 flex h-12 max-md:h-14 items-center gap-2 max-md:gap-1 border-t border-white/10 bg-slate-950/90 px-3 max-md:px-2 backdrop-blur max-md:pb-[max(env(safe-area-inset-bottom),0.25rem)]">

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<footer class="absolute bottom-0 left-0 right-0 z-40 flex h-12 max-md:h-14 items-center gap-2 max-md:gap-1 border-t border-white/10 bg-slate-950/90 px-3 max-md:px-2 backdrop-blur max-md:pb-[max(env(safe-area-inset-bottom),0.25rem)]">
<footer class="absolute max-md:fixed bottom-0 left-0 right-0 z-40 flex h-12 max-md:h-14 items-center gap-2 max-md:gap-1 border-t border-white/10 bg-slate-950/90 px-3 max-md:px-2 backdrop-blur max-md:pb-[max(env(safe-area-inset-bottom),0.25rem)]">

Pins the sheet to the viewport below md. Verified with the scroll measurement above.

@xcjs

xcjs commented Oct 3, 2026

Copy link
Copy Markdown
Owner Author

Fixed on 9b40518: footer is now max-md:fixed so the sheet stays pinned at the viewport bottom while the cards scroll (measured 844 bottom after full scroll locally). Spec gains the scroll-pinning guard and the non-overlap check; the touch-target step now asserts every sheet button (bumped to max-md:py-2.5 so the window pills reach 44px). Optional items applied too. Full e2e 3/3 green locally.

@xcjs
xcjs requested a review from m-vawter October 3, 2026 14:03

@m-vawter m-vawter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at 9b40518 in a fresh container, with this branch merged into current main. The full e2e suite passes (5/5), and the sheet now stays pinned: after the stacked cards scroll 446px, its bottom edge is still at 844 of 844. Thanks for adding the scroll guard and the overlap check.

One footnote, not blocking. Every sheet button measures 42px tall in that build (the 44px h-11 box minus a 1px border top and bottom), and the spec asserts ≥ 38. That's well above WCAG 2.2's 24px minimum, so it's fine as is. If 44 is the goal, the border is the last 2px.

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.

3.1 Mobile responsive shell: bottom-sheet taskbar, stacked windows below 768px

2 participants