@icen.ai/ui 0.5.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +262 -0
- package/README.md +52 -13
- package/dist/base.css +33 -5
- package/dist/behaviors/accordion.d.mts +7 -0
- package/dist/behaviors/accordion.mjs +37 -11
- package/dist/behaviors/ai-chat.d.mts +59 -0
- package/dist/behaviors/ai-chat.mjs +580 -0
- package/dist/behaviors/ai-composer.d.mts +137 -0
- package/dist/behaviors/ai-composer.mjs +2114 -0
- package/dist/behaviors/ai-core.d.mts +156 -0
- package/dist/behaviors/ai-core.mjs +504 -0
- package/dist/behaviors/ai-diff.d.mts +53 -0
- package/dist/behaviors/ai-diff.mjs +413 -0
- package/dist/behaviors/ai-panel.d.mts +101 -0
- package/dist/behaviors/ai-panel.mjs +1087 -0
- package/dist/behaviors/ai-provider.d.mts +198 -0
- package/dist/behaviors/ai-provider.mjs +1061 -0
- package/dist/behaviors/ai-tool.d.mts +26 -0
- package/dist/behaviors/ai-tool.mjs +2173 -0
- package/dist/behaviors/ai-tools.d.mts +84 -0
- package/dist/behaviors/ai-tools.mjs +1762 -0
- package/dist/behaviors/back-top.d.mts +20 -0
- package/dist/behaviors/back-top.mjs +64 -32
- package/dist/behaviors/carousel.d.mts +7 -0
- package/dist/behaviors/carousel.mjs +72 -27
- package/dist/behaviors/charts.d.mts +296 -0
- package/dist/behaviors/charts.mjs +1364 -440
- package/dist/behaviors/command-palette.d.mts +42 -0
- package/dist/behaviors/command-palette.mjs +167 -27
- package/dist/behaviors/context-menu.d.mts +53 -0
- package/dist/behaviors/context-menu.mjs +127 -39
- package/dist/behaviors/controls.d.mts +59 -0
- package/dist/behaviors/controls.mjs +601 -0
- package/dist/behaviors/copy.d.mts +7 -0
- package/dist/behaviors/copy.mjs +41 -15
- package/dist/behaviors/datatable.d.mts +90 -0
- package/dist/behaviors/datatable.mjs +279 -53
- package/dist/behaviors/date-picker.d.mts +4 -0
- package/dist/behaviors/date-picker.mjs +276 -69
- package/dist/behaviors/dropdown.d.mts +8 -0
- package/dist/behaviors/dropdown.mjs +171 -28
- package/dist/behaviors/events.d.mts +330 -0
- package/dist/behaviors/events.mjs +208 -0
- package/dist/behaviors/input.d.mts +4 -0
- package/dist/behaviors/input.mjs +52 -32
- package/dist/behaviors/modal.d.mts +30 -0
- package/dist/behaviors/modal.mjs +87 -16
- package/dist/behaviors/nav.d.mts +7 -0
- package/dist/behaviors/nav.mjs +57 -15
- package/dist/behaviors/notification.d.mts +188 -0
- package/dist/behaviors/notification.mjs +274 -140
- package/dist/behaviors/popover.d.mts +82 -0
- package/dist/behaviors/popover.mjs +92 -5
- package/dist/behaviors/select.d.mts +6 -0
- package/dist/behaviors/select.mjs +287 -45
- package/dist/behaviors/sidebar.d.mts +7 -0
- package/dist/behaviors/sidebar.mjs +44 -13
- package/dist/behaviors/slider.d.mts +7 -0
- package/dist/behaviors/slider.mjs +55 -43
- package/dist/behaviors/split-pane.d.mts +8 -0
- package/dist/behaviors/split-pane.mjs +70 -29
- package/dist/behaviors/tabs.d.mts +9 -0
- package/dist/behaviors/tabs.mjs +102 -26
- package/dist/behaviors/tag-input.d.mts +7 -0
- package/dist/behaviors/tag-input.mjs +66 -17
- package/dist/behaviors/theme.d.mts +36 -0
- package/dist/behaviors/theme.mjs +51 -5
- package/dist/behaviors/toast.d.mts +48 -0
- package/dist/behaviors/toast.mjs +98 -25
- package/dist/behaviors/tree.d.mts +7 -0
- package/dist/behaviors/tree.mjs +39 -15
- package/dist/behaviors/upload.d.mts +4 -0
- package/dist/behaviors/upload.mjs +79 -44
- package/dist/cli.mjs +1 -1
- 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/badge.css +5 -9
- package/dist/components/breadcrumb.css +0 -36
- package/dist/components/btn.css +4 -7
- package/dist/components/card.css +3 -6
- package/dist/components/chart-bar.css +10 -0
- package/dist/components/chart-calendar.mjs +4 -0
- package/dist/components/chart-gauge.css +1 -1
- package/dist/components/chart-heatmap.css +40 -0
- package/dist/components/chart-pie.css +0 -6
- package/dist/components/chart-scatter.css +24 -0
- package/dist/components/chart-scatter.mjs +4 -0
- package/dist/components/charts.css +87 -2
- package/dist/components/charts.mjs +1 -0
- package/dist/components/command-palette.css +2 -2
- package/dist/components/content.css +11 -25
- package/dist/components/copy.css +8 -8
- package/dist/components/datatable.css +5 -9
- package/dist/components/date-picker.css +4 -8
- package/dist/components/empty.css +0 -4
- package/dist/components/feedback.css +10 -17
- package/dist/components/form.css +13 -14
- package/dist/components/input.css +2 -2
- package/dist/components/layout.css +36 -36
- package/dist/components/media.css +12 -8
- package/dist/components/menu.css +2 -2
- package/dist/components/modal.css +1 -1
- package/dist/components/nav.css +11 -6
- package/dist/components/notification.css +27 -28
- package/dist/components/pagination.css +4 -2
- package/dist/components/pagination.mjs +1 -0
- package/dist/components/panel.css +16 -16
- package/dist/components/pill.css +15 -15
- package/dist/components/rating.mjs +1 -0
- package/dist/components/segmented.css +14 -6
- package/dist/components/segmented.mjs +1 -0
- package/dist/components/select.css +79 -24
- package/dist/components/sidebar.css +1 -1
- package/dist/components/split-pane.css +0 -1
- package/dist/components/stat.css +14 -18
- package/dist/components/steps.css +15 -5
- package/dist/components/steps.mjs +1 -0
- package/dist/components/switch.css +11 -13
- package/dist/components/switch.mjs +1 -0
- package/dist/components/table.css +46 -47
- package/dist/components/table.mjs +1 -0
- package/dist/components/tabs.css +57 -45
- package/dist/components/tag-input.css +5 -10
- package/dist/components/toast.css +16 -13
- package/dist/components/tooltip.css +2 -2
- package/dist/components/upload.css +0 -15
- package/dist/index.d.mts +39 -0
- package/dist/index.mjs +11243 -3312
- package/dist/registry.json +147 -9
- package/dist/retro-effects.css +23 -32
- package/dist/tokens/colors.css +4 -1
- package/dist/tokens/retro-effects.css +23 -32
- package/dist/tokens/style-profiles.css +24 -2
- package/dist/tokens/typography.css +2 -0
- package/dist/tokens.css +30 -3
- package/dist/ui.css +3420 -645
- package/docs/spec/ai-native.md +662 -0
- package/package.json +15 -5
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
// src/behaviors/events.ts
|
|
2
|
+
function emitIcen(source, type, detail) {
|
|
3
|
+
return source.dispatchEvent(
|
|
4
|
+
new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable: true })
|
|
5
|
+
);
|
|
6
|
+
}
|
|
7
|
+
|
|
1
8
|
// src/behaviors/sidebar.ts
|
|
2
9
|
function setup(sidebar) {
|
|
3
10
|
const el = sidebar;
|
|
4
|
-
if (el.__icenSidebarInit) return;
|
|
11
|
+
if (el.__icenSidebarInit) return void 0;
|
|
5
12
|
el.__icenSidebarInit = true;
|
|
6
13
|
sidebar.querySelectorAll(".sidebar-group-title").forEach((title) => {
|
|
7
14
|
const group = title.closest(".sidebar-group");
|
|
@@ -11,9 +18,21 @@ function setup(sidebar) {
|
|
|
11
18
|
title.setAttribute("aria-controls", panel.id);
|
|
12
19
|
}
|
|
13
20
|
});
|
|
14
|
-
|
|
21
|
+
sidebar.querySelectorAll("[data-sidebar-collapse]").forEach((btn) => {
|
|
22
|
+
if (!btn.hasAttribute("aria-expanded")) {
|
|
23
|
+
btn.setAttribute("aria-expanded", String(!sidebar.classList.contains("sidebar--collapsed")));
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
const onClick = (ev) => {
|
|
15
27
|
const target = ev.target;
|
|
16
28
|
if (!(target instanceof Element)) return;
|
|
29
|
+
const collapseBtn = target.closest("[data-sidebar-collapse]");
|
|
30
|
+
if (collapseBtn && collapseBtn.closest(".sidebar") === el) {
|
|
31
|
+
const collapsed = el.classList.toggle("sidebar--collapsed");
|
|
32
|
+
collapseBtn.setAttribute("aria-expanded", String(!collapsed));
|
|
33
|
+
emitIcen(el, "icen:sidebar-collapse", { collapsed });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
17
36
|
const title = target.closest(".sidebar-group-title");
|
|
18
37
|
if (!(title instanceof HTMLElement)) return;
|
|
19
38
|
if (title.closest(".sidebar") !== el) return;
|
|
@@ -21,18 +40,30 @@ function setup(sidebar) {
|
|
|
21
40
|
if (!group) return;
|
|
22
41
|
const open = group.classList.toggle("is-open");
|
|
23
42
|
title.setAttribute("aria-expanded", String(open));
|
|
24
|
-
sidebar
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
43
|
+
emitIcen(sidebar, "icen:sidebar-toggle", { group, open });
|
|
44
|
+
};
|
|
45
|
+
el.addEventListener("click", onClick);
|
|
46
|
+
return () => {
|
|
47
|
+
el.removeEventListener("click", onClick);
|
|
48
|
+
el.__icenSidebarInit = false;
|
|
49
|
+
};
|
|
29
50
|
}
|
|
30
|
-
function initSidebar(root
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
51
|
+
function initSidebar(root) {
|
|
52
|
+
const cleanups = [];
|
|
53
|
+
if (typeof document !== "undefined") {
|
|
54
|
+
const scope = root ?? document;
|
|
55
|
+
const sidebars = [];
|
|
56
|
+
if (scope instanceof Element && scope.matches(".sidebar")) sidebars.push(scope);
|
|
57
|
+
sidebars.push(...Array.from(scope.querySelectorAll(".sidebar")));
|
|
58
|
+
for (const s of sidebars) {
|
|
59
|
+
const cleanup = setup(s);
|
|
60
|
+
if (cleanup) cleanups.push(cleanup);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return () => {
|
|
64
|
+
for (const fn of cleanups) fn();
|
|
65
|
+
cleanups.length = 0;
|
|
66
|
+
};
|
|
36
67
|
}
|
|
37
68
|
export {
|
|
38
69
|
initSidebar
|
|
@@ -1,4 +1,15 @@
|
|
|
1
|
+
// src/behaviors/events.ts
|
|
2
|
+
function emitIcen(source, type, detail) {
|
|
3
|
+
return source.dispatchEvent(
|
|
4
|
+
new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable: true })
|
|
5
|
+
);
|
|
6
|
+
}
|
|
7
|
+
|
|
1
8
|
// src/behaviors/slider.ts
|
|
9
|
+
function bind(target, type, fn, bag) {
|
|
10
|
+
target.addEventListener(type, fn);
|
|
11
|
+
bag.push(() => target.removeEventListener(type, fn));
|
|
12
|
+
}
|
|
2
13
|
function pct(input) {
|
|
3
14
|
const min = Number(input.min || "0");
|
|
4
15
|
const max = Number(input.max || "100");
|
|
@@ -6,7 +17,7 @@ function pct(input) {
|
|
|
6
17
|
if (range <= 0) return 0;
|
|
7
18
|
return (Number(input.value) - min) / range * 100;
|
|
8
19
|
}
|
|
9
|
-
function setupSingle(slider, native) {
|
|
20
|
+
function setupSingle(slider, native, bag) {
|
|
10
21
|
const fill = slider.querySelector(".slider-fill");
|
|
11
22
|
const thumb = slider.querySelector(".slider-thumb");
|
|
12
23
|
const sync = () => {
|
|
@@ -14,22 +25,16 @@ function setupSingle(slider, native) {
|
|
|
14
25
|
if (fill) fill.style.width = `${p}%`;
|
|
15
26
|
if (thumb) thumb.style.left = `calc(${p}% - 8px)`;
|
|
16
27
|
};
|
|
17
|
-
native
|
|
28
|
+
bind(native, "input", () => {
|
|
18
29
|
sync();
|
|
19
|
-
slider
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
})
|
|
23
|
-
});
|
|
24
|
-
native.addEventListener("change", () => {
|
|
25
|
-
slider.dispatchEvent(new CustomEvent("icen:slider-change", {
|
|
26
|
-
bubbles: true,
|
|
27
|
-
detail: { value: Number(native.value) }
|
|
28
|
-
}));
|
|
29
|
-
});
|
|
30
|
+
emitIcen(slider, "icen:slider-input", { value: Number(native.value) });
|
|
31
|
+
}, bag);
|
|
32
|
+
bind(native, "change", () => {
|
|
33
|
+
emitIcen(slider, "icen:slider-change", { value: Number(native.value) });
|
|
34
|
+
}, bag);
|
|
30
35
|
sync();
|
|
31
36
|
}
|
|
32
|
-
function setupDual(slider, natives) {
|
|
37
|
+
function setupDual(slider, natives, bag) {
|
|
33
38
|
const lo = natives.find((n) => n.getAttribute("data-thumb") === "lo") ?? natives[0];
|
|
34
39
|
const hi = natives.find((n) => n.getAttribute("data-thumb") === "hi") ?? natives[1];
|
|
35
40
|
if (!lo || !hi || lo === hi) return;
|
|
@@ -59,45 +64,52 @@ function setupDual(slider, natives) {
|
|
|
59
64
|
lo.style.clipPath = `inset(0 ${100 - pLo}% 0 0)`;
|
|
60
65
|
hi.style.clipPath = `inset(0 0 0 ${pHi}%)`;
|
|
61
66
|
};
|
|
62
|
-
lo
|
|
67
|
+
bind(lo, "input", () => {
|
|
63
68
|
sync(lo);
|
|
64
|
-
slider
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}));
|
|
68
|
-
});
|
|
69
|
-
hi.addEventListener("input", () => {
|
|
69
|
+
emitIcen(slider, "icen:slider-input", { lo: Number(lo.value), hi: Number(hi.value) });
|
|
70
|
+
}, bag);
|
|
71
|
+
bind(hi, "input", () => {
|
|
70
72
|
sync(hi);
|
|
71
|
-
slider
|
|
72
|
-
|
|
73
|
-
detail: { lo: Number(lo.value), hi: Number(hi.value) }
|
|
74
|
-
}));
|
|
75
|
-
});
|
|
73
|
+
emitIcen(slider, "icen:slider-input", { lo: Number(lo.value), hi: Number(hi.value) });
|
|
74
|
+
}, bag);
|
|
76
75
|
const onChange = () => {
|
|
77
|
-
slider
|
|
78
|
-
bubbles: true,
|
|
79
|
-
detail: { lo: Number(lo.value), hi: Number(hi.value) }
|
|
80
|
-
}));
|
|
76
|
+
emitIcen(slider, "icen:slider-change", { lo: Number(lo.value), hi: Number(hi.value) });
|
|
81
77
|
};
|
|
82
|
-
lo
|
|
83
|
-
hi
|
|
78
|
+
bind(lo, "change", onChange, bag);
|
|
79
|
+
bind(hi, "change", onChange, bag);
|
|
84
80
|
sync(null);
|
|
85
81
|
}
|
|
86
82
|
function setup(slider) {
|
|
87
83
|
const el = slider;
|
|
88
|
-
if (el.__icenSliderInit) return;
|
|
89
|
-
el.__icenSliderInit = true;
|
|
84
|
+
if (el.__icenSliderInit) return void 0;
|
|
90
85
|
const natives = Array.from(slider.querySelectorAll(".slider-native"));
|
|
91
|
-
if (natives.length === 0) return;
|
|
92
|
-
|
|
93
|
-
|
|
86
|
+
if (natives.length === 0) return void 0;
|
|
87
|
+
el.__icenSliderInit = true;
|
|
88
|
+
const bag = [];
|
|
89
|
+
if (slider.classList.contains("slider--dual")) setupDual(slider, natives, bag);
|
|
90
|
+
else setupSingle(slider, natives[0], bag);
|
|
91
|
+
return () => {
|
|
92
|
+
for (const off of bag) off();
|
|
93
|
+
bag.length = 0;
|
|
94
|
+
el.__icenSliderInit = false;
|
|
95
|
+
};
|
|
94
96
|
}
|
|
95
|
-
function initSlider(root
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
function initSlider(root) {
|
|
98
|
+
const cleanups = [];
|
|
99
|
+
if (typeof document !== "undefined") {
|
|
100
|
+
const scope = root ?? document;
|
|
101
|
+
const containers = [];
|
|
102
|
+
if (scope instanceof Element && scope.matches(".slider")) containers.push(scope);
|
|
103
|
+
containers.push(...Array.from(scope.querySelectorAll(".slider")));
|
|
104
|
+
for (const c of containers) {
|
|
105
|
+
const cleanup = setup(c);
|
|
106
|
+
if (cleanup) cleanups.push(cleanup);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return () => {
|
|
110
|
+
for (const fn of cleanups) fn();
|
|
111
|
+
cleanups.length = 0;
|
|
112
|
+
};
|
|
101
113
|
}
|
|
102
114
|
export {
|
|
103
115
|
initSlider
|
|
@@ -1,18 +1,28 @@
|
|
|
1
|
+
// src/behaviors/events.ts
|
|
2
|
+
function emitIcen(source, type, detail) {
|
|
3
|
+
return source.dispatchEvent(
|
|
4
|
+
new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable: true })
|
|
5
|
+
);
|
|
6
|
+
}
|
|
7
|
+
|
|
1
8
|
// src/behaviors/split-pane.ts
|
|
2
|
-
|
|
9
|
+
function numOr(value, fallback) {
|
|
10
|
+
if (value === void 0 || value === "") return fallback;
|
|
11
|
+
const n = Number(value);
|
|
12
|
+
return Number.isFinite(n) ? n : fallback;
|
|
13
|
+
}
|
|
3
14
|
function pct(value, min, max) {
|
|
4
15
|
return Math.min(max, Math.max(min, value));
|
|
5
16
|
}
|
|
6
17
|
function setup(container) {
|
|
7
18
|
const el = container;
|
|
8
|
-
if (el.__icenSplitPaneInit) return;
|
|
19
|
+
if (el.__icenSplitPaneInit) return void 0;
|
|
20
|
+
const divider = el.querySelector(".split-pane-divider");
|
|
21
|
+
if (!divider) return void 0;
|
|
9
22
|
el.__icenSplitPaneInit = true;
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
const min = Number(el.dataset.splitMin ?? "10") || 10;
|
|
14
|
-
const max = Number(el.dataset.splitMax ?? "90") || 90;
|
|
15
|
-
const step = Number(el.dataset.splitStep ?? "2") || 2;
|
|
23
|
+
const min = numOr(el.dataset.splitMin, 10);
|
|
24
|
+
const max = numOr(el.dataset.splitMax, 90);
|
|
25
|
+
const step = numOr(el.dataset.splitStep, 2);
|
|
16
26
|
const vertical = el.classList.contains("split-pane--vertical");
|
|
17
27
|
const orientation = vertical ? "vertical" : "horizontal";
|
|
18
28
|
divider.setAttribute("role", "separator");
|
|
@@ -24,20 +34,25 @@ function setup(container) {
|
|
|
24
34
|
const v = parseFloat(getComputedStyle(el).getPropertyValue("--split"));
|
|
25
35
|
return Number.isFinite(v) ? v : 50;
|
|
26
36
|
}
|
|
27
|
-
|
|
37
|
+
let cur = readCurrent();
|
|
38
|
+
const write = (p) => {
|
|
28
39
|
const v = pct(p, min, max);
|
|
40
|
+
cur = v;
|
|
29
41
|
el.style.setProperty("--split", v + "%");
|
|
30
42
|
divider.setAttribute("aria-valuenow", String(Math.round(v)));
|
|
43
|
+
};
|
|
44
|
+
function emitChange() {
|
|
45
|
+
emitIcen(el, "icen:split-change", { value: Math.round(cur * 10) / 10 });
|
|
31
46
|
}
|
|
32
47
|
if (!el.style.getPropertyValue("--split")) write(50);
|
|
33
48
|
divider.setAttribute("aria-valuenow", String(Math.round(readCurrent())));
|
|
34
49
|
let dragging = false;
|
|
35
|
-
|
|
50
|
+
const onDown = (ev) => {
|
|
36
51
|
dragging = true;
|
|
37
52
|
divider.classList.add("is-dragging");
|
|
38
53
|
divider.setPointerCapture(ev.pointerId);
|
|
39
54
|
el.style.userSelect = "none";
|
|
40
|
-
}
|
|
55
|
+
};
|
|
41
56
|
const onUp = (ev) => {
|
|
42
57
|
if (!dragging) return;
|
|
43
58
|
dragging = false;
|
|
@@ -47,14 +62,16 @@ function setup(container) {
|
|
|
47
62
|
} catch {
|
|
48
63
|
}
|
|
49
64
|
el.style.userSelect = "";
|
|
65
|
+
emitChange();
|
|
50
66
|
};
|
|
51
|
-
|
|
52
|
-
|
|
67
|
+
const onCancel = () => {
|
|
68
|
+
if (!dragging) return;
|
|
53
69
|
dragging = false;
|
|
54
70
|
divider.classList.remove("is-dragging");
|
|
55
71
|
el.style.userSelect = "";
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
emitChange();
|
|
73
|
+
};
|
|
74
|
+
const onMove = (ev) => {
|
|
58
75
|
if (!dragging) return;
|
|
59
76
|
const rect = el.getBoundingClientRect();
|
|
60
77
|
if (vertical) {
|
|
@@ -66,9 +83,12 @@ function setup(container) {
|
|
|
66
83
|
const p = (ev.clientX - rect.left) / rect.width * 100;
|
|
67
84
|
write(p);
|
|
68
85
|
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
86
|
+
};
|
|
87
|
+
const onDblClick = () => {
|
|
88
|
+
write(50);
|
|
89
|
+
emitChange();
|
|
90
|
+
};
|
|
91
|
+
const onKeyDown = (ev) => {
|
|
72
92
|
const current = readCurrent();
|
|
73
93
|
let next = null;
|
|
74
94
|
if (vertical) {
|
|
@@ -83,19 +103,40 @@ function setup(container) {
|
|
|
83
103
|
if (next === null) return;
|
|
84
104
|
ev.preventDefault();
|
|
85
105
|
write(next);
|
|
86
|
-
|
|
106
|
+
emitChange();
|
|
107
|
+
};
|
|
108
|
+
divider.addEventListener("pointerdown", onDown);
|
|
109
|
+
divider.addEventListener("pointerup", onUp);
|
|
110
|
+
divider.addEventListener("pointercancel", onCancel);
|
|
111
|
+
divider.addEventListener("pointermove", onMove);
|
|
112
|
+
divider.addEventListener("dblclick", onDblClick);
|
|
113
|
+
divider.addEventListener("keydown", onKeyDown);
|
|
114
|
+
return () => {
|
|
115
|
+
divider.removeEventListener("pointerdown", onDown);
|
|
116
|
+
divider.removeEventListener("pointerup", onUp);
|
|
117
|
+
divider.removeEventListener("pointercancel", onCancel);
|
|
118
|
+
divider.removeEventListener("pointermove", onMove);
|
|
119
|
+
divider.removeEventListener("dblclick", onDblClick);
|
|
120
|
+
divider.removeEventListener("keydown", onKeyDown);
|
|
121
|
+
el.__icenSplitPaneInit = false;
|
|
122
|
+
};
|
|
87
123
|
}
|
|
88
|
-
function initSplitPane(root
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
124
|
+
function initSplitPane(root) {
|
|
125
|
+
const cleanups = [];
|
|
126
|
+
if (typeof document !== "undefined") {
|
|
127
|
+
const scope = root ?? document;
|
|
128
|
+
const panes = [];
|
|
129
|
+
if (scope instanceof Element && scope.matches(".split-pane[data-split-pane]")) panes.push(scope);
|
|
130
|
+
panes.push(...Array.from(scope.querySelectorAll(".split-pane[data-split-pane]")));
|
|
131
|
+
for (const p of panes) {
|
|
132
|
+
const cleanup = setup(p);
|
|
133
|
+
if (cleanup) cleanups.push(cleanup);
|
|
134
|
+
}
|
|
98
135
|
}
|
|
136
|
+
return () => {
|
|
137
|
+
for (const fn of cleanups) fn();
|
|
138
|
+
cleanups.length = 0;
|
|
139
|
+
};
|
|
99
140
|
}
|
|
100
141
|
export {
|
|
101
142
|
initSplitPane
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 为 root 下每个 [data-tabs] 容器初始化(root 自身是 [data-tabs] 也算)。
|
|
3
|
+
* 返回销毁函数:调用后移除所有容器监听、window hashchange/resize 监听与行为层创建的
|
|
4
|
+
* .tabs-ink 元素,并复位幂等标记(销毁后的容器可被再次 initTabs 重新接管)。
|
|
5
|
+
* SSR 下返回 no-op 函数。
|
|
6
|
+
*/
|
|
7
|
+
declare function initTabs(root?: ParentNode): () => void;
|
|
8
|
+
|
|
9
|
+
export { initTabs };
|
package/dist/behaviors/tabs.mjs
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
// src/behaviors/events.ts
|
|
2
|
+
function emitIcen(source, type, detail) {
|
|
3
|
+
return source.dispatchEvent(
|
|
4
|
+
new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable: true })
|
|
5
|
+
);
|
|
6
|
+
}
|
|
7
|
+
|
|
1
8
|
// src/behaviors/tabs.ts
|
|
2
9
|
function tabButtons(container) {
|
|
3
10
|
return Array.from(container.querySelectorAll("[data-tab]"));
|
|
@@ -8,27 +15,52 @@ function tabNames(container) {
|
|
|
8
15
|
function currentHash() {
|
|
9
16
|
return window.location.hash.replace(/^#/, "");
|
|
10
17
|
}
|
|
11
|
-
function
|
|
18
|
+
function hashWriteMethod(container) {
|
|
19
|
+
const raw = container.getAttribute("data-tabs-hash");
|
|
20
|
+
if (raw === null || raw === "false") return null;
|
|
21
|
+
return raw === "push" ? "push" : "replace";
|
|
22
|
+
}
|
|
23
|
+
function syncInk(container, name) {
|
|
24
|
+
const btn = tabButtons(container).find((b) => b.getAttribute("data-tab") === name);
|
|
25
|
+
const tablist = btn?.parentElement;
|
|
26
|
+
if (!btn || !tablist) return;
|
|
27
|
+
const ink = tablist.querySelector(":scope > .tabs-ink");
|
|
28
|
+
if (!ink) return;
|
|
29
|
+
ink.style.width = `${btn.offsetWidth}px`;
|
|
30
|
+
ink.style.transform = `translateX(${btn.offsetLeft}px)`;
|
|
31
|
+
}
|
|
32
|
+
function activate(container, name, writeHash, emit = false) {
|
|
12
33
|
const buttons = tabButtons(container);
|
|
13
34
|
let matched = false;
|
|
14
35
|
buttons.forEach((t) => {
|
|
15
36
|
const on = t.getAttribute("data-tab") === name;
|
|
16
37
|
if (on) matched = true;
|
|
17
|
-
t.classList.toggle("active", on);
|
|
38
|
+
t.classList.toggle("is-active", on);
|
|
18
39
|
t.setAttribute("aria-selected", String(on));
|
|
19
40
|
t.tabIndex = on ? 0 : -1;
|
|
20
41
|
});
|
|
21
42
|
if (!matched) return;
|
|
43
|
+
let panel = null;
|
|
22
44
|
container.querySelectorAll("[data-tab-panel]").forEach((p) => {
|
|
23
45
|
const isOn = p.getAttribute("data-tab-panel") === name;
|
|
24
46
|
p.hidden = !isOn;
|
|
25
|
-
if (isOn
|
|
26
|
-
|
|
27
|
-
if (
|
|
47
|
+
if (isOn) {
|
|
48
|
+
panel = p;
|
|
49
|
+
if (!p.getAttribute("aria-labelledby")) {
|
|
50
|
+
const btn = buttons.find((b) => b.getAttribute("data-tab") === name);
|
|
51
|
+
if (btn?.id) p.setAttribute("aria-labelledby", btn.id);
|
|
52
|
+
}
|
|
28
53
|
}
|
|
29
54
|
});
|
|
30
55
|
if (writeHash && typeof window !== "undefined") {
|
|
31
|
-
|
|
56
|
+
const method = hashWriteMethod(container);
|
|
57
|
+
if (method) window.history[method === "push" ? "pushState" : "replaceState"](null, "", `#${name}`);
|
|
58
|
+
}
|
|
59
|
+
syncInk(container, name);
|
|
60
|
+
if (emit) {
|
|
61
|
+
const index = buttons.findIndex((b) => b.getAttribute("data-tab") === name);
|
|
62
|
+
const tab = buttons[index] ?? null;
|
|
63
|
+
emitIcen(container, "icen:tab-change", { tab, panel, index });
|
|
32
64
|
}
|
|
33
65
|
}
|
|
34
66
|
function isDisabled(btn) {
|
|
@@ -52,11 +84,12 @@ function nextEnabled(container, from, dir) {
|
|
|
52
84
|
}
|
|
53
85
|
function setup(container) {
|
|
54
86
|
const el = container;
|
|
55
|
-
if (el.__icenTabsInit) return;
|
|
56
|
-
|
|
57
|
-
const
|
|
87
|
+
if (el.__icenTabsInit) return void 0;
|
|
88
|
+
const useHash = hashWriteMethod(container) !== null;
|
|
89
|
+
const autoActivate = container.getAttribute("data-tabs-activation") === "auto";
|
|
58
90
|
const names = tabNames(container);
|
|
59
91
|
if (names.length === 0) return;
|
|
92
|
+
el.__icenTabsInit = true;
|
|
60
93
|
const buttons = tabButtons(container);
|
|
61
94
|
buttons.forEach((b) => {
|
|
62
95
|
if (!b.getAttribute("role")) b.setAttribute("role", "tab");
|
|
@@ -64,6 +97,18 @@ function setup(container) {
|
|
|
64
97
|
});
|
|
65
98
|
const tablist = buttons[0]?.parentElement;
|
|
66
99
|
if (tablist && !tablist.getAttribute("role")) tablist.setAttribute("role", "tablist");
|
|
100
|
+
let ink = null;
|
|
101
|
+
if (tablist instanceof HTMLElement && tablist.hasAttribute("data-tabs-ink")) {
|
|
102
|
+
ink = document.createElement("div");
|
|
103
|
+
ink.className = "tabs-ink";
|
|
104
|
+
ink.setAttribute("aria-hidden", "true");
|
|
105
|
+
const initialBtn = buttons.find((b) => b.classList.contains("is-active")) ?? buttons.find((b) => b.getAttribute("data-tab") === names[0]);
|
|
106
|
+
if (initialBtn) {
|
|
107
|
+
ink.style.width = `${initialBtn.offsetWidth}px`;
|
|
108
|
+
ink.style.transform = `translateX(${initialBtn.offsetLeft}px)`;
|
|
109
|
+
}
|
|
110
|
+
tablist.appendChild(ink);
|
|
111
|
+
}
|
|
67
112
|
const onClick = (ev) => {
|
|
68
113
|
const target = ev.target;
|
|
69
114
|
if (!(target instanceof Element)) return;
|
|
@@ -71,7 +116,15 @@ function setup(container) {
|
|
|
71
116
|
if (!btn || btn.closest("[data-tabs]") !== container) return;
|
|
72
117
|
if (btn instanceof HTMLElement && isDisabled(btn)) return;
|
|
73
118
|
const name = btn.getAttribute("data-tab");
|
|
74
|
-
if (name) activate(container, name, useHash);
|
|
119
|
+
if (name) activate(container, name, useHash, true);
|
|
120
|
+
};
|
|
121
|
+
const followFocus = (idx) => {
|
|
122
|
+
focusTab(container, idx);
|
|
123
|
+
if (!autoActivate) return;
|
|
124
|
+
const target = tabButtons(container)[idx];
|
|
125
|
+
if (!target || isDisabled(target)) return;
|
|
126
|
+
const name = target.getAttribute("data-tab");
|
|
127
|
+
if (name) activate(container, name, useHash, true);
|
|
75
128
|
};
|
|
76
129
|
const onKeyDown = (ev) => {
|
|
77
130
|
const kev = ev;
|
|
@@ -86,25 +139,23 @@ function setup(container) {
|
|
|
86
139
|
case "ArrowRight":
|
|
87
140
|
case "ArrowDown": {
|
|
88
141
|
kev.preventDefault();
|
|
89
|
-
|
|
90
|
-
if (next >= 0) focusTab(container, next);
|
|
142
|
+
followFocus(nextEnabled(container, currentIdx, 1));
|
|
91
143
|
break;
|
|
92
144
|
}
|
|
93
145
|
case "ArrowLeft":
|
|
94
146
|
case "ArrowUp": {
|
|
95
147
|
kev.preventDefault();
|
|
96
|
-
|
|
97
|
-
if (prev >= 0) focusTab(container, prev);
|
|
148
|
+
followFocus(nextEnabled(container, currentIdx, -1));
|
|
98
149
|
break;
|
|
99
150
|
}
|
|
100
151
|
case "Home": {
|
|
101
152
|
kev.preventDefault();
|
|
102
|
-
|
|
153
|
+
followFocus(nextEnabled(container, -1, 1));
|
|
103
154
|
break;
|
|
104
155
|
}
|
|
105
156
|
case "End": {
|
|
106
157
|
kev.preventDefault();
|
|
107
|
-
|
|
158
|
+
followFocus(nextEnabled(container, buttonsList.length, -1));
|
|
108
159
|
break;
|
|
109
160
|
}
|
|
110
161
|
case "Enter":
|
|
@@ -112,7 +163,7 @@ function setup(container) {
|
|
|
112
163
|
if (btn instanceof HTMLElement && !isDisabled(btn)) {
|
|
113
164
|
kev.preventDefault();
|
|
114
165
|
const name = btn.getAttribute("data-tab");
|
|
115
|
-
if (name) activate(container, name, useHash);
|
|
166
|
+
if (name) activate(container, name, useHash, true);
|
|
116
167
|
}
|
|
117
168
|
break;
|
|
118
169
|
}
|
|
@@ -125,29 +176,54 @@ function setup(container) {
|
|
|
125
176
|
const h = currentHash();
|
|
126
177
|
if (names.includes(h)) initial = h;
|
|
127
178
|
} else {
|
|
128
|
-
const n = container.querySelector("[data-tab].active")?.getAttribute("data-tab");
|
|
179
|
+
const n = container.querySelector("[data-tab].is-active")?.getAttribute("data-tab");
|
|
129
180
|
if (n && names.includes(n)) initial = n;
|
|
130
181
|
}
|
|
131
182
|
activate(container, initial ?? names[0], false);
|
|
132
183
|
const onHash = () => {
|
|
133
184
|
const h = currentHash();
|
|
134
|
-
if (names.includes(h)) activate(container, h, false);
|
|
185
|
+
if (names.includes(h)) activate(container, h, false, true);
|
|
135
186
|
};
|
|
136
187
|
if (useHash && typeof window !== "undefined") {
|
|
137
188
|
window.addEventListener("hashchange", onHash);
|
|
138
189
|
}
|
|
139
|
-
|
|
190
|
+
const onResize = () => {
|
|
191
|
+
const active = tabButtons(container).find((b) => b.classList.contains("is-active"));
|
|
192
|
+
const name = active?.getAttribute("data-tab");
|
|
193
|
+
if (name) syncInk(container, name);
|
|
194
|
+
};
|
|
195
|
+
if (ink && typeof window !== "undefined") {
|
|
196
|
+
window.addEventListener("resize", onResize, { passive: true });
|
|
197
|
+
}
|
|
198
|
+
const cleanup = () => {
|
|
140
199
|
container.removeEventListener("click", onClick);
|
|
141
200
|
container.removeEventListener("keydown", onKeyDown);
|
|
142
201
|
window.removeEventListener("hashchange", onHash);
|
|
202
|
+
if (ink) {
|
|
203
|
+
window.removeEventListener("resize", onResize);
|
|
204
|
+
ink.remove();
|
|
205
|
+
ink = null;
|
|
206
|
+
}
|
|
207
|
+
el.__icenTabsInit = false;
|
|
143
208
|
};
|
|
209
|
+
return cleanup;
|
|
144
210
|
}
|
|
145
|
-
function initTabs(root
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
211
|
+
function initTabs(root) {
|
|
212
|
+
const cleanups = [];
|
|
213
|
+
if (typeof document !== "undefined") {
|
|
214
|
+
const scope = root ?? document;
|
|
215
|
+
const containers = [];
|
|
216
|
+
if (scope instanceof Element && scope.matches("[data-tabs]")) containers.push(scope);
|
|
217
|
+
containers.push(...Array.from(scope.querySelectorAll("[data-tabs]")));
|
|
218
|
+
for (const c of containers) {
|
|
219
|
+
const cleanup = setup(c);
|
|
220
|
+
if (cleanup) cleanups.push(cleanup);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return () => {
|
|
224
|
+
for (const fn of cleanups) fn();
|
|
225
|
+
cleanups.length = 0;
|
|
226
|
+
};
|
|
151
227
|
}
|
|
152
228
|
export {
|
|
153
229
|
initTabs
|