@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
@@ -0,0 +1,82 @@
1
+ /** 布局锚矩形(视口坐标,宽高齐全,元素锚或手工锚共用此形状)。 */
2
+ interface PopoverRect {
3
+ left: number;
4
+ top: number;
5
+ right: number;
6
+ bottom: number;
7
+ width: number;
8
+ height: number;
9
+ }
10
+ /** 视口尺寸(可用空间计算的基准)。 */
11
+ interface PopoverViewport {
12
+ width: number;
13
+ height: number;
14
+ }
15
+ /** 浮层相对锚的展开侧。 */
16
+ type PopoverSide = 'top' | 'bottom' | 'left' | 'right';
17
+ /** 浮层与锚的对齐方式(stretch = 纵向侧时随锚宽拉伸)。 */
18
+ type PopoverAlign = 'start' | 'center' | 'end' | 'stretch';
19
+ interface PopoverLayoutOptions {
20
+ side?: PopoverSide;
21
+ align?: PopoverAlign;
22
+ offset?: number;
23
+ margin?: number;
24
+ minWidth?: number;
25
+ maxWidth?: number;
26
+ minHeight?: number;
27
+ maxHeight?: number;
28
+ /** 内容预期高度:避免用被裁剪的实测高度参与选侧 */
29
+ contentHeightHint?: number;
30
+ }
31
+ interface PanelSizing {
32
+ /** 固定宽(最高优先,跳过 min/max 夹取) */
33
+ width?: number;
34
+ /** 最小宽(默认 = 锚点/trigger 宽或组件 CSS 默认) */
35
+ minWidth?: number;
36
+ /** 最大宽 */
37
+ maxWidth?: number;
38
+ /** 最小高(与 PopoverLayoutOptions.minHeight 对齐) */
39
+ minHeight?: number;
40
+ /** 高度上限 */
41
+ maxHeight?: number;
42
+ }
43
+ /** 从元素读取 data-panel-* 尺寸属性(非法/缺失字段跳过,不抛异常)。 */
44
+ declare function readPanelSizing(el: Element | null | undefined): PanelSizing;
45
+ /** 合并尺寸:程序面 overrides 优先于属性面(undefined 字段不覆盖)。 */
46
+ declare function resolvePanelSizing(el: Element | null | undefined, overrides?: PanelSizing | null): PanelSizing;
47
+ /** 把 PanelSizing 折算进布局 opts:固定宽 → min=max;否则 min/max 夹取;maxHeight 透传。 */
48
+ declare function sizingToLayout<T extends {
49
+ minWidth?: number;
50
+ maxWidth?: number;
51
+ maxHeight?: number;
52
+ }>(sizing: PanelSizing, opts: T): T;
53
+ /** 把尺寸契约内联到自建面板(dropdown / context-menu / date-picker 等 fixed 面板)。
54
+ * 传 null 复位全部内联尺寸——单例面板跨实例复用时防「上次的设置串味」。
55
+ * 视口守卫:width / maxWidth 一律夹到 视口宽 − 24px(窄屏上过大的 data-panel-width 不溢出,
56
+ * 与 computePopoverLayout 的 availableWidth 同一纪律,不再两套标准)。 */
57
+ declare function applyPanelSizing(panel: HTMLElement, sizing: PanelSizing | null): void;
58
+ interface PopoverLayout {
59
+ side: PopoverSide;
60
+ left: number;
61
+ top: number;
62
+ width: number;
63
+ maxHeight: number;
64
+ }
65
+ interface OpenPopoverOptions extends PopoverLayoutOptions {
66
+ anchor: Element | PopoverRect;
67
+ /** 统一面板尺寸契约(优先于面板元素的 data-panel-* 与上方直接尺寸字段) */
68
+ sizing?: PanelSizing;
69
+ /** 关闭(含外点/Esc 触发的关闭)后的回调 */
70
+ onClose?: () => void;
71
+ }
72
+ /** 视口智能布局:返回 {side, left, top, width, maxHeight}。
73
+ * 纵向侧(top/bottom):选侧按上下空间,宽按 min/max 夹取,高按内容预期。
74
+ * 横向侧(left/right):锚点左右翻转,宽取 min(无内容测量,横向侧取下限),
75
+ * 高占满可用空间,纵向按 align(start/center/end;stretch 视作 start)贴锚。 */
76
+ declare function computePopoverLayout(anchor: PopoverRect, viewport: PopoverViewport, opts?: PopoverLayoutOptions): PopoverLayout;
77
+ /** 打开浮层:panel 移入 body 并按 anchor 布局;重复调用同一 panel 为先关后开。 */
78
+ declare function openPopover(panel: HTMLElement, options: OpenPopoverOptions): void;
79
+ /** 关闭浮层:移除监听并把 panel 还原到打开前的位置与 hidden 状态。 */
80
+ declare function closePopover(panel: HTMLElement): void;
81
+
82
+ export { type OpenPopoverOptions, type PanelSizing, type PopoverAlign, type PopoverLayout, type PopoverLayoutOptions, type PopoverRect, type PopoverSide, type PopoverViewport, applyPanelSizing, closePopover, computePopoverLayout, openPopover, readPanelSizing, resolvePanelSizing, sizingToLayout };
@@ -1,4 +1,63 @@
1
1
  // src/behaviors/popover.ts
