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

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
  ```
@@ -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>
@@ -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>
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.26",
4
+ "version": "0.1.0-alpha.27",
5
5
  "description": "Hai Framework UI component library for Svelte 5 Runes.",
6
6
  "license": "Apache-2.0",
7
7
  "svelte": "./dist/index.js",
@@ -15,6 +15,10 @@
15
15
  "types": "./auto-import.d.ts",
16
16
  "import": "./auto-import.js"
17
17
  },
18
+ "./vite": {
19
+ "types": "./vite.d.ts",
20
+ "import": "./vite.js"
21
+ },
18
22
  "./components/*": {
19
23
  "types": "./dist/components/*.d.ts",
20
24
  "svelte": "./dist/components/*.js",
@@ -27,7 +31,9 @@
27
31
  "files": [
28
32
  "auto-import.d.ts",
29
33
  "auto-import.js",
30
- "dist"
34
+ "dist",
35
+ "vite.d.ts",
36
+ "vite.js"
31
37
  ],
32
38
  "publishConfig": {
33
39
  "access": "public"
@@ -45,7 +51,7 @@
45
51
  "mermaid": "^11.12.0",
46
52
  "shiki": "^4.0.2",
47
53
  "tailwind-merge": "^3.5.0",
48
- "@h-ai/core": "0.1.0-alpha.26"
54
+ "@h-ai/core": "0.1.0-alpha.27"
49
55
  },
50
56
  "devDependencies": {
51
57
  "@sveltejs/package": "^2.5.7",
package/vite.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * 类型声明:@h-ai/ui/vite 开发优化辅助。
3
+ */
4
+
5
+ /**
6
+ * hai 框架在开发态需要提前预打包的纯 JS 依赖清单。
7
+ */
8
+ export declare const haiPrebundledDeps: readonly string[]
9
+
10
+ /**
11
+ * 需要从 Vite 依赖预打包中排除的依赖清单(bits-ui 及其 @internationalized/date 单实例)。
12
+ */
13
+ export declare const haiOptimizeExclude: readonly string[]
package/vite.js ADDED
@@ -0,0 +1,95 @@
1
+ /**
2
+ * =============================================================================
3
+ * @h-ai/ui - Vite 开发优化辅助
4
+ * =============================================================================
5
+ * 说明:
6
+ * @h-ai/ui 的 barrel 入口会静态引入语法高亮(shiki + 27 种语言定义)、
7
+ * Mermaid、Markdown、PDF 等较重的运行时依赖。这些依赖以纯 JS 形态存在,
8
+ * 若在首次进入相关路由时才被 Vite 依赖优化器发现,会触发一次
9
+ * "new dependencies optimized -> reloading" 的整页刷新(并伴随 daisyUI
10
+ * CSS 重新编译),表现为"点击后要等一会才刷新可用"。
11
+ *
12
+ * 将这些依赖加入各应用 `optimizeDeps.include`,让 Vite 在开发服务器启动时
13
+ * 一次性预打包,即可消除运行中途的再优化与整页刷新。
14
+ *
15
+ * 用法(应用 vite.config.ts):
16
+ * import { haiPrebundledDeps } from '@h-ai/ui/vite'
17
+ * optimizeDeps: {
18
+ * exclude: ['bits-ui'],
19
+ * include: [...haiPrebundledDeps],
20
+ * }
21
+ * =============================================================================
22
+ */
23
+
24
+ /**
25
+ * hai 框架在开发态需要提前预打包的纯 JS 依赖清单。
26
+ *
27
+ * 这些依赖经由 @h-ai/ui 等框架包间接引入(如语法高亮、Mermaid、PDF、加密传输等),
28
+ * 若延迟到首次进入业务路由时才被 Vite 依赖优化器发现,会触发整页刷新。
29
+ * shiki 语言列表需与 `src/lib/components/scenes/ai/highlight.ts` 保持一致。
30
+ *
31
+ * @type {readonly string[]}
32
+ */
33
+ export const haiPrebundledDeps = Object.freeze([
34
+ // ─── 语法高亮(shiki 核心 + 引擎 + 语言定义) ───
35
+ 'shiki/core',
36
+ 'shiki/engine/javascript',
37
+ '@shikijs/langs/bash',
38
+ '@shikijs/langs/c',
39
+ '@shikijs/langs/cpp',
40
+ '@shikijs/langs/csharp',
41
+ '@shikijs/langs/css',
42
+ '@shikijs/langs/diff',
43
+ '@shikijs/langs/go',
44
+ '@shikijs/langs/graphql',
45
+ '@shikijs/langs/html',
46
+ '@shikijs/langs/java',
47
+ '@shikijs/langs/javascript',
48
+ '@shikijs/langs/json',
49
+ '@shikijs/langs/kotlin',
50
+ '@shikijs/langs/lua',
51
+ '@shikijs/langs/markdown',
52
+ '@shikijs/langs/php',
53
+ '@shikijs/langs/python',
54
+ '@shikijs/langs/ruby',
55
+ '@shikijs/langs/rust',
56
+ '@shikijs/langs/scss',
57
+ '@shikijs/langs/shellscript',
58
+ '@shikijs/langs/sql',
59
+ '@shikijs/langs/swift',
60
+ '@shikijs/langs/toml',
61
+ '@shikijs/langs/typescript',
62
+ '@shikijs/langs/xml',
63
+ '@shikijs/langs/yaml',
64
+ // ─── 文档 / 图表 / 导出 ───
65
+ 'marked',
66
+ 'mermaid',
67
+ 'jspdf',
68
+ 'html2canvas',
69
+ // ─── 工具 ───
70
+ 'tailwind-merge',
71
+ // ─── 框架运行时(@h-ai/core / @h-ai/crypto / @h-ai/kit 在登录等路径引入) ───
72
+ 'nanoid',
73
+ 'process',
74
+ 'sm-crypto',
75
+ // ─── bits-ui 的纯 JS 传递依赖(bits-ui 自身因含 .svelte 仍需 exclude) ───
76
+ 'bits-ui > @floating-ui/dom',
77
+ 'bits-ui > @floating-ui/core',
78
+ 'bits-ui > tabbable',
79
+ 'bits-ui > esm-env',
80
+ ])
81
+
82
+ /**
83
+ * 需要从 Vite 依赖预打包中排除的依赖清单。
84
+ *
85
+ * - bits-ui:自带 .svelte 源码,无法被 esbuild 预打包,须交给 vite-plugin-svelte。
86
+ * - @internationalized/date:bits-ui(被 exclude,按源码加载)内部用 `instanceof CalendarDate`
87
+ * 判别日期类型。若应用侧用「已优化」副本、bits-ui 用「原始」副本,会产生两个模块实例导致
88
+ * instanceof 失败并抛 "Unknown date type"。统一排除,使两侧都加载同一份原始 ESM,单实例。
89
+ *
90
+ * @type {readonly string[]}
91
+ */
92
+ export const haiOptimizeExclude = Object.freeze([
93
+ 'bits-ui',
94
+ '@internationalized/date',
95
+ ])