diff --git a/.planning/STATE.md b/.planning/STATE.md index 1257450..462f4ec 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -3,14 +3,14 @@ gsd_state_version: 1.0 milestone: v3.0 milestone_name: Design System & Visual Polish status: executing -last_updated: "2026-05-16T20:47:02.232Z" -last_activity: 2026-05-16 -- Phase 15 execution started +last_updated: "2026-05-16T21:30:26.245Z" +last_activity: 2026-05-16 -- Phase 16 planning complete progress: total_phases: 5 completed_phases: 3 - total_plans: 10 + total_plans: 14 completed_plans: 10 - percent: 100 + percent: 71 --- # STATE @@ -30,8 +30,8 @@ See: `.planning/PROJECT.md` (updated 2026-05-16) Phase: 15 (dashboard-tablos) — EXECUTING Plan: 1 of 3 -Status: Executing Phase 15 -Last activity: 2026-05-16 -- Phase 15 execution started +Status: Ready to execute +Last activity: 2026-05-16 -- Phase 16 planning complete ## Previous Milestone Status diff --git a/.planning/phases/16-tablo-detail/16-PATTERNS.md b/.planning/phases/16-tablo-detail/16-PATTERNS.md new file mode 100644 index 0000000..89f45dd --- /dev/null +++ b/.planning/phases/16-tablo-detail/16-PATTERNS.md @@ -0,0 +1,915 @@ +# Phase 16: Tablo Detail - Pattern Map + +**Mapped:** 2026-05-16 +**Files analyzed:** 5 (4 templ files + 1 CSS file) +**Analogs found:** 5 / 5 + +--- + +## File Classification + +| New/Modified File | Role | Data Flow | Closest Analog | Match Quality | +|-------------------|------|-----------|----------------|---------------| +| `backend/internal/web/ui/app.css` | config/styles | transform | `go-backend/internal/web/ui/app.css` | exact (source of CSS blocks to port) | +| `backend/internal/web/ui/icon_button.templ` | component | request-response | existing icon cases in same file | exact (additive switch cases) | +| `backend/templates/tablos.templ` | component | request-response | same file — `TabloProjectCard`, `TablosDashboard` | exact (existing patterns within same file) | +| `backend/templates/tasks.templ` | component | request-response | `go-backend/internal/web/views/tasks.templ`, same file | role-match | +| `backend/templates/etapes.templ` | component | request-response | same file (EtapeStrip removal) | exact (deletion, no replacement pattern needed) | +| `backend/templates/files.templ` | component | request-response | same file — `TablosDashboard` (EmptyState), `FilesTabFragment` | role-match | + +--- + +## Pattern Assignments + +### `backend/internal/web/ui/app.css` — CSS additions + +**Analog:** `go-backend/internal/web/ui/app.css` (verbatim source for all new CSS blocks) + +**Existing CSS already in `backend/internal/web/ui/app.css` (do NOT re-add):** + +Lines 344–361: `.overview-section`, `.overview-section-heading`, shared `h3` font-size rule +Lines 386–407: `.project-card-top`, icon button overrides inside `.project-card-top` +Lines 420–437: `.project-avatar` +Lines 134–152: `.sidebar-nav-item`, `.sidebar-nav-item:hover`, `.sidebar-nav-item.is-active` + +**New CSS blocks to append** — verbatim from `go-backend/internal/web/ui/app.css`: + +**Block 1: Tasks section (go-backend lines 1253–1348):** +```css +/* ============================================================ + Section 19 — Tasks section (kanban column container) + ============================================================ */ + +.tasks-section { + background: var(--color-surface-default); + border: 1px solid var(--color-border-subtle); + border-radius: 1rem; + overflow: hidden; +} + +.tasks-section-header { + align-items: center; + border-bottom: 1px solid var(--color-border-muted); + display: flex; + justify-content: space-between; + padding: 1.2rem 1rem; +} + +.tasks-add-button { + align-items: center; + background: var(--color-surface-default); + border: 1px solid var(--color-border-muted); + border-radius: 0.7rem; + color: var(--color-text-secondary); + cursor: pointer; + display: inline-flex; + font-weight: 500; + gap: 0.5rem; + min-height: 2.75rem; + padding: 0.7rem 1rem; +} + +.task-list { + display: flex; + flex-direction: column; +} + +.task-row { + align-items: center; + border-bottom: 1px solid var(--color-border-muted); + cursor: pointer; + display: flex; + gap: 0.75rem; + padding: 0.9rem 1rem; + transition: background-color 0.2s ease; +} + +.task-row:hover { + background: var(--color-surface-neutral-hover); +} + +.task-check { + align-items: center; + background: var(--color-surface-default); + border: 2px solid var(--color-border-strong); + border-radius: 999px; + color: var(--color-text-inverse); + cursor: pointer; + display: inline-flex; + flex-shrink: 0; + height: 2rem; + justify-content: center; + width: 2rem; +} + +.task-check.is-complete { + background: var(--color-text-brand-strong); + border-color: var(--color-text-brand-strong); +} + +.task-body { + flex: 1; + min-width: 0; +} + +.task-body p { + color: var(--color-surface-muted-inverse); + font-size: 0.95rem; + font-weight: 500; + margin: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.task-row.is-complete .task-body p { + color: var(--color-text-faint); + text-decoration: line-through; +} + +.task-meta { + align-items: center; + color: var(--color-text-muted); + display: flex; + flex-wrap: wrap; + font-size: 0.75rem; + gap: 0.45rem; + margin-top: 0.3rem; +} +``` + +**Block 2: Progress bar (go-backend lines 1061–1072):** +```css +/* ============================================================ + Section 20 — Progress track and bar + ============================================================ */ + +.project-progress-track { + background: var(--color-surface-muted); + border-radius: 999px; + height: 0.5rem; + overflow: hidden; +} + +.project-progress-bar { + background: var(--project-color, var(--color-project-fallback)); + border-radius: 999px; + height: 100%; +} +``` + +**Block 3: New classes for Phase 16 (not in go-backend, spec from UI-SPEC.md):** +```css +/* ============================================================ + Section 21 — Tab nav (tablo detail tab bar) + ============================================================ */ + +.tab-nav { + align-items: center; + border-bottom: 1px solid var(--color-border-muted); + display: flex; + gap: 1.5rem; + margin-bottom: 1.5rem; + overflow-x: auto; +} + +.tab-nav-item { + align-items: center; + border-bottom: 2px solid transparent; + color: var(--color-text-muted); + display: flex; + font-size: 0.875rem; + font-weight: 400; + gap: 0.5rem; + min-height: 44px; + padding-bottom: 0.75rem; + padding-inline: 0.25rem; + transition: color 0.15s ease, border-color 0.15s ease; +} + +.tab-nav-item.is-active { + border-bottom-color: var(--color-brand-primary); + color: var(--color-text-brand); + font-weight: 600; +} + +.tab-nav-item:hover:not(.is-active) { + color: var(--color-text-primary); +} + +/* ============================================================ + Section 22 — Tablo detail metadata row + ============================================================ */ + +.tablo-metadata-row { + align-items: center; + border-bottom: 1px solid var(--color-border-muted); + display: flex; + flex-wrap: wrap; + gap: 1rem; + margin-bottom: 1rem; + padding-bottom: 1rem; +} + +.tablo-metadata-date { + align-items: center; + color: var(--color-text-muted); + display: flex; + font-size: 0.875rem; + gap: 0.5rem; +} + +/* ============================================================ + Section 23 — Kanban column wrapper + ============================================================ */ + +.kanban-column { + flex-shrink: 0; + width: 18rem; +} + +/* Scope column h3 to 1rem — base rule is 1.6rem (full-width views). + 18rem columns overflow at 1.6rem. Base rule left unchanged. */ +.kanban-column .tasks-section-header h3 { + font-size: 1rem; +} + +/* ============================================================ + Section 24 — Etape group sub-headings inside kanban columns + ============================================================ */ + +.etape-group-header { + align-items: center; + background: var(--color-surface-muted); + border-bottom: 1px solid var(--color-border-muted); + display: flex; + gap: 0.5rem; + padding: 0.5rem 1rem; +} + +.etape-group-color-dot { + background: var(--project-color, var(--color-project-fallback)); + border-radius: 999px; + flex-shrink: 0; + height: 0.5rem; + width: 0.5rem; +} + +.etape-group-label { + color: var(--color-text-secondary); + font-size: 0.875rem; + font-weight: 600; +} + +.etape-group-label.is-unassigned { + color: var(--color-text-muted); + font-weight: 400; +} + +/* ============================================================ + Section 25 — Empty task list placeholder + ============================================================ */ + +.task-list-empty { + color: var(--color-text-faint); + font-size: 0.875rem; + font-style: italic; + padding: 0.75rem 1rem; +} +``` + +--- + +### `backend/internal/web/ui/icon_button.templ` — add `download` and `chat` icon cases + +**Analog:** Existing switch cases in same file (lines 18–73), specifically the `trash` case (lines 63–70) as the model for SVG structure. + +**Import pattern** (lines 1–1 — `package ui` header only, no imports needed, templ handles it): +```go +package ui +``` + +**Core pattern — add two new cases before `default:`** (insert after line 70, before line 71): +```go + case "download": + + case "chat": + +``` + +**Critical note:** The `default` case (line 71) emits `{kind}` as raw text — any IconButton with an unknown icon renders its icon name as visible text. The `download` and `chat` cases MUST be added before using those icon names in header or file row buttons. + +--- + +### `backend/templates/tablos.templ` — header, tab nav, overview tab + +**Analog within same file:** +- Header layout: `TabloProjectCard` (lines 71–117) — `.project-card-top`, `.project-card-title-row`, `.project-avatar`, `@ui.IconButton` usage +- Overview section pattern: `TablosDashboard` (lines 12–42) — `.overview-section`, `.overview-section-heading` +- EmptyState pattern: `TablosEmptyState` (lines 47–64) — `@ui.EmptyState` call signature +- Delete confirm pattern: `TabloDeleteConfirmFragment` (lines 584–623) — `@ui.Button` + HTMX confirm flow + +**Imports pattern** (lines 1–7 — unchanged): +```go +package templates + +import ( + "backend/internal/auth" + "backend/internal/db/sqlc" + "backend/internal/web/ui" +) +``` + +**Color avatar pattern** (from `TabloProjectCard` lines 103–112 — reuse for detail header): +```go +// Current (renders no character inside avatar — Phase 16 adds first-char text): +if card.Tablo.Color.Valid && card.Tablo.Color.String != "" { + +} else { + +} + +// Phase 16 target (add first character + guard for empty title): +if tablo.Color.Valid && tablo.Color.String != "" { + + if len(tablo.Title) > 0 { + { string([]rune(tablo.Title)[0:1]) } + } + +} else { + + if len(tablo.Title) > 0 { + { string([]rune(tablo.Title)[0:1]) } + } + +} +``` + +**Tab nav active state pattern** (from `TabloDetailPage` lines 306–310 — replace inline Tailwind hex with token classes): +```go +// Current (hardcoded hex — remove this): +if activeTab == "overview" || activeTab == "" { + class="flex items-center gap-2 pb-3 px-1 text-sm font-semibold transition-colors border-b-2 shrink-0 min-h-[44px] text-[#804EEC] border-[#804EEC]" +} else { + class="flex items-center gap-2 pb-3 px-1 text-sm font-semibold transition-colors border-b-2 border-transparent shrink-0 min-h-[44px] text-[#667085] hover:text-gray-900" +} + +// Phase 16 target (design token CSS classes — matches sidebar-nav-item.is-active pattern): +if activeTab == "overview" || activeTab == "" { + class="tab-nav-item is-active" +} else { + class="tab-nav-item" +} +``` +Apply this pattern to all 5 tab `` elements (overview, tasks, files, discussion, events). + +**IconButton for Discussion link** (replaces hardcoded `` button at lines 252–262): +```go +// Phase 16: @ui.IconButton with "chat" icon and an outer for navigation +// Note: Discussion is a navigation link, not a button — wrap differently: +Discussion +``` + +**Button for Invite Member** (replaces inline ` + +// Phase 16 target: + +``` + +**TaskCard restyled** (replaces `.task-card` div at lines 138–165 — use `.task-row` CSS): +```go +// Current (lines 136-167): +
+
+
+
+
+

