+ @ui.IconButton(ui.IconButtonProps{
+ Label: "Delete tablo",
+ Icon: "trash",
+ Variant: ui.IconButtonVariantDanger,
+ Tone: ui.IconButtonToneGhost,
+ Type: "button",
+ Attrs: templ.Attributes{
+ "hx-get": "/tablos/" + tablo.ID.String() + "/delete-confirm",
+ "hx-target": "closest .tablo-delete-zone",
+ "hx-swap": "outerHTML",
+ },
+ })
+
+```
+
+**Metadata row with Badge** (replaces inline status span at lines 279–282):
+```go
+` at line 107):
+```go
+// Current column container (lines 107-131):
+
+
+
{ TaskColumnLabels[status] }
+
+ @ui.Badge(ui.BadgeProps{Label: strconv.Itoa(len(tasks)), Variant: ui.BadgeVariantInfo})
+
+
+
+ ...task cards...
+
+
+ @AddTaskTrigger(tabloID, status, csrfToken, filter)
+
+
+
+// Phase 16 target (tasks-section layout with etape grouping):
+
+
+
+
+ if len(tasks) == 0 {
+
No tasks yet
+ } else {
+ {{ groups := groupTasksByEtape(tasks, etapes) }}
+ for _, group := range groups {
+ @EtapeGroupHeader(group)
+ for _, task := range group.Tasks {
+ @TaskCard(tabloID, task, csrfToken)
+ }
+ }
+ }
+
+
+
+```
+
+**AddTaskTrigger restyled** (replaces current button at lines 372–379 — use `.tasks-add-button` class):
+```go
+// Current (uses ui-button classes directly):
+
+
+// Phase 16 target:
+
+```
+
+**TaskCard restyled** (replaces `.task-card` div at lines 138–165 — use `.task-row` CSS):
+```go
+// Current (lines 136-167):
+
+
+// Phase 16 target (task-row pattern from go-backend):
+
+
+
+
+ @ui.IconButton(ui.IconButtonProps{
+ Label: "Delete task: " + task.Title,
+ Icon: "trash",
+ Variant: ui.IconButtonVariantDanger,
+ Tone: ui.IconButtonToneGhost,
+ Type: "button",
+ Attrs: templ.Attributes{
+ "hx-get": "/tablos/" + tabloID.String() + "/tasks/" + task.ID.String() + "/delete-confirm",
+ "hx-target": "closest .task-card-zone",
+ "hx-swap": "outerHTML",
+ },
+ })
+
+
+```
+
+**EtapeGroupHeader helper component** (new templ function in `tasks.templ`):
+```go
+// EtapeGroupHeader renders the sub-heading row for an etape group within a kanban column.
+// "No etape" / unassigned group omits the color dot and uses muted label style.
+templ EtapeGroupHeader(group EtapeGroup) {
+
+}
+```
+
+**EtapeStrip OOB removal** (from `TaskCardGone` line 386 and `TaskCardOOB` line 398):
+```go
+// DELETE from TaskCardGone (line 386):
+@EtapeStrip(tabloID, etapes, counts, filter, csrfToken, true)
+
+// DELETE from TaskCardOOB (line 398):
+@EtapeStrip(tabloID, etapes, counts, filter, csrfToken, true)
+
+// Keep etapes/counts parameters on TaskCardGone and TaskCardOOB signatures
+// to avoid handler signature changes that would break tests.
+// Mark with TODO comment for future cleanup.
+```
+
+---
+
+### `backend/templates/etapes.templ` — EtapeStrip removal
+
+**No new pattern needed.** The `EtapeStrip` function itself is retained (lines 11–101) but called from nowhere after removal from `tablos.templ`, `TaskCardGone`, and `TaskCardOOB`. `EtapeEditFormFragment`, `EtapeCreateFormFragment`, and `EtapeDeleteConfirmFragment` are preserved unchanged.
+
+The `@ui.CSRFField` + `@ui.Button` pattern in etape forms (lines 103–195) is already correct — no restyling needed.
+
+---
+
+### `backend/templates/files.templ` — files section restyling
+
+**Analog within same file:** `FileDeleteConfirmFragment` (lines 104–146) for the confirm HTMX pattern. `TablosDashboard` in `tablos.templ` for `@ui.EmptyState` and `.overview-section-heading`.
+
+**FilesTabFragment — new structure** (replaces lines 12–27):
+```go
+// Current:
+templ FilesTabFragment(tablo sqlc.Tablo, files []sqlc.TabloFile, csrfToken string) {
+
+ @FileUploadForm(tablo.ID, csrfToken, "")
+
+ if len(files) == 0 {
+ @FileListEmpty()
+ } else {
+
+ for _, f := range files {
+ @FileListRow(tablo.ID, f)
+ }
+
+ }
+
+
+}
+
+// Phase 16 target:
+templ FilesTabFragment(tablo sqlc.Tablo, files []sqlc.TabloFile, csrfToken string) {
+
+
+
Files
+ @ui.Button(ui.ButtonProps{
+ Label: "Upload file",
+ Variant: ui.ButtonVariantDefault,
+ Tone: ui.ButtonToneSolid,
+ Size: ui.SizeMD,
+ Type: "button",
+ Attrs: templ.Attributes{
+ // hx-get to reveal FileUploadForm inline or toggle a slot
+ "hx-get": "/tablos/" + tablo.ID.String() + "/files/upload-form",
+ "hx-target": "#file-upload-slot",
+ "hx-swap": "innerHTML",
+ },
+ })
+
+
+ if len(files) == 0 {
+ @ui.EmptyState(ui.EmptyStateProps{
+ Title: "No files yet",
+ Description: "Upload your first file to get started.",
+ })
+ } else {
+ @ui.Table(ui.TableProps{
+ Head: fileTableHead(),
+ Body: fileTableBody(tablo.ID, files),
+ })
+ }
+
+}
+```
+
+**FileTableHead helper** (new private templ in `files.templ`):
+```go
+// fileTableHead renders the
content for the files table.
+// @ui.Table places this inside ; render | ... |
.
+templ fileTableHead() {
+
+ | Filename |
+ Size |
+ Uploaded |
+ Actions |
+
+}
+```
+
+**FileTableBody helper** (new private templ in `files.templ`):
+```go
+// fileTableBody renders the content for the files table.
+templ fileTableBody(tabloID uuid.UUID, files []sqlc.TabloFile) {
+ for _, f := range files {
+ @FileListRow(tabloID, f)
+ }
+}
+```
+
+**FileListRow — must be `` for @ui.Table** (replaces `` at lines 72–99):
+```go
+// Current (uses — incompatible with @ui.Table's ):
+templ FileListRow(tabloID uuid.UUID, file sqlc.TabloFile) {
+ ...
+
+// Phase 16 target (CRITICAL: outer must be for valid HTML inside
):
+templ FileListRow(tabloID uuid.UUID, file sqlc.TabloFile) {
+
+ | { file.Filename } |
+ { formatBytes(file.SizeBytes) } |
+
+ if file.CreatedAt.Valid {
+ { file.CreatedAt.Time.Format("2006-01-02") }
+ }
+ |
+
+
+ @ui.IconButton(ui.IconButtonProps{
+ Label: "Download file",
+ Icon: "download",
+ Variant: ui.IconButtonVariantNeutral,
+ Tone: ui.IconButtonToneGhost,
+ Type: "button",
+ })
+
+ @ui.IconButton(ui.IconButtonProps{
+ Label: "Delete file",
+ Icon: "trash",
+ Variant: ui.IconButtonVariantDanger,
+ Tone: ui.IconButtonToneGhost,
+ Type: "button",
+ Attrs: templ.Attributes{
+ "hx-get": "/tablos/" + tabloID.String() + "/files/" + file.ID.String() + "/delete-confirm",
+ "hx-target": "closest .file-row-zone",
+ "hx-swap": "outerHTML",
+ },
+ })
+ |
+
+}
+```
+
+**FileDeleteConfirmFragment — must also be ``** (replaces `` at lines 104–146 — see Pitfall 2):
+```go
+// Current outer element (line 105):
+
+
+// Phase 16 target — outerHTML swap requires matching element type:
+
+ |
+ // confirm dialog content (buttons unchanged)
+ |
+
+```
+
+**FileListEmpty is replaced by @ui.EmptyState** — the `FileListEmpty()` function (lines 149–151) can be retained for backward compatibility but should not be called from `FilesTabFragment` or `UploadErrorFragment` anymore.
+
+---
+
+## Shared Patterns
+
+### IconButton + HTMX outerHTML delete flow
+**Source:** `backend/templates/files.templ` `FileDeleteConfirmFragment` (lines 104–146), `backend/templates/tasks.templ` `TaskDeleteConfirmFragment` (lines 326–367)
+**Apply to:** All delete icon buttons in file rows and task cards.
+```go
+// Pattern: trigger button on the row → HTMX loads confirm fragment → outerHTML swap
+// The trigger button and the confirm fragment MUST share the same zone class and id.
+Attrs: templ.Attributes{
+ "hx-get": "/tablos/" + tabloID.String() + "/files/" + file.ID.String() + "/delete-confirm",
+ "hx-target": "closest .file-row-zone",
+ "hx-swap": "outerHTML",
+},
+```
+
+### Design token enforcement (no hardcoded hex)
+**Source:** `backend/internal/web/ui/base.css` + existing `app.css` sections
+**Apply to:** All new CSS rules in `app.css` and all new inline styles in `.templ` files.
+```css
+/* ALWAYS use tokens: */
+color: var(--color-text-muted); /* NOT #667085 */
+color: var(--color-text-brand); /* NOT #804EEC */
+border-color: var(--color-border-muted); /* NOT #F2F4F7 */
+background: var(--color-surface-muted); /* NOT #F9FAFB */
+```
+
+### @ui.Badge usage (task count + status pill)
+**Source:** `backend/templates/tasks.templ` lines 110–112 (task count badge in KanbanColumn)
+**Apply to:** Column header task count, status pill in metadata row.
+```go
+// Task count in column header (existing pattern — keep BadgeVariantInfo):
+@ui.Badge(ui.BadgeProps{Label: strconv.Itoa(len(tasks)), Variant: ui.BadgeVariantInfo})
+
+// Status pill in metadata row (Phase 16 — use BadgeVariantPrimary per UI-SPEC):
+@ui.Badge(ui.BadgeProps{Label: "In progress", Variant: ui.BadgeVariantPrimary})
+```
+
+### .overview-section-heading header pattern
+**Source:** `backend/templates/tablos.templ` `TablosDashboard` (lines 14–29), `backend/internal/web/ui/app.css` lines 348–361
+**Apply to:** Files section header (D-F01).
+```go
+// Analog from TablosDashboard:
+
+
+
Your Tablos
+ @ui.Button(...)
+
+ ...
+
+```
+
+### @ui.EmptyState usage
+**Source:** `backend/templates/tablos.templ` `TablosEmptyState` (lines 47–64)
+**Apply to:** `FilesTabFragment` empty state (replaces `FileListEmpty()`).
+```go
+// Analog (TablosEmptyState):
+@ui.EmptyState(ui.EmptyStateProps{
+ Title: "No tablos yet",
+ Description: "Create your first tablo to get started.",
+ Action: ui.Button(...),
+})
+
+// Phase 16 usage (no action button in files empty state):
+@ui.EmptyState(ui.EmptyStateProps{
+ Title: "No files yet",
+ Description: "Upload your first file to get started.",
+})
+```
+
+### CSRF token in forms
+**Source:** All existing forms use `@ui.CSRFField(csrfToken)` — no exceptions.
+**Apply to:** Any new form elements added in Phase 16 (file upload trigger form, etape create/edit forms already have this).
+
+---
+
+## No Analog Found
+
+None. All files have strong analogs.
+
+---
+
+## Implementation Order (Wave Map)
+
+| Wave | Files | Gate |
+|------|-------|------|
+| 0 | `icon_button.templ` (add `download` + `chat` cases) | `templ generate && go build ./...` |
+| 1 | `app.css` (append Sections 19–25) | Visual diff in browser |
+| 2 | `tablos.templ` (header, tab nav, overview tab, TasksTabFragment EtapeStrip removal) | `go test ./backend/internal/web/... -run TestTablos -count=1` |
+| 3 | `tasks.templ` (groupTasksByEtape, KanbanBoard/Column/Card restyling, EtapeStrip OOB removal), `etapes.templ` (no-op, EtapeStrip already unused) | `go test ./backend/internal/web/... -run TestTask -count=1` |
+| 4 | `files.templ` (FilesTabFragment, FileListRow as `
`, FileDeleteConfirmFragment as `
`, EmptyState) | `go test ./backend/internal/web/... -run TestFilesTab -count=1` |
+
+---
+
+## Critical Constraints
+
+1. **`
` requirement:** `FileListRow` and `FileDeleteConfirmFragment` must use `
` as their outer element when rendered inside `@ui.Table`. The HTMX `hx-target="closest .file-row-zone"` + `hx-swap="outerHTML"` depends on element identity — both the trigger row and the confirm row must use the same element type (`
`).
+
+2. **KanbanBoard call sites:** Three locations must be updated when adding `etapes []sqlc.Etape` parameter — compile will fail if any is missed. Run `templ generate && go build ./...` after changing the signature.
+
+3. **EtapeStrip OOB removal:** Remove `@EtapeStrip(...)` from `TaskCardGone` (line 386) and `TaskCardOOB` (line 398). Keep the `etapes []sqlc.Etape` and `counts EtapeTaskCounts` parameters on those components to avoid handler changes.
+
+4. **TabloDeleteButtonFragment unchanged:** The dashboard delete button fragment must not be modified. Phase 16 adds a separate inline `@ui.IconButton` in `TabloDetailPage` header instead.
+
+5. **No Tailwind utility classes in new CSS:** All new CSS rules in `app.css` use `var(--...)` tokens only. Existing Tailwind utilities in edit forms are left as-is.
+
+---
+
+## Metadata
+
+**Analog search scope:** `backend/templates/`, `backend/internal/web/ui/`, `go-backend/internal/web/ui/`, `go-backend/internal/web/views/`
+**Files scanned:** 11
+**Pattern extraction date:** 2026-05-16