Plan 14-01: Auth foundation (logo assets, auth.css, auth_components.templ, auth_layout.templ) Plan 14-02: Page migration (auth_login.templ, auth_signup.templ → AuthLayout + ui.FormField + nav links) Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
22 KiB
Phase 14: Auth Pages - Pattern Map
Mapped: 2026-05-16 Files analyzed: 7 (5 templates/CSS + 2 static assets) Analogs found: 7 / 7
File Classification
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|---|---|---|---|---|
backend/templates/auth_layout.templ |
template/layout | request-response | backend/templates/layout.templ |
exact-role |
backend/templates/auth_components.templ |
template/component | static-render | go-backend/internal/web/views/auth_components.templ |
exact |
backend/templates/auth_login.templ |
template/page | request-response | backend/templates/auth_signup.templ (peer) |
exact |
backend/templates/auth_signup.templ |
template/page | request-response | backend/templates/auth_login.templ (peer) |
exact |
backend/internal/web/ui/auth.css |
stylesheet | static-render | go-backend/static/styles.css lines 1063–1510, 2533–2947 |
exact (extract) |
backend/static/logo_dark.png |
static asset | file-copy | go-backend/static/logo_dark.png |
exact |
backend/static/logo_white.png |
static asset | file-copy | go-backend/static/logo_white.png |
exact |
Pattern Assignments
backend/templates/auth_layout.templ (layout, request-response)
Analog: backend/templates/layout.templ
Package and imports pattern (backend/templates/layout.templ lines 1–9):
package templates
import (
"backend/internal/web/ui"
)
AuthLayout does NOT import backend/internal/auth — auth pages are always unauthenticated. No user *auth.User parameter.
HTML shell pattern (backend/templates/layout.templ lines 25–59 adapted):
templ AuthLayout(title string, csrfToken string) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>{ title }</title>
<link rel="stylesheet" href="/static/tailwind.css"/>
</head>
<body>
<div class="login-screen">
@AnimatedBackground()
<div class="card-wrap">
<div class="card-glow"></div>
<div class="auth-card-shell">
{ children... }
</div>
</div>
</div>
<script src="/static/htmx.min.js" defer></script>
</body>
</html>
}
Key deltas from Layout:
- No
<header>, no<footer>, no<main>wrapper - Body wraps in
.login-screen(notmin-h-screen bg-white) - Scripts: keep
htmx.min.js; dropsortable.min.jsanddiscussion-sse.js(not needed on auth pages) csrfTokenis threaded through because the page children (form fragments) useui.CSRFField(csrfToken)@AnimatedBackground()is called fromauth_components.templin the same package
CSRF threading note: csrfToken on AuthLayout is passed to child page templates (LoginPage, SignupPage) which forward it into their form fragments. The layout itself does not embed a CSRF field — only the forms do.
backend/templates/auth_components.templ (component, static-render)
Analog: go-backend/internal/web/views/auth_components.templ (port verbatim, with adaptations noted)
Package declaration:
package templates
(not package views — must match the backend package)
AnimatedBackground component (go-backend/internal/web/views/auth_components.templ lines 108–158):
Port verbatim from go-backend. Two adaptations:
- Image src path: change
/logo_dark.png→/static/logo_dark.png(backend serves static files under/static/) - Dark-mode images (
light-only/dark-onlypairs): Phase 14 useslogo_dark.pngonly — replace the two-image pattern with a single<img>per element (dark mode deferred per DEFERRED section). Thelight-only/dark-onlyCSS classes are defined inbase.csslines 206–213 but should not be emitted on images in Phase 14.
templ AnimatedBackground() {
<div class="background-layer" aria-hidden="true">
<div class="background-logo bg-01 animate-move-right-slow opacity-04">
<img alt="Xtablo" class="logo-asset size-16 animate-spin-slow" src="/static/logo_dark.png"/>
</div>
<div class="background-logo bg-02 animate-move-right-medium opacity-03">
<img alt="Xtablo" class="logo-asset size-12 animate-bounce-gentle" src="/static/logo_dark.png"/>
</div>
// ... all 35 elements following the same single-img pattern
// See go-backend source lines 110–156 for the full class list per element
</div>
}
Full element list (go-backend lines 110–156 — class assignments per slot):
| Slot | Outer classes | Inner img classes |
|---|---|---|
| bg-01 | animate-move-right-slow opacity-04 |
size-16 animate-spin-slow |
| bg-02 | animate-move-right-medium opacity-03 |
size-12 animate-bounce-gentle |
| bg-03 | animate-move-right-fast opacity-05 |
size-20 animate-pulse-gentle |
| bg-04 | animate-move-right-slow opacity-02 |
size-14 animate-wiggle |
| bg-05 | animate-move-right-medium opacity-03 |
size-18 animate-float-gentle |
| bg-06 | animate-move-diagonal-1 opacity-03 |
size-10 animate-spin-reverse |
| bg-07 | animate-move-diagonal-2 opacity-04 |
size-16 animate-scale-gentle |
| bg-08 | animate-move-diagonal-3 opacity-02 |
size-12 animate-rotate-gentle |
| bg-09 | animate-move-down-slow opacity-03 |
size-14 animate-bounce-soft |
| bg-10 | animate-move-down-medium opacity-04 |
size-16 animate-sway |
| bg-11 | animate-orbit-1 opacity-02 |
size-08 (no img animation) |
| bg-12 | animate-orbit-2 opacity-03 |
size-10 (no img animation) |
| bg-13 | animate-orbit-3 opacity-02 |
size-06 (no img animation) |
| bg-14 | animate-orbit-4 opacity-03 |
size-12 animate-spin-fast |
| bg-15 | animate-orbit-5 opacity-02 |
size-07 animate-pulse-fast |
| bg-16 | animate-zigzag-1 opacity-04 |
size-14 animate-wobble |
| bg-17 | animate-zigzag-2 opacity-03 |
size-11 animate-shake |
| bg-18 | animate-zigzag-3 opacity-05 |
size-16 animate-bounce-crazy |
| bg-19 | animate-spiral-1 opacity-03 |
size-09 animate-spin-wobble |
| bg-20 | animate-spiral-2 opacity-04 |
size-13 animate-flip |
| bg-21 | animate-float-random-1 opacity-02 |
size-08 animate-twirl |
| bg-22 | animate-float-random-2 opacity-03 |
size-10 animate-dance |
| bg-23 | animate-float-random-3 opacity-04 |
size-12 animate-jiggle |
| bg-24 | animate-float-random-4 opacity-02 |
size-09 animate-vibrate |
| bg-25 | animate-wave-1 opacity-03 |
size-11 animate-swing |
| bg-26 | animate-wave-2 opacity-04 |
size-13 animate-pendulum |
| bg-27 | animate-wave-3 opacity-02 |
size-10 animate-elastic |
| bg-28 | animate-wave-4 opacity-05 |
size-15 animate-rubber |
| bg-29 | animate-corner-shoot-1 opacity-03 |
size-12 animate-rocket |
| bg-30 | animate-corner-shoot-2 opacity-04 |
size-14 animate-comet |
| bg-31 | animate-corner-shoot-3 opacity-02 |
size-10 animate-meteor |
| bg-32 | animate-corner-shoot-4 opacity-05 |
size-16 animate-blast |
| bg-33 | animate-bounce-ball-1 opacity-04 |
size-08 animate-spin-bounce |
| bg-34 | animate-bounce-ball-2 opacity-03 |
size-11 animate-flip-bounce |
| bg-35 | animate-bounce-ball-3 opacity-05 |
size-13 animate-scale-bounce |
GoogleButton component (go-backend/internal/web/views/auth_components.templ lines 89–106):
Port structure verbatim; change label text to English per D-G03:
templ GoogleButton(href string, configured bool) {
if configured {
<a class="gsi-material-button" href={ templ.SafeURL(href) }>
<div class="gsi-material-button-state"></div>
<div class="gsi-material-button-content-wrapper">
<div class="gsi-material-button-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" aria-hidden="true">
<path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"></path>
<path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"></path>
<path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"></path>
<path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"></path>
<path fill="none" d="M0 0h48v48H0z"></path>
</svg>
</div>
<span class="gsi-material-button-contents">Sign in with Google</span>
<span class="visually-hidden">Sign in with Google</span>
</div>
</a>
} else {
<button type="button" class="gsi-material-button" disabled aria-disabled="true">
// same inner structure, same label
</button>
}
}
Note: go-backend GoogleButton is a <button> unconditionally. The backend already has the configured/unconfigured split via AuthProviderButtonControl — wire GoogleButton to accept (href string, configured bool) to preserve this logic.
AuthDivider component (go-backend/internal/web/views/auth_components.templ lines 81–87):
templ AuthDivider() {
<div class="divider-row">
<div class="divider-line"></div>
<span class="divider-pill">or</span>
<div class="divider-line"></div>
</div>
}
Change "Ou continuer avec" → "or" per D-G03 English convention.
backend/templates/auth_login.templ (page, request-response)
Analog: backend/templates/auth_login.templ (current — this file is being refactored)
Function signatures stay compatible with handlers (do not change):
templ LoginPage(form LoginForm, errs LoginErrors, csrfToken string, providers AuthProviderButtons) {
@AuthLayout("Sign in to Xtablo", csrfToken) {
<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>Sign in to Xtablo</h1>
</div>
<div class="auth-body">
@GoogleButton(providers.Google.StartURL, providers.Google.Configured)
@AuthDivider()
@LoginFormFragment(form, errs, csrfToken)
</div>
}
}
LoginFormFragment pattern (migrate raw inputs to @ui.FormField + @ui.Input):
Current pattern (lines 28–75 of backend/templates/auth_login.templ) uses raw <input> with Tailwind classes. Replace with:
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"
>
@ui.CSRFField(csrfToken)
@GeneralError(errs.General)
@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",
})
<p class="signup-copy">
Don't have an account?
<a class="signup-link" href="/signup">Sign up</a>
</p>
</form>
}
Key changes from current:
class="space-y-5"on form →class="login-form"(auth.css drives layout)- Raw
<input>elements →@ui.FormField+@ui.Input @FieldError(errs.X)calls removed — errors wired throughFormFieldProps.Error- Submit label: "Sign in" → "Sign in to Xtablo" (copywriting contract)
- Nav link added below submit button inside the form element
- HTMX attributes preserved exactly:
hx-post="/login" hx-target="#login-form" hx-swap="outerHTML" AuthProviderButtonsBlockcomponent replaced byGoogleButton+AuthDividercalled from the page template (not the fragment)
backend/templates/auth_signup.templ (page, request-response)
Analog: backend/templates/auth_login.templ (mirror of login pattern)
Function signature (unchanged for handler compatibility):
templ SignupPage(form SignupForm, errs SignupErrors, csrfToken string, providers AuthProviderButtons) {
@AuthLayout("Create your account", csrfToken) {
<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>
<div class="auth-body">
@GoogleButton(providers.Google.StartURL, providers.Google.Configured)
@AuthDivider()
@SignupFormFragment(form, errs, csrfToken)
</div>
}
}
SignupFormFragment pattern (mirror of login, differences noted):
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"
>
@ui.CSRFField(csrfToken)
@GeneralError(errs.General)
@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: "12 characters minimum", // differs from login
Required: true,
Attrs: templ.Attributes{"autocomplete": "new-password"}, // differs from login
}),
Error: errs.Password,
})
@ui.Button(ui.ButtonProps{
Label: "Create account", // differs from login
Variant: ui.ButtonVariantDefault,
Tone: ui.ButtonToneSolid,
Size: ui.SizeMD,
Type: "submit",
})
<p class="signup-copy">
Already have an account?
<a class="signup-link" href="/login">Sign in</a>
</p>
</form>
}
backend/internal/web/ui/auth.css (stylesheet, replace entirely)
Analog: go-backend/static/styles.css (extract auth-relevant sections only — do NOT import the file)
This file replaces the existing minimal auth.css (62 lines). The new file is a full extraction from go-backend's styles.css. The existing .auth-provider-stack, .auth-provider-button, .auth-provider-separator rules should be kept at the bottom for any non-Google controls.
Section map — what to extract and from where:
| CSS section | go-backend source lines | Notes |
|---|---|---|
.login-screen |
1063–1079 | Two declarations; merge into one |
.background-layer |
1081–1086 | |
.background-logo |
1088–1090 | |
.logo-asset |
1092–1097 | |
.size-06 through .size-20 |
1099–1111 | 13 size utilities |
.opacity-02 through .opacity-05 |
1113–1116 | 4 opacity utilities |
.bg-01 through .bg-35 |
1118–1155 | 35 position rules |
.card-wrap |
1157–1163 | |
.card-glow |
1165–1172 | |
.auth-card-shell |
1174–1182 | padding: 1.25rem ported verbatim |
.auth-card-topbar |
1184–1189 | |
.brand-header |
1237–1241 | |
.brand-logo |
1243–1248 | |
.title-group and .title-group h1 |
1250–1259 | |
.auth-body |
1279–1284 | |
.login-form |
1286–1293 | Keep for HTMX swap compat |
.divider-row |
1356–1363 | |
.divider-line |
1365–1368 | |
.divider-pill |
1370–1379 | |
.signup-copy |
1381–1386 | |
.signup-link |
1388–1402 | Includes hover state |
.status-slot, .status-banner, .status-success, .status-error |
1404–1425 | |
.gsi-material-button and all sub-classes |
1427–1509 | Full block verbatim |
All @keyframes definitions |
2533–2882 | ~350 lines |
All .animate-* utility classes |
2883–2947 | ~65 lines |
Critical CSS values to copy verbatim (go-backend lines 1174–1182):
.auth-card-shell {
backdrop-filter: blur(12px);
background: var(--card);
border: 1px solid var(--border);
border-radius: 1rem;
box-shadow: var(--shadow-auth-card);
padding: 1.25rem; /* NOT a design token — ported as-is */
position: relative;
}
Tokens already defined in base.css (no need to redefine):
--gradient-shell(line 139)--gradient-card-glow(line 143)--shadow-auth-card(line 127)--color-surface-default,--color-border-google,--color-text-google(lines 20, 45, 14)--overlay-google-state(line 126)--shadow-google-button(lines 135–137)--card,--border,--muted-foreground,--foreground,--accent,--background(legacy alias block, lines 161–171)
backend/static/logo_dark.png and backend/static/logo_white.png (static assets, file-copy)
Source: go-backend/static/logo_dark.png and go-backend/static/logo_white.png
Action: Binary file copy. No transformation.
cp go-backend/static/logo_dark.png backend/static/logo_dark.png
cp go-backend/static/logo_white.png backend/static/logo_white.png
AnimatedBackground references /static/logo_dark.png — this path must resolve before templ generate is run and before the server starts. Copy the asset before any template work.
Shared Patterns
templ package declaration
Apply to: auth_layout.templ, auth_components.templ
package templates
All files in backend/templates/ use this package. Components in auth_components.templ are called with @AnimatedBackground(), @AuthDivider(), @GoogleButton(...) from sibling files in the same package — no import needed.
ui package import
Source: backend/templates/auth_login.templ line 3; backend/internal/web/ui/input.templ, form_field.templ
Apply to: auth_login.templ, auth_signup.templ
import "backend/internal/web/ui"
@ui.FormField, @ui.Input, @ui.Button, ui.CSRFField are all called with this import.
CSRF field threading
Source: backend/templates/layout.templ line 41; backend/templates/auth_login.templ line 38
Apply to: auth_layout.templ, auth_login.templ, auth_signup.templ
// AuthLayout receives csrfToken and passes it to children via { children... }
// The page template (LoginPage/SignupPage) forwards it to FormFragment
// The fragment embeds it: @ui.CSRFField(csrfToken)
HTMX swap pattern
Source: backend/templates/auth_login.templ lines 29–36
Apply to: LoginFormFragment, SignupFormFragment (preserve exactly — no changes)
hx-post="/login"
hx-target="#login-form"
hx-swap="outerHTML"
// form id must match hx-target: id="login-form" / id="signup-form"
templ.SafeURL for external hrefs
Source: go-backend/internal/web/views/auth_components.templ line 69
Apply to: GoogleButton (when rendering the <a> variant), nav links
href={ templ.SafeURL(href) }
// Use for any runtime-constructed URL; static strings like "/signup" can be plain href="/signup"
CSS design token usage
Source: backend/internal/web/ui/base.css
Apply to: auth.css (all CSS rules)
- Use
var(--card)for glassmorphism card background (=rgba(255,255,255,0.8)) - Use
var(--border)for card border - Use
var(--gradient-shell)for.login-screenbackground - Use
var(--gradient-card-glow)for.card-glow - Use
var(--shadow-auth-card)for card box-shadow - Use
var(--muted-foreground)for.signup-copy,.divider-pill - Use
var(--foreground)for.signup-linkbase color - Use
var(--accent)for.signup-link:hoverbackground - Use
var(--background)for.divider-pillbackground and input backgrounds - Do NOT hardcode colors that have token equivalents
No Analog Found
No files in this phase are without an analog. All patterns have direct source counterparts.
Metadata
Analog search scope:
backend/templates/— current backend templatesbackend/internal/web/ui/— design system components and CSSgo-backend/internal/web/views/— reference implementation templatesgo-backend/static/styles.css— reference CSS source
Files scanned: 8 source files read in full or in targeted sections Pattern extraction date: 2026-05-16
Implementation Order
Execute in this order to avoid broken references:
- Copy static assets (
logo_dark.png,logo_white.png) — required before template renders - Write
auth_components.templ—AnimatedBackground,GoogleButton,AuthDividercomponents - Write
auth_layout.templ— calls@AnimatedBackground()from step 2 - Replace
auth.css— all CSS classes must exist before browser renders - Update
auth_login.templ— switches to@AuthLayout, migrates inputs - Update
auth_signup.templ— same changes as login