docs(14): create phase plan for auth pages restyle

Two-wave plan: Plan 01 creates foundation (logo assets, full auth.css
replacement with animations, auth_components.templ, auth_layout.templ);
Plan 02 migrates auth_login.templ and auth_signup.templ to AuthLayout
with @ui.FormField inputs and cross-page nav links, closing AUTH-UI-01..03.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arthur Belleville 2026-05-16 18:58:59 +02:00
parent 848f7480a8
commit 5ed9291d76
No known key found for this signature in database
3 changed files with 628 additions and 1 deletions

View file

@ -64,6 +64,7 @@ Plans:
**Mode:** mvp **Mode:** mvp
**Status:** Pending **Status:** Pending
**Requirements:** AUTH-UI-01, AUTH-UI-02, AUTH-UI-03 **Requirements:** AUTH-UI-01, AUTH-UI-02, AUTH-UI-03
**Plans:** 2 plans
**Success Criteria:** **Success Criteria:**
1. Login page has gradient background with animated background layer, centered auth card with brand logo, and status banner using design tokens 1. Login page has gradient background with animated background layer, centered auth card with brand logo, and status banner using design tokens
2. Signup page matches the same visual treatment as login 2. Signup page matches the same visual treatment as login
@ -71,7 +72,14 @@ Plans:
4. All existing auth handler tests pass unchanged 4. All existing auth handler tests pass unchanged
5. Browser walkthrough of login and signup matches the go-backend app.css auth-card design 5. Browser walkthrough of login and signup matches the go-backend app.css auth-card design
**User-in-loop:** Share design references (screenshots from JS app) before this phase begins; /frontend-design skill applies them during plan execution. Plans:
**Wave 1**
- [ ] 14-01-PLAN.md — Auth foundation: logo assets + auth.css replacement + auth_components.templ + auth_layout.templ
**Wave 2** *(blocked on Wave 1 completion)*
- [ ] 14-02-PLAN.md — Page migration: update auth_login.templ and auth_signup.templ to use AuthLayout + FormField inputs + nav links + browser verify checkpoint
**User-in-loop:** Browser walkthrough checkpoint in Plan 02 — approve visual result before considering the phase complete.
### Phase 15: Dashboard & Tablos ### Phase 15: Dashboard & Tablos
**Goal:** Restyle the layout shell (sidebar + main) and tablo list/dashboard to match the JS app's sidebar + project-card layout. **Goal:** Restyle the layout shell (sidebar + main) and tablo list/dashboard to match the JS app's sidebar + project-card layout.

View file

