Skip to content

Share design tokens and API contracts across React and Angular#30

Open
sjoerdbeentjes wants to merge 7 commits into
mainfrom
feat/shared-tokens-contracts
Open

Share design tokens and API contracts across React and Angular#30
sjoerdbeentjes wants to merge 7 commits into
mainfrom
feat/shared-tokens-contracts

Conversation

@sjoerdbeentjes

@sjoerdbeentjes sjoerdbeentjes commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

What

Two shared packages so tokens and Button API live in one place across @surfnet/react and @surfnet/angular.

  • @surfnet/tokens (publishable): DTCG JSON compiled by Style Dictionary into tokens.css (:root + .dark, oklch verbatim) plus a typed token map and TokenName union. Reconciles prior drift (canonical dark --sidebar-primary, shared --chart-*).
  • @surfnet/contracts (private, build-time only): buttonContract derives the variant/size name unions. Compile-time satisfies in both Buttons makes adding or omitting a variant fail pnpm lint. Types erase from each published dist.
  • Both frameworks wired: each imports the shared token CSS (inlined into React's published styles), keeps its own Tailwind wiring, applies satisfies, and sources story variants/docs from the contract.
  • Docs: AGENTS.md, README.md, and the add-component skill updated.

Why

The oklch tokens and Button variant APIs were hand-duplicated and had already drifted. Centralising tokens removes the drift; the contract makes API parity a compile-time guarantee.

Reviewer notes

  • Tokens use only the name/kebab transform (no color transform), so oklch is untouched. The JS map and literal TokenName union use small custom formats (built-in TS declarations give no literal union).
  • Parity guarantees the same variant/size names exist, not pixel-identical rendering. The h-8 vs h-9 default-size difference is intentionally left as-is.

Out of scope (separate tickets)

Release tooling, test runner, cva dimensional parity, Angular library CSS distribution.

The package exported raw TypeScript source (src/index.ts). Vite
transpiles that fine, but Angular's webpack Storybook does not transpile
.ts from node_modules, so buttonContract resolved to undefined at runtime
and the Button stories crashed reading `.description`.

Compile to dist (tsc) and point exports at the built JS + d.ts. The
satisfies-based parity still erases from each framework's published dist
(types only); only the story runtime data now comes from compiled JS.
Wiring the shared token CSS accidentally dropped the local
--radius: 0.625rem from :root. The Spartan preset derives the whole
radius scale (--radius-sm/md/lg/...) from --radius via calc(), so without
it every rounded-* utility resolved to an undefined value and buttons
rendered square. --radius is local wiring (React keeps its own), not a
shared token.
@sjoerdbeentjes sjoerdbeentjes force-pushed the feat/shared-tokens-contracts branch from b47cb8f to 60bc9c3 Compare June 17, 2026 09:57
@surfnet/contracts now resolves through its built dist/*.d.ts, so
type-checking a consumer needs that dist to exist. The lint task had no
build dependency, so in CI (clean tree, lint before build) ngc could not
find @surfnet/contracts. Add dependsOn ^build to lint so sibling outputs
are present when each package is type-checked.
@sjoerdbeentjes sjoerdbeentjes requested a review from oharsta June 17, 2026 12:39
@sjoerdbeentjes sjoerdbeentjes linked an issue Jun 17, 2026 that may be closed by this pull request
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.

Shared package voor gedeelde utils opzetten

1 participant