docs(16): research phase domain

This commit is contained in:
Arthur Belleville 2026-05-16 23:07:57 +02:00
parent 30c446fc0e
commit 73780323d2
No known key found for this signature in database

View file

@ -0,0 +1,772 @@
# Phase 16: Tablo Detail - Research
**Researched:** 2026-05-16
**Domain:** Go templ templates, CSS design system, HTMX fragment wiring
**Confidence:** HIGH
## Summary
Phase 16 is a pure visual restyling phase — no new routes, no new data models, no handler
signature changes. The work touches four templ files and one CSS file:
`backend/templates/tablos.templ`, `backend/templates/tasks.templ`,
`backend/templates/etapes.templ`, `backend/templates/files.templ`, and
`backend/internal/web/ui/app.css`.
All design system components (`@ui.Badge`, `@ui.Button`, `@ui.IconButton`, `@ui.Table`,
`@ui.EmptyState`) are already present in `backend/internal/web/ui/`. The CSS classes to be
ported (`.tasks-section`, `.tasks-section-header`, `.task-row`, `.task-check`, `.task-body`,
`.task-meta`, `.tasks-add-button`, `.task-list`, `.project-progress-track`,
`.project-progress-bar`) are verbatim in `go-backend/internal/web/ui/app.css` and need to be
appended to `backend/internal/web/ui/app.css`. New classes needed for this phase
(`.kanban-column`, `.etape-group`, `.etape-group-header`, `.etape-group-color-dot`,
`.etape-group-label`, `.tab-nav`, `.tab-nav-item`, `.tablo-metadata-row`,
`.tablo-metadata-date`, `.task-list-empty`) are fully specified in the UI-SPEC.
The single structural Go-level change is etape grouping inside `KanbanColumn`: a
`groupTasksByEtape` helper (parallel to the existing `groupTasksByStatus`) produces per-etape
task slices for server-side rendering inside each column, eliminating the need for
`EtapeStrip` in the tasks tab.
**Primary recommendation:** Implement in four incremental waves: (1) CSS additions to
`app.css`, (2) header + metadata + tab nav + overview tab in `tablos.templ`, (3) kanban
restyling + etape grouping in `tasks.templ` and `etapes.templ` (EtapeStrip removal), (4)
files section restyling in `files.templ`. Each wave can be verified independently with the
existing test suite.
---
<user_constraints>
## User Constraints (from CONTEXT.md)
### Locked Decisions
**Header (D-H01 through D-H06)**
- Header uses `.project-card-top` layout (already in `app.css`): tablo color circle avatar
(left) + inline-editable title zone + action controls (right)
- Action controls: Discussion link button, Invite button, Delete icon button — all restyled
with `ui.Button` / `ui.IconButton`; hardcoded `#804EEC` replaced with design tokens
- Tablo color avatar: `.project-avatar` pattern with inline `background-color` style;
falls back to `var(--color-project-fallback)` when empty
- Metadata row (Created date, Status pill, Progress bar): kept, restyled with design tokens;
status pill uses `@ui.Badge(...)`
- Description zone moves from persistent header into the Overview tab content
- Tab nav bar: replace hardcoded color classes with `.tab-nav` / `.tab-nav-item.is-active`
design token CSS; preserve all 5 existing tabs and their HTMX behavior
**Kanban (D-K01 through D-K04)**
- Horizontal 3-column layout stays; Sortable.js wiring unchanged
- Column headers use `.tasks-section-header` CSS: status label + task count badge + Add task
button on right
- Task cards use row style matching `.tasks-section` layout: checkbox + title + meta
- `KanbanBoard`, `KanbanColumn`, `TaskCard` restyled in-place; no endpoint/handler changes
**Etapes (D-E01 through D-E05)**
- `EtapeStrip` removed from the tasks tab UI
- Tasks grouped by etape inside each kanban column (server-side grouping in `KanbanColumn`)
- Etape group headings always expanded, no collapse toggle
- Grouping computed server-side; unassigned tasks rendered last under "No etape" label
- `EtapeFilter` type retained for handler compatibility; filter defaults to "all" permanently
**Files (D-F01 through D-F04)**
- Files section header: `.overview-section-heading` with "Files" label + upload trigger right
- File list: `@ui.Table(...)` with columns Filename / Size / Uploaded / Actions
- Each file row: Download `@ui.IconButton` + Delete `@ui.IconButton`; existing
`FileDeleteConfirmFragment` preserved intact
- Empty state: `@ui.EmptyState(...)` replaces `FileListEmpty()`
### Claude's Discretion
- Exact sub-heading style for etape groups inside kanban columns
- Whether `EtapeFilter` param is fully removed from all handlers or just hidden from UI
(recommend keeping the type for handler compatibility, defaulting to "show all")
- Exact badge token for the "In progress" status pill (BadgeVariantPrimary or
BadgeVariantNeutral per design token alignment)
- Tab nav active state CSS — whether to use `.is-active` class or inline conditional classes
(match Phase 15 sidebar-nav-item pattern)
### Deferred Ideas (OUT OF SCOPE)
- Collapse toggle for etape groups
- Mobile-responsive kanban
- Etape management UI from within the tablo detail
- Task priority/assignee display in task rows
- Files preview pane or thumbnail generation
</user_constraints>
---
<phase_requirements>
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|------------------|
| DETAIL-01 | Tablo detail header area matches the project-card-top design | `.project-card-top` already in `app.css` (Section 15); `project-avatar` and `project-card-title-row` already defined; `@ui.IconButton` and `@ui.Button` ready |
| DETAIL-02 | Task kanban board uses the tasks-section design (section header, task rows, add control) | `.tasks-section*` and `.task-*` CSS in `go-backend/app.css` lines 12531348; `KanbanBoard` / `KanbanColumn` / `TaskCard` components identified for in-place restyling |
| DETAIL-03 | Etapes section is visually consistent with the task section | Server-side grouping required; `groupTasksByStatus` pattern (lines 1218 of `tasks.templ`) is the model for `groupTasksByEtape`; `EtapeStrip` call in `TasksTabFragment` + `TaskCardGone` + `TaskCardOOB` needs removal |
| DETAIL-04 | Files section uses the table component | `@ui.Table` exists in `backend/internal/web/ui/table.templ`; `@ui.EmptyState` exists; `FileListRow` and `FilesTabFragment` are the targets; `FileDeleteConfirmFragment` preserved unchanged |
</phase_requirements>
---
## Architectural Responsibility Map
| Capability | Primary Tier | Secondary Tier | Rationale |
|------------|-------------|----------------|-----------|
| Header layout restyling | Frontend Server (templ) | CSS | Go template renders HTML; CSS controls visual |
| Tab nav active state | Frontend Server (templ) | CSS | Active tab computed server-side (Go `activeTab` param) |
| Etape grouping | Frontend Server (templ/Go) | — | D-E04: grouping is server-side in `KanbanColumn`; pure Go logic |
| CSS class additions | CDN / Static (CSS file) | — | `app.css` is a static asset served by the Go embed |
| Task kanban restyling | Frontend Server (templ) | CSS | Template structure + CSS classes |
| File table restyling | Frontend Server (templ) | CSS | `@ui.Table` component already handles structure |
---
## Standard Stack
### Core (already in `backend/`)
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| `github.com/a-h/templ` | v0.3.1020 (pinned) | Type-safe Go HTML templates | Project standard; all UI is templ |
| Custom CSS + design tokens | — | Visual styling via `var(--...)` tokens | Established in Phase 13; no Tailwind utility classes in new CSS |
| HTMX | v2 (pinned in justfile) | Server-driven interactivity | Project constraint; no JS framework |
| Sortable.js | v1.15.7 (pinned) | Drag-and-drop kanban | Already wired in `KanbanBoard`; must not be changed |
### Supporting UI Components (all already exist in `backend/internal/web/ui/`)
| Component | Templ function | Phase 13 status |
|-----------|---------------|-----------------|
| Badge | `@ui.Badge(ui.BadgeProps{...})` | Complete |
| Button | `@ui.Button(ui.ButtonProps{...})` | Complete |
| IconButton | `@ui.IconButton(ui.IconButtonProps{...})` | Complete |
| Table | `@ui.Table(ui.TableProps{Head: ..., Body: ...})` | Complete |
| EmptyState | `@ui.EmptyState(ui.EmptyStateProps{...})` | Complete |
**Key gap discovered:** The `UIIcon` switch in `icon_button.templ` supports: `plus`,
`grid3x3`, `list`, `filter`, `search`, `calendar`, `pencil`, `trash`. It does NOT have a
`download` icon or a `chat`/`message` icon. The `IconButton` component's `default` case
renders the icon name as raw text. [VERIFIED: read `icon_button.templ` lines 1973]
**Action required:** Add `download` and `chat` (or `message-circle`) cases to the `UIIcon`
switch before using `@ui.IconButton` for the Discussion link and file Download buttons.
These are SVG additions only — no logic change.
---
## Architecture Patterns
### System Architecture Diagram
```
Browser GET /tablos/{id}
|
v
TabloDetailPage (tablos.templ)
├── AppLayout (layout.templ) — sidebar, shell
├── Header zone ──── .project-card-top
│ ├── .project-card-title-row: .project-avatar + .tablo-title-zone
│ └── action controls: IconButton(chat) + Button(invite) + IconButton(trash)
├── Metadata row ──── .tablo-metadata-row
│ ├── .tablo-metadata-date + @ui.Badge(status) + progress track/bar
├── Tab nav ───────── .tab-nav
│ └── .tab-nav-item[.is-active] × 5 tabs
│ hx-get → /tablos/{id}/{tab} → #tab-content innerHTML
└── #tab-content
├── overview → TabloOverviewTabFragment (desc zone moved here)
├── tasks → TasksTabFragment
│ └── KanbanBoard → KanbanColumn × 3
│ └── .tasks-section
│ ├── .tasks-section-header (h3 + badge + add button)
│ └── .task-list
│ └── .etape-group × N [server-side grouped]
│ ├── .etape-group-header (dot + name)
│ └── .task-row × M (checkbox + body + meta)
├── files → FilesTabFragment
│ ├── .overview-section-heading (h3 + upload button)
│ ├── @ui.Table (filename / size / date / actions)
│ └── @ui.EmptyState (when no files)
├── discussion → DiscussionTabFragment (unchanged)
└── events → EventsTabFragment (unchanged)
```
### Recommended Project Structure
No new directories. All changes are in-place edits to existing files:
```
backend/
├── internal/web/ui/
│ ├── app.css ← append CSS sections (tasks, etape-group, tab-nav, metadata, progress)
│ └── icon_button.templ ← add "download" and "chat" icon cases to UIIcon switch
└── templates/
├── tablos.templ ← restyle TabloDetailPage header, tab nav, TabloOverviewTabFragment
├── tasks.templ ← restyle KanbanBoard, KanbanColumn, TaskCard; add etape grouping
├── etapes.templ ← remove EtapeStrip (keep EtapeEdit/Create/DeleteConfirmFragment)
└── files.templ ← restyle FilesTabFragment with @ui.Table + @ui.EmptyState
```
### Pattern 1: Server-Side Etape Grouping in KanbanColumn
**What:** Mirror `groupTasksByStatus` with a `groupTasksByEtape` helper that returns an
ordered structure for rendering inside a column.
**When to use:** Called from within the `KanbanColumn` templ component via a Go expression.
```go
// Source: [VERIFIED: tasks.templ lines 12-18 — groupTasksByStatus is the model]
// EtapeGroup holds tasks for a single etape label within a kanban column.
type EtapeGroup struct {
EtapeID string // "" for unassigned
EtapeTitle string // "No etape" for unassigned
EtapeColor string // "" for unassigned
Tasks []sqlc.Task
}
// groupTasksByEtape groups a task slice into ordered EtapeGroup slices.
// Preserves etape declaration order; unassigned tasks always appear last.
func groupTasksByEtape(tasks []sqlc.Task, etapes []sqlc.Etape) []EtapeGroup {
// Build index: etapeID → etape for O(1) lookup
// Iterate etapes in order to build groups
// Append unassigned group at end if any tasks have nil EtapeID
}
```
The `KanbanColumn` signature gains an `etapes []sqlc.Etape` parameter. Since
`KanbanColumn` is called from `KanbanBoard`, `KanbanBoard` also receives `etapes` and
passes it through. `KanbanBoard` already receives tasks and filter — adding etapes matches
the existing `TasksTabFragment` signature pattern.
**KanbanBoard current signature:**
```go
// Source: [VERIFIED: tasks.templ line 23]
templ KanbanBoard(tabloID uuid.UUID, csrfToken string, tasks []sqlc.Task, filter EtapeFilter)
```
**Updated signature:**
```go
templ KanbanBoard(tabloID uuid.UUID, csrfToken string, tasks []sqlc.Task, filter EtapeFilter, etapes []sqlc.Etape)
```
All `KanbanBoard` call sites must be updated:
- `tablos.templ` line 413: `@KanbanBoard(tablo.ID, csrfToken, tasks, filter)` [VERIFIED]
- `handlers_tasks.go` lines 589, 639: `templates.KanbanBoard(tablo.ID, csrf.Token(r), tasks, filter)` [VERIFIED]
### Pattern 2: Tab Nav Active State via CSS Class
**What:** Replace the inline conditional class strings (hardcoded `#804EEC` hex) with
`.tab-nav-item` + `.is-active` modifier. Match the Phase 15 `sidebar-nav-item.is-active`
pattern already in `app.css`.
```templ
// Source: [VERIFIED: tablos.templ lines 306-310, 321-325 — current pattern]
// Current (hardcoded hex):
if activeTab == "overview" || activeTab == "" {
class="flex items-center gap-2 pb-3 px-1 text-sm font-semibold ... text-[#804EEC] border-[#804EEC]"
} else {
class="flex items-center gap-2 pb-3 px-1 text-sm font-semibold ... text-[#667085] hover:text-gray-900"
}
// New (design token classes):
if activeTab == "overview" || activeTab == "" {
class="tab-nav-item is-active"
} else {
class="tab-nav-item"
}
```
### Pattern 3: Project Avatar in Header Context
The `.project-avatar` CSS class with inline `background-color` style is already implemented
in `TabloProjectCard` (Phase 15) and the CSS is in `app.css`. Reuse verbatim for the detail
header.
```templ
// Source: [VERIFIED: tablos.templ lines 104-107 — Phase 15 pattern]
if card.Tablo.Color.Valid && card.Tablo.Color.String != "" {
<span class="project-avatar" style={ "background-color: " + card.Tablo.Color.String }>
{ string([]rune(card.Tablo.Title)[0:1]) }
</span>
} else {
<span class="project-avatar"></span>
}
```
Note: The UI-SPEC requires the first character of the title to appear inside the avatar as
text. The current `TabloProjectCard` implementation shows an empty avatar (no character).
Phase 16 should render the first character per the UI-SPEC.
### Pattern 4: EtapeStrip Removal and OOB Impact
`EtapeStrip` is called from three sites [VERIFIED]:
1. `tablos.templ` `TasksTabFragment` line 412: `@EtapeStrip(tablo.ID, etapes, counts, filter, csrfToken, false)`
2. `tasks.templ` `TaskCardGone` line 386: `@EtapeStrip(tabloID, etapes, counts, filter, csrfToken, true)` (OOB)
3. `tasks.templ` `TaskCardOOB` line 398: `@EtapeStrip(tabloID, etapes, counts, filter, csrfToken, true)` (OOB)
After removal:
- Site 1: Simply delete the `@EtapeStrip(...)` call from `TasksTabFragment`. Also remove
the `#etape-strip` div from the tasks tab since it no longer exists.
- Sites 2 and 3: Remove the `@EtapeStrip(...)` OOB call. Since EtapeStrip no longer renders
the `id="etape-strip"` element, the OOB swap target won't exist — removing the call is
safe and required.
- The `EtapeStrip` templ function itself should be **retained** in `etapes.templ` but not
called from anywhere. The `etapes []sqlc.Etape`, `counts EtapeTaskCounts`, and
`filter EtapeFilter` parameters continue flowing through handlers and `TasksTabFragment`
unchanged (they feed `KanbanBoard` for etape grouping).
### Anti-Patterns to Avoid
- **Changing handler signatures:** `TabloTasksTabHandler`, `TaskCreateHandler`,
`TaskDeleteHandler`, `TaskReorderHandler`, etc. must not change. All 19 task tests and
9 file tests verify handler behavior — any signature change breaks them.
- **Removing `EtapeFilter` from handler flow:** The filter is still parsed in
`handlers_tasks.go` `loadTasksTabData` and passed through templates. Only the UI element
that generated the filter value (`EtapeStrip`) is removed. The filter param stays "all"
because nothing sets it anymore.
- **Using Tailwind utility classes in new CSS:** All new CSS in `app.css` must use
`var(--...)` tokens. Tailwind is compiled separately — adding `tw-` utility classes to
`.templ` files risks class name collisions (Tailwind uses `@layer` and purging).
- **Modifying `FileDeleteConfirmFragment`:** This fragment's HTMX `hx-target` points to
`.file-row-zone`. The new `@ui.Table`-based layout must preserve `<tr>` elements with
`class="file-row-zone"` and `id="file-{id}"` so the existing delete confirm fragment's
outerHTML swap continues to work.
- **Nesting OOB elements:** `TaskCardOOB` and `TaskCardGone` use `hx-swap-oob` — these
must remain top-level siblings, never nested inside the primary swap target.
---
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Status badge / count badge | Custom `<span>` with inline styles | `@ui.Badge(...)` | Phase 13 established component; CSS already defined |
| Action buttons (Discussion, Invite, Delete) | Raw `<button>` or `<a>` with hardcoded classes | `@ui.IconButton(...)` / `@ui.Button(...)` | Consistent hover/focus states, aria-label enforcement |
| File list | Custom `<ul>` / `<table>` | `@ui.Table(...)` | DETAIL-04 requirement; table component handles `ui-table-shell` CSS |
| Empty state | Custom `<p>` italic placeholder | `@ui.EmptyState(...)` | DS-07 requirement; consistent padding and typography |
| Design tokens | Hardcoded hex values | `var(--color-*)` references | All hex must be replaced — this is the core requirement of the phase |
---
## Runtime State Inventory
This phase is a visual restyling only. No data migrations, no stored keys, no OS-registered
state.
| Category | Items Found | Action Required |
|----------|-------------|------------------|
| Stored data | None — visual CSS/templ changes only | None |
| Live service config | None | None |
| OS-registered state | None | None |
| Secrets/env vars | None | None |
| Build artifacts | `templ generate` must run after `.templ` file edits | Run `just generate` (or `templ generate`) after each templ edit |
---
## Common Pitfalls
### Pitfall 1: `UIIcon` Missing `download` and `chat` Icon Cases
**What goes wrong:** `@ui.IconButton(ui.IconButtonProps{Icon: "download", ...})` renders
the literal string `"download"` as button text instead of an SVG — the `default` case in
`UIIcon` emits `<span>{kind}</span>`.
**Why it happens:** The `UIIcon` switch was built incrementally; only icons needed by prior
phases were added. `download` and a discussion/chat icon were never added.
**How to avoid:** Wave 0 task — add `download` and `chat` (or `message-circle`) SVG cases
to `UIIcon` in `icon_button.templ` before implementing header or file row IconButtons.
**Warning signs:** Rendered button shows "download" or "chat" as text rather than an icon.
### Pitfall 2: `@ui.Table` Requires Wrapping `<tr>` in Body for HTMX File Delete
**What goes wrong:** The existing file delete flow uses `hx-target="closest .file-row-zone"`
with `hx-swap="outerHTML"`. If `FileListRow` is rendered as a `<tr>` inside
`@ui.Table`, the `class="file-row-zone"` and `id="file-{id}"` must be on the `<tr>` element.
If the structure changes (e.g., wrapping in a `<div>`), the outerHTML swap target breaks.
**Why it happens:** `@ui.Table` renders a `<table>` / `<tbody>` — rows must be `<tr>`
elements. The HTMX swap target class must be on the `<tr>` itself.
**How to avoid:** `FileListRow` should render a `<tr class="file-row-zone" id="file-{id}">`.
`FileDeleteConfirmFragment` similarly renders a `<tr class="file-row-zone" id="file-{id}">`.
Both use outerHTML swap — the container element class must match.
**Warning signs:** Delete confirm button causes a full-page reload or HTMX swap error in
browser DevTools.
### Pitfall 3: `KanbanBoard` Call Site Count
**What goes wrong:** `KanbanBoard` is called from three places; missing any one causes a
compile error (templ components are type-checked at generation time) or a runtime panic.
**Why it happens:** `tablos.templ` has one call; `handlers_tasks.go` has two. All three must
receive the new `etapes []sqlc.Etape` parameter when the signature is extended.
**How to avoid:** Run `templ generate && go build ./...` after changing the `KanbanBoard`
signature. Compiler catches all missing parameters immediately.
**Warning signs:** `go build` errors referencing `KanbanBoard` argument count mismatch.
### Pitfall 4: EtapeStrip OOB Removal Leaves Orphaned Swap Target
**What goes wrong:** `TaskCardGone` and `TaskCardOOB` currently emit an OOB swap for
`#etape-strip`. After `EtapeStrip` is removed from the tasks tab, the `#etape-strip` element
no longer exists in the DOM. HTMX silently ignores OOB swaps for missing targets (no error),
but the calls are dead code and should be removed.
**Why it happens:** The OOB pattern was added to keep the etape filter counts fresh after
task operations. With EtapeStrip gone, there is nothing to refresh.
**How to avoid:** Remove the `@EtapeStrip(...)` lines from both `TaskCardGone` and
`TaskCardOOB` as part of the EtapeStrip removal task.
**Warning signs:** Unused `etapes` and `counts` parameters passed to `TaskCardGone` and
`TaskCardOOB` — these can be removed from those component signatures after EtapeStrip removal.
### Pitfall 5: `tasks-section-header h3` Font Size Override
**What goes wrong:** The base `.tasks-section-header h3` rule is 1.6rem (ported from
go-backend's 1.75rem). Inside 18rem-wide kanban columns, 1.6rem headings overflow or look
oversized.
**Why it happens:** The base rule is designed for full-width list views. Kanban columns are
narrow (18rem fixed).
**How to avoid:** Add the scoped override exactly as specified in the UI-SPEC:
```css
.kanban-column .tasks-section-header h3 { font-size: 1rem; }
```
This scoped rule does not change the base rule — future list-view use is unaffected.
### Pitfall 6: `TabloDeleteButtonFragment` Uses `@ui.Button` Not `@ui.IconButton`
**What goes wrong:** The current `TabloDeleteButtonFragment` renders a `@ui.Button` labelled
"Delete". The design decision (D-H02) requires a trash `@ui.IconButton` in the detail header.
However, `TabloDeleteButtonFragment` is also used by `TabloProjectCard` on the dashboard —
changing it breaks the dashboard card.
**Why it happens:** The fragment is the "canonical single source of truth" for the delete
zone (documented in `tablos.templ` comment). The detail page header and the dashboard card
share it.
**How to avoid:** Either (a) parameterize `TabloDeleteButtonFragment` with a variant enum, or
(b) introduce a new `TabloDetailDeleteButton` inline in `TabloDetailPage` that does NOT use
`TabloDeleteButtonFragment`, while leaving the existing fragment for dashboard cards. Option
(b) is simpler and avoids parameter threading.
**Warning signs:** Dashboard card delete button visual changes unexpectedly, or detail page
delete button doesn't match the icon-button style.
---
## Code Examples
### Verified Pattern: `groupTasksByStatus` (model for `groupTasksByEtape`)
```go
// Source: [VERIFIED: backend/templates/tasks.templ lines 12-18]
func groupTasksByStatus(tasks []sqlc.Task) map[sqlc.TaskStatus][]sqlc.Task {
result := make(map[sqlc.TaskStatus][]sqlc.Task, len(TaskColumns))
for _, t := range tasks {
result[t.Status] = append(result[t.Status], t)
}
return result
}
```
### Verified Pattern: `@ui.Badge` for task count in column header
```go
// Source: [VERIFIED: backend/templates/tasks.templ lines 111-112]
<span id={ "task-count-badge-" + string(status) }>
@ui.Badge(ui.BadgeProps{Label: strconv.Itoa(len(tasks)), Variant: ui.BadgeVariantInfo})
</span>
```
### Verified Pattern: `project-avatar` color fallback
```go
// Source: [VERIFIED: backend/templates/tablos.templ lines 104-107]
if card.Tablo.Color.Valid && card.Tablo.Color.String != "" {
<span class="project-avatar" style={ "background-color: " + card.Tablo.Color.String }></span>
} else {
<span class="project-avatar"></span>
}
```
### Verified Pattern: `@ui.Table` component signature
```go
// Source: [VERIFIED: backend/internal/web/ui/table.templ]
type TableProps struct {
Head templ.Component
Body templ.Component
}
// Usage:
@ui.Table(ui.TableProps{
Head: fileTableHead(),
Body: fileTableBody(tabloID, files),
})
```
### Verified Pattern: `@ui.EmptyState` component signature
```go
// Source: [VERIFIED: backend/internal/web/ui/empty_state.templ]
@ui.EmptyState(ui.EmptyStateProps{
Title: "No files yet",
Description: "Upload your first file to get started.",
})
```
### CSS to Port: `.tasks-section` block (from go-backend)
```css
/* Source: [VERIFIED: go-backend/internal/web/ui/app.css lines 1253-1348] */
.tasks-section {
background: var(--color-surface-default);
border: 1px solid var(--color-border-subtle);
border-radius: 1rem;
overflow: hidden;
}
.tasks-section-header {
align-items: center;
border-bottom: 1px solid var(--color-border-muted);
display: flex;
justify-content: space-between;
padding: 1.2rem 1rem;
}
.tasks-add-button {
align-items: center;
background: var(--color-surface-default);
border: 1px solid var(--color-border-muted);
border-radius: 0.7rem;
color: var(--color-text-secondary);
cursor: pointer;
display: inline-flex;
font-weight: 500;
gap: 0.5rem;
min-height: 2.75rem;
padding: 0.7rem 1rem;
}
.task-list { display: flex; flex-direction: column; }
.task-row {
align-items: center;
border-bottom: 1px solid var(--color-border-muted);
cursor: pointer;
display: flex;
gap: 0.75rem;
padding: 0.9rem 1rem;
transition: background-color 0.2s ease;
}
.task-row:hover { background: var(--color-surface-neutral-hover); }
.task-check {
align-items: center;
background: var(--color-surface-default);
border: 2px solid var(--color-border-strong);
border-radius: 999px;
color: var(--color-text-inverse);
cursor: pointer;
display: inline-flex;
flex-shrink: 0;
height: 2rem;
justify-content: center;
width: 2rem;
}
.task-check.is-complete {
background: var(--color-text-brand-strong);
border-color: var(--color-text-brand-strong);
}
.task-body { flex: 1; min-width: 0; }
.task-body p {
color: var(--color-surface-muted-inverse);
font-size: 0.95rem;
font-weight: 500;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-row.is-complete .task-body p {
color: var(--color-text-faint);
text-decoration: line-through;
}
.task-meta {
align-items: center;
color: var(--color-text-muted);
display: flex;
flex-wrap: wrap;
font-size: 0.75rem;
gap: 0.45rem;
margin-top: 0.3rem;
}
```
### CSS to Port: Progress bar (from go-backend)
```css
/* Source: [VERIFIED: go-backend/internal/web/ui/app.css lines 1061-1071] */
.project-progress-track {
background: var(--color-surface-muted);
border-radius: 999px;
height: 0.5rem;
overflow: hidden;
}
.project-progress-bar {
background: var(--project-color, var(--color-project-fallback));
border-radius: 999px;
height: 100%;
}
```
---
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Hardcoded `#804EEC` and `#667085` hex in class attributes | `var(--color-brand-primary)` and `var(--color-text-muted)` design tokens | Phase 13 (tokens), Phase 15 (applied to dashboard) | Phase 16 applies same token discipline to detail page |
| Tailwind utility classes for all styling | CSS classes with design tokens for new UI | Phase 13 | New CSS in `app.css`; existing Tailwind utilities in edit forms are left in place |
| EtapeStrip filter pills | Server-side etape grouping in kanban columns | Phase 16 (this phase) | Simpler mental model; no filter state to manage |
| Flat task list per column | Etape-grouped task list per column | Phase 16 (this phase) | Tasks organized by etape within each status column |
---
## Assumptions Log
| # | Claim | Section | Risk if Wrong |
|---|-------|---------|---------------|
| A1 | The first character of `tablo.Title` is safe to render with `string([]rune(title)[0:1])` — titles are never empty (DB NOT NULL + required form validation) | Code Examples, Pattern 3 | Panic on empty title; add len guard | [ASSUMED] |
| A2 | `ButtonVariantGhost` renders with a border when used for "Invite Member" (matches the `border border-[#804EEC]` current style) | User Constraints D-H02 | Invite button may appear without visible border if ghost variant has no border CSS | [ASSUMED] |
| A3 | `FileDeleteConfirmFragment` can be changed to use `<tr>` as its outer element (instead of `<div>`) without breaking its HTMX outerHTML swap behavior | Pitfall 2 | Test breakage in file delete confirm flow | [ASSUMED] |
---
## Open Questions
1. **`ButtonVariantGhost` visual treatment**
- What we know: `ButtonVariantGhost` exists and produces `ui-button ui-button-ghost ui-button-md` classes [VERIFIED: `variants.go` lines 162-163]
- What's unclear: Whether the ghost CSS renders a visible border (matching the current `border border-[#804EEC]` Invite button) or is borderless
- Recommendation: Check `app.css` for `.ui-button-ghost` definition; if borderless, use `ButtonVariantDefault` with `ButtonToneSoft` or add a ghost-outline variant
2. **`TaskCardGone` and `TaskCardOOB` parameter cleanup after EtapeStrip removal**
- What we know: Both components receive `etapes []sqlc.Etape` and `counts EtapeTaskCounts` solely for the `EtapeStrip` OOB call [VERIFIED: tasks.templ lines 384-398]
- What's unclear: Whether removing those parameters would require handler signature changes that could break tests
- Recommendation: Remove the `@EtapeStrip(...)` OOB calls from both components but keep the parameters (simpler, no handler changes); mark them with a `// TODO: remove after Phase 16` comment for a future cleanup
3. **`FileDeleteConfirmFragment` using `<tr>` as outer element**
- What we know: Currently renders `<div class="file-row-zone">` [VERIFIED: files.templ line 105]
- What's unclear: Whether HTMX outerHTML swap of a `<tr>` inside `<tbody>` works correctly (some browsers reject non-`<tr>` children in `<tbody>`, but the reverse — replacing a `<tr>` — should work)
- Recommendation: Use `<tr class="file-row-zone" id="file-{id}">` as the wrapper in both `FileListRow` and `FileDeleteConfirmFragment`; this is valid HTML within a `<tbody>`
---
## Environment Availability
Step 2.6: SKIPPED — Phase 16 is purely CSS/templ changes. No external services, runtimes, or CLIs beyond the existing `just generate` (templ) and `go build` that the project already uses.
---
## Validation Architecture
### Test Framework
| Property | Value |
|----------|-------|
| Framework | Go testing + httptest (standard library) |
| Config file | none — `go test ./...` discovers all `_test.go` files |
| Quick run command | `go test ./backend/internal/web/... -run TestTask -count=1` |
| Full suite command | `TEST_DATABASE_URL='postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable' go test ./... -count=1` |
### Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| DETAIL-01 | Header renders with project-card-top layout | smoke (template render) | `go test ./backend/internal/web/... -run TestTablos -count=1` | ✅ `handlers_tablos_test.go` |
| DETAIL-02 | Task kanban renders with tasks-section CSS classes | smoke (template render) | `go test ./backend/internal/web/... -run TestTasksKanbanRenders -count=1` | ✅ `handlers_tasks_test.go` line 44 |
| DETAIL-03 | Etapes section consistent — no EtapeStrip, grouping in columns | smoke + integration | `TEST_DATABASE_URL=... go test ./backend/internal/web/... -run TestTask -count=1` | ✅ 19 task tests + 8 etape tests |
| DETAIL-04 | Files section uses table component | smoke (template render) | `go test ./backend/internal/web/... -run TestFilesTab -count=1` | ✅ `handlers_files_test.go` line 292 |
### Sampling Rate
- **Per task commit:** `go test ./backend/internal/web/... -count=1` (unit tests, no DB)
- **Per wave merge:** `TEST_DATABASE_URL='postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable' go test ./... -count=1`
- **Phase gate:** Full suite green before `/gsd-verify-work`
### Wave 0 Gaps
- [ ] Add `download` icon SVG case to `UIIcon` switch in `backend/internal/web/ui/icon_button.templ`
- [ ] Add `chat` (or `message-circle`) icon SVG case to `UIIcon` switch
- [ ] Verify `.ui-button-ghost` CSS rule in `app.css` renders a visible border (or determine correct variant for Invite button)
*(Existing test infrastructure covers all phase requirements — no new test files needed. Wave 0 is icon additions + CSS verification only.)*
---
## Security Domain
> This phase is purely visual restyling — no new routes, no authentication changes, no user
> input validation changes, no cryptographic operations. All existing security controls
> (CSRF tokens, ownership checks via `loadOwnedTablo`, auth session middleware) are
> preserved unchanged.
| ASVS Category | Applies | Standard Control |
|---------------|---------|-----------------|
| V2 Authentication | no | unchanged |
| V3 Session Management | no | unchanged |
| V4 Access Control | no | existing `loadOwnedTablo` ownership guard unchanged |
| V5 Input Validation | no | no new form inputs |
| V6 Cryptography | no | no new crypto operations |
No new threat surface introduced.
---
## Sources
### Primary (HIGH confidence)
- `backend/templates/tablos.templ` — verified all current template structures, class names, HTMX wiring
- `backend/templates/tasks.templ` — verified `KanbanBoard`, `KanbanColumn`, `TaskCard`, `EtapeStrip` call sites
- `backend/templates/etapes.templ` — verified `EtapeStrip` structure and all call sites
- `backend/templates/files.templ` — verified `FilesTabFragment`, `FileListRow`, `FileDeleteConfirmFragment` structure
- `backend/internal/web/ui/app.css` — verified which CSS classes are already present vs. need porting
- `backend/internal/web/ui/icon_button.templ` — verified `UIIcon` switch cases; identified missing `download` and `chat` icons
- `backend/internal/web/ui/table.templ` — verified `TableProps` signature
- `backend/internal/web/ui/empty_state.templ` — verified `EmptyStateProps` signature
- `backend/internal/web/ui/variants.go` — verified all variant/tone constant names
- `go-backend/internal/web/ui/app.css` lines 12531348 — verified `.tasks-section*` and `.task-*` CSS; lines 10611071 verified progress bar CSS
- `backend/internal/web/handlers_tasks.go` — verified `KanbanBoard` call sites and `EtapeFilter` flow
- `backend/internal/web/handlers_files.go` — verified `TabloTasksTabHandler` and `KanbanBoard` call site
- `.planning/phases/16-tablo-detail/16-CONTEXT.md` — all decisions, canonical refs, specifics
- `.planning/phases/16-tablo-detail/16-UI-SPEC.md` — full layout contracts, CSS class specifications, typography, spacing
### Secondary (MEDIUM confidence)
- `backend/internal/web/handlers_tasks_test.go` — 19 test functions identified; all must pass unchanged [VERIFIED: file read + grep]
- `backend/internal/web/handlers_files_test.go` — 9 test functions identified; all must pass unchanged [VERIFIED: grep]
---
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH — all components verified in-tree
- Architecture: HIGH — all call sites verified by code read
- Pitfalls: HIGH — identified from direct code inspection; Pitfall 1 (missing icon) is CRITICAL
- CSS porting: HIGH — source CSS verified in go-backend
**Research date:** 2026-05-16
**Valid until:** 2026-06-16 (stable CSS/templ stack; no fast-moving dependencies)