* feat: show controls in the demo and tidy the component panels
The demo gains a Controls page: a switch, checkbox, slider, tabs, text
field, and a button with interaction states, written as Reka-named design
JSX, and a settings card of their instances to try in preview.
Go to main component and Detach instance move into the instance's panel
header as icon buttons. The Behaviour section's "Still needed" line is a
status rather than a button that only moved focus, and the rows it names
are marked instead.
The DOM projection writes flex-start for auto layout's start alignment:
CSS stretches children by default, so a hugging button in a column filled
its card in preview and HTML export. The paint page snapshot catches up
with the frame names #930 draws.
* perf: open the demo as a prebuilt .fig instead of generating it
/demo built every page in the browser and laid them all out in one task,
which froze the page for about six seconds. The demo's content moves to
tools/generate/demo, which builds public/demo.fig headlessly, before Vite
serves or bundles the app and only when its inputs changed; /demo opens
that file like any other, off the main thread and behind the loader. The
document is now lint-clean: small captions are 12 px, and the badge and
disabled button text meet contrast.
Opening the demo from .fig showed three bugs in saving and reading files,
fixed here:
- A layer's blend mode was never written, so Multiply and Screen reopened
as pass-through.
- Underlines drawn from saved glyphs ran to the text box edge; glyphs now
keep their advance, read from and written to .fig, and the underline
ends where the text does.
- A frame laid out on its own because of a saved hug size ignored the
width its parent stretched it to, so centred content moved to the start.
* feat(design-jsx): write Reka elements in TSX
The Reka element names were only part of the JSX string grammar, so code
that builds trees in TypeScript had to assemble strings by hand, unchecked.
@open-pencil/design-jsx now exports each Reka namespace as typed element
functions (Switch.Root, Slider.Thumb, Tabs.Trigger, …), derived from the
same table the string renderer reads, with props typed as the behaviour
props a root binds. The demo's controls are TSX built with them.
* fix(canvas): draw a component set's editing border at the live zoom
A component set without a stroke got its dashed border in the scene, sized
by dividing by the zoom. The scene is cached as retained pictures and
scaled while navigating, so the border was recorded at one zoom and grew
into thick dashes when zooming in, until the page was redrawn. It is
editor chrome, so it now comes from the overlay pass, which draws every
frame at the live zoom, as slot outlines do. The label cache tracks every
component set on the page for it, and image export no longer includes the
border.
* test: fit the P3 paint page to the canvas size it is captured at
The P3 test zoomed to the paint page while the wide-gamut notice still
took space, then dismissed it, so the first capture fit a shorter canvas
than the second and the two only matched by timing. focusPaintEffects
waits for the canvas to take a window resize, the test focuses again after
the notice goes, and the snapshot shows the demo as it now opens.
* refactor(canvas): fill sections and sets through one helper
Without its own fallback border, a component set fills and strokes its rounded bounds the way a section does; fillRoundedBounds fills them for both.
* test: check the demo controls on their own page and type pixel reads
The demo test built and round-tripped the whole four-page document to
check the controls, which took six seconds on CI and timed out; it now
builds the Controls page alone through the same measured-text setup the
build uses. The underline test reads CanvasKit pixels as any number array,
since readPixels may return floats.
* fix(canvas): measure long text extents without spreading glyphs
Spreading every glyph into Math.min/max overflows the argument limit on long texts.
* fix(demo): keep a tab touched while the demo loads and report a failed load
A file opened or a layer drawn during the fetch keeps the tab; a failed fetch shows the open-file error instead of an unhandled rejection.
* test: check a component set border stays one pixel wide when zoomed
* ci: build and check packages in parallel
The Vue SDK's declarations used the tsc resolver, which took 21 of the 32 seconds a local package build takes; tsdown's default oxc resolver writes byte-identical output in 4 seconds. Packages now build level by level, each level's packages together, with their output printed whole. Package checks run npm and Bun packing side by side and ATTW on every core instead of two.
* ci: skip the merge queue's suites for a tree its PR already passed
The merge queue reran every check even when master had not moved, so the queued commit had exactly the tree the pull request's CI had just passed. A passing PR run now records that tree as a commit status on the PR head, and the queue's classification compares its own tree with it: a match runs only the always-on checks, anything else the full suites. Fork PRs cannot write the status and keep the full run.
* ci: accept a verified tree only from its pull request's passing CI run
Any writer can post a commit status, and another pull request's CI could post one on this head, so a status alone could skip the queue's suites. The record now links the run that wrote it, and the queue accepts it only when GitHub shows Actions created it and the run is this repository's CI workflow on pull_request, passed, and ran on this exact head. Recording no longer fails the gate when the status cannot be written. Parallel packs and builds now all settle before a failure is reported, so none writes into a directory that is being removed or rebuilt.
* refactor(ci): group the verified-tree lookup and recorder in one folder
* chore: prefer es-toolkit helpers and lint the mechanical cases
AGENTS.md now names the es-toolkit helpers to reach for instead of hand-written equivalents, and the exceptions: a single clear native call or a measured hot path. The new open-pencil/prefer-es-toolkit rule rejects filter(Boolean) and Set round trips on arrays, the two cases that need no type information, and the existing 45 sites use compact and uniq. tools/ci/policy runs before dependencies are installed, so the rule is off there.
* refactor: deduplicate diagnostic categories with uniq
* fix: keep es-toolkit out of serialized Playwright callbacks
The codemod rewrote a filter(Boolean) inside a page.evaluate callback, which Playwright runs in the page where the compact import does not exist. The spec filters there again, and the rule now skips callbacks passed to evaluate, $eval, $$eval, evaluateHandle, addInitScript and waitForFunction, and filter calls on iterators from values, keys, entries and matchAll, which compact cannot take.
* test: write the prefer-es-toolkit cases like the other rule tests
Short standalone snippets, as in the base64 and JSON rule tests, instead of a declaration prefix on every case and inline object types.
* ci: run CI and the PR title check for the merge queue
A merge queue tests each queued change on top of the ones ahead of it and
waits for the required checks, CI result and PR title, on that merge
group. Both workflows now run on merge_group, reading the base and head
from either event. The title was checked on the pull request, so the
queue run reports success without a title to read.
* docs: explain stacked pull requests and the merge queue
Agents had to be told what a stacked pull request is each time.
CONTRIBUTING.md now covers stacks with gh stack, keeping them linear,
and landing them through the merge queue; AGENTS.md links to it, and
tools/AGENTS.md records that required checks must run on merge_group.
* docs: shorten the stacked pull request and merge queue guidance
The AGENTS.md line loads into every agent context, so keep it to the
rules; CONTRIBUTING.md keeps the commands.
Tools live under tools/<role>/<domain> (checks, generate, release, ci, dev), every tool is a workspace named @open-pencil/<domain>-tools, a shared tools/tsconfig.json backs the new check:tools gate that fixed 55 latent type errors, test:tools runs through bun --filter, the placement check is its own checks/test-homes package, and every tool resolves the repository through resolveWorkspaceRoot. Bun, Node, and mdast types live in a tools-root workspace so they never reach the app program.
Scene Graph unit tests move to packages/scene-graph/tests with a package-local assert helper and test type-checking; five Core- and fig-owned tests move to their owners' engine homes. check:test-homes rejects any new test under tests/engine against a reviewed baseline so the migration debt only shrinks.
Root AGENTS.md becomes a map plus cross-cutting rules; folder rules live in one AGENTS.md per package and top-level folder, checked by check:docs. CONTRIBUTING.md owns process; README and the docs page link to it. Release preparation copies the root LICENSE into every published package, Core, CLI, and MCP gain READMEs, and the release workflow test packs its fixture in-process instead of through npm.