diff --git a/.planning/phases/08-stack-upgrade-backend-decomposition/08-UI-SPEC.md b/.planning/phases/08-stack-upgrade-backend-decomposition/08-UI-SPEC.md new file mode 100644 index 0000000..e2a8c60 --- /dev/null +++ b/.planning/phases/08-stack-upgrade-backend-decomposition/08-UI-SPEC.md @@ -0,0 +1,227 @@ +--- +phase: 8 +slug: stack-upgrade-backend-decomposition +status: draft +shadcn_initialized: false +preset: none +created: 2026-06-07 +--- + +# Phase 8 — UI Design Contract + +> Visual and interaction contract for Phase 8: Stack Upgrade & Backend Decomposition. +> Generated by gsd-ui-researcher. Verified by gsd-ui-checker. + +## Scope Note + +Phase 8 is a refactoring phase. The backend router decomposition, frontend API client +decomposition, and dependency bumps are invisible to users. The ONLY user-facing UI +surface in this phase is: + +1. **sessions-revoked notification** — `SettingsAccountTab.vue` and `TotpEnrollment.vue` + already display inline session-revoked feedback using local `ref` state. Phase 8 (Wave 1 — + Phase 7.1 absorbed) replaces that local state with a call to `toastStore.show(...)`. +2. **`useToastStore` stub** — a new Pinia store with a defined `show()` API contract that + Phase 10 will implement fully. The stub must not render anything; it only defines the call + contract so Phase 7.1 call sites and Phase 10 implementation are aligned. + +All other design contract sections (spacing, typography, color) document the **existing +design system** inherited by Phase 8. No new visual patterns are introduced. + +--- + +## Design System + +| Property | Value | +|----------|-------| +| Tool | none — Tailwind CSS utility classes only | +| Preset | not applicable | +| Component library | none (hand-rolled Vue 3 components) | +| Icon library | Heroicons — inline SVG paths (`stroke="currentColor"`, `stroke-width="1.5"` or `"2"`) | +| Font | system-ui / browser default (no custom font loaded) | + +Source: `frontend/tailwind.config.js` (no plugins, no theme extensions), `App.vue`, existing component audit. + +--- + +## Spacing Scale + +Declared values (must be multiples of 4): + +| Token | Value | Usage | +|-------|-------|-------| +| xs | 4px | Icon gaps (`gap-1`, `gap-1.5`), badge inner padding | +| sm | 8px | Compact element spacing (`gap-2`, `mb-1`, `py-2`) | +| md | 16px | Default element spacing (`space-y-4`, `gap-4`, `px-3 py-3` inputs) | +| lg | 24px | Section padding (`p-6`), section gaps (`space-y-6`) | +| xl | 32px | Layout gaps (sidebar + main content separation) | +| 2xl | 48px | Not actively used in Phase 8 scope | +| 3xl | 64px | Not actively used in Phase 8 scope | + +Exceptions: none for Phase 8 scope. + +Source: `SettingsAccountTab.vue` (`p-6`, `space-y-6`, `space-y-4`, `gap-3`), `TotpEnrollment.vue` (`space-y-4`, `gap-2`, `px-6 py-2.5`). + +--- + +## Typography + +| Role | Size | Weight | Line Height | +|------|------|--------|-------------| +| Body | 14px (`text-sm`) | 400 (regular) | 1.5 | +| Label / caption | 12px (`text-xs`) | 400 (regular) | 1.5 | +| Section heading | 14px (`text-sm`) | 600 (`font-semibold`) | 1.2 | +| Form label | 14px (`text-sm`) | 600 (`font-semibold`) | 1.2 | + +Note: Phase 8 introduces no new typography. The system uses exactly 2 sizes (14px, 12px) +and exactly 2 weights (400, 600) for the settings/auth component surface touched in Wave 1. + +Source: `SettingsAccountTab.vue` (`text-sm font-semibold text-gray-800` for headings, +`text-sm text-gray-600/700` for body, `text-xs text-red-600` for error captions). + +--- + +## Color + +| Role | Value | Usage | +|------|-------|-------| +| Dominant (60%) | `#ffffff` / `bg-white` | Page background, card surfaces | +| Secondary (30%) | `#f9fafb` / `bg-gray-50` | Input backgrounds, code blocks, secondary surfaces | +| Accent (10%) | `#4f46e5` / `bg-indigo-600` | Primary action buttons, focus rings (`focus:ring-indigo-500`) | +| Success semantic | `#16a34a` / `text-green-600`, `border-green-200` | Session-revoked notification, success confirmations | +| Destructive | `#dc2626` / `text-red-600`, `border-red-300` | Destructive action buttons ("Disable 2FA", "Sign out all devices"), error states | + +Accent (`indigo-600`) reserved for: +- Primary submit buttons (`bg-indigo-600 hover:bg-indigo-700`) +- Input focus rings (`focus:ring-indigo-500 focus:border-indigo-500`) +- Role badge for admin users (`bg-indigo-100 text-indigo-700`) + +Source: `SettingsAccountTab.vue`, `TotpEnrollment.vue` — exhaustive class audit. + +--- + +## `useToastStore` API Contract + +This is the primary design deliverable for Phase 8 (Wave 1). The stub Pinia store must +define and export exactly this `show()` signature. Phase 10 will implement the rendering. + +### Store location + +`frontend/src/stores/toast.js` + +### `show()` method signature + +```js +toastStore.show(message, type = 'success', duration = 4000) +``` + +| Parameter | Type | Values | Default | Notes | +|-----------|------|--------|---------|-------| +| `message` | `string` | Any non-empty string | required | Plain text only — no HTML | +| `type` | `string` | `'success'` \| `'error'` \| `'info'` | `'success'` | Controls icon and border color in Phase 10 | +| `duration` | `number` | Milliseconds until auto-dismiss | `4000` | `0` = persist until manually dismissed (Phase 10 contract) | + +### Stub implementation contract + +The stub MUST: +- Export `useToastStore` as a named export from `stores/toast.js` +- Expose `show(message, type, duration)` as a callable method +- NOT throw, NOT warn, NOT render anything — silently no-op in Phase 8 + +The stub MUST NOT: +- Accept an object argument shape (e.g. `show({ message, type })`) — positional parameters only, for simplicity +- Render a DOM element or inject CSS +- Import or depend on any component + +### Phase 10 rendering contract (locked now to align implementor) + +When Phase 10 implements the full store, it MUST honor the same `show()` signature without +modification to any Phase 8 call site. The rendering target is a fixed-positioned stack at +`top-4 right-4 z-50` (matching the existing inline toast placement in `SettingsAccountTab.vue`). +Auto-dismiss fires after `duration` ms. Manual dismiss on click. Toasts stack vertically with +`gap-2` between items. No interaction blocking. + +--- + +## Sessions-Revoked Notification — Interaction Contract + +### Current state (before Phase 8 Wave 1) + +Both `SettingsAccountTab.vue` and `TotpEnrollment.vue` implement sessions-revoked feedback +with identical local state: `const sessionRevokedToast = ref(false)` + `setTimeout(..., 5000)`. + +### Target state (after Phase 8 Wave 1) + +The local `sessionRevokedToast` ref and `setTimeout` are removed from both components. +The API response handler calls `toastStore.show(...)` instead. + +### Trigger conditions + +| Component | Trigger | Call | +|-----------|---------|------| +| `SettingsAccountTab.vue` — `changePassword()` | `data.sessions_revoked > 0` | `toastStore.show('Other sessions have been terminated.', 'success')` | +| `SettingsAccountTab.vue` — `disableTotp()` | `data.sessions_revoked > 0` | `toastStore.show('Other sessions have been terminated.', 'success')` | +| `TotpEnrollment.vue` — `confirmEnrollment()` | `data.sessions_revoked > 0` | `toastStore.show('Other sessions have been terminated.', 'success')` | + +### Message copy (locked) + +`'Other sessions have been terminated.'` + +This exact string is used in all three call sites. It matches the copy already displayed +by the existing inline implementation (confirmed in both component files). + +### Notification visual spec (Phase 10 will render; locked here for alignment) + +| Property | Value | +|----------|-------| +| Position | Fixed, `top-4 right-4`, `z-50` | +| Background | `bg-white` | +| Border | `border border-green-200` | +| Border radius | `rounded-xl` | +| Shadow | `shadow-lg` | +| Padding | `px-5 py-4` | +| Max width | `max-w-sm` | +| Icon | Heroicons `check-circle` (outline, `w-5 h-5 text-green-500`) | +| Text | `text-sm font-semibold text-gray-900` | +| Dismiss button | `text-gray-400 hover:text-gray-600`, Heroicons `x-mark` `w-4 h-4` | +| Auto-dismiss | After 4000ms (using `duration` param default) | + +Source: existing inline implementation in `SettingsAccountTab.vue` lines 5–25, adapted to +use the `duration` param default of 4000ms instead of the current hardcoded 5000ms. + +--- + +## Copywriting Contract + +| Element | Copy | +|---------|------| +| Sessions-revoked notification | `Other sessions have been terminated.` | +| Toast dismiss aria-label | `Dismiss notification` | + +No other new user-facing copy is introduced in Phase 8. All other interactions (backend +decomposition, client.js refactor, dependency bumps) are invisible to the user. + +--- + +## Registry Safety + +| Registry | Blocks Used | Safety Gate | +|----------|-------------|-------------| +| shadcn official | none — not initialized | not applicable | +| Third-party | none | not applicable | + +No third-party component registries are used. No new UI components are added beyond the +`useToastStore` stub (a store, not a component). + +--- + +## Checker Sign-Off + +- [ ] Dimension 1 Copywriting: PASS +- [ ] Dimension 2 Visuals: PASS +- [ ] Dimension 3 Color: PASS +- [ ] Dimension 4 Typography: PASS +- [ ] Dimension 5 Spacing: PASS +- [ ] Dimension 6 Registry Safety: PASS + +**Approval:** pending