Skip to content

Tasks 025 — Painterly containers ​

Execution skill: superpowers:subagent-driven-development — one implementer per task, then a two-stage review (spec compliance, then code quality). superpowers:test-driven-development applies inside every task: no production code before a failing test that demands it. Reach for superpowers:systematic-debugging on any surprise rather than guessing.

Derived from plan.md (approved). Each task is small, independently verifiable, and reviewed as its own diff. Split where a reviewer could reject one task while approving its neighbour — not where the work merely changes subject. A task is done only when it satisfies the definition of done in CLAUDE.md.

Global Constraints in plan.md apply to every task and are not repeated per task.


T1 — The worktree carries no spike code ​

Satisfies: R13

Discovery left five files and a modified paint/styles.ts in the tree, plus the theme-scope changes applied as spike scaffolding. All of it goes; the implementation re-lands through TDD in T2–T4. The numbers survive in scratchpad/025-theme-scope.patch and in research.md, which is where they belong.

  • [ ] Delete PaintedTooltip.spike.stories.tsx, PaintedDialog.spike.stories.tsx, ThemeScope.spike.stories.tsx (all in apps/web/src/shared/ui/paint/) and ConnectAccountPrompt.spike.stories.tsx (in apps/web/src/shared/ui/).
  • [ ] git checkout -- apps/web/panda.config.ts apps/web/src/shared/ui/paint/paintedSurfaceRecipe.ts apps/web/src/shared/ui/paint/styles.ts — back to the committed state.
  • [ ] Run git status --short: the only changes left are specs/. Run pnpm test — green, and green for the pre-025 reasons.
  • [ ] Commit.

Verified by: git status --short showing no apps/web changes; pnpm test green.


T2 — Shared paint fills exist as tokens ​

Satisfies: R2

  • [ ] RED: in apps/web/src/__tests__/tokens.test.ts, add it('R2: the shared paint fills are tokens', …) asserting tokenTypes contains paint.fill and paint.fillMuted — the file's existing pattern of asserting against the generated tokens.d.ts. Watch it fail because the tokens do not exist, not because the path is wrong.
  • [ ] GREEN: add fill: { value: 'rgba(0,0,0,0.22)' } and fillMuted: { value: 'rgba(0,0,0,0.12)' } to theme.extend.tokens.colors.paint in apps/web/panda.config.ts, beside shadow and glow, with a comment saying why they are shared rather than per-theme (research V2). Run pnpm --filter @gw2priory/web prepare so codegen regenerates tokens.d.ts.
  • [ ] Confirm the test has teeth — rename fill to fillX in the config, re-run codegen, watch the test fail, restore.
  • [ ] Commit.

Verified by: tokens.test.ts — "R2: the shared paint fills are tokens".


T3 — A painted surface is a theme scope ​

Satisfies: R1, R3, P1 #1, P1 #2, P1 #3, P1 #4, SC1, SC2

The heart of the spec. The scope is a data structure before it is CSS, so it is asserted as one — the same call paintThemes.test.ts already makes.

  • [ ] RED: create apps/web/src/shared/ui/paint/__tests__/paintedSurfaceRecipe.test.ts with two tests. First, "R1/P1 #1-#2: every theme re-points the six semantic roles" — for each of the five themes, read paintedSurfaceRecipe.variants.theme[theme].root and assert each of the six properties is present and points at that theme's token (or the shared fill):
ts
const root = paintedSurfaceRecipe.variants?.theme?.[theme]?.root ?? {};
expect(root['--colors-text-strong']).toBe(`{colors.paint.${theme}.textStrong}`);
expect(root['--colors-card']).toBe('{colors.paint.fill}');
  • [ ] RED: second test, "P1 #3/SC2: the scope re-points exactly six roles — rarity is never one" — assert the set of --colors-* keys equals exactly the six. This is the test that fails if someone later adds --colors-rarity-legendary or --colors-surface:
