@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
@@ -42,37 +42,52 @@ function syncContainerPosition(el) {
42
42
  el.dataset.position = cfg.position;
43
43
  }
44
44
  var DEFAULT_PERSIST_KEY = "icen.ui.notifications";
45
- function persistKey() {
46
- if (cfg.persist === true) return DEFAULT_PERSIST_KEY;
47
- if (typeof cfg.persist === "string") return cfg.persist;
45
+ var knownPersistKeys = /* @__PURE__ */ new Set();
46
+ function persistKeyFor(p) {
47
+ if (p === true) return DEFAULT_PERSIST_KEY;
48
+ if (typeof p === "string" && p.length > 0) return p;
48
49
  return null;
49
50
  }
51
+ function recordPersist(record) {
52
+ return record.cfgRef ? record.cfgRef.persist : cfg.persist;
53
+ }
50
54
  function persistSave() {
51
- const key = persistKey();
52
- if (!key) return;
53
55
  try {
54
- const items = Array.from(store.values()).filter((r) => !r.opts.duration || r.opts.duration === 0).map((r) => ({
55
- id: r.id,
56
- title: r.title,
57
- kind: r.kind,
58
- description: r.opts.description ?? "",
59
- tag: r.opts.tag,
60
- unread: r.opts.unread,
61
- priority: r.opts.priority,
62
- progress: r.opts.progress,
63
- progressLabel: r.opts.progressLabel,
64
- link: r.opts.link,
65
- linkLabel: r.opts.linkLabel,
66
- data: r.opts.data,
67
- createdAt: r.createdAt
68
- }));
69
- localStorage.setItem(key, JSON.stringify(items));
56
+ const groups = /* @__PURE__ */ new Map();
57
+ for (const key of knownPersistKeys) groups.set(key, []);
58
+ for (const r of store.values()) {
59
+ const key = persistKeyFor(recordPersist(r));
60
+ if (!key) continue;
61
+ knownPersistKeys.add(key);
62
+ let bucket = groups.get(key);
63
+ if (!bucket) {
64
+ bucket = [];
65
+ groups.set(key, bucket);
66
+ }
67
+ if (r.opts.duration) continue;
68
+ bucket.push({
69
+ id: r.id,
70
+ title: r.title,
71
+ kind: r.kind,
72
+ description: r.opts.description ?? "",
73
+ tag: r.opts.tag,
74
+ unread: r.opts.unread,
75
+ priority: r.opts.priority,
76
+ progress: r.opts.progress,
77
+ progressLabel: r.opts.progressLabel,
78
+ link: r.opts.link,
79
+ linkLabel: r.opts.linkLabel,
80
+ data: r.opts.data,
81
+ createdAt: r.createdAt
82
+ });
83
+ }
84
+ for (const [key, items] of groups) {
85
+ localStorage.setItem(key, JSON.stringify(items));
86
+ }
70
87
  } catch {
71
88
  }
72
89
  }
73
- function persistLoad() {
74
- const key = persistKey();
75
- if (!key) return [];
90
+ function persistLoad(key) {
76
91
  try {
77
92
  const raw = localStorage.getItem(key);
78
93
  if (!raw) return [];
@@ -82,12 +97,49 @@ function persistLoad() {
82
97
  return [];
83
98
  }
84
99
  }
100
+ function restorePersisted(container, key, cfgRef) {
101
+ const records = [];
102
+ for (const item of persistLoad(key)) {
103
+ const record = {
104
+ id: item.id,
105
+ title: item.title,
106
+ kind: item.kind,
107
+ opts: {
108
+ id: item.id,
109
+ description: item.description,
110
+ tag: item.tag,
111
+ unread: item.unread,
112
+ priority: item.priority,
113
+ progress: item.progress,
114
+ progressLabel: item.progressLabel,
115
+ link: item.link,
116
+ linkLabel: item.linkLabel,
117
+ data: item.data
118
+ },
119
+ el: null,
120
+ createdAt: item.createdAt ?? Date.now(),
121
+ duration: 0,
122
+ remaining: 0,
123
+ paused: false,
124
+ timer: 0,
125
+ startedAt: 0,
126
+ cfgRef
127
+ };
128
+ const handle = makeHandle(record);
129
+ const el = renderNotification(record, container, () => handle);
130
+ record.el = el;
131
+ handle.el = el;
132
+ pushToContainer(container, el, record.opts.priority ?? "normal");
133
+ records.push(record);
134
+ }
135
+ return records;
136
+ }
85
137
  var audioCtx = null;
86
- function playSound() {
87
- if (!cfg.sound) return;
138
+ function playSound(sound) {
139
+ if (!sound) return;
88
140
  try {
89
- if (typeof cfg.sound === "string") {
90
- const audio = new Audio(cfg.sound);
141
+ if (typeof sound === "string") {
142
+ const audio = new Audio(sound);
91
143
  void audio.play();
92
144
  return;
93
145
  }
@@ -115,6 +167,28 @@ function playSound() {
115
167
  } catch {
116
168
  }
117
169
  }
170
+ function stripUnsafeSvg(el) {
171
+ for (const attr of Array.from(el.attributes)) {
172
+ if (attr.name.toLowerCase().startsWith("on")) el.removeAttribute(attr.name);
173
+ }
174
+ for (const child of Array.from(el.children)) {
175
+ const tag = child.tagName.toLowerCase();
176
+ if (tag === "script" || tag === "foreignobject") child.remove();
177
+ else stripUnsafeSvg(child);
178
+ }
179
+ }
180
+ function sanitizeSvgIcon(source) {
181
+ if (!source.trimStart().startsWith("<svg")) return null;
182
+ try {
183
+ const doc = new DOMParser().parseFromString(source, "image/svg+xml");
184
+ const svg = doc.documentElement;
185
+ if (!svg || svg.tagName.toLowerCase() !== "svg") return null;
186
+ stripUnsafeSvg(svg);
187
+ return document.importNode(svg, true);
188
+ } catch {
189
+ return null;
190
+ }
191
+ }
118
192
  function buildIcon(record) {
119
193
  const wrap = document.createElement("span");
120
194
  wrap.className = "notification-icon";
@@ -141,15 +215,16 @@ function buildIcon(record) {
141
215
  img.alt = "";
142
216
  wrap.appendChild(img);
143
217
  } else {
144
- wrap.innerHTML = icon;
218
+ const svg = sanitizeSvgIcon(icon);
219
+ if (svg) wrap.appendChild(svg);
220
+ else wrap.textContent = icon;
145
221
  }
146
222
  return wrap;
147
223
  }
148
- if (record.opts.progress !== void 0 && record.opts.progress >= 0) {
149
- wrap.innerHTML = PROGRESS_ICON;
150
- } else {
151
- wrap.innerHTML = KIND_ICON[record.kind];
152
- }
224
+ const fallback = sanitizeSvgIcon(
225
+ record.opts.progress !== void 0 && record.opts.progress >= 0 ? PROGRESS_ICON : KIND_ICON[record.kind]
226
+ );
227
+ if (fallback) wrap.appendChild(fallback);
153
228
  return wrap;
154
229
  }
155
230
  function buildTitle(title) {
@@ -165,7 +240,7 @@ function buildDescription(text) {
165
240
  el.textContent = text;
166
241
  return el;
167
242
  }
168
- function buildActions(record, getHandle) {
243
+ function buildActions(record) {
169
244
  const actions = record.opts.actions?.length ? record.opts.actions : record.opts.actionLabel ? [{ label: record.opts.actionLabel, onClick: record.opts.onAction }] : null;
170
245
  if (!actions || actions.length === 0) return null;
171
246
  const wrap = document.createElement("div");
@@ -179,13 +254,11 @@ function buildActions(record, getHandle) {
179
254
  btn.textContent = a.label;
180
255
  btn.addEventListener("click", (ev) => {
181
256
  ev.stopPropagation();
182
- const h = getHandle();
183
257
  if (a.onClick) {
184
258
  const ret = a.onClick();
185
259
  if (ret === false) return;
186
260
  }
187
261
  if (!a.keepOpen) dismissRecord(record.id);
188
- else void h;
189
262
  });
190
263
  wrap.appendChild(btn);
191
264
  }
@@ -243,13 +316,39 @@ function buildCountdownBar(record) {
243
316
  bar.appendChild(fill);
244
317
  return bar;
245
318
  }
319
+ function syncCountdownBar(record) {
320
+ const el = record.el;
321
+ if (!el) return;
322
+ const existing = el.querySelector(".notification-countdown");
323
+ if (record.duration <= 0 || record.opts.showCountdown === false) {
324
+ existing?.remove();
325
+ return;
326
+ }
327
+ let fill;
328
+ if (existing) {
329
+ fill = existing.querySelector(".notification-countdown-fill");
330
+ } else {
331
+ const bar = buildCountdownBar(record);
332
+ if (!bar) return;
333
+ el.appendChild(bar);
334
+ fill = bar.querySelector(".notification-countdown-fill");
335
+ }
336
+ if (fill) {
337
+ fill.style.animation = "none";
338
+ void fill.offsetWidth;
339
+ fill.style.animation = "";
340
+ fill.style.animationDuration = record.duration + "ms";
341
+ fill.style.animationPlayState = "running";
342
+ }
343
+ }
246
344
  function buildClose(record) {
247
345
  if (record.opts.closable === false) return null;
248
346
  const btn = document.createElement("button");
249
347
  btn.type = "button";
250
348
  btn.className = "notification-close";
251
349
  btn.setAttribute("aria-label", "\u5173\u95ED");
252
- btn.innerHTML = CLOSE_SVG;
350
+ const svg = sanitizeSvgIcon(CLOSE_SVG);
351
+ if (svg) btn.appendChild(svg);
253
352
  btn.addEventListener("click", (ev) => {
254
353
  ev.stopPropagation();
255
354
  dismissRecord(record.id);
@@ -258,7 +357,7 @@ function buildClose(record) {
258
357
  }
259
358
  function renderNotification(record, container, getHandle) {
260
359
  const el = document.createElement("div");
261
- let cls = record.kind === "info" ? "notification" : `notification notif--${record.kind}`;
360
+ let cls = record.kind === "info" ? "notification" : `notification notification--${record.kind}`;
262
361
  if (record.opts.priority === "high") cls += " is-priority";
263
362
  if (record.opts.unread) cls += " is-unread";
264
363
  if (record.opts.progress !== void 0 && record.opts.progress >= 0) cls += " is-progress";
@@ -278,7 +377,7 @@ function renderNotification(record, container, getHandle) {
278
377
  content.className = "notification-content";
279
378
  content.appendChild(buildTitle(record.title));
280
379
  if (record.opts.description) content.appendChild(buildDescription(record.opts.description));
281
- const actions = buildActions(record, getHandle);
380
+ const actions = buildActions(record);
282
381
  if (actions) content.appendChild(actions);
283
382
  const link = buildLink(record);
284
383
  if (link) content.appendChild(link);
@@ -314,9 +413,10 @@ function pushToContainer(container, el, priority) {
314
413
  container.appendChild(el);
315
414
  }
316
415
  }
416
+ var LEAVE_FALLBACK_MS = 360;
317
417
  function removeOne(el, onClose) {
318
- if (el.classList.contains("leaving")) return;
319
- el.classList.add("leaving");
418
+ if (el.classList.contains("is-leaving")) return;
419
+ el.classList.add("is-leaving");
320
420
  let done = false;
321
421
  const finish = () => {
322
422
  if (done) return;
@@ -325,32 +425,43 @@ function removeOne(el, onClose) {
325
425
  onClose?.();
326
426
  };
327
427
  el.addEventListener("transitionend", finish, { once: true });
328
- window.setTimeout(finish, 360);
428
+ window.setTimeout(finish, LEAVE_FALLBACK_MS);
329
429
  }
330
- function trimStack(container) {
430
+ function trimStack(container, maxStack, remove) {
331
431
  for (; ; ) {
332
432
  const items = Array.from(container.querySelectorAll(":scope > .notification"));
333
- if (items.length <= cfg.maxStack) return;
433
+ if (items.length <= maxStack) return;
334
434
  const target = items.find((it) => !it.classList.contains("is-priority")) ?? items[0];
335
435
  if (!target) return;
436
+ const rec = target.id ? store.get(target.id) : void 0;
437
+ if (rec?.timer) window.clearTimeout(rec.timer);
438
+ if (target.classList.contains("is-leaving")) {
439
+ target.remove();
440
+ if (target.id) remove(target.id);
441
+ continue;
442
+ }
336
443
  removeOne(target);
444
+ if (target.id) remove(target.id);
445
+ return;
337
446
  }
338
447
  }
339
448
  function attachTimer(record, handle) {
340
- if (!record.duration || record.duration <= 0) return;
341
449
  const el = record.el;
342
450
  if (!el) return;
343
451
  const start = () => {
344
452
  if (record.timer) window.clearTimeout(record.timer);
453
+ record.timer = 0;
454
+ if (record.remaining <= 0) return;
455
+ record.startedAt = Date.now();
345
456
  record.timer = window.setTimeout(() => dismissRecord(record.id), record.remaining);
346
457
  record.paused = false;
347
458
  };
348
459
  const pause = () => {
349
460
  if (record.paused) return;
350
- if (record.timer) {
351
- window.clearTimeout(record.timer);
352
- record.timer = 0;
353
- }
461
+ if (!record.timer) return;
462
+ window.clearTimeout(record.timer);
463
+ record.timer = 0;
464
+ record.remaining = Math.max(0, record.remaining - (Date.now() - record.startedAt));
354
465
  record.paused = true;
355
466
  el.classList.add("is-paused");
356
467
  const fill = el.querySelector(".notification-countdown-fill");
@@ -358,19 +469,33 @@ function attachTimer(record, handle) {
358
469
  };
359
470
  const resume = () => {
360
471
  if (!record.paused) return;
472
+ record.paused = false;
361
473
  el.classList.remove("is-paused");
362
474
  const fill = el.querySelector(".notification-countdown-fill");
363
475
  if (fill) fill.style.animationPlayState = "running";
476
+ if (record.remaining <= 0) {
477
+ dismissRecord(record.id);
478
+ return;
479
+ }
480
+ start();
481
+ };
482
+ record.restartTimer = () => {
483
+ record.paused = false;
484
+ el.classList.remove("is-paused");
364
485
  start();
365
486
  };
366
- const pauseOnHover = record.opts.pauseOnHover ?? cfg.pauseOnHover;
367
- if (pauseOnHover) {
487
+ const pauseOnHover = record.opts.pauseOnHover ?? record.cfgRef?.pauseOnHover ?? cfg.pauseOnHover;
488
+ if (pauseOnHover && !record.hoverBound) {
489
+ record.hoverBound = true;
368
490
  el.addEventListener("mouseenter", () => handle.pause());
369
491
  el.addEventListener("mouseleave", () => handle.resume());
370
492
  }
371
493
  handle.pause = pause;
372
494
  handle.resume = resume;
373
- start();
495
+ if (record.duration > 0) {
496
+ record.remaining = record.duration;
497
+ start();
498
+ }
374
499
  }
375
500
  function makeHandle(record) {
376
501
  const handle = {
@@ -390,37 +515,49 @@ function makeHandle(record) {
390
515
  };
391
516
  return handle;
392
517
  }
393
- function showRecord(title, kind, opts) {
394
- if (!isBrowser()) return null;
395
- const container = ensureContainer();
518
+ function pushRecord(title, kind, opts, container, maxStack, cfgRef, ownerStore) {
396
519
  const id = opts.id ?? genId();
397
- if (store.has(id)) dismissRecord(id);
398
520
  const duration = opts.duration ?? 0;
399
521
  const record = {
400
522
  id,
401
523
  title,
402
524
  kind,
403
- opts: { unread: opts.unread ?? false, ...opts, id },
525
+ // 默认值放展开前会被显式传入的 { unread: undefined } 覆盖回 undefined,故先展开再兜底
526
+ opts: { ...opts, unread: opts.unread ?? false, id },
404
527
  el: null,
405
528
  createdAt: Date.now(),
406
529
  duration,
407
530
  remaining: duration,
408
531
  paused: false,
409
- timer: 0
532
+ timer: 0,
533
+ startedAt: 0,
534
+ cfgRef,
535
+ ownerStore
410
536
  };
411
537
  const handle = makeHandle(record);
412
538
  const el = renderNotification(record, container, () => handle);
413
539
  record.el = el;
414
540
  handle.el = el;
415
541
  pushToContainer(container, el, record.opts.priority ?? "normal");
542
+ ownerStore?.set(id, record);
416
543
  store.set(id, record);
417
544
  attachTimer(record, handle);
418
- trimStack(container);
545
+ trimStack(container, maxStack, (removedId) => {
546
+ ownerStore?.delete(removedId);
547
+ store.delete(removedId);
548
+ });
419
549
  persistSave();
420
- playSound();
550
+ playSound(cfgRef?.sound ?? cfg.sound);
421
551
  opts.onShow?.(handle);
422
552
  return handle;
423
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
+ }
424
561
  function dismissRecord(id) {
425
562
  const record = store.get(id);
426
563
  if (!record) {
@@ -432,6 +569,7 @@ function dismissRecord(id) {
432
569
  const handle = makeHandle(record);
433
570
  const onClose = record.opts.onClose;
434
571
  store.delete(id);
572
+ record.ownerStore?.delete(id);
435
573
  if (record.el) removeOne(record.el, () => onClose?.(handle));
436
574
  else onClose?.(handle);
437
575
  persistSave();
@@ -457,6 +595,11 @@ function updateRecord(id, patch) {
457
595
  if (patch.linkLabel !== void 0) record.opts.linkLabel = patch.linkLabel;
458
596
  const el = record.el;
459
597
  if (!el) return;
598
+ if (patch.duration !== void 0 && el.isConnected) {
599
+ syncCountdownBar(record);
600
+ if (record.restartTimer) record.restartTimer();
601
+ else if (patch.duration > 0) attachTimer(record, makeHandle(record));
602
+ }
460
603
  if (patch.title !== void 0) {
461
604
  const t = el.querySelector(".notification-title");
462
605
  if (t) t.textContent = patch.title;
@@ -547,7 +690,7 @@ function confirmDialog(title, opts = {}) {
547
690
  if (!handle) settle(false);
548
691
  });
549
692
  }
550
- function createNotificationCenter(parent = document.body, opts = {}) {
693
+ function createNotificationCenter(parent, opts = {}) {
551
694
  if (!isBrowser()) {
552
695
  return {
553
696
  push: () => null,
@@ -557,13 +700,17 @@ function createNotificationCenter(parent = document.body, opts = {}) {
557
700
  },
558
701
  markAllRead() {
559
702
  },
703
+ destroy() {
704
+ },
560
705
  get: () => null,
561
706
  getAll: () => [],
562
707
  config() {
563
708
  },
709
+ // SSR 桩:非真实元素,destroy 前不得触碰 el(本分支内其余方法均为 no-op)
564
710
  el: {}
565
711
  };
566
712
  }
713
+ const mount = parent ?? document.body;
567
714
  const local = {
568
715
  position: opts.position ?? "top-right",
569
716
  maxStack: opts.maxStack ?? 5,
@@ -576,48 +723,29 @@ function createNotificationCenter(parent = document.body, opts = {}) {
576
723
  container.setAttribute("role", "region");
577
724
  container.setAttribute("aria-label", "\u901A\u77E5");
578
725
  container.dataset.position = local.position;
579
- parent.appendChild(container);
726
+ mount.appendChild(container);
580
727
  const localStore = /* @__PURE__ */ new Map();
581
- function push(input) {
582
- const kind = input.kind ?? "info";
583
- const opts2 = input.options ?? {};
584
- const id = opts2.id ?? genId();
585
- const duration = opts2.duration ?? 0;
586
- const record = {
587
- id,
588
- title: input.title,
589
- kind,
590
- opts: { unread: opts2.unread ?? false, ...opts2, id },
591
- el: null,
592
- createdAt: Date.now(),
593
- duration,
594
- remaining: duration,
595
- paused: false,
596
- timer: 0
597
- };
598
- const handle = makeHandle(record);
599
- const el = renderNotification(record, container, () => handle);
600
- record.el = el;
601
- handle.el = el;
602
- pushToContainer(container, el, record.opts.priority ?? "normal");
603
- localStore.set(id, record);
604
- store.set(id, record);
605
- attachTimer(record, handle);
606
- for (; ; ) {
607
- const items = Array.from(container.querySelectorAll(":scope > .notification"));
608
- if (items.length <= local.maxStack) break;
609
- const target = items.find((it) => !it.classList.contains("is-priority")) ?? items[0];
610
- if (!target) break;
611
- const tid = target.id;
612
- removeOne(target);
613
- const rec = tid ? localStore.get(tid) : null;
614
- if (rec) {
615
- localStore.delete(tid);
616
- store.delete(tid);
617
- }
728
+ let destroyed = false;
729
+ const pKey = persistKeyFor(local.persist);
730
+ if (pKey) {
731
+ knownPersistKeys.add(pKey);
732
+ for (const r of restorePersisted(container, pKey, local)) {
733
+ r.ownerStore = localStore;
734
+ localStore.set(r.id, r);
735
+ store.set(r.id, r);
618
736
  }
619
- opts2.onShow?.(handle);
620
- return handle;
737
+ }
738
+ function push(input) {
739
+ if (destroyed) return null;
740
+ return pushRecord(
741
+ input.title,
742
+ input.kind ?? "info",
743
+ input.options ?? {},
744
+ container,
745
+ local.maxStack,
746
+ local,
747
+ localStore
748
+ );
621
749
  }
622
750
  function dismiss(id) {
623
751
  if (id) {
@@ -628,6 +756,7 @@ function createNotificationCenter(parent = document.body, opts = {}) {
628
756
  localStore.delete(id);
629
757
  store.delete(id);
630
758
  if (rec.el) removeOne(rec.el, () => rec.opts.onClose?.(handle));
759
+ persistSave();
631
760
  return;
632
761
  }
633
762
  Array.from(localStore.keys()).forEach((k) => dismiss(k));
@@ -641,11 +770,22 @@ function createNotificationCenter(parent = document.body, opts = {}) {
641
770
  rec.el?.classList.remove("is-unread");
642
771
  });
643
772
  }
773
+ function destroy() {
774
+ if (destroyed) return;
775
+ destroyed = true;
776
+ localStore.forEach((rec) => {
777
+ if (rec.timer) window.clearTimeout(rec.timer);
778
+ store.delete(rec.id);
779
+ });
780
+ localStore.clear();
781
+ container.remove();
782
+ }
644
783
  return {
645
784
  push,
646
785
  dismiss,
647
786
  clear,
648
787
  markAllRead,
788
+ destroy,
649
789
  get: (id) => localStore.get(id) ?? null,
650
790
  getAll: () => Array.from(localStore.values()),
651
791
  config: (next) => {
@@ -655,7 +795,11 @@ function createNotificationCenter(parent = document.body, opts = {}) {
655
795
  }
656
796
  if (typeof next.maxStack === "number" && next.maxStack > 0) local.maxStack = next.maxStack;
657
797
  if (typeof next.pauseOnHover === "boolean") local.pauseOnHover = next.pauseOnHover;
658
- if (next.persist !== void 0) local.persist = next.persist;
798
+ if (next.persist !== void 0) {
799
+ local.persist = next.persist;
800
+ const k = persistKeyFor(next.persist);
801
+ if (k) knownPersistKeys.add(k);
802
+ }
659
803
  if (next.sound !== void 0) local.sound = next.sound;
660
804
  },
661
805
  el: container
@@ -701,51 +845,41 @@ var notify = Object.assign(
701
845
  }
702
846
  if (typeof opts.maxStack === "number" && opts.maxStack > 0) cfg.maxStack = opts.maxStack;
703
847
  if (typeof opts.pauseOnHover === "boolean") cfg.pauseOnHover = opts.pauseOnHover;
704
- if (opts.persist !== void 0) cfg.persist = opts.persist;
848
+ if (opts.persist !== void 0) {
849
+ cfg.persist = opts.persist;
850
+ const k = persistKeyFor(opts.persist);
851
+ if (k) knownPersistKeys.add(k);
852
+ }
705
853
  if (opts.sound !== void 0) cfg.sound = opts.sound;
706
854
  }
707
855
  }
708
856
  );
709
857
  function initNotification() {
710
- if (!isBrowser()) return;
711
- if (globalInit.done) return;
858
+ if (!isBrowser()) return () => {
859
+ };
860
+ if (globalInit.done) return () => {
861
+ };
712
862
  globalInit.done = true;
713
- ensureContainer();
714
- if (cfg.persist) {
715
- const items = persistLoad();
716
- const container = ensureContainer();
717
- for (const item of items) {
718
- const record = {
719
- id: item.id,
720
- title: item.title,
721
- kind: item.kind,
722
- opts: {
723
- id: item.id,
724
- description: item.description,
725
- tag: item.tag,
726
- unread: item.unread,
727
- priority: item.priority,
728
- progress: item.progress,
729
- progressLabel: item.progressLabel,
730
- link: item.link,
731
- linkLabel: item.linkLabel,
732
- data: item.data
733
- },
734
- el: null,
735
- createdAt: item.createdAt ?? Date.now(),
736
- duration: 0,
737
- remaining: 0,
738
- paused: false,
739
- timer: 0
740
- };
741
- const handle = makeHandle(record);
742
- const el = renderNotification(record, container, () => handle);
743
- record.el = el;
744
- handle.el = el;
745
- pushToContainer(container, el, record.opts.priority ?? "normal");
746
- store.set(record.id, record);
863
+ const container = ensureContainer();
864
+ const key = persistKeyFor(cfg.persist);
865
+ const restored = [];
866
+ if (key) {
867
+ knownPersistKeys.add(key);
868
+ for (const r of restorePersisted(container, key)) {
869
+ store.set(r.id, r);
870
+ restored.push(r);
747
871
  }
748
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
+ };
749
883
  }
750
884
  export {
751
885
  createNotificationCenter,