Files
kite/.planning/phases/02-users-authentication/02-UI-SPEC.md
T
2026-05-22 15:12:02 +02:00

23 KiB
Raw Blame History

phase, slug, status, shadcn_initialized, preset, created
phase slug status shadcn_initialized preset created
2 users-authentication approved false none 2026-05-22

Phase 2 — UI Design Contract

Visual and interaction contract for Phase 2: Users & Authentication. Generated by gsd-ui-researcher, verified by gsd-ui-checker.


Design System

Property Value
Tool none — raw Tailwind CSS v3
Preset not applicable
Component library none
Icon library Inline SVG heroicons (stroke, no fill) — matches existing AppSidebar and DocumentCard patterns
Font System font stack (Tailwind default: ui-sans-serif, system-ui, sans-serif)

Source: Detected from frontend/package.json (no icon library installed), frontend/tailwind.config.js (empty theme.extend), and existing component audit.


Spacing Scale

Declared values (multiples of 4 only):

Token Value Usage
xs 4px Icon-to-label gaps, badge internal padding, inline list gaps
sm 8px Input icon padding, compact inline spacing
md 16px Default field padding, card internal padding, form row gap
lg 24px Section vertical gap, card-to-card gap
xl 32px Form block top margin, admin table row height padding
2xl 48px Auth page vertical centering padding
3xl 64px Auth card max-height clearance from viewport edge

Exceptions:

  • 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). 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 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.

Typography

Role Size Weight Line Height Tailwind Class
Body 14px 400 (regular) 1.5 text-sm
Label 14px 600 (semibold) 1.4 text-sm font-semibold
Heading 20px 600 (semibold) 1.3 text-xl font-semibold
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.

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.


Color

Role Value Tailwind Token Usage
Dominant (60%) #f9fafb bg-gray-50 Page background — all views
Secondary (30%) #ffffff bg-white Auth cards, form panels, admin table rows, sidebar
Accent (10%) #4f46e5 indigo-600 / indigo-700 Reserved for: primary CTA buttons only, active nav link text, brand logo text
Accent subtle #eef2ff indigo-50 Active nav link background, icon container backgrounds
Destructive #dc2626 red-600 Deactivate user action, sign-out-all confirmation button, field validation errors
Warning #d97706 amber-600 Password strength: weak/medium indicator only
Success #16a34a green-600 Password strength: strong indicator, TOTP enrollment success
Neutral border #e5e7eb gray-200 Card borders, input default border, table dividers
Muted text #9ca3af gray-400 Helper text, placeholder text, secondary metadata

Source: style.css (body bg-gray-50 text-gray-900), AppSidebar.vue (indigo-600 brand, indigo-50 active bg, indigo-700 active text), DocumentCard.vue (gray-200 border, hover:indigo-300). Destructive and warning added for auth-specific needs.

Accent reserved for: primary CTA buttons, active sidebar nav link text, DocuVault brand/logo text. Accent is NOT used on: input focus rings (use ring-indigo-500 only), links within prose, or secondary actions.

Input focus ring: focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 focus:outline-none


Form Field States

All text inputs (<input type="text|email|password">) follow this contract:

State Border Background Ring Text
Default border-gray-300 bg-white none text-gray-900
Focused border-indigo-500 bg-white ring-2 ring-indigo-500 text-gray-900
Error border-red-500 bg-white ring-2 ring-red-500 text-gray-900
Disabled border-gray-200 bg-gray-50 none text-gray-400
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-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.


Password Strength Indicator

Displayed below the password input on Register and Password Reset (new password) screens only.

Visual contract:

  • 4-segment horizontal bar, full width of the input
  • Each segment: 4px tall, rounded, gap-1 between segments
  • Segments light up left-to-right based on score
Score Segments lit Bar color Label
0 (empty) 0 hidden
1 (too short / fails rules) 1 bg-red-500 "Too weak"
2 (fails 2+ rules) 2 bg-amber-500 "Weak"
3 (fails 1 rule) 3 bg-amber-400 "Fair"
4 (all rules pass) 4 bg-green-500 "Strong"

