@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.
Files changed (151) hide show
  1. package/AGENTS.md +262 -0
  2. package/README.md +52 -13
  3. package/dist/base.css +33 -5
  4. package/dist/behaviors/accordion.d.mts +7 -0
  5. package/dist/behaviors/accordion.mjs +37 -11
  6. package/dist/behaviors/ai-chat.d.mts +59 -0
  7. package/dist/behaviors/ai-chat.mjs +580 -0
  8. package/dist/behaviors/ai-composer.d.mts +137 -0
  9. package/dist/behaviors/ai-composer.mjs +2114 -0
  10. package/dist/behaviors/ai-core.d.mts +156 -0
  11. package/dist/behaviors/ai-core.mjs +504 -0
  12. package/dist/behaviors/ai-diff.d.mts +53 -0
  13. package/dist/behaviors/ai-diff.mjs +413 -0
  14. package/dist/behaviors/ai-panel.d.mts +101 -0
  15. package/dist/behaviors/ai-panel.mjs +1087 -0
  16. package/dist/behaviors/ai-provider.d.mts +198 -0
  17. package/dist/behaviors/ai-provider.mjs +1061 -0
  18. package/dist/behaviors/ai-tool.d.mts +26 -0
  19. package/dist/behaviors/ai-tool.mjs +2173 -0
  20. package/dist/behaviors/ai-tools.d.mts +84 -0
  21. package/dist/behaviors/ai-tools.mjs +1762 -0
  22. package/dist/behaviors/back-top.d.mts +20 -0
  23. package/dist/behaviors/back-top.mjs +64 -32
  24. package/dist/behaviors/carousel.d.mts +7 -0
  25. package/dist/behaviors/carousel.mjs +72 -27
  26. package/dist/behaviors/charts.d.mts +296 -0
  27. package/dist/behaviors/charts.mjs +1364 -440
  28. package/dist/behaviors/command-palette.d.mts +42 -0
  29. package/dist/behaviors/command-palette.mjs +167 -27
  30. package/dist/behaviors/context-menu.d.mts +53 -0
  31. package/dist/behaviors/context-menu.mjs +127 -39
  32. package/dist/behaviors/controls.d.mts +59 -0
  33. package/dist/behaviors/controls.mjs +601 -0
  34. package/dist/behaviors/copy.d.mts +7 -0
  35. package/dist/behaviors/copy.mjs +41 -15
  36. package/dist/behaviors/datatable.d.mts +90 -0
  37. package/dist/behaviors/datatable.mjs +279 -53
  38. package/dist/behaviors/date-picker.d.mts +4 -0
  39. package/dist/behaviors/date-picker.mjs +276 -69
  40. package/dist/behaviors/dropdown.d.mts +8 -0
  41. package/dist/behaviors/dropdown.mjs +171 -28
  42. package/dist/behaviors/events.d.mts +330 -0
  43. package/dist/behaviors/events.mjs +208 -0
  44. package/dist/behaviors/input.d.mts +4 -0
  45. package/dist/behaviors/input.mjs +52 -32
  46. package/dist/behaviors/modal.d.mts +30 -0
  47. package/dist/behaviors/modal.mjs +87 -16
  48. package/dist/behaviors/nav.d.mts +7 -0
  49. package/dist/behaviors/nav.mjs +57 -15
  50. package/dist/behaviors/notification.d.mts +188 -0
  51. package/dist/behaviors/notification.mjs +274 -140
  52. package/dist/behaviors/popover.d.mts +82 -0
  53. package/dist/behaviors/popover.mjs +92 -5
  54. package/dist/behaviors/select.d.mts +6 -0
  55. package/dist/behaviors/select.mjs +287 -45
  56. package/dist/behaviors/sidebar.d.mts +7 -0
  57. package/dist/behaviors/sidebar.mjs +44 -13
  58. package/dist/behaviors/slider.d.mts +7 -0
  59. package/dist/behaviors/slider.mjs +55 -43
  60. package/dist/behaviors/split-pane.d.mts +8 -0
  61. package/dist/behaviors/split-pane.mjs +70 -29
  62. package/dist/behaviors/tabs.d.mts +9 -0
  63. package/dist/behaviors/tabs.mjs +102 -26
  64. package/dist/behaviors/tag-input.d.mts +7 -0
  65. package/dist/behaviors/tag-input.mjs +66 -17
  66. package/dist/behaviors/theme.d.mts +36 -0
  67. package/dist/behaviors/theme.mjs +51 -5
  68. package/dist/behaviors/toast.d.mts +48 -0
  69. package/dist/behaviors/toast.mjs +98 -25
  70. package/dist/behaviors/tree.d.mts +7 -0
  71. package/dist/behaviors/tree.mjs +39 -15
  72. package/dist/behaviors/upload.d.mts +4 -0
  73. package/dist/behaviors/upload.mjs +79 -44
  74. package/dist/cli.mjs +1 -1
  75. package/dist/components/ai-chat.css +1037 -0
  76. package/dist/components/ai-chat.mjs +3 -0
  77. package/dist/components/ai-composer.mjs +3 -0
  78. package/dist/components/ai-context.mjs +2 -0
  79. package/dist/components/ai-diff.css +311 -0
  80. package/dist/components/ai-diff.mjs +3 -0
  81. package/dist/components/ai-files.mjs +3 -0
  82. package/dist/components/ai-message.mjs +3 -0
  83. package/dist/components/ai-panel.css +682 -0
  84. package/dist/components/ai-reasoning.mjs +3 -0
  85. package/dist/components/ai-subagent.mjs +2 -0
  86. package/dist/components/ai-todo.mjs +2 -0
  87. package/dist/components/ai-tool-call.mjs +2 -0
  88. package/dist/components/ai-tool.css +532 -0
  89. package/dist/components/ai-usage.mjs +3 -0
  90. package/dist/components/badge.css +5 -9
  91. package/dist/components/breadcrumb.css +0 -36
  92. package/dist/components/btn.css +4 -7
  93. package/dist/components/card.css +3 -6
  94. package/dist/components/chart-bar.css +10 -0
  95. package/dist/components/chart-calendar.mjs +4 -0
  96. package/dist/components/chart-gauge.css +1 -1
  97. package/dist/components/chart-heatmap.css +40 -0
  98. package/dist/components/chart-pie.css +0 -6
  99. package/dist/components/chart-scatter.css +24 -0
  100. package/dist/components/chart-scatter.mjs +4 -0
  101. package/dist/components/charts.css +87 -2
  102. package/dist/components/charts.mjs +1 -0
  103. package/dist/components/command-palette.css +2 -2
  104. package/dist/components/content.css +11 -25
  105. package/dist/components/copy.css +8 -8
  106. package/dist/components/datatable.css +5 -9
  107. package/dist/components/date-picker.css +4 -8
  108. package/dist/components/empty.css +0 -4
  109. package/dist/components/feedback.css +10 -17
  110. package/dist/components/form.css +13 -14
  111. package/dist/components/input.css +2 -2
  112. package/dist/components/layout.css +36 -36
  113. package/dist/components/media.css +12 -8
  114. package/dist/components/menu.css +2 -2
  115. package/dist/components/modal.css +1 -1
  116. package/dist/components/nav.css +11 -6
  117. package/dist/components/notification.css +27 -28
  118. package/dist/components/pagination.css +4 -2
  119. package/dist/components/pagination.mjs +1 -0
  120. package/dist/components/panel.css +16 -16
  121. package/dist/components/pill.css +15 -15
  122. package/dist/components/rating.mjs +1 -0
  123. package/dist/components/segmented.css +14 -6
  124. package/dist/components/segmented.mjs +1 -0
  125. package/dist/components/select.css +79 -24
  126. package/dist/components/sidebar.css +1 -1
  127. package/dist/components/split-pane.css +0 -1
  128. package/dist/components/stat.css +14 -18
  129. package/dist/components/steps.css +15 -5
  130. package/dist/components/steps.mjs +1 -0
  131. package/dist/components/switch.css +11 -13
  132. package/dist/components/switch.mjs +1 -0
  133. package/dist/components/table.css +46 -47
  134. package/dist/components/table.mjs +1 -0
  135. package/dist/components/tabs.css +57 -45
  136. package/dist/components/tag-input.css +5 -10
  137. package/dist/components/toast.css +16 -13
  138. package/dist/components/tooltip.css +2 -2
  139. package/dist/components/upload.css +0 -15
  140. package/dist/index.d.mts +39 -0
  141. package/dist/index.mjs +11243 -3312
  142. package/dist/registry.json +147 -9
  143. package/dist/retro-effects.css +23 -32
  144. package/dist/tokens/colors.css +4 -1
  145. package/dist/tokens/retro-effects.css +23 -32
  146. package/dist/tokens/style-profiles.css +24 -2
  147. package/dist/tokens/typography.css +2 -0
  148. package/dist/tokens.css +30 -3
  149. package/dist/ui.css +3420 -645
  150. package/docs/spec/ai-native.md +662 -0
  151. package/package.json +15 -5