{ task.Title }

+
+ +
+
+
+ +// Phase 16 target (task-row pattern from go-backend): +
+
+ +
+

{ task.Title }

+
+ @ui.IconButton(ui.IconButtonProps{ + Label: "Delete task: " + task.Title, + Icon: "trash", + Variant: ui.IconButtonVariantDanger, + Tone: ui.IconButtonToneGhost, + Type: "button", + Attrs: templ.Attributes{ + "hx-get": "/tablos/" + tabloID.String() + "/tasks/" + task.ID.String() + "/delete-confirm", + "hx-target": "closest .task-card-zone", + "hx-swap": "outerHTML", + }, + }) +
+
+``` + +**EtapeGroupHeader helper component** (new templ function in `tasks.templ`): +```go +// EtapeGroupHeader renders the sub-heading row for an etape group within a kanban column. +// "No etape" / unassigned group omits the color dot and uses muted label style. +templ EtapeGroupHeader(group EtapeGroup) { +
+ if group.EtapeColor != "" { + + } + if group.EtapeID == "" { + { group.EtapeTitle } + } else { + { group.EtapeTitle } + } +
+} +``` + +**EtapeStrip OOB removal** (from `TaskCardGone` line 386 and `TaskCardOOB` line 398): +```go +// DELETE from TaskCardGone (line 386): +@EtapeStrip(tabloID, etapes, counts, filter, csrfToken, true) + +// DELETE from TaskCardOOB (line 398): +@EtapeStrip(tabloID, etapes, counts, filter, csrfToken, true) + +// Keep etapes/counts parameters on TaskCardGone and TaskCardOOB signatures +// to avoid handler signature changes that would break tests. +// Mark with TODO comment for future cleanup. +``` + +--- + +### `backend/templates/etapes.templ` — EtapeStrip removal + +**No new pattern needed.** The `EtapeStrip` function itself is retained (lines 11–101) but called from nowhere after removal from `tablos.templ`, `TaskCardGone`, and `TaskCardOOB`. `EtapeEditFormFragment`, `EtapeCreateFormFragment`, and `EtapeDeleteConfirmFragment` are preserved unchanged. + +The `@ui.CSRFField` + `@ui.Button` pattern in etape forms (lines 103–195) is already correct — no restyling needed. + +--- + +### `backend/templates/files.templ` — files section restyling + +**Analog within same file:** `FileDeleteConfirmFragment` (lines 104–146) for the confirm HTMX pattern. `TablosDashboard` in `tablos.templ` for `@ui.EmptyState` and `.overview-section-heading`. + +**FilesTabFragment — new structure** (replaces lines 12–27): +```go +// Current: +templ FilesTabFragment(tablo sqlc.Tablo, files []sqlc.TabloFile, csrfToken string) { +
+ @FileUploadForm(tablo.ID, csrfToken, "") +
+ if len(files) == 0 { + @FileListEmpty() + } else { + + } +
+
+} + +// Phase 16 target: +templ FilesTabFragment(tablo sqlc.Tablo, files []sqlc.TabloFile, csrfToken string) { +
+
+