Rules checked client-side (≥12 chars, uppercase, lowercase, digit, special char — matches AUTH-01):

  • Label: text-xs font-semibold at same color as the bar, right-aligned
  • Unlit segments: bg-gray-200
  • Layout: mt-2 space-y-1

HaveIBeenPwned breach check runs on blur (not on every keystroke). If the password is pwned, an inline error appears below the strength bar: "This password has appeared in a data breach. Choose a different password." — same text-xs text-red-600 style. The strength bar is NOT cleared; both messages coexist.


TOTP Enrollment Flow

Step 1 — QR Code Display

  • 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
  • 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."

Step 2 — Code Verification

  • Single 6-digit code input: rendered as one <input type="text" inputmode="numeric" maxlength="6"> field, NOT 6 separate cells
  • Width: w-36 (144px), centered
  • Error state on wrong code: field switches to error state + inline message "Incorrect code. Try again." — generic, no hint about time window
  • On success: green checkmark icon (w-5 h-5 text-green-600) + "Authenticator connected." transition before proceeding to backup codes

Step 3 — Backup Codes Display

Layout: 2-column grid of codes (grid grid-cols-2 gap-2)

Each code cell:

  • font-mono text-sm text-gray-800
  • 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)

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

  • Checkbox + label: "I have saved these codes in a secure place. I understand they will not be shown again."
  • Checkbox class: rounded border-gray-300 text-indigo-600 focus:ring-indigo-500
  • "Enable two-factor authentication" CTA button: disabled (opacity-50 cursor-not-allowed) until checkbox is checked

Login Flow — TOTP Step

Login is a two-step flow, not a single form:

Step 1: Email + password fields + "Sign in" button Step 2 (only for TOTP-enrolled users): Separate screen/card with:

  • Heading: "Two-factor authentication"
  • Helper: "Enter the 6-digit code from your authenticator app."
  • Single code input (same spec as enrollment verification above)
  • Secondary link below: "Use a backup code instead" — text-sm text-indigo-600 hover:underline
  • Back link: "Back to sign in" — text-sm text-gray-500 hover:text-gray-700

Backup code entry: replaces the code input with <input type="text" placeholder="XXXXXXXX"> and label "Enter a backup code". Same error state on invalid/already-used code, generic message: "Invalid or already used code."

The step transition (password accepted → TOTP screen) is NOT a page navigation. The card content swaps in place (v-if swap). URL stays at /login.


Loading States

All async actions must show a loading state. Contract:

Action Loading indicator Button state
Login submit Spinner (w-4 h-4, inline left of button label) disabled, opacity-75
Register submit Spinner inline disabled, opacity-75
HaveIBeenPwned breach check Spinner (w-3 h-3) right-aligned inside input field Input remains editable
Password reset email send Spinner inline disabled, opacity-75
TOTP code verify Spinner inline disabled, opacity-75
Admin: create user Spinner inline disabled, opacity-75
Admin: deactivate user Row-level spinner in action column Row actions pointer-events-none
Admin: reset password Row-level spinner Row actions pointer-events-none
Admin: save quota Spinner inline in edit cell Save button disabled
Sign-out-all Spinner inline disabled, opacity-75

Spinner: animate-spin rounded-full border-2 border-current border-t-transparent — inherits button text color. No external spinner library.


Error Message Placement

Error type Placement Style
Field validation (required, format, strength) Inline, below field mt-1 text-xs text-red-600
Server field error (email taken, invalid creds) Inline, below affected field mt-1 text-xs text-red-600
Form-level server error (generic 500, network) Above the submit button, inside the card p-3 rounded-lg bg-red-50 border border-red-200 text-sm text-red-700
Session expired (401 on any page) Toast, top-right, 5s auto-dismiss bg-gray-900 text-white text-sm px-4 py-3 rounded-lg shadow-lg
Rate limit hit Inline form-level block Same as form-level server error, with copy "Too many attempts. Try again in X minutes."
Success (password reset email sent) Inline, replaces form p-4 bg-green-50 border border-green-200 text-sm text-green-800 rounded-lg

