@icen.ai/ui 0.4.0 → 0.8.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 (155) hide show
  1. package/AGENTS.md +256 -0
  2. package/README.md +47 -13
  3. package/dist/base.css +31 -4
  4. package/dist/behaviors/accordion.d.mts +4 -0
  5. package/dist/behaviors/accordion.mjs +4 -3
  6. package/dist/behaviors/ai-chat.d.mts +57 -0
  7. package/dist/behaviors/ai-chat.mjs +557 -0
  8. package/dist/behaviors/ai-composer.d.mts +112 -0
  9. package/dist/behaviors/ai-composer.mjs +2032 -0
  10. package/dist/behaviors/ai-core.d.mts +151 -0
  11. package/dist/behaviors/ai-core.mjs +491 -0
  12. package/dist/behaviors/ai-diff.d.mts +53 -0
  13. package/dist/behaviors/ai-diff.mjs +282 -0
  14. package/dist/behaviors/ai-panel.d.mts +98 -0
  15. package/dist/behaviors/ai-panel.mjs +1012 -0
  16. package/dist/behaviors/ai-provider.d.mts +198 -0
  17. package/dist/behaviors/ai-provider.mjs +1055 -0
  18. package/dist/behaviors/ai-tool.d.mts +18 -0
  19. package/dist/behaviors/ai-tool.mjs +2125 -0
  20. package/dist/behaviors/ai-tools.d.mts +81 -0
  21. package/dist/behaviors/ai-tools.mjs +1658 -0
  22. package/dist/behaviors/back-top.d.mts +20 -0
  23. package/dist/behaviors/back-top.mjs +101 -0
  24. package/dist/behaviors/carousel.d.mts +4 -0
  25. package/dist/behaviors/carousel.mjs +34 -16
  26. package/dist/behaviors/charts.d.mts +250 -0
  27. package/dist/behaviors/charts.mjs +1301 -448
  28. package/dist/behaviors/command-palette.d.mts +8 -0
  29. package/dist/behaviors/command-palette.mjs +189 -0
  30. package/dist/behaviors/context-menu.d.mts +36 -0
  31. package/dist/behaviors/context-menu.mjs +6 -2
  32. package/dist/behaviors/copy.d.mts +4 -0
  33. package/dist/behaviors/copy.mjs +7 -6
  34. package/dist/behaviors/datatable.d.mts +78 -0
  35. package/dist/behaviors/datatable.mjs +54 -11
  36. package/dist/behaviors/date-picker.d.mts +4 -0
  37. package/dist/behaviors/date-picker.mjs +296 -0
  38. package/dist/behaviors/dropdown.d.mts +4 -0
  39. package/dist/behaviors/dropdown.mjs +7 -3
  40. package/dist/behaviors/input.d.mts +4 -0
  41. package/dist/behaviors/input.mjs +184 -8
  42. package/dist/behaviors/modal.d.mts +11 -0
  43. package/dist/behaviors/modal.mjs +11 -6
  44. package/dist/behaviors/nav.d.mts +7 -0
  45. package/dist/behaviors/nav.mjs +25 -9
  46. package/dist/behaviors/notification.d.mts +172 -0
  47. package/dist/behaviors/notification.mjs +908 -0
  48. package/dist/behaviors/popover.d.mts +46 -0
  49. package/dist/behaviors/popover.mjs +2 -1
  50. package/dist/behaviors/select.d.mts +4 -0
  51. package/dist/behaviors/select.mjs +268 -34
  52. package/dist/behaviors/sidebar.d.mts +4 -0
  53. package/dist/behaviors/sidebar.mjs +4 -3
  54. package/dist/behaviors/slider.d.mts +4 -0
  55. package/dist/behaviors/slider.mjs +4 -3
  56. package/dist/behaviors/split-pane.d.mts +4 -0
  57. package/dist/behaviors/split-pane.mjs +102 -0
  58. package/dist/behaviors/tabs.d.mts +8 -0
  59. package/dist/behaviors/tabs.mjs +44 -19
  60. package/dist/behaviors/tag-input.d.mts +4 -0
  61. package/dist/behaviors/tag-input.mjs +12 -3
  62. package/dist/behaviors/theme.d.mts +25 -0
  63. package/dist/behaviors/theme.mjs +3 -1
  64. package/dist/behaviors/toast.d.mts +26 -0
  65. package/dist/behaviors/toast.mjs +24 -18
  66. package/dist/behaviors/tree.d.mts +4 -0
  67. package/dist/behaviors/tree.mjs +4 -3
  68. package/dist/behaviors/upload.d.mts +4 -0
  69. package/dist/behaviors/upload.mjs +205 -17
  70. package/dist/components/ai-chat.css +1037 -0
  71. package/dist/components/ai-chat.mjs +3 -0
  72. package/dist/components/ai-composer.mjs +3 -0
  73. package/dist/components/ai-context.mjs +2 -0
  74. package/dist/components/ai-diff.css +311 -0
  75. package/dist/components/ai-diff.mjs +3 -0
  76. package/dist/components/ai-files.mjs +3 -0
  77. package/dist/components/ai-message.mjs +3 -0
  78. package/dist/components/ai-panel.css +682 -0
  79. package/dist/components/ai-reasoning.mjs +3 -0
  80. package/dist/components/ai-subagent.mjs +2 -0
  81. package/dist/components/ai-todo.mjs +2 -0
  82. package/dist/components/ai-tool-call.mjs +2 -0
  83. package/dist/components/ai-tool.css +532 -0
  84. package/dist/components/ai-usage.mjs +3 -0
  85. package/dist/components/back-top.css +88 -0
  86. package/dist/components/back-top.mjs +3 -0
  87. package/dist/components/badge.css +127 -0
  88. package/dist/components/badge.mjs +2 -0
  89. package/dist/components/breadcrumb.css +53 -0
  90. package/dist/components/btn.css +4 -7
  91. package/dist/components/card.css +66 -1
  92. package/dist/components/chart-bar.css +10 -0
  93. package/dist/components/chart-calendar.mjs +5 -0
  94. package/dist/components/chart-gauge.css +1 -1
  95. package/dist/components/chart-heatmap.css +40 -0
  96. package/dist/components/chart-scatter.css +24 -0
  97. package/dist/components/chart-scatter.mjs +4 -0
  98. package/dist/components/charts.css +86 -1
  99. package/dist/components/charts.mjs +1 -0
  100. package/dist/components/command-palette.css +246 -0
  101. package/dist/components/command-palette.mjs +3 -0
  102. package/dist/components/content.css +221 -8
  103. package/dist/components/copy.css +93 -0
  104. package/dist/components/copy.mjs +3 -0
  105. package/dist/components/datatable.css +4 -8
  106. package/dist/components/date-picker.css +240 -0
  107. package/dist/components/date-picker.mjs +3 -0
  108. package/dist/components/empty.css +69 -3
  109. package/dist/components/feedback.css +241 -10
  110. package/dist/components/form.css +129 -11
  111. package/dist/components/form.mjs +2 -0
  112. package/dist/components/input.css +25 -2
  113. package/dist/components/layout.css +36 -36
  114. package/dist/components/media.css +153 -6
  115. package/dist/components/menu.css +2 -2
  116. package/dist/components/modal.css +1 -1
  117. package/dist/components/nav.css +106 -2
  118. package/dist/components/notification.css +387 -0
  119. package/dist/components/notification.mjs +3 -0
  120. package/dist/components/pagination.css +1 -1
  121. package/dist/components/panel.css +104 -9
  122. package/dist/components/pill.css +82 -12
  123. package/dist/components/scroll-area.css +112 -0
  124. package/dist/components/scroll-area.mjs +2 -0
  125. package/dist/components/segmented.css +80 -5
  126. package/dist/components/segmented.mjs +1 -0
  127. package/dist/components/select.css +104 -17
  128. package/dist/components/sidebar.css +119 -1
  129. package/dist/components/slider.css +113 -0
  130. package/dist/components/split-pane.css +111 -0
  131. package/dist/components/split-pane.mjs +3 -0
  132. package/dist/components/stat.css +131 -12
  133. package/dist/components/steps.css +98 -1
  134. package/dist/components/switch.css +145 -7
  135. package/dist/components/table.css +111 -29
  136. package/dist/components/tabs.css +132 -22
  137. package/dist/components/tag-input.css +4 -4
  138. package/dist/components/tag.css +164 -0
  139. package/dist/components/tag.mjs +2 -0
  140. package/dist/components/toast.css +122 -5
  141. package/dist/components/toolbar.css +101 -0
  142. package/dist/components/toolbar.mjs +2 -0
  143. package/dist/components/tooltip.css +2 -2
  144. package/dist/components/upload.css +61 -4
  145. package/dist/index.d.mts +37 -0
  146. package/dist/index.mjs +9409 -929
  147. package/dist/registry.json +176 -4
  148. package/dist/retro-effects.css +21 -17
  149. package/dist/tokens/colors.css +4 -1
  150. package/dist/tokens/retro-effects.css +21 -17
  151. package/dist/tokens/style-profiles.css +21 -0
  152. package/dist/tokens.css +25 -1
  153. package/dist/ui.css +9616 -2839
  154. package/docs/spec/ai-native.md +659 -0
  155. package/package.json +15 -5
