---
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, etape group header padding-block |
| sm-plus | 12px (0.75rem) | Tab nav padding-bottom, task row gap, compact list padding (between sm and md; 8px is too tight, 16px is too loose for medium-density list items) |
| 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 Member 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: 8px (0.5rem) 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 | 14px (0.875rem) | 400 (normal) | 1.4 | `.task-meta`, `.project-date-row`, `.tab-nav-item`, `.tablo-metadata-date`, `.task-list-empty`, column count badge, etape labels |
| Heading | 16px (1rem) | 600 (semibold) | 1.2 | Column header `h3` (`.tasks-section-header h3` scoped to 1rem inside kanban; see note) |
| Display | 28px (1.75rem) | 600 (semibold) | 1.2 | Tablo title (`.tablo-title-zone`), 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.75rem — 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: 1.75rem (shared with Display tier above — no separate size).
- Weight: 600 (semibold).
- Color: `var(--color-text-primary)` default; `var(--color-text-brand)` on hover.

Primary focal point: tablo title + color avatar.

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 Member 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 Member", Variant: ui.ButtonVariantGhost, ...})` | Invite Member button in header |
| IconButton | `@ui.IconButton(ui.IconButtonProps{...})` | Discussion link (aria-label="Discussion"), Delete tablo (aria-label="Delete tablo"), Download file (aria-label="Download file"), Delete file (aria-label="Delete file") |
| 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 600) ]
  Right: [ action controls row, gap 0.75rem (sm-plus) ]
           [ Discussion: @ui.IconButton ghost/neutral + label "Discussion", aria-label="Discussion" ]
           [ Invite Member: @ui.Button ghost variant, label "Invite Member" ]
           [ Delete: @ui.IconButton ghost/danger, trash icon, aria-label="Delete tablo" ]
```

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 (md), border-bottom 1px var(--color-border-muted), padding-bottom 1rem (md), margin-bottom 1rem (md)

  [ 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 (lg), border-bottom 1px `var(--color-border-muted)`, margin-bottom 1.5rem (lg).

Each tab link:
- Inactive: `color: var(--color-text-muted)`, `border-bottom: 2px solid transparent`, font-weight 400, 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` (sm-plus / 12px), `padding-inline: 0.25rem` (xs / 4px).
- Active state toggled via Go template conditional — CSS class `.is-active` applied to the active `<a>`.

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; font-weight: 400; ... }
.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 (md), overflow-x auto, padding-bottom 1rem (md)

  [ .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 (md)
        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 (sm),
                                    padding 0.5rem (sm) 1rem (md),
                                    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.875rem (label tier), weight 600, color var(--color-text-secondary) ]

        [ task rows within group... ]
          [ .task-row ] — flex row, align-center, gap 0.75rem (sm-plus / 12px), padding 1rem (md),
                          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 400
                             .is-complete → color var(--color-text-faint), text-decoration line-through
            [ .task-meta ] — etape label (if applicable), 0.875rem (label tier), 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: `<p class="task-list-empty">No tasks yet</p>` — `color: var(--color-text-faint)`, `font-size: 0.875rem`, italic, padding 0.75rem (sm-plus) 1rem (md).

### Files Section

```
[ .overview-section ]

  [ .overview-section-heading ]
    Left:  [ h3 "Files", font-size 1.75rem, 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, aria-label="Download file") + @ui.IconButton(ghost/danger, trash icon, aria-label="Delete file") ]

  [ @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 Member" |
| 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 `<a>` 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;        /* sm / 8px */
  padding: 0.5rem 1rem; /* sm / 8px vertical, md / 16px horizontal */
}

.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.875rem;  /* label / 14px */
  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 — Task row */
.task-row {
  align-items: center;
  border-bottom: 1px solid var(--color-border-muted);
  display: flex;
  gap: 0.75rem;       /* sm-plus / 12px */
  padding: 1rem;      /* md / 16px */
}

.task-row:hover {
  background: var(--color-surface-neutral-hover);
}

/* New for Phase 16 — Task body weight */
.task-body {
  flex: 1;
  font-size: 0.95rem;
  font-weight: 400;
}

.task-row.is-complete .task-body p {
  color: var(--color-text-faint);
  text-decoration: line-through;
}

/* New for Phase 16 — Tab nav */
.tab-nav {
  border-bottom: 1px solid var(--color-border-muted);
  display: flex;
  gap: 1.5rem;        /* lg / 24px */
  margin-bottom: 1.5rem; /* lg / 24px */
  overflow-x: auto;
}

.tab-nav-item {
  border-bottom: 2px solid transparent;
  color: var(--color-text-muted);
  display: flex;
  align-items: center;
  gap: 0.5rem;        /* sm / 8px */
  font-size: 0.875rem;
  font-weight: 400;
  min-height: 44px;
  padding-bottom: 0.75rem;  /* sm-plus / 12px */
  padding-inline: 0.25rem;  /* xs / 4px */
  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;          /* md / 16px */
  margin-bottom: 1rem; /* md / 16px */
  padding-bottom: 1rem; /* md / 16px */
}

.tablo-metadata-date {
  align-items: center;
  color: var(--color-text-muted);
  display: flex;
  font-size: 0.875rem;
  gap: 0.5rem;        /* sm / 8px */
}

.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; /* sm-plus / 12px vertical, md / 16px horizontal */
}
```

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 `<decisions>` | 16 decisions (D-H01–H06, D-K01–K04, D-E01–E05, D-F01–F04) |
| CONTEXT.md `<specifics>` | Etape group div structure, upload button placement, status badge variant call |
| CONTEXT.md `<canonical_refs>` | 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 revision | 3 blocking fixes: typography consolidated to 4 sizes / 2 weights; spacing values corrected to multiples of 4; 12px (sm-plus) token added to spacing scale |

---

## 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
