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 inapps/web/src/shared/ui/paint/) andConnectAccountPrompt.spike.stories.tsx(inapps/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 arespecs/. Runpnpm 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, addit('R2: the shared paint fills are tokens', …)assertingtokenTypescontainspaint.fillandpaint.fillMuted— the file's existing pattern of asserting against the generatedtokens.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)' }andfillMuted: { value: 'rgba(0,0,0,0.12)' }totheme.extend.tokens.colors.paintinapps/web/panda.config.ts, besideshadowandglow, with a comment saying why they are shared rather than per-theme (research V2). Runpnpm --filter @gw2priory/web prepareso codegen regeneratestokens.d.ts. - [ ] Confirm the test has teeth — rename
filltofillXin 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.tswith two tests. First,"R1/P1 #1-#2: every theme re-points the six semantic roles"— for each of the five themes, readpaintedSurfaceRecipe.variants.theme[theme].rootand assert each of the six properties is present and points at that theme's token (or the shared fill):
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-legendaryor--colors-surface:
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)inpaintedSurfaceRecipe.tswith 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"assertingpaintedSurfaceRecipe.base.content.colorisvar(--paint-text); then set it. The comment must say why the re-pointing alone is insufficient —colorinherits 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"— assertPRESETS.chip.tear.scale === 0(a 34px popup is shorter than a 13px displacement) andPRESETS.chip.blooms.sizeis smaller than every panel preset's. Watch it fail on the missing key. - [ ] GREEN: add
chiptoPRESETSwith the values discovery settled (research.mdF4, 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 theIntensityunion. 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.scaleto 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:
expect(document.querySelector('.painted-surface__root')).toBeInTheDocument();- [ ] GREEN: in
Dialog.tsx, wrap the popup's internals inPaintedSurfacewith a module-levelconst THEME = 'Bark'andclassName={classes.paint}. IndialogRecipe.ts, stripbg,boxShadow,borderRadiusandpfrompopup(leaving position, size and layout) and add apaintslot carrying the padding. R7: no radius is reintroduced — the torn edge is the point. - [ ] Run the whole file: the three pre-existing tests (
P3 #1focus,P3 #2Escape,P3 #3accessible name) must pass unmodified. If one needs editing, stop — the public behaviour changed, which SC3 forbids. - [ ] Confirm the new test has teeth — remove the
PaintedSurfacewrapper, 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 assertdocument.querySelector('.painted-surface__root')is present. - [ ] GREEN: in
Tooltip.tsx, wrap the popup content inPaintedSurfacewithTHEMEandintensity="chip". IntooltipRecipe.ts, keepmaxWonpopup, movepx/py/fontSizeto a newpaintslot, and dropbg,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 insideMemoryRouteras the existing test does, and assert the painted root is in the container (no portal here). - [ ] GREEN: wrap the prompt's block in
PaintedSurfacewithTHEMEand the panel preset — nointensityoverride (research F4: the human chose panel over chip). AddpromptPaintStylestoapps/web/src/shared/ui/styles.tsfor the padding and pass it asclassName.promptStylesandpromptLinkStylesare not touched: they nametext.mutedandprimary, and the scope re-tints them (P2 #2). - [ ] Verify P2 #2 by reading the diff:
ConnectAccountPrompt.tsxnames 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.tsxwith aMemoryRouterdecorator (research F2 — theLinkthrows 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 storybookand confirm the story renders painted. Runpnpm 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.mdwith the theme-scope rule: which roles are re-pointed and which are never; that fills are shared and why; thatcolorinheritance 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.mdrecording research F1: Storybook has no env configuration, so any story reachingsrc/apithrowsInvalid VITE_APP_ENV: undefined; citeapiBase.ts:16andvitest.config.ts:25, and name the fix (viteFinaldefine 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 buildis the React Compiler gate — a destructured-parameter default or a hand-writtenuseMemofails there and nowhere else. - [ ] Human review in Storybook: all three containers painted, ink legible,
raritycolours unchanged. This is the SC1 check no test can make. - [ ] Confirm no spike file survives (
git log --statover the branch) and thatpaint/styles.tsis 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.mdandscratchpad/025-theme-scope.patch; if T2–T4 reproduce them exactly, the re-landed implementation is the reviewed one, arrived at through tests.