@h-ai/ui 0.1.0-alpha.26 → 0.1.0-alpha.28

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.
@@ -4,10 +4,17 @@
4
4
  =============================================================================
5
5
  Toast 容器组件,显示所有 Toast 通知
6
6
 
7
- 使用 Svelte 5 Runes ($derived)
7
+ 使用 Svelte 5 Runes ($derived / $effect)
8
+
9
+ 叠加策略(关键):原生 <dialog>.showModal() 处于浏览器 top layer,且在 Chromium 中
10
+ 始终渲染在 Popover 之上。为保证 Toast 永远可见、不被弹窗遮挡:
11
+ - 当存在已打开的 modal dialog 时,将 Toast 层寄宿到该 dialog 内部(同处 top layer,
12
+ 作为最后子节点渲染在其内容之上);
13
+ - 否则寄宿到 <body> 并以 Popover 进入 top layer,叠加在普通内容之上。
8
14
  =============================================================================
9
15
  -->
10
16
  <script lang='ts'>
17
+ import { onMount } from 'svelte'
11
18
  import { uiM } from '../../messages.js'
12
19
  import { toast } from '../../toast.svelte.js'
13
20
  import { cn, getAlertVariantClass } from '../../utils.js'
@@ -22,6 +29,11 @@
22
29
  'bottom-center': 'toast-bottom toast-center',
23
30
  }
24
31
 
32
+ // 容器根元素引用。
33
+ let layerElement: HTMLDivElement | undefined = $state()
34
+
35
+ const hasItems = $derived(toast.items.length > 0)
36
+
25
37
  // 按位置分组
26
38
  const groupedItems = $derived(() => {
27
39
  const groups: Record<string, typeof toast.items> = {}
@@ -36,24 +48,151 @@
36
48
 
37
49
  return groups
38
50
  })
51
+
52
+ /** 找到最顶层处于打开状态的原生 modal dialog(位于浏览器 top layer)。 */
53
+ function findTopModalDialog(): HTMLDialogElement | null {
54
+ if (typeof document === 'undefined') {
55
+ return null
56
+ }
57
+
58
+ const dialogs = document.querySelectorAll<HTMLDialogElement>('dialog')
59
+ let top: HTMLDialogElement | null = null
60
+ for (const dialog of dialogs) {
61
+ if (dialog.open && typeof dialog.matches === 'function' && dialog.matches(':modal')) {
62
+ top = dialog
63
+ }
64
+ }
65
+ return top
66
+ }
67
+
68
+ function safeShowPopover(el: HTMLElement) {
69
+ try {
70
+ if (typeof el.showPopover === 'function' && el.isConnected && !el.matches(':popover-open')) {
71
+ el.showPopover()
72
+ }
73
+ }
74
+ catch {
75
+ // 元素未连接或重复调用时忽略。
76
+ }
77
+ }
78
+
79
+ function safeHidePopover(el: HTMLElement) {
80
+ try {
81
+ if (typeof el.hidePopover === 'function' && el.matches(':popover-open')) {
82
+ el.hidePopover()
83
+ }
84
+ }
85
+ catch {
86
+ // 同上,忽略异常。
87
+ }
88
+ }
89
+
90
+ /**
91
+ * 将 Toast 层寄宿到正确的容器:
92
+ * - 有打开的 modal dialog 时,去掉 popover 属性并寄宿到该 dialog 内部,渲染在其内容之上;
93
+ * - 否则寄宿到 <body>,恢复 popover 属性并按需进入 / 退出 top layer。
94
+ */
95
+ function relocate() {
96
+ const el = layerElement
97
+ if (!el) {
98
+ return
99
+ }
100
+
101
+ const modal = hasItems ? findTopModalDialog() : null
102
+
103
+ if (modal) {
104
+ // 寄宿到 dialog:popover 元素未显示时会 display:none,故先移除 popover 属性。
105
+ safeHidePopover(el)
106
+ if (el.hasAttribute('popover')) {
107
+ el.removeAttribute('popover')
108
+ }
109
+ if (el.parentElement !== modal) {
110
+ modal.appendChild(el)
111
+ }
112
+ return
113
+ }
114
+
115
+ if (el.parentElement !== document.body) {
116
+ document.body.appendChild(el)
117
+ }
118
+ if (!el.hasAttribute('popover')) {
119
+ el.setAttribute('popover', 'manual')
120
+ }
121
+ if (hasItems) {
122
+ safeShowPopover(el)
123
+ }
124
+ else {
125
+ safeHidePopover(el)
126
+ }
127
+ }
128
+
129
+ onMount(() => {
130
+ relocate()
131
+
132
+ // dialog 通过 showModal()/close() 切换 `open` 属性;据此重新寄宿 Toast 层。
133
+ const observer = new MutationObserver(() => relocate())
134
+ observer.observe(document.body, { subtree: true, attributes: true, attributeFilter: ['open'] })
135
+
136
+ return () => {
137
+ observer.disconnect()
138
+ const el = layerElement
139
+ if (el) {
140
+ safeHidePopover(el)
141
+ el.remove()
142
+ }
143
+ }
144
+ })
145
+
146
+ // Toast 数量变化时重新寄宿(出现 / 消失、以及在 dialog 与 body 之间切换)。
147
+ $effect(() => {
148
+ void hasItems
149
+ relocate()
150
+ })
39
151
  </script>
