docs(03): UI design contract for Tablos CRUD phase
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ab5b58cd06
commit
c779d2aee1
1 changed files with 322 additions and 0 deletions
322
.planning/phases/03-tablos-crud/03-UI-SPEC.md
Normal file
322
.planning/phases/03-tablos-crud/03-UI-SPEC.md
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
---
|
||||
phase: 3
|
||||
slug: tablos-crud
|
||||
status: draft
|
||||
shadcn_initialized: false
|
||||
preset: none
|
||||
created: 2026-05-14
|
||||
---
|
||||
|
||||
# Phase 3 — UI Design Contract
|
||||
|
||||
> Visual and interaction contract for Phase 3: Tablos CRUD.
|
||||
> Generated by gsd-ui-researcher. Stack is Go + templ + HTMX + Tailwind v4 — no React, no shadcn.
|
||||
|
||||
---
|
||||
|
||||
## Design System
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Tool | none (plain HTML + Tailwind v4) |
|
||||
| Preset | not applicable |
|
||||
| Component library | ui package — `backend/internal/web/ui/` (Card, Button, Badge, CSRFField) |
|
||||
| Icon library | none — Phase 3 uses text labels and Unicode characters only |
|
||||
| Font | system-ui, -apple-system, "Segoe UI", Roboto, sans-serif (inherited from base.css) |
|
||||
|
||||
shadcn gate: Not applicable. Stack is Go + templ, not React/Next.js/Vite.
|
||||
|
||||
---
|
||||
|
||||
## Base Layout Contract
|
||||
|
||||
Inherited from Phase 1/2 (`backend/templates/layout.templ`). Do NOT modify the shell.
|
||||
|
||||
| Property | Value | Source |
|
||||
|----------|-------|--------|
|
||||
| Container max-width | max-w-5xl | layout.templ — locked |
|
||||
| Horizontal padding | px-4 sm:px-6 | layout.templ — locked |
|
||||
| Main content top padding | py-8 | layout.templ — locked |
|
||||
| Header background | bg-slate-50 border-b border-slate-200 | layout.templ — locked |
|
||||
| Body background | bg-white | layout.templ (base.css) — locked |
|
||||
| HTMX script | `/static/htmx.min.js` deferred — no CDN | layout.templ — locked |
|
||||
|
||||
The footer text "Phase 2 · Authentication" must be updated to "Phase 3 · Tablos" in layout.templ.
|
||||
|
||||
---
|
||||
|
||||
## Spacing Scale
|
||||
|
||||
Declared values (multiples of 4 only):
|
||||
|
||||
| Token | Value | Tailwind class | Usage |
|
||||
|-------|-------|----------------|-------|
|
||||
| xs | 4px | gap-1, p-1 | Icon gaps, inline chip padding |
|
||||
| sm | 8px | gap-2, p-2 | Compact element spacing, badge padding |
|
||||
| md | 16px | gap-4, p-4 | Default element spacing, card internal gap |
|
||||
| lg | 24px | gap-6, p-6 | Card padding (matches `.ui-card` 1.5rem) |
|
||||
| xl | 32px | gap-8, mt-8 | Section breaks within page |
|
||||
| 2xl | 48px | gap-12 | Major section separators |
|
||||
| 3xl | 64px | — | Not used in Phase 3 |
|
||||
|
||||
Exceptions:
|
||||
- Touch-target minimum 44px height for all interactive buttons (per WCAG 2.5.5). Achieved via `py-2 px-4` on `.ui-button-solid-default-md` (already 40px rendered; add `min-h-[44px]` when implementing new danger/neutral button variants for Phase 3).
|
||||
- Form `space-y-5` (20px) is an existing pattern from auth forms — reuse exactly for tablo create/edit forms.
|
||||
|
||||
---
|
||||
|
||||
## Typography
|
||||
|
||||
All sizes sourced from existing codebase patterns in `templates/` and `ui/`.
|
||||
|
||||
| Role | Size | Weight | Line Height | Tailwind classes | Usage |
|
||||
|------|------|--------|-------------|-----------------|-------|
|
||||
| Display | 28px | 600 (semibold) | 1.2 (tight) | `text-[28px] font-semibold leading-tight` | Dashboard page heading ("Your Tablos") |
|
||||
| Heading | 20px | 600 (semibold) | 1.3 (snug) | `text-xl font-semibold leading-snug` | Tablo card title, detail page section headings |
|
||||
| Body | 16px | 400 (regular) | 1.5 | `text-base text-slate-600` | Tablo descriptions, paragraph content |
|
||||
| Label | 14px | 500 (medium) | 1.4 | `text-sm font-medium text-slate-700` | Form labels, metadata (created date, owner email) |
|
||||
| Small/muted | 14px | 400 (regular) | 1.4 | `text-sm text-slate-500` | "Signed in as" subtext, timestamps |
|
||||
|
||||
Two declared weights: 400 (regular) and 600 (semibold). The 500 (medium) weight is used only for form labels — this follows the existing auth form pattern (`text-sm font-medium text-slate-700`).
|
||||
|
||||
---
|
||||
|
||||
## Color
|
||||
|
||||
| Role | Hex | Tailwind | Usage |
|
||||
|------|-----|----------|-------|
|
||||
| Dominant surface (60%) | #ffffff | bg-white | Page background, form inputs |
|
||||
| Secondary surface (30%) | #f8fafc | bg-slate-50 | Cards (`ui-card`), header strip, input focus hover |
|
||||
| Accent (10%) | #2563eb | bg-blue-600 | Primary CTA button only (see accent reserved list) |
|
||||
| Destructive | #b91c1c | bg-red-700 / text-red-700 | Delete confirmation button, field error text, general error banner |
|
||||
|
||||
Accent (#2563eb / blue-600) reserved for:
|
||||
1. Primary action button — "New tablo" (create) and "Save" (edit submit)
|
||||
2. Focus ring on all interactive elements (`outline: 2px solid #2563eb`)
|
||||
3. Nothing else — links, secondary actions, and navigation use slate tones
|
||||
|
||||
Borders: slate-200 (`#e2e8f0`) for cards and input default state. slate-300 (`#cbd5e1`) for input borders.
|
||||
|
||||
Text: slate-900 (`#0f172a`) for primary content, slate-700 (`#334155`) for labels, slate-600 (`#475569`) for secondary text, slate-500 (`#64748b`) for muted/metadata text.
|
||||
|
||||
Tablo color field rendering: render as a 10px × 10px filled dot (CSS circle using `inline-block w-2.5 h-2.5 rounded-full`) on the dashboard card, using the stored hex or Tailwind color as inline `background-color` style. If `color` is null or empty, omit the dot entirely. No color picker UI in Phase 3 — accept hex via plain text input.
|
||||
|
||||
---
|
||||
|
||||
## Component Inventory
|
||||
|
||||
### Existing (reuse without modification)
|
||||
|
||||
| Component | Import | Usage in Phase 3 |
|
||||
|-----------|--------|-----------------|
|
||||
| `ui.Card` | `backend/internal/web/ui` | Tablo list card, create form container |
|
||||
| `ui.Button` (solid/default/md) | `backend/internal/web/ui` | "New tablo" CTA, "Save" on edit, "Create tablo" submit |
|
||||
| `ui.Badge` | `backend/internal/web/ui` | Optional: status labels if needed |
|
||||
| `ui.CSRFField` | `backend/internal/web/ui` | All forms (create, edit, delete) — AUTH-06 |
|
||||
| `templates.FieldError` | `backend/templates` | Inline field validation on create/edit forms |
|
||||
| `templates.GeneralError` | `backend/templates` | General error banner on form submission failures |
|
||||
| `templates.Layout` | `backend/templates` | All tablo pages |
|
||||
|
||||
### New (must be implemented in Phase 3)
|
||||
|
||||
| Component | File | Description |
|
||||
|-----------|------|-------------|
|
||||
| `templates.TablosDashboard` | `backend/templates/tablos.templ` | Full dashboard page (tablo list + create form slot) |
|
||||
| `templates.TablosListFragment` | `backend/templates/tablos.templ` | HTMX-swappable list of tablo cards (reused on create/delete) |
|
||||
| `templates.TabloCard` | `backend/templates/tablos.templ` | Single tablo card (title, description excerpt, color dot, delete button) |
|
||||
| `templates.TabloCreateFormFragment` | `backend/templates/tablos.templ` | Inline create form (HTMX swap on validation error) |
|
||||
| `templates.TabloDetailPage` | `backend/templates/tablos.templ` | Tablo detail page with inline edit areas |
|
||||
| `templates.TabloTitleDisplay` | `backend/templates/tablos.templ` | Display fragment for title (swapped to edit input on click) |
|
||||
| `templates.TabloTitleEditFragment` | `backend/templates/tablos.templ` | Edit input fragment for title |
|
||||
| `templates.TabloDescDisplay` | `backend/templates/tablos.templ` | Display fragment for description |
|
||||
| `templates.TabloDescEditFragment` | `backend/templates/tablos.templ` | Edit textarea fragment for description |
|
||||
| `templates.TabloDeleteButtonFragment` | `backend/templates/tablos.templ` | Delete button (swaps to confirmation row) |
|
||||
| `templates.TabloDeleteConfirmFragment` | `backend/templates/tablos.templ` | "Delete tablo? Yes / Cancel" confirmation row |
|
||||
| `ui.ButtonDanger` (soft/danger/md CSS) | `backend/internal/web/ui/button.css` | Delete-confirm "Yes, delete" button (red variant) |
|
||||
| `ui.ButtonNeutral` (soft/neutral/md CSS) | `backend/internal/web/ui/button.css` | "Cancel" button (slate/ghost variant) |
|
||||
|
||||
---
|
||||
|
||||
## Interaction Contracts
|
||||
|
||||
### 1. Dashboard — Tablo List
|
||||
|
||||
- Route: `GET /`
|
||||
- Template: `templates.TablosDashboard`
|
||||
- Container: `<div id="tablos-list">` wraps the list of `TabloCard` items.
|
||||
- Sort: newest-first (from DB `ORDER BY created_at DESC`).
|
||||
- Empty state: when no tablos exist, render the empty state inside `#tablos-list` (see Copywriting section).
|
||||
- No HTMX on initial load — full page render.
|
||||
|
||||
### 2. Create Tablo — Inline Form
|
||||
|
||||
- Trigger: "New tablo" button (blue, solid, md) fires `hx-get="/tablos/new"` targeting `#create-form-slot` with `hx-swap="innerHTML"`.
|
||||
- `#create-form-slot` is an empty `<div>` above `#tablos-list` in the dashboard.
|
||||
- Form: `hx-post="/tablos"` → `hx-target="#create-form-slot"` → `hx-swap="innerHTML"`.
|
||||
- On validation error (422): server returns the form fragment with field errors. Form re-renders in-place.
|
||||
- On success (200 + HX-Trigger or fragment): server returns an empty `#create-form-slot` (form collapses) AND the new tablo card is prepended to `#tablos-list` using `hx-swap="afterbegin"` on the list, or via `HX-Retarget`/`HX-Reswap` response headers. Preferred approach: use `HX-Retarget: #tablos-list` + `HX-Reswap: afterbegin` response headers from the handler so a single HTMX response both clears the form slot and prepends the card.
|
||||
- Non-HTMX fallback: standard `POST /tablos` → `303 /` redirect.
|
||||
- Form fields: Title (required, text input), Description (optional, textarea 3 rows), Color (optional, text input with placeholder "#6366f1 or indigo").
|
||||
|
||||
### 3. Tablo Detail — View
|
||||
|
||||
- Route: `GET /tablos/{id}`
|
||||
- Non-owner or unauthenticated: 404 (no 403 per D-04).
|
||||
- Template: `templates.TabloDetailPage` extending `Layout`.
|
||||
- Title and description rendered as display fragments with an "Edit" affordance.
|
||||
|
||||
### 4. Edit Tablo — Inline on Detail Page
|
||||
|
||||
- Trigger: Clicking the title or description area fires `hx-get="/tablos/{id}/edit-title"` (or `/edit-desc`) targeting the respective display element with `hx-swap="outerHTML"`.
|
||||
- Edit fragment renders an `<input>` (title) or `<textarea>` (description) with a hidden CSRF field.
|
||||
- Save: `hx-post="/tablos/{id}"` (with `_method=PATCH` hidden input for semantic clarity, handled by chi) targeting the edit fragment container, `hx-swap="outerHTML"`.
|
||||
- On success: server returns the updated display fragment.
|
||||
- On validation error: server returns the edit fragment with a field error.
|
||||
- Cancel: A "Cancel" button fires `hx-get="/tablos/{id}/show-title"` (or `/show-desc`) to restore the original display fragment without modifying the DB.
|
||||
- Non-HTMX fallback: `POST /tablos/{id}` with `_method=PATCH` → `303 /tablos/{id}` redirect.
|
||||
|
||||
### 5. Delete Tablo — Inline Confirmation
|
||||
|
||||
- Context: delete button visible on both the dashboard card and the detail page.
|
||||
- Trigger: Delete button fires `hx-get="/tablos/{id}/delete-confirm"` targeting the button's container (`hx-target="closest .tablo-delete-zone"`) with `hx-swap="outerHTML"`.
|
||||
- Confirmation row: renders "Delete tablo? [Yes, delete] [Cancel]" inline.
|
||||
- Confirm: `hx-delete="/tablos/{id}"` (or `hx-post` with `_method=DELETE`) with `hx-target="closest .ui-card"` and `hx-swap="outerHTML"` (removes the card from the DOM on success).
|
||||
- Cancel: fires `hx-get="/tablos/{id}/delete-cancel"` to restore the original delete button fragment.
|
||||
- On success (dashboard): card removed from `#tablos-list`. If list is now empty, server should return the empty state fragment via `HX-Retarget: #tablos-list` + `HX-Reswap: innerHTML`.
|
||||
- On success (detail page): redirect to `/` via `HX-Redirect: /` response header (HTMX honored) or `303 /` for non-HTMX.
|
||||
- Non-HTMX fallback: `POST /tablos/{id}/delete` with CSRF → `303 /`.
|
||||
|
||||
---
|
||||
|
||||
## Copywriting Contract
|
||||
|
||||
| Element | Copy |
|
||||
|---------|------|
|
||||
| Page title (browser tab) | "Tablos — Xtablo" |
|
||||
| Dashboard heading | "Your Tablos" |
|
||||
| Primary CTA (create) | "New tablo" |
|
||||
| Create form heading | "Create a tablo" |
|
||||
| Create submit button | "Create tablo" |
|
||||
| Edit save button | "Save" |
|
||||
| Edit cancel button | "Cancel" |
|
||||
| Delete button (card/detail) | "Delete" |
|
||||
| Delete confirm heading | "Delete tablo?" |
|
||||
| Delete confirm body | "This cannot be undone." |
|
||||
| Delete confirm action | "Yes, delete" |
|
||||
| Delete cancel action | "Cancel" |
|
||||
| Empty state heading | "No tablos yet" |
|
||||
| Empty state body | "Create your first tablo to get started." |
|
||||
| Empty state CTA | "New tablo" (same blue button) |
|
||||
| Validation — title required | "Title is required." |
|
||||
| Validation — title too long | "Title must be 255 characters or fewer." |
|
||||
| General server error | "Something went wrong. Please try again." |
|
||||
| 404 page heading | "Not found" |
|
||||
| 404 page body | "This tablo doesn't exist or you don't have access." |
|
||||
| Footer text | "Phase 3 · Tablos" |
|
||||
|
||||
---
|
||||
|
||||
## Form Field Styles
|
||||
|
||||
Reuse the exact Tailwind classes from auth forms (locked pattern):
|
||||
|
||||
```
|
||||
Input: mt-1 block w-full rounded border border-slate-300 px-3 py-2 text-sm
|
||||
placeholder-slate-400 focus:border-slate-500 focus:outline-none
|
||||
Textarea: mt-1 block w-full rounded border border-slate-300 px-3 py-2 text-sm
|
||||
placeholder-slate-400 focus:border-slate-500 focus:outline-none resize-y
|
||||
Label: block text-sm font-medium text-slate-700
|
||||
```
|
||||
|
||||
Field error: `<p class="mt-1 text-sm text-red-700">` (existing `templates.FieldError`).
|
||||
General error banner: existing `templates.GeneralError` (border-red-300 bg-red-50).
|
||||
|
||||
---
|
||||
|
||||
## New Button Variants Required
|
||||
|
||||
Phase 3 needs two new CSS rules in `backend/internal/web/ui/button.css`:
|
||||
|
||||
**Danger solid md** (delete confirm "Yes, delete"):
|
||||
```
|
||||
.ui-button-solid-danger-md {
|
||||
background-color: #b91c1c; /* red-700 */
|
||||
color: #ffffff;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.ui-button-solid-danger-md:hover { background-color: #991b1b; }
|
||||
```
|
||||
|
||||
**Neutral soft md** (cancel, secondary actions):
|
||||
```
|
||||
.ui-button-soft-neutral-md {
|
||||
background-color: #f1f5f9; /* slate-100 */
|
||||
color: #334155; /* slate-700 */
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
border: 1px solid #e2e8f0; /* slate-200 */
|
||||
}
|
||||
.ui-button-soft-neutral-md:hover { background-color: #e2e8f0; }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Registry Safety
|
||||
|
||||
Not applicable. Stack is Go + templ — no shadcn, no npm component registries.
|
||||
|
||||
| Registry | Blocks Used | Safety Gate |
|
||||
|----------|-------------|-------------|
|
||||
| shadcn official | none | not applicable — Go stack |
|
||||
| third-party | none | not applicable — Go stack |
|
||||
|
||||
---
|
||||
|
||||
## HTMX Attribute Reference
|
||||
|
||||
Established patterns to reuse exactly (do not invent new patterns):
|
||||
|
||||
| Interaction | hx-method | hx-target | hx-swap |
|
||||
|-------------|-----------|-----------|---------|
|
||||
| Show create form | hx-get="/tablos/new" | #create-form-slot | innerHTML |
|
||||
| Submit create (success) | hx-post="/tablos" | #create-form-slot | innerHTML + HX-Retarget header |
|
||||
| Submit create (error) | hx-post="/tablos" | #create-form-slot | innerHTML |
|
||||
| Show edit title | hx-get="/tablos/{id}/edit-title" | `.tablo-title-zone` | outerHTML |
|
||||
| Save edit title | hx-post="/tablos/{id}" | `.tablo-title-zone` | outerHTML |
|
||||
| Cancel edit title | hx-get="/tablos/{id}/show-title" | `.tablo-title-zone` | outerHTML |
|
||||
| Show delete confirm | hx-get="/tablos/{id}/delete-confirm" | `.tablo-delete-zone` | outerHTML |
|
||||
| Confirm delete | hx-post="/tablos/{id}/delete" | `.ui-card` (closest) | outerHTML |
|
||||
| Cancel delete confirm | hx-get="/tablos/{id}/delete-cancel" | `.tablo-delete-zone` | outerHTML |
|
||||
|
||||
All state-changing requests must include the CSRF token. The `@ui.CSRFField(csrfToken)` helper must be in every `<form>` element.
|
||||
|
||||
HTMX loading indicator: `class="htmx-indicator"` with `opacity-0` default, shown via `.htmx-request .htmx-indicator` CSS (already in Tailwind output from Phase 1 demo).
|
||||
|
||||
---
|
||||
|
||||
## Accessibility Floor
|
||||
|
||||
Inherited from `base.css` `:focus-visible` rule. Phase 3 additions:
|
||||
|
||||
- All form `<input>` and `<textarea>` elements must have an associated `<label for="...">`.
|
||||
- Delete confirmation buttons use `aria-label="Confirm delete tablo"` and `aria-label="Cancel delete"` to distinguish from other Cancel buttons on the page.
|
||||
- Empty state CTA button has `aria-label="Create your first tablo"`.
|
||||
- `<main>` landmark present in `Layout` — no additional landmark required in Phase 3.
|
||||
|
||||
---
|
||||
|
||||
## 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