diff --git a/docs/superpowers/specs/2026-09-21-phase-10-gantt-plan-redesign-design.md b/docs/superpowers/specs/2026-09-21-phase-10-gantt-plan-redesign-design.md new file mode 100644 index 0000000..6fb749f --- /dev/null +++ b/docs/superpowers/specs/2026-09-21-phase-10-gantt-plan-redesign-design.md @@ -0,0 +1,193 @@ +# Phase 10 — Gantt plan redesign + +**Date:** 2026-09-21 +**Status:** Draft for review +**Phase:** 10 +**Depends on:** Phase 5 (planning and roadmap view), the existing `cards` / `subtasks` schema +**Supersedes:** The Puzzle + Timeline tabs of the Phase 5 `/plan` page. The data they read (`start_date`, `target_date` on cards) is reused; only the view and a small schema add ship here. + +--- + +## 1. Purpose + +Replace the current `/plan` screen — its Now/Next/Later grouping and its client-grouped timeline do not work for the developer — with a real Gantt where each row is one of the developer's own work items, indented subtasks sit under it, and every bar represents a duration. + +The inspiration is a reference Gantt (dark navy palette, monthly header above weekly ticks, parent bars in a saturated tone with their subtasks in a lighter tint). The reference is treated as **layout, colour direction, and shape** only — none of its domain language ("Planning / Packaging / Marketing") is carried over. What ships is keyed to the existing `projects → cards → subtasks` tree. + +The result answers two questions the current page answers badly: + +- *"How long will this take, and how does it sit against the rest of the work?"* — every card bar shows duration. +- *"What does this card actually involve?"* — the subtasks under it are visible without leaving the planning page. + +## 2. Data — the only schema add + +Two new fields, both optional and idempotent through `ensureXxxFields`-style helpers: + +**`subtasks.start_date` (date, optional)** +**`subtasks.target_date` (date, optional)** + +The existing `subtasks` collection is currently `{ card, title, done, position, created, updated }` and acts as the checklist on the card detail page. The new date fields extend that record so the same row can both be a checklist item **and** appear on the Gantt when it has dates. A subtask with neither date set is invisible on the Gantt, exactly like the Phase 5 timeline's "undated lane" treatment — except here we count them once per project section instead of giving them a row. + +**`cards.assignee` (text, optional, max ~120)** + +A free-text label rendered above the parent bar. Empty by default; nothing in the rest of the app reads it. The Phase 5 timeline never had this and the reference image's "Bob Johnson / Henry Leblanc" labels read as human assignments, so the field exists to support that look. One developer means it will usually be empty in practice; that is intentional, not underbuilt. + +Everything else — `cards.start_date`, `cards.target_date`, `cards.title`, `cards.project`, the existing `subtasks.card` relation, the existing `(card, position)` index — is reused without change. + +No new collections. No new rules. The Phase 1 read rules already scope a client manager to their own client's work; that carries over as-is. + +## 3. The new `/plan` page + +### 3.1 Vertical structure + +For every active project, in client-then-project alphabetical order, render one **project section** with this shape: + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ ● Project Name Client · 12 cards │ ← section header +├──────────────────────────────────────────────────────────────────────┤ +│ MONTHS January February │ ← month row +│ WEEKS W1 W2 W3 W4 W1 W2 W3 W4 W1 W2 W3 W4 │ ← week row +├───────────────────────────────────┬──────────────────────────────────┤ +│ Card title │ ████████ │ ← card row +│ Subtask one │ ████ │ ← subtask row +│ Subtask two │ ████ │ ← subtask row +│ Card title without dates │ — not scheduled — │ ← undated card row +│ Card title │ ████ │ ← card row +├───────────────────────────────────┴──────────────────────────────────┤ +│ 2 cards not scheduled · 3 subtasks not scheduled [Add dates] │ ← footer +└──────────────────────────────────────────────────────────────────────┘ +``` + +Two or more project sections stack vertically. A horizontal divider separates them. + +### 3.2 Inside one project section + +One flat row array drives both the left **label column** and the right **timeline track**, indexed identically. That is the same invariant the existing `TimelineView` already relies on (one row array, two views) — it guarantees a row's label and its bar cannot drift, and it makes subtask indentation a matter of CSS padding rather than a parallel data structure. The array entries are: + +- `{ kind: "card", card, datedSubtasks: Subtask[] }` — renders the parent bar from `card.start_date → card.target_date` and one indented subtask bar per dated subtask. +- `{ kind: "undated", card }` — renders the undated card row. Position below the dated rows within the same project section. + +`datedSubtasks` is pre-filtered at loader time to subtasks where `start_date` or `target_date` is set; subtasks without dates are counted into the section footer instead. + +### 3.3 The bars + +- Parent card bar: `bg-client-brand` with the client's brand colour (already on `clients.brand_color`, today rendered as the row dot). Rounded ends, same height across all cards (12 px). If the card has no assignee, no label is drawn above the bar. +- Subtask bars: same colour, **opacity 0.5** (a single Tailwind opacity modifier against the parent's coloured background; avoids a second CSS variable per project). Inset to the right of the parent bar by `padding-left: 1.5rem`. +- Card bar without dates: not drawn. The label column shows an em-dash and the text "not scheduled". + +### 3.4 Header + +The header above the timeline reuses `monthTicks` and `weekTicks` from `src/lib/roadmap/timeline.ts`. Month label sits centred above the weeks it spans. Each `W1..W4` segment is one quarter-month. The today marker (vertical line + "Today" pill) is reused unchanged. + +### 3.5 Section header + +The project section header is a single row above the timeline header inside the same panel: + +- A 2 px coloured bar at the top edge in the project client's brand colour (this becomes the section's only visual brand cue and removes the need for a separate icon column). +- The project name in larger type on the left. +- The client name + active card count on the right. + +### 3.6 Empty and sparse cases + +- **No active projects at all** — the page shows the existing "No active projects" tile. Same wording as before so it doesn't read as a new feature. +- **No dated cards and no dated subtasks** — the page shows a single "Nothing scheduled yet" panel with the same copy as Phase 5 §4.2. We do not invent an undated lane for the Gantt. +- **A section with only undated cards** — section still renders, timeline area collapses to a single row per undated card with "not scheduled" text. The section footer counts them. + +### 3.7 Interactions (this phase) + +Read-only. + +- Clicking a card bar → opens `/p/[client]/[project]/c/[id]` (existing). +- Clicking a subtask bar → opens the parent card; the existing card detail page renders subtasks, and the subtask with `start_date`/`target_date` set gets a small "scheduled" indicator next to its checkbox (text only, no Gantt duplicated on the card page). +- Hovering a bar shows a tooltip with the card title, the client, and the explicit date range (`"5 Mar → 19 Mar"`). +- Keyboard: every bar is a `role="button"` with `tabIndex=0`, focusable. Enter / Space opens the card. The focus ring uses the existing `--brand` token so it reads against the dark background. + +Editing dates happens on the card detail page or the subtask detail panel, as today. **Drag-to-reschedule is out of scope** for this phase — see §6. + +## 4. Loader + +`loadRoadmap` is replaced by a new `loadPlan` returning the structure the view needs in one server-side pass: + +```ts +type PlanSection = { + project: { id: string; name: string; slug: string }; + client: { id: string; name: string; slug: string; brandColor: string }; + cards: PlanCard[]; +}; + +type PlanCard = { + id: string; + title: string; + priority: CardPriority; + startDate: string | null; + targetDate: string | null; + assignee: string; + href: string; + datedSubtasks: PlanSubtask[]; + undatedSubtaskCount: number; +}; + +type PlanSubtask = { + id: string; + title: string; + done: boolean; + startDate: string | null; + targetDate: string | null; +}; +``` + +Implementation notes: + +- One PocketBase trip per collection (`projects`, `clients`, `cards`, `subtasks`), in the same shape as `loadRoadmap` today. +- Sort: cards within a project by `start_date` ASC NULLS LAST, then by `target_date` ASC NULLS LAST, then by title. Subtasks within a card by `position` ASC. +- Scope: read with the caller's own PocketBase token. The Phase 1 read rules already constrain a client manager to their own client's projects. +- `datedSubtasks` is filtered to rows where either date is present; the rest are counted. + +The existing `RoadmapCard` type and `src/lib/roadmap/band.ts` are dropped from the `/plan` page but kept (used by the dashboard tile if applicable — to confirm before deletion in the plan). The new loader does not need `band`. + +## 5. Files touched + +- `scripts/setup-remote.ts` — add `ensureSubtaskDateFields` (adds `start_date`, `target_date`) and `ensureCardAssigneeField` (adds `assignee`). Both follow the `ensureXxxFields` idempotent pattern (`Phase 8c` background-md, `Phase 9a` proposal fields). +- `src/lib/roadmap/load.ts` — add `loadPlan()` alongside `loadRoadmap`, or rename `loadRoadmap` → `loadPlan` and update callers if the dashboard tile does not need the legacy band/shape. Choice deferred to the implementation plan. +- `src/app/plan/page.tsx` — replace the `RoadmapTabs` body with `GanttPlanView`. Keep the `TopBar` title "Planning". +- `src/components/roadmap/gantt-plan-view.tsx` — new. Single section component; does the heavy lifting (row array, row alignment, month/week ticks via existing helpers). +- `src/components/roadmap/project-section.tsx` — new. One project's section, including its header. +- `src/components/roadmap/gantt-bar.tsx` — new. One bar (card or subtask variant). +- Tests: `src/lib/roadmap/load.test.ts` for the loader; `tests/plan.test.ts` for the page integration. + +## 6. Out of scope + +- **Drag-to-reschedule.** The Phase 5 audit-log shape (owner moving a client-owned `target_date`) applies here too; adding drag to a denser widget without the activity log in place would be a regression. A future phase can wire drag with the same audit-log shape as Phase 5 §5. +- **Subtask editing on the Gantt.** Dates are edited on the card page or subtask panel, as today. +- **Filtering by client, priority, or status.** Not in the request. +- **Connector lines** between parent and child bars (the reference Gantt has them; the indented alignment reads cleanly without them, and the dark palette makes 1px rules tricky to get right). +- **Hover-only tooltips that pop out of the bar** (we use a native `title` attribute for now). +- **Removing the legacy `RoadmapTabs` / `TimelineView` / `PuzzleView` / `band-view` files.** If the dashboard still uses them, leave them. If not, the plan may delete them. This keeps the diff small and reversible. + +## 7. Migration + +`ensureSubtaskDateFields` and `ensureCardAssigneeField` are idempotent. Re-running `npm run setup-remote` over the live instance adds the fields without affecting existing rows. No backfill: existing rows simply show as unscheduled on the new page, exactly the way Phase 5's "undated lane" handled the same case. + +## 8. Acceptance criteria + +1. The `/plan` page loads for the owner and shows one section per active project, sorted client-then-project alphabetical. +2. A client manager sees only their own client's project sections. +3. Within a section, dated cards render as parent bars coloured with the client's brand colour. Their dated subtasks render as indented lighter-tint bars below them. +4. A card with an `assignee` shows that label above its bar; a card without one shows nothing above the bar. +5. Cards without dates show as "not scheduled" rows in the same section, below the dated rows, and are counted in the section footer. +6. Subtasks without dates are invisible on the Gantt but counted in the section footer. +7. The header shows month labels and `W1..W4` segments using the existing `monthTicks` / `weekTicks` helpers. A vertical "Today" line + pill marks the current date. +8. Clicking a card or subtask bar opens the existing card detail page. +9. Every bar is keyboard-focusable and activates on Enter / Space. +10. With no dated cards or subtasks across any project, the page shows a single "Nothing scheduled yet" panel. +11. With no active projects at all, the page shows the existing "No active projects" tile. +12. The schema additions survive a re-run of `scripts/setup-remote.ts` without duplicating fields. +13. The existing `/p/[client]/[project]/c/[id]` page still renders subtasks correctly when a subtask has dates set. +14. Unit tests cover the new loader shape. Integration tests cover the page rendering against the demo data set. + +## 9. Risks + +- **Background colour contrast.** Subtasks use `opacity-50` against the project's brand colour, which is already opaque. If the brand colour is light (e.g. `#ffffff` or close to it), the subtask bar may be hard to read. Mitigation: a follow-up phase can clamp luminance or use a derived darker tint per project; not worth blocking Phase 10 on. +- **Wide projects.** A project with many dated cards and subtasks becomes a long vertical list. Mitigated by the existing `overflow-x-auto` on the timeline track; the label column stays the same width. No change in this phase. +- **Subtasks as both checklist and planning.** A subtask with `done = true` but a `target_date` in the future shows as a completed-but-scheduled row. We draw it with a strikethrough on its title. No rule changes; the user can choose to ignore either signal.