@icen.ai/ui 0.8.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 (106) hide show
  1. package/AGENTS.md +15 -9
  2. package/README.md +8 -3
  3. package/dist/base.css +4 -3
  4. package/dist/behaviors/accordion.d.mts +5 -2
  5. package/dist/behaviors/accordion.mjs +36 -11
  6. package/dist/behaviors/ai-chat.d.mts +3 -1
  7. package/dist/behaviors/ai-chat.mjs +66 -43
  8. package/dist/behaviors/ai-composer.d.mts +28 -3
  9. package/dist/behaviors/ai-composer.mjs +291 -209
  10. package/dist/behaviors/ai-core.d.mts +9 -4
  11. package/dist/behaviors/ai-core.mjs +21 -8
  12. package/dist/behaviors/ai-diff.d.mts +2 -2
  13. package/dist/behaviors/ai-diff.mjs +172 -41
  14. package/dist/behaviors/ai-panel.d.mts +11 -8
  15. package/dist/behaviors/ai-panel.mjs +185 -110
  16. package/dist/behaviors/ai-provider.mjs +54 -48
  17. package/dist/behaviors/ai-tool.d.mts +12 -4
  18. package/dist/behaviors/ai-tool.mjs +311 -263
  19. package/dist/behaviors/ai-tools.d.mts +9 -6
  20. package/dist/behaviors/ai-tools.mjs +259 -155
  21. package/dist/behaviors/back-top.mjs +16 -1
  22. package/dist/behaviors/carousel.d.mts +5 -2
  23. package/dist/behaviors/carousel.mjs +53 -26
  24. package/dist/behaviors/charts.d.mts +68 -22
  25. package/dist/behaviors/charts.mjs +221 -150
  26. package/dist/behaviors/command-palette.d.mts +40 -6
  27. package/dist/behaviors/command-palette.mjs +143 -15
  28. package/dist/behaviors/context-menu.d.mts +21 -4
  29. package/dist/behaviors/context-menu.mjs +122 -38
  30. package/dist/behaviors/controls.d.mts +59 -0
  31. package/dist/behaviors/controls.mjs +601 -0
  32. package/dist/behaviors/copy.d.mts +5 -2
  33. package/dist/behaviors/copy.mjs +37 -12
  34. package/dist/behaviors/datatable.d.mts +12 -0
  35. package/dist/behaviors/datatable.mjs +226 -43
  36. package/dist/behaviors/date-picker.d.mts +2 -2
  37. package/dist/behaviors/date-picker.mjs +171 -32
  38. package/dist/behaviors/dropdown.d.mts +6 -2
  39. package/dist/behaviors/dropdown.mjs +164 -25
  40. package/dist/behaviors/events.d.mts +330 -0
  41. package/dist/behaviors/events.mjs +208 -0
  42. package/dist/behaviors/input.mjs +25 -12
  43. package/dist/behaviors/modal.d.mts +24 -5
  44. package/dist/behaviors/modal.mjs +76 -10
  45. package/dist/behaviors/nav.d.mts +1 -1
  46. package/dist/behaviors/nav.mjs +33 -7
  47. package/dist/behaviors/notification.d.mts +20 -4
  48. package/dist/behaviors/notification.mjs +53 -73
  49. package/dist/behaviors/popover.d.mts +39 -3
  50. package/dist/behaviors/popover.mjs +90 -4
  51. package/dist/behaviors/select.d.mts +4 -2
  52. package/dist/behaviors/select.mjs +161 -24
  53. package/dist/behaviors/sidebar.d.mts +5 -2
  54. package/dist/behaviors/sidebar.mjs +43 -13
  55. package/dist/behaviors/slider.d.mts +5 -2
  56. package/dist/behaviors/slider.mjs +54 -43
  57. package/dist/behaviors/split-pane.d.mts +6 -2
  58. package/dist/behaviors/split-pane.mjs +62 -21
  59. package/dist/behaviors/tabs.d.mts +3 -2
  60. package/dist/behaviors/tabs.mjs +65 -14
  61. package/dist/behaviors/tag-input.d.mts +5 -2
  62. package/dist/behaviors/tag-input.mjs +61 -21
  63. package/dist/behaviors/theme.d.mts +13 -2
  64. package/dist/behaviors/theme.mjs +48 -4
  65. package/dist/behaviors/toast.d.mts +30 -8
  66. package/dist/behaviors/toast.mjs +74 -7
  67. package/dist/behaviors/tree.d.mts +5 -2
  68. package/dist/behaviors/tree.mjs +38 -15
  69. package/dist/behaviors/upload.mjs +21 -23
  70. package/dist/cli.mjs +1 -1
  71. package/dist/components/ai-panel.css +1 -1
  72. package/dist/components/ai-tool-call.mjs +1 -1
  73. package/dist/components/badge.css +1 -1
  74. package/dist/components/breadcrumb.css +0 -32
  75. package/dist/components/chart-calendar.mjs +1 -2
  76. package/dist/components/chart-pie.css +0 -6
  77. package/dist/components/charts.css +1 -1
  78. package/dist/components/content.css +3 -16
  79. package/dist/components/datatable.css +1 -1
  80. package/dist/components/date-picker.css +1 -1
  81. package/dist/components/notification.css +2 -3
  82. package/dist/components/pagination.css +3 -1
  83. package/dist/components/pagination.mjs +1 -0
  84. package/dist/components/rating.mjs +1 -0
  85. package/dist/components/segmented.css +9 -1
  86. package/dist/components/segmented.mjs +1 -1
  87. package/dist/components/select.css +67 -4
  88. package/dist/components/steps.css +13 -3
  89. package/dist/components/steps.mjs +1 -0
  90. package/dist/components/switch.css +3 -1
  91. package/dist/components/switch.mjs +1 -0
  92. package/dist/components/table.mjs +1 -0
  93. package/dist/components/tabs.css +23 -0
  94. package/dist/components/tag-input.css +1 -6
  95. package/dist/components/upload.css +0 -15
  96. package/dist/index.d.mts +12 -10
  97. package/dist/index.mjs +2700 -1460
  98. package/dist/registry.json +42 -13
  99. package/dist/retro-effects.css +3 -16
  100. package/dist/tokens/retro-effects.css +3 -16
  101. package/dist/tokens/style-profiles.css +3 -2
  102. package/dist/tokens/typography.css +2 -0
  103. package/dist/tokens.css +5 -2
  104. package/dist/ui.css +138 -98
  105. package/docs/spec/ai-native.md +7 -4
  106. package/package.json +1 -1
