@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
@@ -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) {
@@ -243,13 +318,39 @@ function buildCountdownBar(record) {
243
318
  bar.appendChild(fill);
244
319
  return bar;
245
320
  }
321
+ function syncCountdownBar(record) {
322
+ const el = record.el;
323
+ if (!el) return;
324
+ const existing = el.querySelector(".notification-countdown");
325
+ if (record.duration <= 0 || record.opts.showCountdown === false) {
326
+ existing?.remove();
327
+ return;
328
+ }
329
+ let fill;
330
+ if (existing) {
331
+ fill = existing.querySelector(".notification-countdown-fill");
332
+ } else {
333
+ const bar = buildCountdownBar(record);
334
+ if (!bar) return;
335
+ el.appendChild(bar);
336
+ fill = bar.querySelector(".notification-countdown-fill");
337
+ }
338
+ if (fill) {
339
+ fill.style.animation = "none";
340
+ void fill.offsetWidth;
341
+ fill.style.animation = "";
342
+ fill.style.animationDuration = record.duration + "ms";
343
+ fill.style.animationPlayState = "running";
344
+ }
345
+ }
246
346
  function buildClose(record) {
247
347
  if (record.opts.closable === false) return null;
248
348
  const btn = document.createElement("button");
249
349
  btn.type = "button";
250
350
  btn.className = "notification-close";
251
351
  btn.setAttribute("aria-label", "\u5173\u95ED");
252
- btn.innerHTML = CLOSE_SVG;
352
+ const svg = sanitizeSvgIcon(CLOSE_SVG);
353
+ if (svg) btn.appendChild(svg);
253
354
  btn.addEventListener("click", (ev) => {
254
355
  ev.stopPropagation();
255
356
  dismissRecord(record.id);
@@ -258,7 +359,7 @@ function buildClose(record) {
258
359
  }
259
360
  function renderNotification(record, container, getHandle) {
260
361
  const el = document.createElement("div");
261
- let cls = record.kind === "info" ? "notification" : `notification notif--${record.kind}`;
362
+ let cls = record.kind === "info" ? "notification" : `notification notification--${record.kind}`;
262
363
  if (record.opts.priority === "high") cls += " is-priority";
263
364
  if (record.opts.unread) cls += " is-unread";
264
365
  if (record.opts.progress !== void 0 && record.opts.progress >= 0) cls += " is-progress";
@@ -315,8 +416,8 @@ function pushToContainer(container, el, priority) {
315
416
  }
316
417
  }
317
418
  function removeOne(el, onClose) {
318
- if (el.classList.contains("leaving")) return;
319
- el.classList.add("leaving");
419
+ if (el.classList.contains("is-leaving")) return;
420
+ el.classList.add("is-leaving");
320
421
  let done = false;
321
422
  const finish = () => {
322
423
  if (done) return;
@@ -333,24 +434,35 @@ function trimStack(container) {
333
434
  if (items.length <= cfg.maxStack) return;
334
435
  const target = items.find((it) => !it.classList.contains("is-priority")) ?? items[0];
335
436
  if (!target) return;
437
+ const rec = target.id ? store.get(target.id) : void 0;
438
+ if (rec?.timer) window.clearTimeout(rec.timer);
439
+ if (target.classList.contains("is-leaving")) {
440
+ target.remove();
441
+ if (target.id) store.delete(target.id);
442
+ continue;
443
+ }
336
444
  removeOne(target);
445
+ if (target.id) store.delete(target.id);
446
+ return;
337
447
  }
338
448
  }
339
449
  function attachTimer(record, handle) {
340
- if (!record.duration || record.duration <= 0) return;
341
450
  const el = record.el;
342
451
  if (!el) return;
343
452
  const start = () => {
344
453
  if (record.timer) window.clearTimeout(record.timer);
454
+ record.timer = 0;
455
+ if (record.remaining <= 0) return;
456
+ record.startedAt = Date.now();
345
457
  record.timer = window.setTimeout(() => dismissRecord(record.id), record.remaining);
346
458
  record.paused = false;
347
459
  };
348
460
  const pause = () => {
349
461
  if (record.paused) return;
350
- if (record.timer) {
351
- window.clearTimeout(record.timer);
352
- record.timer = 0;
353
- }
462
+ if (!record.timer) return;
463
+ window.clearTimeout(record.timer);
464
+ record.timer = 0;
465
+ record.remaining = Math.max(0, record.remaining - (Date.now() - record.startedAt));
354
466
  record.paused = true;
355
467
  el.classList.add("is-paused");
356
468
  const fill = el.querySelector(".notification-countdown-fill");
@@ -358,19 +470,33 @@ function attachTimer(record, handle) {
358
470
  };
359
471
  const resume = () => {
360
472
  if (!record.paused) return;
473
+ record.paused = false;
361
474
  el.classList.remove("is-paused");
362
475
  const fill = el.querySelector(".notification-countdown-fill");
363
476
  if (fill) fill.style.animationPlayState = "running";
477
+ if (record.remaining <= 0) {
478
+ dismissRecord(record.id);
479
+ return;
480
+ }
481
+ start();
482
+ };
483
+ record.restartTimer = () => {
484
+ record.paused = false;
485
+ el.classList.remove("is-paused");
364
486
  start();
365
487
  };
366
- const pauseOnHover = record.opts.pauseOnHover ?? cfg.pauseOnHover;
367
- if (pauseOnHover) {
488
+ const pauseOnHover = record.opts.pauseOnHover ?? record.cfgRef?.pauseOnHover ?? cfg.pauseOnHover;
489
+ if (pauseOnHover && !record.hoverBound) {
490
+ record.hoverBound = true;
368
491
  el.addEventListener("mouseenter", () => handle.pause());
369
492
  el.addEventListener("mouseleave", () => handle.resume());
370
493
  }
371
494
  handle.pause = pause;
372
495
  handle.resume = resume;
373
- start();
496
+ if (record.duration > 0) {
497
+ record.remaining = record.duration;
498
+ start();
499
+ }
374
500
  }
375
501
  function makeHandle(record) {
376
502
  const handle = {
@@ -406,7 +532,8 @@ function showRecord(title, kind, opts) {
406
532
  duration,
407
533
  remaining: duration,
408
534
  paused: false,
409
- timer: 0
535
+ timer: 0,
536
+ startedAt: 0
410
537
  };
411
538
  const handle = makeHandle(record);
412
539
  const el = renderNotification(record, container, () => handle);
@@ -417,7 +544,7 @@ function showRecord(title, kind, opts) {
417
544
  attachTimer(record, handle);
418
545
  trimStack(container);
419
546
  persistSave();
420
- playSound();
547
+ playSound(record.cfgRef?.sound ?? cfg.sound);
421
548
  opts.onShow?.(handle);
422
549
  return handle;
423
550
  }
@@ -432,6 +559,7 @@ function dismissRecord(id) {
432
559
  const handle = makeHandle(record);
433
560
  const onClose = record.opts.onClose;
434
561
  store.delete(id);
562
+ record.ownerStore?.delete(id);
435
563
  if (record.el) removeOne(record.el, () => onClose?.(handle));
436
564
  else onClose?.(handle);
437
565
  persistSave();
@@ -457,6 +585,11 @@ function updateRecord(id, patch) {
457
585
  if (patch.linkLabel !== void 0) record.opts.linkLabel = patch.linkLabel;
458
586
  const el = record.el;
459
587
  if (!el) return;
588
+ if (patch.duration !== void 0 && el.isConnected) {
589
+ syncCountdownBar(record);
590
+ if (record.restartTimer) record.restartTimer();
591
+ else if (patch.duration > 0) attachTimer(record, makeHandle(record));
592
+ }
460
593
  if (patch.title !== void 0) {
461
594
  const t = el.querySelector(".notification-title");
462
595
  if (t) t.textContent = patch.title;
@@ -547,7 +680,7 @@ function confirmDialog(title, opts = {}) {
547
680
  if (!handle) settle(false);
548
681
  });
549
682
  }
550
- function createNotificationCenter(parent = document.body, opts = {}) {
683
+ function createNotificationCenter(parent, opts = {}) {
551
684
  if (!isBrowser()) {
552
685
  return {
553
686
  push: () => null,
@@ -557,6 +690,8 @@ function createNotificationCenter(parent = document.body, opts = {}) {
557
690
  },
558
691
  markAllRead() {
559
692
  },
693
+ destroy() {
694
+ },
560
695
  get: () => null,
561
696
  getAll: () => [],
562
697
  config() {
@@ -564,6 +699,7 @@ function createNotificationCenter(parent = document.body, opts = {}) {
564
699
  el: {}
565
700
  };
566
701
  }
702
+ const mount = parent ?? document.body;
567
703
  const local = {
568
704
  position: opts.position ?? "top-right",
569
705
  maxStack: opts.maxStack ?? 5,
@@ -576,9 +712,20 @@ function createNotificationCenter(parent = document.body, opts = {}) {
576
712
  container.setAttribute("role", "region");
577
713
  container.setAttribute("aria-label", "\u901A\u77E5");
578
714
  container.dataset.position = local.position;
579
- parent.appendChild(container);
715
+ mount.appendChild(container);
580
716
  const localStore = /* @__PURE__ */ new Map();
717
+ let destroyed = false;
718
+ const pKey = persistKeyFor(local.persist);
719
+ if (pKey) {
720
+ knownPersistKeys.add(pKey);
721
+ for (const r of restorePersisted(container, pKey, local)) {
722
+ r.ownerStore = localStore;
723
+ localStore.set(r.id, r);
724
+ store.set(r.id, r);
725
+ }
726
+ }
581
727
  function push(input) {
728
+ if (destroyed) return null;
582
729
  const kind = input.kind ?? "info";
583
730
  const opts2 = input.options ?? {};
584
731
  const id = opts2.id ?? genId();
@@ -593,7 +740,10 @@ function createNotificationCenter(parent = document.body, opts = {}) {
593
740
  duration,
594
741
  remaining: duration,
595
742
  paused: false,
596
- timer: 0
743
+ timer: 0,
744
+ startedAt: 0,
745
+ cfgRef: local,
746
+ ownerStore: localStore
597
747
  };
598
748
  const handle = makeHandle(record);
599
749
  const el = renderNotification(record, container, () => handle);
@@ -609,13 +759,25 @@ function createNotificationCenter(parent = document.body, opts = {}) {
609
759
  const target = items.find((it) => !it.classList.contains("is-priority")) ?? items[0];
610
760
  if (!target) break;
611
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
+ }
612
772
  removeOne(target);
613
- const rec = tid ? localStore.get(tid) : null;
614
- if (rec) {
773
+ if (tid) {
615
774
  localStore.delete(tid);
616
775
  store.delete(tid);
617
776
  }
777
+ break;
618
778
  }
779
+ persistSave();
780
+ playSound(local.sound);
619
781
  opts2.onShow?.(handle);
620
782
  return handle;
621
783
  }
@@ -628,6 +790,7 @@ function createNotificationCenter(parent = document.body, opts = {}) {
628
790
  localStore.delete(id);
629
791
  store.delete(id);
630
792
  if (rec.el) removeOne(rec.el, () => rec.opts.onClose?.(handle));
793
+ persistSave();
631
794
  return;
632
795
  }
633
796
  Array.from(localStore.keys()).forEach((k) => dismiss(k));
@@ -641,11 +804,22 @@ function createNotificationCenter(parent = document.body, opts = {}) {
641
804
  rec.el?.classList.remove("is-unread");
642
805
  });
643
806
  }
807
+ function destroy() {
808
+ if (destroyed) return;
809
+ destroyed = true;
810
+ localStore.forEach((rec) => {
811
+ if (rec.timer) window.clearTimeout(rec.timer);
812
+ store.delete(rec.id);
813
+ });
814
+ localStore.clear();
815
+ container.remove();
816
+ }
644
817
  return {
645
818
  push,
646
819
  dismiss,
647
820
  clear,
648
821
  markAllRead,
822
+ destroy,
649
823
  get: (id) => localStore.get(id) ?? null,
650
824
  getAll: () => Array.from(localStore.values()),
651
825
  config: (next) => {
@@ -655,7 +829,11 @@ function createNotificationCenter(parent = document.body, opts = {}) {
655
829
  }
656
830
  if (typeof next.maxStack === "number" && next.maxStack > 0) local.maxStack = next.maxStack;
657
831
  if (typeof next.pauseOnHover === "boolean") local.pauseOnHover = next.pauseOnHover;
658
- if (next.persist !== void 0) local.persist = next.persist;
832
+ if (next.persist !== void 0) {
833
+ local.persist = next.persist;
834
+ const k = persistKeyFor(next.persist);
835
+ if (k) knownPersistKeys.add(k);
836
+ }
659
837
  if (next.sound !== void 0) local.sound = next.sound;
660
838
  },
661
839
  el: container
@@ -701,7 +879,11 @@ var notify = Object.assign(
701
879
  }
702
880
  if (typeof opts.maxStack === "number" && opts.maxStack > 0) cfg.maxStack = opts.maxStack;
703
881
  if (typeof opts.pauseOnHover === "boolean") cfg.pauseOnHover = opts.pauseOnHover;
704
- if (opts.persist !== void 0) cfg.persist = opts.persist;
882
+ if (opts.persist !== void 0) {
883
+ cfg.persist = opts.persist;
884
+ const k = persistKeyFor(opts.persist);
885
+ if (k) knownPersistKeys.add(k);
886
+ }
705
887
  if (opts.sound !== void 0) cfg.sound = opts.sound;
706
888
  }
707
889
  }
@@ -710,40 +892,12 @@ function initNotification() {
710
892
  if (!isBrowser()) return;
711
893
  if (globalInit.done) return;
712
894
  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);
895
+ const container = ensureContainer();
896
+ const key = persistKeyFor(cfg.persist);
897
+ if (key) {
898
+ knownPersistKeys.add(key);
899
+ for (const r of restorePersisted(container, key)) {
900
+ store.set(r.id, r);
747
901
  }
748
902
  }
749
903
  }
@@ -0,0 +1,46 @@
1
+ interface PopoverRect {
2
+ left: number;
3
+ top: number;
4
+ right: number;
5
+ bottom: number;
6
+ width: number;
7
+ height: number;
8
+ }
9
+ interface PopoverViewport {
10
+ width: number;
11
+ height: number;
12
+ }
13
+ type PopoverSide = 'top' | 'bottom';
14
+ type PopoverAlign = 'start' | 'center' | 'end' | 'stretch';
15
+ interface PopoverLayoutOptions {
16
+ side?: PopoverSide;
17
+ align?: PopoverAlign;
18
+ offset?: number;
19
+ margin?: number;
20
+ minWidth?: number;
21
+ maxWidth?: number;
22
+ minHeight?: number;
23
+ maxHeight?: number;
24
+ /** 内容预期高度:避免用被裁剪的实测高度参与选侧 */
25
+ contentHeightHint?: number;
26
+ }
27
+ interface PopoverLayout {
28
+ side: PopoverSide;
29
+ left: number;
30
+ top: number;
31
+ width: number;
32
+ maxHeight: number;
33
+ }
34
+ interface OpenPopoverOptions extends PopoverLayoutOptions {
35
+ anchor: Element | PopoverRect;
36
+ /** 关闭(含外点/Esc 触发的关闭)后的回调 */
37
+ onClose?: () => void;
38
+ }
39
+ /** 视口智能布局:返回 {side, left, top, width, maxHeight}。 */
40
+ declare function computePopoverLayout(anchor: PopoverRect, viewport: PopoverViewport, opts?: PopoverLayoutOptions): PopoverLayout;
41
+ /** 打开浮层:panel 移入 body 并按 anchor 布局;重复调用同一 panel 为先关后开。 */
42
+ declare function openPopover(panel: HTMLElement, options: OpenPopoverOptions): void;
43
+ /** 关闭浮层:移除监听并把 panel 还原到打开前的位置与 hidden 状态。 */
44
+ declare function closePopover(panel: HTMLElement): void;
45
+
46
+ export { type OpenPopoverOptions, type PopoverAlign, type PopoverLayout, type PopoverLayoutOptions, type PopoverRect, type PopoverSide, type PopoverViewport, closePopover, computePopoverLayout, openPopover };
@@ -139,7 +139,8 @@ function closePopover(panel) {
139
139
  if (!state) return;
140
140
  openPanels.delete(panel);
141
141
  panel.style.visibility = state.prevVisibility;
142
- panel.hidden = state.wasHidden;
142
+ if (typeof state.wasHidden === "string") panel.setAttribute("hidden", state.wasHidden);
143
+ else panel.hidden = state.wasHidden;
143
144
  panel.style.left = "";
144
145
  panel.style.top = "";
145
146
  panel.style.width = "";
@@ -0,0 +1,4 @@
1
+ /** 为 root 下每个 [data-select] 容器初始化(root 自身是 [data-select] 也算)。 */
2
+ declare function initSelect(root?: ParentNode): void;
3
+
4
+ export { initSelect };