@bkstg/design-systems 1.0.0 → 1.0.1

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 (125) hide show
  1. package/package.json +1 -1
  2. package/tether/latest/README.md +44 -0
  3. package/tether/latest/charts-core.mjs +349 -0
  4. package/tether/latest/charts.js +58 -0
  5. package/tether/latest/motion.js +168 -0
  6. package/tether/latest/react/ActivityFeed.jsx +18 -0
  7. package/tether/latest/react/Alert.jsx +28 -0
  8. package/tether/latest/react/Annotations.jsx +24 -0
  9. package/tether/latest/react/AppShell.jsx +87 -0
  10. package/tether/latest/react/Avatar.jsx +19 -0
  11. package/tether/latest/react/Badge.jsx +14 -0
  12. package/tether/latest/react/Banner.jsx +17 -0
  13. package/tether/latest/react/Breadcrumbs.jsx +23 -0
  14. package/tether/latest/react/Button.jsx +31 -0
  15. package/tether/latest/react/ButtonGroup.jsx +29 -0
  16. package/tether/latest/react/Calendar.jsx +57 -0
  17. package/tether/latest/react/CardHeader.jsx +14 -0
  18. package/tether/latest/react/Chart.jsx +42 -0
  19. package/tether/latest/react/Chat.jsx +34 -0
  20. package/tether/latest/react/Checkbox.jsx +32 -0
  21. package/tether/latest/react/CodeSnippet.jsx +17 -0
  22. package/tether/latest/react/ColorPicker.jsx +35 -0
  23. package/tether/latest/react/CommandMenu.jsx +54 -0
  24. package/tether/latest/react/DatePicker.jsx +34 -0
  25. package/tether/latest/react/DeviceFrame.jsx +12 -0
  26. package/tether/latest/react/Dialog.jsx +43 -0
  27. package/tether/latest/react/Divider.jsx +10 -0
  28. package/tether/latest/react/Editor.jsx +85 -0
  29. package/tether/latest/react/EmptyState.jsx +13 -0
  30. package/tether/latest/react/Field.jsx +32 -0
  31. package/tether/latest/react/FileUpload.jsx +38 -0
  32. package/tether/latest/react/Filters.jsx +19 -0
  33. package/tether/latest/react/Infographics.jsx +54 -0
  34. package/tether/latest/react/InlineCTA.jsx +14 -0
  35. package/tether/latest/react/LineChart.jsx +23 -0
  36. package/tether/latest/react/Marketing.jsx +161 -0
  37. package/tether/latest/react/Menu.jsx +76 -0
  38. package/tether/latest/react/Metric.jsx +28 -0
  39. package/tether/latest/react/PageHeader.jsx +22 -0
  40. package/tether/latest/react/Pagination.jsx +32 -0
  41. package/tether/latest/react/Progress.jsx +26 -0
  42. package/tether/latest/react/RadioGroup.jsx +27 -0
  43. package/tether/latest/react/Reveal.jsx +17 -0
  44. package/tether/latest/react/SectionHeader.jsx +24 -0
  45. package/tether/latest/react/Select.jsx +19 -0
  46. package/tether/latest/react/Sheet.jsx +25 -0
  47. package/tether/latest/react/SideNav.jsx +37 -0
  48. package/tether/latest/react/Skeleton.jsx +16 -0
  49. package/tether/latest/react/Slider.jsx +27 -0
  50. package/tether/latest/react/Spinner.jsx +14 -0
  51. package/tether/latest/react/Steps.jsx +22 -0
  52. package/tether/latest/react/Switch.jsx +11 -0
  53. package/tether/latest/react/Table.jsx +24 -0
  54. package/tether/latest/react/Tabs.jsx +80 -0
  55. package/tether/latest/react/Tag.jsx +34 -0
  56. package/tether/latest/react/Tooltip.jsx +25 -0
  57. package/tether/latest/react/Tree.jsx +41 -0
  58. package/tether/latest/react/VideoPlayer.jsx +57 -0
  59. package/tether/latest/react/cx.js +4 -0
  60. package/tether/latest/react/index.js +59 -0
  61. package/tether/latest/system.json +44 -0
  62. package/tether/latest/tether.css +1661 -0
  63. package/tether/latest/tether.js +703 -0
  64. package/tether/v1/README.md +44 -0
  65. package/tether/v1/charts-core.mjs +349 -0
  66. package/tether/v1/charts.js +58 -0
  67. package/tether/v1/motion.js +168 -0
  68. package/tether/v1/react/ActivityFeed.jsx +18 -0
  69. package/tether/v1/react/Alert.jsx +28 -0
  70. package/tether/v1/react/Annotations.jsx +24 -0
  71. package/tether/v1/react/AppShell.jsx +87 -0
  72. package/tether/v1/react/Avatar.jsx +19 -0
  73. package/tether/v1/react/Badge.jsx +14 -0
  74. package/tether/v1/react/Banner.jsx +17 -0
  75. package/tether/v1/react/Breadcrumbs.jsx +23 -0
  76. package/tether/v1/react/Button.jsx +31 -0
  77. package/tether/v1/react/ButtonGroup.jsx +29 -0
  78. package/tether/v1/react/Calendar.jsx +57 -0
  79. package/tether/v1/react/CardHeader.jsx +14 -0
  80. package/tether/v1/react/Chart.jsx +42 -0
  81. package/tether/v1/react/Chat.jsx +34 -0
  82. package/tether/v1/react/Checkbox.jsx +32 -0
  83. package/tether/v1/react/CodeSnippet.jsx +17 -0
  84. package/tether/v1/react/ColorPicker.jsx +35 -0
  85. package/tether/v1/react/CommandMenu.jsx +54 -0
  86. package/tether/v1/react/DatePicker.jsx +34 -0
  87. package/tether/v1/react/DeviceFrame.jsx +12 -0
  88. package/tether/v1/react/Dialog.jsx +43 -0
  89. package/tether/v1/react/Divider.jsx +10 -0
  90. package/tether/v1/react/Editor.jsx +85 -0
  91. package/tether/v1/react/EmptyState.jsx +13 -0
  92. package/tether/v1/react/Field.jsx +32 -0
  93. package/tether/v1/react/FileUpload.jsx +38 -0
  94. package/tether/v1/react/Filters.jsx +19 -0
  95. package/tether/v1/react/Infographics.jsx +54 -0
  96. package/tether/v1/react/InlineCTA.jsx +14 -0
  97. package/tether/v1/react/LineChart.jsx +23 -0
  98. package/tether/v1/react/Marketing.jsx +161 -0
  99. package/tether/v1/react/Menu.jsx +76 -0
  100. package/tether/v1/react/Metric.jsx +28 -0
  101. package/tether/v1/react/PageHeader.jsx +22 -0
  102. package/tether/v1/react/Pagination.jsx +32 -0
  103. package/tether/v1/react/Progress.jsx +26 -0
  104. package/tether/v1/react/RadioGroup.jsx +27 -0
  105. package/tether/v1/react/Reveal.jsx +17 -0
  106. package/tether/v1/react/SectionHeader.jsx +24 -0
  107. package/tether/v1/react/Select.jsx +19 -0
  108. package/tether/v1/react/Sheet.jsx +25 -0
  109. package/tether/v1/react/SideNav.jsx +37 -0
  110. package/tether/v1/react/Skeleton.jsx +16 -0
  111. package/tether/v1/react/Slider.jsx +27 -0
  112. package/tether/v1/react/Spinner.jsx +14 -0
  113. package/tether/v1/react/Steps.jsx +22 -0
  114. package/tether/v1/react/Switch.jsx +11 -0
  115. package/tether/v1/react/Table.jsx +24 -0
  116. package/tether/v1/react/Tabs.jsx +80 -0
  117. package/tether/v1/react/Tag.jsx +34 -0
  118. package/tether/v1/react/Tooltip.jsx +25 -0
  119. package/tether/v1/react/Tree.jsx +41 -0
  120. package/tether/v1/react/VideoPlayer.jsx +57 -0
  121. package/tether/v1/react/cx.js +4 -0
  122. package/tether/v1/react/index.js +59 -0
  123. package/tether/v1/system.json +44 -0
  124. package/tether/v1/tether.css +1661 -0
  125. package/tether/v1/tether.js +703 -0
