diff --git a/.planning/phases/16-tablo-detail/16-UI-SPEC.md b/.planning/phases/16-tablo-detail/16-UI-SPEC.md new file mode 100644 index 0000000..ef90da1 --- /dev/null +++ b/.planning/phases/16-tablo-detail/16-UI-SPEC.md @@ -0,0 +1,485 @@ +--- +phase: 16 +slug: tablo-detail +status: draft +shadcn_initialized: false +preset: none +created: 2026-05-16 +--- + +# Phase 16 — UI Design Contract: Tablo Detail + +> Visual and interaction contract for the tablo detail restyling. +> Generated by gsd-ui-researcher, verified by gsd-ui-checker. +> +> This is a Go + HTMX project. There is no shadcn, React, or Tailwind utility class authoring. +> All styles are authored as CSS classes in `backend/internal/web/ui/app.css` using +> `var(--...)` design tokens from `backend/internal/web/ui/base.css`. +> UI components are templ functions in `backend/internal/web/ui/`. + +--- + +## Design System + +| Property | Value | +|----------|-------| +| Tool | none (custom CSS + templ component library) | +| Preset | not applicable | +| Component library | Custom: `@ui.Badge`, `@ui.Button`, `@ui.IconButton`, `@ui.Table`, `@ui.EmptyState` (Phase 13) | +| Icon library | Inline SVGs (Heroicons / Lucide-style, 1rem / 1.25rem sizing) | +| Font | `ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif` (declared in `base.css`) | + +Source: CONTEXT.md canonical refs, `base.css` body declaration. + +--- + +## Spacing Scale + +Declared values (multiples of 4 only). Token names map to CSS `rem` values used in `app.css`: + +| Token | Value | Usage | +|-------|-------|-------| +| xs | 4px (0.25rem) | Icon gaps, tight inline spacing | +| sm | 8px (0.5rem) | Compact badges, task-meta gap, column header internal gap | +| md | 16px (1rem) | Default element spacing, card padding, row padding | +| lg | 24px (1.5rem) | Section heading margin-bottom, dashboard-main gap | +| xl | 32px (2rem) | Page-level padding (`dashboard-main`) | +| 2xl | 48px (3rem) | Major section breaks (not used in this phase) | +| 3xl | 64px (4rem) | Not used in this phase | + +Exceptions: +- Touch targets (Discussion link, Invite button, Delete icon, tab nav items): minimum 44px height — matches existing `min-h-[44px]` pattern in tabs and `td.text-right .borderless-icon-button` at `min-height: 44px`. +- Kanban column width: fixed `18rem` (`w-72` equivalent) per existing `KanbanColumn` width. +- Etape group sub-heading: 12px (0.75rem) vertical padding, 16px horizontal — visually subordinate to column header. + +Source: `app.css` measured values, CONTEXT.md D-K02, go-backend reference CSS. + +--- + +## Typography + +| Role | Size | Weight | Line Height | Token / Class | +|------|------|--------|-------------|---------------| +| Body | 15px (0.95rem) | 400 (normal) | 1.5 | task-body p, task-row text | +| Label | 12px (0.75rem) | 400 (normal) | 1.4 | `.task-meta`, `.project-date-row`, column count badge | +| Heading | 16px (1rem) | 600 (semibold) | 1.2 | Column header `h3` (`.tasks-section-header h3` overridden to 1rem for kanban context; see note) | +| Display | 25.6px (1.6rem) | 600 (semibold) | 1.2 | Section headings `.overview-section-heading h3`, `.tasks-section-header h3` base rule | + +Note on column headers: The base `.tasks-section-header h3` rule is 1.6rem (26px) — designed for full-width list views. Inside the 3-column kanban layout the column is 18rem wide, so column header `h3` is scoped with `.kanban-column .tasks-section-header h3 { font-size: 1rem; }` to fit without overflow. The base rule remains unchanged for future list-view use. + +Tablo title (inline-editable `tablo-title-zone`): +- Display size: 24–30px responsive (existing `text-xl md:text-3xl` pattern, replaced with token-based `font-size: 1.75rem`). +- Weight: 700 (bold). +- Color: `var(--color-text-primary)` default; `var(--color-text-brand)` on hover. + +Source: `go-backend/app.css` measured values, existing `tablos.templ` class audit. + +--- + +## Color + +| Role | Token | Usage | +|------|-------|-------| +| Dominant (60%) | `var(--color-surface-default)` = #ffffff | Page background, card surfaces, task row backgrounds | +| Secondary (30%) | `var(--color-surface-muted)` = #f3f4f6 | Metadata row border separator, tab nav inactive area, kanban column background | +| Accent (10%) | `var(--color-brand-primary)` = #804eec | Reserved-for list below | +| Destructive | `var(--color-status-danger-strong)` = #dc2626 | Delete icon button hover, file delete confirm text | + +Accent (`#804eec` / `var(--color-brand-primary)`) reserved for: +- Active tab nav indicator (bottom border + text color on active tab) +- Tablo title hover color +- Task checkbox fill when complete (`var(--color-text-brand-strong)` = #7c3aed — same family) +- Invite button border and text (ghost/outline style) +- Focus ring on interactive elements (`var(--color-focus-ring)`) + +No other elements use the brand purple. Discussion link uses neutral text, not accent. + +Status pill colors (metadata row and kanban column badge): +- "In progress" → `BadgeVariantPrimary` → `ui-badge-primary` (purple soft tone, consistent with brand) +- "Done" → `BadgeVariantSuccess` → `ui-badge-success` (green) +- "Todo" → `BadgeVariantInfo` → `ui-badge-info` (blue) + +Etape group sub-heading dot: uses `tablo.Color` inline style (`background-color: {etapeColor}`) — same pattern as `.project-avatar` / `.sidebar-project-icon`. Falls back to `var(--color-project-fallback)` (#3b82f6) when etape color is empty. + +Source: `base.css` token values, CONTEXT.md D-H02, D-H03, D-K02, variants.go. + +--- + +## Component Inventory + +All components already exist in `backend/internal/web/ui/`. No new components needed. + +| Component | Templ call | Usage in this phase | +|-----------|-----------|---------------------| +| Badge | `@ui.Badge(ui.BadgeProps{Label: ..., Variant: ...})` | Status pill in metadata row; task count in column header | +| Button | `@ui.Button(ui.ButtonProps{Label: "Invite", Variant: ui.ButtonVariantGhost, ...})` | Invite button in header | +| IconButton | `@ui.IconButton(ui.IconButtonProps{...})` | Discussion link, Delete (header), Download + Delete (file rows) | +| Table | `@ui.Table(ui.TableProps{Head: ..., Body: ...})` | Files section list | +| EmptyState | `@ui.EmptyState(ui.EmptyStateProps{Title: ..., Description: ...})` | Files empty state | + +Source: CONTEXT.md code_context section, Phase 13 design system. + +--- + +## Layout Contracts + +### Tablo Detail Header + +Structure: `.project-card-top` wrapping row (already in `app.css`). + +``` +[ .project-card-top ] + Left: [ .project-card-title-row ] + [ .project-avatar (color circle, 3rem × 3rem, border-radius 0.85rem) ] + [ .tablo-title-zone (inline-editable h1, font-size 1.75rem, weight 700) ] + Right: [ action controls row, gap 0.75rem ] + [ Discussion: @ui.IconButton ghost/neutral + label "Discussion" ] + [ Invite: @ui.Button ghost variant, label "Invite" ] + [ Delete: @ui.IconButton ghost/danger, trash icon ] +``` + +Color avatar: +- CSS class `.project-avatar`, `style="background-color: {tablo.Color}"` when `tablo.Color` is non-empty. +- Falls back to `var(--color-project-fallback)` via CSS `var(--project-color, var(--color-project-fallback))` pattern — set `--project-color` inline when color is present. +- First character of tablo title rendered as text inside avatar (uppercase), color `var(--color-text-inverse)`. + +### Metadata Row + +``` +[ .tablo-metadata-row ] — flex row, flex-wrap, gap 1rem, border-bottom 1px var(--color-border-muted), padding-bottom 1rem, margin-bottom 1rem + + [ created date: calendar icon (1rem) + date string, color var(--color-text-muted), font-size 0.875rem ] + [ separator: border-right 1px var(--color-border-muted) — only on md+ breakpoint ] + [ status pill: @ui.Badge(BadgeVariantPrimary, label "In progress") ] + [ progress bar: .project-progress-track / .project-progress-bar pattern from go-backend ] +``` + +Progress bar: +- Track: `.project-progress-track` (background `var(--color-surface-muted)`, height 0.5rem, border-radius 999px). +- Fill: `.project-progress-bar` (background `var(--project-color, var(--color-project-fallback))`, width set as inline style percentage). + +### Tab Nav + +Structure: flex row, gap 1.5rem, border-bottom 1px `var(--color-border-muted)`, margin-bottom 1.5rem. + +Each tab link: +- Inactive: `color: var(--color-text-muted)`, `border-bottom: 2px solid transparent`, font-weight 500, font-size 0.875rem. +- Active: `color: var(--color-text-brand)`, `border-bottom: 2px solid var(--color-brand-primary)`, font-weight 600. +- Hover (inactive): `color: var(--color-text-primary)`. +- All tabs: `min-height: 44px`, `padding-bottom: 0.75rem`, `padding-inline: 0.25rem`. +- Active state toggled via Go template conditional — CSS class `.is-active` applied to the active ``. + +CSS class pattern (match Phase 15 `sidebar-nav-item.is-active`): +```css +.tab-nav-item { color: var(--color-text-muted); border-bottom: 2px solid transparent; ... } +.tab-nav-item.is-active { color: var(--color-text-brand); border-bottom-color: var(--color-brand-primary); font-weight: 600; } +.tab-nav-item:hover:not(.is-active) { color: var(--color-text-primary); } +``` + +### Kanban Board (Tasks Tab) + +``` +[ #kanban-board ] — flex row, gap 1rem, overflow-x auto, padding-bottom 1rem + + [ .kanban-column ] — flex-shrink 0, width 18rem (fixed) + + [ .tasks-section ] — border 1px var(--color-border-subtle), border-radius 1rem, overflow hidden + + [ .tasks-section-header ] — flex row, justify-between, align-center, + border-bottom 1px var(--color-border-muted), padding 1rem + Left: [ h3 "Todo" font-size 1rem, weight 600 ] + [ @ui.Badge count ] + Right: [ .tasks-add-button "Add task" button ] + + [ .task-list ] — flex column + + [ .etape-group ] — per etape in column (server-side grouped) + [ .etape-group-header ] — flex row, align-center, gap 0.5rem, + padding 0.5rem 1rem, + background var(--color-surface-muted), + border-bottom 1px var(--color-border-muted) + [ color dot: 0.5rem circle, background {etapeColor} inline style ] + [ etape name: font-size 0.75rem, weight 600, color var(--color-text-secondary) ] + + [ task rows within group... ] + [ .task-row ] — flex row, align-center, gap 0.75rem, padding 0.9rem 1rem, + border-bottom 1px var(--color-border-muted), + hover: background var(--color-surface-neutral-hover) + [ .task-check ] — 2rem circle checkbox, border 2px var(--color-border-strong) + .is-complete → background + border-color var(--color-text-brand-strong) + [ .task-body ] — flex 1, task title 0.95rem weight 500 + .is-complete → color var(--color-text-faint), text-decoration line-through + [ .task-meta ] — etape label (if applicable), 0.75rem muted + + [ unassigned group (last, if any unassigned tasks exist) ] + [ .etape-group-header with label "No etape", no color dot ] + [ task rows... ] +``` + +Etape group sub-heading for "No etape" / unassigned: same `.etape-group-header` class, omit the color dot element, label text `var(--color-text-muted)` instead of `var(--color-text-secondary)`. + +Empty column state: `

