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:
parent
e18bf66dbf
commit
20e0a02edc
4 changed files with 996 additions and 1 deletions
|
|
@ -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 | — |
|
||||
|
|
|
|||
305
.planning/phases/20-tablo-detail-kanban-restyle/20-01-PLAN.md
Normal file
305
.planning/phases/20-tablo-detail-kanban-restyle/20-01-PLAN.md
Normal 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>
|
||||
326
.planning/phases/20-tablo-detail-kanban-restyle/20-02-PLAN.md
Normal file
326
.planning/phases/20-tablo-detail-kanban-restyle/20-02-PLAN.md
Normal 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>
|
||||
358
.planning/phases/20-tablo-detail-kanban-restyle/20-03-PLAN.md
Normal file
358
.planning/phases/20-tablo-detail-kanban-restyle/20-03-PLAN.md
Normal 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>
|
||||
Loading…
Reference in a new issue