@h-ai/ui 0.1.0-alpha.27 → 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.
package/auto-import.d.ts CHANGED
@@ -57,6 +57,9 @@ declare global {
57
57
  const Form: typeof import('@h-ai/ui')['Form']
58
58
  const FormField: typeof import('@h-ai/ui')['FormField']
59
59
  const InfiniteScroll: typeof import('@h-ai/ui')['InfiniteScroll']
60
+ const MessageContainer: typeof import('@h-ai/ui')['MessageContainer']
61
+ const MessageBoxContainer: typeof import('@h-ai/ui')['MessageBoxContainer']
62
+ const MessageBoxDialog: typeof import('@h-ai/ui')['MessageBoxDialog']
60
63
  const Modal: typeof import('@h-ai/ui')['Modal']
61
64
  const PageHeader: typeof import('@h-ai/ui')['PageHeader']
62
65
  const Pagination: typeof import('@h-ai/ui')['Pagination']
package/auto-import.js CHANGED
@@ -48,6 +48,9 @@ const UI_COMPONENTS = new Set([
48
48
  'Form',
49
49
  'FormField',
50
50
  'InfiniteScroll',
51
+ 'MessageContainer',
52
+ 'MessageBoxContainer',
53
+ 'MessageBoxDialog',
51
54
  'Modal',
52
55
  'PageHeader',
53
56
  'Pagination',
@@ -0,0 +1,18 @@
1
+ <!--
2
+ =============================================================================
3
+ @h-ai/ui - MessageBoxContainer 组件
4
+ =============================================================================
5
+ 命令式弹框容器,渲染 messageBox store 中的所有弹框。
6
+ 每个弹框由 MessageBoxDialog 独立管理 showModal()/close()。
7
+
8
+ 使用 DaisyUI 样式 + tabler icon,不依赖 ElementUI。
9
+ =============================================================================
10
+ -->
11
+ <script lang='ts'>
12
+ import { messageBox } from '../../messagebox.svelte.js'
13
+ import MessageBoxDialog from './MessageBoxDialog.svelte'
14
+ </script>
15
+
16
+ {#each messageBox.items as item (item.id)}
17
+ <MessageBoxDialog {item} />
18
+ {/each}
@@ -0,0 +1,18 @@
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ declare const MessageBoxContainer: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type MessageBoxContainer = InstanceType<typeof MessageBoxContainer>;
18
+ export default MessageBoxContainer;
@@ -0,0 +1,193 @@
1
+ <!--
2
+ 单个 MessageBox 弹框,由 MessageBoxContainer 渲染。
3
+ 内部使用原生 <dialog>.showModal() 进入浏览器 top layer。
4
+ 行为对标 ElementUI MessageBox:
5
+ - 始终显示标题(默认"提示")和右上角关闭按钮;
6
+ - 遮罩层点击可通过 closeOnClickModal 配置。
7
+ -->
8
+ <script lang='ts'>
9
+ import type { MessageBoxItem } from '../../messagebox.svelte.js'
10
+ import { messageBox } from '../../messagebox.svelte.js'
11
+ import { uiM } from '../../messages.js'
12
+ import { cn } from '../../utils.js'
13
+ import Button from '../primitives/Button.svelte'
14
+
15
+ const { item }: { item: MessageBoxItem } = $props()
16
+
17
+ /** 图标类型到 tabler icon 的映射 */
18
+ const iconMap: Record<string, string> = {
19
+ info: 'icon-[tabler--info-circle]',
20
+ success: 'icon-[tabler--circle-check]',
21
+ warning: 'icon-[tabler--alert-triangle]',
22
+ error: 'icon-[tabler--circle-x]',
23
+ }
24
+
25
+ /** 图标背景色映射 */
26
+ const iconBgMap: Record<string, string> = {
27
+ info: 'bg-info/15 text-info',
28
+ success: 'bg-success/15 text-success',
29
+ warning: 'bg-warning/15 text-warning',
30
+ error: 'bg-error/15 text-error',
31
+ }
32
+
33
+ let dialogEl: HTMLDialogElement | undefined = $state()
34
+
35
+ $effect(() => {
36
+ if (dialogEl && !dialogEl.open) {
37
+ dialogEl.showModal()
38
+ }
39
+ })
40
+
41
+ function handleBackdropClick(e: MouseEvent) {
42
+ if (e.target === e.currentTarget && item.closeOnClickModal) {
43
+ messageBox.handleClose(item)
44
+ }
45
+ }
46
+ </script>
47
+
48
+ <!-- 使用 hai-messagebox 而非 DaisyUI .modal,避免 ::backdrop 被隐藏 -->
49
+ <dialog
50
+ bind:this={dialogEl}
51
+ class='hai-messagebox'
52
+ oncancel={e => e.preventDefault()}
53
+ onclick={handleBackdropClick}
54
+ >
55
+ <div class='hai-messagebox__panel'>
56
+ <!-- 标题栏:始终显示,右侧关闭按钮 -->
57
+ <div class='hai-messagebox__header'>
58
+ <h3 class='font-bold text-lg'>{item.title}</h3>
59
+ <button
60
+ type='button'
61
+ class='hai-messagebox__close'
62
+ aria-label={uiM('common_close')}
63
+ onclick={() => messageBox.handleClose(item)}
64
+ >
65
+ <span class='icon-[tabler--x] size-5'></span>
66
+ </button>
67
+ </div>
68
+
69
+ <!-- 图标 + 消息内容同行 -->
70
+ <div class='hai-messagebox__body flex items-center gap-3'>
71
+ {#if item.iconType}
72
+ <span class={cn(
73
+ 'inline-flex items-center justify-center rounded-full w-10 h-10 shrink-0',
74
+ iconBgMap[item.iconType] ?? iconBgMap.info,
75
+ )}>
76
+ <span class={cn(iconMap[item.iconType] ?? iconMap.info, 'size-5')}></span>
77
+ </span>
78
+ {/if}
79
+ <p class='flex-1 min-w-0 text-base-content/80 text-sm leading-relaxed'>{item.message}</p>
80
+ </div>
81
+
82
+ <!-- 操作按钮 -->
83
+ <div class='hai-messagebox__footer'>
84
+ {#if item.showCancel}
85
+ <Button
86
+ variant='ghost'
87
+ disabled={item.loading}
88
+ onclick={() => messageBox.handleCancel(item)}
89
+ >
90
+ {item.cancelText || uiM('confirm_cancel')}
91
+ </Button>
92
+ {/if}
93
+ <Button
94
+ variant={item.confirmVariant || 'primary'}
95
+ loading={item.loading}
96
+ disabled={item.loading}
97
+ onclick={() => messageBox.handleConfirm(item)}
98
+ >
99
+ {item.confirmText || uiM('confirm_ok')}
100
+ </Button>
101
+ </div>
102
+ </div>
103
+ </dialog>
104
+
105
+ <style>
106
+ dialog.hai-messagebox {
107
+ margin: 0;
108
+ padding: 0;
109
+ border: 0;
110
+ background: transparent;
111
+ outline: none;
112
+ overflow: visible;
113
+ position: fixed;
114
+ inset: 0;
115
+ width: 100%;
116
+ height: 100%;
117
+ display: grid;
118
+ place-items: center;
119
+ z-index: 999;
120
+ }
121
+
122
+ dialog.hai-messagebox::backdrop {
123
+ background: color-mix(in srgb, var(--color-base-content) 25%, transparent);
124
+ backdrop-filter: blur(2px);
125
+ }
126
+
127
+ .hai-messagebox__panel {
128
+ position: relative;
129
+ width: min(100%, 28rem);
130
+ max-width: calc(100vw - 2rem);
131
+ max-height: calc(100vh - 4rem);
132
+ background: var(--color-base-100);
133
+ border-radius: 1rem;
134
+ padding: 1.5rem;
135
+ box-shadow: 0 25px 50px -12px oklch(0% 0 0 / 0.25);
136
+ animation: hai-msgbox-enter 0.22s cubic-bezier(0.16, 1, 0.3, 1);
137
+ overflow-y: auto;
138
+ }
139
+
140
+ .hai-messagebox__header {
141
+ display: flex;
142
+ align-items: center;
143
+ justify-content: space-between;
144
+ margin-bottom: 0.75rem;
145
+ }
146
+
147
+ .hai-messagebox__close {
148
+ display: inline-flex;
149
+ align-items: center;
150
+ justify-content: center;
151
+ width: 1.5rem;
152
+ height: 1.5rem;
153
+ color: color-mix(in srgb, var(--color-base-content) 45%, transparent);
154
+ cursor: pointer;
155
+ border: none;
156
+ background: none;
157
+ padding: 0;
158
+ outline: none;
159
+ transition: color 160ms ease;
160
+ }
161
+
162
+ .hai-messagebox__close:hover {
163
+ color: var(--color-base-content);
164
+ }
165
+
166
+ .hai-messagebox__close:focus-visible {
167
+ outline: 2px solid color-mix(in srgb, var(--color-primary) 40%, transparent);
168
+ outline-offset: 2px;
169
+ border-radius: 2px;
170
+ }
171
+
172
+ .hai-messagebox__body {
173
+ margin-bottom: 1rem;
174
+ }
175
+
176
+ .hai-messagebox__footer {
177
+ display: flex;
178
+ justify-content: flex-end;
179
+ gap: 0.5rem;
180
+ margin-top: 0.5rem;
181
+ }
182
+
183
+ @keyframes hai-msgbox-enter {
184
+ from {
185
+ opacity: 0;
186
+ transform: translateY(10px) scale(0.985);
187
+ }
188
+ to {
189
+ opacity: 1;
190
+ transform: translateY(0) scale(1);
191
+ }
192
+ }
193
+ </style>
@@ -0,0 +1,7 @@
1
+ import type { MessageBoxItem } from '../../messagebox.svelte.js';
2
+ type $$ComponentProps = {
3
+ item: MessageBoxItem;
4
+ };
5
+ declare const MessageBoxDialog: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type MessageBoxDialog = ReturnType<typeof MessageBoxDialog>;
7
+ export default MessageBoxDialog;
@@ -0,0 +1,200 @@
1
+ <!--
2
+ =============================================================================
3
+ @h-ai/ui - MessageContainer 组件
4
+ =============================================================================
5
+ 轻量消息提示容器,渲染 message store 中的消息条目。
6
+ 视觉风格:顶部居中、带类型图标、自动滑入滑出。
7
+
8
+ 叠加策略与 ToastContainer 一致:
9
+ - 有打开的 modal dialog 时,寄宿到 dialog 内部(top layer);
10
+ - 否则寄宿到 <body> 以 popover 进入 top layer。
11
+ =============================================================================
12
+ -->
13
+ <script lang='ts'>
14
+ import { onMount } from 'svelte'
15
+ import { message } from '../../message.svelte.js'
16
+ import { cn } from '../../utils.js'
17
+ import IconButton from '../primitives/IconButton.svelte'
18
+
19
+ /** 图标类型到 tabler icon 的映射 */
20
+ const iconMap: Record<string, string> = {
21
+ info: 'icon-[tabler--info-circle]',
22
+ success: 'icon-[tabler--circle-check]',
23
+ warning: 'icon-[tabler--alert-triangle]',
24
+ error: 'icon-[tabler--circle-x]',
25
+ }
26
+
27
+ /** 图标颜色映射 */
28
+ const iconColorMap: Record<string, string> = {
29
+ info: 'text-info',
30
+ success: 'text-success',
31
+ warning: 'text-warning',
32
+ error: 'text-error',
33
+ }
34
+
35
+ /** 背景/边框映射 */
36
+ const bgMap: Record<string, string> = {
37
+ info: 'border-info/20 bg-info/5',
38
+ success: 'border-success/20 bg-success/5',
39
+ warning: 'border-warning/20 bg-warning/5',
40
+ error: 'border-error/20 bg-error/5',
41
+ }
42
+
43
+ let layerElement: HTMLDivElement | undefined = $state()
44
+
45
+ const hasItems = $derived(message.items.length > 0)
46
+
47
+ /** 找到最顶层处于打开状态的原生 modal dialog。 */
48
+ function findTopModalDialog(): HTMLDialogElement | null {
49
+ if (typeof document === 'undefined')
50
+ return null
51
+ const dialogs = document.querySelectorAll<HTMLDialogElement>('dialog')
52
+ let top: HTMLDialogElement | null = null
53
+ for (const dialog of dialogs) {
54
+ if (dialog.open && typeof dialog.matches === 'function' && dialog.matches(':modal')) {
55
+ top = dialog
56
+ }
57
+ }
58
+ return top
59
+ }
60
+
61
+ function safeShowPopover(el: HTMLElement) {
62
+ try {
63
+ if (typeof el.showPopover === 'function' && el.isConnected && !el.matches(':popover-open')) {
64
+ el.showPopover()
65
+ }
66
+ }
67
+ catch { /* 忽略异常 */ }
68
+ }
69
+
70
+ function safeHidePopover(el: HTMLElement) {
71
+ try {
72
+ if (typeof el.hidePopover === 'function' && el.matches(':popover-open')) {
73
+ el.hidePopover()
74
+ }
75
+ }
76
+ catch { /* 忽略异常 */ }
77
+ }
78
+
79
+ function relocate() {
80
+ const el = layerElement
81
+ if (!el)
82
+ return
83
+
84
+ const modal = hasItems ? findTopModalDialog() : null
85
+
86
+ if (modal) {
87
+ safeHidePopover(el)
88
+ if (el.hasAttribute('popover'))
89
+ el.removeAttribute('popover')
90
+ if (el.parentElement !== modal)
91
+ modal.appendChild(el)
92
+ return
93
+ }
94
+
95
+ if (el.parentElement !== document.body)
96
+ document.body.appendChild(el)
97
+ if (!el.hasAttribute('popover'))
98
+ el.setAttribute('popover', 'manual')
99
+ if (hasItems) {
100
+ safeShowPopover(el)
101
+ }
102
+ else {
103
+ safeHidePopover(el)
104
+ }
105
+ }
106
+
107
+ onMount(() => {
108
+ relocate()
109
+ const observer = new MutationObserver(() => relocate())
110
+ observer.observe(document.body, { subtree: true, attributes: true, attributeFilter: ['open'] })
111
+ return () => {
112
+ observer.disconnect()
113
+ const el = layerElement
114
+ if (el) {
115
+ safeHidePopover(el)
116
+ el.remove()
117
+ }
118
+ }
119
+ })
120
+
121
+ $effect(() => {
122
+ void hasItems
123
+ relocate()
124
+ })
125
+ </script>
126
+
127
+ <div bind:this={layerElement} popover='manual' class='hai-message-layer'>
128
+ <div class='hai-message-stack'>
129
+ {#each message.items as item (item.id)}
130
+ <div
131
+ class={cn(
132
+ 'hai-message-item alert shadow-lg border',
133
+ bgMap[item.type] ?? bgMap.info,
134
+ )}
135
+ role='alert'
136
+ >
137
+ <span class={cn(iconMap[item.type] ?? iconMap.info, 'size-5 shrink-0', iconColorMap[item.type] ?? iconColorMap.info)}></span>
138
+ <span class='text-sm'>{item.message}</span>
139
+ {#if item.closable}
140
+ <IconButton
141
+ variant='ghost'
142
+ size='sm'
143
+ onclick={() => message.remove(item.id)}
144
+ ariaLabel='close'
145
+ >
146
+ ✕
147
+ </IconButton>
148
+ {/if}
149
+ </div>
150
+ {/each}
151
+ </div>
152
+ </div>
153
+
154
+ <style>
155
+ .hai-message-layer {
156
+ position: fixed;
157
+ inset: 0;
158
+ width: 100%;
159
+ height: 100%;
160
+ max-width: none;
161
+ max-height: none;
162
+ margin: 0;
163
+ padding: 0;
164
+ border: 0;
165
+ background: transparent;
166
+ overflow: visible;
167
+ pointer-events: none;
168
+ z-index: 9999;
169
+ }
170
+
171
+ .hai-message-stack {
172
+ position: absolute;
173
+ top: 1rem;
174
+ left: 50%;
175
+ transform: translateX(-50%);
176
+ display: flex;
177
+ flex-direction: column;
178
+ align-items: center;
179
+ gap: 0.5rem;
180
+ pointer-events: auto;
181
+ }
182
+
183
+ .hai-message-item {
184
+ animation: hai-message-enter 0.3s cubic-bezier(0.16, 1, 0.3, 1);
185
+ min-width: 16rem;
186
+ max-width: 32rem;
187
+ white-space: nowrap;
188
+ }
189
+
190
+ @keyframes hai-message-enter {
191
+ from {
192
+ opacity: 0;
193
+ transform: translateY(-1rem);
194
+ }
195
+ to {
196
+ opacity: 1;
197
+ transform: translateY(0);
198
+ }
199
+ }
200
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const MessageContainer: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type MessageContainer = ReturnType<typeof MessageContainer>;
3
+ export default MessageContainer;
@@ -143,7 +143,6 @@
143
143
  >
144
144
  <div
145
145
  class='hai-modal__viewport'
146
- role='button'
147
146
  tabindex='-1'
148
147
  aria-label={uiM('common_close')}
149
148
  onclick={handleDialogClick}
@@ -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": "关闭",
package/dist/types.d.ts CHANGED
@@ -970,3 +970,61 @@ export interface PopoverProps {
970
970
  /** 内容插槽 */
971
971
  children?: Snippet;
972
972
  }
973
+ /**
974
+ * MessageBox 类型
975
+ * - confirm: 双按钮(确认 / 取消)
976
+ * - alert: 单按钮(仅确认)
977
+ * - prompt: 带输入框的确认(预留)
978
+ */
979
+ export type MessageBoxAction = 'confirm' | 'cancel' | 'close';
980
+ /**
981
+ * MessageBox 图标类型
982
+ */
983
+ export type MessageBoxIconType = 'info' | 'success' | 'warning' | 'error';
984
+ /**
985
+ * MessageBox 配置项
986
+ */
987
+ export interface MessageBoxOptions {
988
+ /** 标题 */
989
+ title?: string;
990
+ /** 消息内容(纯文本或 HTML 字符串) */
991
+ message: string;
992
+ /** 类型:confirm 双按钮 / alert 单按钮 */
993
+ type?: 'confirm' | 'alert';
994
+ /** 图标类型 */
995
+ iconType?: MessageBoxIconType;
996
+ /** 确认按钮文本 */
997
+ confirmText?: string;
998
+ /** 取消按钮文本 */
999
+ cancelText?: string;
1000
+ /** 确认按钮变体 */
1001
+ confirmVariant?: 'default' | 'primary' | 'warning' | 'error';
1002
+ /** 是否显示取消按钮(alert 模式下默认 false) */
1003
+ showCancel?: boolean;
1004
+ /** 点击遮罩是否关闭(默认 false,需要显式操作) */
1005
+ closeOnClickModal?: boolean;
1006
+ /** 是否显示右上角关闭按钮(默认 false) */
1007
+ showClose?: boolean;
1008
+ /** 确认前回调(返回 false 可阻止关闭) */
1009
+ beforeClose?: (action: MessageBoxAction, instance: MessageBoxInstance) => boolean | Promise<boolean>;
1010
+ }
1011
+ /**
1012
+ * MessageBox 实例(供 beforeClose 回调使用)
1013
+ */
1014
+ export interface MessageBoxInstance {
1015
+ /** 设置加载态 */
1016
+ setLoading: (loading: boolean) => void;
1017
+ }
1018
+ /**
1019
+ * Message 消息提示配置项
1020
+ */
1021
+ export interface MessageOptions {
1022
+ /** 消息文本 */
1023
+ message: string;
1024
+ /** 类型 */
1025
+ type?: 'info' | 'success' | 'warning' | 'error';
1026
+ /** 显示时长(毫秒),0 表示不自动关闭 */
1027
+ duration?: number;
1028
+ /** 是否可手动关闭 */
1029
+ closable?: boolean;
1030
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@h-ai/ui",
3
3
  "type": "module",
4
- "version": "0.1.0-alpha.27",
4
+ "version": "0.1.0-alpha.28",
5
5
  "description": "Hai Framework UI component library for Svelte 5 Runes.",
6
6
  "license": "Apache-2.0",
7
7
  "svelte": "./dist/index.js",
@@ -51,7 +51,7 @@
51
51
  "mermaid": "^11.12.0",
52
52
  "shiki": "^4.0.2",
53
53
  "tailwind-merge": "^3.5.0",
54
- "@h-ai/core": "0.1.0-alpha.27"
54
+ "@h-ai/core": "0.1.0-alpha.28"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@sveltejs/package": "^2.5.7",