Skip to content

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>/ with index.tsx, schema.ts, preset.ts, index.test.ts. Registered in src/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 via lockupPrefixSchema.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; renderers src/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 wide pulse timing), 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=0 keeps the reference overlay, --flat zeroes rotations). betta-component-catalogue is a code defined Studio composition (src/fixtures/component-catalogue.ts, one scene per preset). pnpm build-component-library generates the on disk template templates/betta-component-library/ from the same scenes for the Template Builder; templates/betta-component-library/document.test.ts fails 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-templates all green in the compositions package.
  • Known gaps: a layer level shared wipe on a tilted lockup trims corners (EffectEntry.boundsOverhang not 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

  1. /Users/matthew.a/Development/monorepo/.designs/component-catalogue-design-prompt.md in full. Sections "Component Control", "Component Parts" and "Component Library" are the scope of this handoff; "Component Creation" and "Component Manifest" are done.
  2. /Users/matthew.a/Development/monorepo/.designs/component-library-part-system.md in full. It defines what a part is and the additive parts schema shape.
  3. The repo gates and preferences are already loaded from ~/.claude/CLAUDE.md, the worktree CLAUDE.md and CLAUDE.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-v1 on feature/neo-1984-component-library-v1; every commit lands on draft PR #1333. Commit at each phase boundary with imperative subjects in the house style (see git 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 parts schema 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 renderStill path render-component uses, committed to the repo, one per COMPONENT_OPTIONS entry that has a preset. The Library page shows images; nothing renders in the browser.
  • Capability: add view:component-library to CAPABILITIES and CAPABILITY_LABELS in packages/shared/src/capabilities.ts, next to view:product-library, and to the same role bundle. No manage: 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:

  1. 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).
  2. Prefer the smaller change that keeps every existing template, manifest and binding valid and unchanged. parts is additive; old documents must validate and render exactly as before.
  3. New schema primitives follow the rs.* namespacing convention. No ad hoc top level names.
  4. Record every non obvious decision, with the alternative you rejected and why, in /Users/matthew.a/Development/monorepo/.designs/component-parts-decisions.md as you go (that directory is gitignored by design; never commit it). The final PR body carries a condensed version.
  5. 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

  1. In the existing worktree, confirm git status is clean, the branch is feature/neo-1984-component-library-v1 at or after 4cc79c0b1, and the full compositions verification is green before touching anything.
  2. 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), into out/baseline/. Use --box 200,150,670,519 for betta-cashback-price-stack and 200,150,420,400 for betta-price-save-stack (fixed box components). Keep these files for the rest of the run (out/ is gitignored).
  3. 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.

  1. Add parts?: Record<partId, PartSchema> to componentLayerSchema (src/zod.ts, around line 1052) exactly as the part system doc shapes it: each part carries a layer type discriminant (boxText, pointText, image, shape, ...) plus optional props, animations, effects, transitions, reusing the existing schemas for the last three (commonLayerFields is the precedent). props is the component's own part props, opaque at this level the same way component props is today.
  2. Make binding paths through parts first class: ['parts', id, 'props', ...] must resolve through binding-paths.ts, resolve.ts (applyBindings, applyLayerBindings) and generate-manifest.ts exactly like ['props', ...] does. generate-manifest.ts resolves component props.* bindings against the component schema via componentSchema(layerName); extend that so a parts.<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).
  3. Validation: validateTemplate must reject a part id a component does not declare and a part type that does not match the declared one, and must accept every existing template unchanged.
  4. 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.

  1. Design the part hosting layer once, then apply it 22 times. Requirements:
  2. 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). EffectEntry gains whatever it needs for this; keep it Remotion free where prop-schemas.ts needs it, since the web app imports that module.
  3. preset.ts becomes 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.
  4. The per part motion that is authored inside props today (wipeIn, fadeIn, pulse, hidden, offset) moves onto the part's transitions, animations and effects where the existing schema can express it (a wipe or slide entrance is a transition; the pulse is a looping return style animation; hidden is 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.
  5. Components render each part through one shared host (for example a PartHost around the existing LockupBlock/LockupImage) that reads the part's resolved data from layer.parts[id] and applies its transitions, animations and effects. The interpreter's component layer path (src/interpreter/layers/ComponentLayer.tsx and its resolution) provides that data. Existing seven Betta components do not use the host and keep working.
  6. 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 against out/baseline/ and report the pixel deltas.
  7. Regenerate the derived artefacts (build-component-library, build-templates, regen-assets, build-registry) and update render-component, the Studio fixture and the disk template for the new preset shape.
  8. Gate: full compositions verification green, pixel baseline zero delta for all 22 at all three frames (or documented exceptions), pnpm validate-templates green 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.

  1. Property panel: AddedLayerEditor renders 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.
  2. Manifest bindings: a component layer placed in RetailStudio, and every component layer in the generated betta-component-library template, carries bindings for its parts' fields so the existing field flow (applyBindings, generateManifest) drives them the same way pointText fields are driven today. Extend scripts/build-component-library.ts to emit these bindings from the part declarations.
  3. 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-schemas if presets.ts needs one.
  4. 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-component on at least the four banners.
  5. Tests: vitest in apps/web for 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 typecheck and pnpm test green in apps/web, plus root pnpm lint.

Phase 4: Component Library screen (apps/web, compositions, shared)

  1. Capability as decided in section 3.
  2. Thumbnails: scripts/build-component-thumbnails.ts in the compositions package renders every COMPONENT_OPTIONS entry with a preset through the isolated harness path at the default frame, 16:9, on a neutral background, at a fixed output size, and writes apps/web/public/component-library/<name>.png. Register it in package.json and 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).
  3. Page: apps/web/src/pages/ComponentLibraryPage.tsx at /component-library, modelled on ProductLibraryPage.tsx and its route in App.tsx (line 241 area), gated by view: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.
  4. Tests for the page states and the capability gate.

Phase 5: integration, PR, self review

  1. Full verification everywhere touched: compositions (typecheck, lint, test, validate-templates, check-templates, check-registry), schema package tests, apps/web (typecheck, test), packages/shared tests, root pnpm lint.
  2. Render the betta-component-catalogue composition at three frames via pnpm render-stills betta-component-catalogue <fraction> and eyeball a sample of scenes against out/baseline/.
  3. Commit and push. Do not open a new PR: update the body of draft PR #1333 with gh pr edit 1333 --body-file so 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.
  4. 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 1333 until CI is green or a failure is not yours to fix; if CI fails on your change, fix it.
  5. 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 add of .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.