@ponchia/ui 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +230 -8
- package/MIGRATIONS.json +92 -0
- package/README.md +9 -6
- package/annotations/index.d.ts +280 -0
- package/annotations/index.js +522 -0
- package/behaviors/carousel.js +197 -0
- package/behaviors/combobox.js +195 -0
- package/behaviors/command.js +187 -0
- package/behaviors/connectors.js +96 -0
- package/behaviors/crosshair.js +58 -0
- package/behaviors/dialog.js +73 -0
- package/behaviors/disclosure.js +25 -0
- package/behaviors/dismissible.js +24 -0
- package/behaviors/forms.js +158 -0
- package/behaviors/glyph.js +109 -0
- package/behaviors/index.d.ts +79 -0
- package/behaviors/index.js +18 -1409
- package/behaviors/internal.js +50 -0
- package/behaviors/legend.js +46 -0
- package/behaviors/menu.js +46 -0
- package/behaviors/popover.js +108 -0
- package/behaviors/spotlight.js +53 -0
- package/behaviors/table.js +109 -0
- package/behaviors/tabs.js +103 -0
- package/behaviors/theme.js +82 -0
- package/behaviors/toast.js +152 -0
- package/classes/index.d.ts +280 -2
- package/classes/index.js +313 -2
- package/connectors/index.d.ts +71 -0
- package/connectors/index.js +179 -0
- package/css/analytical.css +21 -0
- package/css/annotations.css +292 -0
- package/css/command.css +97 -0
- package/css/connectors.css +93 -0
- package/css/crosshair.css +100 -0
- package/css/feedback.css +51 -0
- package/css/fonts.css +11 -7
- package/css/generated.css +117 -0
- package/css/legend.css +268 -0
- package/css/marks.css +144 -0
- package/css/primitives.css +18 -0
- package/css/report.css +12 -31
- package/css/selection.css +46 -0
- package/css/sources.css +179 -0
- package/css/spotlight.css +104 -0
- package/css/state.css +121 -0
- package/css/tokens.css +25 -37
- package/css/workbench.css +83 -0
- package/dist/bronto.css +1 -1
- package/dist/css/analytical.css +1 -0
- package/dist/css/annotations.css +1 -0
- package/dist/css/command.css +1 -0
- package/dist/css/connectors.css +1 -0
- package/dist/css/crosshair.css +1 -0
- package/dist/css/feedback.css +1 -1
- package/dist/css/fonts.css +1 -1
- package/dist/css/generated.css +1 -0
- package/dist/css/legend.css +1 -0
- package/dist/css/marks.css +1 -0
- package/dist/css/primitives.css +1 -1
- package/dist/css/report.css +1 -1
- package/dist/css/selection.css +1 -0
- package/dist/css/sources.css +1 -0
- package/dist/css/spotlight.css +1 -0
- package/dist/css/state.css +1 -0
- package/dist/css/workbench.css +1 -0
- package/docs/adr/0003-theme-model.md +7 -4
- package/docs/annotations.md +345 -0
- package/docs/architecture.md +202 -0
- package/docs/command.md +95 -0
- package/docs/connectors.md +91 -0
- package/docs/crosshair.md +63 -0
- package/docs/generated.md +91 -0
- package/docs/legends.md +168 -0
- package/docs/marks.md +86 -0
- package/docs/reference.md +309 -3
- package/docs/reporting.md +49 -14
- package/docs/selection.md +40 -0
- package/docs/sources.md +110 -0
- package/docs/spotlight.md +78 -0
- package/docs/stability.md +16 -1
- package/docs/state.md +85 -0
- package/docs/usage.md +22 -0
- package/docs/workbench.md +72 -0
- package/fonts/doto-400.woff2 +0 -0
- package/fonts/doto-500.woff2 +0 -0
- package/fonts/doto-600.woff2 +0 -0
- package/fonts/doto-700.woff2 +0 -0
- package/fonts/doto-800.woff2 +0 -0
- package/fonts/doto-900.woff2 +0 -0
- package/llms.txt +229 -6
- package/package.json +69 -4
- package/qwik/index.d.ts +5 -0
- package/qwik/index.js +20 -0
- package/react/index.d.ts +5 -0
- package/react/index.js +10 -0
- package/solid/index.d.ts +5 -0
- package/solid/index.js +10 -0
- package/tokens/index.js +9 -5
- package/fonts/doto-400.ttf +0 -0
- package/fonts/doto-500.ttf +0 -0
- package/fonts/doto-600.ttf +0 -0
- package/fonts/doto-700.ttf +0 -0
- package/fonts/doto-800.ttf +0 -0
- package/fonts/doto-900.ttf +0 -0
package/dist/css/report.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
@layer bronto{.ui-report{--report-gap: var(--space-lg);--report-measure: 74ch;--report-page-margin: 18mm;color: var(--text-soft);display: grid;gap: var(--report-gap);inline-size: min(100%,var(--report-width,72rem));margin-inline: auto;padding: var(--report-padding-block,var(--space-2xl)) var(--space-md)}.ui-report--compact{--report-gap: var(--space-md);--report-padding-block: var(--space-xl)}.ui-report--numbered{counter-reset: report-section}.ui-report__cover{align-content: end;border-block-end: 1px solid var(--line);display: grid;gap: var(--space-md);min-block-size: min(62vh,34rem);padding-block-end: var(--space-xl)}.ui-report__cover--compact{min-block-size: auto;padding-block: var(--space-xl) var(--space-lg)}.ui-report__header{align-items: end;border-block-end: 1px solid var(--line);display: flex;flex-wrap: wrap;gap: var(--space-md);justify-content: space-between;padding-block-end: var(--space-md)}.ui-report__title{color: var(--text);font-family: var(--display);font-size: calc(var(--text-xl) * 1.35);letter-spacing: 0;line-height: 1.05;margin: 0;max-inline-size: var(--report-measure);text-transform: uppercase;text-wrap: balance}.ui-report__subtitle{color: var(--text-dim);font-size: var(--text-lg);line-height: 1.5;margin: 0;max-inline-size: var(--report-measure)}.ui-report__meta{align-items: center;color: var(--text-dim);display: flex;flex-wrap: wrap;font-family: var(--mono);font-size: var(--text-2xs);gap: 0.55rem;letter-spacing: 0;list-style: none;margin: 0;padding: 0;text-transform: uppercase}.ui-report__meta > *{align-items: center;display: inline-flex;gap: 0.55rem}.ui-report__meta > :not(:last-child)::after{background: var(--line-strong);border-radius: 50%;block-size: 0.22rem;content: '';inline-size: 0.22rem}.ui-report__toc{border: 1px solid var(--line);border-radius: var(--radius-md);display: grid;gap: var(--space-xs);padding: var(--space-md)}.ui-report__toc ol,.ui-report__toc ul{display: grid;gap: 0.35rem;margin: 0;padding-inline-start: var(--space-md)}.ui-report__toc a{color: var(--text);text-decoration: underline;text-decoration-color: color-mix(in srgb,var(--accent) 45%,transparent);text-underline-offset: 0.2rem}.ui-report__summary,.ui-report__finding,.ui-report__evidence{background: var(--panel);border: 1px solid var(--line);border-radius: var(--radius-md);display: grid;gap: var(--space-sm);padding: var(--space-md)}.ui-report__summary{border-inline-start: 2px solid var(--accent)}.ui-report__finding{border-inline-start: 2px solid var(--line-strong)}.ui-report__evidence{background: var(--panel-soft)}.ui-report__evidence:has(> .ui-table-wrap:only-child){padding: 0}.ui-report__evidence:has(> .ui-table-wrap:only-child) > .ui-table-wrap{border: 0;border-radius: var(--radius-md)}.ui-report__section{display: grid;gap: var(--space-md);scroll-margin-block-start: 6rem}.ui-report__section + .ui-report__section{border-block-start: 1px solid var(--line);padding-block-start: var(--space-xl)}.ui-report__section-head{color: var(--text);font-family: var(--display);font-size: var(--text-xl);letter-spacing: 0;line-height: 1.1;margin: 0;text-transform: uppercase;text-wrap: balance}.ui-report--numbered .ui-report__section-head::before{color: var(--accent-text);content: counter(report-section,decimal-leading-zero) ' / ';counter-increment: report-section}.ui-report--numbered .ui-report__section--unnumbered > .ui-report__section-head::before{content: none;counter-increment: none}.ui-report__figure{display: grid;gap: var(--space-sm);margin: 0}.ui-report__caption,.ui-chart__caption{color: var(--text-dim);font-family: var(--mono);font-size: var(--text-2xs);letter-spacing: 0;line-height: 1.5;text-transform: uppercase}.ui-report__sources,.ui-report__appendix,.ui-report__footnotes{border-block-start: 1px solid var(--line);color: var(--text-dim);display: grid;font-size: var(--text-sm);gap: var(--space-sm);padding-block-start: var(--space-md)}.ui-chart{border: 1px solid var(--line);border-radius: var(--radius-md);display: grid;gap: var(--space-sm);padding: var(--space-md)}.ui-
|
|
1
|
+
@layer bronto{.ui-report{--report-gap: var(--space-lg);--report-measure: 74ch;--report-page-margin: 18mm;color: var(--text-soft);display: grid;gap: var(--report-gap);inline-size: min(100%,var(--report-width,72rem));margin-inline: auto;padding: var(--report-padding-block,var(--space-2xl)) var(--space-md)}.ui-report--compact{--report-gap: var(--space-md);--report-padding-block: var(--space-xl)}.ui-report--numbered{counter-reset: report-section}.ui-report__cover{align-content: end;border-block-end: 1px solid var(--line);display: grid;gap: var(--space-md);min-block-size: min(62vh,34rem);padding-block-end: var(--space-xl)}.ui-report__cover--compact{min-block-size: auto;padding-block: var(--space-xl) var(--space-lg)}.ui-report__header{align-items: end;border-block-end: 1px solid var(--line);display: flex;flex-wrap: wrap;gap: var(--space-md);justify-content: space-between;padding-block-end: var(--space-md)}.ui-report__title{color: var(--text);font-family: var(--display);font-size: calc(var(--text-xl) * 1.35);letter-spacing: 0;line-height: 1.05;margin: 0;max-inline-size: var(--report-measure);text-transform: uppercase;text-wrap: balance}.ui-report__subtitle{color: var(--text-dim);font-size: var(--text-lg);line-height: 1.5;margin: 0;max-inline-size: var(--report-measure)}.ui-report__meta{align-items: center;color: var(--text-dim);display: flex;flex-wrap: wrap;font-family: var(--mono);font-size: var(--text-2xs);gap: 0.55rem;letter-spacing: 0;list-style: none;margin: 0;padding: 0;text-transform: uppercase}.ui-report__meta > *{align-items: center;display: inline-flex;gap: 0.55rem}.ui-report__meta > :not(:last-child)::after{background: var(--line-strong);border-radius: 50%;block-size: 0.22rem;content: '';inline-size: 0.22rem}.ui-report__toc{border: 1px solid var(--line);border-radius: var(--radius-md);display: grid;gap: var(--space-xs);padding: var(--space-md)}.ui-report__toc ol,.ui-report__toc ul{display: grid;gap: 0.35rem;margin: 0;padding-inline-start: var(--space-md)}.ui-report__toc a{color: var(--text);text-decoration: underline;text-decoration-color: color-mix(in srgb,var(--accent) 45%,transparent);text-underline-offset: 0.2rem}.ui-report__summary,.ui-report__finding,.ui-report__evidence{background: var(--panel);border: 1px solid var(--line);border-radius: var(--radius-md);display: grid;gap: var(--space-sm);padding: var(--space-md)}.ui-report__summary{border-inline-start: 2px solid var(--accent)}.ui-report__finding{border-inline-start: 2px solid var(--line-strong)}.ui-report__evidence{background: var(--panel-soft)}.ui-report__evidence:has(> .ui-table-wrap:only-child){padding: 0}.ui-report__evidence:has(> .ui-table-wrap:only-child) > .ui-table-wrap{border: 0;border-radius: var(--radius-md)}.ui-report__section{display: grid;gap: var(--space-md);scroll-margin-block-start: 6rem}.ui-report__section + .ui-report__section{border-block-start: 1px solid var(--line);padding-block-start: var(--space-xl)}.ui-report__section-head{color: var(--text);font-family: var(--display);font-size: var(--text-xl);letter-spacing: 0;line-height: 1.1;margin: 0;text-transform: uppercase;text-wrap: balance}.ui-report--numbered .ui-report__section-head::before{color: var(--accent-text);content: counter(report-section,decimal-leading-zero) ' / ';counter-increment: report-section}.ui-report--numbered .ui-report__section--unnumbered > .ui-report__section-head::before{content: none;counter-increment: none}.ui-report__figure{display: grid;gap: var(--space-sm);margin: 0}.ui-report__caption,.ui-chart__caption{color: var(--text-dim);font-family: var(--mono);font-size: var(--text-2xs);letter-spacing: 0;line-height: 1.5;text-transform: uppercase}.ui-report__sources,.ui-report__appendix,.ui-report__footnotes{border-block-start: 1px solid var(--line);color: var(--text-dim);display: grid;font-size: var(--text-sm);gap: var(--space-sm);padding-block-start: var(--space-md)}.ui-chart{border: 1px solid var(--line);border-radius: var(--radius-md);display: grid;gap: var(--space-sm);padding: var(--space-md)}.ui-chart__plot{display: grid;gap: var(--space-xs)}.ui-chart__bar{--chart-value: 0%;display: grid;gap: 0.35rem}.ui-chart__bar[hidden]{display: none}.ui-chart__label{align-items: center;color: var(--text-soft);display: flex;font-family: var(--mono);font-size: var(--text-xs);gap: var(--space-sm);justify-content: space-between}.ui-chart__track{background: var(--panel-soft);border: 1px solid var(--line);block-size: 0.8rem;min-inline-size: 0}.ui-chart__fill{background: var(--chart-color,var(--chart-1,var(--accent)));background-image: var(--chart-pattern,var(--chart-pattern-1,none));background-size: var(--chart-pattern-size,8px);block-size: 100%;inline-size: clamp(0%,var(--chart-value),100%)}.ui-chart__fallback{margin-block-start: var(--space-sm)}.ui-print-only{display: none !important}.ui-print-exact{-webkit-print-color-adjust: exact;print-color-adjust: exact}@media print{@page{margin: var(--report-page-margin)}.ui-report{--report-padding-block: 0;color-scheme: light;--text: #111;--text-soft: #2a2a2a;--text-dim: #555;--panel: #fff;--panel-soft: #f7f7f7;--line: #d9d9d9;--line-strong: #b3b3b3;inline-size: auto;padding: 0}.ui-chart__fill{-webkit-print-color-adjust: exact;print-color-adjust: exact}.ui-report__cover{min-block-size: auto}.ui-report__cover,.ui-report__header,.ui-report__section,.ui-report__summary,.ui-report__finding,.ui-report__evidence,.ui-report__figure,.ui-chart{break-inside: avoid}.ui-report__section-head{break-after: avoid}.ui-print-only{display: var(--print-display,block) !important}.ui-screen-only{display: none !important}.ui-break-before{break-before: page}.ui-break-after{break-after: page}.ui-keep{break-inside: avoid}}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@layer bronto{.ui-sel{transition: opacity 0.12s ease,outline-color 0.12s ease}@media (prefers-reduced-motion: reduce){.ui-sel{transition: none}}.ui-sel--on{outline: 2px solid var(--accent);outline-offset: 1px}.ui-sel--off{opacity: 0.35}.ui-sel--maybe{outline: 1px dashed var(--accent);outline-offset: 1px}@media (forced-colors: active){.ui-sel--on{outline-color: Highlight}}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@layer bronto{.ui-citation,.ui-source-card,.ui-provenance{--src-tone: var(--text-dim)}.ui-src--verified{--src-tone: var(--success)}.ui-src--reviewed{--src-tone: var(--accent)}.ui-src--generated{--src-tone: var(--info)}.ui-src--unverified{--src-tone: var(--text-dim)}.ui-src--stale{--src-tone: var(--warning)}.ui-src--conflict{--src-tone: var(--danger)}.ui-citation{color: var(--accent-text);font-family: var(--mono);font-size: 0.72em;font-weight: 600;text-decoration: none;vertical-align: super}.ui-citation:hover{text-decoration: underline}.ui-citation--chip{align-items: center;background: var(--panel-soft);border: 1px solid var(--line);border-radius: var(--radius-pill);color: var(--text-soft);display: inline-flex;font-size: var(--text-2xs);gap: 0.35rem;padding: 0.08rem 0.55rem;vertical-align: baseline}.ui-citation--chip::before{background: var(--src-tone);border-radius: 50%;block-size: 0.45rem;content: '';inline-size: 0.45rem;print-color-adjust: exact}.ui-source-list{display: grid;gap: var(--space-sm);list-style: none;margin: 0;padding: 0}.ui-source-list__item{margin: 0}.ui-source-card{background: var(--panel-soft);border: 1px solid var(--line);border-inline-start: 2px solid var(--src-tone);border-radius: var(--radius-md);display: grid;gap: 0.3rem;padding: 0.75rem 0.9rem;print-color-adjust: exact}.ui-source-card__title{color: var(--text);font-size: var(--text-sm);font-weight: 600;margin: 0}.ui-source-card__origin{color: var(--text-soft);font-family: var(--mono);font-size: var(--text-2xs);letter-spacing: var(--tracking-wide)}.ui-source-card__time{color: var(--text-dim);font-family: var(--mono);font-size: var(--text-2xs)}.ui-source-card__excerpt{color: var(--text-soft);margin: 0}.ui-source-card__actions{display: flex;flex-wrap: wrap;gap: 0.5rem;margin-block-start: 0.2rem}.ui-provenance{align-items: center;color: var(--text-dim);display: inline-flex;flex-wrap: wrap;font-family: var(--mono);font-size: var(--text-2xs);gap: 0.3rem 0.75rem;letter-spacing: var(--tracking-wide)}.ui-provenance__item{align-items: center;display: inline-flex;gap: 0.35rem}.ui-provenance__item::before{background: var(--src-tone);border-radius: 50%;block-size: 0.45rem;content: '';inline-size: 0.45rem;print-color-adjust: exact}@media (forced-colors: active){.ui-citation--chip::before,.ui-provenance__item::before{background: CanvasText}.ui-source-card{border-inline-start-color: CanvasText}}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@layer bronto{.ui-spotlight{--spot-x: 50%;--spot-y: 50%;--spot-w: 0px;--spot-h: 0px;--spot-pad: 8px;--spot-radius: var(--radius-md);--spot-backdrop: color-mix(in srgb,#000 55%,transparent);inset: 0;pointer-events: none;position: fixed;z-index: var(--z-overlay,1000)}.ui-spotlight__hole{background: transparent;block-size: calc(var(--spot-h) + var(--spot-pad) * 2);border-radius: var(--spot-radius);box-shadow: 0 0 0 100vmax var(--spot-backdrop);inline-size: calc(var(--spot-w) + var(--spot-pad) * 2);inset-block-start: 0;inset-inline-start: 0;position: absolute;transform: translate( calc(var(--spot-x) - var(--spot-pad)),calc(var(--spot-y) - var(--spot-pad)) );transition: transform 0.2s ease,inline-size 0.2s ease,block-size 0.2s ease}@media (prefers-reduced-motion: reduce){.ui-spotlight__hole{transition: none}}.ui-spotlight--ring .ui-spotlight__hole{outline: 2px solid var(--accent);outline-offset: 0}.ui-tour-note{background: var(--panel);border: 1px solid var(--line);border-radius: var(--radius-md);box-shadow: var(--shadow-raised);color: var(--text);display: grid;gap: var(--space-sm);max-inline-size: 22rem;padding: var(--space-md);pointer-events: auto}.ui-tour-note__step{color: var(--text-dim);font-family: var(--mono);font-size: var(--text-2xs);letter-spacing: 0;text-transform: uppercase}.ui-tour-note__title{color: var(--text);font-size: var(--text-lg);font-weight: 600;margin: 0}.ui-tour-note__body{color: var(--text-soft);margin: 0}.ui-tour-note__actions{display: flex;gap: var(--space-sm);justify-content: flex-end}@media (forced-colors: active){.ui-spotlight__hole{outline: 2px solid CanvasText}}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@layer bronto{.ui-state{--state-tone: var(--text-dim);align-items: center;display: inline-flex;font-size: var(--text-sm);gap: 0.4rem}.ui-state::before{background: var(--state-tone);border-radius: 50%;block-size: 0.5rem;content: '';flex: none;inline-size: 0.5rem;print-color-adjust: exact}.ui-state__label{color: var(--text)}.ui-state__detail{color: var(--text-dim);font-size: var(--text-2xs)}.ui-state--busy::before{animation: uiStatePulse 1.1s ease-in-out infinite}@keyframes uiStatePulse{50%{opacity: 0.3}}@media (prefers-reduced-motion: reduce){.ui-state--busy::before{animation: none}}.ui-state--saving{--state-tone: var(--accent)}.ui-state--saved{--state-tone: var(--success)}.ui-state--queued{--state-tone: var(--text-dim)}.ui-state--offline{--state-tone: var(--warning)}.ui-state--stale{--state-tone: var(--warning)}.ui-state--conflict{--state-tone: var(--danger)}.ui-state--error{--state-tone: var(--danger)}.ui-state--locked{--state-tone: var(--text-dim)}.ui-state--reviewed{--state-tone: var(--success)}.ui-state--needs-review{--state-tone: var(--warning)}.ui-syncbar{align-items: center;border-block-end: 1px solid var(--line);display: flex;flex-wrap: wrap;gap: var(--space-sm) var(--space-md);justify-content: space-between;padding-block: var(--space-xs)}@media (forced-colors: active){.ui-state::before{background: CanvasText}}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@layer bronto{.ui-inspector{background: var(--panel);border: 1px solid var(--line);border-radius: var(--radius-md);display: grid;gap: var(--space-sm);padding: var(--space-md)}.ui-inspector__header{align-items: baseline;border-block-end: 1px solid var(--line);display: flex;gap: var(--space-sm);justify-content: space-between;padding-block-end: var(--space-xs)}.ui-inspector__body{display: grid;gap: var(--space-2xs)}.ui-property{align-items: baseline;display: grid;gap: var(--space-sm);grid-template-columns: minmax(6rem,38%) 1fr}.ui-property__label{color: var(--text-dim);font-family: var(--mono);font-size: var(--text-2xs);letter-spacing: var(--tracking-wide);text-transform: uppercase}.ui-property__value{color: var(--text);font-size: var(--text-sm);min-inline-size: 0}.ui-selectionbar{align-items: center;background: var(--panel-strong);border: 1px solid var(--line-strong);border-radius: var(--radius-md);box-shadow: var(--shadow-raised);display: flex;flex-wrap: wrap;gap: var(--space-sm) var(--space-md);justify-content: space-between;padding: 0.5rem 0.85rem}.ui-selectionbar__count{color: var(--text);font-family: var(--mono);font-size: var(--text-sm)}.ui-selectionbar__actions{display: flex;flex-wrap: wrap;gap: var(--space-xs)}}
|
|
@@ -77,10 +77,13 @@ early-warning that the Lc 36 regression would have tripped.
|
|
|
77
77
|
- Re-tuning dark token *values* changes the default rendered output in dark mode
|
|
78
78
|
→ the dark Playwright visual baselines must be regenerated in the pinned Linux
|
|
79
79
|
container (`visual-baselines.yml`). Kept in 0.4.1 per the maintainer's call.
|
|
80
|
-
- The dark palette
|
|
81
|
-
`[data-theme]` block, and the `cssVars.dark` JS mirror)
|
|
82
|
-
`
|
|
83
|
-
`tokens.css`
|
|
80
|
+
- The dark palette was originally written in three places (the `@media` block,
|
|
81
|
+
the `[data-theme]` block, and the `cssVars.dark` JS mirror). **Resolved in
|
|
82
|
+
0.4.2:** `tokens/index.js` (`cssVars`) is now the single source, and the four
|
|
83
|
+
`:root` palette blocks of `css/tokens.css` are generated from it
|
|
84
|
+
(`scripts/gen-tokens-css.mjs`); the two dark blocks are identical by
|
|
85
|
+
construction, so a dark edit is a one-place edit, still guarded by
|
|
86
|
+
`check:tokens`.
|
|
84
87
|
- `data-surface` joins `data-theme`/`data-bronto-skin`/`data-density`/
|
|
85
88
|
`data-contrast` as a documented root-level attribute (theming.md). The kitchen
|
|
86
89
|
sink ships a unified picker (theme × colorway × surface) so the full axis set
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
# SVG annotations
|
|
2
|
+
|
|
3
|
+
`@ponchia/ui/css/annotations.css` is an opt-in SVG annotation layer for charts,
|
|
4
|
+
reports, and analytical figures. It follows the same grammar as
|
|
5
|
+
d3-annotation: a **subject** marks the thing being discussed, a **connector**
|
|
6
|
+
points away from it, and a **note** carries the visible explanation.
|
|
7
|
+
|
|
8
|
+
```css
|
|
9
|
+
@import '@ponchia/ui';
|
|
10
|
+
@import '@ponchia/ui/css/annotations.css';
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Use it with any SVG renderer. Bronto supplies classes and tiny geometry helpers;
|
|
14
|
+
it does not own chart scales, mutate the DOM, or provide draggable edit mode.
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
import {
|
|
18
|
+
annotationParts,
|
|
19
|
+
annotationTransform,
|
|
20
|
+
axisThresholdPath,
|
|
21
|
+
bracketSubjectPath,
|
|
22
|
+
circleSubjectPath,
|
|
23
|
+
connectorLine,
|
|
24
|
+
evidenceMarkerPath,
|
|
25
|
+
notePlacement,
|
|
26
|
+
noteTransform,
|
|
27
|
+
} from '@ponchia/ui/annotations';
|
|
28
|
+
|
|
29
|
+
const transform = annotationTransform({ x: 180, y: 72 });
|
|
30
|
+
const subject = circleSubjectPath({ radius: 18 });
|
|
31
|
+
const connector = connectorLine({
|
|
32
|
+
dx: 88,
|
|
33
|
+
dy: -42,
|
|
34
|
+
subject: { type: 'circle', radius: 18, radiusPadding: 4 },
|
|
35
|
+
});
|
|
36
|
+
const parts = annotationParts({
|
|
37
|
+
x: 180,
|
|
38
|
+
y: 72,
|
|
39
|
+
dx: 88,
|
|
40
|
+
dy: -42,
|
|
41
|
+
subject: { type: 'circle', radius: 18, radiusPadding: 4 },
|
|
42
|
+
});
|
|
43
|
+
const note = notePlacement({
|
|
44
|
+
x: 180,
|
|
45
|
+
y: 72,
|
|
46
|
+
width: 96,
|
|
47
|
+
height: 44,
|
|
48
|
+
bounds: { x: 0, y: 0, width: 360, height: 180 },
|
|
49
|
+
preferred: 'right',
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Markup model
|
|
54
|
+
|
|
55
|
+
Author annotation groups at the subject anchor. Use `dx` / `dy` as the note
|
|
56
|
+
offset, matching d3-annotation's mental model.
|
|
57
|
+
|
|
58
|
+
```html
|
|
59
|
+
<svg viewBox="0 0 360 180" role="img" aria-labelledby="chart-title chart-desc">
|
|
60
|
+
<title id="chart-title">Annotated delivery chart</title>
|
|
61
|
+
<desc id="chart-desc">A callout marks the delivery peak.</desc>
|
|
62
|
+
|
|
63
|
+
<g
|
|
64
|
+
class="ui-annotation ui-annotation--circle ui-annotation--accent"
|
|
65
|
+
transform="translate(180, 72)"
|
|
66
|
+
>
|
|
67
|
+
<path class="ui-annotation__subject" d="M0,-18A18,18 0 1 1 0,18A18,18 0 1 1 0,-18Z" />
|
|
68
|
+
<path class="ui-annotation__connector" d="M15.556,-7.424L88,-42" />
|
|
69
|
+
<g class="ui-annotation__note" transform="translate(88, -42)">
|
|
70
|
+
<path class="ui-annotation__note-line" d="M0,0H76" />
|
|
71
|
+
<text class="ui-annotation__title" y="-8">Peak</text>
|
|
72
|
+
<text class="ui-annotation__label" y="12">Delivery spike</text>
|
|
73
|
+
</g>
|
|
74
|
+
</g>
|
|
75
|
+
</svg>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The visible note text should also be represented in the figure caption,
|
|
79
|
+
`<desc>`, fallback table, or surrounding prose when the figure is complex.
|
|
80
|
+
Use `aria-hidden="true"` only for decorative annotations.
|
|
81
|
+
|
|
82
|
+
## Variants and motion
|
|
83
|
+
|
|
84
|
+
Use one variant class per annotation group. Variants describe the visual
|
|
85
|
+
grammar, not data semantics:
|
|
86
|
+
|
|
87
|
+
| Variant | Use |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `ui-annotation--label` | Direct label with no connector. |
|
|
90
|
+
| `ui-annotation--callout` | Plain point-to-note callout. |
|
|
91
|
+
| `ui-annotation--elbow` | Dogleg connector around dense marks. |
|
|
92
|
+
| `ui-annotation--curve` | Softer connector for paths or flows. |
|
|
93
|
+
| `ui-annotation--circle` | Circular subject around a point or local cluster. |
|
|
94
|
+
| `ui-annotation--rect` | Rectangular subject around a bar, block, or region. |
|
|
95
|
+
| `ui-annotation--threshold` | Horizontal or vertical limit rule. |
|
|
96
|
+
| `ui-annotation--badge` | Compact numbered or categorical mark. |
|
|
97
|
+
| `ui-annotation--bracket` | Range span on one axis. |
|
|
98
|
+
| `ui-annotation--band` | Interval, confidence band, or risk window. |
|
|
99
|
+
| `ui-annotation--slope` | Trend or slope segment. |
|
|
100
|
+
| `ui-annotation--compare` | Before/after or A/B grouping. |
|
|
101
|
+
| `ui-annotation--cluster` | Several nearby outliers. |
|
|
102
|
+
| `ui-annotation--axis` | Axis milestone or reference tick. |
|
|
103
|
+
| `ui-annotation--timeline` | Event pin on a timeline. |
|
|
104
|
+
| `ui-annotation--evidence` | Proof, source, or evidence marker. |
|
|
105
|
+
|
|
106
|
+
Tones are `accent`, `muted`, `success`, `warning`, `danger`, and `info`.
|
|
107
|
+
Status tones (`success`, `warning`, `danger`, `info`) are only for annotations
|
|
108
|
+
that carry that status meaning.
|
|
109
|
+
|
|
110
|
+
Motion is opt-in and respects `prefers-reduced-motion`:
|
|
111
|
+
|
|
112
|
+
| Motion class | Effect |
|
|
113
|
+
| --- | --- |
|
|
114
|
+
| `ui-annotation--draw` | Connectors draw in once; subjects reveal without losing dashed variant styling. |
|
|
115
|
+
| `ui-annotation--reveal` | Note fades into place. |
|
|
116
|
+
| `ui-annotation--pulse` | Subject or badge pulses gently. |
|
|
117
|
+
| `ui-annotation--focus` | Static emphasis with a stronger subject stroke. |
|
|
118
|
+
|
|
119
|
+
The class recipe mirrors this surface:
|
|
120
|
+
|
|
121
|
+
```js
|
|
122
|
+
ui.annotation({ variant: 'bracket', tone: 'info', motion: 'draw' });
|
|
123
|
+
// "ui-annotation ui-annotation--bracket ui-annotation--info ui-annotation--draw"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Geometry helpers
|
|
127
|
+
|
|
128
|
+
The helper module returns SVG strings only. It does not know about scales,
|
|
129
|
+
selections, DOM nodes, or frameworks.
|
|
130
|
+
|
|
131
|
+
| Helper | Returns |
|
|
132
|
+
| --- | --- |
|
|
133
|
+
| `annotationTransform({ x, y })` | Group transform for the subject anchor. |
|
|
134
|
+
| `noteTransform({ dx, dy, align, valign, width, height })` | Note transform from the subject anchor, with optional alignment. |
|
|
135
|
+
| `notePlacement({ x, y, width, height, bounds, preferred })` | Bounded note offset, alignment and transform for one annotation. |
|
|
136
|
+
| `declutterLabels(items, { gap, min, max })` | Adjusted centres for `items` (`[{ pos, size }]`) so labels don't overlap along one axis (order-preserving). |
|
|
137
|
+
| `directLabels(items, { axis, cross, gap, min, max, shape })` | Decluttered label points **and** a leader path per item: `[{ x, y, anchor, key, d }]`. |
|
|
138
|
+
| `circleSubjectPath({ radius })` | Circle subject path. |
|
|
139
|
+
| `rectSubjectPath({ x, y, width, height, padding })` | Rect subject path. |
|
|
140
|
+
| `thresholdPath({ x1, y1, x2, y2 })` | Arbitrary threshold/rule path. |
|
|
141
|
+
| `axisThresholdPath({ orientation, value, start, end })` | Horizontal or vertical axis-aligned threshold. |
|
|
142
|
+
| `bracketSubjectPath({ x1, y1, x2, y2, depth })` | Dogleg bracket path. |
|
|
143
|
+
| `bandSubjectPath({ x, y, width, height, padding })` | Band or interval path. |
|
|
144
|
+
| `slopeSubjectPath({ x1, y1, x2, y2 })` | Trend segment path. |
|
|
145
|
+
| `comparisonBracePath({ x1, y1, x2, y2, depth })` | Comparison brace path. |
|
|
146
|
+
| `outlierClusterPath({ points, radius })` | Repeated circle subjects for a cluster. |
|
|
147
|
+
| `timelineEventPath({ size, direction })` | Event pin marker path. |
|
|
148
|
+
| `evidenceMarkerPath({ x, y, width, height, padding })` | Centered square/rect evidence marker path. |
|
|
149
|
+
| `connectorLine({ dx, dy, subject })` | Straight connector, trimmed against circle/rect subjects. |
|
|
150
|
+
| `connectorElbow({ dx, dy, subject })` | D3-like dogleg connector. |
|
|
151
|
+
| `connectorCurve({ dx, dy, subject })` | Deterministic cubic connector. |
|
|
152
|
+
| `connectorEndDot({ x, y, radius })` | Dot marker path. |
|
|
153
|
+
| `connectorEndArrow({ x1, y1, x2, y2, size })` | Arrow marker path. |
|
|
154
|
+
| `annotationParts(options)` | Convenience object with `transform`, `subject`, `connector`, and `note`. |
|
|
155
|
+
|
|
156
|
+
`declutterLabels` is a deliberately small, deterministic **1-D** declutter for
|
|
157
|
+
direct labels or axis ticks — sort, push overlaps apart by `size + gap`, slide
|
|
158
|
+
to fit under `max`. It is **not** a general 2-D collision solver: if more labels
|
|
159
|
+
are requested than the axis can hold, the overflow is yours to resolve (fewer
|
|
160
|
+
labels, a longer axis, or rotation). It returns numbers; you own the scale and
|
|
161
|
+
the DOM.
|
|
162
|
+
|
|
163
|
+
`directLabels` is the **direct-labeling** companion: it declutters labels along
|
|
164
|
+
one axis _and_ draws the leader from each true anchor to its placed label,
|
|
165
|
+
reusing the connector kernel. Each `items[i]` is `{ anchor: {x, y}, size, key? }`
|
|
166
|
+
in figure coordinates; labels declutter along `axis` (`'y'` = a vertical column,
|
|
167
|
+
the default) and sit at the fixed `cross` coordinate. It returns, in input
|
|
168
|
+
order, the placed label point `{ x, y }`, the echoed `anchor`/`key`, and the
|
|
169
|
+
leader path `d` (`shape`: `straight` · `elbow` · `curve`). Like everything here
|
|
170
|
+
it owns no scales, no DOM, and no 2-D placement — map data → figure coordinates
|
|
171
|
+
first, then drop each `d` into a `<path class="ui-annotation__connector">` and
|
|
172
|
+
position the label at `{ x, y }`:
|
|
173
|
+
|
|
174
|
+
```js
|
|
175
|
+
import { directLabels } from '@ponchia/ui/annotations';
|
|
176
|
+
|
|
177
|
+
// anchors are data points already projected into the figure's SVG coords
|
|
178
|
+
const labels = directLabels(
|
|
179
|
+
points.map((p) => ({ anchor: p, size: 18, key: p.id })),
|
|
180
|
+
{ axis: 'y', cross: width - 8, gap: 6, min: 12, max: height - 12 },
|
|
181
|
+
);
|
|
182
|
+
// labels[i] → { x, y, anchor, key, d }
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
All numeric inputs must be finite. Negative radius, width, height, padding, and
|
|
186
|
+
marker size throw `RangeError`; non-finite values throw `TypeError`. Path
|
|
187
|
+
numbers are rounded to three decimals with trailing zeros removed so snapshots
|
|
188
|
+
and unit tests stay stable.
|
|
189
|
+
|
|
190
|
+
`notePlacement()` is intentionally small: it places one note inside explicit SVG
|
|
191
|
+
bounds using a preferred side (`right`, `left`, `top`, or `bottom`) and falls
|
|
192
|
+
back to another side or a clamped note transform. It is not a collision solver
|
|
193
|
+
for a whole chart. For dense annotation sets, pre-compute positions or author a
|
|
194
|
+
mobile-specific SVG.
|
|
195
|
+
|
|
196
|
+
## Density and responsive rules
|
|
197
|
+
|
|
198
|
+
Annotations are strongest when they explain the few things a reader would miss.
|
|
199
|
+
As a default, keep a single chart to three to five visible callouts. Use direct
|
|
200
|
+
labels for stable context, one accent callout for the main insight, and status
|
|
201
|
+
tones only for genuine status.
|
|
202
|
+
|
|
203
|
+
Dense SVGs should not shrink until the notes become unreadable. Use one of
|
|
204
|
+
these patterns:
|
|
205
|
+
|
|
206
|
+
- Keep the chart wide in a horizontally scrollable figure and provide fallback
|
|
207
|
+
table text.
|
|
208
|
+
- Author a simpler mobile SVG with fewer annotations.
|
|
209
|
+
- Move low-priority annotation text into the caption or fallback table on small
|
|
210
|
+
screens.
|
|
211
|
+
|
|
212
|
+
## Recipes
|
|
213
|
+
|
|
214
|
+
### Label
|
|
215
|
+
|
|
216
|
+
Use `ui-annotation--label` for direct labels when the subject is already clear.
|
|
217
|
+
|
|
218
|
+
```html
|
|
219
|
+
<g class="ui-annotation ui-annotation--label ui-annotation--muted" transform="translate(112, 48)">
|
|
220
|
+
<g class="ui-annotation__note">
|
|
221
|
+
<text class="ui-annotation__title">Baseline</text>
|
|
222
|
+
<text class="ui-annotation__label" y="16">Previous quarter</text>
|
|
223
|
+
</g>
|
|
224
|
+
</g>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Circle callout
|
|
228
|
+
|
|
229
|
+
Use a circle subject when the referenced point or local cluster matters.
|
|
230
|
+
|
|
231
|
+
```html
|
|
232
|
+
<g class="ui-annotation ui-annotation--circle ui-annotation--accent" transform="translate(180, 72)">
|
|
233
|
+
<path class="ui-annotation__subject" d="M0,-18A18,18 0 1 1 0,18A18,18 0 1 1 0,-18Z" />
|
|
234
|
+
<path class="ui-annotation__connector" d="M15.556,-7.424L88,-42" />
|
|
235
|
+
<g class="ui-annotation__note" transform="translate(88, -42)">
|
|
236
|
+
<path class="ui-annotation__note-line" d="M0,0H84" />
|
|
237
|
+
<text class="ui-annotation__title" y="-8">Spike</text>
|
|
238
|
+
<text class="ui-annotation__label" y="12">Investigate change</text>
|
|
239
|
+
</g>
|
|
240
|
+
</g>
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Rect callout
|
|
244
|
+
|
|
245
|
+
Use a rect subject for a band, bar, table region, or evidence block inside an
|
|
246
|
+
SVG figure.
|
|
247
|
+
|
|
248
|
+
```html
|
|
249
|
+
<g class="ui-annotation ui-annotation--rect ui-annotation--warning" transform="translate(206, 92)">
|
|
250
|
+
<path class="ui-annotation__subject" d="M-34,-16H34V16H-34Z" />
|
|
251
|
+
<path class="ui-annotation__connector" d="M34,-16L72,-46" />
|
|
252
|
+
<g class="ui-annotation__note" transform="translate(72, -46)">
|
|
253
|
+
<path class="ui-annotation__note-line" d="M0,0H96" />
|
|
254
|
+
<text class="ui-annotation__title" y="-8">Watch</text>
|
|
255
|
+
<text class="ui-annotation__label" y="12">Lower confidence</text>
|
|
256
|
+
</g>
|
|
257
|
+
</g>
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Threshold
|
|
261
|
+
|
|
262
|
+
Use `ui-annotation--threshold` when a horizontal or vertical rule is the
|
|
263
|
+
subject.
|
|
264
|
+
|
|
265
|
+
```html
|
|
266
|
+
<g class="ui-annotation ui-annotation--threshold ui-annotation--danger" transform="translate(0, 96)">
|
|
267
|
+
<path class="ui-annotation__subject" d="M36,0L324,0" />
|
|
268
|
+
<path class="ui-annotation__connector" d="M240,0L282,-32" />
|
|
269
|
+
<g class="ui-annotation__note" transform="translate(282, -32)">
|
|
270
|
+
<text class="ui-annotation__title">Limit</text>
|
|
271
|
+
<text class="ui-annotation__label" y="16">Do not exceed</text>
|
|
272
|
+
</g>
|
|
273
|
+
</g>
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Badge
|
|
277
|
+
|
|
278
|
+
Use badges for compact numbered or categorical markers. Do not rely on the badge
|
|
279
|
+
color alone; pair it with visible text, a caption, or a table row.
|
|
280
|
+
|
|
281
|
+
```html
|
|
282
|
+
<g class="ui-annotation ui-annotation--badge ui-annotation--info" transform="translate(72, 84)">
|
|
283
|
+
<circle class="ui-annotation__badge" r="12" />
|
|
284
|
+
<text class="ui-annotation__title" text-anchor="middle" dominant-baseline="central">1</text>
|
|
285
|
+
</g>
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Chart figure recipe
|
|
289
|
+
|
|
290
|
+
Inside a report, keep the existing chart structure: caption, legend or direct
|
|
291
|
+
labels, annotated SVG, and fallback data. A useful annotated figure should show
|
|
292
|
+
more than one annotation family when the story needs it: direct labels for
|
|
293
|
+
stable references, threshold annotations for limits, circle/rect subjects for
|
|
294
|
+
specific data, and badge markers for compact index points.
|
|
295
|
+
|
|
296
|
+
```html
|
|
297
|
+
<figure class="ui-report__figure ui-chart ui-print-exact" role="group" aria-labelledby="annotated-chart">
|
|
298
|
+
<figcaption id="annotated-chart" class="ui-chart__caption">
|
|
299
|
+
Fig 2 - Weekly throughput, annotated at the peak
|
|
300
|
+
</figcaption>
|
|
301
|
+
<svg viewBox="0 0 360 160" role="img" aria-labelledby="throughput-title throughput-desc">
|
|
302
|
+
<title id="throughput-title">Weekly throughput with a peak callout</title>
|
|
303
|
+
<desc id="throughput-desc">Annotations mark the baseline, limit and highest research week.</desc>
|
|
304
|
+
<line x1="36" y1="112" x2="324" y2="112" stroke="var(--line)" />
|
|
305
|
+
<rect x="88" y="42" width="72" height="70" fill="var(--chart-1)" />
|
|
306
|
+
<rect x="188" y="70" width="72" height="42" fill="var(--chart-2)" />
|
|
307
|
+
<g class="ui-annotation ui-annotation--label ui-annotation--muted" transform="translate(36, 132)">
|
|
308
|
+
<g class="ui-annotation__note">
|
|
309
|
+
<text class="ui-annotation__title">Baseline</text>
|
|
310
|
+
<text class="ui-annotation__label" y="16">Previous quarter</text>
|
|
311
|
+
</g>
|
|
312
|
+
</g>
|
|
313
|
+
<g class="ui-annotation ui-annotation--threshold ui-annotation--danger" transform="translate(0, 66)">
|
|
314
|
+
<path class="ui-annotation__subject" d="M36,0L324,0" />
|
|
315
|
+
<path class="ui-annotation__connector" d="M272,0L304,-28" />
|
|
316
|
+
<g class="ui-annotation__note" transform="translate(234, -52)">
|
|
317
|
+
<text class="ui-annotation__title">Limit</text>
|
|
318
|
+
<text class="ui-annotation__label" y="16">Watch capacity</text>
|
|
319
|
+
</g>
|
|
320
|
+
</g>
|
|
321
|
+
<g class="ui-annotation ui-annotation--circle ui-annotation--accent" transform="translate(124, 42)">
|
|
322
|
+
<path class="ui-annotation__subject" d="M0,-18A18,18 0 1 1 0,18A18,18 0 1 1 0,-18Z" />
|
|
323
|
+
<path class="ui-annotation__connector" d="M16,-8L76,-36" />
|
|
324
|
+
<g class="ui-annotation__note" transform="translate(76, -36)">
|
|
325
|
+
<path class="ui-annotation__note-line" d="M0,0H80" />
|
|
326
|
+
<text class="ui-annotation__title" y="-8">Peak</text>
|
|
327
|
+
<text class="ui-annotation__label" y="12">Research high</text>
|
|
328
|
+
</g>
|
|
329
|
+
</g>
|
|
330
|
+
</svg>
|
|
331
|
+
<div class="ui-chart__fallback">
|
|
332
|
+
<div class="ui-table-wrap">
|
|
333
|
+
<table class="ui-table ui-table--dense">
|
|
334
|
+
<caption>Annotated chart source data</caption>
|
|
335
|
+
<thead><tr><th>Week</th><th class="is-num">Hours</th></tr></thead>
|
|
336
|
+
<tbody><tr><td>Week 4</td><td class="is-num">18</td></tr></tbody>
|
|
337
|
+
</table>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
</figure>
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Status tones (`success`, `warning`, `danger`, `info`) are only for annotations
|
|
344
|
+
that carry that status meaning. Use `accent` for the primary insight and
|
|
345
|
+
`muted` for secondary callouts.
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Architecture & Decisions
|
|
2
|
+
|
|
3
|
+
Status: accepted · 2026-05-15 · applies from v0.2.0
|
|
4
|
+
|
|
5
|
+
> **Separate ADRs.** Larger, self-contained decisions live under
|
|
6
|
+
> [`docs/adr/`](./adr/):
|
|
7
|
+
>
|
|
8
|
+
> - [ADR-0001 — Color system: governed evolution beyond monochrome](./adr/0001-color-system.md)
|
|
9
|
+
> (accepted; steps 1–8 implemented in 0.4.0) — the five-tier color
|
|
10
|
+
> constitution, the `check:color-policy`/`check:skins`/`check:charts`
|
|
11
|
+
> gates, opt-in colorways, data-viz, APCA advisory reporting, and the
|
|
12
|
+
> OKLCH core accent ramp.
|
|
13
|
+
|
|
14
|
+
## Context
|
|
15
|
+
|
|
16
|
+
`@ponchia/ui` is the shared design layer for several projects on
|
|
17
|
+
different stacks: Astro, SvelteKit, and an
|
|
18
|
+
open-ended set of future apps (React, Solid, Qwik, plain HTML, server-rendered
|
|
19
|
+
templates). The question driving this document: is plain CSS the right
|
|
20
|
+
universal substrate, or should the framework ship per-framework components?
|
|
21
|
+
|
|
22
|
+
## Decision
|
|
23
|
+
|
|
24
|
+
**Plain, class-based CSS is the canonical and only universal layer.** It is
|
|
25
|
+
the single artifact every target consumes natively with zero adapter. A
|
|
26
|
+
per-framework component library would make every non-chosen framework a
|
|
27
|
+
second-class citizen and multiply the maintenance surface for the same button.
|
|
28
|
+
|
|
29
|
+
The known gaps of a pure-CSS framework — contract visibility, a home for
|
|
30
|
+
unavoidable JS, and distribution — are addressed as **thin, optional layers
|
|
31
|
+
on top of the CSS, none of which require a framework commitment**:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
@ponchia/ui
|
|
35
|
+
├── css/ canonical universal layer (the framework) [required]
|
|
36
|
+
├── tokens/ design tokens as JS/JSON, for JS/canvas/tooling [optional]
|
|
37
|
+
├── classes/ typed class-name contract + recipe builders [optional]
|
|
38
|
+
├── behaviors/ vanilla, SSR-safe JS for stateful widgets [optional]
|
|
39
|
+
├── connectors/ pure SVG leader-line geometry kernel (no DOM) [optional]
|
|
40
|
+
├── annotations/ pure SVG callout geometry (builds on connectors) [optional]
|
|
41
|
+
├── glyphs/ dot-matrix glyph registry/renderers [optional]
|
|
42
|
+
├── react/ thin React hooks over behaviors [optional peer]
|
|
43
|
+
├── solid/ thin Solid primitives over behaviors [optional peer]
|
|
44
|
+
└── qwik/ thin Qwik hooks over behaviors (useVisibleTask$) [optional peer]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Consequences of each layer
|
|
48
|
+
|
|
49
|
+
- **css/** — wrapped in a single `@layer bronto`. Any un-layered CSS in a
|
|
50
|
+
consumer wins the cascade without specificity wars or `!important`. This is
|
|
51
|
+
a deliberate behavioural change vs. unlayered v0.1.0; consumers pin a tag
|
|
52
|
+
so it ships only on the next version bump.
|
|
53
|
+
- **Fonts** — `@font-face` moved out of `tokens.css` into `css/fonts.css`
|
|
54
|
+
with URLs relative to the package (`../fonts/*`), so font hosting is
|
|
55
|
+
decoupled from the token layer and resolves through bundlers or static
|
|
56
|
+
serving without an absolute `/fonts` assumption.
|
|
57
|
+
- **tokens/** — `index.js` (`cssVars`) is the single source of truth for token
|
|
58
|
+
values. The four `:root` palette blocks of `css/tokens.css` are **generated**
|
|
59
|
+
from it (`scripts/gen-tokens-css.mjs`), as are the JSON artifacts (`index.json`,
|
|
60
|
+
`tokens.dtcg.json`, `resolved.json`). So the dark palette is authored once,
|
|
61
|
+
not in three places (the two CSS dark blocks are now identical by
|
|
62
|
+
construction), resolving the duplication ADR-0003 flagged. The CSS-only
|
|
63
|
+
presets (density / contrast / OLED) stay hand-authored below a marker and are
|
|
64
|
+
preserved across regeneration. `scripts/check-tokens.mjs` fails CI if
|
|
65
|
+
`css/tokens.css` drifts from the model.
|
|
66
|
+
- **classes/** — `cls` is the flat registry; recipes only emit from it;
|
|
67
|
+
`scripts/check-classes.mjs` enforces a bidirectional match with the
|
|
68
|
+
stylesheet's `.ui-*` selectors. The class contract cannot silently rot.
|
|
69
|
+
- **behaviors/** — vanilla, dependency-free, side-effect-free on import,
|
|
70
|
+
SSR-safe. Chosen over Web Components (SSR/hydration friction with Astro
|
|
71
|
+
islands and SvelteKit) and over per-framework packages (maintenance
|
|
72
|
+
multiplier). Revisit Web Components only if stateful widgets accumulate.
|
|
73
|
+
`index.js` is a barrel; each behavior lives in its own module
|
|
74
|
+
(`dialog.js`, `combobox.js`, …) over a shared `internal.js` of DOM helpers,
|
|
75
|
+
so the public import surface is unchanged.
|
|
76
|
+
- **glyphs/** — static bitmap data and SSR-safe render helpers. The
|
|
77
|
+
256-cell DOM renderers are for display and solid inline icons; the `.ui-icon`
|
|
78
|
+
mask renderer is for dense icon-at-scale use.
|
|
79
|
+
- **react/** / **solid/** / **qwik/** — optional lifecycle adapters over `behaviors/`.
|
|
80
|
+
They do not define markup, own state, or fork behavior logic; they only run
|
|
81
|
+
the vanilla initializers on mount and cleanup on unmount/dispose.
|
|
82
|
+
|
|
83
|
+
## Drift control
|
|
84
|
+
|
|
85
|
+
Every data mirror is backed by a check wired into `npm run check`, run by CI
|
|
86
|
+
on every push/PR and again by `release.yml` before publish (see "Release
|
|
87
|
+
gating" below), so a version that fails any invariant never reaches npm.
|
|
88
|
+
|
|
89
|
+
| Invariant | Enforced by |
|
|
90
|
+
| ----------------------------------------------- | ------------------- |
|
|
91
|
+
| exports / import graph / `files` consistent | `check-exports.mjs` |
|
|
92
|
+
| `tokens.css` ⇄ `tokens/index.js` ⇄ `.json` | `check-tokens.mjs` |
|
|
93
|
+
| `classes` `cls` ⇄ `.ui-*` selectors | `check-classes.mjs` |
|
|
94
|
+
| `classes`/`tokens` `.d.ts` ⇄ JS runtime (exact) | `check-dts.mjs` |
|
|
95
|
+
| `annotations`/`connectors` hand-written `.d.ts` ⇄ exports | `check-helpers-dts.mjs` |
|
|
96
|
+
| legend swatch colours ⊆ `charts.js` · opt-in | `check-legend.mjs` |
|
|
97
|
+
| `tokens.dtcg.json` ⇄ token model | `check-dtcg.mjs` |
|
|
98
|
+
| color tokens tiered · no raw chromatic color in components | `check-color-policy.mjs` |
|
|
99
|
+
| `css/skins.css` ⇄ `tokens/skins.js` · colorways opt-in | `check-skins.mjs` |
|
|
100
|
+
| every shipped colorway accent meets its WCAG floor | `check-contrast.mjs` |
|
|
101
|
+
| `dataviz.css`/`charts.json`/`charts.d.ts` ⇄ `tokens/charts.js` · CVD-distinguishable · opt-in | `check-charts.mjs` |
|
|
102
|
+
| `shiki/nothing.json` valid + on rationed palette | `check-shiki.mjs` |
|
|
103
|
+
| `dist/*.css` == fresh build of `css/` + budget | `check-dist.mjs` |
|
|
104
|
+
| published tarball == intended `files` only | `check-pack.mjs` |
|
|
105
|
+
| published `.d.ts` compile + reject typos | `tsc` (`check:types`) |
|
|
106
|
+
| CSS style/correctness | Stylelint |
|
|
107
|
+
| non-CSS source style | Prettier (`check:format`) |
|
|
108
|
+
|
|
109
|
+
`check-dist` is the most supply-chain-critical row: `dist/bronto.css` is
|
|
110
|
+
the default `exports["."]` consumers actually load, so its byte-equality
|
|
111
|
+
to a fresh build of `css/` is what makes the committed bundle trustworthy.
|
|
112
|
+
The `check-dist` size ceiling (`BUDGET` in `build-dist.mjs`) is calibrated
|
|
113
|
+
to the current bundle with deliberate headroom — it is the consumer-facing
|
|
114
|
+
payload contract, raised only intentionally with a CHANGELOG note.
|
|
115
|
+
`check:types` compiles the published declarations against
|
|
116
|
+
`test/types.test-d.ts`, whose `@ts-expect-error`s would fail to compile
|
|
117
|
+
if the generated literal `cls`/token types stopped rejecting typos —
|
|
118
|
+
so the *value* of the generated `.d.ts` is itself gated, not just their
|
|
119
|
+
freshness (`check-dts`).
|
|
120
|
+
|
|
121
|
+
## Release gating
|
|
122
|
+
|
|
123
|
+
`release.yml` (on a pushed `v*` tag) is a five-job DAG, serialized by a
|
|
124
|
+
`concurrency: release-publish` group so two tags can't race the dist-tag
|
|
125
|
+
pointer:
|
|
126
|
+
|
|
127
|
+
- `validate` — read-only: `npm run check` + tag↔version match. `check`
|
|
128
|
+
includes `check:release`; for a prerelease tag the base version's
|
|
129
|
+
CHANGELOG section need only exist (`## Unreleased — x.y.z` is fine) —
|
|
130
|
+
only a stable release must carry a dated heading.
|
|
131
|
+
- `e2e` — Playwright (visual + axe a11y, both themes, cross-engine) in
|
|
132
|
+
the pinned `mcr.microsoft.com/playwright` container.
|
|
133
|
+
- `examples` — `needs: validate`: builds the downstream example
|
|
134
|
+
apps against the **packed tarball**, mirroring CI. Catches a broken
|
|
135
|
+
published surface (exports map / missing file / unresolved subpath)
|
|
136
|
+
that `check:pack`'s file-allowlist inspection cannot — so the release
|
|
137
|
+
path runs the same consumer smoke as merge-to-main.
|
|
138
|
+
- `publish-npm` — `needs: [validate, e2e, examples]`: `npm publish` with
|
|
139
|
+
provenance. Runs in the `npm-publish` **Environment** (required-reviewer
|
|
140
|
+
protection), so after the gates pass the run pauses for a manual approval
|
|
141
|
+
in the Actions UI before anything reaches npm — a guard against an
|
|
142
|
+
accidental tag push publishing. Dist-tag is derived from the tag: stable
|
|
143
|
+
(`v0.4.0`) → `latest`; SemVer prerelease (`v0.4.0-rc.1`, any hyphenated
|
|
144
|
+
identifier) → `next`, so the default `npm i @ponchia/ui` never moves onto
|
|
145
|
+
an unstable build (opt in with `@ponchia/ui@next`).
|
|
146
|
+
- `release-notes` — `needs: publish-npm`: a GitHub Release for visibility
|
|
147
|
+
(transitively gated on a successful publish, hence on the gates above);
|
|
148
|
+
prerelease tags are flagged so they aren't surfaced as "Latest". The Release
|
|
149
|
+
**body is the curated `CHANGELOG.md` section** for the tag
|
|
150
|
+
(`scripts/changelog-section.mjs`), not GitHub's auto-generated PR list — one
|
|
151
|
+
source of truth, surfaced where readers look.
|
|
152
|
+
|
|
153
|
+
Because the documented install path is the npm package, **the npm publish
|
|
154
|
+
is a real gate**: if `validate`, `e2e`, *or* `examples` fails,
|
|
155
|
+
`publish-npm` never runs, the version never reaches the registry, and
|
|
156
|
+
consumers never resolve it.
|
|
157
|
+
(Corollary: a flaky `e2e` blocks releases — that is deliberate; fix the
|
|
158
|
+
flake, don't bypass the gate.) Permissions are least-privilege per job
|
|
159
|
+
(only `release-notes` gets `contents: write`; only `publish-npm` gets
|
|
160
|
+
`id-token: write` for provenance).
|
|
161
|
+
|
|
162
|
+
GitHub still serves the raw tag tarball `archive/refs/tags/vX.Y.Z.tar.gz`
|
|
163
|
+
for any tag, ungated — that path is legacy/fallback, deliberately *not* the
|
|
164
|
+
documented install, so it is no longer the safeguard-critical surface.
|
|
165
|
+
Process still applies: bump `package.json`, land on `main`, go green, tag.
|
|
166
|
+
|
|
167
|
+
## Decision — distribution: npm public `@ponchia/ui`
|
|
168
|
+
|
|
169
|
+
Decided 2026-05-15. The framework is consumed by a growing set of
|
|
170
|
+
heterogeneous web frontends (Astro, SvelteKit, React, Solid, Qwik, vanilla),
|
|
171
|
+
several deploying via third-party CI. The only option where onboarding a new
|
|
172
|
+
frontend is `npm i @ponchia/ui` with zero per-consumer config is **npm
|
|
173
|
+
public**, and it uniquely also closes the release-gating gap (publish *is*
|
|
174
|
+
the gate). GitHub Packages was rejected: it requires auth to install even
|
|
175
|
+
public packages, i.e. an `.npmrc` + token on every frontend and CI runner —
|
|
176
|
+
the exact friction to avoid. The raw tag tarball is kept as an ungated
|
|
177
|
+
legacy/fallback only.
|
|
178
|
+
|
|
179
|
+
The npm scope `@bronto` is not ownable, so the package name is
|
|
180
|
+
**`@ponchia/ui`**. Naming layers, intentionally distinct:
|
|
181
|
+
|
|
182
|
+
- **npm package**: `@ponchia/ui` (registry identity).
|
|
183
|
+
- **CSS cascade layer**: `@layer bronto` and `data-bronto-*` behavior
|
|
184
|
+
attributes (the design-system namespace — unchanged; renaming gains
|
|
185
|
+
nothing and risks consumer overrides).
|
|
186
|
+
- **Workspace / brand**: "Bronto" (repo `Ponchia/bronto-ui`) — unchanged.
|
|
187
|
+
|
|
188
|
+
This split is deliberate; the README states it so the apparent mismatch is
|
|
189
|
+
explained, not surprising.
|
|
190
|
+
|
|
191
|
+
### Post-publish checklist
|
|
192
|
+
|
|
193
|
+
- Confirm npm `latest` points at the tagged version and the package page shows
|
|
194
|
+
provenance.
|
|
195
|
+
- Run `npm pack --dry-run --json` locally or from CI logs and confirm the
|
|
196
|
+
intended file count/payload.
|
|
197
|
+
- Build the packed examples matrix (vanilla, Astro, SvelteKit, React, Solid, Qwik)
|
|
198
|
+
from the tarball, not a workspace link.
|
|
199
|
+
- Confirm the GitHub Release body matches the curated changelog section.
|
|
200
|
+
- If a bad package is published, deprecate that exact version on npm, publish a
|
|
201
|
+
patched version, and link the deprecation note to the changelog/security
|
|
202
|
+
advisory as appropriate.
|