--- 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