@ponchia/ui 0.6.5 → 0.6.7

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 (108) hide show
  1. package/CHANGELOG.md +170 -0
  2. package/README.md +43 -23
  3. package/behaviors/carousel.d.ts.map +1 -1
  4. package/behaviors/carousel.js +3 -0
  5. package/behaviors/dialog.d.ts.map +1 -1
  6. package/behaviors/dialog.js +14 -8
  7. package/behaviors/forms.d.ts.map +1 -1
  8. package/behaviors/forms.js +11 -5
  9. package/behaviors/index.d.ts +2 -0
  10. package/behaviors/index.d.ts.map +1 -1
  11. package/behaviors/index.js +2 -0
  12. package/behaviors/internal.d.ts +2 -1
  13. package/behaviors/internal.d.ts.map +1 -1
  14. package/behaviors/internal.js +23 -3
  15. package/behaviors/legend.d.ts.map +1 -1
  16. package/behaviors/legend.js +41 -9
  17. package/behaviors/splitter.d.ts +26 -0
  18. package/behaviors/splitter.d.ts.map +1 -0
  19. package/behaviors/splitter.js +200 -0
  20. package/behaviors/table.js +3 -3
  21. package/behaviors/theme.js +2 -2
  22. package/classes/classes.json +230 -4
  23. package/classes/index.d.ts +49 -1
  24. package/classes/index.js +56 -1
  25. package/classes/vscode.css-custom-data.json +1 -1
  26. package/css/analytical.css +3 -1
  27. package/css/app.css +4 -4
  28. package/css/clamp.css +92 -0
  29. package/css/figure.css +102 -0
  30. package/css/highlights.css +50 -0
  31. package/css/interval.css +90 -0
  32. package/css/primitives.css +2 -3
  33. package/css/report-kit.css +38 -0
  34. package/css/report.css +51 -4
  35. package/css/sidenote.css +12 -2
  36. package/css/site.css +2 -1
  37. package/css/sources.css +5 -0
  38. package/css/state.css +120 -1
  39. package/css/table.css +4 -0
  40. package/css/tokens.css +9 -9
  41. package/css/workbench.css +101 -8
  42. package/dist/bronto.css +1 -1
  43. package/dist/css/analytical.css +1 -1
  44. package/dist/css/app.css +1 -1
  45. package/dist/css/clamp.css +1 -0
  46. package/dist/css/figure.css +1 -0
  47. package/dist/css/highlights.css +1 -0
  48. package/dist/css/interval.css +1 -0
  49. package/dist/css/primitives.css +1 -1
  50. package/dist/css/report-kit.css +1 -0
  51. package/dist/css/report.css +1 -1
  52. package/dist/css/sidenote.css +1 -1
  53. package/dist/css/site.css +1 -1
  54. package/dist/css/sources.css +1 -1
  55. package/dist/css/state.css +1 -1
  56. package/dist/css/table.css +1 -1
  57. package/dist/css/tokens.css +1 -1
  58. package/dist/css/workbench.css +1 -1
  59. package/docs/adr/0002-scope-and-2026-baseline.md +1 -1
  60. package/docs/architecture.md +67 -43
  61. package/docs/clamp.md +49 -0
  62. package/docs/contrast.md +34 -24
  63. package/docs/d2.md +37 -0
  64. package/docs/figure.md +71 -0
  65. package/docs/frontier-primitives.md +48 -23
  66. package/docs/highlights.md +52 -0
  67. package/docs/interop/tailwind.md +148 -0
  68. package/docs/interval.md +55 -0
  69. package/docs/legends.md +3 -2
  70. package/docs/mermaid.md +6 -0
  71. package/docs/migrations/0.2-to-0.3.md +80 -0
  72. package/docs/migrations/0.3-to-0.4.md +48 -0
  73. package/docs/migrations/0.4-to-0.5.md +96 -0
  74. package/docs/migrations/0.5-to-0.6.md +82 -0
  75. package/docs/package-contract.md +40 -2
  76. package/docs/reference.md +79 -6
  77. package/docs/reporting.md +132 -56
  78. package/docs/sidenote.md +7 -1
  79. package/docs/sources.md +1 -1
  80. package/docs/stability.md +5 -3
  81. package/docs/state.md +67 -10
  82. package/docs/theming.md +10 -2
  83. package/docs/usage.md +31 -11
  84. package/docs/workbench.md +59 -18
  85. package/llms.txt +82 -14
  86. package/package.json +68 -6
  87. package/qwik/index.d.ts +1 -0
  88. package/qwik/index.d.ts.map +1 -1
  89. package/qwik/index.js +26 -21
  90. package/react/index.d.ts +1 -0
  91. package/react/index.d.ts.map +1 -1
  92. package/react/index.js +4 -1
  93. package/schemas/report-claims.v1.schema.json +137 -0
  94. package/solid/index.d.ts +2 -0
  95. package/solid/index.d.ts.map +1 -1
  96. package/solid/index.js +3 -0
  97. package/svelte/index.d.ts +88 -0
  98. package/svelte/index.d.ts.map +1 -0
  99. package/svelte/index.js +166 -0
  100. package/tailwind.css +87 -0
  101. package/tokens/figma.variables.json +2241 -0
  102. package/tokens/index.js +1 -1
  103. package/tokens/index.json +2 -2
  104. package/tokens/resolved.json +3 -3
  105. package/tokens/tokens.dtcg.json +1 -1
  106. package/vue/index.d.ts +79 -0
  107. package/vue/index.d.ts.map +1 -0
  108. package/vue/index.js +197 -0
