@gardenerim/css 1.0.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 (92) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/LICENSE +21 -0
  3. package/README.md +591 -0
  4. package/dist/components/ai-compositions.min.css +2 -0
  5. package/dist/components/ai-extended.min.css +2 -0
  6. package/dist/components/ai.min.css +2 -0
  7. package/dist/components/auth-compositions.min.css +2 -0
  8. package/dist/components/basic.min.css +2 -0
  9. package/dist/components/catalog.min.css +2 -0
  10. package/dist/components/commerce-compositions.min.css +2 -0
  11. package/dist/components/content-compositions.min.css +2 -0
  12. package/dist/components/data-compositions.min.css +2 -0
  13. package/dist/components/data.min.css +2 -0
  14. package/dist/components/desktop-compositions.min.css +2 -0
  15. package/dist/components/desktop.min.css +2 -0
  16. package/dist/components/extended.min.css +2 -0
  17. package/dist/components/feedback.min.css +2 -0
  18. package/dist/components/form-compositions.min.css +2 -0
  19. package/dist/components/forms.min.css +2 -0
  20. package/dist/components/grid.min.css +2 -0
  21. package/dist/components/help.min.css +2 -0
  22. package/dist/components/layouts.min.css +2 -0
  23. package/dist/components/mobile-compositions.min.css +2 -0
  24. package/dist/components/navigation-compositions.min.css +2 -0
  25. package/dist/components/navigation.min.css +2 -0
  26. package/dist/components/page.min.css +2 -0
  27. package/dist/components/primitives.min.css +2 -0
  28. package/dist/components/recipes.min.css +2 -0
  29. package/dist/components/regions.min.css +2 -0
  30. package/dist/components/selection-compositions.min.css +2 -0
  31. package/dist/components/solution-compositions.min.css +2 -0
  32. package/dist/gardener.ai.css +236 -0
  33. package/dist/gardener.ai.min.css +3 -0
  34. package/dist/gardener.ai.min.css.map +7 -0
  35. package/dist/gardener.builds.json +7491 -0
  36. package/dist/gardener.capabilities.json +3784 -0
  37. package/dist/gardener.compatibility.json +1209 -0
  38. package/dist/gardener.components.css +3496 -0
  39. package/dist/gardener.components.min.css +3 -0
  40. package/dist/gardener.components.min.css.map +7 -0
  41. package/dist/gardener.core.css +1486 -0
  42. package/dist/gardener.core.min.css +3 -0
  43. package/dist/gardener.core.min.css.map +7 -0
  44. package/dist/gardener.css +7153 -0
  45. package/dist/gardener.d.ts +42 -0
  46. package/dist/gardener.electron.d.ts +5 -0
  47. package/dist/gardener.electron.js +24 -0
  48. package/dist/gardener.electron.min.js +3 -0
  49. package/dist/gardener.electron.min.js.map +7 -0
  50. package/dist/gardener.manifest.json +14557 -0
  51. package/dist/gardener.min.css +3 -0
  52. package/dist/gardener.min.css.map +7 -0
  53. package/dist/gardener.performance.json +1628 -0
  54. package/dist/gardener.public-api.json +2169 -0
  55. package/dist/gardener.recipes.json +1444 -0
  56. package/dist/gardener.runtime.js +2224 -0
  57. package/dist/gardener.runtime.min.js +3 -0
  58. package/dist/gardener.runtime.min.js.map +7 -0
  59. package/dist/gardener.tauri.d.ts +5 -0
  60. package/dist/gardener.tauri.js +28 -0
  61. package/dist/gardener.tauri.min.js +3 -0
  62. package/dist/gardener.tauri.min.js.map +7 -0
  63. package/dist/gardener.themes.css +1415 -0
  64. package/dist/gardener.themes.min.css +3 -0
  65. package/dist/gardener.themes.min.css.map +7 -0
  66. package/dist/gardener.utilities.css +1940 -0
  67. package/dist/gardener.utilities.json +92279 -0
  68. package/dist/gardener.utilities.min.css +3 -0
  69. package/dist/gardener.utilities.min.css.map +7 -0
  70. package/dist/platforms/gardener.desktop.min.css +2 -0
  71. package/dist/platforms/gardener.electron.min.css +2 -0
  72. package/dist/platforms/gardener.mobile.min.css +2 -0
  73. package/dist/platforms/gardener.tauri.min.css +2 -0
  74. package/dist/platforms/gardener.web.min.css +2 -0
  75. package/metadata/builds.schema.json +148 -0
  76. package/metadata/capabilities.json +3784 -0
  77. package/metadata/capabilities.schema.json +61 -0
  78. package/metadata/compatibility.json +1209 -0
  79. package/metadata/compatibility.schema.json +85 -0
  80. package/metadata/components.json +9082 -0
  81. package/metadata/components.schema.json +58 -0
  82. package/metadata/custom-build.schema.json +46 -0
  83. package/metadata/manifest.schema.json +47 -0
  84. package/metadata/performance-budgets.schema.json +103 -0
  85. package/metadata/performance-report.schema.json +194 -0
  86. package/metadata/public-api.json +2169 -0
  87. package/metadata/public-api.schema.json +165 -0
  88. package/metadata/recipes.json +1444 -0
  89. package/metadata/recipes.schema.json +39 -0
  90. package/metadata/schema.json +7 -0
  91. package/metadata/utilities.schema.json +29 -0
  92. package/package.json +177 -0
