@icen.ai/ui 0.1.1 → 0.2.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.
Files changed (45) hide show
  1. package/README.md +11 -0
  2. package/dist/base.css +72 -0
  3. package/dist/behaviors/accordion.mjs +34 -0
  4. package/dist/behaviors/carousel.mjs +50 -0
  5. package/dist/behaviors/charts.mjs +267 -0
  6. package/dist/behaviors/context-menu.mjs +223 -0
  7. package/dist/behaviors/dropdown.mjs +136 -0
  8. package/dist/behaviors/input.mjs +102 -0
  9. package/dist/behaviors/modal.mjs +107 -0
  10. package/dist/behaviors/nav.mjs +29 -0
  11. package/dist/behaviors/popover.mjs +164 -0
  12. package/dist/behaviors/select.mjs +132 -0
  13. package/dist/behaviors/sidebar.mjs +27 -0
  14. package/dist/behaviors/slider.mjs +72 -0
  15. package/dist/behaviors/tag-input.mjs +74 -0
  16. package/dist/behaviors/tree.mjs +44 -0
  17. package/dist/behaviors/upload.mjs +55 -0
  18. package/dist/components/breadcrumb.css +31 -0
  19. package/dist/components/card.css +56 -0
  20. package/dist/components/charts.css +95 -0
  21. package/dist/components/content.css +195 -0
  22. package/dist/components/empty.css +25 -0
  23. package/dist/components/feedback.css +106 -0
  24. package/dist/components/input.css +82 -0
  25. package/dist/components/layout.css +109 -0
  26. package/dist/components/media.css +133 -0
  27. package/dist/components/menu.css +85 -0
  28. package/dist/components/modal.css +107 -0
  29. package/dist/components/nav.css +59 -0
  30. package/dist/components/pagination.css +32 -0
  31. package/dist/components/popover.css +25 -0
  32. package/dist/components/segmented.css +112 -0
  33. package/dist/components/select.css +79 -0
  34. package/dist/components/sidebar.css +54 -0
  35. package/dist/components/slider.css +69 -0
  36. package/dist/components/steps.css +61 -0
  37. package/dist/components/switch.css +95 -0
  38. package/dist/components/tag-input.css +48 -0
  39. package/dist/components/toast.css +1 -1
  40. package/dist/components/tooltip.css +56 -0
  41. package/dist/components/upload.css +43 -0
  42. package/dist/index.mjs +1502 -1
  43. package/dist/registry.json +41 -3
  44. package/dist/ui.css +1899 -1
  45. 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; }