2
+ var PANEL_SIZING_ATTRS = {
3
+ width: "data-panel-width",
4
+ minWidth: "data-panel-min",
5
+ maxWidth: "data-panel-max",
6
+ minHeight: "data-panel-min-height",
7
+ maxHeight: "data-panel-max-height"
8
+ };
9
+ function readPanelSizing(el) {
10
+ const out = {};
11
+ if (!el) return out;
12
+ for (const key of Object.keys(PANEL_SIZING_ATTRS)) {
13
+ const raw = el.getAttribute(PANEL_SIZING_ATTRS[key]);
14
+ if (raw == null) continue;
15
+ const n = Number(raw);
16
+ if (Number.isFinite(n) && n > 0) out[key] = n;
17
+ }
18
+ return out;
19
+ }
20
+ function resolvePanelSizing(el, overrides) {
21
+ const base = readPanelSizing(el);
22
+ if (!overrides) return base;
23
+ for (const key of Object.keys(overrides)) {
24
+ const v = overrides[key];
25
+ if (typeof v === "number" && Number.isFinite(v) && v > 0) base[key] = v;
26
+ }
27
+ return base;
28
+ }
29
+ function sizingToLayout(sizing, opts) {
30
+ const out = { ...opts };
31
+ if (sizing.minWidth != null) out.minWidth = sizing.minWidth;
32
+ if (sizing.maxWidth != null) out.maxWidth = sizing.maxWidth;
33
+ if (sizing.width != null) {
34
+ out.minWidth = sizing.width;
35
+ out.maxWidth = sizing.width;
36
+ }
37
+ if (sizing.maxHeight != null) out.maxHeight = sizing.maxHeight;
38
+ return out;
39
+ }
40
+ function applyPanelSizing(panel, sizing) {
41
+ if (sizing == null) {
42
+ panel.style.width = "";
43
+ panel.style.minWidth = "";
44
+ panel.style.maxWidth = "";
45
+ panel.style.minHeight = "";
46
+ panel.style.maxHeight = "";
47
+ return;
48
+ }
49
+ const vw = typeof document === "undefined" ? Number.POSITIVE_INFINITY : document.documentElement.clientWidth;
50
+ const fitW = (n) => Math.min(n, Math.max(80, vw - 24));
51
+ if (sizing.width != null) {
52
+ const w = fitW(sizing.width);
53
+ panel.style.width = `${w}px`;
54
+ panel.style.maxWidth = `${w}px`;
55
+ }
56
+ if (sizing.minWidth != null) panel.style.minWidth = `${fitW(sizing.minWidth)}px`;
57
+ if (sizing.maxWidth != null && sizing.width == null) panel.style.maxWidth = `${fitW(sizing.maxWidth)}px`;
58
+ if (sizing.minHeight != null) panel.style.minHeight = `${sizing.minHeight}px`;
59
+ if (sizing.maxHeight != null) panel.style.maxHeight = `${sizing.maxHeight}px`;
60
+ }
2
61
  var DEFAULT_MIN_WIDTH = 220;
3
62
  var DEFAULT_MAX_WIDTH = 420;
4
63
  var DEFAULT_MIN_HEIGHT = 120;