@@ -0,0 +1,2224 @@
1
+ /** Gardener runtime v1.0.0 — framework-agnostic, accessible component behavior. */
2
+
3
+ const instanceStores = new WeakMap();
4
+ const registry = new Map();
5
+ const focusableSelector = [
6
+ "a[href]", "button:not([disabled])", "input:not([disabled])", "select:not([disabled])",
7
+ "textarea:not([disabled])", "[tabindex]:not([tabindex='-1'])", "[contenteditable='true']"
8
+ ].join(",");
9
+
10
+ const uid = (prefix = "g") => `${prefix}-${Math.random().toString(36).slice(2, 9)}`;
11
+ const numeric = (value, fallback) => value !== "" && value != null && Number.isFinite(Number(value)) ? Number(value) : fallback;
12
+ const targetById = (value) => value ? document.getElementById(value.replace(/^#/, "")) : null;
13
+ const visibleFocusable = (container) => [...container.querySelectorAll(focusableSelector)].filter((item) => item.getClientRects().length > 0);
14
+
15
+ function emit(element, name, detail = {}) {
16
+ return element.dispatchEvent(new CustomEvent(`gardener:${name}`, { bubbles: true, cancelable: true, detail }));
17
+ }
18
+
19
+ function storeFor(element) {
20
+ if (!instanceStores.has(element)) instanceStores.set(element, new Map());
21
+ return instanceStores.get(element);
22
+ }
23
+
24
+ function lockScroll() {
25
+ const root = document.documentElement;
26
+ const count = Number(root.dataset.gScrollLocks || 0) + 1;
27
+ root.dataset.gScrollLocks = String(count);
28
+ if (count === 1) {
29
+ root.dataset.gPreviousOverflow = root.style.overflow;
30
+ root.style.overflow = "hidden";
31
+ }
32
+ }
33
+
34
+ function unlockScroll() {
35
+ const root = document.documentElement;
36
+ const count = Math.max(0, Number(root.dataset.gScrollLocks || 0) - 1);
37
+ root.dataset.gScrollLocks = String(count);
38
+ if (count === 0) {
39
+ root.style.overflow = root.dataset.gPreviousOverflow || "";
40
+ delete root.dataset.gPreviousOverflow;
41
+ }
42
+ }
43
+
44
+ function createOverlay(element, type) {
45
+ let trigger = null;
46
+ let open = false;
47
+ let closeTimer = null;
48
+ let closeFinished = true;
49
+ const panel = element.querySelector(type === "dialog" ? ".g-dialog" : type === "mobile-sheet" ? ".g-mobile-sheet-panel" : ".g-drawer") || element;
50
+ panel.tabIndex = panel.tabIndex < 0 ? -1 : panel.tabIndex;
51
+ if (type === "dialog" || type === "mobile-sheet") {
52
+ panel.setAttribute("role", panel.getAttribute("role") || "dialog");
53
+ panel.setAttribute("aria-modal", "true");
54
+ }
55
+
56
+ function onKeydown(event) {
57
+ if (event.key === "Escape" && element.dataset.gDismissible !== "false") close("escape");
58
+ if (event.key !== "Tab") return;
59
+ const items = visibleFocusable(panel);
60
+ if (!items.length) {
61
+ event.preventDefault();
62
+ panel.focus();
63
+ return;
64
+ }
65
+ const first = items[0];
66
+ const last = items.at(-1);
67
+ if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
68
+ else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
69
+ }
70
+
71
+ function finishClose(reason) {
72
+ if (open || closeFinished) return;
73
+ closeFinished = true;
74
+ clearTimeout(closeTimer);
75
+ element.hidden = true;
76
+ element.setAttribute("aria-hidden", "true");
77
+ trigger?.focus?.({ preventScroll: true });
78
+ emit(element, "close", { reason });
79
+ }
80
+
81
+ function openOverlay(source) {
82
+ if (open || !emit(element, "beforeopen", { source })) return;
83
+ clearTimeout(closeTimer);
84
+ open = true;
85
+ closeFinished = false;
86
+ trigger = source instanceof HTMLElement ? source : document.activeElement;
87
+ element.hidden = false;
88
+ element.setAttribute("aria-hidden", "false");
89
+ lockScroll();
90
+ requestAnimationFrame(() => {
91
+ element.classList.add("is-open");
92
+ (visibleFocusable(panel)[0] || panel).focus({ preventScroll: true });
93
+ });
94
+ document.addEventListener("keydown", onKeydown);
95
+ emit(element, "open", { source });
96
+ }
97
+
98
+ function close(reason = "api") {
99
+ if (!open || !emit(element, "beforeclose", { reason })) return;
100
+ open = false;
101
+ element.classList.remove("is-open");
102
+ document.removeEventListener("keydown", onKeydown);
103
+ unlockScroll();
104
+ const onTransitionEnd = (event) => {
105
+ if (event.target === element) finishClose(reason);
106
+ };
107
+ element.addEventListener("transitionend", onTransitionEnd, { once: true });
108
+ closeTimer = window.setTimeout(() => finishClose(reason), 350);
109
+ }
110
+
111
+ function onClick(event) {
112
+ if (event.target === element && element.dataset.gDismissible !== "false") close("backdrop");
113
+ if (event.target.closest("[data-g-close]")) close("button");
114
+ }
115
+
116
+ const startsOpen = !element.hidden;
117
+ element.hidden = true;
118
+ element.setAttribute("aria-hidden", "true");
119
+ element.addEventListener("click", onClick);
120
+ if (startsOpen) queueMicrotask(() => openOverlay());
121
+ return { open: openOverlay, close, toggle: (source) => open ? close("toggle") : openOverlay(source), isOpen: () => open, destroy: () => { clearTimeout(closeTimer); if (open) unlockScroll(); open = false; element.classList.remove("is-open"); element.hidden = true; element.removeEventListener("click", onClick); document.removeEventListener("keydown", onKeydown); } };
122
+ }
123
+
124
+ function createDropdown(element) {
125
+ const trigger = element.querySelector("[data-g-dropdown-trigger]");
126
+ const menu = element.querySelector("[data-g-dropdown-menu]") || element.querySelector(".g-dropdown-menu");
127
+ if (!trigger || !menu) return null;
128
+ let search = "";
129
+ let searchTimer;
130
+ const startsOpen = !menu.hidden;
131
+ const items = () => visibleFocusable(menu).filter((item) => item.getAttribute("aria-disabled") !== "true");
132
+
133
+ function open() {
134
+ if (!menu.hidden) return;
135
+ menu.hidden = false;
136
+ trigger.setAttribute("aria-expanded", "true");
137
+ document.addEventListener("pointerdown", outside, true);
138
+ document.addEventListener("keydown", keydown);
139
+ emit(element, "open");
140
+ }
141
+ function close(focus = false) {
142
+ if (menu.hidden) return;
143
+ menu.hidden = true;
144
+ trigger.setAttribute("aria-expanded", "false");
145
+ document.removeEventListener("pointerdown", outside, true);
146
+ document.removeEventListener("keydown", keydown);
147
+ if (focus) trigger.focus();
148
+ emit(element, "close");
149
+ }
150
+ function outside(event) { if (!element.contains(event.target)) close(); }
151
+ function keydown(event) {
152
+ const options = items();
153
+ const index = options.indexOf(document.activeElement);
154
+ if (event.key === "Escape") return close(true);
155
+ if (event.key === "ArrowDown") { event.preventDefault(); (options[index + 1] || options[0])?.focus(); }
156
+ else if (event.key === "ArrowUp") { event.preventDefault(); (options[index - 1] || options.at(-1))?.focus(); }
157
+ else if (event.key === "Home") { event.preventDefault(); options[0]?.focus(); }
158
+ else if (event.key === "End") { event.preventDefault(); options.at(-1)?.focus(); }
159
+ else if (event.key.length === 1 && /\S/.test(event.key)) {
160
+ search += event.key.toLocaleLowerCase();
161
+ clearTimeout(searchTimer);
162
+ searchTimer = setTimeout(() => { search = ""; }, 500);
163
+ options.find((item) => item.textContent.trim().toLocaleLowerCase().startsWith(search))?.focus();
164
+ }
165
+ }
166
+ function toggle() { menu.hidden ? open() : close(); }
167
+ function menuClick(event) { if (event.target.closest("[role='menuitem']") && !event.target.closest("[aria-haspopup='menu']")) close(); }
168
+ trigger.setAttribute("aria-haspopup", trigger.getAttribute("aria-haspopup") || "menu");
169
+ menu.setAttribute("role", menu.getAttribute("role") || "menu");
170
+ menu.hidden = true;
171
+ trigger.setAttribute("aria-expanded", "false");
172
+ trigger.addEventListener("click", toggle);
173
+ menu.addEventListener("click", menuClick);
174
+ if (startsOpen) queueMicrotask(open);
175
+ return { open, close, toggle, destroy: () => { clearTimeout(searchTimer); trigger.removeEventListener("click", toggle); menu.removeEventListener("click", menuClick); document.removeEventListener("pointerdown", outside, true); document.removeEventListener("keydown", keydown); } };
176
+ }
177
+
178
+ function createTabs(element) {
179
+ const tabs = [...element.querySelectorAll("[role='tab']")];
180
+ if (!tabs.length) return null;
181
+ const vertical = () => element.getAttribute("aria-orientation") === "vertical";
182
+ function select(tab, focus = false) {
183
+ if (!tab || tab.getAttribute("aria-disabled") === "true") return;
184
+ tabs.forEach((item) => {
185
+ const selected = item === tab;
186
+ item.setAttribute("aria-selected", String(selected));
187
+ item.tabIndex = selected ? 0 : -1;
188
+ const panel = targetById(item.getAttribute("aria-controls"));
189
+ if (panel) panel.hidden = !selected;
190
+ });
191
+ if (focus) tab.focus();
192
+ emit(element, "change", { tab });
193
+ }
194
+ function click(event) { const tab = event.target.closest("[role='tab']"); if (tab) select(tab); }
195
+ function keydown(event) {
196
+ const current = tabs.indexOf(event.target.closest("[role='tab']"));
197
+ if (current < 0) return;
198
+ const forward = vertical() ? "ArrowDown" : "ArrowRight";
199
+ const backward = vertical() ? "ArrowUp" : "ArrowLeft";
200
+ const enabled = tabs.filter((tab) => tab.getAttribute("aria-disabled") !== "true" && !tab.disabled);
201
+ if (!enabled.length) return;
202
+ const enabledIndex = enabled.indexOf(tabs[current]);
203
+ let next;
204
+ if (event.key === forward) next = enabled[(enabledIndex + 1 + enabled.length) % enabled.length];
205
+ else if (event.key === backward) next = enabled[(enabledIndex - 1 + enabled.length) % enabled.length];
206
+ else if (event.key === "Home") next = enabled[0];
207
+ else if (event.key === "End") next = enabled.at(-1);
208
+ else return;
209
+ event.preventDefault();
210
+ select(next, true);
211
+ }
212
+ element.addEventListener("click", click);
213
+ element.addEventListener("keydown", keydown);
214
+ select(tabs.find((tab) => tab.getAttribute("aria-selected") === "true") || tabs[0]);
215
+ return { select, destroy: () => { element.removeEventListener("click", click); element.removeEventListener("keydown", keydown); } };
216
+ }
217
+
218
+ function createAccordion(element) {
219
+ const triggerSelector = ".g-accordion-trigger, .g-faq-question, .g-thinking-trigger, [data-g-tool-call-toggle]";
220
+ const triggers = [...element.querySelectorAll(triggerSelector)];
221
+ function set(trigger, expanded) {
222
+ trigger.setAttribute("aria-expanded", String(expanded));
223
+ const panel = targetById(trigger.getAttribute("aria-controls"));
224
+ if (panel) panel.hidden = !expanded;
225
+ emit(element, "change", { trigger, expanded });
226
+ }
227
+ function toggle(trigger) {
228
+ if (trigger.disabled || trigger.getAttribute("aria-disabled") === "true") return;
229
+ const expanded = trigger.getAttribute("aria-expanded") === "true";
230
+ if (!expanded && element.dataset.gMultiple !== "true") triggers.forEach((other) => { if (other !== trigger) set(other, false); });
231
+ set(trigger, !expanded);
232
+ }
233
+ function click(event) { const trigger = event.target.closest(triggerSelector); if (trigger) toggle(trigger); }
234
+ element.addEventListener("click", click);
235
+ triggers.forEach((trigger) => set(trigger, trigger.getAttribute("aria-expanded") === "true"));
236
+ return { toggle, destroy: () => element.removeEventListener("click", click) };
237
+ }
238
+
239
+ function createAutoResize(element) {
240
+ function resize() {
241
+ element.style.height = "auto";
242
+ element.style.height = `${Math.min(element.scrollHeight, Number(element.dataset.gMaxHeight || 224))}px`;
243
+ }
244
+ element.addEventListener("input", resize);
245
+ resize();
246
+ return { resize, destroy: () => element.removeEventListener("input", resize) };
247
+ }
248
+
249
+ function createPasswordToggle(element) {
250
+ const input = element.querySelector("input");
251
+ const button = element.querySelector("[data-g-password-button]");
252
+ if (!input || !button) return null;
253
+ const showLabel = button.dataset.gShowLabel || "显示密码";
254
+ const hideLabel = button.dataset.gHideLabel || "隐藏密码";
255
+ function update() {
256
+ const visible = input.type === "text";
257
+ button.setAttribute("aria-pressed", String(visible));
258
+ button.setAttribute("aria-label", visible ? hideLabel : showLabel);
259
+ const label = button.querySelector("[data-g-password-label]");
260
+ if (label) label.textContent = visible ? hideLabel : showLabel;
261
+ }
262
+ function toggle() {
263
+ const start = input.selectionStart;
264
+ const end = input.selectionEnd;
265
+ input.type = input.type === "password" ? "text" : "password";
266
+ update();
267
+ input.focus({ preventScroll: true });
268
+ if (start != null && end != null) input.setSelectionRange(start, end);
269
+ emit(element, "change", { visible: input.type === "text" });
270
+ }
271
+ button.addEventListener("click", toggle);
272
+ update();
273
+ return { toggle, destroy: () => button.removeEventListener("click", toggle) };
274
+ }
275
+
276
+ function createOtpInput(element) {
277
+ const cells = [...element.querySelectorAll("[data-g-otp-cell], .g-otp-cell")];
278
+ const output = element.querySelector("[data-g-otp-output]");
279
+ if (!cells.length) return null;
280
+ const digits = (value) => String(value || "").replace(/\D/g, "");
281
+ function sync(reason = "api") {
282
+ const value = cells.map((cell) => digits(cell.value).slice(-1)).join("");
283
+ cells.forEach((cell, index) => {
284
+ const digit = digits(cell.value).slice(-1);
285
+ if (cell.value !== digit) cell.value = digit;
286
+ cell.setAttribute("aria-label", cell.getAttribute("aria-label") || `验证码第 ${index + 1} 位,共 ${cells.length} 位`);
287
+ });
288
+ element.dataset.gOtpValue = value;
289
+ const valueOutput = element.querySelector("[data-g-otp-output]") || output;
290
+ if (valueOutput) { valueOutput.value = value; valueOutput.defaultValue = value; valueOutput.setAttribute("value", value); }
291
+ const complete = value.length === cells.length;
292
+ element.classList.toggle("is-complete", complete);
293
+ element.setAttribute("aria-invalid", String(element.classList.contains("is-invalid")));
294
+ emit(element, "otpchange", { value, complete, reason });
295
+ if (complete) emit(element, "otpcomplete", { value, reason });
296
+ return value;
297
+ }
298
+ function fill(value, start = 0, reason = "api") {
299
+ const values = digits(value).slice(0, cells.length - start);
300
+ [...values].forEach((digit, offset) => { cells[start + offset].value = digit; });
301
+ const next = Math.min(start + values.length, cells.length - 1);
302
+ cells[next]?.focus({ preventScroll: true });
303
+ sync(reason);
304
+ }
305
+ function input(event) {
306
+ const index = cells.indexOf(event.target);
307
+ if (index < 0) return;
308
+ const value = digits(event.target.value);
309
+ if (value.length > 1) fill(value, index, "input");
310
+ else { event.target.value = value.slice(-1); if (value && index < cells.length - 1) cells[index + 1].focus({ preventScroll: true }); sync("input"); }
311
+ }
312
+ function keydown(event) {
313
+ const index = cells.indexOf(event.target);
314
+ if (index < 0) return;
315
+ if (event.key === "Backspace" && !event.target.value && index > 0) { event.preventDefault(); cells[index - 1].value = ""; cells[index - 1].focus({ preventScroll: true }); sync("backspace"); }
316
+ if (event.key === "ArrowLeft" && index > 0) { event.preventDefault(); cells[index - 1].focus({ preventScroll: true }); }
317
+ if (event.key === "ArrowRight" && index < cells.length - 1) { event.preventDefault(); cells[index + 1].focus({ preventScroll: true }); }
318
+ if (event.key === "Home") { event.preventDefault(); cells[0].focus({ preventScroll: true }); }
319
+ if (event.key === "End") { event.preventDefault(); cells.at(-1).focus({ preventScroll: true }); }
320
+ }
321
+ function paste(event) { const index = cells.indexOf(event.target); if (index >= 0) { event.preventDefault(); fill(event.clipboardData?.getData("text") || "", index, "paste"); } }
322
+ function clear() { cells.forEach((cell) => { cell.value = ""; }); element.classList.remove("is-invalid"); cells[0].focus({ preventScroll: true }); sync("clear"); }
323
+ element.addEventListener("input", input); element.addEventListener("keydown", keydown); element.addEventListener("paste", paste); sync("init");
324
+ return { value: () => sync("read"), fill, clear, destroy: () => { element.removeEventListener("input", input); element.removeEventListener("keydown", keydown); element.removeEventListener("paste", paste); } };
325
+ }
326
+
327
+ function createPasswordStrength(element) {
328
+ const input = targetById(element.dataset.gPasswordTarget) || element.querySelector("input[type='password'], input");
329
+ const label = element.querySelector("[data-g-password-strength-label]");
330
+ const meter = element.querySelector("[data-g-password-strength-meter], .g-password-strength-meter");
331
+ const rules = [...element.querySelectorAll("[data-g-password-rule]")];
332
+ if (!input) return null;
333
+ const tests = {
334
+ length: (value) => value.length >= Number(element.dataset.gMinLength || input.minLength || 8),
335
+ upper: (value) => /[A-Z]/.test(value), lower: (value) => /[a-z]/.test(value),
336
+ number: (value) => /\d/.test(value), symbol: (value) => /[^\w\s]/.test(value)
337
+ };
338
+ function update(reason = "input") {
339
+ const value = input.value; const checks = Object.fromEntries(Object.entries(tests).map(([key, test]) => [key, test(value)]));
340
+ const passed = Object.values(checks).filter(Boolean).length;
341
+ const score = value ? Math.min(4, Math.max(1, Math.ceil((passed / Object.keys(checks).length) * 4))) : 0;
342
+ const labels = ["未设置", "较弱", "一般", "良好", "强"];
343
+ element.dataset.gStrength = String(score); element.style.setProperty("--g-password-strength", `${score * 25}%`);
344
+ if (label) label.textContent = labels[score];
345
+ if (meter) { meter.setAttribute("role", "progressbar"); meter.setAttribute("aria-label", "密码强度"); meter.setAttribute("aria-valuemin", "0"); meter.setAttribute("aria-valuemax", "4"); meter.setAttribute("aria-valuenow", String(score)); meter.setAttribute("aria-valuetext", labels[score]); }
346
+ rules.forEach((rule) => { const valid = Boolean(checks[rule.dataset.gPasswordRule]); rule.classList.toggle("is-valid", valid); rule.setAttribute("aria-label", `${rule.textContent.trim()}:${valid ? "已满足" : "未满足"}`); });
347
+ emit(element, "passwordstrength", { score, checks, reason }); return { score, checks };
348
+ }
349
+ input.addEventListener("input", update); update("init");
350
+ return { update, destroy: () => input.removeEventListener("input", update) };
351
+ }
352
+
353
+ function createAuthTimer(element) {
354
+ const output = element.querySelector("[data-g-auth-time]") || element;
355
+ const resetButton = element.querySelector("[data-g-auth-timer-reset]");
356
+ const duration = Math.max(0, Number(element.dataset.gDuration || 60));
357
+ let remaining = duration; let timer = 0;
358
+ function format(seconds) { const minutes = Math.floor(seconds / 60); const rest = seconds % 60; return minutes ? `${minutes}:${String(rest).padStart(2, "0")}` : `${rest} 秒`; }
359
+ function render(reason = "tick") { output.textContent = format(remaining); const expired = remaining <= 0; element.dataset.gTimerState = expired ? "expired" : "active"; element.setAttribute("aria-live", expired ? "polite" : "off"); if (expired) { window.clearInterval(timer); timer = 0; emit(element, "authtimerexpired", { reason }); } }
360
+ function start(seconds = duration, reason = "start") { window.clearInterval(timer); remaining = Math.max(0, Number(seconds)); render(reason); if (remaining > 0) timer = window.setInterval(() => { remaining -= 1; render(); }, 1000); emit(element, "authtimerstart", { remaining, reason }); }
361
+ function reset() { start(duration, "reset"); }
362
+ function click(event) { if (event.target.closest("[data-g-auth-timer-reset]")) { event.preventDefault(); reset(); } }
363
+ element.addEventListener("click", click); start(duration, "init");
364
+ return { start, reset, expire: () => start(0, "api"), remaining: () => remaining, destroy: () => { window.clearInterval(timer); element.removeEventListener("click", click); } };
365
+ }
366
+
367
+ function createClearInput(element) {
368
+ const input = element.querySelector("input, textarea");
369
+ const button = element.querySelector("[data-g-clear-button]");
370
+ if (!input || !button) return null;
371
+ function update() { button.hidden = !input.value || input.disabled || input.readOnly; }
372
+ function clear() {
373
+ if (input.disabled || input.readOnly) return;
374
+ input.value = "";
375
+ input.dispatchEvent(new Event("input", { bubbles: true }));
376
+ input.dispatchEvent(new Event("change", { bubbles: true }));
377
+ input.focus({ preventScroll: true });
378
+ emit(element, "clear", { input });
379
+ }
380
+ if (!button.hasAttribute("aria-label")) button.setAttribute("aria-label", "清空输入");
381
+ input.addEventListener("input", update);
382
+ button.addEventListener("click", clear);
383
+ update();
384
+ return { clear, update, destroy: () => { input.removeEventListener("input", update); button.removeEventListener("click", clear); } };
385
+ }
386
+
387
+ function createCharacterCount(element) {
388
+ const input = element.querySelector("textarea, input");
389
+ const output = element.querySelector("[data-g-character-output], .g-character-count");
390
+ if (!input || !output) return null;
391
+ const maximum = Number(element.dataset.gCharacterCount || input.maxLength || 0);
392
+ function update() {
393
+ const count = [...input.value].length;
394
+ output.textContent = maximum > 0 ? `${count} / ${maximum}` : String(count);
395
+ output.classList.toggle("is-near-limit", maximum > 0 && count >= maximum * .85 && count <= maximum);
396
+ output.classList.toggle("is-over-limit", maximum > 0 && count > maximum);
397
+ output.setAttribute("aria-label", maximum > 0 ? `已输入 ${count} 个字符,最多 ${maximum} 个` : `已输入 ${count} 个字符`);
398
+ emit(element, "count", { count, maximum });
399
+ }
400
+ input.addEventListener("input", update);
401
+ update();
402
+ return { update, destroy: () => input.removeEventListener("input", update) };
403
+ }
404
+
405
+ function createConditionalField(element) {
406
+ const selector = element.dataset.gController;
407
+ if (!selector) return null;
408
+ let controllers;
409
+ try { controllers = [...document.querySelectorAll(selector)]; }
410
+ catch { return null; }
411
+ if (!controllers.length) return null;
412
+ const expected = new Set(String(element.dataset.gShowWhen ?? "true").split(",").map((value) => value.trim()));
413
+ function value() {
414
+ const checkedRadio = controllers.find((controller) => controller.type === "radio" && controller.checked);
415
+ if (checkedRadio) return checkedRadio.value;
416
+ const controller = controllers[0];
417
+ if (controller.type === "checkbox") return controller.checked ? (controller.value || "true") : "false";
418
+ return controller.value;
419
+ }
420
+ function update() {
421
+ const visible = expected.has(String(value()));
422
+ element.hidden = !visible;
423
+ element.toggleAttribute("inert", !visible);
424
+ element.setAttribute("aria-hidden", String(!visible));
425
+ emit(element, "change", { visible, value: value() });
426
+ }
427
+ controllers.forEach((controller) => controller.addEventListener("change", update));
428
+ update();
429
+ return { update, destroy: () => controllers.forEach((controller) => controller.removeEventListener("change", update)) };
430
+ }
431
+
432
+ function createRepeatableField(element) {
433
+ const list = element.querySelector("[data-g-repeatable-list], .g-repeatable-list");
434
+ const template = element.querySelector("template[data-g-repeatable-template]");
435
+ const addButton = element.querySelector("[data-g-repeatable-add]");
436
+ if (!list || !template || !addButton) return null;
437
+ const minimum = Math.max(0, Number(element.dataset.gMinItems || 0));
438
+ const maximum = Math.max(minimum, Number(element.dataset.gMaxItems || Infinity));
439
+ const items = () => [...list.querySelectorAll(":scope > .g-repeatable-item")];
440
+ function replaceIndex(node, attribute, index) {
441
+ const current = node.getAttribute(attribute);
442
+ if (current == null) return;
443
+ const dataKey = `g${attribute[0].toUpperCase()}${attribute.slice(1)}Template`;
444
+ if (current.includes("__INDEX__")) node.dataset[dataKey] = current;
445
+ const source = node.dataset[dataKey];
446
+ if (source) node.setAttribute(attribute, source.replaceAll("__INDEX__", String(index)));
447
+ }
448
+ function sync() {
449
+ const currentItems = items();
450
+ currentItems.forEach((item, index) => {
451
+ item.dataset.gIndex = String(index);
452
+ item.querySelectorAll("[data-g-item-number]").forEach((node) => { node.textContent = String(index + 1); });
453
+ item.querySelectorAll("[name], [id], [for]").forEach((node) => { replaceIndex(node, "name", index); replaceIndex(node, "id", index); replaceIndex(node, "for", index); });
454
+ item.querySelectorAll("[data-g-repeatable-remove]").forEach((button) => { button.disabled = currentItems.length <= minimum; });
455
+ });
456
+ addButton.disabled = currentItems.length >= maximum;
457
+ }
458
+ function add() {
459
+ if (items().length >= maximum) return;
460
+ const fragment = template.content.cloneNode(true);
461
+ list.append(fragment);
462
+ sync();
463
+ const item = items().at(-1);
464
+ init(item);
465
+ visibleFocusable(item)[0]?.focus({ preventScroll: true });
466
+ emit(element, "add", { item, index: items().length - 1 });
467
+ }
468
+ function click(event) {
469
+ const removeButton = event.target.closest("[data-g-repeatable-remove]");
470
+ if (!removeButton) return;
471
+ const item = removeButton.closest(".g-repeatable-item");
472
+ if (!item || items().length <= minimum) return;
473
+ const index = items().indexOf(item);
474
+ item.remove();
475
+ sync();
476
+ addButton.focus({ preventScroll: true });
477
+ emit(element, "remove", { index });
478
+ }
479
+ addButton.addEventListener("click", add);
480
+ list.addEventListener("click", click);
481
+ sync();
482
+ return { add, sync, destroy: () => { addButton.removeEventListener("click", add); list.removeEventListener("click", click); } };
483
+ }
484
+
485
+ function createCombobox(element) {
486
+ const input = element.querySelector("[role='combobox'], input");
487
+ const list = element.querySelector("[role='listbox'], .g-combobox-list");
488
+ if (!input || !list) return null;
489
+ const options = () => [...list.querySelectorAll("[role='option'], .g-combobox-option")].filter((option) => !option.hidden && option.getAttribute("aria-disabled") !== "true");
490
+ let activeIndex = -1;
491
+ if (!list.id) list.id = uid("g-listbox");
492
+ input.setAttribute("role", "combobox");
493
+ input.setAttribute("aria-controls", list.id);
494
+ input.setAttribute("aria-autocomplete", input.getAttribute("aria-autocomplete") || "list");
495
+
496
+ function open() { if (!list.hidden) return; list.hidden = false; input.setAttribute("aria-expanded", "true"); emit(element, "open"); }
497
+ function close() { if (list.hidden) return; list.hidden = true; input.setAttribute("aria-expanded", "false"); input.removeAttribute("aria-activedescendant"); activeIndex = -1; emit(element, "close"); }
498
+ function activate(index) {
499
+ const items = options();
500
+ if (!items.length) { activeIndex = -1; input.removeAttribute("aria-activedescendant"); return; }
501
+ activeIndex = (index + items.length) % items.length;
502
+ items.forEach((option, optionIndex) => {
503
+ if (!option.id) option.id = uid("g-option");
504
+ option.classList.toggle("is-active", optionIndex === activeIndex);
505
+ });
506
+ input.setAttribute("aria-activedescendant", items[activeIndex].id);
507
+ items[activeIndex].scrollIntoView({ block: "nearest" });
508
+ }
509
+ function choose(option) {
510
+ input.value = option.dataset.gValue ?? option.textContent.trim();
511
+ options().forEach((item) => item.setAttribute("aria-selected", String(item === option)));
512
+ emit(element, "change", { option, value: input.value });
513
+ close();
514
+ }
515
+ function filter() {
516
+ const query = input.value.trim().toLocaleLowerCase();
517
+ [...list.querySelectorAll("[role='option'], .g-combobox-option")].forEach((option) => {
518
+ option.hidden = Boolean(query) && !option.textContent.toLocaleLowerCase().includes(query);
519
+ });
520
+ open();
521
+ activate(0);
522
+ }
523
+ function keydown(event) {
524
+ if (event.key === "ArrowDown") { event.preventDefault(); open(); activate(activeIndex + 1); }
525
+ else if (event.key === "ArrowUp") { event.preventDefault(); open(); activate(activeIndex - 1); }
526
+ else if (event.key === "Enter" && activeIndex >= 0) { event.preventDefault(); choose(options()[activeIndex]); }
527
+ else if (event.key === "Escape") close();
528
+ }
529
+ function click(event) { const option = event.target.closest("[role='option'], .g-combobox-option"); if (option) choose(option); }
530
+ function outside(event) { if (!element.contains(event.target)) close(); }
531
+ input.addEventListener("input", filter);
532
+ input.addEventListener("keydown", keydown);
533
+ input.addEventListener("focus", open);
534
+ list.addEventListener("pointerdown", click);
535
+ document.addEventListener("pointerdown", outside, true);
536
+ list.hidden = true;
537
+ input.setAttribute("aria-expanded", "false");
538
+ return { open, close, choose, destroy: () => { input.removeEventListener("input", filter); input.removeEventListener("keydown", keydown); input.removeEventListener("focus", open); list.removeEventListener("pointerdown", click); document.removeEventListener("pointerdown", outside, true); } };
539
+ }
540
+
541
+ function positionFloating(trigger, panel, requestedPlacement = "bottom-start") {
542
+ const rect = trigger.getBoundingClientRect();
543
+ const panelRect = panel.getBoundingClientRect();
544
+ const gap = 8;
545
+ const rtl = getComputedStyle(trigger).direction === "rtl";
546
+ const [rawSide, rawAlign = "start"] = String(requestedPlacement || "bottom-start").toLowerCase().split("-");
547
+ let side = rawSide === "start" ? (rtl ? "right" : "left") : rawSide === "end" ? (rtl ? "left" : "right") : rawSide;
548
+ if (!["top", "bottom", "left", "right"].includes(side)) side = "bottom";
549
+ const align = ["start", "center", "end"].includes(rawAlign) ? rawAlign : "start";
550
+
551
+ function coordinates(candidate) {
552
+ let top;
553
+ let left;
554
+ if (candidate === "top" || candidate === "bottom") {
555
+ top = candidate === "top" ? rect.top - panelRect.height - gap : rect.bottom + gap;
556
+ if (align === "center") left = rect.left + (rect.width - panelRect.width) / 2;
557
+ else if (align === "end") left = rtl ? rect.left : rect.right - panelRect.width;
558
+ else left = rtl ? rect.right - panelRect.width : rect.left;
559
+ } else {
560
+ left = candidate === "left" ? rect.left - panelRect.width - gap : rect.right + gap;
561
+ if (align === "center") top = rect.top + (rect.height - panelRect.height) / 2;
562
+ else if (align === "end") top = rect.bottom - panelRect.height;
563
+ else top = rect.top;
564
+ }
565
+ return { top, left };
566
+ }
567
+
568
+ let position = coordinates(side);
569
+ if (side === "bottom" && position.top + panelRect.height > window.innerHeight - gap && rect.top >= panelRect.height + gap) side = "top";
570
+ else if (side === "top" && position.top < gap && window.innerHeight - rect.bottom >= panelRect.height + gap) side = "bottom";
571
+ else if (side === "right" && position.left + panelRect.width > window.innerWidth - gap && rect.left >= panelRect.width + gap) side = "left";
572
+ else if (side === "left" && position.left < gap && window.innerWidth - rect.right >= panelRect.width + gap) side = "right";
573
+ position = coordinates(side);
574
+ const top = Math.max(gap, Math.min(position.top, window.innerHeight - panelRect.height - gap));
575
+ const left = Math.max(gap, Math.min(position.left, window.innerWidth - panelRect.width - gap));
576
+ panel.style.position = "fixed";
577
+ panel.style.inset = `${top}px auto auto ${left}px`;
578
+ panel.dataset.gPlacement = `${side}-${align}`;
579
+ panel.style.setProperty("--g-floating-arrow-inline", `${Math.max(10, Math.min(panelRect.width - 18, rect.left + rect.width / 2 - left - 6))}px`);
580
+ panel.style.setProperty("--g-floating-arrow-block", `${Math.max(10, Math.min(panelRect.height - 18, rect.top + rect.height / 2 - top - 6))}px`);
581
+ }
582
+
583
+ function createFloating(element, type) {
584
+ const trigger = element.querySelector(`[data-g-${type}-trigger]`) || element.previousElementSibling;
585
+ const panel = element.querySelector(`[data-g-${type}-content]`) || element.querySelector(`.g-${type}, .g-${type}-rich, .g-help-${type}`) || element;
586
+ if (!trigger || !panel) return null;
587
+ const startsOpen = !panel.hidden;
588
+ let open = false;
589
+ const interactive = type === "popover";
590
+ if (!panel.id) panel.id = uid(`g-${type}`);
591
+ if (interactive) {
592
+ trigger.setAttribute("aria-haspopup", trigger.getAttribute("aria-haspopup") || "dialog");
593
+ trigger.setAttribute("aria-controls", panel.id);
594
+ trigger.setAttribute("aria-expanded", "false");
595
+ } else {
596
+ panel.setAttribute("role", "tooltip");
597
+ const describedBy = new Set((trigger.getAttribute("aria-describedby") || "").split(/\s+/).filter(Boolean));
598
+ describedBy.add(panel.id);
599
+ trigger.setAttribute("aria-describedby", [...describedBy].join(" "));
600
+ }
601
+ panel.hidden = true;
602
+ const placement = () => element.dataset.gPlacement || panel.dataset.gPlacement || trigger.dataset.gPlacement || "bottom-start";
603
+ function show() { if (open) return; open = true; panel.hidden = false; if (interactive) trigger.setAttribute("aria-expanded", "true"); requestAnimationFrame(() => positionFloating(trigger, panel, placement())); emit(element, "open"); }
604
+ function hide() { if (!open) return; open = false; panel.hidden = true; if (interactive) trigger.setAttribute("aria-expanded", "false"); emit(element, "close"); }
605
+ function toggle() { open ? hide() : show(); }
606
+ function outside(event) { if (!element.contains(event.target) && !trigger.contains(event.target)) hide(); }
607
+ function keydown(event) { if (event.key === "Escape") { hide(); trigger.focus(); } }
608
+ function reposition() { if (open) positionFloating(trigger, panel, placement()); }
609
+ if (interactive) trigger.addEventListener("click", toggle);
610
+ else {
611
+ trigger.addEventListener("pointerenter", show);
612
+ trigger.addEventListener("pointerleave", hide);
613
+ trigger.addEventListener("focus", show);
614
+ trigger.addEventListener("blur", hide);
615
+ }
616
+ document.addEventListener("pointerdown", outside, true);
617
+ document.addEventListener("keydown", keydown);
618
+ window.addEventListener("resize", reposition);
619
+ window.addEventListener("scroll", reposition, true);
620
+ if (startsOpen) queueMicrotask(show);
621
+ return { open: show, close: hide, toggle, destroy: () => { trigger.removeEventListener("click", toggle); trigger.removeEventListener("pointerenter", show); trigger.removeEventListener("pointerleave", hide); trigger.removeEventListener("focus", show); trigger.removeEventListener("blur", hide); document.removeEventListener("pointerdown", outside, true); document.removeEventListener("keydown", keydown); window.removeEventListener("resize", reposition); window.removeEventListener("scroll", reposition, true); } };
622
+ }
623
+
624
+ function createTour(element) {
625
+ const steps = [...element.querySelectorAll(".g-tour-step")];
626
+ if (!steps.length) return null;
627
+ if (!element.id) element.id = uid("g-tour");
628
+ const startsOpen = !element.hidden || element.dataset.gStartOpen === "true";
629
+ const progress = element.querySelector("[data-g-tour-progress], .g-tour-progress");
630
+ const spotlight = document.createElement("div");
631
+ spotlight.className = "g-spotlight";
632
+ spotlight.hidden = true;
633
+ spotlight.setAttribute("aria-hidden", "true");
634
+ document.body.append(spotlight);
635
+ element.setAttribute("role", element.getAttribute("role") || "dialog");
636
+ element.setAttribute("aria-modal", "false");
637
+ element.hidden = true;
638
+ element.setAttribute("aria-hidden", "true");
639
+ let index = 0;
640
+ let open = false;
641
+ let trigger = null;
642
+
643
+ function targetFor(step) {
644
+ try { return step.dataset.gTarget ? document.querySelector(step.dataset.gTarget) : null; }
645
+ catch { return null; }
646
+ }
647
+
648
+ function position() {
649
+ if (!open) return;
650
+ const step = steps[index];
651
+ const target = targetFor(step);
652
+ if (!target) {
653
+ spotlight.hidden = true;
654
+ element.style.position = "fixed";
655
+ element.style.inset = "50% auto auto 50%";
656
+ element.style.transform = "translate(-50%, -50%)";
657
+ return;
658
+ }
659
+ const rect = target.getBoundingClientRect();
660
+ const padding = Number(step.dataset.gSpotlightPadding || element.dataset.gSpotlightPadding || 6);
661
+ spotlight.hidden = false;
662
+ spotlight.style.inset = `${Math.max(0, rect.top - padding)}px auto auto ${Math.max(0, rect.left - padding)}px`;
663
+ spotlight.style.inlineSize = `${Math.min(window.innerWidth, rect.width + padding * 2)}px`;
664
+ spotlight.style.blockSize = `${Math.min(window.innerHeight, rect.height + padding * 2)}px`;
665
+ element.style.transform = "none";
666
+ positionFloating(target, element, step.dataset.gPlacement || element.dataset.gPlacement || "bottom-start");
667
+ }
668
+
669
+ function showStep(nextIndex, source = "api") {
670
+ index = Math.max(0, Math.min(steps.length - 1, nextIndex));
671
+ steps.forEach((step, stepIndex) => {
672
+ step.hidden = stepIndex !== index;
673
+ step.setAttribute("aria-hidden", String(stepIndex !== index));
674
+ });
675
+ if (progress) progress.textContent = `${index + 1} / ${steps.length}`;
676
+ element.querySelectorAll("[data-g-tour-prev]").forEach((button) => { button.disabled = index === 0; });
677
+ element.querySelectorAll("[data-g-tour-next]").forEach((button) => { button.textContent = index === steps.length - 1 ? (button.dataset.gFinishLabel || "完成") : (button.dataset.gNextLabel || "下一步"); });
678
+ requestAnimationFrame(position);
679
+ emit(element, "change", { index, step: steps[index], source });
680
+ }
681
+
682
+ function openTour(source) {
683
+ if (open) return;
684
+ open = true;
685
+ trigger = source instanceof HTMLElement ? source : document.activeElement;
686
+ element.hidden = false;
687
+ element.setAttribute("aria-hidden", "false");
688
+ window.addEventListener("resize", position);
689
+ window.addEventListener("scroll", position, true);
690
+ document.addEventListener("keydown", keydown);
691
+ showStep(Number(element.dataset.gInitialStep || 0), "open");
692
+ emit(element, "open", { source });
693
+ }
694
+
695
+ function close(reason = "api") {
696
+ if (!open) return;
697
+ open = false;
698
+ element.hidden = true;
699
+ element.setAttribute("aria-hidden", "true");
700
+ spotlight.hidden = true;
701
+ window.removeEventListener("resize", position);
702
+ window.removeEventListener("scroll", position, true);
703
+ document.removeEventListener("keydown", keydown);
704
+ trigger?.focus?.({ preventScroll: true });
705
+ emit(element, "close", { reason, index });
706
+ }
707
+
708
+ function click(event) {
709
+ if (event.target.closest("[data-g-tour-prev]")) showStep(index - 1, "previous");
710
+ else if (event.target.closest("[data-g-tour-next]")) index === steps.length - 1 ? close("complete") : showStep(index + 1, "next");
711
+ else if (event.target.closest("[data-g-tour-close], [data-g-tour-skip]")) close(event.target.closest("[data-g-tour-skip]") ? "skip" : "close");
712
+ }
713
+ function keydown(event) { if (event.key === "Escape" && element.dataset.gDismissible !== "false") close("escape"); }
714
+ element.addEventListener("click", click);
715
+ if (startsOpen) queueMicrotask(openTour);
716
+ return { open: openTour, close, next: () => showStep(index + 1, "api"), previous: () => showStep(index - 1, "api"), go: (stepIndex) => showStep(stepIndex, "api"), isOpen: () => open, current: () => index, destroy: () => { close("destroy"); spotlight.remove(); element.removeEventListener("click", click); } };
717
+ }
718
+
719
+ function createCarousel(element) {
720
+ const slides = [...element.querySelectorAll(".g-carousel-slide, [data-g-carousel-slide]")];
721
+ const previous = element.querySelector(".g-carousel-prev, [data-g-carousel-prev]");
722
+ const next = element.querySelector(".g-carousel-next, [data-g-carousel-next]");
723
+ const dots = [...element.querySelectorAll(".g-carousel-dot, [data-g-carousel-dot]")];
724
+ if (!slides.length) return null;
725
+ let index = Math.max(0, slides.findIndex((slide) => slide.getAttribute("aria-hidden") !== "true"));
726
+ let timer;
727
+ const interval = Number(element.dataset.gInterval || 0);
728
+ const dotHandlers = dots.map((dot, dotIndex) => [dot, () => go(dotIndex, "dot")]);
729
+ element.setAttribute("aria-roledescription", "carousel");
730
+ function go(nextIndex, source = "api") {
731
+ index = (nextIndex + slides.length) % slides.length;
732
+ element.style.setProperty("--g-slide-index", index);
733
+ slides.forEach((slide, slideIndex) => { slide.setAttribute("aria-hidden", String(slideIndex !== index)); slide.inert = slideIndex !== index; });
734
+ dots.forEach((dot, dotIndex) => dot.setAttribute("aria-current", String(dotIndex === index)));
735
+ emit(element, "change", { index, source });
736
+ }
737
+ function start() { clearInterval(timer); if (interval > 0 && !matchMedia("(prefers-reduced-motion: reduce)").matches) timer = setInterval(() => go(index + 1, "auto"), interval); }
738
+ function stop() { clearInterval(timer); }
739
+ const prevClick = () => go(index - 1, "previous");
740
+ const nextClick = () => go(index + 1, "next");
741
+ previous?.addEventListener("click", prevClick);
742
+ next?.addEventListener("click", nextClick);
743
+ dotHandlers.forEach(([dot, handler]) => dot.addEventListener("click", handler));
744
+ const focusOut = (event) => { if (!element.contains(event.relatedTarget)) start(); };
745
+ element.addEventListener("focusin", stop);
746
+ element.addEventListener("focusout", focusOut);
747
+ element.addEventListener("pointerenter", stop);
748
+ element.addEventListener("pointerleave", start);
749
+ go(index, "init");
750
+ start();
751
+ return { go, next: nextClick, previous: prevClick, start, stop, destroy: () => { stop(); previous?.removeEventListener("click", prevClick); next?.removeEventListener("click", nextClick); dotHandlers.forEach(([dot, handler]) => dot.removeEventListener("click", handler)); element.removeEventListener("focusin", stop); element.removeEventListener("focusout", focusOut); element.removeEventListener("pointerenter", stop); element.removeEventListener("pointerleave", start); } };
752
+ }
753
+
754
+ function createSplitter(element) {
755
+ const handle = element.querySelector(".g-split-handle, [role='separator']");
756
+ const panels = [...element.querySelectorAll(":scope > .g-split-panel")];
757
+ if (!handle || panels.length < 2) return null;
758
+ const vertical = () => element.dataset.gOrientation === "vertical";
759
+ handle.setAttribute("role", "separator");
760
+ handle.tabIndex = 0;
761
+ handle.setAttribute("aria-orientation", vertical() ? "horizontal" : "vertical");
762
+ handle.setAttribute("aria-valuemin", "10");
763
+ handle.setAttribute("aria-valuemax", "90");
764
+ let activeMove = null;
765
+ let activeUp = null;
766
+ function setPercent(percent) {
767
+ const value = Math.max(10, Math.min(90, percent));
768
+ panels[0].style.flex = `0 0 ${value}%`;
769
+ panels[1].style.flex = "1 1 0";
770
+ handle.setAttribute("aria-valuenow", String(Math.round(value)));
771
+ emit(element, "resize", { value });
772
+ }
773
+ function pointerDown(event) {
774
+ event.preventDefault();
775
+ handle.setPointerCapture?.(event.pointerId);
776
+ handle.classList.add("is-dragging");
777
+ const rect = element.getBoundingClientRect();
778
+ activeMove = (moveEvent) => setPercent(vertical() ? (moveEvent.clientY - rect.top) / rect.height * 100 : (moveEvent.clientX - rect.left) / rect.width * 100);
779
+ activeUp = () => { handle.classList.remove("is-dragging"); window.removeEventListener("pointermove", activeMove); window.removeEventListener("pointerup", activeUp); window.removeEventListener("pointercancel", activeUp); activeMove = null; activeUp = null; };
780
+ window.addEventListener("pointermove", activeMove);
781
+ window.addEventListener("pointerup", activeUp, { once: true });
782
+ window.addEventListener("pointercancel", activeUp, { once: true });
783
+ }
784
+ function keydown(event) {
785
+ const delta = event.shiftKey ? 10 : 2;
786
+ const current = Number(handle.getAttribute("aria-valuenow") || 50);
787
+ if (event.key === (vertical() ? "ArrowUp" : "ArrowLeft")) { event.preventDefault(); setPercent(current - delta); }
788
+ else if (event.key === (vertical() ? "ArrowDown" : "ArrowRight")) { event.preventDefault(); setPercent(current + delta); }
789
+ else if (event.key === "Home") { event.preventDefault(); setPercent(10); }
790
+ else if (event.key === "End") { event.preventDefault(); setPercent(90); }
791
+ }
792
+ handle.addEventListener("pointerdown", pointerDown);
793
+ handle.addEventListener("keydown", keydown);
794
+ setPercent(Number(element.dataset.gSplit || 50));
795
+ return { set: setPercent, destroy: () => { activeUp?.(); handle.removeEventListener("pointerdown", pointerDown); handle.removeEventListener("keydown", keydown); } };
796
+ }
797
+
798
+ function createTree(element) {
799
+ const items = () => [...element.querySelectorAll("[role='treeitem']")].filter((item) => item.getClientRects().length > 0);
800
+ element.setAttribute("role", "tree");
801
+ function focusItem(item) { items().forEach((node) => { node.tabIndex = node === item ? 0 : -1; }); item?.focus(); }
802
+ function keydown(event) {
803
+ const item = event.target.closest("[role='treeitem']");
804
+ if (!item) return;
805
+ const nodes = items();
806
+ const index = nodes.indexOf(item);
807
+ if (event.key === "ArrowDown") { event.preventDefault(); focusItem(nodes[index + 1] || nodes[0]); }
808
+ else if (event.key === "ArrowUp") { event.preventDefault(); focusItem(nodes[index - 1] || nodes.at(-1)); }
809
+ else if (event.key === "Home") { event.preventDefault(); focusItem(nodes[0]); }
810
+ else if (event.key === "End") { event.preventDefault(); focusItem(nodes.at(-1)); }
811
+ else if (event.key === "ArrowRight") {
812
+ if (item.getAttribute("aria-expanded") === "false") { event.preventDefault(); item.setAttribute("aria-expanded", "true"); emit(element, "expand", { item }); }
813
+ else { const child = item.querySelector(":scope > [role='group'] [role='treeitem']"); if (child) { event.preventDefault(); focusItem(child); } }
814
+ }
815
+ else if (event.key === "ArrowLeft") {
816
+ if (item.getAttribute("aria-expanded") === "true") { event.preventDefault(); item.setAttribute("aria-expanded", "false"); emit(element, "collapse", { item }); }
817
+ else { const parent = item.parentElement?.closest("[role='treeitem']"); if (parent) { event.preventDefault(); focusItem(parent); } }
818
+ }
819
+ else if (event.key === "Enter" || event.key === " ") { event.preventDefault(); item.setAttribute("aria-selected", String(item.getAttribute("aria-selected") !== "true")); emit(element, "change", { item }); }
820
+ }
821
+ const first = items()[0];
822
+ items().forEach((item) => { item.tabIndex = item === first ? 0 : -1; });
823
+ element.addEventListener("keydown", keydown);
824
+ return { focus: focusItem, destroy: () => element.removeEventListener("keydown", keydown) };
825
+ }
826
+
827
+ function createDataGrid(element) {
828
+ const cells = () => [...element.querySelectorAll("[role='gridcell'], [role='columnheader'], [role='rowheader']")].filter((cell) => cell.getClientRects().length > 0);
829
+ element.setAttribute("role", "grid");
830
+ function focusCell(cell) { cells().forEach((item) => { item.tabIndex = item === cell ? 0 : -1; }); cell?.focus(); }
831
+ function keydown(event) {
832
+ const cell = event.target.closest("[role='gridcell'], [role='columnheader'], [role='rowheader']");
833
+ if (!cell) return;
834
+ const row = cell.closest("[role='row']");
835
+ const rowCells = [...row.querySelectorAll("[role='gridcell'], [role='columnheader'], [role='rowheader']")];
836
+ const rows = [...element.querySelectorAll("[role='row']")];
837
+ const columnIndex = rowCells.indexOf(cell);
838
+ const rowIndex = rows.indexOf(row);
839
+ let target;
840
+ if (event.key === "ArrowRight") target = rowCells[columnIndex + 1];
841
+ else if (event.key === "ArrowLeft") target = rowCells[columnIndex - 1];
842
+ else if (event.key === "ArrowDown") target = rows[rowIndex + 1]?.querySelectorAll("[role='gridcell'], [role='columnheader'], [role='rowheader']")[columnIndex];
843
+ else if (event.key === "ArrowUp") target = rows[rowIndex - 1]?.querySelectorAll("[role='gridcell'], [role='columnheader'], [role='rowheader']")[columnIndex];
844
+ else if (event.key === "Home") target = event.ctrlKey ? cells()[0] : rowCells[0];
845
+ else if (event.key === "End") target = event.ctrlKey ? cells().at(-1) : rowCells.at(-1);
846
+ if (target) { event.preventDefault(); focusCell(target); }
847
+ }
848
+ const first = cells()[0];
849
+ cells().forEach((cell) => { cell.tabIndex = cell === first ? 0 : -1; });
850
+ element.addEventListener("keydown", keydown);
851
+ return { focus: focusCell, destroy: () => element.removeEventListener("keydown", keydown) };
852
+ }
853
+
854
+ function createTableSort(element) {
855
+ const table = element.matches("table") ? element : element.querySelector("table");
856
+ const body = table?.tBodies?.[0];
857
+ const buttons = table ? [...table.querySelectorAll("[data-g-sort-key]")] : [];
858
+ if (!body || !buttons.length) return null;
859
+ const collator = new Intl.Collator(element.lang || document.documentElement.lang || undefined, { numeric: true, sensitivity: "base" });
860
+
861
+ function valueFor(row, key, index, type) {
862
+ const cell = row.querySelector(`[data-g-column="${CSS.escape(key)}"]`) || row.cells[index];
863
+ const raw = cell?.dataset.gSortValue ?? cell?.textContent?.trim() ?? "";
864
+ if (type === "number") return Number(String(raw).replace(/[^\d+-.]/g, "")) || 0;
865
+ if (type === "date") return Date.parse(raw) || 0;
866
+ return raw;
867
+ }
868
+ function sort(button, direction) {
869
+ const key = button.dataset.gSortKey;
870
+ const header = button.closest("th, [role='columnheader']");
871
+ const headers = [...table.querySelectorAll("th, [role='columnheader']")];
872
+ const index = headers.indexOf(header);
873
+ const type = button.dataset.gSortType || "text";
874
+ const rows = [...body.rows].filter((row) => !row.hasAttribute("data-g-detail-row") && !row.hasAttribute("data-g-group-row"));
875
+ const details = new Map([...body.querySelectorAll("[data-g-detail-row]")].map((row) => [row.dataset.gDetailRow, row]));
876
+ const stable = rows.map((row, order) => ({ row, order, value: valueFor(row, key, index, type) }));
877
+ stable.sort((left, right) => {
878
+ const result = type === "text" ? collator.compare(String(left.value), String(right.value)) : left.value - right.value;
879
+ return (direction === "descending" ? -result : result) || left.order - right.order;
880
+ });
881
+ stable.forEach(({ row }) => {
882
+ body.append(row);
883
+ const detail = details.get(row.id);
884
+ if (detail) body.append(detail);
885
+ });
886
+ headers.forEach((item) => item.setAttribute("aria-sort", item === header ? direction : "none"));
887
+ buttons.forEach((item) => item.dataset.gSortDirection = item === button ? direction : "none");
888
+ emit(element, "sort", { key, direction, type, button });
889
+ }
890
+ function click(event) {
891
+ const button = event.target.closest("[data-g-sort-key]");
892
+ if (!button || !element.contains(button)) return;
893
+ const current = button.dataset.gSortDirection || button.closest("th, [role='columnheader']")?.getAttribute("aria-sort") || "none";
894
+ sort(button, current === "ascending" ? "descending" : "ascending");
895
+ }
896
+ table.querySelectorAll("th, [role='columnheader']").forEach((header) => {
897
+ if (header.querySelector("[data-g-sort-key]") && !header.hasAttribute("aria-sort")) header.setAttribute("aria-sort", "none");
898
+ });
899
+ element.addEventListener("click", click);
900
+ return { sort, destroy: () => element.removeEventListener("click", click) };
901
+ }
902
+
903
+ function createRowSelect(element) {
904
+ const master = element.querySelector("[data-g-select-all]");
905
+ const boxes = () => [...element.querySelectorAll("[data-g-select-row]")].filter((box) => !box.disabled);
906
+ const outputs = () => [...element.querySelectorAll("[data-g-selection-count]")];
907
+ const totals = () => [...element.querySelectorAll("[data-g-selection-total]")];
908
+ const summaries = () => [...element.querySelectorAll("[data-g-selection-summary], [data-g-batch-toolbar]")];
909
+ let lastIndex = -1;
910
+
911
+ function sync(reason = "api") {
912
+ const items = boxes();
913
+ const selected = items.filter((box) => box.checked);
914
+ items.forEach((box) => {
915
+ const row = box.closest("tr, [role='row'], .g-list-item, [data-g-selectable-row]");
916
+ row?.classList.toggle("is-selected", box.checked);
917
+ if (row?.matches("tr, [role='row'], [role='option'], [role='tab'], [role='treeitem']")) row.setAttribute("aria-selected", String(box.checked));
918
+ else row?.removeAttribute("aria-selected");
919
+ });
920
+ if (master) {
921
+ master.checked = items.length > 0 && selected.length === items.length;
922
+ master.indeterminate = selected.length > 0 && selected.length < items.length;
923
+ }
924
+ outputs().forEach((output) => { output.textContent = String(selected.length); });
925
+ totals().forEach((output) => { output.textContent = String(items.length); });
926
+ summaries().forEach((summary) => { summary.hidden = selected.length === 0; });
927
+ element.querySelectorAll("[data-g-selection-requires]").forEach((control) => { control.disabled = selected.length === 0; });
928
+ emit(element, "selectionchange", { selected: selected.map((box) => box.value), count: selected.length, reason });
929
+ }
930
+ function change(event) {
931
+ if (event.target === master) boxes().forEach((box) => { box.checked = master.checked; });
932
+ if (event.target.matches?.("[data-g-select-row]")) lastIndex = boxes().indexOf(event.target);
933
+ sync(event.target === master ? "all" : "item");
934
+ }
935
+ function click(event) {
936
+ const action = event.target.closest?.("[data-g-select-all-action], [data-g-select-visible], [data-g-invert-selection], [data-g-clear-selection]");
937
+ if (action) {
938
+ event.preventDefault();
939
+ if (action.hasAttribute("data-g-clear-selection")) boxes().forEach((item) => { item.checked = false; });
940
+ else if (action.hasAttribute("data-g-invert-selection")) boxes().forEach((item) => { item.checked = !item.checked; });
941
+ else if (action.hasAttribute("data-g-select-visible")) boxes().filter((item) => !item.closest("[hidden]")).forEach((item) => { item.checked = true; });
942
+ else boxes().forEach((item) => { item.checked = true; });
943
+ sync(action.hasAttribute("data-g-invert-selection") ? "invert" : action.hasAttribute("data-g-clear-selection") ? "clear" : "all-action");
944
+ return;
945
+ }
946
+ const box = event.target.closest?.("[data-g-select-row]");
947
+ if (!box || !event.shiftKey || lastIndex < 0) return;
948
+ const items = boxes();
949
+ const current = items.indexOf(box);
950
+ if (current < 0) return;
951
+ const [start, end] = [lastIndex, current].sort((a, b) => a - b);
952
+ items.slice(start, end + 1).forEach((item) => { item.checked = box.checked; });
953
+ sync("range");
954
+ }
955
+ element.addEventListener("change", change);
956
+ element.addEventListener("click", click);
957
+ sync("init");
958
+ return {
959
+ sync,
960
+ clear: () => { boxes().forEach((box) => { box.checked = false; }); sync("clear"); },
961
+ selectAll: () => { boxes().forEach((box) => { box.checked = true; }); sync("all-api"); },
962
+ invert: () => { boxes().forEach((box) => { box.checked = !box.checked; }); sync("invert-api"); },
963
+ selected: () => boxes().filter((box) => box.checked),
964
+ destroy: () => { element.removeEventListener("change", change); element.removeEventListener("click", click); }
965
+ };
966
+ }
967
+
968
+ function createTransfer(element) {
969
+ const source = element.querySelector("[data-g-transfer-source]");
970
+ const target = element.querySelector("[data-g-transfer-target]");
971
+ if (!source || !target) return null;
972
+ const optionSelector = "[data-g-transfer-option]";
973
+ const options = (pane) => [...pane.querySelectorAll(optionSelector)];
974
+ const checked = (pane) => options(pane).filter((option) => option.querySelector("input[type='checkbox']")?.checked || option.getAttribute("aria-selected") === "true");
975
+ function setChecked(option, value) {
976
+ const input = option.querySelector("input[type='checkbox']");
977
+ if (input) input.checked = value;
978
+ if (input) option.removeAttribute("aria-selected");
979
+ else { option.setAttribute("role", option.getAttribute("role") || "option"); option.setAttribute("aria-selected", String(value)); }
980
+ }
981
+ function sync(reason = "api") {
982
+ [[source, "source"], [target, "target"]].forEach(([pane, name]) => {
983
+ const list = options(pane);
984
+ const count = element.querySelector(`[data-g-transfer-count="${name}"]`);
985
+ const empty = pane.querySelector("[data-g-transfer-empty]");
986
+ if (count) count.textContent = String(list.length);
987
+ if (empty) empty.hidden = list.length !== 0;
988
+ });
989
+ element.querySelectorAll("[data-g-transfer-move]").forEach((button) => {
990
+ const direction = button.dataset.gTransferMove;
991
+ const from = direction.endsWith("target") ? source : target;
992
+ button.disabled = direction.startsWith("all") ? options(from).length === 0 : checked(from).length === 0;
993
+ });
994
+ emit(element, "transferchange", { source: options(source).map((item) => item.dataset.gValue), target: options(target).map((item) => item.dataset.gValue), reason });
995
+ }
996
+ function move(direction) {
997
+ const toTarget = direction.endsWith("target");
998
+ const from = toTarget ? source : target;
999
+ const to = toTarget ? target : source;
1000
+ const moving = direction.startsWith("all") ? options(from) : checked(from);
1001
+ moving.forEach((option) => { setChecked(option, false); to.append(option); });
1002
+ sync(direction);
1003
+ moving[0]?.focus?.();
1004
+ }
1005
+ function click(event) {
1006
+ const button = event.target.closest("[data-g-transfer-move]");
1007
+ if (button) { event.preventDefault(); move(button.dataset.gTransferMove); return; }
1008
+ const option = event.target.closest(optionSelector);
1009
+ if (option && !option.querySelector("input[type='checkbox']") && !event.target.matches("button,a")) { setChecked(option, option.getAttribute("aria-selected") !== "true"); sync("option"); }
1010
+ }
1011
+ function change(event) {
1012
+ const option = event.target.closest(optionSelector);
1013
+ if (option) { setChecked(option, event.target.checked); sync("option"); }
1014
+ }
1015
+ function input(event) {
1016
+ const field = event.target.closest("[data-g-transfer-search]");
1017
+ if (!field) return;
1018
+ const pane = field.dataset.gTransferSearch === "target" ? target : source;
1019
+ const query = field.value.normalize("NFKD").toLocaleLowerCase().trim();
1020
+ options(pane).forEach((option) => { option.hidden = Boolean(query) && !String(option.dataset.gFilterText || option.textContent).normalize("NFKD").toLocaleLowerCase().includes(query); });
1021
+ }
1022
+ function keydown(event) {
1023
+ const option = event.target.closest(optionSelector);
1024
+ if (!option || !(event.ctrlKey || event.metaKey)) return;
1025
+ if (event.key === "ArrowRight" && source.contains(option)) { event.preventDefault(); setChecked(option, true); move("selected-to-target"); }
1026
+ if (event.key === "ArrowLeft" && target.contains(option)) { event.preventDefault(); setChecked(option, true); move("selected-to-source"); }
1027
+ }
1028
+ element.addEventListener("click", click);
1029
+ element.addEventListener("change", change);
1030
+ element.addEventListener("input", input);
1031
+ element.addEventListener("keydown", keydown);
1032
+ sync("init");
1033
+ return { move, sync, values: () => options(target).map((item) => item.dataset.gValue), destroy: () => { element.removeEventListener("click", click); element.removeEventListener("change", change); element.removeEventListener("input", input); element.removeEventListener("keydown", keydown); } };
1034
+ }
1035
+
1036
+ function createPicker(element) {
1037
+ const panel = element.querySelector("[data-g-picker-panel]");
1038
+ const trigger = element.querySelector("[data-g-picker-trigger]");
1039
+ const input = element.querySelector("[data-g-picker-input]");
1040
+ const valuesHost = element.querySelector("[data-g-picker-values]");
1041
+ const output = element.querySelector("[data-g-picker-output]");
1042
+ const options = () => [...element.querySelectorAll("[data-g-picker-option]")];
1043
+ const multiple = element.hasAttribute("data-g-picker-multiple");
1044
+ if (!panel || (!trigger && !input) || !options().length) return null;
1045
+ let open = !panel.hidden;
1046
+ function selected() { return options().filter((option) => option.getAttribute("aria-selected") === "true"); }
1047
+ function sync(reason = "api") {
1048
+ const current = selected();
1049
+ options().forEach((option) => option.classList.toggle("is-selected", current.includes(option)));
1050
+ if (output) {
1051
+ const value = multiple ? current.map((option) => option.dataset.gValue || option.textContent.trim()).join(",") : current[0]?.dataset.gValue || "";
1052
+ output.value = value;
1053
+ output.setAttribute("value", value);
1054
+ }
1055
+ element.querySelectorAll("[data-g-picker-count]").forEach((node) => { node.textContent = String(current.length); });
1056
+ if (valuesHost) {
1057
+ valuesHost.replaceChildren(...current.map((option) => {
1058
+ const chip = document.createElement("span"); chip.className = "g-picker-value"; chip.dataset.gValue = option.dataset.gValue || option.textContent.trim();
1059
+ const label = document.createElement("span"); label.className = "g-picker-value-label"; label.textContent = option.dataset.gLabel || option.querySelector(".g-picker-option-title")?.textContent || option.textContent.trim(); chip.append(label);
1060
+ const remove = document.createElement("button"); remove.type = "button"; remove.className = "g-picker-value-remove"; remove.dataset.gPickerRemove = chip.dataset.gValue; remove.setAttribute("aria-label", `移除 ${label.textContent}`); remove.textContent = "×"; chip.append(remove); return chip;
1061
+ }));
1062
+ }
1063
+ emit(element, "pickerchange", { values: current.map((option) => option.dataset.gValue), reason });
1064
+ }
1065
+ function setOpen(value, reason = "api") {
1066
+ open = value; panel.hidden = !open; trigger?.setAttribute("aria-expanded", String(open)); input?.setAttribute("aria-expanded", String(open));
1067
+ if (open) queueMicrotask(() => input?.focus());
1068
+ emit(element, open ? "open" : "close", { reason });
1069
+ }
1070
+ function choose(option, reason = "option") {
1071
+ const next = option.getAttribute("aria-selected") !== "true";
1072
+ if (!multiple) options().forEach((item) => item.setAttribute("aria-selected", "false"));
1073
+ option.setAttribute("aria-selected", String(next)); sync(reason); if (!multiple) setOpen(false, "selection");
1074
+ }
1075
+ function click(event) {
1076
+ const remove = event.target.closest("[data-g-picker-remove]");
1077
+ if (remove) { const option = options().find((item) => (item.dataset.gValue || item.textContent.trim()) === remove.dataset.gPickerRemove); option?.setAttribute("aria-selected", "false"); sync("remove"); return; }
1078
+ const option = event.target.closest("[data-g-picker-option]");
1079
+ if (option) { event.preventDefault(); choose(option); return; }
1080
+ if (event.target.closest("[data-g-picker-trigger]")) { const inputTarget = event.target.closest("[data-g-picker-input]"); if (!inputTarget) event.preventDefault(); setOpen(inputTarget ? true : !open, "trigger"); }
1081
+ }
1082
+ function filter() {
1083
+ const query = String(input?.value || "").normalize("NFKD").toLocaleLowerCase().trim(); let visible = 0;
1084
+ options().forEach((option) => { const match = !query || String(option.dataset.gFilterText || option.textContent).normalize("NFKD").toLocaleLowerCase().includes(query); option.hidden = !match; if (match) visible += 1; });
1085
+ element.querySelectorAll("[data-g-picker-empty]").forEach((empty) => { empty.hidden = visible !== 0; });
1086
+ if (!open) setOpen(true, "input");
1087
+ }
1088
+ function keydown(event) {
1089
+ if (event.key === "Escape" && open) { event.preventDefault(); setOpen(false, "escape"); trigger?.focus(); return; }
1090
+ if (!["ArrowDown", "ArrowUp", "Home", "End", "Enter", " "].includes(event.key)) return;
1091
+ const visible = options().filter((option) => !option.hidden); if (!visible.length) return;
1092
+ const current = visible.indexOf(document.activeElement);
1093
+ if ((event.key === "Enter" || event.key === " ") && current >= 0) { event.preventDefault(); choose(visible[current], "keyboard"); return; }
1094
+ if (["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) { event.preventDefault(); setOpen(true, "keyboard"); const index = event.key === "Home" ? 0 : event.key === "End" ? visible.length - 1 : event.key === "ArrowDown" ? (current + 1 + visible.length) % visible.length : (current - 1 + visible.length) % visible.length; visible[index].focus(); }
1095
+ }
1096
+ function outside(event) { if (open && !element.contains(event.target)) setOpen(false, "outside"); }
1097
+ element.addEventListener("click", click); element.addEventListener("keydown", keydown); input?.addEventListener("input", filter); document.addEventListener("pointerdown", outside);
1098
+ options().forEach((option) => { option.tabIndex ||= -1; option.setAttribute("role", option.getAttribute("role") || "option"); if (!option.hasAttribute("aria-selected")) option.setAttribute("aria-selected", "false"); });
1099
+ sync("init"); setOpen(open, "init");
1100
+ return { open: () => setOpen(true), close: () => setOpen(false), selected, choose, destroy: () => { element.removeEventListener("click", click); element.removeEventListener("keydown", keydown); input?.removeEventListener("input", filter); document.removeEventListener("pointerdown", outside); } };
1101
+ }
1102
+
1103
+ function createCascader(element) {
1104
+ const columns = () => [...element.querySelectorAll("[data-g-cascade-column]")];
1105
+ const output = element.querySelector("[data-g-cascade-output]");
1106
+ const path = element.querySelector("[data-g-cascade-path]");
1107
+ if (!columns().length) return null;
1108
+ function choose(option, reason = "option") {
1109
+ const column = option.closest("[data-g-cascade-column]"); const index = columns().indexOf(column);
1110
+ [...column.querySelectorAll("[data-g-cascade-option]")].forEach((item) => { item.setAttribute("aria-current", String(item === option)); });
1111
+ columns().slice(index + 1).forEach((next) => { next.hidden = next.id !== option.dataset.gCascadeNext; });
1112
+ const values = columns().filter((item) => !item.hidden).map((item) => item.querySelector('[data-g-cascade-option][aria-current="true"]')).filter(Boolean);
1113
+ if (path) path.textContent = values.map((item) => item.dataset.gLabel || item.textContent.trim()).join(" / ");
1114
+ if (output) { const value = values.at(-1)?.dataset.gValue || ""; output.value = value; output.setAttribute("value", value); }
1115
+ emit(element, "cascadechange", { value: output?.value, path: values.map((item) => item.dataset.gValue), reason });
1116
+ }
1117
+ function click(event) { const option = event.target.closest("[data-g-cascade-option]"); if (option) { event.preventDefault(); choose(option); } }
1118
+ function keydown(event) { const option = event.target.closest("[data-g-cascade-option]"); if (!option) return; if (["Enter", " ", "ArrowRight"].includes(event.key)) { event.preventDefault(); choose(option, "keyboard"); const next = option.dataset.gCascadeNext && targetById(option.dataset.gCascadeNext); next?.querySelector("[data-g-cascade-option]")?.focus(); } if (event.key === "ArrowLeft") { event.preventDefault(); const column = option.closest("[data-g-cascade-column]"); columns()[Math.max(0, columns().indexOf(column) - 1)]?.querySelector('[data-g-cascade-option][aria-current="true"], [data-g-cascade-option]')?.focus(); } }
1119
+ element.addEventListener("click", click); element.addEventListener("keydown", keydown);
1120
+ return { choose, destroy: () => { element.removeEventListener("click", click); element.removeEventListener("keydown", keydown); } };
1121
+ }
1122
+
1123
+ function createSavedChoice(element) {
1124
+ const items = () => [...element.querySelectorAll("[data-g-saved-choice-item]")];
1125
+ if (!items().length) return null;
1126
+ function select(item, reason = "option") { const multiple = element.hasAttribute("data-g-multiple"); if (!multiple) items().forEach((option) => option.setAttribute("aria-pressed", "false")); item.setAttribute("aria-pressed", String(multiple ? item.getAttribute("aria-pressed") !== "true" : true)); emit(element, "savedchoice", { values: items().filter((option) => option.getAttribute("aria-pressed") === "true").map((option) => option.dataset.gValue), reason }); }
1127
+ function click(event) { const item = event.target.closest("[data-g-saved-choice-item]"); if (item) { event.preventDefault(); select(item); } }
1128
+ element.addEventListener("click", click); return { select, destroy: () => element.removeEventListener("click", click) };
1129
+ }
1130
+
1131
+ function createBuilderList(element) {
1132
+ const list = element.querySelector("[data-g-builder-items]"); const template = targetById(element.dataset.gBuilderTemplate); if (!list || !template) return null;
1133
+ function sync(reason = "api") { [...list.children].forEach((item, index) => { item.dataset.gBuilderIndex = String(index); item.querySelectorAll("[data-g-builder-position]").forEach((output) => { output.textContent = String(index + 1); }); }); emit(element, "builderchange", { count: list.children.length, reason }); }
1134
+ function add() { const fragment = template.content ? template.content.cloneNode(true) : template.cloneNode(true); list.append(fragment); sync("add"); list.lastElementChild?.querySelector(focusableSelector)?.focus(); }
1135
+ function click(event) { const control = event.target.closest("[data-g-builder-add], [data-g-builder-remove], [data-g-builder-up], [data-g-builder-down]"); if (!control) return; event.preventDefault(); if (control.hasAttribute("data-g-builder-add")) { add(); return; } const item = control.closest("[data-g-builder-item]"); if (!item) return; if (control.hasAttribute("data-g-builder-remove")) item.remove(); else if (control.hasAttribute("data-g-builder-up") && item.previousElementSibling) list.insertBefore(item, item.previousElementSibling); else if (control.hasAttribute("data-g-builder-down") && item.nextElementSibling) list.insertBefore(item.nextElementSibling, item); sync(control.hasAttribute("data-g-builder-remove") ? "remove" : "move"); }
1136
+ element.addEventListener("click", click); sync("init"); return { add, sync, destroy: () => element.removeEventListener("click", click) };
1137
+ }
1138
+
1139
+ function createRowDisclosure(element) {
1140
+ const triggers = [...element.querySelectorAll("[data-g-row-toggle]")];
1141
+ if (!triggers.length) return null;
1142
+ function descendants(parent) { return [...element.querySelectorAll(`[data-g-parent="${CSS.escape(parent)}"]`)]; }
1143
+ function set(trigger, expanded, reason = "api") {
1144
+ const value = trigger.dataset.gRowToggle;
1145
+ const target = targetById(value);
1146
+ const children = target ? [target] : descendants(value);
1147
+ trigger.setAttribute("aria-expanded", String(expanded));
1148
+ if (target?.id) trigger.setAttribute("aria-controls", target.id);
1149
+ children.forEach((child) => {
1150
+ child.hidden = !expanded;
1151
+ child.setAttribute("aria-hidden", String(!expanded));
1152
+ if (!expanded && child.id) {
1153
+ descendants(child.id).forEach((nested) => { nested.hidden = true; nested.setAttribute("aria-hidden", "true"); });
1154
+ }
1155
+ });
1156
+ emit(element, "disclosure", { trigger, targets: children, expanded, reason });
1157
+ }
1158
+ function click(event) {
1159
+ const trigger = event.target.closest("[data-g-row-toggle]");
1160
+ if (trigger) set(trigger, trigger.getAttribute("aria-expanded") !== "true", "toggle");
1161
+ }
1162
+ function keydown(event) {
1163
+ const trigger = event.target.closest("[data-g-row-toggle]");
1164
+ if (trigger && event.key === "Escape" && trigger.getAttribute("aria-expanded") === "true") { event.preventDefault(); set(trigger, false, "escape"); trigger.focus(); }
1165
+ }
1166
+ triggers.forEach((trigger) => {
1167
+ const expanded = trigger.getAttribute("aria-expanded") === "true";
1168
+ set(trigger, expanded, "init");
1169
+ });
1170
+ element.addEventListener("click", click);
1171
+ element.addEventListener("keydown", keydown);
1172
+ return { set, destroy: () => { element.removeEventListener("click", click); element.removeEventListener("keydown", keydown); } };
1173
+ }
1174
+
1175
+ function createColumnToggle(element) {
1176
+ const table = element.querySelector("table, [role='grid']");
1177
+ const toggles = [...element.querySelectorAll("[data-g-toggle-column]")];
1178
+ if (!table || !toggles.length) return null;
1179
+ function set(key, visible, source) {
1180
+ const active = toggles.filter((toggle) => toggle.dataset.gToggleColumn === key ? visible : toggle.checked);
1181
+ if (!visible && active.length === 0 && element.dataset.gAllowEmptyColumns !== "true") {
1182
+ if (source) source.checked = true;
1183
+ return false;
1184
+ }
1185
+ table.querySelectorAll(`[data-g-column="${CSS.escape(key)}"]`).forEach((cell) => {
1186
+ cell.hidden = !visible;
1187
+ cell.setAttribute("aria-hidden", String(!visible));
1188
+ });
1189
+ element.querySelectorAll(`[data-g-toggle-column="${CSS.escape(key)}"]`).forEach((toggle) => { toggle.checked = visible; });
1190
+ emit(element, "columnchange", { key, visible });
1191
+ return true;
1192
+ }
1193
+ function change(event) { if (event.target.matches("[data-g-toggle-column]")) set(event.target.dataset.gToggleColumn, event.target.checked, event.target); }
1194
+ toggles.forEach((toggle) => set(toggle.dataset.gToggleColumn, toggle.checked, toggle));
1195
+ element.addEventListener("change", change);
1196
+ return { set, destroy: () => element.removeEventListener("change", change) };
1197
+ }
1198
+
1199
+ function createDataFilter(element) {
1200
+ const input = element.querySelector("[data-g-filter-input]") || (element.matches("input") ? element : null);
1201
+ const scope = targetById(element.dataset.gFilterTarget) || element;
1202
+ const items = () => [...scope.querySelectorAll("[data-g-filter-item]")];
1203
+ const empty = scope.querySelector("[data-g-filter-empty]");
1204
+ const output = element.querySelector("[data-g-filter-count]") || scope.querySelector("[data-g-filter-count]");
1205
+ if (!input || !items().length) return null;
1206
+ const normalize = (value) => String(value).normalize("NFKD").toLocaleLowerCase().trim();
1207
+ function filter(value = input.value) {
1208
+ const terms = normalize(value).split(/\s+/).filter(Boolean);
1209
+ let visible = 0;
1210
+ items().forEach((item) => {
1211
+ const haystack = normalize(item.dataset.gFilterText || item.textContent);
1212
+ const match = terms.every((term) => haystack.includes(term));
1213
+ item.hidden = !match;
1214
+ const related = item.id
1215
+ ? [...scope.querySelectorAll(`[data-g-detail-row="${CSS.escape(item.id)}"], [data-g-parent="${CSS.escape(item.id)}"]`)]
1216
+ : [];
1217
+ related.forEach((row) => {
1218
+ if (!match && !row.hasAttribute("data-g-filter-previous-hidden")) {
1219
+ row.dataset.gFilterPreviousHidden = String(row.hidden);
1220
+ row.hidden = true;
1221
+ } else if (match && row.hasAttribute("data-g-filter-previous-hidden")) {
1222
+ row.hidden = row.dataset.gFilterPreviousHidden === "true";
1223
+ delete row.dataset.gFilterPreviousHidden;
1224
+ }
1225
+ });
1226
+ if (match) visible += 1;
1227
+ });
1228
+ if (empty) empty.hidden = visible !== 0;
1229
+ if (output) output.textContent = String(visible);
1230
+ emit(element, "filter", { value, terms, count: visible });
1231
+ return visible;
1232
+ }
1233
+ const inputHandler = () => filter();
1234
+ input.addEventListener("input", inputHandler);
1235
+ filter();
1236
+ return { filter, clear: () => { input.value = ""; filter(); }, destroy: () => input.removeEventListener("input", inputHandler) };
1237
+ }
1238
+
1239
+ function createDataView(element) {
1240
+ const controls = [...element.querySelectorAll("[data-g-view]")];
1241
+ const panels = [...element.querySelectorAll("[data-g-view-panel]")];
1242
+ if (!controls.length) return null;
1243
+ function select(view, focus = false) {
1244
+ element.dataset.gCurrentView = view;
1245
+ controls.forEach((control) => {
1246
+ const active = control.dataset.gView === view;
1247
+ control.setAttribute("aria-pressed", String(active));
1248
+ control.tabIndex = active ? 0 : -1;
1249
+ if (active && focus) control.focus();
1250
+ });
1251
+ panels.forEach((panel) => { panel.hidden = panel.dataset.gViewPanel !== view; });
1252
+ emit(element, "viewchange", { view });
1253
+ }
1254
+ function click(event) { const control = event.target.closest("[data-g-view]"); if (control) select(control.dataset.gView); }
1255
+ function keydown(event) {
1256
+ const current = event.target.closest("[data-g-view]");
1257
+ if (!current) return;
1258
+ const index = controls.indexOf(current);
1259
+ let next;
1260
+ if (event.key === "ArrowRight" || event.key === "ArrowDown") next = controls[(index + 1) % controls.length];
1261
+ else if (event.key === "ArrowLeft" || event.key === "ArrowUp") next = controls[(index - 1 + controls.length) % controls.length];
1262
+ else if (event.key === "Home") next = controls[0];
1263
+ else if (event.key === "End") next = controls.at(-1);
1264
+ else return;
1265
+ event.preventDefault();
1266
+ select(next.dataset.gView, true);
1267
+ }
1268
+ element.addEventListener("click", click);
1269
+ element.addEventListener("keydown", keydown);
1270
+ select(element.dataset.gCurrentView || controls.find((control) => control.getAttribute("aria-pressed") === "true")?.dataset.gView || controls[0].dataset.gView);
1271
+ return { select, destroy: () => { element.removeEventListener("click", click); element.removeEventListener("keydown", keydown); } };
1272
+ }
1273
+
1274
+ function createToast(element) {
1275
+ let timer;
1276
+ let remaining = Number(element.dataset.gTimeout || 5000);
1277
+ let startedAt;
1278
+ const pauses = new Set();
1279
+ function dismiss(reason = "timeout") { clearTimeout(timer); if (emit(element, "dismiss", { reason })) element.remove(); }
1280
+ function schedule() { clearTimeout(timer); if (pauses.size || remaining <= 0 || element.dataset.gPersistent === "true") return; startedAt = Date.now(); timer = setTimeout(() => dismiss(), remaining); }
1281
+ function pause(reason = "api") { if (!pauses.size && startedAt) remaining = Math.max(0, remaining - (Date.now() - startedAt)); startedAt = null; pauses.add(reason); clearTimeout(timer); }
1282
+ function resume(reason = "api") { pauses.delete(reason); schedule(); }
1283
+ const pointerEnter = () => pause("pointer");
1284
+ const pointerLeave = () => resume("pointer");
1285
+ const focusIn = () => pause("focus");
1286
+ const focusOut = (event) => { if (!element.contains(event.relatedTarget)) resume("focus"); };
1287
+ element.addEventListener("pointerenter", pointerEnter);
1288
+ element.addEventListener("pointerleave", pointerLeave);
1289
+ element.addEventListener("focusin", focusIn);
1290
+ element.addEventListener("focusout", focusOut);
1291
+ schedule();
1292
+ return { dismiss, pause, resume, destroy: () => { clearTimeout(timer); element.removeEventListener("pointerenter", pointerEnter); element.removeEventListener("pointerleave", pointerLeave); element.removeEventListener("focusin", focusIn); element.removeEventListener("focusout", focusOut); } };
1293
+ }
1294
+
1295
+ function createCopy(element) {
1296
+ async function copy() {
1297
+ const target = targetById(element.dataset.gCopy);
1298
+ const value = element.dataset.gCopyValue ?? target?.value ?? target?.textContent ?? "";
1299
+ try {
1300
+ const text = String(value);
1301
+ if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(text);
1302
+ else {
1303
+ const fallback = document.createElement("textarea");
1304
+ fallback.value = text;
1305
+ fallback.setAttribute("readonly", "");
1306
+ fallback.style.cssText = "position:fixed;opacity:0;pointer-events:none";
1307
+ document.body.append(fallback);
1308
+ fallback.select();
1309
+ const copied = document.execCommand("copy");
1310
+ fallback.remove();
1311
+ if (!copied) throw new Error("Clipboard API is unavailable.");
1312
+ }
1313
+ element.dataset.gCopyState = "success";
1314
+ emit(element, "copy", { value: text });
1315
+ } catch (error) {
1316
+ element.dataset.gCopyState = "error";
1317
+ emit(element, "error", { action: "copy", error });
1318
+ }
1319
+ }
1320
+ element.addEventListener("click", copy);
1321
+ return { copy, destroy: () => element.removeEventListener("click", copy) };
1322
+ }
1323
+
1324
+ function createFullscreen(element) {
1325
+ const target = targetById(element.dataset.gFullscreen) || document.documentElement;
1326
+ async function toggle() {
1327
+ try {
1328
+ if (document.fullscreenElement) await document.exitFullscreen?.();
1329
+ else if (target.requestFullscreen) await target.requestFullscreen();
1330
+ else throw new Error("Fullscreen API is unavailable.");
1331
+ } catch (error) { emit(element, "error", { action: "fullscreen", error }); }
1332
+ }
1333
+ function change() {
1334
+ const active = document.fullscreenElement === target;
1335
+ element.setAttribute("aria-pressed", String(active));
1336
+ emit(element, "fullscreenchange", { active, target });
1337
+ }
1338
+ element.setAttribute("aria-pressed", String(document.fullscreenElement === target));
1339
+ element.addEventListener("click", toggle);
1340
+ document.addEventListener("fullscreenchange", change);
1341
+ return { toggle, destroy: () => { element.removeEventListener("click", toggle); document.removeEventListener("fullscreenchange", change); } };
1342
+ }
1343
+
1344
+ function createScrollTop(element) {
1345
+ const threshold = Number(element.dataset.gScrollThreshold || 320);
1346
+ function update() { element.hidden = window.scrollY < threshold; }
1347
+ function scroll() { window.scrollTo({ top: 0, behavior: matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth" }); }
1348
+ element.addEventListener("click", scroll);
1349
+ window.addEventListener("scroll", update, { passive: true });
1350
+ update();
1351
+ return { update, scroll, destroy: () => { element.removeEventListener("click", scroll); window.removeEventListener("scroll", update); } };
1352
+ }
1353
+
1354
+ function createNavToggle(element) {
1355
+ const target = targetById(element.dataset.gNavToggle);
1356
+ if (!target) return null;
1357
+ let closeTimer;
1358
+ if (!target.hasAttribute("data-g-nav-open")) target.dataset.gNavOpen = String(!target.hidden || target.classList.contains("is-open"));
1359
+ const isOpen = () => target.dataset.gNavOpen === "true";
1360
+ const syncExpanded = (value) => document.querySelectorAll("[data-g-nav-toggle]").forEach((trigger) => {
1361
+ if (trigger.dataset.gNavToggle?.replace(/^#/, "") === target.id) trigger.setAttribute("aria-expanded", String(value));
1362
+ });
1363
+ element.setAttribute("aria-controls", target.id);
1364
+ syncExpanded(isOpen());
1365
+ target.setAttribute("aria-hidden", String(!isOpen()));
1366
+
1367
+ function show() {
1368
+ if (isOpen() || !emit(target, "beforeopen", { trigger: element })) return;
1369
+ clearTimeout(closeTimer);
1370
+ target.dataset.gNavOpen = "true";
1371
+ target.hidden = false;
1372
+ target.setAttribute("aria-hidden", "false");
1373
+ syncExpanded(true);
1374
+ requestAnimationFrame(() => target.classList.add("is-open"));
1375
+ document.addEventListener("keydown", keydown);
1376
+ if (element.dataset.gDismissOutside === "true") document.addEventListener("pointerdown", outside, true);
1377
+ emit(target, "open", { trigger: element });
1378
+ }
1379
+ function hide(reason = "api") {
1380
+ if (!isOpen() || !emit(target, "beforeclose", { reason, trigger: element })) return;
1381
+ target.dataset.gNavOpen = "false";
1382
+ target.classList.remove("is-open");
1383
+ target.setAttribute("aria-hidden", "true");
1384
+ syncExpanded(false);
1385
+ document.removeEventListener("keydown", keydown);
1386
+ document.removeEventListener("pointerdown", outside, true);
1387
+ closeTimer = window.setTimeout(() => { if (!isOpen()) target.hidden = true; }, 300);
1388
+ if (reason === "escape") element.focus({ preventScroll: true });
1389
+ emit(target, "close", { reason, trigger: element });
1390
+ }
1391
+ function toggle() { isOpen() ? hide("toggle") : show(); }
1392
+ function keydown(event) { if (event.key === "Escape") { event.preventDefault(); hide("escape"); } }
1393
+ function outside(event) { if (!target.contains(event.target) && !element.contains(event.target)) hide("outside"); }
1394
+ function targetClose() {
1395
+ document.removeEventListener("keydown", keydown);
1396
+ document.removeEventListener("pointerdown", outside, true);
1397
+ }
1398
+ element.addEventListener("click", toggle);
1399
+ target.addEventListener("gardener:close", targetClose);
1400
+ return { open: show, close: hide, toggle, isOpen, destroy: () => { clearTimeout(closeTimer); element.removeEventListener("click", toggle); target.removeEventListener("gardener:close", targetClose); targetClose(); } };
1401
+ }
1402
+
1403
+ function createRovingNav(element) {
1404
+ const selector = ".g-nav-item, .g-nav-rail-item, .g-bottom-nav-item, .g-mobile-tab-item, .g-command-nav-item, [role='menuitem']";
1405
+ const items = () => [...element.querySelectorAll(selector)].filter((item) => !item.disabled && item.getAttribute("aria-disabled") !== "true" && !item.hidden);
1406
+ const vertical = () => element.getAttribute("aria-orientation") === "vertical" || element.dataset.gOrientation === "vertical";
1407
+ function select(item, focus = false) {
1408
+ if (!item) return;
1409
+ items().forEach((node) => { node.tabIndex = node === item ? 0 : -1; });
1410
+ if (element.dataset.gSelection === "true") {
1411
+ items().forEach((node) => node.setAttribute("aria-selected", String(node === item)));
1412
+ emit(element, "change", { item });
1413
+ }
1414
+ if (focus) item.focus();
1415
+ }
1416
+ function click(event) { const item = event.target.closest(selector); if (item && element.contains(item)) select(item); }
1417
+ function keydown(event) {
1418
+ const current = event.target.closest(selector);
1419
+ if (!current) return;
1420
+ const nodes = items();
1421
+ const index = nodes.indexOf(current);
1422
+ const rtl = getComputedStyle(element).direction === "rtl";
1423
+ const forward = vertical() ? "ArrowDown" : rtl ? "ArrowLeft" : "ArrowRight";
1424
+ const backward = vertical() ? "ArrowUp" : rtl ? "ArrowRight" : "ArrowLeft";
1425
+ let next;
1426
+ if (event.key === forward) next = nodes[(index + 1 + nodes.length) % nodes.length];
1427
+ else if (event.key === backward) next = nodes[(index - 1 + nodes.length) % nodes.length];
1428
+ else if (event.key === "Home") next = nodes[0];
1429
+ else if (event.key === "End") next = nodes.at(-1);
1430
+ else return;
1431
+ event.preventDefault();
1432
+ select(next, true);
1433
+ }
1434
+ const initial = items().find((item) => item.matches("[aria-current], [aria-selected='true'], .is-active")) || items()[0];
1435
+ select(initial);
1436
+ element.addEventListener("click", click);
1437
+ element.addEventListener("keydown", keydown);
1438
+ return { select, destroy: () => { element.removeEventListener("click", click); element.removeEventListener("keydown", keydown); } };
1439
+ }
1440
+
1441
+ function createContextMenu(element) {
1442
+ const target = element.querySelector("[data-g-context-target]") || element;
1443
+ const menu = element.querySelector("[data-g-context-menu-panel]") || element.querySelector(".g-context-menu");
1444
+ if (!menu || menu === target) return null;
1445
+ let open = !menu.hidden;
1446
+ let previousFocus = null;
1447
+ const items = () => visibleFocusable(menu).filter((item) => item.getAttribute("aria-disabled") !== "true");
1448
+ menu.setAttribute("role", menu.getAttribute("role") || "menu");
1449
+ menu.hidden = true;
1450
+
1451
+ function position(x, y) {
1452
+ menu.style.inset = "auto";
1453
+ menu.style.left = `${Math.max(4, x)}px`;
1454
+ menu.style.top = `${Math.max(4, y)}px`;
1455
+ const rect = menu.getBoundingClientRect();
1456
+ menu.style.left = `${Math.max(4, Math.min(x, window.innerWidth - rect.width - 4))}px`;
1457
+ menu.style.top = `${Math.max(4, Math.min(y, window.innerHeight - rect.height - 4))}px`;
1458
+ }
1459
+ function show(x, y, source = target) {
1460
+ if (!emit(element, "beforeopen", { source })) return;
1461
+ open = true;
1462
+ previousFocus = document.activeElement;
1463
+ menu.hidden = false;
1464
+ menu.setAttribute("aria-hidden", "false");
1465
+ position(x, y);
1466
+ document.addEventListener("pointerdown", outside, true);
1467
+ document.addEventListener("keydown", keydown);
1468
+ items()[0]?.focus();
1469
+ emit(element, "open", { source, x, y });
1470
+ }
1471
+ function hide(reason = "api", restore = false) {
1472
+ if (!open) return;
1473
+ open = false;
1474
+ menu.hidden = true;
1475
+ menu.setAttribute("aria-hidden", "true");
1476
+ document.removeEventListener("pointerdown", outside, true);
1477
+ document.removeEventListener("keydown", keydown);
1478
+ if (restore) previousFocus?.focus?.({ preventScroll: true });
1479
+ emit(element, "close", { reason });
1480
+ }
1481
+ function context(event) { event.preventDefault(); show(event.clientX, event.clientY, event.target); }
1482
+ function targetKeydown(event) {
1483
+ if ((event.shiftKey && event.key === "F10") || event.key === "ContextMenu") {
1484
+ event.preventDefault();
1485
+ const rect = target.getBoundingClientRect();
1486
+ show(rect.left + 8, rect.top + 8, target);
1487
+ }
1488
+ }
1489
+ function outside(event) { if (!menu.contains(event.target)) hide("outside"); }
1490
+ function keydown(event) {
1491
+ const nodes = items();
1492
+ const index = nodes.indexOf(document.activeElement);
1493
+ if (event.key === "Escape") { event.preventDefault(); hide("escape", true); }
1494
+ else if (event.key === "ArrowDown") { event.preventDefault(); (nodes[index + 1] || nodes[0])?.focus(); }
1495
+ else if (event.key === "ArrowUp") { event.preventDefault(); (nodes[index - 1] || nodes.at(-1))?.focus(); }
1496
+ else if (event.key === "Home") { event.preventDefault(); nodes[0]?.focus(); }
1497
+ else if (event.key === "End") { event.preventDefault(); nodes.at(-1)?.focus(); }
1498
+ }
1499
+ function menuClick(event) { if (event.target.closest("[role='menuitem'], .g-menu-item")) hide("select"); }
1500
+ target.addEventListener("contextmenu", context);
1501
+ target.addEventListener("keydown", targetKeydown);
1502
+ menu.addEventListener("click", menuClick);
1503
+ return { open: show, close: hide, isOpen: () => open, destroy: () => { target.removeEventListener("contextmenu", context); target.removeEventListener("keydown", targetKeydown); menu.removeEventListener("click", menuClick); document.removeEventListener("pointerdown", outside, true); document.removeEventListener("keydown", keydown); } };
1504
+ }
1505
+
1506
+ function createScrollspy(element) {
1507
+ const links = [...element.querySelectorAll("a[href^='#']")];
1508
+ const entries = links.map((link) => ({ link, section: targetById(link.getAttribute("href")) })).filter((entry) => entry.section);
1509
+ if (!entries.length) return null;
1510
+ const offset = Number(element.dataset.gScrollspyOffset || 96);
1511
+ const scrollRoot = targetById(element.dataset.gScrollspyRoot) || window;
1512
+ let current = null;
1513
+ let frame;
1514
+ function update() {
1515
+ frame = null;
1516
+ let active = entries[0];
1517
+ const rootTop = scrollRoot === window ? 0 : scrollRoot.getBoundingClientRect().top;
1518
+ for (const entry of entries) if (entry.section.getBoundingClientRect().top <= rootTop + offset) active = entry;
1519
+ const atEnd = scrollRoot === window
1520
+ ? window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 1
1521
+ : scrollRoot.scrollTop + scrollRoot.clientHeight >= scrollRoot.scrollHeight - 1;
1522
+ if (atEnd) active = entries.at(-1);
1523
+ entries.forEach((entry) => {
1524
+ if (entry === active) entry.link.setAttribute("aria-current", "location");
1525
+ else entry.link.removeAttribute("aria-current");
1526
+ });
1527
+ if (current !== active) {
1528
+ current = active;
1529
+ emit(element, "change", { link: active.link, section: active.section });
1530
+ }
1531
+ }
1532
+ function schedule() { if (!frame) frame = requestAnimationFrame(update); }
1533
+ scrollRoot.addEventListener("scroll", schedule, { passive: true });
1534
+ window.addEventListener("resize", schedule);
1535
+ update();
1536
+ return { update, destroy: () => { if (frame) cancelAnimationFrame(frame); scrollRoot.removeEventListener("scroll", schedule); window.removeEventListener("resize", schedule); } };
1537
+ }
1538
+
1539
+ function createJumpNav(element) {
1540
+ const select = element.matches("select") ? element : element.querySelector("select");
1541
+ if (!select) return null;
1542
+ function jump() {
1543
+ const target = targetById(select.value);
1544
+ if (!target) return;
1545
+ target.scrollIntoView({ behavior: matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth", block: "start" });
1546
+ if (target.id && history.replaceState) history.replaceState(null, "", `#${target.id}`);
1547
+ emit(element, "change", { target, value: select.value });
1548
+ }
1549
+ select.addEventListener("change", jump);
1550
+ return { jump, destroy: () => select.removeEventListener("change", jump) };
1551
+ }
1552
+
1553
+ function createDropzone(element) {
1554
+ let depth = 0;
1555
+ const input = element.querySelector("input[type='file']");
1556
+ const nativeLabel = element.matches("label") && Boolean(input);
1557
+ function enter(event) { event.preventDefault(); depth += 1; element.classList.add("is-dragging"); }
1558
+ function over(event) { event.preventDefault(); if (event.dataTransfer) event.dataTransfer.dropEffect = "copy"; }
1559
+ function leave(event) { event.preventDefault(); depth = Math.max(0, depth - 1); if (!depth) element.classList.remove("is-dragging"); }
1560
+ function drop(event) {
1561
+ event.preventDefault();
1562
+ depth = 0;
1563
+ element.classList.remove("is-dragging");
1564
+ const files = [...(event.dataTransfer?.files || [])];
1565
+ emit(element, "drop", { files, dataTransfer: event.dataTransfer });
1566
+ }
1567
+ function choose() { const files = [...(input?.files || [])]; emit(element, "files", { files, input }); }
1568
+ function activate(event) {
1569
+ if (!input || event.target === input || event.target.closest("button, a, input, select, textarea")) return;
1570
+ if (event.type === "keydown" && event.key !== "Enter" && event.key !== " ") return;
1571
+ event.preventDefault();
1572
+ input.click();
1573
+ }
1574
+ if (input) {
1575
+ if (!nativeLabel && !element.hasAttribute("role")) element.setAttribute("role", "button");
1576
+ if (!nativeLabel && !element.hasAttribute("tabindex")) element.tabIndex = 0;
1577
+ input.addEventListener("change", choose);
1578
+ if (!nativeLabel) {
1579
+ element.addEventListener("click", activate);
1580
+ element.addEventListener("keydown", activate);
1581
+ }
1582
+ }
1583
+ element.addEventListener("dragenter", enter);
1584
+ element.addEventListener("dragover", over);
1585
+ element.addEventListener("dragleave", leave);
1586
+ element.addEventListener("drop", drop);
1587
+ return { destroy: () => { element.removeEventListener("dragenter", enter); element.removeEventListener("dragover", over); element.removeEventListener("dragleave", leave); element.removeEventListener("drop", drop); input?.removeEventListener("change", choose); element.removeEventListener("click", activate); element.removeEventListener("keydown", activate); } };
1588
+ }
1589
+
1590
+ function createCommandPalette(element) {
1591
+ let trigger = null;
1592
+ let open = !element.hidden;
1593
+ if (!element.id) element.id = uid("g-command-palette");
1594
+ const panel = element.querySelector("[role='dialog'], .g-command-palette") || element;
1595
+ const triggers = [...document.querySelectorAll("[data-g-command-trigger], [aria-controls]")].filter((button) => (button.dataset.gCommandTrigger || button.getAttribute("aria-controls"))?.replace(/^#/, "") === element.id);
1596
+ panel.setAttribute("role", "dialog");
1597
+ panel.setAttribute("aria-modal", "true");
1598
+ element.setAttribute("aria-hidden", String(!open));
1599
+ triggers.forEach((button) => {
1600
+ button.setAttribute("aria-haspopup", "dialog");
1601
+ button.setAttribute("aria-controls", element.id);
1602
+ button.setAttribute("aria-expanded", String(open));
1603
+ });
1604
+ function syncExpanded(value) { triggers.forEach((button) => button.setAttribute("aria-expanded", String(value))); }
1605
+ function show(source = document.activeElement) {
1606
+ if (open || !emit(element, "beforeopen", { source })) return;
1607
+ open = true;
1608
+ trigger = source;
1609
+ if (trigger instanceof HTMLElement && trigger.hasAttribute("data-g-command-trigger")) trigger.setAttribute("aria-expanded", "true");
1610
+ element.hidden = false;
1611
+ element.setAttribute("aria-hidden", "false");
1612
+ element.classList.add("is-open");
1613
+ syncExpanded(true);
1614
+ (visibleFocusable(panel)[0] || panel).focus();
1615
+ emit(element, "open", { source });
1616
+ }
1617
+ function hide(reason = "api") {
1618
+ if (!open || !emit(element, "beforeclose", { reason })) return;
1619
+ open = false;
1620
+ element.hidden = true;
1621
+ element.setAttribute("aria-hidden", "true");
1622
+ element.classList.remove("is-open");
1623
+ syncExpanded(false);
1624
+ if (trigger instanceof HTMLElement && trigger.hasAttribute("data-g-command-trigger")) trigger.setAttribute("aria-expanded", "false");
1625
+ trigger?.focus?.();
1626
+ emit(element, "close", { reason });
1627
+ }
1628
+ function keydown(event) {
1629
+ if ((event.ctrlKey || event.metaKey) && event.key.toLocaleLowerCase() === "k") { event.preventDefault(); open ? hide("shortcut") : show(document.activeElement); }
1630
+ else if (open && event.key === "Escape") { event.preventDefault(); hide("escape"); }
1631
+ else if (open && event.key === "Tab") {
1632
+ const items = visibleFocusable(panel);
1633
+ if (!items.length) { event.preventDefault(); panel.focus(); return; }
1634
+ const first = items[0];
1635
+ const last = items.at(-1);
1636
+ if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
1637
+ else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
1638
+ }
1639
+ }
1640
+ function backdrop(event) { if (event.target === element) hide("backdrop"); }
1641
+ if (open) { open = false; queueMicrotask(show); }
1642
+ document.addEventListener("keydown", keydown);
1643
+ element.addEventListener("pointerdown", backdrop);
1644
+ return { open: show, close: hide, toggle: (source) => open ? hide("toggle") : show(source), isOpen: () => open, destroy: () => { document.removeEventListener("keydown", keydown); element.removeEventListener("pointerdown", backdrop); open = false; element.hidden = true; element.setAttribute("aria-hidden", "true"); element.classList.remove("is-open"); syncExpanded(false); } };
1645
+ }
1646
+
1647
+ function createUploadManager(element) {
1648
+ const list = element.querySelector("[data-g-upload-list]") || element;
1649
+ const input = element.querySelector("input[type='file'][data-g-upload-input]");
1650
+ const template = targetById(element.dataset.gUploadTemplate);
1651
+ const items = () => [...list.querySelectorAll("[data-g-upload-item]")];
1652
+ function statusText(item) {
1653
+ const state = item.dataset.gStatus || "queued";
1654
+ const progress = Math.max(0, Math.min(100, Number(item.dataset.gProgress || 0)));
1655
+ return state === "success" ? "已完成" : state === "error" ? "上传失败" : state === "paused" ? "已暂停" : state === "uploading" ? `上传中 ${progress}%` : "等待上传";
1656
+ }
1657
+ function sync(reason = "api") {
1658
+ const current = items();
1659
+ current.forEach((item) => {
1660
+ const progress = Math.max(0, Math.min(100, Number(item.dataset.gProgress || 0)));
1661
+ item.style.setProperty("--g-progress", `${progress}%`);
1662
+ const bar = item.querySelector("[data-g-upload-progress]");
1663
+ if (bar) { bar.style.setProperty("--g-progress", `${progress}%`); bar.setAttribute("aria-valuenow", String(progress)); }
1664
+ const status = item.querySelector("[data-g-upload-status]");
1665
+ if (status) status.textContent = statusText(item);
1666
+ item.querySelectorAll("[data-g-upload-action='pause']").forEach((button) => { button.textContent = item.dataset.gStatus === "paused" ? "继续" : "暂停"; });
1667
+ });
1668
+ const counts = { total: current.length, success: current.filter((item) => item.dataset.gStatus === "success").length, error: current.filter((item) => item.dataset.gStatus === "error").length, active: current.filter((item) => ["queued", "uploading", "paused"].includes(item.dataset.gStatus || "queued")).length };
1669
+ Object.entries(counts).forEach(([name, value]) => element.querySelectorAll(`[data-g-upload-count="${name}"]`).forEach((node) => { node.textContent = String(value); }));
1670
+ emit(element, "uploadchange", { items: current, counts, reason });
1671
+ }
1672
+ function addFile(file) {
1673
+ let item;
1674
+ if (template?.content) { const fragment = template.content.cloneNode(true); item = fragment.querySelector("[data-g-upload-item]") || fragment.firstElementChild; list.append(fragment); }
1675
+ if (!item) { item = document.createElement("div"); item.className = "g-upload-item"; item.dataset.gUploadItem = ""; item.innerHTML = '<span class="g-file-icon">FILE</span><span class="g-file-main"><strong class="g-file-name"></strong><span class="g-file-meta"></span><span class="g-upload-status" data-g-upload-status></span></span><span class="g-upload-progress" data-g-upload-progress role="progressbar" aria-valuemin="0" aria-valuemax="100"><i class="g-upload-progress-bar"></i></span><span class="g-upload-actions"><button class="g-btn g-btn-ghost g-btn-sm" type="button" data-g-upload-action="remove">移除</button></span>'; list.append(item); }
1676
+ item.dataset.gStatus = "queued"; item.dataset.gProgress = "0"; item.querySelector(".g-file-name")?.replaceChildren(document.createTextNode(file.name));
1677
+ const meta = item.querySelector(".g-file-meta"); if (meta) meta.textContent = file.size ? `${Math.max(1, Math.round(file.size / 1024))} KB` : "等待计算";
1678
+ emit(element, "uploadadd", { file, item });
1679
+ }
1680
+ function action(item, name) {
1681
+ if (name === "remove") { item.remove(); sync("remove"); return; }
1682
+ if (name === "retry") { item.dataset.gStatus = "queued"; item.dataset.gProgress = "0"; }
1683
+ if (name === "pause") item.dataset.gStatus = item.dataset.gStatus === "paused" ? "uploading" : "paused";
1684
+ if (name === "advance" && item.dataset.gStatus !== "paused") { const progress = Math.min(100, Number(item.dataset.gProgress || 0) + 25); item.dataset.gProgress = String(progress); item.dataset.gStatus = progress >= 100 ? "success" : "uploading"; }
1685
+ sync(name);
1686
+ }
1687
+ function click(event) { const button = event.target.closest("[data-g-upload-action]"); const item = button?.closest("[data-g-upload-item]"); if (button && item) { event.preventDefault(); action(item, button.dataset.gUploadAction); } }
1688
+ function change() { [...(input?.files || [])].forEach(addFile); if (input) input.value = ""; sync("files"); }
1689
+ element.addEventListener("click", click); input?.addEventListener("change", change); sync("init");
1690
+ return { sync, addFile, items, destroy: () => { element.removeEventListener("click", click); input?.removeEventListener("change", change); } };
1691
+ }
1692
+
1693
+ function createFileBrowser(element) {
1694
+ const items = () => [...element.querySelectorAll("[data-g-file-item]")];
1695
+ const search = element.querySelector("[data-g-file-search]");
1696
+ let selected = items().find((item) => item.getAttribute("aria-selected") === "true") || null;
1697
+ function select(item, reason = "api") {
1698
+ items().forEach((entry) => { const active = entry === item; entry.setAttribute("aria-selected", String(active)); entry.tabIndex = active ? 0 : -1; });
1699
+ selected = item;
1700
+ element.querySelectorAll("[data-g-file-selection]").forEach((node) => { node.textContent = item?.dataset.gFileName || item?.querySelector(".g-file-name, .g-media-label")?.textContent || "未选择"; });
1701
+ emit(element, "fileselect", { item, value: item?.dataset.gValue, reason });
1702
+ }
1703
+ function filter() {
1704
+ const query = String(search?.value || "").normalize("NFKD").toLocaleLowerCase().trim();
1705
+ let visible = 0;
1706
+ items().forEach((item) => { const match = !query || String(item.dataset.gFilterText || item.dataset.gFileName || item.textContent).normalize("NFKD").toLocaleLowerCase().includes(query); item.hidden = !match; if (match) visible += 1; });
1707
+ element.querySelectorAll("[data-g-file-count]").forEach((node) => { node.textContent = String(visible); });
1708
+ emit(element, "filefilter", { query, visible });
1709
+ }
1710
+ function click(event) {
1711
+ const view = event.target.closest("[data-g-file-view]");
1712
+ if (view) { element.dataset.gView = view.dataset.gFileView; element.querySelectorAll("[data-g-file-view]").forEach((button) => button.setAttribute("aria-pressed", String(button === view))); emit(element, "fileview", { view: element.dataset.gView }); return; }
1713
+ const item = event.target.closest("[data-g-file-item]"); if (item) select(item, "pointer");
1714
+ }
1715
+ function keydown(event) {
1716
+ const item = event.target.closest("[data-g-file-item]"); if (!item || !["ArrowDown", "ArrowUp", "ArrowRight", "ArrowLeft", "Home", "End", "Enter", " "].includes(event.key)) return;
1717
+ const visible = items().filter((entry) => !entry.hidden); if (!visible.length) return; event.preventDefault();
1718
+ if (["Enter", " "].includes(event.key)) { select(item, "keyboard"); return; }
1719
+ const current = visible.indexOf(item); const forward = ["ArrowDown", "ArrowRight"].includes(event.key); const index = event.key === "Home" ? 0 : event.key === "End" ? visible.length - 1 : (current + (forward ? 1 : -1) + visible.length) % visible.length; visible[index].focus();
1720
+ }
1721
+ element.addEventListener("click", click); element.addEventListener("keydown", keydown); search?.addEventListener("input", filter);
1722
+ items().forEach((item, index) => {
1723
+ item.tabIndex = item === selected || (!selected && index === 0) ? 0 : -1;
1724
+ item.setAttribute("role", item.getAttribute("role") || "option");
1725
+ if (!item.hasAttribute("aria-selected")) item.setAttribute("aria-selected", "false");
1726
+ const collection = item.parentElement;
1727
+ if (collection) {
1728
+ if (!collection.hasAttribute("role")) collection.setAttribute("role", "listbox");
1729
+ if (!collection.hasAttribute("aria-label") && !collection.hasAttribute("aria-labelledby")) collection.setAttribute("aria-label", element.getAttribute("aria-label") || "文件列表");
1730
+ }
1731
+ });
1732
+ if (selected) select(selected, "init"); filter();
1733
+ return { select, filter, selected: () => selected, destroy: () => { element.removeEventListener("click", click); element.removeEventListener("keydown", keydown); search?.removeEventListener("input", filter); } };
1734
+ }
1735
+
1736
+ function createEditorShell(element) {
1737
+ const surface = element.querySelector("[data-g-editor-surface]");
1738
+ if (!surface) return null;
1739
+ const tools = [...element.querySelectorAll("[data-g-editor-command]")];
1740
+ function counts() {
1741
+ const text = String("value" in surface ? surface.value : surface.innerText || surface.textContent || "");
1742
+ const trimmed = text.trim(); const words = trimmed ? (trimmed.match(/[\p{L}\p{N}]+/gu) || []).length : 0;
1743
+ element.querySelectorAll("[data-g-editor-count='characters']").forEach((node) => { node.textContent = String(text.length); });
1744
+ element.querySelectorAll("[data-g-editor-count='words']").forEach((node) => { node.textContent = String(words); });
1745
+ emit(element, "editorchange", { text, characters: text.length, words });
1746
+ }
1747
+ function run(tool) {
1748
+ const command = tool.dataset.gEditorCommand; const value = tool.dataset.gEditorValue || null;
1749
+ surface.focus();
1750
+ if (command === "undo" || command === "redo" || surface.isContentEditable) document.execCommand?.(command, false, value);
1751
+ if (tool.hasAttribute("aria-pressed")) tool.setAttribute("aria-pressed", String(tool.getAttribute("aria-pressed") !== "true"));
1752
+ counts(); emit(element, "editorcommand", { command, value, tool });
1753
+ }
1754
+ function click(event) { const tool = event.target.closest("[data-g-editor-command]"); if (tool) { event.preventDefault(); run(tool); } }
1755
+ function keydown(event) { if ((event.ctrlKey || event.metaKey) && ["b", "i", "u"].includes(event.key.toLocaleLowerCase())) { event.preventDefault(); const map = { b: "bold", i: "italic", u: "underline" }; const tool = tools.find((item) => item.dataset.gEditorCommand === map[event.key.toLocaleLowerCase()]); if (tool) run(tool); } }
1756
+ element.addEventListener("click", click); surface.addEventListener("input", counts); surface.addEventListener("keydown", keydown); counts();
1757
+ return { run, counts, surface, destroy: () => { element.removeEventListener("click", click); surface.removeEventListener("input", counts); surface.removeEventListener("keydown", keydown); } };
1758
+ }
1759
+
1760
+ function createRevisionCompare(element) {
1761
+ const controls = [...element.querySelectorAll("[data-g-revision-view]")];
1762
+ function setView(view, reason = "api") { element.dataset.gRevisionView = view; controls.forEach((button) => button.setAttribute("aria-pressed", String(button.dataset.gRevisionView === view))); emit(element, "revisionview", { view, reason }); }
1763
+ function click(event) { const button = event.target.closest("[data-g-revision-view]"); if (button) { event.preventDefault(); setView(button.dataset.gRevisionView, "pointer"); } }
1764
+ element.addEventListener("click", click); setView(element.dataset.gRevisionView || controls.find((button) => button.getAttribute("aria-pressed") === "true")?.dataset.gRevisionView || "split", "init");
1765
+ return { setView, view: () => element.dataset.gRevisionView, destroy: () => element.removeEventListener("click", click) };
1766
+ }
1767
+
1768
+ function createAutosave(element) {
1769
+ const scope = targetById(element.dataset.gAutosaveScope) || element.closest("form, [data-g-editor-shell]") || element.parentElement;
1770
+ const delay = Math.max(150, Number(element.dataset.gAutosaveDelay || 900));
1771
+ let timer = 0; let saveTimer = 0;
1772
+ function setState(state, reason = "api") { element.dataset.gAutosaveState = state; const label = element.querySelector("[data-g-autosave-label]"); if (label) label.textContent = state === "dirty" ? "有未保存更改" : state === "saving" ? "正在保存…" : state === "error" ? "保存失败" : "已保存"; emit(element, "autosavestate", { state, reason }); }
1773
+ function save(reason = "api") { clearTimeout(timer); clearTimeout(saveTimer); setState("saving", reason); saveTimer = window.setTimeout(() => setState("saved", "complete"), 260); }
1774
+ function dirty() { clearTimeout(timer); setState("dirty", "input"); timer = window.setTimeout(() => save("delay"), delay); }
1775
+ function click(event) { if (event.target.closest("[data-g-autosave-now]")) { event.preventDefault(); save("button"); } }
1776
+ scope?.addEventListener("input", dirty); scope?.addEventListener("change", dirty); element.addEventListener("click", click); setState(element.dataset.gAutosaveState || "saved", "init");
1777
+ return { save, state: () => element.dataset.gAutosaveState, destroy: () => { clearTimeout(timer); clearTimeout(saveTimer); scope?.removeEventListener("input", dirty); scope?.removeEventListener("change", dirty); element.removeEventListener("click", click); } };
1778
+ }
1779
+
1780
+ function createQuantityStepper(element) {
1781
+ const input = element.querySelector("[data-g-quantity-input]") || element.querySelector("input[type='number']");
1782
+ const decrement = element.querySelector("[data-g-quantity-decrement]");
1783
+ const increment = element.querySelector("[data-g-quantity-increment]");
1784
+ if (!input) return null;
1785
+ const limits = () => ({ min: numeric(input.min, 0), max: input.max === "" ? Infinity : numeric(input.max, Infinity), step: Math.max(.000001, numeric(input.step, 1)) });
1786
+ function set(value, reason = "api") {
1787
+ const { min, max, step } = limits();
1788
+ const next = Math.min(max, Math.max(min, numeric(value, min)));
1789
+ const precision = Math.max(0, String(step).split(".")[1]?.length || 0);
1790
+ input.value = String(Number(next.toFixed(precision)));
1791
+ decrement?.toggleAttribute("disabled", next <= min);
1792
+ increment?.toggleAttribute("disabled", next >= max);
1793
+ element.dataset.gQuantityValue = input.value;
1794
+ emit(element, "quantitychange", { value: next, reason });
1795
+ return next;
1796
+ }
1797
+ function click(event) {
1798
+ if (event.target.closest("[data-g-quantity-decrement]")) set(numeric(input.value, 0) - limits().step, "decrement");
1799
+ if (event.target.closest("[data-g-quantity-increment]")) set(numeric(input.value, 0) + limits().step, "increment");
1800
+ }
1801
+ function change() { set(input.value, "input"); }
1802
+ element.addEventListener("click", click);
1803
+ input.addEventListener("change", change);
1804
+ set(input.value, "init");
1805
+ return { value: () => numeric(input.value, 0), set, increment: () => set(numeric(input.value, 0) + limits().step, "api"), decrement: () => set(numeric(input.value, 0) - limits().step, "api"), destroy: () => { element.removeEventListener("click", click); input.removeEventListener("change", change); } };
1806
+ }
1807
+
1808
+ function createSkuSelector(element) {
1809
+ const groups = [...element.querySelectorAll("[data-g-sku-group]")];
1810
+ const output = element.querySelector("[data-g-sku-output]");
1811
+ const summary = element.querySelector("[data-g-sku-selection]");
1812
+ if (!groups.length) return null;
1813
+ function options(group) { return group ? [...group.querySelectorAll("[data-g-sku-option]")] : []; }
1814
+ function selection() {
1815
+ return Object.fromEntries(groups.map((group, index) => {
1816
+ const chosen = options(group).find((option) => option.getAttribute("aria-pressed") === "true");
1817
+ return [group.dataset.gSkuGroup || `option${index + 1}`, chosen?.dataset.gValue || chosen?.textContent.trim() || ""];
1818
+ }));
1819
+ }
1820
+ function sync(reason = "api") {
1821
+ const value = selection();
1822
+ const complete = Object.values(value).every(Boolean);
1823
+ element.classList.toggle("is-complete", complete);
1824
+ element.dataset.gSkuComplete = String(complete);
1825
+ if (output) output.value = JSON.stringify(value);
1826
+ if (summary) summary.textContent = Object.values(value).filter(Boolean).join(" / ") || element.dataset.gEmptyLabel || "请选择规格";
1827
+ emit(element, "skuchange", { value, complete, reason });
1828
+ return value;
1829
+ }
1830
+ function choose(option, reason = "pointer") {
1831
+ if (!option || option.disabled || option.getAttribute("aria-disabled") === "true") return;
1832
+ const group = option.closest("[data-g-sku-group]");
1833
+ options(group).forEach((item) => { item.setAttribute("aria-pressed", String(item === option)); item.tabIndex = item === option ? 0 : -1; });
1834
+ sync(reason);
1835
+ }
1836
+ function click(event) { const option = event.target.closest("[data-g-sku-option]"); if (option && element.contains(option)) choose(option); }
1837
+ function keydown(event) {
1838
+ const current = event.target.closest("[data-g-sku-option]");
1839
+ if (!current) return;
1840
+ if (["Enter", " "].includes(event.key)) { event.preventDefault(); choose(current, "keyboard"); return; }
1841
+ if (!["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"].includes(event.key)) return;
1842
+ event.preventDefault();
1843
+ const enabled = options(current.closest("[data-g-sku-group]")).filter((item) => !item.disabled && item.getAttribute("aria-disabled") !== "true");
1844
+ const index = enabled.indexOf(current);
1845
+ const next = event.key === "Home" ? enabled[0] : event.key === "End" ? enabled.at(-1) : ["ArrowRight", "ArrowDown"].includes(event.key) ? enabled[index + 1] || enabled[0] : enabled[index - 1] || enabled.at(-1);
1846
+ next?.focus(); choose(next, "keyboard");
1847
+ }
1848
+ groups.forEach((group) => { const items = options(group); const selected = items.find((item) => item.getAttribute("aria-pressed") === "true") || items.find((item) => !item.disabled && item.getAttribute("aria-disabled") !== "true"); items.forEach((item) => { item.setAttribute("aria-pressed", String(item === selected)); item.tabIndex = item === selected ? 0 : -1; }); });
1849
+ element.addEventListener("click", click); element.addEventListener("keydown", keydown); sync("init");
1850
+ return { value: selection, select: (groupName, value) => { const group = groups.find((item) => item.dataset.gSkuGroup === groupName); choose(options(group).find((item) => (item.dataset.gValue || item.textContent.trim()) === value), "api"); }, destroy: () => { element.removeEventListener("click", click); element.removeEventListener("keydown", keydown); } };
1851
+ }
1852
+
1853
+ function createCart(element) {
1854
+ const locale = element.dataset.gLocale || document.documentElement.lang || "zh-CN";
1855
+ const currency = element.dataset.gCurrency || "CNY";
1856
+ const formatter = new Intl.NumberFormat(locale, { style: "currency", currency });
1857
+ const number = (value) => Number.isFinite(Number(value)) ? Number(value) : 0;
1858
+ function update(reason = "api") {
1859
+ const items = [...element.querySelectorAll("[data-g-cart-item]")];
1860
+ let subtotal = 0; let count = 0;
1861
+ items.forEach((item) => {
1862
+ const input = item.querySelector("[data-g-quantity-input]") || item.querySelector("input[type='number']");
1863
+ const quantity = Math.max(0, number(input?.value || item.dataset.gQuantity || 1));
1864
+ const total = number(item.dataset.gUnitPrice) * quantity;
1865
+ subtotal += total; count += quantity;
1866
+ const line = item.querySelector("[data-g-line-total]");
1867
+ if (line) line.textContent = formatter.format(total / 100);
1868
+ });
1869
+ const discount = number(element.dataset.gDiscount);
1870
+ const shipping = number(element.dataset.gShipping);
1871
+ const tax = number(element.dataset.gTax);
1872
+ const total = Math.max(0, subtotal - discount + shipping + tax);
1873
+ const values = { "[data-g-cart-subtotal]": subtotal, "[data-g-cart-discount]": -discount, "[data-g-cart-shipping]": shipping, "[data-g-cart-tax]": tax, "[data-g-cart-total]": total };
1874
+ Object.entries(values).forEach(([selector, value]) => { element.querySelectorAll(selector).forEach((node) => { node.textContent = formatter.format(value / 100); }); });
1875
+ element.querySelectorAll("[data-g-cart-count]").forEach((node) => { node.textContent = String(count); });
1876
+ element.querySelector("[data-g-cart-empty]")?.toggleAttribute("hidden", items.length > 0);
1877
+ element.classList.toggle("is-empty", items.length === 0);
1878
+ emit(element, "cartchange", { subtotal, discount, shipping, tax, total, count, reason });
1879
+ return { subtotal, discount, shipping, tax, total, count };
1880
+ }
1881
+ function click(event) { const remove = event.target.closest("[data-g-cart-remove]"); if (remove) { const item = remove.closest("[data-g-cart-item]"); if (item && emit(item, "cartremove", { item })) { item.remove(); update("remove"); } } }
1882
+ function change(event) { if (event.target.matches("[data-g-quantity-input], input[type='number']")) update("quantity"); }
1883
+ function quantity(event) { if (element.contains(event.target)) update("quantity"); }
1884
+ element.addEventListener("click", click); element.addEventListener("change", change); element.addEventListener("gardener:quantitychange", quantity); update("init");
1885
+ return { update, summary: () => update("api"), destroy: () => { element.removeEventListener("click", click); element.removeEventListener("change", change); element.removeEventListener("gardener:quantitychange", quantity); } };
1886
+ }
1887
+
1888
+ function createCoupon(element) {
1889
+ const input = element.querySelector("[data-g-coupon-input]") || element.querySelector("input");
1890
+ const status = element.querySelector("[data-g-coupon-status]");
1891
+ if (!input) return null;
1892
+ const validCodes = (element.dataset.gValidCodes || "").split(",").map((code) => code.trim().toLocaleUpperCase()).filter(Boolean);
1893
+ function setState(state, code = "", reason = "api") {
1894
+ element.classList.toggle("is-applied", state === "applied"); element.classList.toggle("is-invalid", state === "invalid");
1895
+ element.dataset.gCouponState = state; input.setAttribute("aria-invalid", String(state === "invalid"));
1896
+ if (status) { status.textContent = state === "applied" ? element.dataset.gAppliedMessage || `优惠码 ${code} 已使用` : state === "invalid" ? element.dataset.gInvalidMessage || "优惠码无效或已过期" : element.dataset.gIdleMessage || ""; status.setAttribute("role", state === "invalid" ? "alert" : "status"); }
1897
+ emit(element, "couponchange", { state, code, reason });
1898
+ }
1899
+ function apply(reason = "button") { const code = input.value.trim().toLocaleUpperCase(); if (!code || (validCodes.length && !validCodes.includes(code))) setState("invalid", code, reason); else { input.value = code; setState("applied", code, reason); } }
1900
+ function clear() { input.value = ""; setState("idle", "", "clear"); input.focus(); }
1901
+ function click(event) { if (event.target.closest("[data-g-coupon-apply]")) { event.preventDefault(); apply(); } if (event.target.closest("[data-g-coupon-clear]")) { event.preventDefault(); clear(); } }
1902
+ function keydown(event) { if (event.key === "Enter") { event.preventDefault(); apply("keyboard"); } }
1903
+ element.addEventListener("click", click); input.addEventListener("keydown", keydown); setState("idle", "", "init");
1904
+ return { apply, clear, state: () => element.dataset.gCouponState, destroy: () => { element.removeEventListener("click", click); input.removeEventListener("keydown", keydown); } };
1905
+ }
1906
+
1907
+ function register(name, factory) {
1908
+ if (!name || typeof factory !== "function") throw new TypeError("Gardener.register requires a name and factory function.");
1909
+ registry.set(name, factory);
1910
+ }
1911
+
1912
+ function createPullRefresh(element) {
1913
+ const content = element.querySelector(".g-mobile-pull-refresh-content") || element.firstElementChild;
1914
+ const indicator = element.querySelector("[data-g-pull-indicator]");
1915
+ const threshold = numeric(element.dataset.gPullThreshold, 72);
1916
+ let startY = 0; let distance = 0; let tracking = false; let refreshing = false;
1917
+ function paint(value) { distance = Math.max(0, Math.min(value, threshold * 1.5)); element.style.setProperty("--g-pull-distance", `${distance}px`); element.classList.toggle("is-pulling", tracking); if (indicator) indicator.textContent = distance >= threshold ? element.dataset.gReleaseLabel || "松开刷新" : element.dataset.gPullLabel || "下拉刷新"; }
1918
+ function begin(event) { if (refreshing || element.scrollTop > 0 || event.pointerType === "mouse" && event.button !== 0) return; startY = event.clientY; tracking = true; element.setPointerCapture?.(event.pointerId); }
1919
+ function move(event) { if (!tracking) return; const delta = event.clientY - startY; if (delta <= 0) return paint(0); if (delta > 8) event.preventDefault(); paint(Math.sqrt(delta) * 8); }
1920
+ function finish() { if (!refreshing) return; refreshing = false; element.classList.remove("is-refreshing"); element.setAttribute("aria-busy", "false"); paint(0); if (indicator) indicator.textContent = element.dataset.gPullLabel || "下拉刷新"; emit(element, "refreshcomplete"); }
1921
+ function refresh(reason = "gesture") { if (refreshing || !emit(element, "refresh", { reason, complete: finish })) return; refreshing = true; tracking = false; element.classList.add("is-refreshing"); element.setAttribute("aria-busy", "true"); paint(threshold / 2); if (indicator) indicator.textContent = element.dataset.gRefreshingLabel || "正在刷新…"; const timeout = numeric(element.dataset.gRefreshTimeout, 0); if (timeout > 0) window.setTimeout(finish, timeout); }
1922
+ function end() { if (!tracking) return; tracking = false; element.classList.remove("is-pulling"); distance >= threshold ? refresh() : paint(0); }
1923
+ function click(event) { if (event.target.closest("[data-g-refresh]")) refresh("button"); }
1924
+ element.addEventListener("pointerdown", begin); element.addEventListener("pointermove", move, { passive: false }); element.addEventListener("pointerup", end); element.addEventListener("pointercancel", end); element.addEventListener("click", click);
1925
+ return { refresh, complete: finish, isRefreshing: () => refreshing, destroy: () => { element.removeEventListener("pointerdown", begin); element.removeEventListener("pointermove", move); element.removeEventListener("pointerup", end); element.removeEventListener("pointercancel", end); element.removeEventListener("click", click); content?.style.removeProperty("transform"); element.style.removeProperty("--g-pull-distance"); } };
1926
+ }
1927
+
1928
+ function createInfiniteLoad(element) {
1929
+ const sentinel = element.querySelector("[data-g-infinite-sentinel]") || element;
1930
+ let loading = false; let done = false; let observer;
1931
+ function complete(options = {}) { loading = false; done = Boolean(options.done); element.classList.toggle("is-loading", false); element.classList.toggle("is-done", done); element.setAttribute("aria-busy", "false"); if (done) observer?.disconnect(); emit(element, "loadcomplete", { done }); }
1932
+ function load(reason = "observer") { if (loading || done || !emit(element, "loadmore", { reason, complete })) return; loading = true; element.classList.add("is-loading"); element.setAttribute("aria-busy", "true"); }
1933
+ function click(event) { if (event.target.closest("[data-g-infinite-load-more]")) load("button"); }
1934
+ if (typeof IntersectionObserver !== "undefined") { observer = new IntersectionObserver((entries) => { if (entries.some((entry) => entry.isIntersecting)) load(); }, { rootMargin: element.dataset.gRootMargin || "200px" }); observer.observe(sentinel); }
1935
+ element.addEventListener("click", click);
1936
+ return { load, complete, isLoading: () => loading, destroy: () => { observer?.disconnect(); element.removeEventListener("click", click); } };
1937
+ }
1938
+
1939
+ function createSwipeActions(element) {
1940
+ let startX = 0; let startY = 0; let tracking = false;
1941
+ function set(revealed, reason = "api") { element.classList.toggle("is-revealed", revealed); element.querySelector("[data-g-swipe-toggle]")?.setAttribute("aria-expanded", String(revealed)); emit(element, "swipechange", { revealed, reason }); }
1942
+ function begin(event) { if (event.pointerType === "mouse" && event.button !== 0) return; startX = event.clientX; startY = event.clientY; tracking = true; element.setPointerCapture?.(event.pointerId); }
1943
+ function end(event) { if (!tracking) return; tracking = false; const dx = event.clientX - startX; const dy = event.clientY - startY; if (Math.abs(dx) < 36 || Math.abs(dx) < Math.abs(dy)) return; const rtl = getComputedStyle(element).direction === "rtl"; set(rtl ? dx > 0 : dx < 0, "gesture"); }
1944
+ function click(event) { if (event.target.closest("[data-g-swipe-toggle]")) set(!element.classList.contains("is-revealed"), "button"); else if (event.target.closest("[data-g-swipe-close]")) set(false, "action"); }
1945
+ function keydown(event) { if (event.key === "Escape") set(false, "keyboard"); }
1946
+ element.addEventListener("pointerdown", begin); element.addEventListener("pointerup", end); element.addEventListener("pointercancel", () => { tracking = false; }); element.addEventListener("click", click); element.addEventListener("keydown", keydown);
1947
+ return { reveal: () => set(true), close: () => set(false), toggle: () => set(!element.classList.contains("is-revealed")), destroy: () => { element.removeEventListener("pointerdown", begin); element.removeEventListener("pointerup", end); element.removeEventListener("click", click); element.removeEventListener("keydown", keydown); } };
1948
+ }
1949
+
1950
+ function createWheelPicker(element) {
1951
+ const columns = [...element.querySelectorAll("[data-g-wheel-column]")]; const output = element.querySelector("[data-g-wheel-output]"); const timers = new WeakMap();
1952
+ function options(column) { return [...column.querySelectorAll("[data-g-wheel-option]")]; }
1953
+ function select(option, reason = "option") { const column = option.closest("[data-g-wheel-column]"); if (!column) return; options(column).forEach((item) => item.setAttribute("aria-selected", String(item === option))); option.scrollIntoView({ block: "center", behavior: reason === "scroll" ? "auto" : "smooth" }); const values = columns.map((item) => item.querySelector('[data-g-wheel-option][aria-selected="true"]')?.dataset.gValue || ""); if (output) output.value = JSON.stringify(values); emit(element, "wheelchange", { values, reason }); }
1954
+ function click(event) { const option = event.target.closest("[data-g-wheel-option]"); if (option) select(option); }
1955
+ function keydown(event) { const option = event.target.closest("[data-g-wheel-option]"); if (!option || !["ArrowUp", "ArrowDown", "Home", "End"].includes(event.key)) return; event.preventDefault(); const list = options(option.closest("[data-g-wheel-column]")); const index = list.indexOf(option); const next = event.key === "Home" ? list[0] : event.key === "End" ? list.at(-1) : list[index + (event.key === "ArrowDown" ? 1 : -1)] || option; next.focus(); select(next, "keyboard"); }
1956
+ function scroll(event) { const column = event.currentTarget; clearTimeout(timers.get(column)); timers.set(column, setTimeout(() => { const center = column.getBoundingClientRect().top + column.clientHeight / 2; const nearest = options(column).sort((a, b) => Math.abs(a.getBoundingClientRect().top + a.clientHeight / 2 - center) - Math.abs(b.getBoundingClientRect().top + b.clientHeight / 2 - center))[0]; if (nearest) select(nearest, "scroll"); }, 90)); }
1957
+ columns.forEach((column) => { column.addEventListener("scroll", scroll, { passive: true }); const selected = column.querySelector('[data-g-wheel-option][aria-selected="true"]') || options(column)[0]; if (selected) select(selected, "init"); }); element.addEventListener("click", click); element.addEventListener("keydown", keydown);
1958
+ return { select, values: () => columns.map((column) => column.querySelector('[data-g-wheel-option][aria-selected="true"]')?.dataset.gValue || ""), destroy: () => { columns.forEach((column) => { clearTimeout(timers.get(column)); column.removeEventListener("scroll", scroll); }); element.removeEventListener("click", click); element.removeEventListener("keydown", keydown); } };
1959
+ }
1960
+
1961
+ function createAIComposer(element) {
1962
+ const input = element.querySelector("[data-g-composer-input], .g-composer-input");
1963
+ const send = element.querySelector("[data-g-composer-send]");
1964
+ const stop = element.querySelector("[data-g-composer-stop]");
1965
+ const status = element.querySelector("[data-g-composer-status]");
1966
+ if (!input) return null;
1967
+ let streaming = element.classList.contains("is-streaming");
1968
+ function resize() { input.style.height = "auto"; input.style.height = `${Math.min(input.scrollHeight, Number(element.dataset.gMaxHeight || 224))}px`; }
1969
+ function sync() { const empty = !input.value.trim(); if (send) send.disabled = empty || streaming; if (stop) stop.hidden = !streaming; element.classList.toggle("is-empty", empty); if (status && !streaming) status.textContent = empty ? element.dataset.gEmptyLabel || "输入消息,Enter 发送,Shift+Enter 换行" : element.dataset.gReadyLabel || "已准备发送"; resize(); }
1970
+ function setStreaming(value, reason = "api") { streaming = Boolean(value); element.classList.toggle("is-streaming", streaming); element.setAttribute("aria-busy", String(streaming)); if (status) status.textContent = streaming ? element.dataset.gStreamingLabel || "正在生成;可停止" : element.dataset.gReadyLabel || "已准备发送"; sync(); emit(element, "composerstate", { streaming, reason }); }
1971
+ function submit(reason = "api") { const value = input.value.trim(); if (!value || streaming || !emit(element, "beforepromptsubmit", { value, reason, input })) return; emit(element, "promptsubmit", { value, reason, input }); if (element.dataset.gClearOnSend !== "false") input.value = ""; sync(); }
1972
+ function stopGeneration(reason = "api") { if (!streaming || !emit(element, "beforepromptstop", { reason })) return; setStreaming(false, reason); emit(element, "promptstop", { reason }); }
1973
+ function keydown(event) { if (event.key === "Enter" && !event.shiftKey && !event.altKey && !event.isComposing) { event.preventDefault(); submit("keyboard"); } }
1974
+ function click(event) { if (event.target.closest("[data-g-composer-send]")) submit("button"); else if (event.target.closest("[data-g-composer-stop]")) stopGeneration("button"); else { const remove = event.target.closest("[data-g-attachment-remove]"); if (remove) { const attachment = remove.closest(".g-attachment"); if (attachment && emit(element, "attachmentremove", { attachment })) attachment.remove(); } } }
1975
+ input.addEventListener("input", sync); input.addEventListener("keydown", keydown); element.addEventListener("click", click); sync();
1976
+ return { submit, stop: stopGeneration, setStreaming, value: () => input.value, focus: () => input.focus(), destroy: () => { input.removeEventListener("input", sync); input.removeEventListener("keydown", keydown); element.removeEventListener("click", click); } };
1977
+ }
1978
+
1979
+ function createPromptFill(element) {
1980
+ function click(event) { const item = event.target.closest("[data-g-prompt-value]"); if (!item) return; const target = targetById(item.dataset.gPromptTarget || element.dataset.gPromptTarget); if (!target) return; const value = item.dataset.gPromptValue || item.dataset.gValue || item.textContent.trim(); target.value = value; target.dispatchEvent(new Event("input", { bubbles: true })); target.focus(); emit(element, "promptfill", { value, item, target }); }
1981
+ element.addEventListener("click", click);
1982
+ return { fill: (item) => click({ target: item }), destroy: () => element.removeEventListener("click", click) };
1983
+ }
1984
+
1985
+ function createAIApproval(element) {
1986
+ const choices = [...element.querySelectorAll("[data-g-approval-choice]")];
1987
+ function choose(choice, reason = "api") { if (!choice || choice.disabled) return; const value = choice.dataset.gApprovalChoice; if (!emit(element, "beforeapproval", { value, choice, reason })) return; element.dataset.gApprovalState = value; ["approved", "rejected", "cancelled"].forEach((state) => element.classList.toggle(`is-${state}`, value === state)); choices.forEach((item) => { item.setAttribute("aria-pressed", String(item === choice)); if (element.dataset.gKeepEnabled !== "true") item.disabled = true; }); emit(element, "approval", { value, choice, reason }); }
1988
+ function click(event) { choose(event.target.closest("[data-g-approval-choice]"), "pointer"); }
1989
+ element.addEventListener("click", click);
1990
+ return { choose, reset: () => { delete element.dataset.gApprovalState; element.classList.remove("is-approved", "is-rejected", "is-cancelled"); choices.forEach((item) => { item.disabled = false; item.setAttribute("aria-pressed", "false"); }); }, destroy: () => element.removeEventListener("click", click) };
1991
+ }
1992
+
1993
+ function createAIFeedback(element) {
1994
+ const options = [...element.querySelectorAll("[data-g-feedback-value]")];
1995
+ const detail = element.querySelector("[data-g-feedback-detail]");
1996
+ const comment = element.querySelector("[data-g-feedback-comment]");
1997
+ const output = element.querySelector("[data-g-feedback-output]");
1998
+ let value = output?.value || "";
1999
+ function select(option, reason = "api") { if (!option) return; value = option.dataset.gFeedbackValue; options.forEach((item) => item.setAttribute("aria-pressed", String(item === option))); if (output) output.value = value; if (detail) detail.hidden = false; emit(element, "feedbackchange", { value, option, reason }); }
2000
+ function submit(reason = "api") { if (!value) return; emit(element, "feedbacksubmit", { value, comment: comment?.value || "", reason }); element.classList.add("is-submitted"); }
2001
+ function click(event) { const option = event.target.closest("[data-g-feedback-value]"); if (option) select(option, "pointer"); else if (event.target.closest("[data-g-feedback-submit]")) submit("button"); }
2002
+ element.addEventListener("click", click); if (value) select(options.find((item) => item.dataset.gFeedbackValue === value), "init");
2003
+ return { select, submit, value: () => value, destroy: () => element.removeEventListener("click", click) };
2004
+ }
2005
+
2006
+ function createShortcutRecorder(element) {
2007
+ const control = element.querySelector("[data-g-shortcut-control]") || element;
2008
+ const output = element.querySelector("[data-g-shortcut-output]");
2009
+ const display = element.querySelector("[data-g-shortcut-display]");
2010
+ const status = element.querySelector("[data-g-shortcut-status]");
2011
+ let recording = false; let value = output?.value || element.dataset.gValue || "";
2012
+ const modifierOrder = ["Control", "Alt", "Shift", "Meta"];
2013
+ function format(shortcut) { return shortcut ? shortcut.replaceAll("Control", navigator.platform.includes("Mac") ? "⌃" : "Ctrl").replaceAll("Meta", navigator.platform.includes("Mac") ? "⌘" : "Meta").replaceAll("Alt", navigator.platform.includes("Mac") ? "⌥" : "Alt").replaceAll("Shift", navigator.platform.includes("Mac") ? "⇧" : "Shift") : element.dataset.gEmptyLabel || "未设置"; }
2014
+ function sync(reason = "api") { if (output) output.value = value; if (display) display.textContent = format(value); element.dataset.gValue = value; emit(element, "shortcutchange", { value, reason }); }
2015
+ function stop(reason = "cancel") { recording = false; control.classList.remove("is-recording"); control.setAttribute("aria-pressed", "false"); if (status) status.textContent = reason === "recorded" ? element.dataset.gSavedLabel || "快捷键已记录" : element.dataset.gIdleLabel || "点击后按下组合键"; }
2016
+ function start() { recording = true; control.classList.add("is-recording"); control.setAttribute("aria-pressed", "true"); if (status) status.textContent = element.dataset.gRecordingLabel || "请按下快捷键;Esc 取消"; control.focus(); }
2017
+ function keydown(event) { if (!recording) return; event.preventDefault(); event.stopPropagation(); if (event.key === "Escape") return stop(); if (["Backspace", "Delete"].includes(event.key)) { value = ""; sync("clear"); return stop("recorded"); } if (["Control", "Alt", "Shift", "Meta"].includes(event.key)) return; const pressed = { Control: event.ctrlKey, Alt: event.altKey, Shift: event.shiftKey, Meta: event.metaKey }; const modifiers = modifierOrder.filter((name) => pressed[name]); const key = event.key.length === 1 ? event.key.toLocaleUpperCase() : event.key; value = [...modifiers, key].join("+"); sync("keyboard"); stop("recorded"); }
2018
+ function click(event) { if (event.target.closest("[data-g-shortcut-clear]")) { value = ""; sync("clear"); stop("recorded"); } else start(); }
2019
+ control.addEventListener("click", click); control.addEventListener("keydown", keydown); sync("init"); stop();
2020
+ return { start, stop, clear: () => { value = ""; sync("api"); }, value: () => value, destroy: () => { control.removeEventListener("click", click); control.removeEventListener("keydown", keydown); } };
2021
+ }
2022
+
2023
+ function createDesktopTabs(element) {
2024
+ const tabs = () => [...element.querySelectorAll("[data-g-desktop-tab]")];
2025
+ function select(tab, reason = "api", focus = false) { if (!tab || tab.getAttribute("aria-disabled") === "true") return; tabs().forEach((item) => { const active = item === tab; item.setAttribute("aria-selected", String(active)); item.classList.toggle("is-active", active); item.tabIndex = active ? 0 : -1; const panel = targetById(item.getAttribute("aria-controls")); if (panel) panel.hidden = !active; }); if (focus) tab.focus(); emit(element, "desktoptabchange", { tab, reason }); }
2026
+ function close(tab, reason = "api") { if (!tab || !emit(element, "beforetabclose", { tab, reason, dirty: tab.classList.contains("is-dirty") })) return; const list = tabs(); const index = list.indexOf(tab); const wasActive = tab.getAttribute("aria-selected") === "true" || tab.classList.contains("is-active"); const panel = targetById(tab.getAttribute("aria-controls")); panel?.remove(); tab.remove(); const remaining = tabs(); if (wasActive) select(remaining[Math.min(index, remaining.length - 1)], "close", true); emit(element, "desktoptabclose", { reason }); }
2027
+ function click(event) { const closeButton = event.target.closest("[data-g-desktop-tab-close]"); const tab = event.target.closest("[data-g-desktop-tab]"); if (closeButton) { event.stopPropagation(); close(closeButton.closest("[data-g-desktop-tab]"), "button"); } else if (tab) select(tab, "pointer"); }
2028
+ function pointerdown(event) { if (event.button === 1) { const tab = event.target.closest("[data-g-desktop-tab]"); if (tab) { event.preventDefault(); close(tab, "middle-click"); } } }
2029
+ function keydown(event) { const list = tabs(); const current = event.target.closest("[data-g-desktop-tab]") || list.find((tab) => tab.getAttribute("aria-selected") === "true"); if (!current) return; const index = list.indexOf(current); if ((event.ctrlKey || event.metaKey) && event.key === "Tab") { event.preventDefault(); select(list[(index + (event.shiftKey ? -1 : 1) + list.length) % list.length], "shortcut", true); } else if ((event.ctrlKey || event.metaKey) && event.key.toLocaleLowerCase() === "w") { event.preventDefault(); close(current, "shortcut"); } else if (["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) { event.preventDefault(); const next = event.key === "Home" ? list[0] : event.key === "End" ? list.at(-1) : list[(index + (event.key === "ArrowRight" ? 1 : -1) + list.length) % list.length]; select(next, "keyboard", true); } }
2030
+ element.setAttribute("role", element.getAttribute("role") || "tablist"); tabs().forEach((tab) => tab.setAttribute("role", tab.getAttribute("role") || "tab")); element.addEventListener("click", click); element.addEventListener("pointerdown", pointerdown); element.addEventListener("keydown", keydown); const initial = tabs().find((tab) => tab.getAttribute("aria-selected") === "true") || tabs()[0]; if (initial) select(initial, "init");
2031
+ return { select, close, tabs, destroy: () => { element.removeEventListener("click", click); element.removeEventListener("pointerdown", pointerdown); element.removeEventListener("keydown", keydown); } };
2032
+ }
2033
+
2034
+ function createNativeFilePicker(element) {
2035
+ const input = element.querySelector("input[type='file']"); const trigger = element.querySelector("[data-g-native-file-trigger]"); const value = element.querySelector("[data-g-native-file-value]"); if (!input || !trigger) return null;
2036
+ function sync(reason = "change") { const files = [...input.files]; if (value) value.textContent = files.length ? files.map((file) => file.name).join(", ") : element.dataset.gEmptyLabel || "未选择文件"; emit(element, "nativefiles", { files, input, reason }); }
2037
+ function open() { if (!emit(element, "beforefilepicker", { input })) return; input.click(); }
2038
+ function change() { sync(); }
2039
+ trigger.addEventListener("click", open); input.addEventListener("change", change); sync("init");
2040
+ return { open, files: () => [...input.files], clear: () => { input.value = ""; sync("clear"); }, destroy: () => { trigger.removeEventListener("click", open); input.removeEventListener("change", change); } };
2041
+ }
2042
+
2043
+ function createWindowSwitcher(element) {
2044
+ const panel = element.querySelector(".g-desktop-window-switcher-panel") || element; const items = () => [...element.querySelectorAll("[data-g-window-item]")]; let open = !element.hidden; let trigger = null;
2045
+ function select(item, reason = "api") { if (!item) return; items().forEach((node) => node.setAttribute("aria-selected", String(node === item))); item.focus(); emit(element, "windowselect", { item, value: item.dataset.gValue, reason }); }
2046
+ function show(source = document.activeElement) { if (open) return; open = true; trigger = source; element.hidden = false; element.setAttribute("aria-hidden", "false"); select(items().find((item) => item.getAttribute("aria-selected") === "true") || items()[0], "open"); emit(element, "open", { source }); }
2047
+ function hide(reason = "api") { if (!open) return; open = false; element.hidden = true; element.setAttribute("aria-hidden", "true"); trigger?.focus?.(); emit(element, "close", { reason }); }
2048
+ function keydown(event) { const list = items(); const current = list.indexOf(document.activeElement); if ((event.ctrlKey || event.metaKey) && event.key === "`") { event.preventDefault(); open ? select(list[(current + 1) % list.length], "shortcut") : show(); } else if (!open) return; else if (event.key === "Escape") { event.preventDefault(); hide("escape"); } else if (["ArrowDown", "ArrowRight", "ArrowUp", "ArrowLeft"].includes(event.key)) { event.preventDefault(); select(list[(current + (["ArrowDown", "ArrowRight"].includes(event.key) ? 1 : -1) + list.length) % list.length], "keyboard"); } else if (event.key === "Enter") { event.preventDefault(); emit(element, "windowactivate", { item: document.activeElement, value: document.activeElement?.dataset.gValue }); hide("activate"); } }
2049
+ function click(event) { const item = event.target.closest("[data-g-window-item]"); if (item) { select(item, "pointer"); emit(element, "windowactivate", { item, value: item.dataset.gValue }); hide("activate"); } else if (event.target === element) hide("backdrop"); }
2050
+ if (open) { open = false; queueMicrotask(show); } else { element.hidden = true; element.setAttribute("aria-hidden", "true"); } document.addEventListener("keydown", keydown); element.addEventListener("click", click); panel.tabIndex = panel.tabIndex < 0 ? -1 : panel.tabIndex;
2051
+ return { open: show, close: hide, select, isOpen: () => open, destroy: () => { document.removeEventListener("keydown", keydown); element.removeEventListener("click", click); element.hidden = true; } };
2052
+ }
2053
+
2054
+ [
2055
+ ["dialog", (element) => createOverlay(element, "dialog")],
2056
+ ["drawer", (element) => createOverlay(element, "drawer")],
2057
+ ["mobile-sheet", (element) => createOverlay(element, "mobile-sheet")],
2058
+ ["dropdown", createDropdown], ["tabs", createTabs], ["accordion", createAccordion],
2059
+ ["auto-resize", createAutoResize], ["combobox", createCombobox],
2060
+ ["password-toggle", createPasswordToggle], ["clear-input", createClearInput],
2061
+ ["otp-input", createOtpInput], ["password-strength", createPasswordStrength], ["auth-timer", createAuthTimer],
2062
+ ["quantity-stepper", createQuantityStepper], ["sku-selector", createSkuSelector], ["cart", createCart], ["coupon", createCoupon],
2063
+ ["pull-refresh", createPullRefresh], ["infinite-load", createInfiniteLoad], ["swipe-actions", createSwipeActions], ["wheel-picker", createWheelPicker],
2064
+ ["ai-composer", createAIComposer], ["prompt-fill", createPromptFill], ["ai-approval", createAIApproval], ["ai-feedback", createAIFeedback],
2065
+ ["shortcut-recorder", createShortcutRecorder], ["desktop-tabs", createDesktopTabs], ["native-file-picker", createNativeFilePicker], ["window-switcher", createWindowSwitcher],
2066
+ ["character-count", createCharacterCount], ["conditional-field", createConditionalField],
2067
+ ["repeatable-field", createRepeatableField],
2068
+ ["tooltip", (element) => createFloating(element, "tooltip")], ["popover", (element) => createFloating(element, "popover")],
2069
+ ["tour", createTour],
2070
+ ["carousel", createCarousel], ["split-pane", createSplitter], ["tree", createTree],
2071
+ ["data-grid", createDataGrid], ["table-sort", createTableSort], ["row-select", createRowSelect],
2072
+ ["row-disclosure", createRowDisclosure], ["column-toggle", createColumnToggle],
2073
+ ["data-filter", createDataFilter], ["data-view", createDataView],
2074
+ ["transfer", createTransfer], ["picker", createPicker], ["cascader", createCascader],
2075
+ ["saved-choice", createSavedChoice], ["builder-list", createBuilderList],
2076
+ ["toast", createToast], ["copy", createCopy],
2077
+ ["fullscreen", createFullscreen], ["scroll-top", createScrollTop], ["dropzone", createDropzone],
2078
+ ["nav-toggle", createNavToggle], ["roving-nav", createRovingNav], ["context-menu", createContextMenu],
2079
+ ["scrollspy", createScrollspy], ["jump-nav", createJumpNav],
2080
+ ["upload-manager", createUploadManager], ["file-browser", createFileBrowser],
2081
+ ["editor-shell", createEditorShell], ["revision-compare", createRevisionCompare], ["autosave", createAutosave],
2082
+ ["command-palette", createCommandPalette]
2083
+ ].forEach(([name, factory]) => register(name, factory));
2084
+
2085
+ function initElement(element, name) {
2086
+ const store = storeFor(element);
2087
+ if (store.has(name)) return store.get(name);
2088
+ const factory = registry.get(name);
2089
+ if (!factory) return null;
2090
+ const instance = factory(element);
2091
+ if (instance) { store.set(name, instance); emit(element, "init", { name, instance }); }
2092
+ return instance;
2093
+ }
2094
+
2095
+ function init(root = document) {
2096
+ for (const [name] of registry) {
2097
+ const selector = `[data-g-${name}]`;
2098
+ if (root instanceof Element && root.matches(selector)) initElement(root, name);
2099
+ root.querySelectorAll?.(selector).forEach((element) => initElement(element, name));
2100
+ }
2101
+ return Gardener;
2102
+ }
2103
+
2104
+ function getInstance(elementOrSelector, name) {
2105
+ const element = typeof elementOrSelector === "string" ? document.querySelector(elementOrSelector) : elementOrSelector;
2106
+ const store = element ? instanceStores.get(element) : null;
2107
+ if (!store) return null;
2108
+ return name ? store.get(name) || null : store.size === 1 ? store.values().next().value : Object.fromEntries(store);
2109
+ }
2110
+
2111
+ function destroy(root = document) {
2112
+ const candidates = [];
2113
+ if (root instanceof Element) candidates.push(root);
2114
+ root.querySelectorAll?.("*").forEach((element) => candidates.push(element));
2115
+ candidates.forEach((element) => {
2116
+ const store = instanceStores.get(element);
2117
+ if (!store) return;
2118
+ store.forEach((instance) => instance?.destroy?.());
2119
+ instanceStores.delete(element);
2120
+ });
2121
+ }
2122
+
2123
+ function toast(options = {}) {
2124
+ let region = document.querySelector(".g-toast-region");
2125
+ if (!region) {
2126
+ region = document.createElement("div");
2127
+ region.className = "g-toast-region";
2128
+ region.setAttribute("aria-live", options.tone === "danger" ? "assertive" : "polite");
2129
+ document.body.append(region);
2130
+ }
2131
+ const element = document.createElement("div");
2132
+ element.className = `g-toast${options.tone ? ` g-toast-${options.tone}` : ""}`;
2133
+ element.setAttribute("role", options.tone === "danger" ? "alert" : "status");
2134
+ element.dataset.gToast = "";
2135
+ if (options.timeout != null) element.dataset.gTimeout = String(options.timeout);
2136
+ element.innerHTML = `<div class="g-toast-content"><div class="g-toast-title"></div><div class="g-toast-message"></div></div><button class="g-btn g-btn-ghost g-btn-icon" data-g-dismiss aria-label="Close">×</button>`;
2137
+ element.querySelector(".g-toast-title").textContent = options.title || "";
2138
+ element.querySelector(".g-toast-message").textContent = options.message || "";
2139
+ region.append(element);
2140
+ init(element);
2141
+ return element;
2142
+ }
2143
+
2144
+ function delegateClick(event) {
2145
+ const commandTrigger = event.target.closest("[data-g-command-trigger]");
2146
+ if (commandTrigger) {
2147
+ const target = targetById(commandTrigger.dataset.gCommandTrigger);
2148
+ if (target) {
2149
+ commandTrigger.setAttribute("aria-haspopup", "dialog");
2150
+ commandTrigger.setAttribute("aria-controls", target.id);
2151
+ init(target);
2152
+ getInstance(target, "command-palette")?.toggle(commandTrigger);
2153
+ }
2154
+ }
2155
+ const opener = event.target.closest("[data-g-dialog-open], [data-g-drawer-open], [data-g-mobile-sheet-open]");
2156
+ if (opener) {
2157
+ const id = opener.dataset.gDialogOpen || opener.dataset.gDrawerOpen || opener.dataset.gMobileSheetOpen;
2158
+ const target = targetById(id);
2159
+ if (target) {
2160
+ init(target);
2161
+ getInstance(target, opener.dataset.gDialogOpen ? "dialog" : opener.dataset.gDrawerOpen ? "drawer" : "mobile-sheet")?.open(opener);
2162
+ }
2163
+ }
2164
+ const tourOpener = event.target.closest("[data-g-tour-open]");
2165
+ if (tourOpener) {
2166
+ const target = targetById(tourOpener.dataset.gTourOpen);
2167
+ if (target) {
2168
+ tourOpener.setAttribute("aria-haspopup", "dialog");
2169
+ tourOpener.setAttribute("aria-controls", target.id);
2170
+ init(target);
2171
+ getInstance(target, "tour")?.open(tourOpener);
2172
+ }
2173
+ }
2174
+ const toggle = event.target.closest("[data-g-toggle]");
2175
+ if (toggle) {
2176
+ const target = targetById(toggle.dataset.gToggle);
2177
+ if (target) {
2178
+ const expanded = toggle.getAttribute("aria-expanded") !== "true";
2179
+ toggle.setAttribute("aria-expanded", String(expanded));
2180
+ target.hidden = !expanded;
2181
+ target.classList.toggle("is-open", expanded);
2182
+ emit(target, "toggle", { expanded, trigger: toggle });
2183
+ }
2184
+ }
2185
+ const dismiss = event.target.closest("[data-g-dismiss]");
2186
+ if (dismiss) {
2187
+ const target = dismiss.closest(".g-alert, .g-toast, .g-banner, [data-g-dismissible]");
2188
+ const toastInstance = target ? getInstance(target, "toast") : null;
2189
+ if (toastInstance) toastInstance.dismiss("button");
2190
+ else if (target && emit(target, "dismiss", { reason: "button" })) target.remove();
2191
+ }
2192
+ }
2193
+
2194
+ let observer;
2195
+ function observe() {
2196
+ if (observer || typeof MutationObserver === "undefined") return;
2197
+ observer = new MutationObserver((records) => records.forEach((record) => {
2198
+ record.addedNodes.forEach((node) => { if (node instanceof Element) init(node); });
2199
+ record.removedNodes.forEach((node) => { if (node instanceof Element) destroy(node); });
2200
+ if (record.type === "attributes" && record.target instanceof Element && record.attributeName?.startsWith("data-g-")) {
2201
+ const name = record.attributeName.slice(7);
2202
+ if (registry.has(name) && record.target.hasAttribute(record.attributeName)) initElement(record.target, name);
2203
+ else if (registry.has(name)) {
2204
+ const store = instanceStores.get(record.target);
2205
+ store?.get(name)?.destroy?.();
2206
+ store?.delete(name);
2207
+ if (store?.size === 0) instanceStores.delete(record.target);
2208
+ }
2209
+ }
2210
+ }));
2211
+ observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true });
2212
+ }
2213
+
2214
+ const Gardener = Object.freeze({ version: "1.0.0", get behaviors() { return Object.freeze([...registry.keys()]); }, init, destroy, register, getInstance, emit, toast, observe });
2215
+
2216
+ if (typeof document !== "undefined") {
2217
+ document.addEventListener("click", delegateClick);
2218
+ const start = () => { init(); observe(); };
2219
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start, { once: true });
2220
+ else queueMicrotask(start);
2221
+ }
2222
+
2223
+ export { Gardener, init, destroy, register, getInstance, emit, toast, observe };
2224
+ export default Gardener;