Adding A Primitive
Use this playbook for a new CSS primitive, component, opt-in report surface, or small behavior-backed widget. It follows the contracts in CONTRIBUTING.md and architecture.md.
Bring the evidence first. New public surface is admitted when a named non-example consumer has already built it by hand, and the PR says who and where. Fixes to existing surface need no such argument. Package examples are compatibility proof, not downstream adoption — see ROADMAP.md.
1. Choose the layer
Pick the lane before adding classes or exports:
- Recipe/docs only: use this when a pattern can be taught with existing classes, tokens, and behaviors. This is the default when the surface is not clearly repeated.
- Core identity: universal application chrome, platform glue, or primitives
that belong in the default
dist/bronto.cssbundle. Source lives in an existing core CSS leaf or in a new leaf imported bycss/core.css. - Opt-in toolbox: report, analytical, provenance, generated-content,
renderer-theme, workbench, or command vocabulary. Source lives in an explicit
subpath such as
css/<leaf>.css, not incss/core.css.
Do not add product logic. The host application owns data fetching, routing, persistence, chart scales, workflow execution, action registries, and component state. Bronto owns visual grammar, CSS contracts, pure geometry, and narrow delegated accessibility behavior.
2. Add the CSS
Use .ui-* class names and keep the authored CSS in css/.
- Existing family: edit the matching
css/<leaf>.css. - New core leaf: add
css/<leaf>.css, import it fromcss/core.cssin cascade order, and add package targets for both the layered import./css/<leaf>.css -> ./dist/css/<leaf>.cssand the raw escape hatch./css/unlayered/<leaf>.css -> ./css/<leaf>.css. - New opt-in leaf: add
css/<leaf>.css, add it toEXTRA_LEAVESinscripts/build-dist.mjs, and add the same layered and unlayered package export targets. - Analytical/report roll-up: update
css/analytical.cssonly for the nine analytical leaves it intentionally owns, and updatecss/report-kit.cssonly when the static-report kit should import the leaf.
If a new primitive needs token values, edit tokens/index.js for core tokens.
Use tokens/skins.js only for root-level data-bronto-skin colorways and
tokens/charts.js only for chart/data-viz palettes. Do not add raw chromatic
component colors to CSS; scripts/check-color-policy.mjs gates that boundary.
3. Add the class contract
Add every public selector to classes/index.js:
- Add base, part, modifier, and author-applied state classes to
cls. - Add or extend a
ui.*recipe only when it prevents repeated string assembly. - Add recipe options to
scripts/gen-dts.mjs. The generatedclasses/index.d.tsemits the literalclsmap fromclasses/index.js, but the option interfaces andUirecipe signatures are curated in that script.
scripts/check-classes.mjs enforces the bidirectional match between
classes/index.js and stylesheet .ui-* selectors.
4. Add behavior only when CSS cannot own it
If the primitive needs JS, add the vanilla behavior under behaviors/ and export
it from behaviors/index.js only if it is public.
Public behavior exports must have:
- Docs ownership in the relevant
docs/*.mdfile. - Unit ownership in
test/behaviors.test.mjsor the appropriate test file. - Browser ownership in a non-pixel Playwright spec discovered by
scripts/test-e2e-nonpixel.mjs.
scripts/check-behavior-matrix.mjs enforces those three owners.
Verify the vanilla lifecycle in the maintained React and Svelte examples when
the behavior contract changes. The package has no framework adapter matrix.
If the new public surface is a helper in classes/, annotations/,
connectors/, or glyphs/, add it to scripts/check-helper-matrix.mjs with
docs, unit, and type-test owners.
5. Declare published surface
Public paths are declared in package.json:
- Add
exportsentries for new CSS, JS, JSON, schema, or doc subpaths. - Keep exported files covered by
files. - Keep runtime dependencies empty. Only the documented optional framework peers
belong in
peerDependencies.
scripts/check-exports.mjs validates export targets, CSS layered/unlayered
pairs, package metadata, and dependency policy. scripts/check-pack.mjs proves
the packed tarball contains the intended files and no dev-only directories.
6. Add docs, demo, and specs
A shipped CSS leaf must be matrix-owned:
- Add a row to
scripts/check-component-matrix.mjs. - Foundation rows need a docs owner and an executable proof owner.
- Component surface rows need docs, a demo, and a non-pixel Playwright spec.
- Demo owners other than
demo/index.htmlmust be listed intest/e2e/demos.spec.mjsunderSHOWCASEorGUARD_ONLY. - Visual snapshot owners need matching
data-shot="<name>"indemo/index.htmland committed dark/light PNG baselines undertest/e2e/__screenshots__/.
For report-relevant CSS leaves, add the routing row in reporting.md. The analytical toolbox table is how report and LLM consumers discover when to import the leaf.
Docs that describe public classes, behavior names, imports, or HTML snippets are
checked by scripts/check-doc-links.mjs, scripts/check-contract.mjs,
scripts/check-doc-recipes.mjs, and scripts/check-report.mjs.
7. Regenerate and run gates
After implementation, regenerate committed artifacts with:
npm run build:artifacts
The full required gate is still npm run check. For primitive work, expect these
targeted gates to be relevant:
npm run check:exportsnpm run check:freshnpm run check:classesnpm run check:recipe-typesnpm run check:dts-emitnpm run check:typesnpm run check:distnpm run check:packnpm run check:component-matrixnpm run check:behavior-matrixwhen public behavior is involvednpm run check:behavior-matrixwhen a delegated public behavior is involvednpm run check:helper-matrixwhen a public helper is involvednpm run check:schemaswhen public schemas changenpm run check:variablesnpm run check:color-policynpm run check:skins,npm run check:charts,npm run check:contrast,npm run check:mermaid,npm run check:d2, ornpm run check:vegawhen token, color, or renderer-theme data changesnpm run check:doc-linksnpm run check:doc-recipesnpm run check:contractnpm run check:report
Run npm run test:e2e:nonpixel for local browser coverage. Do not regenerate
committed pixel baselines on a dev machine; use the workflow described in
CONTRIBUTING.md.