---
phase: 20
slug: tablo-detail-kanban-restyle
status: draft
shadcn_initialized: false
preset: none
created: 2026-05-18
---

# Phase 20 — UI Design Contract
## Tablo Detail & Kanban Restyle

> Visual and interaction contract for the tablo detail page header, tab bar, kanban board, task cards, and files table. Generated by gsd-ui-researcher, verified by gsd-ui-checker.

---

## Design System

| Property | Value |
|----------|-------|
| Tool | none — custom CSS custom properties |
| Preset | not applicable |
| Component library | none (templ components + CSS tokens in `backend/internal/web/ui/`) |
| Icon library | Lucide SVG inline — stroke-width 2, 16×16 in tabs, 20×20 in actions |
| Font | `ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif` |

Source: `backend/internal/web/ui/base.css` — all tokens already defined, no additions required for this phase.

---

## Spacing Scale

Declared values (multiples of 4):

| Token | Value | Usage |
|-------|-------|-------|
| xs | 4px | Pill padding vertical, icon-to-label gap |
| sm | 8px | Badge gap, drag-handle gap, task-meta item gap, task card padding-block |
| md | 16px | Column padding, task row padding-inline |
| lg | 24px | Page horizontal padding, header padding-top |
| xl | 32px | Section break between header and tab bar |
| 2xl | 48px | — (unused in this phase) |
| 3xl | 64px | — (unused in this phase) |

Exceptions:
- Column width: 18rem (288px, not grid-aligned — Sortable.js layout constraint; keep as-is)
- Kanban gap between columns: 16px (`gap-4`)
- Tab nav padding-bottom: 12px per tab item (existing `.tab-nav-item` — keep)
- Progress bar height: 5px — cosmetic dimension, not a layout spacing value; matches Figma spec.

Source: existing `app.css` measurements, confirmed against Figma screenshots.

---

## Typography

| Role | Size | Weight | Line Height |
|------|------|--------|-------------|
| Page title (tablo name h1) | 1.75rem (28px) | 600 | 1.2 |
| Tab label | 0.875rem (14px) | 400 inactive / 600 active | 1.4 |
| Column header h3 | 1rem (16px) | 600 | 1.3 |
| Task card title | 0.875rem (14px) | 400 | 1.4 |
| Metadata / caption | 0.75rem (12px) | 400 | 1.4 |

Notes:
- Body text uses `0.875rem` throughout task cards and metadata rows (source: sketch-findings `0.875rem body/rows`).
- Exactly 2 font weights in use: `400` (body, inactive labels, task card title) and `600` (headings, active tab, emphasis).
- Priority pill is not rendered in Phase 20 — no priority field in the data model yet. Priority typography belongs to Phase 21.
- No custom font loading — system stack only.

---

## Color

| Role | Value | Usage |
|------|-------|-------|
| Dominant (60%) | `var(--color-surface-default)` = `#ffffff` | Page background, kanban column bodies, task cards |
| Secondary (30%) | `var(--color-surface-muted)` = `#f3f4f6` | Column header background, etape group headers, progress track |
| Accent (10%) | `var(--color-brand-primary)` = `#804eec` | Active tab underline, active tab text, progress bar fill, "Create Task" primary CTA |
| Destructive | `var(--color-status-danger-foreground)` = `#dc2626` | Delete task icon button hover only |

Accent reserved for:
1. Active tab bar underline (`border-bottom-color`)
2. Active tab text color
3. Progress bar fill (`background`)
4. Primary CTA button (Create Task, Upload File)
5. "Add task" link text in column header

Status badge colors (locked from sketch-findings):
- Active/In Progress: background `#ecfdf3`, text `#16a34a` — `var(--color-status-success-soft-bg)` / `var(--color-status-success-foreground)`
- Archived/Not Started: background `var(--color-surface-muted)`, text `var(--color-text-muted)`
- In Review: background `var(--color-status-warning-soft-bg)` = `#fff4e2`, text `var(--color-status-warning-foreground)` = `#db9729`
- Done: background `var(--color-status-success-soft-bg)`, text `var(--color-status-success-foreground)`

Priority pill colors (deferred to Phase 21 — not rendered in Phase 20).

---

## Surfaces: Tablo Detail Header

**Goal:** Match Board.png Figma — tablo name large, owner avatar, due date metadata row, status badge, progress bar + percentage, right-aligned action buttons (Group Chat, Invite).

**Primary focal point:** `h1.tablo-title-zone` — the tablo name is the first visual anchor on the detail page.

### Header layout