ts
const repointed = Object.keys(root).filter((k) => k.startsWith('--colors-')).sort();
expect(repointed).toEqual([
  '--colors-border', '--colors-card', '--colors-muted',
  '--colors-primary', '--colors-text-muted', '--colors-text-strong',
]);
  • [ ] Watch both fail: today only the four --paint-* properties exist.
  • [ ] GREEN: extend themeVars(name) in paintedSurfaceRecipe.ts with the six re-points, keeping the four --paint-* publications (R4 — the explicit opt-in stays).
  • [ ] RED then GREEN for R3: add "R3/P1 #4: unclassed children inherit the theme's ink" asserting paintedSurfaceRecipe.base.content.color is var(--paint-text); then set it. The comment must say why the re-pointing alone is insufficient — color inherits its computed value, so markup that sets no colour never consults the override.
  • [ ] Confirm the tests have teeth — delete one re-point, watch the first test fail; add a seventh, watch the second fail; restore.
  • [ ] Commit.

Verified by: paintedSurfaceRecipe.test.ts — "R1/P1 #1-#2: every theme re-points the six semantic roles", "P1 #3/SC2: the scope re-points exactly six roles — rarity is never one", "R3/P1 #4: unclassed children inherit the theme's ink".


T4 — chip, the preset for elements smaller than a raster tile ​

