@ponchia/ui 0.5.0 → 0.6.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 (117) hide show
  1. package/CHANGELOG.md +322 -0
  2. package/MIGRATIONS.json +14 -0
  3. package/README.md +28 -5
  4. package/annotations/index.d.ts +398 -276
  5. package/annotations/index.d.ts.map +1 -0
  6. package/annotations/index.js +315 -45
  7. package/behaviors/carousel.js +17 -16
  8. package/behaviors/combobox.js +47 -16
  9. package/behaviors/command.js +18 -15
  10. package/behaviors/connectors.js +4 -5
  11. package/behaviors/crosshair.js +4 -5
  12. package/behaviors/dialog.js +3 -2
  13. package/behaviors/disclosure.js +3 -2
  14. package/behaviors/dismissible.js +3 -2
  15. package/behaviors/forms.js +41 -13
  16. package/behaviors/glyph.js +4 -5
  17. package/behaviors/internal.js +47 -0
  18. package/behaviors/legend.js +23 -2
  19. package/behaviors/menu.js +3 -2
  20. package/behaviors/popover.js +78 -7
  21. package/behaviors/spotlight.js +4 -5
  22. package/behaviors/table.js +39 -12
  23. package/behaviors/tabs.js +14 -14
  24. package/behaviors/theme.js +5 -3
  25. package/behaviors/toast.js +13 -1
  26. package/classes/classes.json +1857 -0
  27. package/classes/index.d.ts +28 -13
  28. package/classes/index.js +34 -18
  29. package/classes/vscode.css-custom-data.json +12 -0
  30. package/connectors/index.d.ts +189 -69
  31. package/connectors/index.d.ts.map +1 -0
  32. package/connectors/index.js +120 -24
  33. package/css/app.css +43 -13
  34. package/css/base.css +15 -10
  35. package/css/connectors.css +17 -0
  36. package/css/content.css +7 -1
  37. package/css/dataviz.css +5 -1
  38. package/css/disclosure.css +38 -6
  39. package/css/dots.css +57 -0
  40. package/css/feedback.css +60 -2
  41. package/css/forms.css +42 -1
  42. package/css/legend.css +11 -7
  43. package/css/marks.css +38 -8
  44. package/css/motion.css +24 -44
  45. package/css/navigation.css +7 -0
  46. package/css/overlay.css +31 -1
  47. package/css/primitives.css +91 -5
  48. package/css/report.css +40 -63
  49. package/css/site.css +16 -2
  50. package/css/sources.css +43 -1
  51. package/css/spotlight.css +1 -1
  52. package/css/tokens.css +36 -1
  53. package/css/workbench.css +1 -1
  54. package/dist/bronto.css +1 -1
  55. package/dist/css/analytical.css +1 -1
  56. package/dist/css/app.css +1 -1
  57. package/dist/css/base.css +1 -1
  58. package/dist/css/connectors.css +1 -1
  59. package/dist/css/content.css +1 -1
  60. package/dist/css/disclosure.css +1 -1
  61. package/dist/css/dots.css +1 -1
  62. package/dist/css/feedback.css +1 -1
  63. package/dist/css/forms.css +1 -1
  64. package/dist/css/legend.css +1 -1
  65. package/dist/css/marks.css +1 -1
  66. package/dist/css/motion.css +1 -1
  67. package/dist/css/navigation.css +1 -1
  68. package/dist/css/overlay.css +1 -1
  69. package/dist/css/primitives.css +1 -1
  70. package/dist/css/report.css +1 -1
  71. package/dist/css/site.css +1 -1
  72. package/dist/css/sources.css +1 -1
  73. package/dist/css/spotlight.css +1 -1
  74. package/dist/css/tokens.css +1 -1
  75. package/dist/css/workbench.css +1 -1
  76. package/docs/adr/0003-theme-model.md +1 -1
  77. package/docs/annotations.md +94 -14
  78. package/docs/architecture.md +50 -6
  79. package/docs/contrast.md +116 -92
  80. package/docs/d2.md +195 -0
  81. package/docs/legends.md +18 -2
  82. package/docs/marks.md +9 -2
  83. package/docs/mermaid.md +152 -0
  84. package/docs/reference.md +78 -22
  85. package/docs/reporting.md +395 -57
  86. package/docs/sources.md +27 -0
  87. package/docs/stability.md +9 -2
  88. package/docs/usage.md +101 -4
  89. package/docs/vega.md +225 -0
  90. package/docs/workbench.md +7 -1
  91. package/glyphs/glyphs.js +6 -4
  92. package/llms.txt +139 -14
  93. package/package.json +50 -12
  94. package/qwik/index.d.ts +42 -59
  95. package/qwik/index.d.ts.map +1 -0
  96. package/qwik/index.js +55 -3
  97. package/react/index.d.ts +39 -61
  98. package/react/index.d.ts.map +1 -0
  99. package/react/index.js +57 -3
  100. package/solid/index.d.ts +64 -61
  101. package/solid/index.d.ts.map +1 -0
  102. package/solid/index.js +60 -3
  103. package/tokens/d2.d.ts +38 -0
  104. package/tokens/d2.js +71 -0
  105. package/tokens/d2.json +43 -0
  106. package/tokens/index.d.ts +5 -5
  107. package/tokens/index.js +15 -1
  108. package/tokens/index.json +9 -0
  109. package/tokens/mermaid.d.ts +23 -0
  110. package/tokens/mermaid.js +181 -0
  111. package/tokens/mermaid.json +163 -0
  112. package/tokens/resolved.json +45 -1
  113. package/tokens/skins.js +3 -2
  114. package/tokens/tokens.dtcg.json +26 -0
  115. package/tokens/vega.d.ts +34 -0
  116. package/tokens/vega.js +155 -0
  117. package/tokens/vega.json +179 -0