@@ -29,14 +29,18 @@ export function initLegend({ root } = {}) {
29
29
  const host = resolveHost(root);
30
30
  if (!host) return noop;
31
31
  const isButton = (el) => el.tagName === 'BUTTON' || el.getAttribute('role') === 'button';
32
- const onClick = (e) => {
33
- const item = e.target.closest('.ui-legend__item');
32
+ const legendFor = (item) => {
34
33
  if (!item || !host.contains(item)) return;
35
34
  const legend = item.closest('[data-bronto-legend]');
36
35
  if (!legend || !host.contains(legend)) return;
36
+ return legend;
37
+ };
38
+ const toggle = (item) => {
39
+ const legend = legendFor(item);
40
+ if (!legend) return;
37
41
  // The contract requires a real `<button>` (keyboard-operable, focusable). A
38
- // non-button item is mouse-only refuse to toggle it rather than ship a
39
- // pointer-only control (WCAG 2.1.1 C11). The author is warned at bind.
42
+ // non-button item is mouse-only unless role=button is keyboard-normalized
43
+ // below refuse anything else rather than ship a pointer-only control.
40
44
  if (!isButton(item)) return;
41
45
  const active = item.getAttribute('aria-pressed') !== 'false';
42
46
  const next = !active;
@@ -54,23 +58,51 @@ export function initLegend({ root } = {}) {
54
58
  }),
55
59
  );
56
60
  };
61
+ const onClick = (e) => {
62
+ toggle(e.target.closest('.ui-legend__item'));
63
+ };
64
+ const onKey = (e) => {
65
+ if (e.key !== 'Enter' && e.key !== ' ') return;
66
+ const item = e.target.closest('.ui-legend__item');
67
+ if (!item || item.tagName === 'BUTTON' || item.getAttribute('role') !== 'button') return;
68
+ e.preventDefault();
69
+ toggle(item);
70
+ };
57
71
  return bindOnce(host, 'legend', () => {
58
- // Warn once per non-button item present at bind: it gets cursor:pointer from
59
- // the CSS but is neither focusable nor keyboard-operable (C11).
72
+ // Normalize role=button entries and warn once per unsupported non-button
73
+ // item present at bind. A real <button> remains the recommended markup.
74
+ const legends = [...(host.querySelectorAll?.('[data-bronto-legend]') ?? [])];
75
+ for (const legend of legends) {
76
+ for (const el of legend.querySelectorAll('.ui-legend__item')) {
77
+ if (el.closest('[data-bronto-legend]') !== legend) continue;
78
+ if (el.tagName === 'BUTTON' && !el.hasAttribute('type')) el.type = 'button';
79
+ if (
80
+ el.tagName !== 'BUTTON' &&
81
+ el.getAttribute('role') === 'button' &&
82
+ !el.hasAttribute('tabindex')
83
+ ) {
84
+ el.tabIndex = 0;
85
+ }
86
+ }
87
+ }
60
88
  if (typeof console !== 'undefined') {
61
- for (const legend of host.querySelectorAll?.('[data-bronto-legend]') ?? []) {
89
+ for (const legend of legends) {
62
90
  const stray = [...legend.querySelectorAll('.ui-legend__item')].some(
63
91
  (el) => el.closest('[data-bronto-legend]') === legend && !isButton(el),
64
92
  );
65
93
  if (stray) {
66
94
  console.warn(
67
- '[bronto] initLegend(): interactive legend entries must be <button> (or role="button") to be keyboard-operable a non-button .ui-legend__item is ignored.',
95
+ '[bronto] initLegend(): interactive legend entries must be <button> or role="button" — unsupported .ui-legend__item controls are ignored.',
68
96
  );
69
97
  break;
70
98
  }
71
99
  }
72
100
  }
73
101
  host.addEventListener('click', onClick);
74
- return () => host.removeEventListener('click', onClick);
102
+ host.addEventListener('keydown', onKey);
103
+ return () => {
104
+ host.removeEventListener('click', onClick);
105
+ host.removeEventListener('keydown', onKey);
106
+ };
75
107
  });
76
108
  }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Wire focusable ARIA splitters. Each `[data-bronto-splitter]` host contains
