docs: mobile responsive design spec and implementation plan for web/

This commit is contained in:
mrhid6
2026-07-27 22:24:26 +01:00
parent 20e57d19c7
commit effd991c31
2 changed files with 1105 additions and 0 deletions
@@ -0,0 +1,177 @@
# Control plane (`web/`) — mobile responsive design
Date: 2026-07-27
Scope: `web/` only. `site/` and `adminsite/` are untouched.
## Problem
`web/` was built for a desktop console and has no mobile handling at all.
- `Sidebar` is a fixed `w-60 h-screen` aside rendered unconditionally by
`app/(app)/layout.tsx`. On a 390px phone it eats 62% of the width.
- Every page opens with `p-8` — 64px of horizontal padding on a screen that has
390px to give.
- Six list pages render 46 column tables. They scroll horizontally, so nothing
overflows the page, but reading a row means swiping.
- The workflow builder is a hard `grid-cols-[1fr_320px]` with `w-[340px]` nodes.
At 390px the inspector alone exceeds the viewport.
- Several grids are unprefixed (`grid-cols-3`, `grid-cols-2`, `grid-cols-4`) and
never collapse.
Next's App Router injects `width=device-width, initial-scale=1` by default, so
the breakpoints *do* fire. This is a layout problem, not a viewport one.
## Decisions
| Decision | Choice | Why |
| --- | --- | --- |
| Sidebar collapse breakpoint | `lg` (< 1024px) | Content is dense — tables plus `lg:grid-cols-3` side rails. Reclaiming 240px helps tablets as much as phones, and `lg` is already where the app's own two-and-three column layouts switch. |
| Table treatment on phones | Card stack below `sm` | Horizontal swiping to read a hostname's status is the single worst thing about the current app on a phone. |
| Workflow builder / console | Best-effort responsive | Usable, not redesigned. No blocking notice — a cramped console beats no console. |
| Verification | Static audit + `next build` + `next lint` | The app is auth-gated behind Mongo, Redis and the Go server; none run in this environment. |
## Design
### 1. The shell
A new client component `web/components/AppShell.tsx` owns the responsive chrome
so `app/(app)/layout.tsx` stays a server component:
```
AppShell (client, holds `open` state)
├── <aside class="hidden lg:flex"> ← permanent sidebar, unchanged look
├── mobile top bar (lg:hidden, sticky, h-14)
│ hamburger · Logo · "Vantage" · instance name
├── offcanvas (lg:hidden, fixed inset-0 z-50)
│ backdrop (bg-black/60) + w-72 panel, translate-x transition
└── <main class="flex-1 overflow-y-auto"> ← LicenseBanner + children
```
`Sidebar.tsx` splits into:
- `SidebarContent` — the nav list, user block and logout. **One copy**, rendered
by both the permanent aside and the offcanvas panel. It takes an optional
`onNavigate` callback so the offcanvas can close on link click.
- `Sidebar` — the permanent `hidden lg:flex` aside.
- `SidebarDrawer` — the offcanvas.
`navItems` and the `activeHref` reduction move to module scope so both
containers share them. The active-item accent bar, the instance name in the
header and the user/logout footer all appear in both, unchanged.
Offcanvas behaviour:
- Closes on route change (`usePathname` effect), on Escape, on backdrop click
and on any nav link click.
- Locks `document.body.style.overflow` while open, restores on close.
- `aria-expanded` / `aria-controls` on the hamburger; `role="dialog"` and
`aria-modal="true"` on the panel; `aria-label` on the button.
- Focus moves into the panel on open and returns to the hamburger on close.
- The panel is always mounted so the slide transition runs in both directions;
it carries `pointer-events-none invisible` when closed rather than being
unmounted.
The top bar is `sticky top-0 z-40` inside the scroll container so it stays
reachable on long pages.
### 2. Tables become card stacks without duplicating markup
The responsive mode lives in the primitives (`web/components/ui/Table.tsx`),
not in each page. Writing two parallel trees per page — a `<table>` for desktop
and a `<div>` stack for mobile — would double six pages of markup and drift
apart on the first edit.
`Td` gains an optional `label`. Below `sm` the table flips to block layout:
| Element | Added classes (below `sm`) |
| --- | --- |
| `Table` | `max-sm:block` |
| `Thead` | `max-sm:hidden` |
| `Tbody` | `max-sm:block max-sm:divide-y-0 max-sm:space-y-3 max-sm:p-3` |
| `Tr` | `max-sm:block max-sm:rounded max-sm:border max-sm:border-border max-sm:bg-surface-2/40 max-sm:p-3` |
| `Td` | `max-sm:flex max-sm:items-start max-sm:justify-between max-sm:gap-4 max-sm:px-0 max-sm:py-1.5` |
When `label` is present, `Td` renders it in a `sm:hidden` span using the exact
mono keyed-label idiom `Th` already uses — `font-mono text-[0.68rem] uppercase
tracking-[0.13em] text-text-secondary`. The key/value pairing on a phone is the
same visual device as the column head on a desktop, because it means the same
thing.
A `Td` with no `label` (the trailing action cell) renders its child alone,
right-aligned in the card.
Pages change only by adding `label="Hostname"` to their cells. Affected:
`servers`, `keys`, `monitors`, `secrets`, `secrets/[group]`, `workflows`,
`workflows/[id]/runs`, `audit`, `keys/[id]`, `servers/[id]` (two tables),
`monitors/[id]`, and `components/settings/MembersCard.tsx`.
### 3. Page padding and headers
- `p-8``p-4 sm:p-6 lg:p-8`, everywhere it opens a page or a page-level
error/loading state — 30 occurrences across 21 files.
- Title-plus-action header rows: `flex items-center justify-between`
`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between`. The
action button then sits under the title on a phone rather than squeezing it.
### 4. Modal becomes a bottom sheet under `sm`
`Modal.tsx`: `items-center``items-end sm:items-center`, wrapper `p-4`
`p-0 sm:p-4`, panel gets `rounded-b-none sm:rounded` and `max-h-[85dvh]`
(`dvh`, not `vh` — mobile browser chrome makes `vh` overshoot). Sheets are what
phones expect for a modal, and it costs four classes.
### 5. Workflow builder
Below `lg` the fixed-height two-column grid is dropped entirely: single column,
natural page flow, canvas scrolls with the page.
- `grid h-[calc(100vh-53px)] grid-cols-[1fr_320px]`
`flex flex-col lg:grid lg:h-[calc(100dvh-53px)] lg:grid-cols-[1fr_320px]`.
The viewport-height calculation is `lg:`-only, which matters because the
mobile top bar changes the arithmetic and `100vh` is wrong on mobile anyway.
- Node width `w-[340px]``w-full lg:w-[340px]`; the column wrapper
`w-[340px]``w-full max-w-[340px]`.
- Canvas padding `p-8``p-4 sm:p-6 lg:p-8`.
- The inspector `<aside>` becomes a collapsible bottom panel below `lg`: it
keeps its place in the flex column, gains a top border instead of a left one,
and is hidden until a step is selected (on a phone an empty "Select a step to
configure it" panel is noise).
- The builder's own header row wraps: the action cluster moves to a second line
under `sm`.
### 6. Remaining fixed layouts
| File | Change |
| --- | --- |
| `servers/[id]/page.tsx:164` | `grid-cols-3``grid-cols-2 sm:grid-cols-3` |
| `servers/[id]/page.tsx:495` | install one-liner `min-w-64``min-w-0` so it scrolls internally instead of widening the page |
| `secrets/page.tsx:53` | `grid-cols-2``grid-cols-1 sm:grid-cols-2` |
| `monitors/MonitorForm.tsx:76` | `grid-cols-4``grid-cols-2 sm:grid-cols-4` |
| `monitors/MonitorForm.tsx:98,123,144` | `grid-cols-2``grid-cols-1 sm:grid-cols-2` |
| `workflows/StepPickerModal.tsx:132,168` | `grid-cols-2``grid-cols-1 sm:grid-cols-2` |
| `workflows/[id]/runs/[runId]/page.tsx:254` | matrix table wrapped in `overflow-x-auto`; it is a genuine matrix and stays scrollable |
| `workflows/[id]/runs/[runId]/page.tsx:343` | `p-8``p-4 sm:p-6 lg:p-8` |
| `servers/[id]/console/page.tsx:168` | `p-8``p-4 sm:p-6 lg:p-8`; header/toolbar rows wrap |
The run-detail matrix and the console canvas are the two places that keep
horizontal scrolling. Both are genuinely two-dimensional; stacking them would
destroy the information.
## Non-goals
- No changes to `site/` or `adminsite/`.
- No redesign of the console for touch input (no on-screen keyboard work).
- No new dependencies. Tailwind's `max-sm:` variant and `translate-x` are
enough; no headless-UI or animation library.
- No changes to any API, route or data shape. This is presentation only.
## Verification
1. **Audit** — after the edits, `grep` must return no unprefixed `p-8`,
no unprefixed `grid-cols-[2-9]`, and no `w-[3` fixed node widths outside a
`lg:` prefix in `web/app` and `web/components`.
2. `npx next lint` passes with no new warnings.
3. `npx next build` succeeds.
Screenshot verification is out of scope: the app is auth-gated behind Mongo,
Redis and the Go server, none of which run in this environment.