# @ponchia/ui
> CSS-first, framework-agnostic UI framework. Zero runtime dependencies.
> One `@layer bronto` cascade, a typed class vocabulary, design tokens as
> data, and optional SSR-safe vanilla behaviors. Token-driven restraint:
> neutral by default, one core accent for UI emphasis, dot-matrix display type,
> plus separate governed tiers for status, display colorways, and data-viz.
>
> This file orients an LLM/agent. The authoritative, always-correct API
> is the TypeScript declarations shipped in this package (paths below):
> they are generated from the runtime sources and CI-drift-checked, so
> they never lie. Read them before guessing class or token names. For the
> manifest-level inventory, use `docs/package-contract.md`: it is generated
> from package.json and lists every export, shipped path, and provenance gate.
## Use the framework
CSS (pick one; do not import both):
```css
@import '@ponchia/ui'; /* dist/bronto.css — flattened, minified, one file (recommended) */
@import '@ponchia/ui/css'; /* css/core.css — @import fan-out, needs a bundler */
```
`import '@ponchia/ui'` is supported only as a CSS side-effect import in a
CSS-aware bundler. Do not import the package root from Node/runtime JS; use the
explicit JS subpaths below.
JS subpaths are ESM-only. In CommonJS, use dynamic `import()`.
Everything lands in `@layer bronto`. Consumer styles in no layer (or a
later layer) win ordinary framework styles regardless of selector specificity —
this is the intended override mechanism. Do not fight normal component styling
with `!important`; the framework reserves `!important` for print and
reduced-motion safeguards.
Loading a stylesheet — read before copying a `` below. The opt-in
leaves are written as `@ponchia/ui/css/.css` for brevity. That
package-specifier form resolves ONLY inside a CSS-aware bundler
(Vite/webpack); in a standalone `.html` file it does not resolve. For plain
HTML (the usual case when an LLM emits a report) use a real URL — and note
the path changes from source `css/` to built `dist/css/`:
```html
```
The flattened default bundle is `dist/bronto.css` (bundler shorthand
`@ponchia/ui`). A complete, copy-pasteable CDN report is in `docs/reporting.md`.
Typed class vocabulary (returns plain strings — works in any framework):
```js
import { cls, ui, cx } from '@ponchia/ui/classes';
// ui.button({ variant: 'ghost' }) -> "ui-button ui-button--ghost"
// cx(ui.dot({ tone: 'success' }), 'my-extra') -> joined, falsy-skipped
// cls is the frozen flat registry of every class the framework defines
```
Tokens as data (theming + reading the palette):
```js
import { cssVars, tokens, themeColor } from '@ponchia/ui/tokens';
```
Optional vanilla behaviors (theme toggle, etc. — SSR-safe, tree-shakeable):
```js
import { applyStoredTheme } from '@ponchia/ui/behaviors';
```
Notable behaviors beyond the obvious: `initMenu()` adds close affordances to a
native `` dropdown (outside-click/Escape/select-close);
`initModal()` wires the controlled `.ui-modal.is-open` path (inert focus-trap +
role/aria-modal); `initDisabledGuard()` makes every `aria-disabled` control
keyboard-inert (CSS alone is only pointer-inert). For the value-bearing fills,
`attrs.meter(value)` / `attrs.progress(value)` from `@ponchia/ui/classes` return
`role`+`aria-valuenow/min/max`+the `--value` style to spread onto the host.
`initSources()` wires `[data-bronto-sources]` citation/source islands:
`.ui-citation[href^="#"]` and `[data-bronto-source-ref]` focus/highlight the
matching `.ui-source-card`, seed lightweight preview metadata, and emit
`bronto:source:focus`; the host still owns numbering, fetching, trust decisions,
and any rich preview popover.
Optional lifecycle adapters wrap those behaviors without owning markup or state.
React/Solid/Qwik use hook bindings (peer deps `react` / `solid-js` /
`@builder.io/qwik`, optional — core stays zero-dep); Svelte uses dependency-free
actions, and Vue uses dependency-free directives/plugin helpers:
```js
import { useDialog, useToast } from '@ponchia/ui/react'; // or '@ponchia/ui/solid' or '@ponchia/ui/qwik'
// React: scope with { root: reactRef } or a resolver, not { root: ref.current } during render.
// Qwik: hooks run in useVisibleTask$; scope with a Qwik signal — useDialog({ root: useSignal() }).
// Svelte: import { dialog, toast } from '@ponchia/ui/svelte'.
// Vue: import { vDialog, useToast } from '@ponchia/ui/vue', or app.use(brontoVue).
```
Optional display glyphs — dot-matrix bitmaps on the `.ui-dotmatrix` primitive
(decorative by default; `renderGlyph` is SSR-safe, `initDotGlyph` is the DOM form).
`findGlyphs('delete')` resolves an intent word to real names; `renderReadout`
composes digits into a big Nothing-style numeric. Details: `docs/glyphs.md`.
```js
import { renderGlyph, findGlyphs, renderReadout, GLYPH_NAMES } from '@ponchia/ui/glyphs';
el.innerHTML = renderGlyph('check', { label: 'Done' });
el.innerHTML = renderReadout('73%', { label: '73 percent of quota' });
```
The dot family also has data-bound reporting surfaces — `.ui-waffle` (unit
chart), `.ui-activity` (contribution grid), `.ui-level` (LED meter),
`.ui-dotgauge` (radial gauge), `.ui-halftone`, `.ui-readout` — same host-sets-
the-data / leaf-paints boundary as `ui-spark`/`ui-meter`. Details: `docs/dots.md`.
Layout primitives (core bundle, no media queries — intrinsic Every-Layout
composition that responds to content + available space): `ui-stack` (vertical
rhythm), `ui-cluster` (wrapping inline row), `ui-grid` (auto-fit columns),
`ui-sidebar` (sidebar + main that stacks when cramped), `ui-switcher` (row →
column together), `ui-center` (measure-bounded column), `ui-ratio` (aspect-ratio
box — one child only), and the `ui-app-shell`/`ui-app-rail`/`ui-app-nav` admin
shell. `ui-cq` makes descendant layouts respond to THAT box (container queries),
not the viewport — wrap a `ui-grid`/`ui-statgrid` in it for an island-safe
collapse in a slim pane. Each is tuned with an inline custom property rather than
a modifier — set `style="--grid-min: 12rem"`, `--sidebar-width`, `--sidebar-gap`,
`--switcher-min`, `--switcher-gap`, `--center-max` (the INNER measure; gutters add
to the total), `--ratio`, `--stack-gap`, `--cluster-gap`, `--app-rail`, etc. The
full list with defaults is in `classes.json` `customProperties`. App-shell/sitenav
current page is `aria-current="page"`.
Motion utilities (core bundle, all reduced-motion-safe — they collapse to the
static end state, not just a zeroed duration): `ui-animate-in`,
`ui-animate-fade`, `ui-animate-dot`, `ui-animate-matrix` (enter animations),
`ui-stagger` (+ `--i` per child, or `ui-stagger--auto` to
derive the delay from `:nth-child`), `ui-reveal` (JS/IntersectionObserver — adds
`is-visible`), `ui-scroll-reveal` and `ui-scroll-progress` (zero-JS,
scroll-driven; the recommended reveal for static reports), and `ui-vt` (View
Transitions — needs the required `--ui-vt-name`, which must be **unique per
document**: reusing one name across sibling items silently drops the transition).
Prefer `ui-scroll-reveal` over `ui-reveal` when no JS will run. Details:
`docs/usage.md`.
Optional display colorways — opt-in, never in the default bundle. Root-level,
like `data-theme` (the derived accent family only recomputes at `:root`):
```html
```
Each re-points the one `--accent` (OKLCH, per-theme, contrast-gated); status
colours and the neutral canvas are untouched. Details in `docs/theming.md`.
Optional data-viz palette for dashboards — opt-in, charts-only (never UI
chrome), never in the default bundle:
```html
```
```js
import charts from '@ponchia/ui/charts.json' with { type: 'json' };
// charts.dark.categorical → ['#ff3b41', …] (series 0 = the resolved accent)
```
On-brand Mermaid diagrams use the same tokens-as-data path — Mermaid is the
consumer's renderer, bronto just hands it a resolved `base` theme (Mermaid can't
read `var(--x)`):
```js
import { brontoMermaidTheme } from '@ponchia/ui/mermaid';
mermaid.initialize(brontoMermaidTheme('dark')); // { theme: 'base', themeVariables }
```
Annotate a rendered diagram with the SVG annotation layer as a build-time
recipe. Details: `docs/mermaid.md`.
D2 (d2lang.com) is themed the same way — it compiles to a frozen SVG, so the
slots take resolved hex; the base ramp is monochrome and the accent is opt-in
per node:
```js
import { brontoD2Vars } from '@ponchia/ui/d2';
const source = brontoD2Vars() + 'a -> b'; // prepend the vars block, then render
```
Details: `docs/d2.md`.
Charting is the same path: bronto does NOT ship a chart component (a chart needs
scales + data binding, which the analytical layer refuses). For charts, recommend
Vega-Lite — a declarative JSON spec an external system can emit — themed by the
on-brand `config` (resolved hex; Vega bakes colours into SVG/canvas, can't read
`var()`). `range.category` is the `--chart-*` palette, series 1 = accent:
```js
import { brontoVegaConfig, brontoVegaAccent, brontoVegaNeutral } from '@ponchia/ui/vega';
vegaEmbed('#chart', spec, { config: brontoVegaConfig('dark'), renderer: 'svg', actions: false });
```
Pass `renderer: 'svg'` (canvas is the wrong default for print/inspection).
`brontoVegaAccent(theme)`/`brontoVegaNeutral(theme)` give the exact hexes to spend
the accent on one mark. From a CDN, pin the `/build/*.min.js` UMD bundles (a bare
`vega@6` tag has no `window.vega`; keep Vega-Lite 6 / Vega 6 / vega-embed 7
aligned) and INLINE the config from `vega.json` — a
`file://` report can't `import`/`fetch` it (CORS). A label on an accent fill in a
foreign renderer (a themed Vega/D2/Mermaid node or bar) reads the `--on-accent`
token, never `--accent-text` (that is accent-coloured text for a neutral bg).
`--on-accent` is a READ-ONLY export for those renderers — no in-DOM `.ui-*` rule
consumes it, so setting it does not re-ink a component; to colour text on an
accent-filled DOM control, set `--button-text`. Details: `docs/vega.md`. (Observable
Plot works too — it inherits the page CSS, so it needs even less theming;
Vega-Lite is the recommended LLM-emittable path.)
`--chart-1..8` (categorical; series 1 = accent; colourblind-safe, gated under
simulated protan/deutan/tritan), `--chart-seq-*` (sequential), `--chart-div-*`
(diverging), and `--chart-pattern-1..8` (dot-matrix fills — pair colour N with
pattern N; colour is never the sole signal). Details in `docs/theming.md`.
Building analytical / generated-report UI? Import the roll-up
`@ponchia/ui/css/analytical.css` (bundles figure + annotations + legend +
marks + connectors + spotlight + crosshair + selection + highlights) instead
of the nine leaves; add `css/dataviz.css` for chart colours and `css/report.css`
for the document grammar. Interval and clamp are report/evidence leaves but are
NOT in the roll-up; link them individually. The individual layers (each opt-in,
none in the default bundle):
Optional figure stage for charts, diagrams, screenshots, and annotated media —
opt-in, never in the default bundle. It composes with `ui-report__figure` but
does not replace it:
```html
```
Use `ui-figure` on the ``, `ui-figure__caption` on the caption,
`ui-figure__body` and optional `ui-figure__body--key-right` for stage + key
layout, `ui-figure__stage` for the centered media box, `ui-figure__media` for
the chart/SVG/img/canvas, `ui-figure__overlay` for pointer-transparent absolute
overlays, `ui-figure__key` for legends, and `ui-figure__data` for fallback
tables. Author-set knobs: `--figure-max-inline`, `--figure-min-block`,
`--figure-key-width`. Bronto owns layout only; the host owns data, scales,
chart rendering, overlay content, and accessibility text. Details:
`docs/figure.md`.
Optional SVG annotations for charts/reports — opt-in, never in the default
bundle. This is Bronto's subject / connector / note grammar inspired by
d3-annotation, not an authoring engine. For placement, collision handling,
renderers, editing, and chart/diagram adapters, use the sibling
`@ponchia/annotations` package; `@ponchia/ui/annotations` stays a
dependency-free Bronto static-helper compatibility surface:
```html
```
```js
import { annotationParts, connectorLine, circleSubjectPath } from '@ponchia/ui/annotations';
```
Use `ui-annotation` groups inside SVG with `ui-annotation__subject`,
`ui-annotation__connector`, `ui-annotation__note`, `ui-annotation__title`, and
`ui-annotation__label`. `ui.annotation({ variant, tone, motion })` builds the
base class string. Variants cover labels, callouts, elbow/curve connectors,
circle/rect/threshold subjects, badges, brackets, bands, slopes, comparisons,
clusters, axis marks, timeline pins, and evidence markers. Motion is opt-in
(`draw`, `reveal`, `pulse`, `focus`) and respects reduced-motion preferences.
Use `notePlacement()` for one-note bounded placement, but do not treat it as a
whole-chart collision solver. `declutterLabels(items,{gap,min,max})` is a
deterministic 1-D declutter; `directLabels(items,{axis,cross,gap,min,max,shape})`
adds the leader line (it declutters labels along an axis and returns each placed
point plus a leader path `d` for `ui-annotation__connector`) — neither owns
scales, DOM, or 2-D placement. Status tones are only for status-bearing callouts.
Keep charts sparse; dense mobile figures need horizontal scrolling, a simplified
SVG, or complete caption/fallback text. Details: `docs/annotations.md`.
Optional legends / data keys for charts and figures — opt-in, never in the
default bundle. Reads the `--chart-*` tokens, so a key never drifts from its
series:
```html
```
Use a `ui-legend` container with `ui-legend__item` rows, each holding a
`ui-legend__swatch` (decorative, `aria-hidden`) and a `ui-legend__label` (the
required non-colour channel — WCAG 1.4.1; colour alone never carries meaning).
Set the swatch colour inline (`style="--chart-color: var(--chart-3)"`, plus
`--chart-pattern` to match a patterned mark) or with a `ui-legend__swatch--1..8`
index helper. Variants: `ui-legend--vertical`/`--compact`/`--with-values`,
`ui-legend--gradient` (+`--diverging`, with `ui-legend__track`/`__ticks`/`__tick`),
and `ui-legend--threshold`. `ui.legend()`/`ui.legendItem()`/`ui.legendSwatch()`
build the class strings. Interactive (series-toggling) legends are opt-in:
author entries as `