3
+ * two `.ui-splitter__pane` elements separated by one `.ui-splitter__handle`
4
+ * (`role="separator"`). The behavior keeps `--splitter-pos` and
5
+ * `aria-valuenow` in sync for keyboard and pointer resizing, then dispatches
6
+ * `bronto:splitter:resize` with `{ value, orientation }`.
7
+ *
8
+ * Bronto owns the control affordance only. The host owns pane content,
9
+ * persistence, min/max policy, collapse behavior, and any saved layout state.
10
+ * SSR-safe and idempotent per splitter; returns a cleanup function.
11
+ *
12
+ * @param {import('./internal.js').DelegateOpts} [opts]
13
+ * @returns {import('./internal.js').Cleanup}
14
+ */
15
+ export function initSplitter({ root }?: import("./internal.js").DelegateOpts): import("./internal.js").Cleanup;
16
+ export type SplitterResizeDetail = {
17
+ /**
18
+ * The first pane size as a 0..100 percentage.
19
+ */
20
+ value: number;
21
+ /**
22
+ * Splitter orientation.
23
+ */
24
+ orientation: "vertical" | "horizontal";
25
+ };
26
+ //# sourceMappingURL=splitter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"splitter.d.ts","sourceRoot":"","sources":["splitter.js"],"names":[],"mappings":"AAiLA;;;;;;;;;;;;;GAaG;AACH,wCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAU3C;;;;;WA3La,MAAM;;;;iBACN,UAAU,GAAG,YAAY"}
@@ -0,0 +1,200 @@
1
+ import { hasDom, resolveHost, noop, bindOnce, collectHosts } from './internal.js';
2
+
3
+ const SELECTOR = '[data-bronto-splitter]';
4
+ const HANDLE_SELECTOR = '.ui-splitter__handle';
5
+ const DEFAULT_MIN = 20;
6
+ const DEFAULT_MAX = 80;
7
+ const DEFAULT_VALUE = 50;
8
+ const STEP = 2;
9
+ const LARGE_STEP = 10;
10
+
11
+ /**
12
+ * @typedef {object} SplitterResizeDetail
13
+ * @property {number} value The first pane size as a 0..100 percentage.
14
+ * @property {'vertical' | 'horizontal'} orientation Splitter orientation.
15
+ */
16
+
17
+ const num = (v, fallback) => {
18
+ const n = Number.parseFloat(String(v ?? '').trim());
19
+ return Number.isFinite(n) ? n : fallback;
20
+ };
21
+
22
+ const fmt = (v) => String(Math.round(v * 10) / 10);
23
+
24
+ const clamp = (v, min, max) => Math.min(max, Math.max(min, v));
25
+
26
+ const readCssValue = (splitter) => splitter.style.getPropertyValue('--splitter-pos');
27
+
28
+ const readOrientation = (splitter, handle) => {
29
+ const data = splitter.getAttribute('data-bronto-splitter');
30
+ if (data === 'horizontal' || data === 'vertical') return data;
31
+ if (splitter.classList.contains('ui-splitter--horizontal')) return 'horizontal';
32
+ if (splitter.classList.contains('ui-splitter--vertical')) return 'vertical';
33
+ return handle.getAttribute('aria-orientation') === 'horizontal' ? 'horizontal' : 'vertical';
34
+ };
35
+
36
+ const getView = (el) => el.ownerDocument?.defaultView || null;
37
+
38
+ const dispatchResize = (splitter, detail) => {
39
+ splitter.dispatchEvent(
40
+ new CustomEvent('bronto:splitter:resize', {
41
+ bubbles: true,
42
+ detail,
43
+ }),
44
+ );
45
+ };
46
+
47
+ function wireSplitter(splitter) {
48
+ const handle = splitter.querySelector(HANDLE_SELECTOR);
49
+ if (!handle) return noop;
50
+
51
+ const orientation = readOrientation(splitter, handle);
52
+ const min = num(handle.getAttribute('aria-valuemin'), DEFAULT_MIN);
53
+ const max = Math.max(min, num(handle.getAttribute('aria-valuemax'), DEFAULT_MAX));
54
+ let value = clamp(
55
+ num(handle.getAttribute('aria-valuenow'), num(readCssValue(splitter), DEFAULT_VALUE)),
56
+ min,
57
+ max,
58
+ );
59
+ let activePointer = null;
60
+
61
+ const apply = (next, { emit = true } = {}) => {
62
+ value = clamp(next, min, max);
63
+ const label = fmt(value);
64
+ splitter.style.setProperty('--splitter-pos', `${label}%`);
65
+ handle.setAttribute('aria-valuenow', label);
66
+ if (emit) dispatchResize(splitter, { value, orientation });
67
+ };
68
+
69
+ if (!handle.hasAttribute('role')) handle.setAttribute('role', 'separator');
70
+ if (!handle.hasAttribute('tabindex')) handle.tabIndex = 0;
71
+ if (!handle.hasAttribute('aria-orientation'))
72
+ handle.setAttribute('aria-orientation', orientation);
73
+ if (!handle.hasAttribute('aria-valuemin')) handle.setAttribute('aria-valuemin', fmt(min));
74
+ if (!handle.hasAttribute('aria-valuemax')) handle.setAttribute('aria-valuemax', fmt(max));
75
+ apply(value, { emit: false });
76
+
77
+ const fromPointer = (event) => {
78
+ const rect = splitter.getBoundingClientRect();
79
+ const size = orientation === 'horizontal' ? rect.height : rect.width;
80
+ if (!size) return value;
81
+ if (orientation === 'horizontal') {
82
+ return ((event.clientY - rect.top) / size) * 100;
83
+ }
84
+ const view = getView(splitter);
85
+ const dir = view?.getComputedStyle?.(splitter).direction;
86
+ const x = dir === 'rtl' ? rect.right - event.clientX : event.clientX - rect.left;
87
+ return (x / size) * 100;
88
+ };
89
+
90
+ const capturePointer = (pointerId) => {
91
+ if (pointerId === undefined || pointerId === null) return;
92
+ try {
93
+ handle.setPointerCapture?.(pointerId);
94
+ } catch {
95
+ /* Pointer capture is an affordance; drag still works through document listeners. */
96
+ }
97
+ };
98
+
99
+ const releasePointer = (pointerId = activePointer) => {
100
+ if (pointerId === undefined || pointerId === null) return;
101
+ try {
102
+ if (!handle.hasPointerCapture || handle.hasPointerCapture(pointerId)) {
103
+ handle.releasePointerCapture?.(pointerId);
104
+ }
105
+ } catch {
106
+ /* The element may have been removed or capture may already be gone. */
107
+ }
108
+ };
109
+
110
+ const onKeydown = (event) => {
111
+ let next = value;
112
+ if (event.key === 'Home') next = min;
113
+ else if (event.key === 'End') next = max;
114
+ else if (event.key === 'PageUp') next += LARGE_STEP;
115
+ else if (event.key === 'PageDown') next -= LARGE_STEP;
116
+ else if (event.key === 'ArrowRight' || event.key === 'ArrowDown')
117
+ next += event.shiftKey ? LARGE_STEP : STEP;
118
+ else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp')
119
+ next -= event.shiftKey ? LARGE_STEP : STEP;
120
+ else return;
121
+ event.preventDefault();
122
+ apply(next);
123
+ };
124
+
125
+ const onPointerMove = (event) => {
126
+ if (
127
+ activePointer !== null &&
128
+ event.pointerId !== undefined &&
129
+ event.pointerId !== activePointer
130
+ )
131
+ return;
132
+ apply(fromPointer(event));
133
+ };
134
+
135
+ const onPointerUp = (event) => {
136
+ if (
137
+ activePointer !== null &&
138
+ event.pointerId !== undefined &&
139
+ event.pointerId !== activePointer
140
+ )
141
+ return;
142
+ releasePointer(event.pointerId);
143
+ activePointer = null;
144
+ handle.classList.remove('is-active');
145
+ splitter.ownerDocument.removeEventListener('pointermove', onPointerMove);
146
+ splitter.ownerDocument.removeEventListener('pointerup', onPointerUp);
147
+ splitter.ownerDocument.removeEventListener('pointercancel', onPointerUp);
148
+ };
149
+
150
+ const onPointerDown = (event) => {
151
+ if (event.button !== undefined && event.button !== 0) return;
152
+ event.preventDefault();
153
+ activePointer = event.pointerId ?? null;
154
+ capturePointer(activePointer);
155
+ handle.classList.add('is-active');
156
+ apply(fromPointer(event));
157
+ splitter.ownerDocument.addEventListener('pointermove', onPointerMove);
158
+ splitter.ownerDocument.addEventListener('pointerup', onPointerUp);
159
+ splitter.ownerDocument.addEventListener('pointercancel', onPointerUp);
160
+ };
161
+
162
+ return bindOnce(splitter, 'splitter', () => {
163
+ handle.addEventListener('keydown', onKeydown);
164
+ handle.addEventListener('pointerdown', onPointerDown);
165
+ return () => {
166
+ handle.removeEventListener('keydown', onKeydown);
167
+ handle.removeEventListener('pointerdown', onPointerDown);
168
+ splitter.ownerDocument.removeEventListener('pointermove', onPointerMove);
169
+ splitter.ownerDocument.removeEventListener('pointerup', onPointerUp);
170
+ splitter.ownerDocument.removeEventListener('pointercancel', onPointerUp);
171
+ releasePointer();
172
+ handle.classList.remove('is-active');
173
+ activePointer = null;
174
+ };
175
+ });
176
+ }
177
+
178
+ /**
179
+ * Wire focusable ARIA splitters. Each `[data-bronto-splitter]` host contains
180
+ * two `.ui-splitter__pane` elements separated by one `.ui-splitter__handle`
181
+ * (`role="separator"`). The behavior keeps `--splitter-pos` and
182
+ * `aria-valuenow` in sync for keyboard and pointer resizing, then dispatches
183
+ * `bronto:splitter:resize` with `{ value, orientation }`.
184
+ *
185
+ * Bronto owns the control affordance only. The host owns pane content,
186
+ * persistence, min/max policy, collapse behavior, and any saved layout state.
187
+ * SSR-safe and idempotent per splitter; returns a cleanup function.
188
+ *
189
+ * @param {import('./internal.js').DelegateOpts} [opts]
190
+ * @returns {import('./internal.js').Cleanup}
191
+ */
192
+ export function initSplitter({ root } = {}) {
193
+ if (!hasDom()) return noop;
194
+ const host = resolveHost(root);
195
+ if (!host) return noop;
196
+ const splitters = collectHosts(host, SELECTOR);
197
+ if (!splitters.length) return noop;
198
+ const cleanups = splitters.map(wireSplitter).filter(Boolean);
199
+ return () => cleanups.forEach((fn) => fn());
200
+ }
@@ -49,6 +49,7 @@ export function initTableSort({ root } = {}) {
49
49
  // announces the column as sortable from the start (it was unset until the
50
50
  // first click — C10).
51
51
  for (const sort of table.querySelectorAll('.ui-table__sort')) {
52
+ if (sort.tagName === 'BUTTON' && !sort.hasAttribute('type')) sort.type = 'button';
52
53
  const th = sort.closest('th');
53
54
  if (th && !th.hasAttribute('aria-sort')) th.setAttribute('aria-sort', 'none');
54
55
  }
@@ -110,9 +111,8 @@ export function initTableSort({ root } = {}) {
110
111
  return cmp * sign;
111
112
  });
