docs(01): UI design contract for foundation phase

Lock base layout, spacing/typography/color tokens, and the canonical
HTMX interaction pattern that later phases inherit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arthur Belleville 2026-05-14 16:53:08 +02:00
parent 5f63929477
commit 5a14166583
No known key found for this signature in database

View file

@ -0,0 +1,298 @@
---
phase: 1
slug: foundation
status: draft
shadcn_initialized: false
preset: none
created: 2026-05-14
---
# Phase 1 — UI Design Contract
> Visual and interaction contract for the Foundation phase. This phase ships a Walking Skeleton: a single Tailwind-styled root page rendered by templ with one working `hx-get` demo. The tokens declared here are load-bearing for every subsequent phase. shadcn is not applicable — the stack is Go templ + HTMX + Tailwind v4 standalone, with no Node/JS toolchain in `backend/`.
---
## Design System
| Property | Value |
|----------|-------|
| Tool | none (no shadcn — Go/templ/HTMX stack, no React) |
| Preset | not applicable |
| Component library | none (raw templ components + Tailwind utilities) |
| Icon library | none in Phase 1 (deferred — inline SVG when needed in Phase 3+) |
| Font | System font stack: `ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif` (Tailwind default `font-sans`) |
**Rationale:** Phase 1 is a Walking Skeleton. Pulling in a webfont or icon library before there is any UI to motivate it adds bytes and bootstrap complexity for zero value. System fonts render instantly with no FOUT and align with the "single binary + static" thesis from CONTEXT D-10 / D-12.
**Static assets (vendored, served from `/static/`, no CDN — per CONTEXT D-10):**
- `htmx.min.js` (HTMX v2.x)
- `tailwind.css` (compiled by Tailwind standalone CLI v4.x)
---
## Spacing Scale
Declared values (multiples of 4, mapped 1:1 to Tailwind defaults — no custom spacing tokens in Phase 1):
| Token | Value | Tailwind utility | Usage |
|-------|-------|------------------|-------|
| xs | 4px | `*-1` | Inline icon gaps, tight inline padding |
| sm | 8px | `*-2` | Compact element gaps, button inner padding-y |
| md | 16px | `*-4` | Default element spacing, button inner padding-x, form field gaps |
| lg | 24px | `*-6` | Section internal padding, card padding |
| xl | 32px | `*-8` | Layout gaps between sections, page header padding-y |
| 2xl | 48px | `*-12` | Major section breaks |
| 3xl | 64px | `*-16` | Page-level vertical rhythm, hero spacing |
**Container:** Page content is constrained to `max-w-5xl` (1024px) and centered with `mx-auto`. Horizontal page padding: `px-4` on mobile (<640px), `px-6` from `sm:` up.
**Exceptions:** none. Phase 1 has no touch-target-only elements; the `hx-get` demo button uses `md`/`sm` padding which yields >40px height.
---
## Typography
Three sizes, two weights — enforced through Tailwind utility classes.
| Role | Size | Tailwind | Weight | Line Height |
|------|------|----------|--------|-------------|
| Body | 16px | `text-base` | 400 (`font-normal`) | 1.5 (`leading-normal`) |
| Label / small | 14px | `text-sm` | 400 (`font-normal`) | 1.5 (`leading-normal`) |
| Heading (H2 / section) | 20px | `text-xl` | 600 (`font-semibold`) | 1.3 (`leading-snug`) |
| Display (H1 / page title) | 28px | `text-3xl` adjusted via `text-[28px]` or default `text-2xl` (24px) acceptable | 600 (`font-semibold`) | 1.2 (`leading-tight`) |
**Rule:** Only weights 400 and 600 may appear in Phase 1 markup. No 500, no 700.
**Rule:** Only the four sizes above may appear. No `text-lg`, `text-2xl` etc. unless they match the declared px value above.
**Heading element mapping:**
- Page title (root page `/`): `<h1>` styled with the Display row.
- Demo section heading: `<h2>` styled with the Heading row.
- Body copy and the timestamp fragment: Body row.
- The `hx-get` button label: Body row, weight 600 allowed for button text only.
---
## Color
Single palette, light-mode only in Phase 1. Dark mode is deferred (no requirement from FOUND-01..05). All values are Tailwind v4 default palette references to keep the contract checkable from CSS alone.
| Role | Value | Tailwind class | Usage |
|------|-------|---------------|-------|
| Dominant (60%) | `#ffffff` | `bg-white` | Page background, content surface |
| Secondary (30%) | `#f8fafc` (slate-50) | `bg-slate-50` | Header strip, demo card background, any panel that needs visual separation from the page |
| Foreground primary | `#0f172a` (slate-900) | `text-slate-900` | All headings and body text |
| Foreground muted | `#475569` (slate-600) | `text-slate-600` | Secondary copy, helper text, the timestamp fragment when idle |
| Border | `#e2e8f0` (slate-200) | `border-slate-200` | Card borders, divider lines |
| Accent (10%) | `#2563eb` (blue-600) | `bg-blue-600`, `text-blue-600`, `hover:bg-blue-700` | **Reserved for: the primary `hx-get` demo button only.** Hover state: `blue-700`. Focus ring: `ring-2 ring-blue-600/40 ring-offset-2`. |
| Accent foreground | `#ffffff` | `text-white` | Text on accent backgrounds (button label) |
| Destructive | `#dc2626` (red-600) | `text-red-600`, `bg-red-600` | Not used in Phase 1 (no destructive actions exist). Declared here so Phase 3+ inherits the token. |
| Success indicator | `#16a34a` (green-600) | `text-green-600` | `/healthz` "ok" badge if surfaced visually. Not strictly required for Phase 1 but reserved. |
**Accent reserved for:** the `hx-get` demo button on the root page. Nothing else in Phase 1 may use `blue-*`. No links, no headings, no underlines in the brand blue.
**60/30/10 split:** ~60% white surface, ~30% slate-50 strip/card, ~10% accent confined to the single primary button.
---
## Copywriting Contract
| Element | Copy |
|---------|------|
| Page `<title>` | `Xtablo — Foundation` |
| Page H1 | `Xtablo` |
| Page subtitle (under H1, muted) | `Go + HTMX foundation. Sign-in and the Tablos workflow ship in later phases.` |
| Demo section H2 | `HTMX demo` |
| Demo helper text (above button) | `Click the button to fetch the server time as an HTML fragment.` |
| Primary CTA (the only button) | `Fetch server time` |
| Initial fragment placeholder (`#demo-out` empty state) | `No time fetched yet.` (rendered in `text-slate-600`) |
| Fragment success payload | `<span>{ISO-8601 UTC timestamp}</span>` — e.g. `2026-05-14T14:42:38Z` |
| HTMX failure copy (server 5xx, rendered via `hx-target-error` or fallback) | `Could not reach the server. Refresh the page and try again.` |
| `/healthz` JSON success | `{"status":"ok","db":"ok"}` (locked in CONTEXT D-20) |
| `/healthz` JSON degraded | `{"status":"degraded","db":"down"}` (locked in CONTEXT D-20) |
| Footer (small, muted, optional) | `Phase 1 · Walking skeleton` |
**Destructive actions:** none in Phase 1. The contract row exists for downstream phases but is intentionally empty here.
**Tone rules:**
- Friendly, declarative, no marketing voice.
- No emoji.
- No exclamation marks.
- Sentence case for headings (not Title Case).
- Errors describe the problem and the next action ("Refresh the page and try again."), never just "Error".
---
## Base Layout Contract
The base templ layout (`templates/layout.templ`) is the foundation every later phase extends. It must contain exactly the following structural regions:
```
<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 class="min-h-screen bg-white text-slate-900 antialiased">
<header class="bg-slate-50 border-b border-slate-200">
<div class="mx-auto max-w-5xl px-4 sm:px-6 py-4">
<!-- brand slot: <h1> on the root page; smaller mark on inner pages (later phases) -->
</div>
</header>
<main class="mx-auto max-w-5xl px-4 sm:px-6 py-8">
{ children... }
</main>
<footer class="mx-auto max-w-5xl px-4 sm:px-6 py-6 text-sm text-slate-600">
Phase 1 · Walking skeleton
</footer>
<script src="/static/htmx.min.js" defer></script>
</body>
</html>
```
**Rules:**
- `htmx.min.js` is loaded once, at the bottom of `<body>`, with `defer`.
- `tailwind.css` is loaded in `<head>` so there is no FOUC.
- No inline `<style>` blocks. No inline `style="..."` attributes.
- No CDN references for any asset.
- The `<main>` container width (`max-w-5xl`) and horizontal padding (`px-4 sm:px-6`) is the single content frame for the entire product. Later phases must not reinvent this container.
---
## HTMX Interaction Pattern
The `hx-get` demo on the root page is the canonical interaction pattern. **Every future HTMX call in the product must follow this shape** unless a specific later phase contract overrides it.
| Concern | Contract |
|---------|----------|
| Trigger element | `<button>` (or `<a>` for navigation, never `<div hx-get>`) |
| Required attributes | `hx-get` (or `hx-post`), `hx-target`, `hx-swap` |
| `hx-target` value | A CSS `#id` selector pointing to a stable element in the current document |
| `hx-swap` value | `innerHTML` is the default. Use `outerHTML` only when the target itself is being replaced. |
| Indicator | `hx-indicator="#demo-spinner"` references a `<span class="htmx-indicator">…</span>` styled with `text-slate-600`. Spinner content is the text `Loading…` in Phase 1 (no SVG spinner). |
| Disabled state during request | The triggering button receives the class `htmx-request:opacity-60 htmx-request:pointer-events-none` via Tailwind variant. |
| Error response | Server returns 4xx/5xx with a small HTML fragment containing the failure copy declared above. HTMX swaps it into the same target. |
| URL convention | `/{resource}/{action}` (e.g. `/demo/time`). Fragments live under the same path tree as the page that requests them. No `/api` prefix in Phase 1 — HTMX endpoints are first-class routes. |
**Concrete demo markup (canonical):**
```html
<section class="rounded-lg border border-slate-200 bg-slate-50 p-6">
<h2 class="text-xl font-semibold leading-snug">HTMX demo</h2>
<p class="mt-2 text-base text-slate-600">
Click the button to fetch the server time as an HTML fragment.
</p>
<div class="mt-4 flex items-center gap-4">
<button
type="button"
hx-get="/demo/time"
hx-target="#demo-out"
hx-swap="innerHTML"
hx-indicator="#demo-spinner"
class="inline-flex items-center rounded-md bg-blue-600 px-4 py-2
text-base font-semibold text-white
hover:bg-blue-700
focus:outline-none focus:ring-2 focus:ring-blue-600/40 focus:ring-offset-2
htmx-request:opacity-60 htmx-request:pointer-events-none">
Fetch server time
</button>
<span id="demo-spinner" class="htmx-indicator text-sm text-slate-600">Loading…</span>
</div>
<div id="demo-out" class="mt-4 text-base text-slate-600">
No time fetched yet.
</div>
</section>
```
**Fragment response (from `GET /demo/time`):**
```html
<span class="text-slate-900">2026-05-14T14:42:38Z</span>
```
`Content-Type: text/html; charset=utf-8`. No JSON. No envelope.
---
## State Inventory (Phase 1)
Each interactive surface declares its states explicitly. Phase 1 has exactly one interactive surface.
| Surface | States |
|---------|--------|
| `Fetch server time` button | default · hover · focus · in-flight (`htmx-request`) |
| `#demo-out` fragment target | empty (`No time fetched yet.`) · success (timestamp `<span>`) · error (failure copy) |
| `/healthz` (non-UI) | ok (200 JSON) · degraded (503 JSON) |
No loading skeletons. No empty-state illustrations. No toasts. These are intentionally out of scope for the Walking Skeleton and will be specified per-phase from Phase 3 onward.
---
## Accessibility Floor
Minimum bar Phase 1 must meet. Later phases inherit and extend.
- All interactive elements reachable by keyboard (Tab order follows DOM order).
- Focus rings visible on the demo button (`focus:ring-2 focus:ring-blue-600/40 focus:ring-offset-2`). No `outline-none` without a replacement ring.
- Color contrast: `text-slate-900` on `bg-white` and `bg-slate-50` ≥ 7:1 (AAA body). `text-white` on `bg-blue-600` ≥ 4.5:1 (AA large). `text-slate-600` on `bg-white` ≥ 4.5:1.
- `<html lang="en">` declared.
- HTMX-swapped fragments do not require ARIA live regions in Phase 1 (single, user-initiated interaction). This becomes a requirement when async/auto-loading is introduced.
- No motion or auto-playing animation in Phase 1, so `prefers-reduced-motion` is implicitly honored.
---
## Registry Safety
| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| shadcn official | none | not applicable (no shadcn; stack is Go/templ/HTMX/Tailwind) |
| third-party | none | not applicable |
No registry-installed components in Phase 1. All markup is hand-authored templ + Tailwind utility classes.
---
## Tailwind Configuration Contract
Phase 1 commits to Tailwind v4 standalone CLI (CONTEXT D-12). The input CSS file pins the content sources so JIT does not purge classes that only appear in `.templ` files (Research Pitfall 3).
**`tailwind.input.css` (canonical):**
```css
@import "tailwindcss";
@source "../templates/**/*.templ";
@source "../internal/web/**/*.go";
```
No `tailwind.config.js`. No custom theme extensions in Phase 1 — every token in this contract is reachable through Tailwind defaults. Custom tokens land only when a later phase has a justified need.
---
## What This Contract Deliberately Does Not Cover
- Dark mode (deferred — no FOUND requirement)
- Icons (deferred — first need lands in Phase 3 tablo list)
- Form components (deferred — first form is the login form in Phase 2)
- Modal/dialog patterns (deferred — first modal is "create tablo" in Phase 3)
- Toasts / flash messages (deferred — first need lands in Phase 2 for login failures)
- Empty-state illustrations (deferred — first list-empty state lands in Phase 3)
- Kanban board layout (Phase 4)
- File upload UI (Phase 5)
Each later phase's UI-SPEC will extend (never override) the tokens declared here.
---
## 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