@icen.ai/ui 0.5.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 (138) 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 +49 -32
  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 +24 -12
  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 +104 -36
  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 +29 -22
  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 +243 -89
  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 +131 -26
  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 +13 -13
  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 +63 -26
  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/badge.css +4 -8
  86. package/dist/components/breadcrumb.css +1 -5
  87. package/dist/components/btn.css +4 -7
  88. package/dist/components/card.css +3 -6
  89. package/dist/components/chart-bar.css +10 -0
  90. package/dist/components/chart-calendar.mjs +5 -0
  91. package/dist/components/chart-gauge.css +1 -1
  92. package/dist/components/chart-heatmap.css +40 -0
  93. package/dist/components/chart-scatter.css +24 -0
  94. package/dist/components/chart-scatter.mjs +4 -0
  95. package/dist/components/charts.css +86 -1
  96. package/dist/components/charts.mjs +1 -0
  97. package/dist/components/command-palette.css +2 -2
  98. package/dist/components/content.css +8 -9
  99. package/dist/components/copy.css +8 -8
  100. package/dist/components/datatable.css +4 -8
  101. package/dist/components/date-picker.css +3 -7
  102. package/dist/components/empty.css +0 -4
  103. package/dist/components/feedback.css +10 -17
  104. package/dist/components/form.css +13 -14
  105. package/dist/components/input.css +2 -2
  106. package/dist/components/layout.css +36 -36
  107. package/dist/components/media.css +12 -8
  108. package/dist/components/menu.css +2 -2
  109. package/dist/components/modal.css +1 -1
  110. package/dist/components/nav.css +11 -6
  111. package/dist/components/notification.css +28 -28
  112. package/dist/components/pagination.css +1 -1
  113. package/dist/components/panel.css +16 -16
  114. package/dist/components/pill.css +15 -15
  115. package/dist/components/segmented.css +5 -5
  116. package/dist/components/segmented.mjs +1 -0
  117. package/dist/components/select.css +12 -20
  118. package/dist/components/sidebar.css +1 -1
  119. package/dist/components/split-pane.css +0 -1
  120. package/dist/components/stat.css +14 -18
  121. package/dist/components/steps.css +2 -2
  122. package/dist/components/switch.css +8 -12
  123. package/dist/components/table.css +46 -47
  124. package/dist/components/tabs.css +35 -46
  125. package/dist/components/tag-input.css +4 -4
  126. package/dist/components/toast.css +16 -13
  127. package/dist/components/tooltip.css +2 -2
  128. package/dist/index.d.mts +37 -0
  129. package/dist/index.mjs +7890 -1199
  130. package/dist/registry.json +113 -4
  131. package/dist/retro-effects.css +21 -17
  132. package/dist/tokens/colors.css +4 -1
  133. package/dist/tokens/retro-effects.css +21 -17
  134. package/dist/tokens/style-profiles.css +21 -0
  135. package/dist/tokens.css +25 -1
  136. package/dist/ui.css +3168 -433
  137. package/docs/spec/ai-native.md +659 -0
  138. package/package.json +15 -5
@@ -75,7 +75,10 @@ function onOutsidePointerDown(e) {
75
75
  if (active.panel.contains(target) || active.trigger.contains(target)) return;
76
76
  closeDropdown();
77
77
  }
78
- function onScroll() {
78
+ function onScroll(e) {
79
+ if (!active) return;
80
+ const target = e.target;
81
+ if (target instanceof Node && active.panel.contains(target)) return;
79
82
  closeDropdown();
80
83
  }
81
84
  function onKeyDown(e) {
@@ -101,9 +104,10 @@ function onKeyDown(e) {
101
104
  item.click();
102
105
  }
103
106
  }
104
- function initDropdown(root = document) {
107
+ function initDropdown(root) {
105
108
  if (typeof document === "undefined") return;
106
- root.querySelectorAll("[data-dropdown-trigger]").forEach((el) => el.setAttribute("aria-expanded", "false"));
109
+ const scope = root ?? document;
110
+ scope.querySelectorAll("[data-dropdown-trigger]").forEach((el) => el.setAttribute("aria-expanded", "false"));
107
111
  if (initialized) return;
108
112
  initialized = true;
109
113
  document.addEventListener("click", (e) => {
@@ -0,0 +1,4 @@
1
+ /** 为 root 下所有输入类结构初始化(root 自身匹配也算)。幂等。 */
2
+ declare function initInput(root?: ParentNode): void;
3
+
4
+ export { initInput };
@@ -53,9 +53,12 @@ function setupAutosize(ta) {
53
53
  resizeTextarea(ta);
54
54
  }
55
55
  function setupCounter(field) {
56
+ const el = field;
57
+ if (el.__icenCounterInit) return;
56
58
  const maxAttr = field.getAttribute("maxlength");
57
59
  const dataCount = field.getAttribute("data-count");
58
60
  if (!maxAttr && dataCount === null) return;
61
+ el.__icenCounterInit = true;
59
62
  const max = maxAttr ? Number(maxAttr) : 0;
60
63
  let counter = null;
61
64
  const wrap = field.closest(".input-wrap, .textarea-wrap");
@@ -88,8 +91,11 @@ function setupCounter(field) {
88
91
  update();
89
92
  }
90
93
  function setupMask(field) {
94
+ const el = field;
95
+ if (el.__icenMaskInit) return;
91
96
  const mask = field.getAttribute("data-mask");
92
97
  if (!mask) return;
98
+ el.__icenMaskInit = true;
93
99
  const maskChars = [];
94
100
  for (const ch of mask) maskChars.push(ch);
95
101
  function isInputSlot(ch) {
@@ -163,10 +169,6 @@ function setupMask(field) {
163
169
  if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
164
170
  if (["Backspace", "Delete", "Tab", "Escape", "ArrowLeft", "ArrowRight", "Home", "End", "Enter"].includes(ev.key)) return;
165
171
  const currentRaw = extractRaw(field.value);
166
- const nextSlotIdx = maskChars.findIndex((ch, i) => {
167
- if (!isInputSlot(ch)) return false;
168
- return i >= field.value.length || !isInputSlot(maskChars[Math.min(field.value.length, maskChars.length - 1)]);
169
- });
170
172
  const nextSlot = maskChars.find((ch, i) => {
171
173
  const filledSlots = maskChars.slice(0, i).filter(isInputSlot).length;
172
174
  return isInputSlot(ch) && filledSlots >= currentRaw.length;
@@ -178,6 +180,9 @@ function setupMask(field) {
178
180
  }
179
181
  function setupTrim(field) {
180
182
  if (!field.hasAttribute("data-trim")) return;
183
+ const el = field;
184
+ if (el.__icenTrimInit) return;
185
+ el.__icenTrimInit = true;
181
186
  field.addEventListener("blur", () => {
182
187
  const trimmed = field.value.trim();
183
188
  if (trimmed !== field.value) {
@@ -234,16 +239,18 @@ function setupOtp(group) {
234
239
  const fireComplete = () => {
235
240
  const code = cells.map((c) => c.value).join("");
236
241
  if (code.length === cells.length && cells.every((c) => c.value.length > 0)) {
242
+ group.dispatchEvent(new CustomEvent("icen:otp-complete", { detail: { code }, bubbles: true }));
237
243
  group.dispatchEvent(new CustomEvent("otp:complete", { detail: { code }, bubbles: true }));
238
244
  }
239
245
  };
240
246
  cells.forEach((cell) => cell.addEventListener("input", fireComplete));
241
247
  }
242
- function initInput(root = document) {
248
+ function initInput(root) {
243
249
  if (typeof document === "undefined") return;
250
+ const scope = root ?? document;
244
251
  const wraps = [];
245
- if (root instanceof Element && root.matches(".input-wrap")) wraps.push(root);
246
- wraps.push(...Array.from(root.querySelectorAll(".input-wrap")));
252
+ if (scope instanceof Element && scope.matches(".input-wrap")) wraps.push(scope);
253
+ wraps.push(...Array.from(scope.querySelectorAll(".input-wrap")));
247
254
  for (const w of wraps) {
248
255
  const el = w;
249
256
  if (el.__icenInputInit) continue;
@@ -252,37 +259,37 @@ function initInput(root = document) {
252
259
  setupPasswordToggle(w);
253
260
  }
254
261
  const textareas = [];
255
- if (root instanceof HTMLTextAreaElement && root.matches("[data-autosize]")) textareas.push(root);
256
- textareas.push(...Array.from(root.querySelectorAll("textarea[data-autosize]")));
262
+ if (scope instanceof HTMLTextAreaElement && scope.matches("[data-autosize]")) textareas.push(scope);
263
+ textareas.push(...Array.from(scope.querySelectorAll("textarea[data-autosize]")));
257
264
  for (const ta of textareas) setupAutosize(ta);
258
265
  const countedFields = [];
259
- if (root instanceof Element) {
260
- const fields = Array.from(root.querySelectorAll(
266
+ if (scope instanceof Element) {
267
+ const fields = Array.from(scope.querySelectorAll(
261
268
  "input[maxlength], input[data-count], textarea[maxlength], textarea[data-count]"
262
269
  ));
263
270
  countedFields.push(...fields);
264
- if (root instanceof HTMLInputElement || root instanceof HTMLTextAreaElement) {
265
- if (root.hasAttribute("maxlength") || root.hasAttribute("data-count")) countedFields.push(root);
271
+ if (scope instanceof HTMLInputElement || scope instanceof HTMLTextAreaElement) {
272
+ if (scope.hasAttribute("maxlength") || scope.hasAttribute("data-count")) countedFields.push(scope);
266
273
  }
267
274
  }
268
275
  for (const f of countedFields) setupCounter(f);
269
276
  const maskedInputs = [];
270
- if (root instanceof Element) {
271
- maskedInputs.push(...Array.from(root.querySelectorAll("input[data-mask]")));
272
- if (root instanceof HTMLInputElement && root.hasAttribute("data-mask")) maskedInputs.push(root);
277
+ if (scope instanceof Element) {
278
+ maskedInputs.push(...Array.from(scope.querySelectorAll("input[data-mask]")));
279
+ if (scope instanceof HTMLInputElement && scope.hasAttribute("data-mask")) maskedInputs.push(scope);
273
280
  }
274
281
  for (const inp of maskedInputs) setupMask(inp);
275
282
  const trimFields = [];
276
- if (root instanceof Element) {
277
- trimFields.push(...Array.from(root.querySelectorAll("[data-trim]")));
278
- if ((root instanceof HTMLInputElement || root instanceof HTMLTextAreaElement) && root.hasAttribute("data-trim")) {
279
- trimFields.push(root);
283
+ if (scope instanceof Element) {
284
+ trimFields.push(...Array.from(scope.querySelectorAll("[data-trim]")));
285
+ if ((scope instanceof HTMLInputElement || scope instanceof HTMLTextAreaElement) && scope.hasAttribute("data-trim")) {
286
+ trimFields.push(scope);
280
287
  }
281
288
  }
282
289
  for (const f of trimFields) setupTrim(f);
283
290
  const otps = [];
284
- if (root instanceof Element && root.matches(".otp")) otps.push(root);
285
- otps.push(...Array.from(root.querySelectorAll(".otp")));
291
+ if (scope instanceof Element && scope.matches(".otp")) otps.push(scope);
292
+ otps.push(...Array.from(scope.querySelectorAll(".otp")));
286
293
  for (const g of otps) setupOtp(g);
287
294
  }
288
295
  export {
@@ -0,0 +1,11 @@
1
+ /** 打开指定 id 的 modal;已有其他 modal 打开时先关闭它。 */
2
+ declare function openModal(id: string): void;
3
+ /** 关闭指定 id 的 modal(backdrop 已被外部移除时也照常复位状态/监听/滚动锁)。 */
4
+ declare function closeModal(id: string): void;
5
+ /**
6
+ * 绑定触发器/关闭钮/backdrop 点击(document 级事件委托,重复调用安全)。
7
+ * root 用于扫描 [data-modal-open] 补 aria-haspopup=dialog。
8
+ */
9
+ declare function initModal(root?: ParentNode): void;
10
+
11
+ export { closeModal, initModal, openModal };
@@ -16,7 +16,10 @@ function openModal(id) {
16
16
  if (openId === id) return;
17
17
  if (openId) closeModal(openId);
18
18
  const backdrop = findBackdrop(id);
19
- if (!backdrop) return;
19
+ if (!backdrop) {
20
+ console.warn(`[@icen.ai/ui] modal: \u672A\u627E\u5230 data-modal="${id}" \u7684 .modal-backdrop\uFF0C\u5DF2\u8DF3\u8FC7\u6253\u5F00`);
21
+ return;
22
+ }
20
23
  const panel = backdrop.querySelector(".modal");
21
24
  if (!panel) return;
22
25
  portal(backdrop);
@@ -52,7 +55,9 @@ function openModal(id) {
52
55
  return;
53
56
  }
54
57
  if (e.key !== "Tab") return;
55
- const els = panel.querySelectorAll(FOCUSABLE);
58
+ const els = Array.from(panel.querySelectorAll(FOCUSABLE)).filter(
59
+ (el) => !el.hidden && el.offsetParent !== null
60
+ );
56
61
  if (els.length === 0) {
57
62
  e.preventDefault();
58
63
  panel.focus();
@@ -77,8 +82,7 @@ function openModal(id) {
77
82
  function closeModal(id) {
78
83
  if (typeof document === "undefined") return;
79
84
  const backdrop = findBackdrop(id);
80
- if (!backdrop) return;
81
- backdrop.hidden = true;
85
+ if (backdrop) backdrop.hidden = true;
82
86
  if (openId !== id) return;
83
87
  openId = null;
84
88
  if (keyHandler) {
@@ -89,9 +93,10 @@ function closeModal(id) {
89
93
  if (restoreFocusTo && restoreFocusTo.isConnected) restoreFocusTo.focus();
90
94
  restoreFocusTo = null;
91
95
  }
92
- function initModal(root = document) {
96
+ function initModal(root) {
93
97
  if (typeof document === "undefined") return;
94
- root.querySelectorAll("[data-modal-open]").forEach((el) => el.setAttribute("aria-haspopup", "dialog"));
98
+ const scope = root ?? document;
99
+ scope.querySelectorAll("[data-modal-open]").forEach((el) => el.setAttribute("aria-haspopup", "dialog"));
95
100
  if (initialized) return;
96
101
  initialized = true;
97
102
  document.addEventListener("click", (e) => {
@@ -0,0 +1,7 @@
1
+ /**
2
+ * 为 root 下每个 .nav[data-scroll-reactive] 初始化(root 自身匹配也算)。
3
+ * 返回销毁函数:移除本次初始化挂上的全部 window scroll/resize 监听。
4
+ */
5
+ declare function initNav(root?: ParentNode): () => void;
6
+
7
+ export { initNav };
@@ -5,9 +5,11 @@ function prefersReducedMotion() {
5
5
  }
6
6
  function setup(nav) {
7
7
  const el = nav;
8
- if (el.__icenNavInit) return;
8
+ if (el.__icenNavInit) return void 0;
9
9
  el.__icenNavInit = true;
10
- const threshold = Number(nav.dataset.navThreshold ?? "12") || 12;
10
+ const rawThreshold = nav.dataset.navThreshold;
11
+ const parsedThreshold = rawThreshold === void 0 || rawThreshold === "" ? NaN : Number(rawThreshold);
12
+ const threshold = Number.isFinite(parsedThreshold) ? parsedThreshold : 12;
11
13
  const hideOnScroll = nav.hasAttribute("data-nav-hide-on-scroll");
12
14
  let ticking = false;
13
15
  let lastY = window.scrollY;
@@ -42,14 +44,28 @@ function setup(nav) {
42
44
  window.addEventListener("scroll", onScroll, { passive: true });
43
45
  window.addEventListener("resize", onResize, { passive: true });
44
46
  apply();
47
+ return () => {
48
+ window.removeEventListener("scroll", onScroll);
49
+ window.removeEventListener("resize", onResize);
50
+ el.__icenNavInit = false;
51
+ };
45
52
  }
46
- function initNav(root = document) {
47
- if (typeof document === "undefined") return;
48
- if (typeof window === "undefined") return;
49
- const navs = [];
50
- if (root instanceof Element && root.matches(".nav[data-scroll-reactive]")) navs.push(root);
51
- navs.push(...Array.from(root.querySelectorAll(".nav[data-scroll-reactive]")));
52
- for (const n of navs) setup(n);
53
+ function initNav(root) {
54
+ const cleanups = [];
55
+ if (typeof document !== "undefined" && typeof window !== "undefined") {
56
+ const scope = root ?? document;
57
+ const navs = [];
58
+ if (scope instanceof Element && scope.matches(".nav[data-scroll-reactive]")) navs.push(scope);
59
+ navs.push(...Array.from(scope.querySelectorAll(".nav[data-scroll-reactive]")));
60
+ for (const n of navs) {
61
+ const cleanup = setup(n);
62
+ if (cleanup) cleanups.push(cleanup);
63
+ }
64
+ }
65
+ return () => {
66
+ for (const fn of cleanups) fn();
67
+ cleanups.length = 0;
68
+ };
53
69
  }
54
70
  export {
55
71
  initNav
@@ -0,0 +1,172 @@
1
+ type NotificationKind = 'success' | 'warning' | 'error' | 'info';
2
+ interface NotificationAction {
3
+ /** 按钮文字 */
4
+ label: string;
5
+ /** 点击回调;返回 false 阻止通知关闭 */
6
+ onClick?: () => void | Promise<void> | boolean;
7
+ /** 视觉变体:默认 ghost / primary 实底 / danger 危险色 */
8
+ type?: 'primary' | 'ghost' | 'danger';
9
+ /** 点击后是否保持通知打开(默认 false) */
10
+ keepOpen?: boolean;
11
+ }
12
+ interface NotificationOptions {
13
+ /** 显式 id(不传自动生成;用于后续 update/dismiss/get) */
14
+ id?: string;
15
+ /** 副标题(正文) */
16
+ description?: string;
17
+ /** 自动关闭延时(毫秒),默认 0 = 不自动关闭 */
18
+ duration?: number;
19
+ /** 是否显示关闭按钮,默认 true */
20
+ closable?: boolean;
21
+ /** hover 时暂停自动关闭倒计时,默认跟随全局 config.pauseOnHover */
22
+ pauseOnHover?: boolean;
23
+ /** duration > 0 时是否显示底部倒计时进度条,默认 true */
24
+ showCountdown?: boolean;
25
+ /** 自定义图标:SVG 字符串 / 图片 URL / false(关闭) */
26
+ icon?: string | false;
27
+ /** 头像 URL(会覆盖 kind 图标) */
28
+ avatar?: string;
29
+ /** 优先级:normal(默认)/ high(置顶) */
30
+ priority?: 'normal' | 'high';
31
+ /** 未读标记,默认 false(push 后可 markRead) */
32
+ unread?: boolean;
33
+ /** 多动作按钮(actions 优先于 actionLabel) */
34
+ actions?: NotificationAction[];
35
+ /** 兼容旧版单按钮 */
36
+ actionLabel?: string;
37
+ onAction?: () => void | Promise<void> | boolean;
38
+ /** 进度(0–1);progress > 0 时显示进度条。推荐用 notify.progress() 返回的 handle.setProgress */
39
+ progress?: number;
40
+ /** 进度条下方文字(如「正在导出 64%」) */
41
+ progressLabel?: string;
42
+ /** 详情链接(绝对 / 相对路径) */
43
+ link?: string;
44
+ linkLabel?: string;
45
+ /** 分组键:同 tag 的多条会聚合显示计数 */
46
+ tag?: string;
47
+ /** 自定义数据(持久化时保留) */
48
+ data?: unknown;
49
+ /** 点击通知体的回调(不含按钮与关闭) */
50
+ onClick?: (handle: NotificationHandle) => void;
51
+ /** 显示后回调 */
52
+ onShow?: (handle: NotificationHandle) => void;
53
+ /** 关闭后回调 */
54
+ onClose?: (handle: NotificationHandle) => void;
55
+ }
56
+ interface NotificationConfig {
57
+ /** 位置:top-right(默认)/ top-left / bottom-left / bottom-right */
58
+ position?: 'top-right' | 'top-left' | 'bottom-left' | 'bottom-right';
59
+ /** 同屏最大堆叠,默认 5;超出从最早的开始折叠/移除 */
60
+ maxStack?: number;
61
+ /** hover 暂停倒计时,默认 true */
62
+ pauseOnHover?: boolean;
63
+ /** 持久化:true 用默认 key / 字符串=自定义 key / 不传=不持久化 */
64
+ persist?: boolean | string;
65
+ /** 声音提示:true 用默认蜂鸣 / 字符串=data URL / 不传=无声 */
66
+ sound?: boolean | string;
67
+ }
68
+ interface NotificationHandle {
69
+ /** 通知 id */
70
+ id: string;
71
+ /** 当前 DOM 元素(可能已脱离 DOM) */
72
+ el: HTMLElement;
73
+ /** 关闭本条 */
74
+ dismiss(): void;
75
+ /** 部分更新(title / description / progress / unread / actions / tag 等) */
76
+ update(patch: NotificationUpdatePatch): void;
77
+ /** 设置进度(0–1)与可选标签 */
78
+ setProgress(value: number, label?: string): void;
79
+ /** 标记已读 */
80
+ markRead(): void;
81
+ /** 暂停自动关闭倒计时 */
82
+ pause(): void;
83
+ /** 恢复自动关闭倒计时(继续剩余时间) */
84
+ resume(): void;
85
+ }
86
+ /** update 接受的局部字段(title 在 push 时传,update 时也接受) */
87
+ interface NotificationUpdatePatch {
88
+ title?: string;
89
+ description?: string;
90
+ duration?: number;
91
+ unread?: boolean;
92
+ progress?: number;
93
+ progressLabel?: string;
94
+ actions?: NotificationAction[];
95
+ tag?: string;
96
+ priority?: 'normal' | 'high';
97
+ icon?: string | false;
98
+ avatar?: string;
99
+ link?: string;
100
+ linkLabel?: string;
101
+ }
102
+ interface NotificationRecord {
103
+ id: string;
104
+ title: string;
105
+ kind: NotificationKind;
106
+ opts: NotificationOptions;
107
+ el: HTMLElement | null;
108
+ createdAt: number;
109
+ duration: number;
110
+ remaining: number;
111
+ paused: boolean;
112
+ timer: number;
113
+ /** 本次计时的起始时间戳(pause 时据此扣减 remaining) */
114
+ startedAt: number;
115
+ /** 所属 NotificationCenter 的局部配置(全局单例通知为 undefined) */
116
+ cfgRef?: Required<NotificationConfig>;
117
+ /** 所属 center 的 localStore(全局通知为 undefined);dismiss 时同步清理,避免残留僵尸记录 */
118
+ ownerStore?: Map<string, NotificationRecord>;
119
+ /** attachTimer 注入的重启入口(update duration 时按新时长重启计时) */
120
+ restartTimer?: () => void;
121
+ /** hover 暂停监听是否已绑定(attachTimer 幂等保护) */
122
+ hoverBound?: boolean;
123
+ }
124
+ interface NotifyFn {
125
+ (title: string, opts?: NotificationOptions): NotificationHandle | null;
126
+ success(title: string, opts?: NotificationOptions): NotificationHandle | null;
127
+ warn(title: string, opts?: NotificationOptions): NotificationHandle | null;
128
+ error(title: string, opts?: NotificationOptions): NotificationHandle | null;
129
+ info(title: string, opts?: NotificationOptions): NotificationHandle | null;
130
+ progress(title: string, opts?: NotificationOptions): NotificationHandle | null;
131
+ confirm(title: string, opts?: NotificationConfirmOptions): Promise<boolean>;
132
+ dismiss(id?: string): void;
133
+ clear(): void;
134
+ markRead(id?: string): void;
135
+ update(id: string, patch: NotificationUpdatePatch): void;
136
+ get(id: string): NotificationRecord | null;
137
+ getAll(): NotificationRecord[];
138
+ config(opts: NotificationConfig): void;
139
+ }
140
+ interface NotificationConfirmOptions extends Omit<NotificationOptions, 'actions' | 'duration' | 'closable'> {
141
+ confirmLabel?: string;
142
+ cancelLabel?: string;
143
+ /** 自动关闭毫秒(默认 0 = 不自动关闭) */
144
+ duration?: number;
145
+ }
146
+ interface NotificationCenterHandle {
147
+ push(notif: NotificationPushInput): NotificationHandle | null;
148
+ dismiss(id?: string): void;
149
+ clear(): void;
150
+ markAllRead(): void;
151
+ /** 销毁容器:移除 DOM、清掉其所有 timer 与监听器;之后 push 返回 null */
152
+ destroy(): void;
153
+ get(id: string): NotificationRecord | null;
154
+ getAll(): NotificationRecord[];
155
+ config(opts: NotificationConfig): void;
156
+ el: HTMLElement;
157
+ }
158
+ interface NotificationPushInput {
159
+ title: string;
160
+ kind?: NotificationKind;
161
+ options?: NotificationOptions;
162
+ }
163
+ /**
164
+ * 创建一个独立的通知中心容器(可同时存在多个,比如顶部=警告、右下=消息)。
165
+ * 注意:默认全局 notify.* 走单例容器,与本函数创建的额外容器并存。
166
+ */
167
+ declare function createNotificationCenter(parent?: ParentNode, opts?: NotificationConfig): NotificationCenterHandle;
168
+ declare const notify: NotifyFn;
169
+ /** 委托初始化:建立默认容器 + 恢复持久化通知。幂等。 */
170
+ declare function initNotification(): void;
171
+
172
+ export { type NotificationAction, type NotificationCenterHandle, type NotificationConfig, type NotificationConfirmOptions, type NotificationHandle, type NotificationKind, type NotificationOptions, type NotificationPushInput, type NotificationUpdatePatch, createNotificationCenter, initNotification, notify };