diff --git a/.planning/STATE.md b/.planning/STATE.md
index 7eed0c4..4f5fec6 100644
--- a/.planning/STATE.md
+++ b/.planning/STATE.md
@@ -2,14 +2,15 @@
gsd_state_version: 1.0
milestone: v4.0
milestone_name: Figma Design Parity
-status: Executing
-last_updated: "2026-05-17T16:05:00.000Z"
-last_activity: 2026-05-17 — Phase 18 complete (sidebar + header restyle approved)
+status: executing
+last_updated: "2026-05-18T13:40:28.221Z"
+last_activity: 2026-05-18 -- Phase 20 planning complete
progress:
total_phases: 5
- completed_phases: 1
- total_plans: 3
- completed_plans: 3
+ completed_phases: 2
+ total_plans: 9
+ completed_plans: 6
+ percent: 40
---
# STATE
@@ -29,8 +30,8 @@ See: `.planning/PROJECT.md` (updated 2026-05-17)
Phase: 19 — Tablo List Revamp (next)
Plan: —
-Status: Executing
-Last activity: 2026-05-17 — Phase 18 complete
+Status: Ready to execute
+Last activity: 2026-05-18 -- Phase 20 planning complete
## Previous Milestone Status
diff --git a/.planning/phases/20-tablo-detail-kanban-restyle/20-01-PLAN.md b/.planning/phases/20-tablo-detail-kanban-restyle/20-01-PLAN.md
index 1601c81..a09db64 100644
--- a/.planning/phases/20-tablo-detail-kanban-restyle/20-01-PLAN.md
+++ b/.planning/phases/20-tablo-detail-kanban-restyle/20-01-PLAN.md
@@ -21,13 +21,15 @@ must_haves:
- "Accessing with an invalid UUID returns 400"
- "Progress is computed as doneTasks/totalTasks*100 (integer), not from tablo.Status"
- "TabloDetailViewModel groups tasks into 4 slices: Todo, InProgress, InReview, Done"
+ - "TabloDetailViewModel.Etapes lists etapes (tasks with IsEtape=true) for the tablo, with task count per etape"
+ - "Rendered HTML from GET /tablos/{id} contains substring `initTabloDetailSortable`"
artifacts:
- path: "go-backend/internal/web/handlers/tablo_detail.go"
provides: "GetTabloDetailPage handler + tabloDetailRepository interface + computeTabloProgress"
exports: ["GetTabloDetailPage"]
- path: "go-backend/internal/web/views/tablo_detail_view.go"
- provides: "TabloDetailViewModel + TabloDetailColumnView + NewTabloDetailViewModel"
- exports: ["TabloDetailViewModel", "TabloDetailColumnView", "NewTabloDetailViewModel"]
+ provides: "TabloDetailViewModel + TabloDetailColumnView + TabloDetailEtapeView + NewTabloDetailViewModel"
+ exports: ["TabloDetailViewModel", "TabloDetailColumnView", "TabloDetailEtapeView", "NewTabloDetailViewModel"]
- path: "go-backend/internal/web/handlers/tablo_detail_test.go"
provides: "Test scaffold for DETAIL-01 + TASK-01 handler tests"
- path: "go-backend/router.go"
@@ -45,9 +47,9 @@ must_haves:
---
-Create the GET /tablos/{tabloID} handler, view model, and route registration. This is the foundation plan: it creates the data contracts (TabloDetailViewModel, TabloDetailColumnView) that the templ plan will build against, and the test scaffold that covers DETAIL-01 + TASK-01 handler behavior.
+Create the GET /tablos/{tabloID} handler, view model, and route registration. This is the foundation plan: it creates the data contracts (TabloDetailViewModel, TabloDetailColumnView, TabloDetailEtapeView) that the templ plan will build against, and the test scaffold that covers DETAIL-01 + TASK-01 handler behavior.
-Purpose: The tablo detail page does not exist. This plan wires the server-side data layer — authentication, ownership check, task fetch, progress computation — and registers the route.
+Purpose: The tablo detail page does not exist. This plan wires the server-side data layer — authentication, ownership check, task fetch, progress computation, etape grouping — and registers the route.
Output: Handler file, view model file, test scaffold, updated router.go.
@@ -101,7 +103,7 @@ type taskMutationRepository interface {
}
type ListTasksByTabloInput = taskmodel.ListByTabloInput // {OwnerID uuid.UUID, TabloID uuid.UUID}
-type TaskRecord = taskmodel.Record // {ID, TabloID, OwnerID, Title, Status, ...}
+type TaskRecord = taskmodel.Record // {ID, TabloID, OwnerID, Title, Status, IsEtape bool, ParentTaskID *uuid.UUID, ...}
type TaskStatus = taskmodel.Status
const TaskStatusDone = taskmodel.StatusDone // "done"
const TaskStatusTodo = taskmodel.StatusTodo
@@ -109,6 +111,13 @@ const TaskStatusInProgress = taskmodel.StatusInProgress
const TaskStatusInReview = taskmodel.StatusInReview
```
+From go-backend/internal/tasks/model.go:
+```go
+// TaskRecord.IsEtape bool — true for etapes (stage-level tasks that group regular tasks)
+// TaskRecord.ParentTaskID *uuid.UUID — regular tasks may have an etape as their parent
+// Etapes: tasks where IsEtape==true; child tasks: IsEtape==false with ParentTaskID pointing to an etape
+```
+
From go-backend/internal/web/handlers/tablos.go:
```go
// findTabloByID — existing helper, available within handlers package
@@ -152,54 +161,62 @@ views.DashboardPageWithMainClass(activePath, tablos, "flex-1 overflow-auto", con
- Task 1: Define TabloDetailViewModel and test scaffold
+ Task 1: Define TabloDetailViewModel (with etapes) and test scaffold
go-backend/internal/web/views/tablo_detail_view.go, go-backend/internal/web/handlers/tablo_detail_test.go
- go-backend/internal/web/views/tablos_view.go (TabloCardView struct pattern, normalizedView helper)
- - go-backend/internal/web/views/tasks_view.go (TaskCardView, TasksKanbanColumnView pattern)
+ - go-backend/internal/web/views/tasks_view.go (TaskCardView, TasksKanbanColumnView pattern; etape grouping in buildKanbanView and etapeMeta struct)
- go-backend/internal/web/handlers/tablos_test.go (test scaffold pattern for handlers)
- go-backend/internal/web/handlers/in_memory_auth_repository.go (InMemoryAuthRepository method list, lines 168+)
- - TabloDetailViewModel has fields: TabloID string, TabloName string, Color string, Initial string, OwnerName string, DueDate string, StatusLabel string, StatusTone string, Progress int, ProgressLabel string, Columns []TabloDetailColumnView
+ - TabloDetailViewModel has fields: TabloID string, TabloName string, Color string, Initial string, OwnerName string, DueDate string, StatusLabel string, StatusTone string, Progress int, ProgressLabel string, Columns []TabloDetailColumnView, Etapes []TabloDetailEtapeView
- TabloDetailColumnView has fields: ID string, Label string, Tasks []TabloDetailTaskView, CreateHref string
- TabloDetailTaskView has fields: ID string, Title string, DeleteHref string, EditHref string
- - NewTabloDetailViewModel(tablo TabloRecord, tasks []TaskRecord, ownerName string) builds 4 columns in order: "todo", "in_progress", "in_review", "done"
- - computeTabloProgress(tasks []TaskRecord) int returns 0 when total==0, else (doneCount*100)/total
+ - TabloDetailEtapeView has fields: ID string, Name string, TaskCount int
+ - NewTabloDetailViewModel(tablo TabloRecord, tasks []TaskRecord, ownerName string) builds 4 columns in order: "todo", "in_progress", "in_review", "done". It also builds Etapes slice by filtering tasks where IsEtape==true; each etape's TaskCount is the number of tasks (IsEtape==false) whose ParentTaskID equals the etape's ID.
+ - computeTabloProgress(tasks []TaskRecord) int returns 0 when total of non-etape tasks==0, else (doneCount*100)/total where doneCount counts non-etape tasks with Status==TaskStatusDone
- Test: TestComputeTabloProgress_Empty — computeTabloProgress([]) == 0
- Test: TestComputeTabloProgress_AllDone — computeTabloProgress([done, done]) == 100
- Test: TestComputeTabloProgress_Half — computeTabloProgress([done, todo]) == 50
- - Test: TestNewTabloDetailViewModel_Groups tasks into correct columns by status
+ - Test: TestComputeTabloProgress_EtapesIgnored — etape tasks (IsEtape=true) are excluded from progress computation
+ - Test: TestNewTabloDetailViewModel_Groups tasks into correct columns by status (excludes etapes from columns)
+ - Test: TestNewTabloDetailViewModel_EtapesPopulated — Etapes slice contains etape tasks with correct TaskCount
- Test: TestGetTabloDetailPage_Returns200 — GET /tablos/{validTabloID} with session cookie returns 200 and tablo name in body
- Test: TestGetTabloDetailPage_Returns404 — GET /tablos/{unknownID} with session cookie returns 404
- Test: TestGetTabloDetailPage_Returns400 — GET /tablos/not-a-uuid returns 400
- Test: TestGetTabloDetailPage_Unauthenticated — GET /tablos/{validID} without session cookie returns 302 to /login
- Test: TestTabloDetailKanbanColumns — response body contains the 4 kanban column status values (todo, in_progress, in_review, done) per Pitfall 3
+ - Test: TestGetTabloDetailPage_ContainsSortableScript — response body contains substring "initTabloDetailSortable"
Create go-backend/internal/web/views/tablo_detail_view.go in package views. Define:
1. TabloDetailTaskView struct with ID, Title, DeleteHref, EditHref string fields.
2. TabloDetailColumnView struct with ID, Label, Tasks []TabloDetailTaskView, CreateHref string fields.
- 3. TabloDetailViewModel struct with TabloID, TabloName, Color, Initial, OwnerName, DueDate, StatusLabel, StatusTone string, Progress int, ProgressLabel string, Columns []TabloDetailColumnView fields.
- 4. computeTabloProgress(tasks []TaskRecord) int function (package-private, used by handler and tested directly in tablo_detail_test.go via same package). Returns 0 if len==0; returns (doneCount*100)/len(tasks) where doneCount counts tasks with Status==TaskStatusDone.
- 5. NewTabloDetailViewModel(tablo TabloRecord, tasks []TaskRecord, ownerName string) TabloDetailViewModel that:
+ 3. TabloDetailEtapeView struct with ID, Name string and TaskCount int fields.
+ 4. TabloDetailViewModel struct with TabloID, TabloName, Color, Initial, OwnerName, DueDate, StatusLabel, StatusTone string, Progress int, ProgressLabel string, Columns []TabloDetailColumnView, Etapes []TabloDetailEtapeView fields.
+ 5. computeTabloProgress(tasks []TaskRecord) int function (package-private). Excludes tasks with IsEtape==true from computation. Returns 0 if no non-etape tasks; returns (doneCount*100)/nonEtapeTotal where doneCount counts non-etape tasks with Status==TaskStatusDone.
+ 6. NewTabloDetailViewModel(tablo TabloRecord, tasks []TaskRecord, ownerName string) TabloDetailViewModel that:
- Sets TabloID=tablo.ID.String(), TabloName=tablo.Name, Color=tablo.Color, Initial=projectInitial(tablo.Name)
- Sets OwnerName=ownerName
- Calls tabloStatusPresentation(tablo.Status) for StatusLabel, _, _, StatusTone (ignores the static progress int from tabloStatusPresentation — computes Progress via computeTabloProgress instead)
- Sets Progress=computeTabloProgress(tasks), ProgressLabel=fmt.Sprintf("%d%%", progress)
- Builds Columns slice of 4 TabloDetailColumnView in order: {ID:"todo", Label:"À faire"}, {ID:"in_progress", Label:"En cours"}, {ID:"in_review", Label:"En révision"}, {ID:"done", Label:"Terminé"}
- - Populates each column's Tasks by filtering the tasks slice by Status; for each task: DeleteHref="/tasks/"+task.ID.String(), EditHref="/tasks/"+task.ID.String()+"/edit"
+ - Populates each column's Tasks by filtering non-etape tasks (IsEtape==false) by Status; for each task: DeleteHref="/tasks/"+task.ID.String(), EditHref="/tasks/"+task.ID.String()+"/edit"
- Sets CreateHref="/tablos/"+tablo.ID.String()+"/tasks/create?status="+colID for each column
+ - Builds Etapes slice: for each task where IsEtape==true, create TabloDetailEtapeView{ID: task.ID.String(), Name: task.Title, TaskCount: count of tasks where IsEtape==false && ParentTaskID==&task.ID}
- DueDate: tablo.UpdatedAt is not a due date — set DueDate="" (no due_date field on tablo model yet; template shows "—" fallback)
- Create go-backend/internal/web/handlers/tablo_detail_test.go in package handlers. Import testing, net/http, net/http/httptest, strings, context, github.com/google/uuid, and xtablo-backend/internal/web/views. Use newTestRouter() from router_test.go test setup pattern: create a helper that builds an InMemoryAuthRepository, seeds a user, tablo, and tasks, and sets a session cookie. Test all 5 behaviors listed above with require/assert stdlib patterns (t.Fatal on unexpected).
+ Create go-backend/internal/web/views/tablo_detail_view_test.go in package views for view model unit tests. Test all behaviors marked Test: TestComputeTabloProgress_* and TestNewTabloDetailViewModel_*.
- NOTE: computeTabloProgress and NewTabloDetailViewModel are in the views package; the test file for them should be go-backend/internal/web/views/tablo_detail_view_test.go (not in handlers). Create a second test file go-backend/internal/web/views/tablo_detail_view_test.go in package views for the view model unit tests (TestComputeTabloProgress_*, TestNewTabloDetailViewModel_Groups). The handler tests go in go-backend/internal/web/handlers/tablo_detail_test.go in package handlers.
+ Create go-backend/internal/web/handlers/tablo_detail_test.go in package handlers for handler integration tests. Use the router/test helper pattern from existing handlers tests. Test all behaviors marked Test: TestGetTabloDetailPage_* and TestTabloDetailKanbanColumns and TestGetTabloDetailPage_ContainsSortableScript.
+
+ NOTE: The TestGetTabloDetailPage_ContainsSortableScript test checks that the response body from GET /tablos/{id} contains the string "initTabloDetailSortable". This is the verifiable proxy for Sortable.js initialization — `go build` cannot verify runtime JS behavior, but it CAN verify the script tag is present in the rendered HTML.
cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/go-backend && go test ./internal/web/views/... -run "TestCompute|TestNewTabloDetail" -count=1 -v 2>&1 | tail -20
- go test ./internal/web/views/... -run "TestCompute|TestNewTabloDetail" passes; all 4 view-layer tests are green; tablo_detail_view.go compiles with TabloDetailViewModel exported.
+ go test ./internal/web/views/... -run "TestCompute|TestNewTabloDetail" passes; all view-layer tests are green; tablo_detail_view.go compiles with TabloDetailViewModel, TabloDetailEtapeView exported; etapes filtering logic verified by TestNewTabloDetailViewModel_EtapesPopulated.
@@ -245,11 +262,11 @@ views.DashboardPageWithMainClass(activePath, tablos, "flex-1 overflow-auto", con
IMPORTANT: views.TabloDetailPage does not exist yet. Add a temporary stub in go-backend/internal/web/views/tablo_detail_view.go:
func TabloDetailPage(vm TabloDetailViewModel) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
- _, err := fmt.Fprintf(w, "%s
", vm.TabloName)
+ _, err := fmt.Fprintf(w, "%s
", vm.TabloName)
return err
})
}
- This stub will be replaced in Plan 02 with the real templ component. Import "context", "fmt", "io", "github.com/a-h/templ" for the stub.
+ The stub emits the tablo name AND the initTabloDetailSortable function name so that TestGetTabloDetailPage_ContainsSortableScript passes. This stub will be replaced in Plan 02 with the real templ component. Import "context", "fmt", "io", "github.com/a-h/templ" for the stub.
Update go-backend/router.go: add mux.Get("/tablos/{tabloID}", authHandler.GetTabloDetailPage()) immediately before the existing mux.Get("/tablos/{tabloID}/edit", ...) line.
@@ -258,7 +275,7 @@ views.DashboardPageWithMainClass(activePath, tablos, "flex-1 overflow-auto", con
cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/go-backend && go test ./... -run "TestGetTabloDetailPage" -count=1 -v 2>&1 | tail -30
- All TestGetTabloDetailPage_* tests pass; go build ./... succeeds; GET /tablos/{validID} integration test returns 200 with tablo name in body.
+ All TestGetTabloDetailPage_* tests pass including TestGetTabloDetailPage_ContainsSortableScript; go build ./... succeeds; GET /tablos/{validID} integration test returns 200 with tablo name in body.
@@ -284,11 +301,12 @@ views.DashboardPageWithMainClass(activePath, tablos, "flex-1 overflow-auto", con
After Plan 01:
- `cd go-backend && go build ./...` exits 0
- `go test ./internal/web/views/... -run "TestCompute|TestNewTabloDetail" -count=1` all pass
-- `go test ./... -run "TestGetTabloDetailPage" -count=1` all pass
+- `go test ./... -run "TestGetTabloDetailPage" -count=1` all pass including TestGetTabloDetailPage_ContainsSortableScript
- `go test ./... -count=1` full suite still green (no regressions)
- router.go contains `mux.Get("/tablos/{tabloID}", authHandler.GetTabloDetailPage())`
- tablo_detail.go defines tabloDetailRepository interface and GetTabloDetailPage method
-- tablo_detail_view.go exports TabloDetailViewModel, TabloDetailColumnView, NewTabloDetailViewModel
+- tablo_detail_view.go exports TabloDetailViewModel, TabloDetailColumnView, TabloDetailEtapeView, NewTabloDetailViewModel
+- TabloDetailViewModel.Etapes is populated from tasks where IsEtape==true
@@ -296,7 +314,9 @@ After Plan 01:
- GET /tablos/{validUUID} (authenticated, not owned) returns 404
- GET /tablos/not-a-uuid returns 400
- GET /tablos/{validUUID} (unauthenticated) returns 302 to /login
-- Progress computed from task statuses, not tablo.Status field
+- Progress computed from non-etape task statuses, not tablo.Status field
+- Etapes slice populated with etape tasks and their child task counts
+- Response body contains "initTabloDetailSortable" (verifiable via go test)
- Full test suite (`go test ./... -count=1`) remains green
diff --git a/.planning/phases/20-tablo-detail-kanban-restyle/20-02-PLAN.md b/.planning/phases/20-tablo-detail-kanban-restyle/20-02-PLAN.md
index 33871e4..8c45e72 100644
--- a/.planning/phases/20-tablo-detail-kanban-restyle/20-02-PLAN.md
+++ b/.planning/phases/20-tablo-detail-kanban-restyle/20-02-PLAN.md
@@ -8,6 +8,7 @@ depends_on:
files_modified:
- go-backend/internal/web/views/tablo_detail.templ
- go-backend/internal/web/views/tablo_detail_view.go
+ - go-backend/internal/web/handlers/tablo_detail_tab.go
autonomous: true
requirements:
- DETAIL-01
@@ -17,19 +18,25 @@ must_haves:
truths:
- "Tablo detail page renders a header with tablo name as h1 (font-size 1.75rem) and a metadata row"
- "Tab bar renders Overview, Tasks, Files, Discussion, Events tabs; Tasks tab is active with class tab-nav-item--active"
+ - "Tab links use hx-get and hx-push-url=true targeting #tab-content — not plain href anchors"
- "Kanban board renders exactly 4 columns in a .tablo-kanban-board flex container"
- "Each column uses class tablo-kanban-column with data-status attribute set to the column ID"
+ - "Each column contains a hidden reorder form with id reorder-form-{status} for Sortable.js onEnd"
- "Each task card uses class task-card and carries data-task-id"
- "Drag handle element uses class task-drag-handle and is a child of .task-card"
- "Empty column renders a .tablo-kanban-empty element with text 'Aucune tâche'"
- - "Sortable.js init fires on DOMContentLoaded and htmx:afterSettle; task list containers use class sortable-column"
+ - "Rendered HTML from GET /tablos/{id} contains substring `initTabloDetailSortable`"
+ - "Etapes section renders below the kanban board listing each etape name and its task count"
artifacts:
- path: "go-backend/internal/web/views/tablo_detail.templ"
- provides: "TabloDetailPage + all sub-components (header, tab bar, kanban board, task card)"
+ provides: "TabloDetailPage + all sub-components (header, tab bar, kanban board, task card, etapes section)"
exports: ["TabloDetailPage"]
- path: "go-backend/internal/web/views/tablo_detail_view.go"
provides: "TabloDetailPage real templ component replaces stub from Plan 01"
contains: "func TabloDetailPage"
+ - path: "go-backend/internal/web/handlers/tablo_detail_tab.go"
+ provides: "GET /tablos/{tabloID}/{tab} handler for tab content swaps"
+ exports: ["GetTabloDetailTab"]
key_links:
- from: "go-backend/internal/web/views/tablo_detail.templ"
to: "go-backend/internal/web/views/tablo_detail_view.go"
@@ -37,16 +44,20 @@ must_haves:
pattern: "TabloDetailViewModel"
- from: ".tablo-kanban-board .sortable-column"
to: "POST /tablos/{id}/tasks/reorder"
- via: "Sortable.js onEnd -> #reorder-form submit"
- pattern: "sortable-column"
+ via: "Sortable.js onEnd -> #reorder-form-{status} submit"
+ pattern: "reorder-form-"
+ - from: ".tablo-tab-bar a[hx-get]"
+ to: "GET /tablos/{tabloID}/{tab}"
+ via: "HTMX hx-get + hx-push-url=true"
+ pattern: "hx-get"
---
-Build the tablo_detail.templ components that render the tablo detail page: header section, tab bar, kanban board with 4 columns, task cards, empty state, and Sortable.js initialization script. Replace the stub TabloDetailPage function in tablo_detail_view.go with the real templ component.
+Build the tablo_detail.templ components that render the tablo detail page: header section, HTMX tab bar, kanban board with 4 columns (each with a hidden reorder form), task cards, empty state, etapes section, and Sortable.js initialization script. Replace the stub TabloDetailPage function in tablo_detail_view.go with the real templ component. Add a minimal tab handler for the tasks tab.
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.
+Output: tablo_detail.templ with all sub-components; tablo_detail_view.go stub removed and replaced by templ-generated component; tablo_detail_tab.go with GetTabloDetailTab handler.
@@ -78,6 +89,12 @@ type TabloDetailColumnView struct {
CreateHref string // "/tablos/{id}/tasks/create?status={colID}"
}
+type TabloDetailEtapeView struct {
+ ID string
+ Name string
+ TaskCount int
+}
+
type TabloDetailViewModel struct {
TabloID string
TabloName string
@@ -90,6 +107,7 @@ type TabloDetailViewModel struct {
Progress int // 0-100
ProgressLabel string // "{N}%"
Columns []TabloDetailColumnView // always 4 columns
+ Etapes []TabloDetailEtapeView // may be empty
}
```
@@ -117,10 +135,12 @@ From go-backend/internal/web/views/tasks.templ (for drag-and-drop reorder patter
```
// 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: 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)
+// Reorder form: hidden form that submits task order to POST /tablos/{id}/tasks/reorder
+// IMPORTANT: form id must be "reorder-form-{status}" — Sortable.js onEnd looks up
+// getElementById('reorder-form-' + el.dataset.status)
// Guard: if (el._sortable) return; prevents double-init after HTMX swap (Pitfall 4)
```
@@ -151,6 +171,8 @@ CSS class contract (defined in Plan 03, used here):
.task-card-title — task title text
.task-card-delete — delete icon button (opacity 0 at rest)
.tablo-kanban-empty — empty column message
+.tablo-etapes-section — etapes section below kanban board
+.tablo-etape-row — each etape row (name + task count)
```
@@ -158,19 +180,20 @@ CSS class contract (defined in Plan 03, used here):
- Task 1: TabloDetailPage templ component — header, tab bar, kanban board
- go-backend/internal/web/views/tablo_detail.templ
+ Task 1: TabloDetailPage templ component — header, HTMX tab bar, kanban board, etapes section
+ go-backend/internal/web/views/tablo_detail.templ, go-backend/internal/web/handlers/tablo_detail_tab.go
- 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/views/tasks.templ (TasksKanbanLayout — for contrast; tablo-detail kanban is SEPARATE and must NOT reuse TasksKanbanLayout; also read the Sortable.js init script and reorder form pattern)
- 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)
+ - go-backend/internal/web/handlers/tablos.go (renderTablosResponse pattern — for how DashboardContentSwapWithMainClass is called, to replicate in tab handler)
- Create go-backend/internal/web/views/tablo_detail.templ in package views. Import "xtablo-backend/internal/web/ui".
+ Create go-backend/internal/web/views/tablo_detail.templ in package views. Import "xtablo-backend/internal/web/ui" and "strconv".
Define the following templ components in this order:
@@ -178,7 +201,9 @@ CSS class contract (defined in Plan 03, used here):
- Outer div class="tablo-detail-page"
- @TabloDetailHeader(vm)
- @TabloDetailTabBar(vm.TabloID)
- - div class="tablo-kanban-board" containing: @TabloDetailKanbanBoard(vm.Columns, vm.TabloID)
+ - div id="tab-content":
+ * @TabloDetailKanbanBoard(vm.Columns, vm.TabloID)
+ * if len(vm.Etapes) > 0: @TabloDetailEtapesSection(vm.Etapes)
- @TabloDetailSortableScript(vm.TabloID)
2. TabloDetailHeader(vm TabloDetailViewModel) — header section:
@@ -194,15 +219,21 @@ CSS class contract (defined in Plan 03, used here):
- 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:
+ 3. TabloDetailTabBar(tabloID string) — HTMX 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 }
+ - Tab slugs: "overview", "tasks", "files", "discussion", "events"
+ - Each anchor MUST use HTMX attributes (per UI-SPEC locked interaction contract):
+ hx-get="/tablos/{tabloID}/{slug}"
+ hx-target="#tab-content"
+ hx-push-url="true"
+ class="tab-nav-item"
- "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
+ - Do NOT use plain href="#slug" anchors — the UI-SPEC requires HTMX push-url navigation for all tabs.
+ - Use templ.SafeURL for the hx-get value to satisfy templ's URL safety requirements.
- 4. TabloDetailKanbanBoard(columns []TabloDetailColumnView, tabloID string) — kanban board:
+ 4. TabloDetailKanbanBoard(columns []TabloDetailColumnView, tabloID string) — renders the kanban board:
+ - Outer div class="tablo-kanban-board"
- For each column: @TabloDetailKanbanColumn(col, tabloID)
5. TabloDetailKanbanColumn(col TabloDetailColumnView, tabloID string) — single column:
@@ -210,10 +241,18 @@ CSS class contract (defined in Plan 03, used here):
- 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"
+ * a class="tablo-kanban-add-link" href={ templ.SafeURL(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) }
+ - Hidden reorder form (REQUIRED for Sortable.js onEnd):
+
- div id={ "create-zone-" + col.ID } (empty create zone for HTMX task create swap)
6. TabloDetailTaskCard(task TabloDetailTaskView, tabloID string) — task card:
@@ -234,15 +273,33 @@ CSS class contract (defined in Plan 03, used here):
},
})
- 7. TabloDetailSortableScript(tabloID string) — JavaScript block:
- Use a templ @raw block (or templ.Raw) to emit the Sortable.js init script that:
+ 7. TabloDetailEtapesSection(etapes []TabloDetailEtapeView) — etapes list below kanban:
+ - section element class="tablo-etapes-section"
+ - h2 or h3 heading: "Étapes" (French)
+ - ul element: for each etape, render a li class="tablo-etape-row" containing:
+ span class="tablo-etape-name" { etape.Name }
+ span class="tablo-etape-count" { strconv.Itoa(etape.TaskCount) + " tâche(s)" }
+
+ 8. TabloDetailSortableScript(tabloID string) — JavaScript block:
+ Use 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.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) { 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