40
152
 
41
- {#each Object.entries(groupedItems()) as [position, items] (position)}
42
- <div class={cn('toast z-50', positionMap[position as keyof typeof positionMap])}>
43
- {#each items as item (item.id)}
44
- <div class={cn('alert', getAlertVariantClass(item.variant ?? 'info'))}>
45
- <span>{item.message}</span>
46
- {#if item.dismissible}
47
- <IconButton
48
- variant='ghost'
49
- size='sm'
50
- onclick={() => toast.remove(item.id)}
51
- ariaLabel={uiM('toast_dismiss')}
52
- >
53
- ✕
54
- </IconButton>
55
- {/if}
56
- </div>
57
- {/each}
58
- </div>
59
- {/each}
153
+ <div bind:this={layerElement} popover='manual' class='hai-toast-layer'>
154
+ {#each Object.entries(groupedItems()) as [position, items] (position)}
155
+ <div class={cn('toast', positionMap[position as keyof typeof positionMap])}>
156
+ {#each items as item (item.id)}
157
+ <div class={cn('alert', getAlertVariantClass(item.variant ?? 'info'))}>
158
+ <span>{item.message}</span>
159
+ {#if item.dismissible}
160
+ <IconButton
161
+ variant='ghost'
162
+ size='sm'
163
+ onclick={() => toast.remove(item.id)}
164
+ ariaLabel={uiM('toast_dismiss')}
165
+ >
166
+ ✕
167
+ </IconButton>
168
+ {/if}
169
+ </div>
170
+ {/each}
171
+ </div>
172
+ {/each}
173
+ </div>
174
+
175
+ <style>
176
+ /* 容器铺满视口但不拦截交互;仅 toast 本体接收指针事件。 */
177
+ .hai-toast-layer {
178
+ position: fixed;
179
+ inset: 0;
180
+ width: 100%;
181
+ height: 100%;
182
+ max-width: none;
183
+ max-height: none;
184
+ margin: 0;
185
+ padding: 0;
186
+ border: 0;
187
+ background: transparent;
188
+ overflow: visible;
189
+ pointer-events: none;
190
+ }
191
+
192
+ /* 未支持 Popover API 的浏览器忽略 popover 属性,容器常驻文档流;
193
+ 此时无 toast 项则内部不渲染任何内容,容器透明且不拦截交互。 */
194
+
195
+ .hai-toast-layer :global(.toast) {
196
+ pointer-events: auto;
197
+ }
198
+ </style>
@@ -25,6 +25,9 @@ export { default as Empty } from './Empty.svelte';
25
25
  export { default as Form } from './Form.svelte';
26
26
  export { default as FormField } from './FormField.svelte';
27
27
  export { default as InfiniteScroll } from './InfiniteScroll.svelte';
28
+ export { default as MessageBoxContainer } from './MessageBoxContainer.svelte';
29
+ export { default as MessageBoxDialog } from './MessageBoxDialog.svelte';
30
+ export { default as MessageContainer } from './MessageContainer.svelte';
28
31
  export { default as Modal } from './Modal.svelte';
29
32
  export { default as PageHeader } from './PageHeader.svelte';
30
33
  export { default as Pagination } from './Pagination.svelte';
@@ -33,6 +33,11 @@ export { default as Form } from './Form.svelte';
33
33
  export { default as FormField } from './FormField.svelte';
34
34
  // 无限滚动 / 上拉加载
35
35
  export { default as InfiniteScroll } from './InfiniteScroll.svelte';
36
+ // MessageBox 命令式弹框(类似 ElementUI MessageBox)
37
+ export { default as MessageBoxContainer } from './MessageBoxContainer.svelte';
38
+ export { default as MessageBoxDialog } from './MessageBoxDialog.svelte';
39
+ // Message 轻量消息提示(类似 ElementUI Message)
40
+ export { default as MessageContainer } from './MessageContainer.svelte';
36
41
  export { default as Modal } from './Modal.svelte';
37
42
  // 页面头部
38
43
  export { default as PageHeader } from './PageHeader.svelte';
@@ -97,7 +97,7 @@
97
97
  }
98
98
  </script>
99
99
 
100
- <div class={cn('', className)}>
100
+ <div class={cn('theme-color-picker', className)}>
101
101
  <div class='flex flex-wrap items-center gap-2'>
102
102
  {#each normalizedPresets as preset (preset.value)}
103
103
  <button
@@ -165,7 +165,7 @@
165
165
  </div>
166
166
 
167
167
  <style>
168
- button {
168
+ .theme-color-picker button {
169
169
  border-color: var(--soft-border, color-mix(in oklab, var(--color-base-content) 12%, transparent));
170
170
  background: var(--soft-surface, var(--color-base-100));
171
171
  color: var(--soft-text-body, var(--color-base-content));
@@ -173,7 +173,7 @@
173
173
  font-weight: 500;
174
174
  }
175
175
 
176
- button:hover:not(:disabled) {
176
+ .theme-color-picker button:hover:not(:disabled) {
177
177
  border-color: color-mix(in oklab, var(--color-primary) 28%, var(--soft-border, transparent) 72%);
178
178
  background: color-mix(
179
179
  in oklab,
@@ -182,7 +182,7 @@
182
182
  );
183
183
  }
184
184
 
185
- button[aria-pressed='true'] {
185
+ .theme-color-picker button[aria-pressed='true'] {
186
186
  border-color: color-mix(in oklab, var(--color-primary) 42%, var(--soft-border, transparent) 58%);
187
187
  background: color-mix(
188
188
  in oklab,
@@ -192,14 +192,14 @@
192
192
  color: var(--color-primary);
193
193
  }
194
194
 
195
- button:focus,
196
- button:focus-visible {
195
+ .theme-color-picker button:focus,
196
+ .theme-color-picker button:focus-visible {
197
197
  outline: none;
198
198
  border-color: color-mix(in oklab, var(--color-primary) 48%, var(--soft-border, transparent) 52%);
199
199
  box-shadow: 0 0 0 4px color-mix(in oklab, var(--color-primary) 12%, transparent 88%);
200
200
  }
201
201
 
202
- button > span:first-child {
202
+ .theme-color-picker button > span:first-child {
203
203
  border-color: color-mix(in oklab, var(--color-base-content) 10%, transparent 90%);
204
204
  box-shadow: none;
205
205
  }
package/dist/index.d.ts CHANGED
@@ -6,6 +6,8 @@
6
6
  */
7
7
  export * from './components/index.js';
8
8
  export * from './i18n.svelte.js';
9
+ export * from './message.svelte.js';
10
+ export * from './messagebox.svelte.js';
9
11
  export * from './theme-config.js';
10
12
  export * from './toast.svelte.js';
11
13
  export * from './types.js';
package/dist/index.js CHANGED
@@ -8,6 +8,10 @@
8
8
  export * from './components/index.js';
9
9
  // ─── i18n 国际化(Paraglide 辅助工具) ───
10
10
  export * from './i18n.svelte.js';
11
+ // ─── Message 轻量消息提示(类似 ElementUI Message) ───
12
+ export * from './message.svelte.js';
13
+ // ─── MessageBox 命令式弹框(类似 ElementUI MessageBox) ───
14
+ export * from './messagebox.svelte.js';
11
15
  // ─── 主题配置 ───
12
16
  export * from './theme-config.js';
13
17
  // ─── Toast 状态管理 ───
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @h-ai/ui — Message 状态管理
3
+ *
4
+ * 命令式轻量消息提示 API,类似 ElementUI Message。
5
+ * 与 Toast 类似但视觉风格不同:顶部居中滑入、带类型图标、可自动关闭。
6
+ * @module message.svelte
7
+ */
8
+ import type { MessageOptions } from './types.js';
9
+ /**
10
+ * 内部消息条目
11
+ */
12
+ export interface MessageItem {
13
+ id: string;
14
+ message: string;
15
+ type: 'info' | 'success' | 'warning' | 'error';
16
+ duration: number;
17
+ closable: boolean;
18
+ }
19
+ /**
20
+ * Message 状态
21
+ */
22
+ declare class MessageState {
23
+ items: MessageItem[];
24
+ private timers;
25
+ /**
26
+ * 添加消息
27
+ */
28
+ add(options: MessageOptions | string): string;
29
+ /**
30
+ * 移除消息
31
+ */
32
+ remove(id: string): void;
33
+ /**
34
+ * 清空所有消息
35
+ */
36
+ clear(): void;
37
+ /**
38
+ * 快捷方法
39
+ */
40
+ info(message: string, duration?: number): string;
41
+ success(message: string, duration?: number): string;
42
+ warning(message: string, duration?: number): string;
43
+ error(message: string, duration?: number): string;
44
+ }
45
+ /**
46
+ * Message 单例
47
+ */
48
+ export declare const message: MessageState;
49
+ export {};
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @h-ai/ui — Message 状态管理
3
+ *
4
+ * 命令式轻量消息提示 API,类似 ElementUI Message。
5
+ * 与 Toast 类似但视觉风格不同:顶部居中滑入、带类型图标、可自动关闭。
6
+ * @module message.svelte
7
+ */
8
+ /**
9
+ * Message 状态
10
+ */
11
+ class MessageState {
12
+ items = $state([]);
13
+ timers = new Map();
14
+ /**
15
+ * 添加消息
16
+ */
17
+ add(options) {
18
+ const opts = typeof options === 'string' ? { message: options } : options;
19
+ const id = Math.random().toString(36).slice(2, 9);
20
+ const item = {
21
+ id,
22
+ message: opts.message,
23
+ type: opts.type ?? 'info',
24
+ duration: opts.duration ?? 3000,
25
+ closable: opts.closable ?? false,
26
+ };
27
+ this.items = [...this.items, item];
28
+ if (item.duration > 0) {
29
+ const timer = setTimeout(() => {
30
+ this.timers.delete(id);
31
+ this.remove(id);
32
+ }, item.duration);
33
+ this.timers.set(id, timer);
34
+ }
35
+ return id;
36
+ }
37
+ /**
38
+ * 移除消息
39
+ */
40
+ remove(id) {
41
+ const timer = this.timers.get(id);
42
+ if (timer) {
43
+ clearTimeout(timer);
44
+ this.timers.delete(id);
45
+ }
46
+ this.items = this.items.filter(i => i.id !== id);
47
+ }
48
+ /**
49
+ * 清空所有消息
50
+ */
51
+ clear() {
52
+ for (const timer of this.timers.values()) {
53
+ clearTimeout(timer);
54
+ }
55
+ this.timers.clear();
56
+ this.items = [];
57
+ }
58
+ /**
59
+ * 快捷方法
60
+ */
61
+ info(message, duration) {
62
+ return this.add({ message, type: 'info', duration });
63
+ }
64
+ success(message, duration) {
65
+ return this.add({ message, type: 'success', duration });
66
+ }
67
+ warning(message, duration) {
68
+ return this.add({ message, type: 'warning', duration });
69
+ }
70
+ error(message, duration) {
71
+ return this.add({ message, type: 'error', duration });
72
+ }
73
+ }
74
+ /**
75
+ * Message 单例
76
+ */
77
+ export const message = new MessageState();
@@ -0,0 +1,82 @@
1
+ /**
2
+ * @h-ai/ui — MessageBox 状态管理
3
+ *
4
+ * 命令式弹框 API,类似 ElementUI MessageBox。
5
+ * 调用 `messageBox.confirm(...)` 或 `messageBox.alert(...)` 返回 Promise<boolean>,
6
+ * 组件内部通过渲染 MessageBoxContainer 消费状态。
7
+ * @module messagebox.svelte
8
+ */
9
+ import type { MessageBoxIconType, MessageBoxOptions } from './types.js';
10
+ /**
11
+ * 内部弹框条目
12
+ */
13
+ export interface MessageBoxItem {
14
+ id: string;
15
+ title: string;
16
+ message: string;
17
+ type: 'confirm' | 'alert';
18
+ iconType: MessageBoxIconType;
19
+ confirmText: string;
20
+ cancelText: string;
21
+ confirmVariant: 'default' | 'primary' | 'warning' | 'error';
22
+ showCancel: boolean;
23
+ closeOnClickModal: boolean;
24
+ showClose: boolean;
25
+ loading: boolean;
26
+ /** 外部 resolve 回调 */
27
+ resolve: (value: boolean) => void;
28
+ /** 可选的 beforeClose 钩子 */
29
+ beforeClose?: MessageBoxOptions['beforeClose'];
30
+ }
31
+ /**
32
+ * MessageBox 状态
33
+ */
34
+ declare class MessageBoxState {
35
+ items: MessageBoxItem[];
36
+ private createItem;
37
+ /**
38
+ * 确认弹框(双按钮,返回 Promise<boolean>)
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * const ok = await messageBox.confirm({
43
+ * title: '提示',
44
+ * message: '确定要删除吗?',
45
+ * confirmText: '确定',
46
+ * cancelText: '取消',
47
+ * })
48
+ * if (ok) { ... }
49
+ * ```
50
+ */
51
+ confirm(options: MessageBoxOptions | string): Promise<boolean>;
52
+ /**
53
+ * 提示弹框(单按钮,返回 Promise<boolean>)
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * await messageBox.alert({
58
+ * title: '提示',
59
+ * message: '操作成功',
60
+ * })
61
+ * ```
62
+ */
63
+ alert(options: MessageBoxOptions | string): Promise<boolean>;
64
+ /**
65
+ * 内部方法:处理确认
66
+ */
67
+ handleConfirm(item: MessageBoxItem): Promise<void>;
68
+ /**
69
+ * 内部方法:处理取消
70
+ */
71
+ handleCancel(item: MessageBoxItem): Promise<void>;
72
+ /**
73
+ * 内部方法:处理关闭(点击关闭按钮或遮罩)
74
+ */
75
+ handleClose(item: MessageBoxItem): Promise<void>;
76
+ private remove;
77
+ }
78
+ /**
79
+ * MessageBox 单例
80
+ */
81
+ export declare const messageBox: MessageBoxState;
82
+ export {};
@@ -0,0 +1,129 @@
1
+ /**
2
+ * @h-ai/ui — MessageBox 状态管理
3
+ *
4
+ * 命令式弹框 API,类似 ElementUI MessageBox。
5
+ * 调用 `messageBox.confirm(...)` 或 `messageBox.alert(...)` 返回 Promise<boolean>,
6
+ * 组件内部通过渲染 MessageBoxContainer 消费状态。
7
+ * @module messagebox.svelte
8
+ */
9
+ import { uiM } from './messages.js';
10
+ /**
11
+ * MessageBox 状态
12
+ */
13
+ class MessageBoxState {
14
+ items = $state([]);
15
+ createItem(options) {
16
+ return new Promise((resolve) => {
17
+ const id = Math.random().toString(36).slice(2, 9);
18
+ const isAlert = options.type === 'alert';
19
+ const item = {
20
+ id,
21
+ title: options.title ?? uiM('messagebox_title'),
22
+ message: options.message,
23
+ type: options.type ?? 'confirm',
24
+ iconType: options.iconType ?? (isAlert ? 'info' : 'warning'),
25
+ confirmText: options.confirmText ?? '',
26
+ cancelText: options.cancelText ?? '',
27
+ confirmVariant: options.confirmVariant ?? 'primary',
28
+ showCancel: options.showCancel ?? !isAlert,
29
+ closeOnClickModal: options.closeOnClickModal ?? true,
30
+ showClose: options.showClose ?? true,
31
+ loading: false,
32
+ resolve,
33
+ beforeClose: options.beforeClose,
34
+ };
35
+ this.items = [...this.items, item];
36
+ });
37
+ }
38
+ /**
39
+ * 确认弹框(双按钮,返回 Promise<boolean>)
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * const ok = await messageBox.confirm({
44
+ * title: '提示',
45
+ * message: '确定要删除吗?',
46
+ * confirmText: '确定',
47
+ * cancelText: '取消',
48
+ * })
49
+ * if (ok) { ... }
50
+ * ```
51
+ */
52
+ confirm(options) {
53
+ const opts = typeof options === 'string' ? { message: options } : options;
54
+ return this.createItem({ type: 'confirm', ...opts });
55
+ }
56
+ /**
57
+ * 提示弹框(单按钮,返回 Promise<boolean>)
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * await messageBox.alert({
62
+ * title: '提示',
63
+ * message: '操作成功',
64
+ * })
65
+ * ```
66
+ */
67
+ alert(options) {
68
+ const opts = typeof options === 'string' ? { message: options } : options;
69
+ return this.createItem({ type: 'alert', showCancel: false, ...opts });
70
+ }
71
+ /**
72
+ * 内部方法:处理确认
73
+ */
74
+ async handleConfirm(item) {
75
+ if (item.beforeClose) {
76
+ const canClose = await item.beforeClose('confirm', {
77
+ setLoading: (loading) => {
78
+ item.loading = loading;
79
+ },
80
+ });
81
+ if (canClose === false) {
82
+ return;
83
+ }
84
+ }
85
+ this.remove(item.id);
86
+ item.resolve(true);
87
+ }
88
+ /**
89
+ * 内部方法:处理取消
90
+ */
91
+ async handleCancel(item) {
92
+ if (item.beforeClose) {
93
+ const canClose = await item.beforeClose('cancel', {
94
+ setLoading: (loading) => {
95
+ item.loading = loading;
96
+ },
97
+ });
98
+ if (canClose === false) {
99
+ return;
100
+ }
101
+ }
102
+ this.remove(item.id);
103
+ item.resolve(false);
104
+ }
105
+ /**
106
+ * 内部方法:处理关闭(点击关闭按钮或遮罩)
107
+ */
108
+ async handleClose(item) {
109
+ if (item.beforeClose) {
110
+ const canClose = await item.beforeClose('close', {
111
+ setLoading: (loading) => {
112
+ item.loading = loading;
113
+ },
114
+ });
115
+ if (canClose === false) {
116
+ return;
117
+ }
118
+ }
119
+ this.remove(item.id);
120
+ item.resolve(false);
121
+ }
122
+ remove(id) {
123
+ this.items = this.items.filter(i => i.id !== id);
124
+ }
125
+ }
126
+ /**
127
+ * MessageBox 单例
128
+ */
129
+ export const messageBox = new MessageBoxState();
@@ -29,6 +29,7 @@
29
29
  "confirm_message": "Are you sure you want to proceed?",
30
30
  "confirm_ok": "Confirm",
31
31
  "confirm_cancel": "Cancel",
32
+ "messagebox_title": "Tip",
32
33
  "empty_title": "No data",
33
34
  "tag_input_placeholder": "Press Enter to add",
34
35
  "common_close": "Close",
@@ -29,6 +29,7 @@
29
29
  "confirm_message": "确定要继续吗?",
30
30
  "confirm_ok": "确认",
31
31
  "confirm_cancel": "取消",
32
+ "messagebox_title": "提示",
32
33
  "empty_title": "暂无数据",
33
34
  "tag_input_placeholder": "按 Enter 添加",
34
35
  "common_close": "关闭",