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>
16 KiB
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:
-
A structured inventory (
ui-inventory.json) of every component, on every surface, with its CSS file and line number. -
Automated checks (contrast, touch targets, token usage) that produce machine-readable pass/fail reports.
-
A survey prompt (
USABILITY-SURVEY.md) with explicit criteria and file paths, so Claude Code cancatthe 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.