@icen.ai/ui 0.2.0 → 0.3.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 (103) hide show
  1. package/README.md +29 -16
  2. package/dist/base.css +389 -48
  3. package/dist/behaviors/accordion.mjs +20 -0
  4. package/dist/behaviors/carousel.mjs +30 -1
  5. package/dist/behaviors/charts.mjs +168 -0
  6. package/dist/behaviors/copy.mjs +4 -3
  7. package/dist/behaviors/datatable.mjs +748 -0
  8. package/dist/behaviors/input.mjs +20 -1
  9. package/dist/behaviors/modal.mjs +15 -1
  10. package/dist/behaviors/nav.mjs +31 -4
  11. package/dist/behaviors/sidebar.mjs +12 -0
  12. package/dist/behaviors/slider.mjs +35 -3
  13. package/dist/behaviors/tabs.mjs +100 -9
  14. package/dist/behaviors/tag-input.mjs +11 -0
  15. package/dist/behaviors/theme.mjs +26 -0
  16. package/dist/behaviors/toast.mjs +83 -15
  17. package/dist/behaviors/tree.mjs +57 -1
  18. package/dist/behaviors/upload.mjs +2 -3
  19. package/dist/cli.mjs +74 -0
  20. package/dist/components/accordion.mjs +3 -0
  21. package/dist/components/alert.mjs +2 -0
  22. package/dist/components/avatar.mjs +2 -0
  23. package/dist/components/breadcrumb.css +91 -18
  24. package/dist/components/breadcrumb.mjs +2 -0
  25. package/dist/components/btn.css +174 -14
  26. package/dist/components/btn.mjs +2 -0
  27. package/dist/components/card.css +118 -18
  28. package/dist/components/card.mjs +2 -0
  29. package/dist/components/carousel.mjs +3 -0
  30. package/dist/components/charts.css +225 -33
  31. package/dist/components/charts.mjs +3 -0
  32. package/dist/components/content.css +256 -74
  33. package/dist/components/context-menu.mjs +3 -0
  34. package/dist/components/datatable.css +343 -0
  35. package/dist/components/datatable.mjs +4 -0
  36. package/dist/components/desc.mjs +2 -0
  37. package/dist/components/dropdown.mjs +3 -0
  38. package/dist/components/empty.css +50 -10
  39. package/dist/components/empty.mjs +2 -0
  40. package/dist/components/feedback.css +158 -47
  41. package/dist/components/form.css +139 -22
  42. package/dist/components/input.css +165 -45
  43. package/dist/components/input.mjs +3 -0
  44. package/dist/components/kbd.mjs +2 -0
  45. package/dist/components/layout.mjs +2 -0
  46. package/dist/components/list.mjs +2 -0
  47. package/dist/components/media-card.mjs +2 -0
  48. package/dist/components/media.css +187 -40
  49. package/dist/components/menu.css +159 -32
  50. package/dist/components/modal.css +140 -34
  51. package/dist/components/modal.mjs +3 -0
  52. package/dist/components/nav.css +134 -32
  53. package/dist/components/nav.mjs +3 -0
  54. package/dist/components/pagination.css +105 -19
  55. package/dist/components/pagination.mjs +2 -0
  56. package/dist/components/panel.css +61 -6
  57. package/dist/components/panel.mjs +2 -0
  58. package/dist/components/pill.css +74 -14
  59. package/dist/components/pill.mjs +2 -0
  60. package/dist/components/popover.css +109 -7
  61. package/dist/components/popover.mjs +3 -0
  62. package/dist/components/progress.mjs +2 -0
  63. package/dist/components/rating.mjs +2 -0
  64. package/dist/components/result.mjs +2 -0
  65. package/dist/components/segmented.css +199 -50
  66. package/dist/components/segmented.mjs +2 -0
  67. package/dist/components/select.css +140 -39
  68. package/dist/components/select.mjs +3 -0
  69. package/dist/components/sidebar.css +162 -34
  70. package/dist/components/sidebar.mjs +3 -0
  71. package/dist/components/skeleton.mjs +2 -0
  72. package/dist/components/slider.css +169 -31
  73. package/dist/components/slider.mjs +3 -0
  74. package/dist/components/spinner.mjs +2 -0
  75. package/dist/components/stat.css +103 -8
  76. package/dist/components/stat.mjs +2 -0
  77. package/dist/components/steps.css +105 -31
  78. package/dist/components/steps.mjs +2 -0
  79. package/dist/components/switch.css +171 -54
  80. package/dist/components/switch.mjs +2 -0
  81. package/dist/components/table.css +114 -12
  82. package/dist/components/table.mjs +2 -0
  83. package/dist/components/tabs.css +104 -17
  84. package/dist/components/tabs.mjs +3 -0
  85. package/dist/components/tag-input.css +119 -23
  86. package/dist/components/tag-input.mjs +3 -0
  87. package/dist/components/timeline.mjs +2 -0
  88. package/dist/components/toast.css +85 -12
  89. package/dist/components/toast.mjs +3 -0
  90. package/dist/components/tooltip.css +86 -26
  91. package/dist/components/tooltip.mjs +2 -0
  92. package/dist/components/tree.mjs +3 -0
  93. package/dist/components/upload.css +144 -17
  94. package/dist/components/upload.mjs +3 -0
  95. package/dist/index.mjs +629 -48
  96. package/dist/registry.json +197 -2
  97. package/dist/retro-effects.css +264 -0
  98. package/dist/tokens/colors.css +18 -90
  99. package/dist/tokens/retro-effects.css +264 -0
  100. package/dist/tokens/style-profiles.css +47 -47
  101. package/dist/tokens.css +65 -137
  102. package/dist/ui.css +4861 -1019
  103. package/package.json +9 -1
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // package.json
2
2
  var package_default = {
3
3
  name: "@icen.ai/ui",
4
- version: "0.2.0",
4
+ version: "0.3.0",
5
5
  description: "Icen Design System \u2014 \u8BBE\u8BA1 tokens\uFF086 \u9884\u8BBE \xD7 \u660E\u6697\uFF09\u3001\u65E0\u6846\u67B6\u7EC4\u4EF6 CSS \u4E0E\u884C\u4E3A JS",
6
6
  repository: {
7
7
  type: "git",
@@ -12,13 +12,21 @@ var package_default = {
12
12
  exports: {
13
13
  ".": "./dist/index.mjs",
14
14
  "./behaviors/*": "./dist/behaviors/*.mjs",
15
+ "./kit/*": "./dist/components/*.mjs",
15
16
  "./tokens.css": "./dist/tokens.css",
16
17
  "./ui.css": "./dist/ui.css",
17
18
  "./base.css": "./dist/base.css",
19
+ "./retro-effects.css": "./dist/retro-effects.css",
18
20
  "./tokens/*": "./dist/tokens/*",
19
21
  "./components/*": "./dist/components/*",
20
22
  "./registry.json": "./dist/registry.json"
21
23
  },
24
+ bin: {
25
+ "icen-ui": "./dist/cli.mjs"
26
+ },
27
+ sideEffects: [
28
+ "**/*.css"
29
+ ],
22
30
  files: [
23
31
  "dist"
24
32
  ],
@@ -104,64 +112,188 @@ function toggleDark() {
104
112
  function initTheme() {
105
113
  const t = getTheme();
106
114
  applyTheme(t);
115
+ if (isBrowser() && typeof window.matchMedia === "function" && !hasPersistedPreference()) {
116
+ let lastDark = t.dark;
117
+ try {
118
+ const mql = window.matchMedia("(prefers-color-scheme: dark)");
119
+ const onChange = (ev) => {
120
+ if (hasPersistedPreference()) return;
121
+ if (ev.matches === lastDark) return;
122
+ lastDark = ev.matches;
123
+ applyTheme({ ...getTheme(), dark: ev.matches });
124
+ };
125
+ if (typeof mql.addEventListener === "function") {
126
+ mql.addEventListener("change", onChange);
127
+ } else {
128
+ mql.addListener(onChange);
129
+ }
130
+ } catch {
131
+ }
132
+ }
107
133
  return t;
108
134
  }
135
+ function hasPersistedPreference() {
136
+ if (!isBrowser()) return false;
137
+ try {
138
+ return window.localStorage.getItem(STORAGE_KEY) !== null;
139
+ } catch {
140
+ return false;
141
+ }
142
+ }
109
143
 
110
144
  // src/behaviors/toast.ts
145
+ var DEFAULT_MS = 2600;
146
+ var ACTION_MS = 6e3;
147
+ var maxStack = 5;
148
+ function isBrowser2() {
149
+ return typeof document !== "undefined";
150
+ }
111
151
  function ensureContainer() {
112
152
  const existing = document.body.querySelector(".toast-container");
113
153
  if (existing) return existing;
114
154
  const el = document.createElement("div");
115
155
  el.className = "toast-container";
156
+ el.setAttribute("role", "status");
157
+ el.setAttribute("aria-live", "polite");
158
+ el.setAttribute("aria-atomic", "false");
116
159
  document.body.appendChild(el);
117
160
  return el;
118
161
  }
119
- function show(message, kind = "info", ms = 2600) {
120
- if (typeof document === "undefined") return;
162
+ function trimStack(container) {
163
+ for (; ; ) {
164
+ const items = Array.from(container.querySelectorAll(":scope > .toast"));
165
+ if (items.length <= maxStack) return;
166
+ const oldest = items[0];
167
+ if (!oldest) return;
168
+ oldest.classList.add("leaving");
169
+ let done = false;
170
+ const finish = () => {
171
+ if (done) return;
172
+ done = true;
173
+ oldest.remove();
174
+ };
175
+ oldest.addEventListener("transitionend", finish, { once: true });
176
+ window.setTimeout(finish, 400);
177
+ }
178
+ }
179
+ function show(message, kind = "info", opts = {}) {
180
+ if (!isBrowser2()) return;
181
+ const { ms, actionLabel, onAction } = opts;
182
+ const container = ensureContainer();
121
183
  const el = document.createElement("div");
122
184
  el.className = kind === "info" ? "toast" : `toast ${kind}`;
123
- el.textContent = message;
124
- ensureContainer().appendChild(el);
185
+ el.setAttribute("role", kind === "err" ? "alert" : "status");
186
+ const msgNode = document.createElement("span");
187
+ msgNode.className = "toast-msg";
188
+ msgNode.textContent = message;
189
+ el.appendChild(msgNode);
190
+ let actionBtn = null;
191
+ if (actionLabel) {
192
+ actionBtn = document.createElement("button");
193
+ actionBtn.type = "button";
194
+ actionBtn.className = "toast-action";
195
+ actionBtn.textContent = actionLabel;
196
+ el.appendChild(actionBtn);
197
+ }
198
+ container.appendChild(el);
199
+ trimStack(container);
125
200
  let removed = false;
126
201
  const remove = () => {
127
202
  if (removed) return;
128
203
  removed = true;
129
- el.remove();
130
- };
131
- window.setTimeout(() => {
132
204
  el.classList.add("leaving");
133
- el.addEventListener("transitionend", remove, { once: true });
134
- window.setTimeout(remove, 400);
135
- }, ms);
136
- }
137
- var toast = Object.assign(show, {
138
- ok: (message, ms) => show(message, "ok", ms),
139
- err: (message, ms) => show(message, "err", ms),
140
- warn: (message, ms) => show(message, "warn", ms)
141
- });
205
+ let done = false;
206
+ const finish = () => {
207
+ if (done) return;
208
+ done = true;
209
+ el.remove();
210
+ };
211
+ el.addEventListener("transitionend", finish, { once: true });
212
+ window.setTimeout(finish, 400);
213
+ };
214
+ const duration = ms ?? (actionLabel ? ACTION_MS : DEFAULT_MS);
215
+ const timer = window.setTimeout(remove, duration);
216
+ if (actionBtn && onAction) {
217
+ actionBtn.addEventListener("click", () => {
218
+ window.clearTimeout(timer);
219
+ const ret = onAction();
220
+ if (ret === false) return;
221
+ remove();
222
+ });
223
+ }
224
+ }
225
+ var toast = Object.assign(
226
+ (message, kind = "info", ms) => show(message, kind, { ms }),
227
+ {
228
+ ok: (message, ms) => show(message, "ok", { ms }),
229
+ err: (message, ms) => show(message, "err", { ms }),
230
+ warn: (message, ms) => show(message, "warn", { ms }),
231
+ info: (message, ms) => show(message, "info", { ms }),
232
+ action: (message, actionLabel, onAction, ms) => show(message, "info", { actionLabel, onAction, ms }),
233
+ dismiss: () => {
234
+ if (!isBrowser2()) return;
235
+ const c = document.body.querySelector(".toast-container");
236
+ if (!c) return;
237
+ c.querySelectorAll(".toast").forEach((t) => t.remove());
238
+ },
239
+ config: (opts) => {
240
+ if (typeof opts.maxStack === "number" && opts.maxStack > 0) maxStack = opts.maxStack;
241
+ }
242
+ }
243
+ );
142
244
 
143
245
  // src/behaviors/tabs.ts
246
+ function tabButtons(container) {
247
+ return Array.from(container.querySelectorAll("[data-tab]"));
248
+ }
144
249
  function tabNames(container) {
145
- return Array.from(container.querySelectorAll("[data-tab]")).map((t) => t.getAttribute("data-tab")).filter((n) => n !== null);
250
+ return tabButtons(container).map((t) => t.getAttribute("data-tab")).filter((n) => n !== null);
146
251
  }
147
252
  function currentHash() {
148
253
  return window.location.hash.replace(/^#/, "");
149
254
  }
150
255
  function activate(container, name, writeHash) {
256
+ const buttons = tabButtons(container);
151
257
  let matched = false;
152
- container.querySelectorAll("[data-tab]").forEach((t) => {
258
+ buttons.forEach((t) => {
153
259
  const on = t.getAttribute("data-tab") === name;
154
260
  if (on) matched = true;
155
261
  t.classList.toggle("active", on);
262
+ t.setAttribute("aria-selected", String(on));
263
+ t.tabIndex = on ? 0 : -1;
156
264
  });
157
265
  if (!matched) return;
158
266
  container.querySelectorAll("[data-tab-panel]").forEach((p) => {
159
- p.hidden = p.getAttribute("data-tab-panel") !== name;
267
+ const isOn = p.getAttribute("data-tab-panel") === name;
268
+ p.hidden = !isOn;
269
+ if (isOn && !p.getAttribute("aria-labelledby")) {
270
+ const btn = buttons.find((b) => b.getAttribute("data-tab") === name);
271
+ if (btn?.id) p.setAttribute("aria-labelledby", btn.id);
272
+ }
160
273
  });
161
274
  if (writeHash && typeof window !== "undefined") {
162
275
  window.history.replaceState(null, "", `#${name}`);
163
276
  }
164
277
  }
278
+ function isDisabled(btn) {
279
+ return btn.hasAttribute("disabled") || btn.getAttribute("aria-disabled") === "true" || btn.classList.contains("is-disabled");
280
+ }
281
+ function focusTab(container, idx) {
282
+ const buttons = tabButtons(container);
283
+ const target = buttons[idx];
284
+ if (!target || isDisabled(target)) return;
285
+ target.focus();
286
+ }
287
+ function nextEnabled(container, from, dir) {
288
+ const buttons = tabButtons(container);
289
+ if (buttons.length === 0) return -1;
290
+ let idx = from;
291
+ for (let i = 0; i < buttons.length; i++) {
292
+ idx = (idx + dir + buttons.length) % buttons.length;
293
+ if (!isDisabled(buttons[idx])) return idx;
294
+ }
295
+ return -1;
296
+ }
165
297
  function setup(container) {
166
298
  const el = container;
167
299
  if (el.__icenTabsInit) return;
@@ -169,14 +301,69 @@ function setup(container) {
169
301
  const useHash = container.hasAttribute("data-tabs-hash");
170
302
  const names = tabNames(container);
171
303
  if (names.length === 0) return;
172
- container.addEventListener("click", (ev) => {
304
+ const buttons = tabButtons(container);
305
+ buttons.forEach((b) => {
306
+ if (!b.getAttribute("role")) b.setAttribute("role", "tab");
307
+ if (!b.hasAttribute("aria-selected")) b.setAttribute("aria-selected", "false");
308
+ });
309
+ const tablist = buttons[0]?.parentElement;
310
+ if (tablist && !tablist.getAttribute("role")) tablist.setAttribute("role", "tablist");
311
+ const onClick2 = (ev) => {
173
312
  const target = ev.target;
174
313
  if (!(target instanceof Element)) return;
175
314
  const btn = target.closest("[data-tab]");
176
315
  if (!btn || btn.closest("[data-tabs]") !== container) return;
316
+ if (btn instanceof HTMLElement && isDisabled(btn)) return;
177
317
  const name = btn.getAttribute("data-tab");
178
318
  if (name) activate(container, name, useHash);
179
- });
319
+ };
320
+ const onKeyDown4 = (ev) => {
321
+ const kev = ev;
322
+ const target = kev.target;
323
+ if (!(target instanceof Element)) return;
324
+ const btn = target.closest("[data-tab]");
325
+ if (!btn || btn.closest("[data-tabs]") !== container) return;
326
+ const buttonsList = tabButtons(container);
327
+ const currentIdx = buttonsList.indexOf(btn);
328
+ if (currentIdx === -1) return;
329
+ switch (kev.key) {
330
+ case "ArrowRight":
331
+ case "ArrowDown": {
332
+ kev.preventDefault();
333
+ const next = nextEnabled(container, currentIdx, 1);
334
+ if (next >= 0) focusTab(container, next);
335
+ break;
336
+ }
337
+ case "ArrowLeft":
338
+ case "ArrowUp": {
339
+ kev.preventDefault();
340
+ const prev = nextEnabled(container, currentIdx, -1);
341
+ if (prev >= 0) focusTab(container, prev);
342
+ break;
343
+ }
344
+ case "Home": {
345
+ kev.preventDefault();
346
+ focusTab(container, 0);
347
+ break;
348
+ }
349
+ case "End": {
350
+ kev.preventDefault();
351
+ focusTab(container, buttonsList.length - 1);
352
+ break;
353
+ }
354
+ case "Enter":
355
+ case " ": {
356
+ if (btn instanceof HTMLElement && !isDisabled(btn)) {
357
+ kev.preventDefault();
358
+ const name = btn.getAttribute("data-tab");
359
+ if (name) activate(container, name, useHash);
360
+ }
361
+ break;
362
+ }
363
+ }
364
+ };
365
+ container.addEventListener("click", onClick2);
366
+ container.addEventListener("keydown", onKeyDown4);
180
367
  let initial;
181
368
  if (useHash) {
182
369
  const h = currentHash();
@@ -186,12 +373,18 @@ function setup(container) {
186
373
  if (n && names.includes(n)) initial = n;
187
374
  }
188
375
  activate(container, initial ?? names[0], false);
376
+ const onHash = () => {
377
+ const h = currentHash();
378
+ if (names.includes(h)) activate(container, h, false);
379
+ };
189
380
  if (useHash && typeof window !== "undefined") {
190
- window.addEventListener("hashchange", () => {
191
- const h = currentHash();
192
- if (names.includes(h)) activate(container, h, false);
193
- });
381
+ window.addEventListener("hashchange", onHash);
194
382
  }
383
+ el.__icenTabsCleanup = () => {
384
+ container.removeEventListener("click", onClick2);
385
+ container.removeEventListener("keydown", onKeyDown4);
386
+ window.removeEventListener("hashchange", onHash);
387
+ };
195
388
  }
196
389
  function initTabs(root = document) {
197
390
  if (typeof document === "undefined") return;
@@ -234,11 +427,12 @@ async function copyText(text) {
234
427
  function flashDone(btn) {
235
428
  const prev = flashing.get(btn);
236
429
  if (prev) window.clearTimeout(prev.timer);
237
- const original = prev ? prev.original : btn.textContent;
238
- btn.textContent = "\u5DF2\u590D\u5236";
430
+ const iconOnly = btn.hasAttribute("data-copy-icon");
431
+ const original = prev ? prev.original : iconOnly ? null : btn.textContent;
432
+ if (!iconOnly) btn.textContent = "\u5DF2\u590D\u5236";
239
433
  btn.classList.add("done");
240
434
  const timer = window.setTimeout(() => {
241
- btn.textContent = original;
435
+ if (!iconOnly) btn.textContent = original;
242
436
  btn.classList.remove("done");
243
437
  flashing.delete(btn);
244
438
  }, 1400);
@@ -311,14 +505,25 @@ function setupOtp(group) {
311
505
  if (el.__icenInputInit) return;
312
506
  el.__icenInputInit = true;
313
507
  const cells = Array.from(group.querySelectorAll(".otp-cell"));
508
+ const syncFilled = (cell) => {
509
+ cell.classList.toggle("is-filled", cell.value.length > 0);
510
+ };
314
511
  cells.forEach((cell, i) => {
512
+ if (!cell.hasAttribute("aria-label")) {
513
+ cell.setAttribute("aria-label", `\u7B2C ${i + 1} \u4F4D\uFF0C\u5171 ${cells.length} \u4F4D`);
514
+ }
515
+ syncFilled(cell);
315
516
  cell.addEventListener("input", () => {
316
517
  const ch = cell.value.slice(-1);
317
518
  cell.value = ch;
519
+ syncFilled(cell);
318
520
  if (ch && i < cells.length - 1) cells[i + 1].focus();
319
521
  });
320
522
  cell.addEventListener("keydown", (ev) => {
321
- if (ev.key === "Backspace" && !cell.value && i > 0) cells[i - 1].focus();
523
+ if (ev.key === "Backspace" && !cell.value && i > 0) {
524
+ ev.preventDefault();
525
+ cells[i - 1].focus();
526
+ }
322
527
  if (ev.key === "ArrowLeft" && i > 0) {
323
528
  ev.preventDefault();
324
529
  cells[i - 1].focus();
@@ -334,10 +539,18 @@ function setupOtp(group) {
334
539
  for (let k = 0; k < text.length; k++) {
335
540
  const target = cells[i + k];
336
541
  target.value = text[k] ?? "";
542
+ syncFilled(target);
337
543
  }
338
544
  cells[Math.min(i + text.length, cells.length - 1)].focus();
339
545
  });
340
546
  });
547
+ const fireComplete = () => {
548
+ const code = cells.map((c) => c.value).join("");
549
+ if (code.length === cells.length && cells.every((c) => c.value.length > 0)) {
550
+ group.dispatchEvent(new CustomEvent("otp:complete", { detail: { code }, bubbles: true }));
551
+ }
552
+ };
553
+ cells.forEach((cell) => cell.addEventListener("input", fireComplete));
341
554
  }
342
555
  function initInput(root = document) {
343
556
  if (typeof document === "undefined") return;
@@ -507,7 +720,19 @@ function setupSingle(slider, native) {
507
720
  if (fill) fill.style.width = `${p}%`;
508
721
  if (thumb) thumb.style.left = `calc(${p}% - 8px)`;
509
722
  };
510
- native.addEventListener("input", sync);
723
+ native.addEventListener("input", () => {
724
+ sync();
725
+ slider.dispatchEvent(new CustomEvent("icen:slider-input", {
726
+ bubbles: true,
727
+ detail: { value: Number(native.value) }
728
+ }));
729
+ });
730
+ native.addEventListener("change", () => {
731
+ slider.dispatchEvent(new CustomEvent("icen:slider-change", {
732
+ bubbles: true,
733
+ detail: { value: Number(native.value) }
734
+ }));
735
+ });
511
736
  sync();
512
737
  }
513
738
  function setupDual(slider, natives) {
@@ -540,8 +765,28 @@ function setupDual(slider, natives) {
540
765
  lo.style.clipPath = `inset(0 ${100 - pLo}% 0 0)`;
541
766
  hi.style.clipPath = `inset(0 0 0 ${pHi}%)`;
542
767
  };
543
- lo.addEventListener("input", () => sync(lo));
544
- hi.addEventListener("input", () => sync(hi));
768
+ lo.addEventListener("input", () => {
769
+ sync(lo);
770
+ slider.dispatchEvent(new CustomEvent("icen:slider-input", {
771
+ bubbles: true,
772
+ detail: { lo: Number(lo.value), hi: Number(hi.value) }
773
+ }));
774
+ });
775
+ hi.addEventListener("input", () => {
776
+ sync(hi);
777
+ slider.dispatchEvent(new CustomEvent("icen:slider-input", {
778
+ bubbles: true,
779
+ detail: { lo: Number(lo.value), hi: Number(hi.value) }
780
+ }));
781
+ });
782
+ const onChange = () => {
783
+ slider.dispatchEvent(new CustomEvent("icen:slider-change", {
784
+ bubbles: true,
785
+ detail: { lo: Number(lo.value), hi: Number(hi.value) }
786
+ }));
787
+ };
788
+ lo.addEventListener("change", onChange);
789
+ hi.addEventListener("change", onChange);
545
790
  sync(null);
546
791
  }
547
792
  function setup3(slider) {
@@ -577,10 +822,20 @@ function setup4(wrap) {
577
822
  wrap.setAttribute("data-tags", tags.join(","));
578
823
  wrap.classList.toggle("is-max", tags.length >= max);
579
824
  field.placeholder = tags.length === 0 ? placeholder : "";
825
+ if (!wrap.hasAttribute("aria-label")) {
826
+ wrap.setAttribute("aria-label", `\u6807\u7B7E\u8F93\u5165\uFF0C\u5171 ${tags.length} \u4E2A`);
827
+ }
828
+ }
829
+ function emit() {
830
+ wrap.dispatchEvent(new CustomEvent("icen:tags-change", {
831
+ bubbles: true,
832
+ detail: { tags: [...tags] }
833
+ }));
580
834
  }
581
835
  function remove(index) {
582
836
  tags.splice(index, 1);
583
837
  render();
838
+ emit();
584
839
  }
585
840
  function chip(tag, index) {
586
841
  const el2 = document.createElement("span");
@@ -613,6 +868,7 @@ function setup4(wrap) {
613
868
  tags.push(v);
614
869
  field.value = "";
615
870
  render();
871
+ emit();
616
872
  }
617
873
  field.addEventListener("keydown", (ev) => {
618
874
  if (ev.key === "Enter" || ev.key === ",") {
@@ -672,10 +928,9 @@ function setup5(zone) {
672
928
  }
673
929
  if (assigned) {
674
930
  input.dispatchEvent(new Event("change", { bubbles: true }));
675
- } else {
676
- const list = Array.from(files);
677
- zone.dispatchEvent(new CustomEvent("icn:upload", { bubbles: true, detail: { files: list } }));
678
931
  }
932
+ const list = Array.from(files);
933
+ zone.dispatchEvent(new CustomEvent("icen:upload", { bubbles: true, detail: { files: list } }));
679
934
  });
680
935
  }
681
936
  function initUpload(root = document) {
@@ -716,6 +971,20 @@ function openModal(id) {
716
971
  panel2.setAttribute("role", "dialog");
717
972
  panel2.setAttribute("aria-modal", "true");
718
973
  if (!panel2.hasAttribute("tabindex")) panel2.tabIndex = -1;
974
+ if (!panel2.hasAttribute("aria-labelledby")) {
975
+ const title = panel2.querySelector(".modal-title");
976
+ if (title) {
977
+ if (!title.id) title.id = `modal-title-${CSS.escape(id)}`;
978
+ panel2.setAttribute("aria-labelledby", title.id);
979
+ }
980
+ }
981
+ if (!panel2.hasAttribute("aria-describedby")) {
982
+ const desc = panel2.querySelector(".modal-description");
983
+ if (desc) {
984
+ if (!desc.id) desc.id = `modal-desc-${CSS.escape(id)}`;
985
+ panel2.setAttribute("aria-describedby", desc.id);
986
+ }
987
+ }
719
988
  const closeOnEscape = backdrop.dataset.closeOnEscape !== "false";
720
989
  keyHandler = (e) => {
721
990
  if (e.key === "Escape") {
@@ -774,7 +1043,7 @@ function initModal(root = document) {
774
1043
  const opener = target.closest("[data-modal-open]");
775
1044
  if (opener?.dataset.modalOpen) {
776
1045
  opener.setAttribute("aria-haspopup", "dialog");
777
- open(opener.dataset.modalOpen);
1046
+ openModal(opener.dataset.modalOpen);
778
1047
  return;
779
1048
  }
780
1049
  const closer = target.closest("[data-modal-close]");
@@ -1314,9 +1583,10 @@ function setup6(container) {
1314
1583
  if (!trigger) return;
1315
1584
  const item = trigger.closest(".accordion-item");
1316
1585
  if (!item || trigger.closest(".accordion") !== container) return;
1317
- const open2 = item.classList.toggle("is-open");
1318
- trigger.setAttribute("aria-expanded", String(open2));
1319
- if (single && open2) {
1586
+ if (item.classList.contains("is-disabled")) return;
1587
+ const open = item.classList.toggle("is-open");
1588
+ trigger.setAttribute("aria-expanded", String(open));
1589
+ if (single && open) {
1320
1590
  container.querySelectorAll(".accordion-item.is-open").forEach((other) => {
1321
1591
  if (other === item) return;
1322
1592
  other.classList.remove("is-open");
@@ -1324,6 +1594,25 @@ function setup6(container) {
1324
1594
  });
1325
1595
  }
1326
1596
  });
1597
+ container.addEventListener("keydown", (ev) => {
1598
+ const kev = ev;
1599
+ const target = kev.target;
1600
+ if (!(target instanceof Element)) return;
1601
+ const trigger = target.closest(".accordion-trigger");
1602
+ if (!trigger || trigger.closest(".accordion") !== container) return;
1603
+ const triggers = Array.from(
1604
+ container.querySelectorAll(".accordion-item:not(.is-disabled) > .accordion-trigger")
1605
+ );
1606
+ const idx = triggers.indexOf(trigger);
1607
+ if (idx === -1) return;
1608
+ if (kev.key === "Home") {
1609
+ kev.preventDefault();
1610
+ triggers[0]?.focus();
1611
+ } else if (kev.key === "End") {
1612
+ kev.preventDefault();
1613
+ triggers[triggers.length - 1]?.focus();
1614
+ }
1615
+ });
1327
1616
  }
1328
1617
  function initAccordion(root = document) {
1329
1618
  if (typeof document === "undefined") return;
@@ -1338,6 +1627,9 @@ function setup7(tree) {
1338
1627
  const el = tree;
1339
1628
  if (el.__icenTreeInit) return;
1340
1629
  el.__icenTreeInit = true;
1630
+ tree.querySelectorAll(".tree-node").forEach((node) => {
1631
+ if (!node.hasAttribute("tabindex")) node.tabIndex = 0;
1632
+ });
1341
1633
  tree.addEventListener("click", (ev) => {
1342
1634
  const target = ev.target;
1343
1635
  if (!(target instanceof Element)) return;
@@ -1363,8 +1655,61 @@ function setup7(tree) {
1363
1655
  return;
1364
1656
  }
1365
1657
  if (childUl) return;
1366
- tree.querySelectorAll(".tree-node.is-selected").forEach((n) => n.classList.remove("is-selected"));
1658
+ tree.querySelectorAll('.tree-node.is-selected, .tree-node[aria-selected="true"]').forEach((n) => {
1659
+ n.classList.remove("is-selected");
1660
+ n.removeAttribute("aria-selected");
1661
+ });
1367
1662
  node.classList.add("is-selected");
1663
+ node.setAttribute("aria-selected", "true");
1664
+ tree.dispatchEvent(new CustomEvent("icen:tree-select", {
1665
+ bubbles: true,
1666
+ detail: { node }
1667
+ }));
1668
+ });
1669
+ tree.addEventListener("keydown", (ev) => {
1670
+ const kev = ev;
1671
+ const target = kev.target;
1672
+ if (!(target instanceof Element)) return;
1673
+ const node = target.closest(".tree-node");
1674
+ if (!node || node.closest(".tree") !== tree) return;
1675
+ if (node.classList.contains("is-disabled")) return;
1676
+ const allNodes = Array.from(tree.querySelectorAll(".tree-node:not(.is-disabled)"));
1677
+ const idx = allNodes.indexOf(node);
1678
+ if (idx === -1) return;
1679
+ switch (kev.key) {
1680
+ case "ArrowDown": {
1681
+ kev.preventDefault();
1682
+ allNodes[(idx + 1) % allNodes.length]?.focus();
1683
+ break;
1684
+ }
1685
+ case "ArrowUp": {
1686
+ kev.preventDefault();
1687
+ allNodes[(idx - 1 + allNodes.length) % allNodes.length]?.focus();
1688
+ break;
1689
+ }
1690
+ case "ArrowRight": {
1691
+ const li = node.closest("li");
1692
+ const childUl = li?.querySelector(":scope > ul");
1693
+ if (childUl?.hasAttribute("hidden")) {
1694
+ kev.preventDefault();
1695
+ childUl.removeAttribute("hidden");
1696
+ node.querySelector(".tree-toggle")?.setAttribute("aria-expanded", "true");
1697
+ node.querySelector(".tree-toggle")?.classList.remove("is-collapsed");
1698
+ }
1699
+ break;
1700
+ }
1701
+ case "ArrowLeft": {
1702
+ const li = node.closest("li");
1703
+ const childUl = li?.querySelector(":scope > ul");
1704
+ if (childUl && !childUl.hasAttribute("hidden")) {
1705
+ kev.preventDefault();
1706
+ childUl.setAttribute("hidden", "");
1707
+ node.querySelector(".tree-toggle")?.setAttribute("aria-expanded", "false");
1708
+ node.querySelector(".tree-toggle")?.classList.add("is-collapsed");
1709
+ }
1710
+ break;
1711
+ }
1712
+ }
1368
1713
  });
1369
1714
  }
1370
1715
  function initTree(root = document) {
@@ -1405,7 +1750,15 @@ function setup8(carousel) {
1405
1750
  if (!track) return;
1406
1751
  track.style.transform = `translateX(${-index * 100}%)`;
1407
1752
  slides.forEach((s, i) => s.setAttribute("aria-hidden", String(i !== index)));
1408
- dots.forEach((d, i) => d.classList.toggle("active", i === index));
1753
+ dots.forEach((d, i) => {
1754
+ d.classList.toggle("active", i === index);
1755
+ if (i === index) d.setAttribute("aria-current", "true");
1756
+ else d.removeAttribute("aria-current");
1757
+ });
1758
+ carousel.dispatchEvent(new CustomEvent("icen:carousel-change", {
1759
+ bubbles: true,
1760
+ detail: { index, count }
1761
+ }));
1409
1762
  }
1410
1763
  function go(i) {
1411
1764
  index = (i % count + count) % count;
@@ -1413,6 +1766,27 @@ function setup8(carousel) {
1413
1766
  }
1414
1767
  prev?.addEventListener("click", () => go(index - 1));
1415
1768
  next?.addEventListener("click", () => go(index + 1));
1769
+ carousel.addEventListener("keydown", (ev) => {
1770
+ const kev = ev;
1771
+ switch (kev.key) {
1772
+ case "ArrowLeft":
1773
+ kev.preventDefault();
1774
+ go(index - 1);
1775
+ break;
1776
+ case "ArrowRight":
1777
+ kev.preventDefault();
1778
+ go(index + 1);
1779
+ break;
1780
+ case "Home":
1781
+ kev.preventDefault();
1782
+ go(0);
1783
+ break;
1784
+ case "End":
1785
+ kev.preventDefault();
1786
+ go(count - 1);
1787
+ break;
1788
+ }
1789
+ });
1416
1790
  render();
1417
1791
  }
1418
1792
  function initCarousel(root = document) {
@@ -1683,24 +2057,216 @@ function renderLine(el, opts) {
1683
2057
  wrap.append(svg, labelsRow(labels.slice(-7)));
1684
2058
  el.appendChild(wrap);
1685
2059
  }
2060
+ function isoDate(d) {
2061
+ const m = String(d.getMonth() + 1).padStart(2, "0");
2062
+ const day = String(d.getDate()).padStart(2, "0");
2063
+ return `${d.getFullYear()}-${m}-${day}`;
2064
+ }
2065
+ function renderHeatmap(el, opts) {
2066
+ if (typeof document === "undefined") return;
2067
+ el.textContent = "";
2068
+ let entries;
2069
+ if (opts.data && opts.data.length > 0) {
2070
+ entries = opts.data;
2071
+ } else if (opts.values && opts.values.length > 0) {
2072
+ const today = /* @__PURE__ */ new Date();
2073
+ entries = opts.values.map((value, i) => {
2074
+ const d = new Date(today);
2075
+ d.setDate(d.getDate() - (opts.values.length - 1 - i));
2076
+ return { date: isoDate(d), value };
2077
+ });
2078
+ } else {
2079
+ renderEmpty(el, opts.emptyLabel);
2080
+ return;
2081
+ }
2082
+ const weeks = opts.weeks ?? Math.ceil(entries.length / 7);
2083
+ if (entries.length > weeks * 7) entries = entries.slice(entries.length - weeks * 7);
2084
+ const max = Math.max(...entries.map((e) => e.value), 1);
2085
+ const levelOf = (v) => v <= 0 ? 0 : Math.min(4, Math.ceil(v / max * 4));
2086
+ const wrap = document.createElement("div");
2087
+ wrap.className = "chart-heatmap";
2088
+ if (opts.tone) wrap.style.setProperty("--chart-tone", toneVar(opts.tone));
2089
+ const grid = document.createElement("div");
2090
+ grid.className = "chart-heatmap-grid";
2091
+ grid.setAttribute("role", "img");
2092
+ const first = /* @__PURE__ */ new Date(`${entries[0].date}T00:00:00`);
2093
+ const pad = (first.getDay() + 6) % 7;
2094
+ for (let i = 0; i < pad; i++) {
2095
+ const blank = document.createElement("span");
2096
+ blank.className = "chart-heatmap-cell is-blank";
2097
+ grid.appendChild(blank);
2098
+ }
2099
+ for (const entry of entries) {
2100
+ const cell = document.createElement("span");
2101
+ cell.className = "chart-heatmap-cell";
2102
+ cell.dataset.level = String(levelOf(entry.value));
2103
+ cell.setAttribute("title", `${entry.date}\uFF1A${fmt(opts, entry.value)}`);
2104
+ grid.appendChild(cell);
2105
+ }
2106
+ const legend = document.createElement("div");
2107
+ legend.className = "chart-heatmap-legend";
2108
+ const less = document.createElement("span");
2109
+ less.textContent = "\u5C11";
2110
+ const more = document.createElement("span");
2111
+ more.textContent = "\u591A";
2112
+ legend.appendChild(less);
2113
+ for (let lv = 0; lv <= 4; lv++) {
2114
+ const cell = document.createElement("span");
2115
+ cell.className = "chart-heatmap-cell";
2116
+ cell.dataset.level = String(lv);
2117
+ legend.appendChild(cell);
2118
+ }
2119
+ legend.appendChild(more);
2120
+ wrap.append(grid, legend);
2121
+ el.appendChild(wrap);
2122
+ }
2123
+ function renderSparkline(el, opts) {
2124
+ if (typeof document === "undefined") return;
2125
+ el.textContent = "";
2126
+ const values = opts.values;
2127
+ if (values.length === 0) {
2128
+ renderEmpty(el, opts.emptyLabel);
2129
+ return;
2130
+ }
2131
+ const width = 96;
2132
+ const height = 28;
2133
+ const pad = 3;
2134
+ const max = Math.max(...values, 1);
2135
+ const tone = toneVar(opts.tone);
2136
+ const points = values.map((value, i) => ({
2137
+ x: values.length <= 1 ? width / 2 : pad + i / (values.length - 1) * (width - pad * 2),
2138
+ y: height - pad - value / max * (height - pad * 2),
2139
+ value
2140
+ }));
2141
+ const line = points.map((p, i) => `${i === 0 ? "M" : "L"}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(" ");
2142
+ const last = points[points.length - 1];
2143
+ const first = points[0];
2144
+ const area = `${line} L${last.x.toFixed(1)},${height - pad} L${first.x.toFixed(1)},${height - pad} Z`;
2145
+ const gradientId = `icen-chart-line-${++lineGradientSeq}`;
2146
+ const wrap = document.createElement("div");
2147
+ wrap.className = "chart-sparkline";
2148
+ const svg = svgEl("svg", { viewBox: `0 0 ${width} ${height}`, role: "img" });
2149
+ const defs = svgEl("defs", {});
2150
+ const gradient = svgEl("linearGradient", { id: gradientId, x1: "0", x2: "0", y1: "0", y2: "1" });
2151
+ gradient.appendChild(svgEl("stop", { offset: "0%", "stop-color": tone, "stop-opacity": "0.28" }));
2152
+ gradient.appendChild(svgEl("stop", { offset: "100%", "stop-color": tone, "stop-opacity": "0.02" }));
2153
+ defs.appendChild(gradient);
2154
+ svg.appendChild(defs);
2155
+ svg.appendChild(svgEl("path", { d: area, fill: `url(#${gradientId})` }));
2156
+ svg.appendChild(svgEl("path", {
2157
+ d: line,
2158
+ fill: "none",
2159
+ stroke: tone,
2160
+ "stroke-width": "1.8",
2161
+ "stroke-linecap": "round",
2162
+ "stroke-linejoin": "round"
2163
+ }));
2164
+ const dot = svgEl("circle", {
2165
+ cx: last.x.toFixed(1),
2166
+ cy: last.y.toFixed(1),
2167
+ r: "2.4",
2168
+ fill: "var(--token-card)",
2169
+ stroke: tone,
2170
+ "stroke-width": "1.6"
2171
+ });
2172
+ const title = svgEl("title", {});
2173
+ title.textContent = fmt(opts, last.value);
2174
+ dot.appendChild(title);
2175
+ svg.appendChild(dot);
2176
+ wrap.appendChild(svg);
2177
+ el.appendChild(wrap);
2178
+ }
2179
+ function renderGauge(el, opts) {
2180
+ if (typeof document === "undefined") return;
2181
+ el.textContent = "";
2182
+ const max = opts.max ?? 100;
2183
+ const ratio = max > 0 ? Math.min(1, Math.max(0, opts.value / max)) : 0;
2184
+ const radius = 42;
2185
+ const circumference = 2 * Math.PI * radius;
2186
+ const wrap = document.createElement("div");
2187
+ wrap.className = "chart-gauge";
2188
+ if (opts.tone) wrap.style.setProperty("--chart-tone", toneVar(opts.tone));
2189
+ const svg = svgEl("svg", { viewBox: "0 0 120 120", role: "img" });
2190
+ svg.style.transform = "rotate(-90deg)";
2191
+ svg.appendChild(svgEl("circle", {
2192
+ cx: "60",
2193
+ cy: "60",
2194
+ r: String(radius),
2195
+ fill: "none",
2196
+ stroke: "var(--token-line-soft)",
2197
+ "stroke-width": "10"
2198
+ }));
2199
+ const dash = ratio * circumference;
2200
+ svg.appendChild(svgEl("circle", {
2201
+ cx: "60",
2202
+ cy: "60",
2203
+ r: String(radius),
2204
+ fill: "none",
2205
+ stroke: toneVar(opts.tone),
2206
+ "stroke-width": "10",
2207
+ "stroke-dasharray": `${dash} ${circumference - dash}`,
2208
+ "stroke-linecap": "round"
2209
+ }));
2210
+ const center = document.createElement("div");
2211
+ center.className = "chart-gauge-center";
2212
+ const val = document.createElement("span");
2213
+ val.className = "chart-gauge-value";
2214
+ val.textContent = opts.formatValue ? opts.formatValue(opts.value) : `${Math.round(ratio * 100)}%`;
2215
+ center.appendChild(val);
2216
+ if (opts.label) {
2217
+ const lab = document.createElement("span");
2218
+ lab.className = "chart-gauge-label";
2219
+ lab.textContent = opts.label;
2220
+ center.appendChild(lab);
2221
+ }
2222
+ wrap.append(svg, center);
2223
+ el.appendChild(wrap);
2224
+ }
1686
2225
 
1687
2226
  // src/behaviors/nav.ts
2227
+ function prefersReducedMotion() {
2228
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
2229
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
2230
+ }
1688
2231
  function setup9(nav) {
1689
2232
  const el = nav;
1690
2233
  if (el.__icenNavInit) return;
1691
2234
  el.__icenNavInit = true;
2235
+ const threshold = Number(nav.dataset.navThreshold ?? "12") || 12;
2236
+ const hideOnScroll = nav.hasAttribute("data-nav-hide-on-scroll");
1692
2237
  let ticking = false;
1693
- const update = () => {
2238
+ let lastY = window.scrollY;
2239
+ let hidden = false;
2240
+ const apply = () => {
1694
2241
  ticking = false;
1695
- el.classList.toggle("is-scrolled", window.scrollY > 12);
2242
+ const y = window.scrollY;
2243
+ nav.classList.toggle("is-scrolled", y > threshold);
2244
+ if (hideOnScroll && !prefersReducedMotion()) {
2245
+ const dy = y - lastY;
2246
+ if (y < 8) {
2247
+ hidden = false;
2248
+ } else if (dy > 6 && !hidden) {
2249
+ hidden = true;
2250
+ } else if (dy < -6 && hidden) {
2251
+ hidden = false;
2252
+ }
2253
+ nav.classList.toggle("is-nav-hidden", hidden);
2254
+ }
2255
+ lastY = y;
1696
2256
  };
1697
2257
  const onScroll2 = () => {
1698
2258
  if (ticking) return;
1699
2259
  ticking = true;
1700
- window.requestAnimationFrame(update);
2260
+ window.requestAnimationFrame(apply);
2261
+ };
2262
+ const onResize = () => {
2263
+ if (ticking) return;
2264
+ ticking = true;
2265
+ window.requestAnimationFrame(apply);
1701
2266
  };
1702
2267
  window.addEventListener("scroll", onScroll2, { passive: true });
1703
- update();
2268
+ window.addEventListener("resize", onResize, { passive: true });
2269
+ apply();
1704
2270
  }
1705
2271
  function initNav(root = document) {
1706
2272
  if (typeof document === "undefined") return;
@@ -1716,6 +2282,14 @@ function setup10(sidebar) {
1716
2282
  const el = sidebar;
1717
2283
  if (el.__icenSidebarInit) return;
1718
2284
  el.__icenSidebarInit = true;
2285
+ sidebar.querySelectorAll(".sidebar-group-title").forEach((title) => {
2286
+ const group = title.closest(".sidebar-group");
2287
+ const panel2 = group?.querySelector(".sidebar-group-panel");
2288
+ if (panel2 && !title.hasAttribute("aria-controls")) {
2289
+ if (!panel2.id) panel2.id = `sb-panel-${Math.random().toString(36).slice(2, 9)}`;
2290
+ title.setAttribute("aria-controls", panel2.id);
2291
+ }
2292
+ });
1719
2293
  el.addEventListener("click", (ev) => {
1720
2294
  const target = ev.target;
1721
2295
  if (!(target instanceof Element)) return;
@@ -1724,8 +2298,12 @@ function setup10(sidebar) {
1724
2298
  if (title.closest(".sidebar") !== el) return;
1725
2299
  const group = title.closest(".sidebar-group");
1726
2300
  if (!group) return;
1727
- const open2 = group.classList.toggle("is-open");
1728
- title.setAttribute("aria-expanded", String(open2));
2301
+ const open = group.classList.toggle("is-open");
2302
+ title.setAttribute("aria-expanded", String(open));
2303
+ sidebar.dispatchEvent(new CustomEvent("icen:sidebar-toggle", {
2304
+ bubbles: true,
2305
+ detail: { group, open }
2306
+ }));
1729
2307
  });
1730
2308
  }
1731
2309
  function initSidebar(root = document) {
@@ -1771,8 +2349,11 @@ export {
1771
2349
  openPopover,
1772
2350
  registerContextMenu,
1773
2351
  renderDonut,
2352
+ renderGauge,
1774
2353
  renderHBar,
2354
+ renderHeatmap,
1775
2355
  renderLine,
2356
+ renderSparkline,
1776
2357
  renderStack,
1777
2358
  renderVBar,
1778
2359
  setTheme,