docs(04): UI design contract for Tasks (Kanban) phase
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
02cf49ac31
commit
cdcb335fec
1 changed files with 337 additions and 0 deletions
337
.planning/phases/04-tasks-kanban/04-UI-SPEC.md
Normal file
337
.planning/phases/04-tasks-kanban/04-UI-SPEC.md
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
---
|
||||
phase: 4
|
||||
slug: tasks-kanban
|
||||
status: draft
|
||||
shadcn_initialized: false
|
||||
preset: none
|
||||
created: 2026-05-15
|
||||
---
|
||||
|
||||
# Phase 4 — UI Design Contract: Tasks (Kanban)
|
||||
|
||||
> Visual and interaction contract for Phase 4. Generated by gsd-ui-researcher.
|
||||
> Downstream consumers: gsd-ui-checker, gsd-planner, gsd-executor, gsd-ui-auditor.
|
||||
|
||||
---
|
||||
|
||||
## Design System
|
||||
|
||||
| Property | Value | Source |
|
||||
|----------|-------|--------|
|
||||
| Tool | none (hand-rolled CSS + Tailwind v4) | codebase — `backend/internal/web/ui/` |
|
||||
| Preset | not applicable | no shadcn |
|
||||
| Component library | Custom (`ui.Card`, `ui.Button`, `ui.Badge`, `ui.CSRFField`) | codebase — `backend/internal/web/ui/` |
|
||||
| Icon library | none (text labels + drag handle via CSS `⠿` or plain `::before` dots) | CONTEXT.md — Claude's discretion |
|
||||
| Font | system-ui, -apple-system, "Segoe UI", Roboto, sans-serif | codebase — `base.css` |
|
||||
|
||||
shadcn gate: Not applicable — project is Go/HTMX, not React/Next.js/Vite. No `components.json` expected.
|
||||
|
||||
---
|
||||
|
||||
## Spacing Scale
|
||||
|
||||
Declared values (multiples of 4 only):
|
||||
|
||||
| Token | Value | Usage |
|
||||
|-------|-------|-------|
|
||||
| xs | 4px | Icon gaps, drag-handle padding, color swatch gutter |
|
||||
| sm | 8px | Task card internal padding (top/bottom), badge gap |
|
||||
| md | 16px | Task card internal padding (left/right), form field gaps, column internal spacing |
|
||||
| lg | 24px | Card padding (matches existing `.ui-card` padding: 1.5rem = 24px) |
|
||||
| xl | 32px | Kanban board top margin below tablo header |
|
||||
| 2xl | 48px | Empty state vertical padding |
|
||||
| 3xl | 64px | Not used in this phase |
|
||||
|
||||
Exceptions:
|
||||
- Drag handle touch target: minimum 44px height on `.task-drag-handle` (matches Phase 3 danger/neutral button `min-height: 44px`)
|
||||
- Column width: fixed `w-72` (288px = 18rem) — not on the 8-point scale but matches RESEARCH.md KanbanColumn templ example; consistent with CONTEXT.md D-10
|
||||
|
||||
---
|
||||
|
||||
## Typography
|
||||
|
||||
All sizes derived from the existing codebase. No new type sizes introduced.
|
||||
|
||||
| Role | Size | Weight | Line Height | Source |
|
||||
|------|------|--------|-------------|--------|
|
||||
| Body | 16px (1rem) | 400 | 1.5 | codebase — base.css, tablos.templ `text-base` |
|
||||
| Label / small | 14px (0.875rem) | 500 | 1.25 | codebase — tablos.templ `text-sm font-medium` for field labels |
|
||||
| Card heading | 20px (1.25rem) | 600 | 1.375 | codebase — tablos.templ `text-xl font-semibold leading-snug` |
|
||||
| Page heading | 28px (1.75rem) | 600 | 1.2 | codebase — tablos.templ `text-[28px] font-semibold leading-tight` |
|
||||
|
||||
Weights in use: 400 (regular) and 600 (semibold). No other weights. Consistent with Phase 3.
|
||||
|
||||
Column header typography: 14px / weight 600 / line-height 1.25 — maps to existing `text-sm font-semibold text-slate-700` pattern from RESEARCH.md KanbanColumn templ example.
|
||||
|
||||
---
|
||||
|
||||
## Color
|
||||
|
||||
All hex values extracted directly from codebase CSS files (`base.css`, `button.css`, `card.css`, `badge.css`).
|
||||
|
||||
| Role | Value | Usage |
|
||||
|------|-------|-------|
|
||||
| Dominant (60%) | `#ffffff` (white) | Page background (`bg-white` in Layout body) |
|
||||
| Secondary (30%) | `#f8fafc` (slate-50) | Card backgrounds (`.ui-card` background, header `bg-slate-50`) |
|
||||
| Accent (10%) | `#2563eb` (blue-600) | Reserved: primary action buttons, focus rings, link text hover |
|
||||
| Destructive | `#b91c1c` (red-700) | Destructive actions only: delete confirm button, delete trigger |
|
||||
|
||||
Accent reserved for:
|
||||
1. "Add task" submit button (primary CTA per column)
|
||||
2. "Save changes" button on task edit form
|
||||
3. Focus ring on all interactive elements (`outline: 2px solid #2563eb`)
|
||||
4. "View" link text on tablo cards (existing pattern: `text-blue-600`)
|
||||
|
||||
Supporting surface colors (from existing codebase, not new decisions):
|
||||
- Border: `#e2e8f0` (slate-200) — card borders, form input borders
|
||||
- Muted text: `#64748b` (slate-500) and `#94a3b8` (slate-400) — placeholder, secondary copy
|
||||
- Body text: `#0f172a` (slate-900) — primary text on white
|
||||
- Secondary text: `#334155` (slate-700) and `#475569` (slate-600) — labels, descriptions
|
||||
|
||||
Column header backgrounds: `#f1f5f9` (slate-100) — a light tinted band above each column's task list. Distinct from card white surface to visually anchor the column. Uses existing `bg-slate-100` Tailwind class (no new color).
|
||||
|
||||
Task card (display state): white `#ffffff` background with `border: 1px solid #e2e8f0`. Matches `.ui-card` exactly — use `ui.Card` directly.
|
||||
|
||||
Task card (drag ghost): `#f1f5f9` (slate-100) — Sortable.js `ghostClass: "bg-slate-100"` per RESEARCH.md Pattern 3.
|
||||
|
||||
---
|
||||
|
||||
## Component Inventory
|
||||
|
||||
All components used in this phase and their source:
|
||||
|
||||
| Component | Type | Source | Notes |
|
||||
|-----------|------|--------|-------|
|
||||
| `ui.Card` | existing | `backend/internal/web/ui/card.templ` | Task card display state; use `templ.Attributes` for `data-task-id`, drag class |
|
||||
| `ui.Button` (solid default md) | existing | `backend/internal/web/ui/button.templ` | "Add task" submit, "Save changes" on edit form |
|
||||
| `ui.Button` (soft neutral md) | existing | `backend/internal/web/ui/button.templ` | "Cancel" on add-task form, "Discard changes" on edit form |
|
||||
| `ui.Button` (solid danger md) | existing | `backend/internal/web/ui/button.templ` | "Yes, delete" in task delete confirmation |
|
||||
| `ui.Button` (soft danger md) | new variant needed | `backend/internal/web/ui/button.css` | Task delete trigger button — same `.ui-button-soft-danger-md` class pattern as Phase 3's `soft-neutral`; add CSS rule |
|
||||
| `ui.Badge` (info) | existing | `backend/internal/web/ui/badge.templ` | Task count per column header: `BadgeVariantInfo`, e.g. "3" |
|
||||
| `ui.CSRFField` | existing | `backend/internal/web/ui/csrf_field.templ` | All forms including hidden reorder form |
|
||||
| `KanbanBoard` | new templ component | `backend/templates/tasks.templ` | Wraps all 4 columns + hidden reorder form |
|
||||
| `KanbanColumn` | new templ component | `backend/templates/tasks.templ` | Single column: header + sortable list + add-task slot |
|
||||
| `TaskCard` | new templ component | `backend/templates/tasks.templ` | Display state; carries `.task-card` + `data-task-id` |
|
||||
| `TaskEditFragment` | new templ component | `backend/templates/tasks.templ` | Inline edit form (title + description); carries `.task-card-zone` |
|
||||
| `TaskCreateForm` | new templ component | `backend/templates/tasks.templ` | Inline add-task form at column bottom (title only) |
|
||||
| `TaskDeleteConfirmFragment` | new templ component | `backend/templates/tasks.templ` | Inline confirmation: "Delete task?" + confirm/cancel |
|
||||
|
||||
New CSS rule needed (add to `backend/internal/web/ui/button.css`):
|
||||
|
||||
```css
|
||||
.ui-button-soft-danger-md {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border-radius: 0.375rem;
|
||||
background-color: #fee2e2;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #b91c1c;
|
||||
border: 1px solid #fecaca;
|
||||
min-height: 44px;
|
||||
}
|
||||
.ui-button-soft-danger-md:hover {
|
||||
background-color: #fecaca;
|
||||
}
|
||||
.ui-button-soft-danger-md:focus-visible {
|
||||
outline: 2px solid #b91c1c;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Interaction Contracts
|
||||
|
||||
### § 1 — Kanban Board (TASK-01)
|
||||
|
||||
**Route:** `GET /tablos/{id}` (extension of Phase 3 tablo detail page)
|
||||
|
||||
**Layout:** Tablo title zone (Phase 3) + description zone (Phase 3) + delete zone (Phase 3) → then below: `<div id="kanban-board">` containing 4 `KanbanColumn` components in left-to-right order: To do | In progress | In review | Done.
|
||||
|
||||
Board container: `flex gap-4 overflow-x-auto pb-4` — horizontal scroll when viewport is narrow.
|
||||
|
||||
Column wrapper: `flex-shrink-0 w-72` (288px). Column list area: `min-h-16 space-y-2 sortable-column` — accepts drag, has minimum height so empty columns remain droppable targets.
|
||||
|
||||
Column header: `flex items-center justify-between mb-2` with `<h3 class="text-sm font-semibold text-slate-700">` + `ui.Badge` (info, count as string). Column header area background: `bg-slate-100 rounded px-3 py-2 mb-2` to visually separate from card list.
|
||||
|
||||
**States:**
|
||||
- Populated column: shows N task cards stacked vertically with `space-y-2`
|
||||
- Empty column: shows a `<p class="text-sm text-slate-400 italic text-center py-4">No tasks yet</p>` inside the sortable div, so the column still accepts drops (min-h-16 ensures drop zone height)
|
||||
|
||||
### § 2 — Task Create (TASK-02)
|
||||
|
||||
**Routes:**
|
||||
- `GET /tablos/{id}/tasks/new?status={status}` — returns `TaskCreateForm` fragment
|
||||
- `POST /tablos/{id}/tasks` — creates task, returns `TaskCard` fragment + OOB reset of add-task slot
|
||||
|
||||
**Trigger:** `+ Add task` text button at bottom of each column. Rendered as `<button class="ui-button ui-button-soft-neutral-md w-full text-left text-sm mt-2" hx-get="..." hx-target="#add-task-slot-{status}" hx-swap="innerHTML">+ Add task</button>`.
|
||||
|
||||
**Form layout:**
|
||||
```
|
||||
[Title input — full width, required, placeholder "Task title"]
|
||||
[Save (solid default md)] [Cancel (soft neutral md, restores trigger button without server round-trip via hx-get)]
|
||||
```
|
||||
|
||||
Title: `<input type="text" name="title" required maxlength="255" placeholder="Task title" class="mt-1 block w-full rounded border border-slate-300 px-3 py-2 text-sm placeholder-slate-400 focus:border-slate-500 focus:outline-none"/>`
|
||||
|
||||
**On submit (HTMX):** New `TaskCard` appended to `#column-{status}` via `hx-target="#column-{status}" hx-swap="beforeend"`. Add-task slot reset to trigger button via OOB swap on `#add-task-slot-{status}`.
|
||||
|
||||
**On cancel:** `hx-get="/tablos/{id}/tasks/cancel-new?status={status}"` returns `AddTaskTrigger` fragment — restores the `+ Add task` button without a POST.
|
||||
|
||||
**Validation error:** Inline `@FieldError(errs.Title)` below title input (same pattern as Phase 3 `FieldError`). Form stays open with error copy inline.
|
||||
|
||||
### § 3 — Task Edit (TASK-03)
|
||||
|
||||
**Routes:**
|
||||
- `GET /tablos/{id}/tasks/{task_id}/edit` — returns `TaskEditFragment`
|
||||
- `POST /tablos/{id}/tasks/{task_id}` — saves, returns `TaskCard`
|
||||
|
||||
**Trigger:** Clicking anywhere on a `TaskCard` (not the delete button, not the drag handle). `hx-get` on the card body `<div>` with `hx-target="closest .task-card-zone" hx-swap="outerHTML"`.
|
||||
|
||||
**Edit form layout:**
|
||||
```
|
||||
[Title input — full width, current value pre-filled]
|
||||
[Description textarea — 3 rows, current value pre-filled, placeholder "Description (optional)"]
|
||||
[Save changes (solid default md)] [Discard changes (soft neutral md)]
|
||||
```
|
||||
|
||||
"Discard changes" uses `hx-get="/tablos/{id}/tasks/{task_id}/show"` to restore the display card without a POST (mirrors Phase 3 cancel pattern).
|
||||
|
||||
**On save (HTMX):** Returns `TaskCard` fragment; `hx-target="closest .task-card-zone" hx-swap="outerHTML"`. Updated title/description appear immediately.
|
||||
|
||||
**Outer element class:** `.task-card-zone` on both `TaskCard` and `TaskEditFragment` wrappers so outerHTML round-trips work correctly (mirrors `.tablo-title-zone` pattern from Phase 3).
|
||||
|
||||
### § 4 — Task Move + Reorder (TASK-04, TASK-05)
|
||||
|
||||
**Route:** `POST /tablos/{id}/tasks/reorder`
|
||||
|
||||
**Mechanism:** Sortable.js 1.15.7 (loaded from `/static/sortable.min.js`). Each `.sortable-column` div is a Sortable group="kanban" instance. The `onEnd` callback populates a hidden `#reorder-form` and triggers it via `htmx.trigger(form, "submit")`.
|
||||
|
||||
**Hidden form:** `<form id="reorder-form" method="POST" action="/tablos/{id}/tasks/reorder" hx-post="..." hx-target="#kanban-board" hx-swap="outerHTML" class="hidden">@ui.CSRFField(csrfToken)</form>`
|
||||
|
||||
**Drag handle:** Dedicated `.task-drag-handle` element at the top-left of each `TaskCard`. Rendered as: `<div class="task-drag-handle text-slate-300 hover:text-slate-500 cursor-grab select-none text-xs leading-none mr-2" aria-hidden="true">⠿</div>` (braille pattern dots — visual grip icon, no external icon library needed). Sortable option: `handle: ".task-drag-handle"`. Full card is NOT draggable — only the handle activates drag. Consistent with CONTEXT.md Claude's discretion.
|
||||
|
||||
**Visual feedback during drag:**
|
||||
- Ghost class: `bg-slate-100` (applied by Sortable to the drag ghost)
|
||||
- Chosen class: `opacity-50` (applied to the card being dragged while in flight)
|
||||
- Sortable animation: `150ms`
|
||||
|
||||
**On drop (HTMX swap):** Returns full `#kanban-board` `outerHTML` (full-board refresh per RESEARCH.md Open Question 2 recommendation). Sortable re-initialized via `htmx.onLoad` wrapper.
|
||||
|
||||
**`+ Add task` form is not draggable:** Sortable option `draggable: ".task-card"` restricts draggable children to only `.task-card` elements. The add-task slot does not carry `.task-card`.
|
||||
|
||||
### § 5 — Task Delete (TASK-06)
|
||||
|
||||
**Routes:**
|
||||
- `GET /tablos/{id}/tasks/{task_id}/delete-confirm` — returns `TaskDeleteConfirmFragment`
|
||||
- `POST /tablos/{id}/tasks/{task_id}/delete` — hard-deletes, returns empty `<div>` with same ID (outerHTML swap removes the card)
|
||||
|
||||
**Trigger:** "Delete" button (soft danger md) on `TaskCard`, positioned top-right of the card. `hx-get="/tablos/{id}/tasks/{task_id}/delete-confirm" hx-target="closest .task-card-zone" hx-swap="outerHTML"`.
|
||||
|
||||
**Confirmation fragment:**
|
||||
```
|
||||
"Delete task?"
|
||||
"This cannot be undone."
|
||||
[Yes, delete (solid danger md)] [Keep task (soft neutral md)]
|
||||
```
|
||||
|
||||
"Keep task" uses `hx-get="/tablos/{id}/tasks/{task_id}/show" hx-target="closest .task-card-zone" hx-swap="outerHTML"` to restore `TaskCard` without POST.
|
||||
|
||||
**On delete (HTMX):** Handler returns `<div id="task-{task_id}" class="task-card-zone"></div>` — empty element with same ID. `hx-swap="outerHTML"` replaces the confirmation fragment with an empty div, effectively removing the card from the DOM. (Per RESEARCH.md Open Question 3 recommendation.)
|
||||
|
||||
### § 6 — Task Ordering Persistence (TASK-07)
|
||||
|
||||
No dedicated UI. Satisfied by the reorder endpoint writing `position` to the DB on every drop. On `GET /tablos/{id}`, tasks are fetched `ORDER BY status, position, created_at` — stable order guaranteed across refreshes. No user-visible affordance needed.
|
||||
|
||||
---
|
||||
|
||||
## Copywriting Contract
|
||||
|
||||
| Element | Copy | Source |
|
||||
|---------|------|--------|
|
||||
| Primary CTA (create) | "Add task" | CONTEXT.md D-09 "Add task" label; verb + noun |
|
||||
| Primary CTA (save edit) | "Save changes" | codebase — tablos.templ existing pattern |
|
||||
| Primary CTA (confirm delete) | "Yes, delete" | codebase — tablos.templ existing pattern |
|
||||
| Cancel edit | "Discard changes" | codebase — tablos.templ existing pattern |
|
||||
| Cancel add | "Cancel" | default — consistent with form cancel patterns |
|
||||
| Cancel delete | "Keep task" | mirroring codebase "Keep tablo" pattern |
|
||||
| Empty column state | "No tasks yet" | default — minimal, specific to context |
|
||||
| Task create form heading | (none — form is compact, column header already names context) | CONTEXT.md D-09 "title only for quick capture" |
|
||||
| Task edit form heading | (none — inline expand, context is clear from card) | CONTEXT.md D-08 |
|
||||
| Delete confirmation heading | "Delete task?" | mirrors "Delete tablo?" pattern from Phase 3 |
|
||||
| Delete confirmation body | "This cannot be undone." | codebase — tablos.templ exact copy |
|
||||
| Column header labels | "To do" / "In progress" / "In review" / "Done" | RESEARCH.md `TaskColumnLabels` map |
|
||||
| Title field validation error | "Title is required." | default — consistent with Phase 3 pattern |
|
||||
| Title too long error | "Title must be 255 characters or fewer." | default — matches maxlength constraint |
|
||||
| Generic server error on create/edit | "Something went wrong. Please try again." | default — consistent with Phase 3 `errs.General` |
|
||||
| Task count badge | "{N}" (integer only, no "tasks" label) | CONTEXT.md specifics — "task count badge" |
|
||||
|
||||
Destructive actions in this phase:
|
||||
- **Delete task:** trigger = "Delete" (soft danger) → confirmation fragment → "Yes, delete" (solid danger). No modal. Inline confirmation inside the card zone. Hard-delete, irreversible. Pattern reuses Phase 3 D-07 exactly.
|
||||
|
||||
---
|
||||
|
||||
## Interaction State Summary
|
||||
|
||||
| Component | States | Transition Mechanism |
|
||||
|-----------|--------|----------------------|
|
||||
| TaskCard | display, editing, delete-confirm | HTMX outerHTML swap on `.task-card-zone` |
|
||||
| KanbanColumn | populated, empty | server-rendered; re-rendered on reorder swap |
|
||||
| AddTaskSlot | trigger-button, form-open | HTMX innerHTML swap on `#add-task-slot-{status}` |
|
||||
| KanbanBoard | loading (htmx-request), idle | `htmx-request` class on `#reorder-form` dims cursor during reorder POST |
|
||||
| TaskDragCard | idle, being-dragged (opacity-50), ghost (bg-slate-100) | Sortable.js classes |
|
||||
|
||||
---
|
||||
|
||||
## Asset Contract
|
||||
|
||||
| Asset | Path | Version | How Loaded |
|
||||
|-------|------|---------|------------|
|
||||
| Sortable.js | `/static/sortable.min.js` | 1.15.7 | `<script src="/static/sortable.min.js" defer></script>` in layout or tablo detail template |
|
||||
| HTMX | `/static/htmx.min.js` | existing | already in `layout.templ` |
|
||||
| Tailwind CSS | `/static/tailwind.css` | existing | already in `layout.templ` |
|
||||
|
||||
Sortable.js must be added to `just bootstrap` recipe (per RESEARCH.md Standard Stack installation instructions). It is NOT loaded from a CDN — consistent with the no-CDN policy (`D-10` anti-pattern).
|
||||
|
||||
Sortable.js script tag placement: in `layout.templ` alongside `htmx.min.js`, both `defer`. This means it loads on all pages (minor overhead); alternatively it can be conditional on tablo detail pages only — planner's call. Default: add to layout for simplicity.
|
||||
|
||||
---
|
||||
|
||||
## Accessibility Floor
|
||||
|
||||
Minimum requirements consistent with Phase 3 patterns:
|
||||
|
||||
| Element | Requirement |
|
||||
|---------|-------------|
|
||||
| Drag handle | `aria-hidden="true"` (decorative), `role` not needed; keyboard users cannot drag (v1 limitation — document in code) |
|
||||
| Task card clickable area | `role="button"` + `aria-label="Edit task: {title}"` on the clickable div |
|
||||
| Delete button | `aria-label="Delete task: {title}"` |
|
||||
| Column sortable list | `aria-label="{Column label} column"` on the `.sortable-column` div |
|
||||
| Form inputs | `<label>` with `for` attribute matching input `id`, consistent with Phase 3 |
|
||||
| Focus ring | `focus-visible: outline: 2px solid #2563eb` — inherited from `base.css` |
|
||||
|
||||
---
|
||||
|
||||
## Registry Safety
|
||||
|
||||
| Registry | Blocks Used | Safety Gate |
|
||||
|----------|-------------|-------------|
|
||||
| shadcn official | none | not applicable — no shadcn |
|
||||
| third-party | none | not applicable |
|
||||
|
||||
No third-party component registries used. Sortable.js is a JS utility downloaded as a static file, not a component registry entry. No vetting gate required.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
Loading…
Reference in a new issue