From 28e05b5fc1a3ce0f98c4c76e0bdee7bbd261d7fe Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sun, 17 May 2026 16:16:27 +0200 Subject: [PATCH] docs(19): research tablo list revamp phase --- .../19-tablo-list-revamp/19-RESEARCH.md | 577 ++++++++++++++++++ 1 file changed, 577 insertions(+) create mode 100644 .planning/phases/19-tablo-list-revamp/19-RESEARCH.md diff --git a/.planning/phases/19-tablo-list-revamp/19-RESEARCH.md b/.planning/phases/19-tablo-list-revamp/19-RESEARCH.md new file mode 100644 index 0000000..b73cc70 --- /dev/null +++ b/.planning/phases/19-tablo-list-revamp/19-RESEARCH.md @@ -0,0 +1,577 @@ +# Phase 19: Tablo List Revamp — Research + +**Researched:** 2026-05-17 +**Domain:** Go + templ UI, CSS layout, PostgreSQL migration, sqlc +**Confidence:** HIGH + +--- + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions +- **D-01:** Screenshots in `screenshots/` are the primary visual reference. `Homepage.png` shows the Figma design; `ssidebar-header.png` shows the production app with: status badge top-left, delete button top-right, colored initial circle, title, date, progress bar "Progression: X%". +- **D-02:** The Figma design is the authoritative design source. Executors MUST read screenshots before making visual decisions. +- **D-03:** View toggle has **no persistence** — resets to card grid on every page load. A small inline JS click handler toggles a class on the container. No server round-trip, no localStorage, no cookie. +- **D-04:** Card grid is the default view. List view shows tablos as rows. +- **D-05:** Progress = `completed_tasks / total_tasks * 100`. Completed tasks = tasks with `status = 'done'`. If no tasks exist, progress = 0%. +- **D-06:** The query for progress must be efficient — single JOIN/aggregation over N+1 queries. +- **D-07:** Add `status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'archived'))` to tablos table. +- **D-08:** Phase 19 only implements the 'active' state in the UI. No archiving button. All existing tablos default to 'active'. +- **D-09:** A status indicator (small badge) is visible on cards and list rows showing "Active". +- **D-10:** Card shows: status badge (top-left), delete button (top-right), colored initial circle, tablo title, creation date, progress bar with percentage. +- **D-11:** Progress bar uses existing `var(--color-accent)` or purple accent token. Bar background is a lighter tint. +- **D-12:** List view shows tablos as table rows: status badge, title, creation date, progress %, delete button. + +### Claude's Discretion +- Exact CSS class naming for new card elements — follow `.tablo-card-*` naming convention. +- Whether progress query is a new sqlc query or computed in handler. +- Table structure for list view — `` or flex rows. + +### Deferred Ideas (OUT OF SCOPE) +- Archiving UI (archive button, confirmation, filter toggle) +- "Show archived" toggle +- Status values beyond active/archived +- Tablo filtering/sorting + + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|------------------| +| LIST-01 | Tablo cards revamped to match Figma — updated layout, typography, progress bar with real task completion % | TabloCardView needs Progress int; batch SQL query identified; CSS classes inventoried | +| LIST-02 | User can toggle between card grid and list view (no persistence, resets on page load) | JS data-attribute toggle pattern confirmed; list view CSS approach documented | +| LIST-03 | Tablos have active/archived status field in DB, visible as UI indicator on cards and list rows | Migration 0010 confirmed; Tablo struct needs Status string; badge component available | + + +--- + +## Summary + +Phase 19 revamps the tablos dashboard to match the production design. The work touches four distinct layers: DB migration, sqlc codegen, Go handler/view-model, and templ template + CSS. + +The current `TabloProjectCard` template renders a colored avatar circle, title, creation date, and edit/delete icon buttons. It has no progress bar and no status badge. The screenshot confirms the target design: status badge top-left (e.g., "En cours"), delete icon top-right, large colored initial circle, title, date, progress bar labeled "Progression: X%". The production screenshot matches this pattern exactly. + +The current `TabloCardView` struct holds only `Tablo sqlc.Tablo` and `DiscussionUnreadCount int64`. It needs `Progress int` (0–100) and `Status string` added. Progress must be fetched in batch — a single SQL query aggregating done/total tasks across all tablo IDs avoids N+1. The new `status` column is added via migration `0010_tablo_status.sql`; sqlc must be re-run after migration. The view toggle is pure CSS+JS: a `data-view` attribute on `#tablos-list` toggled by a button click, with CSS rules for `[data-view="list"]` to switch layout. + +**Primary recommendation:** Three plans — (1) DB migration + sqlc regen + model changes, (2) revamped card template + progress query + handler wiring, (3) list view CSS + toggle JS. + +--- + +## Architectural Responsibility Map + +| Capability | Primary Tier | Secondary Tier | Rationale | +|------------|-------------|----------------|-----------| +| DB status column | Database/Storage | — | Goose migration + CHECK constraint | +| Progress aggregation SQL | Database/Storage | API/Backend | sqlc query, batch join | +| TabloCardView enrichment | API/Backend | — | Handler builds view model before render | +| Card template revamp | Frontend Server (templ) | — | templ components own HTML structure | +| Progress bar rendering | Frontend Server (templ) | — | Inline style `width: X%` from view model | +| Status badge | Frontend Server (templ) | — | Conditional badge in card template | +| View toggle mechanism | Browser/Client | — | JS toggles data attribute; CSS does layout | +| List view CSS | Frontend Server (CSS) | — | Static CSS file, no JS framework | + +--- + +## Screenshot Analysis + +### `screenshots/Homepage.png` (Figma design) +The Figma design shows a "Mes Projets" (My Projects) section with a 3-column card grid. Each card contains: +- **Top row:** A colored status pill badge in the top-left (e.g., "En cours", "À faire"), a small trash/delete icon in the top-right. +- **Avatar row:** A large rounded-square colored initial circle (48px, colored by tablo color), followed by the tablo title. +- **Date row:** Calendar icon + formatted date (e.g., "Apr 15, 2026"). +- **Progress row:** Label "Progression:" with a percentage (e.g., "50%"), and below it a filled progress bar (purple/accent, rounded, ~8px tall). Bar fills to match the percentage. + +Colors: status badge "En cours" uses purple/blue tint (brand color); "À faire" uses a yellow/gray tint. Progress bar fill is purple accent. + +### `screenshots/ssidebar-header.png` (Production app) +Confirms the same layout is already partially implemented but now with production data. Cards show the exact same structure. The status badge values visible are "En cours" and "À faire" (translated). Progress bar is present and labeled "Progression: 50%" or "0%". The avatar circle shows the first letter of the tablo title. Delete icon is in the top-right corner of each card. + +**Key observation:** The production screenshot matches the Figma faithfully. The current `TabloProjectCard` template in `tablos.templ` already has the structural skeleton (`.project-card`, `.project-card-top`, `.project-card-title-row`, `.project-date-row`) but is missing: (a) progress bar + percentage, (b) status badge, and (c) the tablo initial in the avatar circle. + +--- + +## TabloCardView Audit + +### Current struct (in `backend/templates/discussion_forms.go`) +```go +type TabloCardView struct { + Tablo sqlc.Tablo // ID, UserID, Title, Description, Color, CreatedAt, UpdatedAt + DiscussionUnreadCount int64 +} +``` + +### Current `sqlc.Tablo` struct (in `backend/internal/db/sqlc/models.go`) +```go +type Tablo struct { + ID uuid.UUID + UserID uuid.UUID + Title string + Description pgtype.Text + Color pgtype.Text + CreatedAt pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz +} +``` +`Status` is NOT present — it gets added by migration 0010, then sqlc regen adds it to the struct. + +### Required additions to `TabloCardView` +| Field | Type | Purpose | +|-------|------|---------| +| `Progress` | `int` | 0–100, percentage of done tasks | +| `Status` | `string` | "active" or "archived" — display as badge | + +After sqlc regen, `sqlc.Tablo` will gain `Status string`. The `TabloCardView` can either read `card.Tablo.Status` directly or store it as a separate `Status string` field for convenience. Since it mirrors the DB value, reading `card.Tablo.Status` directly in the template is cleaner — no duplication. + +`Progress int` cannot come from the tablo row — it must be computed from the tasks table and stored in `TabloCardView`. + +### Construction path +`TabloCardsFromUnreadRows` in `discussion_forms.go` builds `TabloCardView` slices from `ListTablosByUserWithDiscussionUnreadRow`. After migration + regen, this function must also map the new `Status` column from the row. `Progress` is filled separately by the handler after a batch query. + +--- + +## Progress Query Recommendation + +### Recommended SQL (batch aggregation) +[VERIFIED: tasks.sql — `status` column with `TaskStatusDone = "done"` confirmed] + +```sql +-- name: ListTabloProgressByIDs :many +SELECT + tablo_id, + COUNT(*) FILTER (WHERE status = 'done')::int AS done_tasks, + COUNT(*)::int AS total_tasks +FROM tasks +WHERE tablo_id = ANY(@tablo_ids::uuid[]) +GROUP BY tablo_id; +``` + +This is a single query for all tablos on the dashboard. The handler: +1. Calls `ListTablosByUserWithDiscussionUnread` to get tablo rows. +2. Extracts tablo IDs into a `[]uuid.UUID` slice. +3. Calls `ListTabloProgressByIDs` with that slice. +4. Builds a `map[uuid.UUID]int` of tablo ID → progress %. +5. Assigns `Progress` when constructing each `TabloCardView`. + +**Why not per-tablo query:** N+1 — one query per tablo on a page of 20 tablos would issue 20 DB round-trips. The batch approach is one round-trip regardless of count. [VERIFIED: pattern confirmed from codebase — `ListTablosByUserWithDiscussionUnread` already uses this JOIN approach for unread counts] + +**Progress formula (per D-05):** +```go +func computeProgress(done, total int) int { + if total == 0 { + return 0 + } + return (done * 100) / total +} +``` + +**sqlc type for `tablo_ids`:** sqlc with pgx/v5 handles `ANY(@tablo_ids::uuid[])` with `pgtype.Array[pgtype.UUID]` or `[]uuid.UUID` depending on sqlc config. The existing codebase uses `pgtype.UUID` / `uuid.UUID` — check `sqlc.yaml` for the override. [ASSUMED: exact pgx array type binding — verify against sqlc.yaml and existing query patterns] + +--- + +## Migration Number Confirmation + +[VERIFIED: `ls backend/migrations/`] + +Existing migrations: +``` +0001_init.sql +0002_auth.sql +0003_tablos.sql +0004_tasks.sql +0005_files.sql +0006_social_identities.sql +0007_etapes.sql +0008_events.sql +0009_discussion.sql +``` + +**Next migration number: `0010`** + +File: `backend/migrations/0010_tablo_status.sql` + +```sql +-- migrations/0010_tablo_status.sql +-- Phase 19: Add status column to tablos + +-- +goose Up +ALTER TABLE tablos + ADD COLUMN status text NOT NULL DEFAULT 'active' + CHECK (status IN ('active', 'archived')); + +-- +goose Down +ALTER TABLE tablos DROP COLUMN status; +``` + +After running this migration, sqlc must be regenerated: +```bash +cd backend && sqlc generate +``` + +The `sqlc.Tablo` struct in `models.go` will gain `Status string`. All existing queries (`ListTablosByUser`, `ListTablosByUserWithDiscussionUnread`, `GetTabloByID`, `InsertTablo`, `UpdateTablo`) will automatically include the column in their RETURNING or SELECT * results — but since the queries enumerate columns explicitly, they must be updated to include `status`. + +--- + +## Existing CSS Inventory + +[VERIFIED: `backend/internal/web/ui/app.css`] + +### Relevant existing classes for card work + +| Class | Section | Purpose | +|-------|---------|---------| +| `.project-grid` | §14 | 3-column grid, `gap: 1.25rem` | +| `.project-card` | §15 | Card shell — border, radius 1rem, padding 1rem, hover shadow | +| `.project-card-top` | §15 | Flex row, space-between, `margin-bottom: 1rem` | +| `.project-card-title-row` | §17 | Flex row, gap 0.75rem, `margin-bottom: 1rem` | +| `.project-avatar` | §17 | 3rem × 3rem colored circle, first letter display | +| `.project-date-row` | §18 | Flex row, muted color, `font-size: 0.875rem`, `margin-bottom: 1rem` | +| `.project-progress-track` | §20 | Muted background, `border-radius: 999px`, `height: 0.5rem` | +| `.project-progress-bar` | §20 | Uses `var(--project-color, var(--color-project-fallback))`, fills to width% | + +### What is already wired +- `.project-progress-track` and `.project-progress-bar` exist in app.css (§20) but are NOT used in `TabloProjectCard` — they are used in `TabloDetailPage`'s metadata row. +- `.project-avatar` already renders the initial (see `TabloDetailPage` — it shows `string([]rune(tablo.Title)[0:1])`), but `TabloProjectCard` uses `` without the initial letter. This needs to be added. + +### What needs to be added (new CSS) +- `.project-card-progress-row` — flex row wrapping progress label + bar (mirrors `.project-date-row` pattern) +- `.project-card-progress-label` — "Progression: X%" text styling +- Status badge — can reuse `ui.Badge` component or a bespoke `.tablo-status-badge` class +- List view styles — `[data-view="list"] #tablos-list` override to switch from grid to flex-column rows +- List row styles — `.tablo-list-row` for the row layout in list view + +### Table CSS (`table.css`) +Minimal: only `.ui-table-shell` (overflow-x auto) and `.ui-table` (border-collapse, min-width). Not directly usable for list view rows. D-12 says "use existing table CSS patterns" — the list view should use `
` with `.ui-table` class for consistency, or flex rows following the `.task-row` pattern (§19 in app.css — already used for task rows, similar visual style needed). + +**Recommendation (Claude's discretion):** Use a `
` for list view — this matches D-12 literally and is accessible. The `.ui-table-shell` wrapper handles overflow. + +--- + +## View Toggle Implementation + +### Recommended approach +[VERIFIED: D-03 confirmed — inline JS, no Alpine.js, no persistence] + +**HTML structure:** +```html +
+

