@icen.ai/ui 0.2.0 → 0.3.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 (103) hide show
  1. package/README.md +29 -16
  2. package/dist/base.css +389 -48
  3. package/dist/behaviors/accordion.mjs +20 -0
  4. package/dist/behaviors/carousel.mjs +30 -1
  5. package/dist/behaviors/charts.mjs +168 -0
  6. package/dist/behaviors/copy.mjs +4 -3
  7. package/dist/behaviors/datatable.mjs +748 -0
  8. package/dist/behaviors/input.mjs +20 -1
  9. package/dist/behaviors/modal.mjs +15 -1
  10. package/dist/behaviors/nav.mjs +31 -4
  11. package/dist/behaviors/sidebar.mjs +12 -0
  12. package/dist/behaviors/slider.mjs +35 -3
  13. package/dist/behaviors/tabs.mjs +100 -9
  14. package/dist/behaviors/tag-input.mjs +11 -0
  15. package/dist/behaviors/theme.mjs +26 -0
  16. package/dist/behaviors/toast.mjs +83 -15
  17. package/dist/behaviors/tree.mjs +57 -1
  18. package/dist/behaviors/upload.mjs +2 -3
  19. package/dist/cli.mjs +74 -0
  20. package/dist/components/accordion.mjs +3 -0
  21. package/dist/components/alert.mjs +2 -0
  22. package/dist/components/avatar.mjs +2 -0
  23. package/dist/components/breadcrumb.css +91 -18
  24. package/dist/components/breadcrumb.mjs +2 -0
  25. package/dist/components/btn.css +174 -14
  26. package/dist/components/btn.mjs +2 -0
  27. package/dist/components/card.css +118 -18
  28. package/dist/components/card.mjs +2 -0
  29. package/dist/components/carousel.mjs +3 -0
  30. package/dist/components/charts.css +225 -33
  31. package/dist/components/charts.mjs +3 -0
  32. package/dist/components/content.css +256 -74
  33. package/dist/components/context-menu.mjs +3 -0
  34. package/dist/components/datatable.css +343 -0
  35. package/dist/components/datatable.mjs +4 -0
  36. package/dist/components/desc.mjs +2 -0
  37. package/dist/components/dropdown.mjs +3 -0
  38. package/dist/components/empty.css +50 -10
  39. package/dist/components/empty.mjs +2 -0
  40. package/dist/components/feedback.css +158 -47
  41. package/dist/components/form.css +139 -22
  42. package/dist/components/input.css +165 -45
  43. package/dist/components/input.mjs +3 -0
  44. package/dist/components/kbd.mjs +2 -0
  45. package/dist/components/layout.mjs +2 -0
  46. package/dist/components/list.mjs +2 -0
  47. package/dist/components/media-card.mjs +2 -0
  48. package/dist/components/media.css +187 -40
  49. package/dist/components/menu.css +159 -32
  50. package/dist/components/modal.css +140 -34
  51. package/dist/components/modal.mjs +3 -0
  52. package/dist/components/nav.css +134 -32
  53. package/dist/components/nav.mjs +3 -0
  54. package/dist/components/pagination.css +105 -19
  55. package/dist/components/pagination.mjs +2 -0
  56. package/dist/components/panel.css +61 -6
  57. package/dist/components/panel.mjs +2 -0
  58. package/dist/components/pill.css +74 -14
  59. package/dist/components/pill.mjs +2 -0
  60. package/dist/components/popover.css +109 -7
  61. package/dist/components/popover.mjs +3 -0
  62. package/dist/components/progress.mjs +2 -0
  63. package/dist/components/rating.mjs +2 -0
  64. package/dist/components/result.mjs +2 -0
  65. package/dist/components/segmented.css +199 -50
  66. package/dist/components/segmented.mjs +2 -0
  67. package/dist/components/select.css +140 -39
  68. package/dist/components/select.mjs +3 -0
  69. package/dist/components/sidebar.css +162 -34
  70. package/dist/components/sidebar.mjs +3 -0
  71. package/dist/components/skeleton.mjs +2 -0
  72. package/dist/components/slider.css +169 -31
  73. package/dist/components/slider.mjs +3 -0
  74. package/dist/components/spinner.mjs +2 -0
  75. package/dist/components/stat.css +103 -8
  76. package/dist/components/stat.mjs +2 -0
  77. package/dist/components/steps.css +105 -31
  78. package/dist/components/steps.mjs +2 -0
  79. package/dist/components/switch.css +171 -54
  80. package/dist/components/switch.mjs +2 -0
  81. package/dist/components/table.css +114 -12
  82. package/dist/components/table.mjs +2 -0
  83. package/dist/components/tabs.css +104 -17
  84. package/dist/components/tabs.mjs +3 -0
  85. package/dist/components/tag-input.css +119 -23
  86. package/dist/components/tag-input.mjs +3 -0
  87. package/dist/components/timeline.mjs +2 -0
  88. package/dist/components/toast.css +85 -12
  89. package/dist/components/toast.mjs +3 -0
  90. package/dist/components/tooltip.css +86 -26
  91. package/dist/components/tooltip.mjs +2 -0
  92. package/dist/components/tree.mjs +3 -0
  93. package/dist/components/upload.css +144 -17
  94. package/dist/components/upload.mjs +3 -0
  95. package/dist/index.mjs +629 -48
  96. package/dist/registry.json +197 -2
  97. package/dist/retro-effects.css +264 -0
  98. package/dist/tokens/colors.css +18 -90
  99. package/dist/tokens/retro-effects.css +264 -0
  100. package/dist/tokens/style-profiles.css +47 -47
  101. package/dist/tokens.css +65 -137
  102. package/dist/ui.css +4861 -1019
  103. package/package.json +9 -1
