diff --git a/.planning/phases/05-files/05-UI-SPEC.md b/.planning/phases/05-files/05-UI-SPEC.md new file mode 100644 index 0000000..38aaaec --- /dev/null +++ b/.planning/phases/05-files/05-UI-SPEC.md @@ -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 `` 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 24–27). +- 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 44–66, 88–95, 107–110, 149–151. 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 `