* 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
7.6 KiB
Repo tooling, CI, and releases
Private tooling lives under tools/<role>/<domain>/{src,tests}; Steiger enforces the layout and kebab-case domain names. Every tool is a workspace named @open-pencil/<domain>-tools, listed literally in the root workspaces (the manifest schema rejects globs on purpose), so it can declare dependencies and run through bun --filter. bun run check:tools type-checks every tool through tools/tsconfig.json; bun run test:tools runs every tool with a test script. A tool imports its own files through its package.json imports alias (#ci/*, #release/*), other tools through their package name, and the docs site config through the shared #docs-config/* path.
| Role | Contract | Domains |
|---|---|---|
checks/ |
Read the tree and exit non-zero; never write inside the repository except an own baseline behind an explicit --write |
architecture, lint, i18n, secret-scan, type-shapes, docs, test-homes, package-quality |
generate/ |
Write generated files or artifacts, idempotently | brand, demo, tauri-menu, authoring-reference, visual-oracles |
release/ |
Build, verify, and publish packages and native artifacts | package-artifacts, release-packages |
ci/ |
Consumed by workflows: path classification and gate policy, container images, the review-guidance bot | policy, images, pr-review-guidance |
dev/ |
Test running and benchmarks for humans | unit-tests, navigation-benchmark, dev-server |
- Nothing under
generate/orrelease/runs frombun run checkexcept through a check entrypoint;generate/authoring-referencekeeps itscheck.tsbesidegenerate.tsuntil it splits. - Resolve the workspace with
resolveWorkspaceRootfrom@open-pencil/package-artifacts-tools, not parent-directory traversal. Keep sibling imports relative. checks/test-homesownsengine-baseline.txtandcheck:test-homes;dev/unit-testsowns the shard map and runner (tests/AGENTS.md).- No
scripts/entrypoints: rootpackage.jsonscripts call tool files directly.
CI
.github/workflows/ci.ymlandheavy-tests.ymldefine validation gates. PR CI always classifies changed paths throughtools/ci/policy/src/policy.ts: root docs, package READMEs, everyAGENTS.md,packages/docsMarkdown and assets, and skill Markdown are docs-only; runtime prompt Markdown, executable examples, configuration, and unknown paths require code validation.- Docs-only changes run documentation integrity and the docs build; everything else runs the full suites. The aggregate
CI resultgate requires successful classification and every applicable job; failures, cancellations, and unexpected skips cannot pass. Do not restore workflow-level path filtering on required CI. commitlint.config.tsenforces commit structure in the Commit messages job; the separate PR title workflow validates titles. Preserve theRelease vX.Y.Zexception and product casing when changing rules; the known AI co-author check does not rewrite base history. Gate policy lives intools/ci/policy/src/policy.ts.tools/ci/policyruns right after Bun is set up, before any install, so it imports only Node built-ins;oxlint.jsonswitchesopen-pencil/prefer-es-toolkitoff there. Other CI tools install their workspace first, aspr-review-guidance.ymldoes through.github/actions/setup-bun.- Required checks must also run on
merge_group, the merge queue's event; read base and head frommerge_group.base_sha/head_shathere (ci.yml,pr-title.yml). - A passing pull request run records the tree it tested as the
CI verified treestatus on the PR head; a merge queue commit with that exact tree is classifiedverifiedand runs only the always-on checks. Any other tree, including a group with other PRs ahead, gets the full run (tools/ci/policy/src/verified-tree/). - App and docs production workflows run on
v*tags orworkflow_dispatch, not ordinarymasterpushes.build.ymlchecks the tooling out under.pipeline/and runstools/release/release-packagesfrom there.
Releases
- Update versions in the root and publishable package manifests plus
desktop/tauri.conf.jsonanddesktop/Cargo.toml; moveUnreleasedinto## x.y.z — YYYY-MM-DD; commitRelease vX.Y.Z; tag and pushvX.Y.Z. .github/workflows/build.ymlis the source of truth:v*tags (or dispatches for an immutable stable tag) build shared frontend/package outputs once, build signed desktop artifacts in parallel, verify and attest one complete same-run artifact set, publish npm packages, and replace the draft release assets using the exact changelog section. Policy, provenance, and recovery:tools/release/release-packages/README.md.- Public workspace packages are discovered by the package-artifacts catalog. Bun source exports require the complete
srcdirectory in package contents; Node exports usedist. Release preparation must preserve resolution maps. Prepared publish directories receive the rootLICENSEwhen a package has none of its own, and every package needs aREADME.mdbecause npm renders it. Publishing uses prepared npm tarballs verified through the shared Node/Bun consumer checks; never publish package directories manually.test:packagesfirst runs the packaging guards intools/checks/package-quality/src/smoke/guards.ts: fixture manifests packed with the realnpm packthat must trip the tarball inspector and the Node/Bun consumer checks before those checks vouch for real packages. - Ensure Tauri and Apple signing/notarization secrets are configured. Verify the draft title, body, and artifacts, then publish. Release titles are exactly the tag (
vX.Y.Z) without a product-name prefix. - Homebrew's
openpencilcask is managed upstream: BrewTestBot proposes bumps and Homebrew merges them. Check the upstream cask PR after publication; do not push to the archived custom tap or add bump automation. Users install the app withbrew install --cask openpenciland the CLI through npm or Bun.
Demo document
The /demo document is built from tools/generate/demo/src/document/ into the ignored public/demo.fig, and the app opens it like any .fig file; it is never generated in the browser. ensureDemoDocument runs from vite.config.ts, rebuilds only when its inputs' fingerprint changes, and runs the build in Bun; bun run generate:demo forces it. A section that needs something new from the engine adds that package's src to the fingerprint in tools/generate/demo/src/ensure.ts.
Brand assets
Canonical artwork lives in assets/brand/ (main mark and optical micro master; see its README). tools/generate/brand/ derives web, docs, and native icons with RealFaviconGenerator and Tauri; generated assets are ignored, not committed. Vite and VitePress configs prepare their own targets, and Tauri dev/build hooks prepare native icons.