@@ -48,14 +48,25 @@ function setupOtp(group) {
48
48
  if (el.__icenInputInit) return;
49
49
  el.__icenInputInit = true;
50
50
  const cells = Array.from(group.querySelectorAll(".otp-cell"));
51
+ const syncFilled = (cell) => {
52
+ cell.classList.toggle("is-filled", cell.value.length > 0);
53
+ };
51
54
  cells.forEach((cell, i) => {
55
+ if (!cell.hasAttribute("aria-label")) {
56
+ cell.setAttribute("aria-label", `\u7B2C ${i + 1} \u4F4D\uFF0C\u5171 ${cells.length} \u4F4D`);
57
+ }
58
+ syncFilled(cell);
52
59
  cell.addEventListener("input", () => {
53
60
  const ch = cell.value.slice(-1);
54
61
  cell.value = ch;
62
+ syncFilled(cell);
55
63
  if (ch && i < cells.length - 1) cells[i + 1].focus();
56
64
  });
57
65
  cell.addEventListener("keydown", (ev) => {
58
- if (ev.key === "Backspace" && !cell.value && i > 0) cells[i - 1].focus();
66
+ if (ev.key === "Backspace" && !cell.value && i > 0) {
67
+ ev.preventDefault();
68
+ cells[i - 1].focus();
69
+ }
59
70
  if (ev.key === "ArrowLeft" && i > 0) {
60
71
  ev.preventDefault();
61
72
  cells[i - 1].focus();
@@ -71,10 +82,18 @@ function setupOtp(group) {
71
82
  for (let k = 0; k < text.length; k++) {
72
83
  const target = cells[i + k];
73
84
  target.value = text[k] ?? "";
85
+ syncFilled(target);
74
86
  }
75
87
  cells[Math.min(i + text.length, cells.length - 1)].focus();
76
88
  });
77
89
  });
90
+ const fireComplete = () => {
91
+ const code = cells.map((c) => c.value).join("");
92
+ if (code.length === cells.length && cells.every((c) => c.value.length > 0)) {
93
+ group.dispatchEvent(new CustomEvent("otp:complete", { detail: { code }, bubbles: true }));
94
+ }
95
+ };
96
+ cells.forEach((cell) => cell.addEventListener("input", fireComplete));
78
97
  }
79
98
  function initInput(root = document) {
80
99
  if (typeof document === "undefined") return;
@@ -28,6 +28,20 @@ function openModal(id) {
28
28
  panel.setAttribute("role", "dialog");
29
29
  panel.setAttribute("aria-modal", "true");
30
30
  if (!panel.hasAttribute("tabindex")) panel.tabIndex = -1;
31
+ if (!panel.hasAttribute("aria-labelledby")) {
32
+ const title = panel.querySelector(".modal-title");
33
+ if (title) {
34
+ if (!title.id) title.id = `modal-title-${CSS.escape(id)}`;
35
+ panel.setAttribute("aria-labelledby", title.id);
36
+ }
37
+ }
38
+ if (!panel.hasAttribute("aria-describedby")) {
39
+ const desc = panel.querySelector(".modal-description");
40
+ if (desc) {
41
+ if (!desc.id) desc.id = `modal-desc-${CSS.escape(id)}`;
42
+ panel.setAttribute("aria-describedby", desc.id);
43
+ }
44
+ }
31
45
  const closeOnEscape = backdrop.dataset.closeOnEscape !== "false";
32
46
  keyHandler = (e) => {
33
47
  if (e.key === "Escape") {
@@ -86,7 +100,7 @@ function initModal(root = document) {
86
100
  const opener = target.closest("[data-modal-open]");
87
101
  if (opener?.dataset.modalOpen) {
88
102
  opener.setAttribute("aria-haspopup", "dialog");
89
- open(opener.dataset.modalOpen);
103
+ openModal(opener.dataset.modalOpen);
90
104
  return;
91
105
  }
92
106
  const closer = target.closest("[data-modal-close]");
@@ -1,20 +1,47 @@
1
1
  // src/behaviors/nav.ts
2
+ function prefersReducedMotion() {
3
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
4
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
5
+ }
2
6
  function setup(nav) {
3
7
  const el = nav;
4
8
  if (el.__icenNavInit) return;
5
9
  el.__icenNavInit = true;
10
+ const threshold = Number(nav.dataset.navThreshold ?? "12") || 12;
11
+ const hideOnScroll = nav.hasAttribute("data-nav-hide-on-scroll");
6
12
  let ticking = false;
7
- const update = () => {
13
+ let lastY = window.scrollY;
14
+ let hidden = false;
15
+ const apply = () => {
8
16
  ticking = false;
9
- el.classList.toggle("is-scrolled", window.scrollY > 12);
17
+ const y = window.scrollY;
18
+ nav.classList.toggle("is-scrolled", y > threshold);
19
+ if (hideOnScroll && !prefersReducedMotion()) {
20
+ const dy = y - lastY;
21
+ if (y < 8) {
22
+ hidden = false;
23
+ } else if (dy > 6 && !hidden) {
24
+ hidden = true;
25
+ } else if (dy < -6 && hidden) {
26
+ hidden = false;
27
+ }
28
+ nav.classList.toggle("is-nav-hidden", hidden);
29
+ }
30
+ lastY = y;
10
31
  };
11
32
  const onScroll = () => {
12
33
  if (ticking) return;
13
34
  ticking = true;
14
- window.requestAnimationFrame(update);
35
+ window.requestAnimationFrame(apply);
36
+ };
37
+ const onResize = () => {
38
+ if (ticking) return;
39
+ ticking = true;
40
+ window.requestAnimationFrame(apply);
15
41
  };
16
42
  window.addEventListener("scroll", onScroll, { passive: true });
17
- update();
43
+ window.addEventListener("resize", onResize, { passive: true });
44
+ apply();
18
45
  }
19
46
  function initNav(root = document) {
20
47
  if (typeof document === "undefined") return;
@@ -3,6 +3,14 @@ function setup(sidebar) {
3
3
  const el = sidebar;
4
4
  if (el.__icenSidebarInit) return;
5
5
  el.__icenSidebarInit = true;
6
+ sidebar.querySelectorAll(".sidebar-group-title").forEach((title) => {
7
+ const group = title.closest(".sidebar-group");
8
+ const panel = group?.querySelector(".sidebar-group-panel");
9
+ if (panel && !title.hasAttribute("aria-controls")) {
10
+ if (!panel.id) panel.id = `sb-panel-${Math.random().toString(36).slice(2, 9)}`;
11
+ title.setAttribute("aria-controls", panel.id);
12
+ }
13
+ });
6
14
  el.addEventListener("click", (ev) => {
7
15
  const target = ev.target;
8
16
  if (!(target instanceof Element)) return;
@@ -13,6 +21,10 @@ function setup(sidebar) {
13
21
  if (!group) return;
14
22
  const open = group.classList.toggle("is-open");
15
23
  title.setAttribute("aria-expanded", String(open));
24
+ sidebar.dispatchEvent(new CustomEvent("icen:sidebar-toggle", {
25
+ bubbles: true,
26
+ detail: { group, open }
27
+ }));
16
28
  });
17
29
  }
18
30
  function initSidebar(root = document) {
@@ -14,7 +14,19 @@ function setupSingle(slider, native) {
14
14
  if (fill) fill.style.width = `${p}%`;
15
15
  if (thumb) thumb.style.left = `calc(${p}% - 8px)`;
16
16
  };
17
- native.addEventListener("input", sync);
17
+ native.addEventListener("input", () => {
18
+ sync();
19
+ slider.dispatchEvent(new CustomEvent("icen:slider-input", {
20
+ bubbles: true,
21
+ detail: { value: Number(native.value) }
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
+ });
18
30
  sync();
19
31
  }
20
32
  function setupDual(slider, natives) {
@@ -47,8 +59,28 @@ function setupDual(slider, natives) {
47
59
  lo.style.clipPath = `inset(0 ${100 - pLo}% 0 0)`;
48
60
  hi.style.clipPath = `inset(0 0 0 ${pHi}%)`;
49
61
  };
50
- lo.addEventListener("input", () => sync(lo));
51
- hi.addEventListener("input", () => sync(hi));
62
+ lo.addEventListener("input", () => {
63
+ sync(lo);
64
+ slider.dispatchEvent(new CustomEvent("icen:slider-input", {
65
+ bubbles: true,
66
+ detail: { lo: Number(lo.value), hi: Number(hi.value) }
67
+ }));
68
+ });
69
+ hi.addEventListener("input", () => {
70
+ sync(hi);
71
+ slider.dispatchEvent(new CustomEvent("icen:slider-input", {
72
+ bubbles: true,
73
+ detail: { lo: Number(lo.value), hi: Number(hi.value) }
74
+ }));
75
+ });
76
+ const onChange = () => {
77
+ slider.dispatchEvent(new CustomEvent("icen:slider-change", {
78
+ bubbles: true,
79
+ detail: { lo: Number(lo.value), hi: Number(hi.value) }
80
+ }));
81
+ };
82
+ lo.addEventListener("change", onChange);
83
+ hi.addEventListener("change", onChange);
52
84
  sync(null);
53
85
  }
54
86
  function setup(slider) {
@@ -1,25 +1,55 @@
1
1
  // src/behaviors/tabs.ts
2
+ function tabButtons(container) {
3
+ return Array.from(container.querySelectorAll("[data-tab]"));
4
+ }
2
5
  function tabNames(container) {
3
- return Array.from(container.querySelectorAll("[data-tab]")).map((t) => t.getAttribute("data-tab")).filter((n) => n !== null);
6
+ return tabButtons(container).map((t) => t.getAttribute("data-tab")).filter((n) => n !== null);
4
7
  }
5
8
  function currentHash() {
6
9
  return window.location.hash.replace(/^#/, "");
7
10
  }
8
11
  function activate(container, name, writeHash) {
12
+ const buttons = tabButtons(container);
9
13
  let matched = false;
10
- container.querySelectorAll("[data-tab]").forEach((t) => {
14
+ buttons.forEach((t) => {
11
15
  const on = t.getAttribute("data-tab") === name;
12
16
  if (on) matched = true;
13
17
  t.classList.toggle("active", on);
18
+ t.setAttribute("aria-selected", String(on));
19
+ t.tabIndex = on ? 0 : -1;
14
20
  });
15
21
  if (!matched) return;
16
22
  container.querySelectorAll("[data-tab-panel]").forEach((p) => {
17
- p.hidden = p.getAttribute("data-tab-panel") !== name;
23
+ const isOn = p.getAttribute("data-tab-panel") === name;
24
+ p.hidden = !isOn;
25
+ if (isOn && !p.getAttribute("aria-labelledby")) {
26
+ const btn = buttons.find((b) => b.getAttribute("data-tab") === name);
27
+ if (btn?.id) p.setAttribute("aria-labelledby", btn.id);
28
+ }
18
29
  });
19
30
  if (writeHash && typeof window !== "undefined") {
20
31
  window.history.replaceState(null, "", `#${name}`);
21
32
  }
22
33
  }
34
+ function isDisabled(btn) {
35
+ return btn.hasAttribute("disabled") || btn.getAttribute("aria-disabled") === "true" || btn.classList.contains("is-disabled");
36
+ }
37
+ function focusTab(container, idx) {
38
+ const buttons = tabButtons(container);
39
+ const target = buttons[idx];
40
+ if (!target || isDisabled(target)) return;
41
+ target.focus();
42
+ }
43
+ function nextEnabled(container, from, dir) {
44
+ const buttons = tabButtons(container);
45
+ if (buttons.length === 0) return -1;
46
+ let idx = from;
47
+ for (let i = 0; i < buttons.length; i++) {
48
+ idx = (idx + dir + buttons.length) % buttons.length;
49
+ if (!isDisabled(buttons[idx])) return idx;
50
+ }
51
+ return -1;
52
+ }
23
53
  function setup(container) {
24
54
  const el = container;
25
55
  if (el.__icenTabsInit) return;
@@ -27,14 +57,69 @@ function setup(container) {
27
57
  const useHash = container.hasAttribute("data-tabs-hash");
28
58
  const names = tabNames(container);
29
59
  if (names.length === 0) return;
30
- container.addEventListener("click", (ev) => {
60
+ const buttons = tabButtons(container);
61
+ buttons.forEach((b) => {
62
+ if (!b.getAttribute("role")) b.setAttribute("role", "tab");
63
+ if (!b.hasAttribute("aria-selected")) b.setAttribute("aria-selected", "false");
64
+ });
65
+ const tablist = buttons[0]?.parentElement;
66
+ if (tablist && !tablist.getAttribute("role")) tablist.setAttribute("role", "tablist");
67
+ const onClick = (ev) => {
31
68
  const target = ev.target;
32
69
  if (!(target instanceof Element)) return;
33
70
  const btn = target.closest("[data-tab]");
34
71
  if (!btn || btn.closest("[data-tabs]") !== container) return;
72
+ if (btn instanceof HTMLElement && isDisabled(btn)) return;
35
73
  const name = btn.getAttribute("data-tab");
36
74
  if (name) activate(container, name, useHash);
37
- });
75
+ };
76
+ const onKeyDown = (ev) => {
77
+ const kev = ev;
78
+ const target = kev.target;
79
+ if (!(target instanceof Element)) return;
80
+ const btn = target.closest("[data-tab]");
81
+ if (!btn || btn.closest("[data-tabs]") !== container) return;
82
+ const buttonsList = tabButtons(container);
83
+ const currentIdx = buttonsList.indexOf(btn);
84
+ if (currentIdx === -1) return;
85
+ switch (kev.key) {
86
+ case "ArrowRight":
87
+ case "ArrowDown": {
88
+ kev.preventDefault();
89
+ const next = nextEnabled(container, currentIdx, 1);
90
+ if (next >= 0) focusTab(container, next);
91
+ break;
92
+ }
93
+ case "ArrowLeft":
94
+ case "ArrowUp": {
95
+ kev.preventDefault();
96
+ const prev = nextEnabled(container, currentIdx, -1);
97
+ if (prev >= 0) focusTab(container, prev);
98
+ break;
99
+ }
100
+ case "Home": {
101
+ kev.preventDefault();
102
+ focusTab(container, 0);
103
+ break;
104
+ }
105
+ case "End": {
106
+ kev.preventDefault();
107
+ focusTab(container, buttonsList.length - 1);
108
+ break;
109
+ }
110
+ case "Enter":
111
+ case " ": {
112
+ if (btn instanceof HTMLElement && !isDisabled(btn)) {
113
+ kev.preventDefault();
114
+ const name = btn.getAttribute("data-tab");
115
+ if (name) activate(container, name, useHash);
116
+ }
117
+ break;
118
+ }
119
+ }
120
+ };
121
+ container.addEventListener("click", onClick);
122
+ container.addEventListener("keydown", onKeyDown);
38
123
  let initial;
39
124
  if (useHash) {
40
125
  const h = currentHash();
@@ -44,12 +129,18 @@ function setup(container) {
44
129
  if (n && names.includes(n)) initial = n;
45
130
  }
46
131
  activate(container, initial ?? names[0], false);
132
+ const onHash = () => {
133
+ const h = currentHash();
134
+ if (names.includes(h)) activate(container, h, false);
135
+ };
47
136
  if (useHash && typeof window !== "undefined") {
48
- window.addEventListener("hashchange", () => {
49
- const h = currentHash();
50
- if (names.includes(h)) activate(container, h, false);
51
- });
137
+ window.addEventListener("hashchange", onHash);
52
138
  }
139
+ el.__icenTabsCleanup = () => {
140
+ container.removeEventListener("click", onClick);
141
+ container.removeEventListener("keydown", onKeyDown);
142
+ window.removeEventListener("hashchange", onHash);
143
+ };
53
144
  }
54
145
  function initTabs(root = document) {
55
146
  if (typeof document === "undefined") return;
@@ -14,10 +14,20 @@ function setup(wrap) {
14
14
  wrap.setAttribute("data-tags", tags.join(","));
15
15
  wrap.classList.toggle("is-max", tags.length >= max);
16
16
  field.placeholder = tags.length === 0 ? placeholder : "";
17
+ if (!wrap.hasAttribute("aria-label")) {
18
+ wrap.setAttribute("aria-label", `\u6807\u7B7E\u8F93\u5165\uFF0C\u5171 ${tags.length} \u4E2A`);
19
+ }
20
+ }
21
+ function emit() {
22
+ wrap.dispatchEvent(new CustomEvent("icen:tags-change", {
23
+ bubbles: true,
24
+ detail: { tags: [...tags] }
25
+ }));
17
26
  }
18
27
  function remove(index) {
19
28
  tags.splice(index, 1);
20
29
  render();
30
+ emit();
21
31
  }
22
32
  function chip(tag, index) {
23
33
  const el2 = document.createElement("span");
@@ -50,6 +60,7 @@ function setup(wrap) {
50
60
  tags.push(v);
51
61
  field.value = "";
52
62
  render();
63
+ emit();
53
64
  }
54
65
  field.addEventListener("keydown", (ev) => {
55
66
  if (ev.key === "Enter" || ev.key === ",") {
@@ -65,8 +65,34 @@ function toggleDark() {
65
65
  function initTheme() {
66
66
  const t = getTheme();
67
67
  applyTheme(t);
68
+ if (isBrowser() && typeof window.matchMedia === "function" && !hasPersistedPreference()) {
69
+ let lastDark = t.dark;
70
+ try {
71
+ const mql = window.matchMedia("(prefers-color-scheme: dark)");
72
+ const onChange = (ev) => {
73
+ if (hasPersistedPreference()) return;
74
+ if (ev.matches === lastDark) return;
75
+ lastDark = ev.matches;
76
+ applyTheme({ ...getTheme(), dark: ev.matches });
77
+ };
78
+ if (typeof mql.addEventListener === "function") {
79
+ mql.addEventListener("change", onChange);
80
+ } else {
81
+ mql.addListener(onChange);
82
+ }
83
+ } catch {
84
+ }
85
+ }
68
86
  return t;
69
87
  }
88
+ function hasPersistedPreference() {
89
+ if (!isBrowser()) return false;
90
+ try {
91
+ return window.localStorage.getItem(STORAGE_KEY) !== null;
92
+ } catch {
93
+ return false;
94
+ }
95
+ }
70
96
  export {
71
97
  PRESETS,
72
98
  STORAGE_KEY,
@@ -1,35 +1,103 @@
1
1
  // src/behaviors/toast.ts
2
+ var DEFAULT_MS = 2600;
3
+ var ACTION_MS = 6e3;
4
+ var maxStack = 5;
5
+ function isBrowser() {
6
+ return typeof document !== "undefined";
7
+ }
2
8
  function ensureContainer() {
3
9
  const existing = document.body.querySelector(".toast-container");
4
10
  if (existing) return existing;
5
11
  const el = document.createElement("div");
6
12
  el.className = "toast-container";
13
+ el.setAttribute("role", "status");
14
+ el.setAttribute("aria-live", "polite");
15
+ el.setAttribute("aria-atomic", "false");
7
16
  document.body.appendChild(el);
8
17
  return el;
9
18
  }
10
- function show(message, kind = "info", ms = 2600) {
11
- if (typeof document === "undefined") return;
19
+ function trimStack(container) {
20
+ for (; ; ) {
21
+ const items = Array.from(container.querySelectorAll(":scope > .toast"));
22
+ if (items.length <= maxStack) return;
23
+ const oldest = items[0];
24
+ if (!oldest) return;
25
+ oldest.classList.add("leaving");
26
+ let done = false;
27
+ const finish = () => {
28
+ if (done) return;
29
+ done = true;
30
+ oldest.remove();
31
+ };
32
+ oldest.addEventListener("transitionend", finish, { once: true });
33
+ window.setTimeout(finish, 400);
34
+ }
35
+ }
36
+ function show(message, kind = "info", opts = {}) {
37
+ if (!isBrowser()) return;
38
+ const { ms, actionLabel, onAction } = opts;
39
+ const container = ensureContainer();
12
40
  const el = document.createElement("div");
13
41
  el.className = kind === "info" ? "toast" : `toast ${kind}`;
14
- el.textContent = message;
15
- ensureContainer().appendChild(el);
42
+ el.setAttribute("role", kind === "err" ? "alert" : "status");
43
+ const msgNode = document.createElement("span");
44
+ msgNode.className = "toast-msg";
45
+ msgNode.textContent = message;
46
+ el.appendChild(msgNode);
47
+ let actionBtn = null;
48
+ if (actionLabel) {
49
+ actionBtn = document.createElement("button");
50
+ actionBtn.type = "button";
51
+ actionBtn.className = "toast-action";
52
+ actionBtn.textContent = actionLabel;
53
+ el.appendChild(actionBtn);
54
+ }
55
+ container.appendChild(el);
56
+ trimStack(container);
16
57
  let removed = false;
17
58
  const remove = () => {
18
59
  if (removed) return;
19
60
  removed = true;
20
- el.remove();
21
- };
22
- window.setTimeout(() => {
23
61
  el.classList.add("leaving");
24
- el.addEventListener("transitionend", remove, { once: true });
25
- window.setTimeout(remove, 400);
26
- }, ms);
62
+ let done = false;
63
+ const finish = () => {
64
+ if (done) return;
65
+ done = true;
66
+ el.remove();
67
+ };
68
+ el.addEventListener("transitionend", finish, { once: true });
69
+ window.setTimeout(finish, 400);
70
+ };
71
+ const duration = ms ?? (actionLabel ? ACTION_MS : DEFAULT_MS);
72
+ const timer = window.setTimeout(remove, duration);
73
+ if (actionBtn && onAction) {
74
+ actionBtn.addEventListener("click", () => {
75
+ window.clearTimeout(timer);
76
+ const ret = onAction();
77
+ if (ret === false) return;
78
+ remove();
79
+ });
80
+ }
27
81
  }
28
- var toast = Object.assign(show, {
29
- ok: (message, ms) => show(message, "ok", ms),
30
- err: (message, ms) => show(message, "err", ms),
31
- warn: (message, ms) => show(message, "warn", ms)
32
- });
82
+ var toast = Object.assign(
83
+ (message, kind = "info", ms) => show(message, kind, { ms }),
84
+ {
85
+ ok: (message, ms) => show(message, "ok", { ms }),
86
+ err: (message, ms) => show(message, "err", { ms }),
87
+ warn: (message, ms) => show(message, "warn", { ms }),
88
+ info: (message, ms) => show(message, "info", { ms }),
89
+ action: (message, actionLabel, onAction, ms) => show(message, "info", { actionLabel, onAction, ms }),
90
+ dismiss: () => {
91
+ if (!isBrowser()) return;
92
+ const c = document.body.querySelector(".toast-container");
93
+ if (!c) return;
94
+ c.querySelectorAll(".toast").forEach((t) => t.remove());
95
+ },
96
+ config: (opts) => {
97
+ if (typeof opts.maxStack === "number" && opts.maxStack > 0) maxStack = opts.maxStack;
98
+ }
99
+ }
100
+ );
33
101
  export {
34
102
  toast
35
103
  };
@@ -3,6 +3,9 @@ function setup(tree) {
3
3
  const el = tree;
4
4
  if (el.__icenTreeInit) return;
5
5
  el.__icenTreeInit = true;
6
+ tree.querySelectorAll(".tree-node").forEach((node) => {
7
+ if (!node.hasAttribute("tabindex")) node.tabIndex = 0;
8
+ });
6
9
  tree.addEventListener("click", (ev) => {
7
10
  const target = ev.target;
8
11
  if (!(target instanceof Element)) return;
@@ -28,8 +31,61 @@ function setup(tree) {
28
31
  return;
29
32
  }
30
33
  if (childUl) return;
31
- tree.querySelectorAll(".tree-node.is-selected").forEach((n) => n.classList.remove("is-selected"));
34
+ tree.querySelectorAll('.tree-node.is-selected, .tree-node[aria-selected="true"]').forEach((n) => {
35
+ n.classList.remove("is-selected");
36
+ n.removeAttribute("aria-selected");
37
+ });
32
38
  node.classList.add("is-selected");
39
+ node.setAttribute("aria-selected", "true");
40
+ tree.dispatchEvent(new CustomEvent("icen:tree-select", {
41
+ bubbles: true,
42
+ detail: { node }
43
+ }));
44
+ });
45
+ tree.addEventListener("keydown", (ev) => {
46
+ const kev = ev;
47
+ const target = kev.target;
48
+ if (!(target instanceof Element)) return;
49
+ const node = target.closest(".tree-node");
50
+ if (!node || node.closest(".tree") !== tree) return;
51
+ if (node.classList.contains("is-disabled")) return;
52
+ const allNodes = Array.from(tree.querySelectorAll(".tree-node:not(.is-disabled)"));
53
+ const idx = allNodes.indexOf(node);
54
+ if (idx === -1) return;
55
+ switch (kev.key) {
56
+ case "ArrowDown": {
57
+ kev.preventDefault();
58
+ allNodes[(idx + 1) % allNodes.length]?.focus();
59
+ break;
60
+ }
61
+ case "ArrowUp": {
62
+ kev.preventDefault();
63
+ allNodes[(idx - 1 + allNodes.length) % allNodes.length]?.focus();
64
+ break;
65
+ }
66
+ case "ArrowRight": {
67
+ const li = node.closest("li");
68
+ const childUl = li?.querySelector(":scope > ul");
69
+ if (childUl?.hasAttribute("hidden")) {
70
+ kev.preventDefault();
71
+ childUl.removeAttribute("hidden");
72
+ node.querySelector(".tree-toggle")?.setAttribute("aria-expanded", "true");
73
+ node.querySelector(".tree-toggle")?.classList.remove("is-collapsed");
74
+ }
75
+ break;
76
+ }
77
+ case "ArrowLeft": {
78
+ const li = node.closest("li");
79
+ const childUl = li?.querySelector(":scope > ul");
80
+ if (childUl && !childUl.hasAttribute("hidden")) {
81
+ kev.preventDefault();
82
+ childUl.setAttribute("hidden", "");
83
+ node.querySelector(".tree-toggle")?.setAttribute("aria-expanded", "false");
84
+ node.querySelector(".tree-toggle")?.classList.add("is-collapsed");
85
+ }
86
+ break;
87
+ }
88
+ }
33
89
  });
34
90
  }
35
91
  function initTree(root = document) {
@@ -37,10 +37,9 @@ function setup(zone) {
37
37
  }
38
38
  if (assigned) {
39
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
40
  }
41
+ const list = Array.from(files);
42
+ zone.dispatchEvent(new CustomEvent("icen:upload", { bubbles: true, detail: { files: list } }));
44
43
  });
45
44
  }
46
45
  function initUpload(root = document) {