--- 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 - go-backend/internal/web/handlers/tablo_detail_tab.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" - "Tab links use hx-get and hx-push-url=true targeting #tab-content — not plain href anchors" - "Kanban board renders exactly 4 columns in a .tablo-kanban-board flex container" - "Each column uses class tablo-kanban-column with data-status attribute set to the column ID" - "Each column contains a hidden reorder form with id reorder-form-{status} for Sortable.js onEnd" - "Each task card uses class task-card and carries data-task-id" - "Drag handle element uses class task-drag-handle and is a child of .task-card" - "Empty column renders a .tablo-kanban-empty element with text 'Aucune tâche'" - "Rendered HTML from GET /tablos/{id} contains substring `initTabloDetailSortable`" - "Etapes section renders below the kanban board listing each etape name and its task count" artifacts: - path: "go-backend/internal/web/views/tablo_detail.templ" provides: "TabloDetailPage + all sub-components (header, tab bar, kanban board, task card, etapes section)" exports: ["TabloDetailPage"] - path: "go-backend/internal/web/views/tablo_detail_view.go" provides: "TabloDetailPage real templ component replaces stub from Plan 01" contains: "func TabloDetailPage" - path: "go-backend/internal/web/handlers/tablo_detail_tab.go" provides: "GET /tablos/{tabloID}/{tab} handler for tab content swaps" exports: ["GetTabloDetailTab"] key_links: - from: "go-backend/internal/web/views/tablo_detail.templ" to: "go-backend/internal/web/views/tablo_detail_view.go" via: "TabloDetailViewModel struct fields" pattern: "TabloDetailViewModel" - from: ".tablo-kanban-board .sortable-column" to: "POST /tablos/{id}/tasks/reorder" via: "Sortable.js onEnd -> #reorder-form-{status} submit" pattern: "reorder-form-" - from: ".tablo-tab-bar a[hx-get]" to: "GET /tablos/{tabloID}/{tab}" via: "HTMX hx-get + hx-push-url=true" pattern: "hx-get" --- Build the tablo_detail.templ components that render the tablo detail page: header section, HTMX tab bar, kanban board with 4 columns (each with a hidden reorder form), task cards, empty state, etapes section, and Sortable.js initialization script. Replace the stub TabloDetailPage function in tablo_detail_view.go with the real templ component. Add a minimal tab handler for the tasks tab. Purpose: This plan produces the full HTML surface for DETAIL-01 and TASK-01. All CSS class names used here are defined in Plan 03's CSS work, so the visual output is unstyled at first but structurally correct. Output: tablo_detail.templ with all sub-components; tablo_detail_view.go stub removed and replaced by templ-generated component; tablo_detail_tab.go with GetTabloDetailTab handler. @/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/workflows/execute-plan.md @/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/templates/summary.md @/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/ROADMAP.md @/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/20-tablo-detail-kanban-restyle/20-UI-SPEC.md @/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/20-tablo-detail-kanban-restyle/20-RESEARCH.md From go-backend/internal/web/views/tablo_detail_view.go (created in Plan 01): ```go type TabloDetailTaskView struct { ID string Title string DeleteHref string EditHref string } type TabloDetailColumnView struct { ID string // "todo" | "in_progress" | "in_review" | "done" Label string // "À faire" | "En cours" | "En révision" | "Terminé" Tasks []TabloDetailTaskView CreateHref string // "/tablos/{id}/tasks/create?status={colID}" } type TabloDetailEtapeView struct { ID string Name string TaskCount int } 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 Etapes []TabloDetailEtapeView // may be empty } ``` 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: hidden form that submits task order to POST /tablos/{id}/tasks/reorder // IMPORTANT: form id must be "reorder-form-{status}" — Sortable.js onEnd looks up // getElementById('reorder-form-' + el.dataset.status) // Guard: if (el._sortable) return; prevents double-init after HTMX swap (Pitfall 4) ``` 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 .tablo-etapes-section — etapes section below kanban board .tablo-etape-row — each etape row (name + task count) ``` Task 1: TabloDetailPage templ component — header, HTMX tab bar, kanban board, etapes section go-backend/internal/web/views/tablo_detail.templ, go-backend/internal/web/handlers/tablo_detail_tab.go - go-backend/internal/web/views/tablo_detail_view.go (TabloDetailViewModel fields — just created in Plan 01) - go-backend/internal/web/views/tablos.templ (TabloGridCard pattern, ActionIcon usage, badgeVariantForTone function name) - go-backend/internal/web/views/tasks.templ (TasksKanbanLayout — for contrast; tablo-detail kanban is SEPARATE and must NOT reuse TasksKanbanLayout; also read the Sortable.js init script and reorder form pattern) - go-backend/internal/web/ui/badge.templ (BadgeProps fields) - go-backend/internal/web/ui/icon_button.templ (IconButtonProps fields) - go-backend/internal/web/ui/app.css lines 882-900 (existing .tab-nav, .tab-nav-item classes) - go-backend/internal/web/views/discussion_view.go (for badgeVariantForTone function — confirm it's in views package) - go-backend/internal/web/handlers/tablos.go (renderTablosResponse pattern — for how DashboardContentSwapWithMainClass is called, to replicate in tab handler) Create go-backend/internal/web/views/tablo_detail.templ in package views. Import "xtablo-backend/internal/web/ui" and "strconv". 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 id="tab-content": * @TabloDetailKanbanBoard(vm.Columns, vm.TabloID) * if len(vm.Etapes) > 0: @TabloDetailEtapesSection(vm.Etapes) - @TabloDetailSortableScript(vm.TabloID) 2. TabloDetailHeader(vm TabloDetailViewModel) — header section: - 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) — HTMX tab navigation: - nav element class="tablo-tab-bar" - 5 anchor elements for: "Vue d'ensemble", "Tâches", "Fichiers", "Discussion", "Événements" - Tab slugs: "overview", "tasks", "files", "discussion", "events" - Each anchor MUST use HTMX attributes (per UI-SPEC locked interaction contract): hx-get="/tablos/{tabloID}/{slug}" hx-target="#tab-content" hx-push-url="true" class="tab-nav-item" - "Tâches" tab (tasks slug): class="tab-nav-item tab-nav-item--active" (Phase 20 always shows tasks tab active) - Do NOT use plain href="#slug" anchors — the UI-SPEC requires HTMX push-url navigation for all tabs. - Use templ.SafeURL for the hx-get value to satisfy templ's URL safety requirements. 4. TabloDetailKanbanBoard(columns []TabloDetailColumnView, tabloID string) — renders the kanban board: - Outer div class="tablo-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={ templ.SafeURL(col.CreateHref) } containing "+ Ajouter" - div id={ "task-list-" + col.ID } class="task-list sortable-column" data-status={ col.ID }: * if len(col.Tasks) == 0: div class="tablo-kanban-empty" { "Aucune tâche" } * else: for _, task := range col.Tasks { @TabloDetailTaskCard(task, tabloID) } - Hidden reorder form (REQUIRED for Sortable.js onEnd): - div id={ "create-zone-" + col.ID } (empty create zone for HTMX task create swap) 6. TabloDetailTaskCard(task TabloDetailTaskView, tabloID string) — task card: - 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. TabloDetailEtapesSection(etapes []TabloDetailEtapeView) — etapes list below kanban: - section element class="tablo-etapes-section" - h2 or h3 heading: "Étapes" (French) - ul element: for each etape, render a li class="tablo-etape-row" containing: span class="tablo-etape-name" { etape.Name } span class="tablo-etape-count" { strconv.Itoa(etape.TaskCount) + " tâche(s)" } 8. TabloDetailSortableScript(tabloID string) — JavaScript block: Use templ.Raw to emit the Sortable.js init script that: - Defines function initTabloDetailSortable() - Calls document.querySelectorAll('.sortable-column').forEach(function(el) { if (el._sortable) return; el._sortable = Sortable.create(el, { group: 'tablo-tasks', animation: 150, handle: '.task-drag-handle', draggable: '.task-card', onEnd: function(evt) { 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