Security copy rule: Error messages MUST NOT confirm whether an email address exists in the system. Login failure: "Incorrect email or password." Password reset: "If an account exists for that email, you will receive a reset link shortly." — identical response for existing and non-existing emails (anti-enumeration).


Auth Page Layout

Auth pages (/login, /register, /reset-password) share a single-column centered layout:

  • 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
  • Logo/brand above card: text-xl font-semibold text-indigo-600 tracking-tight centered, mb-6
  • Card heading: text-2xl font-semibold text-gray-900 mb-1
  • Card subheading: text-sm text-gray-500 mb-6
  • 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.


Account View (/account)

Uses the standard sidebar + main content layout (same as HomeView).

Sections (rendered as stacked cards with space-y-6):

  1. Account information — display-only: email, role badge
  2. Two-factor authentication — TOTP status + enrollment/disable controls
  3. Change password — current password + new password + strength indicator
  4. Sessions — "Sign out all devices" with confirmation dialog

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:

  • 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."
  • 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

Admin View (/admin)

Uses standard sidebar layout. Admin link in AppSidebar.vue visible only when useAuthStore().user?.role === 'admin'. Admin nav link styled identically to other nav links (no special styling — admin access is not advertised).

Sub-navigation (within AdminView):

Horizontal tab strip below the page heading:

  • 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-semibold text-indigo-600 border-b-2 border-indigo-600
  • Strip: flex border-b border-gray-200 mb-6
  • Tabs: "Users" | "Quotas" | "AI Config"

Users Tab — Table:

Table structure: full-width, divide-y divide-gray-200, inside a bg-white rounded-xl border border-gray-200 overflow-hidden

Columns: Email | Role | Status | Created | Actions

Row state Background Status badge
Active user bg-white bg-green-100 text-green-700 "Active"
Deactivated user bg-gray-50 bg-gray-100 text-gray-500 "Deactivated"
Admin user (active) bg-white Same as active + role badge

Row text: text-sm text-gray-900 for email, text-sm text-gray-500 for created date.

Actions column (per row): text links, separated by ·

  • Active row: "Reset password" · "Deactivate"
  • Deactivated row: "Reactivate" (no reset password on deactivated accounts)
  • Deactivate link: text-red-600 hover:text-red-700 text-sm
  • Other action links: text-indigo-600 hover:text-indigo-700 text-sm

Create User (Users tab):

"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).

Quotas Tab — Inline Edit:

Table columns: Email | Used | Limit | Usage % | Actions

Limit field is editable inline. Click "Edit" → limit cell becomes <input type="number" min="1" step="1"> (in MB) with Save / Cancel buttons. Warning shown inline if new limit < current usage: text-xs text-amber-600 mt-1 "New limit is below current usage (X MB). Existing documents will not be deleted, but uploads will be blocked."

AI Config Tab:

Table columns: Email | AI Provider | AI Model | Actions

Provider and model: dropdown selectors (not free-text). Populated from the backend's supported provider list. "Save" button per row. No bulk edit.


Sidebar Updates (Phase 2)

AppSidebar.vue gains two additions:

  1. Admin link (bottom section, above Settings): conditionally rendered, v-if="authStore.user?.role === 'admin'" — shield SVG icon (w-4 h-4), label "Admin". Same nav-link styling.

  2. User identity footer: below the settings link at very bottom.

    • 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
    • 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, aria-label="Sign out"

Router Guard Feedback

When an unauthenticated user is redirected to /login from a protected route, the login card shows no banner by default. The router saves the redirect query param (/login?redirect=/account). After successful login, the router navigates to the saved route.

If the redirect was triggered by a 401 (token expired, not initial page load), show the session-expired toast (defined in Error Message Placement above) before the redirect completes.


Copywriting Contract

