docs(20): create phase 20 plan — tablo detail page + kanban restyle

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>
This commit is contained in:
Arthur Belleville 2026-05-18 15:29:41 +02:00
parent e18bf66dbf
commit 20e0a02edc
No known key found for this signature in database
4 changed files with 996 additions and 1 deletions

View file

@ -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 | — |

View file

@ -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"
---
<objective>
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.
</objective>
<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>
<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-RESEARCH.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/20-tablo-detail-kanban-restyle/20-UI-SPEC.md
<interfaces>
<!-- Key types the executor needs. Extracted from codebase. -->
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)
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Define TabloDetailViewModel and test scaffold</name>
<files>go-backend/internal/web/views/tablo_detail_view.go, go-backend/internal/web/handlers/tablo_detail_test.go</files>
<read_first>
- 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+)
</read_first>
<behavior>
- 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
</behavior>
<action>
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.
</action>
<verify>
<automated>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</automated>
</verify>
<done>go test ./internal/web/views/... -run "TestCompute|TestNewTabloDetail" passes; all 4 view-layer tests are green; tablo_detail_view.go compiles with TabloDetailViewModel exported.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: GetTabloDetailPage handler + router registration</name>
<files>go-backend/internal/web/handlers/tablo_detail.go, go-backend/router.go</files>
<read_first>
- 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)
</read_first>
<behavior>
- 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
</behavior>
<action>
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, "<div>%s</div>", 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.
</action>
<verify>
<automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/go-backend && go test ./... -run "TestGetTabloDetailPage" -count=1 -v 2>&1 | tail -30</automated>
</verify>
<done>All TestGetTabloDetailPage_* tests pass; go build ./... succeeds; GET /tablos/{validID} integration test returns 200 with tablo name in body.</done>
</task>
</tasks>
<threat_model>
## 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 |
</threat_model>
<verification>
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
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
Create `.planning/phases/20-tablo-detail-kanban-restyle/20-01-SUMMARY.md` when done.
</output>

View file

@ -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"
---
<objective>
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.
</objective>
<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>
<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.md
<interfaces>
<!-- Key types and contracts the executor needs. -->
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
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: TabloDetailPage templ component — header, tab bar, kanban board</name>
<files>go-backend/internal/web/views/tablo_detail.templ</files>
<read_first>
- 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)
</read_first>
<action>
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
</action>
<verify>
<automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/go-backend && just generate 2>&1 | tail -5 && go build ./... 2>&1</automated>
</verify>
<done>just generate succeeds (tablo_detail_templ.go generated); go build ./... exits 0; tablo_detail.templ defines TabloDetailPage, TabloDetailHeader, TabloDetailTabBar, TabloDetailKanbanBoard, TabloDetailKanbanColumn, TabloDetailTaskCard, TabloDetailSortableScript.</done>
</task>
<task type="auto">
<name>Task 2: Replace TabloDetailPage stub + wire templ component + run handler tests</name>
<files>go-backend/internal/web/views/tablo_detail_view.go</files>
<read_first>
- 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)
</read_first>
<action>
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
</action>
<verify>
<automated>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</automated>
</verify>
<done>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.</done>
</task>
</tasks>
<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>
<verification>
After Plan 02:
- `just generate` exits 0 with tablo_detail_templ.go created
- `go build ./...` exits 0
- `go test ./... -run "TestGetTabloDetailPage|TestComputeTabloProgress|TestNewTabloDetail|TestTabloDetail" -count=1` all pass
- `go test ./... -count=1` full suite green
- tablo_detail.templ defines 7 templ components including TabloDetailPage and TabloDetailTaskCard
- Task cards use class="task-card" and data-task-id attribute
- Task list containers use class="task-list sortable-column"
- Drag handle uses class="task-drag-handle"
- Empty column uses class="tablo-kanban-empty" with text "Aucune tâche"
</verification>
<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>
<output>
Create `.planning/phases/20-tablo-detail-kanban-restyle/20-02-SUMMARY.md` when done.
</output>

View file

