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:
parent
d4a44631b7
commit
0d49c20e42
1 changed files with 395 additions and 0 deletions
395
.planning/phases/14-auth-pages/14-UI-SPEC.md
Normal file
395
.planning/phases/14-auth-pages/14-UI-SPEC.md
Normal 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` | ~1099–1111 | Logo size scale (rem-based) |
|
||||
| `.opacity-02` through `.opacity-05` | ~1113–1116 | Per-element opacity |
|
||||
| `.bg-01` through `.bg-35` | ~1118–1155 | Fixed positions (top/left/right/bottom percentages) |
|
||||
| All `animate-*` utility classes | ~2883–2930+ | 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.5rem–1.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 1174–1182 |
|
||||
| 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
|
||||
Loading…
Reference in a new issue