```
[ avatar-48 ] [ tablo-name h1 ]                    [ member stack ] [ Invite button ]
  Owner: [avatar-24] Name  |  Due Date: YYYY-MM-DD  |  Status: [badge]  |  Progress: [bar] NN%
```

### Header sub-elements

**Tablo avatar:** 48×48px, `border-radius: 12px`, background = tablo color or `var(--color-project-fallback)`, white bold initial letter at 1rem.

**Tablo name h1:** `font-size: 1.75rem`, `font-weight: 600`, `color: var(--color-text-primary)`, inline-edit on click (existing HTMX pattern preserved).

**Metadata row:** horizontal flex, `gap: 24px`, `padding-block: 16px`, `border-bottom: 1px solid var(--color-border-muted)`.
- Owner segment: 24×24 avatar circle + owner display name at 0.875rem
- Due Date segment: calendar icon 16×16 + formatted date string at 0.875rem (show "—" if null)
- Status segment: status badge pill
- Progress segment: `[bar-track] [NN%]` — track width 120px min, fill uses `var(--color-brand-primary)` (not project color), percentage at 0.875rem font-weight 600

**Progress bar dimensions:** height 5px, `border-radius: 9999px`, track `var(--color-surface-muted)`, fill `var(--color-brand-primary)`.

**Member avatar stack (top-right):** up to 3 overlapping 32×32 circles, `-margin-left: 8px` for overlap, `border: 2px solid white`, `border-radius: 50%`.

**Action buttons (top-right):**
- "Group Chat" — icon-button, neutral ghost variant, existing `ui.IconButton`, `aria-label="Groupe de discussion"`
- "Invite" — outline button with person-plus icon, neutral soft variant

---

## Surfaces: Tab Bar

The tab bar is the sticky navigation below the header. Current implementation: `Overview | Tasks | Files | Discussion | Events`.

**Figma tabs (Board.png):** `Board | List | Gantt | Calendar | Files` — these are task-view tabs within the tasks section of the detail page, not the outer tab bar. The outer tab bar stays as-is: `Overview | Tasks | Files | Discussion | Events`. The inner task view switcher (Board/List/Gantt/Calendar) is Phase 21 scope — **do not add a task view switcher in Phase 20**.

### Tab bar spec (unchanged pattern, tokens confirmed)

| Property | Value |
|----------|-------|
| Container | `border-bottom: 1px solid var(--color-border-muted)` |
| Item height | 44px touch target (existing) |
| Gap between items | 24px (existing `gap: 1.5rem`) |
| Inactive color | `var(--color-text-muted)` |
| Active color | `var(--color-text-brand)` = `#804eec` |
| Active underline | `border-bottom: 2px solid var(--color-brand-primary)` |
| Active font-weight | 600 |
| Hover color | `var(--color-text-primary)` |
| Icon size | 16×16 |

No changes required to `.tab-nav` or `.tab-nav-item` CSS rules from the existing code.

---

## Surfaces: Kanban Board

### Board container

`display: flex; gap: 16px; overflow-x: auto; padding-bottom: 16px`

No background panel behind the board — columns sit directly on the page surface.

### Kanban column

| Property | Current | Target (Phase 20) |
|----------|---------|-------------------|
| Width | 18rem (288px) | 18rem — unchanged |
| Background | `var(--color-surface-default)` white | `var(--color-surface-default)` white |
| Border | `1px solid var(--color-border-subtle)` | `1px solid var(--color-border-default)` |
| Border-radius | 16px (`1rem`) | 12px (`0.75rem`) — match card radius from sketch-findings |
| Overflow | hidden | hidden |
| Column gap (board-level) | 16px | 16px — unchanged |

### Column header

| Property | Value |
|----------|-------|
| Background | `var(--color-surface-muted)` = `#f3f4f6` |
| Padding | `12px 16px` |
| Layout | `display: flex; align-items: center; justify-content: space-between` |
| Column title | 0.875rem, weight 600, `var(--color-text-primary)` |
| Task count badge | pill, `var(--color-surface-default)` background, `var(--color-text-secondary)` text, `border: 1px solid var(--color-border-muted)`, `border-radius: 9999px`, `font-size: 0.75rem`, `padding: 0 8px` |
| "+ Add" trigger | `color: var(--color-text-brand)`, 0.875rem, weight 400, no border, ghost button |

Column header color mapping (status → header accent):
- Not Started: no color accent — neutral header
- To Do: no color accent — neutral header
- In Progress: no color accent — neutral header
- Done: no color accent — neutral header

The Figma design uses minimal headers. Do NOT add colored left-border or colored header background per column — keep all column headers the same muted surface.