Satisfies: R9

  • [ ] RED: in apps/web/src/shared/ui/paint/__tests__/paint.test.ts, add "R9: chip is tuned below the paint's raster tiles" — assert PRESETS.chip.tear.scale === 0 (a 34px popup is shorter than a 13px displacement) and PRESETS.chip.blooms.size is smaller than every panel preset's. Watch it fail on the missing key.
  • [ ] GREEN: add chip to PRESETS with the values discovery settled (research.md F4, and the spike's tuned numbers: wash.opacity 0.86, blooms {density .5, size 14, hardness .78, opacity .5, lift 0}, drag {strength .22, angle -9}, grain .5, pool .7, tear 0), and extend the Intensity union. Comment the reason for the shape, not the numbers: the rasters are 512×384 and 160×160, so a smaller element renders one flat crop of them.
  • [ ] Confirm the test has teeth — set tear.scale to 13, watch it fail, restore.
  • [ ] Commit.

Verified by: paint.test.ts — "R9: chip is tuned below the paint's raster tiles".


T5 — Dialog is a painted container ​

Satisfies: R5, R6, R7, R8, P1 #5, SC3, SC5

  • [ ] RED: in apps/web/src/shared/ui/__tests__/Dialog.test.tsx, add "R5/P1 #5: the popup is a painted surface" — render an open dialog and assert the portalled popup contains a painted root. The dialog portals, so query the document, not the container:
ts
expect(document.querySelector('.painted-surface__root')).toBeInTheDocument();
  • [ ] GREEN: in Dialog.tsx, wrap the popup's internals in PaintedSurface with a module-level const THEME = 'Bark' and className={classes.paint}. In dialogRecipe.ts, strip bg, boxShadow, borderRadius and p from popup (leaving position, size and layout) and add a paint slot carrying the padding. R7: no radius is reintroduced — the torn edge is the point.
  • [ ] Run the whole file: the three pre-existing tests (P3 #1 focus, P3 #2 Escape, P3 #3 accessible name) must pass unmodified. If one needs editing, stop — the public behaviour changed, which SC3 forbids.
  • [ ] Confirm the new test has teeth — remove the PaintedSurface wrapper, watch it fail, restore.
  • [ ] Commit.

Verified by: Dialog.test.tsx — "R5/P1 #5: the popup is a painted surface", plus the three pre-existing tests passing untouched.


T6 — Tooltip is a painted container at chip scale ​

Satisfies: R5, R6, R7, R8, R9, SC3, SC5

  • [ ] RED: in apps/web/src/shared/ui/__tests__/Tooltip.test.tsx, add "R5/R9: the popup is a painted surface at chip intensity" — open the tooltip as the existing tests do and assert document.querySelector('.painted-surface__root') is present.
  • [ ] GREEN: in Tooltip.tsx, wrap the popup content in PaintedSurface with THEME and intensity="chip". In tooltipRecipe.ts, keep maxW on popup, move px/py/fontSize to a new paint slot, and drop bg, color, boxShadow, borderRadius.
  • [ ] Run the whole file: every pre-existing test passes unmodified (SC3).
  • [ ] Confirm teeth — drop intensity="chip" and assert the test still passes (it asserts the surface, not the preset), then break the wrapper to see it fail. Note in the commit message that chip selection itself is covered by T4 and human review, not by this test.
  • [ ] Commit.

Verified by: Tooltip.test.tsx — "R5/R9: the popup is a painted surface at chip intensity".


T7 — ConnectAccountPrompt is a painted container ​

Satisfies: R5, R6, P2 #1, P2 #2, SC3

  • [ ] RED: in apps/web/src/shared/ui/__tests__/ConnectAccountPrompt.test.tsx, add "P2 #1: the prompt renders as a painted surface" — render inside MemoryRouter as the existing test does, and assert the painted root is in the container (no portal here).
  • [ ] GREEN: wrap the prompt's block in PaintedSurface with THEME and the panel preset — no intensity override (research F4: the human chose panel over chip). Add promptPaintStyles to apps/web/src/shared/ui/styles.ts for the padding and pass it as className. promptStyles and promptLinkStyles are not touched: they name text.muted and primary, and the scope re-tints them (P2 #2).
  • [ ] Verify P2 #2 by reading the diff: ConnectAccountPrompt.tsx names no colour and no paint parameter beyond the theme constant.
  • [ ] Run the pre-existing test unmodified; run the five call sites' tests (features/materials, features/wallet, features/legendaries) — all green, untouched.
  • [ ] Commit.

Verified by: ConnectAccountPrompt.test.tsx — "P2 #1: the prompt renders as a painted surface", plus the existing message/link test and the call-site tests passing untouched.


T8 — The prompt has a story ​

Satisfies: R12

  • [ ] Create apps/web/src/shared/ui/ConnectAccountPrompt.stories.tsx with a MemoryRouter decorator (research F2 — the Link throws without a router) and one story per real message, e.g. "to see your materials" and the longer reconnect-scopes copy, so wrapping is visible.
  • [ ] The story imports no styled-system (spec 022 V3 — the Biome ban binds stories); layout is plain elements and args.
  • [ ] Run pnpm --filter @gw2priory/web storybook and confirm the story renders painted. Run pnpm lint — the story is subject to the ban and the filename convention.
  • [ ] Commit.

Verified by: the story rendering in Storybook (human review); pnpm lint green.


T9 — The rules are written down ​

Satisfies: R14, and research F1's graduation

  • [ ] Extend the Painted surface section of docs/architecture/design-system.md with the theme-scope rule: which roles are re-pointed and which are never; that fills are shared and why; that color inheritance forces the content-slot ink; that paint is container-level and never nested; that portals carry their own theme; that painted containers are square-edged; that repeated surfaces share a seed; and the caveat from research F3 — the scope handles a container's own surfaces but cannot thin its chrome, so a dense component is assessed for density before it is painted.
  • [ ] Create docs/gaps/storybook-env.md recording research F1: Storybook has no env configuration, so any story reaching src/api throws Invalid VITE_APP_ENV: undefined; cite apiBase.ts:16 and vitest.config.ts:25, and name the fix (viteFinal define in .storybook/main.ts) without making it. It is a record, not a proposal.
  • [ ] Run pnpm docs:build — VitePress compiles the specs and docs through Vue, so fenced code and backticked generics matter.
  • [ ] Commit.

Verified by: pnpm docs:build green; both files present and cited from research.md's Graduation.


T10 — The feature is verifiably done ​

Satisfies: SC8, and the traceability table in spec.md

  • [ ] Fill spec.md's traceability table from the test names in T2–T7 — transcription, not excavation.
  • [ ] Run the full set: pnpm typecheck, pnpm test, pnpm lint, pnpm build, pnpm docs:build, pnpm verify:contract. pnpm build is the React Compiler gate — a destructured-parameter default or a hand-written useMemo fails there and nowhere else.
  • [ ] Human review in Storybook: all three containers painted, ink legible, rarity colours unchanged. This is the SC1 check no test can make.
  • [ ] Confirm no spike file survives (git log --stat over the branch) and that paint/styles.ts is back to its one-line re-export.
  • [ ] Commit, push, open the PR.

Verified by: all six commands green; the traceability table complete; human sign-off in Storybook.


Notes ​

Staging area for decisions and surprises found during implementation — including anything that turned out differently from what plan.md assumed. Move each one into spec.md, research.md, or docs/ before closing the feature; this section is not a home.

  • T1 reverts spike code that the human has already reviewed and approved visually. The numbers are preserved in research.md and scratchpad/025-theme-scope.patch; if T2–T4 reproduce them exactly, the re-landed implementation is the reviewed one, arrived at through tests.