Cookie max-age was 900s (15 min, matching access token) but refresh token lives 7 days — users got bounced to login after 15 min idle because the Go SSR middleware rejected the expired cookie before JS could refresh. Now cookie max-age = 604800s (7 days) on both the client (auth.js) and server (auth.go OIDC callback). Go page-auth middleware accepts expired-but-signed JWTs via new parseJWTIgnoringExpiry() so the page shell renders and the Preact SDK can refresh client-side. API middleware still validates expiry strictly. 6 new middleware tests cover strict/lenient/tampered/garbage cases. VERSION bumped to 0.6.8 (rebrand was already shipped but file missed). ROADMAP-UI.md added with 7 milestones (v0.6.9–v0.6.15). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
172 lines
16 KiB
Markdown
172 lines
16 KiB
Markdown
# Armature — v0.6.x UI Hardening Roadmap
|
||
|
||
> **Goal**: Fix all viewport scaling, banner integration, and styling inconsistencies
|
||
> so Claude Code can run a meaningful automated usability survey against a
|
||
> stable, uniform UI.
|
||
|
||
---
|
||
|
||
## Problem Inventory
|
||
|
||
### P1 — Viewport & Scaling (blocks everything)
|
||
|
||
| # | Issue | Where | Impact |
|
||
|---|-------|-------|--------|
|
||
| 1 | **Dual layout systems** — `base.html` template has its own banner→surface→footer column; `app-shell.js` Preact shell has a separate `sw-shell` → `sw-shell__body` → `sw-shell__surface` column. They never coordinate. | `server/pages/templates/base.html` ↔ `src/js/sw/shell/app-shell.js` | Every surface inherits an ambiguous viewport ancestor. |
|
||
| 2 | **Transform-based scaling is broken** — `transform: scale()` on `#surfaceInner` doesn't reflow layout. Breaks `getBoundingClientRect` (menu.js already carries a scale-correction hack at line 20), scroll containment, pointer events, and the inverse-width/height hack (`100/s%`) doesn't account for banners consuming viewport space. | `base.html:56-67`, `appearance.js:41-53`, `menu.js:20-33` | Menus, tooltips, dropdowns all mis-positioned at any non-100% scale. Click targets wrong. |
|
||
| 3 | **Banner height desync** — `base.html` hardcodes `--banner-h: 28px` and renders banners in-flow. `app-shell.js` ShellBanner measures `offsetHeight` into `--banner-top-height` / `--banner-bottom-height` and uses `position: fixed` + body padding. Two independent banner systems. | `base.html:31,72-75,103-107` ↔ `app-shell.js:25-42`, `sw-shell.css:22-37,63-69` | With banners enabled, content overflows by the banner height or gets double-inset depending on which code path is active. |
|
||
| 4 | **`100vh` on mobile** — `.sw-shell`, `.chat-app`, `.chat-loading`, `.login-shell` use `100vh` which doesn't subtract mobile browser chrome (address bar, toolbar). | `sw-shell.css:9`, `chat/css/main.css:15,25`, `sw-login.css:7` | Content overflows on iOS Safari, Android Chrome. |
|
||
| 5 | **Chat uses `100vh` not `100%`** — `.chat-app { height: 100vh }` ignores parent container (which already excludes banners/footer). Notes correctly uses `height: 100%`. Every extension surface that copies chat's pattern will inherit the bug. | `packages/chat/css/main.css:15` | Chat surface overflows behind banners. |
|
||
|
||
### P2 — CSS Architecture
|
||
|
||
| # | Issue | Where | Impact |
|
||
|---|-------|-------|--------|
|
||
| 6 | **Class name collisions** — `.settings-section` defined in both `modals.css:41-43` and `surfaces.css:75-83` with different padding, margin, and border-bottom rules. `.sw-dropdown` in `primitives.css:443` (styled `<select>`) vs `sw-primitives.css:262` (custom dropdown component). `.sw-tabs` in `primitives.css:459` vs `sw-primitives.css:234`. | Kernel CSS | Styles randomly win depending on load order. Bug reports differ by surface. |
|
||
| 7 | **Old/new primitive duplication** — Two button systems (`.btn-primary`/`.btn-small` in `primitives.css` vs `.sw-btn--primary`/`.sw-btn--sm` in `sw-primitives.css`). Two toast systems (`.toast` vs `.sw-toast`). Two dropdown, two tabs implementations. | `primitives.css` ↔ `sw-primitives.css` | No single source of truth for any component. Extension authors can't know which to use. |
|
||
| 8 | **Extension CSS bleeds globally** — Extension `main.css` loaded via `<link>` at document level, no scoping. Any class name can override kernel or sibling extension styles. | `base.html:26-28`, `extension-surface.css` | Package CSS fights with kernel. Two packages with `.sidebar` both lose. |
|
||
| 9 | **No intermediate breakpoint** — Only `768px` mobile breakpoint. No tablet (768–1024px). Secondary workspace pane hardcoded `480px`. Admin nav hardcoded `200px`. | `layout.css:263`, `surfaces.css:305` | Cramped layout on iPad/small laptops. |
|
||
|
||
### P3 — Visual Consistency
|
||
|
||
| # | Issue | Where | Impact |
|
||
|---|-------|-------|--------|
|
||
| 10 | **Mixed unit systems** — `px`, `rem`, `em` used interchangeably with no rationale. Some font-sizes in `px` (primitives), others in `rem` (sw-primitives), others in `em` (user-picker). | All CSS files | Zoom/scale behaves differently per element. |
|
||
| 11 | **Google Fonts CDN dependency** — `@import url(...)` in `variables.css:5` blocks rendering if CDN is slow; breaks entirely on air-gapped deployments. | `variables.css:5` | FOUT or blank page on slow networks. No offline support. |
|
||
| 12 | **No spacing scale** — Padding/margins are ad-hoc values (10px, 12px, 14px, 16px, 20px, 24px, 28px…). No design-token spacing scale. | All CSS | Inconsistent visual rhythm across surfaces. |
|
||
| 13 | **Hardcoded fallback colors in component CSS** — `sw-shell.css`, `sw-primitives.css` have inline fallbacks like `var(--accent, #b38a4e)` — a gold color from a previous theme that doesn't match the current blue `#6c9fff`. | `sw-shell.css`, `sw-primitives.css` | Wrong colors flash briefly if variables load late. |
|
||
|
||
---
|
||
|
||
## Roadmap
|
||
|
||
### v0.6.9 — Session Lifetime Config
|
||
|
||
Make session duration admin-configurable and expose "keep me logged in" in the
|
||
login UI. The cookie max-age bug is already fixed (v0.6.8 shipped with 15-min
|
||
cookie vs 7-day refresh token; now both are 7 days, and the Go SSR middleware
|
||
accepts expired-but-signed JWTs so JS can refresh client-side).
|
||
|
||
| Step | Description |
|
||
|------|-------------|
|
||
| Admin session settings | Add `session.access_token_ttl` (default `15m`) and `session.refresh_token_ttl` (default `7d`) to the admin settings surface. Stored in the `settings` table, read by `generateTokens()`. Both values clamp to a sane range (access: 5m–60m, refresh: 1h–90d). |
|
||
| "Keep me logged in" checkbox | Login form gets an opt-in checkbox. When checked, the refresh token gets the full `refresh_token_ttl`. When unchecked, refresh token lifetime = `24h` (session-length). The cookie max-age tracks whichever lifetime was chosen. |
|
||
| Token generation uses config | `handlers/auth.go generateTokens()` reads TTLs from config instead of hardcoded `15 * time.Minute` / `7 * 24 * time.Hour`. The `expires_in` field in the JSON response reflects the actual access TTL so the client schedules refresh correctly. |
|
||
| Idle timeout (optional) | If the admin enables idle timeout (default: off), the server checks `last_activity_at` on the refresh-token row. If the gap exceeds `session.idle_timeout` (e.g. `2h`), the refresh is rejected. The client SDK pings `/api/v1/auth/activity` on user interaction (debounced, max once per minute). |
|
||
| Validation | Test matrix: default config, short access (5m), long refresh (30d), keep-me-logged-in on/off, idle timeout on/off. Background tab for >access TTL then navigate — should not redirect to login. |
|
||
|
||
### v0.6.10 — Viewport Foundation
|
||
|
||
Establish a single, correct layout model. Every surface must render inside one
|
||
containment chain: `body → shell → surface`. No dual systems. No transform hacks.
|
||
|
||
| Step | Description |
|
||
|------|-------------|
|
||
| Unify shell layout | Remove the inline `<style>` block from `base.html` that creates a parallel column layout. Make `base.html` render a single `<div class="sw-shell">` wrapper. Banners, announcements, footer, and surface all live inside the Preact shell's flex column OR the template column — pick one, kill the other. **Recommendation**: keep the Go template column (it works without JS) and delete `sw-shell.css`'s parallel layout. The Preact `AppShell` becomes a component that renders *inside* `#surfaceInner`, not a viewport-level container. |
|
||
| Banner single source of truth | One banner rendering path. Template banners (`base.html:72-107`) set `--banner-top-height` and `--banner-bottom-height` via a `<script>` that measures after DOMContentLoaded. Remove `ShellBanner`'s JS measurement. Surfaces use `calc(100vh - var(--banner-top-height) - var(--banner-bottom-height))` or pure flex containment — not both. |
|
||
| Replace transform-scale with CSS `zoom` | CSS `zoom` is now supported in all evergreen browsers (including Firefox 126+, June 2024). `zoom` actually reflows layout — `getBoundingClientRect` returns correct values, scroll containers work, pointer events are accurate. Remove all `transform: scale()` code from `base.html`, `appearance.js`, `menu.js` scale-correction hacks. Apply `zoom` on `#surfaceInner`. The `sw.shell.getScale()` API becomes a no-op returning 1 (or reads `getComputedStyle().zoom`). |
|
||
| Fix `100vh` → `100dvh` | Replace all `height: 100vh` with `height: 100dvh` (fallback: `height: 100vh; height: 100dvh;`). Affects: `sw-shell.css`, `sw-login.css`, `chat/css/main.css`. |
|
||
| Chat `100vh` → `100%` | `.chat-app` and `.chat-loading` use `height: 100%` like Notes. They inherit their height from the extension mount container which is already correctly sized. |
|
||
| Validation | Manual test matrix: banner on/off × scale 80%/100%/150% × mobile/desktop × light/dark. Every surface (admin, settings, team-admin, docs, welcome, notes, chat). No overflow, no gaps, menus positioned correctly. |
|
||
|
||
### v0.6.11 — CSS Deduplication
|
||
|
||
Kill the old primitive system. One class per concept.
|
||
|
||
| Step | Description |
|
||
|------|-------------|
|
||
| Audit collision inventory | Script that finds all duplicate class selectors across kernel CSS files. Produce a machine-readable collision report (JSON). |
|
||
| Migrate `.settings-section` | Remove `modals.css:41-43` definition. The `surfaces.css` definition is authoritative. Modals that used the old styles get explicit overrides scoped to `.modal .settings-section`. |
|
||
| Retire `primitives.css` old components | `.btn-primary`, `.btn-small`, `.btn-danger`, `.btn-full` → migrate all usages to `.sw-btn--*`. `.toast-container` / `.toast` → migrate to `.sw-toast-container` / `.sw-toast`. Old `.popup-menu` → migrate to `.sw-menu`. Old `.sw-dropdown` (styled select in primitives.css) → rename to `.sw-native-select` or delete if unused. Old `.sw-tabs` / `.sw-tab-btn` in primitives.css → delete (sw-primitives.css version is authoritative). |
|
||
| Mark deprecated classes | Any remaining old classes get a `/* DEPRECATED v0.6.11 — use .sw-btn--* */` comment and a 1-version grace period for package authors. |
|
||
| Package CSS audit | Scan all `packages/*/css/main.css` for references to deprecated kernel classes. Fix in-tree packages. |
|
||
|
||
### v0.6.12 — Extension CSS Isolation
|
||
|
||
Prevent extension CSS from leaking into the kernel or sibling extensions.
|
||
|
||
| Step | Description |
|
||
|------|-------------|
|
||
| Scoping strategy | Two options: (A) `@scope (.extension-mount[data-ext="slug"])` — CSS `@scope` is supported in Chrome 118+, Firefox 128+, Safari 17.4+. (B) Prefix enforcement — package CSS linter rejects selectors that don't start with `.ext-{slug}` or `[data-ext="{slug}"]`. **Recommendation**: (B) prefix enforcement. `@scope` support is still patchy; prefix enforcement works everywhere and is trivially lintable. |
|
||
| Linter | `scripts/lint-package-css.sh` — shell script using `grep`/`awk`. Runs in CI. Accepts `.ext-{slug}` or `[data-ext="{slug}"]` prefixed selectors only. Exempts `:root`, `@keyframes`, `@font-face`, and `@media` wrappers. |
|
||
| Kernel CSS contract | Document which kernel classes are stable public API for extensions to reference (`.sw-btn--*`, `.sw-input`, `.sw-field`, `.sw-dialog`, `.sw-toast`, `.sw-menu`, `.sw-tabs`, CSS variables). Everything else is internal. Publish in `docs/EXTENSION-CSS.md`. |
|
||
| Migrate in-tree packages | Add `data-ext` attribute to extension mount container. Prefix all in-tree package CSS. Verify no visual regressions. |
|
||
|
||
### v0.6.13 — Responsive & Spacing
|
||
|
||
Fill the gaps between mobile and desktop.
|
||
|
||
| Step | Description |
|
||
|------|-------------|
|
||
| Tablet breakpoint | Add `@media (min-width: 769px) and (max-width: 1024px)` rules. Secondary workspace pane: `360px` (from `480px`). Admin nav collapses to icon-only at this breakpoint. Settings nav: `180px`. |
|
||
| Spacing scale | Define spacing tokens in `variables.css`: `--space-1: 4px` through `--space-8: 64px` (4px base, doubling). Migrate hardcoded padding/margin values to tokens. Priority: surfaces that users see on every session (sidebar, topbar, admin content areas). |
|
||
| Font-size scale | Define `--text-xs: 0.75rem` through `--text-xl: 1.25rem` (5 stops). Migrate all font-size declarations. Remove `em`-based sizes (user-picker) and bare `px` sizes. |
|
||
| Secondary pane responsive | `workspace-secondary.open` width uses `clamp(320px, 35vw, 560px)` instead of fixed `480px`. |
|
||
|
||
### v0.6.14 — Visual Polish
|
||
|
||
Systematic cleanup of stale values and rendering artifacts.
|
||
|
||
| Step | Description |
|
||
|------|-------------|
|
||
| Purge stale fallback colors | Remove all `var(--foo, #hexvalue)` fallbacks that reference old gold theme colors (`#b38a4e` and variants). If a CSS variable is undefined at this point, that's a bug — don't mask it with a wrong fallback. |
|
||
| Self-host fonts | Bundle DM Sans and JetBrains Mono woff2 files in `src/fonts/`. Replace Google Fonts `@import` with local `@font-face` declarations. Zero external font dependencies. |
|
||
| Light theme audit | Walk every surface in light mode. Fix any remaining hardcoded dark-mode colors, contrast issues, or invisible borders. Document any surface-specific light-mode overrides. |
|
||
| Consistent border-radius | Surfaces currently mix `6px`, `8px`, `10px`, `var(--radius)`, `var(--radius-lg)`. Audit and reduce to 3 values: `--radius-sm: 4px`, `--radius: 8px`, `--radius-lg: 12px`. |
|
||
|
||
### v0.6.15 — Usability Survey Gate
|
||
|
||
Make the UI machine-auditable so Claude Code can run an automated survey.
|
||
|
||
| Step | Description |
|
||
|------|-------------|
|
||
| UI inventory manifest | `scripts/generate-ui-inventory.sh` — walks kernel CSS + package CSS, extracts every rendered class, groups by surface, outputs `ui-inventory.json`. Fields: `{surface, component, classes, file, line, responsive_breakpoints, spacing_tokens_used, font_size_tokens_used}`. |
|
||
| Contrast checker | Script that parses `variables.css` light/dark token pairs and checks WCAG AA contrast ratios for all `text-*` on `bg-*` combinations. Outputs pass/fail report. |
|
||
| Component coverage matrix | Markdown table: each kernel primitive (button, input, dropdown, dialog, toast, menu, tabs, avatar, spinner, tooltip, drawer, banner) × each surface that uses it. Identifies surfaces still using deprecated old-style components. |
|
||
| Touch target audit | Script that finds all `<button>`, `[role=button]`, clickable elements across templates and JS, checks for `min-width/min-height ≥ 44px` on mobile breakpoint. |
|
||
| Claude Code survey prompt | `docs/USABILITY-SURVEY.md` — structured prompt template that Claude Code can execute against the codebase. Sections: viewport correctness, banner integration, responsive behavior, styling consistency, accessibility basics (contrast, touch targets, focus indicators), component uniformity. Each section has pass/fail criteria and file paths to inspect. |
|
||
| Survey dry-run | Run the survey. Fix anything it catches. Tag `v0.6.15` only after clean survey. |
|
||
|
||
---
|
||
|
||
## Sequencing Rationale
|
||
|
||
```
|
||
v0.6.9 Session Lifetime Config ← Cookie bug fix already shipped; make TTLs configurable
|
||
↓
|
||
v0.6.10 Viewport Foundation ← Everything depends on correct containment
|
||
↓
|
||
v0.6.11 CSS Deduplication ← Can't audit styling until there's one source of truth
|
||
↓
|
||
v0.6.12 Extension CSS Isolation ← Scoping requires the kernel CSS to be stable first
|
||
↓
|
||
v0.6.13 Responsive & Spacing ← Spacing tokens need stable class names to attach to
|
||
↓
|
||
v0.6.14 Visual Polish ← Final visual pass after structure is locked
|
||
↓
|
||
v0.6.15 Usability Survey Gate ← Machine-audit the finished product
|
||
```
|
||
|
||
Each version is independently shippable and testable. No version depends on
|
||
anything after it.
|
||
|
||
---
|
||
|
||
## What "Usability Survey by Code" Means
|
||
|
||
The v0.6.15 deliverables give Claude Code (or any automated tool) three things:
|
||
|
||
1. **A structured inventory** (`ui-inventory.json`) of every component, on every
|
||
surface, with its CSS file and line number.
|
||
|
||
2. **Automated checks** (contrast, touch targets, token usage) that produce
|
||
machine-readable pass/fail reports.
|
||
|
||
3. **A survey prompt** (`USABILITY-SURVEY.md`) with explicit criteria and file
|
||
paths, so Claude Code can `cat` the relevant files, run the scripts, and
|
||
produce a scored report without human guidance.
|
||
|
||
The survey is not a substitute for real user testing — it's a structural
|
||
quality gate that catches the class of bugs (overflow, contrast, misalignment,
|
||
inconsistent styling) that have historically slipped through.
|