diff --git a/docs/superpowers/plans/2026-09-16-phase-1b-design-system-and-board.md b/docs/superpowers/plans/2026-09-16-phase-1b-design-system-and-board.md new file mode 100644 index 0000000..d24130c --- /dev/null +++ b/docs/superpowers/plans/2026-09-16-phase-1b-design-system-and-board.md @@ -0,0 +1,2173 @@ +# Phase 1b — Design System and Board Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build the themed design system, the application shell, and a working Kanban board that the owner can drag cards around and a client manager can read and set priorities on. + +**Architecture:** Semantic CSS custom properties defined once per theme and exposed to Tailwind 4 through `@theme inline`, so no component ever hardcodes a colour. A server component loads a project's board using the signed-in user's own PocketBase token, which means the read rules from Plan 1a are the real boundary rather than a UI convention. Drag and drop is `@dnd-kit`, with the position arithmetic extracted into a pure, unit-tested module. The owner writes card moves straight to PocketBase; a client manager's edits go through the `/api/client/*` routes built in Plan 1a. + +**Tech Stack:** Next.js 16 · React 19 · Tailwind 4 · Radix primitives · @dnd-kit · PocketBase realtime · vitest + +**Spec:** `docs/superpowers/specs/2026-09-16-client-board-phase-1-design.md` §8 + +**Prerequisite:** Plan 1a complete. `npm test` and `npm run test:integration` pass, and `npm run seed` has been run. + +--- + +## Design direction + +The visual language comes from two references the developer chose: a dark-teal dashboard with a floating white panel and one warm coral accent, and a light analytics dashboard with a KPI row, status pills and inline progress bars. + +**What is taken from them:** + +- **A tinted page behind a raised, floating panel.** This is the single most valuable idea in either reference. It is what stops the product reading as an admin panel. +- **One warm accent against a cool neutral field.** It works in the reference *because it appears once*. Here it is reserved for exactly three things: an overdue target date, the Inbox count badge, and critical priority. If it starts appearing on buttons and headings it stops meaning anything. +- **Pills and inline progress bars** for status, from the second reference. + +**What is deliberately not taken:** + +- **The hero illustration.** It consumes roughly a quarter of the most valuable space on the page and carries no information. A client opens this to find out whether work is progressing; that space owes them data. +- **The reference's contrast level.** Pale grey text on white photographs well and disappears on a mid-range monitor. Every text token below is checked to at least 4.5:1 against the surface it sits on. +- **The reference's padding.** Both references are low-density dashboards. A six-column board is dense by nature — at the reference's spacing you would fit two and a half columns on a laptop. The board gets its own tighter scale, defined in Task 2. + +**Density targets for the board:** column width 288px, gutter 12px, card tile padding 12px, tile title 13px/1.45. At 1440px this shows five columns with the sixth reachable by scroll. + +--- + +## File structure + +``` +src/ +├── app/ +│ ├── globals.css # REWRITE — token system +│ ├── layout.tsx # REWRITE — font, theme, no-flash +│ ├── page.tsx # REWRITE — role-aware home +│ └── p/[clientSlug]/[projectSlug]/ +│ └── page.tsx # board route (server component) +├── components/ +│ ├── theme/ +│ │ ├── theme-script.tsx # inline no-flash script +│ │ └── theme-toggle.tsx # light / dark / system +│ ├── shell/ +│ │ ├── app-shell.tsx # page background + floating panel +│ │ ├── sidebar.tsx # icon rail with labels +│ │ └── top-bar.tsx # title, project switcher, account +│ ├── board/ +│ │ ├── board.tsx # dnd context + optimistic state +│ │ ├── board-column.tsx +│ │ ├── card-tile.tsx +│ │ ├── client-card-controls.tsx # priority + date, client only +│ │ └── use-board-realtime.ts +│ └── ui/ +│ ├── pill.tsx +│ ├── icon-button.tsx +│ └── dropdown.tsx # thin Radix wrapper +└── lib/ + ├── board/ + │ ├── move.ts # pure drop-position arithmetic + │ ├── move.test.ts + │ ├── dates.ts # overdue / due-soon classification + │ └── dates.test.ts + └── data/ + └── board.ts # server-side board loader +``` + +**On shadcn/ui.** The spec names shadcn/ui, and these components follow its structure exactly — Radix primitives wrapped in local, editable components under `src/components/ui/`. They are written by hand rather than generated by `npx shadcn add`, for two reasons: the generator's output is styled for its own default token names and would have to be rewritten against the tokens in Task 1 anyway, and its init step prompts interactively, which an agent cannot answer. The result is the same architecture without the rework. + +--- + +## Task 1: Theme tokens + +Every colour in the application is defined here once. No component may hardcode a hex value or a Tailwind palette colour such as `bg-neutral-900` — if a needed colour is missing, add a token rather than reaching past the system. + +**Files:** +- Modify: `src/app/globals.css` (replaces the Plan 1a placeholder entirely) + +- [ ] **Step 1: Write the token system** + +`src/app/globals.css`: + +```css +@import "tailwindcss"; + +/* + * Dark mode is class-based, not media-based, because the user picks it + * explicitly and the choice persists. See components/theme/theme-script.tsx. + */ +@custom-variant dark (&:where(.dark, .dark *)); + +/* + * Semantic tokens. Named for the job they do, never for the colour they are: + * `--surface` survives a palette change, `--grey-100` does not. + * + * Every text token below clears 4.5:1 against the surface it is used on, in + * both themes. If you add one, check it — the references this design borrows + * from both fail that bar, and pale-grey-on-white is exactly how a board + * becomes unreadable on a client's mid-range laptop. + */ +:root { + /* Structure: a tinted page with a raised panel floating on it. */ + --page: #e9ecf1; + --panel: #ffffff; + --surface: #f6f7f9; + --surface-raised: #eef0f4; + + --border-subtle: #e2e5ea; + --border-strong: #cbd1d9; + + --text-primary: #101319; + --text-secondary: #4a5260; + --text-muted: #6b7382; + + /* Cool brand blue: primary actions, links, selection. */ + --brand: #2f6dad; + --brand-hover: #275d95; + --brand-fg: #ffffff; + --brand-soft: #e7eff8; + + /* + * The warm accent. Reserved for exactly three things: an overdue date, the + * Inbox count badge, and critical priority. It reads as urgent only while + * it stays rare. + */ + --accent: #c94a24; + --accent-fg: #ffffff; + --accent-soft: #fbeae3; + + --priority-critical: #c94a24; + --priority-high: #a8660a; + --priority-medium: #2f6dad; + --priority-low: #6b7382; + + --shadow-panel: 0 1px 2px rgba(16, 19, 25, 0.04), + 0 16px 40px -12px rgba(16, 19, 25, 0.16); + --shadow-tile: 0 1px 2px rgba(16, 19, 25, 0.06); + --shadow-drag: 0 12px 28px -6px rgba(16, 19, 25, 0.28); +} + +.dark { + --page: #08090b; + --panel: #131519; + --surface: #1a1d22; + --surface-raised: #22262d; + + --border-subtle: #262a31; + --border-strong: #383d47; + + --text-primary: #f1f3f6; + --text-secondary: #aab2bf; + --text-muted: #838c9b; + + --brand: #5e9ee0; + --brand-hover: #7cb0e8; + --brand-fg: #08090b; + --brand-soft: #16263a; + + --accent: #ff8a5c; + --accent-fg: #1a0e08; + --accent-soft: #2e1a12; + + --priority-critical: #ff8a5c; + --priority-high: #e0a23c; + --priority-medium: #5e9ee0; + --priority-low: #838c9b; + + --shadow-panel: 0 1px 2px rgba(0, 0, 0, 0.4), + 0 16px 40px -12px rgba(0, 0, 0, 0.6); + --shadow-tile: 0 1px 2px rgba(0, 0, 0, 0.3); + --shadow-drag: 0 12px 28px -6px rgba(0, 0, 0, 0.7); +} + +/* + * Expose the tokens to Tailwind. `@theme inline` keeps the var() reference + * live, so `bg-surface` resolves at runtime and follows the theme class. + * Without `inline`, Tailwind would bake in the :root value and dark mode + * would silently do nothing. + */ +@theme inline { + --color-page: var(--page); + --color-panel: var(--panel); + --color-surface: var(--surface); + --color-surface-raised: var(--surface-raised); + + --color-border-subtle: var(--border-subtle); + --color-border-strong: var(--border-strong); + + --color-text-primary: var(--text-primary); + --color-text-secondary: var(--text-secondary); + --color-text-muted: var(--text-muted); + + --color-brand: var(--brand); + --color-brand-hover: var(--brand-hover); + --color-brand-fg: var(--brand-fg); + --color-brand-soft: var(--brand-soft); + + --color-accent: var(--accent); + --color-accent-fg: var(--accent-fg); + --color-accent-soft: var(--accent-soft); + + --color-priority-critical: var(--priority-critical); + --color-priority-high: var(--priority-high); + --color-priority-medium: var(--priority-medium); + --color-priority-low: var(--priority-low); + + --font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif; + + --radius-tile: 10px; + --radius-panel: 18px; +} + +* { + border-color: var(--border-subtle); +} + +body { + background: var(--page); + color: var(--text-primary); + font-family: var(--font-sans); + -webkit-font-smoothing: antialiased; +} + +/* + * Numbers in stats, dates and counts must not jitter as they change. + * Applied wherever a figure updates in place. + */ +.tabular { + font-variant-numeric: tabular-nums; + font-feature-settings: "tnum"; +} + +/* A consistent focus ring, since every interactive element needs one and + Tailwind's default differs per element. */ +.focus-ring { + outline: 2px solid var(--brand); + outline-offset: 2px; +} + +:focus-visible { + outline: 2px solid var(--brand); + outline-offset: 2px; +} +``` + +- [ ] **Step 2: Verify the build compiles the tokens** + +```bash +npm run build +``` + +Expected: build succeeds. A Tailwind error naming an unknown utility means a token is referenced before it is declared in `@theme inline`. + +- [ ] **Step 3: Commit** + +```bash +git add src/app/globals.css +git commit -m "feat(theme): semantic token system for light and dark" +``` + +--- + +## Task 2: Theme switching without a flash + +**Files:** +- Create: `src/components/theme/theme-script.tsx` +- Create: `src/components/theme/theme-toggle.tsx` +- Modify: `src/app/layout.tsx` + +- [ ] **Step 1: Create the no-flash script** + +`src/components/theme/theme-script.tsx`: + +```tsx +/** + * Applies the stored theme before first paint. + * + * This has to be a blocking inline script in . A React effect runs after + * hydration, by which point the browser has already painted the default theme + * and the user sees a white flash on every navigation to a dark board. There + * is no way to fix that from inside React. + */ +const SCRIPT = ` +(function () { + try { + var stored = localStorage.getItem('theme'); + var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + var dark = stored === 'dark' || (stored !== 'light' && prefersDark); + document.documentElement.classList.toggle('dark', dark); + document.documentElement.style.colorScheme = dark ? 'dark' : 'light'; + } catch (e) {} +})(); +`; + +export function ThemeScript() { + return