@@ -0,0 +1,46 @@
1
+ interface PopoverRect {
2
+ left: number;
3
+ top: number;
4
+ right: number;
5
+ bottom: number;
6
+ width: number;
7
+ height: number;
8
+ }
9
+ interface PopoverViewport {
10
+ width: number;
11
+ height: number;
12
+ }
13
+ type PopoverSide = 'top' | 'bottom';
14
+ type PopoverAlign = 'start' | 'center' | 'end' | 'stretch';
15
+ interface PopoverLayoutOptions {
16
+ side?: PopoverSide;
17
+ align?: PopoverAlign;
18
+ offset?: number;
19
+ margin?: number;
20
+ minWidth?: number;
21
+ maxWidth?: number;
22
+ minHeight?: number;
23
+ maxHeight?: number;
24
+ /** 内容预期高度:避免用被裁剪的实测高度参与选侧 */
25
+ contentHeightHint?: number;
26
+ }
27
+ interface PopoverLayout {
28
+ side: PopoverSide;
29
+ left: number;
30
+ top: number;
31
+ width: number;
32
+ maxHeight: number;
33
+ }
34
+ interface OpenPopoverOptions extends PopoverLayoutOptions {
35
+ anchor: Element | PopoverRect;
36
+ /** 关闭(含外点/Esc 触发的关闭)后的回调 */
37
+ onClose?: () => void;
38
+ }
39
+ /** 视口智能布局:返回 {side, left, top, width, maxHeight}。 */
40
+ declare function computePopoverLayout(anchor: PopoverRect, viewport: PopoverViewport, opts?: PopoverLayoutOptions): PopoverLayout;
41
+ /** 打开浮层:panel 移入 body 并按 anchor 布局;重复调用同一 panel 为先关后开。 */
42
+ declare function openPopover(panel: HTMLElement, options: OpenPopoverOptions): void;
43
+ /** 关闭浮层:移除监听并把 panel 还原到打开前的位置与 hidden 状态。 */
44
+ declare function closePopover(panel: HTMLElement): void;
45
+
46
+ export { type OpenPopoverOptions, type PopoverAlign, type PopoverLayout, type PopoverLayoutOptions, type PopoverRect, type PopoverSide, type PopoverViewport, closePopover, computePopoverLayout, openPopover };
@@ -139,7 +139,8 @@ function closePopover(panel) {
139
139
  if (!state) return;
140
140
  openPanels.delete(panel);
141
141
  panel.style.visibility = state.prevVisibility;
142
- panel.hidden = state.wasHidden;
142
+ if (typeof state.wasHidden === "string") panel.setAttribute("hidden", state.wasHidden);
143
+ else panel.hidden = state.wasHidden;
143
144
  panel.style.left = "";
144
145
  panel.style.top = "";
145
146
  panel.style.width = "";
@@ -0,0 +1,4 @@
1
+ /** 为 root 下每个 [data-select] 容器初始化(root 自身是 [data-select] 也算)。 */
2
+ declare function initSelect(root?: ParentNode): void;
3
+
4
+ export { initSelect };
@@ -1,4 +1,101 @@
1
+ // src/behaviors/popover.ts
2
+ var DEFAULT_MIN_WIDTH = 220;
3
+ var DEFAULT_MAX_WIDTH = 420;
4
+ var DEFAULT_MIN_HEIGHT = 120;
5
+ var DEFAULT_MAX_HEIGHT = 420;
6
+ function clamp(value, min, max) {
7
+ if (max < min) return min;
8
+ return Math.min(max, Math.max(min, value));
9
+ }
10
+ function chooseSide(preferredSide, availableTop, availableBottom, desiredHeight) {
11
+ const preferredSpace = preferredSide === "top" ? availableTop : availableBottom;
12
+ const oppositeSpace = preferredSide === "top" ? availableBottom : availableTop;
13
+ if (preferredSpace >= desiredHeight) return preferredSide;
14
+ if (oppositeSpace >= desiredHeight) return preferredSide === "top" ? "bottom" : "top";
15
+ if (preferredSpace >= oppositeSpace) return preferredSide;
16
+ return preferredSide === "top" ? "bottom" : "top";
17
+ }
18
+ function horizontalAnchor(anchor, width, align) {
19
+ if (align === "center") return anchor.left + anchor.width / 2 - width / 2;
20
+ if (align === "end") return anchor.right - width;
21
+ return anchor.left;
22
+ }
23
+ function computePopoverLayout(anchor, viewport, opts = {}) {
24
+ const margin = opts.margin ?? 12;
25
+ const offset = opts.offset ?? 8;
26
+ const availableWidth = Math.max(80, viewport.width - margin * 2);
27
+ const requestedMinWidth = Math.max(1, opts.minWidth ?? DEFAULT_MIN_WIDTH);
28
+ const requestedMaxWidth = Math.max(requestedMinWidth, opts.maxWidth ?? DEFAULT_MAX_WIDTH);
29
+ const maxWidth = Math.min(requestedMaxWidth, availableWidth);
30
+ const minWidth = Math.min(requestedMinWidth, maxWidth);
31
+ const preferredSide = opts.side ?? "top";
32
+ const align = opts.align ?? "start";
33
+ const availableTop = Math.max(0, anchor.top - margin - offset);
34
+ const availableBottom = Math.max(0, viewport.height - anchor.bottom - margin - offset);
35
+ const availableHeight = Math.max(80, viewport.height - margin * 2);
36
+ const minHeight = Math.min(Math.max(80, opts.minHeight ?? DEFAULT_MIN_HEIGHT), availableHeight);
37
+ const requestedMaxHeight = Math.max(minHeight, opts.maxHeight ?? DEFAULT_MAX_HEIGHT);
38
+ const fallbackHeight = Math.min(260, requestedMaxHeight);
39
+ const contentHeight = Math.max(opts.contentHeightHint ?? 0, fallbackHeight);
40
+ const desiredHeight = clamp(contentHeight, minHeight, Math.min(requestedMaxHeight, availableHeight));
41
+ const side = chooseSide(preferredSide, availableTop, availableBottom, desiredHeight);
42
+ const sideSpace = side === "top" ? availableTop : availableBottom;
43
+ const maxHeight = Math.min(
44
+ requestedMaxHeight,
45
+ Math.max(80, sideSpace || Math.max(availableTop, availableBottom)),
46
+ availableHeight
47
+ );
48
+ const positionedHeight = clamp(desiredHeight, 40, maxHeight);
49
+ const width = align === "stretch" ? clamp(anchor.width, minWidth, maxWidth) : maxWidth;
50
+ const left = clamp(horizontalAnchor(anchor, width, align), margin, viewport.width - margin - width);
51
+ const desiredTop = side === "top" ? anchor.top - offset - positionedHeight : anchor.bottom + offset;
52
+ const top = clamp(desiredTop, margin, viewport.height - margin - positionedHeight);
53
+ return { side, left, top, width, maxHeight };
54
+ }
55
+
1
56
  // src/behaviors/select.ts
57
+ var openSelect = null;
58
+ var globalBound = false;
59
+ var PANEL_OFFSET = 4;
60
+ var PANEL_MAX_HEIGHT = 240;
61
+ function onGlobalPointerDown(ev) {
62
+ const cur = openSelect;
63
+ if (!cur) return;
64
+ const t = ev.target;
65
+ if (!(t instanceof Node)) return;
66
+ if (cur.container.contains(t) || cur.panel.contains(t)) return;
67
+ cur.close();
68
+ }
69
+ function onGlobalKeyDown(ev) {
70
+ if (ev.key !== "Escape") return;
71
+ const cur = openSelect;
72
+ if (!cur) return;
73
+ cur.close();
74
+ cur.trigger.focus();
75
+ }
76
+ function onWindowChange() {
77
+ const cur = openSelect;
78
+ if (!cur) return;
79
+ if (!cur.trigger.isConnected) {
80
+ cur.close();
81
+ return;
82
+ }
83
+ cur.reposition();
84
+ }
85
+ function bindGlobalListeners() {
86
+ if (globalBound) return;
87
+ globalBound = true;
88
+ document.addEventListener("pointerdown", onGlobalPointerDown);
89
+ document.addEventListener("keydown", onGlobalKeyDown);
90
+ }
91
+ function bindOpenListeners() {
92
+ window.addEventListener("scroll", onWindowChange, true);
93
+ window.addEventListener("resize", onWindowChange);
94
+ }
95
+ function unbindOpenListeners() {
96
+ window.removeEventListener("scroll", onWindowChange, true);
97
+ window.removeEventListener("resize", onWindowChange);
98
+ }
2
99
  function setup(container) {
3
100
  const el = container;
4
101
  if (el.__icenSelectInit) return;
@@ -10,21 +107,39 @@ function setup(container) {
10
107
  if (!triggerEl || !panelEl) return;
11
108
  const trigger = triggerEl;
12
109
  const panel = panelEl;
13
- const options = () => Array.from(panel.querySelectorAll(".select-option"));
14
- const isOpen = () => !panel.hidden;
110
+ const isMultiple = container.hasAttribute("data-select-multiple");
111
+ const isSearchable = container.hasAttribute("data-select-search");
112
+ const maxSelect = Number(container.getAttribute("data-select-max")) || 0;
113
+ const placeholder = container.getAttribute("data-select-placeholder") ?? (valueEl?.classList.contains("is-empty") ? valueEl?.textContent ?? "" : "");
114
+ if (isMultiple) panel.setAttribute("data-select-multiple", "");
115
+ let searchInput = null;
15
116
  let focusIdx = -1;
117
+ if (isSearchable && !panel.querySelector(".select-search-input")) {
118
+ const searchWrap = document.createElement("div");
119
+ searchWrap.className = "select-search";
120
+ const input = document.createElement("input");
121
+ input.className = "select-search-input";
122
+ input.type = "search";
123
+ input.placeholder = "\u641C\u7D22\u2026";
124
+ searchWrap.appendChild(input);
125
+ panel.insertBefore(searchWrap, panel.firstChild);
126
+ }
127
+ searchInput = panel.querySelector(".select-search-input");
128
+ const allOptions = () => Array.from(panel.querySelectorAll(".select-option"));
129
+ const visibleOptions = () => allOptions().filter((o) => !o.hidden);
130
+ const isOpen = () => !panel.hidden;
16
131
  function setFocus(idx) {
17
- const opts = options();
132
+ const opts = visibleOptions();
18
133
  focusIdx = Math.max(-1, Math.min(idx, opts.length - 1));
19
134
  opts.forEach((o, i) => o.classList.toggle("is-focused", i === focusIdx));
20
135
  const cur = focusIdx >= 0 ? opts[focusIdx] : void 0;
21
136
  cur?.scrollIntoView({ block: "nearest" });
22
137
  }
23
138
  function firstEnabled() {
24
- return options().findIndex((o) => !o.classList.contains("is-disabled"));
139
+ return visibleOptions().findIndex((o) => !o.classList.contains("is-disabled"));
25
140
  }
26
141
  function moveFocus(delta) {
27
- const opts = options();
142
+ const opts = visibleOptions();
28
143
  if (opts.length === 0) return;
29
144
  let n = focusIdx;
30
145
  for (let guard = 0; guard < opts.length; guard++) {
@@ -34,13 +149,83 @@ function setup(container) {
34
149
  }
35
150
  setFocus(n);
36
151
  }
152
+ let emptyEl = panel.querySelector(".select-empty");
153
+ if (!emptyEl) {
154
+ emptyEl = document.createElement("div");
155
+ emptyEl.className = "select-empty";
156
+ emptyEl.textContent = "\u65E0\u5339\u914D\u9879";
157
+ emptyEl.hidden = true;
158
+ panel.appendChild(emptyEl);
159
+ }
160
+ function updateEmptyState() {
161
+ const hasVisible = visibleOptions().length > 0;
162
+ if (emptyEl) emptyEl.hidden = hasVisible;
163
+ }
164
+ function filterOptions(query) {
165
+ const q = query.trim().toLowerCase();
166
+ const groups = Array.from(panel.querySelectorAll(".select-group"));
167
+ for (const opt of allOptions()) {
168
+ const text = (opt.textContent ?? "").toLowerCase();
169
+ const kw = (opt.getAttribute("data-keywords") ?? "").toLowerCase();
170
+ opt.hidden = q !== "" && !text.includes(q) && !kw.includes(q);
171
+ }
172
+ for (const g of groups) {
173
+ const hasVisible = Array.from(g.querySelectorAll(".select-option")).some((o) => !o.hidden);
174
+ g.hidden = !hasVisible;
175
+ }
176
+ updateEmptyState();
177
+ setFocus(firstEnabled());
178
+ }
179
+ if (searchInput) {
180
+ searchInput.addEventListener("input", () => filterOptions(searchInput.value));
181
+ searchInput.addEventListener("keydown", (ev) => {
182
+ if (ev.key === "Escape") {
183
+ ev.stopPropagation();
184
+ searchInput.value = "";
185
+ filterOptions("");
186
+ searchInput.focus();
187
+ }
188
+ });
189
+ }
190
+ function positionPanel() {
191
+ const rect = trigger.getBoundingClientRect();
192
+ const layout = computePopoverLayout(
193
+ rect,
194
+ { width: window.innerWidth, height: window.innerHeight },
195
+ {
196
+ side: "bottom",
197
+ align: "stretch",
198
+ offset: PANEL_OFFSET,
199
+ minWidth: rect.width,
200
+ maxWidth: rect.width,
201
+ maxHeight: PANEL_MAX_HEIGHT
202
+ }
203
+ );
204
+ panel.style.left = `${layout.left + window.scrollX}px`;
205
+ panel.style.top = `${layout.top + window.scrollY}px`;
206
+ panel.style.minWidth = `${layout.width}px`;
207
+ panel.style.width = `${layout.width}px`;
208
+ panel.style.maxHeight = `${layout.maxHeight}px`;
209
+ panel.dataset.side = layout.side;
210
+ }
37
211
  function openPanel() {
38
212
  if (isOpen()) return;
213
+ openSelect?.close();
214
+ if (panel.parentElement !== document.body) document.body.appendChild(panel);
39
215
  panel.hidden = false;
40
216
  container.classList.add("is-open");
41
217
  trigger.setAttribute("aria-expanded", "true");
42
- const sel = options().findIndex((o) => o.classList.contains("is-selected"));
43
- setFocus(sel >= 0 ? sel : firstEnabled());
218
+ positionPanel();
219
+ openSelect = { container, trigger, panel, close: closePanel, reposition: positionPanel };
220
+ bindOpenListeners();
221
+ if (searchInput) {
222
+ searchInput.value = "";
223
+ filterOptions("");
224
+ setTimeout(() => searchInput?.focus(), 0);
225
+ } else {
226
+ const sel = allOptions().findIndex((o) => o.classList.contains("is-selected"));
227
+ setFocus(sel >= 0 ? sel : firstEnabled());
228
+ }
44
229
  }
45
230
  function closePanel() {
46
231
  if (!isOpen()) return;
@@ -48,22 +233,77 @@ function setup(container) {
48
233
  container.classList.remove("is-open");
49
234
  trigger.setAttribute("aria-expanded", "false");
50
235
  setFocus(-1);
236
+ if (openSelect?.panel === panel) openSelect = null;
237
+ unbindOpenListeners();
51
238
  }
52
- function choose(opt) {
53
- if (opt.classList.contains("is-disabled")) return;
54
- options().forEach((o) => o.classList.toggle("is-selected", o === opt));
55
- if (valueEl) {
56
- valueEl.textContent = opt.textContent ?? "";
239
+ function getSelectedValues() {
240
+ return allOptions().filter((o) => o.classList.contains("is-selected")).map((o) => o.getAttribute("data-value") ?? "");
241
+ }
242
+ function updateMultiTrigger() {
243
+ if (!valueEl) return;
244
+ const selected = allOptions().filter((o) => o.classList.contains("is-selected"));
245
+ if (selected.length === 0) {
246
+ valueEl.textContent = placeholder;
247
+ valueEl.classList.add("is-empty");
248
+ container.removeAttribute("data-value");
249
+ } else {
57
250
  valueEl.classList.remove("is-empty");
251
+ const maxLabel = Number(container.getAttribute("data-select-max-label")) || 2;
252
+ if (selected.length <= maxLabel) {
253
+ valueEl.textContent = selected.map((o) => o.textContent ?? "").join("\u3001");
254
+ } else {
255
+ valueEl.textContent = `\u5DF2\u9009 ${selected.length} \u9879`;
256
+ }
58
257
  }
59
- const v = opt.getAttribute("data-value") ?? "";
60
- container.setAttribute("data-value", v);
61
- if (hiddenInput) {
62
- hiddenInput.value = v;
63
- hiddenInput.dispatchEvent(new Event("change", { bubbles: true }));
258
+ if (maxSelect > 0) {
259
+ const atMax = selected.length >= maxSelect;
260
+ allOptions().forEach((o) => {
261
+ if (!o.classList.contains("is-selected")) {
262
+ o.classList.toggle("is-disabled", atMax);
263
+ }
264
+ });
64
265
  }
65
- closePanel();
66
- trigger.focus();
266
+ }
267
+ function syncHiddenInput() {
268
+ if (!hiddenInput) return;
269
+ const values = getSelectedValues();
270
+ hiddenInput.value = isMultiple ? values.join(",") : values[0] ?? "";
271
+ hiddenInput.dispatchEvent(new Event("change", { bubbles: true }));
272
+ }
273
+ function choose(opt) {
274
+ if (opt.classList.contains("is-disabled")) return;
275
+ if (isMultiple) {
276
+ opt.classList.toggle("is-selected");
277
+ container.setAttribute(
278
+ "data-value",
279
+ getSelectedValues().join(",")
280
+ );
281
+ updateMultiTrigger();
282
+ syncHiddenInput();
283
+ if (searchInput) searchInput.focus();
284
+ else opt.focus();
285
+ } else {
286
+ allOptions().forEach((o) => o.classList.toggle("is-selected", o === opt));
287
+ if (valueEl) {
288
+ valueEl.textContent = opt.textContent ?? "";
289
+ valueEl.classList.remove("is-empty");
290
+ }
291
+ const v = opt.getAttribute("data-value") ?? "";
292
+ container.setAttribute("data-value", v);
293
+ syncHiddenInput();
294
+ closePanel();
295
+ trigger.focus();
296
+ }
297
+ }
298
+ const clearBtn = container.querySelector(".select-clear");
299
+ if (clearBtn && isMultiple) {
300
+ clearBtn.addEventListener("click", (ev) => {
301
+ ev.stopPropagation();
302
+ allOptions().forEach((o) => o.classList.remove("is-selected"));
303
+ updateMultiTrigger();
304
+ syncHiddenInput();
305
+ });
306
+ clearBtn.addEventListener("pointerdown", (ev) => ev.stopPropagation());
67
307
  }
68
308
  trigger.addEventListener("click", () => isOpen() ? closePanel() : openPanel());
69
309
  trigger.addEventListener("keydown", (ev) => {
@@ -86,7 +326,7 @@ function setup(container) {
86
326
  case "Enter":
87
327
  case " ": {
88
328
  ev.preventDefault();
89
- const opt = options()[focusIdx];
329
+ const opt = visibleOptions()[focusIdx];
90
330
  if (opt) choose(opt);
91
331
  break;
92
332
  }
@@ -106,25 +346,19 @@ function setup(container) {
106
346
  const t = ev.target;
107
347
  if (!(t instanceof Element)) return;
108
348
  const opt = t.closest(".select-option");
109
- if (!opt) return;
110
- const idx = options().indexOf(opt);
349
+ if (!opt || opt.hidden) return;
350
+ const idx = visibleOptions().indexOf(opt);
111
351
  if (idx >= 0 && !opt.classList.contains("is-disabled")) setFocus(idx);
112
352
  });
113
- document.addEventListener("pointerdown", (ev) => {
114
- if (isOpen() && ev.target instanceof Node && !container.contains(ev.target)) closePanel();
115
- });
116
- document.addEventListener("keydown", (ev) => {
117
- if (ev.key === "Escape" && isOpen()) {
118
- closePanel();
119
- trigger.focus();
120
- }
121
- });
353
+ if (isMultiple) updateMultiTrigger();
122
354
  }
123
- function initSelect(root = document) {
355
+ function initSelect(root) {
124
356
  if (typeof document === "undefined") return;
357
+ const scope = root ?? document;
358
+ bindGlobalListeners();
125
359
  const containers = [];
126
- if (root instanceof Element && root.matches("[data-select]")) containers.push(root);
127
- containers.push(...Array.from(root.querySelectorAll("[data-select]")));
360
+ if (scope instanceof Element && scope.matches("[data-select]")) containers.push(scope);
361
+ containers.push(...Array.from(scope.querySelectorAll("[data-select]")));
128
362
  for (const c of containers) setup(c);
129
363
  }
130
364
  export {
@@ -0,0 +1,4 @@
1
+ /** 为 root 下每个 .sidebar 初始化(root 自身是 .sidebar 也算)。 */
2
+ declare function initSidebar(root?: ParentNode): void;
3
+
4
+ export { initSidebar };
@@ -27,11 +27,12 @@ function setup(sidebar) {
27
27
  }));
28
28
  });
29
29
  }
30
- function initSidebar(root = document) {
30
+ function initSidebar(root) {
31
31
  if (typeof document === "undefined") return;
32
+ const scope = root ?? document;
32
33
  const sidebars = [];
33
- if (root instanceof Element && root.matches(".sidebar")) sidebars.push(root);
34
- sidebars.push(...Array.from(root.querySelectorAll(".sidebar")));
34
+ if (scope instanceof Element && scope.matches(".sidebar")) sidebars.push(scope);
35
+ sidebars.push(...Array.from(scope.querySelectorAll(".sidebar")));
35
36
  for (const s of sidebars) setup(s);
36
37
  }
37
38
  export {
@@ -0,0 +1,4 @@
1
+ /** 为 root 下每个 .slider 容器初始化(root 自身是 .slider 也算)。 */
2
+ declare function initSlider(root?: ParentNode): void;
3
+
4
+ export { initSlider };
@@ -92,11 +92,12 @@ function setup(slider) {
92
92
  if (slider.classList.contains("slider--dual")) setupDual(slider, natives);
93
93
  else setupSingle(slider, natives[0]);
94
94
  }
95
- function initSlider(root = document) {
95
+ function initSlider(root) {
96
96
  if (typeof document === "undefined") return;
97
+ const scope = root ?? document;
97
98
  const containers = [];
98
- if (root instanceof Element && root.matches(".slider")) containers.push(root);
99
- containers.push(...Array.from(root.querySelectorAll(".slider")));
99
+ if (scope instanceof Element && scope.matches(".slider")) containers.push(scope);
100
+ containers.push(...Array.from(scope.querySelectorAll(".slider")));
100
101
  for (const c of containers) setup(c);
101
102
  }
102
103
  export {
@@ -0,0 +1,4 @@
1
+ /** 为 root 下每个 .split-pane[data-split-pane] 初始化(root 自身匹配也算)。 */
2
+ declare function initSplitPane(root?: ParentNode): void;
3
+
4
+ export { initSplitPane };
@@ -0,0 +1,102 @@
1
+ // src/behaviors/split-pane.ts
2
+ function numOr(value, fallback) {
3
+ if (value === void 0 || value === "") return fallback;
4
+ const n = Number(value);
5
+ return Number.isFinite(n) ? n : fallback;
6
+ }
7
+ function pct(value, min, max) {
8
+ return Math.min(max, Math.max(min, value));
9
+ }
10
+ function setup(container) {
11
+ const el = container;
12
+ if (el.__icenSplitPaneInit) return;
13
+ el.__icenSplitPaneInit = true;
14
+ const dividerQuery = el.querySelector(".split-pane-divider");
15
+ if (!dividerQuery) return;
16
+ const divider = dividerQuery;
17
+ const min = numOr(el.dataset.splitMin, 10);
18
+ const max = numOr(el.dataset.splitMax, 90);
19
+ const step = numOr(el.dataset.splitStep, 2);
20
+ const vertical = el.classList.contains("split-pane--vertical");
21
+ const orientation = vertical ? "vertical" : "horizontal";
22
+ divider.setAttribute("role", "separator");
23
+ divider.setAttribute("aria-orientation", orientation === "vertical" ? "horizontal" : "vertical");
24
+ divider.setAttribute("aria-valuemin", String(min));
25
+ divider.setAttribute("aria-valuemax", String(max));
26
+ divider.tabIndex = 0;
27
+ function readCurrent() {
28
+ const v = parseFloat(getComputedStyle(el).getPropertyValue("--split"));
29
+ return Number.isFinite(v) ? v : 50;
30
+ }
31
+ function write(p) {
32
+ const v = pct(p, min, max);
33
+ el.style.setProperty("--split", v + "%");
34
+ divider.setAttribute("aria-valuenow", String(Math.round(v)));
35
+ }
36
+ if (!el.style.getPropertyValue("--split")) write(50);
37
+ divider.setAttribute("aria-valuenow", String(Math.round(readCurrent())));
38
+ let dragging = false;
39
+ divider.addEventListener("pointerdown", (ev) => {
40
+ dragging = true;
41
+ divider.classList.add("is-dragging");
42
+ divider.setPointerCapture(ev.pointerId);
43
+ el.style.userSelect = "none";
44
+ });
45
+ const onUp = (ev) => {
46
+ if (!dragging) return;
47
+ dragging = false;
48
+ divider.classList.remove("is-dragging");
49
+ try {
50
+ divider.releasePointerCapture(ev.pointerId);
51
+ } catch {
52
+ }
53
+ el.style.userSelect = "";
54
+ };
55
+ divider.addEventListener("pointerup", onUp);
56
+ divider.addEventListener("pointercancel", () => {
57
+ dragging = false;
58
+ divider.classList.remove("is-dragging");
59
+ el.style.userSelect = "";
60
+ });
61
+ divider.addEventListener("pointermove", (ev) => {
62
+ if (!dragging) return;
63
+ const rect = el.getBoundingClientRect();
64
+ if (vertical) {
65
+ if (rect.height <= 0) return;
66
+ const p = (ev.clientY - rect.top) / rect.height * 100;
67
+ write(p);
68
+ } else {
69
+ if (rect.width <= 0) return;
70
+ const p = (ev.clientX - rect.left) / rect.width * 100;
71
+ write(p);
72
+ }
73
+ });
74
+ divider.addEventListener("dblclick", () => write(50));
75
+ divider.addEventListener("keydown", (ev) => {
76
+ const current = readCurrent();
77
+ let next = null;
78
+ if (vertical) {
79
+ if (ev.key === "ArrowUp") next = current - step;
80
+ else if (ev.key === "ArrowDown") next = current + step;
81
+ } else {
82
+ if (ev.key === "ArrowLeft") next = current - step;
83
+ else if (ev.key === "ArrowRight") next = current + step;
84
+ }
85
+ if (ev.key === "Home") next = min;
86
+ else if (ev.key === "End") next = max;
87
+ if (next === null) return;
88
+ ev.preventDefault();
89
+ write(next);
90
+ });
91
+ }
92
+ function initSplitPane(root) {
93
+ if (typeof document === "undefined") return;
94
+ const scope = root ?? document;
95
+ const panes = [];
96
+ if (scope instanceof Element && scope.matches(".split-pane[data-split-pane]")) panes.push(scope);
97
+ panes.push(...Array.from(scope.querySelectorAll(".split-pane[data-split-pane]")));
98
+ for (const p of panes) setup(p);
99
+ }
100
+ export {
101
+ initSplitPane
102
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * 为 root 下每个 [data-tabs] 容器初始化(root 自身是 [data-tabs] 也算)。
3
+ * 返回销毁函数:调用后移除所有容器监听与 window hashchange,并复位幂等标记
4
+ * (销毁后的容器可被再次 initTabs 重新接管)。SSR 下返回 no-op 函数。
5
+ */
6
+ declare function initTabs(root?: ParentNode): () => void;
7
+
8
+ export { initTabs };