docs(13): UI design contract for design system foundation phase
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ab64a73b1d
commit
a0c21b0582
1 changed files with 660 additions and 0 deletions
660
.planning/phases/13-design-system-foundation/13-UI-SPEC.md
Normal file
660
.planning/phases/13-design-system-foundation/13-UI-SPEC.md
Normal file
|
|
@ -0,0 +1,660 @@
|
|||
---
|
||||
phase: 13
|
||||
slug: design-system-foundation
|
||||
status: draft
|
||||
shadcn_initialized: false
|
||||
preset: none
|
||||
created: 2026-05-16
|
||||
---
|
||||
|
||||
# Phase 13 — UI Design Contract
|
||||
|
||||
> Visual and interaction contract for the Design System Foundation phase.
|
||||
> Generated by gsd-ui-researcher, verified by gsd-ui-checker.
|
||||
>
|
||||
> **Stack note:** This is a Go + HTMX project. There is no React, no shadcn, no
|
||||
> node-based component library. The design system is implemented as:
|
||||
> - CSS custom properties in `backend/internal/web/ui/base.css`
|
||||
> - Component CSS files under `backend/internal/web/ui/`
|
||||
> - Go templ components consumed by HTMX-driven server-rendered pages
|
||||
> - Tailwind v4 utility classes alongside component CSS (no @apply)
|
||||
|
||||
---
|
||||
|
||||
## Design System
|
||||
|
||||
| Property | Value | Source |
|
||||
|----------|-------|--------|
|
||||
| Tool | none (Go + templ, not shadcn) | Project constraint |
|
||||
| Preset | not applicable | n/a |
|
||||
| Component library | Go templ components in `backend/internal/web/ui/` | Existing codebase |
|
||||
| Icon library | Inline SVG via `UIIcon(name string)` templ helper | go-backend pattern |
|
||||
| Font | `ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif` | go-backend `base.css` |
|
||||
|
||||
**Registry safety gate:** not applicable (no npm registry consumption).
|
||||
|
||||
---
|
||||
|
||||
## Token Vocabulary
|
||||
|
||||
All tokens are CSS custom properties declared in `backend/internal/web/ui/base.css`.
|
||||
Phase 13 ports the full 223-line vocabulary from `go-backend/internal/web/ui/base.css` verbatim.
|
||||
Decision D-T01 and D-T02 lock this: no adjustments before Phase 13 lands.
|
||||
|
||||
### Color Tokens (key values — from go-backend base.css)
|
||||
|
||||
| Group | Token | Value |
|
||||
|-------|-------|-------|
|
||||
| Brand | `--color-brand-primary` | `#804eec` |
|
||||
| Brand | `--color-brand-primary-hover` | `#6d28d9` |
|
||||
| Brand | `--color-brand-primary-active` | `#5b21b6` |
|
||||
| Brand | `--color-brand-secondary` | `#a855f7` |
|
||||
| Brand | `--color-brand-accent` | `#3b82f6` |
|
||||
| Brand | `--color-brand-ink` | `#1e1b2e` |
|
||||
| Text | `--color-text-primary` | `hsl(0 0% 9%)` |
|
||||
| Text | `--color-text-secondary` | `#475467` |
|
||||
| Text | `--color-text-muted` | `hsl(0 0% 43.5%)` |
|
||||
| Text | `--color-text-faint` | `#9ca3af` |
|
||||
| Text | `--color-text-inverse` | `#ffffff` |
|
||||
| Text | `--color-text-brand` | `#804eec` |
|
||||
| Text | `--color-text-disabled` | `#667085` |
|
||||
| Surface | `--color-surface-page` | `hsl(0 0% 100%)` |
|
||||
| Surface | `--color-surface-default` | `#ffffff` |
|
||||
| Surface | `--color-surface-card` | `rgba(255,255,255,0.8)` |
|
||||
| Surface | `--color-surface-subtle` | `hsl(0 0% 96.1%)` |
|
||||
| Surface | `--color-surface-muted` | `#f3f4f6` |
|
||||
| Surface | `--color-surface-brand-soft` | `#ede9fe` |
|
||||
| Border | `--color-border-default` | `hsl(0 0% 90.9%)` |
|
||||
| Border | `--color-border-strong` | `#d1d5db` |
|
||||
| Border | `--color-border-subtle` | `#d0d5dd` |
|
||||
| Focus | `--color-focus-ring` | `rgba(124,58,237,0.2)` |
|
||||
| Focus | `--color-focus-ring-strong` | `rgba(139,92,246,0.16)` |
|
||||
| Danger | `--color-status-danger-strong` | `#dc2626` |
|
||||
| Danger | `--color-status-danger-foreground` | `#dc2626` |
|
||||
| Success | `--color-status-success-strong` | `#16a34a` |
|
||||
| Warning | `--color-status-warning-strong` | `#db9729` |
|
||||
|
||||
Full vocabulary: 223 custom properties — see `go-backend/internal/web/ui/base.css` for the complete list. Port verbatim; no value changes in Phase 13.
|
||||
|
||||
---
|
||||
|
||||
## Spacing Scale
|
||||
|
||||
Phase 13 defines spacing both as CSS custom-property-backed SpacingStep utility components and
|
||||
as raw CSS values used inside component rules. All values are as declared in `go-backend/internal/web/ui/spacing.css`.
|
||||
|
||||
| Token | CSS Value | rem | SpacingStep const |
|
||||
|-------|-----------|-----|-------------------|
|
||||
| xs | 4px | 0.25rem | `SpacingStepXS` |
|
||||
| sm | 8px | 0.5rem | `SpacingStepSM` |
|
||||
| md | 12px | 0.75rem | `SpacingStepMD` |
|
||||
| lg | 16px | 1rem | `SpacingStepLG` |
|
||||
| xl | 24px | 1.5rem | `SpacingStepXL` |
|
||||
|
||||
**Note:** The spacing component (`SpaceX` / `SpaceY`) uses this non-standard scale (xs=4, md=12, xl=24)
|
||||
because it is a port of the go-backend spacing utility. This scale serves the spacing *component* only.
|
||||
Layout-level spacing in the catalog fake-shell uses Tailwind utilities directly.
|
||||
|
||||
Touch target exceptions: buttons and icon-buttons enforce `min-height: 44px` and icon-buttons enforce
|
||||
`min-width: 44px` for WCAG 2.5.5 compliance. These override the spacing scale where needed.
|
||||
|
||||
---
|
||||
|
||||
## Typography
|
||||
|
||||
Sourced from `go-backend/internal/web/ui/base.css` body declaration and component CSS measurements.
|
||||
|
||||
| Role | Size | Weight | Line Height | Token / Usage |
|
||||
|------|------|--------|-------------|---------------|
|
||||
| Body | 16px (1rem) | 400 regular | 1.5 | Default prose, descriptions |
|
||||
| Label / form | 15.2px (0.95rem) | 600 semibold | 1.4 | Form labels, button labels |
|
||||
| Small / hint | 14px (0.875rem) | 400 regular | 1.4 | Form hints, error messages, badge text, table headers |
|
||||
| Heading | 18px (1.125rem) | 700 bold | 1.2 | Modal titles, empty-state titles, section headings |
|
||||
|
||||
**Font stack (declared in base.css body rule):**
|
||||
```
|
||||
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif
|
||||
```
|
||||
|
||||
No custom web font is introduced in Phase 13. The system stack is the final choice.
|
||||
|
||||
---
|
||||
|
||||
## Color Contract
|
||||
|
||||
60/30/10 split expressed in terms of the token vocabulary:
|
||||
|
||||
| Role | Token / Value | Usage |
|
||||
|------|---------------|-------|
|
||||
| Dominant (60%) | `--color-surface-page` (#ffffff) | Page background, form field backgrounds |
|
||||
| Secondary (30%) | `--color-surface-muted` (#f3f4f6) | Card backgrounds, icon-button hover states, empty-state icon container, select chips |
|
||||
| Accent (10%) | `--color-brand-primary` (#804eec) | Primary button background, focus rings, brand text, selected option highlight |
|
||||
| Destructive | `--color-status-danger-strong` (#dc2626) | Danger button variant only |
|
||||
|
||||
**Accent reserved for:**
|
||||
- Solid default button (`ui-button-solid-default-*`)
|
||||
- Focus ring on all interactive elements (`--color-focus-ring`)
|
||||
- Brand text (`--color-text-brand`)
|
||||
- Soft default button background (`--color-surface-brand-soft`)
|
||||
|
||||
**Accent is NOT used for:** neutral buttons, card borders, table rows, form field borders at rest, badge backgrounds (badges use status-color tokens).
|
||||
|
||||
---
|
||||
|
||||
## Component Inventory
|
||||
|
||||
All 11 component types must be present as both a templ component and a CSS file.
|
||||
The catalog page must render every variant listed here.
|
||||
|
||||
### Button (DS-02)
|
||||
|
||||
Class pattern: `ui-button ui-button-{tone}-{variant}-{size}`
|
||||
|
||||
| Axis | Values | CSS classes |
|
||||
|------|--------|-------------|
|
||||
| Variant | default, neutral, warning, success, danger, **ghost** (add) | `ui-button-default`, `ui-button-neutral`, `ui-button-warning`, `ui-button-success`, `ui-button-danger`, `ui-button-ghost` |
|
||||
| Tone | solid, soft | `ui-button-solid`, `ui-button-soft` |
|
||||
| Size | sm, md, lg | `ui-button-sm`, `ui-button-md`, `ui-button-lg` |
|
||||
|
||||
**Ghost variant:** `ButtonVariantGhost` added to `variants.go`. Class combo `ui-button-ghost` renders with transparent background + brand-colored text, hover shows `--color-surface-brand-soft`. CSS mirrors go-backend ghost rules.
|
||||
|
||||
Props struct (go-backend parity):
|
||||
```go
|
||||
type ButtonProps struct {
|
||||
Label string
|
||||
Variant ButtonVariant
|
||||
Tone ButtonTone
|
||||
Size Size
|
||||
Type string
|
||||
Icon string // icon name for UIIcon helper
|
||||
Disabled bool // add if missing in backend
|
||||
Attrs templ.Attributes
|
||||
}
|
||||
```
|
||||
|
||||
Interaction states: default → hover (background shift) → active (darker) → focus-visible (3px focus ring `--color-focus-ring`) → disabled (opacity 0.5, cursor not-allowed).
|
||||
|
||||
### Badge (DS-05)
|
||||
|
||||
Class pattern: `ui-badge ui-badge-{variant}`
|
||||
|
||||
| Variant | Background token | Border token | Text token |
|
||||
|---------|-----------------|--------------|------------|
|
||||
| info | `--color-status-info-soft-bg` | `--color-status-info-soft-border` | `--color-status-info-foreground` |
|
||||
| warning | `--color-status-warning-soft-bg` | `--color-status-warning-soft-border` | `--color-status-warning-foreground` |
|
||||
| success | `--color-status-success-soft-bg` | `--color-status-success-soft-border` | `--color-status-success-foreground` |
|
||||
| danger | `--color-status-danger-soft-bg` | `--color-status-danger-soft-border` | `--color-status-danger-foreground` |
|
||||
| **primary** (add) | `--color-surface-brand-soft` | `--color-brand-primary` at 30% opacity | `--color-text-brand` |
|
||||
|
||||
**Primary variant:** `BadgeVariantPrimary` added to `variants.go`. Uses brand-purple tones to signal "active" or "primary" status (distinct from info blue).
|
||||
|
||||
Props struct (go-backend parity):
|
||||
```go
|
||||
type BadgeProps struct {
|
||||
Label string
|
||||
Variant BadgeVariant
|
||||
}
|
||||
```
|
||||
|
||||
### Card (DS-04)
|
||||
|
||||
Class: `ui-card` wrapping `ui-card-header`, `ui-card-body`, `ui-card-footer`
|
||||
|
||||
- Border radius: 1rem
|
||||
- Border: 1px solid `--color-border-default`
|
||||
- Shadow: `--shadow-surface-md`
|
||||
- Section padding: `1.25rem 1.5rem`
|
||||
- Header/footer have a 1px divider at `--color-border-default`
|
||||
|
||||
Props struct (go-backend parity):
|
||||
```go
|
||||
type CardProps struct {
|
||||
Attrs templ.Attributes
|
||||
}
|
||||
```
|
||||
Header, Body, Footer are separate templ components taking `templ.Component` children.
|
||||
|
||||
### Input (DS-03)
|
||||
|
||||
Class: `ui-input`
|
||||
|
||||
- Border radius: 0.75rem
|
||||
- Min-height: 44px (touch target)
|
||||
- Width: 100%
|
||||
- Placeholder color: `--color-text-faint`
|
||||
- Focus: border-color `--color-brand-focus`, box-shadow `0 0 0 3px --color-focus-ring-strong`, outline none
|
||||
|
||||
Props struct:
|
||||
```go
|
||||
type InputProps struct {
|
||||
Name string
|
||||
ID string
|
||||
Type string // defaults to "text"
|
||||
Placeholder string
|
||||
Value string
|
||||
Disabled bool
|
||||
Required bool
|
||||
Attrs templ.Attributes
|
||||
}
|
||||
```
|
||||
|
||||
### Textarea (DS-03)
|
||||
|
||||
Class: `ui-textarea`
|
||||
|
||||
- Border radius: 0.75rem
|
||||
- Min-height: 7rem
|
||||
- Width: 100%, resize: vertical
|
||||
- Same placeholder and focus behavior as input
|
||||
|
||||
Props struct:
|
||||
```go
|
||||
type TextareaProps struct {
|
||||
Name string
|
||||
ID string
|
||||
Placeholder string
|
||||
Value string
|
||||
Rows int
|
||||
Disabled bool
|
||||
Required bool
|
||||
Attrs templ.Attributes
|
||||
}
|
||||
```
|
||||
|
||||
### Select (DS-03)
|
||||
|
||||
Class: `ui-select` wrapping `ui-select-control`, `ui-select-menu`, `ui-select-option`
|
||||
|
||||
- Control: min-height 44px, border-radius 0.75rem, chevron arrow that rotates 180deg when `is-open`
|
||||
- Menu: absolute positioned, max-height 16rem, overflow-y auto, border-radius 0.9rem
|
||||
- Option selected state: `is-selected` class → `--color-status-info-soft-bg` background + `--color-text-brand` text
|
||||
|
||||
Props struct:
|
||||
```go
|
||||
type SelectOption struct {
|
||||
Value string
|
||||
Label string
|
||||
Selected bool
|
||||
Disabled bool
|
||||
}
|
||||
|
||||
type SelectProps struct {
|
||||
Name string
|
||||
ID string
|
||||
Options []SelectOption
|
||||
Disabled bool
|
||||
Attrs templ.Attributes
|
||||
}
|
||||
```
|
||||
|
||||
### Modal (DS-06)
|
||||
|
||||
Class: `ui-modal-backdrop` → `ui-modal-panel` → `ui-modal-header` / `ui-modal-body` / `ui-modal-actions`
|
||||
|
||||
- Backdrop: `position: fixed`, `inset: 0`, `background: --overlay-backdrop-default`, flex center
|
||||
- Panel: max-width 32rem, border-radius 1rem, shadow `--shadow-surface-lg`
|
||||
- Header: `font-size: 1.125rem`, `font-weight: 700`, bottom border divider
|
||||
- Actions: flex row, justify end, gap 0.75rem, top border divider
|
||||
- HTMX pattern: backdrop rendered server-side into `#modal-slot`; close button sets `hx-delete` or swaps empty fragment
|
||||
|
||||
Props struct:
|
||||
```go
|
||||
type ModalProps struct {
|
||||
Title string
|
||||
Attrs templ.Attributes
|
||||
}
|
||||
```
|
||||
Body and Actions are passed as `templ.Component` children.
|
||||
|
||||
### Empty State (DS-07)
|
||||
|
||||
Class: `ui-empty-state` wrapping `ui-empty-state-icon`, `ui-empty-state-title`, `ui-empty-state-description`
|
||||
|
||||
- Border: 1px dashed `--color-border-subtle`
|
||||
- Border radius: 1rem
|
||||
- Icon container: 4rem × 4rem circle, `--color-surface-muted` background, `--color-text-faint` icon
|
||||
- Icon SVG: 2rem × 2rem
|
||||
- Title: `font-size: 1.125rem`, `font-weight: 700`
|
||||
- Description: max-width 32rem, `--color-text-muted`
|
||||
|
||||
Props struct:
|
||||
```go
|
||||
type EmptyStateProps struct {
|
||||
Icon string
|
||||
Title string
|
||||
Description string
|
||||
Attrs templ.Attributes
|
||||
}
|
||||
```
|
||||
CTA button (optional) passed as `templ.Component` child.
|
||||
|
||||
### Table (DS-08)
|
||||
|
||||
Class: `ui-table-shell` → `ui-table`
|
||||
|
||||
- Shell: `overflow-x: auto`, `width: 100%`
|
||||
- Table: `border-collapse: collapse`, `min-width: 100%`
|
||||
- Header cells: 14px, weight 600, `--color-text-secondary`, uppercase tracking (Tailwind utilities)
|
||||
- Body rows: 16px, `--color-text-primary`, hover `--color-surface-subtle`
|
||||
- Borders: 1px `--color-border-default` on row bottoms
|
||||
|
||||
Props struct:
|
||||
```go
|
||||
type TableProps struct {
|
||||
Attrs templ.Attributes
|
||||
}
|
||||
```
|
||||
Head, Body, Row, Cell, HeaderCell are separate templ sub-components.
|
||||
|
||||
### Icon Button (DS-09)
|
||||
|
||||
Class patterns:
|
||||
- Solid: `ui-icon-button ui-icon-button-solid ui-icon-button-{variant}`
|
||||
- Ghost: `borderless-icon-button ui-icon-button-ghost ui-icon-button-{variant}`
|
||||
|
||||
- Min-height: 44px, min-width: 44px (touch targets)
|
||||
- Variants: neutral, warning, success, danger
|
||||
- Tones: solid, ghost
|
||||
- Ghost: transparent background, no border, SVG 1rem × 1rem
|
||||
|
||||
Props struct:
|
||||
```go
|
||||
type IconButtonProps struct {
|
||||
Icon string
|
||||
Variant IconButtonVariant
|
||||
Tone IconButtonTone
|
||||
Type string
|
||||
Label string // aria-label
|
||||
Attrs templ.Attributes
|
||||
}
|
||||
```
|
||||
|
||||
### Form Field (DS-03 wrapper)
|
||||
|
||||
Class: `ui-form-field` wrapping `ui-form-label`, input/textarea/select, `ui-form-hint`, `ui-form-error`
|
||||
|
||||
- Label: `font-size: 0.95rem`, `font-weight: 600`, `--color-text-primary`
|
||||
- Hint: `font-size: 0.875rem`, `--color-text-muted`
|
||||
- Error: `font-size: 0.875rem`, `--color-status-danger-foreground`
|
||||
- Grid layout with `gap: 0.5rem`
|
||||
|
||||
Props struct:
|
||||
```go
|
||||
type FormFieldProps struct {
|
||||
Label string
|
||||
Hint string
|
||||
Error string
|
||||
Required bool
|
||||
ForID string
|
||||
}
|
||||
```
|
||||
Input/textarea/select passed as `templ.Component` child.
|
||||
|
||||
---
|
||||
|
||||
## Spacing Component (utility — not layout)
|
||||
|
||||
The `SpaceX` / `SpaceY` templ components render sized whitespace blocks for vertical/horizontal rhythm.
|
||||
They are distinct from the CSS spacing scale used in layout.
|
||||
|
||||
| Step | Width/Height |
|
||||
|------|-------------|
|
||||
| xs | 0.25rem (4px) |
|
||||
| sm | 0.5rem (8px) |
|
||||
| md | 0.75rem (12px) |
|
||||
| lg | 1rem (16px) |
|
||||
| xl | 1.5rem (24px) |
|
||||
|
||||
---
|
||||
|
||||
## Catalog Page Contract
|
||||
|
||||
**Route:** `GET /ui-catalog`
|
||||
**Build gate:** `//go:build catalog` — compiled only with `-tags catalog`
|
||||
**Template location:** `backend/internal/web/ui/catalog/catalog.templ`
|
||||
|
||||
### Layout
|
||||
|
||||
```
|
||||
+--sidebar (240px, fixed)--+--main content (fluid)--+
|
||||
| [Brand logo] | [Section heading] |
|
||||
| #buttons | [Component grid] |
|
||||
| #badges | [templ call annotation] |
|
||||
| #cards | ...repeated per section |
|
||||
| #inputs | |
|
||||
| #textarea | |
|
||||
| #select | |
|
||||
| #modal | |
|
||||
| #empty-state | |
|
||||
| #table | |
|
||||
| #icon-button | |
|
||||
| #form-field | |
|
||||
+--------------------------+-------------------------+
|
||||
```
|
||||
|
||||
The fake shell uses hand-written Tailwind utility classes (flex, min-h-screen, w-60, etc.) — no import of the real `app.css` which is deferred to Phases 14–17.
|
||||
|
||||
### Section Structure
|
||||
|
||||
Each section follows this pattern:
|
||||
|
||||
```
|
||||
## Button — DS-02
|
||||
|
||||
[Rendered variant grid]
|
||||
|
||||
Each cell:
|
||||
[Rendered component]
|
||||
─────────────────────
|
||||
ui.Button(ui.ButtonProps{
|
||||
Label: "Save",
|
||||
Variant: ui.ButtonVariantDefault,
|
||||
Tone: ui.ButtonToneSolid,
|
||||
Size: ui.SizeMD,
|
||||
})
|
||||
```
|
||||
|
||||
Section order in sidebar: alphabetical by component name (badge, button, card, empty-state, form-field, icon-button, input, modal, select, table, textarea).
|
||||
|
||||
Each section header includes the DS requirement number: `Button — DS-02`.
|
||||
|
||||
### Static only
|
||||
|
||||
No JavaScript on the catalog page. All variants pre-rendered at page load. Modal section renders an open modal panel (without the backdrop toggle) so all states are visible without interaction.
|
||||
|
||||
---
|
||||
|
||||
## Copywriting Contract
|
||||
|
||||
This phase is infrastructure (CSS tokens + component library), not a user-facing feature.
|
||||
The only user-visible surface is the dev-only catalog page.
|
||||
|
||||
| Element | Copy |
|
||||
|---------|------|
|
||||
| Catalog page title | `Component Catalog` |
|
||||
| Catalog nav heading | `Components` |
|
||||
| Each section subheading | `{ComponentName} — {DS-req}` (e.g. `Button — DS-02`) |
|
||||
| Empty state demo title | `Nothing here yet` |
|
||||
| Empty state demo body | `Add your first item to get started.` |
|
||||
| Empty state demo CTA | `Add item` |
|
||||
| Table demo empty row | `No data to display` |
|
||||
| Form error demo | `This field is required.` |
|
||||
| Form hint demo | `Enter a value between 1 and 100.` |
|
||||
| Modal demo title | `Confirm action` |
|
||||
|
||||
---
|
||||
|
||||
## Go-Side API Contract
|
||||
|
||||
### Variants to add in `backend/internal/web/ui/variants.go`
|
||||
|
||||
```go
|
||||
// Add to ButtonVariant enum:
|
||||
ButtonVariantGhost ButtonVariant = "ghost"
|
||||
|
||||
// Add new BadgeVariantPrimary:
|
||||
BadgeVariantPrimary BadgeVariant = "primary"
|
||||
```
|
||||
|
||||
`NormalizedButtonVariant` and `NormalizedBadgeVariant` must include the new cases.
|
||||
|
||||
### Class generation functions to add/update
|
||||
|
||||
```go
|
||||
// ButtonClass already exists — update normalizedButtonVariant to include Ghost
|
||||
// BadgeClass already exists — update normalizedBadgeVariant to include Primary
|
||||
|
||||
// Add for new component types:
|
||||
func CardClass() string
|
||||
func InputClass(disabled bool) string
|
||||
func TextareaClass(disabled bool) string
|
||||
func ModalClass() string
|
||||
func EmptyStateClass() string
|
||||
func TableClass() string
|
||||
func FormFieldClass() string
|
||||
func IconButtonClass(variant IconButtonVariant, tone IconButtonTone) string // already in go-backend
|
||||
func SpaceXClass(step SpacingStep) string // already in go-backend
|
||||
func SpaceYClass(step SpacingStep) string // already in go-backend
|
||||
```
|
||||
|
||||
### Test coverage (`backend/internal/web/ui/ui_test.go`)
|
||||
|
||||
At minimum one smoke test per component type verifying class string output:
|
||||
|
||||
| Component | Test function | Assertion |
|
||||
|-----------|--------------|-----------|
|
||||
| Button | `TestButtonClass` (extend) | Ghost variant → contains `ui-button-ghost` |
|
||||
| Badge | `TestBadgeClass` (extend) | Primary variant → contains `ui-badge-primary` |
|
||||
| Card | `TestCardClass` | Returns `ui-card` |
|
||||
| Input | `TestInputClass` | Returns `ui-input` |
|
||||
| Textarea | `TestTextareaClass` | Returns `ui-textarea` |
|
||||
| Select | `TestSelectClass` | Returns `ui-select-control` |
|
||||
| Modal | `TestModalClass` | Returns `ui-modal-panel` |
|
||||
| EmptyState | `TestEmptyStateClass` | Returns `ui-empty-state` |
|
||||
| Table | `TestTableClass` | Returns `ui-table` |
|
||||
| IconButton | `TestIconButtonClass` | Ghost neutral → contains `borderless-icon-button` |
|
||||
| FormField | `TestFormFieldClass` | Returns `ui-form-field` |
|
||||
|
||||
---
|
||||
|
||||
## CSS File Manifest
|
||||
|
||||
After Phase 13, `backend/tailwind.input.css` imports:
|
||||
|
||||
```css
|
||||
@import "tailwindcss";
|
||||
|
||||
@source "./templates/**/*.templ";
|
||||
@source "./internal/web/**/*.templ";
|
||||
@source "./internal/web/**/*.go";
|
||||
|
||||
@import "./internal/web/ui/base.css";
|
||||
@import "./internal/web/ui/button.css";
|
||||
@import "./internal/web/ui/badge.css";
|
||||
@import "./internal/web/ui/card.css";
|
||||
@import "./internal/web/ui/input.css";
|
||||
@import "./internal/web/ui/textarea.css";
|
||||
@import "./internal/web/ui/select.css";
|
||||
@import "./internal/web/ui/modal.css";
|
||||
@import "./internal/web/ui/empty-state.css";
|
||||
@import "./internal/web/ui/table.css";
|
||||
@import "./internal/web/ui/icon-button.css";
|
||||
@import "./internal/web/ui/form-field.css";
|
||||
@import "./internal/web/ui/spacing.css";
|
||||
```
|
||||
|
||||
Each CSS file is ported from `go-backend/internal/web/ui/{name}.css` with page-level selectors stripped (D-A03). Component CSS stays scoped to `.ui-*` class selectors only.
|
||||
|
||||
---
|
||||
|
||||
## Class Naming Convention
|
||||
|
||||
Go-backend uses two different class-assembly patterns. The backend must match go-backend exactly:
|
||||
|
||||
| Component | Pattern | Example |
|
||||
|-----------|---------|---------|
|
||||
| Button | `ui-button ui-button-{tone} ui-button-{variant} ui-button-{size}` | `ui-button ui-button-solid ui-button-default ui-button-md` |
|
||||
| Badge | `ui-badge ui-badge-{variant}` | `ui-badge ui-badge-info` |
|
||||
| Icon button (solid) | `ui-icon-button ui-icon-button-solid ui-icon-button-{variant}` | `ui-icon-button ui-icon-button-solid ui-icon-button-neutral` |
|
||||
| Icon button (ghost) | `borderless-icon-button ui-icon-button-ghost ui-icon-button-{variant}` | `borderless-icon-button ui-icon-button-ghost ui-icon-button-danger` |
|
||||
| Space X | `ui-space-x ui-space-x-{step}` | `ui-space-x ui-space-x-md` |
|
||||
| Space Y | `ui-space-y ui-space-y-{step}` | `ui-space-y ui-space-y-md` |
|
||||
|
||||
**Note:** The existing `backend/internal/web/ui/variants.go` uses a DIFFERENT pattern:
|
||||
`ui-button ui-button-{tone}-{variant}-{size}` (hyphen-joined). This diverges from go-backend.
|
||||
The planner must decide whether to align backend to go-backend's space-separated pattern or keep the existing joined pattern and update the CSS to match. This is a **pre-planning decision required before Plan 01.**
|
||||
|
||||
---
|
||||
|
||||
## Interaction Contract
|
||||
|
||||
All interactive states are implemented in CSS only (no JavaScript for state). JavaScript is permitted only for:
|
||||
- The select component dropdown open/close toggle (adding/removing `is-open` class and `hidden` attribute on the menu)
|
||||
- Modal show/hide via HTMX swap (no JS state machines)
|
||||
|
||||
| State | Mechanism |
|
||||
|-------|-----------|
|
||||
| Button hover | CSS `:hover` pseudo-class |
|
||||
| Button active | CSS `:active` pseudo-class |
|
||||
| Button focus | CSS `:focus-visible`, 3px ring `--color-focus-ring` |
|
||||
| Button disabled | HTML `disabled` attribute + CSS `[disabled]` or `.is-disabled` |
|
||||
| Input focus | CSS `:focus`, border-color change + 3px ring |
|
||||
| Select open | JS adds `is-open` class; CSS rotates chevron, shows menu |
|
||||
| Modal open | HTMX swaps backdrop+panel into `#modal-slot` |
|
||||
| Modal close | HTMX swaps empty fragment into `#modal-slot` |
|
||||
| Icon button focus | CSS `:focus-visible`, 3px ring `--color-focus-ring` |
|
||||
|
||||
---
|
||||
|
||||
## Accessibility Contract
|
||||
|
||||
- All interactive elements must have `min-height: 44px` (touch target — WCAG 2.5.5)
|
||||
- Icon buttons must have an `aria-label` attribute (no visible text)
|
||||
- Form fields must use `<label for="...">` wired to input `id`
|
||||
- Modal must trap focus when open (HTMX pattern: autofocus first interactive element in swapped fragment)
|
||||
- Focus rings must use `--color-focus-ring` (purple 20% opacity) — never remove focus visible styles
|
||||
- Color is never the sole differentiator for status (badge labels carry semantic meaning)
|
||||
- `.visually-hidden` utility class is ported from go-backend base.css for screen-reader-only text
|
||||
|
||||
---
|
||||
|
||||
## Registry Safety
|
||||
|
||||
| Registry | Blocks Used | Safety Gate |
|
||||
|----------|-------------|-------------|
|
||||
| npm / shadcn | none | not applicable — Go project |
|
||||
| third-party CSS | none | not applicable — port from own go-backend |
|
||||
|
||||
No external CSS frameworks or component registries are introduced in Phase 13.
|
||||
|
||||
---
|
||||
|
||||
## Pre-Planning Decision Required
|
||||
|
||||
Before Plan 01 can be written, one ambiguity must be resolved:
|
||||
|
||||
**Class assembly pattern mismatch:**
|
||||
- `go-backend/variants.go` generates: `ui-button ui-button-solid ui-button-default ui-button-md` (four separate classes)
|
||||
- `backend/variants.go` (current) generates: `ui-button ui-button-solid-default-md` (one compound class)
|
||||
|
||||
The CSS selectors in go-backend's `button.css` use the multi-class pattern:
|
||||
```css
|
||||
.ui-button-solid.ui-button-default { ... }
|
||||
```
|
||||
|
||||
The planner must align the backend `ButtonClass()` function and `button.css` selectors to use go-backend's multi-class pattern. This is a breaking change to `button.css` but button.css is already being replaced in Phase 13, so the cost is zero.
|
||||
|
||||
**Recommended resolution (Claude's discretion per D-CA03):** Adopt go-backend's multi-class pattern in Phase 13. Update `ButtonClass()` in `variants.go` to emit four separate classes, and port `button.css` with go-backend's multi-class selectors.
|
||||
|
||||
---
|
||||
|
||||
## 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