@icen.ai/ui 0.1.1 → 0.2.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/README.md +11 -0
- package/dist/base.css +72 -0
- package/dist/behaviors/accordion.mjs +34 -0
- package/dist/behaviors/carousel.mjs +50 -0
- package/dist/behaviors/charts.mjs +267 -0
- package/dist/behaviors/context-menu.mjs +223 -0
- package/dist/behaviors/dropdown.mjs +136 -0
- package/dist/behaviors/input.mjs +102 -0
- package/dist/behaviors/modal.mjs +107 -0
- package/dist/behaviors/nav.mjs +29 -0
- package/dist/behaviors/popover.mjs +164 -0
- package/dist/behaviors/select.mjs +132 -0
- package/dist/behaviors/sidebar.mjs +27 -0
- package/dist/behaviors/slider.mjs +72 -0
- package/dist/behaviors/tag-input.mjs +74 -0
- package/dist/behaviors/tree.mjs +44 -0
- package/dist/behaviors/upload.mjs +55 -0
- package/dist/components/breadcrumb.css +31 -0
- package/dist/components/card.css +56 -0
- package/dist/components/charts.css +95 -0
- package/dist/components/content.css +195 -0
- package/dist/components/empty.css +25 -0
- package/dist/components/feedback.css +106 -0
- package/dist/components/input.css +82 -0
- package/dist/components/layout.css +109 -0
- package/dist/components/media.css +133 -0
- package/dist/components/menu.css +85 -0
- package/dist/components/modal.css +107 -0
- package/dist/components/nav.css +59 -0
- package/dist/components/pagination.css +32 -0
- package/dist/components/popover.css +25 -0
- package/dist/components/segmented.css +112 -0
- package/dist/components/select.css +79 -0
- package/dist/components/sidebar.css +54 -0
- package/dist/components/slider.css +69 -0
- package/dist/components/steps.css +61 -0
- package/dist/components/switch.css +95 -0
- package/dist/components/tag-input.css +48 -0
- package/dist/components/toast.css +1 -1
- package/dist/components/tooltip.css +56 -0
- package/dist/components/upload.css +43 -0
- package/dist/index.mjs +1502 -1
- package/dist/registry.json +41 -3
- package/dist/ui.css +1899 -1
- package/package.json +1 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/behaviors/sidebar.ts
|
|
2
|
+
function setup(sidebar) {
|
|
3
|
+
const el = sidebar;
|
|
4
|
+
if (el.__icenSidebarInit) return;
|
|
5
|
+
el.__icenSidebarInit = true;
|
|
6
|
+
el.addEventListener("click", (ev) => {
|
|
7
|
+
const target = ev.target;
|
|
8
|
+
if (!(target instanceof Element)) return;
|
|
9
|
+
const title = target.closest(".sidebar-group-title");
|
|
10
|
+
if (!(title instanceof HTMLElement)) return;
|
|
11
|
+
if (title.closest(".sidebar") !== el) return;
|
|
12
|
+
const group = title.closest(".sidebar-group");
|
|
13
|
+
if (!group) return;
|
|
14
|
+
const open = group.classList.toggle("is-open");
|
|
15
|
+
title.setAttribute("aria-expanded", String(open));
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function initSidebar(root = document) {
|
|
19
|
+
if (typeof document === "undefined") return;
|
|
20
|
+
const sidebars = [];
|
|
21
|
+
if (root instanceof Element && root.matches(".sidebar")) sidebars.push(root);
|
|
22
|
+
sidebars.push(...Array.from(root.querySelectorAll(".sidebar")));
|
|
23
|
+
for (const s of sidebars) setup(s);
|
|
24
|
+
}
|
|
25
|
+
export {
|
|
26
|
+
initSidebar
|
|
27
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// src/behaviors/slider.ts
|
|
2
|
+
function pct(input) {
|
|
3
|
+
const min = Number(input.min || "0");
|
|
4
|
+
const max = Number(input.max || "100");
|
|
5
|
+
const range = max - min;
|
|
6
|
+
if (range <= 0) return 0;
|
|
7
|
+
return (Number(input.value) - min) / range * 100;
|
|
8
|
+
}
|
|
9
|
+
function setupSingle(slider, native) {
|
|
10
|
+
const fill = slider.querySelector(".slider-fill");
|
|
11
|
+
const thumb = slider.querySelector(".slider-thumb");
|
|
12
|
+
const sync = () => {
|
|
13
|
+
const p = pct(native);
|
|
14
|
+
if (fill) fill.style.width = `${p}%`;
|
|
15
|
+
if (thumb) thumb.style.left = `calc(${p}% - 8px)`;
|
|
16
|
+
};
|
|
17
|
+
native.addEventListener("input", sync);
|
|
18
|
+
sync();
|
|
19
|
+
}
|
|
20
|
+
function setupDual(slider, natives) {
|
|
21
|
+
const lo = natives.find((n) => n.getAttribute("data-thumb") === "lo") ?? natives[0];
|
|
22
|
+
const hi = natives.find((n) => n.getAttribute("data-thumb") === "hi") ?? natives[1];
|
|
23
|
+
if (!lo || !hi || lo === hi) return;
|
|
24
|
+
const range = slider.querySelector(".slider-range");
|
|
25
|
+
const thumbLo = slider.querySelector(".slider-thumb--lo");
|
|
26
|
+
const thumbHi = slider.querySelector(".slider-thumb--hi");
|
|
27
|
+
const sync = (changed) => {
|
|
28
|
+
const step = Number(lo.step || "1") || 1;
|
|
29
|
+
let vLo = Number(lo.value);
|
|
30
|
+
let vHi = Number(hi.value);
|
|
31
|
+
if (changed === lo && vLo > vHi - step) {
|
|
32
|
+
vLo = vHi - step;
|
|
33
|
+
lo.value = String(vLo);
|
|
34
|
+
}
|
|
35
|
+
if (changed === hi && vHi < vLo + step) {
|
|
36
|
+
vHi = vLo + step;
|
|
37
|
+
hi.value = String(vHi);
|
|
38
|
+
}
|
|
39
|
+
const pLo = pct(lo);
|
|
40
|
+
const pHi = pct(hi);
|
|
41
|
+
if (range) {
|
|
42
|
+
range.style.left = `${pLo}%`;
|
|
43
|
+
range.style.right = `${100 - pHi}%`;
|
|
44
|
+
}
|
|
45
|
+
if (thumbLo) thumbLo.style.left = `calc(${pLo}% - 8px)`;
|
|
46
|
+
if (thumbHi) thumbHi.style.left = `calc(${pHi}% - 8px)`;
|
|
47
|
+
lo.style.clipPath = `inset(0 ${100 - pLo}% 0 0)`;
|
|
48
|
+
hi.style.clipPath = `inset(0 0 0 ${pHi}%)`;
|
|
49
|
+
};
|
|
50
|
+
lo.addEventListener("input", () => sync(lo));
|
|
51
|
+
hi.addEventListener("input", () => sync(hi));
|
|
52
|
+
sync(null);
|
|
53
|
+
}
|
|
54
|
+
function setup(slider) {
|
|
55
|
+
const el = slider;
|
|
56
|
+
if (el.__icenSliderInit) return;
|
|
57
|
+
el.__icenSliderInit = true;
|
|
58
|
+
const natives = Array.from(slider.querySelectorAll(".slider-native"));
|
|
59
|
+
if (natives.length === 0) return;
|
|
60
|
+
if (slider.classList.contains("slider--dual")) setupDual(slider, natives);
|
|
61
|
+
else setupSingle(slider, natives[0]);
|
|
62
|
+
}
|
|
63
|
+
function initSlider(root = document) {
|
|
64
|
+
if (typeof document === "undefined") return;
|
|
65
|
+
const containers = [];
|
|
66
|
+
if (root instanceof Element && root.matches(".slider")) containers.push(root);
|
|
67
|
+
containers.push(...Array.from(root.querySelectorAll(".slider")));
|
|
68
|
+
for (const c of containers) setup(c);
|
|
69
|
+
}
|
|
70
|
+
export {
|
|
71
|
+
initSlider
|
|
72
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// src/behaviors/tag-input.ts
|
|
2
|
+
function setup(wrap) {
|
|
3
|
+
const el = wrap;
|
|
4
|
+
if (el.__icenTagInit) return;
|
|
5
|
+
el.__icenTagInit = true;
|
|
6
|
+
const fieldEl = wrap.querySelector(".tag-input-field");
|
|
7
|
+
if (!fieldEl) return;
|
|
8
|
+
const field = fieldEl;
|
|
9
|
+
const maxAttr = wrap.getAttribute("data-max");
|
|
10
|
+
const max = maxAttr ? Number(maxAttr) || Infinity : Infinity;
|
|
11
|
+
const placeholder = field.placeholder;
|
|
12
|
+
let tags = (wrap.getAttribute("data-tags") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
13
|
+
function sync() {
|
|
14
|
+
wrap.setAttribute("data-tags", tags.join(","));
|
|
15
|
+
wrap.classList.toggle("is-max", tags.length >= max);
|
|
16
|
+
field.placeholder = tags.length === 0 ? placeholder : "";
|
|
17
|
+
}
|
|
18
|
+
function remove(index) {
|
|
19
|
+
tags.splice(index, 1);
|
|
20
|
+
render();
|
|
21
|
+
}
|
|
22
|
+
function chip(tag, index) {
|
|
23
|
+
const el2 = document.createElement("span");
|
|
24
|
+
el2.className = "tag-chip";
|
|
25
|
+
const label = document.createElement("span");
|
|
26
|
+
label.textContent = tag;
|
|
27
|
+
const x = document.createElement("button");
|
|
28
|
+
x.type = "button";
|
|
29
|
+
x.className = "tag-chip-x focus-ring";
|
|
30
|
+
x.textContent = "\xD7";
|
|
31
|
+
x.setAttribute("aria-label", `\u79FB\u9664 ${tag}`);
|
|
32
|
+
x.addEventListener("mousedown", (ev) => ev.preventDefault());
|
|
33
|
+
x.addEventListener("click", () => remove(index));
|
|
34
|
+
el2.append(label, x);
|
|
35
|
+
return el2;
|
|
36
|
+
}
|
|
37
|
+
function render() {
|
|
38
|
+
wrap.querySelectorAll(".tag-chip").forEach((c) => c.remove());
|
|
39
|
+
tags.forEach((t, i) => wrap.insertBefore(chip(t, i), field));
|
|
40
|
+
sync();
|
|
41
|
+
}
|
|
42
|
+
function commit() {
|
|
43
|
+
const v = field.value.trim();
|
|
44
|
+
if (!v) return;
|
|
45
|
+
if (tags.length >= max) return;
|
|
46
|
+
if (tags.includes(v)) {
|
|
47
|
+
field.value = "";
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
tags.push(v);
|
|
51
|
+
field.value = "";
|
|
52
|
+
render();
|
|
53
|
+
}
|
|
54
|
+
field.addEventListener("keydown", (ev) => {
|
|
55
|
+
if (ev.key === "Enter" || ev.key === ",") {
|
|
56
|
+
ev.preventDefault();
|
|
57
|
+
commit();
|
|
58
|
+
} else if (ev.key === "Backspace" && !field.value && tags.length > 0) {
|
|
59
|
+
remove(tags.length - 1);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
field.addEventListener("blur", commit);
|
|
63
|
+
render();
|
|
64
|
+
}
|
|
65
|
+
function initTagInput(root = document) {
|
|
66
|
+
if (typeof document === "undefined") return;
|
|
67
|
+
const containers = [];
|
|
68
|
+
if (root instanceof HTMLElement && root.matches(".tag-input[data-tags]")) containers.push(root);
|
|
69
|
+
containers.push(...Array.from(root.querySelectorAll(".tag-input[data-tags]")));
|
|
70
|
+
for (const c of containers) setup(c);
|
|
71
|
+
}
|
|
72
|
+
export {
|
|
73
|
+
initTagInput
|
|
74
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// src/behaviors/tree.ts
|
|
2
|
+
function setup(tree) {
|
|
3
|
+
const el = tree;
|
|
4
|
+
if (el.__icenTreeInit) return;
|
|
5
|
+
el.__icenTreeInit = true;
|
|
6
|
+
tree.addEventListener("click", (ev) => {
|
|
7
|
+
const target = ev.target;
|
|
8
|
+
if (!(target instanceof Element)) return;
|
|
9
|
+
const node = target.closest(".tree-node");
|
|
10
|
+
if (!node || node.closest(".tree") !== tree) return;
|
|
11
|
+
if (node.classList.contains("is-disabled")) return;
|
|
12
|
+
const li = node.closest("li");
|
|
13
|
+
if (!li) return;
|
|
14
|
+
const childUl = li.querySelector(":scope > ul");
|
|
15
|
+
if (target.closest(".tree-toggle")) {
|
|
16
|
+
if (!childUl) return;
|
|
17
|
+
const toggle = node.querySelector(".tree-toggle");
|
|
18
|
+
const collapsed = childUl.hasAttribute("hidden");
|
|
19
|
+
if (collapsed) {
|
|
20
|
+
childUl.removeAttribute("hidden");
|
|
21
|
+
toggle?.classList.remove("is-collapsed");
|
|
22
|
+
toggle?.setAttribute("aria-expanded", "true");
|
|
23
|
+
} else {
|
|
24
|
+
childUl.setAttribute("hidden", "");
|
|
25
|
+
toggle?.classList.add("is-collapsed");
|
|
26
|
+
toggle?.setAttribute("aria-expanded", "false");
|
|
27
|
+
}
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (childUl) return;
|
|
31
|
+
tree.querySelectorAll(".tree-node.is-selected").forEach((n) => n.classList.remove("is-selected"));
|
|
32
|
+
node.classList.add("is-selected");
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function initTree(root = document) {
|
|
36
|
+
if (typeof document === "undefined") return;
|
|
37
|
+
const trees = [];
|
|
38
|
+
if (root instanceof Element && root.matches(".tree")) trees.push(root);
|
|
39
|
+
trees.push(...Array.from(root.querySelectorAll(".tree")));
|
|
40
|
+
for (const t of trees) setup(t);
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
initTree
|
|
44
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// src/behaviors/upload.ts
|
|
2
|
+
function setup(zone) {
|
|
3
|
+
const el = zone;
|
|
4
|
+
if (el.__icenUploadInit) return;
|
|
5
|
+
el.__icenUploadInit = true;
|
|
6
|
+
const input = zone.querySelector('input[type="file"]');
|
|
7
|
+
if (!input) return;
|
|
8
|
+
const openPicker = () => input.click();
|
|
9
|
+
zone.addEventListener("click", (ev) => {
|
|
10
|
+
if (ev.target === input) return;
|
|
11
|
+
openPicker();
|
|
12
|
+
});
|
|
13
|
+
zone.addEventListener("keydown", (ev) => {
|
|
14
|
+
if (ev.key === "Enter" || ev.key === " ") {
|
|
15
|
+
ev.preventDefault();
|
|
16
|
+
openPicker();
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
zone.addEventListener("dragover", (ev) => {
|
|
20
|
+
ev.preventDefault();
|
|
21
|
+
zone.classList.add("is-drag");
|
|
22
|
+
});
|
|
23
|
+
zone.addEventListener("dragleave", () => zone.classList.remove("is-drag"));
|
|
24
|
+
zone.addEventListener("drop", (ev) => {
|
|
25
|
+
ev.preventDefault();
|
|
26
|
+
zone.classList.remove("is-drag");
|
|
27
|
+
const files = ev.dataTransfer?.files;
|
|
28
|
+
if (!files || files.length === 0) return;
|
|
29
|
+
let assigned = false;
|
|
30
|
+
try {
|
|
31
|
+
const dt = new DataTransfer();
|
|
32
|
+
for (const f of Array.from(files)) dt.items.add(f);
|
|
33
|
+
input.files = dt.files;
|
|
34
|
+
assigned = true;
|
|
35
|
+
} catch {
|
|
36
|
+
assigned = false;
|
|
37
|
+
}
|
|
38
|
+
if (assigned) {
|
|
39
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
40
|
+
} else {
|
|
41
|
+
const list = Array.from(files);
|
|
42
|
+
zone.dispatchEvent(new CustomEvent("icn:upload", { bubbles: true, detail: { files: list } }));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function initUpload(root = document) {
|
|
47
|
+
if (typeof document === "undefined") return;
|
|
48
|
+
const zones = [];
|
|
49
|
+
if (root instanceof HTMLElement && root.matches(".upload")) zones.push(root);
|
|
50
|
+
zones.push(...Array.from(root.querySelectorAll(".upload")));
|
|
51
|
+
for (const z of zones) setup(z);
|
|
52
|
+
}
|
|
53
|
+
export {
|
|
54
|
+
initUpload
|
|
55
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @icen.ai/ui — Component: breadcrumb(面包屑)
|
|
3
|
+
* 用法:<nav class="breadcrumb" aria-label="面包屑"><ol>
|
|
4
|
+
* <li><a class="breadcrumb-link" href="…">首页</a><span class="breadcrumb-sep" aria-hidden="true"></span></li>
|
|
5
|
+
* <li><span class="breadcrumb-current" aria-current="page">当前页</span></li>
|
|
6
|
+
* </ol></nav>
|
|
7
|
+
* 分隔符是 .breadcrumb-sep 的 ::after(›);超长折叠用 .breadcrumb-ellipsis。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
.breadcrumb { display: flex; align-items: center; font-size: 13px; }
|
|
11
|
+
.breadcrumb ol { display: flex; align-items: center; gap: 6px; list-style: none; margin: 0; padding: 0; }
|
|
12
|
+
.breadcrumb li { display: flex; align-items: center; gap: 6px; }
|
|
13
|
+
|
|
14
|
+
.breadcrumb-link {
|
|
15
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
16
|
+
color: var(--token-text-muted); text-decoration: none;
|
|
17
|
+
transition: color var(--duration-fast) var(--ease-out);
|
|
18
|
+
}
|
|
19
|
+
.breadcrumb-link:hover { color: var(--token-accent); text-decoration: none; }
|
|
20
|
+
.breadcrumb-link > svg { width: 13px; height: 13px; flex-shrink: 0; }
|
|
21
|
+
|
|
22
|
+
.breadcrumb-sep { color: var(--token-text-faint); font-size: 13px; line-height: 1; }
|
|
23
|
+
.breadcrumb-sep::after { content: '›'; }
|
|
24
|
+
|
|
25
|
+
.breadcrumb-current {
|
|
26
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
27
|
+
color: var(--token-text); font-weight: 500;
|
|
28
|
+
}
|
|
29
|
+
.breadcrumb-current > svg { width: 13px; height: 13px; flex-shrink: 0; }
|
|
30
|
+
|
|
31
|
+
.breadcrumb-ellipsis { padding: 0 4px; color: var(--token-text-faint); }
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @icen.ai/ui — Component: card(组合卡片容器)
|
|
3
|
+
* 包含:.card 四变体(默认 / elevated / outlined / glass)+ .card--interactive + .is-selected
|
|
4
|
+
* 子结构:.card-head / .card-title / .card-desc / .card-body / .card-foot / .card-meta / .card-action
|
|
5
|
+
* 规格源自 opengal src/presentation/ui/data/card.tsx。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
.card {
|
|
9
|
+
background: var(--token-card);
|
|
10
|
+
border: 1px solid var(--token-outline-soft);
|
|
11
|
+
box-shadow: var(--token-shadow-inset);
|
|
12
|
+
border-radius: var(--radius-lg);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* ── 变体 ── */
|
|
16
|
+
.card--elevated { box-shadow: var(--token-shadow-card); }
|
|
17
|
+
.card--outlined { border-color: var(--token-outline); box-shadow: none; }
|
|
18
|
+
.card--glass {
|
|
19
|
+
background: var(--token-surface-glass);
|
|
20
|
+
backdrop-filter: blur(12px);
|
|
21
|
+
-webkit-backdrop-filter: blur(12px);
|
|
22
|
+
box-shadow: none;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* ── 修饰 ── */
|
|
26
|
+
/* 可交互:cursor + .lift 基元同款效果(纯 CSS 无法组合类,规则在此复制一份) */
|
|
27
|
+
.card--interactive {
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
transition: border-color var(--duration-slow) var(--ease-out),
|
|
30
|
+
box-shadow var(--duration-slow) var(--ease-out),
|
|
31
|
+
transform var(--duration-slow) var(--ease-out);
|
|
32
|
+
}
|
|
33
|
+
.card--interactive:hover {
|
|
34
|
+
transform: translateY(-1px);
|
|
35
|
+
border-color: color-mix(in srgb, var(--token-accent) 26%, var(--token-outline-soft));
|
|
36
|
+
box-shadow: var(--token-shadow-card);
|
|
37
|
+
}
|
|
38
|
+
.card.is-selected {
|
|
39
|
+
box-shadow: 0 0 0 2px var(--token-accent), 0 0 0 4px var(--token-bg);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* ── 子结构 ── */
|
|
43
|
+
.card-head {
|
|
44
|
+
display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
|
|
45
|
+
padding: 16px;
|
|
46
|
+
}
|
|
47
|
+
.card-title { font-size: 15px; font-weight: 600; color: var(--token-text); line-height: 1.25; margin: 0; }
|
|
48
|
+
.card-desc { font-size: 12px; color: var(--token-text-muted); margin: 2px 0 0; }
|
|
49
|
+
.card-body { padding: 0 16px 16px; font-size: 13px; color: var(--token-text-muted); }
|
|
50
|
+
.card-foot {
|
|
51
|
+
display: flex; align-items: center; gap: 8px;
|
|
52
|
+
padding: 12px 16px;
|
|
53
|
+
border-top: 1px solid var(--token-outline-soft);
|
|
54
|
+
}
|
|
55
|
+
.card-meta { display: flex; align-items: center; gap: 8px; font-size: 11px; color: var(--token-text-faint); }
|
|
56
|
+
.card-action { margin-left: auto; }
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @icen.ai/ui — Component: charts(轻量图表,与 behaviors/charts 配套)
|
|
3
|
+
* 包含:.chart 容器 / .chart-empty / .chart-legend(图例行)/ .chart-labels(轴标签行)/
|
|
4
|
+
* vbar(垂直柱状)/ hbar(水平条形)/ stack(堆叠条)/ donut(环形)/ line(折线)
|
|
5
|
+
* DOM 全部由 behaviors/charts.ts 以 createElement(NS) + textContent 构建;
|
|
6
|
+
* 单色调通过容器或条上的 --chart-tone 自定义属性注入(默认 var(--token-accent))。
|
|
7
|
+
* 规格源自 opengal src/presentation/ui/data/charts.tsx。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
.chart { width: 100%; }
|
|
11
|
+
.chart-empty {
|
|
12
|
+
height: 150px;
|
|
13
|
+
display: flex; align-items: center; justify-content: center;
|
|
14
|
+
font-size: 11px; color: var(--token-text-faint);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* ── 图例 / 轴标签(donut / stack / vbar / line 共用) ── */
|
|
18
|
+
.chart-legend { display: flex; flex-direction: column; gap: 6px; min-width: 0; flex: 1; }
|
|
19
|
+
.chart-legend-item { display: flex; align-items: center; gap: 8px; font-size: 11px; min-width: 0; }
|
|
20
|
+
.chart-legend-dot {
|
|
21
|
+
width: 8px; height: 8px; flex-shrink: 0;
|
|
22
|
+
border-radius: var(--radius-full);
|
|
23
|
+
background: var(--chart-tone, var(--token-accent));
|
|
24
|
+
}
|
|
25
|
+
.chart-legend-label {
|
|
26
|
+
flex: 1; min-width: 0;
|
|
27
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
28
|
+
color: var(--token-text-muted);
|
|
29
|
+
}
|
|
30
|
+
.chart-legend-value { flex-shrink: 0; font-variant-numeric: tabular-nums; color: var(--token-text-faint); }
|
|
31
|
+
|
|
32
|
+
.chart-labels { display: flex; gap: 8px; margin-top: 8px; }
|
|
33
|
+
.chart-labels > span {
|
|
34
|
+
flex: 1; min-width: 0;
|
|
35
|
+
text-align: center; font-size: 9px; color: var(--token-text-faint);
|
|
36
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* ── vbar · 垂直柱状 ── */
|
|
40
|
+
.chart-vbar { display: flex; align-items: flex-end; gap: 8px; height: 112px; }
|
|
41
|
+
.chart-vbar-track {
|
|
42
|
+
flex: 1; height: 100%; min-width: 0;
|
|
43
|
+
display: flex; align-items: flex-end;
|
|
44
|
+
background: color-mix(in srgb, var(--token-fill-2) 45%, transparent);
|
|
45
|
+
border-radius: var(--radius-sm);
|
|
46
|
+
}
|
|
47
|
+
.chart-vbar-bar {
|
|
48
|
+
width: 100%;
|
|
49
|
+
border-radius: var(--radius-sm);
|
|
50
|
+
background: var(--chart-tone, var(--token-accent));
|
|
51
|
+
transition: height 500ms var(--ease-out);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* ── hbar · 水平条形 ── */
|
|
55
|
+
.chart-hbar { display: flex; flex-direction: column; gap: 8px; }
|
|
56
|
+
.chart-hbar-row {
|
|
57
|
+
display: grid;
|
|
58
|
+
grid-template-columns: minmax(72px, 1fr) minmax(90px, 2fr) 52px;
|
|
59
|
+
gap: 8px; align-items: center;
|
|
60
|
+
font-size: 11px;
|
|
61
|
+
}
|
|
62
|
+
.chart-hbar-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--token-text-muted); }
|
|
63
|
+
.chart-hbar-track {
|
|
64
|
+
height: 10px; overflow: hidden;
|
|
65
|
+
border-radius: var(--radius-full);
|
|
66
|
+
background: var(--token-fill-2);
|
|
67
|
+
}
|
|
68
|
+
.chart-hbar-bar {
|
|
69
|
+
height: 100%;
|
|
70
|
+
border-radius: var(--radius-full);
|
|
71
|
+
background: var(--chart-tone, var(--token-accent));
|
|
72
|
+
transition: width 500ms var(--ease-out);
|
|
73
|
+
}
|
|
74
|
+
.chart-hbar-value { text-align: right; font-variant-numeric: tabular-nums; color: var(--token-text-faint); }
|
|
75
|
+
|
|
76
|
+
/* ── stack · 堆叠条 ── */
|
|
77
|
+
.chart-stack {
|
|
78
|
+
height: 16px;
|
|
79
|
+
display: flex; overflow: hidden;
|
|
80
|
+
border-radius: var(--radius-full);
|
|
81
|
+
background: var(--token-fill-2);
|
|
82
|
+
margin-bottom: 12px;
|
|
83
|
+
}
|
|
84
|
+
.chart-stack-seg {
|
|
85
|
+
height: 100%; min-width: 3px;
|
|
86
|
+
background: var(--chart-tone, var(--token-accent));
|
|
87
|
+
transition: width 500ms var(--ease-out);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* ── donut · 环形 ── */
|
|
91
|
+
.chart-donut { display: flex; align-items: center; gap: 12px; min-height: 150px; }
|
|
92
|
+
.chart-donut > svg { width: 132px; height: 132px; flex-shrink: 0; }
|
|
93
|
+
|
|
94
|
+
/* ── line · 折线 ── */
|
|
95
|
+
.chart-line > svg { display: block; width: 100%; height: 150px; overflow: visible; }
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @icen.ai/ui — Component: content(内容展示族)
|
|
3
|
+
* 包含:kbd(按键)/ code-inline(行内代码)/ list(分隔列表)/ accordion(手风琴,behaviors/accordion 配套)/
|
|
4
|
+
* timeline(时间线)/ desc(描述列表)/ tree(树形,behaviors/tree 配套)
|
|
5
|
+
* 规格源自 opengal src/presentation/ui/data/exhibit.tsx 与 data/display.tsx。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/* ══════════════ kbd · 按键 ══════════════ */
|
|
9
|
+
.kbd {
|
|
10
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
11
|
+
vertical-align: middle;
|
|
12
|
+
font-family: var(--font-mono); font-weight: 500;
|
|
13
|
+
border-radius: var(--radius-xs);
|
|
14
|
+
border: 1px solid var(--token-outline);
|
|
15
|
+
background: var(--token-control);
|
|
16
|
+
color: var(--token-text-muted);
|
|
17
|
+
box-shadow: var(--token-shadow-inset);
|
|
18
|
+
height: 20px; min-width: 20px;
|
|
19
|
+
padding: 0 6px;
|
|
20
|
+
font-size: 11px;
|
|
21
|
+
}
|
|
22
|
+
.kbd--sm { height: 16px; min-width: 16px; padding: 0 4px; font-size: 10px; }
|
|
23
|
+
|
|
24
|
+
/* ══════════════ code-inline · 行内代码 ══════════════ */
|
|
25
|
+
.code-inline {
|
|
26
|
+
padding: 2px 6px;
|
|
27
|
+
border-radius: var(--radius-xs);
|
|
28
|
+
background: var(--token-control);
|
|
29
|
+
border: 1px solid var(--token-outline-soft);
|
|
30
|
+
font-size: 12px;
|
|
31
|
+
font-family: var(--font-mono);
|
|
32
|
+
color: var(--token-accent);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* ══════════════ list · 分隔列表 ══════════════ */
|
|
36
|
+
.list { list-style: none; margin: 0; padding: 0; }
|
|
37
|
+
.list-item {
|
|
38
|
+
padding: 8px;
|
|
39
|
+
border-radius: var(--radius-md);
|
|
40
|
+
transition: background-color var(--duration-normal) var(--ease-out);
|
|
41
|
+
}
|
|
42
|
+
.list-item:hover { background: var(--token-control-hover); }
|
|
43
|
+
.list .list-item + .list-item { border-top: 1px solid var(--token-outline-soft); }
|
|
44
|
+
|
|
45
|
+
/* ══════════════ accordion · 手风琴(behaviors/accordion 配套) ══════════════
|
|
46
|
+
* DOM 契约:
|
|
47
|
+
* <div class="accordion" data-single?>
|
|
48
|
+
* <div class="accordion-item">
|
|
49
|
+
* <button class="accordion-trigger">标题 <svg class="accordion-chevron"/></button>
|
|
50
|
+
* <div class="accordion-panel"><div class="accordion-panel-inner">内容</div></div>
|
|
51
|
+
* </div>
|
|
52
|
+
* </div>
|
|
53
|
+
* 展开 = item 挂 .is-open;panel 用 grid-template-rows 0fr→1fr 做过渡。
|
|
54
|
+
*/
|
|
55
|
+
.accordion-item { border-bottom: 1px solid var(--token-outline-soft); }
|
|
56
|
+
.accordion-item:last-child { border-bottom: 0; }
|
|
57
|
+
.accordion-trigger {
|
|
58
|
+
width: 100%;
|
|
59
|
+
display: flex; align-items: center; justify-content: space-between; gap: 8px;
|
|
60
|
+
padding: 12px 8px;
|
|
61
|
+
font-size: 14px; font-weight: 500;
|
|
62
|
+
color: var(--token-text); text-align: left;
|
|
63
|
+
background: none; border: 0; cursor: pointer;
|
|
64
|
+
border-radius: var(--radius-md);
|
|
65
|
+
transition: background-color var(--duration-normal) var(--ease-out);
|
|
66
|
+
}
|
|
67
|
+
.accordion-trigger:hover { background: var(--token-control-hover); }
|
|
68
|
+
.accordion-chevron {
|
|
69
|
+
width: 14px; height: 14px; flex-shrink: 0;
|
|
70
|
+
color: var(--token-text-faint);
|
|
71
|
+
transition: transform var(--duration-normal) var(--ease-out);
|
|
72
|
+
}
|
|
73
|
+
.accordion-item.is-open .accordion-chevron { transform: rotate(180deg); }
|
|
74
|
+
.accordion-panel {
|
|
75
|
+
display: grid;
|
|
76
|
+
grid-template-rows: 0fr;
|
|
77
|
+
transition: grid-template-rows var(--duration-normal) var(--ease-out);
|
|
78
|
+
}
|
|
79
|
+
.accordion-item.is-open .accordion-panel { grid-template-rows: 1fr; }
|
|
80
|
+
.accordion-panel-inner {
|
|
81
|
+
overflow: hidden;
|
|
82
|
+
padding: 0 8px 12px;
|
|
83
|
+
font-size: 13px;
|
|
84
|
+
color: var(--token-text-muted);
|
|
85
|
+
line-height: 1.6;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* ══════════════ timeline · 时间线 ══════════════
|
|
89
|
+
* DOM 契约:.timeline-item > .timeline-dot + .timeline-body
|
|
90
|
+
* 连接线 = item 的 ::after(相对 item 绝对定位,从 dot 下方贯穿到下一项),末项不画。
|
|
91
|
+
*/
|
|
92
|
+
.timeline { display: flex; flex-direction: column; }
|
|
93
|
+
.timeline-item {
|
|
94
|
+
position: relative;
|
|
95
|
+
display: flex; gap: 12px;
|
|
96
|
+
padding-bottom: 24px;
|
|
97
|
+
}
|
|
98
|
+
.timeline-item:last-child { padding-bottom: 0; }
|
|
99
|
+
.timeline-item:not(:last-child)::after {
|
|
100
|
+
content: '';
|
|
101
|
+
position: absolute;
|
|
102
|
+
left: 13.5px; /* 28px dot 的水平中心 */
|
|
103
|
+
top: 32px; /* 28px dot + 4px 间距 */
|
|
104
|
+
bottom: 0;
|
|
105
|
+
width: 1px;
|
|
106
|
+
background: var(--token-outline-soft);
|
|
107
|
+
}
|
|
108
|
+
.timeline-dot {
|
|
109
|
+
width: 28px; height: 28px; flex-shrink: 0;
|
|
110
|
+
border-radius: var(--radius-full);
|
|
111
|
+
display: flex; align-items: center; justify-content: center;
|
|
112
|
+
background: var(--token-control);
|
|
113
|
+
border: 1px solid var(--token-outline-soft);
|
|
114
|
+
color: var(--token-text-muted);
|
|
115
|
+
z-index: 1;
|
|
116
|
+
}
|
|
117
|
+
.timeline-dot svg { width: 13px; height: 13px; }
|
|
118
|
+
.timeline-dot--accent { background: var(--token-accent); color: var(--token-on-accent); border-color: transparent; }
|
|
119
|
+
.timeline-dot--success { background: var(--token-success); color: var(--token-on-accent); border-color: transparent; }
|
|
120
|
+
.timeline-dot--warning { background: var(--token-warning); color: var(--token-on-accent); border-color: transparent; }
|
|
121
|
+
.timeline-dot--error { background: var(--token-error); color: var(--token-on-accent); border-color: transparent; }
|
|
122
|
+
.timeline-dot--info { background: var(--token-info); color: var(--token-on-accent); border-color: transparent; }
|
|
123
|
+
.timeline-body { flex: 1; min-width: 0; }
|
|
124
|
+
.timeline-head { display: flex; align-items: center; gap: 8px; }
|
|
125
|
+
.timeline-title { font-size: 13px; font-weight: 500; color: var(--token-text); }
|
|
126
|
+
.timeline-time { font-size: 11px; color: var(--token-text-faint); margin-left: auto; font-variant-numeric: tabular-nums; }
|
|
127
|
+
.timeline-desc { font-size: 12px; color: var(--token-text-muted); margin: 4px 0 0; }
|
|
128
|
+
|
|
129
|
+
/* ══════════════ desc · 描述列表 ══════════════
|
|
130
|
+
* 列数由容器 style="--desc-cols:N" 指定,默认 2;item 可用 style="--desc-span:N" 跨列。
|
|
131
|
+
* --bordered 变体:gap:1px 露出容器底色形成格线。
|
|
132
|
+
*/
|
|
133
|
+
.desc { display: flex; flex-direction: column; gap: 8px; }
|
|
134
|
+
.desc-title { font-size: 13px; font-weight: 500; color: var(--token-text); margin: 0; }
|
|
135
|
+
.desc-grid {
|
|
136
|
+
display: grid;
|
|
137
|
+
grid-template-columns: repeat(var(--desc-cols, 2), minmax(0, 1fr));
|
|
138
|
+
gap: 1px;
|
|
139
|
+
margin: 0;
|
|
140
|
+
}
|
|
141
|
+
.desc--bordered .desc-grid {
|
|
142
|
+
background: var(--token-outline-soft);
|
|
143
|
+
border: 1px solid var(--token-outline-soft);
|
|
144
|
+
border-radius: var(--radius-lg);
|
|
145
|
+
overflow: hidden;
|
|
146
|
+
}
|
|
147
|
+
.desc-item { grid-column: span var(--desc-span, 1); }
|
|
148
|
+
.desc--bordered .desc-item { background: var(--token-card); padding: 8px 12px; }
|
|
149
|
+
.desc:not(.desc--bordered) .desc-item { display: flex; flex-direction: column; gap: 2px; }
|
|
150
|
+
.desc-item dt { font-size: 11px; color: var(--token-text-faint); }
|
|
151
|
+
.desc-item dd { font-size: 13px; color: var(--token-text); margin: 0; }
|
|
152
|
+
|
|
153
|
+
/* ══════════════ tree · 树形(behaviors/tree 配套) ══════════════
|
|
154
|
+
* DOM 契约:
|
|
155
|
+
* <ul class="tree"><li>
|
|
156
|
+
* <div class="tree-node" style="--depth:0">
|
|
157
|
+
* <button class="tree-toggle"><svg/></button> <!-- 叶子用空 span.tree-toggle 占 18px -->
|
|
158
|
+
* <span class="tree-label">节点</span>
|
|
159
|
+
* </div>
|
|
160
|
+
* <ul>…子节点…</ul> <!-- 可选;behavior 切换 hidden -->
|
|
161
|
+
* </li></ul>
|
|
162
|
+
* 折叠 = toggle 挂 .is-collapsed(chevron 转 -90°)+ 子 ul hidden;选中 = node 挂 .is-selected。
|
|
163
|
+
*/
|
|
164
|
+
.tree, .tree ul { list-style: none; margin: 0; padding: 0; }
|
|
165
|
+
.tree { user-select: none; }
|
|
166
|
+
.tree-node {
|
|
167
|
+
display: flex; align-items: center; gap: 6px;
|
|
168
|
+
padding: 6px 8px 6px calc(4px + var(--depth, 0) * 14px);
|
|
169
|
+
border-radius: var(--radius-md);
|
|
170
|
+
font-size: 13px;
|
|
171
|
+
color: var(--token-text);
|
|
172
|
+
cursor: pointer;
|
|
173
|
+
transition: background-color var(--duration-normal) var(--ease-out);
|
|
174
|
+
}
|
|
175
|
+
.tree-node:hover { background: var(--token-control-hover); }
|
|
176
|
+
.tree-node.is-selected {
|
|
177
|
+
background: var(--token-accent-subtle);
|
|
178
|
+
color: var(--token-accent);
|
|
179
|
+
font-weight: 500;
|
|
180
|
+
}
|
|
181
|
+
.tree-node.is-disabled { opacity: 0.4; pointer-events: none; }
|
|
182
|
+
.tree-toggle {
|
|
183
|
+
width: 18px; height: 18px; flex-shrink: 0;
|
|
184
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
185
|
+
padding: 0; border: 0; background: none;
|
|
186
|
+
color: var(--token-text-muted);
|
|
187
|
+
cursor: pointer;
|
|
188
|
+
}
|
|
189
|
+
.tree-toggle:hover { color: var(--token-text); }
|
|
190
|
+
.tree-toggle svg {
|
|
191
|
+
width: 13px; height: 13px;
|
|
192
|
+
transition: transform var(--duration-normal) var(--ease-out);
|
|
193
|
+
}
|
|
194
|
+
.tree-toggle.is-collapsed svg { transform: rotate(-90deg); }
|
|
195
|
+
.tree-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @icen.ai/ui — Component: empty(空状态)
|
|
3
|
+
* 包含:.empty 容器 + .empty-icon / .empty-title / .empty-desc / .empty-action
|
|
4
|
+
* 规格源自 opengal src/presentation/ui/data/display.tsx 的 EmptyState。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
.empty {
|
|
8
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
9
|
+
text-align: center;
|
|
10
|
+
padding: 48px 16px;
|
|
11
|
+
}
|
|
12
|
+
.empty-icon {
|
|
13
|
+
width: 48px; height: 48px;
|
|
14
|
+
border-radius: var(--radius-lg);
|
|
15
|
+
background: var(--token-card);
|
|
16
|
+
border: 1px solid var(--token-outline-soft);
|
|
17
|
+
box-shadow: var(--token-shadow-inset);
|
|
18
|
+
display: flex; align-items: center; justify-content: center;
|
|
19
|
+
color: var(--token-text-faint);
|
|
20
|
+
margin-bottom: 16px;
|
|
21
|
+
}
|
|
22
|
+
.empty-icon svg { width: 24px; height: 24px; }
|
|
23
|
+
.empty-title { font-size: 14px; font-weight: 500; color: var(--token-text); margin: 0; }
|
|
24
|
+
.empty-desc { font-size: 12px; color: var(--token-text-muted); margin: 4px 0 0; max-width: 240px; }
|
|
25
|
+
.empty-action { margin-top: 16px; }
|