docs(02): UI design contract for Phase 2 — Users & Authentication

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-05-22 15:12:02 +02:00
parent 9e28de8c15
commit 333978d7cb
@@ -1,7 +1,7 @@
--- ---
phase: 2 phase: 2
slug: users-authentication slug: users-authentication
status: draft status: approved
shadcn_initialized: false shadcn_initialized: false
preset: none preset: none
created: 2026-05-22 created: 2026-05-22
@@ -44,8 +44,8 @@ Declared values (multiples of 4 only):
Exceptions: Exceptions:
- Touch targets (buttons, checkboxes, radio): minimum 44px height — enforced via `min-h-[44px]` on all interactive controls - Touch targets (buttons, checkboxes, radio): minimum 44px height — enforced via `min-h-[44px]` on all interactive controls
- TOTP code input cells: 48px wide × 56px tall per digit cell (6 cells) - TOTP code input cells: 48px wide × 56px tall per digit cell (6 cells). Justification: 48px (scale) is too compact for touch-target comfort on a digit-only input; 64px wastes vertical space — 56px balances touch ergonomics and form compactness.
- Backup code grid cells: 8px horizontal padding, 12px vertical padding (between xs and sm) - Backup code grid cells and text inputs: 12px vertical/horizontal padding (`py-3 px-3` = 12px). Justification: 16px inflates form card height beyond max-w-sm on mobile viewports; 8px collapses touch targets below 44px when combined with text-sm line height — 12px is the ergonomic midpoint. All instances use the same value for consistency.
--- ---
@@ -54,13 +54,13 @@ Exceptions:
| Role | Size | Weight | Line Height | Tailwind Class | | Role | Size | Weight | Line Height | Tailwind Class |
|------|------|--------|-------------|----------------| |------|------|--------|-------------|----------------|
| Body | 14px | 400 (regular) | 1.5 | `text-sm` | | Body | 14px | 400 (regular) | 1.5 | `text-sm` |
| Label | 14px | 500 (medium) | 1.4 | `text-sm font-medium` | | Label | 14px | 600 (semibold) | 1.4 | `text-sm font-semibold` |
| Heading | 20px | 600 (semibold) | 1.3 | `text-xl font-semibold` | | Heading | 20px | 600 (semibold) | 1.3 | `text-xl font-semibold` |
| Display | 24px | 700 (bold) | 1.2 | `text-2xl font-bold` | | Display | 24px | 600 (semibold) | 1.2 | `text-2xl font-semibold` |
**Source:** Extracted from `HomeView.vue` (`text-2xl font-bold` page heading, `text-sm` body, `text-lg font-semibold` section headings) and `DocumentCard.vue` (`font-medium text-sm`). Heading reduced from text-lg to text-xl for auth page section hierarchy. **Source:** Extracted from `HomeView.vue` (`text-2xl font-bold` page heading, `text-sm` body, `text-lg font-semibold` section headings) and `DocumentCard.vue` (`font-medium text-sm`). Heading reduced from text-lg to text-xl for auth page section hierarchy.
Font weight scale: exactly 2 weights in components (regular 400 + semibold/bold 600/700). Labels use 500 (medium) as the only addition for form ergonomics. Font weight scale: exactly 2 weights regular (400) for body text and helper text; semibold (600) for headings, labels, CTAs, and display text. Differentiation between roles relies on SIZE, not weight. Do not use `font-medium` (500) or `font-bold` (700) anywhere in Phase 2 components.
--- ---
@@ -99,7 +99,7 @@ All text inputs (`<input type="text|email|password">`) follow this contract:
| Disabled | `border-gray-200` | `bg-gray-50` | none | `text-gray-400` | | Disabled | `border-gray-200` | `bg-gray-50` | none | `text-gray-400` |
| Read-only | `border-gray-200` | `bg-gray-50` | none | `text-gray-700` | | Read-only | `border-gray-200` | `bg-gray-50` | none | `text-gray-700` |
Base classes (all states share): `block w-full rounded-lg px-3 py-2.5 text-sm transition-colors` Base classes (all states share): `block w-full rounded-lg px-3 py-3 text-sm transition-colors`
Error messages appear **inline**, directly below the field, never as a toast. Class: `mt-1 text-xs text-red-600`. Maximum 1 error line per field. Error messages appear **inline**, directly below the field, never as a toast. Class: `mt-1 text-xs text-red-600`. Maximum 1 error line per field.
@@ -123,7 +123,7 @@ Visual contract:
| 4 (all rules pass) | 4 | `bg-green-500` | "Strong" | | 4 (all rules pass) | 4 | `bg-green-500` | "Strong" |
Rules checked client-side (≥12 chars, uppercase, lowercase, digit, special char — matches AUTH-01): Rules checked client-side (≥12 chars, uppercase, lowercase, digit, special char — matches AUTH-01):
- Label: `text-xs font-medium` at same color as the bar, right-aligned - Label: `text-xs font-semibold` at same color as the bar, right-aligned
- Unlit segments: `bg-gray-200` - Unlit segments: `bg-gray-200`
- Layout: `mt-2 space-y-1` - Layout: `mt-2 space-y-1`
@@ -137,7 +137,7 @@ HaveIBeenPwned breach check runs on blur (not on every keystroke). If the passwo
- QR code: 200px × 200px centered in a white card with `border border-gray-200 rounded-xl p-6` - QR code: 200px × 200px centered in a white card with `border border-gray-200 rounded-xl p-6`
- Manual secret: `font-mono text-sm text-gray-700 bg-gray-50 px-3 py-2 rounded-md border border-gray-200 break-all` - Manual secret: `font-mono text-sm text-gray-700 bg-gray-50 px-3 py-2 rounded-md border border-gray-200 break-all`
- Copy secret button: icon-only (clipboard SVG, w-4 h-4), `text-gray-400 hover:text-gray-600`, positioned inline right of the secret block - Copy secret button: icon-only (clipboard SVG, w-4 h-4), `text-gray-400 hover:text-gray-600`, positioned inline right of the secret block. Must include `aria-label="Copy secret key"` for accessibility.
- Instruction copy: "Open your authenticator app and scan this QR code, or enter the key manually." - Instruction copy: "Open your authenticator app and scan this QR code, or enter the key manually."
**Step 2 — Code Verification** **Step 2 — Code Verification**
@@ -156,7 +156,7 @@ Each code cell:
- `bg-gray-50 border border-gray-200 rounded-md px-3 py-2 text-center` - `bg-gray-50 border border-gray-200 rounded-md px-3 py-2 text-center`
- Do not number them (no 1., 2. prefix — prevents users from thinking order matters) - Do not number them (no 1., 2. prefix — prevents users from thinking order matters)
Copy-all button: `"Copy all codes"` — text button with clipboard icon, `text-indigo-600 hover:text-indigo-700 text-sm font-medium`. On click: copies newline-separated codes to clipboard, button text changes to `"Copied"` for 2 seconds. Copy-all button: `"Copy all codes"` — text button with clipboard icon, `text-indigo-600 hover:text-indigo-700 text-sm font-semibold`. On click: copies newline-separated codes to clipboard, button text changes to `"Copied"` for 2 seconds.
Acknowledgment checkbox (required before proceeding): Acknowledgment checkbox (required before proceeding):
- Checkbox + label: `"I have saved these codes in a secure place. I understand they will not be shown again."` - Checkbox + label: `"I have saved these codes in a secure place. I understand they will not be shown again."`
@@ -226,11 +226,13 @@ Auth pages (`/login`, `/register`, `/reset-password`) share a single-column cent
- Page background: `bg-gray-50 min-h-screen flex items-center justify-center` - Page background: `bg-gray-50 min-h-screen flex items-center justify-center`
- Card: `bg-white rounded-2xl shadow-sm border border-gray-200 p-8 w-full max-w-sm` - Card: `bg-white rounded-2xl shadow-sm border border-gray-200 p-8 w-full max-w-sm`
- Logo/brand above card: `text-lg font-bold text-indigo-600 tracking-tight` centered, `mb-6` - Logo/brand above card: `text-xl font-semibold text-indigo-600 tracking-tight` centered, `mb-6`
- Card heading: `text-2xl font-bold text-gray-900 mb-1` - Card heading: `text-2xl font-semibold text-gray-900 mb-1`
- Card subheading: `text-sm text-gray-500 mb-6` - Card subheading: `text-sm text-gray-500 mb-6`
- No sidebar on auth pages — full-width layout - No sidebar on auth pages — full-width layout
Primary focal point of all auth screens is the card heading (Display / 24px / text-gray-900 font-semibold); secondary anchor is the primary CTA button (indigo-600 bg).
Auth pages are NOT wrapped in the main `App.vue` sidebar layout. They use a bare layout with no navigation. Auth pages are NOT wrapped in the main `App.vue` sidebar layout. They use a bare layout with no navigation.
--- ---
@@ -246,13 +248,13 @@ Sections (rendered as stacked cards with `space-y-6`):
3. **Change password** — current password + new password + strength indicator 3. **Change password** — current password + new password + strength indicator
4. **Sessions** — "Sign out all devices" with confirmation dialog 4. **Sessions** — "Sign out all devices" with confirmation dialog
Role badge: `inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-indigo-100 text-indigo-700` for admin; `bg-gray-100 text-gray-600` for user. Role badge: `inline-flex items-center px-2 py-1 rounded text-xs font-semibold bg-indigo-100 text-indigo-700` for admin; `bg-gray-100 text-gray-600` for user.
**Sign-out-all confirmation dialog:** **Sign-out-all confirmation dialog:**
- Not a browser `confirm()`. Rendered as an inline confirmation block that replaces the button on click. - Not a browser `confirm()`. Rendered as an inline confirmation block that replaces the button on click.
- Copy: "This will sign you out of all devices, including this one. You will need to sign in again." - Copy: "This will sign you out of all devices, including this one. You will need to sign in again."
- Two buttons: "Cancel" (`text-gray-600 hover:text-gray-800 text-sm`) + "Sign out all devices" (`bg-red-600 hover:bg-red-700 text-white text-sm font-medium px-4 py-2 rounded-lg`) - Two buttons: "Keep signed in" (`text-gray-600 hover:text-gray-800 text-sm`) + "Sign out all devices" (`bg-red-600 hover:bg-red-700 text-white text-sm font-semibold px-4 py-2 rounded-lg`)
- After confirmation: loading state on the destructive button, then redirect to `/login` - After confirmation: loading state on the destructive button, then redirect to `/login`
--- ---
@@ -264,8 +266,8 @@ Uses standard sidebar layout. Admin link in `AppSidebar.vue` visible only when `
**Sub-navigation (within AdminView):** **Sub-navigation (within AdminView):**
Horizontal tab strip below the page heading: Horizontal tab strip below the page heading:
- Tab class (default): `px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-700 border-b-2 border-transparent` - Tab class (default): `px-4 py-2 text-sm font-semibold text-gray-500 hover:text-gray-700 border-b-2 border-transparent`
- Tab class (active): `px-4 py-2 text-sm font-medium text-indigo-600 border-b-2 border-indigo-600` - Tab class (active): `px-4 py-2 text-sm font-semibold text-indigo-600 border-b-2 border-indigo-600`
- Strip: `flex border-b border-gray-200 mb-6` - Strip: `flex border-b border-gray-200 mb-6`
- Tabs: "Users" | "Quotas" | "AI Config" - Tabs: "Users" | "Quotas" | "AI Config"
@@ -291,7 +293,7 @@ Actions column (per row): text links, separated by `·`
**Create User (Users tab):** **Create User (Users tab):**
"Create user" button: positioned top-right of table, `bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-medium px-4 py-2 rounded-lg`. "Create user" button: positioned top-right of table, `bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold px-4 py-2 rounded-lg`.
Create user form renders as an inline panel above the table (not a modal), with: email field, temporary password field (pre-generated, read-only with copy button), role selector (dropdown: User / Admin). Create user form renders as an inline panel above the table (not a modal), with: email field, temporary password field (pre-generated, read-only with copy button), role selector (dropdown: User / Admin).
@@ -319,7 +321,7 @@ Provider and model: dropdown selectors (not free-text). Populated from the backe
- Layout: `flex items-center gap-3 px-4 py-3 border-t border-gray-100` - Layout: `flex items-center gap-3 px-4 py-3 border-t border-gray-100`
- Avatar: initials-based circle, 32px, `bg-indigo-100 text-indigo-700 text-xs font-semibold rounded-full w-8 h-8 flex items-center justify-center shrink-0` - Avatar: initials-based circle, 32px, `bg-indigo-100 text-indigo-700 text-xs font-semibold rounded-full w-8 h-8 flex items-center justify-center shrink-0`
- Email: `text-xs text-gray-600 truncate flex-1` - Email: `text-xs text-gray-600 truncate flex-1`
- Sign-out icon button: arrow-right-on-rectangle SVG (w-4 h-4), `text-gray-400 hover:text-gray-600` - Sign-out icon button: arrow-right-on-rectangle SVG (w-4 h-4), `text-gray-400 hover:text-gray-600`, `aria-label="Sign out"`
--- ---
@@ -353,7 +355,7 @@ If the redirect was triggered by a 401 (token expired, not initial page load), s
| New password success copy | "Password updated. Please sign in." (redirects to /login, not auto-login — per AUTH-05) | | New password success copy | "Password updated. Please sign in." (redirects to /login, not auto-login — per AUTH-05) |
| TOTP step heading | "Two-factor authentication" | | TOTP step heading | "Two-factor authentication" |
| TOTP step helper | "Enter the 6-digit code from your authenticator app." | | TOTP step helper | "Enter the 6-digit code from your authenticator app." |
| TOTP verify CTA | "Verify" | | TOTP verify CTA | "Verify code" |
| TOTP backup code link | "Use a backup code instead" | | TOTP backup code link | "Use a backup code instead" |
| TOTP enrollment heading | "Set up two-factor authentication" | | TOTP enrollment heading | "Set up two-factor authentication" |
| TOTP backup codes heading | "Save your backup codes" | | TOTP backup codes heading | "Save your backup codes" |
@@ -364,13 +366,14 @@ If the redirect was triggered by a 401 (token expired, not initial page load), s
| Account page heading | "Account settings" | | Account page heading | "Account settings" |
| Sign-out-all label | "Sign out all devices" | | Sign-out-all label | "Sign out all devices" |
| Sign-out-all confirmation copy | "This will sign you out of all devices, including this one. You will need to sign in again." | | Sign-out-all confirmation copy | "This will sign you out of all devices, including this one. You will need to sign in again." |
| Sign-out-all cancel CTA | "Keep signed in" |
| Sign-out-all destructive CTA | "Sign out all devices" | | Sign-out-all destructive CTA | "Sign out all devices" |
| Admin page heading | "Admin panel" | | Admin page heading | "Admin panel" |
| Admin: create user CTA | "Create user" | | Admin: create user CTA | "Create user" |
| Admin: deactivate action | "Deactivate" | | Admin: deactivate action | "Deactivate" |
| Admin: reactivate action | "Reactivate" | | Admin: reactivate action | "Reactivate" |
| Admin: reset password action | "Reset password" | | Admin: reset password action | "Reset password" |
| Admin: deactivate confirmation | Inline: "Deactivate [email]? They will lose access immediately. Their data is preserved." + "Deactivate" / "Cancel" | | Admin: deactivate confirmation | Inline: "Deactivate [email]? They will lose access immediately. Their data is preserved." + "Deactivate" / "Keep account" |
| Empty state (users table) | Heading: "No users yet" / Body: "Create the first user account to get started." | | Empty state (users table) | Heading: "No users yet" / Body: "Create the first user account to get started." |
| Generic server error | "Something went wrong. Please try again or contact support if the problem persists." | | Generic server error | "Something went wrong. Please try again or contact support if the problem persists." |
| Login failure | "Incorrect email or password." | | Login failure | "Incorrect email or password." |
@@ -425,11 +428,11 @@ Components to create, with their file paths:
## Checker Sign-Off ## Checker Sign-Off
- [ ] Dimension 1 Copywriting: PASS - [x] Dimension 1 Copywriting: PASS
- [ ] Dimension 2 Visuals: PASS - [x] Dimension 2 Visuals: PASS
- [ ] Dimension 3 Color: PASS - [x] Dimension 3 Color: PASS
- [ ] Dimension 4 Typography: PASS - [x] Dimension 4 Typography: PASS
- [ ] Dimension 5 Spacing: PASS - [x] Dimension 5 Spacing: PASS
- [ ] Dimension 6 Registry Safety: PASS - [x] Dimension 6 Registry Safety: PASS
**Approval:** pending **Approval:** approved — 2026-05-22