docs(16): research phase domain
This commit is contained in:
parent
30c446fc0e
commit
73780323d2
1 changed files with 772 additions and 0 deletions
772
.planning/phases/16-tablo-detail/16-RESEARCH.md
Normal file
772
.planning/phases/16-tablo-detail/16-RESEARCH.md
Normal 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 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 |
|
||||
</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 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 != "" {
|
||||
<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 1253–1348 — verified `.tasks-section*` and `.task-*` CSS; lines 1061–1071 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)
|
||||
Loading…
Reference in a new issue