@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
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** 打开指定 id 的命令面板。 */
|
|
2
|
+
declare function openCommandPalette(id: string): void;
|
|
3
|
+
/** 关闭指定 id 的命令面板(backdrop 已被外部移除时也照常复位状态/监听/滚动锁)。 */
|
|
4
|
+
declare function closeCommandPalette(id: string): void;
|
|
5
|
+
/** 初始化:委托监听触发器 / 搜索 / 外点 / 全局快捷键。 */
|
|
6
|
+
declare function initCommandPalette(root?: ParentNode): void;
|
|
7
|
+
|
|
8
|
+
export { closeCommandPalette, initCommandPalette, openCommandPalette };
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// src/behaviors/command-palette.ts
|
|
2
|
+
var FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
3
|
+
var openId = null;
|
|
4
|
+
var restoreFocusTo = null;
|
|
5
|
+
var prevBodyOverflow = "";
|
|
6
|
+
var keyHandler = null;
|
|
7
|
+
var globalKeyHandler = null;
|
|
8
|
+
var initialized = false;
|
|
9
|
+
function findBackdrop(id) {
|
|
10
|
+
return document.querySelector(
|
|
11
|
+
`.command-palette-backdrop[data-command-palette="${CSS.escape(id)}"]`
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
function getVisibleItems(panel) {
|
|
15
|
+
return Array.from(
|
|
16
|
+
panel.querySelectorAll(
|
|
17
|
+
'.command-palette-item:not([hidden]):not([disabled]):not([aria-disabled="true"])'
|
|
18
|
+
)
|
|
19
|
+
).filter((el) => el.offsetParent !== null || el.getClientRects().length > 0);
|
|
20
|
+
}
|
|
21
|
+
function isDisabledItem(el) {
|
|
22
|
+
return el.hasAttribute("disabled") || el.getAttribute("aria-disabled") === "true";
|
|
23
|
+
}
|
|
24
|
+
function setActive(panel, idx) {
|
|
25
|
+
const items = getVisibleItems(panel);
|
|
26
|
+
items.forEach((el, i) => el.classList.toggle("is-active", i === idx));
|
|
27
|
+
const active = items[idx];
|
|
28
|
+
if (active && typeof active.scrollIntoView === "function") {
|
|
29
|
+
active.scrollIntoView({ block: "nearest" });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function filterPanel(panel, query) {
|
|
33
|
+
const q = query.trim().toLowerCase();
|
|
34
|
+
let visibleCount = 0;
|
|
35
|
+
panel.querySelectorAll(".command-palette-item").forEach((item) => {
|
|
36
|
+
if (isDisabledItem(item)) return;
|
|
37
|
+
const label = (item.querySelector(".command-palette-item-label")?.textContent ?? "").toLowerCase();
|
|
38
|
+
const hint = (item.querySelector(".command-palette-item-hint")?.textContent ?? "").toLowerCase();
|
|
39
|
+
const keyword = (item.dataset.commandPaletteKeyword ?? "").toLowerCase();
|
|
40
|
+
const match = !q || label.includes(q) || hint.includes(q) || keyword.includes(q);
|
|
41
|
+
item.hidden = !match;
|
|
42
|
+
item.classList.remove("is-active");
|
|
43
|
+
if (match) visibleCount++;
|
|
44
|
+
});
|
|
45
|
+
panel.querySelectorAll(".command-palette-group").forEach((group) => {
|
|
46
|
+
const hasVisible = Array.from(group.querySelectorAll(".command-palette-item")).some(
|
|
47
|
+
(it) => !it.hidden
|
|
48
|
+
);
|
|
49
|
+
group.hidden = !hasVisible;
|
|
50
|
+
});
|
|
51
|
+
const empty = panel.querySelector(".command-palette-empty");
|
|
52
|
+
if (empty) empty.hidden = visibleCount !== 0;
|
|
53
|
+
if (visibleCount > 0) setActive(panel, 0);
|
|
54
|
+
}
|
|
55
|
+
function openCommandPalette(id) {
|
|
56
|
+
if (typeof document === "undefined") return;
|
|
57
|
+
if (openId === id) return;
|
|
58
|
+
if (openId) closeCommandPalette(openId);
|
|
59
|
+
const backdrop = findBackdrop(id);
|
|
60
|
+
if (!backdrop) {
|
|
61
|
+
console.warn(`[@icen.ai/ui] command-palette: \u672A\u627E\u5230 data-command-palette="${id}" \u7684 .command-palette-backdrop\uFF0C\u5DF2\u8DF3\u8FC7\u6253\u5F00`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const panel = backdrop.querySelector(".command-palette");
|
|
65
|
+
if (!panel) return;
|
|
66
|
+
if (backdrop.parentElement !== document.body) document.body.appendChild(backdrop);
|
|
67
|
+
restoreFocusTo = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
68
|
+
openId = id;
|
|
69
|
+
backdrop.hidden = false;
|
|
70
|
+
prevBodyOverflow = document.body.style.overflow;
|
|
71
|
+
document.body.style.overflow = "hidden";
|
|
72
|
+
const input = panel.querySelector(".command-palette-input");
|
|
73
|
+
if (input) {
|
|
74
|
+
input.value = "";
|
|
75
|
+
input.focus();
|
|
76
|
+
} else {
|
|
77
|
+
panel.focus();
|
|
78
|
+
}
|
|
79
|
+
filterPanel(panel, "");
|
|
80
|
+
keyHandler = (e) => {
|
|
81
|
+
if (e.key === "Escape") {
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
closeCommandPalette(id);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
const items = getVisibleItems(panel);
|
|
89
|
+
if (items.length === 0) return;
|
|
90
|
+
const cur = items.findIndex((el) => el.classList.contains("is-active"));
|
|
91
|
+
const next = e.key === "ArrowDown" ? (cur + 1) % items.length : (cur - 1 + items.length) % items.length;
|
|
92
|
+
setActive(panel, next);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (e.key === "Enter") {
|
|
96
|
+
const active = panel.querySelector(".command-palette-item.is-active");
|
|
97
|
+
if (active && !isDisabledItem(active)) {
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
active.click();
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (e.key === "Tab") {
|
|
104
|
+
e.preventDefault();
|
|
105
|
+
const els = Array.from(panel.querySelectorAll(FOCUSABLE)).filter(
|
|
106
|
+
(el) => !el.hidden && el.offsetParent !== null
|
|
107
|
+
);
|
|
108
|
+
if (els.length === 0) return;
|
|
109
|
+
const cur = els.findIndex((el) => el === document.activeElement);
|
|
110
|
+
const next = e.shiftKey ? (cur - 1 + els.length) % els.length : (cur + 1) % els.length;
|
|
111
|
+
els[next]?.focus();
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
window.addEventListener("keydown", keyHandler);
|
|
115
|
+
}
|
|
116
|
+
function closeCommandPalette(id) {
|
|
117
|
+
if (typeof document === "undefined") return;
|
|
118
|
+
const backdrop = findBackdrop(id);
|
|
119
|
+
if (backdrop) backdrop.hidden = true;
|
|
120
|
+
if (openId !== id) return;
|
|
121
|
+
openId = null;
|
|
122
|
+
if (keyHandler) {
|
|
123
|
+
window.removeEventListener("keydown", keyHandler);
|
|
124
|
+
keyHandler = null;
|
|
125
|
+
}
|
|
126
|
+
document.body.style.overflow = prevBodyOverflow;
|
|
127
|
+
if (restoreFocusTo && restoreFocusTo.isConnected) restoreFocusTo.focus();
|
|
128
|
+
restoreFocusTo = null;
|
|
129
|
+
}
|
|
130
|
+
function initCommandPalette(root) {
|
|
131
|
+
if (typeof document === "undefined") return;
|
|
132
|
+
if (typeof window === "undefined") return;
|
|
133
|
+
const scope = root ?? document;
|
|
134
|
+
scope.querySelectorAll("[data-command-palette-open]").forEach((el) => el.setAttribute("aria-haspopup", "dialog"));
|
|
135
|
+
if (initialized) return;
|
|
136
|
+
initialized = true;
|
|
137
|
+
document.addEventListener("click", (e) => {
|
|
138
|
+
const target = e.target instanceof Element ? e.target : null;
|
|
139
|
+
if (!target) return;
|
|
140
|
+
const opener = target.closest("[data-command-palette-open]");
|
|
141
|
+
if (opener?.dataset.commandPaletteOpen) {
|
|
142
|
+
openCommandPalette(opener.dataset.commandPaletteOpen);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const item = target.closest(".command-palette-item");
|
|
146
|
+
if (item) {
|
|
147
|
+
const backdrop = item.closest("[data-command-palette]");
|
|
148
|
+
if (backdrop?.dataset.commandPalette) {
|
|
149
|
+
if (!item.hasAttribute("data-command-palette-keep-open")) {
|
|
150
|
+
closeCommandPalette(backdrop.dataset.commandPalette);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (target instanceof HTMLElement && target.classList.contains("command-palette-backdrop") && target.dataset.commandPalette) {
|
|
156
|
+
closeCommandPalette(target.dataset.commandPalette);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
document.addEventListener("input", (e) => {
|
|
160
|
+
if (!(e.target instanceof HTMLElement)) return;
|
|
161
|
+
if (!e.target.matches(".command-palette-input")) return;
|
|
162
|
+
const backdrop = e.target.closest("[data-command-palette]");
|
|
163
|
+
const panel = backdrop?.querySelector(".command-palette");
|
|
164
|
+
if (panel) filterPanel(panel, e.target.value);
|
|
165
|
+
});
|
|
166
|
+
globalKeyHandler = (e) => {
|
|
167
|
+
if (!(e.metaKey || e.ctrlKey)) return;
|
|
168
|
+
const key = e.key.toLowerCase();
|
|
169
|
+
const palettes = document.querySelectorAll(
|
|
170
|
+
".command-palette-backdrop[data-command-palette]"
|
|
171
|
+
);
|
|
172
|
+
for (const backdrop of Array.from(palettes)) {
|
|
173
|
+
const paletteKey = (backdrop.dataset.commandPaletteKey ?? "k").toLowerCase();
|
|
174
|
+
if (paletteKey !== key) continue;
|
|
175
|
+
const id = backdrop.dataset.commandPalette;
|
|
176
|
+
if (!id) continue;
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
if (openId === id) closeCommandPalette(id);
|
|
179
|
+
else openCommandPalette(id);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
window.addEventListener("keydown", globalKeyHandler);
|
|
184
|
+
}
|
|
185
|
+
export {
|
|
186
|
+
closeCommandPalette,
|
|
187
|
+
initCommandPalette,
|
|
188
|
+
openCommandPalette
|
|
189
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
interface ContextMenuEntry {
|
|
2
|
+
label: string;
|
|
3
|
+
subLabel?: string;
|
|
4
|
+
/** 可信 svg 字符串(唯一走 innerHTML 的字段) */
|
|
5
|
+
icon?: string;
|
|
6
|
+
shortcut?: string;
|
|
7
|
+
danger?: boolean;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
onClick?: () => void;
|
|
10
|
+
}
|
|
11
|
+
interface ContextMenuSeparator {
|
|
12
|
+
type: 'separator';
|
|
13
|
+
}
|
|
14
|
+
interface ContextMenuHeader {
|
|
15
|
+
type: 'header';
|
|
16
|
+
label: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
}
|
|
19
|
+
type ContextMenuItem = ContextMenuEntry | ContextMenuSeparator | ContextMenuHeader;
|
|
20
|
+
interface ContextMenuOptions {
|
|
21
|
+
/** 替换内置默认菜单(未命中任何注册 id 时使用) */
|
|
22
|
+
items?: ContextMenuItem[];
|
|
23
|
+
}
|
|
24
|
+
/** 注册指定 id 的右键菜单项(对应 DOM 中的 [data-context-menu="id"])。 */
|
|
25
|
+
declare function registerContextMenu(id: string, items: ContextMenuItem[]): void;
|
|
26
|
+
/** 在 (x, y) 打开右键菜单(视口边缘自动翻转并 clamp)。 */
|
|
27
|
+
declare function openContextMenu(x: number, y: number, items: ContextMenuItem[]): void;
|
|
28
|
+
/** 关闭当前右键菜单(未打开时为 no-op)。 */
|
|
29
|
+
declare function closeMenu(): void;
|
|
30
|
+
/**
|
|
31
|
+
* 启动全局右键菜单(重复调用安全)。
|
|
32
|
+
* opts.items 提供时替换内置默认菜单。
|
|
33
|
+
*/
|
|
34
|
+
declare function initContextMenu(opts?: ContextMenuOptions): void;
|
|
35
|
+
|
|
36
|
+
export { type ContextMenuEntry, type ContextMenuHeader, type ContextMenuItem, type ContextMenuOptions, type ContextMenuSeparator, closeMenu, initContextMenu, openContextMenu, registerContextMenu };
|
|
@@ -127,7 +127,7 @@ function openContextMenu(x, y, items) {
|
|
|
127
127
|
setFocused(0);
|
|
128
128
|
});
|
|
129
129
|
document.addEventListener("pointerdown", onOutsidePointerDown, true);
|
|
130
|
-
document.addEventListener("scroll",
|
|
130
|
+
document.addEventListener("scroll", onScrollSignal, true);
|
|
131
131
|
window.addEventListener("resize", onCloseSignal);
|
|
132
132
|
window.addEventListener("keydown", onKeyDown);
|
|
133
133
|
}
|
|
@@ -138,7 +138,7 @@ function closeMenu() {
|
|
|
138
138
|
focusableItems = [];
|
|
139
139
|
focusedIndex = -1;
|
|
140
140
|
document.removeEventListener("pointerdown", onOutsidePointerDown, true);
|
|
141
|
-
document.removeEventListener("scroll",
|
|
141
|
+
document.removeEventListener("scroll", onScrollSignal, true);
|
|
142
142
|
window.removeEventListener("resize", onCloseSignal);
|
|
143
143
|
window.removeEventListener("keydown", onKeyDown);
|
|
144
144
|
}
|
|
@@ -152,6 +152,10 @@ function onOutsidePointerDown(e) {
|
|
|
152
152
|
const target = e.target;
|
|
153
153
|
if (!(target instanceof Node) || !panel.contains(target)) closeMenu();
|
|
154
154
|
}
|
|
155
|
+
function onScrollSignal(e) {
|
|
156
|
+
if (panel && e.target instanceof Node && panel.contains(e.target)) return;
|
|
157
|
+
closeMenu();
|
|
158
|
+
}
|
|
155
159
|
function onCloseSignal() {
|
|
156
160
|
closeMenu();
|
|
157
161
|
}
|
package/dist/behaviors/copy.mjs
CHANGED
|
@@ -34,10 +34,10 @@ function flashDone(btn) {
|
|
|
34
34
|
const iconOnly = btn.hasAttribute("data-copy-icon");
|
|
35
35
|
const original = prev ? prev.original : iconOnly ? null : btn.textContent;
|
|
36
36
|
if (!iconOnly) btn.textContent = "\u5DF2\u590D\u5236";
|
|
37
|
-
btn.classList.add("done");
|
|
37
|
+
btn.classList.add("is-done");
|
|
38
38
|
const timer = window.setTimeout(() => {
|
|
39
39
|
if (!iconOnly) btn.textContent = original;
|
|
40
|
-
btn.classList.remove("done");
|
|
40
|
+
btn.classList.remove("is-done");
|
|
41
41
|
flashing.delete(btn);
|
|
42
42
|
}, 1400);
|
|
43
43
|
flashing.set(btn, { original, timer });
|
|
@@ -52,11 +52,12 @@ function onClick(root, ev) {
|
|
|
52
52
|
if (ok) flashDone(btn);
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
|
-
function initCopy(root
|
|
55
|
+
function initCopy(root) {
|
|
56
56
|
if (typeof document === "undefined") return;
|
|
57
|
-
|
|
58
|
-
initialized.
|
|
59
|
-
|
|
57
|
+
const scope = root ?? document;
|
|
58
|
+
if (initialized.has(scope)) return;
|
|
59
|
+
initialized.add(scope);
|
|
60
|
+
scope.addEventListener("click", (ev) => onClick(scope, ev));
|
|
60
61
|
}
|
|
61
62
|
export {
|
|
62
63
|
initCopy
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ContextMenuItem } from './context-menu.js';
|
|
2
|
+
|
|
3
|
+
interface TableColumn<Row = Record<string, unknown>> {
|
|
4
|
+
key: string;
|
|
5
|
+
title: string;
|
|
6
|
+
/** CSS 宽度:'72px' / '120px' / '2fr';缺省 minmax(0,1fr) */
|
|
7
|
+
width?: string;
|
|
8
|
+
align?: 'left' | 'center' | 'right';
|
|
9
|
+
sortable?: boolean;
|
|
10
|
+
/** 自定义比较(默认:数字感知的 localeCompare) */
|
|
11
|
+
sortFn?: (a: Row, b: Row) => number;
|
|
12
|
+
filterable?: boolean;
|
|
13
|
+
/** 返回 Node 可嵌套任意组件;返回 string/number 走 textContent(安全) */
|
|
14
|
+
render?: (value: unknown, row: Row, rowIndex: number) => Node | string | number | null;
|
|
15
|
+
hidden?: boolean;
|
|
16
|
+
/** 冻结列(左固定,横向滚动时不跟随) */
|
|
17
|
+
frozen?: boolean;
|
|
18
|
+
}
|
|
19
|
+
interface TableOptions<Row = Record<string, unknown>> {
|
|
20
|
+
columns: TableColumn<Row>[];
|
|
21
|
+
data: Row[];
|
|
22
|
+
/** 行唯一键:字段名或函数(多选/展开跨页保持的依据),缺省用行索引 */
|
|
23
|
+
rowKey?: keyof Row | ((row: Row) => string);
|
|
24
|
+
searchable?: boolean;
|
|
25
|
+
searchPlaceholder?: string;
|
|
26
|
+
selectable?: boolean;
|
|
27
|
+
/** 分页(默认开启,pageSize 默认 10 条/页;传 false 关闭;virtual 时忽略) */
|
|
28
|
+
pagination?: boolean;
|
|
29
|
+
pageSize?: number;
|
|
30
|
+
pageSizes?: number[];
|
|
31
|
+
/** 虚拟滚动(大数据量;与 pagination 互斥,virtual 优先) */
|
|
32
|
+
virtual?: boolean;
|
|
33
|
+
/** 虚拟行高 px(默认 44)与窗口高度(默认 '420px') */
|
|
34
|
+
rowHeight?: number;
|
|
35
|
+
height?: string;
|
|
36
|
+
/** 行展开内容(虚拟模式下忽略) */
|
|
37
|
+
expandable?: (row: Row) => Node | string;
|
|
38
|
+
/** 行右键菜单(复用 context-menu;需 menu.css,kit 入口已带) */
|
|
39
|
+
contextMenu?: (row: Row) => ContextMenuItem[];
|
|
40
|
+
striped?: boolean;
|
|
41
|
+
emptyLabel?: string;
|
|
42
|
+
loading?: boolean;
|
|
43
|
+
/** 密度:normal 默认 44px 行高 / compact 紧凑 32px */
|
|
44
|
+
density?: 'normal' | 'compact';
|
|
45
|
+
/** 列宽可拖拽调整(默认 true) */
|
|
46
|
+
resizable?: boolean;
|
|
47
|
+
/** 工具栏列显隐开关(默认 false) */
|
|
48
|
+
columnVisibility?: boolean;
|
|
49
|
+
/** 导出 CSV 按钮显示在工具栏(默认 false) */
|
|
50
|
+
exportable?: boolean;
|
|
51
|
+
/** 行自定义 class(条件着色 / 状态行) */
|
|
52
|
+
rowClassName?: (row: Row, index: number) => string;
|
|
53
|
+
onSelectionChange?: (rows: Row[]) => void;
|
|
54
|
+
onRowClick?: (row: Row) => void;
|
|
55
|
+
onSortChange?: (key: string | null, dir: 'asc' | 'desc') => void;
|
|
56
|
+
}
|
|
57
|
+
interface TableHandle<Row = Record<string, unknown>> {
|
|
58
|
+
setData(data: Row[]): void;
|
|
59
|
+
getSelected(): Row[];
|
|
60
|
+
clearSelection(): void;
|
|
61
|
+
setSearch(query: string): void;
|
|
62
|
+
/** values=null 清除该列筛选 */
|
|
63
|
+
setFilter(key: string, values: string[] | null): void;
|
|
64
|
+
setSort(key: string | null, dir?: 'asc' | 'desc'): void;
|
|
65
|
+
/** 获取当前管线(搜索→筛选→排序)后的数据 */
|
|
66
|
+
getData(): Row[];
|
|
67
|
+
/** 导出当前管线数据为 CSV 并触发下载 */
|
|
68
|
+
exportCSV(filename?: string): void;
|
|
69
|
+
/** 显示/隐藏列 */
|
|
70
|
+
showColumn(key: string): void;
|
|
71
|
+
hideColumn(key: string): void;
|
|
72
|
+
/** 数据被外部原地修改后重渲 */
|
|
73
|
+
refresh(): void;
|
|
74
|
+
destroy(): void;
|
|
75
|
+
}
|
|
76
|
+
declare function createTable<Row = Record<string, unknown>>(el: HTMLElement, opts: TableOptions<Row>): TableHandle<Row>;
|
|
77
|
+
|
|
78
|
+
export { type TableColumn, type TableHandle, type TableOptions, createTable };
|
|
@@ -87,7 +87,7 @@ function openContextMenu(x, y, items) {
|
|
|
87
87
|
setFocused(0);
|
|
88
88
|
});
|
|
89
89
|
document.addEventListener("pointerdown", onOutsidePointerDown, true);
|
|
90
|
-
document.addEventListener("scroll",
|
|
90
|
+
document.addEventListener("scroll", onScrollSignal, true);
|
|
91
91
|
window.addEventListener("resize", onCloseSignal);
|
|
92
92
|
window.addEventListener("keydown", onKeyDown);
|
|
93
93
|
}
|
|
@@ -98,7 +98,7 @@ function closeMenu() {
|
|
|
98
98
|
focusableItems = [];
|
|
99
99
|
focusedIndex = -1;
|
|
100
100
|
document.removeEventListener("pointerdown", onOutsidePointerDown, true);
|
|
101
|
-
document.removeEventListener("scroll",
|
|
101
|
+
document.removeEventListener("scroll", onScrollSignal, true);
|
|
102
102
|
window.removeEventListener("resize", onCloseSignal);
|
|
103
103
|
window.removeEventListener("keydown", onKeyDown);
|
|
104
104
|
}
|
|
@@ -112,6 +112,10 @@ function onOutsidePointerDown(e) {
|
|
|
112
112
|
const target = e.target;
|
|
113
113
|
if (!(target instanceof Node) || !panel.contains(target)) closeMenu();
|
|
114
114
|
}
|
|
115
|
+
function onScrollSignal(e) {
|
|
116
|
+
if (panel && e.target instanceof Node && panel.contains(e.target)) return;
|
|
117
|
+
closeMenu();
|
|
118
|
+
}
|
|
115
119
|
function onCloseSignal() {
|
|
116
120
|
closeMenu();
|
|
117
121
|
}
|
|
@@ -410,7 +414,7 @@ function createTable(el, opts) {
|
|
|
410
414
|
else allKeys.forEach((k) => selected.delete(k));
|
|
411
415
|
emitSelection();
|
|
412
416
|
renderBody();
|
|
413
|
-
|
|
417
|
+
syncHeadState();
|
|
414
418
|
});
|
|
415
419
|
cell.appendChild(box);
|
|
416
420
|
headRow.appendChild(cell);
|
|
@@ -466,6 +470,7 @@ function createTable(el, opts) {
|
|
|
466
470
|
const btn = document.createElement("button");
|
|
467
471
|
btn.type = "button";
|
|
468
472
|
btn.className = "dt-filter-btn";
|
|
473
|
+
btn.dataset.colKey = col.key;
|
|
469
474
|
if (hasFilter) btn.classList.add("is-active");
|
|
470
475
|
btn.setAttribute("aria-label", `\u7B5B\u9009\u300C${col.title}\u300D`);
|
|
471
476
|
btn.appendChild(svgIcon(ICON_FUNNEL, 12));
|
|
@@ -491,6 +496,33 @@ function createTable(el, opts) {
|
|
|
491
496
|
headRow.appendChild(cell);
|
|
492
497
|
}
|
|
493
498
|
}
|
|
499
|
+
function syncHeadState() {
|
|
500
|
+
const headBox = headRow.querySelector(".dt-cell--check .dt-check");
|
|
501
|
+
if (headBox) {
|
|
502
|
+
const allKeys = pipeline().map((r) => keyOfRow(r));
|
|
503
|
+
const selCount = allKeys.filter((k) => selected.has(k)).length;
|
|
504
|
+
headBox.checked = selCount > 0 && selCount === allKeys.length;
|
|
505
|
+
headBox.indeterminate = selCount > 0 && selCount < allKeys.length;
|
|
506
|
+
}
|
|
507
|
+
headRow.querySelectorAll(".dt-filter-btn").forEach((b) => {
|
|
508
|
+
const key = b.dataset.colKey;
|
|
509
|
+
if (key) b.classList.toggle("is-active", filters.has(key));
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
function renderHeadKeepPanel() {
|
|
513
|
+
const keep = openPanel;
|
|
514
|
+
renderHead();
|
|
515
|
+
if (!keep) return;
|
|
516
|
+
openPanel = keep;
|
|
517
|
+
document.body.appendChild(keep.el);
|
|
518
|
+
window.addEventListener("scroll", onPanelScroll, true);
|
|
519
|
+
window.addEventListener("resize", onPanelScroll);
|
|
520
|
+
if (keep.key !== "__cols") {
|
|
521
|
+
headRow.querySelectorAll(".dt-filter-btn").forEach((b) => {
|
|
522
|
+
if (b.dataset.colKey === keep.key) b.classList.add("is-open");
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}
|
|
494
526
|
function attachResize(handle, col) {
|
|
495
527
|
handle.addEventListener("pointerdown", (ev) => {
|
|
496
528
|
ev.preventDefault();
|
|
@@ -508,12 +540,17 @@ function createTable(el, opts) {
|
|
|
508
540
|
});
|
|
509
541
|
};
|
|
510
542
|
const onUp = (e) => {
|
|
511
|
-
|
|
543
|
+
try {
|
|
544
|
+
handle.releasePointerCapture(e.pointerId);
|
|
545
|
+
} catch {
|
|
546
|
+
}
|
|
512
547
|
handle.removeEventListener("pointermove", onMove);
|
|
513
548
|
handle.removeEventListener("pointerup", onUp);
|
|
549
|
+
handle.removeEventListener("pointercancel", onUp);
|
|
514
550
|
};
|
|
515
551
|
handle.addEventListener("pointermove", onMove);
|
|
516
552
|
handle.addEventListener("pointerup", onUp);
|
|
553
|
+
handle.addEventListener("pointercancel", onUp);
|
|
517
554
|
});
|
|
518
555
|
}
|
|
519
556
|
function openColumnPanel(btn) {
|
|
@@ -533,7 +570,8 @@ function createTable(el, opts) {
|
|
|
533
570
|
if (box.checked) colHidden.delete(col.key);
|
|
534
571
|
else colHidden.set(col.key, true);
|
|
535
572
|
invalidate();
|
|
536
|
-
|
|
573
|
+
renderBody();
|
|
574
|
+
renderHeadKeepPanel();
|
|
537
575
|
});
|
|
538
576
|
const text = document.createElement("span");
|
|
539
577
|
text.textContent = col.title;
|
|
@@ -569,7 +607,7 @@ function createTable(el, opts) {
|
|
|
569
607
|
a.href = url;
|
|
570
608
|
a.download = filename ?? "export.csv";
|
|
571
609
|
a.click();
|
|
572
|
-
URL.revokeObjectURL(url);
|
|
610
|
+
window.setTimeout(() => URL.revokeObjectURL(url), 1e3);
|
|
573
611
|
}
|
|
574
612
|
function csvEscape(s) {
|
|
575
613
|
if (/[",\r\n]/.test(s)) return `"${s.replace(/"/g, '""')}"`;
|
|
@@ -595,7 +633,8 @@ function createTable(el, opts) {
|
|
|
595
633
|
else filters.set(col.key, cur);
|
|
596
634
|
invalidate();
|
|
597
635
|
page = 1;
|
|
598
|
-
|
|
636
|
+
renderBody();
|
|
637
|
+
syncHeadState();
|
|
599
638
|
});
|
|
600
639
|
const text = document.createElement("span");
|
|
601
640
|
text.textContent = v === "" ? "\uFF08\u7A7A\uFF09" : v;
|
|
@@ -612,8 +651,11 @@ function createTable(el, opts) {
|
|
|
612
651
|
filters.delete(col.key);
|
|
613
652
|
invalidate();
|
|
614
653
|
page = 1;
|
|
615
|
-
|
|
616
|
-
|
|
654
|
+
list.querySelectorAll(".dt-check").forEach((b) => {
|
|
655
|
+
b.checked = true;
|
|
656
|
+
});
|
|
657
|
+
renderBody();
|
|
658
|
+
syncHeadState();
|
|
617
659
|
});
|
|
618
660
|
footRow.appendChild(reset);
|
|
619
661
|
panel2.append(list, footRow);
|
|
@@ -661,7 +703,7 @@ function createTable(el, opts) {
|
|
|
661
703
|
}
|
|
662
704
|
lastClickedIndex = rowIndex;
|
|
663
705
|
emitSelection();
|
|
664
|
-
|
|
706
|
+
syncHeadState();
|
|
665
707
|
renderFoot(pipeline().length);
|
|
666
708
|
});
|
|
667
709
|
cell.appendChild(box);
|
|
@@ -797,7 +839,8 @@ function createTable(el, opts) {
|
|
|
797
839
|
clear.addEventListener("click", () => {
|
|
798
840
|
selected.clear();
|
|
799
841
|
emitSelection();
|
|
800
|
-
|
|
842
|
+
renderBody();
|
|
843
|
+
syncHeadState();
|
|
801
844
|
});
|
|
802
845
|
sel.appendChild(clear);
|
|
803
846
|
foot.appendChild(sel);
|