diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index fed0787..d755dcd 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -111,6 +111,7 @@ Plans: **Mode:** mvp **Status:** Pending **Requirements:** DETAIL-01, DETAIL-02, DETAIL-03, DETAIL-04 +**Plans:** 4 plans **Success Criteria:** 1. Tablo detail header uses project-card-top layout with title, avatar, and action controls 2. Task kanban uses tasks-section design: section header with add button, task rows with checkbox and meta @@ -118,7 +119,20 @@ Plans: 4. Files section uses the table component with consistent row actions 5. All existing task, etape, and file handler tests pass unchanged -**User-in-loop:** Approve kanban layout shape (whether columns stay horizontal or fold to list) and etape grouping UI before implementation. +Plans: +**Wave 1** +- [ ] 16-01-PLAN.md — CSS foundation + icons: append CSS Sections 19–25 to app.css; add download + chat icon cases to UIIcon switch + +**Wave 2** *(blocked on Wave 1 completion)* +- [ ] 16-02-PLAN.md — Header + tab nav + overview tab: restyle TabloDetailPage header (project-card-top), metadata row, tab nav (design token classes), move desc to overview tab, remove EtapeStrip call from TasksTabFragment + +**Wave 3** *(blocked on Wave 2 completion)* +- [ ] 16-03-PLAN.md — Kanban + etape grouping: groupTasksByEtape helper, restyled KanbanBoard/Column/TaskCard, EtapeStrip OOB removal, handlers_tasks.go call sites updated + +**Wave 4** *(blocked on Wave 3 completion)* +- [ ] 16-04-PLAN.md — Files section: @ui.Table, @ui.EmptyState, FileListRow as tr, FileDeleteConfirmFragment as tr, browser verify checkpoint + +**User-in-loop:** Browser verify checkpoint in Plan 04 — approve visual result before considering the phase complete. ### Phase 17: Chat & Planning **Goal:** Restyle the discussion view and planning page using design system components to make them visually consistent with the rest of the app. diff --git a/.planning/phases/16-tablo-detail/16-01-PLAN.md b/.planning/phases/16-tablo-detail/16-01-PLAN.md new file mode 100644 index 0000000..9b80d08 --- /dev/null +++ b/.planning/phases/16-tablo-detail/16-01-PLAN.md @@ -0,0 +1,202 @@ +--- +phase: 16-tablo-detail +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - backend/internal/web/ui/icon_button.templ + - backend/internal/web/ui/app.css +autonomous: true +requirements: + - DETAIL-01 + - DETAIL-02 + - DETAIL-03 + - DETAIL-04 + +must_haves: + truths: + - "icon_button.templ UIIcon switch contains a case for 'download' that emits a download SVG arrow" + - "icon_button.templ UIIcon switch contains a case for 'chat' that emits a speech-bubble SVG" + - "app.css contains .tasks-section, .task-row, .task-check, .task-list, .task-body, .task-meta, .tasks-add-button CSS rules using var(--...) tokens" + - "app.css contains .project-progress-track and .project-progress-bar CSS rules" + - "app.css contains .tab-nav, .tab-nav-item, .tab-nav-item.is-active, .tab-nav-item:hover CSS rules" + - "app.css contains .tablo-metadata-row and .tablo-metadata-date CSS rules" + - "app.css contains .kanban-column and scoped .kanban-column .tasks-section-header h3 { font-size: 1rem } override" + - "app.css contains .etape-group-header, .etape-group-color-dot, .etape-group-label CSS rules" + - "app.css contains .task-list-empty CSS rule" + - "templ generate && go build ./... exits 0 after changes" + artifacts: + - path: backend/internal/web/ui/icon_button.templ + provides: Download and chat icon SVG cases in UIIcon switch + contains: "case \"download\":" + - path: backend/internal/web/ui/app.css + provides: All new CSS sections for Phase 16 (19–25) + contains: ".tasks-section {" + key_links: + - from: backend/internal/web/ui/icon_button.templ + to: backend/internal/web/ui/icon_button_templ.go + via: templ generate + pattern: "case \"download\"" + - from: backend/internal/web/ui/app.css + to: browser render + via: static asset embed + pattern: ".tab-nav-item.is-active" +--- + +## Phase Goal + +**As a** tablo owner, **I want to** see a fully restyled tablo detail page — header, kanban, etapes, and files — **so that** the detail view looks visually consistent with the design system established in Phases 13–15. + + +Add the two missing icon SVG cases (`download`, `chat`) to `UIIcon` in `icon_button.templ`, and append all Phase 16 CSS sections (19–25) to `app.css`. This is the shared substrate that plans 02–04 build on — without it, `@ui.IconButton` with `Icon: "download"` or `Icon: "chat"` renders the icon name as raw text. + +Purpose: Unblock header restyling (Discussion/Download IconButtons), kanban restyling (task-row CSS), and files restyling (table + icons). +Output: Updated `icon_button.templ` with `download` and `chat` cases; `app.css` with Sections 19–25 appended. + + + +@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/workflows/execute-plan.md +@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/templates/summary.md + + + +@.planning/ROADMAP.md +@.planning/phases/16-tablo-detail/16-CONTEXT.md +@.planning/phases/16-tablo-detail/16-RESEARCH.md +@.planning/phases/16-tablo-detail/16-PATTERNS.md +@.planning/phases/16-tablo-detail/16-UI-SPEC.md + + + + +From backend/internal/web/ui/icon_button.templ (current UIIcon switch structure): +- Package: `package ui` +- Switch starts at line ~18, cases: "plus", "grid3x3", "list", "filter", "search", "calendar", "pencil", "trash" +- `default:` case emits `{kind}` (renders icon name as text — this is the bug) +- Insert `case "download":` and `case "chat":` BEFORE the `default:` case + +From backend/internal/web/ui/app.css (current state): +- File is 450 lines; ends after Section 18 (project-card-top, project-avatar) +- Line 355-361: `.overview-section-heading h3, .tasks-section-header h3` shared rule exists (do NOT add a second `.tasks-section-header h3` rule; add only the scoped `.kanban-column .tasks-section-header h3 { font-size: 1rem; }` override) +- Sections to append: 19 (tasks-section block), 20 (progress bar), 21 (tab nav), 22 (metadata row), 23 (kanban-column wrapper), 24 (etape group), 25 (task-list-empty) +- All token names: `var(--color-surface-default)`, `var(--color-border-subtle)`, `var(--color-border-muted)`, `var(--color-border-strong)`, `var(--color-text-secondary)`, `var(--color-text-muted)`, `var(--color-text-faint)`, `var(--color-text-primary)`, `var(--color-text-brand)`, `var(--color-text-brand-strong)`, `var(--color-text-inverse)`, `var(--color-surface-muted)`, `var(--color-surface-neutral-hover)`, `var(--color-brand-primary)`, `var(--color-project-fallback)`, `var(--project-color, var(--color-project-fallback))` + + + + + + + Task 1: Add `download` and `chat` icon cases to UIIcon switch + backend/internal/web/ui/icon_button.templ + + - backend/internal/web/ui/icon_button.templ (read entire file — ~80 lines — before editing; understand the full switch structure and the `default:` position) + + + Read the full `icon_button.templ` file. Find the UIIcon switch's `default:` case. Insert two new cases immediately before `default:`: + + Case "download": emit an SVG with `viewBox="0 0 24 24"`, `fill="none"`, `stroke="currentColor"`, `stroke-width="2"`, `stroke-linecap="round"`, `stroke-linejoin="round"`, `aria-hidden="true"`. Contents: `path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"`, `polyline points="7 10 12 15 17 10"`, `line x1="12" x2="12" y1="15" y2="3"`. + + Case "chat": emit an SVG with same attributes. Contents: `path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"`. + + Do NOT modify any other part of the file. Do NOT change the `default:` case or any existing cases. + + After editing, run: `just generate` (or `templ generate ./backend/...`) to regenerate `icon_button_templ.go`. + + + grep -c 'case "download"' /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/internal/web/ui/icon_button.templ && grep -c 'case "chat"' /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/internal/web/ui/icon_button.templ && cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source && go build ./backend/... + + + - `icon_button.templ` contains `case "download":` with a polyline + line SVG body + - `icon_button.templ` contains `case "chat":` with a path SVG body + - Both new cases appear before the `default:` case in the switch + - `go build ./backend/...` exits 0 (templ-generated file updated, no compile errors) + - The `default:` case and all 8 existing cases are unchanged + + UIIcon switch contains "download" and "chat" cases; project compiles cleanly. + + + + Task 2: Append CSS Sections 19–25 to app.css + backend/internal/web/ui/app.css + + - backend/internal/web/ui/app.css (read lines 340–450 to confirm the end of the file and the existing shared `.tasks-section-header h3` rule at lines 355–361; verify no duplication before appending) + - .planning/phases/16-tablo-detail/16-PATTERNS.md (Pattern Assignments section: "New CSS blocks to append — Blocks 1, 2, 3" — these are the verbatim CSS blocks to append) + - .planning/phases/16-tablo-detail/16-UI-SPEC.md (New CSS Classes Required section — additional property values that refine the blocks) + + + Append the following seven CSS sections to the END of `backend/internal/web/ui/app.css`. Do NOT modify any existing rules. The shared `.tasks-section-header h3` rule at lines ~355-361 already exists — do NOT add another one; only add the scoped `.kanban-column .tasks-section-header h3 { font-size: 1rem; }` override. + + Append in order: + + Section 19 — Tasks section (ported from go-backend): `.tasks-section`, `.tasks-section-header`, `.tasks-add-button`, `.task-list`, `.task-row`, `.task-row:hover`, `.task-check`, `.task-check.is-complete`, `.task-body`, `.task-body p`, `.task-row.is-complete .task-body p`, `.task-meta`. All values from PATTERNS.md Block 1 verbatim. + + Section 20 — Progress bar: `.project-progress-track`, `.project-progress-bar`. All values from PATTERNS.md Block 2 verbatim. + + Section 21 — Tab nav: `.tab-nav`, `.tab-nav-item`, `.tab-nav-item.is-active`, `.tab-nav-item:hover:not(.is-active)`. Values from PATTERNS.md Block 3 / UI-SPEC. + + Section 22 — Tablo detail metadata row: `.tablo-metadata-row`, `.tablo-metadata-date`, `.tablo-metadata-date svg`. Values from PATTERNS.md Block 3 / UI-SPEC. + + Section 23 — Kanban column wrapper: `.kanban-column { flex-shrink: 0; width: 18rem; }` and `.kanban-column .tasks-section-header h3 { font-size: 1rem; }`. + + Section 24 — Etape group sub-headings: `.etape-group-header`, `.etape-group-color-dot`, `.etape-group-label`, `.etape-group-label.is-unassigned`. Values from PATTERNS.md Block 3 / UI-SPEC. + + Section 25 — Empty task list: `.task-list-empty { color: var(--color-text-faint); font-size: 0.875rem; font-style: italic; padding: 0.75rem 1rem; }`. + + Every CSS value must use `var(--...)` tokens. No hardcoded hex values. No Tailwind utility classes. + + + grep -c "\.tasks-section {" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/internal/web/ui/app.css && grep -c "\.tab-nav-item\.is-active" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/internal/web/ui/app.css && grep -c "\.kanban-column" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/internal/web/ui/app.css && grep -c "\.etape-group-header" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/internal/web/ui/app.css && grep -c "\.task-list-empty" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/internal/web/ui/app.css + + + - `app.css` contains exactly one `.tasks-section {` rule (no duplication) + - `app.css` contains `.tab-nav-item.is-active` with `border-bottom-color: var(--color-brand-primary)` and `color: var(--color-text-brand)` + - `app.css` contains `.kanban-column` with `width: 18rem` + - `app.css` contains `.kanban-column .tasks-section-header h3` with `font-size: 1rem` + - `app.css` contains `.etape-group-header` with `background: var(--color-surface-muted)` + - `app.css` contains `.task-list-empty` with `font-style: italic` + - `app.css` contains `.project-progress-track` and `.project-progress-bar` + - `grep -c "#[0-9a-fA-F]\{3,6\}" app.css` returns 0 for newly appended lines (no hardcoded hex in new rules) + - `go build ./backend/...` exits 0 (CSS is a static asset, build verifies no templ regressions) + + app.css has all Phase 16 CSS sections appended; no hardcoded hex values; project compiles cleanly. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Static asset → browser | CSS and templ-generated HTML served as static embed; no user input processed | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-16-01-01 | Tampering | app.css static embed | accept | CSS is embedded at build time; no runtime user modification possible; existing CSP controls unchanged | +| T-16-01-02 | Information Disclosure | SVG icon markup | accept | SVG icons are decorative, contain no user data; aria-hidden="true" on all decorative SVGs | + +No new trust boundaries. No user input, no authentication changes, no handler changes. + + + +After both tasks complete: +```bash +cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source +go test ./backend/internal/web/... -count=1 +``` +All existing tests must pass. The CSS and icon changes do not affect test behavior (tests verify handler output, not CSS class names in most cases). + + + +- `icon_button.templ` has `case "download":` and `case "chat":` before `default:` +- `app.css` has 7 new sections (19–25) appended with no hardcoded hex values +- `go test ./backend/internal/web/... -count=1` passes with no new failures +- `go build ./backend/...` exits 0 + + + +After completion, create `.planning/phases/16-tablo-detail/16-01-SUMMARY.md` using the summary template. + diff --git a/.planning/phases/16-tablo-detail/16-02-PLAN.md b/.planning/phases/16-tablo-detail/16-02-PLAN.md new file mode 100644 index 0000000..ba1491f --- /dev/null +++ b/.planning/phases/16-tablo-detail/16-02-PLAN.md @@ -0,0 +1,235 @@ +--- +phase: 16-tablo-detail +plan: 02 +type: execute +wave: 2 +depends_on: + - 16-01 +files_modified: + - backend/templates/tablos.templ +autonomous: true +requirements: + - DETAIL-01 + +must_haves: + truths: + - "Tablo detail page header uses .project-card-top layout with color avatar, inline-editable title zone, and action controls" + - "Action controls use @ui.IconButton for Discussion and Delete, @ui.Button for Invite Member — no hardcoded #804EEC hex" + - "Metadata row uses .tablo-metadata-row with created date, @ui.Badge status pill, and .project-progress-track/bar" + - "Tab nav uses .tab-nav / .tab-nav-item / .tab-nav-item.is-active classes replacing hardcoded Tailwind hex classes" + - "Description zone renders inside TabloOverviewTabFragment (not in the persistent header)" + - "EtapeStrip is no longer called from TasksTabFragment; KanbanBoard call passes etapes parameter" + - "All existing tablo handler tests pass unchanged" + artifacts: + - path: backend/templates/tablos.templ + provides: Restyled TabloDetailPage header, tab nav, overview tab, TasksTabFragment EtapeStrip removal + contains: ".project-card-top" + - path: backend/templates/tablos_templ.go + provides: Generated Go from restyled tablos.templ + exports: [] + key_links: + - from: backend/templates/tablos.templ TabloDetailPage + to: backend/internal/web/ui/app.css .project-card-top + via: CSS class on div element + pattern: "project-card-top" + - from: backend/templates/tablos.templ TabloOverviewTabFragment + to: TabloDescDisplay / TabloDescEditFragment + via: "@TabloDescDisplay(tablo, csrfToken)" call inside overview tab + pattern: "TabloDescDisplay" + - from: backend/templates/tablos.templ TasksTabFragment + to: KanbanBoard + via: "@KanbanBoard(tablo.ID, csrfToken, tasks, filter, etapes)" + pattern: "KanbanBoard.*etapes" +--- + + +Restyle `TabloDetailPage` header area (project-card-top layout, color avatar, action controls), metadata row, tab nav bar, and overview tab content. Move the description zone from the persistent header into `TabloOverviewTabFragment`. Remove the `@EtapeStrip` call from `TasksTabFragment` and update the `KanbanBoard` call to pass `etapes`. All HTMX wiring, inline-edit zones, delete flow, and tab routing are preserved unchanged. + +Purpose: Deliver DETAIL-01 — the tablo detail header matches the project-card-top design. +Output: Restyled `tablos.templ` with no hardcoded hex values in the detail page section. + + + +@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/workflows/execute-plan.md +@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/templates/summary.md + + + +@.planning/ROADMAP.md +@.planning/phases/16-tablo-detail/16-CONTEXT.md +@.planning/phases/16-tablo-detail/16-RESEARCH.md +@.planning/phases/16-tablo-detail/16-PATTERNS.md +@.planning/phases/16-tablo-detail/16-UI-SPEC.md +@.planning/phases/16-tablo-detail/16-01-SUMMARY.md + + + + +From backend/templates/tablos.templ (current state — read the file before editing): + +TabloDetailPage signature (line ~200): + templ TabloDetailPage(tablo sqlc.Tablo, tasks []sqlc.Task, etapes []sqlc.Etape, counts EtapeTaskCounts, filter EtapeFilter, csrfToken string, activeTab string, user auth.User) + +Current header structure uses raw inline Tailwind classes with hardcoded #804EEC hex. Replace with design token classes. + +Color avatar pattern from TabloProjectCard (lines ~103–112) — reuse for detail header, adding first-character text: + if tablo.Color.Valid && tablo.Color.String != "" { + span class="project-avatar" style="background-color: {tablo.Color.String}" + if len(tablo.Title) > 0 { string([]rune(tablo.Title)[0:1]) } + } else { + span class="project-avatar" + if len(tablo.Title) > 0 { string([]rune(tablo.Title)[0:1]) } + } + +Tab nav active state — current pattern (lines ~306–325) uses inline Tailwind hex classes per-tab. Replace ALL 5 tabs (overview, tasks, files, discussion, events): + if activeTab == "overview" || activeTab == "" { class="tab-nav-item is-active" } else { class="tab-nav-item" } + +EtapeStrip call in TasksTabFragment (line ~412): DELETE this call + @EtapeStrip(tablo.ID, etapes, counts, filter, csrfToken, false) + +KanbanBoard call in TasksTabFragment (line ~413): UPDATE to add etapes parameter + @KanbanBoard(tablo.ID, csrfToken, tasks, filter) + → @KanbanBoard(tablo.ID, csrfToken, tasks, filter, etapes) + +Delete button in detail header — per Pitfall 6: do NOT modify TabloDeleteButtonFragment (used by dashboard cards). +Create an INLINE IconButton inside a .tablo-delete-zone div in TabloDetailPage header: + div class="tablo-delete-zone" + @ui.IconButton(ui.IconButtonProps{Label: "Delete tablo", Icon: "trash", Variant: ui.IconButtonVariantDanger, Tone: ui.IconButtonToneGhost, Type: "button", Attrs: templ.Attributes{"hx-get": "/tablos/{id}/delete-confirm", "hx-target": "closest .tablo-delete-zone", "hx-swap": "outerHTML"}}) + +Discussion action in header: @ui.IconButton with Icon "chat", Label "Discussion", linking/navigating to discussion tab (ghost/neutral variant). Since Discussion is navigation, wrap in an tag or use hx-get to load the discussion tab fragment: + a href="/tablos/{id}/discussion" hx-get="/tablos/{id}/discussion" hx-target="#tab-content" hx-swap="innerHTML" hx-push-url="true" class="tab-nav-item" aria-label="Go to Discussion tab" + @ui.IconButton(...Icon: "chat"...) -- OR render the button label directly as link text with chat icon + +Invite button: @ui.Button(ui.ButtonProps{Label: "Invite Member", Variant: ui.ButtonVariantGhost, Tone: ui.ButtonToneSolid, Size: ui.SizeMD, Type: "button"}) + +Metadata row: + div class="tablo-metadata-row" + div class="tablo-metadata-date" + [calendar SVG 1rem aria-hidden] + span "Created" + span { tablo.CreatedAt.Time.Format("Jan 2, 2006") } + @ui.Badge(ui.BadgeProps{Label: "In progress", Variant: ui.BadgeVariantPrimary}) + div class="project-progress-track" + div class="project-progress-bar" style="width: 0%;" + +Overview tab (TabloOverviewTabFragment): move the tablo-desc-zone call here (currently in persistent header). The component already exists — just relocate the @TabloDescDisplay(tablo, csrfToken) call inside a div class="tablo-desc-zone" inside TabloOverviewTabFragment. + +Tab nav wrapper: change the outer div for the tab links to div class="tab-nav" (replaces existing raw flex Tailwind classes). + +ui package variant identifiers (from backend/internal/web/ui/variants.go): + ui.ButtonVariantGhost, ui.ButtonVariantDefault, ui.ButtonToneSolid + ui.IconButtonVariantDanger, ui.IconButtonVariantNeutral, ui.IconButtonToneGhost + ui.BadgeVariantPrimary, ui.BadgeVariantInfo + ui.SizeMD + + + + + + + Task 1: Restyle TabloDetailPage header, metadata row, and tab nav + backend/templates/tablos.templ + + - backend/templates/tablos.templ (read the FULL file before editing — you need to understand the current header structure, tab nav structure, action control positions, and the persistent desc zone location; the file is ~650 lines) + - backend/internal/web/ui/variants.go (read to confirm exact Go identifiers for variants/tones: ButtonVariantGhost, IconButtonVariantDanger, IconButtonToneGhost, BadgeVariantPrimary, SizeMD — use these exact names) + - .planning/phases/16-tablo-detail/16-PATTERNS.md (section "backend/templates/tablos.templ" — color avatar pattern, tab nav pattern, IconButton for delete, metadata row pattern, description zone relocation) + + + Edit `backend/templates/tablos.templ` to restyle `TabloDetailPage`. Make the following targeted changes: + + 1. HEADER — replace the current header div (which uses inline Tailwind flex classes with #804EEC) with a `.project-card-top` layout: + - Outer div: `class="project-card-top"` + - Left: `div class="project-card-title-row"` containing the project-avatar span (with tablo color + first char of title per PATTERNS.md color avatar pattern) followed by the existing `.tablo-title-zone` inline-edit zone (`@TabloTitleDisplay(tablo, csrfToken)` preserved as-is) + - Right: action controls row with gap, containing: (a) Discussion `@ui.IconButton` with `Icon: "chat"`, `Label: "Discussion"`, ghost/neutral variant, wrapped so it navigates to the discussion tab (use hx-get="/tablos/{id}/discussion" hx-target="#tab-content" hx-swap="innerHTML" hx-push-url="true" as Attrs); (b) `@ui.Button` for "Invite Member" with `ButtonVariantGhost`; (c) a `.tablo-delete-zone` div containing the trash `@ui.IconButton` (DO NOT use or modify `@TabloDeleteButtonFragment`) + + 2. METADATA ROW — replace the current metadata span/div elements (which use hardcoded color classes) with `div class="tablo-metadata-row"` containing: tablo-metadata-date div (calendar SVG + "Created" + formatted date), `@ui.Badge` for "In progress" with `BadgeVariantPrimary`, and the progress track/bar divs using `.project-progress-track` + `.project-progress-bar` CSS classes (per PATTERNS.md metadata row pattern) + + 3. TAB NAV — change the outer container of the 5 tab links to `div class="tab-nav"`. For each of the 5 tabs (overview, tasks, files, discussion, events): replace the long inline Tailwind class strings (which contain `text-[#804EEC]`, `border-[#804EEC]`, etc.) with the simple conditional: `if activeTab == "{tab}" { class="tab-nav-item is-active" } else { class="tab-nav-item" }`. Preserve all `hx-get`, `hx-target`, `hx-swap`, `hx-push-url`, and `href` attributes on each tab link unchanged. + + 4. DESCRIPTION ZONE RELOCATION — Remove the `tablo-desc-zone` div (containing `@TabloDescDisplay`) from the persistent header area (above the tab nav). Move it into `TabloOverviewTabFragment` as the first element inside that component. The `TabloDescDisplay` and `TabloDescEditFragment` components themselves are not modified — only the call site location changes. + + After editing, run: `just generate` to regenerate `tablos_templ.go`. + + + grep -c "project-card-top" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/tablos.templ && grep -c "tab-nav-item is-active" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/tablos.templ && grep -c "tablo-metadata-row" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/tablos.templ && grep -c "804EEC" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/tablos.templ && cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source && go test ./backend/internal/web/... -run TestTablos -count=1 + + + - `tablos.templ` contains `class="project-card-top"` in TabloDetailPage header + - `tablos.templ` contains `class="tab-nav-item is-active"` (applied conditionally to the active tab) + - `tablos.templ` contains `class="tablo-metadata-row"` + - `tablos.templ` contains `@ui.Badge(ui.BadgeProps{Label: "In progress", Variant: ui.BadgeVariantPrimary})` + - `tablos.templ` contains `.project-progress-track` and `.project-progress-bar` class references + - `grep -c "#804EEC" backend/templates/tablos.templ` returns 0 (all hardcoded hex removed from detail page) + - `tablos.templ` contains `@TabloDescDisplay` call inside `TabloOverviewTabFragment` (not in the persistent header) + - `go test ./backend/internal/web/... -run TestTablos -count=1` passes + - `TabloDeleteButtonFragment` function body is unchanged (grep its content to confirm) + + TabloDetailPage header uses project-card-top layout; tab nav uses design token classes; description is in overview tab; no hardcoded hex values remain in the detail page header/nav section. + + + + Task 2: Remove EtapeStrip call from TasksTabFragment and update KanbanBoard call site + backend/templates/tablos.templ + + - backend/templates/tablos.templ (already read in Task 1 — find TasksTabFragment, line ~407–420; confirm EtapeStrip and KanbanBoard call positions) + - .planning/phases/16-tablo-detail/16-RESEARCH.md (Pattern 4: EtapeStrip Removal and OOB Impact — explains which EtapeStrip call sites exist in tablos.templ; this task handles Site 1 only; Sites 2 and 3 in tasks.templ are handled in Plan 03) + + + In `TasksTabFragment` inside `tablos.templ`: + + 1. DELETE the line: `@EtapeStrip(tablo.ID, etapes, counts, filter, csrfToken, false)` — remove this entire call (per D-E01; the EtapeStrip UI strip is removed). Also remove any surrounding `div id="etape-strip"` wrapper if one exists. + + 2. UPDATE the KanbanBoard call: change `@KanbanBoard(tablo.ID, csrfToken, tasks, filter)` to `@KanbanBoard(tablo.ID, csrfToken, tasks, filter, etapes)` — adding `etapes` as the 5th argument. Note: the `KanbanBoard` templ signature update happens in Plan 03; this call site change MUST be done simultaneously with that signature change to avoid a compile error. If Plan 03 has not yet run, add a TODO comment next to this line noting the parameter will be added when KanbanBoard signature is updated in Plan 03. The `templ generate && go build ./...` gate in Plan 03 will catch any mismatch. + + After editing, run: `just generate` to regenerate `tablos_templ.go`. + + + grep -c "EtapeStrip" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/tablos.templ && grep "KanbanBoard" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/tablos.templ + + + - `grep "EtapeStrip" backend/templates/tablos.templ` returns 0 (the EtapeStrip call is gone from TasksTabFragment) + - `grep "KanbanBoard" backend/templates/tablos.templ` shows the call includes `etapes` as a parameter (or has a TODO comment pending Plan 03) + - `go test ./backend/internal/web/... -run TestTablos -count=1` passes (or compile error from KanbanBoard signature mismatch is expected and noted — will be resolved in Plan 03) + + EtapeStrip is not called from TasksTabFragment; KanbanBoard call is updated to pass etapes. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Go template → HTML response | tablos.templ renders tablo data from DB into HTML; existing ownership guard (`loadOwnedTablo`) unchanged | +| HTMX GET requests | hx-get endpoints for tab fragments are authenticated routes; no change to auth middleware | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-16-02-01 | Tampering | TabloDetailPage inline-edit zones | accept | tablo-title-zone and tablo-desc-zone HTMX endpoints validate CSRF token and ownership — unchanged; only CSS classes change | +| T-16-02-02 | Spoofing | Discussion/Delete action controls | accept | Auth middleware on all /tablos/{id}/* routes unchanged; restyling does not change route protection | +| T-16-02-03 | Information Disclosure | tablo.Color inline style | accept | tablo.Color is user-supplied but rendered as a CSS value in a style attribute; no script injection possible via `background-color: {value}` — browsers do not execute style values as JS | + + + +```bash +cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source +go test ./backend/internal/web/... -count=1 +``` +All existing tablo tests pass. No hardcoded #804EEC hex in tablos.templ for the detail page section. + + + +- `tablos.templ` header uses `.project-card-top`, `.project-avatar` (with first char), `.tablo-title-zone` +- All 5 tab nav items use `.tab-nav-item` / `.tab-nav-item.is-active` (no hardcoded hex) +- Description zone is inside `TabloOverviewTabFragment` +- `EtapeStrip` is not called from `TasksTabFragment` +- `KanbanBoard` call passes `etapes` +- `go test ./backend/internal/web/... -count=1` passes + + + +After completion, create `.planning/phases/16-tablo-detail/16-02-SUMMARY.md` using the summary template. + diff --git a/.planning/phases/16-tablo-detail/16-03-PLAN.md b/.planning/phases/16-tablo-detail/16-03-PLAN.md new file mode 100644 index 0000000..e33292e --- /dev/null +++ b/.planning/phases/16-tablo-detail/16-03-PLAN.md @@ -0,0 +1,274 @@ +--- +phase: 16-tablo-detail +plan: 03 +type: execute +wave: 3 +depends_on: + - 16-02 +files_modified: + - backend/templates/tasks.templ + - backend/internal/web/handlers_tasks.go +autonomous: true +requirements: + - DETAIL-02 + - DETAIL-03 + +must_haves: + truths: + - "KanbanBoard templ signature accepts etapes []sqlc.Etape as 5th parameter" + - "KanbanColumn renders tasks grouped by etape using groupTasksByEtape helper; unassigned tasks appear last" + - "Each kanban column uses .kanban-column / .tasks-section / .tasks-section-header / .task-list CSS layout" + - "TaskCard uses .task-row layout: .task-check + .task-body + trash @ui.IconButton" + - "AddTaskTrigger uses .tasks-add-button class (not raw ui-button classes)" + - "EtapeStrip OOB calls are removed from TaskCardGone and TaskCardOOB (etapes/counts params kept)" + - "Both KanbanBoard call sites in handlers_tasks.go pass etapes as 5th argument" + - "All 19 existing task handler tests pass unchanged" + artifacts: + - path: backend/templates/tasks.templ + provides: groupTasksByEtape helper, EtapeGroup type, EtapeGroupHeader component, restyled KanbanBoard/Column/TaskCard + contains: "groupTasksByEtape" + - path: backend/internal/web/handlers_tasks.go + provides: Updated KanbanBoard call sites (lines ~594, ~645) + contains: "KanbanBoard.*etapes" + key_links: + - from: backend/templates/tasks.templ groupTasksByEtape + to: backend/templates/tasks.templ KanbanColumn + via: "groups := groupTasksByEtape(tasks, etapes)" expression inside KanbanColumn + pattern: "groupTasksByEtape" + - from: backend/internal/web/handlers_tasks.go + to: backend/templates/tasks.templ KanbanBoard + via: "templates.KanbanBoard(tablo.ID, csrf.Token(r), tasks, filter, etapes)" + pattern: "KanbanBoard.*etapes" +--- + + +Restyle the task kanban board (DETAIL-02) and implement server-side etape grouping (DETAIL-03). Changes: add `groupTasksByEtape` helper and `EtapeGroup` type to `tasks.templ`; add `EtapeGroupHeader` templ component; update `KanbanBoard` and `KanbanColumn` signatures to accept `etapes []sqlc.Etape`; restyle `KanbanColumn` with `.tasks-section` layout; restyle `TaskCard` with `.task-row` layout; restyle `AddTaskTrigger` with `.tasks-add-button`; remove `@EtapeStrip` OOB calls from `TaskCardGone` and `TaskCardOOB`; update both `KanbanBoard` call sites in `handlers_tasks.go`. + +Purpose: Deliver DETAIL-02 (tasks-section kanban) and DETAIL-03 (etape grouping, EtapeStrip removal). +Output: Restyled kanban with etape-grouped task rows; all 19 task handler tests pass. + + + +@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/workflows/execute-plan.md +@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/templates/summary.md + + + +@.planning/ROADMAP.md +@.planning/phases/16-tablo-detail/16-CONTEXT.md +@.planning/phases/16-tablo-detail/16-RESEARCH.md +@.planning/phases/16-tablo-detail/16-PATTERNS.md +@.planning/phases/16-tablo-detail/16-UI-SPEC.md +@.planning/phases/16-tablo-detail/16-02-SUMMARY.md + + + + +From backend/templates/tasks.templ (current state — read the file before editing): + +Existing groupTasksByStatus (lines ~10-18) — model for groupTasksByEtape: + 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 + } + +Current KanbanBoard signature (line ~23): + templ KanbanBoard(tabloID uuid.UUID, csrfToken string, tasks []sqlc.Task, filter EtapeFilter) + +Target KanbanBoard signature: + templ KanbanBoard(tabloID uuid.UUID, csrfToken string, tasks []sqlc.Task, filter EtapeFilter, etapes []sqlc.Etape) + +KanbanColumn — receives tasks for one status. After change, also receives etapes: + templ KanbanColumn(tabloID uuid.UUID, status sqlc.TaskStatus, tasks []sqlc.Task, csrfToken string, filter EtapeFilter, etapes []sqlc.Etape) + +EtapeGroup type to add (before groupTasksByEtape func): + type EtapeGroup struct { + EtapeID string + EtapeTitle string + EtapeColor string + Tasks []sqlc.Task + } + +groupTasksByEtape logic: + - Build etapeID→Etape map from etapes slice for O(1) lookup + - Use etapes slice ORDER to determine group order (not map iteration) + - For each etape in order: collect tasks whose EtapeID matches; only add group if non-empty + - sqlc.Task.EtapeID field: check the actual type (likely pgtype.UUID or *uuid.UUID) — use appropriate nil/zero check + - At the end: collect tasks with nil/zero EtapeID → append as EtapeGroup{EtapeID: "", EtapeTitle: "No etape", EtapeColor: "", Tasks: unassigned} + - If zero etapes are defined (etapes slice is empty), treat all tasks as unassigned + +TaskCardGone current signature (line ~384): has etapes []sqlc.Etape and counts EtapeTaskCounts params +TaskCardOOB current signature (line ~396): same +Action: remove the @EtapeStrip(..., true) OOB call from both; KEEP the etapes and counts params (to avoid handler signature changes); add TODO comment + +KanbanBoard call sites in handlers_tasks.go: + Line ~594: templates.KanbanBoard(tablo.ID, csrf.Token(r), tasks, filter) → add etapes (from loadTasksTabData return) + Line ~645: templates.KanbanBoard(tablo.ID, csrf.Token(r), tasks, filter) → add etapes (etapes is already available in scope) + +loadTasksTabData return signature (check actual): returns (tasks, etapes, counts, filter, ok bool) — etapes is the 2nd return value + +From backend/internal/web/ui/variants.go (for TaskCard IconButton): + ui.IconButtonVariantDanger, ui.IconButtonToneGhost + +From backend/internal/web/ui/app.css (Section 23 added in Plan 01): + .kanban-column { flex-shrink: 0; width: 18rem; } + .tasks-section { border: 1px solid var(--color-border-subtle); border-radius: 1rem; overflow: hidden; } + .tasks-section-header { display: flex; justify-content: space-between; align-items: center; padding: 1.2rem 1rem; } + .task-list { display: flex; flex-direction: column; } + .task-row { display: flex; align-items: center; gap: 0.75rem; padding: 0.9rem 1rem; border-bottom: 1px solid var(--color-border-muted); } + .tasks-add-button { display: inline-flex; align-items: center; gap: 0.5rem; ... } + + + + + + + Task 1: Add groupTasksByEtape helper, EtapeGroup type, EtapeGroupHeader component; update KanbanBoard/Column signatures and restyled column/card/trigger + backend/templates/tasks.templ + + - backend/templates/tasks.templ (read the FULL file before editing — ~420 lines; you need the current: groupTasksByStatus pattern, KanbanBoard body, KanbanColumn body, TaskCard body, AddTaskTrigger body, TaskCardGone and TaskCardOOB bodies, existing import block, sqlc.Task.EtapeID field type) + - backend/internal/db/sqlc/querier.go or backend/internal/db/sqlc/models.go (grep for "EtapeID" field on Task struct to confirm the exact type — pgtype.UUID, *uuid.UUID, or sql.NullString) + - .planning/phases/16-tablo-detail/16-PATTERNS.md (sections: groupTasksByEtape helper, KanbanBoard signature change, KanbanColumn restyled, TaskCard restyled, EtapeGroupHeader component, EtapeStrip OOB removal) + + + Edit `backend/templates/tasks.templ` to make the following changes. Read the entire file first to understand current structure before starting any edits. + + 1. ADD EtapeGroup type and groupTasksByEtape function after the existing `groupTasksByStatus` function (before the `KanbanBoard` templ declaration): + - `EtapeGroup` struct: fields EtapeID string, EtapeTitle string, EtapeColor string, Tasks []sqlc.Task + - `groupTasksByEtape(tasks []sqlc.Task, etapes []sqlc.Etape) []EtapeGroup`: + * Build index map from etape ID string to sqlc.Etape for O(1) lookup + * Iterate etapes in slice order; for each etape, collect tasks matching that etape ID; only append a group if it has tasks + * Append an unassigned group (EtapeID: "", EtapeTitle: "No etape", EtapeColor: "") for tasks with nil/zero EtapeID — check the actual EtapeID field type (pgtype.UUID: use !t.EtapeID.Valid; *uuid.UUID: use t.EtapeID == nil; uuid.UUID zero value: use t.EtapeID == uuid.Nil) + * If all tasks are assigned (no unassigned), omit the unassigned group entirely + + 2. ADD EtapeGroupHeader templ component (after the groupTasksByEtape function): + `templ EtapeGroupHeader(group EtapeGroup)` rendering a `div class="etape-group-header"`: + - If group.EtapeColor != "": render `span class="etape-group-color-dot" style="background-color: {group.EtapeColor}"` + - If group.EtapeID == "": render `span class="etape-group-label is-unassigned"` with group.EtapeTitle + - Else: render `span class="etape-group-label"` with group.EtapeTitle + + 3. UPDATE KanbanBoard signature: add `etapes []sqlc.Etape` as 5th parameter. Inside KanbanBoard, pass `etapes` through to each `@KanbanColumn(...)` call. + + 4. UPDATE KanbanColumn signature: add `etapes []sqlc.Etape` as 6th parameter. Restyle the column structure: + - Outer: `div class="kanban-column"` (replaces the current flex-shrink-0 w-72 div) + - Inner: `div class="tasks-section"` containing: + * `div class="tasks-section-header"`: left side has h3 with TaskColumnLabels[status] + span with task-count-badge `@ui.Badge(...BadgeVariantInfo...)`, right side has `div id="add-task-slot-{status}"` containing `@AddTaskTrigger(...)` + * `div class="task-list sortable-column" data-status="{status}" id="column-{status}" aria-label="{label} column"`: + - If len(tasks) == 0: render `p class="task-list-empty" "No tasks yet"` + - Else: compute `groups := groupTasksByEtape(tasks, etapes)`, then `for _, group := range groups { @EtapeGroupHeader(group); for _, task := range group.Tasks { @TaskCard(tabloID, task, csrfToken) } }` + - Preserve existing `data-status` and `id="column-..."` attributes for Sortable.js compatibility + + 5. RESTYLE TaskCard: replace the current `.task-card bg-white rounded border border-slate-200...` inner div with `.task-row task-card` div: + - Outer zone wrapper: keep `div class="task-card-zone" id="task-{task.ID.String()}"` unchanged (HTMX swap target) + - Inner: `div class="task-row task-card" data-task-id="{task.ID.String()}" hx-get=... hx-target="closest .task-card-zone" hx-swap="outerHTML" role="button" aria-label="Edit task: {task.Title}"` + - Children: `div class="task-check"` (round checkbox, role="checkbox" aria-checked="false") + `div class="task-body"` (p with task.Title) + trash `@ui.IconButton(ui.IconButtonProps{Label: "Delete task: {task.Title}", Icon: "trash", Variant: ui.IconButtonVariantDanger, Tone: ui.IconButtonToneGhost, Type: "button", Attrs: templ.Attributes{"hx-get": "/tablos/{tabloID}/tasks/{task.ID}/delete-confirm", "hx-target": "closest .task-card-zone", "hx-swap": "outerHTML"}})` + - Remove the old drag handle div (⠿) + + 6. RESTYLE AddTaskTrigger: change the button class from the current `ui-button ui-button-soft...` compound to simply `tasks-add-button`. Preserve all hx-* attributes. + + 7. REMOVE EtapeStrip OOB calls: in TaskCardGone and TaskCardOOB, delete the `@EtapeStrip(tabloID, etapes, counts, filter, csrfToken, true)` lines. Keep the `etapes []sqlc.Etape` and `counts EtapeTaskCounts` parameters on both component signatures. Add `// TODO: remove etapes and counts params after Phase 16 cleanup` comment. + + After editing, run: `just generate` to regenerate `tasks_templ.go`. + + + grep -c "groupTasksByEtape" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/tasks.templ && grep -c "EtapeGroupHeader" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/tasks.templ && grep -c "kanban-column" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/tasks.templ && grep -c "tasks-section" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/tasks.templ && grep -c "task-row" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/tasks.templ && grep -c "EtapeStrip" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/tasks.templ + + + - `tasks.templ` contains `func groupTasksByEtape(tasks []sqlc.Task, etapes []sqlc.Etape) []EtapeGroup` + - `tasks.templ` contains `type EtapeGroup struct` + - `tasks.templ` contains `templ EtapeGroupHeader(group EtapeGroup)` + - `tasks.templ` contains `class="kanban-column"` (replacing w-72 Tailwind class) + - `tasks.templ` contains `class="tasks-section"` inside kanban column + - `tasks.templ` contains `class="task-row task-card"` inside TaskCard + - `tasks.templ` contains `class="tasks-add-button"` in AddTaskTrigger + - `grep -c "EtapeStrip" backend/templates/tasks.templ` returns 0 (OOB calls removed) + - `grep "etapes \[\]sqlc.Etape" backend/templates/tasks.templ` shows parameter in both KanbanBoard and KanbanColumn signatures + - `just generate` exits 0 (templ compiles cleanly) + + tasks.templ has groupTasksByEtape, EtapeGroupHeader, restyled KanbanColumn/TaskCard/AddTaskTrigger, and no EtapeStrip OOB calls. + + + + Task 2: Update KanbanBoard call sites in handlers_tasks.go and verify full test suite + backend/internal/web/handlers_tasks.go + + - backend/internal/web/handlers_tasks.go (read lines 580–660 to see the two KanbanBoard call sites and the loadTasksTabData return variables in scope at each call site; confirm etapes is the 2nd return from loadTasksTabData: `tasks, etapes, counts, filter, ok := loadTasksTabData(...)`) + - .planning/phases/16-tablo-detail/16-RESEARCH.md (Pattern: KanbanBoard call site count — three call sites must all be updated; tablos.templ was updated in Plan 02; handlers_tasks.go has two) + + + Edit `backend/internal/web/handlers_tasks.go` at the two `KanbanBoard` call sites: + + Line ~594 (inside TaskReorderHandler or similar): change + `templates.KanbanBoard(tablo.ID, csrf.Token(r), tasks, filter)` + to + `templates.KanbanBoard(tablo.ID, csrf.Token(r), tasks, filter, etapes)` + + Line ~645 (second KanbanBoard call): change the same pattern. + + At each call site, verify that `etapes` is already in scope from the `loadTasksTabData` return values. The function returns `(tasks, etapes, counts, filter, ok)` — use the `etapes` variable directly. + + After editing, run: `just generate && go build ./backend/...` to confirm all three KanbanBoard call sites match the updated signature. + + Then run the full task test suite: + `go test ./backend/internal/web/... -run TestTask -count=1` + + And the full handler test suite (no DB required): + `go test ./backend/internal/web/... -count=1` + + + grep -c "KanbanBoard.*etapes" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/internal/web/handlers_tasks.go && cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source && go build ./backend/... && go test ./backend/internal/web/... -run TestTask -count=1 + + + - `grep "KanbanBoard" backend/internal/web/handlers_tasks.go` shows both call sites include `etapes` as 5th argument + - `go build ./backend/...` exits 0 (no KanbanBoard argument count mismatch) + - `go test ./backend/internal/web/... -run TestTask -count=1` exits 0 (all 19 task tests pass) + - `go test ./backend/internal/web/... -count=1` exits 0 (full handler test suite passes with no regressions) + + Both KanbanBoard call sites in handlers_tasks.go pass etapes; all task tests pass; build is clean. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Handler → KanbanBoard template | etapes []sqlc.Etape passed from handler; data comes from authenticated DB query | +| EtapeGroup rendering | EtapeColor rendered as inline style value (background-color) — same pattern as project-avatar; no script injection via CSS values | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-16-03-01 | Information Disclosure | EtapeGroup server-side grouping | accept | groupTasksByEtape operates on data already fetched via authenticated query; no new data access introduced | +| T-16-03-02 | Tampering | EtapeColor inline style value | accept | Etape color is stored in DB as user-supplied value but rendered only as CSS background-color; browsers do not execute CSS property values as code | +| T-16-03-03 | Spoofing | EtapeStrip OOB removal | accept | Removing dead OOB calls has no security impact; HTMX ignores swaps for missing targets | +| T-16-03-04 | Denial of Service | groupTasksByEtape O(n*m) | accept | n = tasks per column (bounded by UI; typically < 50), m = etapes per tablo (typically < 10); no external input controls loop bounds; not a real DoS surface | + + + +```bash +cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source +go build ./backend/... +go test ./backend/internal/web/... -count=1 +``` +Build clean; full handler test suite passes (all 19 task tests + all etape tests + all file tests). + + + +- `groupTasksByEtape` helper groups tasks by etape in etape declaration order with unassigned last +- `KanbanBoard` and `KanbanColumn` both accept `etapes []sqlc.Etape` +- Both `handlers_tasks.go` call sites pass `etapes` +- Kanban column uses `.kanban-column` / `.tasks-section` / `.tasks-section-header` / `.task-list` CSS classes +- TaskCard uses `.task-row` with `.task-check` + `.task-body` + trash `@ui.IconButton` +- EtapeStrip OOB calls removed from TaskCardGone and TaskCardOOB +- `go test ./backend/internal/web/... -count=1` passes (all 19 task tests) + + + +After completion, create `.planning/phases/16-tablo-detail/16-03-SUMMARY.md` using the summary template. + diff --git a/.planning/phases/16-tablo-detail/16-04-PLAN.md b/.planning/phases/16-tablo-detail/16-04-PLAN.md new file mode 100644 index 0000000..dd04d59 --- /dev/null +++ b/.planning/phases/16-tablo-detail/16-04-PLAN.md @@ -0,0 +1,262 @@ +--- +phase: 16-tablo-detail +plan: 04 +type: execute +wave: 4 +depends_on: + - 16-03 +files_modified: + - backend/templates/files.templ +autonomous: false +requirements: + - DETAIL-04 + +must_haves: + truths: + - "Files section uses .overview-section / .overview-section-heading layout with h3 'Files' on left and Upload file button on right" + - "File list uses @ui.Table with Filename / Size / Uploaded / Actions columns" + - "Each file row renders as with Download and Delete @ui.IconButton actions" + - "FileDeleteConfirmFragment outer element is (matching element type for outerHTML swap)" + - "Empty state uses @ui.EmptyState with Title 'No files yet' and Description 'Upload your first file to get started.'" + - "All 9 existing file handler tests pass unchanged" + - "Browser walkthrough confirms: files tab shows correct table layout, delete confirm swap works, empty state renders correctly" + artifacts: + - path: backend/templates/files.templ + provides: Restyled FilesTabFragment, fileTableHead, fileTableBody, FileListRow as tr, FileDeleteConfirmFragment as tr + contains: "@ui.Table(ui.TableProps{" + - path: backend/templates/files_templ.go + provides: Generated Go from restyled files.templ + exports: [] + key_links: + - from: backend/templates/files.templ FileListRow + to: backend/templates/files.templ FileDeleteConfirmFragment + via: "hx-target='closest .file-row-zone' hx-swap='outerHTML'" + pattern: "class=\"file-row-zone\"" + - from: backend/templates/files.templ FilesTabFragment + to: backend/internal/web/ui/table.templ @ui.Table + via: "@ui.Table(ui.TableProps{Head: fileTableHead(), Body: fileTableBody(...)})" + pattern: "ui.Table" +--- + + +Restyle the files section (DETAIL-04): replace raw `