@icen.ai/ui 0.4.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.
- package/AGENTS.md +256 -0
- package/README.md +47 -13
- package/dist/base.css +31 -4
- package/dist/behaviors/accordion.d.mts +4 -0
- package/dist/behaviors/accordion.mjs +4 -3
- package/dist/behaviors/ai-chat.d.mts +57 -0
- package/dist/behaviors/ai-chat.mjs +557 -0
- package/dist/behaviors/ai-composer.d.mts +112 -0
- package/dist/behaviors/ai-composer.mjs +2032 -0
- package/dist/behaviors/ai-core.d.mts +151 -0
- package/dist/behaviors/ai-core.mjs +491 -0
- package/dist/behaviors/ai-diff.d.mts +53 -0
- package/dist/behaviors/ai-diff.mjs +282 -0
- package/dist/behaviors/ai-panel.d.mts +98 -0
- package/dist/behaviors/ai-panel.mjs +1012 -0
- package/dist/behaviors/ai-provider.d.mts +198 -0
- package/dist/behaviors/ai-provider.mjs +1055 -0
- package/dist/behaviors/ai-tool.d.mts +18 -0
- package/dist/behaviors/ai-tool.mjs +2125 -0
- package/dist/behaviors/ai-tools.d.mts +81 -0
- package/dist/behaviors/ai-tools.mjs +1658 -0
- package/dist/behaviors/back-top.d.mts +20 -0
- package/dist/behaviors/back-top.mjs +101 -0
- package/dist/behaviors/carousel.d.mts +4 -0
- package/dist/behaviors/carousel.mjs +34 -16
- package/dist/behaviors/charts.d.mts +250 -0
- package/dist/behaviors/charts.mjs +1301 -448
- package/dist/behaviors/command-palette.d.mts +8 -0
- package/dist/behaviors/command-palette.mjs +189 -0
- package/dist/behaviors/context-menu.d.mts +36 -0
- package/dist/behaviors/context-menu.mjs +6 -2
- package/dist/behaviors/copy.d.mts +4 -0
- package/dist/behaviors/copy.mjs +7 -6
- package/dist/behaviors/datatable.d.mts +78 -0
- package/dist/behaviors/datatable.mjs +54 -11
- package/dist/behaviors/date-picker.d.mts +4 -0
- package/dist/behaviors/date-picker.mjs +296 -0
- package/dist/behaviors/dropdown.d.mts +4 -0
- package/dist/behaviors/dropdown.mjs +7 -3
- package/dist/behaviors/input.d.mts +4 -0
- package/dist/behaviors/input.mjs +184 -8
- package/dist/behaviors/modal.d.mts +11 -0
- package/dist/behaviors/modal.mjs +11 -6
- package/dist/behaviors/nav.d.mts +7 -0
- package/dist/behaviors/nav.mjs +25 -9
- package/dist/behaviors/notification.d.mts +172 -0
- package/dist/behaviors/notification.mjs +908 -0
- package/dist/behaviors/popover.d.mts +46 -0
- package/dist/behaviors/popover.mjs +2 -1
- package/dist/behaviors/select.d.mts +4 -0
- package/dist/behaviors/select.mjs +268 -34
- package/dist/behaviors/sidebar.d.mts +4 -0
- package/dist/behaviors/sidebar.mjs +4 -3
- package/dist/behaviors/slider.d.mts +4 -0
- package/dist/behaviors/slider.mjs +4 -3
- package/dist/behaviors/split-pane.d.mts +4 -0
- package/dist/behaviors/split-pane.mjs +102 -0
- package/dist/behaviors/tabs.d.mts +8 -0
- package/dist/behaviors/tabs.mjs +44 -19
- package/dist/behaviors/tag-input.d.mts +4 -0
- package/dist/behaviors/tag-input.mjs +12 -3
- package/dist/behaviors/theme.d.mts +25 -0
- package/dist/behaviors/theme.mjs +3 -1
- package/dist/behaviors/toast.d.mts +26 -0
- package/dist/behaviors/toast.mjs +24 -18
- package/dist/behaviors/tree.d.mts +4 -0
- package/dist/behaviors/tree.mjs +4 -3
- package/dist/behaviors/upload.d.mts +4 -0
- package/dist/behaviors/upload.mjs +205 -17
- package/dist/components/ai-chat.css +1037 -0
- package/dist/components/ai-chat.mjs +3 -0
- package/dist/components/ai-composer.mjs +3 -0
- package/dist/components/ai-context.mjs +2 -0
- package/dist/components/ai-diff.css +311 -0
- package/dist/components/ai-diff.mjs +3 -0
- package/dist/components/ai-files.mjs +3 -0
- package/dist/components/ai-message.mjs +3 -0
- package/dist/components/ai-panel.css +682 -0
- package/dist/components/ai-reasoning.mjs +3 -0
- package/dist/components/ai-subagent.mjs +2 -0
- package/dist/components/ai-todo.mjs +2 -0
- package/dist/components/ai-tool-call.mjs +2 -0
- package/dist/components/ai-tool.css +532 -0
- package/dist/components/ai-usage.mjs +3 -0
- package/dist/components/back-top.css +88 -0
- package/dist/components/back-top.mjs +3 -0
- package/dist/components/badge.css +127 -0
- package/dist/components/badge.mjs +2 -0
- package/dist/components/breadcrumb.css +53 -0
- package/dist/components/btn.css +4 -7
- package/dist/components/card.css +66 -1
- package/dist/components/chart-bar.css +10 -0
- package/dist/components/chart-calendar.mjs +5 -0
- package/dist/components/chart-gauge.css +1 -1
- package/dist/components/chart-heatmap.css +40 -0
- package/dist/components/chart-scatter.css +24 -0
- package/dist/components/chart-scatter.mjs +4 -0
- package/dist/components/charts.css +86 -1
- package/dist/components/charts.mjs +1 -0
- package/dist/components/command-palette.css +246 -0
- package/dist/components/command-palette.mjs +3 -0
- package/dist/components/content.css +221 -8
- package/dist/components/copy.css +93 -0
- package/dist/components/copy.mjs +3 -0
- package/dist/components/datatable.css +4 -8
- package/dist/components/date-picker.css +240 -0
- package/dist/components/date-picker.mjs +3 -0
- package/dist/components/empty.css +69 -3
- package/dist/components/feedback.css +241 -10
- package/dist/components/form.css +129 -11
- package/dist/components/form.mjs +2 -0
- package/dist/components/input.css +25 -2
- package/dist/components/layout.css +36 -36
- package/dist/components/media.css +153 -6
- package/dist/components/menu.css +2 -2
- package/dist/components/modal.css +1 -1
- package/dist/components/nav.css +106 -2
- package/dist/components/notification.css +387 -0
- package/dist/components/notification.mjs +3 -0
- package/dist/components/pagination.css +1 -1
- package/dist/components/panel.css +104 -9
- package/dist/components/pill.css +82 -12
- package/dist/components/scroll-area.css +112 -0
- package/dist/components/scroll-area.mjs +2 -0
- package/dist/components/segmented.css +80 -5
- package/dist/components/segmented.mjs +1 -0
- package/dist/components/select.css +104 -17
- package/dist/components/sidebar.css +119 -1
- package/dist/components/slider.css +113 -0
- package/dist/components/split-pane.css +111 -0
- package/dist/components/split-pane.mjs +3 -0
- package/dist/components/stat.css +131 -12
- package/dist/components/steps.css +98 -1
- package/dist/components/switch.css +145 -7
- package/dist/components/table.css +111 -29
- package/dist/components/tabs.css +132 -22
- package/dist/components/tag-input.css +4 -4
- package/dist/components/tag.css +164 -0
- package/dist/components/tag.mjs +2 -0
- package/dist/components/toast.css +122 -5
- package/dist/components/toolbar.css +101 -0
- package/dist/components/toolbar.mjs +2 -0
- package/dist/components/tooltip.css +2 -2
- package/dist/components/upload.css +61 -4
- package/dist/index.d.mts +37 -0
- package/dist/index.mjs +9409 -929
- package/dist/registry.json +176 -4
- package/dist/retro-effects.css +21 -17
- package/dist/tokens/colors.css +4 -1
- package/dist/tokens/retro-effects.css +21 -17
- package/dist/tokens/style-profiles.css +21 -0
- package/dist/tokens.css +25 -1
- package/dist/ui.css +9616 -2839
- package/docs/spec/ai-native.md +659 -0
- package/package.json +15 -5
package/dist/behaviors/tabs.mjs
CHANGED
|
@@ -8,28 +8,40 @@ function tabNames(container) {
|
|
|
8
8
|
function currentHash() {
|
|
9
9
|
return window.location.hash.replace(/^#/, "");
|
|
10
10
|
}
|
|
11
|
-
function activate(container, name, writeHash) {
|
|
11
|
+
function activate(container, name, writeHash, emit = false) {
|
|
12
12
|
const buttons = tabButtons(container);
|
|
13
13
|
let matched = false;
|
|
14
14
|
buttons.forEach((t) => {
|
|
15
15
|
const on = t.getAttribute("data-tab") === name;
|
|
16
16
|
if (on) matched = true;
|
|
17
|
-
t.classList.toggle("active", on);
|
|
17
|
+
t.classList.toggle("is-active", on);
|
|
18
18
|
t.setAttribute("aria-selected", String(on));
|
|
19
19
|
t.tabIndex = on ? 0 : -1;
|
|
20
20
|
});
|
|
21
21
|
if (!matched) return;
|
|
22
|
+
let panel = null;
|
|
22
23
|
container.querySelectorAll("[data-tab-panel]").forEach((p) => {
|
|
23
24
|
const isOn = p.getAttribute("data-tab-panel") === name;
|
|
24
25
|
p.hidden = !isOn;
|
|
25
|
-
if (isOn
|
|
26
|
-
|
|
27
|
-
if (
|
|
26
|
+
if (isOn) {
|
|
27
|
+
panel = p;
|
|
28
|
+
if (!p.getAttribute("aria-labelledby")) {
|
|
29
|
+
const btn = buttons.find((b) => b.getAttribute("data-tab") === name);
|
|
30
|
+
if (btn?.id) p.setAttribute("aria-labelledby", btn.id);
|
|
31
|
+
}
|
|
28
32
|
}
|
|
29
33
|
});
|
|
30
34
|
if (writeHash && typeof window !== "undefined") {
|
|
31
35
|
window.history.replaceState(null, "", `#${name}`);
|
|
32
36
|
}
|
|
37
|
+
if (emit) {
|
|
38
|
+
const index = buttons.findIndex((b) => b.getAttribute("data-tab") === name);
|
|
39
|
+
const tab = buttons[index] ?? null;
|
|
40
|
+
container.dispatchEvent(new CustomEvent("icen:tab-change", {
|
|
41
|
+
bubbles: true,
|
|
42
|
+
detail: { tab, panel, index }
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
33
45
|
}
|
|
34
46
|
function isDisabled(btn) {
|
|
35
47
|
return btn.hasAttribute("disabled") || btn.getAttribute("aria-disabled") === "true" || btn.classList.contains("is-disabled");
|
|
@@ -52,7 +64,7 @@ function nextEnabled(container, from, dir) {
|
|
|
52
64
|
}
|
|
53
65
|
function setup(container) {
|
|
54
66
|
const el = container;
|
|
55
|
-
if (el.__icenTabsInit) return;
|
|
67
|
+
if (el.__icenTabsInit) return void 0;
|
|
56
68
|
el.__icenTabsInit = true;
|
|
57
69
|
const useHash = container.hasAttribute("data-tabs-hash");
|
|
58
70
|
const names = tabNames(container);
|
|
@@ -71,7 +83,7 @@ function setup(container) {
|
|
|
71
83
|
if (!btn || btn.closest("[data-tabs]") !== container) return;
|
|
72
84
|
if (btn instanceof HTMLElement && isDisabled(btn)) return;
|
|
73
85
|
const name = btn.getAttribute("data-tab");
|
|
74
|
-
if (name) activate(container, name, useHash);
|
|
86
|
+
if (name) activate(container, name, useHash, true);
|
|
75
87
|
};
|
|
76
88
|
const onKeyDown = (ev) => {
|
|
77
89
|
const kev = ev;
|
|
@@ -99,12 +111,12 @@ function setup(container) {
|
|
|
99
111
|
}
|
|
100
112
|
case "Home": {
|
|
101
113
|
kev.preventDefault();
|
|
102
|
-
focusTab(container,
|
|
114
|
+
focusTab(container, nextEnabled(container, -1, 1));
|
|
103
115
|
break;
|
|
104
116
|
}
|
|
105
117
|
case "End": {
|
|
106
118
|
kev.preventDefault();
|
|
107
|
-
focusTab(container, buttonsList.length -
|
|
119
|
+
focusTab(container, nextEnabled(container, buttonsList.length, -1));
|
|
108
120
|
break;
|
|
109
121
|
}
|
|
110
122
|
case "Enter":
|
|
@@ -112,7 +124,7 @@ function setup(container) {
|
|
|
112
124
|
if (btn instanceof HTMLElement && !isDisabled(btn)) {
|
|
113
125
|
kev.preventDefault();
|
|
114
126
|
const name = btn.getAttribute("data-tab");
|
|
115
|
-
if (name) activate(container, name, useHash);
|
|
127
|
+
if (name) activate(container, name, useHash, true);
|
|
116
128
|
}
|
|
117
129
|
break;
|
|
118
130
|
}
|
|
@@ -125,29 +137,42 @@ function setup(container) {
|
|
|
125
137
|
const h = currentHash();
|
|
126
138
|
if (names.includes(h)) initial = h;
|
|
127
139
|
} else {
|
|
128
|
-
const n = container.querySelector("[data-tab].active")?.getAttribute("data-tab");
|
|
140
|
+
const n = container.querySelector("[data-tab].is-active")?.getAttribute("data-tab");
|
|
129
141
|
if (n && names.includes(n)) initial = n;
|
|
130
142
|
}
|
|
131
143
|
activate(container, initial ?? names[0], false);
|
|
132
144
|
const onHash = () => {
|
|
133
145
|
const h = currentHash();
|
|
134
|
-
if (names.includes(h)) activate(container, h, false);
|
|
146
|
+
if (names.includes(h)) activate(container, h, false, true);
|
|
135
147
|
};
|
|
136
148
|
if (useHash && typeof window !== "undefined") {
|
|
137
149
|
window.addEventListener("hashchange", onHash);
|
|
138
150
|
}
|
|
139
|
-
|
|
151
|
+
const cleanup = () => {
|
|
140
152
|
container.removeEventListener("click", onClick);
|
|
141
153
|
container.removeEventListener("keydown", onKeyDown);
|
|
142
154
|
window.removeEventListener("hashchange", onHash);
|
|
155
|
+
el.__icenTabsInit = false;
|
|
143
156
|
};
|
|
157
|
+
el.__icenTabsCleanup = cleanup;
|
|
158
|
+
return cleanup;
|
|
144
159
|
}
|
|
145
|
-
function initTabs(root
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
160
|
+
function initTabs(root) {
|
|
161
|
+
const cleanups = [];
|
|
162
|
+
if (typeof document !== "undefined") {
|
|
163
|
+
const scope = root ?? document;
|
|
164
|
+
const containers = [];
|
|
165
|
+
if (scope instanceof Element && scope.matches("[data-tabs]")) containers.push(scope);
|
|
166
|
+
containers.push(...Array.from(scope.querySelectorAll("[data-tabs]")));
|
|
167
|
+
for (const c of containers) {
|
|
168
|
+
const cleanup = setup(c);
|
|
169
|
+
if (cleanup) cleanups.push(cleanup);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return () => {
|
|
173
|
+
for (const fn of cleanups) fn();
|
|
174
|
+
cleanups.length = 0;
|
|
175
|
+
};
|
|
151
176
|
}
|
|
152
177
|
export {
|
|
153
178
|
initTabs
|
|
@@ -9,6 +9,13 @@ function setup(wrap) {
|
|
|
9
9
|
const maxAttr = wrap.getAttribute("data-max");
|
|
10
10
|
const max = maxAttr ? Number(maxAttr) || Infinity : Infinity;
|
|
11
11
|
const placeholder = field.placeholder;
|
|
12
|
+
let isComposing = false;
|
|
13
|
+
field.addEventListener("compositionstart", () => {
|
|
14
|
+
isComposing = true;
|
|
15
|
+
});
|
|
16
|
+
field.addEventListener("compositionend", () => {
|
|
17
|
+
isComposing = false;
|
|
18
|
+
});
|
|
12
19
|
let tags = (wrap.getAttribute("data-tags") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
13
20
|
function sync() {
|
|
14
21
|
wrap.setAttribute("data-tags", tags.join(","));
|
|
@@ -63,6 +70,7 @@ function setup(wrap) {
|
|
|
63
70
|
emit();
|
|
64
71
|
}
|
|
65
72
|
field.addEventListener("keydown", (ev) => {
|
|
73
|
+
if (isComposing || ev.isComposing) return;
|
|
66
74
|
if (ev.key === "Enter" || ev.key === ",") {
|
|
67
75
|
ev.preventDefault();
|
|
68
76
|
commit();
|
|
@@ -73,11 +81,12 @@ function setup(wrap) {
|
|
|
73
81
|
field.addEventListener("blur", commit);
|
|
74
82
|
render();
|
|
75
83
|
}
|
|
76
|
-
function initTagInput(root
|
|
84
|
+
function initTagInput(root) {
|
|
77
85
|
if (typeof document === "undefined") return;
|
|
86
|
+
const scope = root ?? document;
|
|
78
87
|
const containers = [];
|
|
79
|
-
if (
|
|
80
|
-
containers.push(...Array.from(
|
|
88
|
+
if (scope instanceof HTMLElement && scope.matches(".tag-input")) containers.push(scope);
|
|
89
|
+
containers.push(...Array.from(scope.querySelectorAll(".tag-input")));
|
|
81
90
|
for (const c of containers) setup(c);
|
|
82
91
|
}
|
|
83
92
|
export {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
interface ThemeState {
|
|
2
|
+
preset: string;
|
|
3
|
+
dark: boolean;
|
|
4
|
+
style: string;
|
|
5
|
+
}
|
|
6
|
+
declare const PRESETS: readonly ["clay", "piano", "art", "vangogh", "ink", "retro"];
|
|
7
|
+
declare const STYLES: readonly ["modern", "retro", "terminal"];
|
|
8
|
+
declare const STORAGE_KEY = "icen.ui.theme";
|
|
9
|
+
/** 默认主题:clay + 跟随系统明暗 + modern。SSR 下返回离线兜底值。 */
|
|
10
|
+
declare function getDefaultTheme(): ThemeState;
|
|
11
|
+
/** 读 localStorage 中的持久化主题;缺失或坏数据回退默认。 */
|
|
12
|
+
declare function getTheme(): ThemeState;
|
|
13
|
+
/** 把主题应用到 <html>:先清掉所有 preset/style/dark 相关类,再按 t 挂回。 */
|
|
14
|
+
declare function applyTheme(t: ThemeState): void;
|
|
15
|
+
/** 合并当前主题与 patch,应用并持久化,返回新态。patch 中的非法枚举值回退默认。 */
|
|
16
|
+
declare function setTheme(patch: Partial<ThemeState>): ThemeState;
|
|
17
|
+
/** 切换明暗,返回新态。 */
|
|
18
|
+
declare function toggleDark(): ThemeState;
|
|
19
|
+
/** 页面加载早期调用:读持久化主题并应用,防闪烁。
|
|
20
|
+
* 无持久化偏好时,自动跟随系统的 prefers-color-scheme 变化(实时);
|
|
21
|
+
* 一旦用户显式 setTheme / toggleDark,跟随停止(持久化优先)。
|
|
22
|
+
* 重复调用幂等(系统监听不会重复挂)。 */
|
|
23
|
+
declare function initTheme(): ThemeState;
|
|
24
|
+
|
|
25
|
+
export { PRESETS, STORAGE_KEY, STYLES, type ThemeState, applyTheme, getDefaultTheme, getTheme, initTheme, setTheme, toggleDark };
|
package/dist/behaviors/theme.mjs
CHANGED
|
@@ -62,10 +62,12 @@ function setTheme(patch) {
|
|
|
62
62
|
function toggleDark() {
|
|
63
63
|
return setTheme({ dark: !getTheme().dark });
|
|
64
64
|
}
|
|
65
|
+
var systemListenerAttached = false;
|
|
65
66
|
function initTheme() {
|
|
66
67
|
const t = getTheme();
|
|
67
68
|
applyTheme(t);
|
|
68
|
-
if (isBrowser() && typeof window.matchMedia === "function" && !hasPersistedPreference()) {
|
|
69
|
+
if (isBrowser() && typeof window.matchMedia === "function" && !systemListenerAttached && !hasPersistedPreference()) {
|
|
70
|
+
systemListenerAttached = true;
|
|
69
71
|
let lastDark = t.dark;
|
|
70
72
|
try {
|
|
71
73
|
const mql = window.matchMedia("(prefers-color-scheme: dark)");
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
type ToastKind = 'ok' | 'err' | 'warn' | 'info';
|
|
2
|
+
interface ToastOptions {
|
|
3
|
+
/** 保留时长(毫秒),默认 2600;带 action 时默认 6000 */
|
|
4
|
+
ms?: number;
|
|
5
|
+
/** 配合 action:可撤销 / 重试 / 查看等 */
|
|
6
|
+
actionLabel?: string;
|
|
7
|
+
/** action 触发回调;返回 false 阻止 toast 关闭 */
|
|
8
|
+
onAction?: () => void | Promise<void> | boolean;
|
|
9
|
+
}
|
|
10
|
+
interface ToastConfig {
|
|
11
|
+
/** 同屏最大堆叠,默认 5;超出从最早的开始移除 */
|
|
12
|
+
maxStack?: number;
|
|
13
|
+
}
|
|
14
|
+
interface ToastFn {
|
|
15
|
+
(message: string, kind?: ToastKind, ms?: number): void;
|
|
16
|
+
ok(message: string, ms?: number): void;
|
|
17
|
+
err(message: string, ms?: number): void;
|
|
18
|
+
warn(message: string, ms?: number): void;
|
|
19
|
+
info(message: string, ms?: number): void;
|
|
20
|
+
action(message: string, actionLabel: string, onAction: () => void, ms?: number): void;
|
|
21
|
+
dismiss(): void;
|
|
22
|
+
config(opts: ToastConfig): void;
|
|
23
|
+
}
|
|
24
|
+
declare const toast: ToastFn;
|
|
25
|
+
|
|
26
|
+
export { type ToastConfig, type ToastKind, type ToastOptions, toast };
|
package/dist/behaviors/toast.mjs
CHANGED
|
@@ -16,21 +16,35 @@ function ensureContainer() {
|
|
|
16
16
|
document.body.appendChild(el);
|
|
17
17
|
return el;
|
|
18
18
|
}
|
|
19
|
+
var KIND_CLASS = {
|
|
20
|
+
ok: "toast--success",
|
|
21
|
+
err: "toast--error",
|
|
22
|
+
warn: "toast--warning",
|
|
23
|
+
info: "toast--info"
|
|
24
|
+
};
|
|
25
|
+
function scheduleRemove(el) {
|
|
26
|
+
el.classList.add("is-leaving");
|
|
27
|
+
let done = false;
|
|
28
|
+
const finish = () => {
|
|
29
|
+
if (done) return;
|
|
30
|
+
done = true;
|
|
31
|
+
el.remove();
|
|
32
|
+
};
|
|
33
|
+
el.addEventListener("transitionend", finish, { once: true });
|
|
34
|
+
window.setTimeout(finish, 400);
|
|
35
|
+
}
|
|
19
36
|
function trimStack(container) {
|
|
20
37
|
for (; ; ) {
|
|
21
38
|
const items = Array.from(container.querySelectorAll(":scope > .toast"));
|
|
22
39
|
if (items.length <= maxStack) return;
|
|
23
40
|
const oldest = items[0];
|
|
24
41
|
if (!oldest) return;
|
|
25
|
-
oldest.classList.
|
|
26
|
-
let done = false;
|
|
27
|
-
const finish = () => {
|
|
28
|
-
if (done) return;
|
|
29
|
-
done = true;
|
|
42
|
+
if (oldest.classList.contains("is-leaving")) {
|
|
30
43
|
oldest.remove();
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
scheduleRemove(oldest);
|
|
47
|
+
return;
|
|
34
48
|
}
|
|
35
49
|
}
|
|
36
50
|
function show(message, kind = "info", opts = {}) {
|
|
@@ -38,7 +52,7 @@ function show(message, kind = "info", opts = {}) {
|
|
|
38
52
|
const { ms, actionLabel, onAction } = opts;
|
|
39
53
|
const container = ensureContainer();
|
|
40
54
|
const el = document.createElement("div");
|
|
41
|
-
el.className =
|
|
55
|
+
el.className = `toast ${KIND_CLASS[kind]}`;
|
|
42
56
|
el.setAttribute("role", kind === "err" ? "alert" : "status");
|
|
43
57
|
const msgNode = document.createElement("span");
|
|
44
58
|
msgNode.className = "toast-msg";
|
|
@@ -58,15 +72,7 @@ function show(message, kind = "info", opts = {}) {
|
|
|
58
72
|
const remove = () => {
|
|
59
73
|
if (removed) return;
|
|
60
74
|
removed = true;
|
|
61
|
-
el
|
|
62
|
-
let done = false;
|
|
63
|
-
const finish = () => {
|
|
64
|
-
if (done) return;
|
|
65
|
-
done = true;
|
|
66
|
-
el.remove();
|
|
67
|
-
};
|
|
68
|
-
el.addEventListener("transitionend", finish, { once: true });
|
|
69
|
-
window.setTimeout(finish, 400);
|
|
75
|
+
scheduleRemove(el);
|
|
70
76
|
};
|
|
71
77
|
const duration = ms ?? (actionLabel ? ACTION_MS : DEFAULT_MS);
|
|
72
78
|
const timer = window.setTimeout(remove, duration);
|
package/dist/behaviors/tree.mjs
CHANGED
|
@@ -88,11 +88,12 @@ function setup(tree) {
|
|
|
88
88
|
}
|
|
89
89
|
});
|
|
90
90
|
}
|
|
91
|
-
function initTree(root
|
|
91
|
+
function initTree(root) {
|
|
92
92
|
if (typeof document === "undefined") return;
|
|
93
|
+
const scope = root ?? document;
|
|
93
94
|
const trees = [];
|
|
94
|
-
if (
|
|
95
|
-
trees.push(...Array.from(
|
|
95
|
+
if (scope instanceof Element && scope.matches(".tree")) trees.push(scope);
|
|
96
|
+
trees.push(...Array.from(scope.querySelectorAll(".tree")));
|
|
96
97
|
for (const t of trees) setup(t);
|
|
97
98
|
}
|
|
98
99
|
export {
|
|
@@ -1,13 +1,190 @@
|
|
|
1
1
|
// src/behaviors/upload.ts
|
|
2
|
+
function formatSize(bytes) {
|
|
3
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
4
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
5
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
6
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
7
|
+
}
|
|
8
|
+
function isAccepted(file, accept) {
|
|
9
|
+
if (!accept.trim()) return true;
|
|
10
|
+
const types = accept.split(",").map((t) => t.trim().toLowerCase());
|
|
11
|
+
const fileName = file.name.toLowerCase();
|
|
12
|
+
const mime = file.type.toLowerCase();
|
|
13
|
+
return types.some((t) => {
|
|
14
|
+
if (t.startsWith(".")) return fileName.endsWith(t);
|
|
15
|
+
if (t.endsWith("/*")) return mime.startsWith(t.slice(0, -1));
|
|
16
|
+
return mime === t;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
var fileIconSvg = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>';
|
|
20
|
+
var xIconSvg = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg>';
|
|
21
|
+
function parseSvg(source) {
|
|
22
|
+
try {
|
|
23
|
+
const doc = new DOMParser().parseFromString(source, "image/svg+xml");
|
|
24
|
+
const svg = doc.documentElement;
|
|
25
|
+
if (!svg || svg.tagName.toLowerCase() !== "svg") return null;
|
|
26
|
+
return document.importNode(svg, true);
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function sameFile(a, b) {
|
|
32
|
+
return a.name === b.name && a.size === b.size && a.lastModified === b.lastModified;
|
|
33
|
+
}
|
|
2
34
|
function setup(zone) {
|
|
3
35
|
const el = zone;
|
|
4
36
|
if (el.__icenUploadInit) return;
|
|
5
37
|
el.__icenUploadInit = true;
|
|
6
38
|
const input = zone.querySelector('input[type="file"]');
|
|
7
39
|
if (!input) return;
|
|
40
|
+
const hasList = zone.hasAttribute("data-upload-list");
|
|
41
|
+
const maxSize = Number(zone.getAttribute("data-max-size")) || 0;
|
|
42
|
+
const maxFiles = Number(zone.getAttribute("data-max-files")) || 0;
|
|
43
|
+
const accept = input.getAttribute("accept") ?? "";
|
|
44
|
+
let currentFiles = [];
|
|
45
|
+
const urlMap = /* @__PURE__ */ new Map();
|
|
46
|
+
let syncing = false;
|
|
47
|
+
function getListEl() {
|
|
48
|
+
if (!hasList) return null;
|
|
49
|
+
let list = zone.querySelector(".upload-list");
|
|
50
|
+
if (!list) {
|
|
51
|
+
list = document.createElement("ul");
|
|
52
|
+
list.className = "upload-list";
|
|
53
|
+
zone.appendChild(list);
|
|
54
|
+
}
|
|
55
|
+
return list;
|
|
56
|
+
}
|
|
57
|
+
function renderList() {
|
|
58
|
+
const list = getListEl();
|
|
59
|
+
if (!list) return;
|
|
60
|
+
urlMap.forEach((url, f) => {
|
|
61
|
+
if (!currentFiles.includes(f)) {
|
|
62
|
+
URL.revokeObjectURL(url);
|
|
63
|
+
urlMap.delete(f);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
list.textContent = "";
|
|
67
|
+
if (currentFiles.length === 0) {
|
|
68
|
+
list.hidden = true;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
list.hidden = false;
|
|
72
|
+
for (const file of currentFiles) {
|
|
73
|
+
const li = document.createElement("li");
|
|
74
|
+
li.className = "upload-list-item";
|
|
75
|
+
const isImage = file.type.startsWith("image/");
|
|
76
|
+
let thumbUrl;
|
|
77
|
+
if (isImage) {
|
|
78
|
+
thumbUrl = urlMap.get(file);
|
|
79
|
+
if (!thumbUrl) {
|
|
80
|
+
try {
|
|
81
|
+
thumbUrl = URL.createObjectURL(file);
|
|
82
|
+
urlMap.set(file, thumbUrl);
|
|
83
|
+
} catch {
|
|
84
|
+
thumbUrl = void 0;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (thumbUrl) {
|
|
89
|
+
const img = document.createElement("img");
|
|
90
|
+
img.className = "upload-list-item-thumb";
|
|
91
|
+
img.src = thumbUrl;
|
|
92
|
+
img.alt = file.name;
|
|
93
|
+
li.appendChild(img);
|
|
94
|
+
} else {
|
|
95
|
+
const icon = document.createElement("span");
|
|
96
|
+
icon.className = "upload-list-item-icon";
|
|
97
|
+
const svg = parseSvg(fileIconSvg);
|
|
98
|
+
if (svg) icon.appendChild(svg);
|
|
99
|
+
li.appendChild(icon);
|
|
100
|
+
}
|
|
101
|
+
const info = document.createElement("div");
|
|
102
|
+
info.className = "upload-list-item-info";
|
|
103
|
+
const name = document.createElement("span");
|
|
104
|
+
name.className = "upload-list-item-name";
|
|
105
|
+
name.textContent = file.name;
|
|
106
|
+
const size = document.createElement("span");
|
|
107
|
+
size.className = "upload-list-item-size";
|
|
108
|
+
size.textContent = formatSize(file.size);
|
|
109
|
+
info.appendChild(name);
|
|
110
|
+
info.appendChild(size);
|
|
111
|
+
li.appendChild(info);
|
|
112
|
+
const removeBtn = document.createElement("button");
|
|
113
|
+
removeBtn.className = "upload-list-item-x";
|
|
114
|
+
removeBtn.type = "button";
|
|
115
|
+
removeBtn.setAttribute("aria-label", `\u79FB\u9664 ${file.name}`);
|
|
116
|
+
const xSvg = parseSvg(xIconSvg);
|
|
117
|
+
if (xSvg) removeBtn.appendChild(xSvg);
|
|
118
|
+
removeBtn.addEventListener("click", (ev) => {
|
|
119
|
+
ev.stopPropagation();
|
|
120
|
+
currentFiles = currentFiles.filter((f) => f !== file);
|
|
121
|
+
zone.dispatchEvent(new CustomEvent("icen:upload-remove", { bubbles: true, detail: { file } }));
|
|
122
|
+
renderList();
|
|
123
|
+
syncInputFiles();
|
|
124
|
+
});
|
|
125
|
+
li.appendChild(removeBtn);
|
|
126
|
+
list.appendChild(li);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function syncInputFiles() {
|
|
130
|
+
try {
|
|
131
|
+
const dt = new DataTransfer();
|
|
132
|
+
for (const f of currentFiles) dt.items.add(f);
|
|
133
|
+
syncing = true;
|
|
134
|
+
input.files = dt.files;
|
|
135
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
136
|
+
} catch {
|
|
137
|
+
} finally {
|
|
138
|
+
syncing = false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function validate(file) {
|
|
142
|
+
if (accept && !isAccepted(file, accept)) return "\u6587\u4EF6\u7C7B\u578B\u4E0D\u652F\u6301";
|
|
143
|
+
if (maxSize > 0 && file.size > maxSize) return `\u6587\u4EF6\u8D85\u8FC7 ${formatSize(maxSize)}`;
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
function handleFiles(files) {
|
|
147
|
+
const valid = [];
|
|
148
|
+
for (const file of files) {
|
|
149
|
+
const reason = validate(file);
|
|
150
|
+
if (reason) {
|
|
151
|
+
zone.dispatchEvent(new CustomEvent("icen:upload-error", { bubbles: true, detail: { file, reason } }));
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
valid.push(file);
|
|
155
|
+
}
|
|
156
|
+
if (valid.length === 0) return;
|
|
157
|
+
if (hasList) {
|
|
158
|
+
const fresh = [];
|
|
159
|
+
for (const f of valid) {
|
|
160
|
+
if (currentFiles.some((c) => sameFile(c, f)) || fresh.some((c) => sameFile(c, f))) continue;
|
|
161
|
+
fresh.push(f);
|
|
162
|
+
}
|
|
163
|
+
if (fresh.length === 0) return;
|
|
164
|
+
if (maxFiles > 0) {
|
|
165
|
+
const remaining = maxFiles - currentFiles.length;
|
|
166
|
+
if (remaining <= 0) {
|
|
167
|
+
zone.dispatchEvent(new CustomEvent("icen:upload-error", {
|
|
168
|
+
bubbles: true,
|
|
169
|
+
detail: { file: null, reason: `\u6700\u591A ${maxFiles} \u4E2A\u6587\u4EF6` }
|
|
170
|
+
}));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (fresh.length > remaining) fresh.length = remaining;
|
|
174
|
+
}
|
|
175
|
+
currentFiles.push(...fresh);
|
|
176
|
+
renderList();
|
|
177
|
+
syncInputFiles();
|
|
178
|
+
zone.dispatchEvent(new CustomEvent("icen:upload", { bubbles: true, detail: { files: fresh } }));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
zone.dispatchEvent(new CustomEvent("icen:upload", { bubbles: true, detail: { files: valid } }));
|
|
182
|
+
}
|
|
8
183
|
const openPicker = () => input.click();
|
|
9
184
|
zone.addEventListener("click", (ev) => {
|
|
10
185
|
if (ev.target === input) return;
|
|
186
|
+
const tgt = ev.target;
|
|
187
|
+
if (tgt instanceof Element && tgt.closest(".upload-list")) return;
|
|
11
188
|
openPicker();
|
|
12
189
|
});
|
|
13
190
|
zone.addEventListener("keydown", (ev) => {
|
|
@@ -16,37 +193,48 @@ function setup(zone) {
|
|
|
16
193
|
openPicker();
|
|
17
194
|
}
|
|
18
195
|
});
|
|
196
|
+
input.addEventListener("change", () => {
|
|
197
|
+
if (syncing) return;
|
|
198
|
+
if (!input.files) return;
|
|
199
|
+
const files = Array.from(input.files);
|
|
200
|
+
if (hasList) {
|
|
201
|
+
input.value = "";
|
|
202
|
+
}
|
|
203
|
+
handleFiles(files);
|
|
204
|
+
});
|
|
19
205
|
zone.addEventListener("dragover", (ev) => {
|
|
20
206
|
ev.preventDefault();
|
|
21
207
|
zone.classList.add("is-drag");
|
|
22
208
|
});
|
|
23
|
-
zone.addEventListener("dragleave", () =>
|
|
209
|
+
zone.addEventListener("dragleave", (ev) => {
|
|
210
|
+
if (ev.target === zone) zone.classList.remove("is-drag");
|
|
211
|
+
});
|
|
24
212
|
zone.addEventListener("drop", (ev) => {
|
|
25
213
|
ev.preventDefault();
|
|
26
214
|
zone.classList.remove("is-drag");
|
|
27
215
|
const files = ev.dataTransfer?.files;
|
|
28
216
|
if (!files || files.length === 0) return;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
217
|
+
handleFiles(Array.from(files));
|
|
218
|
+
if (!hasList) {
|
|
219
|
+
try {
|
|
220
|
+
const dt = new DataTransfer();
|
|
221
|
+
for (const f of Array.from(files)) dt.items.add(f);
|
|
222
|
+
syncing = true;
|
|
223
|
+
input.files = dt.files;
|
|
224
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
225
|
+
} catch {
|
|
226
|
+
} finally {
|
|
227
|
+
syncing = false;
|
|
228
|
+
}
|
|
40
229
|
}
|
|
41
|
-
const list = Array.from(files);
|
|
42
|
-
zone.dispatchEvent(new CustomEvent("icen:upload", { bubbles: true, detail: { files: list } }));
|
|
43
230
|
});
|
|
44
231
|
}
|
|
45
|
-
function initUpload(root
|
|
232
|
+
function initUpload(root) {
|
|
46
233
|
if (typeof document === "undefined") return;
|
|
234
|
+
const scope = root ?? document;
|
|
47
235
|
const zones = [];
|
|
48
|
-
if (
|
|
49
|
-
zones.push(...Array.from(
|
|
236
|
+
if (scope instanceof HTMLElement && scope.matches(".upload")) zones.push(scope);
|
|
237
|
+
zones.push(...Array.from(scope.querySelectorAll(".upload")));
|
|
50
238
|
for (const z of zones) setup(z);
|
|
51
239
|
}
|
|
52
240
|
export {
|