@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
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 为 root 下每个 .split-pane[data-split-pane] 初始化(root 自身匹配也算)。
|
|
3
|
+
* 返回销毁函数:移除本次初始化挂上的全部监听并复位幂等标记(销毁后可重新 init)。
|
|
4
|
+
* SSR 下返回 no-op。
|
|
5
|
+
*/
|
|
6
|
+
declare function initSplitPane(root?: ParentNode): () => void;
|
|
3
7
|
|
|
4
8
|
export { initSplitPane };
|
|
@@ -1,3 +1,10 @@
|
|
|
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/split-pane.ts
|
|
2
9
|
function numOr(value, fallback) {
|
|
3
10
|
if (value === void 0 || value === "") return fallback;
|
|
@@ -9,11 +16,10 @@ function pct(value, min, max) {
|
|
|
9
16
|
}
|
|
10
17
|
function setup(container) {
|
|
11
18
|
const el = container;
|
|
12
|
-
if (el.__icenSplitPaneInit) return;
|
|
19
|
+
if (el.__icenSplitPaneInit) return void 0;
|
|
20
|
+
const divider = el.querySelector(".split-pane-divider");
|
|
21
|
+
if (!divider) return void 0;
|
|
13
22
|
el.__icenSplitPaneInit = true;
|
|
14
|
-
const dividerQuery = el.querySelector(".split-pane-divider");
|
|
15
|
-
if (!dividerQuery) return;
|
|
16
|
-
const divider = dividerQuery;
|
|
17
23
|
const min = numOr(el.dataset.splitMin, 10);
|
|
18
24
|
const max = numOr(el.dataset.splitMax, 90);
|
|
19
25
|
const step = numOr(el.dataset.splitStep, 2);
|
|
@@ -28,20 +34,25 @@ function setup(container) {
|
|
|
28
34
|
const v = parseFloat(getComputedStyle(el).getPropertyValue("--split"));
|
|
29
35
|
return Number.isFinite(v) ? v : 50;
|
|
30
36
|
}
|
|
31
|
-
|
|
37
|
+
let cur = readCurrent();
|
|
38
|
+
const write = (p) => {
|
|
32
39
|
const v = pct(p, min, max);
|
|
40
|
+
cur = v;
|
|
33
41
|
el.style.setProperty("--split", v + "%");
|
|
34
42
|
divider.setAttribute("aria-valuenow", String(Math.round(v)));
|
|
43
|
+
};
|
|
44
|
+
function emitChange() {
|
|
45
|
+
emitIcen(el, "icen:split-change", { value: Math.round(cur * 10) / 10 });
|
|
35
46
|
}
|
|
36
47
|
if (!el.style.getPropertyValue("--split")) write(50);
|
|
37
48
|
divider.setAttribute("aria-valuenow", String(Math.round(readCurrent())));
|
|
38
49
|
let dragging = false;
|
|
39
|
-
|
|
50
|
+
const onDown = (ev) => {
|
|
40
51
|
dragging = true;
|
|
41
52
|
divider.classList.add("is-dragging");
|
|
42
53
|
divider.setPointerCapture(ev.pointerId);
|
|
43
54
|
el.style.userSelect = "none";
|
|
44
|
-
}
|
|
55
|
+
};
|
|
45
56
|
const onUp = (ev) => {
|
|
46
57
|
if (!dragging) return;
|
|
47
58
|
dragging = false;
|
|
@@ -51,14 +62,16 @@ function setup(container) {
|
|
|
51
62
|
} catch {
|
|
52
63
|
}
|
|
53
64
|
el.style.userSelect = "";
|
|
65
|
+
emitChange();
|
|
54
66
|
};
|
|
55
|
-
|
|
56
|
-
|
|
67
|
+
const onCancel = () => {
|
|
68
|
+
if (!dragging) return;
|
|
57
69
|
dragging = false;
|
|
58
70
|
divider.classList.remove("is-dragging");
|
|
59
71
|
el.style.userSelect = "";
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
emitChange();
|
|
73
|
+
};
|
|
74
|
+
const onMove = (ev) => {
|
|
62
75
|
if (!dragging) return;
|
|
63
76
|
const rect = el.getBoundingClientRect();
|
|
64
77
|
if (vertical) {
|
|
@@ -70,9 +83,12 @@ function setup(container) {
|
|
|
70
83
|
const p = (ev.clientX - rect.left) / rect.width * 100;
|
|
71
84
|
write(p);
|
|
72
85
|
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
86
|
+
};
|
|
87
|
+
const onDblClick = () => {
|
|
88
|
+
write(50);
|
|
89
|
+
emitChange();
|
|
90
|
+
};
|
|
91
|
+
const onKeyDown = (ev) => {
|
|
76
92
|
const current = readCurrent();
|
|
77
93
|
let next = null;
|
|
78
94
|
if (vertical) {
|
|
@@ -87,15 +103,40 @@ function setup(container) {
|
|
|
87
103
|
if (next === null) return;
|
|
88
104
|
ev.preventDefault();
|
|
89
105
|
write(next);
|
|
90
|
-
|
|
106
|
+
emitChange();
|
|
107
|
+
};
|
|
108
|
+
divider.addEventListener("pointerdown", onDown);
|
|
109
|
+
divider.addEventListener("pointerup", onUp);
|
|
110
|
+
divider.addEventListener("pointercancel", onCancel);
|
|
111
|
+
divider.addEventListener("pointermove", onMove);
|
|
112
|
+
divider.addEventListener("dblclick", onDblClick);
|
|
113
|
+
divider.addEventListener("keydown", onKeyDown);
|
|
114
|
+
return () => {
|
|
115
|
+
divider.removeEventListener("pointerdown", onDown);
|
|
116
|
+
divider.removeEventListener("pointerup", onUp);
|
|
117
|
+
divider.removeEventListener("pointercancel", onCancel);
|
|
118
|
+
divider.removeEventListener("pointermove", onMove);
|
|
119
|
+
divider.removeEventListener("dblclick", onDblClick);
|
|
120
|
+
divider.removeEventListener("keydown", onKeyDown);
|
|
121
|
+
el.__icenSplitPaneInit = false;
|
|
122
|
+
};
|
|
91
123
|
}
|
|
92
124
|
function initSplitPane(root) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
125
|
+
const cleanups = [];
|
|
126
|
+
if (typeof document !== "undefined") {
|
|
127
|
+
const scope = root ?? document;
|
|
128
|
+
const panes = [];
|
|
129
|
+
if (scope instanceof Element && scope.matches(".split-pane[data-split-pane]")) panes.push(scope);
|
|
130
|
+
panes.push(...Array.from(scope.querySelectorAll(".split-pane[data-split-pane]")));
|
|
131
|
+
for (const p of panes) {
|
|
132
|
+
const cleanup = setup(p);
|
|
133
|
+
if (cleanup) cleanups.push(cleanup);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return () => {
|
|
137
|
+
for (const fn of cleanups) fn();
|
|
138
|
+
cleanups.length = 0;
|
|
139
|
+
};
|
|
99
140
|
}
|
|
100
141
|
export {
|
|
101
142
|
initSplitPane
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 为 root 下每个 [data-tabs] 容器初始化(root 自身是 [data-tabs] 也算)。
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* 返回销毁函数:调用后移除所有容器监听、window hashchange/resize 监听与行为层创建的
|
|
4
|
+
* .tabs-ink 元素,并复位幂等标记(销毁后的容器可被再次 initTabs 重新接管)。
|
|
5
|
+
* SSR 下返回 no-op 函数。
|
|
5
6
|
*/
|
|
6
7
|
declare function initTabs(root?: ParentNode): () => void;
|
|
7
8
|
|
package/dist/behaviors/tabs.mjs
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
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/tabs.ts
|
|
2
9
|
function tabButtons(container) {
|
|
3
10
|
return Array.from(container.querySelectorAll("[data-tab]"));
|
|
@@ -8,6 +15,20 @@ function tabNames(container) {
|
|
|
8
15
|
function currentHash() {
|
|
9
16
|
return window.location.hash.replace(/^#/, "");
|
|
10
17
|
}
|
|
18
|
+
function hashWriteMethod(container) {
|
|
19
|
+
const raw = container.getAttribute("data-tabs-hash");
|
|
20
|
+
if (raw === null || raw === "false") return null;
|
|
21
|
+
return raw === "push" ? "push" : "replace";
|
|
22
|
+
}
|
|
23
|
+
function syncInk(container, name) {
|
|
24
|
+
const btn = tabButtons(container).find((b) => b.getAttribute("data-tab") === name);
|
|
25
|
+
const tablist = btn?.parentElement;
|
|
26
|
+
if (!btn || !tablist) return;
|
|
27
|
+
const ink = tablist.querySelector(":scope > .tabs-ink");
|
|
28
|
+
if (!ink) return;
|
|
29
|
+
ink.style.width = `${btn.offsetWidth}px`;
|
|
30
|
+
ink.style.transform = `translateX(${btn.offsetLeft}px)`;
|
|
31
|
+
}
|
|
11
32
|
function activate(container, name, writeHash, emit = false) {
|
|
12
33
|
const buttons = tabButtons(container);
|
|
13
34
|
let matched = false;
|
|
@@ -32,15 +53,14 @@ function activate(container, name, writeHash, emit = false) {
|
|
|
32
53
|
}
|
|
33
54
|
});
|
|
34
55
|
if (writeHash && typeof window !== "undefined") {
|
|
35
|
-
|
|
56
|
+
const method = hashWriteMethod(container);
|
|
57
|
+
if (method) window.history[method === "push" ? "pushState" : "replaceState"](null, "", `#${name}`);
|
|
36
58
|
}
|
|
59
|
+
syncInk(container, name);
|
|
37
60
|
if (emit) {
|
|
38
61
|
const index = buttons.findIndex((b) => b.getAttribute("data-tab") === name);
|
|
39
62
|
const tab = buttons[index] ?? null;
|
|
40
|
-
container
|
|
41
|
-
bubbles: true,
|
|
42
|
-
detail: { tab, panel, index }
|
|
43
|
-
}));
|
|
63
|
+
emitIcen(container, "icen:tab-change", { tab, panel, index });
|
|
44
64
|
}
|
|
45
65
|
}
|
|
46
66
|
function isDisabled(btn) {
|
|
@@ -65,10 +85,11 @@ function nextEnabled(container, from, dir) {
|
|
|
65
85
|
function setup(container) {
|
|
66
86
|
const el = container;
|
|
67
87
|
if (el.__icenTabsInit) return void 0;
|
|
68
|
-
|
|
69
|
-
const
|
|
88
|
+
const useHash = hashWriteMethod(container) !== null;
|
|
89
|
+
const autoActivate = container.getAttribute("data-tabs-activation") === "auto";
|
|
70
90
|
const names = tabNames(container);
|
|
71
91
|
if (names.length === 0) return;
|
|
92
|
+
el.__icenTabsInit = true;
|
|
72
93
|
const buttons = tabButtons(container);
|
|
73
94
|
buttons.forEach((b) => {
|
|
74
95
|
if (!b.getAttribute("role")) b.setAttribute("role", "tab");
|
|
@@ -76,6 +97,18 @@ function setup(container) {
|
|
|
76
97
|
});
|
|
77
98
|
const tablist = buttons[0]?.parentElement;
|
|
78
99
|
if (tablist && !tablist.getAttribute("role")) tablist.setAttribute("role", "tablist");
|
|
100
|
+
let ink = null;
|
|
101
|
+
if (tablist instanceof HTMLElement && tablist.hasAttribute("data-tabs-ink")) {
|
|
102
|
+
ink = document.createElement("div");
|
|
103
|
+
ink.className = "tabs-ink";
|
|
104
|
+
ink.setAttribute("aria-hidden", "true");
|
|
105
|
+
const initialBtn = buttons.find((b) => b.classList.contains("is-active")) ?? buttons.find((b) => b.getAttribute("data-tab") === names[0]);
|
|
106
|
+
if (initialBtn) {
|
|
107
|
+
ink.style.width = `${initialBtn.offsetWidth}px`;
|
|
108
|
+
ink.style.transform = `translateX(${initialBtn.offsetLeft}px)`;
|
|
109
|
+
}
|
|
110
|
+
tablist.appendChild(ink);
|
|
111
|
+
}
|
|
79
112
|
const onClick = (ev) => {
|
|
80
113
|
const target = ev.target;
|
|
81
114
|
if (!(target instanceof Element)) return;
|
|
@@ -85,6 +118,14 @@ function setup(container) {
|
|
|
85
118
|
const name = btn.getAttribute("data-tab");
|
|
86
119
|
if (name) activate(container, name, useHash, true);
|
|
87
120
|
};
|
|
121
|
+
const followFocus = (idx) => {
|
|
122
|
+
focusTab(container, idx);
|
|
123
|
+
if (!autoActivate) return;
|
|
124
|
+
const target = tabButtons(container)[idx];
|
|
125
|
+
if (!target || isDisabled(target)) return;
|
|
126
|
+
const name = target.getAttribute("data-tab");
|
|
127
|
+
if (name) activate(container, name, useHash, true);
|
|
128
|
+
};
|
|
88
129
|
const onKeyDown = (ev) => {
|
|
89
130
|
const kev = ev;
|
|
90
131
|
const target = kev.target;
|
|
@@ -98,25 +139,23 @@ function setup(container) {
|
|
|
98
139
|
case "ArrowRight":
|
|
99
140
|
case "ArrowDown": {
|
|
100
141
|
kev.preventDefault();
|
|
101
|
-
|
|
102
|
-
if (next >= 0) focusTab(container, next);
|
|
142
|
+
followFocus(nextEnabled(container, currentIdx, 1));
|
|
103
143
|
break;
|
|
104
144
|
}
|
|
105
145
|
case "ArrowLeft":
|
|
106
146
|
case "ArrowUp": {
|
|
107
147
|
kev.preventDefault();
|
|
108
|
-
|
|
109
|
-
if (prev >= 0) focusTab(container, prev);
|
|
148
|
+
followFocus(nextEnabled(container, currentIdx, -1));
|
|
110
149
|
break;
|
|
111
150
|
}
|
|
112
151
|
case "Home": {
|
|
113
152
|
kev.preventDefault();
|
|
114
|
-
|
|
153
|
+
followFocus(nextEnabled(container, -1, 1));
|
|
115
154
|
break;
|
|
116
155
|
}
|
|
117
156
|
case "End": {
|
|
118
157
|
kev.preventDefault();
|
|
119
|
-
|
|
158
|
+
followFocus(nextEnabled(container, buttonsList.length, -1));
|
|
120
159
|
break;
|
|
121
160
|
}
|
|
122
161
|
case "Enter":
|
|
@@ -148,13 +187,25 @@ function setup(container) {
|
|
|
148
187
|
if (useHash && typeof window !== "undefined") {
|
|
149
188
|
window.addEventListener("hashchange", onHash);
|
|
150
189
|
}
|
|
190
|
+
const onResize = () => {
|
|
191
|
+
const active = tabButtons(container).find((b) => b.classList.contains("is-active"));
|
|
192
|
+
const name = active?.getAttribute("data-tab");
|
|
193
|
+
if (name) syncInk(container, name);
|
|
194
|
+
};
|
|
195
|
+
if (ink && typeof window !== "undefined") {
|
|
196
|
+
window.addEventListener("resize", onResize, { passive: true });
|
|
197
|
+
}
|
|
151
198
|
const cleanup = () => {
|
|
152
199
|
container.removeEventListener("click", onClick);
|
|
153
200
|
container.removeEventListener("keydown", onKeyDown);
|
|
154
201
|
window.removeEventListener("hashchange", onHash);
|
|
202
|
+
if (ink) {
|
|
203
|
+
window.removeEventListener("resize", onResize);
|
|
204
|
+
ink.remove();
|
|
205
|
+
ink = null;
|
|
206
|
+
}
|
|
155
207
|
el.__icenTabsInit = false;
|
|
156
208
|
};
|
|
157
|
-
el.__icenTabsCleanup = cleanup;
|
|
158
209
|
return cleanup;
|
|
159
210
|
}
|
|
160
211
|
function initTabs(root) {
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 为 root 下每个 .tag-input 容器初始化(root 自身匹配也算;data-tags 为可选初始值配置)。
|
|
3
|
+
* 返回销毁函数:移除本次挂上的全部监听并复位幂等标记(销毁后可重新 init)。SSR 下返回 no-op。
|
|
4
|
+
*/
|
|
5
|
+
declare function initTagInput(root?: ParentNode): () => void;
|
|
3
6
|
|
|
4
7
|
export { initTagInput };
|
|
@@ -1,37 +1,54 @@
|
|
|
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/tag-input.ts
|
|
2
9
|
function setup(wrap) {
|
|
3
10
|
const el = wrap;
|
|
4
|
-
if (el.__icenTagInit) return;
|
|
11
|
+
if (el.__icenTagInit) return void 0;
|
|
5
12
|
el.__icenTagInit = true;
|
|
6
13
|
const fieldEl = wrap.querySelector(".tag-input-field");
|
|
7
|
-
if (!fieldEl) return;
|
|
14
|
+
if (!fieldEl) return void 0;
|
|
8
15
|
const field = fieldEl;
|
|
9
16
|
const maxAttr = wrap.getAttribute("data-max");
|
|
10
17
|
const max = maxAttr ? Number(maxAttr) || Infinity : Infinity;
|
|
11
18
|
const placeholder = field.placeholder;
|
|
19
|
+
let alive = true;
|
|
12
20
|
let isComposing = false;
|
|
13
|
-
|
|
21
|
+
const onCompositionStart = () => {
|
|
14
22
|
isComposing = true;
|
|
15
|
-
}
|
|
16
|
-
|
|
23
|
+
};
|
|
24
|
+
const onCompositionEnd = () => {
|
|
17
25
|
isComposing = false;
|
|
18
|
-
}
|
|
26
|
+
};
|
|
19
27
|
let tags = (wrap.getAttribute("data-tags") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
28
|
+
const autoLabel = !wrap.hasAttribute("aria-label");
|
|
20
29
|
function sync() {
|
|
21
30
|
wrap.setAttribute("data-tags", tags.join(","));
|
|
22
31
|
wrap.classList.toggle("is-max", tags.length >= max);
|
|
23
32
|
field.placeholder = tags.length === 0 ? placeholder : "";
|
|
24
|
-
if (
|
|
25
|
-
wrap.setAttribute("aria-label", `\u6807\u7B7E\u8F93\u5165\uFF0C\u5171 ${tags.length} \u4E2A`);
|
|
26
|
-
}
|
|
33
|
+
if (autoLabel) wrap.setAttribute("aria-label", `\u6807\u7B7E\u8F93\u5165\uFF0C\u5171 ${tags.length} \u4E2A`);
|
|
27
34
|
}
|
|
28
35
|
function emit() {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
if (!alive) return;
|
|
37
|
+
emitIcen(wrap, "icen:tags-change", { tags: [...tags] });
|
|
38
|
+
}
|
|
39
|
+
let shakeTimer = null;
|
|
40
|
+
function shake() {
|
|
41
|
+
el.classList.remove("is-shake");
|
|
42
|
+
void el.offsetWidth;
|
|
43
|
+
el.classList.add("is-shake");
|
|
44
|
+
if (shakeTimer) clearTimeout(shakeTimer);
|
|
45
|
+
shakeTimer = setTimeout(() => {
|
|
46
|
+
el.classList.remove("is-shake");
|
|
47
|
+
shakeTimer = null;
|
|
48
|
+
}, 320);
|
|
33
49
|
}
|
|
34
50
|
function remove(index) {
|
|
51
|
+
if (!alive) return;
|
|
35
52
|
tags.splice(index, 1);
|
|
36
53
|
render();
|
|
37
54
|
emit();
|
|
@@ -57,11 +74,13 @@ function setup(wrap) {
|
|
|
57
74
|
sync();
|
|
58
75
|
}
|
|
59
76
|
function commit() {
|
|
77
|
+
if (!alive) return;
|
|
60
78
|
const v = field.value.trim();
|
|
61
79
|
if (!v) return;
|
|
62
80
|
if (tags.length >= max) return;
|
|
63
81
|
if (tags.includes(v)) {
|
|
64
82
|
field.value = "";
|
|
83
|
+
shake();
|
|
65
84
|
return;
|
|
66
85
|
}
|
|
67
86
|
tags.push(v);
|
|
@@ -69,7 +88,7 @@ function setup(wrap) {
|
|
|
69
88
|
render();
|
|
70
89
|
emit();
|
|
71
90
|
}
|
|
72
|
-
|
|
91
|
+
const onKeyDown = (ev) => {
|
|
73
92
|
if (isComposing || ev.isComposing) return;
|
|
74
93
|
if (ev.key === "Enter" || ev.key === ",") {
|
|
75
94
|
ev.preventDefault();
|
|
@@ -77,17 +96,38 @@ function setup(wrap) {
|
|
|
77
96
|
} else if (ev.key === "Backspace" && !field.value && tags.length > 0) {
|
|
78
97
|
remove(tags.length - 1);
|
|
79
98
|
}
|
|
80
|
-
}
|
|
99
|
+
};
|
|
100
|
+
field.addEventListener("compositionstart", onCompositionStart);
|
|
101
|
+
field.addEventListener("compositionend", onCompositionEnd);
|
|
102
|
+
field.addEventListener("keydown", onKeyDown);
|
|
81
103
|
field.addEventListener("blur", commit);
|
|
82
104
|
render();
|
|
105
|
+
return () => {
|
|
106
|
+
alive = false;
|
|
107
|
+
if (shakeTimer) clearTimeout(shakeTimer);
|
|
108
|
+
field.removeEventListener("compositionstart", onCompositionStart);
|
|
109
|
+
field.removeEventListener("compositionend", onCompositionEnd);
|
|
110
|
+
field.removeEventListener("keydown", onKeyDown);
|
|
111
|
+
field.removeEventListener("blur", commit);
|
|
112
|
+
el.__icenTagInit = false;
|
|
113
|
+
};
|
|
83
114
|
}
|
|
84
115
|
function initTagInput(root) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
116
|
+
const cleanups = [];
|
|
117
|
+
if (typeof document !== "undefined") {
|
|
118
|
+
const scope = root ?? document;
|
|
119
|
+
const containers = [];
|
|
120
|
+
if (scope instanceof HTMLElement && scope.matches(".tag-input")) containers.push(scope);
|
|
121
|
+
containers.push(...Array.from(scope.querySelectorAll(".tag-input")));
|
|
122
|
+
for (const c of containers) {
|
|
123
|
+
const cleanup = setup(c);
|
|
124
|
+
if (cleanup) cleanups.push(cleanup);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return () => {
|
|
128
|
+
for (const fn of cleanups) fn();
|
|
129
|
+
cleanups.length = 0;
|
|
130
|
+
};
|
|
91
131
|
}
|
|
92
132
|
export {
|
|
93
133
|
initTagInput
|
|
@@ -6,20 +6,31 @@ interface ThemeState {
|
|
|
6
6
|
declare const PRESETS: readonly ["clay", "piano", "art", "vangogh", "ink", "retro"];
|
|
7
7
|
declare const STYLES: readonly ["modern", "retro", "terminal"];
|
|
8
8
|
declare const STORAGE_KEY = "icen.ui.theme";
|
|
9
|
+
/**
|
|
10
|
+
* 注册宿主自建预设(宿主先在自家 CSS 写好 `.<name>` 与 `.<name>.dark` 完整 token 面,
|
|
11
|
+
* 仿照 colors.css 现有 6 套)。名字须命中 /^[a-z][a-z0-9-]{1,20}$/ 且不与
|
|
12
|
+
* dark / style-* 保留字冲突;注册只影响 listThemePresets() 枚举(UI 切换器用),
|
|
13
|
+
* 不注册的自建预设同样可以 setTheme(校验同样放行)。返回是否接受注册。
|
|
14
|
+
*/
|
|
15
|
+
declare function registerThemePreset(name: string): boolean;
|
|
16
|
+
/** 全部可用预设名:内置 6 套 + 已注册的自建预设(UI 切换器枚举用,顺序稳定)。 */
|
|
17
|
+
declare function listThemePresets(): string[];
|
|
9
18
|
/** 默认主题:clay + 跟随系统明暗 + modern。SSR 下返回离线兜底值。 */
|
|
10
19
|
declare function getDefaultTheme(): ThemeState;
|
|
11
20
|
/** 读 localStorage 中的持久化主题;缺失或坏数据回退默认。 */
|
|
12
21
|
declare function getTheme(): ThemeState;
|
|
13
|
-
/** 把主题应用到 <html
|
|
22
|
+
/** 把主题应用到 <html> 并在状态实际变化时派发 icen:theme-change(见私有 apply)。 */
|
|
14
23
|
declare function applyTheme(t: ThemeState): void;
|
|
15
24
|
/** 合并当前主题与 patch,应用并持久化,返回新态。patch 中的非法枚举值回退默认。 */
|
|
16
25
|
declare function setTheme(patch: Partial<ThemeState>): ThemeState;
|
|
17
26
|
/** 切换明暗,返回新态。 */
|
|
18
27
|
declare function toggleDark(): ThemeState;
|
|
28
|
+
/** 只切风格 profile(modern / retro / terminal;非法值回退 modern),返回新态。 */
|
|
29
|
+
declare function setStyle(style: string): ThemeState;
|
|
19
30
|
/** 页面加载早期调用:读持久化主题并应用,防闪烁。
|
|
20
31
|
* 无持久化偏好时,自动跟随系统的 prefers-color-scheme 变化(实时);
|
|
21
32
|
* 一旦用户显式 setTheme / toggleDark,跟随停止(持久化优先)。
|
|
22
33
|
* 重复调用幂等(系统监听不会重复挂)。 */
|
|
23
34
|
declare function initTheme(): ThemeState;
|
|
24
35
|
|
|
25
|
-
export { PRESETS, STORAGE_KEY, STYLES, type ThemeState, applyTheme, getDefaultTheme, getTheme, initTheme, setTheme, toggleDark };
|
|
36
|
+
export { PRESETS, STORAGE_KEY, STYLES, type ThemeState, applyTheme, getDefaultTheme, getTheme, initTheme, listThemePresets, registerThemePreset, setStyle, setTheme, toggleDark };
|
package/dist/behaviors/theme.mjs
CHANGED
|
@@ -1,11 +1,32 @@
|
|
|
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/theme.ts
|
|
2
9
|
var PRESETS = ["clay", "piano", "art", "vangogh", "ink", "retro"];
|
|
3
10
|
var STYLES = ["modern", "retro", "terminal"];
|
|
4
11
|
var STORAGE_KEY = "icen.ui.theme";
|
|
12
|
+
var PRESET_NAME_RE = /^[a-z][a-z0-9-]{1,20}$/;
|
|
13
|
+
var RESERVED_PRESET_NAMES = /* @__PURE__ */ new Set(["dark", ...STYLES.map((s) => `style-${s}`)]);
|
|
14
|
+
var customPresets = /* @__PURE__ */ new Set();
|
|
5
15
|
var FALLBACK = { preset: "clay", dark: false, style: "modern" };
|
|
6
16
|
function isBrowser() {
|
|
7
17
|
return typeof document !== "undefined";
|
|
8
18
|
}
|
|
19
|
+
function isValidPreset(name) {
|
|
20
|
+
return PRESETS.includes(name) || PRESET_NAME_RE.test(name) && !RESERVED_PRESET_NAMES.has(name);
|
|
21
|
+
}
|
|
22
|
+
function registerThemePreset(name) {
|
|
23
|
+
if (!PRESET_NAME_RE.test(name) || RESERVED_PRESET_NAMES.has(name)) return false;
|
|
24
|
+
customPresets.add(name);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
function listThemePresets() {
|
|
28
|
+
return [...PRESETS, ...customPresets];
|
|
29
|
+
}
|
|
9
30
|
function getDefaultTheme() {
|
|
10
31
|
if (!isBrowser() || typeof window.matchMedia !== "function") {
|
|
11
32
|
return { ...FALLBACK };
|
|
@@ -19,7 +40,8 @@ function getDefaultTheme() {
|
|
|
19
40
|
function isThemeState(v) {
|
|
20
41
|
if (typeof v !== "object" || v === null) return false;
|
|
21
42
|
const t = v;
|
|
22
|
-
return typeof t.preset === "string" &&
|
|
43
|
+
return typeof t.preset === "string" && /* 持久化路径与 setTheme 同一开放白名单:自建预设跨刷新存活 */
|
|
44
|
+
isValidPreset(t.preset) && typeof t.dark === "boolean" && typeof t.style === "string" && STYLES.includes(t.style);
|
|
23
45
|
}
|
|
24
46
|
function getTheme() {
|
|
25
47
|
if (!isBrowser()) return getDefaultTheme();
|
|
@@ -33,24 +55,40 @@ function getTheme() {
|
|
|
33
55
|
return getDefaultTheme();
|
|
34
56
|
}
|
|
35
57
|
}
|
|
36
|
-
|
|
58
|
+
var lastApplied = null;
|
|
59
|
+
function apply(t) {
|
|
37
60
|
if (!isBrowser()) return;
|
|
38
61
|
const cls = document.documentElement.classList;
|
|
62
|
+
if (lastApplied && !PRESETS.includes(lastApplied.preset)) {
|
|
63
|
+
cls.remove(lastApplied.preset);
|
|
64
|
+
}
|
|
39
65
|
for (const p of PRESETS) cls.remove(p);
|
|
40
66
|
for (const s of STYLES) cls.remove(`style-${s}`);
|
|
41
67
|
cls.remove("dark");
|
|
42
68
|
cls.add(t.preset);
|
|
43
69
|
if (t.dark) cls.add("dark");
|
|
44
70
|
cls.add(`style-${t.style}`);
|
|
71
|
+
const changed = lastApplied !== null && (lastApplied.preset !== t.preset || lastApplied.dark !== t.dark || lastApplied.style !== t.style);
|
|
72
|
+
lastApplied = { ...t };
|
|
73
|
+
if (changed) {
|
|
74
|
+
emitIcen(document.documentElement, "icen:theme-change", {
|
|
75
|
+
preset: t.preset,
|
|
76
|
+
dark: t.dark,
|
|
77
|
+
style: t.style
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function applyTheme(t) {
|
|
82
|
+
apply(t);
|
|
45
83
|
}
|
|
46
84
|
function setTheme(patch) {
|
|
47
85
|
const merged = { ...getTheme(), ...patch };
|
|
48
86
|
const next = {
|
|
49
|
-
preset:
|
|
87
|
+
preset: isValidPreset(merged.preset) ? merged.preset : FALLBACK.preset,
|
|
50
88
|
dark: merged.dark === true,
|
|
51
89
|
style: STYLES.includes(merged.style) ? merged.style : FALLBACK.style
|
|
52
90
|
};
|
|
53
|
-
|
|
91
|
+
apply(next);
|
|
54
92
|
if (isBrowser()) {
|
|
55
93
|
try {
|
|
56
94
|
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
|
|
@@ -62,6 +100,9 @@ function setTheme(patch) {
|
|
|
62
100
|
function toggleDark() {
|
|
63
101
|
return setTheme({ dark: !getTheme().dark });
|
|
64
102
|
}
|
|
103
|
+
function setStyle(style) {
|
|
104
|
+
return setTheme({ style });
|
|
105
|
+
}
|
|
65
106
|
var systemListenerAttached = false;
|
|
66
107
|
function initTheme() {
|
|
67
108
|
const t = getTheme();
|
|
@@ -103,6 +144,9 @@ export {
|
|
|
103
144
|
getDefaultTheme,
|
|
104
145
|
getTheme,
|
|
105
146
|
initTheme,
|
|
147
|
+
listThemePresets,
|
|
148
|
+
registerThemePreset,
|
|
149
|
+
setStyle,
|
|
106
150
|
setTheme,
|
|
107
151
|
toggleDark
|
|
108
152
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
type ToastKind = 'ok' | 'err' | 'warn' | 'info';
|
|
1
|
+
type ToastKind = 'ok' | 'err' | 'warn' | 'info' | 'success' | 'error';
|
|
2
2
|
interface ToastOptions {
|
|
3
3
|
/** 保留时长(毫秒),默认 2600;带 action 时默认 6000 */
|
|
4
4
|
ms?: number;
|
|
@@ -6,21 +6,43 @@ interface ToastOptions {
|
|
|
6
6
|
actionLabel?: string;
|
|
7
7
|
/** action 触发回调;返回 false 阻止 toast 关闭 */
|
|
8
8
|
onAction?: () => void | Promise<void> | boolean;
|
|
9
|
+
/** 内联 SVG 字符串图标(消毒后渲染为 .toast-icon);非 <svg> 开头或解析失败静默忽略 */
|
|
10
|
+
icon?: string;
|
|
11
|
+
/** 显示右上角关闭按钮(.toast-close,点击即关闭本条),默认 false */
|
|
12
|
+
closable?: boolean;
|
|
13
|
+
/** 显示底部超时进度条(.toast-progress,动画时长 = 本条保留时长),默认 false */
|
|
14
|
+
progress?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** 单条 toast 句柄:dismiss() 立即关闭本条(幂等,可重复调用)。 */
|
|
17
|
+
interface ToastHandle {
|
|
18
|
+
dismiss(): void;
|
|
9
19
|
}
|
|
10
20
|
interface ToastConfig {
|
|
11
21
|
/** 同屏最大堆叠,默认 5;超出从最早的开始移除 */
|
|
12
22
|
maxStack?: number;
|
|
13
23
|
}
|
|
14
24
|
interface ToastFn {
|
|
15
|
-
(message: string, kind?: ToastKind, ms?: number):
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
(message: string, kind?: ToastKind, ms?: number): ToastHandle;
|
|
26
|
+
/** success 语义(.toast--success),规范词表入口 */
|
|
27
|
+
success(message: string, ms?: number): ToastHandle;
|
|
28
|
+
/**
|
|
29
|
+
* success 语义(.toast--success)。
|
|
30
|
+
* @deprecated 词表统一:改用 toast.success(),本别名下个大版本删除。
|
|
31
|
+
*/
|
|
32
|
+
ok(message: string, ms?: number): ToastHandle;
|
|
33
|
+
/** error 语义(.toast--error),规范词表入口 */
|
|
34
|
+
error(message: string, ms?: number): ToastHandle;
|
|
35
|
+
/**
|
|
36
|
+
* error 语义(.toast--error)。
|
|
37
|
+
* @deprecated 词表统一:改用 toast.error(),本别名下个大版本删除。
|
|
38
|
+
*/
|
|
39
|
+
err(message: string, ms?: number): ToastHandle;
|
|
40
|
+
warn(message: string, ms?: number): ToastHandle;
|
|
41
|
+
info(message: string, ms?: number): ToastHandle;
|
|
42
|
+
action(message: string, actionLabel: string, onAction: () => void, ms?: number): ToastHandle;
|
|
21
43
|
dismiss(): void;
|
|
22
44
|
config(opts: ToastConfig): void;
|
|
23
45
|
}
|
|
24
46
|
declare const toast: ToastFn;
|
|
25
47
|
|
|
26
|
-
export { type ToastConfig, type ToastKind, type ToastOptions, toast };
|
|
48
|
+
export { type ToastConfig, type ToastHandle, type ToastKind, type ToastOptions, toast };
|