Skip to content

feat(motion-kit): render the Studio and capture site pages as Claude Motion films - #82

Merged
jm-rivera merged 7 commits into
devfrom
feat/motion-kit
Oct 29, 2026
Merged

jm-rivera merged 7 commits into
devfrom
feat/motion-kit

Conversation

@jm-rivera

@jm-rivera jm-rivera commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

motion-kit/ turns the site into Claude Motion films that show the real interface. pnpm motion-kit <spec> renders a Studio workspace from the app's own components into one static HTML fragment and scoped CSS, with charts drawn by Chart.js into SVG. pnpm motion-kit capture <page> drives Chrome through the running site and snapshots each state of any other route. A cue sheet beside each spec times the film, and runtime/dfo-motion.js plays it in Motion as a pure function of time, using the app's own keyframes, prompt growth and transcript pinning.

Each run writes the Motion store documents and a preview.html that plays the film from disk with a scrubber; for a capture it plays every state in turn, as the film's scenes do. The generator refuses what a film cannot play: a missing hook, state or keyframe, a cue field of the wrong type, a store document over 200 KiB or a module name a film's store does not take, an outside URL, or a media query it cannot decide. motion-kit/README.md documents the specs, cue kinds and outputs; motion-kit/demos/kenya-debt-health.json is the worked example, and three page specs capture data search, a search refined from gross to net disbursements (uganda-oda-search.json, with its cue sheet), and the IDS source page.

The dfo-motion-demo skill, which walks a demo from a data question to a published film with the figures taken from the DFO MCP server, ships in the dfo-dev plugin of the ONE data marketplace (https://github.com/ONEcampaign/one-data-claude-marketplace/pull/26).

The one app change moves ChartCard's dataset builders into canvas/chart/chartDatasets.ts unchanged, so the kit draws the same datasets. Nothing under motion-kit/ is imported by the app, and playwright-core is a dev dependency.

Checks: node --test motion-kit/test/*.test.ts passes 18 tests, including a film scrubbed forwards and backwards with every frame compared, and a fixture page captured and played through its preview. pnpm typecheck and pnpm lint pass. The database tests in pnpm test need a local Postgres and were not run.

…sets.ts

barDatasets, lineDatasets, barCap and legendShown move out of ChartCard.tsx
unchanged, so code outside the card can build the same Chart.js datasets.
motion-kit draws a chart card's plot from them.
…Motion films

pnpm motion-kit <spec> renders a Studio workspace from the app's own
components (Header, WorkspaceToolbar, ChatPanel with PlanCard and
ApprovedPlanPill, the card components) as one static HTML fragment and
its scoped CSS, with each chart drawn by Chart.js into SVG. pnpm
motion-kit capture <page> drives Chrome through the running site and
snapshots each state of any route as inert HTML and CSS.

The manifest records the app's own keyframes and transitions, read in
Chrome. A cue sheet beside the spec times the film: typing that grows the
prompt as PromptForm does, streamed replies, the plan approved into its
pill, cards arriving with their own keyframes, series drawing, and a
camera that centres on named hooks. runtime/dfo-motion.js plays it as a
pure function of time, restoring each element's own style every frame.

Every run writes the Motion store documents and a preview.html that plays
the film from disk. The generator refuses a cue, spec or module a film
cannot play: a missing hook, state or keyframe, a field of the wrong
type, a store document over 200 KiB, an outside URL, a media query it
cannot decide. motion-kit/demos/kenya-debt-health.json is the worked
example, and motion-kit/README.md documents the specs, cues and outputs.
The skill takes a demo from a data question to a published Motion film:
the figures from the DFO MCP server (health aid as CRS 120 plus 130), a
Studio or page-capture spec, the cue sheet, generating and previewing,
and loading the modules into a film. .gitignore now keeps
.claude/skills/ while ignoring the rest of .claude/.
The runtime measured a hook from layout offsets, which leave out the
translate a capture uses for its scroll, so a camera key on a hook in a
scrolled state aimed hundreds of px off. box() now measures the element
as rendered inside the frame and divides out the frame's own scale, so
every transform inside the frame counts, the transcript pin included.

A capture's preview.html offers "film" when its camera keys name states:
it mounts each state from its first key's time, as the film's scenes do,
so a whole capture film plays and scrubs before it reaches Motion.

motion-kit/pages/uganda-oda-search.json captures a data search on the
site, "ODA to Uganda since 2015": the question typed, its match, the
answer, the measure menu, net disbursements chosen, and the results
table, with a cue sheet that frames each state.
A stylesheet over the module limit split into dfo-ui.css~1, ~2 and so
on, but a Motion film's store names a module with letters, digits, _, .
and - only. The player could not load the pieces, so a large page
capture played without its stylesheet. The pieces are now dfo-ui-1.css,
dfo-ui-2.css and so on, and writeFilm refuses a module name the store
does not take or modules that hold more than 2 MiB together.
The skill now ships in the dfo-dev plugin of
ONEcampaign/one-data-claude-marketplace, so it leaves .claude/skills/
here and .gitignore ignores all of .claude/ again.
… and fonts in the preview

A tab or state cue wrote both its states at every frame, so a later cue
undid an earlier one: Chat to Activity at 5 s and back at 10 s showed
Activity at 0 s. The runtime now restores every attribute a cue writes
(style, class or a state's own) at the start of each frame, and a tab or
state cue writes only from its own time.

A capture translated an ancestor of a fixed element by the scroll, which
made that ancestor the fixed element's containing block: a header fixed
at 20 px showed thousands of px down. The scroll now moves the children
of an element that holds a fixed one, never the element itself, so fixed
elements stay anchored to the frame.

preview.html embeds Outfit and DM Mono from @fontsource and mounts the
film once they load, since the stylesheet carries no @font-face and a
machine without them wrapped text, and measured the camera, in fallback
fonts.
@jm-rivera
jm-rivera marked this pull request as ready for review October 29, 2026 16:16
@jm-rivera
jm-rivera merged commit 95731cd into dev Oct 29, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant