@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,42 @@
1
+ /** 编程命令条目(openCommandPalette 的 opts.items 元素类型)。 */
2
+ interface CommandPaletteCommand {
3
+ /** 条目名(.command-palette-item-label,textContent 渲染) */
4
+ label: string;
5
+ /** 副标题(.command-palette-item-hint) */
6
+ hint?: string;
7
+ /** 额外搜索关键字(data-command-palette-keyword) */
8
+ keyword?: string;
9
+ /** 选中回调:先派 icen:menu-select,再执行本回调 */
10
+ onSelect?: () => void;
11
+ /** 选中后保持面板打开(data-command-palette-keep-open) */
12
+ keepOpen?: boolean;
13
+ /** 附加到条目元素的自定义属性(如 data-*) */
14
+ attrs?: Record<string, string>;
15
+ }
16
+ interface CommandPaletteOpenOptions {
17
+ /** 编程条目:传入时重建面板条目 DOM(替换面板内全部 .command-palette-group) */
18
+ items?: CommandPaletteCommand[];
19
+ /** 覆盖默认 includes 过滤(query 为原始输入;空 query 仍视为匹配全部)。只作用于编程条目 */
20
+ filter?: (query: string, item: CommandPaletteCommand) => boolean;
21
+ }
22
+ /**
23
+ * 打开指定 id 的命令面板。打开成功后从 document 广播 icen:command-palette-open。
24
+ * opts.items 传入时重建面板条目 DOM(编程条目,onSelect 选中时调用);
25
+ * opts.filter 覆盖默认 includes 过滤(仅编程条目生效);幂等不变(同 id 重复打开 no-op)。
26
+ */
27
+ declare function openCommandPalette(id: string, opts?: CommandPaletteOpenOptions): void;
28
+ /**
29
+ * 关闭指定 id 的命令面板(backdrop 已被外部移除时也照常复位状态/监听/滚动锁)。
30
+ * 关闭已打开的面板后从 document 广播 icen:command-palette-close;
31
+ * 关闭未被接管打开的面板(幂等空操作路径)不派发。
32
+ */
33
+ declare function closeCommandPalette(id: string): void;
34
+ /**
35
+ * 初始化:委托监听触发器 / 搜索 / 外点 / 全局快捷键。
36
+ * 返回销毁函数:摘除全部委托监听并复位幂等标记(销毁后可重新 init);
37
+ * 销毁时若有面板开着会顺带关闭(复位滚动锁/监听/焦点)。
38
+ * 重复调用安全:已初始化时返回既有销毁函数(同一份,共享)。SSR 下返回 no-op。
39
+ */
40
+ declare function initCommandPalette(root?: ParentNode): () => void;
41
+
42
+ export { type CommandPaletteCommand, type CommandPaletteOpenOptions, closeCommandPalette, initCommandPalette, openCommandPalette };
@@ -1,11 +1,111 @@
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 applyPanelSizing(panel, sizing) {
21
+ if (sizing == null) {
22
+ panel.style.width = "";
23
+ panel.style.minWidth = "";
24
+ panel.style.maxWidth = "";
25
+ panel.style.minHeight = "";
26
+ panel.style.maxHeight = "";
27
+ return;
28
+ }
29
+ const vw = typeof document === "undefined" ? Number.POSITIVE_INFINITY : document.documentElement.clientWidth;
30
+ const fitW = (n) => Math.min(n, Math.max(80, vw - 24));
31
+ if (sizing.width != null) {
32
+ const w = fitW(sizing.width);
33
+ panel.style.width = `${w}px`;
34
+ panel.style.maxWidth = `${w}px`;
35
+ }
36
+ if (sizing.minWidth != null) panel.style.minWidth = `${fitW(sizing.minWidth)}px`;
37
+ if (sizing.maxWidth != null && sizing.width == null) panel.style.maxWidth = `${fitW(sizing.maxWidth)}px`;
38
+ if (sizing.minHeight != null) panel.style.minHeight = `${sizing.minHeight}px`;
39
+ if (sizing.maxHeight != null) panel.style.maxHeight = `${sizing.maxHeight}px`;
40
+ }
41
+
42
+ // src/behaviors/events.ts
43
+ function emitIcen(source, type, detail) {
44
+ return source.dispatchEvent(
45
+ new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable: true })
46
+ );
47
+ }
48
+
49
+ // src/behaviors/context-menu.ts
50
+ function emitMenuSelect(source, item) {
51
+ emitIcen(item, "icen:menu-select", {
52
+ source,
53
+ item,
54
+ value: item.dataset.value ?? "",
55
+ /* 优先专用 label 子元素(避开快捷键/图标文本混入),无则退整项文本 */
56
+ label: (item.querySelector(".menu-item-label, .command-item-label")?.textContent ?? item.textContent ?? "").trim().slice(0, 80)
57
+ });
58
+ }
59
+
60
+ // src/behaviors/modal.ts
61
+ var FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]), [contenteditable]:not([contenteditable="false"]), audio, video, details>summary';
62
+
1
63
  // src/behaviors/command-palette.ts
2
- var FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), [tabindex]:not([tabindex="-1"])';
3
64
  var openId = null;
4
65
  var restoreFocusTo = null;
5
66
  var prevBodyOverflow = "";
6
67
  var keyHandler = null;
7
68
  var globalKeyHandler = null;
8
- var initialized = false;
69
+ var teardown = null;
70
+ var programmaticCommands = /* @__PURE__ */ new WeakMap();
71
+ var customFilter = null;
72
+ function buildCommandItems(panel, commands) {
73
+ const body = panel.querySelector(".command-palette-body");
74
+ if (!body) return;
75
+ body.querySelectorAll(".command-palette-group").forEach((g) => g.remove());
76
+ const group = document.createElement("div");
77
+ group.className = "command-palette-group";
78
+ for (const cmd of commands) group.appendChild(buildCommandItem(cmd));
79
+ const empty = body.querySelector(".command-palette-empty");
80
+ if (empty) body.insertBefore(group, empty);
81
+ else body.appendChild(group);
82
+ }
83
+ function buildCommandItem(cmd) {
84
+ const btn = document.createElement("button");
85
+ btn.type = "button";
86
+ btn.className = "command-palette-item";
87
+ const label = document.createElement("span");
88
+ label.className = "command-palette-item-label";
89
+ label.textContent = cmd.label;
90
+ btn.appendChild(label);
91
+ if (cmd.hint) {
92
+ const hint = document.createElement("span");
93
+ hint.className = "command-palette-item-hint";
94
+ hint.textContent = cmd.hint;
95
+ btn.appendChild(hint);
96
+ }
97
+ if (cmd.keyword) btn.setAttribute("data-command-palette-keyword", cmd.keyword);
98
+ if (cmd.keepOpen) btn.setAttribute("data-command-palette-keep-open", "");
99
+ if (cmd.attrs) {
100
+ for (const [name, value] of Object.entries(cmd.attrs)) btn.setAttribute(name, value);
101
+ }
102
+ programmaticCommands.set(btn, cmd);
103
+ btn.addEventListener("click", () => {
104
+ emitMenuSelect(btn.closest(".command-palette") ?? btn, btn);
105
+ cmd.onSelect?.();
106
+ });
107
+ return btn;
108
+ }
9
109
  function findBackdrop(id) {
10
110
  return document.querySelector(
11
111
  `.command-palette-backdrop[data-command-palette="${CSS.escape(id)}"]`
@@ -13,7 +113,9 @@ function findBackdrop(id) {
13
113
  }
14
114
  function getVisibleItems(panel) {
15
115
  return Array.from(
16
- panel.querySelectorAll(".command-palette-item:not([hidden]):not([disabled])")
116
+ panel.querySelectorAll(
117
+ '.command-palette-item:not([hidden]):not([disabled]):not([aria-disabled="true"])'
118
+ )
17
119
  ).filter((el) => el.offsetParent !== null || el.getClientRects().length > 0);
18
120
  }
19
121
  function isDisabledItem(el) {
@@ -32,10 +134,16 @@ function filterPanel(panel, query) {
32
134
  let visibleCount = 0;
33
135
  panel.querySelectorAll(".command-palette-item").forEach((item) => {
34
136
  if (isDisabledItem(item)) return;
35
- const label = (item.querySelector(".command-palette-item-label")?.textContent ?? "").toLowerCase();
36
- const hint = (item.querySelector(".command-palette-item-hint")?.textContent ?? "").toLowerCase();
37
- const keyword = (item.dataset.commandPaletteKeyword ?? "").toLowerCase();
38
- const match = !q || label.includes(q) || hint.includes(q) || keyword.includes(q);
137
+ const def = programmaticCommands.get(item);
138
+ let match;
139
+ if (def && customFilter) {
140
+ match = !q || customFilter(query, def);
141
+ } else {
142
+ const label = (item.querySelector(".command-palette-item-label")?.textContent ?? "").toLowerCase();
143
+ const hint = (item.querySelector(".command-palette-item-hint")?.textContent ?? "").toLowerCase();
144
+ const keyword = (item.dataset.commandPaletteKeyword ?? "").toLowerCase();
145
+ match = !q || label.includes(q) || hint.includes(q) || keyword.includes(q);
146
+ }
39
147
  item.hidden = !match;
40
148
  item.classList.remove("is-active");
41
149
  if (match) visibleCount++;
@@ -50,14 +158,20 @@ function filterPanel(panel, query) {
50
158
  if (empty) empty.hidden = visibleCount !== 0;
51
159
  if (visibleCount > 0) setActive(panel, 0);
52
160
  }
53
- function openCommandPalette(id) {
161
+ function openCommandPalette(id, opts) {
54
162
  if (typeof document === "undefined") return;
55
163
  if (openId === id) return;
56
164
  if (openId) closeCommandPalette(openId);
57
165
  const backdrop = findBackdrop(id);
58
- if (!backdrop) return;
166
+ if (!backdrop) {
167
+ console.warn(`[@icen.ai/ui] command-palette: \u672A\u627E\u5230 data-command-palette="${id}" \u7684 .command-palette-backdrop\uFF0C\u5DF2\u8DF3\u8FC7\u6253\u5F00`);
168
+ return;
169
+ }
59
170
  const panel = backdrop.querySelector(".command-palette");
60
171
  if (!panel) return;
172
+ applyPanelSizing(panel, readPanelSizing(backdrop));
173
+ customFilter = opts?.filter ?? null;
174
+ if (opts?.items) buildCommandItems(panel, opts.items);
61
175
  if (backdrop.parentElement !== document.body) document.body.appendChild(backdrop);
62
176
  restoreFocusTo = document.activeElement instanceof HTMLElement ? document.activeElement : null;
63
177
  openId = id;
@@ -91,6 +205,7 @@ function openCommandPalette(id) {
91
205
  const active = panel.querySelector(".command-palette-item.is-active");
92
206
  if (active && !isDisabledItem(active)) {
93
207
  e.preventDefault();
208
+ if (!programmaticCommands.has(active)) emitMenuSelect(panel, active);
94
209
  active.click();
95
210
  }
96
211
  return;
@@ -107,12 +222,12 @@ function openCommandPalette(id) {
107
222
  }
108
223
  };
109
224
  window.addEventListener("keydown", keyHandler);
225
+ emitIcen(document, "icen:command-palette-open", {});
110
226
  }
111
227
  function closeCommandPalette(id) {
112
228
  if (typeof document === "undefined") return;
113
229
  const backdrop = findBackdrop(id);
114
- if (!backdrop) return;
115
- backdrop.hidden = true;
230
+ if (backdrop) backdrop.hidden = true;
116
231
  if (openId !== id) return;
117
232
  openId = null;
118
233
  if (keyHandler) {
@@ -122,14 +237,17 @@ function closeCommandPalette(id) {
122
237
  document.body.style.overflow = prevBodyOverflow;
123
238
  if (restoreFocusTo && restoreFocusTo.isConnected) restoreFocusTo.focus();
124
239
  restoreFocusTo = null;
240
+ emitIcen(document, "icen:command-palette-close", {});
125
241
  }
126
- function initCommandPalette(root = document) {
127
- if (typeof document === "undefined") return;
128
- if (typeof window === "undefined") return;
129
- root.querySelectorAll("[data-command-palette-open]").forEach((el) => el.setAttribute("aria-haspopup", "dialog"));
130
- if (initialized) return;
131
- initialized = true;
132
- document.addEventListener("click", (e) => {
242
+ function initCommandPalette(root) {
243
+ if (typeof document === "undefined") return () => {
244
+ };
245
+ if (typeof window === "undefined") return () => {
246
+ };
247
+ const scope = root ?? document;
248
+ scope.querySelectorAll("[data-command-palette-open]").forEach((el) => el.setAttribute("aria-haspopup", "dialog"));
249
+ if (teardown) return teardown;
250
+ const onClick = (e) => {
133
251
  const target = e.target instanceof Element ? e.target : null;
134
252
  if (!target) return;
135
253
  const opener = target.closest("[data-command-palette-open]");
@@ -141,6 +259,8 @@ function initCommandPalette(root = document) {
141
259
  if (item) {
142
260
  const backdrop = item.closest("[data-command-palette]");
143
261
  if (backdrop?.dataset.commandPalette) {
262
+ const panelEl = item.closest(".command-palette");
263
+ if (panelEl && !programmaticCommands.has(item)) emitMenuSelect(panelEl, item);
144
264
  if (!item.hasAttribute("data-command-palette-keep-open")) {
145
265
  closeCommandPalette(backdrop.dataset.commandPalette);
146
266
  }
@@ -150,25 +270,45 @@ function initCommandPalette(root = document) {
150
270
  if (target instanceof HTMLElement && target.classList.contains("command-palette-backdrop") && target.dataset.commandPalette) {
151
271
  closeCommandPalette(target.dataset.commandPalette);
152
272
  }
153
- });
154
- document.addEventListener("input", (e) => {
273
+ };
274
+ document.addEventListener("click", onClick);
275
+ const onInput = (e) => {
155
276
  if (!(e.target instanceof HTMLElement)) return;
156
277
  if (!e.target.matches(".command-palette-input")) return;
157
278
  const backdrop = e.target.closest("[data-command-palette]");
158
279
  const panel = backdrop?.querySelector(".command-palette");
159
280
  if (panel) filterPanel(panel, e.target.value);
160
- });
281
+ };
282
+ document.addEventListener("input", onInput);
161
283
  globalKeyHandler = (e) => {
162
- if ((e.metaKey || e.ctrlKey) && (e.key === "k" || e.key === "K")) {
284
+ if (!(e.metaKey || e.ctrlKey)) return;
285
+ const key = e.key.toLowerCase();
286
+ const palettes = document.querySelectorAll(
287
+ ".command-palette-backdrop[data-command-palette]"
288
+ );
289
+ for (const backdrop of Array.from(palettes)) {
290
+ const paletteKey = (backdrop.dataset.commandPaletteKey ?? "k").toLowerCase();
291
+ if (paletteKey !== key) continue;
292
+ const id = backdrop.dataset.commandPalette;
293
+ if (!id) continue;
163
294
  e.preventDefault();
164
- const first = document.querySelector(".command-palette-backdrop[data-command-palette]");
165
- if (first?.dataset.commandPalette) {
166
- if (openId === first.dataset.commandPalette) closeCommandPalette(openId);
167
- else openCommandPalette(first.dataset.commandPalette);
168
- }
295
+ if (openId === id) closeCommandPalette(id);
296
+ else openCommandPalette(id);
297
+ return;
169
298
  }
170
299
  };
171
300
  window.addEventListener("keydown", globalKeyHandler);
301
+ teardown = () => {
302
+ document.removeEventListener("click", onClick);
303
+ document.removeEventListener("input", onInput);
304
+ if (globalKeyHandler) {
305
+ window.removeEventListener("keydown", globalKeyHandler);
306
+ globalKeyHandler = null;
307
+ }
308
+ if (openId) closeCommandPalette(openId);
309
+ teardown = null;
310
+ };
311
+ return teardown;
172
312
  }
173
313
  export {
174
314
  closeCommandPalette,
@@ -0,0 +1,53 @@
1
+ import { PanelSizing } from './popover.js';
2
+
3
+ interface ContextMenuEntry {
4
+ label: string;
5
+ subLabel?: string;
6
+ /** 可信 svg 字符串(唯一走 innerHTML 的字段) */
7
+ icon?: string;
8
+ shortcut?: string;
9
+ danger?: boolean;
10
+ disabled?: boolean;
11
+ onClick?: () => void;
12
+ }
13
+ interface ContextMenuSeparator {
14
+ type: 'separator';
15
+ }
16
+ interface ContextMenuHeader {
17
+ type: 'header';
18
+ label: string;
19
+ description?: string;
20
+ }
21
+ type ContextMenuItem = ContextMenuEntry | ContextMenuSeparator | ContextMenuHeader;
22
+ interface ContextMenuOptions {
23
+ /** 替换内置默认菜单(未命中任何注册 id 时使用) */
24
+ items?: ContextMenuItem[];
25
+ }
26
+
27
+ /** 菜单选中统一事件(dropdown / context-menu / command-palette 同一契约)。
28
+ * 本模块为唯一定义点,dropdown / command-palette 改为 import 复用(收敛三份逐字拷贝)。 */
29
+ declare function emitMenuSelect(source: HTMLElement, item: HTMLElement): void;
30
+ /** 注册指定 id 的右键菜单项(对应 DOM 中的 [data-context-menu="id"])。 */
31
+ declare function registerContextMenu(id: string, items: ContextMenuItem[]): void;
32
+ /** 在 (x, y) 打开右键菜单(视口边缘自动翻转并 clamp)。
33
+ 尺寸走全库统一 PanelSizing 契约:opts.sizing 直接传入(面板无固定宿主元素,
34
+ 由调用方从其根元素 readPanelSizing 读入,如 datatable 行右键)。 */
35
+ declare function openContextMenu(x: number, y: number, items: ContextMenuItem[], opts?: {
36
+ sizing?: PanelSizing;
37
+ }): void;
38
+ /** 关闭当前右键菜单(未打开时为 no-op)。 */
39
+ declare function closeContextMenu(): void;
40
+ /**
41
+ * @deprecated 已更名 closeContextMenu(与全库浮层 close* 命名对齐);
42
+ * 本别名仅为兼容保留,下个大版本删除。
43
+ */
44
+ declare function closeMenu(): void;
45
+ /**
46
+ * 启动全局右键菜单(重复调用安全)。
47
+ * opts.items 提供时替换内置默认菜单。
48
+ * 返回销毁函数:摘除 init 注册的全部 document 监听并复位幂等标记(销毁后可重新
49
+ * init);销毁时若有菜单开着会顺带关闭(摘净 openContextMenu 挂的浮层监听)。
50
+ */
51
+ declare function initContextMenu(opts?: ContextMenuOptions): () => void;
52
+
53
+ export { type ContextMenuEntry, type ContextMenuHeader, type ContextMenuItem, type ContextMenuOptions, type ContextMenuSeparator, closeContextMenu, closeMenu, emitMenuSelect, initContextMenu, openContextMenu, registerContextMenu };
@@ -1,8 +1,65 @@
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 applyPanelSizing(panel2, sizing) {
21
+ if (sizing == null) {
22
+ panel2.style.width = "";
23
+ panel2.style.minWidth = "";
24
+ panel2.style.maxWidth = "";
25
+ panel2.style.minHeight = "";
26
+ panel2.style.maxHeight = "";
27
+ return;
28
+ }
29
+ const vw = typeof document === "undefined" ? Number.POSITIVE_INFINITY : document.documentElement.clientWidth;
30
+ const fitW = (n) => Math.min(n, Math.max(80, vw - 24));
31
+ if (sizing.width != null) {
32
+ const w = fitW(sizing.width);
33
+ panel2.style.width = `${w}px`;
34
+ panel2.style.maxWidth = `${w}px`;
35
+ }
36
+ if (sizing.minWidth != null) panel2.style.minWidth = `${fitW(sizing.minWidth)}px`;
37
+ if (sizing.maxWidth != null && sizing.width == null) panel2.style.maxWidth = `${fitW(sizing.maxWidth)}px`;
38
+ if (sizing.minHeight != null) panel2.style.minHeight = `${sizing.minHeight}px`;
39
+ if (sizing.maxHeight != null) panel2.style.maxHeight = `${sizing.maxHeight}px`;
40
+ }
41
+
42
+ // src/behaviors/events.ts
43
+ function emitIcen(source, type, detail) {
44
+ return source.dispatchEvent(
45
+ new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable: true })
46
+ );
47
+ }
48
+
1
49
  // src/behaviors/context-menu.ts
2
50
  var LONG_PRESS_DELAY = 520;
3
51
  var LONG_PRESS_MOVE_LIMIT = 12;
4
52
  var FLIP_MARGIN = 10;
5
53
  var CLAMP_MARGIN = 8;
54
+ function emitMenuSelect(source, item) {
55
+ emitIcen(item, "icen:menu-select", {
56
+ source,
57
+ item,
58
+ value: item.dataset.value ?? "",
59
+ /* 优先专用 label 子元素(避开快捷键/图标文本混入),无则退整项文本 */
60
+ label: (item.querySelector(".menu-item-label, .command-item-label")?.textContent ?? item.textContent ?? "").trim().slice(0, 80)
61
+ });
62
+ }
6
63
  var registry = /* @__PURE__ */ new Map();
7
64
  var fallbackItems = null;
8
65
  var initialized = false;
@@ -44,7 +101,7 @@ function resolveItems(target) {
44
101
  function isEditable(target) {
45
102
  return Boolean(target?.closest('input, textarea, [contenteditable="true"]'));
46
103
  }
47
- function buildItem(item) {
104
+ function buildItem(item, menuPanel) {
48
105
  if ("type" in item && item.type === "separator") {
49
106
  const sep = document.createElement("div");
50
107
  sep.className = "menu-separator";
@@ -94,19 +151,21 @@ function buildItem(item) {
94
151
  btn.appendChild(shortcut);
95
152
  }
96
153
  btn.addEventListener("click", () => {
154
+ emitMenuSelect(menuPanel, btn);
97
155
  entry.onClick?.();
98
- closeMenu();
156
+ closeContextMenu();
99
157
  });
100
158
  return btn;
101
159
  }
102
- function openContextMenu(x, y, items) {
160
+ function openContextMenu(x, y, items, opts) {
103
161
  if (typeof document === "undefined") return;
104
- closeMenu();
162
+ closeContextMenu();
105
163
  panel = document.createElement("div");
106
164
  panel.className = "menu menu--context";
107
165
  panel.setAttribute("role", "menu");
108
166
  panel.style.visibility = "hidden";
109
- for (const item of items) panel.appendChild(buildItem(item));
167
+ applyPanelSizing(panel, opts?.sizing ?? {});
168
+ for (const item of items) panel.appendChild(buildItem(item, panel));
110
169
  document.body.appendChild(panel);
111
170
  const rect = panel.getBoundingClientRect();
112
171
  const vw = window.innerWidth;
@@ -117,6 +176,7 @@ function openContextMenu(x, y, items) {
117
176
  if (y + rect.height > vh - FLIP_MARGIN) ny = y - rect.height;
118
177
  if (nx < CLAMP_MARGIN) nx = CLAMP_MARGIN;
119
178
  if (ny < CLAMP_MARGIN) ny = CLAMP_MARGIN;
179
+ panel.dataset.side = ny < y ? "top" : "bottom";
120
180
  panel.style.left = `${nx}px`;
121
181
  panel.style.top = `${ny}px`;
122
182
  panel.style.visibility = "";
@@ -127,21 +187,24 @@ function openContextMenu(x, y, items) {
127
187
  setFocused(0);
128
188
  });
129
189
  document.addEventListener("pointerdown", onOutsidePointerDown, true);
130
- document.addEventListener("scroll", onCloseSignal, true);
190
+ document.addEventListener("scroll", onScrollSignal, true);
131
191
  window.addEventListener("resize", onCloseSignal);
132
192
  window.addEventListener("keydown", onKeyDown);
133
193
  }
134
- function closeMenu() {
194
+ function closeContextMenu() {
135
195
  if (!panel) return;
136
196
  panel.remove();
137
197
  panel = null;
138
198
  focusableItems = [];
139
199
  focusedIndex = -1;
140
200
  document.removeEventListener("pointerdown", onOutsidePointerDown, true);
141
- document.removeEventListener("scroll", onCloseSignal, true);
201
+ document.removeEventListener("scroll", onScrollSignal, true);
142
202
  window.removeEventListener("resize", onCloseSignal);
143
203
  window.removeEventListener("keydown", onKeyDown);
144
204
  }
205
+ function closeMenu() {
206
+ closeContextMenu();
207
+ }
145
208
  function setFocused(next) {
146
209
  if (!panel || focusableItems.length === 0) return;
147
210
  focusedIndex = (next % focusableItems.length + focusableItems.length) % focusableItems.length;
@@ -150,16 +213,20 @@ function setFocused(next) {
150
213
  function onOutsidePointerDown(e) {
151
214
  if (!panel) return;
152
215
  const target = e.target;
153
- if (!(target instanceof Node) || !panel.contains(target)) closeMenu();
216
+ if (!(target instanceof Node) || !panel.contains(target)) closeContextMenu();
217
+ }
218
+ function onScrollSignal(e) {
219
+ if (panel && e.target instanceof Node && panel.contains(e.target)) return;
220
+ closeContextMenu();
154
221
  }
155
222
  function onCloseSignal() {
156
- closeMenu();
223
+ closeContextMenu();
157
224
  }
158
225
  function onKeyDown(e) {
159
226
  if (!panel) return;
160
227
  if (e.key === "Escape") {
161
228
  e.preventDefault();
162
- closeMenu();
229
+ closeContextMenu();
163
230
  return;
164
231
  }
165
232
  if (focusableItems.length === 0) return;
@@ -180,43 +247,64 @@ function clearLongPress() {
180
247
  longPress = null;
181
248
  }
182
249
  function openForTarget(target, x, y) {
183
- openContextMenu(x, y, resolveItems(target));
250
+ openContextMenu(
251
+ x,
252
+ y,
253
+ resolveItems(target),
254
+ { sizing: readPanelSizing(target instanceof Element ? target.closest("[data-context-menu]") ?? target : target) }
255
+ );
256
+ }
257
+ function onContextMenu(e) {
258
+ const target = e.target instanceof Element ? e.target : null;
259
+ if (isEditable(target)) return;
260
+ e.preventDefault();
261
+ openForTarget(target, e.clientX, e.clientY);
262
+ }
263
+ function onLongPressStart(e) {
264
+ if (e.pointerType !== "touch") return;
265
+ const target = e.target instanceof Element ? e.target : null;
266
+ if (!target || isEditable(target)) return;
267
+ clearLongPress();
268
+ const startX = e.clientX;
269
+ const startY = e.clientY;
270
+ const timer = window.setTimeout(() => {
271
+ longPress = null;
272
+ openForTarget(target, startX, startY);
273
+ }, LONG_PRESS_DELAY);
274
+ longPress = { timer, x: startX, y: startY, target };
275
+ }
276
+ function onLongPressMove(e) {
277
+ if (!longPress) return;
278
+ if (Math.hypot(e.clientX - longPress.x, e.clientY - longPress.y) > LONG_PRESS_MOVE_LIMIT) {
279
+ clearLongPress();
280
+ }
184
281
  }
185
282
  function initContextMenu(opts = {}) {
186
- if (typeof document === "undefined") return;
283
+ if (typeof document === "undefined") return () => {
284
+ };
187
285
  if (opts.items) fallbackItems = opts.items;
188
- if (initialized) return;
286
+ if (initialized) return () => {
287
+ };
189
288
  initialized = true;
190
- document.addEventListener("contextmenu", (e) => {
191
- const target = e.target instanceof Element ? e.target : null;
192
- if (isEditable(target)) return;
193
- e.preventDefault();
194
- openForTarget(target, e.clientX, e.clientY);
195
- });
196
- document.addEventListener("pointerdown", (e) => {
197
- if (e.pointerType !== "touch") return;
198
- const target = e.target instanceof Element ? e.target : null;
199
- if (!target || isEditable(target)) return;
200
- clearLongPress();
201
- const startX = e.clientX;
202
- const startY = e.clientY;
203
- const timer = window.setTimeout(() => {
204
- longPress = null;
205
- openForTarget(target, startX, startY);
206
- }, LONG_PRESS_DELAY);
207
- longPress = { timer, x: startX, y: startY, target };
208
- });
209
- document.addEventListener("pointermove", (e) => {
210
- if (!longPress) return;
211
- if (Math.hypot(e.clientX - longPress.x, e.clientY - longPress.y) > LONG_PRESS_MOVE_LIMIT) {
212
- clearLongPress();
213
- }
214
- });
289
+ document.addEventListener("contextmenu", onContextMenu);
290
+ document.addEventListener("pointerdown", onLongPressStart);
291
+ document.addEventListener("pointermove", onLongPressMove);
215
292
  document.addEventListener("pointerup", clearLongPress);
216
293
  document.addEventListener("pointercancel", clearLongPress);
294
+ return () => {
295
+ document.removeEventListener("contextmenu", onContextMenu);
296
+ document.removeEventListener("pointerdown", onLongPressStart);
297
+ document.removeEventListener("pointermove", onLongPressMove);
298
+ document.removeEventListener("pointerup", clearLongPress);
299
+ document.removeEventListener("pointercancel", clearLongPress);
300
+ closeContextMenu();
301
+ initialized = false;
302
+ };
217
303
  }
218
304
  export {
305
+ closeContextMenu,
219
306
  closeMenu,
307
+ emitMenuSelect,
220
308
  initContextMenu,
221
309
  openContextMenu,
222
310
  registerContextMenu