@@ -1,14 +1,16 @@
1
- import { hasDom, noop, bindOnce } from './internal.js';
1
+ import { hasDom, resolveHost, noop, bindOnce, collectHosts } from './internal.js';
2
2
 
3
3
  /**
4
4
  * Client-side sortable + selectable data table. Wires
5
5
  * `[data-bronto-sortable]`:
6
6
  *
7
- * - clicking a header's `.ui-table__sort` (or a `th[data-sort]`)
8
- * sorts the tbody by that column, cycling `aria-sort`
9
- * none ascending descending and clearing the other headers.
7
+ * - clicking a header's `.ui-table__sort` button sorts the tbody by
8
+ * that column. Sortable headers are seeded `aria-sort="none"`; a
9
+ * click toggles that header ascending descending (first click =
10
+ * ascending) and resets the other sortable headers to `none`.
10
11
  * Numeric columns (`data-sort="num"` or `.is-num` cells) sort
11
- * numerically; everything else, locale string compare.
12
+ * numerically; everything else, locale string compare. Any
13
+ * `.ui-table__empty` sentinel row is kept last after a sort.
12
14
  * - a `[data-bronto-select-all]` checkbox toggles every
13
15
  * `[data-bronto-select]` row checkbox and the rows'
14
16
  * `aria-selected`; toggling a row keeps the header checkbox's
@@ -16,29 +18,47 @@ import { hasDom, noop, bindOnce } from './internal.js';
16
18
  * ({ detail: { count } }) on the table.
17
19
  *
18
20
  * SSR-safe, idempotent per table; returns a cleanup function.
21
+ *
22
+ * The numeric sort parses each cell as display text (strips non-[0-9.-] chars),
23
+ * so it is locale-naive — group/decimal separators beyond `.`/`-` are not
24
+ * interpreted. It is a client-side convenience sorter, not a data grid.
19
25
  */