@@ -1,5 +1,53 @@
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
+
1
49
  // src/behaviors/modal.ts
2
- var FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
50
+ 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';
3
51
  var openId = null;
4
52
  var restoreFocusTo = null;
5
53
  var prevBodyOverflow = "";
@@ -11,7 +59,7 @@ function findBackdrop(id) {
11
59
  function portal(backdrop) {
12
60
  if (backdrop.parentElement !== document.body) document.body.appendChild(backdrop);
13
61
  }
14
- function openModal(id) {
62
+ function openModal(id, opts) {
15
63
  if (typeof document === "undefined") return;
16
64
  if (openId === id) return;
17
65
  if (openId) closeModal(openId);
@@ -22,6 +70,7 @@ function openModal(id) {
22
70
  }
23
71
  const panel = backdrop.querySelector(".modal");
24
72
  if (!panel) return;
73
+ applyPanelSizing(panel, readPanelSizing(backdrop));
25
74
  portal(backdrop);
26
75
  restoreFocusTo = document.activeElement instanceof HTMLElement ? document.activeElement : null;
27
76
  openId = id;
@@ -34,14 +83,14 @@ function openModal(id) {
34
83
  if (!panel.hasAttribute("aria-labelledby")) {
35
84
  const title = panel.querySelector(".modal-title");
36
85
  if (title) {
37
- if (!title.id) title.id = `modal-title-${CSS.escape(id)}`;
86
+ if (!title.id) title.id = `modal-title-${id.replace(/[^\w-]/g, "-")}`;
38
87
  panel.setAttribute("aria-labelledby", title.id);
39
88
  }
40
89
  }
41
90
  if (!panel.hasAttribute("aria-describedby")) {
42
91
  const desc = panel.querySelector(".modal-description");
43
92
  if (desc) {
44
- if (!desc.id) desc.id = `modal-desc-${CSS.escape(id)}`;
93
+ if (!desc.id) desc.id = `modal-desc-${id.replace(/[^\w-]/g, "-")}`;
45
94
  panel.setAttribute("aria-describedby", desc.id);
46
95
  }
47
96
  }
@@ -76,8 +125,14 @@ function openModal(id) {
76
125
  }
77
126
  };
78
127
  window.addEventListener("keydown", keyHandler);
79
- const focusable = panel.querySelector(FOCUSABLE);
80
- (focusable ?? panel).focus();
128
+ if (opts?.initialFocus === "none") {
129
+ } else if (opts?.initialFocus instanceof HTMLElement) {
130
+ opts.initialFocus.focus();
131
+ } else {
132
+ const focusable = panel.querySelector(FOCUSABLE);
133
+ (focusable ?? panel).focus();
134
+ }
135
+ emitIcen(document, "icen:modal-open", { id });
81
136
  }
82
137
  function closeModal(id) {
83
138
  if (typeof document === "undefined") return;
@@ -92,14 +147,17 @@ function closeModal(id) {
92
147
  document.body.style.overflow = prevBodyOverflow;
93
148
  if (restoreFocusTo && restoreFocusTo.isConnected) restoreFocusTo.focus();
94
149
  restoreFocusTo = null;
150
+ emitIcen(document, "icen:modal-close", { id });
95
151
  }
96
152
  function initModal(root) {
97
- if (typeof document === "undefined") return;
153
+ if (typeof document === "undefined") return () => {
154
+ };
98
155
  const scope = root ?? document;
99
156
  scope.querySelectorAll("[data-modal-open]").forEach((el) => el.setAttribute("aria-haspopup", "dialog"));
100
- if (initialized) return;
157
+ if (initialized) return () => {
158
+ };
101
159
  initialized = true;
102
- document.addEventListener("click", (e) => {
160
+ const onClick = (e) => {
103
161
  const target = e.target instanceof Element ? e.target : null;
104
162
  if (!target) return;
105
163
  const opener = target.closest("[data-modal-open]");
@@ -117,9 +175,17 @@ function initModal(root) {
117
175
  if (target instanceof HTMLElement && target.classList.contains("modal-backdrop") && target.dataset.modal && target.dataset.closeOnOverlay !== "false") {
118
176
  closeModal(target.dataset.modal);
119
177
  }
120
- });
178
+ };
179
+ document.addEventListener("click", onClick);
180
+ const destroy = () => {
181
+ document.removeEventListener("click", onClick);
182
+ if (openId) closeModal(openId);
183
+ initialized = false;
184
+ };
185
+ return destroy;
121
186
  }
122
187
  export {
188
+ FOCUSABLE,
123
189
  closeModal,
124
190
  initModal,
125
191
  openModal
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * 为 root 下每个 .nav[data-scroll-reactive] 初始化(root 自身匹配也算)。
3
- * 返回销毁函数:移除本次初始化挂上的全部 window scroll/resize 监听。
3
+ * 返回销毁函数:移除本次初始化挂上的全部 window scroll/resize 与 nav click 监听。
4
4
  */
5
5
  declare function initNav(root?: ParentNode): () => void;
6
6
 
@@ -1,8 +1,19 @@
1
+ // src/behaviors/events.ts
2
+ function emitIcen(source, type, detail) {
3
+ return source.dispatchEvent(
4
+ new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable: true })
5
+ );
6
+ }
7
+
1
8
  // src/behaviors/nav.ts
2
9
  function prefersReducedMotion() {
3
10
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
4
11
  return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
5
12
  }
13
+ var NAV_ITEM_SELECTOR = ".nav-btn, .nav-link";
14
+ function isNavItemDisabled(item) {
15
+ return item.classList.contains("is-disabled") || item.getAttribute("aria-disabled") === "true" || item instanceof HTMLButtonElement && item.disabled;
16
+ }
6
17
  function setup(nav) {
7
18
  const el = nav;
8
19
  if (el.__icenNavInit) return void 0;
@@ -36,17 +47,32 @@ function setup(nav) {
36
47
  ticking = true;
37
48
  window.requestAnimationFrame(apply);
38
49
  };
39
- const onResize = () => {
40
- if (ticking) return;
41
- ticking = true;
42
- window.requestAnimationFrame(apply);
43
- };
44
50
  window.addEventListener("scroll", onScroll, { passive: true });
45
- window.addEventListener("resize", onResize, { passive: true });
51
+ window.addEventListener("resize", onScroll, { passive: true });
46
52
  apply();
53
+ const onSelect = (ev) => {
54
+ const target = ev.target;
55
+ if (!(target instanceof Element)) return;
56
+ const item = target.closest(NAV_ITEM_SELECTOR);
57
+ if (!item || item.closest(".nav") !== nav) return;
58
+ if (isNavItemDisabled(item)) return;
59
+ const items = Array.from(nav.querySelectorAll(NAV_ITEM_SELECTOR));
60
+ const index = items.indexOf(item);
61
+ if (index === -1) return;
62
+ items.forEach((it) => {
63
+ if (it === item) return;
64
+ it.classList.remove("is-active");
65
+ if (it.getAttribute("aria-current") === "page") it.removeAttribute("aria-current");
66
+ });
67
+ item.classList.add("is-active");
68
+ item.setAttribute("aria-current", "page");
69
+ emitIcen(item, "icen:nav-select", { item, index });
70
+ };
71
+ nav.addEventListener("click", onSelect);
47
72
  return () => {
48
73
  window.removeEventListener("scroll", onScroll);
49
- window.removeEventListener("resize", onResize);
74
+ window.removeEventListener("resize", onScroll);
75
+ nav.removeEventListener("click", onSelect);
50
76
  el.__icenNavInit = false;
51
77
  };
52
78
  }
@@ -70,7 +70,10 @@ interface NotificationHandle {
70
70
  id: string;
71
71
  /** 当前 DOM 元素(可能已脱离 DOM) */
72
72
  el: HTMLElement;
73
- /** 关闭本条 */
73
+ /**
74
+ * 关闭本条。设计边界:dismiss 不可拦截(无 before-close 钩子,onClose 只能事后观察);
75
+ * 持久化走 localStorage 是当前设计边界(无网络/多端同步,配额或隐私模式受限时静默放弃)。
76
+ */
74
77
  dismiss(): void;
75
78
  /** 部分更新(title / description / progress / unread / actions / tag 等) */
76
79
  update(patch: NotificationUpdatePatch): void;
@@ -99,6 +102,10 @@ interface NotificationUpdatePatch {
99
102
  link?: string;
100
103
  linkLabel?: string;
101
104
  }
105
+ /**
106
+ * 通知的内部记录形态,同时是 get(id) / getAll() 的公开返回类型
107
+ * (导出以便消费方对返回值做类型注解)。
108
+ */
102
109
  interface NotificationRecord {
103
110
  id: string;
104
111
  title: string;
@@ -129,6 +136,10 @@ interface NotifyFn {
129
136
  info(title: string, opts?: NotificationOptions): NotificationHandle | null;
130
137
  progress(title: string, opts?: NotificationOptions): NotificationHandle | null;
131
138
  confirm(title: string, opts?: NotificationConfirmOptions): Promise<boolean>;
139
+ /**
140
+ * 关闭单条(id 缺省 = 全部)。设计边界:dismiss 不可拦截、持久化走 localStorage
141
+ * 是当前设计边界(详见 NotificationHandle.dismiss)。
142
+ */
132
143
  dismiss(id?: string): void;
133
144
  clear(): void;
134
145
  markRead(id?: string): void;
@@ -166,7 +177,12 @@ interface NotificationPushInput {
166
177
  */
167
178
  declare function createNotificationCenter(parent?: ParentNode, opts?: NotificationConfig): NotificationCenterHandle;
168
179
  declare const notify: NotifyFn;
169
- /** 委托初始化:建立默认容器 + 恢复持久化通知。幂等。 */
170
- declare function initNotification(): void;
180
+ /**
181
+ * 委托初始化:建立默认容器 + 恢复持久化通知。幂等。
182
+ * 返回销毁函数:移除本次恢复的持久化通知(连同 DOM 与 store 记录),默认容器已空时
183
+ * 一并回收,并复位幂等标记(可重新 init)。注意:销毁只回收 init 恢复的内容;
184
+ * notify() 逐条创建的通知不在回收范围(由宿主自行 dismiss),destroy 后 push 仍可用。
185
+ */
186
+ declare function initNotification(): () => void;
171
187
 
172
- export { type NotificationAction, type NotificationCenterHandle, type NotificationConfig, type NotificationConfirmOptions, type NotificationHandle, type NotificationKind, type NotificationOptions, type NotificationPushInput, type NotificationUpdatePatch, createNotificationCenter, initNotification, notify };
188
+ export { type NotificationAction, type NotificationCenterHandle, type NotificationConfig, type NotificationConfirmOptions, type NotificationHandle, type NotificationKind, type NotificationOptions, type NotificationPushInput, type NotificationRecord, type NotificationUpdatePatch, createNotificationCenter, initNotification, notify };
@@ -240,7 +240,7 @@ function buildDescription(text) {
240
240
  el.textContent = text;
241
241
  return el;
242
242
  }
243
- function buildActions(record, getHandle) {
243
+ function buildActions(record) {
244
244
  const actions = record.opts.actions?.length ? record.opts.actions : record.opts.actionLabel ? [{ label: record.opts.actionLabel, onClick: record.opts.onAction }] : null;
245
245
  if (!actions || actions.length === 0) return null;
246
246
  const wrap = document.createElement("div");
@@ -254,13 +254,11 @@ function buildActions(record, getHandle) {
254
254
  btn.textContent = a.label;
255
255
  btn.addEventListener("click", (ev) => {
256
256
  ev.stopPropagation();
257
- const h = getHandle();
258
257
  if (a.onClick) {
259
258
  const ret = a.onClick();
260
259
  if (ret === false) return;
261
260
  }
262
261
  if (!a.keepOpen) dismissRecord(record.id);
263
- else void h;
264
262
  });
265
263
  wrap.appendChild(btn);
266
264
  }
@@ -379,7 +377,7 @@ function renderNotification(record, container, getHandle) {
379
377
  content.className = "notification-content";
380
378
  content.appendChild(buildTitle(record.title));
381
379
  if (record.opts.description) content.appendChild(buildDescription(record.opts.description));
382
- const actions = buildActions(record, getHandle);
380
+ const actions = buildActions(record);
383
381
  if (actions) content.appendChild(actions);
384
382
  const link = buildLink(record);
385
383
  if (link) content.appendChild(link);
@@ -415,6 +413,7 @@ function pushToContainer(container, el, priority) {
415
413
  container.appendChild(el);
416
414
  }
417
415
  }
416
+ var LEAVE_FALLBACK_MS = 360;
418
417
  function removeOne(el, onClose) {
419
418
  if (el.classList.contains("is-leaving")) return;
420
419
  el.classList.add("is-leaving");
@@ -426,23 +425,23 @@ function removeOne(el, onClose) {
426
425
  onClose?.();
427
426
  };
428
427
  el.addEventListener("transitionend", finish, { once: true });
429
- window.setTimeout(finish, 360);
428
+ window.setTimeout(finish, LEAVE_FALLBACK_MS);
430
429
  }
431
- function trimStack(container) {
430
+ function trimStack(container, maxStack, remove) {
432
431
  for (; ; ) {
433
432
  const items = Array.from(container.querySelectorAll(":scope > .notification"));
434
- if (items.length <= cfg.maxStack) return;
433
+ if (items.length <= maxStack) return;
435
434
  const target = items.find((it) => !it.classList.contains("is-priority")) ?? items[0];
436
435
  if (!target) return;
437
436
  const rec = target.id ? store.get(target.id) : void 0;
438
437
  if (rec?.timer) window.clearTimeout(rec.timer);
439
438
  if (target.classList.contains("is-leaving")) {
440
439
  target.remove();
441
- if (target.id) store.delete(target.id);
440
+ if (target.id) remove(target.id);
442
441
  continue;
443
442
  }
444
443
  removeOne(target);
445
- if (target.id) store.delete(target.id);
444
+ if (target.id) remove(target.id);
446
445
  return;
447
446
  }
448
447
  }
@@ -516,38 +515,49 @@ function makeHandle(record) {
516
515
  };
517
516
  return handle;
518
517
  }
519
- function showRecord(title, kind, opts) {
520
- if (!isBrowser()) return null;
521
- const container = ensureContainer();
518
+ function pushRecord(title, kind, opts, container, maxStack, cfgRef, ownerStore) {
522
519
  const id = opts.id ?? genId();
523
- if (store.has(id)) dismissRecord(id);
524
520
  const duration = opts.duration ?? 0;
525
521
  const record = {
526
522
  id,
527
523
  title,
528
524
  kind,
529
- opts: { unread: opts.unread ?? false, ...opts, id },
525
+ // 默认值放展开前会被显式传入的 { unread: undefined } 覆盖回 undefined,故先展开再兜底
526
+ opts: { ...opts, unread: opts.unread ?? false, id },
530
527
  el: null,
531
528
  createdAt: Date.now(),
532
529
  duration,
533
530
  remaining: duration,
534
531
  paused: false,
535
532
  timer: 0,
536
- startedAt: 0
533
+ startedAt: 0,
534
+ cfgRef,
535
+ ownerStore
537
536
  };
538
537
  const handle = makeHandle(record);
539
538
  const el = renderNotification(record, container, () => handle);
540
539
  record.el = el;
541
540
  handle.el = el;
542
541
  pushToContainer(container, el, record.opts.priority ?? "normal");
542
+ ownerStore?.set(id, record);
543
543
  store.set(id, record);
544
544
  attachTimer(record, handle);
545
- trimStack(container);
545
+ trimStack(container, maxStack, (removedId) => {
546
+ ownerStore?.delete(removedId);
547
+ store.delete(removedId);
548
+ });
546
549
  persistSave();
547
- playSound(record.cfgRef?.sound ?? cfg.sound);
550
+ playSound(cfgRef?.sound ?? cfg.sound);
548
551
  opts.onShow?.(handle);
549
552
  return handle;
550
553
  }
554
+ function showRecord(title, kind, opts) {
555
+ if (!isBrowser()) return null;
556
+ const container = ensureContainer();
557
+ const id = opts.id ?? genId();
558
+ if (store.has(id)) dismissRecord(id);
559
+ return pushRecord(title, kind, opts, container, cfg.maxStack);
560
+ }
551
561
  function dismissRecord(id) {
552
562
  const record = store.get(id);
553
563
  if (!record) {
@@ -696,6 +706,7 @@ function createNotificationCenter(parent, opts = {}) {
696
706
  getAll: () => [],
697
707
  config() {
698
708
  },
709
+ // SSR 桩:非真实元素,destroy 前不得触碰 el(本分支内其余方法均为 no-op)
699
710
  el: {}
700
711
  };
701
712
  }
@@ -726,60 +737,15 @@ function createNotificationCenter(parent, opts = {}) {
726
737
  }
727
738
  function push(input) {
728
739
  if (destroyed) return null;
729
- const kind = input.kind ?? "info";
730
- const opts2 = input.options ?? {};
731
- const id = opts2.id ?? genId();
732
- const duration = opts2.duration ?? 0;
733
- const record = {
734
- id,
735
- title: input.title,
736
- kind,
737
- opts: { unread: opts2.unread ?? false, ...opts2, id },
738
- el: null,
739
- createdAt: Date.now(),
740
- duration,
741
- remaining: duration,
742
- paused: false,
743
- timer: 0,
744
- startedAt: 0,
745
- cfgRef: local,
746
- ownerStore: localStore
747
- };
748
- const handle = makeHandle(record);
749
- const el = renderNotification(record, container, () => handle);
750
- record.el = el;
751
- handle.el = el;
752
- pushToContainer(container, el, record.opts.priority ?? "normal");
753
- localStore.set(id, record);
754
- store.set(id, record);
755
- attachTimer(record, handle);
756
- for (; ; ) {
757
- const items = Array.from(container.querySelectorAll(":scope > .notification"));
758
- if (items.length <= local.maxStack) break;
759
- const target = items.find((it) => !it.classList.contains("is-priority")) ?? items[0];
760
- if (!target) break;
761
- const tid = target.id;
762
- const rec = tid ? localStore.get(tid) : void 0;
763
- if (rec?.timer) window.clearTimeout(rec.timer);
764
- if (target.classList.contains("is-leaving")) {
765
- target.remove();
766
- if (tid) {
767
- localStore.delete(tid);
768
- store.delete(tid);
769
- }
770
- continue;
771
- }
772
- removeOne(target);
773
- if (tid) {
774
- localStore.delete(tid);
775
- store.delete(tid);
776
- }
777
- break;
778
- }
779
- persistSave();
780
- playSound(local.sound);
781
- opts2.onShow?.(handle);
782
- return handle;
740
+ return pushRecord(
741
+ input.title,
742
+ input.kind ?? "info",
743
+ input.options ?? {},
744
+ container,
745
+ local.maxStack,
746
+ local,
747
+ localStore
748
+ );
783
749
  }
784
750
  function dismiss(id) {
785
751
  if (id) {
@@ -889,17 +855,31 @@ var notify = Object.assign(
889
855
  }
890
856
  );
891
857
  function initNotification() {
892
- if (!isBrowser()) return;
893
- if (globalInit.done) return;
858
+ if (!isBrowser()) return () => {
859
+ };
860
+ if (globalInit.done) return () => {
861
+ };
894
862
  globalInit.done = true;
895
863
  const container = ensureContainer();
896
864
  const key = persistKeyFor(cfg.persist);
865
+ const restored = [];
897
866
  if (key) {
898
867
  knownPersistKeys.add(key);
899
868
  for (const r of restorePersisted(container, key)) {
900
869
  store.set(r.id, r);
870
+ restored.push(r);
901
871
  }
902
872
  }
873
+ return () => {
874
+ for (const r of restored) {
875
+ if (r.timer) window.clearTimeout(r.timer);
876
+ store.delete(r.id);
877
+ r.el?.remove();
878
+ }
879
+ restored.length = 0;
880
+ if (container.childElementCount === 0) container.remove();
881
+ globalInit.done = false;
882
+ };
903
883
  }
904
884
  export {
905
885
  createNotificationCenter,
@@ -1,3 +1,4 @@
1
+ /** 布局锚矩形(视口坐标,宽高齐全,元素锚或手工锚共用此形状)。 */
1
2
  interface PopoverRect {
2
3
  left: number;
3
4
  top: number;
@@ -6,11 +7,14 @@ interface PopoverRect {
6
7
  width: number;
7
8
  height: number;
8
9
  }
10
+ /** 视口尺寸(可用空间计算的基准)。 */
9
11
  interface PopoverViewport {
10
12
  width: number;
11
13
  height: number;
12
14
  }
13
- type PopoverSide = 'top' | 'bottom';
15
+ /** 浮层相对锚的展开侧。 */
16
+ type PopoverSide = 'top' | 'bottom' | 'left' | 'right';
17
+ /** 浮层与锚的对齐方式(stretch = 纵向侧时随锚宽拉伸)。 */
14
18
  type PopoverAlign = 'start' | 'center' | 'end' | 'stretch';
15
19
  interface PopoverLayoutOptions {
16
20
  side?: PopoverSide;
@@ -24,6 +28,33 @@ interface PopoverLayoutOptions {
24
28
  /** 内容预期高度:避免用被裁剪的实测高度参与选侧 */
25
29
  contentHeightHint?: number;
26
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;
27
58
  interface PopoverLayout {
28
59
  side: PopoverSide;
29
60
  left: number;
@@ -33,14 +64,19 @@ interface PopoverLayout {
33
64
  }
34
65
  interface OpenPopoverOptions extends PopoverLayoutOptions {
35
66
  anchor: Element | PopoverRect;
67
+ /** 统一面板尺寸契约(优先于面板元素的 data-panel-* 与上方直接尺寸字段) */
68
+ sizing?: PanelSizing;
36
69
  /** 关闭(含外点/Esc 触发的关闭)后的回调 */
37
70
  onClose?: () => void;
38
71
  }
39
- /** 视口智能布局:返回 {side, left, top, width, maxHeight}。 */
72
+ /** 视口智能布局:返回 {side, left, top, width, maxHeight}。
73
+ * 纵向侧(top/bottom):选侧按上下空间,宽按 min/max 夹取,高按内容预期。
74
+ * 横向侧(left/right):锚点左右翻转,宽取 min(无内容测量,横向侧取下限),
75
+ * 高占满可用空间,纵向按 align(start/center/end;stretch 视作 start)贴锚。 */
40
76
  declare function computePopoverLayout(anchor: PopoverRect, viewport: PopoverViewport, opts?: PopoverLayoutOptions): PopoverLayout;
41
77
  /** 打开浮层:panel 移入 body 并按 anchor 布局;重复调用同一 panel 为先关后开。 */
42
78
  declare function openPopover(panel: HTMLElement, options: OpenPopoverOptions): void;
43
79
  /** 关闭浮层:移除监听并把 panel 还原到打开前的位置与 hidden 状态。 */
44
80
  declare function closePopover(panel: HTMLElement): void;
45
81
 
46
- export { type OpenPopoverOptions, type PopoverAlign, type PopoverLayout, type PopoverLayoutOptions, type PopoverRect, type PopoverSide, type PopoverViewport, closePopover, computePopoverLayout, openPopover };
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 };