Handoff: make the Betta component catalogue usable (Parts, Control, Library)¶
You are the conductor for an unattended, end to end build. Matthew is not available to answer questions. Read this document in full, then read the two design docs it points at, then execute every phase below to completion: code, tests, verification, commits, push, one draft PR. Where something is undecided, decide it yourself using the rules in "Decision policy", record the decision, and keep going. Do not stop to ask. The only acceptable stopping points are "done, here is the PR" or a hard blocker you have genuinely exhausted (see "When to stop").
1. What already exists (wave one, PR #1333)¶
Branch feature/neo-1984-component-library-v1, commit 4cc79c0b1, draft PR
https://github.com/Neosframe/monorepo/pull/1333. Everything lives in
packages/retail-studio-compositions unless stated.
- 22 standalone Betta components under
src/components/betta/betta-<slug>/withindex.tsx,schema.ts,preset.ts,index.test.ts. Registered insrc/effects/registry.ts,src/effects/catalog.ts(COMPONENT_OPTIONS, sorted by label),src/components/prop-schemas.ts(COMPONENT_PROP_SCHEMAS,COMPONENT_AUTO_SIZES,KNOWN_COMPONENT_NAMES),src/components/presets.ts(COMPONENT_PRESETS, Remotion free). Every part boundary is marked with a// PART: <name> (<layer type>). ...comment directly above its JSX. - Shared lockup toolkit:
src/components/lockup-schema.ts(text, panel, offer panel with a switchable "UP TO" prefix vialockupPrefixSchema.enabled, image, amount, pulse timing, fade/slide, wipe, offset, per part motion),lockup-motion.ts,lockup-amount.ts,preset-ratios.ts,lockup-test-kit.ts; rendererssrc/miscellaneous/LockupBlock.tsx,LockupRow.tsx,LockupImage.tsx,LockupRoot.tsx,shared-pulse-origin.ts(all parts of a lockup pulse about the lockup centre; the root is found from the DOM in a layout effect because a ref is not attached yet at that point). - Per part motion today is authored INSIDE each component's props:
wipeIn,fadeIn,pulse: true(opt in to the component widepulsetiming),hidden(visibility hidden, no reflow),offset. Components read RAW resolved props, so schema.default()never applies at render; defaults are restated at read sites. - Tooling:
pnpm render-component <name> [--ratio] [--frame] [--box x,y,w,h] [--reference png] [--ref-box] [--bg] [--wipe] [--flat]renders one component in isolation with real brand fonts (HIDE_REF=0keeps the reference overlay,--flatzeroes rotations).betta-component-catalogueis a code defined Studio composition (src/fixtures/component-catalogue.ts, one scene per preset).pnpm build-component-librarygenerates the on disk templatetemplates/betta-component-library/from the same scenes for the Template Builder;templates/betta-component-library/document.test.tsfails on drift. After any preset change run:pnpm build-component-library && pnpm build-templates && pnpm regen-assets && pnpm build-registry. - Verification baseline:
pnpm typecheck,pnpm lint,pnpm test(949 tests),pnpm validate-templatesall green in the compositions package. - Known gaps: a layer level shared
wipeon a tilted lockup trims corners (EffectEntry.boundsOverhangnot implemented). Disk templates get no brand fonts in Studio. Rendered text is about 2% wider than Photoshop's (optical kerning); panels are sized to the real render ink, do not "fix" this. - Gotcha: the Template Builder autosaves WIP under
packages/retail-studio-compositions/.wip-tmp/and restores it over the disk template; a stale WIP looks like a stale component. Discard the WIP, do not debug the component.
2. Read these first¶
/Users/matthew.a/Development/monorepo/.designs/component-catalogue-design-prompt.mdin full. Sections "Component Control", "Component Parts" and "Component Library" are the scope of this handoff; "Component Creation" and "Component Manifest" are done./Users/matthew.a/Development/monorepo/.designs/component-library-part-system.mdin full. It defines what a part is and the additivepartsschema shape.- The repo gates and preferences are already loaded from
~/.claude/CLAUDE.md, the worktreeCLAUDE.mdandCLAUDE.local.md. They apply throughout.
3. Decisions already made (do not reopen)¶
- Delivery: the same branch and the same PR as wave one. Work in the existing
Orca worktree
/Users/matthew.a/Development/.wt/monorepo/neo-1984-component-library-v1onfeature/neo-1984-component-library-v1; every commit lands on draft PR #1333. Commit at each phase boundary with imperative subjects in the house style (seegit log), push after each commit. Never merge. Never add Claude or Anthropic attribution anywhere. Do not create a new worktree, branch or PR. Post Orca checkpoints (orca worktree set --worktree active --comment "...") at each phase. - Parts: adopt the
partsschema from the part system doc. Migrate all 22 wave one components from flat props to parts. Leave the seven pre existing Betta components (betta-cashback-callout,betta-cashback-blocks,betta-discount-box,betta-partner-bar,betta-payment-opt-bar,betta-price-box,betta-product-strip) untouched. - RetailStudio add layer: yes. The RetailStudio layer panel gains an add component menu, filtered to the campaign's brand through the existing brand matrix, placing a new component layer from its preset. The Component Library page is browse and preview only; it is not a drag source.
- Thumbnails: pre rendered PNGs generated by a compositions script through the
same bundle plus
renderStillpathrender-componentuses, committed to the repo, one perCOMPONENT_OPTIONSentry that has a preset. The Library page shows images; nothing renders in the browser. - Capability: add
view:component-librarytoCAPABILITIESandCAPABILITY_LABELSinpackages/shared/src/capabilities.ts, next toview:product-library, and to the same role bundle. Nomanage:counterpart. - Pins and grouping (the open question at the end of the part system doc) are out of scope. Reflow between parts stays each component's own layout problem.
- Visual invariance: migrating a component to parts must not change its rendered output. This is enforced with a pixel baseline (Phase 0).
4. Decision policy¶
When the brief is silent or two readings differ:
- Prefer the reading that reuses an existing mechanism over inventing one (the part system doc's whole argument is that a part is the existing layer type system applied one level deeper).
- Prefer the smaller change that keeps every existing template, manifest and
binding valid and unchanged.
partsis additive; old documents must validate and render exactly as before. - New schema primitives follow the
rs.*namespacing convention. No ad hoc top level names. - Record every non obvious decision, with the alternative you rejected and why,
in
/Users/matthew.a/Development/monorepo/.designs/component-parts-decisions.mdas you go (that directory is gitignored by design; never commit it). The final PR body carries a condensed version. - If you find the brief contradicts the code, the code wins for facts about the current state; the brief wins for intent.
5. Phases¶
Run phases in order. Inside a phase, fan out to subagents where the lanes are independent, then integrate and run the full verification set yourself before committing. Give every subagent: the exact files it owns, the exact tests it must add or keep green, the invariants from this document, and an instruction to report back with a diff summary rather than prose. Never let two agents edit the same file concurrently. Prefer the standard coding models for broad exploratory lanes; the narrow, well specified lanes (a schema addition with a given shape, a test file, a page that mirrors an existing one) are fine on a smaller model.
Phase 0: baseline¶
- In the existing worktree, confirm
git statusis clean, the branch isfeature/neo-1984-component-library-v1at or after4cc79c0b1, and the full compositions verification is green before touching anything. - Render a pixel baseline for every component with a preset:
pnpm render-component <name> --ratio 16:9 --box 200,150,1400,800 --bg "#cfd8e3"at the default frame, plus--frame 6(mid entrance) and--frame 50(pulse peak), intoout/baseline/. Use--box 200,150,670,519forbetta-cashback-price-stackand200,150,420,400forbetta-price-save-stack(fixed box components). Keep these files for the rest of the run (out/is gitignored). - Write a small compare script (scratchpad, not committed) that reports per image the count of pixels differing from the baseline. The migration gate is: zero differing pixels, or a difference you can name and justify in the decisions log.
Phase 1: the parts schema (retail-studio-schema)¶
Package packages/retail-studio-schema.
- Add
parts?: Record<partId, PartSchema>tocomponentLayerSchema(src/zod.ts, around line 1052) exactly as the part system doc shapes it: each part carries a layertypediscriminant (boxText,pointText,image,shape, ...) plus optionalprops,animations,effects,transitions, reusing the existing schemas for the last three (commonLayerFieldsis the precedent).propsis the component's own part props, opaque at this level the same way componentpropsis today. - Make binding paths through parts first class:
['parts', id, 'props', ...]must resolve throughbinding-paths.ts,resolve.ts(applyBindings,applyLayerBindings) andgenerate-manifest.tsexactly like['props', ...]does.generate-manifest.tsresolves componentprops.*bindings against the component schema viacomponentSchema(layerName); extend that so aparts.<id>.props.*binding resolves against that part's schema. Decide and document how a component declares its part schemas (see Phase 2, the two must agree). - Validation:
validateTemplatemust reject a part id a component does not declare and a parttypethat does not match the declared one, and must accept every existing template unchanged. - Tests in the schema package for all of the above. Every existing schema test stays green.
Phase 2: part hosting in the interpreter and the 22 components¶
Package packages/retail-studio-compositions.
- Design the part hosting layer once, then apply it 22 times. Requirements:
- A component declares its parts in code, next to its schema: id, layer type,
the part's own props schema, and which of its fields are standard versus
advanced (this drives the property panel in Phase 3).
EffectEntrygains whatever it needs for this; keep it Remotion free whereprop-schemas.tsneeds it, since the web app imports that module. preset.tsbecomes part scoped: the preset's props are keyed by part id where the field belongs to a part, with component wide settings (spacing, typography base, pulse timing) staying at the top level.- The per part motion that is authored inside props today (
wipeIn,fadeIn,pulse,hidden,offset) moves onto the part'stransitions,animationsandeffectswhere the existing schema can express it (a wipe or slide entrance is a transition; the pulse is a looping return style animation;hiddenis a part level visibility). Where the existing schema genuinely cannot express something, keep it as a part prop and write down why. The shared pulse origin stays inside component render code, as the part system doc says. - Components render each part through one shared host (for example a
PartHostaround the existingLockupBlock/LockupImage) that reads the part's resolved data fromlayer.parts[id]and applies its transitions, animations and effects. The interpreter's component layer path (src/interpreter/layers/ComponentLayer.tsxand its resolution) provides that data. Existing seven Betta components do not use the host and keep working. - Fan out the 22 migrations across agents in groups that share a shape
(banners; bonus cashback family; price modules; image bearing; flag and cover;
plain text and text box). Each agent: migrate schema, preset, index, tests for
its group; keep the
// PART:comments truthful; run the group's tests and the pixel compare againstout/baseline/and report the pixel deltas. - Regenerate the derived artefacts (
build-component-library,build-templates,regen-assets,build-registry) and updaterender-component, the Studio fixture and the disk template for the new preset shape. - Gate: full compositions verification green, pixel baseline zero delta for all
22 at all three frames (or documented exceptions),
pnpm validate-templatesgreen for all templates.
Phase 3: Component Control in RetailStudio (apps/web)¶
Current state, verified: apps/web/src/components/RetailStudio/Scenes/AddedLayerEditor.tsx
renders nothing for a component layer and AddedLayerEditor.test.tsx asserts
it; RetailStudioLayerPanel.tsx has no add layer UI. TemplateBuilder already adds
component layers via apps/web/src/components/TemplateBuilder/AddLayerMenu.tsx
driven by COMPONENT_OPTIONS, and its zodForm/LayerInterfaceEditor.tsx edits
component props from componentPropSchema. CanvasComponentItemLayer.tsx and
CanvasComponentItemOverlay.tsx already exist for component items on the canvas.
- Property panel:
AddedLayerEditorrenders a component layer as inline, collapsible sections per part, in declaration order, each section reusing the existing per layer type field rendering (AddedTextLayerEditor,AddedImageLayerEditor, shape and so on) pointed at the part's data instead of the layer's. Standard fields show by default; advanced fields sit in an opt in section, driven by the component author's declaration from Phase 2. Per part the client can change everything in the Component Control checklist: text value, text colour, background colour, rotation, effects, animations and transitions on or off, and anything else simple the schema exposes. Position is never exposed per part. - Manifest bindings: a component layer placed in RetailStudio, and every
component layer in the generated
betta-component-librarytemplate, carries bindings for its parts' fields so the existing field flow (applyBindings,generateManifest) drives them the same waypointTextfields are driven today. Extendscripts/build-component-library.tsto emit these bindings from the part declarations. - Add layer: the RetailStudio layer panel gains an add component menu listing the
catalogue entries available to the campaign's brand (reuse
packages/retail-studio-compositions/src/brand-matrix.ts, which already covers Betta; do not add a second matrix), placing the component at a sensible default position with its preset as the initial props and parts. Presets must be importable by the web app without pulling in Remotion; add a package export next to./prop-schemasifpresets.tsneeds one. - Responsiveness: with a long text value in any text part the whole component
reflows without overlap (the brief's "This is the last Friday of the month"
case). Add a test per component family that renders a long value through the
test kit and asserts nothing is clipped or overlapping in the markup where
that is checkable, and verify visually with
render-componenton at least the four banners. - Tests: vitest in
apps/webfor the editor sections, the add menu, the brand filtering and the binding flow; the existing "renders nothing for component" assertion is replaced, not deleted silently.pnpm typecheckandpnpm testgreen inapps/web, plus rootpnpm lint.
Phase 4: Component Library screen (apps/web, compositions, shared)¶
- Capability as decided in section 3.
- Thumbnails:
scripts/build-component-thumbnails.tsin the compositions package renders everyCOMPONENT_OPTIONSentry with a preset through the isolated harness path at the default frame, 16:9, on a neutral background, at a fixed output size, and writesapps/web/public/component-library/<name>.png. Register it inpackage.jsonand the compositions README table. Add a test that every catalogue entry with a preset has a thumbnail file (existence, not bytes, since PNG encoding is not byte stable across machines). - Page:
apps/web/src/pages/ComponentLibraryPage.tsxat/component-library, modelled onProductLibraryPage.tsxand its route inApp.tsx(line 241 area), gated byview:component-library, with a nav entry wherever the product library has one. A mosaic grid of thumbnails with the component label, filtered to the brands the organisation has access to, click to open a larger preview with the label and the component's part list. No add, edit or delete anywhere. Follow the design gate: theme tokens only, Radix primitives, loading, empty and error states, TanStack Query for any server data. - Tests for the page states and the capability gate.
Phase 5: integration, PR, self review¶
- Full verification everywhere touched: compositions (
typecheck,lint,test,validate-templates,check-templates,check-registry), schema package tests,apps/web(typecheck,test),packages/sharedtests, rootpnpm lint. - Render the
betta-component-cataloguecomposition at three frames viapnpm render-stills betta-component-catalogue <fraction>and eyeball a sample of scenes againstout/baseline/. - Commit and push. Do not open a new PR: update the body of draft PR #1333 with
gh pr edit 1333 --body-fileso it describes both waves, keeping the existing wave one sections and adding what was built per phase here, the decisions log condensed, the verification run and the known gaps carried forward. No attribution. - Run a self review pass over the PR diff against the engineering gates
(coding standards, change discipline, frontend patterns, tanstack query,
security). Fix what you find, commit, push. Check
gh pr checks 1333until CI is green or a failure is not yours to fix; if CI fails on your change, fix it. - Update the Orca worktree comment with the final state (it is already in review).
6. Invariants that hold across every phase¶
- Every existing template, manifest and binding validates and renders unchanged.
- No existing component's output changes; the 22 migrated components match the pixel baseline.
- No
git addof.designs/,out/,.wip-tmp/or anything gitignored. - No dev server left running when a step finishes (
pnpm studio, vite). Use one shot renders and tests for verification. - User facing copy has no hyphens or em dashes.
- Comments only where the why is non obvious; keep the
// PART:convention.
7. Compaction safety¶
This run is long. Every time you finish a phase, and whenever context feels
large, write a checkpoint to ~/.claude/handoffs/component-parts-<timestamp>.md
with: phase reached, exact files changed, decisions taken, the next concrete step,
and the commands to verify the current state. If you resume from a compacted
context, re read this document, the decisions log and the latest checkpoint before
touching code. The branch, the commits and the PR are the source of truth for what
is done.
8. When to stop¶
Stop only when every phase is pushed to PR #1333, its body is updated, and CI is green or its failures are provably unrelated. If you hit a blocker you cannot resolve after a genuine attempt (a package that will not build, an external service that is down, a contradiction in the brief that any choice would lock in wrongly), write the checkpoint, commit and push what is green, state the blocker at the top of the PR body, and end with a report that names the blocker, what you tried, and the one decision Matthew has to make.
9. Final report¶
End with a short message: the PR link (#1333), one line per phase on what landed, the verification results as a table, the decisions Matthew should look at first, and any gap carried forward. No narration of the process.