@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.
- package/AGENTS.md +15 -9
- package/README.md +8 -3
- package/dist/base.css +4 -3
- package/dist/behaviors/accordion.d.mts +5 -2
- package/dist/behaviors/accordion.mjs +36 -11
- package/dist/behaviors/ai-chat.d.mts +3 -1
- package/dist/behaviors/ai-chat.mjs +66 -43
- package/dist/behaviors/ai-composer.d.mts +28 -3
- package/dist/behaviors/ai-composer.mjs +291 -209
- package/dist/behaviors/ai-core.d.mts +9 -4
- package/dist/behaviors/ai-core.mjs +21 -8
- package/dist/behaviors/ai-diff.d.mts +2 -2
- package/dist/behaviors/ai-diff.mjs +172 -41
- package/dist/behaviors/ai-panel.d.mts +11 -8
- package/dist/behaviors/ai-panel.mjs +185 -110
- package/dist/behaviors/ai-provider.mjs +54 -48
- package/dist/behaviors/ai-tool.d.mts +12 -4
- package/dist/behaviors/ai-tool.mjs +311 -263
- package/dist/behaviors/ai-tools.d.mts +9 -6
- package/dist/behaviors/ai-tools.mjs +259 -155
- package/dist/behaviors/back-top.mjs +16 -1
- package/dist/behaviors/carousel.d.mts +5 -2
- package/dist/behaviors/carousel.mjs +53 -26
- package/dist/behaviors/charts.d.mts +68 -22
- package/dist/behaviors/charts.mjs +221 -150
- package/dist/behaviors/command-palette.d.mts +40 -6
- package/dist/behaviors/command-palette.mjs +143 -15
- package/dist/behaviors/context-menu.d.mts +21 -4
- package/dist/behaviors/context-menu.mjs +122 -38
- package/dist/behaviors/controls.d.mts +59 -0
- package/dist/behaviors/controls.mjs +601 -0
- package/dist/behaviors/copy.d.mts +5 -2
- package/dist/behaviors/copy.mjs +37 -12
- package/dist/behaviors/datatable.d.mts +12 -0
- package/dist/behaviors/datatable.mjs +226 -43
- package/dist/behaviors/date-picker.d.mts +2 -2
- package/dist/behaviors/date-picker.mjs +171 -32
- package/dist/behaviors/dropdown.d.mts +6 -2
- package/dist/behaviors/dropdown.mjs +164 -25
- package/dist/behaviors/events.d.mts +330 -0
- package/dist/behaviors/events.mjs +208 -0
- package/dist/behaviors/input.mjs +25 -12
- package/dist/behaviors/modal.d.mts +24 -5
- package/dist/behaviors/modal.mjs +76 -10
- package/dist/behaviors/nav.d.mts +1 -1
- package/dist/behaviors/nav.mjs +33 -7
- package/dist/behaviors/notification.d.mts +20 -4
- package/dist/behaviors/notification.mjs +53 -73
- package/dist/behaviors/popover.d.mts +39 -3
- package/dist/behaviors/popover.mjs +90 -4
- package/dist/behaviors/select.d.mts +4 -2
- package/dist/behaviors/select.mjs +161 -24
- package/dist/behaviors/sidebar.d.mts +5 -2
- package/dist/behaviors/sidebar.mjs +43 -13
- package/dist/behaviors/slider.d.mts +5 -2
- package/dist/behaviors/slider.mjs +54 -43
- package/dist/behaviors/split-pane.d.mts +6 -2
- package/dist/behaviors/split-pane.mjs +62 -21
- package/dist/behaviors/tabs.d.mts +3 -2
- package/dist/behaviors/tabs.mjs +65 -14
- package/dist/behaviors/tag-input.d.mts +5 -2
- package/dist/behaviors/tag-input.mjs +61 -21
- package/dist/behaviors/theme.d.mts +13 -2
- package/dist/behaviors/theme.mjs +48 -4
- package/dist/behaviors/toast.d.mts +30 -8
- package/dist/behaviors/toast.mjs +74 -7
- package/dist/behaviors/tree.d.mts +5 -2
- package/dist/behaviors/tree.mjs +38 -15
- package/dist/behaviors/upload.mjs +21 -23
- package/dist/cli.mjs +1 -1
- package/dist/components/ai-panel.css +1 -1
- package/dist/components/ai-tool-call.mjs +1 -1
- package/dist/components/badge.css +1 -1
- package/dist/components/breadcrumb.css +0 -32
- package/dist/components/chart-calendar.mjs +1 -2
- package/dist/components/chart-pie.css +0 -6
- package/dist/components/charts.css +1 -1
- package/dist/components/content.css +3 -16
- package/dist/components/datatable.css +1 -1
- package/dist/components/date-picker.css +1 -1
- package/dist/components/notification.css +2 -3
- package/dist/components/pagination.css +3 -1
- package/dist/components/pagination.mjs +1 -0
- package/dist/components/rating.mjs +1 -0
- package/dist/components/segmented.css +9 -1
- package/dist/components/segmented.mjs +1 -1
- package/dist/components/select.css +67 -4
- package/dist/components/steps.css +13 -3
- package/dist/components/steps.mjs +1 -0
- package/dist/components/switch.css +3 -1
- package/dist/components/switch.mjs +1 -0
- package/dist/components/table.mjs +1 -0
- package/dist/components/tabs.css +23 -0
- package/dist/components/tag-input.css +1 -6
- package/dist/components/upload.css +0 -15
- package/dist/index.d.mts +12 -10
- package/dist/index.mjs +2700 -1460
- package/dist/registry.json +42 -13
- package/dist/retro-effects.css +3 -16
- package/dist/tokens/retro-effects.css +3 -16
- package/dist/tokens/style-profiles.css +3 -2
- package/dist/tokens/typography.css +2 -0
- package/dist/tokens.css +5 -2
- package/dist/ui.css +138 -98
- package/docs/spec/ai-native.md +7 -4
- package/package.json +1 -1
package/dist/behaviors/modal.mjs
CHANGED
|
@@ -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-${
|
|
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-${
|
|
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
|
-
|
|
80
|
-
(
|
|
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
|
-
|
|
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
|
package/dist/behaviors/nav.d.mts
CHANGED
package/dist/behaviors/nav.mjs
CHANGED
|
@@ -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",
|
|
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",
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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 <=
|
|
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)
|
|
440
|
+
if (target.id) remove(target.id);
|
|
442
441
|
continue;
|
|
443
442
|
}
|
|
444
443
|
removeOne(target);
|
|
445
|
-
if (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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|