@ -0,0 +1,276 @@
---
phase: 14-auth-pages
plan: "01"
type: execute
wave: 1
depends_on: []
files_modified:
- backend/static/logo_dark.png
- backend/static/logo_white.png
- backend/internal/web/ui/auth.css
- backend/templates/auth_components.templ
- backend/templates/auth_layout.templ
autonomous: true
requirements: [AUTH-UI-01, AUTH-UI-02, AUTH-UI-03]
must_haves:
truths:
- "logo_dark.png and logo_white.png exist under backend/static/"
- "`just generate` completes without error after the new templ files are created"
- "auth.css contains all auth card, animated background, gsi-material-button, and animation CSS extracted from go-backend"
- "AnimatedBackground templ component emits exactly 35 .background-logo elements each with a single /static/logo_dark.png img"
- "GoogleButton templ component renders the gsi-material-button structure with the Google SVG and 'Sign in with Google' label"
- "AuthLayout templ component compiles and provides the .login-screen / .card-wrap / .auth-card-shell HTML shell"
artifacts:
- path: "backend/static/logo_dark.png"
provides: "Logo asset for AnimatedBackground and brand header"
- path: "backend/internal/web/ui/auth.css"
provides: "All auth page CSS: layout, animated background, auth card, Google button, animations"
contains: ".gsi-material-button"
- path: "backend/templates/auth_components.templ"
provides: "AnimatedBackground (35 elements), GoogleButton (gsi-material-button), AuthDivider components"
exports: ["AnimatedBackground", "GoogleButton", "AuthDivider"]
- path: "backend/templates/auth_layout.templ"
provides: "Standalone auth page HTML shell with .login-screen wrapper"
exports: ["AuthLayout"]
key_links:
- from: "backend/templates/auth_layout.templ"
to: "backend/templates/auth_components.templ"
via: "@AnimatedBackground() call"
pattern: "AnimatedBackground"
- from: "backend/internal/web/ui/auth.css"
to: "backend/tailwind.input.css"
via: "@import already present from Phase 13"
pattern: "auth.css"
---
## Phase Goal
**As a** user opening the login page, **I want to** see a visually polished auth page with animated background and branded card, **so that** signing in feels like a professional product experience.
<objective>
Create the auth page foundation: copy logo assets, replace auth.css with full auth card + animation CSS from go-backend, and create the new templ components (AnimatedBackground, GoogleButton, AuthDivider, AuthLayout). These artifacts are consumed by Plan 02 when it migrates the existing login/signup pages.
Purpose: Plan 01 delivers all new files without touching existing pages. Plan 02 (wave 2) wires them in.
Output: logo_dark.png + logo_white.png copied; auth.css replaced (~500 lines); auth_components.templ created (35-element animated bg + Google button + divider); auth_layout.templ created (standalone HTML shell).
</objective>
<execution_context>
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/workflows/execute-plan.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/ROADMAP.md
@.planning/phases/14-auth-pages/14-CONTEXT.md
@.planning/phases/14-auth-pages/14-UI-SPEC.md
@.planning/phases/14-auth-pages/14-PATTERNS.md
<interfaces>
<!-- Key types and patterns the executor needs. -->
From backend/templates/layout.templ (pattern for AuthLayout):
```go
package templates
import (
"backend/internal/auth"
"backend/internal/web/ui"
)
templ Layout(title string, user *auth.User, csrfToken string) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>{ title }</title>
<link rel="stylesheet" href="/static/tailwind.css"/>
</head>
<body ...>
{ children... }
<script src="/static/htmx.min.js" defer></script>
</body>
</html>
}
```
AuthLayout signature (from PATTERNS.md):
```go
templ AuthLayout(title string, csrfToken string) { ... }
// No auth.User param — auth pages are always unauthenticated
// No sortable.min.js or discussion-sse.js — not needed on auth pages
```
From go-backend/internal/web/views/auth_components.templ (reference — package name changes):
- AnimatedBackground: 35 .background-logo elements; single img per element (Phase 14 uses logo_dark.png only, no light-only/dark-only pair)
- GoogleButton: accepts (href string, configured bool); <a> when configured, <button disabled> when not
- AuthDivider: .divider-row with .divider-line / "or" pill / .divider-line
go-backend logo paths use /logo_dark.png — backend uses /static/logo_dark.png (note path difference)
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Copy logo assets and replace auth.css</name>
<files>backend/static/logo_dark.png, backend/static/logo_white.png, backend/internal/web/ui/auth.css</files>
<read_first>
- backend/internal/web/ui/auth.css (current 62-line stub being replaced)
- go-backend/static/styles.css lines 10621510 (auth CSS to extract: .login-screen, .background-layer, .background-logo, .logo-asset, .size-* utilities, .opacity-* utilities, .bg-01 through .bg-35, .card-wrap, .card-glow, .auth-card-shell, .auth-card-topbar, .brand-header, .brand-logo, .title-group, .auth-body, .login-form, .divider-row, .divider-line, .divider-pill, .signup-copy, .signup-link, .status-slot, .status-banner, .status-success, .status-error, .gsi-material-button and all sub-classes)
- go-backend/static/styles.css lines 25332947 (all @keyframes definitions and .animate-* utility classes)
- backend/tailwind.input.css (confirm auth.css import is already present)
</read_first>
<action>
1. Copy binary files verbatim: `cp go-backend/static/logo_dark.png backend/static/logo_dark.png` and `cp go-backend/static/logo_white.png backend/static/logo_white.png`.
2. Replace backend/internal/web/ui/auth.css entirely. The new file must contain these sections in order (extracted from go-backend/static/styles.css — do NOT copy app-level rules like .app-shell, .dashboard-shell, .sidebar-*, etc.):
Section A — Page shell (lines ~10621079, auth-only portion): `.login-screen` with min-height 100vh, flex center, background var(--gradient-shell), overflow hidden, padding 2rem 1rem, position relative. Do NOT include `.app-shell` rule.
Section B — Animated background (lines ~10811155): `.background-layer`, `.background-logo`, `.logo-asset`, all `.size-06` through `.size-20` utilities (13 classes), all `.opacity-02` through `.opacity-05` utilities (4 classes), all `.bg-01` through `.bg-35` position rules (35 classes).
Section C — Card wrapper (lines ~11571182): `.card-wrap`, `.card-glow`, `.auth-card-shell` (padding: 1.25rem verbatim — not a design token).
Section D — Card internals (lines ~11841425): `.auth-card-topbar`, `.brand-header`, `.brand-logo`, `.title-group`, `.title-group h1` (clamp font-size), `.auth-body`, `.login-form`, `.divider-row`, `.divider-line`, `.divider-pill`, `.signup-copy`, `.signup-link` (including hover state), `.status-slot`, `.status-banner`, `.status-success`, `.status-error`. Skip `.back-home-link`, `.theme-toggle-button`, `.new-experience-link-wrap`, `.forgot-password-row`, `.submit-button`, `.field-stack` (these are go-backend specifics not used in backend's template pattern). Also skip `.field-stack` since inputs use ui-input from Phase 13.
Section E — Google button (lines ~14271509): `.gsi-material-button` and ALL sub-class rules verbatim. Add `display: block; text-decoration: none;` to `.gsi-material-button` to handle the `<a>` variant (go-backend uses `<button>` only, backend uses `<a>` when configured).
Section F — Keep existing auth-provider rules at bottom for any non-Google controls: `.auth-provider-stack`, `.auth-provider-button`, `.auth-provider-button:hover`, `.auth-provider-button:focus-visible`, `.auth-provider-button-disabled`, `.auth-provider-separator`, `.auth-provider-separator span`, `.auth-provider-separator em` (copy verbatim from the current auth.css stub).
Section G — Keyframes (lines ~25332882): all @keyframes definitions verbatim.
Section H — Animation utilities (lines ~28832947): all .animate-* utility classes verbatim.
3. No change to backend/tailwind.input.css — the @import "./internal/web/ui/auth.css" line was added in Phase 13 and remains valid.
</action>
<verify>
<automated>
ls backend/static/logo_dark.png backend/static/logo_white.png &&
grep -c 'gsi-material-button' backend/internal/web/ui/auth.css &&
grep -c '@keyframes' backend/internal/web/ui/auth.css &&
grep -c 'animate-move-right-slow' backend/internal/web/ui/auth.css &&
grep 'login-screen' backend/internal/web/ui/auth.css &&
grep 'auth-card-shell' backend/internal/web/ui/auth.css &&
grep 'signup-link' backend/internal/web/ui/auth.css
</automated>
</verify>
<acceptance_criteria>
- backend/static/logo_dark.png exists (binary file, non-zero size)
- backend/static/logo_white.png exists (binary file, non-zero size)
- auth.css contains `.gsi-material-button` (grep -c returns >= 1)
- auth.css contains >= 10 @keyframes definitions (grep -c '@keyframes' returns >= 10)
- auth.css contains `.animate-move-right-slow` (at least one animate-* utility)
- auth.css contains `.login-screen` rule
- auth.css contains `.auth-card-shell` with `padding: 1.25rem`
- auth.css contains `.signup-link` rule
- auth.css does NOT contain `.app-shell` (wrong rule from go-backend)
- auth.css does NOT contain `.dashboard-shell` (wrong rule from go-backend)
</acceptance_criteria>
<done>Logo assets exist in backend/static/, auth.css is fully replaced with all auth card, animated background, Google button, and animation CSS (~500+ lines total).</done>
</task>
<task type="auto">
<name>Task 2: Create auth_components.templ and auth_layout.templ</name>
<files>backend/templates/auth_components.templ, backend/templates/auth_layout.templ</files>
<read_first>
- backend/templates/layout.templ (pattern for HTML shell structure, script tags, head elements)
- go-backend/internal/web/views/auth_components.templ (AnimatedBackground 35 elements, GoogleButton structure, AuthDivider — port verbatim with two adaptations: image path /logo_dark.png → /static/logo_dark.png, remove light-only/dark-only pairs)
- backend/templates/auth_login.templ (current file — to understand existing AuthProviderButtons type used by GoogleButton)
- .planning/phases/14-auth-pages/14-PATTERNS.md (AnimatedBackground element class list: bg-01..bg-35 with outer/inner class assignments per slot)
</read_first>
<action>
1. Create backend/templates/auth_components.templ with package declaration `package templates` and three components:
AnimatedBackground(): Port all 35 elements from go-backend/internal/web/views/auth_components.templ lines 108158. Adaptations:
- Change all img src from "/logo_dark.png" to "/static/logo_dark.png"
- Replace every two-image (light-only/dark-only) pattern with a single img element using just the classes from the light-only image (drop the dark-only img entirely; Phase 14 uses logo_dark.png only per D-AB02 / DEFERRED section)
- Parent div carries aria-hidden="true" on .background-layer
- Exact class assignments per slot per 14-PATTERNS.md element table (bg-01 through bg-35)
GoogleButton(href string, configured bool): Renders the Material Design button structure.
- When configured=true: `<a class="gsi-material-button" href={ templ.SafeURL(href) }>` wrapping .gsi-material-button-state div, .gsi-material-button-content-wrapper div containing .gsi-material-button-icon div (Google SVG 4 paths), `<span class="gsi-material-button-contents">Sign in with Google</span>`, `<span class="visually-hidden">Sign in with Google</span>`.
- When configured=false: `<button type="button" class="gsi-material-button" disabled aria-disabled="true">` with same inner structure.
- Google SVG paths (verbatim from go-backend): #EA4335 (path M24 9.5...), #4285F4 (path M46.98 24.55...), #FBBC05 (path M10.53 28.59...), #34A853 (path M24 48...), none fill (path M0 0h48v48H0z).
- Label text: "Sign in with Google" (English per D-G03; go-backend uses French — do not copy French text).
AuthDivider(): Single component, no params.
- `<div class="divider-row">` containing `.divider-line` div, `<span class="divider-pill">or</span>`, `.divider-line` div.
- "or" in English (not "Ou continuer avec" from go-backend per D-G03 English convention).
2. Create backend/templates/auth_layout.templ with package declaration `package templates` and signature:
`templ AuthLayout(title string, csrfToken string)`
- No import of "backend/internal/auth" — auth pages are always unauthenticated (no *auth.User param)
- Full HTML shell: `<!DOCTYPE html>`, `<html lang="en">`, head with charset/viewport meta, `<title>{ title }</title>`, `<link rel="stylesheet" href="/static/tailwind.css"/>`
- Body: `<div class="login-screen">` containing `@AnimatedBackground()`, `<div class="card-wrap">` containing `<div class="card-glow"></div>` and `<div class="auth-card-shell">{ children... }</div>`
- Script: `<script src="/static/htmx.min.js" defer></script>` — auth forms are HTMX-driven. Do NOT include sortable.min.js or discussion-sse.js.
- csrfToken parameter is carried through { children... } — the layout itself does not embed a CSRF field; only the form fragments do.
</action>
<verify>
<automated>
cd backend && just generate 2>&1 | tail -5 &&
grep -c 'background-logo' backend/templates/auth_components.templ &&
grep 'Sign in with Google' backend/templates/auth_components.templ &&
grep 'AnimatedBackground' backend/templates/auth_layout.templ &&
grep 'login-screen' backend/templates/auth_layout.templ
</automated>
</verify>
<acceptance_criteria>
- `just generate` completes with exit code 0 (no templ compile errors)
- auth_components.templ contains exactly 35 occurrences of "background-logo" (one per slot)
- auth_components.templ contains "Sign in with Google" (English label, D-G03)
- auth_components.templ does NOT contain "Continuer avec Google" or "Ou continuer avec" (French text from go-backend)
- auth_components.templ contains "/static/logo_dark.png" (not "/logo_dark.png")
- auth_components.templ does NOT contain "light-only" or "dark-only" classes (deferred per DEFERRED section)
- auth_layout.templ contains `@AnimatedBackground()`
- auth_layout.templ contains `login-screen`
- auth_layout.templ contains `htmx.min.js`
- auth_layout.templ does NOT contain "auth.User" (no user parameter)
- auth_layout.templ does NOT contain "sortable.min.js"
- `go test ./backend/... -count=1` passes (or `cd backend && go test ./... -count=1`)
</acceptance_criteria>
<done>auth_components.templ and auth_layout.templ exist in backend/templates/, `just generate` compiles both files without error, and all existing tests remain green.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Static file serving | logo_dark.png served at /static/logo_dark.png — binary file from go-backend source, no user input |
| Template rendering | auth_components.templ and auth_layout.templ render server-controlled static content only |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-14-01-01 | Spoofing | logo_dark.png static file | accept | File is served from backend/static/ with no user-controlled path; binary asset copied from trusted go-backend source |
| T-14-01-02 | Tampering | auth.css in tailwind build | accept | CSS is build-time artifact; no runtime user input affects CSS output; tailwind.css is generated once and served as static |
| T-14-01-03 | Information Disclosure | AnimatedBackground aria-hidden | mitigate | .background-layer carries aria-hidden="true" per accessibility contract; decorative logos not exposed to screen readers |
| T-14-01-04 | Elevation of Privilege | AuthLayout no-auth check | accept | AuthLayout has no auth.User param by design; pages using it are always unauthenticated; handler-level RedirectIfAuthed middleware handles the actual gate |
</threat_model>
<verification>
After plan 01 completes:
1. `ls backend/static/logo_dark.png backend/static/logo_white.png` — both files exist
2. `wc -l backend/internal/web/ui/auth.css` — file is > 200 lines (full extraction)
3. `grep -c 'background-logo' backend/templates/auth_components.templ` — returns 35
4. `grep 'AnimatedBackground' backend/templates/auth_layout.templ` — matches
5. `cd backend && just generate` — exits 0
6. `cd backend && go test ./... -count=1` — all tests pass (no regressions from new files)
</verification>
<success_criteria>
- logo_dark.png and logo_white.png present in backend/static/
- auth.css contains full auth card CSS, gsi-material-button CSS, all @keyframes, and all .animate-* utilities (file > 200 lines)
- auth_components.templ: AnimatedBackground with exactly 35 background-logo elements using /static/logo_dark.png, GoogleButton with English label, AuthDivider with "or"
- auth_layout.templ: standalone HTML shell calling @AnimatedBackground(), no auth.User param
- `just generate` exits 0
- All existing tests pass unchanged
</success_criteria>
<output>
After completion, create `.planning/phases/14-auth-pages/14-01-SUMMARY.md`
</output>

View file

@ -0,0 +1,343 @@
---
phase: 14-auth-pages
plan: "02"
type: execute
wave: 2
depends_on: ["14-01"]
files_modified:
- backend/templates/auth_login.templ
- backend/templates/auth_signup.templ
autonomous: false
requirements: [AUTH-UI-01, AUTH-UI-02, AUTH-UI-03]
must_haves:
truths:
- "User visits /login and sees gradient background with animated logo icons — no plain white page"
- "User sees the brand logo above the 'Sign in to Xtablo' heading inside the auth card"
- "User sees the gsi-material-button Google button above the 'or' divider"
- "User sees email and password inputs styled by the ui-input design system component"
- "User sees 'Don't have an account? Sign up' link below the login form"
- "User visits /signup and sees the same visual treatment with 'Create your account' heading and 'Already have an account? Sign in' link"
- "HTMX swap behavior is preserved: submitting login form triggers hx-post=/login hx-target=#login-form hx-swap=outerHTML"
- "All existing auth handler tests pass unchanged"
artifacts:
- path: "backend/templates/auth_login.templ"
provides: "Updated login page using AuthLayout, GoogleButton, ui.FormField/ui.Input, nav link"
contains: "@AuthLayout"
- path: "backend/templates/auth_signup.templ"
provides: "Updated signup page using AuthLayout, GoogleButton, ui.FormField/ui.Input, nav link"
contains: "@AuthLayout"
key_links:
- from: "backend/templates/auth_login.templ LoginPage"
to: "backend/templates/auth_layout.templ AuthLayout"
via: "@AuthLayout(\"Sign in to Xtablo\", csrfToken)"
pattern: "@AuthLayout"
- from: "backend/templates/auth_login.templ LoginPage"
to: "backend/templates/auth_components.templ GoogleButton"
via: "@GoogleButton(providers.Google.StartURL, providers.Google.Configured)"
pattern: "@GoogleButton"
- from: "backend/templates/auth_login.templ LoginFormFragment"
to: "backend/internal/web/ui/form_field.templ"
via: "@ui.FormField(ui.FormFieldProps{...})"
pattern: "ui.FormField"
---
## Phase Goal
**As a** user opening the login or signup page, **I want to** see a visually polished auth page with animated background and branded card, **so that** signing in feels like a professional product experience.
<objective>
Migrate auth_login.templ and auth_signup.templ from the old Layout+Card pattern to AuthLayout with the full auth card structure. Replaces raw inputs with @ui.FormField/@ui.Input, wires in GoogleButton and AuthDivider, and adds cross-page navigation links. After this plan, a user visiting /login or /signup sees the complete visual redesign.
Purpose: This is the deliverable vertical slice. Plan 01 delivered the components; this plan connects them to the actual pages users see.
Output: Updated auth_login.templ and auth_signup.templ. Handler signatures unchanged. All existing tests remain green.
</objective>
<execution_context>
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/workflows/execute-plan.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/ROADMAP.md
@.planning/phases/14-auth-pages/14-CONTEXT.md
@.planning/phases/14-auth-pages/14-UI-SPEC.md
@.planning/phases/14-auth-pages/14-PATTERNS.md
@.planning/phases/14-auth-pages/14-01-SUMMARY.md
<interfaces>
<!-- Key types and contracts the executor needs. -->
From backend/templates/auth_login.templ (current — being replaced):
```go
templ LoginPage(form LoginForm, errs LoginErrors, csrfToken string, providers AuthProviderButtons) { ... }
templ LoginFormFragment(form LoginForm, errs LoginErrors, csrfToken string) { ... }
templ AuthProviderButtonsBlock(providers AuthProviderButtons) { ... }
templ AuthProviderButtonControl(provider AuthProviderButton) { ... }
```
Handler-level type (backend/templates/auth_forms.go or equivalent — do not change):
```go
type AuthProviderButtons struct {
Google AuthProviderButton
}
type AuthProviderButton struct {
Configured bool
StartURL string
Label string
DisabledLabel string
}
```
From backend/internal/web/ui/form_field.templ:
```go
type FormFieldProps struct {
Label string
For string
Field templ.Component
Error string
Hint string
}
templ FormField(props FormFieldProps) { ... }
```
From backend/internal/web/ui/input.templ:
```go
type InputProps struct {
ID string
Name string
Value string
Placeholder string
Type string
Disabled bool
Required bool
Attrs templ.Attributes
}
templ Input(props InputProps) { ... }
```
From backend/internal/web/ui/button.templ (already used — keep as-is):
```go
@ui.Button(ui.ButtonProps{
Label: "...",
Variant: ui.ButtonVariantDefault,
Tone: ui.ButtonToneSolid,
Size: ui.SizeMD,
Type: "submit",
})
```
From backend/templates/auth_components.templ (created in Plan 01):
```go
templ AuthLayout(title string, csrfToken string) { ... }
templ GoogleButton(href string, configured bool) { ... }
templ AuthDivider() { ... }
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Migrate auth_login.templ to AuthLayout with ui.FormField inputs</name>
<files>backend/templates/auth_login.templ</files>
<read_first>
- backend/templates/auth_login.templ (current file — read the full file before editing; understand LoginPage, loginCardBody, LoginFormFragment, AuthProviderButtonsBlock, AuthProviderButtonControl)
- backend/templates/auth_signup.templ (peer file — read to understand the existing AuthProviderButtons usage pattern to keep in mind for Task 2)
- backend/templates/auth_forms.go (or wherever LoginForm, LoginErrors, AuthProviderButtons types are defined — read to confirm field names)
- backend/internal/web/ui/form_field.templ (FormFieldProps struct and FormField signature)
- backend/internal/web/ui/input.templ (InputProps struct and Input signature)
- .planning/phases/14-auth-pages/14-PATTERNS.md (LoginPage and LoginFormFragment pattern sections for exact field values and structure)
</read_first>
<action>
Replace backend/templates/auth_login.templ entirely. The file must contain exactly these components:
1. Package declaration: `package templates`
2. Import: `"backend/internal/web/ui"` (needed for @ui.FormField, @ui.Input, @ui.Button, ui.CSRFField)
3. `templ LoginPage(form LoginForm, errs LoginErrors, csrfToken string, providers AuthProviderButtons)` — function signature MUST remain identical (handler compatibility).
Body: `@AuthLayout("Sign in to Xtablo", csrfToken)` with child content:
- `<div class="auth-card-topbar"></div>` (empty placeholder, no back-link in Phase 14)
- `<div class="brand-header"><img class="brand-logo" src="/static/logo_dark.png" alt="Xtablo"/></div>`
- `<div class="title-group"><h1>Sign in to Xtablo</h1></div>`
- `<div class="auth-body">` containing: `@GoogleButton(providers.Google.StartURL, providers.Google.Configured)`, `@AuthDivider()`, `@LoginFormFragment(form, errs, csrfToken)` — in that order (per UI-SPEC element order contract)
4. `templ LoginFormFragment(form LoginForm, errs LoginErrors, csrfToken string)`:
`<form id="login-form" method="POST" action="/login" hx-post="/login" hx-target="#login-form" hx-swap="outerHTML" class="login-form">`
Inside the form (in order):
- `@ui.CSRFField(csrfToken)` (preserve exactly)
- `@GeneralError(errs.General)` (preserve exactly)
- `@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})`
- `@ui.FormField(ui.FormFieldProps{Label: "Password", For: "password", Field: ui.Input(ui.InputProps{ID: "password", Name: "password", Type: "password", Placeholder: "Your password", Required: true, Attrs: templ.Attributes{"autocomplete": "current-password"}}), Error: errs.Password})`
- `@ui.Button(ui.ButtonProps{Label: "Sign in to Xtablo", Variant: ui.ButtonVariantDefault, Tone: ui.ButtonToneSolid, Size: ui.SizeMD, Type: "submit"})` (label changes from "Sign in" to "Sign in to Xtablo" per copywriting contract)
- `<p class="signup-copy">Don't have an account? <a class="signup-link" href="/signup">Sign up</a></p>` (per D-NL01)
- Close form
HTMX attributes preserved exactly: `hx-post="/login" hx-target="#login-form" hx-swap="outerHTML"`, form id="login-form".
The old `class="space-y-5"` on form → `class="login-form"` (auth.css handles layout).
Old raw `<input>` elements removed — replaced by @ui.FormField + @ui.Input.
Old `@FieldError(errs.X)` calls removed — errors wired through FormFieldProps.Error field.
5. REMOVE AuthProviderButtonsBlock and AuthProviderButtonControl components from this file — they are superseded by GoogleButton from auth_components.templ. Verify no other file in backend/templates/ imports or calls these before removing; if called elsewhere, keep the stubs but empty them. (auth_signup.templ also uses AuthProviderButtonsBlock — that file is updated in Task 2 simultaneously.)
Note: `loginCardBody` helper templ (private, was used by old LoginPage) is no longer needed — remove it.
</action>
<verify>
<automated>
cd backend && just generate 2>&1 | tail -5 &&
grep '@AuthLayout' backend/templates/auth_login.templ &&
grep 'GoogleButton' backend/templates/auth_login.templ &&
grep 'ui.FormField' backend/templates/auth_login.templ &&
grep 'signup-copy' backend/templates/auth_login.templ &&
grep 'hx-post="/login"' backend/templates/auth_login.templ &&
go test ./templates/... -count=1 2>/dev/null || go test ./... -count=1 -run TestLogin
</automated>
</verify>
<acceptance_criteria>
- auth_login.templ contains `@AuthLayout("Sign in to Xtablo", csrfToken)`
- auth_login.templ contains `@GoogleButton(providers.Google.StartURL, providers.Google.Configured)`
- auth_login.templ contains `@AuthDivider()`
- auth_login.templ contains `@ui.FormField` (at least 2 occurrences — email and password fields)
- auth_login.templ contains `class="login-form"` on the form element
- auth_login.templ contains `hx-post="/login"` and `hx-target="#login-form"` and `hx-swap="outerHTML"` (HTMX preserved)
- auth_login.templ contains `class="signup-copy"` with href="/signup" link
- auth_login.templ contains `Sign in to Xtablo` as submit button label
- auth_login.templ does NOT contain raw `<input type="email"` or `<input type="password"` (replaced by @ui.Input)
- auth_login.templ does NOT contain `@Layout(` (old layout removed)
- `just generate` exits 0
- `cd backend && go test ./... -count=1` all tests pass
</acceptance_criteria>
<done>Login page uses AuthLayout with full auth card structure, Google button, FormField inputs, and signup navigation link. HTMX swap behavior preserved. All tests green.</done>
</task>
<task type="auto">
<name>Task 2: Migrate auth_signup.templ to AuthLayout with ui.FormField inputs</name>
<files>backend/templates/auth_signup.templ</files>
<read_first>
- backend/templates/auth_signup.templ (current file — read before editing)
- backend/templates/auth_login.templ (just updated in Task 1 — use as pattern reference; signup mirrors login with specific differences noted below)
- backend/templates/auth_forms.go (SignupForm, SignupErrors types — confirm field names: Email, Password, General)
- .planning/phases/14-auth-pages/14-PATTERNS.md (SignupPage and SignupFormFragment pattern sections)
</read_first>
<action>
Replace backend/templates/auth_signup.templ entirely. Mirror the login page structure with these differences:
1. Package declaration: `package templates`
2. Import: `"backend/internal/web/ui"`
3. `templ SignupPage(form SignupForm, errs SignupErrors, csrfToken string, providers AuthProviderButtons)` — signature unchanged.
Body: `@AuthLayout("Create your account", csrfToken)` with child content:
- `<div class="auth-card-topbar"></div>`
- `<div class="brand-header"><img class="brand-logo" src="/static/logo_dark.png" alt="Xtablo"/></div>`
- `<div class="title-group"><h1>Create your account</h1></div>` (different heading)
- `<div class="auth-body">` containing: `@GoogleButton(providers.Google.StartURL, providers.Google.Configured)`, `@AuthDivider()`, `@SignupFormFragment(form, errs, csrfToken)`
4. `templ SignupFormFragment(form SignupForm, errs SignupErrors, csrfToken string)`:
`<form id="signup-form" method="POST" action="/signup" hx-post="/signup" hx-target="#signup-form" hx-swap="outerHTML" class="login-form">`
Inside the form (in order):
- `@ui.CSRFField(csrfToken)`
- `@GeneralError(errs.General)`
- Email FormField: identical to login (same Label, Placeholder, autocomplete="email")
- Password FormField: `Label: "Password"`, `For: "password"`, `Placeholder: "12 characters minimum"` (differs from login), `Attrs: templ.Attributes{"autocomplete": "new-password"}` (differs from login — new-password not current-password), `Error: errs.Password`
- `@ui.Button(ui.ButtonProps{Label: "Create account", ...})` (label differs from login: "Create account" not "Sign in to Xtablo")
- `<p class="signup-copy">Already have an account? <a class="signup-link" href="/login">Sign in</a></p>` (per D-NL02; different copy and href from login)
- Close form
HTMX preserved: `hx-post="/signup" hx-target="#signup-form" hx-swap="outerHTML"`, form id="signup-form".
5. Remove signupCardBody helper templ (no longer needed).
</action>
<verify>
<automated>
cd backend && just generate 2>&1 | tail -5 &&
grep '@AuthLayout' backend/templates/auth_signup.templ &&
grep 'Create your account' backend/templates/auth_signup.templ &&
grep 'hx-post="/signup"' backend/templates/auth_signup.templ &&
grep 'signup-copy' backend/templates/auth_signup.templ &&
grep 'href="/login"' backend/templates/auth_signup.templ &&
cd backend && go test ./... -count=1
</automated>
</verify>
<acceptance_criteria>
- auth_signup.templ contains `@AuthLayout("Create your account", csrfToken)`
- auth_signup.templ contains `@GoogleButton(providers.Google.StartURL, providers.Google.Configured)`
- auth_signup.templ contains `@AuthDivider()`
- auth_signup.templ contains `@ui.FormField` (at least 2 occurrences)
- auth_signup.templ contains `hx-post="/signup"` and `hx-target="#signup-form"` and `hx-swap="outerHTML"`
- auth_signup.templ contains `class="signup-copy"` with href="/login" link (pointing to login, not signup)
- auth_signup.templ contains `12 characters minimum` as password placeholder
- auth_signup.templ contains `autocomplete": "new-password"` (not current-password)
- auth_signup.templ contains `Create account` as submit button label
- auth_signup.templ does NOT contain raw `<input type="email"` or `<input type="password"`
- auth_signup.templ does NOT contain `@Layout(`
- `just generate` exits 0
- `cd backend && go test ./... -count=1` all tests pass (including auth_signup_test.go)
</acceptance_criteria>
<done>Signup page uses AuthLayout with full auth card structure, Google button, FormField inputs, and login navigation link. HTMX swap behavior preserved. All tests green.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>Both auth pages (login and signup) have been migrated to the new auth-card layout. The animated background with 35 logo icons, gradient shell, brand logo, gsi-material-button Google sign-in button, and design-system inputs are all wired in.</what-built>
<how-to-verify>
1. Start the backend server: `cd backend && just run` (or `go run ./cmd/web`)
2. Visit http://localhost:8080/login
- Confirm: gradient background (purple tones) with animated logos floating across the screen
- Confirm: centered white/frosted card with brand logo (Xtablo icon) above "Sign in to Xtablo" heading
- Confirm: Google sign-in button with Google logo icon and "Sign in with Google" text (Material Design style)
- Confirm: "or" divider with lines on each side
- Confirm: "Email address" and "Password" inputs using the ui-input design system style (rounded, consistent padding)
- Confirm: "Sign in to Xtablo" submit button
- Confirm: "Don't have an account? Sign up" link below the form
3. Click the "Sign up" link → should navigate to /signup
- Confirm: same visual treatment (gradient bg, animated logos, auth card)
- Confirm: "Create your account" heading
- Confirm: password placeholder reads "12 characters minimum"
- Confirm: "Already have an account? Sign in" link below the form
4. Click the "Sign in" link → navigates back to /login
5. Submit the login form with invalid credentials → HTMX swap replaces the form inline (form stays inside the card, no full page reload, error message appears)
6. Submit the signup form with mismatched or weak password → HTMX swap replaces the form inline with error
</how-to-verify>
<resume-signal>Type "approved" if the visual result matches the above. Or describe any issues found (missing animation, wrong colors, HTMX not swapping, etc.).</resume-signal>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| form input → POST /login | Email and password from user cross the trust boundary into the login handler |
| form input → POST /signup | Email and password from user cross the trust boundary into the signup handler |
| CSRF token | csrf.Token(r) injected by middleware; ui.CSRFField(csrfToken) embeds it in the form |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-14-02-01 | Spoofing | CSRF token in auth forms | mitigate | @ui.CSRFField(csrfToken) preserved in both LoginFormFragment and SignupFormFragment; token validated server-side by gorilla/csrf middleware (Phase 2 wiring, unchanged) |
| T-14-02-02 | Tampering | HTMX hx-post targets | accept | hx-post="/login" and hx-post="/signup" are hardcoded strings in server-rendered templates; not user-controlled; HTMX makes standard POST requests identical to HTML form submission |
| T-14-02-03 | Information Disclosure | Error messages via @GeneralError / FormFieldProps.Error | accept | Error strings are server-controlled (handler generates them); templ auto-escapes all string values preventing XSS; field-level errors communicate only field validity state |
| T-14-02-04 | Elevation of Privilege | GoogleButton href via templ.SafeURL | mitigate | providers.Google.StartURL is constructed server-side from configured OAuth start URL; templ.SafeURL validates the URL before rendering as href attribute; unconfigured state renders disabled button with no href |
| T-14-02-05 | Denial of Service | AnimatedBackground 35 CSS animations | accept | All 35 animations are pure CSS with pointer-events:none; no JS involved; browser handles animation scheduling; no server-side performance impact |
</threat_model>
<verification>
After plan 02 completes:
1. `cd backend && just generate` — exits 0, both auth_login_templ.go and auth_signup_templ.go regenerated
2. `cd backend && go test ./... -count=1` — all tests pass including auth handler tests and auth_signup_test.go
3. Browser: GET /login → gradient + animated bg + auth card + Google button + FormField inputs + nav link
4. Browser: GET /signup → same visual treatment + "Create your account" heading + nav link back to /login
5. Browser: submit invalid login → HTMX swaps form inline (card does not disappear, error appears within card)
</verification>
<success_criteria>
AUTH-UI-01: Login page has gradient background with animated background layer, centered auth card with brand logo, and status banner capability using design tokens — confirmed by browser walkthrough step 2.
AUTH-UI-02: Signup page matches the same visual treatment as login — confirmed by browser walkthrough step 3.
AUTH-UI-03: Google sign-in button uses Material Design gsi-material-button style — confirmed by browser walkthrough (Google icon + "Sign in with Google" text, pill border radius, 40px height).
All auth handler tests pass unchanged (handler signatures not modified).
</success_criteria>
<output>
After completion, create `.planning/phases/14-auth-pages/14-02-SUMMARY.md`
</output>