No tasks yet

` — `color: var(--color-text-faint)`, `font-size: 0.875rem`, italic, padding 0.75rem 1rem. + +### Files Section + +``` +[ .overview-section ] + + [ .overview-section-heading ] + Left: [ h3 "Files", font-size 1.6rem, weight 600 ] + Right: [ @ui.Button(label "Upload file", Variant ButtonVariantDefault, Tone ButtonToneSolid) ] + [ — triggers inline FileUploadForm via HTMX swap ] + + [ @ui.Table ] + Head: [ Filename | Size | Uploaded | Actions ] + Body: per file row + [ filename (truncated, max-width 20rem) ] + [ human-readable size ] + [ formatted date ] + [ actions: @ui.IconButton(ghost/neutral, download icon) + @ui.IconButton(ghost/danger, trash icon) ] + + [ @ui.EmptyState (when no files) ] + Title: "No files yet" + Description: "Upload your first file to get started." +``` + +File table column widths: +- Filename: `flex: 1` (takes remaining space). +- Size: fixed `6rem`. +- Uploaded: fixed `9rem`. +- Actions: fixed `6rem`, text-align right. + +Source: CONTEXT.md D-F01, D-F02, D-F03, D-F04. + +--- + +## Copywriting Contract + +| Element | Copy | +|---------|------| +| Primary CTA — header | "Discussion" (link button, navigates to discussion tab) | +| Primary CTA — files | "Upload file" (button label, triggers upload form) | +| Add task button | "Add task" (inside each kanban column header) | +| Empty state — files heading | "No files yet" | +| Empty state — files body | "Upload your first file to get started." | +| Empty state — kanban column | "No tasks yet" (italic, faint) | +| Error — file upload | "Upload failed. Check the file size and try again." (inline, inside FileUploadForm) | +| Destructive — delete tablo | "Delete tablo?" / "This cannot be undone." + "Delete" confirm button + "Cancel" | +| Destructive — delete file | "Delete file?" / filename (truncated) / "This cannot be undone." + "Delete" + "Cancel" | +| Status pill — In progress | "In progress" | +| Status pill — Done | "Done" | +| Status pill — Todo | "Todo" | +| Invite button | "Invite" | +| Column headers | "Todo" / "In Progress" / "Done" (existing `TaskColumnLabels` map — no change) | +| Etape group — unassigned | "No etape" | +| Tab nav labels | "Overview" / "Tasks" / "Files" / "Discussion" / "Events" (unchanged) | +| Description placeholder | "Add a description" (existing `TabloDescDisplay` — unchanged) | + +Source: CONTEXT.md specifics, existing template audit, D-F04, D-E02. + +--- + +## Interaction Contracts + +### Inline Edit (tablo title) +- Click `.tablo-title-zone` → HTMX swap to `TabloTitleEditFragment` (outerHTML). +- Save → swap back to `TabloTitleDisplay`. No JS, no state management. +- Unchanged from current implementation; only CSS class tokens change. + +### Inline Edit (tablo description — moved to Overview tab) +- Click `.tablo-desc-zone` → HTMX swap to `TabloDescEditFragment` (outerHTML). +- Unchanged from current implementation. + +### Tab switching +- Each tab `
` is a full GET request to `/tablos/{id}/{tab}`. +- `hx-target="#tab-content"` + `hx-push-url="true"` — existing HTMX wiring preserved. +- Active tab determined server-side; `.is-active` class applied in Go template. + +### Kanban drag-and-drop +- Sortable.js wiring in `KanbanBoard` preserved exactly — no JS changes. +- Reorder form posts to `/tablos/{id}/tasks/reorder`. + +### Add task +- `.tasks-add-button` triggers HTMX GET to show `TaskCreateFormFragment` inline at bottom of column. +- Existing HTMX `hx-target` / `hx-swap` wiring preserved. + +### File upload +- "Upload file" button in `.overview-section-heading` triggers HTMX to swap `FileUploadForm` into place (below heading or inline). +- Existing `FileUploadForm` component preserved; only surrounding layout changes. + +### File delete +- Trash `@ui.IconButton` triggers HTMX GET → `FileDeleteConfirmFragment` swaps the `.file-row-zone` (outerHTML). +- Confirm button POSTs delete. Cancel reverts to `FileListRow`. Unchanged. + +### Delete tablo +- Delete `@ui.IconButton` (ghost/danger) in header triggers HTMX GET → `TabloDeleteConfirmFragment` swaps `.tablo-delete-zone` (outerHTML). +- Unchanged interaction; only visual restyling. + +--- + +## New CSS Classes Required + +The following CSS classes must be added to `backend/internal/web/ui/app.css`. +All values use `var(--...)` tokens — no hardcoded hex. + +``` +/* Ported from go-backend/internal/web/ui/app.css — Tasks section */ + +.tasks-section { ... } /* already defined in go-backend */ +.tasks-section-header { ... } /* already defined in go-backend */ +.tasks-add-button { ... } /* already defined in go-backend */ +.task-list { ... } /* already defined in go-backend */ +.task-row { ... } /* already defined in go-backend */ +.task-check { ... } /* already defined in go-backend */ +.task-check.is-complete { ... } /* already defined in go-backend */ +.task-body { ... } /* already defined in go-backend */ +.task-row.is-complete .task-body p { ... } +.task-meta { ... } /* already defined in go-backend */ + +/* New for Phase 16 — Kanban column wrapper */ +.kanban-column { flex-shrink: 0; width: 18rem; } + +/* New for Phase 16 — Etape group inside kanban column */ +.etape-group { display: flex; flex-direction: column; } + +.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.375rem 1rem; +} + +.etape-group-color-dot { + border-radius: 999px; + flex-shrink: 0; + height: 0.5rem; + width: 0.5rem; +} + +.etape-group-label { + color: var(--color-text-secondary); + font-size: 0.75rem; + font-weight: 600; +} + +.etape-group-label.is-unassigned { + color: var(--color-text-muted); + font-weight: 400; +} + +/* New for Phase 16 — Kanban column scoped header font */ +.kanban-column .tasks-section-header h3 { + font-size: 1rem; +} + +/* New for Phase 16 — Tab nav */ +.tab-nav { + border-bottom: 1px solid var(--color-border-muted); + display: flex; + gap: 1.5rem; + margin-bottom: 1.5rem; + overflow-x: auto; +} + +.tab-nav-item { + border-bottom: 2px solid transparent; + color: var(--color-text-muted); + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.875rem; + font-weight: 500; + min-height: 44px; + padding-bottom: 0.75rem; + padding-inline: 0.25rem; + white-space: nowrap; + transition: color 0.2s ease, border-bottom-color 0.2s 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); +} + +/* New for Phase 16 — 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.375rem; +} + +.tablo-metadata-date svg { + height: 1rem; + width: 1rem; +} + +/* New for Phase 16 — Task list empty state within column */ +.task-list-empty { + color: var(--color-text-faint); + font-size: 0.875rem; + font-style: italic; + padding: 0.75rem 1rem; +} +``` + +Source: go-backend `app.css` sections 13–18, CONTEXT.md D-K02, D-K03, D-E02. + +--- + +## Registry Safety + +| Registry | Blocks Used | Safety Gate | +|----------|-------------|-------------| +| shadcn official | none | not applicable | +| third-party | none | not applicable | + +No shadcn, no third-party registries. All components are in-tree templ files. + +--- + +## Pre-Population Sources + +| Source | Decisions Used | +|--------|---------------| +| CONTEXT.md `` | 16 decisions (D-H01–H06, D-K01–K04, D-E01–E05, D-F01–F04) | +| CONTEXT.md `` | Etape group div structure, upload button placement, status badge variant call | +| CONTEXT.md `` | CSS class names from go-backend, component names from Phase 13 | +| `backend/internal/web/ui/base.css` | All color token values | +| `backend/internal/web/ui/app.css` | `.project-card-top`, `.overview-section-heading`, spacing values | +| `go-backend/internal/web/ui/app.css` | `.tasks-section`, `.task-row`, `.task-check`, `.tasks-add-button`, progress bar classes | +| `backend/internal/web/ui/variants.go` | `BadgeVariantPrimary`, `IconButtonToneGhost`, `ButtonVariantGhost` exact identifiers | +| `backend/templates/tablos.templ` | Tab nav current class patterns, title zone class names | +| `backend/templates/tasks.templ` | Kanban column width, task card class names | +| `backend/templates/files.templ` | FileDeleteConfirmFragment zone pattern | +| User input | 0 (--auto mode; all decisions from context) | + +--- + +## Checker Sign-Off + +- [ ] Dimension 1 Copywriting: PASS +- [ ] Dimension 2 Visuals: PASS +- [ ] Dimension 3 Color: PASS +- [ ] Dimension 4 Typography: PASS +- [ ] Dimension 5 Spacing: PASS +- [ ] Dimension 6 Registry Safety: PASS + +**Approval:** pending