@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.
package/README.md CHANGED
@@ -32,13 +32,21 @@ export default config
32
32
  ### 2. 配置 vite.config.ts
33
33
 
34
34
  ```ts
35
+ import { haiOptimizeExclude, haiPrebundledDeps } from '@h-ai/ui/vite'
35
36
  import { sveltekit } from '@sveltejs/kit/vite'
36
37
  import tailwindcss from '@tailwindcss/vite'
37
38
  import { defineConfig } from 'vite'
38
39
 
39
40
  export default defineConfig({
40
41
  plugins: [sveltekit(), tailwindcss()],
41
- optimizeDeps: { exclude: ['bits-ui'] },
42
+ optimizeDeps: {
43
+ // bits-ui 含 .svelte 源码须 exclude;@internationalized/date 须与 bits-ui 同为原始副本,
44
+ // 否则日期组件会因双实例 instanceof 失败抛 "Unknown date type"
45
+ exclude: [...haiOptimizeExclude],
46
+ // 预打包 @h-ai/ui 的重型纯 JS 依赖(语法高亮 / Mermaid / PDF 等),
47
+ // 避免首次进入业务路由时触发依赖再优化与整页刷新
48
+ include: [...haiPrebundledDeps],
49
+ },
42
50
  ssr: { noExternal: [/@h-ai\//] },
43
51
  })
44
52
  ```
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',
@@ -88,7 +88,7 @@
88
88
  <BitsCalendar.Grid class='w-full border-collapse'>
89
89
  <BitsCalendar.GridHead>
90
90
  <BitsCalendar.GridRow class='flex w-full'>
91
- {#each weekdays as day (day)}
91
+ {#each weekdays as day, weekdayIndex (weekdayIndex)}
92
92
  <BitsCalendar.HeadCell class='w-9 text-center text-xs font-medium text-base-content/50'>
93
93
  {day}
94
94
  </BitsCalendar.HeadCell>
@@ -101,7 +101,7 @@
101
101
  <BitsDatePicker.Grid class='w-full border-collapse'>
102
102
  <BitsDatePicker.GridHead>
103
103
  <BitsDatePicker.GridRow class='flex w-full'>
104
- {#each weekdays as day (day)}
104
+ {#each weekdays as day, weekdayIndex (weekdayIndex)}
105
105
  <BitsDatePicker.HeadCell class='w-9 text-center text-xs font-medium text-base-content/50'>
106
106
  {day}
107
107
  </BitsDatePicker.HeadCell>
@@ -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}