@h-ai/ui 0.1.0-alpha.31 → 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/dist/components/compounds/Combobox.svelte +59 -20
- package/dist/components/compounds/Pagination.svelte +3 -5
- package/dist/components/primitives/Select.svelte +53 -11
- 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/utils.d.ts +13 -0
- package/dist/utils.js +24 -0
- package/package.json +2 -2
|
@@ -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}
|
|
@@ -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}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
-->
|
|
11
11
|
<script lang='ts' generics="T = string">
|
|
12
12
|
import type { DataAttributes, SelectProps } from '../../types.js'
|
|
13
|
-
import { cn, getDataAttributes } from '../../utils.js'
|
|
13
|
+
import { cn, getDataAttributes, portal } from '../../utils.js'
|
|
14
14
|
|
|
15
15
|
let {
|
|
16
16
|
value = $bindable<T>(),
|
|
@@ -31,16 +31,21 @@
|
|
|
31
31
|
const dataAttributes = $derived(getDataAttributes(restProps))
|
|
32
32
|
let inputRef: HTMLInputElement | undefined = $state()
|
|
33
33
|
let dropdownRef: HTMLDivElement | undefined = $state()
|
|
34
|
+
let triggerRef: HTMLDivElement | undefined = $state()
|
|
35
|
+
let listboxRef: HTMLUListElement | undefined = $state()
|
|
34
36
|
let isDropdownOpen = $state(false)
|
|
35
37
|
let filterText = $state('')
|
|
36
38
|
let isHovered = $state(false)
|
|
39
|
+
// 下拉层定位样式(portal 到 body,用 fixed 逃逸 overflow/stacking)
|
|
40
|
+
let dropdownStyle = $state('')
|
|
37
41
|
|
|
38
|
-
//
|
|
42
|
+
// 尺寸映射(与 Input 保持一致)
|
|
39
43
|
const sizeClasses: Record<string, string> = {
|
|
40
|
-
xs: 'h-
|
|
44
|
+
xs: 'h-8 text-xs px-2.5',
|
|
41
45
|
sm: 'h-9 text-sm px-3',
|
|
42
46
|
md: 'h-10 text-sm px-3',
|
|
43
47
|
lg: 'h-12 text-base px-4',
|
|
48
|
+
xl: 'h-14 text-lg px-4',
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
// 选项文字尺寸
|
|
@@ -49,6 +54,8 @@
|
|
|
49
54
|
? 'text-xs'
|
|
50
55
|
: size === 'lg'
|
|
51
56
|
? 'text-base'
|
|
57
|
+
: size === 'xl'
|
|
58
|
+
? 'text-lg'
|
|
52
59
|
: 'text-sm',
|
|
53
60
|
)
|
|
54
61
|
|
|
@@ -73,7 +80,7 @@
|
|
|
73
80
|
|
|
74
81
|
const wrapperClass = $derived(
|
|
75
82
|
cn(
|
|
76
|
-
'relative flex w-full items-center rounded-lg border bg-base-100 cursor-pointer',
|
|
83
|
+
'relative flex w-full min-w-0 items-center rounded-lg border bg-base-100 cursor-pointer',
|
|
77
84
|
sizeClasses[size] || sizeClasses.md,
|
|
78
85
|
error
|
|
79
86
|
? 'border-error/60 focus-within:ring-2 focus-within:ring-error/15'
|
|
@@ -82,6 +89,7 @@
|
|
|
82
89
|
: 'border-base-content/15 hover:border-base-content/25 focus-within:border-primary/50 focus-within:ring-2 focus-within:ring-primary/10',
|
|
83
90
|
'transition-[border-color,box-shadow] duration-150',
|
|
84
91
|
disabled && 'opacity-50 cursor-not-allowed',
|
|
92
|
+
className,
|
|
85
93
|
),
|
|
86
94
|
)
|
|
87
95
|
|
|
@@ -91,14 +99,16 @@
|
|
|
91
99
|
'focus:outline-none focus:ring-0 focus:shadow-none',
|
|
92
100
|
'placeholder:text-base-content/40',
|
|
93
101
|
error && 'text-error',
|
|
94
|
-
className,
|
|
95
102
|
),
|
|
96
103
|
)
|
|
97
104
|
|
|
98
|
-
//
|
|
105
|
+
// 点击外部关闭下拉框(下拉层已 portal 到 body,需同时排除 listbox)
|
|
99
106
|
$effect(() => {
|
|
100
107
|
function handleClickOutside(e: MouseEvent) {
|
|
101
|
-
|
|
108
|
+
const target = e.target as Node
|
|
109
|
+
const inTrigger = dropdownRef?.contains(target) ?? false
|
|
110
|
+
const inListbox = listboxRef?.contains(target) ?? false
|
|
111
|
+
if (!inTrigger && !inListbox) {
|
|
102
112
|
isDropdownOpen = false
|
|
103
113
|
filterText = ''
|
|
104
114
|
}
|
|
@@ -110,6 +120,35 @@
|
|
|
110
120
|
}
|
|
111
121
|
})
|
|
112
122
|
|
|
123
|
+
// 计算下拉层位置(fixed,基于触发器视口坐标;空间不足时向上翻转)
|
|
124
|
+
function updateDropdownPosition() {
|
|
125
|
+
if (!triggerRef)
|
|
126
|
+
return
|
|
127
|
+
const rect = triggerRef.getBoundingClientRect()
|
|
128
|
+
const gap = 4
|
|
129
|
+
const maxHeight = 240
|
|
130
|
+
const spaceBelow = window.innerHeight - rect.bottom
|
|
131
|
+
const openUp = spaceBelow < maxHeight + gap && rect.top > spaceBelow
|
|
132
|
+
const vertical = openUp
|
|
133
|
+
? `bottom:${Math.round(window.innerHeight - rect.top + gap)}px`
|
|
134
|
+
: `top:${Math.round(rect.bottom + gap)}px`
|
|
135
|
+
dropdownStyle = `position:fixed;left:${Math.round(rect.left)}px;width:${Math.round(rect.width)}px;${vertical};z-index:1200;`
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 打开时定位并跟随滚动/缩放更新
|
|
139
|
+
$effect(() => {
|
|
140
|
+
if (!isDropdownOpen)
|
|
141
|
+
return
|
|
142
|
+
updateDropdownPosition()
|
|
143
|
+
const scrollOpts = { passive: true, capture: true } as const
|
|
144
|
+
window.addEventListener('scroll', updateDropdownPosition, scrollOpts)
|
|
145
|
+
window.addEventListener('resize', updateDropdownPosition)
|
|
146
|
+
return () => {
|
|
147
|
+
window.removeEventListener('scroll', updateDropdownPosition, scrollOpts)
|
|
148
|
+
window.removeEventListener('resize', updateDropdownPosition)
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
|
|
113
152
|
function handleWrapperClick() {
|
|
114
153
|
if (disabled)
|
|
115
154
|
return
|
|
@@ -173,7 +212,7 @@
|
|
|
173
212
|
|
|
174
213
|
<div
|
|
175
214
|
{...dataAttributes}
|
|
176
|
-
class='fieldset w-full'
|
|
215
|
+
class='fieldset w-full min-w-0'
|
|
177
216
|
bind:this={dropdownRef}
|
|
178
217
|
role='combobox'
|
|
179
218
|
aria-expanded={isDropdownOpen}
|
|
@@ -183,6 +222,7 @@
|
|
|
183
222
|
>
|
|
184
223
|
<div
|
|
185
224
|
class={wrapperClass}
|
|
225
|
+
bind:this={triggerRef}
|
|
186
226
|
onclick={handleWrapperClick}
|
|
187
227
|
onkeydown={(e) => {
|
|
188
228
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
@@ -255,13 +295,15 @@
|
|
|
255
295
|
{/if}
|
|
256
296
|
</div>
|
|
257
297
|
|
|
258
|
-
<!--
|
|
298
|
+
<!-- 下拉选项列表(portal 到 body,fixed 定位,逃逸 overflow/stacking 裁剪) -->
|
|
259
299
|
{#if isDropdownOpen}
|
|
260
300
|
<ul
|
|
301
|
+
bind:this={listboxRef}
|
|
302
|
+
use:portal
|
|
261
303
|
id='{id}-listbox'
|
|
262
|
-
class='
|
|
304
|
+
class='max-h-60 overflow-auto rounded-xl border border-base-content/10 bg-base-100 p-1.5 shadow-xl shadow-base-content/10'
|
|
305
|
+
style={dropdownStyle}
|
|
263
306
|
role='listbox'
|
|
264
|
-
style='top: 100%;'
|
|
265
307
|
>
|
|
266
308
|
{#each filteredOptions() as option (String(option.value))}
|
|
267
309
|
<li
|
|
@@ -55,9 +55,10 @@
|
|
|
55
55
|
open?: boolean
|
|
56
56
|
labels?: FeedbackLabels
|
|
57
57
|
onsubmit?: (data: FeedbackData) => Promise<void>
|
|
58
|
+
class?: string
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
let { open = $bindable(false), labels = {}, onsubmit, ...restProps }: Props & DataAttributes = $props()
|
|
61
|
+
let { open = $bindable(false), labels = {}, onsubmit, class: className = '', ...restProps }: Props & DataAttributes = $props()
|
|
61
62
|
|
|
62
63
|
const dataAttributes = $derived(getDataAttributes(restProps))
|
|
63
64
|
// 文案优先使用传入的 labels,缺省回退到内置消息 uiM(...)
|
|
@@ -109,7 +110,7 @@
|
|
|
109
110
|
}
|
|
110
111
|
</script>
|
|
111
112
|
|
|
112
|
-
<Modal {...dataAttributes} bind:open title={labels.title ?? uiM('feedback_title')}>
|
|
113
|
+
<Modal {...dataAttributes} class={className} bind:open title={labels.title ?? uiM('feedback_title')}>
|
|
113
114
|
<p class='text-base-content/70 mb-4'>{labels.description ?? uiM('feedback_description')}</p>
|
|
114
115
|
|
|
115
116
|
{#if error}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<script lang='ts'>
|
|
13
13
|
import type { DataAttributes } from '../../../types.js'
|
|
14
14
|
import { uiM } from '../../../messages.js'
|
|
15
|
-
import { getDataAttributes } from '../../../utils.js'
|
|
15
|
+
import { cn, getDataAttributes } from '../../../utils.js'
|
|
16
16
|
import BareButton from '../../primitives/BareButton.svelte'
|
|
17
17
|
import IconButton from '../../primitives/IconButton.svelte'
|
|
18
18
|
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
onlanguagechange?: (lang: string) => void
|
|
26
26
|
onthemechange?: (theme: string) => void
|
|
27
27
|
onclose?: () => void
|
|
28
|
+
class?: string
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
let {
|
|
@@ -45,6 +46,7 @@
|
|
|
45
46
|
onlanguagechange,
|
|
46
47
|
onthemechange,
|
|
47
48
|
onclose,
|
|
49
|
+
class: className = '',
|
|
48
50
|
...restProps
|
|
49
51
|
}: Props & DataAttributes = $props()
|
|
50
52
|
|
|
@@ -76,7 +78,7 @@
|
|
|
76
78
|
onclick={handleBackdropClick}
|
|
77
79
|
>
|
|
78
80
|
<div
|
|
79
|
-
class='relative w-full max-w-lg mx-4 transform overflow-hidden rounded-2xl bg-base-100 shadow-xl transition-all'
|
|
81
|
+
class={cn('relative w-full max-w-lg mx-4 transform overflow-hidden rounded-2xl bg-base-100 shadow-xl transition-all', className)}
|
|
80
82
|
role='dialog'
|
|
81
83
|
aria-modal='true'
|
|
82
84
|
>
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
message = '',
|
|
19
19
|
onconfirm,
|
|
20
20
|
oncancel,
|
|
21
|
+
class: className = '',
|
|
21
22
|
...restProps
|
|
22
23
|
}: {
|
|
23
24
|
open?: boolean
|
|
@@ -26,12 +27,14 @@
|
|
|
26
27
|
message?: string
|
|
27
28
|
onconfirm?: () => Promise<void> | void
|
|
28
29
|
oncancel?: () => void
|
|
30
|
+
class?: string
|
|
29
31
|
} & DataAttributes = $props()
|
|
30
32
|
|
|
31
33
|
const dataAttributes = $derived(getDataAttributes(restProps))
|
|
32
34
|
</script>
|
|
33
35
|
|
|
34
36
|
<Confirm {...dataAttributes}
|
|
37
|
+
class={className}
|
|
35
38
|
bind:open
|
|
36
39
|
title={title || uiM('crud_delete_title')}
|
|
37
40
|
message={message || uiM('crud_delete_message')}
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
onedit,
|
|
42
42
|
onclose,
|
|
43
43
|
detailExtra,
|
|
44
|
+
class: className = '',
|
|
44
45
|
...restProps
|
|
45
46
|
}: {
|
|
46
47
|
open?: boolean
|
|
@@ -57,6 +58,7 @@
|
|
|
57
58
|
onedit?: () => void
|
|
58
59
|
onclose?: () => void
|
|
59
60
|
detailExtra?: Snippet<[Record<string, unknown>]>
|
|
61
|
+
class?: string
|
|
60
62
|
} & DataAttributes = $props()
|
|
61
63
|
|
|
62
64
|
const dataAttributes = $derived(getDataAttributes(restProps))
|
|
@@ -171,6 +173,7 @@
|
|
|
171
173
|
{#if variant === 'modal'}
|
|
172
174
|
<Modal
|
|
173
175
|
bind:open
|
|
176
|
+
class={className}
|
|
174
177
|
{title}
|
|
175
178
|
size={modalSize}
|
|
176
179
|
width={modalWidth}
|
|
@@ -184,7 +187,7 @@
|
|
|
184
187
|
{/snippet}
|
|
185
188
|
</Modal>
|
|
186
189
|
{:else}
|
|
187
|
-
<Drawer bind:open {title} position='right' {size} width={drawerWidth} onclose={handleClose}>
|
|
190
|
+
<Drawer bind:open {title} class={className} position='right' {size} width={drawerWidth} onclose={handleClose}>
|
|
188
191
|
{#if item}
|
|
189
192
|
<div class='pb-20'>
|
|
190
193
|
{@render detailBody()}
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
onedit,
|
|
43
43
|
onclose,
|
|
44
44
|
detailExtra,
|
|
45
|
+
class: className = '',
|
|
45
46
|
...restProps
|
|
46
47
|
}: {
|
|
47
48
|
open?: boolean
|
|
@@ -59,6 +60,7 @@
|
|
|
59
60
|
onedit?: () => void
|
|
60
61
|
onclose?: () => void
|
|
61
62
|
detailExtra?: Snippet<[Record<string, unknown>]>
|
|
63
|
+
class?: string
|
|
62
64
|
} & DataAttributes = $props()
|
|
63
65
|
|
|
64
66
|
const dataAttributes = $derived(getDataAttributes(restProps))
|
|
@@ -179,6 +181,7 @@
|
|
|
179
181
|
{#if variant === 'modal'}
|
|
180
182
|
<Modal
|
|
181
183
|
bind:open
|
|
184
|
+
class={className}
|
|
182
185
|
{title}
|
|
183
186
|
size={modalSize}
|
|
184
187
|
width={modalWidth}
|
|
@@ -192,7 +195,7 @@
|
|
|
192
195
|
{/snippet}
|
|
193
196
|
</Modal>
|
|
194
197
|
{:else}
|
|
195
|
-
<Drawer bind:open {title} position='right' {size} width={drawerWidth} onclose={handleClose}>
|
|
198
|
+
<Drawer bind:open {title} class={className} position='right' {size} width={drawerWidth} onclose={handleClose}>
|
|
196
199
|
{#if item}
|
|
197
200
|
<div class={drawerBodyClass}>
|
|
198
201
|
{@render detailBody()}
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
onclose,
|
|
51
51
|
editFormExtra,
|
|
52
52
|
editingItem = null,
|
|
53
|
+
class: className = '',
|
|
53
54
|
...restProps
|
|
54
55
|
}: {
|
|
55
56
|
open?: boolean
|
|
@@ -70,6 +71,7 @@
|
|
|
70
71
|
onclose?: () => void
|
|
71
72
|
editFormExtra?: Snippet<[Record<string, unknown> | null, 'create' | 'edit']>
|
|
72
73
|
editingItem?: Record<string, unknown> | null
|
|
74
|
+
class?: string
|
|
73
75
|
} & DataAttributes = $props()
|
|
74
76
|
|
|
75
77
|
const dataAttributes = $derived(getDataAttributes(restProps))
|
|
@@ -171,13 +173,12 @@
|
|
|
171
173
|
size={controlSize}
|
|
172
174
|
value={String(fieldValue ?? '')}
|
|
173
175
|
disabled={submitting || isReadonly}
|
|
176
|
+
options={[
|
|
177
|
+
{ value: '', label: placeholder || uiM('crud_filter_all') },
|
|
178
|
+
...opts.map(opt => ({ value: String(opt.value), label: opt.label })),
|
|
179
|
+
]}
|
|
174
180
|
onchange={value => updateField(field.id, value)}
|
|
175
|
-
|
|
176
|
-
<option value="">{placeholder || uiM('crud_filter_all')}</option>
|
|
177
|
-
{#each opts as opt (String(opt.value))}
|
|
178
|
-
<option value={String(opt.value)}>{opt.label}</option>
|
|
179
|
-
{/each}
|
|
180
|
-
</Select>
|
|
181
|
+
/>
|
|
181
182
|
</FormField>
|
|
182
183
|
|
|
183
184
|
{:else if field.type === 'multi-select'}
|
|
@@ -255,6 +256,7 @@
|
|
|
255
256
|
{#if variant === 'modal'}
|
|
256
257
|
<Modal
|
|
257
258
|
bind:open
|
|
259
|
+
class={className}
|
|
258
260
|
{title}
|
|
259
261
|
size={modalSize}
|
|
260
262
|
width={modalWidth}
|
|
@@ -271,7 +273,7 @@
|
|
|
271
273
|
{/snippet}
|
|
272
274
|
</Modal>
|
|
273
275
|
{:else}
|
|
274
|
-
<Drawer bind:open {title} position='right' {size} width={drawerWidth} onclose={handleClose} closeOnBackdrop={false}>
|
|
276
|
+
<Drawer bind:open {title} class={className} position='right' {size} width={drawerWidth} onclose={handleClose} closeOnBackdrop={false}>
|
|
275
277
|
<form onsubmit={handleSubmit} class={drawerFormClass}>
|
|
276
278
|
{@render formFields()}
|
|
277
279
|
</form>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<script lang='ts'>
|
|
8
8
|
import type { DataAttributes } from '../../../types.js'
|
|
9
9
|
import { uiM } from '../../../messages.js'
|
|
10
|
-
import { getDataAttributes } from '../../../utils.js'
|
|
10
|
+
import { cn, getDataAttributes } from '../../../utils.js'
|
|
11
11
|
import BareButton from '../../primitives/BareButton.svelte'
|
|
12
12
|
import Input from '../../primitives/Input.svelte'
|
|
13
13
|
import Select from '../../primitives/Select.svelte'
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
onsearch,
|
|
35
35
|
onfilterchange,
|
|
36
36
|
onreset,
|
|
37
|
+
class: className = '',
|
|
37
38
|
...restProps
|
|
38
39
|
}: {
|
|
39
40
|
searchable?: boolean
|
|
@@ -44,6 +45,7 @@
|
|
|
44
45
|
onsearch?: (search: string) => void
|
|
45
46
|
onfilterchange?: (filters: Record<string, unknown>) => void
|
|
46
47
|
onreset?: () => void
|
|
48
|
+
class?: string
|
|
47
49
|
} & DataAttributes = $props()
|
|
48
50
|
|
|
49
51
|
const dataAttributes = $derived(getDataAttributes(restProps))
|
|
@@ -120,7 +122,7 @@
|
|
|
120
122
|
}
|
|
121
123
|
</script>
|
|
122
124
|
|
|
123
|
-
<div {...dataAttributes} class='flex flex-wrap items-center gap-2 [&_.fieldset]:m-0 [&_.fieldset]:min-w-0'>
|
|
125
|
+
<div {...dataAttributes} class={cn('flex flex-wrap items-center gap-2 [&_.fieldset]:m-0 [&_.fieldset]:min-w-0', className)}>
|
|
124
126
|
{#if searchable}
|
|
125
127
|
<div class='relative w-full sm:w-64 md:w-72'>
|
|
126
128
|
<span class='icon-[tabler--search] pointer-events-none absolute left-3 top-1/2 z-10 size-4 -translate-y-1/2 text-base-content/35'></span>
|
|
@@ -170,12 +172,13 @@
|
|
|
170
172
|
size='sm'
|
|
171
173
|
class='h-8 border-base-content/20 bg-base-100 shadow-none'
|
|
172
174
|
value={String(filterValues[field.id] ?? '')}
|
|
175
|
+
options={[
|
|
176
|
+
{ value: '', label: uiM('crud_filter_all') },
|
|
177
|
+
{ value: 'true', label: uiM('crud_filter_yes') },
|
|
178
|
+
{ value: 'false', label: uiM('crud_filter_no') },
|
|
179
|
+
]}
|
|
173
180
|
onchange={(value: string) => handleFilterChange(field.id, value)}
|
|
174
|
-
|
|
175
|
-
<option value="">{uiM('crud_filter_all')}</option>
|
|
176
|
-
<option value='true'>{uiM('crud_filter_yes')}</option>
|
|
177
|
-
<option value='false'>{uiM('crud_filter_no')}</option>
|
|
178
|
-
</Select>
|
|
181
|
+
/>
|
|
179
182
|
</div>
|
|
180
183
|
{:else if isDateField(field)}
|
|
181
184
|
<div class='w-36 shrink-0'>
|
|
@@ -207,13 +210,12 @@
|
|
|
207
210
|
size='sm'
|
|
208
211
|
class='h-8 border-base-content/20 bg-base-100 shadow-none'
|
|
209
212
|
value={String(filterValues[field.id] ?? '')}
|
|
213
|
+
options={[
|
|
214
|
+
{ value: '', label: uiM('crud_filter_all') },
|
|
215
|
+
...opts.map(opt => ({ value: String(opt.value), label: opt.label })),
|
|
216
|
+
]}
|
|
210
217
|
onchange={(value: string) => handleFilterChange(field.id, value)}
|
|
211
|
-
|
|
212
|
-
<option value="">{uiM('crud_filter_all')}</option>
|
|
213
|
-
{#each opts as opt (String(opt.value))}
|
|
214
|
-
<option value={String(opt.value)}>{opt.label}</option>
|
|
215
|
-
{/each}
|
|
216
|
-
</Select>
|
|
218
|
+
/>
|
|
217
219
|
</div>
|
|
218
220
|
{:else if isTextField(field)}
|
|
219
221
|
<div class='w-44 shrink-0'>
|
package/dist/utils.d.ts
CHANGED
|
@@ -49,3 +49,16 @@ export declare function getProgressVariantClass(variant: Variant): string;
|
|
|
49
49
|
* 生成唯一 ID
|
|
50
50
|
*/
|
|
51
51
|
export declare function generateId(prefix?: string): string;
|
|
52
|
+
/**
|
|
53
|
+
* Portal action:将节点移动到目标容器(默认 document.body)下渲染。
|
|
54
|
+
*
|
|
55
|
+
* 用于弹出层(下拉、气泡等)逃逸祖先的 `overflow: hidden` 裁剪与
|
|
56
|
+
* 堆叠上下文(stacking context)限制,避免被相邻控件遮挡。
|
|
57
|
+
*
|
|
58
|
+
* @param node - 要移动的节点
|
|
59
|
+
* @param target - 目标容器或选择器;默认 body
|
|
60
|
+
*/
|
|
61
|
+
export declare function portal(node: HTMLElement, target?: HTMLElement | string): {
|
|
62
|
+
update(next: HTMLElement | string): void;
|
|
63
|
+
destroy(): void;
|
|
64
|
+
};
|
package/dist/utils.js
CHANGED
|
@@ -176,3 +176,27 @@ export function getProgressVariantClass(variant) {
|
|
|
176
176
|
export function generateId(prefix = 'hai') {
|
|
177
177
|
return `${prefix}-${Math.random().toString(36).slice(2, 9)}`;
|
|
178
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Portal action:将节点移动到目标容器(默认 document.body)下渲染。
|
|
181
|
+
*
|
|
182
|
+
* 用于弹出层(下拉、气泡等)逃逸祖先的 `overflow: hidden` 裁剪与
|
|
183
|
+
* 堆叠上下文(stacking context)限制,避免被相邻控件遮挡。
|
|
184
|
+
*
|
|
185
|
+
* @param node - 要移动的节点
|
|
186
|
+
* @param target - 目标容器或选择器;默认 body
|
|
187
|
+
*/
|
|
188
|
+
export function portal(node, target = 'body') {
|
|
189
|
+
function mount(to) {
|
|
190
|
+
const targetEl = typeof to === 'string' ? document.querySelector(to) : to;
|
|
191
|
+
targetEl?.appendChild(node);
|
|
192
|
+
}
|
|
193
|
+
mount(target);
|
|
194
|
+
return {
|
|
195
|
+
update(next) {
|
|
196
|
+
mount(next);
|
|
197
|
+
},
|
|
198
|
+
destroy() {
|
|
199
|
+
node.parentNode?.removeChild(node);
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
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.
|
|
4
|
+
"version": "0.1.0-alpha.32",
|
|
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.
|
|
54
|
+
"@h-ai/core": "0.1.0-alpha.32"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@sveltejs/package": "^2.5.7",
|