@h-ai/ui 0.1.0-alpha.30 → 0.1.0-alpha.32
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 +38 -3
- package/dist/components/compounds/Combobox.svelte +59 -20
- package/dist/components/compounds/MessageContainer.svelte +37 -64
- package/dist/components/compounds/Modal.svelte +24 -40
- package/dist/components/compounds/Pagination.svelte +3 -5
- package/dist/components/primitives/Select.svelte +293 -46
- package/dist/components/primitives/Textarea.svelte +28 -15
- package/dist/components/scenes/ai/AiDocumentEditor.svelte +34 -7
- package/dist/components/scenes/ai/MarkdownRenderer.svelte +46 -5
- package/dist/components/scenes/ai/document-parse.d.ts +2 -0
- package/dist/components/scenes/ai/document-parse.js +16 -17
- package/dist/components/scenes/ai/document-types.d.ts +4 -0
- package/dist/components/scenes/ai/markdown-parse.d.ts +2 -0
- package/dist/components/scenes/ai/markdown-parse.js +17 -20
- package/dist/components/scenes/ai/safe-html.d.ts +16 -0
- package/dist/components/scenes/ai/safe-html.js +284 -0
- package/dist/components/scenes/app/FeedbackModal.svelte +3 -2
- package/dist/components/scenes/app/FeedbackModal.svelte.d.ts +1 -0
- package/dist/components/scenes/app/SettingsModal.svelte +4 -2
- package/dist/components/scenes/app/SettingsModal.svelte.d.ts +1 -0
- package/dist/components/scenes/crud/CrudDeleteConfirm.svelte +3 -0
- package/dist/components/scenes/crud/CrudDeleteConfirm.svelte.d.ts +1 -0
- package/dist/components/scenes/crud/CrudDetailDrawer.svelte +4 -1
- package/dist/components/scenes/crud/CrudDetailDrawer.svelte.d.ts +1 -0
- package/dist/components/scenes/crud/CrudDetailPanel.svelte +4 -1
- package/dist/components/scenes/crud/CrudDetailPanel.svelte.d.ts +1 -0
- package/dist/components/scenes/crud/CrudEditPanel.svelte +9 -7
- package/dist/components/scenes/crud/CrudEditPanel.svelte.d.ts +1 -0
- package/dist/components/scenes/crud/CrudFilterBar.svelte +15 -13
- package/dist/components/scenes/crud/CrudFilterBar.svelte.d.ts +1 -0
- package/dist/components/scenes/types.d.ts +4 -0
- package/dist/styles/global.css +5 -0
- package/dist/types.d.ts +5 -5
- package/dist/utils.d.ts +13 -0
- package/dist/utils.js +25 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -135,7 +135,7 @@ components/
|
|
|
135
135
|
| `Input` | 输入框 | `type`, `value`, `size`, `error`, `validationMessage` |
|
|
136
136
|
| `BareInput` | 无样式输入框 | `type`, `class`, `accept`, `multiple` |
|
|
137
137
|
| `Textarea` | 文本域 | `value`, `rows`, `size`, `autoResize`, `error` |
|
|
138
|
-
| `Select` | 下拉选择 | `options`, `value`, `placeholder`, `size`
|
|
138
|
+
| `Select` | 下拉选择 | `options`, `value`, `placeholder`, `size`, `clearable`, `filterable` |
|
|
139
139
|
| `Checkbox` | 复选框 | `checked`, `label`, `size`, `indeterminate` |
|
|
140
140
|
| `Switch` | 开关 | `checked`, `label`, `size` |
|
|
141
141
|
| `Radio` | 单选组 | `options`, `value`, `direction`, `size` |
|
|
@@ -283,14 +283,31 @@ components/
|
|
|
283
283
|
|
|
284
284
|
| 组件 | 描述 | 主要属性 / 能力 |
|
|
285
285
|
| ------------------------ | ---------------- | ---------------- |
|
|
286
|
-
| `MarkdownRenderer` | Markdown 渲染器 | `content`, `showCopyButton`, `enableHighlight` |
|
|
286
|
+
| `MarkdownRenderer` | Markdown 渲染器 | `content`, `fontSize`, `allowHtmlTags`, `showCopyButton`, `enableHighlight` |
|
|
287
287
|
| `AiDocumentDownloadMenu` | 文档下载菜单 | `actions`, `ondownload` |
|
|
288
|
-
| `AiDocumentEditor` | AI 文档编辑器 | `content`, `showRunButton`, `showCodePreviewToggle`, `allowUnsafeCodePreview` |
|
|
288
|
+
| `AiDocumentEditor` | AI 文档编辑器 | `content`, `fontSize`, `allowHtmlTags`, `showRunButton`, `showCodePreviewToggle`, `allowUnsafeCodePreview` |
|
|
289
289
|
| `AiTableEditor` | AI 表格编辑器 | `columns`, `rows`, `editable`, `ondownload` |
|
|
290
290
|
|
|
291
291
|
> `AiDocumentEditor` 默认只允许 Markdown 内置预览。HTML / JS / CSS 等高风险预览需要显式设置 `allowUnsafeCodePreview`,或通过 `oncoderun` 返回受控的预览结果。
|
|
292
292
|
>
|
|
293
293
|
> Mermaid 图表开箱即用:文档(`sourceKind='document'`)里的 ```` ```mermaid ```` 代码块在阅读态自动渲染为图表;code 模式(`sourceKind='code'` + `showCodePreviewToggle`)下可在「代码 / 预览」间切换。Mermaid 以 `securityLevel: 'strict'` 渲染为消毒后的 SVG,无需开启 `allowUnsafeCodePreview`。
|
|
294
|
+
>
|
|
295
|
+
> `MarkdownRenderer` 与 `AiDocumentEditor` 还支持展示层配置:
|
|
296
|
+
> - `fontSize` 支持 `number`(按 px 解释)或任意 CSS 长度字符串(如 `1.125rem`)
|
|
297
|
+
> - `allowHtmlTags` 默认关闭;开启后会按安全白名单解析 `<b>` / `<i>` / `<u>` / `<mark>` 等 HTML 标签,危险标签与属性仍会被消毒
|
|
298
|
+
|
|
299
|
+
```svelte
|
|
300
|
+
<MarkdownRenderer content={markdown} fontSize='1.125rem' allowHtmlTags />
|
|
301
|
+
|
|
302
|
+
<AiDocumentEditor
|
|
303
|
+
title='方案文档'
|
|
304
|
+
content={documentMarkdown}
|
|
305
|
+
fontSize={18}
|
|
306
|
+
allowHtmlTags
|
|
307
|
+
showToolbar
|
|
308
|
+
showOutline={false}
|
|
309
|
+
/>
|
|
310
|
+
```
|
|
294
311
|
|
|
295
312
|
#### CRUD 场景(5 个)
|
|
296
313
|
|
|
@@ -340,6 +357,24 @@ components/
|
|
|
340
357
|
<ToastContainer />
|
|
341
358
|
```
|
|
342
359
|
|
|
360
|
+
### Message 消息提示
|
|
361
|
+
|
|
362
|
+
```svelte
|
|
363
|
+
<script>
|
|
364
|
+
import { message, MessageContainer } from '@h-ai/ui'
|
|
365
|
+
|
|
366
|
+
function showMessage() {
|
|
367
|
+
message.success('操作成功')
|
|
368
|
+
message.error('操作失败')
|
|
369
|
+
message.warning('请注意')
|
|
370
|
+
message.info('提示信息', 5000)
|
|
371
|
+
}
|
|
372
|
+
</script>
|
|
373
|
+
|
|
374
|
+
<button onclick={showMessage}>消息</button>
|
|
375
|
+
<MessageContainer />
|
|
376
|
+
```
|
|
377
|
+
|
|
343
378
|
### 典型 CRUD 页面
|
|
344
379
|
|
|
345
380
|
```svelte
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
import type { DataAttributes } from '../../types.js'
|
|
37
37
|
import { Combobox } from 'bits-ui'
|
|
38
38
|
import { uiM } from '../../messages.js'
|
|
39
|
-
import { getDataAttributes } from '../../utils.js'
|
|
39
|
+
import { cn, getDataAttributes } from '../../utils.js'
|
|
40
40
|
import BareButton from '../primitives/BareButton.svelte'
|
|
41
41
|
|
|
42
42
|
/** 选项定义 */
|
|
@@ -93,8 +93,18 @@
|
|
|
93
93
|
value = ''
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
// ─── 统一视觉样式(与 Select 对齐:rounded-lg / 柔和边框 / focus ring) ───
|
|
97
|
+
const controlStateClass = $derived(
|
|
98
|
+
error
|
|
99
|
+
? 'border-error/60 focus-within:ring-2 focus-within:ring-error/15'
|
|
100
|
+
: 'border-base-content/15 hover:border-base-content/25 focus-within:border-primary/50 focus-within:ring-2 focus-within:ring-primary/10',
|
|
101
|
+
)
|
|
102
|
+
const contentClass = 'z-[1200] mt-1.5 max-h-60 w-(--bits-combobox-anchor-width) overflow-y-auto rounded-xl border border-base-content/10 bg-base-100 p-1.5 shadow-xl shadow-base-content/10'
|
|
103
|
+
const itemClass = 'flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2.5 text-sm transition-colors hover:bg-base-content/5 data-highlighted:bg-base-content/5 data-disabled:cursor-not-allowed data-disabled:opacity-40'
|
|
104
|
+
|
|
96
105
|
let searchValue = $state('')
|
|
97
106
|
let open = $state(false)
|
|
107
|
+
let inputRef = $state<HTMLInputElement | null>(null)
|
|
98
108
|
|
|
99
109
|
/** 根据搜索关键词过滤选项 */
|
|
100
110
|
const filteredOptions = $derived(
|
|
@@ -131,6 +141,19 @@
|
|
|
131
141
|
/** 单选模式:提供给 Combobox.Root 的类型安全值 */
|
|
132
142
|
const singleVal = $derived(typeof value === 'string' ? value : '')
|
|
133
143
|
|
|
144
|
+
function syncInputElementValue() {
|
|
145
|
+
if (!inputRef)
|
|
146
|
+
return
|
|
147
|
+
const nextValue = multiple ? multiInputValue : singleInputValue
|
|
148
|
+
if (inputRef.value !== nextValue) {
|
|
149
|
+
inputRef.value = nextValue
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
$effect(() => {
|
|
154
|
+
syncInputElementValue()
|
|
155
|
+
})
|
|
156
|
+
|
|
134
157
|
function handleInput(e: Event & { currentTarget: HTMLInputElement }) {
|
|
135
158
|
searchValue = e.currentTarget.value
|
|
136
159
|
if (!open)
|
|
@@ -145,11 +168,19 @@
|
|
|
145
168
|
function handleMultiValueChange(v: string[]) {
|
|
146
169
|
value = v
|
|
147
170
|
onchange?.(v)
|
|
171
|
+
setTimeout(() => {
|
|
172
|
+
searchValue = ''
|
|
173
|
+
syncInputElementValue()
|
|
174
|
+
}, 0)
|
|
148
175
|
}
|
|
149
176
|
|
|
150
177
|
function handleSingleValueChange(v: string) {
|
|
151
178
|
value = v
|
|
152
179
|
onchange?.(v)
|
|
180
|
+
setTimeout(() => {
|
|
181
|
+
searchValue = ''
|
|
182
|
+
syncInputElementValue()
|
|
183
|
+
}, 0)
|
|
153
184
|
}
|
|
154
185
|
|
|
155
186
|
/** 多选模式下移除某个已选项 */
|
|
@@ -182,18 +213,20 @@
|
|
|
182
213
|
>
|
|
183
214
|
<div class='relative'>
|
|
184
215
|
<div
|
|
185
|
-
class=
|
|
186
|
-
|
|
187
|
-
|
|
216
|
+
class={cn(
|
|
217
|
+
'min-h-10 rounded-lg border bg-base-100 flex flex-wrap gap-1.5 items-center py-1.5 pl-2.5 pr-9 transition-[border-color,box-shadow] duration-150',
|
|
218
|
+
disabled ? 'opacity-50 cursor-not-allowed bg-base-200' : 'cursor-text',
|
|
219
|
+
controlStateClass,
|
|
220
|
+
)}
|
|
188
221
|
>
|
|
189
222
|
{#each selectedOptions as opt (opt.value)}
|
|
190
|
-
<span class='inline-flex items-center gap-1 px-2 py-0.5
|
|
223
|
+
<span class='inline-flex items-center gap-1 rounded-md bg-primary/10 px-2 py-0.5 text-sm text-primary'>
|
|
191
224
|
<span class='truncate max-w-32'>{opt.label}</span>
|
|
192
225
|
{#if !disabled}
|
|
193
226
|
<BareButton
|
|
194
227
|
type='button'
|
|
195
|
-
class='
|
|
196
|
-
onclick={e => removeItem(opt.value, e)}
|
|
228
|
+
class='rounded p-0.5 transition-colors hover:bg-primary/20'
|
|
229
|
+
onclick={(e: MouseEvent) => removeItem(opt.value, e)}
|
|
197
230
|
ariaLabel="{uiM('combobox_remove')} {opt.label}"
|
|
198
231
|
>
|
|
199
232
|
<span class='icon-[tabler--x] size-3'></span>
|
|
@@ -203,7 +236,8 @@
|
|
|
203
236
|
{/each}
|
|
204
237
|
|
|
205
238
|
<Combobox.Input
|
|
206
|
-
|
|
239
|
+
bind:ref={inputRef}
|
|
240
|
+
class='flex-1 min-w-20 bg-transparent py-0.5 text-sm outline-none placeholder:text-base-content/40'
|
|
207
241
|
placeholder={selectedOptions.length === 0 ? placeholder : ''}
|
|
208
242
|
{disabled}
|
|
209
243
|
oninput={handleInput}
|
|
@@ -215,7 +249,7 @@
|
|
|
215
249
|
</div>
|
|
216
250
|
|
|
217
251
|
<Combobox.Trigger
|
|
218
|
-
class='absolute right-2 top-1/2 -translate-y-1/2
|
|
252
|
+
class='absolute right-2.5 top-1/2 flex size-5 -translate-y-1/2 items-center justify-center text-base-content/40 transition-colors hover:text-base-content/70'
|
|
219
253
|
>
|
|
220
254
|
<span class="icon-[tabler--chevron-down] size-4 transition-transform {open ? 'rotate-180' : ''}"></span>
|
|
221
255
|
</Combobox.Trigger>
|
|
@@ -223,11 +257,11 @@
|
|
|
223
257
|
|
|
224
258
|
<Combobox.Portal>
|
|
225
259
|
<Combobox.Content
|
|
226
|
-
class=
|
|
260
|
+
class={contentClass}
|
|
227
261
|
sideOffset={4}
|
|
228
262
|
>
|
|
229
263
|
{#if filteredOptions.length === 0}
|
|
230
|
-
<div class='px-3 py-2 text-sm text-base-content/50'>
|
|
264
|
+
<div class='px-3 py-2.5 text-sm text-base-content/50'>
|
|
231
265
|
{uiM('combobox_no_match')}
|
|
232
266
|
</div>
|
|
233
267
|
{:else}
|
|
@@ -236,10 +270,10 @@
|
|
|
236
270
|
value={opt.value}
|
|
237
271
|
label={opt.label}
|
|
238
272
|
disabled={opt.disabled}
|
|
239
|
-
class=
|
|
273
|
+
class={itemClass}
|
|
240
274
|
>
|
|
241
275
|
{#snippet children({ selected })}
|
|
242
|
-
<span class="size-4 shrink-0 rounded border border-base-content/30
|
|
276
|
+
<span class="flex size-4 shrink-0 items-center justify-center rounded border border-base-content/30 {selected ? 'border-primary bg-primary' : ''}">
|
|
243
277
|
{#if selected}
|
|
244
278
|
<span class='icon-[tabler--check] size-3 text-primary-content'></span>
|
|
245
279
|
{/if}
|
|
@@ -247,7 +281,7 @@
|
|
|
247
281
|
<div class='flex-1 min-w-0'>
|
|
248
282
|
<span>{opt.label}</span>
|
|
249
283
|
{#if opt.description}
|
|
250
|
-
<div class='text-xs text-base-content/50
|
|
284
|
+
<div class='truncate text-xs text-base-content/50'>{opt.description}</div>
|
|
251
285
|
{/if}
|
|
252
286
|
</div>
|
|
253
287
|
{/snippet}
|
|
@@ -271,7 +305,12 @@
|
|
|
271
305
|
>
|
|
272
306
|
<div class='relative'>
|
|
273
307
|
<Combobox.Input
|
|
274
|
-
|
|
308
|
+
bind:ref={inputRef}
|
|
309
|
+
class={cn(
|
|
310
|
+
'h-10 w-full rounded-lg border bg-base-100 pl-3 pr-9 text-sm outline-none transition-[border-color,box-shadow] duration-150 placeholder:text-base-content/40',
|
|
311
|
+
controlStateClass,
|
|
312
|
+
disabled && 'opacity-50 cursor-not-allowed',
|
|
313
|
+
)}
|
|
275
314
|
{placeholder}
|
|
276
315
|
{disabled}
|
|
277
316
|
oninput={handleInput}
|
|
@@ -281,7 +320,7 @@
|
|
|
281
320
|
}}
|
|
282
321
|
/>
|
|
283
322
|
<Combobox.Trigger
|
|
284
|
-
class='absolute right-2 top-1/2 -translate-y-1/2
|
|
323
|
+
class='absolute right-2.5 top-1/2 flex size-5 -translate-y-1/2 items-center justify-center text-base-content/40 transition-colors hover:text-base-content/70'
|
|
285
324
|
>
|
|
286
325
|
<span class="icon-[tabler--chevron-down] size-4 transition-transform {open ? 'rotate-180' : ''}"></span>
|
|
287
326
|
</Combobox.Trigger>
|
|
@@ -289,11 +328,11 @@
|
|
|
289
328
|
|
|
290
329
|
<Combobox.Portal>
|
|
291
330
|
<Combobox.Content
|
|
292
|
-
class=
|
|
331
|
+
class={contentClass}
|
|
293
332
|
sideOffset={4}
|
|
294
333
|
>
|
|
295
334
|
{#if filteredOptions.length === 0}
|
|
296
|
-
<div class='px-3 py-2 text-sm text-base-content/50'>
|
|
335
|
+
<div class='px-3 py-2.5 text-sm text-base-content/50'>
|
|
297
336
|
{uiM('combobox_no_match')}
|
|
298
337
|
</div>
|
|
299
338
|
{:else}
|
|
@@ -302,7 +341,7 @@
|
|
|
302
341
|
value={opt.value}
|
|
303
342
|
label={opt.label}
|
|
304
343
|
disabled={opt.disabled}
|
|
305
|
-
class='
|
|
344
|
+
class={cn(itemClass, 'data-selected:bg-primary/10 data-selected:text-primary data-selected:font-medium')}
|
|
306
345
|
>
|
|
307
346
|
{#snippet children({ selected })}
|
|
308
347
|
<span class='size-4 shrink-0'>
|
|
@@ -313,7 +352,7 @@
|
|
|
313
352
|
<div class='flex-1 min-w-0'>
|
|
314
353
|
<span>{opt.label}</span>
|
|
315
354
|
{#if opt.description}
|
|
316
|
-
<div class='text-xs text-base-content/50
|
|
355
|
+
<div class='truncate text-xs text-base-content/50'>{opt.description}</div>
|
|
317
356
|
{/if}
|
|
318
357
|
</div>
|
|
319
358
|
{/snippet}
|
|
@@ -5,9 +5,7 @@
|
|
|
5
5
|
轻量消息提示容器,渲染 message store 中的消息条目。
|
|
6
6
|
视觉风格:顶部居中、带类型图标、自动滑入滑出。
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
- 有打开的 modal dialog 时,寄宿到 dialog 内部(top layer);
|
|
10
|
-
- 否则寄宿到 <body> 以 popover 进入 top layer。
|
|
8
|
+
叠加策略:消息层始终挂载到 `document.body`,使用 fixed overlay + 超高 z-index(高于 Modal 的 `--hai-z-modal`),保证不被任何弹层遮挡;空消息时隐藏容器避免拦截交互。
|
|
11
9
|
=============================================================================
|
|
12
10
|
-->
|
|
13
11
|
<script lang='ts'>
|
|
@@ -36,84 +34,61 @@
|
|
|
36
34
|
error: 'text-error',
|
|
37
35
|
}
|
|
38
36
|
|
|
37
|
+
/** 文案颜色映射 */
|
|
38
|
+
const textColorMap: Record<string, string> = {
|
|
39
|
+
info: 'text-info',
|
|
40
|
+
success: 'text-success',
|
|
41
|
+
warning: 'text-warning',
|
|
42
|
+
error: 'text-error',
|
|
43
|
+
}
|
|
44
|
+
|
|
39
45
|
/** 背景/边框映射 */
|
|
40
46
|
const bgMap: Record<string, string> = {
|
|
41
|
-
info: 'border-info/
|
|
42
|
-
success: 'border-success/
|
|
43
|
-
warning: 'border-warning/
|
|
44
|
-
error: 'border-error/
|
|
47
|
+
info: 'border-info/30 bg-info/15',
|
|
48
|
+
success: 'border-success/30 bg-success/15',
|
|
49
|
+
warning: 'border-warning/30 bg-warning/15',
|
|
50
|
+
error: 'border-error/30 bg-error/15',
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
let layerElement: HTMLDivElement | undefined = $state()
|
|
48
|
-
|
|
49
54
|
const hasItems = $derived(message.items.length > 0)
|
|
50
55
|
|
|
51
|
-
/** 找到最顶层处于打开状态的原生 modal dialog。 */
|
|
52
|
-
function findTopModalDialog(): HTMLDialogElement | null {
|
|
53
|
-
if (typeof document === 'undefined')
|
|
54
|
-
return null
|
|
55
|
-
const dialogs = document.querySelectorAll<HTMLDialogElement>('dialog')
|
|
56
|
-
let top: HTMLDialogElement | null = null
|
|
57
|
-
for (const dialog of dialogs) {
|
|
58
|
-
if (dialog.open && typeof dialog.matches === 'function' && dialog.matches(':modal')) {
|
|
59
|
-
top = dialog
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return top
|
|
63
|
-
}
|
|
64
|
-
|
|
65
56
|
function safeShowPopover(el: HTMLElement) {
|
|
66
57
|
try {
|
|
67
|
-
if (typeof el.showPopover === 'function' && el.isConnected && !el.matches(':popover-open'))
|
|
58
|
+
if (typeof el.showPopover === 'function' && el.isConnected && !el.matches(':popover-open'))
|
|
68
59
|
el.showPopover()
|
|
69
|
-
}
|
|
70
60
|
}
|
|
71
|
-
catch {
|
|
61
|
+
catch {
|
|
62
|
+
// 元素未连接或重复调用时忽略。
|
|
63
|
+
}
|
|
72
64
|
}
|
|
73
65
|
|
|
74
66
|
function safeHidePopover(el: HTMLElement) {
|
|
75
67
|
try {
|
|
76
|
-
if (typeof el.hidePopover === 'function' && el.matches(':popover-open'))
|
|
68
|
+
if (typeof el.hidePopover === 'function' && el.matches(':popover-open'))
|
|
77
69
|
el.hidePopover()
|
|
78
|
-
}
|
|
79
70
|
}
|
|
80
|
-
catch {
|
|
71
|
+
catch {
|
|
72
|
+
// 同上,忽略异常。
|
|
73
|
+
}
|
|
81
74
|
}
|
|
82
75
|
|
|
83
|
-
|
|
76
|
+
$effect(() => {
|
|
84
77
|
const el = layerElement
|
|
85
78
|
if (!el)
|
|
86
79
|
return
|
|
87
|
-
|
|
88
|
-
const modal = hasItems ? findTopModalDialog() : null
|
|
89
|
-
|
|
90
|
-
if (modal) {
|
|
91
|
-
safeHidePopover(el)
|
|
92
|
-
if (el.hasAttribute('popover'))
|
|
93
|
-
el.removeAttribute('popover')
|
|
94
|
-
if (el.parentElement !== modal)
|
|
95
|
-
modal.appendChild(el)
|
|
96
|
-
return
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (el.parentElement !== document.body)
|
|
100
|
-
document.body.appendChild(el)
|
|
101
|
-
if (!el.hasAttribute('popover'))
|
|
102
|
-
el.setAttribute('popover', 'manual')
|
|
103
|
-
if (hasItems) {
|
|
80
|
+
if (hasItems)
|
|
104
81
|
safeShowPopover(el)
|
|
105
|
-
|
|
106
|
-
else {
|
|
82
|
+
else
|
|
107
83
|
safeHidePopover(el)
|
|
108
|
-
|
|
109
|
-
}
|
|
84
|
+
})
|
|
110
85
|
|
|
111
86
|
onMount(() => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
87
|
+
const el = layerElement
|
|
88
|
+
if (el && document.body && el.parentElement !== document.body)
|
|
89
|
+
document.body.appendChild(el)
|
|
90
|
+
|
|
115
91
|
return () => {
|
|
116
|
-
observer.disconnect()
|
|
117
92
|
const el = layerElement
|
|
118
93
|
if (el) {
|
|
119
94
|
safeHidePopover(el)
|
|
@@ -121,11 +96,6 @@
|
|
|
121
96
|
}
|
|
122
97
|
}
|
|
123
98
|
})
|
|
124
|
-
|
|
125
|
-
$effect(() => {
|
|
126
|
-
void hasItems
|
|
127
|
-
relocate()
|
|
128
|
-
})
|
|
129
99
|
</script>
|
|
130
100
|
|
|
131
101
|
<div {...dataAttributes} bind:this={layerElement} popover='manual' class='hai-message-layer'>
|
|
@@ -135,6 +105,7 @@
|
|
|
135
105
|
class={cn(
|
|
136
106
|
'hai-message-item alert shadow-lg border',
|
|
137
107
|
bgMap[item.type] ?? bgMap.info,
|
|
108
|
+
textColorMap[item.type] ?? textColorMap.info,
|
|
138
109
|
)}
|
|
139
110
|
role='alert'
|
|
140
111
|
>
|
|
@@ -161,22 +132,24 @@
|
|
|
161
132
|
inset: 0;
|
|
162
133
|
width: 100%;
|
|
163
134
|
height: 100%;
|
|
164
|
-
max-width: none;
|
|
165
|
-
max-height: none;
|
|
166
135
|
margin: 0;
|
|
167
136
|
padding: 0;
|
|
168
137
|
border: 0;
|
|
169
138
|
background: transparent;
|
|
170
139
|
overflow: visible;
|
|
171
140
|
pointer-events: none;
|
|
172
|
-
z-index:
|
|
141
|
+
z-index: 2147483647;
|
|
173
142
|
}
|
|
174
143
|
|
|
175
144
|
.hai-message-stack {
|
|
176
145
|
position: absolute;
|
|
177
146
|
top: 1rem;
|
|
178
|
-
left:
|
|
179
|
-
|
|
147
|
+
left: 0;
|
|
148
|
+
right: 0;
|
|
149
|
+
margin-left: auto;
|
|
150
|
+
margin-right: auto;
|
|
151
|
+
width: max-content;
|
|
152
|
+
max-width: 100%;
|
|
180
153
|
display: flex;
|
|
181
154
|
flex-direction: column;
|
|
182
155
|
align-items: center;
|
|
@@ -28,9 +28,8 @@
|
|
|
28
28
|
}: ModalProps & DataAttributes = $props()
|
|
29
29
|
|
|
30
30
|
const dataAttributes = $derived(getDataAttributes(restProps))
|
|
31
|
-
// modalElement
|
|
32
|
-
|
|
33
|
-
let modalElement: HTMLDialogElement | undefined = $state()
|
|
31
|
+
// modalElement 持有 overlay 容器引用;不再依赖原生 `<dialog>`,改用 fixed + z-index 叠加。
|
|
32
|
+
let modalElement: HTMLDivElement | undefined = $state()
|
|
34
33
|
|
|
35
34
|
// 标题区仅在显式提供标题或自定义 header 时出现;
|
|
36
35
|
// 仅需关闭按钮的场景改为右上角悬浮按钮,避免业务侧额外占一整行头部。
|
|
@@ -111,37 +110,17 @@
|
|
|
111
110
|
}
|
|
112
111
|
}
|
|
113
112
|
|
|
114
|
-
function handleDialogCancel(event: Event) {
|
|
115
|
-
// 原生 dialog 在按下 Escape 时会先触发 cancel;
|
|
116
|
-
// 这里统一接管关闭逻辑,避免浏览器直接 close 后跳过组件自己的 onclose 回调。
|
|
117
|
-
event.preventDefault()
|
|
118
|
-
if (closeOnEscape) {
|
|
119
|
-
handleClose()
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
$effect(() => {
|
|
124
|
-
if (!modalElement) {
|
|
125
|
-
return
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (open) {
|
|
129
|
-
if (!modalElement.open) {
|
|
130
|
-
modalElement.showModal()
|
|
131
|
-
}
|
|
132
|
-
return
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (modalElement.open) {
|
|
136
|
-
modalElement.close()
|
|
137
|
-
}
|
|
138
|
-
})
|
|
139
113
|
</script>
|
|
140
114
|
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
115
|
+
<div {...dataAttributes}
|
|
116
|
+
bind:this={modalElement}
|
|
117
|
+
class='hai-modal'
|
|
118
|
+
style:display={open ? '' : 'none'}
|
|
119
|
+
aria-hidden={open ? undefined : 'true'}
|
|
120
|
+
onkeydown={(e) => {
|
|
121
|
+
if (closeOnEscape && e.key === 'Escape')
|
|
122
|
+
handleClose()
|
|
123
|
+
}}
|
|
145
124
|
>
|
|
146
125
|
<div
|
|
147
126
|
class='hai-modal__viewport'
|
|
@@ -223,11 +202,10 @@
|
|
|
223
202
|
{/if}
|
|
224
203
|
</div>
|
|
225
204
|
</div>
|
|
226
|
-
</
|
|
205
|
+
</div>
|
|
227
206
|
|
|
228
207
|
<style>
|
|
229
|
-
|
|
230
|
-
dialog.hai-modal:modal {
|
|
208
|
+
.hai-modal {
|
|
231
209
|
margin: 0;
|
|
232
210
|
padding: 0;
|
|
233
211
|
border: 0;
|
|
@@ -237,17 +215,23 @@
|
|
|
237
215
|
box-sizing: border-box;
|
|
238
216
|
}
|
|
239
217
|
|
|
218
|
+
.hai-modal {
|
|
219
|
+
position: fixed;
|
|
220
|
+
inset: 0;
|
|
221
|
+
z-index: var(--hai-z-modal, 1100);
|
|
222
|
+
display: block;
|
|
223
|
+
pointer-events: none;
|
|
224
|
+
}
|
|
225
|
+
|
|
240
226
|
.hai-modal__viewport {
|
|
241
227
|
position: fixed;
|
|
242
228
|
inset: 0;
|
|
243
229
|
display: grid;
|
|
244
230
|
place-items: center;
|
|
245
231
|
padding: clamp(1rem, 2.6vw, 2rem);
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
dialog.hai-modal::backdrop {
|
|
232
|
+
pointer-events: auto;
|
|
249
233
|
background: rgb(15 23 42 / 0.26);
|
|
250
|
-
background: color-mix(in srgb, var(--color-base-content)
|
|
234
|
+
background: color-mix(in srgb, var(--color-base-content) 15%, transparent);
|
|
251
235
|
backdrop-filter: blur(2px);
|
|
252
236
|
}
|
|
253
237
|
|
|
@@ -373,7 +357,7 @@
|
|
|
373
357
|
}
|
|
374
358
|
|
|
375
359
|
@media (max-width: 768px) {
|
|
376
|
-
|
|
360
|
+
.hai-modal {
|
|
377
361
|
padding: 0.85rem;
|
|
378
362
|
}
|
|
379
363
|
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
const controlSize = $derived(size === 'xs' ? 'xs' : 'sm')
|
|
42
42
|
const jumperControlSize = 'xs'
|
|
43
43
|
const sizeChangerClass = $derived(size === 'xs' ? 'w-17 shrink-0' : 'w-18 shrink-0')
|
|
44
|
+
const pageSizeSelectOptions = $derived(pageSizeOptions.map(opt => ({ value: String(opt), label: String(opt) })))
|
|
44
45
|
const jumperWrapClass = $derived(size === 'xs' ? 'flex min-w-fit items-center gap-1.5 whitespace-nowrap' : 'flex min-w-fit items-center gap-2 whitespace-nowrap')
|
|
45
46
|
const jumperInputClass = $derived(
|
|
46
47
|
size === 'xs'
|
|
@@ -105,12 +106,9 @@
|
|
|
105
106
|
<Select
|
|
106
107
|
size={controlSize}
|
|
107
108
|
value={String(pageSize)}
|
|
109
|
+
options={pageSizeSelectOptions}
|
|
108
110
|
onchange={handlePageSizeChange}
|
|
109
|
-
|
|
110
|
-
{#each pageSizeOptions as opt (opt)}
|
|
111
|
-
<option value={String(opt)}>{opt}</option>
|
|
112
|
-
{/each}
|
|
113
|
-
</Select>
|
|
111
|
+
/>
|
|
114
112
|
</div>
|
|
115
113
|
</div>
|
|
116
114
|
{/if}
|