20
26
  export function initTableSort({ root } = {}) {
21
27
  if (!hasDom()) return noop;
22
- const host = root || document;
23
- const tables = [];
24
- if (host !== document && host.matches?.('[data-bronto-sortable]')) tables.push(host);
25
- tables.push(...(host.querySelectorAll?.('[data-bronto-sortable]') ?? []));
28
+ const host = resolveHost(root);
29
+ if (!host) return noop;
30
+ const tables = collectHosts(host, '[data-bronto-sortable]');
26
31
  const cleanups = [];
27
32
 
28
33
  for (const table of tables) {
29
34
  const tbody = table.tBodies[0];
30
35
  if (!tbody) continue;
31
36
 
37
+ // Seed the resting `aria-sort="none"` on every sortable header so AT
38
+ // announces the column as sortable from the start (it was unset until the
39
+ // first click — C10).
40
+ for (const sort of table.querySelectorAll('.ui-table__sort')) {
41
+ const th = sort.closest('th');
42
+ if (th && !th.hasAttribute('aria-sort')) th.setAttribute('aria-sort', 'none');
43
+ }
44
+
32
45
  const colIndex = (th) => [...th.parentElement.children].indexOf(th);
33
46
  const cellText = (row, i) => row.children[i]?.textContent.trim() ?? '';
34
47
 
35
48
  const sortBy = (th, numeric) => {
36
49
  const headers = th.closest('tr').querySelectorAll('th');
37
50
  const dir = th.getAttribute('aria-sort') === 'ascending' ? 'descending' : 'ascending';
38
- headers.forEach((h) => h.removeAttribute('aria-sort'));
51
+ // Reset the OTHER sortable headers to `none` (not removed) so they keep
52
+ // announcing sortability; only previously-sortable headers carry aria-sort.
53
+ headers.forEach((h) => {
54
+ if (h !== th && h.hasAttribute('aria-sort')) h.setAttribute('aria-sort', 'none');
55
+ });
39
56
  th.setAttribute('aria-sort', dir);
40
57
  const i = colIndex(th);
41
58
  const sign = dir === 'ascending' ? 1 : -1;
59
+ // Empty/sentinel rows sort out of the data set AND must re-append LAST,
60
+ // or after a sort they float above the real rows (C29).
61
+ const emptyRows = [...tbody.rows].filter((r) => r.classList.contains('ui-table__empty'));
42
62
  const rows = [...tbody.rows].filter((r) => !r.classList.contains('ui-table__empty'));
43
63
  rows.sort((a, b) => {
44
64
  const x = cellText(a, i);
@@ -49,7 +69,10 @@ export function initTableSort({ root } = {}) {
49
69
  : x.localeCompare(y);
50
70
  return cmp * sign;
51
71
  });
52
- rows.forEach((r) => tbody.appendChild(r));
72
+ // Re-parent in document order: sorted data rows, then any empty/sentinel
73
+ // row last. A single appendChild pass over the existing <tr> nodes (no
74
+ // markup is created — these are trusted DOM elements being moved).
75
+ for (const r of [...rows, ...emptyRows]) tbody.appendChild(r);
53
76
  };
54
77
 
55
78
  const allBox = table.querySelector('[data-bronto-select-all]');
@@ -71,7 +94,11 @@ export function initTableSort({ root } = {}) {
71
94
  };
72
95
 
73
96
  const onClick = (e) => {
74
- const sorter = e.target.closest('.ui-table__sort, th[data-sort]');
97
+ // Only the focusable `.ui-table__sort` button is a sort trigger — it is
98
+ // keyboard-operable and carries the `::after` sort glyph. The bare
99
+ // `th[data-sort]` path was mouse-only with no affordance, so it is gone
100
+ // (C10); `data-sort="num"` is still read from the button or its th.
101
+ const sorter = e.target.closest('.ui-table__sort');
75
102
  if (sorter && table.contains(sorter)) {
76
103
  const th = sorter.closest('th');
77
104
  const numeric =
package/behaviors/tabs.js CHANGED
@@ -1,9 +1,4 @@
1
- import { hasDom, noop, bindOnce } from './internal.js';
2
-
3
- // Module-global so tab ids stay unique across *every* initTabs() call.
4
- // A per-call counter makes separate islands/roots all mint `bronto-tab-1`,
5
- // which collides aria-controls/aria-labelledby across the document.
6
- let tabUid = 0;
1
+ import { hasDom, resolveHost, noop, bindOnce, nextFieldUid, collectHosts } from './internal.js';
7
2
 
8
3
  /**
9
4
  * Wire `[data-bronto-tabs]` groups for full keyboard a11y. The framework
@@ -22,13 +17,10 @@ let tabUid = 0;
22
17
  */
23
18
  export function initTabs({ root } = {}) {
24
19
  if (!hasDom()) return noop;
25
- const host = root || document;
20
+ const host = resolveHost(root);
21
+ if (!host) return noop;
26
22
  const cleanups = [];
27
- // querySelectorAll only matches descendants, so a `root` that *is* a
28
- // tab group would be skipped — include it explicitly.
29
- const groups = [];
30
- if (host !== document && host.matches?.('[data-bronto-tabs]')) groups.push(host);
31
- groups.push(...host.querySelectorAll('[data-bronto-tabs]'));
23
+ const groups = collectHosts(host, '[data-bronto-tabs]');
32
24
  for (const group of groups) {
33
25
  // Own group only — a tab/panel inside a nested [data-bronto-tabs]
34
26
  // belongs to that inner group, not this one.
@@ -44,7 +36,7 @@ export function initTabs({ root } = {}) {
44
36
  for (const t of tabs) {
45
37
  const p = panels.find((x) => x.dataset.panel === t.dataset.tab);
46
38
  if (!p) continue;
47
- const n = ++tabUid;
39
+ const n = nextFieldUid();
48
40
  if (!t.id) t.id = `bronto-tab-${n}`;
49
41
  if (!p.id) p.id = `bronto-tabpanel-${n}`;
50
42
  t.setAttribute('aria-controls', p.id);
@@ -59,9 +51,17 @@ export function initTabs({ root } = {}) {
59
51
  t.setAttribute('aria-selected', String(on));
60
52
  t.tabIndex = on ? 0 : -1;
61
53
  }
54
+ // Only retarget panels when this tab actually controls one. A panel-less
55
+ // tab must NOT hide every panel — leave the prior panel visible (C30).
56
+ if (!panels.some((p) => p.dataset.panel === tab.dataset.tab)) return;
62
57
  for (const p of panels) {
63
58
  p.setAttribute('role', 'tabpanel');
64
- p.hidden = p.dataset.panel !== tab.dataset.tab;
59
+ const shown = p.dataset.panel === tab.dataset.tab;
60
+ p.hidden = !shown;
61
+ // APG: a tabpanel is focusable so keyboard users can reach a text-only
62
+ // panel; hidden panels drop out of the tab order (C30).
63
+ if (shown) p.tabIndex = 0;
64
+ else p.removeAttribute('tabindex');
65
65
  }
66
66
  };
67
67
  const onClick = (e) => {
@@ -1,4 +1,4 @@
1
- import { hasDom, noop, bindOnce } from './internal.js';
1
+ import { hasDom, resolveHost, noop, bindOnce } from './internal.js';
2
2
 
3
3
  const THEMES = ['light', 'dark'];
4
4
 
@@ -9,7 +9,8 @@ const THEMES = ['light', 'dark'];
9
9
  */
10
10
  export function applyStoredTheme({ storageKey = 'bronto-theme', root } = {}) {
11
11
  if (!hasDom()) return;
12
- const el = root || document.documentElement;
12
+ const el = resolveHost(root, document.documentElement);
13
+ if (!el) return;
13
14
  let stored = null;
14
15
  try {
15
16
  stored = localStorage.getItem(storageKey);
@@ -33,7 +34,8 @@ export function applyStoredTheme({ storageKey = 'bronto-theme', root } = {}) {
33
34
  */
34
35
  export function initThemeToggle({ storageKey = 'bronto-theme', root } = {}) {
35
36
  if (!hasDom()) return noop;
36
- const host = root || document;
37
+ const host = resolveHost(root);
38
+ if (!host) return noop;
37
39
  const docEl = document.documentElement;
38
40
 
39
41
  const prefersDark = () =>
@@ -1,5 +1,11 @@
1
1
  import { hasDom, noop } from './internal.js';
2
2
 
3
+ // The tones that have a `.ui-toast--*` rule. The TS type already unions these,
4
+ // but a plain-JS / LLM caller can pass any string — and an unknown tone built a
5
+ // `.ui-toast--error` class that matches no CSS, yielding a silent neutral toast.
6
+ // Validate so an unknown tone degrades to neutral *and warns*, never lies. (C16)
7
+ const TOAST_TONES = new Set(['accent', 'success', 'warning', 'danger', 'info']);
8
+
3
9
  // First-toast deferral queue. The very first toast on a brand-new stack
4
10
  // is appended next frame so AT observes the empty aria-live region
5
11
  // before its first child. Any further toasts created *before* that frame
@@ -43,7 +49,13 @@ function enqueueToast(place, freshStack) {
43
49
 
44
50
  function toastElement(message, { tone, title }) {
45
51
  const el = document.createElement('div');
46
- el.className = tone ? `ui-toast ui-toast--${tone}` : 'ui-toast';
52
+ const validTone = tone && TOAST_TONES.has(tone) ? tone : null;
53
+ if (tone && !validTone && typeof console !== 'undefined') {
54
+ console.warn(
55
+ `[bronto] toast(): unknown tone "${tone}" — expected ${[...TOAST_TONES].join('/')}. Rendering neutral.`,
56
+ );
57
+ }
58
+ el.className = validTone ? `ui-toast ui-toast--${validTone}` : 'ui-toast';
47
59
  // No per-item role: the stack itself is the live region; a nested
48
60
  // live region risks double announcement in some SRs.
49
61
  if (title) {