112
113
  // Re-parent in document order: sorted data rows, then any empty/sentinel
113
- // row last. A single appendChild pass over the existing <tr> nodes (no
114
- // markup is created — these are trusted DOM elements being moved).
115
- for (const r of [...rows, ...emptyRows]) tbody.appendChild(r);
114
+ // row last. These are existing <tr> nodes being moved; no markup is parsed.
115
+ tbody.append(...rows, ...emptyRows);
116
116
  };
117
117
 
118
118
  const allBox = table.querySelector('[data-bronto-select-all]');
@@ -1,4 +1,4 @@
1
- import { hasDom, resolveHost, noop, bindOnce } from './internal.js';
1
+ import { hasDom, resolveHost, noop, bindOnce, collectHosts } from './internal.js';
2
2
 
3
3
  const THEMES = ['light', 'dark'];
4
4
 
@@ -66,7 +66,7 @@ export function initThemeToggle({ storageKey = 'bronto-theme', root } = {}) {
66
66
 
67
67
  const reflect = () => {
68
68
  const c = current();
69
- host.querySelectorAll('[data-bronto-theme-toggle]').forEach((el) => {
69
+ collectHosts(host, '[data-bronto-theme-toggle]').forEach((el) => {
70
70
  const forced = el.getAttribute('data-bronto-theme-toggle');
71
71
  // A forced control is "pressed" when its theme is the active one;
72
72
  // a plain toggle reflects whether dark is active.