@@ -0,0 +1,703 @@
1
+ // components.js: behavior for the kit's interactive components. Plain script, no framework, no build.
2
+ // Load after the markup (or with defer). It wires everything already on the page, and exposes
3
+ // window.UI so markup added later can be wired with UI.init(container).
4
+ //
5
+ // Tabs [data-ui-tabs] arrows, Home/End, roving focus, sliding indicator
6
+ // Dialog [data-ui-open="<dialog id>"] opens a <dialog class="ui-dialog"> as a modal;
7
+ // [data-ui-close] closes it; Escape and a click on the backdrop close it too;
8
+ // focus returns to whatever opened it
9
+ // Tooltip .ui-tooltip-wrap Escape hides an open tooltip until the pointer or focus leaves (WCAG 1.4.13)
10
+ // Chart .ui-chart[data-ui-chart] draws a line chart from data-values="3,5,4,…" (and data-labels="Jan,Feb,…");
11
+ // the values also become the chart's text alternative
12
+ // Tag [data-ui-dismiss] removes its .ui-tag; focus moves to the next remove button, or the
13
+ // previous one, or the tag list, never lost to the top of the page
14
+ // Checkbox [data-indeterminate] starts mixed; [data-ui-checkall="<name>"] checks every box of that name
15
+ // Slider .ui-slider the filled track follows the value; an <output for="<id>"> shows it
16
+ // Button group .ui-btngroup toggles aria-pressed; with data-ui-single, one at a time
17
+ // Menu [aria-haspopup="menu"] opens its aria-controls menu; arrows, Home/End, a letter jumps,
18
+ // Escape closes and focus returns; choosing fires "ui-select"
19
+ // Editor [data-ui-editor] toolbar tools run editing commands; arrow keys move between tools
20
+ // Video [data-ui-video] play/pause, seek, mute, full screen; k or Space plays and pauses
21
+ // Pages [data-ui-pagination] data-page and data-count draw the pages; choosing one fires "ui-page"
22
+ // Calendar [data-ui-calendar] a month grid: arrows, Page Up/Down, Home/End; choosing fires "ui-date"
23
+ // Date picker [data-ui-datepicker] a button that opens a calendar; Escape or a choice closes it
24
+ // Copy [data-ui-copy="#id"] copies that element's text
25
+ // Color [data-ui-colorpick] swatches, native picker and hex field kept in step; fires "ui-color"
26
+ // Command dialog[data-ui-command] ⌘K / Ctrl+K or [data-ui-command-open]; type, arrows, Enter fires "ui-command"
27
+ // Files [data-ui-dropzone] click or drop; lists the files (never uploads); fires "ui-files"
28
+ // Chips .ui-chip[aria-pressed] toggles; fires "ui-change"
29
+ // Chat [data-ui-chat] Enter sends, Shift+Enter a new line; fires "ui-send"
30
+ // Tree .ui-tree[role=tree] arrows, Home/End, Enter selects; fires "ui-select"
31
+ // Site header .ui-sitenav-toggle opens and closes its aria-controls panel; Escape closes it
32
+ // Banner [data-ui-banner-close] hides its banner; fires "ui-dismiss"
33
+ // Annotations .ui-annot [data-annot] a pin and its note light up together
34
+ (function () {
35
+ "use strict";
36
+
37
+ function tabs(root) {
38
+ if (root._uiTabs) return root._uiTabs;
39
+ var list = root.querySelector('[role="tablist"]');
40
+ var all = Array.prototype.slice.call(list.querySelectorAll('[role="tab"]'));
41
+ var indicator = list.querySelector(".ui-tab-indicator");
42
+
43
+ function enabled() { return all.filter(function (t) { return !t.disabled; }); }
44
+ function move() {
45
+ var t = all.filter(function (x) { return x.getAttribute("aria-selected") === "true"; })[0];
46
+ if (!indicator || !t) return;
47
+ indicator.style.setProperty("--x", t.offsetLeft + "px");
48
+ indicator.style.setProperty("--w", t.offsetWidth + "px");
49
+ }
50
+ function select(tab, focus) {
51
+ all.forEach(function (t) {
52
+ var on = t === tab;
53
+ t.setAttribute("aria-selected", String(on));
54
+ t.tabIndex = on ? 0 : -1;
55
+ var panel = document.getElementById(t.getAttribute("aria-controls"));
56
+ if (panel) panel.hidden = !on;
57
+ });
58
+ if (focus) tab.focus();
59
+ move();
60
+ // bubbles, so a page can listen once for every tab group in it
61
+ root.dispatchEvent(new CustomEvent("ui-tabchange", { bubbles: true, detail: { tab: tab } }));
62
+ }
63
+ all.forEach(function (t) {
64
+ t.addEventListener("click", function () { if (!t.disabled) select(t, false); });
65
+ t.addEventListener("keydown", function (e) {
66
+ var on = enabled(), i = on.indexOf(t), next = null;
67
+ if (e.key === "ArrowRight") next = on[(i + 1) % on.length];
68
+ else if (e.key === "ArrowLeft") next = on[(i - 1 + on.length) % on.length];
69
+ else if (e.key === "Home") next = on[0];
70
+ else if (e.key === "End") next = on[on.length - 1];
71
+ if (next) { e.preventDefault(); select(next, true); }
72
+ });
73
+ });
74
+ var first = all.filter(function (x) { return x.getAttribute("aria-selected") === "true"; })[0] || enabled()[0];
75
+ if (first) select(first, false);
76
+ // the indicator follows layout changes: fonts loading, the shape axis, a resize
77
+ if (window.ResizeObserver) new ResizeObserver(move).observe(list);
78
+ if (document.fonts && document.fonts.ready) document.fonts.ready.then(move);
79
+ root._uiTabs = { select: select, move: move };
80
+ return root._uiTabs;
81
+ }
82
+
83
+ var lastOpener = new WeakMap();
84
+ function openDialog(dlg, opener) {
85
+ if (!dlg || dlg.open) return;
86
+ lastOpener.set(dlg, opener || document.activeElement);
87
+ dlg.showModal();
88
+ }
89
+ function closeDialog(dlg) { if (dlg && dlg.open) dlg.close(); }
90
+ function wireDialog(dlg) {
91
+ if (dlg._ui) return;
92
+ dlg._ui = true;
93
+ // a click whose target is the dialog element itself landed on the backdrop
94
+ dlg.addEventListener("click", function (e) { if (e.target === dlg) dlg.close(); });
95
+ dlg.addEventListener("close", function () {
96
+ var back = lastOpener.get(dlg);
97
+ if (back && back.focus && document.contains(back)) back.focus();
98
+ });
99
+ }
100
+
101
+ // A line chart in an SVG that stretches to its box; strokes keep their width (non-scaling-stroke)
102
+ function chartSvg(values, w, h) {
103
+ var max = Math.max.apply(null, values), min = Math.min.apply(null, values), span = max - min || 1, pad = 6;
104
+ var pts = values.map(function (v, i) { return [i * w / (values.length - 1), pad + (h - 2 * pad) * (1 - (v - min) / span)]; });
105
+ var line = pts.map(function (p, i) { return (i ? "L" : "M") + p[0].toFixed(1) + " " + p[1].toFixed(1); }).join(" ");
106
+ var grid = [0.25, 0.5, 0.75].map(function (f) { return '<line class="ui-chart-grid" x1="0" x2="' + w + '" y1="' + h * f + '" y2="' + h * f + '" vector-effect="non-scaling-stroke"/>'; }).join("");
107
+ return '<svg viewBox="0 0 ' + w + " " + h + '" preserveAspectRatio="none" aria-hidden="true">' + grid +
108
+ '<path class="ui-chart-area" d="' + line + " L" + w + " " + h + " L0 " + h + ' Z"/>' +
109
+ '<path class="ui-chart-line" d="' + line + '" vector-effect="non-scaling-stroke"/></svg>';
110
+ }
111
+ function chart(el) {
112
+ var values = (el.getAttribute("data-values") || "").split(",").map(Number).filter(function (n) { return !isNaN(n); });
113
+ if (values.length < 2) return;
114
+ var labels = (el.getAttribute("data-labels") || "").split(",").filter(Boolean);
115
+ var name = el.getAttribute("data-label") || "Chart";
116
+ el.setAttribute("role", "img");
117
+ el.setAttribute("aria-label", name + ": " + values.map(function (v, i) { return (labels[i] ? labels[i] + " " : "") + v; }).join(", "));
118
+ el._uiChart = { values: values, labels: labels };
119
+ el.innerHTML = chartSvg(values, 600, 200) + '<span class="ui-chart-marker" aria-hidden="true" hidden></span><span class="ui-chart-tip" aria-hidden="true" hidden></span>' + (labels.length ? '<div class="ui-chart-axis" aria-hidden="true">' + labels.map(function (l) { return "<span>" + l + "</span>"; }).join("") + "</div>" : "");
120
+ }
121
+
122
+ // Hover: a marker and the nearest point's value. Pointer only; the values are already the chart's text alternative.
123
+ document.addEventListener("pointermove", function (e) {
124
+ var el = e.target.closest && e.target.closest(".ui-chart");
125
+ document.querySelectorAll(".ui-chart-tip:not([hidden])").forEach(function (t) { if (!el || !el.contains(t)) { t.hidden = true; t.previousElementSibling.hidden = true; } });
126
+ if (!el || !el._uiChart || e.pointerType === "touch" && e.type !== "pointermove") return;
127
+ var svg = el.querySelector("svg"), d = el._uiChart, r = svg.getBoundingClientRect();
128
+ if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) return;
129
+ var i = Math.round((e.clientX - r.left) / r.width * (d.values.length - 1));
130
+ var max = Math.max.apply(null, d.values), min = Math.min.apply(null, d.values), span = max - min || 1;
131
+ var x = i / (d.values.length - 1) * r.width, y = (6 + (200 - 12) * (1 - (d.values[i] - min) / span)) / 200 * r.height;
132
+ var tip = el.querySelector(".ui-chart-tip"), mark = el.querySelector(".ui-chart-marker");
133
+ mark.hidden = false; tip.hidden = false;
134
+ mark.style.left = x + "px"; mark.style.height = r.height + "px";
135
+ tip.textContent = (d.labels[i] ? d.labels[i] + ": " : "") + d.values[i].toLocaleString();
136
+ tip.style.left = Math.max(0, Math.min(r.width - tip.offsetWidth, x - tip.offsetWidth / 2)) + "px";
137
+ tip.style.top = Math.max(0, y - 34) + "px";
138
+ });
139
+
140
+ // ── checkboxes: markup cannot set "indeterminate", so data-indeterminate does; data-ui-checkall="<name>" makes
141
+ // a box that checks every box of that name and shows mixed when some are checked
142
+ function syncCheckAll(all) {
143
+ var name = all.getAttribute("data-ui-checkall");
144
+ // a disabled box cannot be changed from here, so it does not count toward "all"
145
+ var boxes = Array.prototype.slice.call(document.querySelectorAll('input[type="checkbox"][name="' + name + '"]:not(:disabled)'));
146
+ var on = boxes.filter(function (b) { return b.checked; }).length;
147
+ all.checked = on === boxes.length && on > 0;
148
+ all.indeterminate = on > 0 && on < boxes.length;
149
+ }
150
+ document.addEventListener("change", function (e) {
151
+ var t = e.target;
152
+ if (!t.matches) return;
153
+ if (t.matches("[data-ui-checkall]")) {
154
+ document.querySelectorAll('input[type="checkbox"][name="' + t.getAttribute("data-ui-checkall") + '"]').forEach(function (b) { if (!b.disabled) b.checked = t.checked; });
155
+ syncCheckAll(t);
156
+ } else if (t.type === "checkbox" && t.name) {
157
+ document.querySelectorAll('[data-ui-checkall="' + t.name + '"]').forEach(syncCheckAll);
158
+ }
159
+ });
160
+
161
+ // ── slider: the filled part of the track follows the value
162
+ function slider(el) {
163
+ var min = +el.min || 0, max = el.max === "" ? 100 : +el.max, pct = (el.value - min) / (max - min || 1) * 100;
164
+ el.style.setProperty("--value", pct + "%");
165
+ var out = el.id && document.querySelector('output[for~="' + el.id + '"]');
166
+ if (out) out.textContent = (el.getAttribute("data-prefix") || "") + Number(el.value).toLocaleString() + (el.getAttribute("data-suffix") || "");
167
+ }
168
+ document.addEventListener("input", function (e) { if (e.target.classList && e.target.classList.contains("ui-slider")) slider(e.target); });
169
+
170
+ // ── button group: with data-ui-single on the group, pressing one releases the others
171
+ document.addEventListener("click", function (e) {
172
+ var b = e.target.closest && e.target.closest(".ui-btngroup-item[aria-pressed]");
173
+ if (!b || b.disabled) return;
174
+ var group = b.closest(".ui-btngroup");
175
+ if (group && group.hasAttribute("data-ui-single")) {
176
+ group.querySelectorAll(".ui-btngroup-item[aria-pressed]").forEach(function (x) { x.setAttribute("aria-pressed", String(x === b)); });
177
+ } else b.setAttribute("aria-pressed", String(b.getAttribute("aria-pressed") !== "true"));
178
+ group && group.dispatchEvent(new CustomEvent("ui-change", { bubbles: true, detail: { item: b } }));
179
+ });
180
+
181
+ // ── dropdown menu (the menu button pattern): Enter, Space or ArrowDown opens on the first item, ArrowUp on the last
182
+ var openMenuBtn = null;
183
+ function menuOf(btn) { return document.getElementById(btn.getAttribute("aria-controls")); }
184
+ function menuItems(menu) { return Array.prototype.slice.call(menu.querySelectorAll('[role="menuitem"]:not([aria-disabled="true"])')); }
185
+ function openMenu(btn, at) {
186
+ if (openMenuBtn && openMenuBtn !== btn) closeMenu(false);
187
+ var menu = menuOf(btn), wrap = btn.closest(".ui-menu-wrap");
188
+ if (!menu) return;
189
+ menu.hidden = false; btn.setAttribute("aria-expanded", "true"); openMenuBtn = btn;
190
+ // flip to the right edge when the menu would run off the screen
191
+ if (wrap) { wrap.removeAttribute("data-align"); if (menu.getBoundingClientRect().right > window.innerWidth - 8) wrap.setAttribute("data-align", "end"); }
192
+ var items = menuItems(menu);
193
+ if (items.length) (at === "last" ? items[items.length - 1] : items[0]).focus();
194
+ }
195
+ function closeMenu(focusBack) {
196
+ var btn = openMenuBtn;
197
+ if (!btn) return;
198
+ var menu = menuOf(btn);
199
+ if (menu) menu.hidden = true;
200
+ btn.setAttribute("aria-expanded", "false"); openMenuBtn = null;
201
+ if (focusBack) btn.focus();
202
+ }
203
+ document.addEventListener("click", function (e) {
204
+ var btn = e.target.closest && e.target.closest('[aria-haspopup="menu"]');
205
+ if (btn) { if (btn.getAttribute("aria-expanded") === "true") closeMenu(true); else openMenu(btn); return; }
206
+ var item = e.target.closest && e.target.closest('[role="menuitem"]');
207
+ if (item && openMenuBtn && menuOf(openMenuBtn).contains(item)) {
208
+ if (item.getAttribute("aria-disabled") === "true") return;
209
+ item.dispatchEvent(new CustomEvent("ui-select", { bubbles: true, detail: { item: item } }));
210
+ closeMenu(true); return;
211
+ }
212
+ if (openMenuBtn && !(menuOf(openMenuBtn) || document.body).contains(e.target)) closeMenu(false);
213
+ });
214
+ document.addEventListener("keydown", function (e) {
215
+ var btn = e.target.closest && e.target.closest('[aria-haspopup="menu"]');
216
+ if (btn && (e.key === "ArrowDown" || e.key === "ArrowUp")) { e.preventDefault(); openMenu(btn, e.key === "ArrowUp" ? "last" : "first"); return; }
217
+ if (!openMenuBtn) return;
218
+ var menu = menuOf(openMenuBtn);
219
+ if (!menu || !menu.contains(e.target)) return;
220
+ var items = menuItems(menu), i = items.indexOf(document.activeElement), next = null;
221
+ if (e.key === "ArrowDown") next = items[(i + 1) % items.length];
222
+ else if (e.key === "ArrowUp") next = items[(i - 1 + items.length) % items.length];
223
+ else if (e.key === "Home") next = items[0];
224
+ else if (e.key === "End") next = items[items.length - 1];
225
+ else if (e.key === "Escape") { e.preventDefault(); e.stopPropagation(); closeMenu(true); return; }
226
+ else if (e.key === "Tab") { closeMenu(false); return; }
227
+ else if (e.key.length === 1 && /\S/.test(e.key)) {
228
+ // type to jump: the next item starting with that letter
229
+ var k = e.key.toLowerCase(), order = items.slice(i + 1).concat(items.slice(0, i + 1));
230
+ next = order.filter(function (x) { return x.textContent.trim().toLowerCase().indexOf(k) === 0; })[0] || null;
231
+ }
232
+ if (next) { e.preventDefault(); next.focus(); }
233
+ });
234
+
235
+ // ── text editor: a toolbar of the browser's editing commands over a contenteditable area. execCommand is old but
236
+ // still supported everywhere and keeps undo working; a product that needs more should use a real editor library
237
+ function editor(root) {
238
+ if (root._uiEditor) return;
239
+ root._uiEditor = true;
240
+ var area = root.querySelector(".ui-editor-area"), tools = Array.prototype.slice.call(root.querySelectorAll(".ui-editor-tool"));
241
+ var count = root.querySelector("[data-ui-count]");
242
+ tools.forEach(function (t, i) { t.tabIndex = i === 0 ? 0 : -1; });
243
+ function sync() {
244
+ tools.forEach(function (t) {
245
+ var cmd = t.getAttribute("data-cmd"), on = false;
246
+ try { on = cmd === "formatBlock" ? /blockquote/i.test(document.queryCommandValue("formatBlock")) : document.queryCommandState(cmd); } catch (x) { /* not a state command */ }
247
+ if (t.hasAttribute("aria-pressed")) t.setAttribute("aria-pressed", String(!!on));
248
+ });
249
+ if (count) { var words = area.textContent.trim().split(/\s+/).filter(Boolean).length; count.textContent = words + (words === 1 ? " word" : " words"); }
250
+ }
251
+ root.querySelector(".ui-editor-toolbar").addEventListener("keydown", function (e) {
252
+ var i = tools.indexOf(document.activeElement), next = null;
253
+ if (e.key === "ArrowRight") next = tools[(i + 1) % tools.length];
254
+ else if (e.key === "ArrowLeft") next = tools[(i - 1 + tools.length) % tools.length];
255
+ else if (e.key === "Home") next = tools[0];
256
+ else if (e.key === "End") next = tools[tools.length - 1];
257
+ if (next) { e.preventDefault(); tools.forEach(function (t) { t.tabIndex = -1; }); next.tabIndex = 0; next.focus(); }
258
+ });
259
+ // mousedown keeps the selection in the text while a tool is clicked
260
+ tools.forEach(function (t) {
261
+ t.addEventListener("mousedown", function (e) { e.preventDefault(); });
262
+ t.addEventListener("click", function () {
263
+ var cmd = t.getAttribute("data-cmd");
264
+ area.focus();
265
+ if (cmd === "formatBlock") document.execCommand("formatBlock", false, /blockquote/i.test(document.queryCommandValue("formatBlock")) ? "p" : "blockquote");
266
+ else document.execCommand(cmd, false, null);
267
+ sync();
268
+ area.dispatchEvent(new Event("input", { bubbles: true }));
269
+ });
270
+ });
271
+ area.addEventListener("input", sync);
272
+ document.addEventListener("selectionchange", function () { if (area.contains(document.getSelection().anchorNode)) sync(); });
273
+ sync();
274
+ }
275
+
276
+ // ── video player
277
+ var ICON = {
278
+ play: '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7 4.5v15a1 1 0 0 0 1.5.86l12.5-7.5a1 1 0 0 0 0-1.72L8.5 3.64A1 1 0 0 0 7 4.5Z"/></svg>',
279
+ pause: '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="4" width="4" height="16" rx="1"/><rect x="14" y="4" width="4" height="16" rx="1"/></svg>',
280
+ sound: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 5 6 9H2v6h4l5 4V5Z"/><path d="M15.5 8.5a5 5 0 0 1 0 7M19 5a10 10 0 0 1 0 14"/></svg>',
281
+ muted: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 5 6 9H2v6h4l5 4V5Z"/><path d="m22 9-6 6M16 9l6 6"/></svg>'
282
+ };
283
+ function clock(s) { s = Math.max(0, Math.floor(s || 0)); return Math.floor(s / 60) + ":" + String(s % 60).padStart(2, "0"); }
284
+ function video(root) {
285
+ if (root._uiVideo) return;
286
+ root._uiVideo = true;
287
+ var v = root.querySelector("video"), play = root.querySelector("[data-ui-play]"), big = root.querySelector(".ui-video-big");
288
+ var mute = root.querySelector("[data-ui-mute]"), full = root.querySelector("[data-ui-full]"), seek = root.querySelector(".ui-slider"), time = root.querySelector(".ui-video-time");
289
+ var idle = null;
290
+ function wake() { root.removeAttribute("data-idle"); clearTimeout(idle); idle = setTimeout(function () { if (!root.contains(document.activeElement) || document.activeElement === v) root.setAttribute("data-idle", ""); }, 2000); }
291
+ function paint() {
292
+ var on = !v.paused;
293
+ root.toggleAttribute("data-playing", on);
294
+ if (play) { play.innerHTML = on ? ICON.pause : ICON.play; play.setAttribute("aria-label", on ? "Pause" : "Play"); }
295
+ if (mute) { mute.innerHTML = v.muted ? ICON.muted : ICON.sound; mute.setAttribute("aria-label", v.muted ? "Unmute" : "Mute"); }
296
+ if (seek && v.duration) {
297
+ seek.max = String(Math.round(v.duration * 10)); seek.value = String(Math.round(v.currentTime * 10));
298
+ seek.setAttribute("aria-valuetext", clock(v.currentTime) + " of " + clock(v.duration)); slider(seek);
299
+ }
300
+ if (time) time.textContent = clock(v.currentTime) + " / " + clock(v.duration);
301
+ }
302
+ function toggle() { if (v.paused) v.play(); else v.pause(); }
303
+ [play, big].forEach(function (b) { if (b) b.addEventListener("click", toggle); });
304
+ v.addEventListener("click", toggle);
305
+ if (mute) mute.addEventListener("click", function () { v.muted = !v.muted; paint(); });
306
+ if (full) full.addEventListener("click", function () { if (document.fullscreenElement) document.exitFullscreen(); else if (root.requestFullscreen) root.requestFullscreen(); });
307
+ if (seek) seek.addEventListener("input", function () { v.currentTime = seek.value / 10; });
308
+ ["play", "pause", "timeupdate", "loadedmetadata", "volumechange", "ended"].forEach(function (n) { v.addEventListener(n, paint); });
309
+ root.addEventListener("pointermove", wake);
310
+ root.addEventListener("focusin", wake);
311
+ // k or Space plays and pauses, unless focus is on a control that uses the key itself
312
+ root.addEventListener("keydown", function (e) {
313
+ if (e.target.closest("button, input")) return;
314
+ if (e.key === " " || e.key === "k") { e.preventDefault(); toggle(); }
315
+ });
316
+ paint();
317
+ }
318
+
319
+ // ── pagination: [data-ui-pagination data-page data-count] draws itself and moves; fires "ui-page" with the new page
320
+ function pageList(page, count) {
321
+ var want = [1, count, page - 1, page, page + 1].filter(function (n, i, a) { return n >= 1 && n <= count && a.indexOf(n) === i; }).sort(function (a, b) { return a - b; });
322
+ var out = []; want.forEach(function (n, i) { if (i && n - want[i - 1] > 1) out.push(null); out.push(n); }); return out;
323
+ }
324
+ var ARROW_L = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>';
325
+ var ARROW_R = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 18 6-6-6-6"/></svg>';
326
+ function pagination(nav, focusPage) {
327
+ var page = +nav.getAttribute("data-page") || 1, count = +nav.getAttribute("data-count") || 1;
328
+ nav.innerHTML = '<button type="button" class="ui-btn ui-btn--sm" data-ui-to="' + (page - 1) + '"' + (page <= 1 ? " disabled" : "") + ">" + ARROW_L + "Previous</button>" +
329
+ '<ul class="ui-pagination-pages">' + pageList(page, count).map(function (n) {
330
+ return "<li>" + (n === null ? '<span class="ui-page-gap" aria-hidden="true">…</span>' : '<button type="button" class="ui-page" data-ui-to="' + n + '" aria-label="Page ' + n + '"' + (n === page ? ' aria-current="page"' : "") + ">" + n + "</button>") + "</li>";
331
+ }).join("") + "</ul>" +
332
+ '<span class="ui-pagination-info" aria-live="polite">Page ' + page + " of " + count + "</span>" +
333
+ '<button type="button" class="ui-btn ui-btn--sm" data-ui-to="' + (page + 1) + '"' + (page >= count ? " disabled" : "") + ">Next" + ARROW_R + "</button>";
334
+ if (focusPage) { var cur = nav.querySelector('[aria-current="page"]'); if (cur) cur.focus(); }
335
+ }
336
+ document.addEventListener("click", function (e) {
337
+ var b = e.target.closest && e.target.closest("[data-ui-pagination] [data-ui-to]");
338
+ if (!b || b.disabled) return;
339
+ var nav = b.closest("[data-ui-pagination]");
340
+ nav.setAttribute("data-page", b.getAttribute("data-ui-to"));
341
+ pagination(nav, true);
342
+ nav.dispatchEvent(new CustomEvent("ui-page", { bubbles: true, detail: { page: +nav.getAttribute("data-page") } }));
343
+ });
344
+
345
+ // ── calendar: [data-ui-calendar data-value="2026-04-18" data-events="2026-04-11,…"] draws a month and keeps
346
+ // the roving day; choosing a day fires "ui-date" with { value: "YYYY-MM-DD" }
347
+ var MONTH_NAMES = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
348
+ var DOW = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"], DOW_LONG = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
349
+ function iso(d) { return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0"); }
350
+ function fromIso(s) { var m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s || ""); return m ? new Date(+m[1], +m[2] - 1, +m[3]) : null; }
351
+ function calendar(el, focusDay) {
352
+ var value = fromIso(el.getAttribute("data-value")), today = new Date(), events = (el.getAttribute("data-events") || "").split(",");
353
+ var cursor = fromIso(el.getAttribute("data-cursor")) || value || today;
354
+ el.setAttribute("data-cursor", iso(cursor));
355
+ var y = cursor.getFullYear(), mo = cursor.getMonth(), first = new Date(y, mo, 1), start = new Date(y, mo, 1 - first.getDay());
356
+ var id = el.id || (el.id = "cal-" + Math.random().toString(36).slice(2, 8));
357
+ var rows = "";
358
+ for (var w = 0; w < 6; w++) {
359
+ rows += "<tr>";
360
+ for (var d = 0; d < 7; d++) {
361
+ var day = new Date(start.getFullYear(), start.getMonth(), start.getDate() + w * 7 + d), s = iso(day);
362
+ rows += '<td role="gridcell"><button type="button" class="ui-day" data-date="' + s + '" tabindex="' + (s === iso(cursor) ? 0 : -1) + '"' +
363
+ ' aria-label="' + DOW_LONG[day.getDay()] + ", " + MONTH_NAMES[day.getMonth()] + " " + day.getDate() + (events.indexOf(s) >= 0 ? ", has events" : "") + '"' +
364
+ (day.getMonth() !== mo ? " data-outside" : "") + (s === iso(today) ? ' aria-current="date"' : "") +
365
+ ' aria-selected="' + (!!value && s === iso(value)) + '"' + (events.indexOf(s) >= 0 ? " data-event" : "") + ">" + day.getDate() + "</button></td>";
366
+ }
367
+ rows += "</tr>";
368
+ }
369
+ var NAV = function (dir, label, path) { return '<button type="button" class="ui-btn ui-btn--sm ui-btn--icon ui-btn--ghost" data-cal-step="' + dir + '" aria-label="' + label + '"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="' + path + '"/></svg></button>'; };
370
+ el.innerHTML = '<div class="ui-calendar-head">' + NAV(-1, "Previous month", "m15 18-6-6 6-6") +
371
+ '<h3 class="ui-calendar-title" id="' + id + '-t" aria-live="polite">' + MONTH_NAMES[mo] + " " + y + "</h3>" + NAV(1, "Next month", "m9 18 6-6-6-6") + "</div>" +
372
+ '<table role="grid" aria-labelledby="' + id + '-t"><thead><tr>' + DOW.map(function (x, i) { return '<th scope="col" abbr="' + DOW_LONG[i] + '">' + x + "</th>"; }).join("") + "</tr></thead><tbody>" + rows + "</tbody></table>";
373
+ if (focusDay) { var f = el.querySelector('[tabindex="0"]'); if (f) f.focus(); }
374
+ }
375
+ function calMove(el, days, months) {
376
+ var c = fromIso(el.getAttribute("data-cursor")), n = months ? new Date(c.getFullYear(), c.getMonth() + months, Math.min(c.getDate(), 28)) : new Date(c.getFullYear(), c.getMonth(), c.getDate() + days);
377
+ el.setAttribute("data-cursor", iso(n)); calendar(el, true);
378
+ }
379
+ document.addEventListener("click", function (e) {
380
+ var step = e.target.closest && e.target.closest("[data-ui-calendar] [data-cal-step]");
381
+ if (step) { var cal = step.closest("[data-ui-calendar]"), c = fromIso(cal.getAttribute("data-cursor")); cal.setAttribute("data-cursor", iso(new Date(c.getFullYear(), c.getMonth() + +step.getAttribute("data-cal-step"), 1))); calendar(cal, false); return; }
382
+ var day = e.target.closest && e.target.closest("[data-ui-calendar] .ui-day");
383
+ if (day && !day.disabled) {
384
+ var el = day.closest("[data-ui-calendar]");
385
+ el.setAttribute("data-value", day.getAttribute("data-date")); el.setAttribute("data-cursor", day.getAttribute("data-date")); calendar(el, true);
386
+ el.dispatchEvent(new CustomEvent("ui-date", { bubbles: true, detail: { value: day.getAttribute("data-date") } }));
387
+ }
388
+ });
389
+ document.addEventListener("keydown", function (e) {
390
+ var day = e.target.closest && e.target.closest("[data-ui-calendar] .ui-day");
391
+ if (!day) return;
392
+ var el = day.closest("[data-ui-calendar]"), k = e.key, c = fromIso(el.getAttribute("data-cursor"));
393
+ var moves = { ArrowLeft: [-1], ArrowRight: [1], ArrowUp: [-7], ArrowDown: [7], Home: [-c.getDay()], End: [6 - c.getDay()], PageUp: [0, -1], PageDown: [0, 1] };
394
+ if (moves[k]) { e.preventDefault(); calMove(el, moves[k][0], moves[k][1]); }
395
+ });
396
+
397
+ // ── date picker: [data-ui-datepicker] around a .ui-datepicker-btn and a hidden .ui-datepicker-pop with a calendar
398
+ function datepickerShow(dp, open) {
399
+ var pop = dp.querySelector(".ui-datepicker-pop"), btn = dp.querySelector(".ui-datepicker-btn");
400
+ pop.hidden = !open; btn.setAttribute("aria-expanded", String(open));
401
+ if (open) {
402
+ dp.removeAttribute("data-align");
403
+ if (pop.getBoundingClientRect().right > window.innerWidth - 8) dp.setAttribute("data-align", "end");
404
+ var cal = pop.querySelector("[data-ui-calendar]"); calendar(cal, true);
405
+ }
406
+ }
407
+ function datepickerLabel(dp) {
408
+ var v = fromIso(dp.querySelector("[data-ui-calendar]").getAttribute("data-value")), btn = dp.querySelector(".ui-datepicker-btn"), out = btn.querySelector("span");
409
+ if (v) { out.textContent = v.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric", year: "numeric" }); btn.removeAttribute("data-empty"); }
410
+ }
411
+ document.addEventListener("click", function (e) {
412
+ var btn = e.target.closest && e.target.closest("[data-ui-datepicker] .ui-datepicker-btn");
413
+ if (btn) { var dp = btn.closest("[data-ui-datepicker]"); datepickerShow(dp, dp.querySelector(".ui-datepicker-pop").hidden); return; }
414
+ document.querySelectorAll("[data-ui-datepicker]").forEach(function (dp) { if (!dp.contains(e.target) && !dp.querySelector(".ui-datepicker-pop").hidden) datepickerShow(dp, false); });
415
+ });
416
+ document.addEventListener("ui-date", function (e) {
417
+ var dp = e.target.closest && e.target.closest("[data-ui-datepicker]");
418
+ if (!dp) return;
419
+ datepickerLabel(dp); datepickerShow(dp, false); dp.querySelector(".ui-datepicker-btn").focus();
420
+ });
421
+ document.addEventListener("keydown", function (e) {
422
+ if (e.key !== "Escape") return;
423
+ var dp = e.target.closest && e.target.closest("[data-ui-datepicker]");
424
+ if (dp && !dp.querySelector(".ui-datepicker-pop").hidden) { e.preventDefault(); e.stopPropagation(); datepickerShow(dp, false); dp.querySelector(".ui-datepicker-btn").focus(); }
425
+ });
426
+
427
+ // ── copy: [data-ui-copy="#id"] copies that element's text, then says so on the button for two seconds
428
+ document.addEventListener("click", function (e) {
429
+ var b = e.target.closest && e.target.closest("[data-ui-copy]");
430
+ if (!b) return;
431
+ var src = document.querySelector(b.getAttribute("data-ui-copy")), label = b.querySelector("span") || b, was = label.textContent;
432
+ if (!src || !navigator.clipboard) return;
433
+ navigator.clipboard.writeText(src.textContent).then(function () { label.textContent = "Copied"; setTimeout(function () { label.textContent = was; }, 2000); },
434
+ function () { label.textContent = "Copy failed"; setTimeout(function () { label.textContent = was; }, 2000); });
435
+ });
436
+
437
+ // ── color picker: [data-ui-colorpick] keeps its swatches (a radiogroup), the native picker and the hex field in step;
438
+ // fires "ui-color" with { value: "#rrggbb" }
439
+ function colorpickSet(root, hex, from) {
440
+ hex = hex.toLowerCase();
441
+ root.querySelectorAll(".ui-swatch").forEach(function (s) { var on = s.getAttribute("data-color").toLowerCase() === hex; s.setAttribute("aria-checked", String(on)); s.tabIndex = on ? 0 : -1; });
442
+ if (!root.querySelector('.ui-swatch[aria-checked="true"]')) { var f = root.querySelector(".ui-swatch"); if (f) f.tabIndex = 0; }
443
+ var native = root.querySelector('input[type="color"]'), text = root.querySelector('input[type="text"]');
444
+ if (native && from !== native) native.value = hex;
445
+ if (text && from !== text) text.value = hex;
446
+ root.dispatchEvent(new CustomEvent("ui-color", { bubbles: true, detail: { value: hex } }));
447
+ }
448
+ document.addEventListener("click", function (e) {
449
+ var s = e.target.closest && e.target.closest("[data-ui-colorpick] .ui-swatch");
450
+ if (s) colorpickSet(s.closest("[data-ui-colorpick]"), s.getAttribute("data-color"), s);
451
+ });
452
+ document.addEventListener("input", function (e) {
453
+ var root = e.target.closest && e.target.closest("[data-ui-colorpick]");
454
+ if (!root || e.target.tagName !== "INPUT") return;
455
+ var v = e.target.value.trim();
456
+ if (e.target.type === "text") { if (/^#?[0-9a-f]{6}$/i.test(v)) colorpickSet(root, v[0] === "#" ? v : "#" + v, e.target); e.target.setAttribute("aria-invalid", String(!/^#?[0-9a-f]{6}$/i.test(v))); }
457
+ else colorpickSet(root, v, e.target);
458
+ });
459
+ document.addEventListener("keydown", function (e) {
460
+ var s = e.target.closest && e.target.closest("[data-ui-colorpick] .ui-swatch");
461
+ if (!s) return;
462
+ var all = Array.prototype.slice.call(s.parentElement.querySelectorAll(".ui-swatch")), i = all.indexOf(s), n = null;
463
+ if (e.key === "ArrowRight" || e.key === "ArrowDown") n = all[(i + 1) % all.length];
464
+ else if (e.key === "ArrowLeft" || e.key === "ArrowUp") n = all[(i - 1 + all.length) % all.length];
465
+ if (n) { e.preventDefault(); colorpickSet(s.closest("[data-ui-colorpick]"), n.getAttribute("data-color"), n); n.focus(); }
466
+ });
467
+
468
+ // ── command menu: a <dialog data-ui-command> with an input (role="combobox") over a listbox of
469
+ // [role="option"][data-keywords]; ⌘K or Ctrl+K opens the first one on the page; Enter fires "ui-command"
470
+ function commandFilter(dlg) {
471
+ var q = dlg.querySelector('[role="combobox"]').value.trim().toLowerCase(), shown = 0;
472
+ dlg.querySelectorAll('[role="option"]').forEach(function (o) { var hit = !q || (o.textContent + " " + (o.getAttribute("data-keywords") || "")).toLowerCase().indexOf(q) >= 0; o.hidden = !hit; if (hit) shown++; });
473
+ dlg.querySelectorAll('[role="group"]').forEach(function (g) { g.hidden = !g.querySelector('[role="option"]:not([hidden])'); });
474
+ var empty = dlg.querySelector(".ui-command-empty"); if (empty) empty.hidden = shown > 0;
475
+ commandActive(dlg, dlg.querySelector('[role="option"]:not([hidden])'));
476
+ }
477
+ function commandActive(dlg, opt) {
478
+ var input = dlg.querySelector('[role="combobox"]');
479
+ dlg.querySelectorAll('[role="option"]').forEach(function (o) { o.setAttribute("aria-selected", String(o === opt)); });
480
+ if (opt) { input.setAttribute("aria-activedescendant", opt.id); opt.scrollIntoView({ block: "nearest" }); } else input.removeAttribute("aria-activedescendant");
481
+ }
482
+ function commandRun(dlg, opt) { if (!opt) return; opt.dispatchEvent(new CustomEvent("ui-command", { bubbles: true, detail: { item: opt } })); dlg.close(); }
483
+ function commandOpen(dlg, opener) { var input = dlg.querySelector('[role="combobox"]'); input.value = ""; commandFilter(dlg); openDialog(dlg, opener); input.focus(); }
484
+ document.addEventListener("input", function (e) { var dlg = e.target.closest && e.target.closest("[data-ui-command]"); if (dlg && e.target.getAttribute("role") === "combobox") commandFilter(dlg); });
485
+ document.addEventListener("keydown", function (e) {
486
+ if ((e.metaKey || e.ctrlKey) && (e.key === "k" || e.key === "K")) { var d = document.querySelector("dialog[data-ui-command]"); if (d && !d.open) { e.preventDefault(); wireDialog(d); commandOpen(d, document.activeElement); } return; }
487
+ var dlg = e.target.closest && e.target.closest("[data-ui-command]");
488
+ if (!dlg || e.target.getAttribute("role") !== "combobox") return;
489
+ var opts = Array.prototype.slice.call(dlg.querySelectorAll('[role="option"]:not([hidden])')), cur = dlg.querySelector('[role="option"][aria-selected="true"]'), i = opts.indexOf(cur);
490
+ if (e.key === "ArrowDown") { e.preventDefault(); commandActive(dlg, opts[(i + 1) % opts.length]); }
491
+ else if (e.key === "ArrowUp") { e.preventDefault(); commandActive(dlg, opts[(i - 1 + opts.length) % opts.length]); }
492
+ else if (e.key === "Enter") { e.preventDefault(); commandRun(dlg, cur); }
493
+ });
494
+ document.addEventListener("click", function (e) {
495
+ var open = e.target.closest && e.target.closest("[data-ui-command-open]");
496
+ if (open) { var d = document.getElementById(open.getAttribute("data-ui-command-open")); if (d) { wireDialog(d); commandOpen(d, open); } return; }
497
+ var opt = e.target.closest && e.target.closest("[data-ui-command] [role='option']");
498
+ if (opt) commandRun(opt.closest("[data-ui-command]"), opt);
499
+ });
500
+ document.addEventListener("mousemove", function (e) { var opt = e.target.closest && e.target.closest("[data-ui-command] [role='option']"); if (opt && opt.getAttribute("aria-selected") !== "true") commandActive(opt.closest("[data-ui-command]"), opt); });
501
+
502
+ // ── file drop zone: [data-ui-dropzone] around a label.ui-dropzone (with its file input) and a ul.ui-files;
503
+ // lists what was chosen or dropped (nothing is uploaded) and fires "ui-files" with the File list
504
+ function fileSize(n) { return n < 1024 ? n + " B" : n < 1048576 ? (n / 1024).toFixed(1) + " KB" : (n / 1048576).toFixed(1) + " MB"; }
505
+ var FILE_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/></svg>';
506
+ var X_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg>';
507
+ function dropzoneAdd(root, files) {
508
+ var list = root.querySelector(".ui-files");
509
+ Array.prototype.forEach.call(files, function (f) {
510
+ var li = document.createElement("li"); li.className = "ui-file";
511
+ li.innerHTML = FILE_ICON + '<span class="ui-file-name"></span><span class="ui-file-size">' + fileSize(f.size) + '</span><button type="button" class="ui-tag-remove" data-ui-file-remove></button>';
512
+ li.querySelector(".ui-file-name").textContent = f.name;
513
+ li.querySelector("button").setAttribute("aria-label", "Remove " + f.name); li.querySelector("button").innerHTML = X_ICON;
514
+ list.appendChild(li);
515
+ });
516
+ root.dispatchEvent(new CustomEvent("ui-files", { bubbles: true, detail: { files: files } }));
517
+ }
518
+ document.addEventListener("change", function (e) { var root = e.target.closest && e.target.closest("[data-ui-dropzone]"); if (root && e.target.type === "file") { dropzoneAdd(root, e.target.files); e.target.value = ""; } });
519
+ ["dragenter", "dragover"].forEach(function (n) { document.addEventListener(n, function (e) { var z = e.target.closest && e.target.closest(".ui-dropzone"); if (z && z.closest("[data-ui-dropzone]")) { e.preventDefault(); z.classList.add("is-drag"); } }); });
520
+ ["dragleave", "drop"].forEach(function (n) { document.addEventListener(n, function (e) { var z = e.target.closest && e.target.closest(".ui-dropzone"); if (z) { z.classList.remove("is-drag"); if (n === "drop" && z.closest("[data-ui-dropzone]")) { e.preventDefault(); dropzoneAdd(z.closest("[data-ui-dropzone]"), e.dataTransfer.files); } } }); });
521
+ document.addEventListener("click", function (e) {
522
+ var x = e.target.closest && e.target.closest("[data-ui-file-remove]"); if (!x) return;
523
+ var li = x.closest(".ui-file"), next = li.nextElementSibling || li.previousElementSibling, root = li.closest("[data-ui-dropzone]");
524
+ li.remove(); (next ? next.querySelector("button") : root.querySelector("input[type=file]")).focus();
525
+ });
526
+
527
+ // ── chips: .ui-chip[aria-pressed] toggles; fires "ui-change" from the chip
528
+ document.addEventListener("click", function (e) {
529
+ var c = e.target.closest && e.target.closest(".ui-chip[aria-pressed]");
530
+ if (!c || c.disabled) return;
531
+ c.setAttribute("aria-pressed", String(c.getAttribute("aria-pressed") !== "true"));
532
+ c.dispatchEvent(new CustomEvent("ui-change", { bubbles: true, detail: { item: c } }));
533
+ });
534
+
535
+ // ── chat: [data-ui-chat] sends from its composer: Enter sends, Shift+Enter adds a line; fires "ui-send" with the text
536
+ function chatSend(root) {
537
+ var box = root.querySelector(".ui-chat-compose textarea"), text = box.value.trim();
538
+ if (!text) return;
539
+ var li = document.createElement("li"); li.className = "ui-msg ui-msg--out";
540
+ li.innerHTML = '<div class="ui-msg-bubble"></div><span class="ui-msg-meta">You · just now</span>';
541
+ li.querySelector(".ui-msg-bubble").textContent = text;
542
+ var log = root.querySelector(".ui-chat-log"); log.appendChild(li); log.scrollTop = log.scrollHeight;
543
+ box.value = ""; box.focus();
544
+ root.dispatchEvent(new CustomEvent("ui-send", { bubbles: true, detail: { text: text } }));
545
+ }
546
+ document.addEventListener("keydown", function (e) {
547
+ if (e.key !== "Enter" || e.shiftKey || e.isComposing) return;
548
+ var root = e.target.closest && e.target.closest("[data-ui-chat]");
549
+ if (root && e.target.tagName === "TEXTAREA") { e.preventDefault(); chatSend(root); }
550
+ });
551
+ document.addEventListener("click", function (e) { var b = e.target.closest && e.target.closest("[data-ui-chat] [data-ui-send]"); if (b) chatSend(b.closest("[data-ui-chat]")); });
552
+
553
+ // ── tree: [role="tree"].ui-tree, one roving tab stop; fires "ui-select" from the chosen item
554
+ function treeVisible(tree) { return Array.prototype.slice.call(tree.querySelectorAll('[role="treeitem"]')).filter(function (t) { var p = t.parentElement.closest('[role="treeitem"]'); while (p) { if (p.getAttribute("aria-expanded") === "false") return false; p = p.parentElement.closest('[role="treeitem"]'); } return true; }); }
555
+ function treeFocus(tree, item) { tree.querySelectorAll('[role="treeitem"]').forEach(function (t) { t.tabIndex = t === item ? 0 : -1; }); item.focus(); }
556
+ function treeSelect(tree, item) { tree.querySelectorAll('[role="treeitem"]').forEach(function (t) { t.setAttribute("aria-selected", String(t === item)); }); item.dispatchEvent(new CustomEvent("ui-select", { bubbles: true, detail: { item: item } })); }
557
+ document.addEventListener("click", function (e) {
558
+ var row = e.target.closest && e.target.closest('.ui-tree .ui-tree-row');
559
+ if (!row) return;
560
+ var item = row.parentElement, tree = item.closest('[role="tree"]');
561
+ if (item.hasAttribute("aria-expanded")) item.setAttribute("aria-expanded", String(item.getAttribute("aria-expanded") !== "true"));
562
+ treeSelect(tree, item); treeFocus(tree, item);
563
+ });
564
+ document.addEventListener("keydown", function (e) {
565
+ var item = e.target.closest && e.target.matches && e.target.matches('.ui-tree [role="treeitem"]') ? e.target : null;
566
+ if (!item) return;
567
+ var tree = item.closest('[role="tree"]'), vis = treeVisible(tree), i = vis.indexOf(item), exp = item.getAttribute("aria-expanded"), n = null;
568
+ if (e.key === "ArrowDown") n = vis[i + 1];
569
+ else if (e.key === "ArrowUp") n = vis[i - 1];
570
+ else if (e.key === "Home") n = vis[0];
571
+ else if (e.key === "End") n = vis[vis.length - 1];
572
+ else if (e.key === "ArrowRight") { if (exp === "false") item.setAttribute("aria-expanded", "true"); else if (exp === "true") n = item.querySelector('[role="treeitem"]'); }
573
+ else if (e.key === "ArrowLeft") { if (exp === "true") item.setAttribute("aria-expanded", "false"); else n = item.parentElement.closest('[role="treeitem"]'); }
574
+ else if (e.key === "Enter" || e.key === " ") { treeSelect(tree, item); if (exp) item.setAttribute("aria-expanded", String(exp !== "true")); }
575
+ else return;
576
+ e.preventDefault();
577
+ if (n) treeFocus(tree, n);
578
+ });
579
+ function tree(t) { var items = t.querySelectorAll('[role="treeitem"]'), sel = t.querySelector('[aria-selected="true"]') || items[0]; items.forEach(function (x) { x.tabIndex = x === sel ? 0 : -1; }); }
580
+
581
+ // ── site header: on a narrow header the links fold into a menu; its button opens and closes the panel
582
+ document.addEventListener("click", function (e) {
583
+ var b = e.target.closest && e.target.closest(".ui-sitenav-toggle");
584
+ if (!b) return;
585
+ var panel = document.getElementById(b.getAttribute("aria-controls")), open = b.getAttribute("aria-expanded") !== "true";
586
+ b.setAttribute("aria-expanded", String(open)); b.setAttribute("aria-label", open ? "Close menu" : "Open menu");
587
+ if (panel) { panel.hidden = !open; if (open) { var first = panel.querySelector("a, button"); if (first) first.focus(); } }
588
+ });
589
+ document.addEventListener("keydown", function (e) {
590
+ if (e.key !== "Escape") return;
591
+ var nav = e.target.closest && e.target.closest(".ui-sitenav"), b = nav && nav.querySelector('.ui-sitenav-toggle[aria-expanded="true"]');
592
+ // claim the key, so a full-screen view or dialog around the header stays open
593
+ if (b) { e.preventDefault(); b.click(); b.focus(); }
594
+ });
595
+
596
+ // ── banner: [data-ui-banner-close] hides its banner and fires "ui-dismiss"; focus moves to the next focusable
597
+ document.addEventListener("click", function (e) {
598
+ var b = e.target.closest && e.target.closest("[data-ui-banner-close]");
599
+ if (!b) return;
600
+ var banner = b.closest(".ui-banner"); if (!banner) return;
601
+ banner.hidden = true; banner.dispatchEvent(new CustomEvent("ui-dismiss", { bubbles: true }));
602
+ var next = banner.nextElementSibling && banner.nextElementSibling.querySelector("a, button, input, select, textarea, [tabindex]");
603
+ if (next) next.focus();
604
+ });
605
+
606
+ // ── annotations: a pin ([data-annot="n"]) and its note ([data-annot-note="n"]) light up together
607
+ function annotShow(root, n) {
608
+ root.querySelectorAll("[data-annot]").forEach(function (p) { p.setAttribute("aria-pressed", String(p.getAttribute("data-annot") === n)); });
609
+ root.querySelectorAll("[data-annot-note]").forEach(function (li) { li.toggleAttribute("data-active", li.getAttribute("data-annot-note") === n); });
610
+ }
611
+ document.addEventListener("click", function (e) {
612
+ var pin = e.target.closest && e.target.closest(".ui-annot [data-annot]");
613
+ if (pin) { var root = pin.closest(".ui-annot"), on = pin.getAttribute("aria-pressed") !== "true"; annotShow(root, on ? pin.getAttribute("data-annot") : ""); }
614
+ });
615
+
616
+ // ── sidebar (ui-sidebar): a rail when wide, a drawer when its app shell is under 720px (components.css) ──
617
+ // data-ui-sidebar on the <aside>; buttons with aria-controls="<its id>" and data-ui-sidebar-toggle open and
618
+ // close it; a group's button (data-ui-sidebar-group) opens its body in place.
619
+ var SIDEBAR_NARROW = 720; // keep in step with the @container query in components.css
620
+ function sidebarNarrow(sb) { var app = sb.closest(".ui-app"); return (app ? app.getBoundingClientRect().width : window.innerWidth) <= SIDEBAR_NARROW; }
621
+ function sidebarSet(sb, open, focusBack) {
622
+ sb.setAttribute("data-state", open ? "expanded" : "collapsed");
623
+ Array.prototype.forEach.call(document.querySelectorAll('[data-ui-sidebar-toggle][aria-controls="' + sb.id + '"]'), function (b) {
624
+ b.setAttribute("aria-expanded", String(open));
625
+ if (b.classList.contains("ui-sidebar-toggle")) b.setAttribute("aria-label", sidebarNarrow(sb) ? "Close menu" : open ? "Collapse sidebar" : "Expand sidebar");
626
+ });
627
+ if (!open && focusBack) { var opener = document.querySelector('.ui-sidebar-open[aria-controls="' + sb.id + '"]'); if (opener && opener.offsetParent) opener.focus(); }
628
+ // a drawer that opens takes focus, so Tab moves through it and Escape closes it
629
+ if (open && focusBack && sidebarNarrow(sb)) { var first = sb.querySelector(".ui-sidebar-toggle, .ui-sidebar-item"); if (first) first.focus(); }
630
+ }
631
+ function sidebar(sb) { if (sidebarNarrow(sb)) sidebarSet(sb, false); else sidebarSet(sb, sb.getAttribute("data-state") !== "collapsed"); }
632
+ document.addEventListener("click", function (e) {
633
+ var t = e.target.closest && e.target.closest("[data-ui-sidebar-toggle]");
634
+ if (t) { var sb = document.getElementById(t.getAttribute("aria-controls")); if (sb) sidebarSet(sb, sb.getAttribute("data-state") !== "expanded", true); return; }
635
+ var g = e.target.closest && e.target.closest("[data-ui-sidebar-group]");
636
+ if (g) {
637
+ var bar = g.closest("[data-ui-sidebar]"), body = document.getElementById(g.getAttribute("aria-controls"));
638
+ if (bar && bar.getAttribute("data-state") === "collapsed" && !sidebarNarrow(bar)) { sidebarSet(bar, true); g.setAttribute("aria-expanded", "true"); if (body) body.hidden = false; return; }
639
+ var open = g.getAttribute("aria-expanded") !== "true"; g.setAttribute("aria-expanded", String(open)); if (body) body.hidden = !open; return;
640
+ }
641
+ // narrow: a tap outside the open drawer closes it
642
+ Array.prototype.forEach.call(document.querySelectorAll('[data-ui-sidebar][data-state="expanded"]'), function (sb) {
643
+ if (sidebarNarrow(sb) && !sb.contains(e.target)) sidebarSet(sb, false);
644
+ });
645
+ });
646
+ document.addEventListener("keydown", function (e) {
647
+ if (e.key !== "Escape" || e.defaultPrevented) return;
648
+ Array.prototype.forEach.call(document.querySelectorAll('[data-ui-sidebar][data-state="expanded"]'), function (sb) {
649
+ if (sidebarNarrow(sb)) { sidebarSet(sb, false, true); e.preventDefault(); }
650
+ });
651
+ });
652
+
653
+ function init(scope) {
654
+ scope = scope || document;
655
+ Array.prototype.forEach.call(scope.querySelectorAll("[data-ui-tabs]"), tabs);
656
+ Array.prototype.forEach.call(scope.querySelectorAll("[data-ui-sidebar]"), sidebar);
657
+ Array.prototype.forEach.call(scope.querySelectorAll("dialog.ui-dialog"), wireDialog);
658
+ Array.prototype.forEach.call(scope.querySelectorAll("[data-ui-chart]"), chart);
659
+ Array.prototype.forEach.call(scope.querySelectorAll("[data-indeterminate]"), function (b) { b.indeterminate = true; });
660
+ Array.prototype.forEach.call(scope.querySelectorAll("[data-ui-checkall]"), syncCheckAll);
661
+ Array.prototype.forEach.call(scope.querySelectorAll(".ui-slider"), slider);
662
+ Array.prototype.forEach.call(scope.querySelectorAll("[data-ui-editor]"), editor);
663
+ Array.prototype.forEach.call(scope.querySelectorAll("[data-ui-video]"), video);
664
+ Array.prototype.forEach.call(scope.querySelectorAll("[data-ui-pagination]"), function (n) { pagination(n, false); });
665
+ Array.prototype.forEach.call(scope.querySelectorAll("[data-ui-calendar]"), function (n) { calendar(n, false); });
666
+ Array.prototype.forEach.call(scope.querySelectorAll("[data-ui-datepicker]"), datepickerLabel);
667
+ Array.prototype.forEach.call(scope.querySelectorAll(".ui-tree[role='tree']"), tree);
668
+ Array.prototype.forEach.call(scope.querySelectorAll("[data-ui-colorpick]"), function (n) { var v = n.querySelector('input[type="color"]'); if (v) colorpickSet(n, v.value); });
669
+ }
670
+
671
+ // Escape dismisses any tooltip that is showing; leaving the wrap re-arms it
672
+ document.addEventListener("keydown", function (e) {
673
+ if (e.key !== "Escape") return;
674
+ Array.prototype.forEach.call(document.querySelectorAll(".ui-tooltip-wrap:hover, .ui-tooltip-wrap:focus-within"), function (w) { w.classList.add("is-dismissed"); });
675
+ });
676
+ function rearm(e) { var w = e.target.closest && e.target.closest(".ui-tooltip-wrap"); if (w && !w.contains(e.relatedTarget)) w.classList.remove("is-dismissed"); }
677
+ document.addEventListener("mouseout", rearm);
678
+ document.addEventListener("focusout", rearm);
679
+
680
+ function dismissTag(btn) {
681
+ var tag = btn.closest(".ui-tag");
682
+ if (!tag) return;
683
+ var list = tag.parentElement, all = Array.prototype.slice.call(list.querySelectorAll("[data-ui-dismiss]")), i = all.indexOf(btn);
684
+ var next = all[i + 1] || all[i - 1] || null;
685
+ tag.dispatchEvent(new CustomEvent("ui-dismiss", { bubbles: true }));
686
+ tag.remove();
687
+ if (next) next.focus();
688
+ else { if (!list.hasAttribute("tabindex")) list.setAttribute("tabindex", "-1"); list.focus(); }
689
+ }
690
+
691
+ document.addEventListener("click", function (e) {
692
+ var dismiss = e.target.closest && e.target.closest("[data-ui-dismiss]");
693
+ if (dismiss) { dismissTag(dismiss); return; }
694
+ var open = e.target.closest && e.target.closest("[data-ui-open]");
695
+ if (open) { var d = document.getElementById(open.getAttribute("data-ui-open")); if (d) { wireDialog(d); openDialog(d, open); } return; }
696
+ var close = e.target.closest && e.target.closest("[data-ui-close]");
697
+ if (close) closeDialog(close.closest("dialog"));
698
+ });
699
+
700
+ window.UI = { init: init, tabs: tabs, chart: chart, openDialog: openDialog, closeDialog: closeDialog };
701
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", function () { init(); });
702
+ else init();
703
+ })();