@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.
Files changed (105) hide show
  1. package/CHANGELOG.md +230 -8
  2. package/MIGRATIONS.json +92 -0
  3. package/README.md +9 -6
  4. package/annotations/index.d.ts +280 -0
  5. package/annotations/index.js +522 -0
  6. package/behaviors/carousel.js +197 -0
  7. package/behaviors/combobox.js +195 -0
  8. package/behaviors/command.js +187 -0
  9. package/behaviors/connectors.js +96 -0
  10. package/behaviors/crosshair.js +58 -0
  11. package/behaviors/dialog.js +73 -0
  12. package/behaviors/disclosure.js +25 -0
  13. package/behaviors/dismissible.js +24 -0
  14. package/behaviors/forms.js +158 -0
  15. package/behaviors/glyph.js +109 -0
  16. package/behaviors/index.d.ts +79 -0
  17. package/behaviors/index.js +18 -1409
  18. package/behaviors/internal.js +50 -0
  19. package/behaviors/legend.js +46 -0
  20. package/behaviors/menu.js +46 -0
  21. package/behaviors/popover.js +108 -0
  22. package/behaviors/spotlight.js +53 -0
  23. package/behaviors/table.js +109 -0
  24. package/behaviors/tabs.js +103 -0
  25. package/behaviors/theme.js +82 -0
  26. package/behaviors/toast.js +152 -0
  27. package/classes/index.d.ts +280 -2
  28. package/classes/index.js +313 -2
  29. package/connectors/index.d.ts +71 -0
  30. package/connectors/index.js +179 -0
  31. package/css/analytical.css +21 -0
  32. package/css/annotations.css +292 -0
  33. package/css/command.css +97 -0
  34. package/css/connectors.css +93 -0
  35. package/css/crosshair.css +100 -0
  36. package/css/feedback.css +51 -0
  37. package/css/fonts.css +11 -7
  38. package/css/generated.css +117 -0
  39. package/css/legend.css +268 -0
  40. package/css/marks.css +144 -0
  41. package/css/primitives.css +18 -0
  42. package/css/report.css +12 -31
  43. package/css/selection.css +46 -0
  44. package/css/sources.css +179 -0
  45. package/css/spotlight.css +104 -0
  46. package/css/state.css +121 -0
  47. package/css/tokens.css +25 -37
  48. package/css/workbench.css +83 -0
  49. package/dist/bronto.css +1 -1
  50. package/dist/css/analytical.css +1 -0
  51. package/dist/css/annotations.css +1 -0
  52. package/dist/css/command.css +1 -0
  53. package/dist/css/connectors.css +1 -0
  54. package/dist/css/crosshair.css +1 -0
  55. package/dist/css/feedback.css +1 -1
  56. package/dist/css/fonts.css +1 -1
  57. package/dist/css/generated.css +1 -0
  58. package/dist/css/legend.css +1 -0
  59. package/dist/css/marks.css +1 -0
  60. package/dist/css/primitives.css +1 -1
  61. package/dist/css/report.css +1 -1
  62. package/dist/css/selection.css +1 -0
  63. package/dist/css/sources.css +1 -0
  64. package/dist/css/spotlight.css +1 -0
  65. package/dist/css/state.css +1 -0
  66. package/dist/css/workbench.css +1 -0
  67. package/docs/adr/0003-theme-model.md +7 -4
  68. package/docs/annotations.md +345 -0
  69. package/docs/architecture.md +202 -0
  70. package/docs/command.md +95 -0
  71. package/docs/connectors.md +91 -0
  72. package/docs/crosshair.md +63 -0
  73. package/docs/generated.md +91 -0
  74. package/docs/legends.md +168 -0
  75. package/docs/marks.md +86 -0
  76. package/docs/reference.md +309 -3
  77. package/docs/reporting.md +49 -14
  78. package/docs/selection.md +40 -0
  79. package/docs/sources.md +110 -0
  80. package/docs/spotlight.md +78 -0
  81. package/docs/stability.md +16 -1
  82. package/docs/state.md +85 -0
  83. package/docs/usage.md +22 -0
  84. package/docs/workbench.md +72 -0
  85. package/fonts/doto-400.woff2 +0 -0
  86. package/fonts/doto-500.woff2 +0 -0
  87. package/fonts/doto-600.woff2 +0 -0
  88. package/fonts/doto-700.woff2 +0 -0
  89. package/fonts/doto-800.woff2 +0 -0
  90. package/fonts/doto-900.woff2 +0 -0
  91. package/llms.txt +229 -6
  92. package/package.json +69 -4
  93. package/qwik/index.d.ts +5 -0
  94. package/qwik/index.js +20 -0
  95. package/react/index.d.ts +5 -0
  96. package/react/index.js +10 -0
  97. package/solid/index.d.ts +5 -0
  98. package/solid/index.js +10 -0
  99. package/tokens/index.js +9 -5
  100. package/fonts/doto-400.ttf +0 -0
  101. package/fonts/doto-500.ttf +0 -0
  102. package/fonts/doto-600.ttf +0 -0
  103. package/fonts/doto-700.ttf +0 -0
  104. package/fonts/doto-800.ttf +0 -0
  105. package/fonts/doto-900.ttf +0 -0
@@ -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-chart__legend{display: flex;flex-wrap: wrap;gap: var(--space-xs);list-style: none;margin: 0;padding: 0}.ui-chart__legend > *{align-items: center;color: var(--text-soft);display: inline-flex;font-family: var(--mono);font-size: var(--text-xs);gap: 0.45rem}.ui-chart__swatch{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);border: 1px solid var(--line-strong);block-size: 0.8rem;flex: 0 0 auto;inline-size: 0.8rem}.ui-chart__plot{display: grid;gap: var(--space-xs)}.ui-chart__bar{--chart-value: 0%;display: grid;gap: 0.35rem}.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,.ui-chart__swatch{-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}}}
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 is written in three places (the `@media` block, the
81
- `[data-theme]` block, and the `cssVars.dark` JS mirror), guarded by
82
- `check:tokens`. Any dark edit is a three-way edit; deduping by generating
83
- `tokens.css` from the JS model is a tracked follow-up.
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.