docs(05): UI design contract for files phase

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arthur Belleville 2026-05-15 19:27:53 +02:00
parent 7d65cb4d94
commit e939563a49
No known key found for this signature in database

View file

@ -0,0 +1,228 @@
---
phase: 5
slug: files
status: draft
shadcn_initialized: false
preset: none
created: 2026-05-15
---
# Phase 5 — UI Design Contract
> Visual and interaction contract for Phase 5: Files (tab navigation + file upload/list/download/delete).
> Generated by gsd-ui-researcher. Pre-populated from codebase scan of implemented templates and design system CSS.
---
## Design System
| Property | Value |
|----------|-------|
| Tool | none — hand-rolled CSS design system in `backend/internal/web/ui/` |
| Preset | not applicable |
| Component library | Custom: `ui.Button`, `ui.Card`, `ui.Badge`, `ui.CSRFField` (templ components) |
| Icon library | none — text labels only in v1 |
| Font | system-ui, -apple-system, "Segoe UI", Roboto, sans-serif (OS stack, no web font loaded) |
Source: `backend/internal/web/ui/base.css` (verified).
---
## Spacing Scale
Declared values (multiples of 4 — Tailwind utility classes enforce this):
| Token | Value | Usage |
|-------|-------|-------|
| xs | 4px (`gap-1`, `px-1`) | Icon gaps, tight inline padding |
| sm | 8px (`px-2`, `py-2`, `gap-2`) | Compact element spacing, badge padding |
| md | 16px (`px-4`, `py-4`, `gap-4`) | Default element spacing, main content padding |
| lg | 24px (`px-6`, `py-6`, `gap-6`) | Section padding, card interior |
| xl | 32px (`mb-8`) | Layout section breaks |
| 2xl | 48px — | Major section breaks (not used in Phase 5) |
| 3xl | 64px — | Page-level spacing (not used in Phase 5) |
Exceptions:
- Touch targets on delete/download actions: minimum 44px height enforced via `min-height: 44px` on `.ui-button-solid-danger-md` and `.ui-button-soft-neutral-md` (source: `button.css` lines 61, 80).
- File input vertical rhythm: `mt-1` (4px) label-to-input gap follows existing form pattern from `tablos.templ`.
Source: scanned `button.css`, `card.css`, `base.css`, `files.templ`, `tablos.templ`, `layout.templ`.
---
## Typography
| Role | Size | Weight | Line Height |
|------|------|--------|-------------|
| Body | 16px (`text-base`) | 400 regular | 1.5 (Tailwind default) |
| Label / small text | 14px (`text-sm`) | 400 regular | 1.25 |
| Small metadata | 12px (`text-xs`) | 400 regular | 1.25 |
| Heading | 20px (`text-xl`) | 600 semibold | tight (1.2) |
| Display | 28px (`text-[28px]`) | 600 semibold | tight (1.2) |
All sizes pre-populated from codebase scan:
- `text-[28px] font-semibold` — dashboard h1 (`tablos.templ` line 15)
- `text-xl font-semibold` — card/form headings (`tablos.templ` lines 73, 110)
- `text-sm font-medium` — labels, file row filenames (`files.templ` lines 50, 76)
- `text-xs` — metadata (file size, date, badge labels) (`files.templ` line 79)
Phase 5 introduces no new type sizes. Four distinct sizes in use; two weights (400, 600). Contract is locked.
---
## Color
| Role | Value | Usage |
|------|-------|-------|
| Dominant (60%) | `#ffffff` white | Page background, body (`base.css` line 22) |
| Secondary (30%) | `#f8fafc` slate-50 / `#f1f5f9` slate-100 | Cards (`.ui-card`), upload form background, header strip, empty-state panel |
| Accent (10%) | `#2563eb` blue-600 | Primary action buttons, focus rings, download links |
| Destructive | `#b91c1c` red-700 | Delete confirm button (solid danger variant) |
Accent (`#2563eb`) reserved for:
- Primary CTA button fill (`.ui-button-solid-default-md`)
- Hyperlink text color (`text-blue-600`) — Download link in `FileListRow`
- Focus outline (`outline: 2px solid #2563eb` on `:focus-visible`)
Supporting semantic colors (not accent, not destructive):
- `#334155` slate-700 — body text, form labels, log-out button
- `#64748b` slate-500 — secondary metadata text (file size, date)
- `#94a3b8` slate-400 — placeholder text, empty state italic copy
- `#e2e8f0` slate-200 — borders on cards, file list dividers, input borders
- `#fee2e2` red-100 / `#fecaca` red-200 — soft-danger button background (delete trigger before confirm)
- `#fef2f2` red-50 / `#fca5a5` red-300 — upload error message panel background/border
Source: `base.css`, `button.css`, `card.css`, `files.templ` (all lines scanned).
---
## Interaction Contract
This section documents the HTMX interaction model for Phase 5's two new UI surfaces: the tab bar and the files tab.
### Tab Bar (new in Phase 5)
- Three tabs: **Overview** / **Tasks** / **Files** on the tablo detail page.
- Active tab: underline or filled indicator distinguishing the selected tab from inactive.
- Tab click: `hx-get` to the sub-route + `hx-target="#tab-content"` + `hx-swap="innerHTML"` + `hx-push-url` to update the browser URL.
- Direct load (no HTMX): full page rendered with the appropriate tab content embedded in `#tab-content`.
- Default tab on `/tablos/{id}`: Overview (per CONTEXT.md Claude's Discretion — Overview is the natural default).
- Active tab indicator class: `tab-active` (underline style, `border-b-2 border-blue-600 text-slate-900`) vs inactive (`text-slate-600 hover:text-slate-800`).
### Upload Form (Files tab)
- Always visible at the top of the Files tab — not collapsed behind a button (simpler; reduces interaction friction for a core action).
- Form: `method="POST"` + `enctype="multipart/form-data"` + `hx-post` + `hx-encoding="multipart/form-data"`.
- On success: HTMX swaps `#tab-content` `innerHTML` with the refreshed files list (upload form + updated list).
- On size error (> 25 MB): server returns 422 + `UploadErrorFragment`; red error panel renders above the file input.
- CSRF token embedded via `@ui.CSRFField(csrfToken)` inside the form.
### File List
- Sorted newest-first (server-side, `ORDER BY created_at DESC` in sqlc query).
- Each row: filename (truncated if long), human-readable size, upload date, Download link, Delete button.
- Download: standard `<a href>` pointing to `GET /tablos/{id}/files/{file_id}/download` — server returns 302 to signed URL (no `target="_blank"` needed; redirect handles the navigation).
- Delete trigger: HTMX `hx-get` to delete-confirm route; replaces the `.file-row-zone` with inline confirmation (same pattern as task delete in Phase 4).
### Delete Confirmation
- Inline in the file row — replaces the row content via `hx-target="closest .file-row-zone"` + `hx-swap="outerHTML"`.
- Confirm button: solid danger (`Yes, delete`) — `POST /tablos/{id}/files/{file_id}/delete`.
- Cancel button: soft neutral (`Keep file`) — `hx-get` back to the files list, restoring the original row.
- On confirmed delete: server returns `FileRowGone` — empty div with same id, removing the row from the DOM.
### Loading States
- Button `htmx-request` class: `.ui-button.htmx-request { opacity: 0.6; pointer-events: none; }` — applied by HTMX automatically during in-flight requests (source: `button.css` lines 2427).
- No spinner or skeleton — opacity feedback is sufficient for v1.
---
## Copywriting Contract
| Element | Copy |
|---------|------|
| Primary CTA (upload) | "Upload" |
| File input label | "Attach a file" |
| Empty state body | "No files attached yet." |
| Delete trigger | "Delete" (inline text link, red) |
| Delete confirm heading | "Delete file?" |
| Delete confirm body | "{filename}" (filename shown truncated for context) + "This cannot be undone." |
| Delete confirm CTA | "Yes, delete" |
| Delete cancel | "Keep file" |
| Error state — file too large | "File too large (max 25 MB)." (shown above the upload form in a red panel) |
| Error state — upload failure | "Upload failed. Please try again." (generic server error path) |
| Download link | "Download" (inline text link, blue) |
Source: `files.templ` lines 4466, 8895, 107110, 149151. All copy is locked from the implemented template — no changes needed.
---
## Component Inventory
New templ components introduced in Phase 5 (all in `backend/templates/files.templ`):
| Component | Description |
|-----------|-------------|
| `FilesTabFragment` | Upload form + file list; rendered as fragment (HTMX) or embedded in `TabloDetailPage` |
| `FileUploadForm` | Multipart form with CSRF field, optional error banner, file input, Upload button |
| `FileListRow` | Single file row: filename, size, date, Download link, Delete trigger |
| `FileDeleteConfirmFragment` | Inline delete confirmation replacing the file row zone |
| `FileListEmpty` | Empty state paragraph: "No files attached yet." |
| `FileRowGone` | Empty div with file id for HTMX outerHTML removal on delete |
| `UploadErrorFragment` | Full tab re-render with error message set in `FileUploadForm` |
Modified templ components:
| Component | Change |
|-----------|--------|
| `TabloDetailPage` (in `tablos.templ`) | Signature extended: `(user, csrfToken, tablo, tasks, files, activeTab string)`. Tab bar added. `#tab-content` area dispatches on `activeTab`. |
Reused design system components (unchanged):
| Component | Usage in Phase 5 |
|-----------|-----------------|
| `ui.Button` | Upload submit, delete confirm, keep-file cancel |
| `ui.Card` | Not used in files tab directly; file list uses a `<ul>` with divide-y border |
| `ui.CSRFField` | All POST forms (upload, delete confirm) |
---
## Registry Safety
| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| shadcn official | none — project does not use shadcn | not applicable |
| third-party registries | none | not applicable |
No external component registries. All UI components are hand-rolled in `backend/internal/web/ui/`.
---
## 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
---
## Pre-Population Sources
| Source | Decisions Used |
|--------|---------------|
| `backend/internal/web/ui/base.css` | Font stack, body color (#0f172a), background (#ffffff), focus ring (#2563eb) |
| `backend/internal/web/ui/button.css` | Button variants (solid-default-md, solid-danger-md, soft-neutral-md, soft-danger-md), min-height 44px touch targets, accent color, destructive color |
| `backend/internal/web/ui/card.css` | Card background (#f8fafc), border color (#e2e8f0), border-radius (0.5rem), padding (1.5rem) |
| `backend/internal/web/ui/badge.css` | Badge sizes (12px), info/success/danger semantic colors |
| `backend/templates/files.templ` | All copywriting strings, component structure, interaction targets, HTMX attributes |
| `backend/templates/tablos.templ` | Display heading size (28px), card heading size (20px), spacing patterns |
| `backend/templates/layout.templ` | Max container width (max-w-5xl), header/main/footer structure, asset references |
| `.planning/phases/05-files/05-CONTEXT.md` | D-07 (3-tab layout), D-08 (HTMX+hx-push-url), D-09 (25 MB limit, friendly error), upload always visible (Claude's discretion), inline delete (Claude's discretion), 302 redirect for download (Claude's discretion) |
| `.planning/phases/05-files/05-RESEARCH.md` | Standard stack (no shadcn, custom CSS), HTMX fragment patterns, tab navigation architecture |
| User input | 0 — all fields pre-populated from codebase and upstream artifacts |