Your Tablos

+
+ + +
+
+
+ ... +
+``` + +**CSS toggle rules (append to app.css):** +```css +/* List view override — toggled via data-view attribute */ +#tablos-list[data-view="list"] { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +#tablos-list[data-view="list"] .project-card { + /* Hide card layout, show as row */ + display: none; +} + +#tablos-list[data-view="list"] .tablo-list-row { + display: flex; /* shown only in list view */ +} + +.tablo-list-row { + display: none; /* hidden in grid view */ +} +``` + +**Implementation pattern:** Each `TabloProjectCard` renders BOTH a `.project-card` element AND a `.tablo-list-row` sibling element, with CSS toggling which is visible. This avoids HTMX round-trips to re-render in a different mode and keeps the toggle instant. + +Alternatively: render only the card, and use CSS to transform the card layout at `[data-view="list"]`. This is simpler (one element, not two) but harder to get right for the table-row list format specified in D-12. + +**Final recommendation (Claude's discretion):** Use the dual-element approach — render `.project-card` and `.tablo-list-row` per tablo, CSS shows one or the other based on `data-view`. This cleanly handles D-12's "table rows" requirement without complex CSS overrides. + +--- + +## Recommended Plan Decomposition + +### Plan 1: DB + sqlc + Model Layer +**Scope:** Everything below the template layer. +1. Create `backend/migrations/0010_tablo_status.sql` +2. Run migration locally (`goose up` or `just migrate`) +3. Update all tablo SQL queries in `backend/internal/db/queries/tablos.sql` to include `status` in SELECT/RETURNING lists +4. Add new sqlc query `ListTabloProgressByIDs` in `tablos.sql` +5. Run `sqlc generate` to regenerate `models.go` and query files +6. Add `Progress int` to `TabloCardView` struct in `discussion_forms.go` +7. Update `TabloCardsFromUnreadRows` to map `Status` from the regenerated row type +8. Update `TabloListHandler` (currently `TablosListHandler`) to call `ListTabloProgressByIDs` and populate `Progress` on each card view + +**Tests:** Extend `handlers_tablos_test.go` — verify GET / response includes "Progression" string when tasks exist. + +### Plan 2: Revamped Card Template + CSS +**Scope:** `TabloProjectCard` template + CSS additions for card + list row. +1. Rewrite `TabloProjectCard` in `tablos.templ`: + - Top row: status badge (left) + delete button (right) + - Title row: colored avatar circle with initial + title + - Date row: calendar icon + formatted date + - Progress row: "Progression: X%" label + progress bar +2. Add `tablo-list-row` sibling element inside `TabloProjectCard` for list view +3. Add CSS to `app.css`: + - `.project-card-progress-row`, `.project-card-progress-label` + - `.tablo-list-row` and its children + - `[data-view="list"]` toggle rules +4. Add the status badge — use `ui.Badge` component with a new `BadgeVariantSubtle` or an inline span with `.tablo-status-badge` class + +**Tests:** Template render test — confirm `.project-progress-bar` has correct `width` style when `Progress = 50`. + +### Plan 3: Toggle Button + View Toggle Integration +**Scope:** Header UI addition only — toggle button in `TablosDashboard`. +1. Add toggle button to `TablosDashboard` heading row (grid/list icon SVGs) +2. Wire `onclick` JS as described in the View Toggle section +3. Verify CSS `data-view` switching works in both directions +4. Accessibility: `aria-pressed` state on button + +**Tests:** Manual smoke test (listed in HUMAN-UAT). Automated: assert toggle button exists in GET / response. + +--- + +## Common Pitfalls + +### Pitfall 1: Forgetting to update explicit SELECT column lists after migration +**What goes wrong:** Migration adds `status` to the table, but all existing queries enumerate columns explicitly (`id, user_id, title, description, color, created_at, updated_at`). sqlc will NOT include `status` in the generated struct fields until the queries are updated. +**Why it happens:** sqlc generates types from the SQL, not from the table schema. `SELECT *` would pick it up automatically, but the existing queries use explicit lists. +**How to avoid:** In Plan 1, update EVERY query in `tablos.sql` that has an explicit column list to add `status`. Also update RETURNING clauses in INSERT/UPDATE. +**Warning signs:** After `sqlc generate`, `sqlc.Tablo` still lacks `Status` field. + +### Pitfall 2: sqlc array type for `ListTabloProgressByIDs` +**What goes wrong:** `WHERE tablo_id = ANY(@tablo_ids::uuid[])` may require a specific pgx type in the generated Go code. If the array binding fails at runtime, progress will always be 0. +**Why it happens:** sqlc + pgx/v5 array handling requires correct type annotation or sqlc override config. +**How to avoid:** Check `backend/sqlc.yaml` for existing array overrides. Test the query in isolation with a small slice. [ASSUMED: array binding behavior — verify against existing patterns] +**Warning signs:** `ListTabloProgressByIDs` returns 0 rows even when tasks exist. + +### Pitfall 3: Progress bar CSS variable vs inline style conflict +**What goes wrong:** `.project-progress-bar` uses `background: var(--project-color, var(--color-project-fallback))`, which makes the bar the same color as the tablo's avatar color. In `TabloDetailPage` this is set via `style="--project-color: X"` on a parent. If the dashboard cards don't set this CSS variable, the bar falls back to `--color-project-fallback`. +**Why it happens:** CSS custom property scoping — the bar color is inherited from an ancestor `style` attribute. +**How to avoid:** Either (a) set `style="--project-color: {{ card.Tablo.Color.String }}"` on `.project-card` when color is valid, or (b) define a separate accent-colored bar class for the dashboard that ignores tablo color (per D-11: "use var(--color-accent)"). +**Warning signs:** Progress bar renders in fallback color on cards even when tablo has a color set. + +### Pitfall 4: Dual-element list rows break HTMX OOB card insertion +**What goes wrong:** `TabloCardWithOOBFormClear` uses `HX-Retarget: #tablos-list` + `afterbegin` to prepend a new card after creation. If each card now renders two sibling elements (`.project-card` + `.tablo-list-row`), `hx-swap="afterbegin"` on a single fragment still works, but the fragment must be a single root element. +**Why it happens:** templ components return a single root element; rendering two siblings from one component call requires a fragment wrapper `
` or `