@icen.ai/ui 0.5.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/AGENTS.md +262 -0
  2. package/README.md +52 -13
  3. package/dist/base.css +33 -5
  4. package/dist/behaviors/accordion.d.mts +7 -0
  5. package/dist/behaviors/accordion.mjs +37 -11
  6. package/dist/behaviors/ai-chat.d.mts +59 -0
  7. package/dist/behaviors/ai-chat.mjs +580 -0
  8. package/dist/behaviors/ai-composer.d.mts +137 -0
  9. package/dist/behaviors/ai-composer.mjs +2114 -0
  10. package/dist/behaviors/ai-core.d.mts +156 -0
  11. package/dist/behaviors/ai-core.mjs +504 -0
  12. package/dist/behaviors/ai-diff.d.mts +53 -0
  13. package/dist/behaviors/ai-diff.mjs +413 -0
  14. package/dist/behaviors/ai-panel.d.mts +101 -0
  15. package/dist/behaviors/ai-panel.mjs +1087 -0
  16. package/dist/behaviors/ai-provider.d.mts +198 -0
  17. package/dist/behaviors/ai-provider.mjs +1061 -0
  18. package/dist/behaviors/ai-tool.d.mts +26 -0
  19. package/dist/behaviors/ai-tool.mjs +2173 -0
  20. package/dist/behaviors/ai-tools.d.mts +84 -0
  21. package/dist/behaviors/ai-tools.mjs +1762 -0
  22. package/dist/behaviors/back-top.d.mts +20 -0
  23. package/dist/behaviors/back-top.mjs +64 -32
  24. package/dist/behaviors/carousel.d.mts +7 -0
  25. package/dist/behaviors/carousel.mjs +72 -27
  26. package/dist/behaviors/charts.d.mts +296 -0
  27. package/dist/behaviors/charts.mjs +1364 -440
  28. package/dist/behaviors/command-palette.d.mts +42 -0
  29. package/dist/behaviors/command-palette.mjs +167 -27
  30. package/dist/behaviors/context-menu.d.mts +53 -0
  31. package/dist/behaviors/context-menu.mjs +127 -39
  32. package/dist/behaviors/controls.d.mts +59 -0
  33. package/dist/behaviors/controls.mjs +601 -0
  34. package/dist/behaviors/copy.d.mts +7 -0
  35. package/dist/behaviors/copy.mjs +41 -15
  36. package/dist/behaviors/datatable.d.mts +90 -0
  37. package/dist/behaviors/datatable.mjs +279 -53
  38. package/dist/behaviors/date-picker.d.mts +4 -0
  39. package/dist/behaviors/date-picker.mjs +276 -69
  40. package/dist/behaviors/dropdown.d.mts +8 -0
  41. package/dist/behaviors/dropdown.mjs +171 -28
  42. package/dist/behaviors/events.d.mts +330 -0
  43. package/dist/behaviors/events.mjs +208 -0
  44. package/dist/behaviors/input.d.mts +4 -0
  45. package/dist/behaviors/input.mjs +52 -32
  46. package/dist/behaviors/modal.d.mts +30 -0
  47. package/dist/behaviors/modal.mjs +87 -16
  48. package/dist/behaviors/nav.d.mts +7 -0
  49. package/dist/behaviors/nav.mjs +57 -15
  50. package/dist/behaviors/notification.d.mts +188 -0
  51. package/dist/behaviors/notification.mjs +274 -140
  52. package/dist/behaviors/popover.d.mts +82 -0
  53. package/dist/behaviors/popover.mjs +92 -5
  54. package/dist/behaviors/select.d.mts +6 -0
  55. package/dist/behaviors/select.mjs +287 -45
  56. package/dist/behaviors/sidebar.d.mts +7 -0
  57. package/dist/behaviors/sidebar.mjs +44 -13
  58. package/dist/behaviors/slider.d.mts +7 -0
  59. package/dist/behaviors/slider.mjs +55 -43
  60. package/dist/behaviors/split-pane.d.mts +8 -0
  61. package/dist/behaviors/split-pane.mjs +70 -29
  62. package/dist/behaviors/tabs.d.mts +9 -0
  63. package/dist/behaviors/tabs.mjs +102 -26
  64. package/dist/behaviors/tag-input.d.mts +7 -0
  65. package/dist/behaviors/tag-input.mjs +66 -17
  66. package/dist/behaviors/theme.d.mts +36 -0
  67. package/dist/behaviors/theme.mjs +51 -5
  68. package/dist/behaviors/toast.d.mts +48 -0
  69. package/dist/behaviors/toast.mjs +98 -25
  70. package/dist/behaviors/tree.d.mts +7 -0
  71. package/dist/behaviors/tree.mjs +39 -15
  72. package/dist/behaviors/upload.d.mts +4 -0
  73. package/dist/behaviors/upload.mjs +79 -44
  74. package/dist/cli.mjs +1 -1
  75. package/dist/components/ai-chat.css +1037 -0
  76. package/dist/components/ai-chat.mjs +3 -0
  77. package/dist/components/ai-composer.mjs +3 -0
  78. package/dist/components/ai-context.mjs +2 -0
  79. package/dist/components/ai-diff.css +311 -0
  80. package/dist/components/ai-diff.mjs +3 -0
  81. package/dist/components/ai-files.mjs +3 -0
  82. package/dist/components/ai-message.mjs +3 -0
  83. package/dist/components/ai-panel.css +682 -0
  84. package/dist/components/ai-reasoning.mjs +3 -0
  85. package/dist/components/ai-subagent.mjs +2 -0
  86. package/dist/components/ai-todo.mjs +2 -0
  87. package/dist/components/ai-tool-call.mjs +2 -0
  88. package/dist/components/ai-tool.css +532 -0
  89. package/dist/components/ai-usage.mjs +3 -0
  90. package/dist/components/badge.css +5 -9
  91. package/dist/components/breadcrumb.css +0 -36
  92. package/dist/components/btn.css +4 -7
  93. package/dist/components/card.css +3 -6
  94. package/dist/components/chart-bar.css +10 -0
  95. package/dist/components/chart-calendar.mjs +4 -0
  96. package/dist/components/chart-gauge.css +1 -1
  97. package/dist/components/chart-heatmap.css +40 -0
  98. package/dist/components/chart-pie.css +0 -6
  99. package/dist/components/chart-scatter.css +24 -0
  100. package/dist/components/chart-scatter.mjs +4 -0
  101. package/dist/components/charts.css +87 -2
  102. package/dist/components/charts.mjs +1 -0
  103. package/dist/components/command-palette.css +2 -2
  104. package/dist/components/content.css +11 -25
  105. package/dist/components/copy.css +8 -8
  106. package/dist/components/datatable.css +5 -9
  107. package/dist/components/date-picker.css +4 -8
  108. package/dist/components/empty.css +0 -4
  109. package/dist/components/feedback.css +10 -17
  110. package/dist/components/form.css +13 -14
  111. package/dist/components/input.css +2 -2
  112. package/dist/components/layout.css +36 -36
  113. package/dist/components/media.css +12 -8
  114. package/dist/components/menu.css +2 -2
  115. package/dist/components/modal.css +1 -1
  116. package/dist/components/nav.css +11 -6
  117. package/dist/components/notification.css +27 -28
  118. package/dist/components/pagination.css +4 -2
  119. package/dist/components/pagination.mjs +1 -0
  120. package/dist/components/panel.css +16 -16
  121. package/dist/components/pill.css +15 -15
  122. package/dist/components/rating.mjs +1 -0
  123. package/dist/components/segmented.css +14 -6
  124. package/dist/components/segmented.mjs +1 -0
  125. package/dist/components/select.css +79 -24
  126. package/dist/components/sidebar.css +1 -1
  127. package/dist/components/split-pane.css +0 -1
  128. package/dist/components/stat.css +14 -18
  129. package/dist/components/steps.css +15 -5
  130. package/dist/components/steps.mjs +1 -0
  131. package/dist/components/switch.css +11 -13
  132. package/dist/components/switch.mjs +1 -0
  133. package/dist/components/table.css +46 -47
  134. package/dist/components/table.mjs +1 -0
  135. package/dist/components/tabs.css +57 -45
  136. package/dist/components/tag-input.css +5 -10
  137. package/dist/components/toast.css +16 -13
  138. package/dist/components/tooltip.css +2 -2
  139. package/dist/components/upload.css +0 -15
  140. package/dist/index.d.mts +39 -0
  141. package/dist/index.mjs +11243 -3312
  142. package/dist/registry.json +147 -9
  143. package/dist/retro-effects.css +23 -32
  144. package/dist/tokens/colors.css +4 -1
  145. package/dist/tokens/retro-effects.css +23 -32
  146. package/dist/tokens/style-profiles.css +24 -2
  147. package/dist/tokens/typography.css +2 -0
  148. package/dist/tokens.css +30 -3
  149. package/dist/ui.css +3420 -645
  150. package/docs/spec/ai-native.md +662 -0
  151. package/package.json +15 -5