Element Copy
Register page heading "Create your account"
Register page subheading "Start managing your documents securely."
Register primary CTA "Create account"
Login page heading "Sign in to DocuVault"
Login page subheading (none)
Login primary CTA "Sign in"
Login: redirect to register "Don't have an account? Create one"
Register: redirect to login "Already have an account? Sign in"
Forgot password link "Forgot your password?"
Password reset page heading "Reset your password"
Password reset subheading "Enter your email and we'll send you a reset link."
Password reset CTA "Send reset link"
Password reset success "If an account exists for that email, you will receive a reset link shortly. Check your inbox."
New password page heading "Set a new password"
New password CTA "Set password"
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 helper "Enter the 6-digit code from your authenticator app."
TOTP verify CTA "Verify code"
TOTP backup code link "Use a backup code instead"
TOTP enrollment heading "Set up two-factor authentication"
TOTP backup codes heading "Save your backup codes"
Backup codes subheading "Store these codes somewhere safe. Each can only be used once if you lose access to your authenticator app."
Backup codes copy-all CTA "Copy all codes"
Backup codes acknowledge checkbox "I have saved these codes in a secure place. I understand they will not be shown again."
TOTP enable CTA "Enable two-factor authentication"
Account page heading "Account settings"
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 cancel CTA "Keep signed in"
Sign-out-all destructive CTA "Sign out all devices"
Admin page heading "Admin panel"
Admin: create user CTA "Create user"
Admin: deactivate action "Deactivate"
Admin: reactivate action "Reactivate"
Admin: reset password action "Reset password"
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."
Generic server error "Something went wrong. Please try again or contact support if the problem persists."
Login failure "Incorrect email or password."
Rate limit error "Too many attempts. Please wait a few minutes and try again."
TOTP wrong code "Incorrect code. Try again."
Backup code invalid "Invalid or already used code."
Password breach error "This password has appeared in a data breach. Choose a different password."
Password too weak "Password must be at least 12 characters and include uppercase, lowercase, a number, and a special character."

Security copy rules:

  • Never confirm or deny whether an email exists (password reset and login)
  • Never include user email or PII in generic error messages
  • Never state the number of remaining TOTP attempts
  • Admin actions on user rows include the email in the confirmation to prevent mis-clicks, but error responses from the server must not echo credentials back

Registry Safety

Registry Blocks Used Safety Gate
shadcn official none not applicable — shadcn not installed
third-party none not applicable

No third-party component registries. All components are handwritten Vue 3 SFC with raw Tailwind CSS, consistent with the existing codebase pattern.


Component Inventory (New — Phase 2)

Components to create, with their file paths:

Component Path Description
LoginView src/views/auth/LoginView.vue Two-step login (password → TOTP)
RegisterView src/views/auth/RegisterView.vue Registration with strength indicator
PasswordResetView src/views/auth/PasswordResetView.vue Reset request form + success state
NewPasswordView src/views/auth/NewPasswordView.vue Token-gated new password form
AccountView src/views/AccountView.vue Account settings, TOTP, sign-out-all
AdminView src/views/AdminView.vue Admin panel with sub-navigation
AuthLayout src/layouts/AuthLayout.vue Bare centered layout (no sidebar) for auth pages
PasswordStrengthBar src/components/auth/PasswordStrengthBar.vue 4-segment strength indicator
TotpEnrollment src/components/auth/TotpEnrollment.vue QR + manual secret + code verify
BackupCodesDisplay src/components/auth/BackupCodesDisplay.vue Grid + copy-all + acknowledge checkbox
AppSpinner src/components/ui/AppSpinner.vue Inline CSS spinner, inherits color
ConfirmBlock src/components/ui/ConfirmBlock.vue Inline confirm/cancel replacement for destructive actions
AdminUsersTab src/components/admin/AdminUsersTab.vue User table + create user form
AdminQuotasTab src/components/admin/AdminQuotasTab.vue Quota inline edit table
AdminAiConfigTab src/components/admin/AdminAiConfigTab.vue AI provider/model dropdowns per user

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: approved — 2026-05-22