3 plans in 2 waves: handler+viewmodel (wave 1), templ components + CSS restyle in parallel (wave 2). Covers DETAIL-01 and TASK-01. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
18 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 20-tablo-detail-kanban-restyle | 02 | execute | 2 |
|
|
true |
|
|
Purpose: This plan produces the full HTML surface for DETAIL-01 and TASK-01. All CSS class names used here are defined in Plan 03's CSS work, so the visual output is unstyled at first but structurally correct.
Output: tablo_detail.templ with all sub-components; tablo_detail_view.go stub removed and replaced by templ-generated component.
<execution_context> @/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 </execution_context>
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/ROADMAP.md @/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/20-tablo-detail-kanban-restyle/20-UI-SPEC.md @/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/20-tablo-detail-kanban-restyle/20-RESEARCH.mdFrom go-backend/internal/web/views/tablo_detail_view.go (created in Plan 01):
type TabloDetailTaskView struct {
ID string
Title string
DeleteHref string
EditHref string
}
type TabloDetailColumnView struct {
ID string // "todo" | "in_progress" | "in_review" | "done"
Label string // "À faire" | "En cours" | "En révision" | "Terminé"
Tasks []TabloDetailTaskView
CreateHref string // "/tablos/{id}/tasks/create?status={colID}"
}
type TabloDetailViewModel struct {
TabloID string
TabloName string
Color string
Initial string
OwnerName string
DueDate string // empty string → show "—" in template
StatusLabel string
StatusTone string // "warning" | "success" | "info"
Progress int // 0-100
ProgressLabel string // "{N}%"
Columns []TabloDetailColumnView // always 4 columns
}
From go-backend/internal/web/ui/badge.templ:
// ui.Badge(ui.BadgeProps{Label: string, Variant: string}) templ.Component
// badge variants: use badgeVariantForTone(tone string) -> string (function in views package)
// tone strings: "success", "warning", "info", "danger", "default"
From go-backend/internal/web/ui/icon_button.templ:
// ui.IconButton(ui.IconButtonProps{Label, Icon, Variant, Tone, Type, Attrs}) templ.Component
// IconButtonVariantNeutral / IconButtonToneGhost — for ghost action buttons
// IconButtonVariantDanger + IconButtonToneGhost — for delete button
From go-backend/internal/web/views/tablos.templ (for ActionIcon usage):
// ActionIcon(kind string) templ.Component — available in views package
// Icons used: "calendar", "message-circle", "user"
From go-backend/internal/web/views/tasks.templ (for drag-and-drop reorder pattern):
// Sortable.js init fires on DOMContentLoaded AND htmx:afterSettle
// Uses: .sortable-column class on task list containers
// Uses: data-status attribute on column containers
// Uses: .task-drag-handle handle class
// Uses: .task-card draggable class
// Reorder form: #reorder-form (hidden form that submits task order to POST /tablos/{id}/tasks/reorder)
// Guard: if (el._sortable) return; prevents double-init after HTMX swap (Pitfall 4)
CSS class contract (defined in Plan 03, used here):
.tablo-detail-page — outer container, px-6 pt-6
.tablo-detail-header — header element
.tablo-detail-title-row — flex row with avatar + h1
.tablo-detail-avatar — 48×48 colored circle with initial
.tablo-detail-title — h1, font-size 1.75rem, weight 600
.tablo-metadata-row — flex row with gap:24px, padding-block:16px
.tablo-meta-segment — each metadata segment in the row
.tablo-meta-progress — the progress segment (bar + label)
.tablo-progress-bar — progress fill (uses brand-primary, NOT project-color)
.tablo-tab-bar — tab navigation below header
.tab-nav-item — each tab item (existing class)
.tab-nav-item--active — active tab (existing class)
.tablo-kanban-board — flex container for columns
.tablo-kanban-column — each 18rem column
.tablo-kanban-column-header — column header area
.tablo-kanban-column-title — h3 inside column header
.tablo-kanban-task-count — count pill
.tablo-kanban-add-link — "+ Ajouter" ghost link
.task-list.sortable-column — the sortable task list container
.task-card — individual task card (replaces .task-row)
.task-card-top-row — row with drag handle, title, delete icon
.task-drag-handle — drag handle (opacity 0 at rest, 1 on card hover)
.task-card-title — task title text
.task-card-delete — delete icon button (opacity 0 at rest)
.tablo-kanban-empty — empty column message
Task 1: TabloDetailPage templ component — header, tab bar, kanban board
go-backend/internal/web/views/tablo_detail.templ
- go-backend/internal/web/views/tablo_detail_view.go (TabloDetailViewModel fields — just created in Plan 01)
- go-backend/internal/web/views/tablos.templ (TabloGridCard pattern, ActionIcon usage, badgeVariantForTone function name)
- go-backend/internal/web/views/tasks.templ (TasksKanbanLayout — for contrast; tablo-detail kanban is SEPARATE and must NOT reuse TasksKanbanLayout)
- go-backend/internal/web/ui/badge.templ (BadgeProps fields)
- go-backend/internal/web/ui/icon_button.templ (IconButtonProps fields)
- go-backend/internal/web/ui/app.css lines 882-900 (existing .tab-nav, .tab-nav-item classes)
- go-backend/internal/web/views/discussion_view.go (for badgeVariantForTone function — confirm it's in views package)
Create go-backend/internal/web/views/tablo_detail.templ in package views. Import "xtablo-backend/internal/web/ui".
Define the following templ components in this order:
1. TabloDetailPage(vm TabloDetailViewModel) — outer wrapper:
- Outer div class="tablo-detail-page"
- @TabloDetailHeader(vm)
- @TabloDetailTabBar(vm.TabloID)
- div class="tablo-kanban-board" containing: @TabloDetailKanbanBoard(vm.Columns, vm.TabloID)
- @TabloDetailSortableScript(vm.TabloID)
2. TabloDetailHeader(vm TabloDetailViewModel) — header section:
- header element class="tablo-detail-header"
- Inner div class="tablo-detail-title-row":
* div class="tablo-detail-avatar" style={ "background:" + vm.Color } containing { vm.Initial }
* h1 class="tablo-detail-title" containing { vm.TabloName }
- div class="tablo-metadata-row":
* span class="tablo-meta-segment" containing: 24×24 avatar div with owner initial + " " + { vm.OwnerName }
* span class="tablo-meta-segment" containing: @ActionIcon("calendar") + if vm.DueDate != "" { vm.DueDate } else { "—" }
* span class="tablo-meta-segment" containing: @ui.Badge(ui.BadgeProps{Label: vm.StatusLabel, Variant: badgeVariantForTone(vm.StatusTone)})
* span class="tablo-meta-segment tablo-meta-progress" containing:
- div class="project-progress-track" style="min-width:120px" with inner div class="tablo-progress-bar" style={ "width:" + vm.ProgressLabel }
- strong { vm.ProgressLabel }
3. TabloDetailTabBar(tabloID string) — tab navigation:
- nav element class="tablo-tab-bar"
- 5 anchor elements for: "Vue d'ensemble", "Tâches", "Fichiers", "Discussion", "Événements"
- Tab IDs/slugs: "overview", "tasks", "files", "discussion", "events"
- Each anchor: class="tab-nav-item" with href={ "/tablos/" + tabloID + "#" + slug }
- "Tâches" tab (tasks slug): class="tab-nav-item tab-nav-item--active" (Phase 20 always shows tasks tab active)
- No hx-get on tabs in Phase 20 — tab switching is Phase 21 scope; use plain href anchors
4. TabloDetailKanbanBoard(columns []TabloDetailColumnView, tabloID string) — kanban board:
- For each column: @TabloDetailKanbanColumn(col, tabloID)
5. TabloDetailKanbanColumn(col TabloDetailColumnView, tabloID string) — single column:
- div class="tablo-kanban-column" data-status={ col.ID }
- div class="tablo-kanban-column-header":
* span class="tablo-kanban-column-title" containing { col.Label }
* span class="tablo-kanban-task-count" containing { strconv.Itoa(len(col.Tasks)) }
* a class="tablo-kanban-add-link" href={ col.CreateHref } containing "+ Ajouter"
- div id={ "task-list-" + col.ID } class="task-list sortable-column" data-status={ col.ID }:
* if len(col.Tasks) == 0: div class="tablo-kanban-empty" { "Aucune tâche" }
* else: for _, task := range col.Tasks { @TabloDetailTaskCard(task, tabloID) }
- div id={ "create-zone-" + col.ID } (empty create zone for HTMX task create swap)
6. TabloDetailTaskCard(task TabloDetailTaskView, tabloID string) — task card:
- article element: class="task-card" data-task-id={ task.ID }
- div class="task-card-top-row":
* span class="task-drag-handle" aria-hidden="true" { "⠿" }
* span class="task-card-title" { task.Title }
* @ui.IconButton(ui.IconButtonProps{
Label: "Supprimer la tâche", Icon: "trash",
Variant: ui.IconButtonVariantDanger, Tone: ui.IconButtonToneGhost,
Type: "button",
Attrs: templ.Attributes{
"class": "task-card-delete",
"hx-delete": task.DeleteHref,
"hx-target": "#app-main-content",
"hx-swap": "outerHTML",
"hx-confirm": "Supprimer cette tâche ?",
},
})
7. TabloDetailSortableScript(tabloID string) — JavaScript block:
Use a templ @raw block (or templ.Raw) to emit the Sortable.js init script that:
- Defines function initTabloDetailSortable()
- Calls document.querySelectorAll('.sortable-column').forEach(function(el) { if (el._sortable) return; el._sortable = Sortable.create(el, { group: 'tablo-tasks', animation: 150, handle: '.task-drag-handle', draggable: '.task-card', onEnd: function(evt) { /* submit hidden reorder form */ var form = document.getElementById('reorder-form-' + el.dataset.status); if (form) form.requestSubmit(); } }); })
- Calls document.addEventListener('DOMContentLoaded', initTabloDetailSortable)
- Calls document.addEventListener('htmx:afterSettle', initTabloDetailSortable)
Wrap in <script> tag.
CRITICAL rules:
- Do NOT import or call TasksKanbanLayout — tablo-detail kanban is a separate surface (RESEARCH Anti-Patterns)
- Do NOT add a task view switcher (Board/List/Gantt) — UI-SPEC says this is Phase 21
- All strings in French per UI-SPEC Copywriting Contract
- Use strconv.Itoa for integer→string conversion in templ (not fmt.Sprintf)
- The tablo-progress-bar uses class "tablo-progress-bar" NOT "project-progress-bar" to avoid Pitfall 5 (project-color vs brand-primary)
- import "strconv" in the templ file header
cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/go-backend && just generate 2>&1 | tail -5 && go build ./... 2>&1
just generate succeeds (tablo_detail_templ.go generated); go build ./... exits 0; tablo_detail.templ defines TabloDetailPage, TabloDetailHeader, TabloDetailTabBar, TabloDetailKanbanBoard, TabloDetailKanbanColumn, TabloDetailTaskCard, TabloDetailSortableScript.
Task 2: Replace TabloDetailPage stub + wire templ component + run handler tests
go-backend/internal/web/views/tablo_detail_view.go
- go-backend/internal/web/views/tablo_detail_view.go (current stub — remove the ComponentFunc stub)
- go-backend/internal/web/views/tablo_detail.templ (just created — TabloDetailPage is now a real templ component)
- go-backend/internal/web/views/tablo_detail_templ.go (generated by just generate — confirm TabloDetailPage signature)
- go-backend/internal/web/handlers/tablo_detail_test.go (handler tests that verify 200 + tablo name in body)
Edit go-backend/internal/web/views/tablo_detail_view.go:
- Remove the stub TabloDetailPage function (the templ.ComponentFunc one added in Plan 01 Task 2)
- Remove "context", "fmt", "io", and "github.com/a-h/templ" imports added for the stub (they are no longer needed in the view.go file since TabloDetailPage is now generated by templ from tablo_detail.templ)
- Keep all struct definitions and NewTabloDetailViewModel + computeTabloProgress functions
After removing the stub, the real TabloDetailPage comes from tablo_detail_templ.go (generated by just generate). Verify with go build.
Then run the full handler tests to confirm the integration is correct:
- TestGetTabloDetailPage_Returns200 should now render real HTML with the tablo name inside the TabloDetailHeader h1
- Update the test assertion if needed: the response body should contain the tablo name string
cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/go-backend && go test ./... -run "TestGetTabloDetailPage|TestComputeTabloProgress|TestNewTabloDetail|TestTabloDetail" -count=1 -v 2>&1 | tail -30
All TestGetTabloDetailPage_* and view model tests pass; go build ./... exits 0; no stub function remains in tablo_detail_view.go; generated tablo_detail_templ.go exports TabloDetailPage(TabloDetailViewModel) templ.Component.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| Template rendering | TabloDetailViewModel data flows from handler into templ — ensure no raw HTML injection |
STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|---|---|---|---|---|
| T-20-04 | Tampering | TabloDetailTaskCard — task.Title rendered in HTML | mitigate | templ auto-escapes all { expr } interpolations — no raw HTML emission for user data |
| T-20-05 | Tampering | Sortable.js onEnd — data-status attribute used in form ID | mitigate | data-status comes from hardcoded column IDs ("todo", "in_progress", "in_review", "done") — not user input |
| T-20-SC | Tampering | npm/pip/cargo installs | accept | No package installs — pure templ + CSS |
| </threat_model> |
<success_criteria>
- GET /tablos/{validID} (authenticated) returns 200 with h1 containing tablo name, .tablo-kanban-board div, and 4 .tablo-kanban-column elements in the response body
- Full test suite green
- Sortable.js init script present in rendered HTML with DOMContentLoaded and htmx:afterSettle listeners
- No TasksKanbanLayout referenced in tablo_detail.templ </success_criteria>