* feat(core): read and write W3C design tokens
Variables export as DTCG 2025.10 token files, one per collection mode in the shape Figma imports, with text and effect styles as typography and shadow tokens and a resolver document that combines them. OpenPencil's own token fields ride in a dev.openpencil extension, so an export reads back as it was.
Import reads plain mode files, resolver documents, and Tokens Studio themes, plans what it would add, update, or skip against the document without changing it, and applies the plan as one undo step: values merge into variables by name, nothing is deleted, and an updated style updates the layers that use it.
* feat: import and export design tokens from the variables dialog
The stylesheet menu exports the document's variables and styles as a zip of W3C design tokens, and Import design tokens reads token files, folders, or a zip into a dialog that shows where each collection and mode goes, what would be added, updated, or skipped and why, before applying it as one undo step.
* feat: import and export design tokens from tools and the CLI
openpencil tokens --format dtcg --out <folder> writes the token files, design_to_tokens takes the same format, and import_design_tokens imports token files into a document for AI, MCP, and openpencil tool call. The editor and these paths share one apply step behind a target interface: the editor makes each change undoable, a headless document takes them directly.
Re-importing an export changes nothing it does not need to: values that match are left alone, layers that use an updated style are changed only outside instances, where they follow their component, and collections that share a name keep their own files apart.
* perf(core): index token lookups when planning an import
Resolving each reference scanned every token and variable, which made importing a large design system quadratic. A resolver now also finds files picked without their folders by name.
* docs: document design token import and export
* test: type the design token result in the tokens command test
* fix: lay out the design token import dialog as a mapping list
Each collection and mode sits beside the picker for where it goes, in one bordered list, so the pickers no longer read as headings. Skipped tokens use AppCollapsible, file problems AppAlert, and the counts move to the footer.
* fix: keep durations, variable metadata, and style edits through design token import
A duration Figma's format writes in seconds goes back into a variable that holds milliseconds. Tokens without OpenPencil's extension leave an updated variable's unit, scopes, and code syntax as they are. A style token whose value was edited in another tool takes that edit instead of the stale exact fields its extension still carries. Aliases to a repeated collection name resolve within one collection, a collection named Styles no longer collides with the resolver's styles set, and the dialog plans again when the document changes and right before applying. The editor imports the apply step directly rather than the whole format barrel.
* fix: keep effect order and explicit alias targets in design token import
Edited shadows take the places of the shadows they replace, so blurs keep their positions. An alias whose Figma data names a collection that is left out or absent is missing rather than bound to the same path in another collection. The resolver reserves the Styles set name only when there is a styles file.
* perf(cli): print token stylesheets without loading every page
A CSS or Tailwind stylesheet reads only variables, which load with the document, so tokens skips populating and laying out every page as variables already does. On an 85 MB design kit this takes the command from about seven minutes to seconds.
* build: typecheck the test suites
Tests were in no TypeScript program: no tsconfig included tests/** or
packages/*/tests/**, and bun strips types without checking them, so a
fixture could drop a required field and keep passing until something
read it.
@types/bun moves to the root because it was installed per package only,
and #cli-tests/* joins the paths the root config already carries.
* test: fix the type errors the test suites were hiding
Typechecking the tests turned up 1123 errors. Most were ordinary
strictness, but some were real: `NodeChange` bound to Figma's plugin
typings rather than the Kiwi codec in thirteen .fig tests,
materializeInstance was called with six arguments against five so the
blobs and source children were dropped, CanvasKit pixels were written
to a plain object that never reached WASM, and assertions were made
through accessors that do not exist, so they asserted nothing.
Fixtures that had quietly lost a required field now carry it, nullable
results are narrowed through the existing expectDefined helper rather
than assumed, and stand-ins for CanvasKit and the editor go through one
named helper instead of an unexplained cast at each site.
No test was deleted, skipped, or weakened, and no `any`, non-null
assertion, or ts-expect-error was introduced.
* docs: record what typechecking the tests established
Pins the app program's global types with an assertion rather than a
note, since an unpinned types list lets any root @types package decide
which platform src/** is judged against.
The two environment faults that look like code regressions — Vite's
dependency pre-bundle outliving a package rebuild, and heavy .fig
suites failing under load — go to the development docs, where an
explanation belongs.
* fix: align @types/bun and keep node types resolvable when extended
The root manifest declared a newer @types/bun than every package, which
check:monorepo rejects, and pinning the app program's types left them
unresolvable from a config that extends this one out of tree.
* fix: fail the test typecheck when the compiler itself fails
The gate matched diagnostics by substring, so a compiler or config
failure that named no test file printed a pass while having checked
nothing. Diagnostics are now split by whether they name a file: an
unscoped one is the run failing and stops the gate, a test file's is a
finding, and a source file's stays out by design.
Also drops the parameter planComponentConstruction never read, and
makes the inner-shadow verification script exit non-zero when it
renders no image instead of logging and succeeding.
* chore: merge master into tests-typecheck
* feat: write variables as a CSS token stylesheet
Copy a collection as CSS custom properties or a Tailwind v4 theme from the variables dialog, print it with openpencil tokens, and rebuild design_to_tokens on the same generator. Default modes go in :root or @theme, other modes override under their condition, and aliases are declared again in each mode scope so they follow it.
* fix: give modes that slug alike their own selector and variant
Two modes in one collection whose names reduce to the same slug, such as Dark and dark!, shared one default selector and Tailwind variant, so the later mode silently overrode the earlier one. Slugs are now numbered in mode order, as variable names already are.