@@ -1,7 +1,14 @@
1
+ // src/behaviors/events.ts
2
+ function emitIcen(source, type, detail) {
3
+ return source.dispatchEvent(
4
+ new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable: true })
5
+ );
6
+ }
7
+
1
8
  // src/behaviors/sidebar.ts
2
9
  function setup(sidebar) {
3
10
  const el = sidebar;
4
- if (el.__icenSidebarInit) return;
11
+ if (el.__icenSidebarInit) return void 0;
5
12
  el.__icenSidebarInit = true;
6
13
  sidebar.querySelectorAll(".sidebar-group-title").forEach((title) => {
7
14
  const group = title.closest(".sidebar-group");
@@ -11,9 +18,21 @@ function setup(sidebar) {
11
18
  title.setAttribute("aria-controls", panel.id);
12
19
  }
13
20
  });
14
- el.addEventListener("click", (ev) => {
21
+ sidebar.querySelectorAll("[data-sidebar-collapse]").forEach((btn) => {
22
+ if (!btn.hasAttribute("aria-expanded")) {
23
+ btn.setAttribute("aria-expanded", String(!sidebar.classList.contains("sidebar--collapsed")));
24
+ }
25
+ });
26
+ const onClick = (ev) => {
15
27
  const target = ev.target;
16
28
  if (!(target instanceof Element)) return;
29
+ const collapseBtn = target.closest("[data-sidebar-collapse]");
30
+ if (collapseBtn && collapseBtn.closest(".sidebar") === el) {
31
+ const collapsed = el.classList.toggle("sidebar--collapsed");
32
+ collapseBtn.setAttribute("aria-expanded", String(!collapsed));
33
+ emitIcen(el, "icen:sidebar-collapse", { collapsed });
34
+ return;
35
+ }
17
36
  const title = target.closest(".sidebar-group-title");
18
37
  if (!(title instanceof HTMLElement)) return;
19
38
  if (title.closest(".sidebar") !== el) return;
@@ -21,18 +40,30 @@ function setup(sidebar) {
21
40
  if (!group) return;
22
41
  const open = group.classList.toggle("is-open");
23
42
  title.setAttribute("aria-expanded", String(open));
24
- sidebar.dispatchEvent(new CustomEvent("icen:sidebar-toggle", {
25
- bubbles: true,
26
- detail: { group, open }
27
- }));
28
- });
43
+ emitIcen(sidebar, "icen:sidebar-toggle", { group, open });
44
+ };
45
+ el.addEventListener("click", onClick);
46
+ return () => {
47
+ el.removeEventListener("click", onClick);
48
+ el.__icenSidebarInit = false;
49
+ };
29
50
  }
30
- function initSidebar(root = document) {
31
- if (typeof document === "undefined") return;
32
- const sidebars = [];
33
- if (root instanceof Element && root.matches(".sidebar")) sidebars.push(root);
34
- sidebars.push(...Array.from(root.querySelectorAll(".sidebar")));
35
- for (const s of sidebars) setup(s);
51
+ function initSidebar(root) {
52
+ const cleanups = [];
53
+ if (typeof document !== "undefined") {
54
+ const scope = root ?? document;
55
+ const sidebars = [];
56
+ if (scope instanceof Element && scope.matches(".sidebar")) sidebars.push(scope);
57
+ sidebars.push(...Array.from(scope.querySelectorAll(".sidebar")));
58
+ for (const s of sidebars) {
59
+ const cleanup = setup(s);
60
+ if (cleanup) cleanups.push(cleanup);
61
+ }
62
+ }
63
+ return () => {
64
+ for (const fn of cleanups) fn();
65
+ cleanups.length = 0;
66
+ };
36
67
  }
37
68
  export {
38
69
  initSidebar
@@ -0,0 +1,7 @@
1
+ /**
2
+ * 为 root 下每个 .slider 容器初始化(root 自身是 .slider 也算)。
3
+ * 返回销毁函数:移除本次挂上的全部监听并复位幂等标记(销毁后可重新 init)。SSR 下返回 no-op。
4
+ */
5
+ declare function initSlider(root?: ParentNode): () => void;
6
+
7
+ export { initSlider };
@@ -1,4 +1,15 @@
1
+ // src/behaviors/events.ts
2
+ function emitIcen(source, type, detail) {
3
+ return source.dispatchEvent(
4
+ new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable: true })
5
+ );
6
+ }
7
+
1
8
  // src/behaviors/slider.ts
9
+ function bind(target, type, fn, bag) {
10
+ target.addEventListener(type, fn);
11
+ bag.push(() => target.removeEventListener(type, fn));
12
+ }
2
13
  function pct(input) {
3
14
  const min = Number(input.min || "0");
4
15
  const max = Number(input.max || "100");
@@ -6,7 +17,7 @@ function pct(input) {
6
17
  if (range <= 0) return 0;
7
18
  return (Number(input.value) - min) / range * 100;
8
19
  }
9
- function setupSingle(slider, native) {
20
+ function setupSingle(slider, native, bag) {
10
21
  const fill = slider.querySelector(".slider-fill");
11
22
  const thumb = slider.querySelector(".slider-thumb");
12
23
  const sync = () => {
@@ -14,22 +25,16 @@ function setupSingle(slider, native) {
14
25
  if (fill) fill.style.width = `${p}%`;
15
26
  if (thumb) thumb.style.left = `calc(${p}% - 8px)`;
16
27
  };
17
- native.addEventListener("input", () => {
28
+ bind(native, "input", () => {
18
29
  sync();
19
- slider.dispatchEvent(new CustomEvent("icen:slider-input", {
20
- bubbles: true,
21
- detail: { value: Number(native.value) }
22
- }));
23
- });
24
- native.addEventListener("change", () => {
25
- slider.dispatchEvent(new CustomEvent("icen:slider-change", {
26
- bubbles: true,
27
- detail: { value: Number(native.value) }
28
- }));
29
- });
30
+ emitIcen(slider, "icen:slider-input", { value: Number(native.value) });
31
+ }, bag);
32
+ bind(native, "change", () => {
33
+ emitIcen(slider, "icen:slider-change", { value: Number(native.value) });
34
+ }, bag);
30
35
  sync();
31
36
  }
32
- function setupDual(slider, natives) {
37
+ function setupDual(slider, natives, bag) {
33
38
  const lo = natives.find((n) => n.getAttribute("data-thumb") === "lo") ?? natives[0];
34
39
  const hi = natives.find((n) => n.getAttribute("data-thumb") === "hi") ?? natives[1];
35
40
  if (!lo || !hi || lo === hi) return;
@@ -59,45 +64,52 @@ function setupDual(slider, natives) {
59
64
  lo.style.clipPath = `inset(0 ${100 - pLo}% 0 0)`;
60
65
  hi.style.clipPath = `inset(0 0 0 ${pHi}%)`;
61
66
  };
62
- lo.addEventListener("input", () => {
67
+ bind(lo, "input", () => {
63
68
  sync(lo);
64
- slider.dispatchEvent(new CustomEvent("icen:slider-input", {
65
- bubbles: true,
66
- detail: { lo: Number(lo.value), hi: Number(hi.value) }
67
- }));
68
- });
69
- hi.addEventListener("input", () => {
69
+ emitIcen(slider, "icen:slider-input", { lo: Number(lo.value), hi: Number(hi.value) });
70
+ }, bag);
71
+ bind(hi, "input", () => {
70
72
  sync(hi);
71
- slider.dispatchEvent(new CustomEvent("icen:slider-input", {
72
- bubbles: true,
73
- detail: { lo: Number(lo.value), hi: Number(hi.value) }
74
- }));
75
- });
73
+ emitIcen(slider, "icen:slider-input", { lo: Number(lo.value), hi: Number(hi.value) });
74
+ }, bag);
76
75
  const onChange = () => {
77
- slider.dispatchEvent(new CustomEvent("icen:slider-change", {
78
- bubbles: true,
79
- detail: { lo: Number(lo.value), hi: Number(hi.value) }
80
- }));
76
+ emitIcen(slider, "icen:slider-change", { lo: Number(lo.value), hi: Number(hi.value) });
81
77
  };
82
- lo.addEventListener("change", onChange);
83
- hi.addEventListener("change", onChange);
78
+ bind(lo, "change", onChange, bag);
79
+ bind(hi, "change", onChange, bag);
84
80
  sync(null);
85
81
  }
86
82
  function setup(slider) {
87
83
  const el = slider;
88
- if (el.__icenSliderInit) return;
89
- el.__icenSliderInit = true;
84
+ if (el.__icenSliderInit) return void 0;
90
85
  const natives = Array.from(slider.querySelectorAll(".slider-native"));
91
- if (natives.length === 0) return;
92
- if (slider.classList.contains("slider--dual")) setupDual(slider, natives);
93
- else setupSingle(slider, natives[0]);
86
+ if (natives.length === 0) return void 0;
87
+ el.__icenSliderInit = true;
88
+ const bag = [];
89
+ if (slider.classList.contains("slider--dual")) setupDual(slider, natives, bag);
90
+ else setupSingle(slider, natives[0], bag);
91
+ return () => {
92
+ for (const off of bag) off();
93
+ bag.length = 0;
94
+ el.__icenSliderInit = false;
95
+ };
94
96
  }
95
- function initSlider(root = document) {
96
- if (typeof document === "undefined") return;
97
- const containers = [];
98
- if (root instanceof Element && root.matches(".slider")) containers.push(root);
99
- containers.push(...Array.from(root.querySelectorAll(".slider")));
100
- for (const c of containers) setup(c);
97
+ function initSlider(root) {
98
+ const cleanups = [];
99
+ if (typeof document !== "undefined") {
100
+ const scope = root ?? document;
101
+ const containers = [];
102
+ if (scope instanceof Element && scope.matches(".slider")) containers.push(scope);
103
+ containers.push(...Array.from(scope.querySelectorAll(".slider")));
104
+ for (const c of containers) {
105
+ const cleanup = setup(c);
106
+ if (cleanup) cleanups.push(cleanup);
107
+ }
108
+ }
109
+ return () => {
110
+ for (const fn of cleanups) fn();
111
+ cleanups.length = 0;
112
+ };
101
113
  }
102
114
  export {
103
115
  initSlider
@@ -0,0 +1,8 @@
1
+ /**
2
+ * 为 root 下每个 .split-pane[data-split-pane] 初始化(root 自身匹配也算)。
3
+ * 返回销毁函数:移除本次初始化挂上的全部监听并复位幂等标记(销毁后可重新 init)。
4
+ * SSR 下返回 no-op。
5
+ */
6
+ declare function initSplitPane(root?: ParentNode): () => void;
7
+
8
+ export { initSplitPane };
@@ -1,18 +1,28 @@
1
+ // src/behaviors/events.ts
2
+ function emitIcen(source, type, detail) {
3
+ return source.dispatchEvent(
4
+ new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable: true })
5
+ );
6
+ }
7
+
1
8
  // src/behaviors/split-pane.ts
2
- var initialized = /* @__PURE__ */ new WeakSet();
9
+ function numOr(value, fallback) {
10
+ if (value === void 0 || value === "") return fallback;
11
+ const n = Number(value);
12
+ return Number.isFinite(n) ? n : fallback;
13
+ }
3
14
  function pct(value, min, max) {
4
15
  return Math.min(max, Math.max(min, value));
5
16
  }
6
17
  function setup(container) {
7
18
  const el = container;
8
- if (el.__icenSplitPaneInit) return;
19
+ if (el.__icenSplitPaneInit) return void 0;
20
+ const divider = el.querySelector(".split-pane-divider");
21
+ if (!divider) return void 0;
9
22
  el.__icenSplitPaneInit = true;
10
- const dividerQuery = el.querySelector(".split-pane-divider");
11
- if (!dividerQuery) return;
12
- const divider = dividerQuery;
13
- const min = Number(el.dataset.splitMin ?? "10") || 10;
14
- const max = Number(el.dataset.splitMax ?? "90") || 90;
15
- const step = Number(el.dataset.splitStep ?? "2") || 2;
23
+ const min = numOr(el.dataset.splitMin, 10);
24
+ const max = numOr(el.dataset.splitMax, 90);
25
+ const step = numOr(el.dataset.splitStep, 2);
16
26
  const vertical = el.classList.contains("split-pane--vertical");
17
27
  const orientation = vertical ? "vertical" : "horizontal";
18
28
  divider.setAttribute("role", "separator");
@@ -24,20 +34,25 @@ function setup(container) {
24
34
  const v = parseFloat(getComputedStyle(el).getPropertyValue("--split"));
25
35
  return Number.isFinite(v) ? v : 50;
26
36
  }
27
- function write(p) {
37
+ let cur = readCurrent();
38
+ const write = (p) => {
28
39
  const v = pct(p, min, max);
40
+ cur = v;
29
41
  el.style.setProperty("--split", v + "%");
30
42
  divider.setAttribute("aria-valuenow", String(Math.round(v)));
43
+ };
44
+ function emitChange() {
45
+ emitIcen(el, "icen:split-change", { value: Math.round(cur * 10) / 10 });
31
46
  }
32
47
  if (!el.style.getPropertyValue("--split")) write(50);
33
48
  divider.setAttribute("aria-valuenow", String(Math.round(readCurrent())));
34
49
  let dragging = false;
35
- divider.addEventListener("pointerdown", (ev) => {
50
+ const onDown = (ev) => {
36
51
  dragging = true;
37
52
  divider.classList.add("is-dragging");
38
53
  divider.setPointerCapture(ev.pointerId);
39
54
  el.style.userSelect = "none";
40
- });
55
+ };
41
56
  const onUp = (ev) => {
42
57
  if (!dragging) return;
43
58
  dragging = false;
@@ -47,14 +62,16 @@ function setup(container) {
47
62
  } catch {
48
63
  }
49
64
  el.style.userSelect = "";
65
+ emitChange();
50
66
  };
51
- divider.addEventListener("pointerup", onUp);
52
- divider.addEventListener("pointercancel", () => {
67
+ const onCancel = () => {
68
+ if (!dragging) return;
53
69
  dragging = false;
54
70
  divider.classList.remove("is-dragging");
55
71
  el.style.userSelect = "";
56
- });
57
- divider.addEventListener("pointermove", (ev) => {
72
+ emitChange();
73
+ };
74
+ const onMove = (ev) => {
58
75
  if (!dragging) return;
59
76
  const rect = el.getBoundingClientRect();
60
77
  if (vertical) {
@@ -66,9 +83,12 @@ function setup(container) {
66
83
  const p = (ev.clientX - rect.left) / rect.width * 100;
67
84
  write(p);
68
85
  }
69
- });
70
- divider.addEventListener("dblclick", () => write(50));
71
- divider.addEventListener("keydown", (ev) => {
86
+ };
87
+ const onDblClick = () => {
88
+ write(50);
89
+ emitChange();
90
+ };
91
+ const onKeyDown = (ev) => {
72
92
  const current = readCurrent();
73
93
  let next = null;
74
94
  if (vertical) {
@@ -83,19 +103,40 @@ function setup(container) {
83
103
  if (next === null) return;
84
104
  ev.preventDefault();
85
105
  write(next);
86
- });
106
+ emitChange();
107
+ };
108
+ divider.addEventListener("pointerdown", onDown);
109
+ divider.addEventListener("pointerup", onUp);
110
+ divider.addEventListener("pointercancel", onCancel);
111
+ divider.addEventListener("pointermove", onMove);
112
+ divider.addEventListener("dblclick", onDblClick);
113
+ divider.addEventListener("keydown", onKeyDown);
114
+ return () => {
115
+ divider.removeEventListener("pointerdown", onDown);
116
+ divider.removeEventListener("pointerup", onUp);
117
+ divider.removeEventListener("pointercancel", onCancel);
118
+ divider.removeEventListener("pointermove", onMove);
119
+ divider.removeEventListener("dblclick", onDblClick);
120
+ divider.removeEventListener("keydown", onKeyDown);
121
+ el.__icenSplitPaneInit = false;
122
+ };
87
123
  }
88
- function initSplitPane(root = document) {
89
- if (typeof document === "undefined") return;
90
- const panes = [];
91
- if (root instanceof Element && root.matches(".split-pane[data-split-pane]")) panes.push(root);
92
- panes.push(...Array.from(root.querySelectorAll(".split-pane[data-split-pane]")));
93
- for (const p of panes) {
94
- const el = p;
95
- if (initialized.has(el)) continue;
96
- initialized.add(el);
97
- setup(el);
124
+ function initSplitPane(root) {
125
+ const cleanups = [];
126
+ if (typeof document !== "undefined") {
127
+ const scope = root ?? document;
128
+ const panes = [];
129
+ if (scope instanceof Element && scope.matches(".split-pane[data-split-pane]")) panes.push(scope);
130
+ panes.push(...Array.from(scope.querySelectorAll(".split-pane[data-split-pane]")));
131
+ for (const p of panes) {
132
+ const cleanup = setup(p);
133
+ if (cleanup) cleanups.push(cleanup);
134
+ }
98
135
  }
136
+ return () => {
137
+ for (const fn of cleanups) fn();
138
+ cleanups.length = 0;
139
+ };
99
140
  }
100
141
  export {
101
142
  initSplitPane
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 为 root 下每个 [data-tabs] 容器初始化(root 自身是 [data-tabs] 也算)。
3
+ * 返回销毁函数:调用后移除所有容器监听、window hashchange/resize 监听与行为层创建的
4
+ * .tabs-ink 元素,并复位幂等标记(销毁后的容器可被再次 initTabs 重新接管)。
5
+ * SSR 下返回 no-op 函数。
6
+ */
7
+ declare function initTabs(root?: ParentNode): () => void;
8
+
9
+ export { initTabs };
@@ -1,3 +1,10 @@
1
+ // src/behaviors/events.ts
2
+ function emitIcen(source, type, detail) {
3
+ return source.dispatchEvent(
4
+ new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable: true })
5
+ );
6
+ }
7
+
1
8
  // src/behaviors/tabs.ts
2
9
  function tabButtons(container) {
3
10
  return Array.from(container.querySelectorAll("[data-tab]"));
@@ -8,27 +15,52 @@ function tabNames(container) {
8
15
  function currentHash() {
9
16
  return window.location.hash.replace(/^#/, "");
10
17
  }
11
- function activate(container, name, writeHash) {
18
+ function hashWriteMethod(container) {
19
+ const raw = container.getAttribute("data-tabs-hash");
20
+ if (raw === null || raw === "false") return null;
21
+ return raw === "push" ? "push" : "replace";
22
+ }
23
+ function syncInk(container, name) {
24
+ const btn = tabButtons(container).find((b) => b.getAttribute("data-tab") === name);
25
+ const tablist = btn?.parentElement;
26
+ if (!btn || !tablist) return;
27
+ const ink = tablist.querySelector(":scope > .tabs-ink");
28
+ if (!ink) return;
29
+ ink.style.width = `${btn.offsetWidth}px`;
30
+ ink.style.transform = `translateX(${btn.offsetLeft}px)`;
31
+ }
32
+ function activate(container, name, writeHash, emit = false) {
12
33
  const buttons = tabButtons(container);
13
34
  let matched = false;
14
35
  buttons.forEach((t) => {
15
36
  const on = t.getAttribute("data-tab") === name;
16
37
  if (on) matched = true;
17
- t.classList.toggle("active", on);
38
+ t.classList.toggle("is-active", on);
18
39
  t.setAttribute("aria-selected", String(on));
19
40
  t.tabIndex = on ? 0 : -1;
20
41
  });
21
42
  if (!matched) return;
43
+ let panel = null;
22
44
  container.querySelectorAll("[data-tab-panel]").forEach((p) => {
23
45
  const isOn = p.getAttribute("data-tab-panel") === name;
24
46
  p.hidden = !isOn;
25
- if (isOn && !p.getAttribute("aria-labelledby")) {
26
- const btn = buttons.find((b) => b.getAttribute("data-tab") === name);
27
- if (btn?.id) p.setAttribute("aria-labelledby", btn.id);
47
+ if (isOn) {
48
+ panel = p;
49
+ if (!p.getAttribute("aria-labelledby")) {
50
+ const btn = buttons.find((b) => b.getAttribute("data-tab") === name);
51
+ if (btn?.id) p.setAttribute("aria-labelledby", btn.id);
52
+ }
28
53
  }
29
54
  });
30
55
  if (writeHash && typeof window !== "undefined") {
31
- window.history.replaceState(null, "", `#${name}`);
56
+ const method = hashWriteMethod(container);
57
+ if (method) window.history[method === "push" ? "pushState" : "replaceState"](null, "", `#${name}`);
58
+ }
59
+ syncInk(container, name);
60
+ if (emit) {
61
+ const index = buttons.findIndex((b) => b.getAttribute("data-tab") === name);
62
+ const tab = buttons[index] ?? null;
63
+ emitIcen(container, "icen:tab-change", { tab, panel, index });
32
64
  }
33
65
  }
34
66
  function isDisabled(btn) {
@@ -52,11 +84,12 @@ function nextEnabled(container, from, dir) {
52
84
  }
53
85
  function setup(container) {
54
86
  const el = container;
55
- if (el.__icenTabsInit) return;
56
- el.__icenTabsInit = true;
57
- const useHash = container.hasAttribute("data-tabs-hash");
87
+ if (el.__icenTabsInit) return void 0;
88
+ const useHash = hashWriteMethod(container) !== null;
89
+ const autoActivate = container.getAttribute("data-tabs-activation") === "auto";
58
90
  const names = tabNames(container);
59
91
  if (names.length === 0) return;
92
+ el.__icenTabsInit = true;
60
93
  const buttons = tabButtons(container);
61
94
  buttons.forEach((b) => {
62
95
  if (!b.getAttribute("role")) b.setAttribute("role", "tab");
@@ -64,6 +97,18 @@ function setup(container) {
64
97
  });
65
98
  const tablist = buttons[0]?.parentElement;
66
99
  if (tablist && !tablist.getAttribute("role")) tablist.setAttribute("role", "tablist");
100
+ let ink = null;
101
+ if (tablist instanceof HTMLElement && tablist.hasAttribute("data-tabs-ink")) {
102
+ ink = document.createElement("div");
103
+ ink.className = "tabs-ink";
104
+ ink.setAttribute("aria-hidden", "true");
105
+ const initialBtn = buttons.find((b) => b.classList.contains("is-active")) ?? buttons.find((b) => b.getAttribute("data-tab") === names[0]);
106
+ if (initialBtn) {
107
+ ink.style.width = `${initialBtn.offsetWidth}px`;
108
+ ink.style.transform = `translateX(${initialBtn.offsetLeft}px)`;
109
+ }
110
+ tablist.appendChild(ink);
111
+ }
67
112
  const onClick = (ev) => {
68
113
  const target = ev.target;
69
114
  if (!(target instanceof Element)) return;
@@ -71,7 +116,15 @@ function setup(container) {
71
116
  if (!btn || btn.closest("[data-tabs]") !== container) return;
72
117
  if (btn instanceof HTMLElement && isDisabled(btn)) return;
73
118
  const name = btn.getAttribute("data-tab");
74
- if (name) activate(container, name, useHash);
119
+ if (name) activate(container, name, useHash, true);
120
+ };
121
+ const followFocus = (idx) => {
122
+ focusTab(container, idx);
123
+ if (!autoActivate) return;
124
+ const target = tabButtons(container)[idx];
125
+ if (!target || isDisabled(target)) return;
126
+ const name = target.getAttribute("data-tab");
127
+ if (name) activate(container, name, useHash, true);
75
128
  };
76
129
  const onKeyDown = (ev) => {
77
130
  const kev = ev;
@@ -86,25 +139,23 @@ function setup(container) {
86
139
  case "ArrowRight":
87
140
  case "ArrowDown": {
88
141
  kev.preventDefault();
89
- const next = nextEnabled(container, currentIdx, 1);
90
- if (next >= 0) focusTab(container, next);
142
+ followFocus(nextEnabled(container, currentIdx, 1));
91
143
  break;
92
144
  }
93
145
  case "ArrowLeft":
94
146
  case "ArrowUp": {
95
147
  kev.preventDefault();
96
- const prev = nextEnabled(container, currentIdx, -1);
97
- if (prev >= 0) focusTab(container, prev);
148
+ followFocus(nextEnabled(container, currentIdx, -1));
98
149
  break;
99
150
  }
100
151
  case "Home": {
101
152
  kev.preventDefault();
102
- focusTab(container, 0);
153
+ followFocus(nextEnabled(container, -1, 1));
103
154
  break;
104
155
  }
105
156
  case "End": {
106
157
  kev.preventDefault();
107
- focusTab(container, buttonsList.length - 1);
158
+ followFocus(nextEnabled(container, buttonsList.length, -1));
108
159
  break;
109
160
  }
110
161
  case "Enter":
@@ -112,7 +163,7 @@ function setup(container) {
112
163
  if (btn instanceof HTMLElement && !isDisabled(btn)) {
113
164
  kev.preventDefault();
114
165
  const name = btn.getAttribute("data-tab");
115
- if (name) activate(container, name, useHash);
166
+ if (name) activate(container, name, useHash, true);
116
167
  }
117
168
  break;
118
169
  }
@@ -125,29 +176,54 @@ function setup(container) {
125
176
  const h = currentHash();
126
177
  if (names.includes(h)) initial = h;
127
178
  } else {
128
- const n = container.querySelector("[data-tab].active")?.getAttribute("data-tab");
179
+ const n = container.querySelector("[data-tab].is-active")?.getAttribute("data-tab");
129
180
  if (n && names.includes(n)) initial = n;
130
181
  }
131
182
  activate(container, initial ?? names[0], false);
132
183
  const onHash = () => {
133
184
  const h = currentHash();
134
- if (names.includes(h)) activate(container, h, false);
185
+ if (names.includes(h)) activate(container, h, false, true);
135
186
  };
136
187
  if (useHash && typeof window !== "undefined") {
137
188
  window.addEventListener("hashchange", onHash);
138
189
  }
139
- el.__icenTabsCleanup = () => {
190
+ const onResize = () => {
191
+ const active = tabButtons(container).find((b) => b.classList.contains("is-active"));
192
+ const name = active?.getAttribute("data-tab");
193
+ if (name) syncInk(container, name);
194
+ };
195
+ if (ink && typeof window !== "undefined") {
196
+ window.addEventListener("resize", onResize, { passive: true });
197
+ }
198
+ const cleanup = () => {
140
199
  container.removeEventListener("click", onClick);
141
200
  container.removeEventListener("keydown", onKeyDown);
142
201
  window.removeEventListener("hashchange", onHash);
202
+ if (ink) {
203
+ window.removeEventListener("resize", onResize);
204
+ ink.remove();
205
+ ink = null;
206
+ }
207
+ el.__icenTabsInit = false;
143
208
  };
209
+ return cleanup;
144
210
  }
145
- function initTabs(root = document) {
146
- if (typeof document === "undefined") return;
147
- const containers = [];
148
- if (root instanceof Element && root.matches("[data-tabs]")) containers.push(root);
149
- containers.push(...Array.from(root.querySelectorAll("[data-tabs]")));
150
- for (const c of containers) setup(c);
211
+ function initTabs(root) {
212
+ const cleanups = [];
213
+ if (typeof document !== "undefined") {
214
+ const scope = root ?? document;
215
+ const containers = [];
216
+ if (scope instanceof Element && scope.matches("[data-tabs]")) containers.push(scope);
217
+ containers.push(...Array.from(scope.querySelectorAll("[data-tabs]")));
218
+ for (const c of containers) {
219
+ const cleanup = setup(c);
220
+ if (cleanup) cleanups.push(cleanup);
221
+ }
222
+ }
223
+ return () => {
224
+ for (const fn of cleanups) fn();
225
+ cleanups.length = 0;
226
+ };
151
227
  }
152
228
  export {
153
229
  initTabs
@@ -0,0 +1,7 @@
1
+ /**
2
+ * 为 root 下每个 .tag-input 容器初始化(root 自身匹配也算;data-tags 为可选初始值配置)。
3
+ * 返回销毁函数:移除本次挂上的全部监听并复位幂等标记(销毁后可重新 init)。SSR 下返回 no-op。
4
+ */
5
+ declare function initTagInput(root?: ParentNode): () => void;
6
+
7
+ export { initTagInput };