---
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

**Page type scale — exactly 4 sizes, 2 weights:**

| Role | Size | Weight | Line Height |
|------|------|--------|-------------|
| Body | 16px (`text-base`) | 400 regular | 1.5 (Tailwind default) |
| Label / metadata | 14px (`text-sm`) | 400 regular | 1.25 |
| Heading | 20px (`text-xl`) | 600 semibold | tight (1.2) |
| Display | 28px (`text-[28px]`) | 600 semibold | tight (1.2) |

Metadata differentiation: file size, upload date, and badge labels all use the 14px label size. Secondary metadata is visually de-emphasized via `#64748b` (slate-500) color, not a smaller font size.

**Badge-system token (outside page type scale):**
`text-xs` / 12px is a badge-internal sizing token defined in `badge.css` and scoped to `.ui-badge` elements. It is not part of the page type scale and must not be applied outside badge components. Executors must not introduce `text-xs` on page-level copy to satisfy this contract.

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, file size, upload date (`files.templ` lines 50, 76, 79)
- `text-xs` — badge-system token only (`.ui-badge` in `badge.css`); NOT a page-scale size

Phase 5 introduces no new type sizes. Four distinct page-scale 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) — differentiates metadata from primary label text at the same 14px size
- `#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 (`#64748b` slate-500 at 14px), upload date (`#64748b` slate-500 at 14px), 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 24–27).
- No spinner or skeleton — opacity feedback is sufficient for v1.

---

## Copywriting Contract

| Element | Copy |
|---------|------|
| Primary CTA (upload) | "Upload File" |
| File input label | "Attach a file" |
| Empty state body | "No files attached yet." |
| Delete trigger | "Delete File" (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 File" (inline text link, blue) |

Source: `files.templ` lines 44–66, 88–95, 107–110, 149–151. CTA labels updated to verb + noun form per design contract ("Upload File", "Delete File", "Download File").

---

## 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 File button |
| `FileListRow` | Single file row: filename, size (slate-500, 14px), date (slate-500, 14px), Download File link, Delete File 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 File 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 badge-system token, scoped to `.ui-badge` — not a page type-scale size) |
| `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 |
| Checker revision (2026-05-15) | Typography: removed 12px from page scale (now 4 sizes); annotated as badge-system token. Copywriting: CTAs updated to verb+noun form. |