@@ -1,30 +1,54 @@
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/tag-input.ts
2
9
  function setup(wrap) {
3
10
  const el = wrap;
4
- if (el.__icenTagInit) return;
11
+ if (el.__icenTagInit) return void 0;
5
12
  el.__icenTagInit = true;
6
13
  const fieldEl = wrap.querySelector(".tag-input-field");
7
- if (!fieldEl) return;
14
+ if (!fieldEl) return void 0;
8
15
  const field = fieldEl;
9
16
  const maxAttr = wrap.getAttribute("data-max");
10
17
  const max = maxAttr ? Number(maxAttr) || Infinity : Infinity;
11
18
  const placeholder = field.placeholder;
19
+ let alive = true;
20
+ let isComposing = false;
21
+ const onCompositionStart = () => {
22
+ isComposing = true;
23
+ };
24
+ const onCompositionEnd = () => {
25
+ isComposing = false;
26
+ };
12
27
  let tags = (wrap.getAttribute("data-tags") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
28
+ const autoLabel = !wrap.hasAttribute("aria-label");
13
29
  function sync() {
14
30
  wrap.setAttribute("data-tags", tags.join(","));
15
31
  wrap.classList.toggle("is-max", tags.length >= max);
16
32
  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
- }
33
+ if (autoLabel) wrap.setAttribute("aria-label", `\u6807\u7B7E\u8F93\u5165\uFF0C\u5171 ${tags.length} \u4E2A`);
20
34
  }
21
35
  function emit() {
22
- wrap.dispatchEvent(new CustomEvent("icen:tags-change", {
23
- bubbles: true,
24
- detail: { tags: [...tags] }
25
- }));
36
+ if (!alive) return;
37
+ emitIcen(wrap, "icen:tags-change", { tags: [...tags] });
38
+ }
39
+ let shakeTimer = null;
40
+ function shake() {
41
+ el.classList.remove("is-shake");
42
+ void el.offsetWidth;
43
+ el.classList.add("is-shake");
44
+ if (shakeTimer) clearTimeout(shakeTimer);
45
+ shakeTimer = setTimeout(() => {
46
+ el.classList.remove("is-shake");
47
+ shakeTimer = null;
48
+ }, 320);
26
49
  }
27
50
  function remove(index) {
51
+ if (!alive) return;
28
52
  tags.splice(index, 1);
29
53
  render();
30
54
  emit();
@@ -50,11 +74,13 @@ function setup(wrap) {
50
74
  sync();
51
75
  }
52
76
  function commit() {
77
+ if (!alive) return;
53
78
  const v = field.value.trim();
54
79
  if (!v) return;
55
80
  if (tags.length >= max) return;
56
81
  if (tags.includes(v)) {
57
82
  field.value = "";
83
+ shake();
58
84
  return;
59
85
  }
60
86
  tags.push(v);
@@ -62,23 +88,46 @@ function setup(wrap) {
62
88
  render();
63
89
  emit();
64
90
  }
65
- field.addEventListener("keydown", (ev) => {
91
+ const onKeyDown = (ev) => {
92
+ if (isComposing || ev.isComposing) return;
66
93
  if (ev.key === "Enter" || ev.key === ",") {
67
94
  ev.preventDefault();
68
95
  commit();
69
96
  } else if (ev.key === "Backspace" && !field.value && tags.length > 0) {
70
97
  remove(tags.length - 1);
71
98
  }
72
- });
99
+ };
100
+ field.addEventListener("compositionstart", onCompositionStart);
101
+ field.addEventListener("compositionend", onCompositionEnd);
102
+ field.addEventListener("keydown", onKeyDown);
73
103
  field.addEventListener("blur", commit);
74
104
  render();
105
+ return () => {
106
+ alive = false;
107
+ if (shakeTimer) clearTimeout(shakeTimer);
108
+ field.removeEventListener("compositionstart", onCompositionStart);
109
+ field.removeEventListener("compositionend", onCompositionEnd);
110
+ field.removeEventListener("keydown", onKeyDown);
111
+ field.removeEventListener("blur", commit);
112
+ el.__icenTagInit = false;
113
+ };
75
114
  }
76
- function initTagInput(root = document) {
77
- if (typeof document === "undefined") return;
78
- const containers = [];
79
- if (root instanceof HTMLElement && root.matches(".tag-input[data-tags]")) containers.push(root);
80
- containers.push(...Array.from(root.querySelectorAll(".tag-input[data-tags]")));
81
- for (const c of containers) setup(c);
115
+ function initTagInput(root) {
116
+ const cleanups = [];
117
+ if (typeof document !== "undefined") {
118
+ const scope = root ?? document;
119
+ const containers = [];
120
+ if (scope instanceof HTMLElement && scope.matches(".tag-input")) containers.push(scope);
121
+ containers.push(...Array.from(scope.querySelectorAll(".tag-input")));
122
+ for (const c of containers) {
123
+ const cleanup = setup(c);
124
+ if (cleanup) cleanups.push(cleanup);
125
+ }
126
+ }
127
+ return () => {
128
+ for (const fn of cleanups) fn();
129
+ cleanups.length = 0;
130
+ };
82
131
  }
83
132
  export {
84
133
  initTagInput
@@ -0,0 +1,36 @@
1
+ interface ThemeState {
2
+ preset: string;
3
+ dark: boolean;
4
+ style: string;
5
+ }
6
+ declare const PRESETS: readonly ["clay", "piano", "art", "vangogh", "ink", "retro"];
7
+ declare const STYLES: readonly ["modern", "retro", "terminal"];
8
+ declare const STORAGE_KEY = "icen.ui.theme";
9
+ /**
10
+ * 注册宿主自建预设(宿主先在自家 CSS 写好 `.<name>` 与 `.<name>.dark` 完整 token 面,
11
+ * 仿照 colors.css 现有 6 套)。名字须命中 /^[a-z][a-z0-9-]{1,20}$/ 且不与
12
+ * dark / style-* 保留字冲突;注册只影响 listThemePresets() 枚举(UI 切换器用),
13
+ * 不注册的自建预设同样可以 setTheme(校验同样放行)。返回是否接受注册。
14
+ */
15
+ declare function registerThemePreset(name: string): boolean;
16
+ /** 全部可用预设名:内置 6 套 + 已注册的自建预设(UI 切换器枚举用,顺序稳定)。 */
17
+ declare function listThemePresets(): string[];
18
+ /** 默认主题:clay + 跟随系统明暗 + modern。SSR 下返回离线兜底值。 */
19
+ declare function getDefaultTheme(): ThemeState;
20
+ /** 读 localStorage 中的持久化主题;缺失或坏数据回退默认。 */
21
+ declare function getTheme(): ThemeState;
22
+ /** 把主题应用到 <html> 并在状态实际变化时派发 icen:theme-change(见私有 apply)。 */
23
+ declare function applyTheme(t: ThemeState): void;
24
+ /** 合并当前主题与 patch,应用并持久化,返回新态。patch 中的非法枚举值回退默认。 */
25
+ declare function setTheme(patch: Partial<ThemeState>): ThemeState;
26
+ /** 切换明暗,返回新态。 */
27
+ declare function toggleDark(): ThemeState;
28
+ /** 只切风格 profile(modern / retro / terminal;非法值回退 modern),返回新态。 */
29
+ declare function setStyle(style: string): ThemeState;
30
+ /** 页面加载早期调用:读持久化主题并应用,防闪烁。
31
+ * 无持久化偏好时,自动跟随系统的 prefers-color-scheme 变化(实时);
32
+ * 一旦用户显式 setTheme / toggleDark,跟随停止(持久化优先)。
33
+ * 重复调用幂等(系统监听不会重复挂)。 */
34
+ declare function initTheme(): ThemeState;
35
+
36
+ export { PRESETS, STORAGE_KEY, STYLES, type ThemeState, applyTheme, getDefaultTheme, getTheme, initTheme, listThemePresets, registerThemePreset, setStyle, setTheme, toggleDark };
@@ -1,11 +1,32 @@
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/theme.ts
2
9
  var PRESETS = ["clay", "piano", "art", "vangogh", "ink", "retro"];
3
10
  var STYLES = ["modern", "retro", "terminal"];
4
11
  var STORAGE_KEY = "icen.ui.theme";
12
+ var PRESET_NAME_RE = /^[a-z][a-z0-9-]{1,20}$/;
13
+ var RESERVED_PRESET_NAMES = /* @__PURE__ */ new Set(["dark", ...STYLES.map((s) => `style-${s}`)]);
14
+ var customPresets = /* @__PURE__ */ new Set();
5
15
  var FALLBACK = { preset: "clay", dark: false, style: "modern" };
6
16
  function isBrowser() {
7
17
  return typeof document !== "undefined";
8
18
  }
19
+ function isValidPreset(name) {
20
+ return PRESETS.includes(name) || PRESET_NAME_RE.test(name) && !RESERVED_PRESET_NAMES.has(name);
21
+ }
22
+ function registerThemePreset(name) {
23
+ if (!PRESET_NAME_RE.test(name) || RESERVED_PRESET_NAMES.has(name)) return false;
24
+ customPresets.add(name);
25
+ return true;
26
+ }
27
+ function listThemePresets() {
28
+ return [...PRESETS, ...customPresets];
29
+ }
9
30
  function getDefaultTheme() {
10
31
  if (!isBrowser() || typeof window.matchMedia !== "function") {
11
32
  return { ...FALLBACK };
@@ -19,7 +40,8 @@ function getDefaultTheme() {
19
40
  function isThemeState(v) {
20
41
  if (typeof v !== "object" || v === null) return false;
21
42
  const t = v;
22
- return typeof t.preset === "string" && PRESETS.includes(t.preset) && typeof t.dark === "boolean" && typeof t.style === "string" && STYLES.includes(t.style);
43
+ return typeof t.preset === "string" && /* 持久化路径与 setTheme 同一开放白名单:自建预设跨刷新存活 */
44
+ isValidPreset(t.preset) && typeof t.dark === "boolean" && typeof t.style === "string" && STYLES.includes(t.style);
23
45
  }
24
46
  function getTheme() {
25
47
  if (!isBrowser()) return getDefaultTheme();
@@ -33,24 +55,40 @@ function getTheme() {
33
55
  return getDefaultTheme();
34
56
  }
35
57
  }
36
- function applyTheme(t) {
58
+ var lastApplied = null;
59
+ function apply(t) {
37
60
  if (!isBrowser()) return;
38
61
  const cls = document.documentElement.classList;
62
+ if (lastApplied && !PRESETS.includes(lastApplied.preset)) {
63
+ cls.remove(lastApplied.preset);
64
+ }
39
65
  for (const p of PRESETS) cls.remove(p);
40
66
  for (const s of STYLES) cls.remove(`style-${s}`);
41
67
  cls.remove("dark");
42
68
  cls.add(t.preset);
43
69
  if (t.dark) cls.add("dark");
44
70
  cls.add(`style-${t.style}`);
71
+ const changed = lastApplied !== null && (lastApplied.preset !== t.preset || lastApplied.dark !== t.dark || lastApplied.style !== t.style);
72
+ lastApplied = { ...t };
73
+ if (changed) {
74
+ emitIcen(document.documentElement, "icen:theme-change", {
75
+ preset: t.preset,
76
+ dark: t.dark,
77
+ style: t.style
78
+ });
79
+ }
80
+ }
81
+ function applyTheme(t) {
82
+ apply(t);
45
83
  }
46
84
  function setTheme(patch) {
47
85
  const merged = { ...getTheme(), ...patch };
48
86
  const next = {
49
- preset: PRESETS.includes(merged.preset) ? merged.preset : FALLBACK.preset,
87
+ preset: isValidPreset(merged.preset) ? merged.preset : FALLBACK.preset,
50
88
  dark: merged.dark === true,
51
89
  style: STYLES.includes(merged.style) ? merged.style : FALLBACK.style
52
90
  };
53
- applyTheme(next);
91
+ apply(next);
54
92
  if (isBrowser()) {
55
93
  try {
56
94
  window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
@@ -62,10 +100,15 @@ function setTheme(patch) {
62
100
  function toggleDark() {
63
101
  return setTheme({ dark: !getTheme().dark });
64
102
  }
103
+ function setStyle(style) {
104
+ return setTheme({ style });
105
+ }
106
+ var systemListenerAttached = false;
65
107
  function initTheme() {
66
108
  const t = getTheme();
67
109
  applyTheme(t);
68
- if (isBrowser() && typeof window.matchMedia === "function" && !hasPersistedPreference()) {
110
+ if (isBrowser() && typeof window.matchMedia === "function" && !systemListenerAttached && !hasPersistedPreference()) {
111
+ systemListenerAttached = true;
69
112
  let lastDark = t.dark;
70
113
  try {
71
114
  const mql = window.matchMedia("(prefers-color-scheme: dark)");
@@ -101,6 +144,9 @@ export {
101
144
  getDefaultTheme,
102
145
  getTheme,
103
146
  initTheme,
147
+ listThemePresets,
148
+ registerThemePreset,
149
+ setStyle,
104
150
  setTheme,
105
151
  toggleDark
106
152
  };
@@ -0,0 +1,48 @@
1
+ type ToastKind = 'ok' | 'err' | 'warn' | 'info' | 'success' | 'error';
2
+ interface ToastOptions {
3
+ /** 保留时长(毫秒),默认 2600;带 action 时默认 6000 */
4
+ ms?: number;
5
+ /** 配合 action:可撤销 / 重试 / 查看等 */
6
+ actionLabel?: string;
7
+ /** action 触发回调;返回 false 阻止 toast 关闭 */
8
+ onAction?: () => void | Promise<void> | boolean;
9
+ /** 内联 SVG 字符串图标(消毒后渲染为 .toast-icon);非 <svg> 开头或解析失败静默忽略 */
10
+ icon?: string;
11
+ /** 显示右上角关闭按钮(.toast-close,点击即关闭本条),默认 false */
12
+ closable?: boolean;
13
+ /** 显示底部超时进度条(.toast-progress,动画时长 = 本条保留时长),默认 false */
14
+ progress?: boolean;
15
+ }
16
+ /** 单条 toast 句柄:dismiss() 立即关闭本条(幂等,可重复调用)。 */
17
+ interface ToastHandle {
18
+ dismiss(): void;
19
+ }
20
+ interface ToastConfig {
21
+ /** 同屏最大堆叠,默认 5;超出从最早的开始移除 */
22
+ maxStack?: number;
23
+ }
24
+ interface ToastFn {
25
+ (message: string, kind?: ToastKind, ms?: number): ToastHandle;
26
+ /** success 语义(.toast--success),规范词表入口 */
27
+ success(message: string, ms?: number): ToastHandle;
28
+ /**
29
+ * success 语义(.toast--success)。
30
+ * @deprecated 词表统一:改用 toast.success(),本别名下个大版本删除。
31
+ */
32
+ ok(message: string, ms?: number): ToastHandle;
33
+ /** error 语义(.toast--error),规范词表入口 */
34
+ error(message: string, ms?: number): ToastHandle;
35
+ /**
36
+ * error 语义(.toast--error)。
37
+ * @deprecated 词表统一:改用 toast.error(),本别名下个大版本删除。
38
+ */
39
+ err(message: string, ms?: number): ToastHandle;
40
+ warn(message: string, ms?: number): ToastHandle;
41
+ info(message: string, ms?: number): ToastHandle;
42
+ action(message: string, actionLabel: string, onAction: () => void, ms?: number): ToastHandle;
43
+ dismiss(): void;
44
+ config(opts: ToastConfig): void;
45
+ }
46
+ declare const toast: ToastFn;
47
+
48
+ export { type ToastConfig, type ToastHandle, type ToastKind, type ToastOptions, toast };
@@ -16,30 +16,85 @@ function ensureContainer() {
16
16
  document.body.appendChild(el);
17
17
  return el;
18
18
  }
19
+ var KIND_CLASS = {
20
+ ok: "toast--success",
21
+ success: "toast--success",
22
+ err: "toast--error",
23
+ error: "toast--error",
24
+ warn: "toast--warning",
25
+ info: "toast--info"
26
+ };
27
+ var CLOSE_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>';
28
+ function stripUnsafeSvg(el) {
29
+ for (const attr of Array.from(el.attributes)) {
30
+ if (attr.name.toLowerCase().startsWith("on")) el.removeAttribute(attr.name);
31
+ }
32
+ for (const child of Array.from(el.children)) {
33
+ const tag = child.tagName.toLowerCase();
34
+ if (tag === "script" || tag === "foreignobject") child.remove();
35
+ else stripUnsafeSvg(child);
36
+ }
37
+ }
38
+ function sanitizeSvgIcon(source) {
39
+ if (!source.trimStart().startsWith("<svg")) return null;
40
+ try {
41
+ const doc = new DOMParser().parseFromString(source, "image/svg+xml");
42
+ const svg = doc.documentElement;
43
+ if (!svg || svg.tagName.toLowerCase() !== "svg") return null;
44
+ stripUnsafeSvg(svg);
45
+ return document.importNode(svg, true);
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+ var closeSvgCache = null;
51
+ function closeSvg() {
52
+ closeSvgCache ??= sanitizeSvgIcon(CLOSE_SVG);
53
+ return closeSvgCache ? closeSvgCache.cloneNode(true) : null;
54
+ }
55
+ function scheduleRemove(el) {
56
+ el.classList.add("is-leaving");
57
+ let done = false;
58
+ const finish = () => {
59
+ if (done) return;
60
+ done = true;
61
+ el.remove();
62
+ };
63
+ el.addEventListener("transitionend", finish, { once: true });
64
+ window.setTimeout(finish, 400);
65
+ }
19
66
  function trimStack(container) {
20
67
  for (; ; ) {
21
68
  const items = Array.from(container.querySelectorAll(":scope > .toast"));
22
69
  if (items.length <= maxStack) return;
23
70
  const oldest = items[0];
24
71
  if (!oldest) return;
25
- oldest.classList.add("leaving");
26
- let done = false;
27
- const finish = () => {
28
- if (done) return;
29
- done = true;
72
+ if (oldest.classList.contains("is-leaving")) {
30
73
  oldest.remove();
31
- };
32
- oldest.addEventListener("transitionend", finish, { once: true });
33
- window.setTimeout(finish, 400);
74
+ continue;
75
+ }
76
+ scheduleRemove(oldest);
77
+ return;
34
78
  }
35
79
  }
36
80
  function show(message, kind = "info", opts = {}) {
37
- if (!isBrowser()) return;
38
- const { ms, actionLabel, onAction } = opts;
81
+ if (!isBrowser()) return { dismiss() {
82
+ } };
83
+ const { ms, actionLabel, onAction, icon, closable, progress } = opts;
39
84
  const container = ensureContainer();
40
85
  const el = document.createElement("div");
41
- el.className = kind === "info" ? "toast" : `toast ${kind}`;
42
- el.setAttribute("role", kind === "err" ? "alert" : "status");
86
+ el.className = `toast ${KIND_CLASS[kind]}`;
87
+ el.setAttribute("role", kind === "err" || kind === "error" ? "alert" : "status");
88
+ if (icon) {
89
+ const svg = sanitizeSvgIcon(icon);
90
+ if (svg) {
91
+ const iconWrap = document.createElement("span");
92
+ iconWrap.className = "toast-icon";
93
+ iconWrap.setAttribute("aria-hidden", "true");
94
+ iconWrap.appendChild(svg);
95
+ el.appendChild(iconWrap);
96
+ }
97
+ }
43
98
  const msgNode = document.createElement("span");
44
99
  msgNode.className = "toast-msg";
45
100
  msgNode.textContent = message;
@@ -52,24 +107,39 @@ function show(message, kind = "info", opts = {}) {
52
107
  actionBtn.textContent = actionLabel;
53
108
  el.appendChild(actionBtn);
54
109
  }
55
- container.appendChild(el);
56
- trimStack(container);
110
+ const duration = ms ?? (actionLabel ? ACTION_MS : DEFAULT_MS);
111
+ let timer = 0;
57
112
  let removed = false;
58
113
  const remove = () => {
59
114
  if (removed) return;
60
115
  removed = true;
61
- el.classList.add("leaving");
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);
116
+ if (timer) window.clearTimeout(timer);
117
+ scheduleRemove(el);
70
118
  };
71
- const duration = ms ?? (actionLabel ? ACTION_MS : DEFAULT_MS);
72
- const timer = window.setTimeout(remove, duration);
119
+ if (closable) {
120
+ const closeBtn = document.createElement("button");
121
+ closeBtn.type = "button";
122
+ closeBtn.className = "toast-close";
123
+ closeBtn.setAttribute("aria-label", "\u5173\u95ED");
124
+ const svg = closeSvg();
125
+ if (svg) closeBtn.appendChild(svg);
126
+ else closeBtn.textContent = "\xD7";
127
+ closeBtn.addEventListener("click", remove);
128
+ el.appendChild(closeBtn);
129
+ }
130
+ if (progress) {
131
+ const bar = document.createElement("div");
132
+ bar.className = "toast-progress";
133
+ bar.setAttribute("aria-hidden", "true");
134
+ const fill = document.createElement("div");
135
+ fill.className = "toast-progress-fill";
136
+ fill.style.animationDuration = `${duration}ms`;
137
+ bar.appendChild(fill);
138
+ el.appendChild(bar);
139
+ }
140
+ container.appendChild(el);
141
+ trimStack(container);
142
+ timer = window.setTimeout(remove, duration);
73
143
  if (actionBtn && onAction) {
74
144
  actionBtn.addEventListener("click", () => {
75
145
  window.clearTimeout(timer);
@@ -78,11 +148,14 @@ function show(message, kind = "info", opts = {}) {
78
148
  remove();
79
149
  });
80
150
  }
151
+ return { dismiss: remove };
81
152
  }
82
153
  var toast = Object.assign(
83
154
  (message, kind = "info", ms) => show(message, kind, { ms }),
84
155
  {
156
+ success: (message, ms) => show(message, "success", { ms }),
85
157
  ok: (message, ms) => show(message, "ok", { ms }),
158
+ error: (message, ms) => show(message, "error", { ms }),
86
159
  err: (message, ms) => show(message, "err", { ms }),
87
160
  warn: (message, ms) => show(message, "warn", { ms }),
88
161
  info: (message, ms) => show(message, "info", { ms }),
@@ -0,0 +1,7 @@
1
+ /**
2
+ * 为 root 下每棵 .tree 初始化(root 自身是 .tree 也算)。
3
+ * 返回销毁函数:移除本次挂上的容器监听并复位幂等标记(销毁后可重新 init)。SSR 下返回 no-op。
4
+ */
5
+ declare function initTree(root?: ParentNode): () => void;
6
+
7
+ export { initTree };
@@ -1,12 +1,19 @@
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/tree.ts
2
9
  function setup(tree) {
3
10
  const el = tree;
4
- if (el.__icenTreeInit) return;
11
+ if (el.__icenTreeInit) return void 0;
5
12
  el.__icenTreeInit = true;
6
13
  tree.querySelectorAll(".tree-node").forEach((node) => {
7
14
  if (!node.hasAttribute("tabindex")) node.tabIndex = 0;
8
15
  });
9
- tree.addEventListener("click", (ev) => {
16
+ const onClick = (ev) => {
10
17
  const target = ev.target;
11
18
  if (!(target instanceof Element)) return;
12
19
  const node = target.closest(".tree-node");
@@ -28,6 +35,7 @@ function setup(tree) {
28
35
  toggle?.classList.add("is-collapsed");
29
36
  toggle?.setAttribute("aria-expanded", "false");
30
37
  }
38
+ emitIcen(tree, "icen:tree-toggle", { node: li, open: !collapsed });
31
39
  return;
32
40
  }
33
41
  if (childUl) return;
@@ -37,12 +45,9 @@ function setup(tree) {
37
45
  });
38
46
  node.classList.add("is-selected");
39
47
  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) => {
48
+ emitIcen(tree, "icen:tree-select", { node });
49
+ };
50
+ const onKeyDown = (ev) => {
46
51
  const kev = ev;
47
52
  const target = kev.target;
48
53
  if (!(target instanceof Element)) return;
@@ -71,6 +76,7 @@ function setup(tree) {
71
76
  childUl.removeAttribute("hidden");
72
77
  node.querySelector(".tree-toggle")?.setAttribute("aria-expanded", "true");
73
78
  node.querySelector(".tree-toggle")?.classList.remove("is-collapsed");
79
+ if (li) emitIcen(tree, "icen:tree-toggle", { node: li, open: true });
74
80
  }
75
81
  break;
76
82
  }
@@ -82,18 +88,36 @@ function setup(tree) {
82
88
  childUl.setAttribute("hidden", "");
83
89
  node.querySelector(".tree-toggle")?.setAttribute("aria-expanded", "false");
84
90
  node.querySelector(".tree-toggle")?.classList.add("is-collapsed");
91
+ if (li) emitIcen(tree, "icen:tree-toggle", { node: li, open: false });
85
92
  }
86
93
  break;
87
94
  }
88
95
  }
89
- });
96
+ };
97
+ tree.addEventListener("click", onClick);
98
+ tree.addEventListener("keydown", onKeyDown);
99
+ return () => {
100
+ tree.removeEventListener("click", onClick);
101
+ tree.removeEventListener("keydown", onKeyDown);
102
+ el.__icenTreeInit = false;
103
+ };
90
104
  }
91
- function initTree(root = document) {
92
- if (typeof document === "undefined") return;
93
- const trees = [];
94
- if (root instanceof Element && root.matches(".tree")) trees.push(root);
95
- trees.push(...Array.from(root.querySelectorAll(".tree")));
96
- for (const t of trees) setup(t);
105
+ function initTree(root) {
106
+ const cleanups = [];
107
+ if (typeof document !== "undefined") {
108
+ const scope = root ?? document;
109
+ const trees = [];
110
+ if (scope instanceof Element && scope.matches(".tree")) trees.push(scope);
111
+ trees.push(...Array.from(scope.querySelectorAll(".tree")));
112
+ for (const t of trees) {
113
+ const cleanup = setup(t);
114
+ if (cleanup) cleanups.push(cleanup);
115
+ }
116
+ }
117
+ return () => {
118
+ for (const fn of cleanups) fn();
119
+ cleanups.length = 0;
120
+ };
97
121
  }
98
122
  export {
99
123
  initTree
@@ -0,0 +1,4 @@
1
+ /** 为 root 下每个 .upload 容器初始化(root 自身是 .upload 也算)。 */
2
+ declare function initUpload(root?: ParentNode): void;
3
+
4
+ export { initUpload };