docs(14): add UI design contract for auth-pages restyle

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arthur Belleville 2026-05-16 18:40:46 +02:00
parent d4a44631b7
commit 0d49c20e42
No known key found for this signature in database

View file

@ -0,0 +1,395 @@
---
phase: 14
slug: auth-pages
status: draft
shadcn_initialized: false
preset: none
created: 2026-05-16
---
# Phase 14 — UI Design Contract
> Visual and interaction contract for the auth-pages restyle. Generated by gsd-ui-researcher.
> Source of truth for: gsd-planner, gsd-executor, gsd-ui-auditor.
---
## Design System
| Property | Value |
|----------|-------|
| Tool | none (custom CSS + design tokens) |
| Preset | not applicable |
| Component library | Phase 13 design system (`@ui.Input`, `@ui.FormField`, `@ui.Button`, `@ui.Card`) |
| Icon library | none (Google SVG inline; no general icon library in use) |
| Font | ui-sans-serif system stack (body); "Roboto", Arial (Google button label only) |
**Source:** CONTEXT.md decisions D-FI01, D-FI02, D-FI03; CLAUDE.md tech stack; `backend/internal/web/ui/base.css`.
No shadcn gate applies — this is a Go + HTMX project with a custom CSS design system, not React/Next.js/Vite.
---
## Page Structure Contract
### AuthLayout shell
AuthLayout replaces `@Layout(...)` for both `/login` and `/signup`. It renders a standalone HTML document with no navigation header or footer.
| Layer | CSS class | Description |
|-------|-----------|-------------|
| Outer shell | `.login-screen` | Full-viewport flex center; `background: var(--gradient-shell)`; `overflow: hidden; position: relative` |
| Animated background | `.background-layer` | `position: absolute; inset: 0; overflow: hidden; pointer-events: none` — contains 35 logo elements |
| Card wrapper | `.card-wrap` | `max-width: 32rem; width: 100%; position: relative; z-index: 1` |
| Card glow | `.card-glow` | `background: var(--gradient-card-glow); border-radius: 1rem; filter: blur(24px); position: absolute; inset: 0; z-index: -1` — decorative only |
| Card shell | `.auth-card-shell` | `backdrop-filter: blur(12px); background: var(--color-surface-card); border: 1px solid var(--color-border-default); border-radius: 1rem; box-shadow: var(--shadow-auth-card); padding: 1.25rem; position: relative` |
**Background gradient for `.login-screen`:** use `var(--gradient-shell)` which resolves to:
```
linear-gradient(135deg, var(--overlay-brand-muted), transparent 30%),
linear-gradient(160deg, var(--overlay-dark-soft), transparent 42%),
linear-gradient(to bottom right, var(--overlay-dark-border), var(--color-surface-page), var(--overlay-brand-faint))
```
This is already defined as a token in `base.css`. Do not hardcode color stops.
**Source:** D-L01, D-L02; go-backend `.login-screen` + `.auth-card-shell` CSS (lines 1063-1179 of `go-backend/static/styles.css`).
### Element order within `.auth-card-shell`
```
1. .auth-card-topbar — (empty in Phase 14; reserved placeholder div, no back-link or theme toggle)
2. .brand-header — logo image: <img src="/static/logo_dark.png" class="brand-logo" alt="Xtablo">
3. .title-group — <h1> page title
4. .auth-body — wraps: Google button → divider → form → nav link
├── Google button (.gsi-material-button as <a> or <button>)
├── Divider (.divider-row → .divider-line + .divider-pill + .divider-line)
├── Form (LoginFormFragment / SignupFormFragment with @ui.FormField + @ui.Input)
└── Nav link (.signup-copy + .signup-link)
```
**Source:** D-AC02; go-backend `AuthScreen` templ + `auth_components.templ` structure; D-AB04 (AnimatedBackground separate component).
---
## Animated Background Contract
### Component: `AnimatedBackground`
- Lives in `backend/templates/` (not `backend/internal/web/ui/`)
- 35 logo elements exactly, matching go-backend `AnimatedBackground` templ verbatim
- Each element: `<div class="background-logo bg-NN animate-*">` containing `<img class="logo-asset size-NN animate-*" src="/static/logo_dark.png" alt="Xtablo">`
- Phase 14 uses `logo_dark.png` only — no dark-mode swap (`light-only`/`dark-only` classes deferred per DEFERRED section)
- `alt="Xtablo"` on each img; parent `.background-layer` carries `aria-hidden="true"`
### Asset requirement
`backend/static/logo_dark.png` must exist before template compiles. Copy from `go-backend/static/logo_dark.png` as part of Phase 14.
### CSS classes to port into `backend/internal/web/ui/auth.css`
From `go-backend/static/styles.css` (extract only, do not import the file directly):
| Class group | Source lines | Purpose |
|-------------|-------------|---------|
| `.background-layer` | ~1081 | Absolute full-viewport container |
| `.background-logo` | ~1088 | Absolute positioning base for each logo |
| `.logo-asset` | ~1092 | `display: block; object-fit: contain` |
| `.size-06` through `.size-20` | ~10991111 | Logo size scale (rem-based) |
| `.opacity-02` through `.opacity-05` | ~11131116 | Per-element opacity |
| `.bg-01` through `.bg-35` | ~11181155 | Fixed positions (top/left/right/bottom percentages) |
| All `animate-*` utility classes | ~28832930+ | Animation assignments |
| All `@keyframes` definitions | below animate-* | The actual keyframe definitions for every named animation |
**Source:** D-AB01, D-AB03.
---
## Spacing Scale
Multiples of 4 only. All values from existing token usage in go-backend auth CSS.
| Token | Value | Auth usage |
|-------|-------|------------|
| xs | 4px | Divider gap between line and pill |
| sm | 8px | AnimatedBackground element gaps; `.auth-provider-stack` gap |
| md | 16px | `.auth-body` flex gap; auth-provider separator margin |
| lg | 24px | `.auth-card-topbar` margin-bottom; `.title-group` margin-bottom; `.brand-header` margin-bottom |
| xl | 32px | — |
| 2xl | 48px | — |
| card-padding | 20px (1.25rem) | `.auth-card-shell` padding — exception to 4pt grid, matches reference exactly |
| screen-padding | 32px (2rem 1rem) | `.login-screen` padding |
Exceptions: card-padding at 20px (1.25rem) — matches go-backend reference; screen horizontal padding at 16px (1rem).
---
## Typography
| Role | Size | Weight | Line Height | Usage |
|------|------|--------|-------------|-------|
| Auth heading | clamp(1.5rem, 4vw, 1.875rem) | 700 | 1.2 | `.title-group h1` — page title ("Sign in to Xtablo", "Create your account") |
| Body / label | 14px (0.875rem) | 500 | 1.4 | Field labels, divider pill text, nav link copy |
| Small / muted | 14px (0.875rem) | 400 | 1.4 | `.signup-copy` nav link paragraph |
| Google button | 14px (Roboto), letter-spacing 0.25px | 500 | — | `.gsi-material-button-contents` only |
**Rules:**
- 3 effective sizes: `clamp(1.5rem1.875rem)` for heading, `0.875rem` for supporting text, `14px` for Google button label.
- 2 weights: 400 (regular) and 500/700 (semibold/bold) — heading uses 700, labels and links use 500, muted copy uses 400.
- Body font: inherited from `base.css` (`ui-sans-serif, system-ui, -apple-system, ...`).
- Google button font: `"Roboto", Arial, sans-serif` scoped to `.gsi-material-button` only.
**Source:** go-backend CSS `.title-group h1`, `.field-stack label`, `.signup-copy`, `.gsi-material-button-contents`.
---
## Color
| Role | Token | Resolved value | Usage |
|------|-------|----------------|-------|
| Dominant (60%) | `--color-surface-page` / `--color-surface-card` | `#ffffff` / `rgba(255,255,255,0.8)` | Auth card shell background (glassmorphism) |
| Secondary (30%) | `--gradient-shell` via `--overlay-brand-muted`, `--overlay-dark-soft`, `--overlay-dark-border`, `--overlay-brand-faint` | See gradient definition above | `.login-screen` full-viewport background |
| Accent (10%) | `--color-brand-primary` | `#804eec` | Reserved for: `.signup-link` text color (inherited from `--foreground` + hover state via `--accent`); focus rings on inputs (`--color-focus-ring: rgba(124,58,237,0.2)`) |
| Destructive | `--color-status-danger-*` | `#dc2626` / banner tokens | Error state banners (`@GeneralError`, `@FieldError`) and `.status-error` banner |
| Muted text | `--color-text-muted` / `--muted-foreground` | `hsl(0 0% 43.5%)` | `.signup-copy`, `.divider-pill`, placeholder text |
| Google button | `--color-surface-default` bg / `--color-border-google` border / `--color-text-google` text | `#ffffff` / `#747775` / `#1f1f1f` | `.gsi-material-button` only |
**Accent reserved for:**
- Input focus ring (via `--color-focus-ring`)
- `.signup-link` hover background (via `--accent` = `var(--color-surface-subtle)`)
- No use of brand purple on any other auth page element
**Source:** go-backend auth CSS; `backend/internal/web/ui/base.css` token definitions.
---
## Component Contracts
### Brand logo
```
.brand-header > img.brand-logo
src="/static/logo_dark.png"
alt="Xtablo"
width: 4rem; height: 4rem; object-fit: contain
```
### Auth card topbar
Empty `<div class="auth-card-topbar">` in Phase 14. No back-link, no theme toggle. Preserve the class for future phases.
### Title group
```
.title-group > h1
Login page: "Sign in to Xtablo"
Signup page: "Create your account"
font-size: clamp(1.5rem, 4vw, 1.875rem); font-weight: 700; margin: 0; text-align: center
```
### Google button (`.gsi-material-button`)
Element: `<a>` (when configured) or `<button disabled>` (when unconfigured), wrapping:
```
.gsi-material-button
└── .gsi-material-button-state (hover/active overlay)
└── .gsi-material-button-content-wrapper
├── .gsi-material-button-icon (20×20 Google SVG, 4-path multicolor)
├── .gsi-material-button-contents "Sign in with Google" ← English label (D-G03)
└── .visually-hidden "Sign in with Google" ← screen reader duplicate
```
The full Google SVG (4 colored paths — `#EA4335`, `#4285F4`, `#FBBC05`, `#34A853`) is inlined as in go-backend `GoogleButton` templ.
Disabled state: `cursor: not-allowed; opacity: 0.6` (or Tailwind equivalent); `aria-disabled="true"`.
**Source:** D-G01, D-G02, D-G03; go-backend `GoogleButton` templ.
### Divider
```
.divider-row
├── .divider-line (flex: 1; border-top: 1px solid var(--border))
├── .divider-pill "or" ← English (not "Ou continuer avec")
└── .divider-line
```
**Source:** go-backend `AuthDivider` templ; English convention per D-G03 pattern.
### Form fields
Replace all raw `<input>` in `LoginFormFragment` and `SignupFormFragment` with `@ui.FormField(ui.FormFieldProps{...})` wrapping `@ui.Input(ui.InputProps{...})`:
```
@ui.FormField(ui.FormFieldProps{
Label: "Email address",
For: "email",
Field: ui.Input(ui.InputProps{
ID: "email",
Name: "email",
Type: "email",
Placeholder: "you@example.com",
Value: form.Email,
Required: true,
Attrs: templ.Attributes{"autocomplete": "email"},
}),
Error: errs.Email, // replaces @FieldError(errs.Email)
})
```
The `@GeneralError(errs.General)` call remains at the top of the form, outside `@ui.FormField`.
### Navigation links
```
login page below form:
<p class="signup-copy">
Don't have an account?
<a class="signup-link" href="/signup">Sign up</a>
</p>
signup page below form:
<p class="signup-copy">
Already have an account?
<a class="signup-link" href="/login">Sign in</a>
</p>
```
**Source:** D-NL01, D-NL02, D-NL03; go-backend `AuthScreenFooter` templ pattern.
---
## Copywriting Contract
| Element | Copy |
|---------|------|
| Login page title | Sign in to Xtablo |
| Signup page title | Create your account |
| Login CTA (submit button) | Sign in |
| Signup CTA (submit button) | Create account |
| Google button label | Sign in with Google |
| Divider label | or |
| Login nav link | Don't have an account? [Sign up] |
| Signup nav link | Already have an account? [Sign in] |
| Email field label | Email address |
| Email placeholder | you@example.com |
| Password field label (login) | Password |
| Password placeholder (login) | Your password |
| Password field label (signup) | Password |
| Password placeholder (signup) | 12 characters minimum |
| General error state | [inline `@GeneralError` — existing component; copy determined by server error message] |
| Field error state | [inline `@FieldError` wired through `FormFieldProps.Error` — existing component] |
| Google button disabled label | Sign in with Google (provider unavailable) |
No empty state: auth pages always show the form. No destructive actions on these pages.
**Source:** D-G03 (English label); existing backend templates copy; cross-page nav copy from D-NL01, D-NL02.
---
## Interaction Contract
### HTMX behavior — preserved exactly, not changed
| Form | `hx-post` | `hx-target` | `hx-swap` |
|------|-----------|-------------|-----------|
| Login form (`#login-form`) | `/login` | `#login-form` | `outerHTML` |
| Signup form (`#signup-form`) | `/signup` | `#signup-form` | `outerHTML` |
**Phase 14 is visual only. No HTMX behavior changes.**
### Focus management
- Tab order: Google button → email input → password input → submit button → nav link
- Focus ring: `box-shadow: 0 0 0 2px var(--color-focus-ring)` on inputs (via `ui-input` class from Phase 13 design system)
- Google button focus: `.gsi-material-button-state` opacity 0.12 on `:focus` (via ported CSS)
### States
| Element | States |
|---------|--------|
| Google button (configured) | default, hover (box-shadow), focus (state overlay 0.12), active (state overlay 0.12) |
| Google button (unconfigured) | disabled (cursor: not-allowed; no hover effect) |
| Input | default, focus (ring), error (error class from `@ui.FormField`) |
| Submit button | default, hover (opacity 0.9 via `@ui.Button`), disabled during submission (`aria-busy`) |
| Nav link | default, hover (background: `var(--accent)`, color transitions to `--foreground`) |
| Animated background logos | 35 independent CSS animations, `pointer-events: none`, `aria-hidden="true"` |
---
## Accessibility Contract
- `.background-layer` has `aria-hidden="true"` — decorative only
- Each `<img>` in `AnimatedBackground` has `alt="Xtablo"` — deferred to remove once `aria-hidden` is confirmed propagated by all browsers; acceptable for now
- Google button includes `.visually-hidden` span with "Sign in with Google" for screen readers
- Form inputs have explicit `<label for="...">` via `@ui.FormField`
- CSRF hidden field is preserved in both forms
- Disabled Google button uses `disabled` attribute + `aria-disabled="true"`
---
## CSS Files Contract
| File | Action | What changes |
|------|--------|-------------|
| `backend/internal/web/ui/auth.css` | Replace entirely | Port full auth CSS from go-backend: `.login-screen`, `.background-layer`/`.background-logo`/sizing/opacity/position classes, all `animate-*` utilities, all `@keyframes`, `.auth-card-shell`, `.auth-card-topbar`, `.brand-header`, `.brand-logo`, `.title-group`, `.auth-body`, `.login-form` (replaced by `@ui.FormField` but keep for compat), `.divider-row`/`.divider-line`/`.divider-pill`, `.signup-copy`, `.signup-link`, `.gsi-material-button` and sub-classes, `.card-wrap`, `.card-glow`, `.status-slot`, `.status-banner`, `.status-success`, `.status-error` |
| `backend/tailwind.input.css` | No change | `auth.css` path is unchanged; import remains valid |
| `backend/internal/web/ui/base.css` | No change | All required tokens already defined |
The existing `.auth-provider-stack`, `.auth-provider-button`, `.auth-provider-separator` rules in `auth.css` are superseded by the Google button restyle — but keep them for any non-Google controls. The `auth-provider-button` is replaced by `gsi-material-button` for Google specifically.
---
## Template Files Contract
| File | Action |
|------|--------|
| `backend/templates/auth_layout.templ` | **Create new** — standalone HTML shell: `<!DOCTYPE html>`, head with `tailwind.css` + `htmx.min.js`, body with `.login-screen` wrapper, `@AnimatedBackground()`, `.card-wrap`, `.card-glow`, `.auth-card-shell` |
| `backend/templates/auth_components.templ` | **Create new**`AnimatedBackground` component (35 elements); optionally `AuthDivider` component |
| `backend/templates/auth_login.templ` | **Update** — switch from `@Layout(...)` to `@AuthLayout(...)`; replace `@ui.Card(...)` wrapper with auth card structure; migrate inputs to `@ui.FormField`/`@ui.Input`; add nav link |
| `backend/templates/auth_signup.templ` | **Update** — same changes as login |
Handler signatures (`templates.LoginPage(...)`, `templates.SignupPage(...)`) must not change — the `AuthLayout` detail is internal to the template.
---
## Registry Safety
| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| shadcn official | none | not applicable |
| third-party | none | not applicable |
This project uses no component registry. All CSS is hand-authored. The only third-party visual asset is the Google SVG (4 colored paths), which is inlined verbatim from the Material Design specification and already present in go-backend source.
---
## Pre-Population Sources
| Decision | Source |
|----------|--------|
| AuthLayout gradient: `var(--gradient-shell)` | `base.css` token + go-backend `.login-screen` CSS |
| Auth card shell CSS (padding, radius, shadow, backdrop) | go-backend `styles.css` lines 11741182 |
| AnimatedBackground 35 elements | go-backend `auth_components.templ` verbatim |
| Logo asset path `/static/logo_dark.png` | D-AB02; go-backend static |
| Google button structure and all CSS | D-G01, D-G02; go-backend `GoogleButton` templ + styles |
| Google button label "Sign in with Google" | D-G03 |
| `@ui.FormField` / `@ui.Input` migration | D-FI01, D-FI02, D-FI03; Phase 13 design system |
| Nav link copy and CSS classes | D-NL01, D-NL02, D-NL03; go-backend `AuthScreenFooter` + `.signup-copy`/`.signup-link` |
| HTMX swap patterns preserved unchanged | CONTEXT.md code_context section |
| Dark mode logo deferred | CONTEXT.md deferred section (DARK-01) |
| Forgot password link deferred | CONTEXT.md deferred section |
| Responsive layout deferred | CONTEXT.md deferred section (RESP-01..03) |
---
## 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