---
phase: 8
slug: stack-upgrade-backend-decomposition
status: approved
shadcn_initialized: false
preset: none
created: 2026-06-07
reviewed_at: 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

- [x] Dimension 1 Copywriting: PASS
- [x] Dimension 2 Visuals: PASS
- [x] Dimension 3 Color: PASS
- [x] Dimension 4 Typography: PASS
- [x] Dimension 5 Spacing: PASS
- [x] Dimension 6 Registry Safety: PASS

**Approval:** approved 2026-06-07