### Task cards

**Current state:** `.task-row` inside `.task-card-zone` — horizontal row with drag handle, checkbox, title, delete button.

**Figma target (Board.png):** Cards are white boxes with:
- File/type icon or colored initial (top-left of card)
- Task title at 0.875rem, weight 400
- Row of meta: due date + assignee avatars (priority pill deferred to Phase 21)
- Drag handle visible on hover only (3-dot vertical handle)
- No visible border on rest state; subtle shadow on hover
- Delete icon replaced by a "..." overflow menu (for this phase: keep delete icon but move to top-right corner of card, ghost on rest, visible on hover)

**Card CSS contract:**

```css
.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);
}
```

Card layout (column direction):
1. Row 1: `[drag-handle] [task-title flex-1] [delete-icon — visible on hover]`
2. Row 2: `[due-date?] [assignee-avatars?]` — all optional; skip row if all empty

For Phase 20, the task data model has: title, status, etape assignment. No priority field yet. **Priority pill is not rendered in Phase 20** — the meta row is omitted if there is no meta. Do not add placeholder meta.

**Drag handle:** `⠿` Braille pattern, `color: var(--color-text-faint)`, `font-size: 1rem`, visible only on `.task-card:hover` (use CSS `opacity: 0` → `opacity: 1` on parent hover, not `display: none`). Transition `0.12s`.

**Task title:** `font-size: 0.875rem`, `font-weight: 400`, `color: var(--color-text-primary)`, single-line ellipsis overflow.

**Task card delete icon:** `color: var(--color-text-faint)` at rest, `color: var(--color-status-danger-foreground)` on hover. Opacity 0 at rest, opacity 1 on `.task-card:hover`. Do NOT use `display: none` — opacity transition preserves layout.

**Between cards:** `gap: 8px` on `.task-list` (currently `display: flex; flex-direction: column`). Change from 0 gap to 8px.

### Empty column state

When a column has no tasks:

| Property | Value |
|----------|-------|
| Copy | "Aucune tâche" |
| Style | `color: var(--color-text-faint)`, `font-size: 0.875rem`, `padding: 24px 16px`, `text-align: center` |
| Icon | None — text only |

---

## Surfaces: Files Table

**Figma (Files.png):** Clean table with columns: Files (icon + name + size), Upload By (avatar + name), Upload Date. Actions column right-aligned with "..." overflow menu.

The existing `ui.Table` component and `FilesTabFragment` are functionally correct. Restyle only:

| Property | Current | Target |
|----------|---------|--------|
| Table wrapper | `ui.Table` default border | `border: 1px solid var(--color-border-default); border-radius: 12px; overflow: hidden` |
| Header row background | browser default | `var(--color-surface-muted)` |
| Header cell text | default | `0.75rem`, weight 600, uppercase, `letter-spacing: 0.04em`, `var(--color-text-muted)` |
| Body row hover | none | `background: var(--color-surface-subtle)` |
| Body row separator | none explicit | `border-bottom: 1px solid var(--color-border-default)` |
| File type icon | none | 32×32px colored icon block (PDF=red `#ef4444`, DOCX=blue `#3b82f6`, other=gray), `border-radius: 6px` |
| File name | plain text | 0.875rem, weight 400, `var(--color-text-primary)` |
| File size | plain text | 0.75rem, `var(--color-text-muted)`, below file name |
| Uploader | name only | 24×24 avatar circle + name at 0.875rem |
| Date | plain text | 0.875rem, `var(--color-text-secondary)` |
| Actions | delete button | "..." icon button, ghost neutral, shows delete option |

For Phase 20: the existing delete action stays as a direct icon button. The "..." pattern is deferred — keep the current delete icon button but apply the updated row hover.

---

## Surfaces: Task Create / Edit Modal

**Figma (Create new task.png):** Full modal overlay with:
- Large title input ("Write task name") at top, 1rem placeholder
- Description textarea below title
- Metadata fields in a 2-column form grid: Status, Project, Assignee, Due date, Priority, Attachment
- Cancel + "Créer la tâche" (purple filled) buttons at bottom-right

**Phase 20 scope:** The existing `TaskCreateFormFragment` and `TaskEditFragment` are inline forms inside the column, not a modal. Converting to a modal is a larger refactor. In Phase 20, restyle the inline forms only:

| Property | Target |
|----------|--------|
| Form container | `background: var(--color-surface-default)`, `border: 1px solid var(--color-border-default)`, `border-radius: 8px`, `padding: 12px`, `box-shadow: 0 8px 24px rgba(15,23,42,.08)` |
| Title input | `border: none`, `border-bottom: 1px solid var(--color-border-default)`, `font-size: 1rem`, `font-weight: 400`, `padding: 4px 0`, `width: 100%` |
| Submit button | `var(--color-brand-primary)` fill, white text — existing solid variant |
| Cancel/Discard | neutral ghost — existing neutral soft variant |

---

## Copywriting Contract

| Element | Copy |
|---------|------|
| Primary CTA — create task | "Créer la tâche" |
| Primary CTA — upload file | "Téléverser un fichier" |
| Secondary CTA — invite member | "Inviter" |
| Empty column state | "Aucune tâche" |
| Empty files state heading | "Aucun fichier" |
| Empty files state body | "Téléversez votre premier fichier pour commencer." |
| Task delete confirmation | "Supprimer cette tâche ?" with "Supprimer" (danger) and "Annuler" |
| Tablo delete confirmation | "Supprimer ce tablo ?" with "Supprimer" (danger) and "Annuler" |
| Progress label | "{N}% complete" — shown inline as text next to progress bar |
| Due date absent | "—" (em dash, not "N/A" or blank) |
| Status label — active | "Actif" |
| Status label — archived | "Archivé" |
| Status label — in progress | "En cours" |
| Status label — not started | "Non démarré" |
| Status label — in review | "En révision" |
| Status label — done | "Terminé" |
| Task create failure | "Impossible de créer la tâche. Réessayez." |
| File upload failure | "Impossible de téléverser le fichier. Réessayez." |

**Localization:** All tablo detail + kanban strings use French throughout.

---

## Interaction Contracts

### Drag-and-drop (TASK-01 continuity requirement)

Sortable.js initialization on `DOMContentLoaded` and `htmx:afterSettle` is PRESERVED verbatim. Do not change:
- `.sortable-column` class on task list containers
- `data-status` attribute on column containers
- `data-task-id` attribute on task cards
- `#reorder-form` hidden form structure
- Sortable `handle: ".task-drag-handle"` and `draggable: ".task-card-zone"` options

Only the visual CSS of `.task-drag-handle` changes (opacity 0 at rest → 1 on card hover).

### Inline task edit

HTMX swap pattern unchanged:
- Click card → `hx-get` on `.task-card` → `outerHTML` swap of `.task-card-zone`
- Form submit → `hx-post` → `outerHTML` swap of `.task-card-zone`
- Discard → `hx-get` `/show` → restore `TaskCard`

### Tab navigation

HTMX push-url + `#tab-content` innerHTML swap is PRESERVED. No changes to routing.

### Inline title edit

Click on `h1.tablo-title-zone` → `hx-get /edit-title` → `outerHTML` swap. Preserved.

### Progress bar update

Progress bar on header shows `0%` until a real tasks query drives it. Phase 20 wires the progress value from the existing `EtapeTaskCounts` data already available in `TabloDetailPage`. The handler already receives task data — compute `doneTasks / totalTasks * 100` and inject as inline style `width: {N}%`.

---

## Component Delta — What Changes vs Current Code

| Component | File | Change |
|-----------|------|--------|
| Tablo header | `tablos.templ` `TabloDetailPage` | Add owner row, due-date field, progress bar wired to real %, member avatar stack |
| Metadata row | `app.css` `.tablo-metadata-row` | Add owner segment + due-date segment styling |
| Progress bar | `app.css` `.project-progress-bar` | Change fill color from `var(--project-color)` to `var(--color-brand-primary)` |
| Column border-radius | `app.css` `.tasks-section` | Change `1rem` → `0.75rem` |
| Task card layout | `app.css` `.task-row` | Change row → column-flex card layout; add hover shadow + border-color; padding-block: 8px |
| Drag handle visibility | `app.css` `.task-drag-handle` | Add `opacity: 0; transition: 0.12s` + `.task-card:hover .task-drag-handle { opacity: 1 }` |
| Delete icon visibility | `app.css` | Add `opacity: 0` at rest + `.task-card:hover` reveals it |
| Task list gap | `app.css` `.task-list` | Add `gap: 8px; padding: 8px` |
| Files table wrapper | `files.templ` `FilesTabFragment` | Add wrapper div with border + border-radius |
| Files table header | `table.css` or inline | `var(--color-surface-muted)` background, uppercase caption style |
| Group Chat button | `tablos.templ` | Add `aria-label="Groupe de discussion"` to the icon-button |

---

## Registry Safety

| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| shadcn official | none | not applicable — no shadcn |
| third-party | none | not applicable |

No external registries. All components are templ + custom CSS.

---

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