Files

+ @ui.Button(ui.ButtonProps{ + Label: "Upload file", + Variant: ui.ButtonVariantDefault, + Tone: ui.ButtonToneSolid, + Size: ui.SizeMD, + Type: "button", + Attrs: templ.Attributes{ + // hx-get to reveal FileUploadForm inline or toggle a slot + "hx-get": "/tablos/" + tablo.ID.String() + "/files/upload-form", + "hx-target": "#file-upload-slot", + "hx-swap": "innerHTML", + }, + }) +
+
+ if len(files) == 0 { + @ui.EmptyState(ui.EmptyStateProps{ + Title: "No files yet", + Description: "Upload your first file to get started.", + }) + } else { + @ui.Table(ui.TableProps{ + Head: fileTableHead(), + Body: fileTableBody(tablo.ID, files), + }) + } +
+} +``` + +**FileTableHead helper** (new private templ in `files.templ`): +```go +// fileTableHead renders the content for the files table. +// @ui.Table places this inside ; render .... +templ fileTableHead() { + + Filename + Size + Uploaded + Actions + +} +``` + +**FileTableBody helper** (new private templ in `files.templ`): +```go +// fileTableBody renders the content for the files table. +templ fileTableBody(tabloID uuid.UUID, files []sqlc.TabloFile) { + for _, f := range files { + @FileListRow(tabloID, f) + } +} +``` + +**FileListRow — must be `` for @ui.Table** (replaces `
  • ` at lines 72–99): +```go +// Current (uses
  • — incompatible with @ui.Table's ): +templ FileListRow(tabloID uuid.UUID, file sqlc.TabloFile) { +
  • ... + +// Phase 16 target (CRITICAL: outer must be for valid HTML inside ): +templ FileListRow(tabloID uuid.UUID, file sqlc.TabloFile) { + + { file.Filename } + { formatBytes(file.SizeBytes) } + + if file.CreatedAt.Valid { + { file.CreatedAt.Time.Format("2006-01-02") } + } + + + + @ui.IconButton(ui.IconButtonProps{ + Label: "Download file", + Icon: "download", + Variant: ui.IconButtonVariantNeutral, + Tone: ui.IconButtonToneGhost, + Type: "button", + }) + + @ui.IconButton(ui.IconButtonProps{ + Label: "Delete file", + Icon: "trash", + Variant: ui.IconButtonVariantDanger, + Tone: ui.IconButtonToneGhost, + Type: "button", + Attrs: templ.Attributes{ + "hx-get": "/tablos/" + tabloID.String() + "/files/" + file.ID.String() + "/delete-confirm", + "hx-target": "closest .file-row-zone", + "hx-swap": "outerHTML", + }, + }) + + +} +``` + +**FileDeleteConfirmFragment — must also be ``** (replaces `
    ` at lines 104–146 — see Pitfall 2): +```go +// Current outer element (line 105): +
    + +// Phase 16 target — outerHTML swap requires matching element type: + + + // confirm dialog content (buttons unchanged) + + +``` + +**FileListEmpty is replaced by @ui.EmptyState** — the `FileListEmpty()` function (lines 149–151) can be retained for backward compatibility but should not be called from `FilesTabFragment` or `UploadErrorFragment` anymore. + +--- + +## Shared Patterns + +### IconButton + HTMX outerHTML delete flow +**Source:** `backend/templates/files.templ` `FileDeleteConfirmFragment` (lines 104–146), `backend/templates/tasks.templ` `TaskDeleteConfirmFragment` (lines 326–367) +**Apply to:** All delete icon buttons in file rows and task cards. +```go +// Pattern: trigger button on the row → HTMX loads confirm fragment → outerHTML swap +// The trigger button and the confirm fragment MUST share the same zone class and id. +Attrs: templ.Attributes{ + "hx-get": "/tablos/" + tabloID.String() + "/files/" + file.ID.String() + "/delete-confirm", + "hx-target": "closest .file-row-zone", + "hx-swap": "outerHTML", +}, +``` + +### Design token enforcement (no hardcoded hex) +**Source:** `backend/internal/web/ui/base.css` + existing `app.css` sections +**Apply to:** All new CSS rules in `app.css` and all new inline styles in `.templ` files. +```css +/* ALWAYS use tokens: */ +color: var(--color-text-muted); /* NOT #667085 */ +color: var(--color-text-brand); /* NOT #804EEC */ +border-color: var(--color-border-muted); /* NOT #F2F4F7 */ +background: var(--color-surface-muted); /* NOT #F9FAFB */ +``` + +### @ui.Badge usage (task count + status pill) +**Source:** `backend/templates/tasks.templ` lines 110–112 (task count badge in KanbanColumn) +**Apply to:** Column header task count, status pill in metadata row. +```go +// Task count in column header (existing pattern — keep BadgeVariantInfo): +@ui.Badge(ui.BadgeProps{Label: strconv.Itoa(len(tasks)), Variant: ui.BadgeVariantInfo}) + +// Status pill in metadata row (Phase 16 — use BadgeVariantPrimary per UI-SPEC): +@ui.Badge(ui.BadgeProps{Label: "In progress", Variant: ui.BadgeVariantPrimary}) +``` + +### .overview-section-heading header pattern +**Source:** `backend/templates/tablos.templ` `TablosDashboard` (lines 14–29), `backend/internal/web/ui/app.css` lines 348–361 +**Apply to:** Files section header (D-F01). +```go +// Analog from TablosDashboard: +
    +
    +

    Your Tablos

    + @ui.Button(...) +
    + ... +
    +``` + +### @ui.EmptyState usage +**Source:** `backend/templates/tablos.templ` `TablosEmptyState` (lines 47–64) +**Apply to:** `FilesTabFragment` empty state (replaces `FileListEmpty()`). +```go +// Analog (TablosEmptyState): +@ui.EmptyState(ui.EmptyStateProps{ + Title: "No tablos yet", + Description: "Create your first tablo to get started.", + Action: ui.Button(...), +}) + +// Phase 16 usage (no action button in files empty state): +@ui.EmptyState(ui.EmptyStateProps{ + Title: "No files yet", + Description: "Upload your first file to get started.", +}) +``` + +### CSRF token in forms +**Source:** All existing forms use `@ui.CSRFField(csrfToken)` — no exceptions. +**Apply to:** Any new form elements added in Phase 16 (file upload trigger form, etape create/edit forms already have this). + +--- + +## No Analog Found + +None. All files have strong analogs. + +--- + +## Implementation Order (Wave Map) + +| Wave | Files | Gate | +|------|-------|------| +| 0 | `icon_button.templ` (add `download` + `chat` cases) | `templ generate && go build ./...` | +| 1 | `app.css` (append Sections 19–25) | Visual diff in browser | +| 2 | `tablos.templ` (header, tab nav, overview tab, TasksTabFragment EtapeStrip removal) | `go test ./backend/internal/web/... -run TestTablos -count=1` | +| 3 | `tasks.templ` (groupTasksByEtape, KanbanBoard/Column/Card restyling, EtapeStrip OOB removal), `etapes.templ` (no-op, EtapeStrip already unused) | `go test ./backend/internal/web/... -run TestTask -count=1` | +| 4 | `files.templ` (FilesTabFragment, FileListRow as ``, FileDeleteConfirmFragment as ``, EmptyState) | `go test ./backend/internal/web/... -run TestFilesTab -count=1` | + +--- + +## Critical Constraints + +1. **`` requirement:** `FileListRow` and `FileDeleteConfirmFragment` must use `` as their outer element when rendered inside `@ui.Table`. The HTMX `hx-target="closest .file-row-zone"` + `hx-swap="outerHTML"` depends on element identity — both the trigger row and the confirm row must use the same element type (``). + +2. **KanbanBoard call sites:** Three locations must be updated when adding `etapes []sqlc.Etape` parameter — compile will fail if any is missed. Run `templ generate && go build ./...` after changing the signature. + +3. **EtapeStrip OOB removal:** Remove `@EtapeStrip(...)` from `TaskCardGone` (line 386) and `TaskCardOOB` (line 398). Keep the `etapes []sqlc.Etape` and `counts EtapeTaskCounts` parameters on those components to avoid handler changes. + +4. **TabloDeleteButtonFragment unchanged:** The dashboard delete button fragment must not be modified. Phase 16 adds a separate inline `@ui.IconButton` in `TabloDetailPage` header instead. + +5. **No Tailwind utility classes in new CSS:** All new CSS rules in `app.css` use `var(--...)` tokens only. Existing Tailwind utilities in edit forms are left as-is. + +--- + +## Metadata + +**Analog search scope:** `backend/templates/`, `backend/internal/web/ui/`, `go-backend/internal/web/ui/`, `go-backend/internal/web/views/` +**Files scanned:** 11 +**Pattern extraction date:** 2026-05-16