617 lines
28 KiB
Markdown
617 lines
28 KiB
Markdown
|
|
# Phase 20: Tablo Detail & Kanban Restyle - Research
|
|||
|
|
|
|||
|
|
**Researched:** 2026-05-18
|
|||
|
|
**Domain:** Go + HTMX + Tailwind + templ — new page creation + CSS restyle
|
|||
|
|
**Confidence:** HIGH
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Summary
|
|||
|
|
|
|||
|
|
Phase 20 has two intertwined goals: (1) create a **tablo detail page** that does not yet exist in the codebase, and (2) deliver the Figma-matching visual restyle for that page's kanban board and task cards. The current Go backend has no `GET /tablos/{tabloID}` route — clicking a tablo in the list has no detail view. This phase must build the page from scratch, then style it to match the UI-SPEC.
|
|||
|
|
|
|||
|
|
The existing kanban implementation lives exclusively inside the global `/tasks` page (`tasks.templ` → `TasksKanbanLayout`). That layout uses a CSS grid (`grid-cols-1 md:grid-cols-2 lg:grid-cols-4`) and the `TaskCard` component. The tablo detail kanban is a separate surface: a flex-row of fixed-width columns (18rem each, `overflow-x: auto`) scoped to one tablo, with Sortable.js drag-and-drop. The Phase 4 code (task creation, reorder handler, Sortable.js init) for the tablo-detail kanban currently exists only in `router_test.go` integration tests referencing `ListTasksByTablo`, implying it was coded in a previous milestone's tablo detail handler that was later replaced by `go-backend/`. All task mutation infrastructure (CreateTask, GetTaskByID, UpdateTask, SoftDeleteTask, ListTasksByTablo) exists in the repository layer and in the InMemory test repository — it just has no route-level handler wiring for the detail page.
|
|||
|
|
|
|||
|
|
The primary recommendation: build `GET /tablos/{tabloID}` as a new handler + templ component, add `GET/POST /tablos/{tabloID}/tasks/...` for task CRU within the detail context, implement the tab-bar, and apply the full CSS restyle from `20-UI-SPEC.md` in `app.css`.
|
|||
|
|
|
|||
|
|
**Primary recommendation:** Create the tablo detail page as a new handler + view layer; reuse existing `ListTasksByTablo` and task mutation repo methods; apply all UI-SPEC CSS changes to `app.css` and the new templ components.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Architectural Responsibility Map
|
|||
|
|
|
|||
|
|
| Capability | Primary Tier | Secondary Tier | Rationale |
|
|||
|
|
|------------|-------------|----------------|-----------|
|
|||
|
|
| Tablo detail route (`GET /tablos/{tabloID}`) | Go handler (server) | — | Fetches tablo record + tasks, builds view model, renders templ |
|
|||
|
|
| Tab bar (Overview/Tasks/Files/Discussion/Events) | templ template | — | Static HTML tabs, HTMX push-url for navigation |
|
|||
|
|
| Kanban board layout | templ template (`tablos_detail.templ`) | CSS (`app.css`) | Column+card HTML; flex layout and card styles in CSS |
|
|||
|
|
| Task CRUD within detail | Go handler (server) | — | CreateTask/UpdateTask/DeleteTask/ReorderTask scoped to tabloID |
|
|||
|
|
| Drag-and-drop reorder | Browser (Sortable.js) | Go handler (reorder endpoint) | JS-side drag; server persists new order via existing handler pattern |
|
|||
|
|
| Progress bar computation | Go handler (server) | — | doneTasks/totalTasks computed at render time from task list |
|
|||
|
|
| CSS restyle (cards, columns, header) | CSS (`app.css`) | — | Token-based rules; no new tokens needed |
|
|||
|
|
| Files tab (restyle only) | templ + CSS | — | Existing `ui.Table`; add border/radius wrapper and header style |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
<phase_requirements>
|
|||
|
|
## Phase Requirements
|
|||
|
|
|
|||
|
|
| ID | Description | Research Support |
|
|||
|
|
|----|-------------|------------------|
|
|||
|
|
| DETAIL-01 | Tablo detail page (header, tasks section, etapes section, files table) is restyled to match Figma | New `GET /tablos/{tabloID}` handler + `TabloDetailPage` templ component + CSS in `app.css` matching UI-SPEC tokens |
|
|||
|
|
| TASK-01 | Kanban board columns, task cards, and drag-and-drop are restyled to match Figma | New tablo-scoped kanban templ + `.task-card` CSS block in `app.css`; Sortable.js init preserved verbatim |
|
|||
|
|
</phase_requirements>
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Standard Stack
|
|||
|
|
|
|||
|
|
### Core (existing — no new packages)
|
|||
|
|
|
|||
|
|
| Library | Version | Purpose | Why Standard |
|
|||
|
|
|---------|---------|---------|--------------|
|
|||
|
|
| `github.com/a-h/templ` | existing | HTML components | Entire view layer uses templ |
|
|||
|
|
| Sortable.js | CDN (existing) | Drag-and-drop | Already wired in router_test; Phase 4 integration |
|
|||
|
|
| HTMX | CDN v4.0.0-beta2 (existing) | Partial page swaps | Entire app uses HTMX for navigation |
|
|||
|
|
| Tailwind CSS | existing build | Utility classes | Used alongside custom CSS classes |
|
|||
|
|
|
|||
|
|
### No new packages needed
|
|||
|
|
|
|||
|
|
The UI-SPEC explicitly confirms: "No external registries. All components are templ + custom CSS." All needed UI primitives (`ui.Button`, `ui.IconButton`, `ui.Badge`, `ui.Table`) exist in `backend/internal/web/ui/`.
|
|||
|
|
|
|||
|
|
**Installation:** none required.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Package Legitimacy Audit
|
|||
|
|
|
|||
|
|
> No new packages to install in this phase. The phase is purely a restyle + new page creation using existing infrastructure.
|
|||
|
|
|
|||
|
|
**Packages removed due to slopcheck:** none
|
|||
|
|
**Packages flagged as suspicious:** none
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Architecture Patterns
|
|||
|
|
|
|||
|
|
### System Architecture Diagram
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Browser
|
|||
|
|
│ GET /tablos/{tabloID}
|
|||
|
|
▼
|
|||
|
|
Go handler: GetTabloDetailPage
|
|||
|
|
│ repo.ListTablos (find single tablo by ID + owner check)
|
|||
|
|
│ repo.ListTasksByTablo (fetch tasks for this tablo)
|
|||
|
|
│ compute progress: doneTasks / totalTasks * 100
|
|||
|
|
▼
|
|||
|
|
TabloDetailPageViewModel {tablo, tasks by status column, progress}
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
templ: TabloDetailPage
|
|||
|
|
├── TabloDetailHeader (name h1, status badge, progress bar, owner row)
|
|||
|
|
├── TabloTabBar (Overview | Tasks | Files | Discussion | Events)
|
|||
|
|
└── tab: Tasks → KanbanBoardSection
|
|||
|
|
├── KanbanColumn × 4 (Todo / In Progress / In Review / Done)
|
|||
|
|
│ ├── ColumnHeader (title, count badge, "+ Add" link)
|
|||
|
|
│ └── TaskCardList (sortable-column)
|
|||
|
|
│ └── TaskCard × N (drag-handle, title, delete icon)
|
|||
|
|
└── TaskCreateInlineForm (htmx fragment, within column)
|
|||
|
|
|
|||
|
|
Browser
|
|||
|
|
Sortable.js init (DOMContentLoaded + htmx:afterSettle)
|
|||
|
|
handles: .sortable-column containers
|
|||
|
|
POST /tablos/{tabloID}/tasks/reorder (existing reorder pattern)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Recommended Project Structure
|
|||
|
|
|
|||
|
|
New files to create:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
go-backend/internal/web/
|
|||
|
|
├── views/
|
|||
|
|
│ ├── tablo_detail.templ # TabloDetailPage + all sub-components
|
|||
|
|
│ └── tablo_detail_view.go # TabloDetailViewModel + helpers
|
|||
|
|
├── handlers/
|
|||
|
|
│ └── tablo_detail.go # GetTabloDetailPage handler
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Modified files:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
go-backend/
|
|||
|
|
├── router.go # Add GET /tablos/{tabloID}
|
|||
|
|
├── internal/web/ui/app.css # All CSS changes from UI-SPEC delta table
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Pattern 1: Tablo Detail Handler (new page + HTMX swap)
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// Source: existing handler pattern from handlers/auth.go + handlers/tablos.go
|
|||
|
|
func (h *AuthHandler) GetTabloDetailPage() http.HandlerFunc {
|
|||
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|||
|
|
user, ok := h.authenticatedUser(r.Context(), r)
|
|||
|
|
if !ok {
|
|||
|
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
tabloID, err := uuid.Parse(r.PathValue("tabloID"))
|
|||
|
|
if err != nil {
|
|||
|
|
http.Error(w, "invalid tablo id", http.StatusBadRequest)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Reuse ListTablos to find the owned tablo (no GetTabloByID sqlc query exists)
|
|||
|
|
tablos, err := h.repo.ListTablos(r.Context(), ListTablosInput{OwnerID: user.ID})
|
|||
|
|
if err != nil {
|
|||
|
|
http.Error(w, "failed to load tablos", http.StatusInternalServerError)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
tablo, ok := findTabloByID(tablos, tabloID)
|
|||
|
|
if !ok {
|
|||
|
|
http.Error(w, "tablo not found", http.StatusNotFound)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ListTasksByTablo is defined in the repo and InMemory repo
|
|||
|
|
taskRepo := h.repo.(interface {
|
|||
|
|
ListTasksByTablo(context.Context, ListTasksByTabloInput) ([]TaskRecord, error)
|
|||
|
|
})
|
|||
|
|
tasks, err := taskRepo.ListTasksByTablo(r.Context(), ListTasksByTabloInput{
|
|||
|
|
OwnerID: user.ID,
|
|||
|
|
TabloID: tabloID,
|
|||
|
|
})
|
|||
|
|
if err != nil {
|
|||
|
|
http.Error(w, "failed to load tasks", http.StatusInternalServerError)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
vm := views.NewTabloDetailViewModel(tablo, tasks)
|
|||
|
|
content := views.TabloDetailPage(vm)
|
|||
|
|
|
|||
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|||
|
|
var renderErr error
|
|||
|
|
if isHXRequest(r) {
|
|||
|
|
renderErr = views.DashboardContentSwap("/tablos/"+tabloID.String(), tablos, content).Render(r.Context(), w)
|
|||
|
|
} else {
|
|||
|
|
renderErr = views.DashboardPage("/tablos/"+tabloID.String(), tablos, content).Render(r.Context(), w)
|
|||
|
|
}
|
|||
|
|
if renderErr != nil {
|
|||
|
|
http.Error(w, "failed to render tablo detail", http.StatusInternalServerError)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**[ASSUMED]** — GetTabloByID does not exist in sqlc queries; using ListTablos + findTabloByID is the current pattern from GetEditTabloModal. For a large tablo list this is an O(n) scan. At v1 scale this is acceptable.
|
|||
|
|
|
|||
|
|
### Pattern 2: Progress Computation
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// Source: [ASSUMED] — derived from existing tabloStatusPresentation pattern
|
|||
|
|
func computeTabloProgress(tasks []TaskRecord) int {
|
|||
|
|
total := len(tasks)
|
|||
|
|
if total == 0 {
|
|||
|
|
return 0
|
|||
|
|
}
|
|||
|
|
var done int
|
|||
|
|
for _, t := range tasks {
|
|||
|
|
if t.Status == TaskStatusDone {
|
|||
|
|
done++
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return (done * 100) / total
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Pattern 3: Kanban Column templ (new, scoped to tablo detail)
|
|||
|
|
|
|||
|
|
```templ
|
|||
|
|
// Source: UI-SPEC + existing TasksKanbanLayout pattern
|
|||
|
|
templ TabloDetailKanbanColumn(col TabloDetailColumnView) {
|
|||
|
|
<div class="tablo-kanban-column" data-status={ col.Status }>
|
|||
|
|
<div class="tablo-kanban-column-header">
|
|||
|
|
<span class="tablo-kanban-column-title">{ col.Label }</span>
|
|||
|
|
<span class="tablo-kanban-task-count">{ len(col.Tasks) }</span>
|
|||
|
|
<a href="#" class="tablo-kanban-add-link" hx-get={ col.CreateHref }
|
|||
|
|
hx-target={ "#create-zone-" + col.Status } hx-swap="innerHTML">
|
|||
|
|
+ Ajouter
|
|||
|
|
</a>
|
|||
|
|
</div>
|
|||
|
|
<div id={ "task-list-" + col.Status } class="task-list sortable-column"
|
|||
|
|
data-status={ col.Status }>
|
|||
|
|
for _, task := range col.Tasks {
|
|||
|
|
@TabloDetailTaskCard(task)
|
|||
|
|
}
|
|||
|
|
</div>
|
|||
|
|
<div id={ "create-zone-" + col.Status }></div>
|
|||
|
|
</div>
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Pattern 4: Drag Handle (opacity-based hover)
|
|||
|
|
|
|||
|
|
```css
|
|||
|
|
/* Source: UI-SPEC Component Delta section */
|
|||
|
|
.task-drag-handle {
|
|||
|
|
color: var(--color-text-faint);
|
|||
|
|
cursor: grab;
|
|||
|
|
font-size: 1rem;
|
|||
|
|
opacity: 0;
|
|||
|
|
transition: opacity 0.12s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.task-card:hover .task-drag-handle {
|
|||
|
|
opacity: 1;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Pattern 5: Sortable.js init (preserved from Phase 4)
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// Source: [ASSUMED] — Sortable.js init pattern from STATE.md Phase 4 decisions
|
|||
|
|
// Must fire on DOMContentLoaded AND htmx:afterSettle
|
|||
|
|
function initSortable() {
|
|||
|
|
document.querySelectorAll('.sortable-column').forEach(function(el) {
|
|||
|
|
if (el._sortable) return; // idempotent
|
|||
|
|
el._sortable = Sortable.create(el, {
|
|||
|
|
group: 'tablo-tasks',
|
|||
|
|
animation: 150,
|
|||
|
|
handle: '.task-drag-handle',
|
|||
|
|
draggable: '.task-card',
|
|||
|
|
onEnd: function(evt) {
|
|||
|
|
// Update hidden reorder form + submit
|
|||
|
|
document.querySelector('#reorder-form').requestSubmit();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
document.addEventListener('DOMContentLoaded', initSortable);
|
|||
|
|
document.addEventListener('htmx:afterSettle', initSortable);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The UI-SPEC states: "Sortable.js initialization on `DOMContentLoaded` and `htmx:afterSettle` is PRESERVED verbatim."
|
|||
|
|
|
|||
|
|
### Anti-Patterns to Avoid
|
|||
|
|
|
|||
|
|
- **Using `display: none` for drag handle / delete icon:** UI-SPEC requires `opacity: 0 → 1` (not display-toggling) to preserve layout during hover.
|
|||
|
|
- **Mixing global tasks kanban with tablo-detail kanban:** The `TasksKanbanLayout` in `tasks.templ` is for the global `/tasks` page and must remain unchanged. The tablo detail gets a new separate `TabloDetailKanbanLayout`.
|
|||
|
|
- **Adding a task-view switcher (Board/List/Gantt):** UI-SPEC explicitly defers the inner task-view switcher to Phase 21. Do not add it.
|
|||
|
|
- **Using inline Tailwind for the card restyle:** The UI-SPEC defines `.task-card` as a named CSS class in `app.css`. Inline Tailwind classes on these components would conflict with the CSS cascade.
|
|||
|
|
- **Routing task create/delete to global `/tasks` endpoints:** Task mutations within the detail page must include `tabloID` context so the response re-renders the detail page, not the global tasks page.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Don't Hand-Roll
|
|||
|
|
|
|||
|
|
| Problem | Don't Build | Use Instead | Why |
|
|||
|
|
|---------|-------------|-------------|-----|
|
|||
|
|
| Drag-and-drop column reorder | Custom JS drag | Sortable.js (already included) | Phase 4 already wired; re-implementing is regression risk |
|
|||
|
|
| Status badge pill | Custom badge HTML | `ui.Badge` with `badgeVariantForTone` | Already has all required tone variants in `badge.css` |
|
|||
|
|
| Modal dialog | Raw HTML `<dialog>` | `ui.Modal` | Already exists in `modal.templ` + `modal.css` |
|
|||
|
|
| Progress bar math | Separate API endpoint | Compute inline in handler from task list | Task list already fetched; no extra query needed |
|
|||
|
|
| Table wrapper | Custom `<table>` | `ui.Table` + wrapper div | `ui.Table` handles shell; Phase 20 adds `border/radius` wrapper div around it |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Common Pitfalls
|
|||
|
|
|
|||
|
|
### Pitfall 1: No `GetTabloByID` sqlc query
|
|||
|
|
**What goes wrong:** Code tries to call `h.repo.GetTabloByID(...)` which doesn't exist in the `AuthRepository` interface or sqlc.
|
|||
|
|
**Why it happens:** The repository only exposes `ListTablos` (filtered list). No single-record lookup was added in earlier phases.
|
|||
|
|
**How to avoid:** Use `findTabloByID(tablos, tabloID)` after `ListTablos`. This is the same pattern used in `GetEditTabloModal`.
|
|||
|
|
**Warning signs:** Compiler error: `AuthRepository has no method GetTabloByID`.
|
|||
|
|
|
|||
|
|
### Pitfall 2: `ListTasksByTablo` not in `AuthRepository` interface
|
|||
|
|
**What goes wrong:** `h.repo` is typed as `AuthRepository` which does not include `ListTasksByTablo`. The method is only on `taskPageRepository` interface (defined in `handlers/tasks.go`).
|
|||
|
|
**Why it happens:** `AuthRepository` in `handlers/auth.go` was not extended with task methods.
|
|||
|
|
**How to avoid:** Either (a) add a `TabloDetailRepository` interface combining the needed methods, or (b) type-assert `h.repo.(taskDetailRepository)` the same way `renderTasksPage` does. Pattern (b) requires the InMemoryAuthRepository to also implement `ListTasksByTablo` — it already does (`in_memory_auth_repository.go:168`).
|
|||
|
|
**Warning signs:** Compiler error about interface satisfaction.
|
|||
|
|
|
|||
|
|
### Pitfall 3: HTMX active-path mismatch in sidebar
|
|||
|
|
**What goes wrong:** The sidebar nav item for the current tablo is not highlighted when viewing `/tablos/{tabloID}`.
|
|||
|
|
**Why it happens:** `sidebarNavItemID` uses the href to generate DOM IDs; tablo-detail is a sub-path that doesn't match any primary nav item's `Href`.
|
|||
|
|
**How to avoid:** Pass `activePath` as `/tablos` (the parent) for all tablo detail pages so the sidebar "Tablos" nav item remains active.
|
|||
|
|
|
|||
|
|
### Pitfall 4: Sortable.js double-init after HTMX swap
|
|||
|
|
**What goes wrong:** After an HTMX swap refreshes the kanban (e.g., after task create), Sortable.js is initialized again, creating a second instance on already-initialized containers, causing duplicate handlers.
|
|||
|
|
**Why it happens:** `htmx:afterSettle` fires every time a swap completes; naive re-init doesn't check for existing instances.
|
|||
|
|
**How to avoid:** Guard init with `if (el._sortable) return;` — set `el._sortable` on first init.
|
|||
|
|
|
|||
|
|
### Pitfall 5: `progressInlineStyle` uses `var(--project-color)`
|
|||
|
|
**What goes wrong:** The progress bar fill color on the tablo detail page matches the project color instead of `var(--color-brand-primary)` as specified.
|
|||
|
|
**Why it happens:** The existing `.project-progress-bar` rule uses `background: var(--project-color, var(--color-project-fallback))` — inherited from Phase 19 tablo cards.
|
|||
|
|
**How to avoid:** UI-SPEC says "Change fill color from `var(--project-color)` to `var(--color-brand-primary)`". Apply a scoped override for the detail page header progress bar: `.tablo-detail-header .project-progress-bar { background: var(--color-brand-primary); }` — or use a separate CSS class `tablo-progress-bar` on the detail page's progress element.
|
|||
|
|
|
|||
|
|
### Pitfall 6: Tablo `Status` vs task `Status` confusion
|
|||
|
|
**What goes wrong:** The tablo status field (`todo/in_progress/done`) is used to drive the progress bar value (0/50/100) on tablo cards — but this is NOT the task completion progress. The tablo detail header's progress must be computed from actual task statuses (done tasks / total tasks), not the tablo's own status field.
|
|||
|
|
**Why it happens:** `buildTabloCardViews` uses `tabloStatusPresentation(tablo.Status)` to set `Progress: 50` for in-progress tablos — that's a placeholder.
|
|||
|
|
**How to avoid:** In the detail handler, compute `progress = computeTabloProgress(tasks)` from real task records.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Code Examples
|
|||
|
|
|
|||
|
|
### CSS: `.task-card` new block (from UI-SPEC)
|
|||
|
|
|
|||
|
|
```css
|
|||
|
|
/* Source: 20-UI-SPEC.md Surfaces: Kanban Board → Task cards */
|
|||
|
|
.task-card {
|
|||
|
|
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-inline: 12px;
|
|||
|
|
padding-block: 8px;
|
|||
|
|
transition: box-shadow 0.12s ease, border-color 0.12s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.task-card:hover {
|
|||
|
|
border-color: var(--color-border-strong);
|
|||
|
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### CSS: Kanban column border-radius change
|
|||
|
|
|
|||
|
|
```css
|
|||
|
|
/* Source: 20-UI-SPEC.md Component Delta table — change 1rem → 0.75rem */
|
|||
|
|
.tablo-kanban-column {
|
|||
|
|
background: var(--color-surface-default);
|
|||
|
|
border: 1px solid var(--color-border-default);
|
|||
|
|
border-radius: 0.75rem; /* was 1rem in tasks-section */
|
|||
|
|
overflow: hidden;
|
|||
|
|
width: 18rem;
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### CSS: Task list gap
|
|||
|
|
|
|||
|
|
```css
|
|||
|
|
/* Source: 20-UI-SPEC.md Component Delta — add gap: 8px; padding: 8px */
|
|||
|
|
.task-list {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 8px;
|
|||
|
|
padding: 8px;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### CSS: Kanban board container
|
|||
|
|
|
|||
|
|
```css
|
|||
|
|
/* Source: 20-UI-SPEC.md Surfaces: Kanban Board → Board container */
|
|||
|
|
.tablo-kanban-board {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 16px;
|
|||
|
|
overflow-x: auto;
|
|||
|
|
padding-bottom: 16px;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### CSS: Empty column state
|
|||
|
|
|
|||
|
|
```css
|
|||
|
|
/* Source: 20-UI-SPEC.md Surfaces: Kanban Board → Empty column state */
|
|||
|
|
.tablo-kanban-empty {
|
|||
|
|
color: var(--color-text-faint);
|
|||
|
|
font-size: 0.875rem;
|
|||
|
|
padding: 24px 16px;
|
|||
|
|
text-align: center;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### CSS: Files table wrapper restyle
|
|||
|
|
|
|||
|
|
```css
|
|||
|
|
/* Source: 20-UI-SPEC.md Surfaces: Files Table */
|
|||
|
|
.tablo-files-table-wrapper {
|
|||
|
|
border: 1px solid var(--color-border-default);
|
|||
|
|
border-radius: 12px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tablo-files-table-wrapper thead tr {
|
|||
|
|
background: var(--color-surface-muted);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tablo-files-table-wrapper tbody tr {
|
|||
|
|
border-bottom: 1px solid var(--color-border-default);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tablo-files-table-wrapper tbody tr:hover {
|
|||
|
|
background: var(--color-surface-subtle);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### templ: Tablo detail header layout
|
|||
|
|
|
|||
|
|
```templ
|
|||
|
|
// Source: [ASSUMED] — pattern derived from UI-SPEC header spec
|
|||
|
|
templ TabloDetailHeader(vm TabloDetailHeaderView) {
|
|||
|
|
<header class="tablo-detail-header">
|
|||
|
|
<div class="tablo-detail-title-row">
|
|||
|
|
<div class="tablo-detail-avatar" style={ projectColorVariableStyle(vm.Color) }>
|
|||
|
|
{ vm.Initial }
|
|||
|
|
</div>
|
|||
|
|
<h1 class="tablo-detail-title">{ vm.Name }</h1>
|
|||
|
|
</div>
|
|||
|
|
<div class="tablo-metadata-row">
|
|||
|
|
<span class="tablo-meta-segment">
|
|||
|
|
<!-- owner avatar 24×24 + name -->
|
|||
|
|
</span>
|
|||
|
|
<span class="tablo-meta-segment">
|
|||
|
|
@ActionIcon("calendar")
|
|||
|
|
if vm.DueDate != "" { vm.DueDate } else { "—" }
|
|||
|
|
</span>
|
|||
|
|
<span class="tablo-meta-segment">
|
|||
|
|
@ui.Badge(ui.BadgeProps{Label: vm.StatusLabel, Variant: badgeVariantForTone(vm.StatusTone)})
|
|||
|
|
</span>
|
|||
|
|
<span class="tablo-meta-segment tablo-meta-progress">
|
|||
|
|
<div class="project-progress-track" style="min-width:120px">
|
|||
|
|
<div class="tablo-detail-progress-bar" style={ progressInlineStyle(vm.Progress) }></div>
|
|||
|
|
</div>
|
|||
|
|
<strong>{ vm.ProgressLabel }</strong>
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</header>
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## State of the Art
|
|||
|
|
|
|||
|
|
| Old Approach | Current Approach | When Changed | Impact |
|
|||
|
|
|--------------|------------------|--------------|--------|
|
|||
|
|
| Tablo progress = tablo.Status mapped to 0/50/100 | Compute from actual task done/total ratio | Phase 20 | Detail page shows real progress |
|
|||
|
|
| No tablo detail page | `GET /tablos/{tabloID}` with tab bar | Phase 20 | First time users can navigate into a tablo |
|
|||
|
|
| Global kanban (flat grid, `grid-cols-4`) | Tablo-scoped kanban (flex row, `overflow-x: auto`) | Phase 20 | Detail page kanban vs global tasks page kanban |
|
|||
|
|
|
|||
|
|
**Deprecated/outdated:**
|
|||
|
|
- `.project-progress-bar { background: var(--project-color) }` for detail page use: replaced by `var(--color-brand-primary)` per UI-SPEC.
|
|||
|
|
- `.tasks-section { border-radius: 1rem }` in cards context: changed to `0.75rem` for kanban columns.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Assumptions Log
|
|||
|
|
|
|||
|
|
| # | Claim | Section | Risk if Wrong |
|
|||
|
|
|---|-------|---------|---------------|
|
|||
|
|
| A1 | `GetTabloByID` sqlc query does not exist; must use `ListTablos + findTabloByID` | Pitfall 1 | If a GetTabloByID query was added via `just generate` after last codebase read, handler can be simplified — low risk either way |
|
|||
|
|
| A2 | `ListTasksByTablo` is not in `AuthRepository` interface; needs type assertion or new interface | Pitfall 2 | If someone added it to the interface, no type-assert needed |
|
|||
|
|
| A3 | Sortable.js CDN is already loaded on the tablo detail page | Pattern 5 | If Sortable.js is not in `DashboardPage` head, it must be added |
|
|||
|
|
| A4 | The tablo detail page has no existing route and no existing templ component | Standard Stack | If a partial implementation exists in a branch, avoid duplication |
|
|||
|
|
| A5 | Files tab shows files from the tablo's R2 bucket; no file listing query currently exists for tablo-scoped files | Files Table section | Phase 20 UI-SPEC says "restyle only" — implies files list already renders. If no file data available, the files tab renders with empty state only |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Open Questions
|
|||
|
|
|
|||
|
|
1. **Tablo detail route navigation from tablo cards**
|
|||
|
|
- What we know: `TabloGridCard` and `TabloListRow` in `tablos.templ` have no `hx-get` to the detail page — they have edit/delete buttons only.
|
|||
|
|
- What's unclear: Should clicking the card title navigate to detail? Or is there a separate "View" button?
|
|||
|
|
- Recommendation: Make the card title/avatar an `hx-get="/tablos/{id}"` link, consistent with the linear-style navigation pattern.
|
|||
|
|
|
|||
|
|
2. **Files tab data for tablo detail**
|
|||
|
|
- What we know: The UI-SPEC says "restyle only" for the files table. No file listing sqlc query for tablo-scoped files was found.
|
|||
|
|
- What's unclear: Are files accessible? Is there a `ListFilesByTablo` repo method?
|
|||
|
|
- Recommendation: If no files query exists, render files tab with empty state (`ui.EmptyState`) only — no blocking dependency for TASK-01 or DETAIL-01.
|
|||
|
|
|
|||
|
|
3. **Owner display for tablo detail header**
|
|||
|
|
- What we know: `tablo.OwnerID` exists; `GetPublicUserByID` exists in `AuthRepository`.
|
|||
|
|
- What's unclear: Whether the handler should fetch the owner record for display.
|
|||
|
|
- Recommendation: Fetch owner display name in handler via `GetPublicUserByID` for the metadata row. Falls back to "Propriétaire" if fetch fails.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Environment Availability
|
|||
|
|
|
|||
|
|
> Phase is code/CSS only. No new external tools or services required.
|
|||
|
|
|
|||
|
|
**Step 2.6: SKIPPED** — No external dependencies beyond existing Go toolchain, templ, and Tailwind build already in place.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Validation Architecture
|
|||
|
|
|
|||
|
|
### Test Framework
|
|||
|
|
|
|||
|
|
| Property | Value |
|
|||
|
|
|----------|-------|
|
|||
|
|
| Framework | Vitest (frontend not applicable) / Go stdlib `testing` + `net/http/httptest` |
|
|||
|
|
| Config file | none (Go stdlib test runner) |
|
|||
|
|
| Quick run command | `go test ./... -run TestTabloDetail -count=1` |
|
|||
|
|
| Full suite command | `go test ./... -count=1` |
|
|||
|
|
|
|||
|
|
### Phase Requirements → Test Map
|
|||
|
|
|
|||
|
|
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|
|||
|
|
|--------|----------|-----------|-------------------|-------------|
|
|||
|
|
| DETAIL-01 | `GET /tablos/{tabloID}` returns 200 with tablo name in response | unit/integration | `go test ./... -run TestGetTabloDetailPage -count=1` | ❌ Wave 0 |
|
|||
|
|
| DETAIL-01 | Progress bar percentage rendered as `{N}%` in response | unit | `go test ./... -run TestTabloDetailProgress -count=1` | ❌ Wave 0 |
|
|||
|
|
| DETAIL-01 | Status badge label rendered in metadata row | unit | `go test ./... -run TestTabloDetailStatusBadge -count=1` | ❌ Wave 0 |
|
|||
|
|
| TASK-01 | Kanban columns render 4 status columns | unit | `go test ./... -run TestTabloDetailKanbanColumns -count=1` | ❌ Wave 0 |
|
|||
|
|
| TASK-01 | Task cards contain `.task-card` class | unit | `go test ./... -run TestTabloDetailTaskCardMarkup -count=1` | ❌ Wave 0 |
|
|||
|
|
| TASK-01 | Drag handle has `opacity:0` at rest (CSS class present) | unit | `go test ./... -run TestTabloDetailDragHandle -count=1` | ❌ Wave 0 |
|
|||
|
|
|
|||
|
|
### Sampling Rate
|
|||
|
|
- **Per task commit:** `go test ./... -run TestTabloDetail -count=1`
|
|||
|
|
- **Per wave merge:** `go test ./... -count=1`
|
|||
|
|
- **Phase gate:** Full suite green before `/gsd:verify-work`
|
|||
|
|
|
|||
|
|
### Wave 0 Gaps
|
|||
|
|
- [ ] `go-backend/internal/web/handlers/tablo_detail_test.go` — covers all DETAIL-01 + TASK-01 handler tests
|
|||
|
|
- [ ] `go-backend/router_test.go` additions — integration-level smoke test for `GET /tablos/{tabloID}`
|
|||
|
|
|
|||
|
|
*(Existing test infrastructure in `handlers/tablos_test.go` provides the scaffold pattern to follow.)*
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Security Domain
|
|||
|
|
|
|||
|
|
### Applicable ASVS Categories
|
|||
|
|
|
|||
|
|
| ASVS Category | Applies | Standard Control |
|
|||
|
|
|---------------|---------|-----------------|
|
|||
|
|
| V2 Authentication | yes | `authenticatedUser(r.Context(), r)` — existing session check, same as all other handlers |
|
|||
|
|
| V3 Session Management | yes | existing `xtablo_session` cookie pattern |
|
|||
|
|
| V4 Access Control | yes | `findTabloByID` filters by `OwnerID` — prevents accessing other users' tablos |
|
|||
|
|
| V5 Input Validation | yes | `uuid.Parse(r.PathValue("tabloID"))` guards invalid IDs |
|
|||
|
|
| V6 Cryptography | no | No crypto in this phase |
|
|||
|
|
|
|||
|
|
### Known Threat Patterns for this stack
|
|||
|
|
|
|||
|
|
| Pattern | STRIDE | Standard Mitigation |
|
|||
|
|
|---------|--------|---------------------|
|
|||
|
|
| IDOR — access another user's tablo by guessing UUID | Elevation of Privilege | `findTabloByID` only returns tablos owned by authenticated user's ID |
|
|||
|
|
| Task mutation across tablo boundary | Tampering | `ListTasksByTablo` includes `OwnerID` in query; UpdateTask/DeleteTask handlers enforce ownership |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Sources
|
|||
|
|
|
|||
|
|
### Primary (HIGH confidence)
|
|||
|
|
- `/go-backend/internal/web/ui/base.css` — all CSS custom property tokens confirmed
|
|||
|
|
- `/go-backend/internal/web/ui/app.css` — existing `.task-row`, `.project-progress-bar`, `.tasks-section` CSS rules
|
|||
|
|
- `/go-backend/internal/web/views/tasks.templ` — existing `TasksKanbanLayout`, `TaskCard`, `TasksKanbanColumnView` patterns
|
|||
|
|
- `/go-backend/internal/web/views/tablos.templ` — existing `TabloGridCard`, header/progress patterns
|
|||
|
|
- `/go-backend/internal/web/handlers/tablos.go` — `findTabloByID`, `tabloStatusPresentation`, existing handler patterns
|
|||
|
|
- `/go-backend/internal/web/handlers/auth.go` — `AuthRepository` interface definition (no `ListTasksByTablo`)
|
|||
|
|
- `/go-backend/router.go` — confirmed no `GET /tablos/{tabloID}` route exists
|
|||
|
|
- `/go-backend/internal/tablos/model.go` — tablo model fields confirmed
|
|||
|
|
- `/go-backend/internal/tasks/model.go` — task status enums, `ListByTabloInput`
|
|||
|
|
- `.planning/phases/20-tablo-detail-kanban-restyle/20-UI-SPEC.md` — locked visual decisions
|
|||
|
|
|
|||
|
|
### Secondary (MEDIUM confidence)
|
|||
|
|
- `.claude/skills/sketch-findings-xtablo-source/SKILL.md` — design direction and CSS patterns verified against existing `base.css` tokens
|
|||
|
|
|
|||
|
|
### Tertiary (LOW confidence)
|
|||
|
|
- `[ASSUMED]` items in the Assumptions Log above — architectural inferences from existing handler patterns
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Metadata
|
|||
|
|
|
|||
|
|
**Confidence breakdown:**
|
|||
|
|
- Standard stack: HIGH — confirmed from codebase; no new packages
|
|||
|
|
- Architecture: HIGH — detail page gap confirmed, handler patterns confirmed, CSS tokens confirmed
|
|||
|
|
- Pitfalls: HIGH — confirmed from direct code inspection (interface gaps, CSS overrides)
|
|||
|
|
|
|||
|
|
**Research date:** 2026-05-18
|
|||
|
|
**Valid until:** 2026-06-17 (30-day window; stable stack)
|