Skip to content

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 03 Jun 17:17
· 38 commits to main since this release
7d57ead

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.50.6).

Added

  • @ponchia/ui/vega (+ vega.json) — an on-brand Vega-Lite / Vega
    config resolved per
    theme (the idiomatic vega-themes shape): monochrome chrome + one rationed
    accent, range.category/ordinal/ramp/heatmap/diverging from the CVD-safe
    data-viz palette. brontoVegaConfig(theme). Resolved hex (Vega bakes colours
    into SVG/canvas, can't read var()); 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. See docs/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 --invert to 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
    ESM cls map or parsing the .d.ts. Generated from cls; drift-checked and
    its states/customProperties gated against the stylesheet.
  • tokens/resolved.json scale block — 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 a spread half-angle on both
    connectorEndArrow and the shared arrowHead kernel.
  • brontoVegaAccent(theme) / brontoVegaNeutral(theme) (@ponchia/ui/vega)
    — the exact per-theme hexes for range.category's accent (series 1) and
    neutral (last series), so spending the accent on one emphasised mark needs no
    palette-index reverse-engineering.
  • --on-accent token — 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-src standalone 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-tone with no
    standalone host, so a lone pill validated against classes.json yet 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__figure with a
    .ui-report__caption and a .ui-legend key. The --chart-value inline knob
    is gone; the --chart-color/--chart-pattern swatch knobs remain (legend).
    See MIGRATIONS.json (0.50.6) and docs/vega.md.

Changed

  • Annotation connectors are crisper. connectorEndArrow now defaults to a
    sharper head (half-angle 0.32 ≈ 37°, size 8 vs the former blunt 0.45 / 7).
    Author-facing geometry only; the arrowHead kernel 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__button under @media (pointer: coarse) — the primary nav
    affordances were below the 44 px target on touch.
  • App shell uses dynamic viewport units. 100vh100dvh (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/--accent now 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-visible gets the same
    row highlight as hover; the segmented control's focus ring is now inset so the
    container's overflow: hidden no longer clips it.
  • Reduced-motion skeleton. .ui-skeleton flattens to a solid placeholder
    under prefers-reduced-motion instead 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.ts tone unions (hand-mirrored in gen-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 new check:recipe-types gate cross-checks every
    factory's string-literal options against its *Opts union 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-link now 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__option via :has(input:disabled),
      plus .ui-range / .ui-file) — they previously looked operable and their
      label kept cursor: 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.css to
      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__bar get a system colour under
      forced-colors, so the measured proportion stays visible.
    • .ui-search gains a 2px keyboard focus ring to match every sibling input
      (it had only a 1px border-colour shift).
    • .ui-prose gets overflow-wrap: break-word — long tokens in
      machine-generated Markdown forced horizontal page scroll.
    • .ui-mark--draw is scoped to fill styles (:not(--underline, --box, --strike))
      so it no longer looks applied while doing nothing.
    • .ui-cq hardcodes its container-name (the @container bronto collapse
      queries hardcode it, so a --cq-name override silently killed the collapse).
    • initPopover() seeds resting ARIA (aria-haspopup, aria-controls,
      aria-expanded) and syncs aria-expanded when the UA closes a native
      popover; toast() validates tone (an unknown string rendered an unstyled
      neutral toast) and warns; the combobox listbox gets an accessible name.
    • .ui-error-summary__title uses the legible sans, not the low-legibility Doto
      display face. .ui-input / .ui-search autofill stays on-theme.
    • .ui-reveal hidden state is gated on scripting: enabled (genuinely degrades
      visible with no JS; the prior comment lied) — and ui-scroll-reveal is 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 carry print-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-citation no longer dumps its full URL mid-sentence when printed
    (the reference list carries the URL); ui-legend--with-values values are
    right-aligned for a clean tabular column.
  • Annotation elbow connector was a 45° chamfer, not a dogleg.
    connectorElbow turned by min(|dx|,|dy|), drawing a diagonal stub the
    stroke-linejoin bevel never matched. It now delegates to the connectors
    geometry kernel's right-angle elbowPath (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 still null at mount, a conditional that hasn't rendered) now no-ops
    instead of silently widening to document-wide delegation. The react/solid/qwik
    bindings emit root: null for the not-ready case so the distinction survives
    the boundary; passing no root still delegates from document exactly as
    before. Affects every delegated behavior (dialog, menu, combobox, …).
  • --report-width / --report-padding-block are now declared defaults on
    .ui-report — they were read with inline fallbacks but never declared, so the
    override surface was undiscoverable and --report-measure looked 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-progress fill painted a 0×0 box. .ui-meter__fill and
    .ui-progress__bar set block-size/inline-size but no display, 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 now display: 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_modules vs CDN) and a copy-pasteable CDN report in
    docs/reporting.md; clarified that dist/bronto.css does 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-open state hooks are valid even
    though they deliberately live outside cls (documented in
    docs/reference.md and classes.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/command are adjacent leaves imported individually — and the root
    . export is CSS-only (no runtime JS at the root). Pre-1.0 stability/pinning
    spelled out in docs/stability.md. docs/workbench.md notes that
    .ui-selectionbar is 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.js UMD bundles and renderer:'svg'
    (a bare cdn.jsdelivr.net/npm/vega@6 tag has no window.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.md recipes: "Theming a live report" (the theme-toggle/re-embed
    foot-guns — clear the host, container-width-while-hidden, Mermaid source vs
    output), live charts are ui-screen-only while the table prints (a kept live
    chart bakes the on-screen theme), ui-meter/ui-quote markup, and the
    sequential/diverging frozen-figure ramp. docs/d2.md gains 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.md states the rule in both directions: a data annotation
    must stay readable (not aria-hidden), a decorative one must be hidden.
  • A second dogfood pass closed the foreign-renderer/contract gaps it found.
    docs/sources.md + llms.txt now document the standalone .ui-src trust
    pill and state that a ui-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/timeline are not covered by the base themeVariables (they
    render with Mermaid's own defaults — prefer the native ui-timeline for a
    report). docs/mermaid.md + docs/d2.md gain the same file:// CORS caveat
    Vega carries (inline the map or pre-render). docs/vega.md: select the themed
    ramp with scale: { range: 'heatmap' }not scheme:, which throws — and
    the accent/neutral series map to --chart-1 / --chart-8, so a legend keys
    them with ui-legend__swatch--1/--8 (docs/legends.md). docs/reporting.md:
    the live-theme recipe now finalize()s the prior Vega view before re-embed
    (was leaking a view per toggle), and notes ui-meter --value clamps 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:versions gate — every @ponchia/ui@X.Y.Z literal in a shipped
    doc (llms.txt, docs/reporting.md, …) must equal package.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 theme config is 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-recipes gate — a <script src> CDN recipe in a shipped doc
    must pin a jsDelivr /build/*.min.js UMD bundle, never a bare
    cdn.jsdelivr.net/npm/<pkg>@N redirect (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.json customProperties expanded to cover the load-bearing,
    no-op-without-it knobs the audit found undocumented: the required
    --icon-mask (a bare .ui-icon paints a solid square without it) and
    --ui-vt-name (.ui-vt is inert without it), plus --icon-size. The
    states manifest 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-accent is annotated at its
    token source as a read-only export for foreign renderers (in-DOM ink is
    --button-text). contrast.md now prints APCA Lc to one decimal so an
    advisory shortfall (e.g. Lc 44.9) no longer rounds to a passing-looking 45.
  • 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.ts option parity, above) and check:chain
    (every check:* script is wired into the aggregate check chain — closes the
    silent-coverage-drop class; it would have caught a forgotten gate). Reconciled
    a latent bug — clamp() had silently diverged between connectors and
    annotations; the two now share one scalar/geometry kernel (the guarded form).
    Dedup that removed real duplication: a shared collectHosts() /
    scrollIntoViewSafe() / wrapIndex() in behaviors/internal.js (~9 behaviors),
    a freshnessErrors() helper reused by 7 drift gates, the shared CSS_COLOR
    regex across the 3 foreign-renderer gates, check-report's opt-in list as a
    loop, check-pack's shipped-docs derived from pkg.files, and a looser
    check-classes recipe-scrape. README hero de-densified; srcTone matched to
    stateTone's idiom; the intentional badge accent-mix (45% vs 40%) documented.
  • check:dist now asserts source-coverage — every css/*.css leaf must be
    bundled, an opt-in EXTRA_LEAVES entry, or a roll-up; an orphaned leaf that
    would ship nothing now fails loudly (the inverse of the existing stale-dist
    guard).
  • check:dts-emit now compares .d.ts.map mapping data (volatile sources
    path normalized), closing a drift hole the code comment had acknowledged.
  • DTCG export types --display-weight* as the spec fontWeight type (was
    number). Corrected stale check-tokens.mjs doc 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-boundary test 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 --value retargeted to .ui-meter__fill, .ui-progress__bar
    (was the .ui-meter, .ui-progress track 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__bar fills and the standalone .ui-src pill paint a non-zero
    box (via getBoundingClientRect, 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.