diff --git a/.planning/phases/16-tablo-detail/16-RESEARCH.md b/.planning/phases/16-tablo-detail/16-RESEARCH.md new file mode 100644 index 0000000..649ec3d --- /dev/null +++ b/.planning/phases/16-tablo-detail/16-RESEARCH.md @@ -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 (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 + + +--- + + +## 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 1253–1348; `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 12–18 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 | + + +--- + +## 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 19–73] + +**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 != "" { + + { string([]rune(card.Tablo.Title)[0:1]) } + +} else { + +} +``` + +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 `` 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 `` with inline styles | `@ui.Badge(...)` | Phase 13 established component; CSS already defined | +| Action buttons (Discussion, Invite, Delete) | Raw `