@@ -23,13 +82,35 @@ function horizontalAnchor(anchor, width, align) {
23
82
  function computePopoverLayout(anchor, viewport, opts = {}) {
24
83
  const margin = opts.margin ?? 12;
25
84
  const offset = opts.offset ?? 8;
85
+ const preferredSide = opts.side ?? "top";
86
+ const align = opts.align ?? "start";
87
+ if (preferredSide === "left" || preferredSide === "right") {
88
+ const availableLeft = Math.max(0, anchor.left - margin - offset);
89
+ const availableRight = Math.max(0, viewport.width - anchor.right - margin - offset);
90
+ const side2 = (preferredSide === "left" ? availableLeft : availableRight) >= DEFAULT_MIN_WIDTH ? preferredSide : preferredSide === "left" ? "right" : "left";
91
+ const minWidth2 = Math.max(1, opts.minWidth ?? DEFAULT_MIN_WIDTH);
92
+ const maxWidth2 = Math.min(
93
+ Math.max(opts.maxWidth ?? DEFAULT_MAX_WIDTH, minWidth2),
94
+ side2 === "left" ? availableLeft : availableRight
95
+ );
96
+ const width2 = minWidth2;
97
+ const left2 = clamp(
98
+ side2 === "left" ? anchor.left - offset - width2 : anchor.right + offset,
99
+ margin,
100
+ viewport.width - margin - width2
101
+ );
102
+ const availableHeight2 = Math.max(80, viewport.height - margin * 2);
103
+ const maxHeight2 = Math.min(opts.maxHeight ?? DEFAULT_MAX_HEIGHT, availableHeight2);
104
+ const desired = clamp(opts.contentHeightHint ?? Math.min(anchor.height, maxHeight2), 40, maxHeight2);
105
+ const anchorTop = align === "center" ? anchor.top + anchor.height / 2 - desired / 2 : align === "end" ? anchor.bottom - desired : anchor.top;
106
+ const top2 = clamp(anchorTop, margin, viewport.height - margin - desired);
107
+ return { side: side2, left: left2, top: top2, width: width2, maxHeight: maxHeight2 };
108
+ }
26
109
  const availableWidth = Math.max(80, viewport.width - margin * 2);
27
110
  const requestedMinWidth = Math.max(1, opts.minWidth ?? DEFAULT_MIN_WIDTH);
28
111
  const requestedMaxWidth = Math.max(requestedMinWidth, opts.maxWidth ?? DEFAULT_MAX_WIDTH);
29
112
  const maxWidth = Math.min(requestedMaxWidth, availableWidth);
30
113
  const minWidth = Math.min(requestedMinWidth, maxWidth);
31
- const preferredSide = opts.side ?? "top";
32
- const align = opts.align ?? "start";
33
114
  const availableTop = Math.max(0, anchor.top - margin - offset);
34
115
  const availableBottom = Math.max(0, viewport.height - anchor.bottom - margin - offset);
35
116
  const availableHeight = Math.max(80, viewport.height - margin * 2);
@@ -107,7 +188,8 @@ function onKeyDown(e) {
107
188
  function openPopover(panel, options) {
108
189
  if (typeof document === "undefined") return;
109
190
  if (openPanels.has(panel)) closePopover(panel);
110
- const { anchor, onClose, ...layoutOpts } = options;
191
+ const { anchor, onClose, sizing, ...rest } = options;
192
+ const layoutOpts = sizingToLayout(resolvePanelSizing(panel, sizing), rest);
111
193
  const placeholder = document.createComment("popover-anchor");
112
194
  panel.parentNode?.insertBefore(placeholder, panel);
113
195
  const wasHidden = panel.hidden;
@@ -139,7 +221,8 @@ function closePopover(panel) {
139
221
  if (!state) return;
140
222
  openPanels.delete(panel);
141
223
  panel.style.visibility = state.prevVisibility;
142
- panel.hidden = state.wasHidden;
224
+ if (typeof state.wasHidden === "string") panel.setAttribute("hidden", state.wasHidden);
225
+ else panel.hidden = state.wasHidden;
143
226
  panel.style.left = "";
144
227
  panel.style.top = "";
145
228
  panel.style.width = "";
@@ -158,7 +241,11 @@ function closePopover(panel) {
158
241
  state.onClose?.();
159
242
  }
160
243
  export {
244
+ applyPanelSizing,
161
245
  closePopover,
162
246
  computePopoverLayout,
163
- openPopover
247
+ openPopover,
248
+ readPanelSizing,
249
+ resolvePanelSizing,
250
+ sizingToLayout
164
251
  };
@@ -0,0 +1,6 @@
1
+ /** 为 root 下每个 [data-select] 容器初始化(root 自身是 [data-select] 也算)。
2
+ * 返回销毁函数:摘除本次初始化容器的全部监听并复位幂等标记(可重新 init);
3
+ * 最后一个容器销毁时同时摘除 document 级单例委托。 */
4
+ declare function initSelect(root?: ParentNode): () => void;
5
+
6
+ export { initSelect };
@@ -1,19 +1,188 @@
1
+ // src/behaviors/popover.ts
2
+ var PANEL_SIZING_ATTRS = {
3
+ width: "data-panel-width",
4
+ minWidth: "data-panel-min",
5
+ maxWidth: "data-panel-max",
6
+ minHeight: "data-panel-min-height",
7
+ maxHeight: "data-panel-max-height"
8
+ };
9
+ function readPanelSizing(el) {
10
+ const out = {};
11
+ if (!el) return out;
12
+ for (const key of Object.keys(PANEL_SIZING_ATTRS)) {
13
+ const raw = el.getAttribute(PANEL_SIZING_ATTRS[key]);
14
+ if (raw == null) continue;
15
+ const n = Number(raw);
16
+ if (Number.isFinite(n) && n > 0) out[key] = n;
17
+ }
18
+ return out;
19
+ }
20
+ function resolvePanelSizing(el, overrides) {
21
+ const base = readPanelSizing(el);
22
+ if (!overrides) return base;
23
+ for (const key of Object.keys(overrides)) {
24
+ const v = overrides[key];
25
+ if (typeof v === "number" && Number.isFinite(v) && v > 0) base[key] = v;
26
+ }
27
+ return base;
28
+ }
29
+ var DEFAULT_MIN_WIDTH = 220;
30
+ var DEFAULT_MAX_WIDTH = 420;
31
+ var DEFAULT_MIN_HEIGHT = 120;
32
+ var DEFAULT_MAX_HEIGHT = 420;
33
+ function clamp(value, min, max) {
34
+ if (max < min) return min;
35
+ return Math.min(max, Math.max(min, value));
36
+ }
37
+ function chooseSide(preferredSide, availableTop, availableBottom, desiredHeight) {
38
+ const preferredSpace = preferredSide === "top" ? availableTop : availableBottom;
39
+ const oppositeSpace = preferredSide === "top" ? availableBottom : availableTop;
40
+ if (preferredSpace >= desiredHeight) return preferredSide;
41
+ if (oppositeSpace >= desiredHeight) return preferredSide === "top" ? "bottom" : "top";
42
+ if (preferredSpace >= oppositeSpace) return preferredSide;
43
+ return preferredSide === "top" ? "bottom" : "top";
44
+ }
45
+ function horizontalAnchor(anchor, width, align) {
46
+ if (align === "center") return anchor.left + anchor.width / 2 - width / 2;
47
+ if (align === "end") return anchor.right - width;
48
+ return anchor.left;
49
+ }
50
+ function computePopoverLayout(anchor, viewport, opts = {}) {
51
+ const margin = opts.margin ?? 12;
52
+ const offset = opts.offset ?? 8;
53
+ const preferredSide = opts.side ?? "top";
54
+ const align = opts.align ?? "start";
55
+ if (preferredSide === "left" || preferredSide === "right") {
56
+ const availableLeft = Math.max(0, anchor.left - margin - offset);
57
+ const availableRight = Math.max(0, viewport.width - anchor.right - margin - offset);
58
+ const side2 = (preferredSide === "left" ? availableLeft : availableRight) >= DEFAULT_MIN_WIDTH ? preferredSide : preferredSide === "left" ? "right" : "left";
59
+ const minWidth2 = Math.max(1, opts.minWidth ?? DEFAULT_MIN_WIDTH);
60
+ const maxWidth2 = Math.min(
61
+ Math.max(opts.maxWidth ?? DEFAULT_MAX_WIDTH, minWidth2),
62
+ side2 === "left" ? availableLeft : availableRight
63
+ );
64
+ const width2 = minWidth2;
65
+ const left2 = clamp(
66
+ side2 === "left" ? anchor.left - offset - width2 : anchor.right + offset,
67
+ margin,
68
+ viewport.width - margin - width2
69
+ );
70
+ const availableHeight2 = Math.max(80, viewport.height - margin * 2);
71
+ const maxHeight2 = Math.min(opts.maxHeight ?? DEFAULT_MAX_HEIGHT, availableHeight2);
72
+ const desired = clamp(opts.contentHeightHint ?? Math.min(anchor.height, maxHeight2), 40, maxHeight2);
73
+ const anchorTop = align === "center" ? anchor.top + anchor.height / 2 - desired / 2 : align === "end" ? anchor.bottom - desired : anchor.top;
74
+ const top2 = clamp(anchorTop, margin, viewport.height - margin - desired);
75
+ return { side: side2, left: left2, top: top2, width: width2, maxHeight: maxHeight2 };
76
+ }
77
+ const availableWidth = Math.max(80, viewport.width - margin * 2);
78
+ const requestedMinWidth = Math.max(1, opts.minWidth ?? DEFAULT_MIN_WIDTH);
79
+ const requestedMaxWidth = Math.max(requestedMinWidth, opts.maxWidth ?? DEFAULT_MAX_WIDTH);
80
+ const maxWidth = Math.min(requestedMaxWidth, availableWidth);
81
+ const minWidth = Math.min(requestedMinWidth, maxWidth);
82
+ const availableTop = Math.max(0, anchor.top - margin - offset);
83
+ const availableBottom = Math.max(0, viewport.height - anchor.bottom - margin - offset);
84
+ const availableHeight = Math.max(80, viewport.height - margin * 2);
85
+ const minHeight = Math.min(Math.max(80, opts.minHeight ?? DEFAULT_MIN_HEIGHT), availableHeight);
86
+ const requestedMaxHeight = Math.max(minHeight, opts.maxHeight ?? DEFAULT_MAX_HEIGHT);
87
+ const fallbackHeight = Math.min(260, requestedMaxHeight);
88
+ const contentHeight = Math.max(opts.contentHeightHint ?? 0, fallbackHeight);
89
+ const desiredHeight = clamp(contentHeight, minHeight, Math.min(requestedMaxHeight, availableHeight));
90
+ const side = chooseSide(preferredSide, availableTop, availableBottom, desiredHeight);
91
+ const sideSpace = side === "top" ? availableTop : availableBottom;
92
+ const maxHeight = Math.min(
93
+ requestedMaxHeight,
94
+ Math.max(80, sideSpace || Math.max(availableTop, availableBottom)),
95
+ availableHeight
96
+ );
97
+ const positionedHeight = clamp(desiredHeight, 40, maxHeight);
98
+ const width = align === "stretch" ? clamp(anchor.width, minWidth, maxWidth) : maxWidth;
99
+ const left = clamp(horizontalAnchor(anchor, width, align), margin, viewport.width - margin - width);
100
+ const desiredTop = side === "top" ? anchor.top - offset - positionedHeight : anchor.bottom + offset;
101
+ const top = clamp(desiredTop, margin, viewport.height - margin - positionedHeight);
102
+ return { side, left, top, width, maxHeight };
103
+ }
104
+
105
+ // src/behaviors/events.ts
106
+ function emitIcen(source, type, detail) {
107
+ return source.dispatchEvent(
108
+ new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable: true })
109
+ );
110
+ }
111
+
1
112
  // src/behaviors/select.ts
113
+ var openSelect = null;
114
+ var globalBound = false;
115
+ var PANEL_OFFSET = 4;
116
+ var PANEL_MAX_HEIGHT = 240;
117
+ function onGlobalPointerDown(ev) {
118
+ const cur = openSelect;
119
+ if (!cur) return;
120
+ const t = ev.target;
121
+ if (!(t instanceof Node)) return;
122
+ if (cur.container.contains(t) || cur.panel.contains(t)) return;
123
+ cur.close();
124
+ }
125
+ function onGlobalKeyDown(ev) {
126
+ if (ev.key !== "Escape") return;
127
+ const cur = openSelect;
128
+ if (!cur) return;
129
+ cur.close();
130
+ cur.trigger.focus();
131
+ }
132
+ function onWindowChange() {
133
+ const cur = openSelect;
134
+ if (!cur) return;
135
+ if (!cur.trigger.isConnected) {
136
+ cur.close();
137
+ return;
138
+ }
139
+ cur.reposition();
140
+ }
141
+ function bindGlobalListeners() {
142
+ if (globalBound) return;
143
+ globalBound = true;
144
+ document.addEventListener("pointerdown", onGlobalPointerDown);
145
+ document.addEventListener("keydown", onGlobalKeyDown);
146
+ }
147
+ function bindOpenListeners() {
148
+ window.addEventListener("scroll", onWindowChange, true);
149
+ window.addEventListener("resize", onWindowChange);
150
+ }
151
+ function unbindOpenListeners() {
152
+ window.removeEventListener("scroll", onWindowChange, true);
153
+ window.removeEventListener("resize", onWindowChange);
154
+ }
2
155
  function setup(container) {
3
156
  const el = container;
4
- if (el.__icenSelectInit) return;
5
- el.__icenSelectInit = true;
157
+ if (el.__icenSelectInit) return () => {
158
+ };
159
+ const ac = new AbortController();
160
+ const on = (t, ev, fn) => {
161
+ const listener = (evt) => fn(evt);
162
+ t.addEventListener(ev, listener, { signal: ac.signal });
163
+ };
6
164
  const triggerEl = container.querySelector(".select-trigger");
7
165
  const panelEl = container.querySelector(".select-panel");
8
166
  const valueEl = container.querySelector(".select-value");
9
167
  const hiddenInput = container.querySelector("input[data-select-value]");
10
- if (!triggerEl || !panelEl) return;
168
+ if (!triggerEl || !panelEl) return () => {
169
+ };
170
+ el.__icenSelectInit = true;
11
171
  const trigger = triggerEl;
12
172
  const panel = panelEl;
13
173
  const isMultiple = container.hasAttribute("data-select-multiple");
14
174
  const isSearchable = container.hasAttribute("data-select-search");
15
175
  const maxSelect = Number(container.getAttribute("data-select-max")) || 0;
176
+ const panelSizing = resolvePanelSizing(container);
177
+ function resolvedPanelWidth(triggerWidth) {
178
+ let w = triggerWidth;
179
+ if (panelSizing.minWidth != null) w = Math.max(w, panelSizing.minWidth);
180
+ if (panelSizing.maxWidth != null) w = Math.min(w, panelSizing.maxWidth);
181
+ if (panelSizing.width != null) w = panelSizing.width;
182
+ return w;
183
+ }
16
184
  const placeholder = container.getAttribute("data-select-placeholder") ?? (valueEl?.classList.contains("is-empty") ? valueEl?.textContent ?? "" : "");
185
+ if (isMultiple) panel.setAttribute("data-select-multiple", "");
17
186
  let searchInput = null;
18
187
  let focusIdx = -1;
19
188
  if (isSearchable && !panel.querySelector(".select-search-input")) {
@@ -79,8 +248,8 @@ function setup(container) {
79
248
  setFocus(firstEnabled());
80
249
  }
81
250
  if (searchInput) {
82
- searchInput.addEventListener("input", () => filterOptions(searchInput.value));
83
- searchInput.addEventListener("keydown", (ev) => {
251
+ on(searchInput, "input", () => filterOptions(searchInput.value));
252
+ on(searchInput, "keydown", (ev) => {
84
253
  if (ev.key === "Escape") {
85
254
  ev.stopPropagation();
86
255
  searchInput.value = "";
@@ -89,11 +258,38 @@ function setup(container) {
89
258
  }
90
259
  });
91
260
  }
261
+ function positionPanel() {
262
+ const rect = trigger.getBoundingClientRect();
263
+ const panelWidth = resolvedPanelWidth(rect.width);
264
+ const layout = computePopoverLayout(
265
+ rect,
266
+ { width: window.innerWidth, height: window.innerHeight },
267
+ {
268
+ side: "bottom",
269
+ align: "stretch",
270
+ offset: PANEL_OFFSET,
271
+ minWidth: panelWidth,
272
+ maxWidth: panelWidth,
273
+ maxHeight: panelSizing.maxHeight ?? PANEL_MAX_HEIGHT
274
+ }
275
+ );
276
+ panel.style.left = `${layout.left + window.scrollX}px`;
277
+ panel.style.top = `${layout.top + window.scrollY}px`;
278
+ panel.style.minWidth = `${layout.width}px`;
279
+ panel.style.width = `${layout.width}px`;
280
+ panel.style.maxHeight = `${layout.maxHeight}px`;
281
+ panel.dataset.side = layout.side;
282
+ }
92
283
  function openPanel() {
93
284
  if (isOpen()) return;
285
+ openSelect?.close();
286
+ if (panel.parentElement !== document.body) document.body.appendChild(panel);
94
287
  panel.hidden = false;
95
288
  container.classList.add("is-open");
96
289
  trigger.setAttribute("aria-expanded", "true");
290
+ positionPanel();
291
+ openSelect = { container, trigger, panel, close: closePanel, reposition: positionPanel };
292
+ bindOpenListeners();
97
293
  if (searchInput) {
98
294
  searchInput.value = "";
99
295
  filterOptions("");
@@ -109,6 +305,8 @@ function setup(container) {
109
305
  container.classList.remove("is-open");
110
306
  trigger.setAttribute("aria-expanded", "false");
111
307
  setFocus(-1);
308
+ if (openSelect?.panel === panel) openSelect = null;
309
+ unbindOpenListeners();
112
310
  }
113
311
  function getSelectedValues() {
114
312
  return allOptions().filter((o) => o.classList.contains("is-selected")).map((o) => o.getAttribute("data-value") ?? "");
@@ -116,19 +314,59 @@ function setup(container) {
116
314
  function updateMultiTrigger() {
117
315
  if (!valueEl) return;
118
316
  const selected = allOptions().filter((o) => o.classList.contains("is-selected"));
317
+ let chipsEl = trigger.querySelector(".select-chips");
119
318
  if (selected.length === 0) {
120
319
  valueEl.textContent = placeholder;
121
320
  valueEl.classList.add("is-empty");
321
+ valueEl.hidden = false;
322
+ if (chipsEl) chipsEl.hidden = true;
122
323
  container.removeAttribute("data-value");
123
324
  } else {
124
325
  valueEl.classList.remove("is-empty");
125
- const maxLabel = Number(container.getAttribute("data-select-max-label")) || 2;
126
- if (selected.length <= maxLabel) {
127
- valueEl.textContent = selected.map((o) => o.textContent ?? "").join("\u3001");
128
- } else {
129
- valueEl.textContent = `\u5DF2\u9009 ${selected.length} \u9879`;
326
+ valueEl.hidden = true;
327
+ if (!chipsEl) {
328
+ chipsEl = document.createElement("span");
329
+ chipsEl.className = "select-chips";
330
+ valueEl.insertAdjacentElement("afterend", chipsEl);
130
331
  }
332
+ chipsEl.hidden = false;
333
+ chipsEl.textContent = "";
334
+ const maxLabel = Number(container.getAttribute("data-select-max-label")) || 3;
335
+ const shown = selected.slice(0, maxLabel);
336
+ const rest = selected.length - shown.length;
337
+ for (const o of shown) {
338
+ const label = o.textContent ?? "";
339
+ const chip = document.createElement("span");
340
+ chip.className = "select-chip";
341
+ const lb = document.createElement("span");
342
+ lb.textContent = label;
343
+ chip.appendChild(lb);
344
+ const x = document.createElement("span");
345
+ x.className = "select-chip-x";
346
+ x.setAttribute("role", "button");
347
+ x.setAttribute("tabindex", "-1");
348
+ x.setAttribute("aria-label", `\u79FB\u9664 ${label}`);
349
+ x.textContent = "\xD7";
350
+ on(x, "pointerdown", (ev) => ev.stopPropagation());
351
+ on(x, "click", (ev) => {
352
+ ev.stopPropagation();
353
+ ev.preventDefault();
354
+ o.classList.remove("is-selected");
355
+ updateMultiTrigger();
356
+ syncHiddenInput();
357
+ });
358
+ chip.appendChild(x);
359
+ chipsEl.appendChild(chip);
360
+ }
361
+ if (rest > 0) {
362
+ const more = document.createElement("span");
363
+ more.className = "select-chip select-chip--more";
364
+ more.textContent = `+${rest}`;
365
+ chipsEl.appendChild(more);
366
+ }
367
+ container.setAttribute("data-value", getSelectedValues().join(","));
131
368
  }
369
+ if (clearBtn) clearBtn.hidden = selected.length === 0;
132
370
  if (maxSelect > 0) {
133
371
  const atMax = selected.length >= maxSelect;
134
372
  allOptions().forEach((o) => {
@@ -139,8 +377,10 @@ function setup(container) {
139
377
  }
140
378
  }
141
379
  function syncHiddenInput() {
142
- if (!hiddenInput) return;
143
380
  const values = getSelectedValues();
381
+ const label = allOptions().filter((o) => o.classList.contains("is-selected")).map((o) => (o.textContent ?? "").trim()).join(", ");
382
+ emitIcen(container, "icen:select-change", { value: values[0] ?? "", values, label });
383
+ if (!hiddenInput) return;
144
384
  hiddenInput.value = isMultiple ? values.join(",") : values[0] ?? "";
145
385
  hiddenInput.dispatchEvent(new Event("change", { bubbles: true }));
146
386
  }
@@ -171,16 +411,16 @@ function setup(container) {
171
411
  }
172
412
  const clearBtn = container.querySelector(".select-clear");
173
413
  if (clearBtn && isMultiple) {
174
- clearBtn.addEventListener("click", (ev) => {
414
+ on(clearBtn, "click", (ev) => {
175
415
  ev.stopPropagation();
176
416
  allOptions().forEach((o) => o.classList.remove("is-selected"));
177
417
  updateMultiTrigger();
178
418
  syncHiddenInput();
179
419
  });
180
- clearBtn.addEventListener("pointerdown", (ev) => ev.stopPropagation());
420
+ on(clearBtn, "pointerdown", (ev) => ev.stopPropagation());
181
421
  }
182
- trigger.addEventListener("click", () => isOpen() ? closePanel() : openPanel());
183
- trigger.addEventListener("keydown", (ev) => {
422
+ on(trigger, "click", () => isOpen() ? closePanel() : openPanel());
423
+ on(trigger, "keydown", (ev) => {
184
424
  if (!isOpen()) {
185
425
  if (ev.key === "ArrowDown" || ev.key === "ArrowUp" || ev.key === "Enter" || ev.key === " ") {
186
426
  ev.preventDefault();
@@ -188,20 +428,6 @@ function setup(container) {
188
428
  }
189
429
  return;
190
430
  }
191
- if (searchInput && document.activeElement === searchInput) {
192
- if (ev.key === "ArrowDown") {
193
- ev.preventDefault();
194
- moveFocus(1);
195
- } else if (ev.key === "ArrowUp") {
196
- ev.preventDefault();
197
- moveFocus(-1);
198
- } else if (ev.key === "Enter") {
199
- ev.preventDefault();
200
- const opt = visibleOptions()[focusIdx];
201
- if (opt) choose(opt);
202
- }
203
- return;
204
- }
205
431
  switch (ev.key) {
206
432
  case "ArrowDown":
207
433
  ev.preventDefault();
@@ -224,13 +450,13 @@ function setup(container) {
224
450
  break;
225
451
  }
226
452
  });
227
- panel.addEventListener("click", (ev) => {
453
+ on(panel, "click", (ev) => {
228
454
  const t = ev.target;
229
455
  if (!(t instanceof Element)) return;
230
456
  const opt = t.closest(".select-option");
231
457
  if (opt instanceof HTMLButtonElement && panel.contains(opt)) choose(opt);
232
458
  });
233
- panel.addEventListener("mouseover", (ev) => {
459
+ on(panel, "mouseover", (ev) => {
234
460
  const t = ev.target;
235
461
  if (!(t instanceof Element)) return;
236
462
  const opt = t.closest(".select-option");
@@ -238,23 +464,39 @@ function setup(container) {
238
464
  const idx = visibleOptions().indexOf(opt);
239
465
  if (idx >= 0 && !opt.classList.contains("is-disabled")) setFocus(idx);
240
466
  });
241
- document.addEventListener("pointerdown", (ev) => {
242
- if (isOpen() && ev.target instanceof Node && !container.contains(ev.target)) closePanel();
243
- });
244
- document.addEventListener("keydown", (ev) => {
245
- if (ev.key === "Escape" && isOpen()) {
246
- closePanel();
247
- trigger.focus();
248
- }
249
- });
250
467
  if (isMultiple) updateMultiTrigger();
468
+ return () => {
469
+ if (openSelect?.panel === panel) closePanel();
470
+ ac.abort();
471
+ el.__icenSelectInit = false;
472
+ };
251
473
  }
252
- function initSelect(root = document) {
253
- if (typeof document === "undefined") return;
474
+ var activeSetups = /* @__PURE__ */ new Set();
475
+ function initSelect(root) {
476
+ if (typeof document === "undefined") return () => {
477
+ };
478
+ const scope = root ?? document;
479
+ bindGlobalListeners();
254
480
  const containers = [];
255
- if (root instanceof Element && root.matches("[data-select]")) containers.push(root);
256
- containers.push(...Array.from(root.querySelectorAll("[data-select]")));
257
- for (const c of containers) setup(c);
481
+ if (scope instanceof Element && scope.matches("[data-select]")) containers.push(scope);
482
+ containers.push(...Array.from(scope.querySelectorAll("[data-select]")));
483
+ const disposers = [];
484
+ for (const c of containers) {
485
+ const dispose = setup(c);
486
+ activeSetups.add(dispose);
487
+ disposers.push(dispose);
488
+ }
489
+ return () => {
490
+ for (const d of disposers) {
491
+ d();
492
+ activeSetups.delete(d);
493
+ }
494
+ if (activeSetups.size === 0 && globalBound) {
495
+ document.removeEventListener("pointerdown", onGlobalPointerDown);
496
+ document.removeEventListener("keydown", onGlobalKeyDown);
497
+ globalBound = false;
498
+ }
499
+ };
258
500
  }
259
501
  export {
260
502
  initSelect
@@ -0,0 +1,7 @@
1
+ /**
2
+ * 为 root 下每个 .sidebar 初始化(root 自身是 .sidebar 也算)。
3
+ * 返回销毁函数:移除本次挂上的容器监听并复位幂等标记(销毁后可重新 init)。SSR 下返回 no-op。
4
+ */
5
+ declare function initSidebar(root?: ParentNode): () => void;
6
+
7
+ export { initSidebar };