docs(16): UI design contract

This commit is contained in:
Arthur Belleville 2026-05-16 23:02:25 +02:00
parent dcb99fb19a
commit 30c446fc0e
No known key found for this signature in database

View file

@ -40,7 +40,8 @@ Declared values (multiples of 4 only). Token names map to CSS `rem` values used
| Token | Value | Usage | | Token | Value | Usage |
|-------|-------|-------| |-------|-------|-------|
| xs | 4px (0.25rem) | Icon gaps, tight inline spacing | | xs | 4px (0.25rem) | Icon gaps, tight inline spacing |
| sm | 8px (0.5rem) | Compact badges, task-meta gap, column header internal gap | | 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 | | md | 16px (1rem) | Default element spacing, card padding, row padding |
| lg | 24px (1.5rem) | Section heading margin-bottom, dashboard-main gap | | lg | 24px (1.5rem) | Section heading margin-bottom, dashboard-main gap |
| xl | 32px (2rem) | Page-level padding (`dashboard-main`) | | xl | 32px (2rem) | Page-level padding (`dashboard-main`) |
@ -48,9 +49,9 @@ Declared values (multiples of 4 only). Token names map to CSS `rem` values used
| 3xl | 64px (4rem) | Not used in this phase | | 3xl | 64px (4rem) | Not used in this phase |
Exceptions: 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`. - 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. - 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. - 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. Source: `app.css` measured values, CONTEXT.md D-K02, go-backend reference CSS.
@ -61,17 +62,19 @@ Source: `app.css` measured values, CONTEXT.md D-K02, go-backend reference CSS.
| Role | Size | Weight | Line Height | Token / Class | | Role | Size | Weight | Line Height | Token / Class |
|------|------|--------|-------------|---------------| |------|------|--------|-------------|---------------|
| Body | 15px (0.95rem) | 400 (normal) | 1.5 | task-body p, task-row text | | 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 | | 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` overridden to 1rem for kanban context; see note) | | Heading | 16px (1rem) | 600 (semibold) | 1.2 | Column header `h3` (`.tasks-section-header h3` scoped to 1rem inside kanban; see note) |
| Display | 25.6px (1.6rem) | 600 (semibold) | 1.2 | Section headings `.overview-section-heading h3`, `.tasks-section-header h3` base rule | | 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.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. 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`): Tablo title (inline-editable `tablo-title-zone`):
- Display size: 2430px responsive (existing `text-xl md:text-3xl` pattern, replaced with token-based `font-size: 1.75rem`). - Display size: 1.75rem (shared with Display tier above — no separate size).
- Weight: 700 (bold). - Weight: 600 (semibold).
- Color: `var(--color-text-primary)` default; `var(--color-text-brand)` on hover. - 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. Source: `go-backend/app.css` measured values, existing `tablos.templ` class audit.
--- ---
@ -89,7 +92,7 @@ Accent (`#804eec` / `var(--color-brand-primary)`) reserved for:
- Active tab nav indicator (bottom border + text color on active tab) - Active tab nav indicator (bottom border + text color on active tab)
- Tablo title hover color - Tablo title hover color
- Task checkbox fill when complete (`var(--color-text-brand-strong)` = #7c3aed — same family) - Task checkbox fill when complete (`var(--color-text-brand-strong)` = #7c3aed — same family)
- Invite button border and text (ghost/outline style) - Invite Member button border and text (ghost/outline style)
- Focus ring on interactive elements (`var(--color-focus-ring)`) - Focus ring on interactive elements (`var(--color-focus-ring)`)
No other elements use the brand purple. Discussion link uses neutral text, not accent. No other elements use the brand purple. Discussion link uses neutral text, not accent.
@ -112,8 +115,8 @@ All components already exist in `backend/internal/web/ui/`. No new components ne
| Component | Templ call | Usage in this phase | | Component | Templ call | Usage in this phase |
|-----------|-----------|---------------------| |-----------|-----------|---------------------|
| Badge | `@ui.Badge(ui.BadgeProps{Label: ..., Variant: ...})` | Status pill in metadata row; task count in column header | | 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 | | Button | `@ui.Button(ui.ButtonProps{Label: "Invite Member", Variant: ui.ButtonVariantGhost, ...})` | Invite Member button in header |
| IconButton | `@ui.IconButton(ui.IconButtonProps{...})` | Discussion link, Delete (header), Download + Delete (file rows) | | 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 | | Table | `@ui.Table(ui.TableProps{Head: ..., Body: ...})` | Files section list |
| EmptyState | `@ui.EmptyState(ui.EmptyStateProps{Title: ..., Description: ...})` | Files empty state | | EmptyState | `@ui.EmptyState(ui.EmptyStateProps{Title: ..., Description: ...})` | Files empty state |
@ -131,11 +134,11 @@ Structure: `.project-card-top` wrapping row (already in `app.css`).
[ .project-card-top ] [ .project-card-top ]
Left: [ .project-card-title-row ] Left: [ .project-card-title-row ]
[ .project-avatar (color circle, 3rem × 3rem, border-radius 0.85rem) ] [ .project-avatar (color circle, 3rem × 3rem, border-radius 0.85rem) ]
[ .tablo-title-zone (inline-editable h1, font-size 1.75rem, weight 700) ] [ .tablo-title-zone (inline-editable h1, font-size 1.75rem, weight 600) ]
Right: [ action controls row, gap 0.75rem ] Right: [ action controls row, gap 0.75rem (sm-plus) ]
[ Discussion: @ui.IconButton ghost/neutral + label "Discussion" ] [ Discussion: @ui.IconButton ghost/neutral + label "Discussion", aria-label="Discussion" ]
[ Invite: @ui.Button ghost variant, label "Invite" ] [ Invite Member: @ui.Button ghost variant, label "Invite Member" ]
[ Delete: @ui.IconButton ghost/danger, trash icon ] [ Delete: @ui.IconButton ghost/danger, trash icon, aria-label="Delete tablo" ]
``` ```
Color avatar: Color avatar:
@ -146,7 +149,7 @@ Color avatar:
### Metadata Row ### Metadata Row
``` ```
[ .tablo-metadata-row ] — flex row, flex-wrap, gap 1rem, border-bottom 1px var(--color-border-muted), padding-bottom 1rem, margin-bottom 1rem [ .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 ] [ 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 ] [ separator: border-right 1px var(--color-border-muted) — only on md+ breakpoint ]
@ -160,18 +163,18 @@ Progress bar:
### Tab Nav ### Tab Nav
Structure: flex row, gap 1.5rem, border-bottom 1px `var(--color-border-muted)`, margin-bottom 1.5rem. Structure: flex row, gap 1.5rem (lg), border-bottom 1px `var(--color-border-muted)`, margin-bottom 1.5rem (lg).
Each tab link: Each tab link:
- Inactive: `color: var(--color-text-muted)`, `border-bottom: 2px solid transparent`, font-weight 500, font-size 0.875rem. - 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. - Active: `color: var(--color-text-brand)`, `border-bottom: 2px solid var(--color-brand-primary)`, font-weight 600.
- Hover (inactive): `color: var(--color-text-primary)`. - Hover (inactive): `color: var(--color-text-primary)`.
- All tabs: `min-height: 44px`, `padding-bottom: 0.75rem`, `padding-inline: 0.25rem`. - 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>`. - 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 class pattern (match Phase 15 `sidebar-nav-item.is-active`):
```css ```css
.tab-nav-item { color: var(--color-text-muted); border-bottom: 2px solid transparent; ... } .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.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); } .tab-nav-item:hover:not(.is-active) { color: var(--color-text-primary); }
``` ```
@ -179,36 +182,36 @@ CSS class pattern (match Phase 15 `sidebar-nav-item.is-active`):
### Kanban Board (Tasks Tab) ### Kanban Board (Tasks Tab)
``` ```
[ #kanban-board ] — flex row, gap 1rem, overflow-x auto, padding-bottom 1rem [ #kanban-board ] — flex row, gap 1rem (md), overflow-x auto, padding-bottom 1rem (md)
[ .kanban-column ] — flex-shrink 0, width 18rem (fixed) [ .kanban-column ] — flex-shrink 0, width 18rem (fixed)
[ .tasks-section ] — border 1px var(--color-border-subtle), border-radius 1rem, overflow hidden [ .tasks-section ] — border 1px var(--color-border-subtle), border-radius 1rem, overflow hidden
[ .tasks-section-header ] — flex row, justify-between, align-center, [ .tasks-section-header ] — flex row, justify-between, align-center,
border-bottom 1px var(--color-border-muted), padding 1rem border-bottom 1px var(--color-border-muted), padding 1rem (md)
Left: [ h3 "Todo" font-size 1rem, weight 600 ] + [ @ui.Badge count ] Left: [ h3 "Todo" font-size 1rem, weight 600 ] + [ @ui.Badge count ]
Right: [ .tasks-add-button "Add task" button ] Right: [ .tasks-add-button "Add task" button ]
[ .task-list ] — flex column [ .task-list ] — flex column
[ .etape-group ] — per etape in column (server-side grouped) [ .etape-group ] — per etape in column (server-side grouped)
[ .etape-group-header ] — flex row, align-center, gap 0.5rem, [ .etape-group-header ] — flex row, align-center, gap 0.5rem (sm),
padding 0.5rem 1rem, padding 0.5rem (sm) 1rem (md),
background var(--color-surface-muted), background var(--color-surface-muted),
border-bottom 1px var(--color-border-muted) border-bottom 1px var(--color-border-muted)
[ color dot: 0.5rem circle, background {etapeColor} inline style ] [ color dot: 0.5rem circle, background {etapeColor} inline style ]
[ etape name: font-size 0.75rem, weight 600, color var(--color-text-secondary) ] [ etape name: font-size 0.875rem (label tier), weight 600, color var(--color-text-secondary) ]
[ task rows within group... ] [ task rows within group... ]
[ .task-row ] — flex row, align-center, gap 0.75rem, padding 0.9rem 1rem, [ .task-row ] — flex row, align-center, gap 0.75rem (sm-plus / 12px), padding 1rem (md),
border-bottom 1px var(--color-border-muted), border-bottom 1px var(--color-border-muted),
hover: background var(--color-surface-neutral-hover) hover: background var(--color-surface-neutral-hover)
[ .task-check ] — 2rem circle checkbox, border 2px var(--color-border-strong) [ .task-check ] — 2rem circle checkbox, border 2px var(--color-border-strong)
.is-complete → background + border-color var(--color-text-brand-strong) .is-complete → background + border-color var(--color-text-brand-strong)
[ .task-body ] — flex 1, task title 0.95rem weight 500 [ .task-body ] — flex 1, task title 0.95rem weight 400
.is-complete → color var(--color-text-faint), text-decoration line-through .is-complete → color var(--color-text-faint), text-decoration line-through
[ .task-meta ] — etape label (if applicable), 0.75rem muted [ .task-meta ] — etape label (if applicable), 0.875rem (label tier), muted
[ unassigned group (last, if any unassigned tasks exist) ] [ unassigned group (last, if any unassigned tasks exist) ]
[ .etape-group-header with label "No etape", no color dot ] [ .etape-group-header with label "No etape", no color dot ]
@ -217,7 +220,7 @@ CSS class pattern (match Phase 15 `sidebar-nav-item.is-active`):
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)`. 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 1rem. 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 ### Files Section
@ -225,7 +228,7 @@ Empty column state: `<p class="task-list-empty">No tasks yet</p>` — `color: va
[ .overview-section ] [ .overview-section ]
[ .overview-section-heading ] [ .overview-section-heading ]
Left: [ h3 "Files", font-size 1.6rem, weight 600 ] Left: [ h3 "Files", font-size 1.75rem, weight 600 ]
Right: [ @ui.Button(label "Upload file", Variant ButtonVariantDefault, Tone ButtonToneSolid) ] Right: [ @ui.Button(label "Upload file", Variant ButtonVariantDefault, Tone ButtonToneSolid) ]
[ — triggers inline FileUploadForm via HTMX swap ] [ — triggers inline FileUploadForm via HTMX swap ]
@ -235,7 +238,7 @@ Empty column state: `<p class="task-list-empty">No tasks yet</p>` — `color: va
[ filename (truncated, max-width 20rem) ] [ filename (truncated, max-width 20rem) ]
[ human-readable size ] [ human-readable size ]
[ formatted date ] [ formatted date ]
[ actions: @ui.IconButton(ghost/neutral, download icon) + @ui.IconButton(ghost/danger, trash icon) ] [ 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) ] [ @ui.EmptyState (when no files) ]
Title: "No files yet" Title: "No files yet"
@ -268,7 +271,7 @@ Source: CONTEXT.md D-F01, D-F02, D-F03, D-F04.
| Status pill — In progress | "In progress" | | Status pill — In progress | "In progress" |
| Status pill — Done | "Done" | | Status pill — Done | "Done" |
| Status pill — Todo | "Todo" | | Status pill — Todo | "Todo" |
| Invite button | "Invite" | | Invite button | "Invite Member" |
| Column headers | "Todo" / "In Progress" / "Done" (existing `TaskColumnLabels` map — no change) | | Column headers | "Todo" / "In Progress" / "Done" (existing `TaskColumnLabels` map — no change) |
| Etape group — unassigned | "No etape" | | Etape group — unassigned | "No etape" |
| Tab nav labels | "Overview" / "Tasks" / "Files" / "Discussion" / "Events" (unchanged) | | Tab nav labels | "Overview" / "Tasks" / "Files" / "Discussion" / "Events" (unchanged) |
@ -346,8 +349,8 @@ All values use `var(--...)` tokens — no hardcoded hex.
background: var(--color-surface-muted); background: var(--color-surface-muted);
border-bottom: 1px solid var(--color-border-muted); border-bottom: 1px solid var(--color-border-muted);
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem; /* sm / 8px */
padding: 0.375rem 1rem; padding: 0.5rem 1rem; /* sm / 8px vertical, md / 16px horizontal */
} }
.etape-group-color-dot { .etape-group-color-dot {
@ -359,7 +362,7 @@ All values use `var(--...)` tokens — no hardcoded hex.
.etape-group-label { .etape-group-label {
color: var(--color-text-secondary); color: var(--color-text-secondary);
font-size: 0.75rem; font-size: 0.875rem; /* label / 14px */
font-weight: 600; font-weight: 600;
} }
@ -373,12 +376,37 @@ All values use `var(--...)` tokens — no hardcoded hex.
font-size: 1rem; 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 */ /* New for Phase 16 — Tab nav */
.tab-nav { .tab-nav {
border-bottom: 1px solid var(--color-border-muted); border-bottom: 1px solid var(--color-border-muted);
display: flex; display: flex;
gap: 1.5rem; gap: 1.5rem; /* lg / 24px */
margin-bottom: 1.5rem; margin-bottom: 1.5rem; /* lg / 24px */
overflow-x: auto; overflow-x: auto;
} }
@ -387,12 +415,12 @@ All values use `var(--...)` tokens — no hardcoded hex.
color: var(--color-text-muted); color: var(--color-text-muted);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem; /* sm / 8px */
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 500; font-weight: 400;
min-height: 44px; min-height: 44px;
padding-bottom: 0.75rem; padding-bottom: 0.75rem; /* sm-plus / 12px */
padding-inline: 0.25rem; padding-inline: 0.25rem; /* xs / 4px */
white-space: nowrap; white-space: nowrap;
transition: color 0.2s ease, border-bottom-color 0.2s ease; transition: color 0.2s ease, border-bottom-color 0.2s ease;
} }
@ -413,9 +441,9 @@ All values use `var(--...)` tokens — no hardcoded hex.
border-bottom: 1px solid var(--color-border-muted); border-bottom: 1px solid var(--color-border-muted);
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 1rem; gap: 1rem; /* md / 16px */
margin-bottom: 1rem; margin-bottom: 1rem; /* md / 16px */
padding-bottom: 1rem; padding-bottom: 1rem; /* md / 16px */
} }
.tablo-metadata-date { .tablo-metadata-date {
@ -423,7 +451,7 @@ All values use `var(--...)` tokens — no hardcoded hex.
color: var(--color-text-muted); color: var(--color-text-muted);
display: flex; display: flex;
font-size: 0.875rem; font-size: 0.875rem;
gap: 0.375rem; gap: 0.5rem; /* sm / 8px */
} }
.tablo-metadata-date svg { .tablo-metadata-date svg {
@ -436,7 +464,7 @@ All values use `var(--...)` tokens — no hardcoded hex.
color: var(--color-text-faint); color: var(--color-text-faint);
font-size: 0.875rem; font-size: 0.875rem;
font-style: italic; font-style: italic;
padding: 0.75rem 1rem; padding: 0.75rem 1rem; /* sm-plus / 12px vertical, md / 16px horizontal */
} }
``` ```
@ -470,6 +498,7 @@ No shadcn, no third-party registries. All components are in-tree templ files.
| `backend/templates/tasks.templ` | Kanban column width, task card class names | | `backend/templates/tasks.templ` | Kanban column width, task card class names |
| `backend/templates/files.templ` | FileDeleteConfirmFragment zone pattern | | `backend/templates/files.templ` | FileDeleteConfirmFragment zone pattern |
| User input | 0 (--auto mode; all decisions from context) | | 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 |
--- ---