@ -0,0 +1,358 @@
---
phase: 20-tablo-detail-kanban-restyle
plan: 03
type: execute
wave: 2
depends_on:
- 20-01
files_modified:
- go-backend/internal/web/ui/app.css
autonomous: true
requirements:
- DETAIL-01
- TASK-01
must_haves:
truths:
- "app.css contains a .tablo-detail-header rule with flex-direction, padding, and border-bottom"
- "app.css contains a .tablo-detail-title rule with font-size 1.75rem and font-weight 600"
- "app.css contains a .tablo-metadata-row rule with display flex and gap 24px"
- "app.css contains a .tablo-kanban-board rule with display flex, gap 16px, and overflow-x auto"
- "app.css contains a .tablo-kanban-column rule with width 18rem, border-radius 0.75rem, and border"
- "app.css contains a .task-card rule with flex-direction column, border-radius 8px, and gap 8px"
- "app.css contains a .task-drag-handle rule with opacity 0 and a .task-card:hover .task-drag-handle rule with opacity 1"
- "app.css contains a .task-card-delete rule with opacity 0 at rest and visible on .task-card:hover"
- "app.css contains a .tablo-progress-bar rule with background var(--color-brand-primary) — NOT var(--project-color)"
- "app.css contains a .tablo-files-table-wrapper rule with border-radius 12px"
- "app.css .task-list rule is updated to include gap 8px and padding 8px"
artifacts:
- path: "go-backend/internal/web/ui/app.css"
provides: "All tablo detail + kanban restyle CSS rules"
contains: ".tablo-detail-header"
key_links:
- from: "go-backend/internal/web/views/tablo_detail.templ"
to: "go-backend/internal/web/ui/app.css"
via: "CSS class names applied to HTML elements"
pattern: ".tablo-kanban-board"
---
<objective>
Apply all CSS changes from the UI-SPEC to app.css: new tablo detail header styles, kanban board layout, task card restyle (column-flex, hover shadow, opacity-based drag handle), progress bar fill fix, files table wrapper, and updated task-list gap. This plan runs in parallel with Plan 02 since CSS class names are already locked in the UI-SPEC.
Purpose: Without these CSS rules, the templ components from Plan 02 render unstyled. This plan is the visual half of the restyle.
Output: Updated app.css with all new and modified CSS blocks from UI-SPEC.
</objective>
<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>
<context>
@/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
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/skills/sketch-findings-xtablo-source/SKILL.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Tablo detail header and metadata CSS</name>
<files>go-backend/internal/web/ui/app.css</files>
<read_first>
- go-backend/internal/web/ui/app.css (read lines 1040-1080 — existing .project-progress-track, .project-progress-bar, .project-progress-label rules to understand context before modifying)
- go-backend/internal/web/ui/app.css (read lines 880-920 — existing .tab-nav, .tab-nav-item rules to confirm no tab changes needed)
- go-backend/internal/web/ui/base.css (read first 60 lines — confirm --color-brand-primary, --color-border-default, --color-surface-muted, --color-text-primary token names)
</read_first>
<action>
Edit go-backend/internal/web/ui/app.css. Append a new section at the end of the file titled "/* === Tablo Detail Page === */". Add the following CSS blocks in this order. Use exact property values from the UI-SPEC — do not approximate or simplify.
BLOCK 1: .tablo-detail-page — outer container
padding: 24px 32px;
BLOCK 2: .tablo-detail-header — header element
border-bottom: 1px solid var(--color-border-muted);
display: flex;
flex-direction: column;
gap: 0;
padding-bottom: 0;
BLOCK 3: .tablo-detail-title-row — flex row with avatar + h1
align-items: center;
display: flex;
gap: 16px;
margin-bottom: 0;
padding-bottom: 16px;
BLOCK 4: .tablo-detail-avatar — 48×48 colored circle
align-items: center;
border-radius: 12px;
color: #ffffff;
display: flex;
flex-shrink: 0;
font-size: 1rem;
font-weight: 700;
height: 48px;
justify-content: center;
width: 48px;
BLOCK 5: .tablo-detail-title — h1 tablo name
color: var(--color-text-primary);
font-size: 1.75rem;
font-weight: 600;
line-height: 1.2;
margin: 0;
BLOCK 6: .tablo-metadata-row — metadata flex row
align-items: center;
border-bottom: 1px solid var(--color-border-muted);
display: flex;
flex-wrap: wrap;
gap: 24px;
padding-block: 16px;
BLOCK 7: .tablo-meta-segment — each segment in metadata row
align-items: center;
color: var(--color-text-secondary);
display: flex;
font-size: 0.875rem;
gap: 8px;
BLOCK 8: .tablo-meta-progress — progress segment override
align-items: center;
display: flex;
gap: 8px;
BLOCK 9: .tablo-progress-bar — detail page progress bar fill (MUST use brand-primary, NOT project-color — Pitfall 5)
background: var(--color-brand-primary);
border-radius: 9999px;
height: 5px;
BLOCK 10: .tablo-tab-bar — tab navigation below header
border-bottom: 1px solid var(--color-border-muted);
display: flex;
gap: 24px;
margin-top: 0;
padding-top: 8px;
BLOCK 11: .tablo-tab-bar .tab-nav-item — override for tabs within tablo detail
font-size: 0.875rem;
padding-bottom: 12px;
Do NOT modify the existing .project-progress-bar rule (it is used by tablo cards on the list page which keep var(--project-color)). The tablo detail page uses .tablo-progress-bar (different class) to avoid the conflict.
Do NOT modify .tab-nav or .tab-nav-item globally — only scope changes to .tablo-tab-bar context if any override is needed.
</action>
<verify>
<automated>grep -c "tablo-detail-header\|tablo-detail-title\|tablo-metadata-row\|tablo-progress-bar\|tablo-tab-bar" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/go-backend/internal/web/ui/app.css</automated>
</verify>
<done>grep count returns 5 or more (one match per class block); .tablo-progress-bar uses var(--color-brand-primary) not var(--project-color); grep for "tablo-progress-bar" returns a rule with "background: var(--color-brand-primary)".</done>
</task>
<task type="auto">
<name>Task 2: Kanban board, task card, files table, and task-list gap CSS</name>
<files>go-backend/internal/web/ui/app.css</files>
<read_first>
- go-backend/internal/web/ui/app.css (read lines 1283-1350 — existing .task-list, .task-row, .task-body, .task-check rules; understand what to modify vs what to add)
- go-backend/internal/web/ui/app.css (read lines 1254-1268 — existing .tasks-section rule with border-radius: 1rem — do NOT change this; kanban column gets its own rule)
- go-backend/internal/web/ui/base.css (confirm --color-surface-default, --color-border-default, --color-border-strong, --color-surface-muted token names)
</read_first>
<action>
Edit go-backend/internal/web/ui/app.css. Continue the "/* === Tablo Detail Page === */" section from Task 1 (or append after it). Add the following blocks:
BLOCK 12: .tablo-kanban-board — flex container for the kanban columns
display: flex;
gap: 16px;
overflow-x: auto;
padding-bottom: 16px;
padding-top: 24px;
BLOCK 13: .tablo-kanban-column — each column card
background: var(--color-surface-default);
border: 1px solid var(--color-border-default);
border-radius: 0.75rem;
flex-shrink: 0;
overflow: hidden;
width: 18rem;
BLOCK 14: .tablo-kanban-column-header — column header area
align-items: center;
background: var(--color-surface-muted);
display: flex;
gap: 8px;
justify-content: space-between;
padding: 12px 16px;
BLOCK 15: .tablo-kanban-column-title — column h3 label
color: var(--color-text-primary);
font-size: 0.875rem;
font-weight: 600;
margin: 0;
BLOCK 16: .tablo-kanban-task-count — task count pill
background: var(--color-surface-default);
border: 1px solid var(--color-border-muted);
border-radius: 9999px;
color: var(--color-text-secondary);
font-size: 0.75rem;
padding: 0 8px;
BLOCK 17: .tablo-kanban-add-link — "+ Ajouter" link
color: var(--color-text-brand);
font-size: 0.875rem;
font-weight: 400;
margin-left: auto;
text-decoration: none;
BLOCK 18: .tablo-kanban-empty — empty column state
color: var(--color-text-faint);
font-size: 0.875rem;
padding: 24px 16px;
text-align: center;
BLOCK 19: .task-card — new card layout (replaces/supplements .task-row for tablo detail)
background: var(--color-surface-default);
border: 1px solid var(--color-border-default);
border-radius: 8px;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 8px;
padding-block: 8px;
padding-inline: 12px;
transition: box-shadow 0.12s ease, border-color 0.12s ease;
BLOCK 20: .task-card:hover
border-color: var(--color-border-strong);
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
BLOCK 21: .task-card-top-row — row with drag handle, title, delete icon
align-items: flex-start;
display: flex;
gap: 6px;
BLOCK 22: .task-drag-handle — drag handle (Braille pattern)
color: var(--color-text-faint);
cursor: grab;
flex-shrink: 0;
font-size: 1rem;
opacity: 0;
transition: opacity 0.12s ease;
BLOCK 23: .task-card:hover .task-drag-handle — reveal on card hover
opacity: 1;
BLOCK 24: .task-card-title — task title text
color: var(--color-text-primary);
flex: 1;
font-size: 0.875rem;
font-weight: 400;
line-height: 1.4;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
BLOCK 25: .task-card-delete — delete icon wrapper (opacity hidden at rest)
flex-shrink: 0;
opacity: 0;
transition: opacity 0.12s ease;
BLOCK 26: .task-card:hover .task-card-delete — reveal on card hover
opacity: 1;
Now modify the EXISTING .task-list rule (around line 1283 in app.css):
Find the current rule:
.task-list {
display: flex;
flex-direction: column;
}
Change it to add gap and padding:
.task-list {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
}
This change affects both the global tasks page and the tablo detail page equally — gap between cards is desired everywhere.
Add files table wrapper CSS:
BLOCK 27: .tablo-files-table-wrapper — wrapper for the files table
border: 1px solid var(--color-border-default);
border-radius: 12px;
overflow: hidden;
BLOCK 28: .tablo-files-table-wrapper thead tr
background: var(--color-surface-muted);
BLOCK 29: .tablo-files-table-wrapper thead th
color: var(--color-text-muted);
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
BLOCK 30: .tablo-files-table-wrapper tbody tr
border-bottom: 1px solid var(--color-border-default);
BLOCK 31: .tablo-files-table-wrapper tbody tr:hover
background: var(--color-surface-subtle);
CRITICAL checks before saving:
- .tablo-progress-bar uses "background: var(--color-brand-primary)" — grep to verify
- .task-card and .task-row are SEPARATE rules — .task-row must NOT be removed (it is used by the global tasks page)
- .tasks-section border-radius (1rem) must NOT be changed — only .tablo-kanban-column gets 0.75rem
- All opacity transitions use "0.12s ease" consistent with design system
</action>
<verify>
<automated>grep -v "^[[:space:]]*//" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/go-backend/internal/web/ui/app.css | grep -c "tablo-kanban-board\|tablo-kanban-column\|task-card\b\|task-drag-handle\|tablo-files-table-wrapper\|tablo-kanban-empty"</automated>
</verify>
<done>grep count returns 6 or more; .task-card block exists with flex-direction: column; .task-drag-handle exists with opacity: 0; .task-card:hover .task-drag-handle exists with opacity: 1; .tablo-kanban-board exists with overflow-x: auto; .task-list updated to include gap: 8px; .task-row unchanged.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| CSS cascade | New .task-card rules must not override or break .task-row rules used on global /tasks page |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-20-06 | Tampering | CSS cascade — .task-card vs .task-row collision | mitigate | .task-card is a new separate selector; .task-row selector is unchanged; they do not overlap since tablo-detail uses task-card and global tasks page uses task-row |
| T-20-SC | Tampering | npm/pip/cargo installs | accept | No package installs — CSS file edit only |
</threat_model>
<verification>
After Plan 03:
- grep for "tablo-kanban-board" in app.css returns at least one match with "overflow-x: auto"
- grep for "tablo-progress-bar" in app.css returns a match with "background: var(--color-brand-primary)"
- grep for "tablo-progress-bar" does NOT return "var(--project-color)"
- grep for ".task-card\b" in app.css returns the column-flex card rule
- grep for ".task-drag-handle" returns opacity: 0 rule
- grep for ".task-card:hover .task-drag-handle" returns opacity: 1 rule
- grep for ".task-row" still returns the original horizontal rule (not removed)
- grep for ".tasks-section" still has border-radius: 1rem (not changed to 0.75rem)
- grep for ".task-list" returns updated rule with gap: 8px
- go build ./... still exits 0 (CSS is static — no compilation risk, but verify Go still compiles cleanly)
</verification>
<success_criteria>
- Visual: tablo detail header shows tablo name in large font, metadata row, and progress bar with purple fill
- Visual: kanban columns are 18rem wide with rounded corners, muted header background
- Visual: task cards are white box with column layout, subtle border, hover shadow
- Visual: drag handle appears on card hover (opacity transition, not display toggle)
- Visual: delete icon appears on card hover
- Visual: empty columns show "Aucune tâche" centered
- CSS regression: global /tasks kanban page still renders correctly (task-row unchanged)
</success_criteria>
<output>
Create `.planning/phases/20-tablo-detail-kanban-restyle/20-03-SUMMARY.md` when done.
</output>