v0.6.0
Accumulates the post-0.5.0 work: a multi-agent audit pass (accessibility
hardening, a behavior/binding scope-safety fix, codegen/gate tightening) plus a
breaking charting realignment. The local static-bar renderer
(.ui-chart*) is removed — a chart needs scales + data binding, which the
analytical layer refuses to own. In its place, bronto becomes a themeable target
for Vega-Lite (@ponchia/ui/vega), the same tokens-as-data path as Mermaid
and D2. The data-viz palette (--chart-*, tokens/charts.json) and the
legend layer are unchanged. Pin ~0.5 → re-pin ~0.6; see
MIGRATIONS.json (0.5→0.6).
Added
@ponchia/ui/vega(+vega.json) — an on-brand Vega-Lite / Vega
configresolved per
theme (the idiomaticvega-themesshape): monochrome chrome + one rationed
accent,range.category/ordinal/ramp/heatmap/divergingfrom the CVD-safe
data-viz palette.brontoVegaConfig(theme). Resolved hex (Vega bakes colours
into SVG/canvas, can't readvar()); gated structurally and by a headless
render-probe that asserts the colours land on a rendered chart. Vega is the
consumer's renderer — config only, not a dependency. Seedocs/vega.md.ui-delta— a standalone trend/change indicator (core primitive): an
arrow glyph (the non-colour channel) plus the figure, with
--up/--down/--flat, and--invertto swap only the tone when "up" is
the bad direction (latency, error rate, cost).ui.delta({ dir, invert }).ui-compare— a fluid side-by-side / before-after layout for the report
layer (css/report.css):__col,__head, and--2up.
ui.compare({ cols }).@ponchia/ui/classes.json— the class vocabulary as language-neutral
data (groups/classes/states/customProperties), so a non-JS/non-TS
host or an external linter can validate emitted markup without executing the
ESMclsmap or parsing the.d.ts. Generated fromcls; drift-checked and
itsstates/customPropertiesgated against the stylesheet.tokens/resolved.jsonscaleblock — the resolved non-colour scales
(spacing/radius/type/z/motion,var()chains flattened), completing the
token contract for non-CSS hosts (previously colour-only).--display-weight/--display-weight-strong(700 / 800) — the weight of
the Doto dot-matrix display face, now a token. Themes/skins can re-tune how
heavy display text renders in one place.- On-brand Mermaid (
@ponchia/ui/mermaid,mermaid.json) and D2
(@ponchia/ui/d2,d2.json) theme maps — resolved per-theme palettes
projected from the same tokens, gated. Diagrams stay the consumer's renderer;
these are config only. - Annotation geometry options:
connectorElbow({ mid })(turn position along the
dominant axis),notePlacement({ inset })(reserve the title stroke-halo so a
placement that "fits" doesn't clip), and aspreadhalf-angle on both
connectorEndArrowand the sharedarrowHeadkernel. brontoVegaAccent(theme)/brontoVegaNeutral(theme)(@ponchia/ui/vega)
— the exact per-theme hexes forrange.category's accent (series 1) and
neutral (last series), so spending the accent on one emphasised mark needs no
palette-index reverse-engineering.--on-accenttoken — the readable ink for a label on any accent fill
(button, badge, themed chart bar, a Vega/D2 node). Resolves to--button-text
(white on the light accent, black on the dark) and is gated ≥ 4.5:1 in
docs/contrast.md. Use it instead of--accent-text, which is the inverse
(accent-coloured text for a neutral background, ~1.3:1 on an accent fill)..ui-srcstandalone trust pill (cls.src,css/sources.css) — wears a
.ui-src--*tone (verified / reviewed / generated / unverified / stale /
conflict) on its own, for a bare trust label outside a citation or source card.
Previously the.ui-src--*modifiers only painted a--src-tonewith no
standalone host, so a lone pill validated againstclasses.jsonyet rendered
nothing.
Removed
- BREAKING: the local static bar-chart renderer (
.ui-chart,.ui-chart__plot,
__bar,__label,__track,__fill,__fallback,__caption). A chart
needs scales and data binding — out of scope for a CSS-first analytical layer
(ADR-0002). Replace with a Vega-Lite chart themed via@ponchia/ui/vega, or a
hand-authored token-themed inline<svg>, inside a.ui-report__figurewith a
.ui-report__captionand a.ui-legendkey. The--chart-valueinline knob
is gone; the--chart-color/--chart-patternswatch knobs remain (legend).
SeeMIGRATIONS.json(0.5→0.6) anddocs/vega.md.
Changed
- Annotation connectors are crisper.
connectorEndArrownow defaults to a
sharper head (half-angle 0.32 ≈ 37°, size 8 vs the former blunt 0.45 / 7).
Author-facing geometry only; thearrowHeadkernel default is unchanged, so
node-connector arrowheads don't move.
Accessibility
- Coarse-pointer tap-target floors extended to navigation. The 2.9 rem
touch floor (already on primitives/forms/feedback) now also covers
.ui-sitenav a,.ui-app-nav a,.ui-sitemenu > summary, and
.ui-themetoggle__buttonunder@media (pointer: coarse)— the primary nav
affordances were below the 44 px target on touch. - App shell uses dynamic viewport units.
100vh→100dvh(shell/body) and
the scrolling rail →100svh, so the rail and its pinned account/footer no
longer fall under the mobile URL bar. - Forced-colors status dots stay distinct.
.ui-dot--success/--warning/--danger/--info
and.ui-dotmatrix__cell--hot/--accentnow map to distinct system colors
under Windows High Contrast instead of collapsing to one — the only signal
these carry is colour. - Keyboard affordance parity.
.ui-menu__item:focus-visiblegets the same
row highlight as hover; the segmented control's focus ring is now inset so the
container'soverflow: hiddenno longer clips it. - Reduced-motion skeleton.
.ui-skeletonflattens to a solid placeholder
underprefers-reduced-motioninstead of freezing mid-shimmer.
Fixed
- Published-type drift (code-quality audit).
ui.meter({ tone: 'info' })and
ui.bracketNote({ tone: 'success' })emit real classes at runtime, but the
generated.d.tstone unions (hand-mirrored ingen-dts.mjs) omitted them, so
a TS consumer got a spurious type error for a value that renders. The unions
now match the factory; a newcheck:recipe-typesgate cross-checks every
factory's string-literal options against its*Optsunion so this whole class
of drift fails CI. - Component-library audit (16-agent dogfood pass) — the validates-but-no-ops
cluster. A whole-surface audit found the meter-style trap (a class/token that
validates and paints but silently does nothing without an undocumented
precondition) recurring across components. Fixed:aria-disabled="true"on.ui-button/.ui-linknow sets
pointer-events: none— it looked dead but a real<a>still navigated.- Disabled affordance reaches the controls that wrap a native input
(.ui-switch/.ui-check/.ui-segmented__optionvia:has(input:disabled),
plus.ui-range/.ui-file) — they previously looked operable and their
label keptcursor: pointer. - Bare
[aria-current]selectors (.ui-sitenav,.ui-breadcrumb__item) now
scope:not([aria-current='false']), so a correctly-authored
aria-current="false"link is no longer styled as current. - The active-tab forced-colors re-assert moved from
base.cssto
disclosure.css(after the default rule) — an earlier bundle leaf let the
accent default override it, so the selected tab lost its only HC cue. .ui-meter__fill/.ui-progress__barget a system colour under
forced-colors, so the measured proportion stays visible..ui-searchgains a 2px keyboard focus ring to match every sibling input
(it had only a 1px border-colour shift)..ui-prosegetsoverflow-wrap: break-word— long tokens in
machine-generated Markdown forced horizontal page scroll..ui-mark--drawis scoped to fill styles (:not(--underline, --box, --strike))
so it no longer looks applied while doing nothing..ui-cqhardcodes its container-name (the@container brontocollapse
queries hardcode it, so a--cq-nameoverride silently killed the collapse).initPopover()seeds resting ARIA (aria-haspopup,aria-controls,
aria-expanded) and syncsaria-expandedwhen the UA closes a native
popover;toast()validatestone(an unknown string rendered an unstyled
neutral toast) and warns; the combobox listbox gets an accessible name..ui-error-summary__titleuses the legible sans, not the low-legibility Doto
display face..ui-input/.ui-searchautofill stays on-theme..ui-revealhidden state is gated onscripting: enabled(genuinely degrades
visible with no JS; the prior comment lied) — andui-scroll-revealis the
documented zero-JS path.- Parity modifiers added:
.ui-meter--info,.ui-bracket-note--success.
- Responsive/mobile hardening across the framework:
rem-rooted type for WCAG
1.4.4, coarse-pointer tap-target floors, combobox/tour-note viewport clamps,
and@media (hover)gating — with a new responsive e2e sweep. - Faint numbers on stat cards.
.ui-stat__value/.ui-app-metric__value
(and the report cover/section titles, rail brand, panel titles,.ui-display,
.ui-quote) set the Doto display face but no weight, so they rendered at the
thinnest cut (400). They now apply--display-weight(-strong)— visibly bolder
and more legible, on screen and in print. - Painted data surfaces dropped in the PDF. Headless-Chromium print drops
backgrounds by default, silently blanking the data-bearing fills. Dot-matrix
cells, the segmented meter, status dots, masked glyphs, highlight marks,
connector lines, and progress/meter fills now carryprint-color-adjust: exact
so they survive the A4 print/PDF that the report kit targets. - Dark-theme cards/tables printed dark-on-white. The dark→ink token remap was
scoped to.ui-report; it is now lifted to the print:root(in the exempt
token-definition file), so a bare.ui-card/.ui-statgrid/.ui-table—
the markup an external LLM emits — also prints legibly. - Inline
ui-citationno longer dumps its full URL mid-sentence when printed
(the reference list carries the URL);ui-legend--with-valuesvalues are
right-aligned for a clean tabular column. - Annotation elbow connector was a 45° chamfer, not a dogleg.
connectorElbowturned bymin(|dx|,|dy|), drawing a diagonal stub the
stroke-linejoinbevel never matched. It now delegates to the connectors
geometry kernel's right-angleelbowPath(H/V/H), so an annotation leader and
a node connector draw the same elbow. - Scoped behaviors no longer hijack the whole document on a null root.
init*({ root })with an explicitly-provided-but-unready root (a framework
ref stillnullat mount, a conditional that hasn't rendered) now no-ops
instead of silently widening to document-wide delegation. The react/solid/qwik
bindings emitroot: nullfor the not-ready case so the distinction survives
the boundary; passing norootstill delegates fromdocumentexactly as
before. Affects every delegated behavior (dialog, menu, combobox, …). --report-width/--report-padding-blockare now declared defaults on
.ui-report— they were read with inline fallbacks but never declared, so the
override surface was undiscoverable and--report-measurelooked like the
width knob when it isn't.- Carousel's IntersectionObserver is now set up and torn down in lockstep with
its event binding, removing a one-tick window where a re-init left two
observers on the same slides. ui-meter/ui-progressfill painted a 0×0 box..ui-meter__filland
.ui-progress__barsetblock-size/inline-sizebut nodisplay, so on the
documented<span>fill (an inline box ignores width/height) the bar rendered
empty — a "validates-but-renders-nothing" trap the registry and docs both
hid. They are nowdisplay: block. Found by a second multi-agent dogfood
pass; guarded going forward by a render-geometry e2e (below).
Documentation
- LLM-authored static reports: a prominent CSS-loading note (bundler vs
node_modulesvs CDN) and a copy-pasteable CDN report in
docs/reporting.md; clarified thatdist/bronto.cssdoes not include the
opt-in report/chart/legend/annotation layers; number/date formatting
guidance; and a standalone, no-build report reference
(demo/report-standalone.html). - Resolved the
is-*self-contradiction: the framework's own
is-num/is-pos/is-neg/is-key/is-openstate hooks are valid even
though they deliberately live outsidecls(documented in
docs/reference.mdandclasses.json). - Clarified two standing contracts in
docs/architecture.md:css/analytical.css
is the roll-up of exactly the seven figure leaves (annotations, legend, marks,
connectors, spotlight, crosshair, selection) —sources/state/generated/
workbench/commandare adjacent leaves imported individually — and the root
.export is CSS-only (no runtime JS at the root). Pre-1.0 stability/pinning
spelled out indocs/stability.md.docs/workbench.mdnotes that
.ui-selectionbaris unrelated to the.ui-sel--*selection-emphasis classes. - Honest JSDoc limits: combobox/command read options from the DOM at init
(re-run after replacing them); popover restores focus on Escape but not on
outside-click; the table sorter is locale-naive display-text; mask-mode glyphs
are single-tone. - Foreign-renderer recipes hardened after a multi-agent dogfooding pass
(build five real reports across the whole stack, review from every POV). The
Vega CDN recipe now pins the/build/*.min.jsUMD bundles andrenderer:'svg'
(a barecdn.jsdelivr.net/npm/vega@6tag has nowindow.vega, so the previous
recipe rendered nothing); the file://-portable path (inline the config — an
imported/fetched config is CORS-blocked from disk) is now explicit. New
docs/reporting.mdrecipes: "Theming a live report" (the theme-toggle/re-embed
foot-guns — clear the host, container-width-while-hidden, Mermaid source vs
output), live charts areui-screen-onlywhile the table prints (a kept live
chart bakes the on-screen theme),ui-meter/ui-quotemarkup, and the
sequential/diverging frozen-figure ramp.docs/d2.mdgains a frozen
inline-<svg>-from-slots recipe and on-accent-ink guidance;docs/vega.md
documents the theme-inverting ramp and the OKLCH-vs-d3 gradient-key drift. docs/annotations.mdstates the rule in both directions: a data annotation
must stay readable (notaria-hidden), a decorative one must be hidden.- A second dogfood pass closed the foreign-renderer/contract gaps it found.
docs/sources.md+llms.txtnow document the standalone.ui-srctrust
pill and state that aui-src--*tone class needs a host (a bare
<span class="ui-src--verified">validates but renders nothing), and name the
source-card body part as__excerpt(not__detail).docs/mermaid.md:
gantt/timelineare not covered by the basethemeVariables(they
render with Mermaid's own defaults — prefer the nativeui-timelinefor a
report).docs/mermaid.md+docs/d2.mdgain the samefile://CORS caveat
Vega carries (inline the map or pre-render).docs/vega.md: select the themed
ramp withscale: { range: 'heatmap' }— notscheme:, which throws — and
the accent/neutral series map to--chart-1/--chart-8, so a legend keys
them withui-legend__swatch--1/--8(docs/legends.md).docs/reporting.md:
the live-theme recipe nowfinalize()s the prior Vega view before re-embed
(was leaking a view per toggle), and notesui-meter --valueclamps at 100
(put an over-target figure in the written label).docs/marks.md:ui-mark
is a behind-text highlight (contrast-safe; never needs--on-accent).
Internal
- New
check:versionsgate — every@ponchia/ui@X.Y.Zliteral in a shipped
doc (llms.txt,docs/reporting.md, …) must equalpackage.json, so a stale
CDN pin can't ship to LLM/copy-paste consumers on the next bump. - Dev-dependency Vega bumped to the v6 stack — the render-probe now runs on
vega@^6.2.0+vega-lite@^6.4.3(Vega-Lite 6 peers Vega 6; a Vega-Lite-6 ÷
Vega-5 mix is incoherent). The themeconfigis version-independent resolved
hex, so the artifacts and the probe assertions are unchanged; the documented
CDN recipe is re-pinned to the matching majors (vega@6.2.0/vega-lite@6.4.3
/vega-embed@7.1.0, all still shipping a UMD/build/*.min.js). Vega remains
the consumer's renderer, not a runtime dependency. - New
check:doc-recipesgate — a<script src>CDN recipe in a shipped doc
must pin a jsDelivr/build/*.min.jsUMD bundle, never a bare
cdn.jsdelivr.net/npm/<pkg>@Nredirect (which serves a module bundle with no
global and renders nothing). Docs are otherwise an untested surface; this is
the structural guard that closes the broken-recipe class the dogfood pass
found.<link href>CSS and prose mentions are exempt. classes.jsoncustomPropertiesexpanded to cover the load-bearing,
no-op-without-it knobs the audit found undocumented: the required
--icon-mask(a bare.ui-iconpaints a solid square without it) and
--ui-vt-name(.ui-vtis inert without it), plus--icon-size. The
statesmanifest comment now explicitly names the runtime-managed hooks it
deliberately excludes (is-leaving/is-visible/is-in/is-on) so the
omission reads as intentional, not a gap.--on-accentis annotated at its
token source as a read-only export for foreign renderers (in-DOM ink is
--button-text).contrast.mdnow prints APCALcto one decimal so an
advisory shortfall (e.g.Lc 44.9) no longer rounds to a passing-looking45.- Raw bundle budget 81 → 82 kB for the component-audit accessibility/state
blocks (gzip held ~14.1 kB — the additions are repetitive media-query and
:has()/:not()rules that compress well). - Code-quality audit (16-agent) — two new gates + targeted dedup, no churn.
A code-health pass (complexity / duplication / AI-slop / missing-best-practice)
that deliberately left working, gate-protected code alone. Added:
check:recipe-types(factory↔.d.tsoption parity, above) andcheck:chain
(everycheck:*script is wired into the aggregatecheckchain — closes the
silent-coverage-drop class; it would have caught a forgotten gate). Reconciled
a latent bug —clamp()had silently diverged betweenconnectorsand
annotations; the two now share one scalar/geometry kernel (the guarded form).
Dedup that removed real duplication: a sharedcollectHosts()/
scrollIntoViewSafe()/wrapIndex()inbehaviors/internal.js(~9 behaviors),
afreshnessErrors()helper reused by 7 drift gates, the sharedCSS_COLOR
regex across the 3 foreign-renderer gates,check-report's opt-in list as a
loop,check-pack's shipped-docs derived frompkg.files, and a looser
check-classesrecipe-scrape. README hero de-densified;srcTonematched to
stateTone's idiom; the intentional badge accent-mix (45% vs 40%) documented. check:distnow asserts source-coverage — everycss/*.cssleaf must be
bundled, an opt-inEXTRA_LEAVESentry, or a roll-up; an orphaned leaf that
would ship nothing now fails loudly (the inverse of the existing stale-dist
guard).check:dts-emitnow compares.d.ts.mapmapping data (volatilesources
path normalized), closing a drift hole the code comment had acknowledged.- DTCG export types
--display-weight*as the specfontWeighttype (was
number). Corrected stalecheck-tokens.mjsdoc references (the real gate is
check:fresh). - Tests: binding hook-surface parity is now derived from the modules (the old
hard-coded list silently omitted the five analytical hooks); a new
analytical-boundarytest makes the "no scales/state/fetch/global-hotkey"
contract executable; a new behavior test pins the null-root no-op. - Removed four dead keyframes (
scan/growBar/drawLine/pulseNode) from
motion.css. Raw bundle budget 80 → 81 kB for the accessibility blocks (gzip
held ~14.0 kB). classes.json--valueretargeted to.ui-meter__fill, .ui-progress__bar
(was the.ui-meter, .ui-progresstrack parent) — the custom property is read
on the fill child, so the machine-readable manifest now matches where an author
actually sets it.- New render-geometry e2e (
test/e2e/render-geometry.spec.mjs) — launches a
browser at the demo's real report primitives and asserts the.ui-meter__fill
/.ui-progress__barfills and the standalone.ui-srcpill paint a non-zero
box (viagetBoundingClientRect, not the inline-box-lying
getComputedStyle().inlineSize). Closes the validates-but-renders-nothing
category that hid the meter regression. The demo gains a standalone.ui-src
pill row to exercise it.