diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index ed3070c..6ead794 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -85,12 +85,18 @@ Plans:
### Phase 20: Tablo Detail & Kanban Restyle
**Goal:** Restyle the tablo detail page and kanban board to match Figma.
**Requirements:** DETAIL-01, TASK-01
+**Plans:** 3 plans
**Success criteria:**
1. Tablo detail header shows tablo name, status, and progress matching Figma
2. Kanban columns and task cards are restyled to match Figma
3. Drag-and-drop reorder continues to work after restyle
4. Etapes section and files table match Figma layout
+Plans:
+- [ ] 20-01-PLAN.md — Handler + view model + route: GET /tablos/{tabloID}, TabloDetailViewModel, test scaffold
+- [ ] 20-02-PLAN.md — TabloDetailPage templ components: header, tab bar, kanban board, task cards
+- [ ] 20-03-PLAN.md — CSS restyle: tablo detail header, kanban board layout, task card, progress bar, files table
+
### Phase 21: Task Grid & Roadmap Views
**Goal:** Add grid/table and roadmap/timeline views to the task section of a tablo.
**Requirements:** TASK-02, TASK-03
@@ -132,6 +138,6 @@ Plans:
| 17. Chat & Planning | v3.0 | 2/2 | Complete | 2026-05-17 |
| 18. App Shell & Navigation | v4.0 | 0/3 | Pending | — |
| 19. Tablo List Revamp | v4.0 | 0/3 | Pending | — |
-| 20. Tablo Detail & Kanban | v4.0 | — | Pending | — |
+| 20. Tablo Detail & Kanban | v4.0 | 0/3 | Pending | — |
| 21. Task Grid & Roadmap Views | v4.0 | — | Pending | — |
| 22. Calendar Rework | v4.0 | — | Pending | — |
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
new file mode 100644
index 0000000..1601c81
--- /dev/null
+++ b/.planning/phases/20-tablo-detail-kanban-restyle/20-01-PLAN.md
@@ -0,0 +1,305 @@
+---
+phase: 20-tablo-detail-kanban-restyle
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - go-backend/internal/web/handlers/tablo_detail.go
+ - go-backend/internal/web/views/tablo_detail_view.go
+ - go-backend/internal/web/handlers/tablo_detail_test.go
+ - go-backend/router.go
+autonomous: true
+requirements:
+ - DETAIL-01
+ - TASK-01
+
+must_haves:
+ truths:
+ - "GET /tablos/{tabloID} returns 200 with the tablo name in the HTML response body"
+ - "Accessing a tablo owned by a different user returns 404"
+ - "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"
+ 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"]
+ - 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"
+ provides: "GET /tablos/{tabloID} route registered"
+ contains: "mux.Get(\"/tablos/{tabloID}\""
+ key_links:
+ - from: "go-backend/router.go"
+ to: "go-backend/internal/web/handlers/tablo_detail.go"
+ via: "authHandler.GetTabloDetailPage()"
+ pattern: "GetTabloDetailPage"
+ - from: "go-backend/internal/web/handlers/tablo_detail.go"
+ to: "h.repo.(tabloDetailRepository)"
+ via: "type assertion for ListTasksByTablo"
+ pattern: "tabloDetailRepository"
+---
+
+
+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.
+
+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.
+
+Output: Handler file, view model file, test scaffold, updated router.go.
+
+
+
+@/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
+
+
+
+@/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-RESEARCH.md
+@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/20-tablo-detail-kanban-restyle/20-UI-SPEC.md
+
+
+
+
+From go-backend/internal/web/handlers/auth.go:
+```go
+// AuthRepository interface — does NOT include ListTasksByTablo
+type AuthRepository interface {
+ CreateAuthUser(ctx context.Context, input CreateAuthUserInput) (uuid.UUID, error)
+ GetAuthUserByEmail(ctx context.Context, email string) (AuthUser, error)
+ GetPublicUserByID(ctx context.Context, id uuid.UUID) (PublicUser, error)
+ CreateSession(ctx context.Context, token string, userID uuid.UUID, expiresAt time.Time) error
+ GetSessionByToken(ctx context.Context, token string) (Session, error)
+ DeleteSessionByToken(ctx context.Context, token string) error
+ CreateTablo(ctx context.Context, input CreateTabloInput) (TabloRecord, error)
+ UpdateTablo(ctx context.Context, input UpdateTabloInput) error
+ ListTablos(ctx context.Context, input ListTablosInput) ([]TabloRecord, error)
+ SoftDeleteTablo(ctx context.Context, tabloID uuid.UUID, ownerID uuid.UUID) error
+}
+
+type AuthHandler struct { repo AuthRepository }
+func (h *AuthHandler) authenticatedUser(ctx context.Context, r *http.Request) (PublicUser, bool)
+```
+
+From go-backend/internal/web/handlers/tasks.go:
+```go
+// taskPageRepository — separate interface for task reads; ListTasksByTablo NOT in AuthRepository
+type taskPageRepository interface {
+ ListTasksByOwner(ctx context.Context, ownerID uuid.UUID) ([]TaskRecord, error)
+}
+
+// taskMutationRepository — separate interface for task writes
+type taskMutationRepository interface {
+ CreateTask(ctx context.Context, input CreateTaskInput) (TaskRecord, error)
+ GetTaskByID(ctx context.Context, taskID uuid.UUID, ownerID uuid.UUID) (TaskRecord, error)
+ UpdateTask(ctx context.Context, input UpdateTaskInput) (TaskRecord, error)
+ SoftDeleteTask(ctx context.Context, taskID uuid.UUID, ownerID uuid.UUID) error
+}
+
+type ListTasksByTabloInput = taskmodel.ListByTabloInput // {OwnerID uuid.UUID, TabloID uuid.UUID}
+type TaskRecord = taskmodel.Record // {ID, TabloID, OwnerID, Title, Status, ...}
+type TaskStatus = taskmodel.Status
+const TaskStatusDone = taskmodel.StatusDone // "done"
+const TaskStatusTodo = taskmodel.StatusTodo
+const TaskStatusInProgress = taskmodel.StatusInProgress
+const TaskStatusInReview = taskmodel.StatusInReview
+```
+
+From go-backend/internal/web/handlers/tablos.go:
+```go
+// findTabloByID — existing helper, available within handlers package
+func findTabloByID(tablos []TabloRecord, targetID uuid.UUID) (TabloRecord, bool)
+func tabloStatusPresentation(status TabloStatus) (string, string, int, string)
+// returns: (statusLabel, statusClass, progress, statusTone)
+
+type TabloRecord = tablomodel.Record // {ID, OwnerID, Name, Color, Status, CreatedAt, UpdatedAt}
+type ListTablosInput = tablomodel.ListInput // {OwnerID, Status*}
+```
+
+From go-backend/internal/web/handlers/in_memory_auth_repository.go (line 168):
+```go
+func (r *InMemoryAuthRepository) ListTasksByTablo(_ context.Context, input ListTasksByTabloInput) ([]TaskRecord, error)
+// InMemoryAuthRepository already implements this — safe to use in tests
+```
+
+From go-backend/router.go:
+```go
+func newRouterWithHandler(authHandler *handlers.AuthHandler) http.Handler {
+ mux := chi.NewRouter()
+ // ... existing routes ...
+ mux.Get("/tablos/{tabloID}/edit", authHandler.GetEditTabloModal())
+ mux.Post("/tablos/{tabloID}", authHandler.PostTabloUpdate())
+ mux.Delete("/tablos/{tabloID}", authHandler.DeleteTablo())
+ // ADD: mux.Get("/tablos/{tabloID}", authHandler.GetTabloDetailPage())
+ // Must be BEFORE the edit/post/delete routes to avoid conflicts — chi uses first-match
+}
+```
+
+From go-backend/internal/web/views (render pattern from tablos.go renderTablosResponse):
+```go
+// DashboardPageWithMainClass and DashboardContentSwapWithMainClass are used for full-width content
+views.DashboardContentSwapWithMainClass(activePath, tablos, "flex-1 overflow-auto", content).Render(r.Context(), w)
+views.DashboardPageWithMainClass(activePath, tablos, "flex-1 overflow-auto", content).Render(r.Context(), w)
+// activePath for tablo detail: "/tablos" (parent, so sidebar Tablos item stays highlighted per Pitfall 3)
+```
+
+
+
+
+
+
+ Task 1: Define TabloDetailViewModel 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/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
+ - 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
+ - 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: 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
+
+
+ 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:
+ - 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"
+ - Sets CreateHref="/tablos/"+tablo.ID.String()+"/tasks/create?status="+colID for each column
+ - 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).
+
+ 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.
+
+
+ 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.
+
+
+
+ Task 2: GetTabloDetailPage handler + router registration
+ go-backend/internal/web/handlers/tablo_detail.go, go-backend/router.go
+
+ - go-backend/internal/web/handlers/tablos.go (findTabloByID, renderTablosResponse pattern, tabloStatusPresentation, authenticatedUser)
+ - go-backend/internal/web/handlers/tasks.go (type assertion pattern h.repo.(taskPageRepository), ListTasksByTabloInput)
+ - go-backend/internal/web/handlers/auth.go (AuthHandler struct, AuthRepository interface)
+ - go-backend/router.go (current route list — locate where to insert GET /tablos/{tabloID} before edit/post/delete)
+ - go-backend/internal/web/views/tablo_detail_view.go (just created — NewTabloDetailViewModel signature)
+
+
+ - tabloDetailRepository interface exposes ListTasksByTablo(ctx, ListTasksByTabloInput) ([]TaskRecord, error)
+ - GetTabloDetailPage returns http.HandlerFunc that: (a) checks auth, (b) parses tabloID as UUID, (c) ListTablos+findTabloByID, (d) type-asserts h.repo to tabloDetailRepository, (e) ListTasksByTablo, (f) fetches owner via GetPublicUserByID (falls back to empty string on error), (g) builds TabloDetailViewModel, (h) renders DashboardPageWithMainClass or DashboardContentSwapWithMainClass with activePath="/tablos" and a placeholder views.TabloDetailPage(vm) stub
+ - On auth failure: 302 /login
+ - On invalid UUID: 400 "invalid tablo id"
+ - On tablo not found: 404 "tablo not found"
+ - On type assertion failure (should never happen in production): 500 "tasks repository not configured"
+ - activePath passed to Dashboard helpers is "/tablos" (not "/tablos/{id}") so sidebar "Tablos" nav item stays highlighted (Pitfall 3 from RESEARCH)
+ - router.go adds: mux.Get("/tablos/{tabloID}", authHandler.GetTabloDetailPage()) — positioned before the existing mux.Get("/tablos/{tabloID}/edit", ...) line
+
+
+ Create go-backend/internal/web/handlers/tablo_detail.go in package handlers. Import context, net/http, github.com/google/uuid, xtablo-backend/internal/web/views.
+
+ Define tabloDetailRepository interface:
+ type tabloDetailRepository interface {
+ ListTasksByTablo(ctx context.Context, input ListTasksByTabloInput) ([]TaskRecord, error)
+ }
+
+ Define GetTabloDetailPage() http.HandlerFunc on *AuthHandler:
+ - Call h.authenticatedUser; on failure redirect /login
+ - uuid.Parse(r.PathValue("tabloID")); on error http.Error 400 "invalid tablo id"
+ - h.repo.ListTablos(ctx, ListTablosInput{OwnerID: user.ID}); on error 500
+ - findTabloByID(tablos, tabloID); on not-found http.Error 404 "tablo not found"
+ - taskRepo, ok := h.repo.(tabloDetailRepository); if !ok http.Error 500 "tasks repository not configured"
+ - taskRepo.ListTasksByTablo(ctx, ListTasksByTabloInput{OwnerID: user.ID, TabloID: tabloID}); on error 500
+ - ownerName: call h.repo.GetPublicUserByID(ctx, user.ID); use owner.DisplayName; on error ownerName=""
+ - vm := views.NewTabloDetailViewModel(tablo, tasks, ownerName)
+ - content := views.TabloDetailPage(vm) [this templ function will be a stub until Plan 02]
+ - Render using DashboardPageWithMainClass (non-HX) or DashboardContentSwapWithMainClass (HX) with activePath="/tablos", mainClass="flex-1 overflow-auto"
+
+ 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)
+ 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.
+
+ Update go-backend/router.go: add mux.Get("/tablos/{tabloID}", authHandler.GetTabloDetailPage()) immediately before the existing mux.Get("/tablos/{tabloID}/edit", ...) line.
+
+ Verify handler tests from tablo_detail_test.go pass.
+
+
+ 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.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| client→GET /tablos/{tabloID} | Unauthenticated or cross-user path value |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-20-01 | Elevation of Privilege | GetTabloDetailPage — IDOR via guessed UUID | mitigate | findTabloByID filters by OwnerID from authenticated session; returns 404 for any tablo not owned by authenticated user |
+| T-20-02 | Spoofing | Cookie-based session auth | mitigate | h.authenticatedUser validates session cookie via GetSessionByToken + expiry check — existing pattern |
+| T-20-03 | Tampering | ListTasksByTablo input | mitigate | OwnerID comes from authenticated session, not from request params; tabloID validated via uuid.Parse before use |
+| T-20-SC | Tampering | npm/pip/cargo installs | accept | No package installs in this plan — pure Go code using existing dependencies |
+
+
+
+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 ./... -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
+
+
+
+- GET /tablos/{validUUID} (authenticated, owned) returns 200 containing tablo name
+- 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
+- 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
new file mode 100644
index 0000000..33871e4
--- /dev/null
+++ b/.planning/phases/20-tablo-detail-kanban-restyle/20-02-PLAN.md
@@ -0,0 +1,326 @@
+---
+phase: 20-tablo-detail-kanban-restyle
+plan: 02
+type: execute
+wave: 2
+depends_on:
+ - 20-01
+files_modified:
+ - go-backend/internal/web/views/tablo_detail.templ
+ - go-backend/internal/web/views/tablo_detail_view.go
+autonomous: true
+requirements:
+ - DETAIL-01
+ - TASK-01
+
+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"
+ - "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 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"
+ artifacts:
+ - path: "go-backend/internal/web/views/tablo_detail.templ"
+ provides: "TabloDetailPage + all sub-components (header, tab bar, kanban board, task card)"
+ 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"
+ key_links:
+ - from: "go-backend/internal/web/views/tablo_detail.templ"
+ to: "go-backend/internal/web/views/tablo_detail_view.go"
+ via: "TabloDetailViewModel struct fields"
+ pattern: "TabloDetailViewModel"
+ - from: ".tablo-kanban-board .sortable-column"
+ to: "POST /tablos/{id}/tasks/reorder"
+ via: "Sortable.js onEnd -> #reorder-form submit"
+ pattern: "sortable-column"
+---
+
+
+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.
+
+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.
+
+
+
+@/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
+
+
+
+@/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.md
+
+
+
+
+From go-backend/internal/web/views/tablo_detail_view.go (created in Plan 01):
+```go
+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:
+```go
+// 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:
+```go
+// 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):
+```go
+// 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