@h-ai/ui 0.1.0-alpha.61 → 0.1.0-alpha.63
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.en.md +93 -8
- package/README.ja.md +93 -8
- package/README.md +94 -8
- package/auto-import.d.ts +3 -0
- package/auto-import.js +3 -0
- package/dist/components/compounds/Combobox.svelte +19 -6
- package/dist/components/compounds/Combobox.svelte.d.ts +2 -2
- package/dist/components/compounds/DatePicker.svelte +14 -5
- package/dist/components/compounds/DatePicker.svelte.d.ts +2 -2
- package/dist/components/compounds/Form.svelte +10 -6
- package/dist/components/compounds/FormField.svelte +78 -13
- package/dist/components/compounds/FormLayout.svelte +59 -0
- package/dist/components/compounds/FormLayout.svelte.d.ts +4 -0
- package/dist/components/compounds/TagInput.svelte +1 -1
- package/dist/components/compounds/index.d.ts +1 -0
- package/dist/components/compounds/index.js +1 -0
- package/dist/components/primitives/BareInput.svelte +7 -1
- package/dist/components/primitives/Checkbox.svelte +8 -2
- package/dist/components/primitives/Input.svelte +22 -11
- package/dist/components/primitives/Select.svelte +77 -18
- package/dist/components/primitives/Textarea.svelte +27 -13
- package/dist/components/primitives/Textarea.svelte.d.ts +1 -1
- package/dist/components/scenes/app/FeedbackModal.svelte +29 -19
- package/dist/components/scenes/app/FeedbackModal.svelte.d.ts +2 -2
- package/dist/components/scenes/crud/CrudEditPanel.svelte +148 -118
- package/dist/components/scenes/crud/CrudEditPanel.svelte.d.ts +4 -25
- package/dist/components/scenes/crud/CrudPage.svelte +5 -41
- package/dist/components/scenes/crud/CrudPage.svelte.d.ts +2 -53
- package/dist/components/scenes/crud/crud-controller.svelte.d.ts +4 -1
- package/dist/components/scenes/crud/crud-form-validation.d.ts +4 -0
- package/dist/components/scenes/crud/crud-form-validation.js +39 -0
- package/dist/components/scenes/crud/crud-types.d.ts +9 -42
- package/dist/components/scenes/guide/GuideHost.svelte +111 -0
- package/dist/components/scenes/guide/GuideHost.svelte.d.ts +16 -0
- package/dist/components/scenes/guide/HelpCenter.svelte +94 -0
- package/dist/components/scenes/guide/HelpCenter.svelte.d.ts +13 -0
- package/dist/components/scenes/guide/index.d.ts +2 -0
- package/dist/components/scenes/guide/index.js +2 -0
- package/dist/components/scenes/iam/AuthPanel.svelte +55 -47
- package/dist/components/scenes/iam/ChangePasswordForm.svelte +57 -59
- package/dist/components/scenes/iam/ForgotPasswordForm.svelte +89 -84
- package/dist/components/scenes/iam/LoginForm.svelte +134 -132
- package/dist/components/scenes/iam/PasswordInput.svelte +25 -7
- package/dist/components/scenes/iam/PasswordInput.svelte.d.ts +2 -2
- package/dist/components/scenes/iam/RegisterForm.svelte +95 -97
- package/dist/components/scenes/iam/ResetPasswordForm.svelte +83 -85
- package/dist/components/scenes/iam/UserProfile.svelte +9 -5
- package/dist/components/scenes/iam/auth-error-message.d.ts +2 -4
- package/dist/components/scenes/index.d.ts +1 -0
- package/dist/components/scenes/index.js +1 -0
- package/dist/components/scenes/storage/FileUpload.svelte +1 -1
- package/dist/components/scenes/storage/ImageUpload.svelte +1 -0
- package/dist/components/scenes/types.d.ts +9 -9
- package/dist/guide-storage.d.ts +6 -0
- package/dist/guide-storage.js +67 -0
- package/dist/guide-types.d.ts +78 -0
- package/dist/guide-types.js +1 -0
- package/dist/guide.d.ts +3 -0
- package/dist/guide.js +3 -0
- package/dist/guide.svelte.d.ts +8 -0
- package/dist/guide.svelte.js +450 -0
- package/dist/i18n.svelte.d.ts +2 -1
- package/dist/i18n.svelte.js +2 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/internal/form-context.d.ts +12 -0
- package/dist/internal/form-context.js +9 -0
- package/dist/internal/form-validation.svelte.d.ts +16 -0
- package/dist/internal/form-validation.svelte.js +76 -0
- package/dist/internal/locale.d.ts +2 -0
- package/dist/internal/locale.js +22 -0
- package/dist/messages/en-US.json +30 -1
- package/dist/messages/zh-CN.json +30 -1
- package/dist/messages.d.ts +2 -1
- package/dist/messages.js +6 -4
- package/dist/styles/guide.css +88 -0
- package/dist/types.d.ts +57 -10
- package/package.json +13 -2
- package/vite.js +1 -0
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
-->
|
|
11
11
|
<script lang='ts' generics="T = string">
|
|
12
12
|
import type { DataAttributes, SelectProps } from '../../types.js'
|
|
13
|
-
import { tick } from 'svelte'
|
|
13
|
+
import { tick, untrack } from 'svelte'
|
|
14
|
+
import { getFormFieldContext } from '../../internal/form-context.js'
|
|
15
|
+
import { createControlValidation, resolveFormText } from '../../internal/form-validation.svelte.js'
|
|
16
|
+
import { uiM } from '../../messages.js'
|
|
14
17
|
import { cn, getDataAttributes, portal } from '../../utils.js'
|
|
15
18
|
import { getFormControlSizeClasses } from '../control-size.js'
|
|
16
19
|
|
|
@@ -24,13 +27,38 @@
|
|
|
24
27
|
clearable = false,
|
|
25
28
|
filterable = false,
|
|
26
29
|
error = '',
|
|
30
|
+
validationMessage = '',
|
|
27
31
|
class: className = '',
|
|
28
32
|
id,
|
|
33
|
+
name,
|
|
29
34
|
onchange,
|
|
30
35
|
...restProps
|
|
31
36
|
}: SelectProps<T> & DataAttributes = $props()
|
|
32
37
|
|
|
33
38
|
const dataAttributes = $derived(getDataAttributes(restProps))
|
|
39
|
+
const field = getFormFieldContext()
|
|
40
|
+
const uniqueId = $props.id()
|
|
41
|
+
const controlId = $derived(id ?? field?.controlId ?? `${uniqueId}-control`)
|
|
42
|
+
const messageId = `${uniqueId}-error`
|
|
43
|
+
const validation = createControlValidation(() => validationMessage)
|
|
44
|
+
const localError = $derived(resolveFormText(error) || validation.message)
|
|
45
|
+
const effectiveError = $derived(field?.error || localError)
|
|
46
|
+
let nativeSelect: HTMLSelectElement | undefined = $state()
|
|
47
|
+
// 独立保存原生选中值,筛选输入的文字不能代替表单实际选项。
|
|
48
|
+
const nativeValue = $derived(value === undefined || value === null ? '' : String(value))
|
|
49
|
+
$effect(() => {
|
|
50
|
+
void options
|
|
51
|
+
void required
|
|
52
|
+
void disabled
|
|
53
|
+
if (nativeSelect) {
|
|
54
|
+
nativeSelect.value = nativeValue
|
|
55
|
+
untrack(() => validation.input(nativeSelect!))
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
$effect(() => {
|
|
59
|
+
if (nativeSelect)
|
|
60
|
+
nativeSelect.setCustomValidity(validation.message)
|
|
61
|
+
})
|
|
34
62
|
let inputRef: HTMLInputElement | undefined = $state()
|
|
35
63
|
let dropdownRef: HTMLDivElement | undefined = $state()
|
|
36
64
|
let triggerRef: HTMLDivElement | undefined = $state()
|
|
@@ -58,7 +86,7 @@
|
|
|
58
86
|
|
|
59
87
|
// 获取当前选中的标签
|
|
60
88
|
const selectedLabel = $derived(() => {
|
|
61
|
-
if (!options ||
|
|
89
|
+
if (!options || value === undefined || value === null)
|
|
62
90
|
return ''
|
|
63
91
|
const selected = options.find(opt => opt.value === value)
|
|
64
92
|
return selected ? selected.label : ''
|
|
@@ -79,7 +107,7 @@
|
|
|
79
107
|
cn(
|
|
80
108
|
'relative flex w-full min-w-0 items-center rounded-lg border bg-base-100 cursor-pointer',
|
|
81
109
|
sizeClasses.control,
|
|
82
|
-
|
|
110
|
+
effectiveError
|
|
83
111
|
? 'border-error/60 focus-within:ring-2 focus-within:ring-error/15'
|
|
84
112
|
: isDropdownOpen
|
|
85
113
|
? 'border-primary/50 ring-2 ring-primary/10'
|
|
@@ -95,7 +123,7 @@
|
|
|
95
123
|
'flex-1 min-w-0 bg-transparent outline-none border-none',
|
|
96
124
|
'focus:outline-none focus:ring-0 focus:shadow-none',
|
|
97
125
|
'placeholder:text-base-content/40',
|
|
98
|
-
|
|
126
|
+
effectiveError && 'text-error',
|
|
99
127
|
),
|
|
100
128
|
)
|
|
101
129
|
|
|
@@ -238,12 +266,31 @@
|
|
|
238
266
|
{...dataAttributes}
|
|
239
267
|
class='fieldset w-full min-w-0'
|
|
240
268
|
bind:this={dropdownRef}
|
|
241
|
-
role='combobox'
|
|
242
|
-
aria-expanded={isDropdownOpen}
|
|
243
|
-
aria-haspopup='listbox'
|
|
244
269
|
onmouseenter={() => isHovered = true}
|
|
245
270
|
onmouseleave={() => isHovered = false}
|
|
246
271
|
>
|
|
272
|
+
<select
|
|
273
|
+
bind:this={nativeSelect}
|
|
274
|
+
class='sr-only'
|
|
275
|
+
tabindex='-1'
|
|
276
|
+
aria-hidden='true'
|
|
277
|
+
{name}
|
|
278
|
+
{required}
|
|
279
|
+
{disabled}
|
|
280
|
+
value={nativeValue}
|
|
281
|
+
oninvalid={(e) => {
|
|
282
|
+
e.preventDefault()
|
|
283
|
+
validation.invalid(e.currentTarget)
|
|
284
|
+
inputRef?.focus()
|
|
285
|
+
}}
|
|
286
|
+
>
|
|
287
|
+
<option value=''></option>
|
|
288
|
+
{#each options ?? [] as option (String(option.value))}
|
|
289
|
+
{#if String(option.value) !== ''}
|
|
290
|
+
<option value={String(option.value)} disabled={option.disabled}>{option.label}</option>
|
|
291
|
+
{/if}
|
|
292
|
+
{/each}
|
|
293
|
+
</select>
|
|
247
294
|
<div
|
|
248
295
|
class={wrapperClass}
|
|
249
296
|
bind:this={triggerRef}
|
|
@@ -260,11 +307,12 @@
|
|
|
260
307
|
<!-- 可筛选模式:可输入的 input -->
|
|
261
308
|
<input
|
|
262
309
|
bind:this={inputRef}
|
|
263
|
-
{
|
|
310
|
+
id={controlId}
|
|
311
|
+
aria-invalid={effectiveError ? true : undefined}
|
|
312
|
+
aria-describedby={[field?.messageId, localError && !field?.error ? messageId : ''].filter(Boolean).join(' ') || undefined}
|
|
264
313
|
type='text'
|
|
265
314
|
class={inputClass}
|
|
266
315
|
{disabled}
|
|
267
|
-
{required}
|
|
268
316
|
{placeholder}
|
|
269
317
|
value={isDropdownOpen ? filterText : selectedLabel()}
|
|
270
318
|
oninput={handleFilterInput}
|
|
@@ -274,22 +322,33 @@
|
|
|
274
322
|
isDropdownOpen = true
|
|
275
323
|
}}
|
|
276
324
|
role='combobox'
|
|
325
|
+
aria-haspopup='listbox'
|
|
277
326
|
aria-autocomplete='list'
|
|
278
|
-
aria-controls='{
|
|
327
|
+
aria-controls='{controlId}-listbox'
|
|
279
328
|
aria-expanded={isDropdownOpen}
|
|
280
329
|
/>
|
|
281
330
|
{:else}
|
|
282
331
|
<!-- 普通模式:只读 input 显示选中值 -->
|
|
283
332
|
<input
|
|
284
|
-
{
|
|
333
|
+
bind:this={inputRef}
|
|
334
|
+
id={controlId}
|
|
335
|
+
aria-invalid={effectiveError ? true : undefined}
|
|
336
|
+
aria-describedby={[field?.messageId, localError && !field?.error ? messageId : ''].filter(Boolean).join(' ') || undefined}
|
|
285
337
|
type='text'
|
|
286
338
|
class={inputClass}
|
|
287
339
|
readonly
|
|
288
340
|
{disabled}
|
|
289
|
-
{required}
|
|
290
341
|
{placeholder}
|
|
291
342
|
value={selectedLabel()}
|
|
292
|
-
|
|
343
|
+
onkeydown={handleInputKeydown}
|
|
344
|
+
onfocus={() => {
|
|
345
|
+
if (!disabled)
|
|
346
|
+
isDropdownOpen = true
|
|
347
|
+
}}
|
|
348
|
+
role='combobox'
|
|
349
|
+
aria-haspopup='listbox'
|
|
350
|
+
aria-controls='{controlId}-listbox'
|
|
351
|
+
aria-expanded={isDropdownOpen}
|
|
293
352
|
/>
|
|
294
353
|
{/if}
|
|
295
354
|
|
|
@@ -301,7 +360,7 @@
|
|
|
301
360
|
class='flex items-center justify-center w-5 h-5 rounded-full hover:bg-base-content/10 transition-colors'
|
|
302
361
|
onclick={handleClear}
|
|
303
362
|
tabindex='-1'
|
|
304
|
-
aria-label='
|
|
363
|
+
aria-label={uiM('select_clear')}
|
|
305
364
|
>
|
|
306
365
|
<svg xmlns='http://www.w3.org/2000/svg' class='h-3.5 w-3.5' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
|
|
307
366
|
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M6 18L18 6M6 6l12 12' />
|
|
@@ -324,7 +383,7 @@
|
|
|
324
383
|
<ul
|
|
325
384
|
bind:this={listboxRef}
|
|
326
385
|
use:portal
|
|
327
|
-
id='{
|
|
386
|
+
id='{controlId}-listbox'
|
|
328
387
|
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'
|
|
329
388
|
style={dropdownStyle}
|
|
330
389
|
role='listbox'
|
|
@@ -356,13 +415,13 @@
|
|
|
356
415
|
</li>
|
|
357
416
|
{:else}
|
|
358
417
|
<li class={cn('px-3 py-3 text-base-content/40 text-center', optionTextClass)}>
|
|
359
|
-
|
|
418
|
+
{uiM('select_no_match')}
|
|
360
419
|
</li>
|
|
361
420
|
{/each}
|
|
362
421
|
</ul>
|
|
363
422
|
{/if}
|
|
364
423
|
</div>
|
|
365
|
-
{#if error}
|
|
366
|
-
<
|
|
424
|
+
{#if localError && !field?.error}
|
|
425
|
+
<p id={messageId} class='mt-1.5 text-xs text-error leading-relaxed break-words' aria-live='polite'>{localError}</p>
|
|
367
426
|
{/if}
|
|
368
427
|
</div>
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
-->
|
|
11
11
|
<script lang='ts'>
|
|
12
12
|
import type { DataAttributes, TextareaProps } from '../../types.js'
|
|
13
|
+
import { getFormFieldContext } from '../../internal/form-context.js'
|
|
14
|
+
import { createControlValidation, resolveFormText } from '../../internal/form-validation.svelte.js'
|
|
13
15
|
import { cn, getDataAttributes, getInputSizeClass } from '../../utils.js'
|
|
14
16
|
|
|
15
17
|
let {
|
|
@@ -23,20 +25,30 @@
|
|
|
23
25
|
autoResize = false,
|
|
24
26
|
error = '',
|
|
25
27
|
validationMessage = '',
|
|
28
|
+
minlength,
|
|
29
|
+
maxlength,
|
|
30
|
+
inputmode,
|
|
31
|
+
textareaRef = $bindable<HTMLTextAreaElement | undefined>(),
|
|
26
32
|
class: className = '',
|
|
27
33
|
id,
|
|
28
34
|
name,
|
|
29
35
|
oninput,
|
|
36
|
+
oninvalid,
|
|
30
37
|
...restProps
|
|
31
38
|
}: TextareaProps & DataAttributes = $props()
|
|
32
39
|
|
|
33
40
|
const dataAttributes = $derived(getDataAttributes(restProps))
|
|
34
|
-
|
|
41
|
+
const field = getFormFieldContext()
|
|
42
|
+
const validation = createControlValidation(() => validationMessage, () => [value, required, readonly, disabled, minlength, maxlength])
|
|
43
|
+
const uniqueId = $props.id()
|
|
44
|
+
const messageId = `${uniqueId}-error`
|
|
45
|
+
const localError = $derived(resolveFormText(error) || validation.message)
|
|
46
|
+
const effectiveError = $derived(field?.error || localError)
|
|
35
47
|
|
|
36
48
|
const wrapperClass = $derived(
|
|
37
49
|
cn(
|
|
38
50
|
'flex w-full rounded-lg border bg-base-100',
|
|
39
|
-
|
|
51
|
+
effectiveError
|
|
40
52
|
? 'border-error/60 focus-within:ring-2 focus-within:ring-error/15'
|
|
41
53
|
: 'border-base-content/15 focus-within:border-primary/50 focus-within:ring-2 focus-within:ring-primary/10',
|
|
42
54
|
'transition-[border-color,box-shadow] duration-150',
|
|
@@ -48,7 +60,7 @@
|
|
|
48
60
|
cn(
|
|
49
61
|
'textarea w-full bg-transparent border-none outline-none',
|
|
50
62
|
getInputSizeClass(size),
|
|
51
|
-
|
|
63
|
+
effectiveError && 'textarea-error',
|
|
52
64
|
className,
|
|
53
65
|
),
|
|
54
66
|
)
|
|
@@ -56,7 +68,7 @@
|
|
|
56
68
|
// 当 validationMessage 变化时更新自定义验证消息
|
|
57
69
|
$effect(() => {
|
|
58
70
|
if (textareaRef) {
|
|
59
|
-
textareaRef.setCustomValidity(
|
|
71
|
+
textareaRef.setCustomValidity(validation.message)
|
|
60
72
|
}
|
|
61
73
|
})
|
|
62
74
|
|
|
@@ -70,17 +82,14 @@
|
|
|
70
82
|
}
|
|
71
83
|
|
|
72
84
|
// 输入时重置自定义验证
|
|
73
|
-
|
|
74
|
-
e.currentTarget.setCustomValidity(validationMessage)
|
|
75
|
-
}
|
|
85
|
+
validation.input(e.currentTarget)
|
|
76
86
|
|
|
77
87
|
oninput?.(e)
|
|
78
88
|
}
|
|
79
89
|
|
|
80
90
|
function handleInvalid(e: Event & { currentTarget: HTMLTextAreaElement }) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
91
|
+
validation.invalid(e.currentTarget)
|
|
92
|
+
oninvalid?.(e)
|
|
84
93
|
}
|
|
85
94
|
</script>
|
|
86
95
|
|
|
@@ -88,20 +97,25 @@
|
|
|
88
97
|
<div class={wrapperClass}>
|
|
89
98
|
<textarea
|
|
90
99
|
bind:this={textareaRef}
|
|
91
|
-
{id}
|
|
100
|
+
id={id ?? field?.controlId}
|
|
101
|
+
aria-invalid={effectiveError ? true : undefined}
|
|
102
|
+
aria-describedby={[field?.messageId, localError && !field?.error ? messageId : ''].filter(Boolean).join(' ') || undefined}
|
|
92
103
|
{name}
|
|
93
104
|
{placeholder}
|
|
94
105
|
{rows}
|
|
95
106
|
{disabled}
|
|
96
107
|
{readonly}
|
|
97
108
|
{required}
|
|
109
|
+
{minlength}
|
|
110
|
+
{maxlength}
|
|
111
|
+
{inputmode}
|
|
98
112
|
class={textareaClass}
|
|
99
113
|
bind:value
|
|
100
114
|
oninput={handleInput}
|
|
101
115
|
oninvalid={handleInvalid}
|
|
102
116
|
></textarea>
|
|
103
117
|
</div>
|
|
104
|
-
{#if error}
|
|
105
|
-
<
|
|
118
|
+
{#if localError && !field?.error}
|
|
119
|
+
<p id={messageId} class='mt-1.5 text-xs text-error leading-relaxed break-words' aria-live='polite'>{localError}</p>
|
|
106
120
|
{/if}
|
|
107
121
|
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DataAttributes, TextareaProps } from '../../types.js';
|
|
2
2
|
type $$ComponentProps = TextareaProps & DataAttributes;
|
|
3
|
-
declare const Textarea: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
3
|
+
declare const Textarea: import("svelte").Component<$$ComponentProps, {}, "value" | "textareaRef">;
|
|
4
4
|
type Textarea = ReturnType<typeof Textarea>;
|
|
5
5
|
export default Textarea;
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
/>
|
|
13
13
|
-->
|
|
14
14
|
<script lang='ts'>
|
|
15
|
+
import FormField from '../../compounds/FormField.svelte'
|
|
16
|
+
import FormLayout from '../../compounds/FormLayout.svelte'
|
|
15
17
|
import Modal from '../../compounds/Modal.svelte'
|
|
16
18
|
import Button from '../../primitives/Button.svelte'
|
|
17
19
|
import Input from '../../primitives/Input.svelte'
|
|
@@ -47,18 +49,18 @@
|
|
|
47
49
|
}
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
import type { DataAttributes } from '../../../types.js'
|
|
52
|
+
import type { DataAttributes, FormLayoutOptions } from '../../../types.js'
|
|
51
53
|
import { uiM } from '../../../messages.js'
|
|
52
54
|
import { getDataAttributes } from '../../../utils.js'
|
|
53
55
|
|
|
54
|
-
interface Props {
|
|
56
|
+
interface Props extends FormLayoutOptions {
|
|
55
57
|
open?: boolean
|
|
56
58
|
labels?: FeedbackLabels
|
|
57
59
|
onsubmit?: (data: FeedbackData) => Promise<void>
|
|
58
60
|
class?: string
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
let { open = $bindable(false), labels = {}, onsubmit, class: className = '', ...restProps }: Props & DataAttributes = $props()
|
|
63
|
+
let { open = $bindable(false), labels = {}, columns = 1, labelPosition = 'top', labelWidth = '8rem', onsubmit, class: className = '', ...restProps }: Props & DataAttributes = $props()
|
|
62
64
|
|
|
63
65
|
const dataAttributes = $derived(getDataAttributes(restProps))
|
|
64
66
|
// 文案优先使用传入的 labels,缺省回退到内置消息 uiM(...)
|
|
@@ -67,7 +69,9 @@
|
|
|
67
69
|
let description = $state('')
|
|
68
70
|
let contact = $state('')
|
|
69
71
|
let loading = $state(false)
|
|
70
|
-
let error = $state('')
|
|
72
|
+
let error = $state<'empty' | 'submit' | ''>('')
|
|
73
|
+
let submitError = $state('')
|
|
74
|
+
const errorText = $derived(error === 'empty' ? labels.errorEmpty ?? uiM('feedback_error_empty') : error === 'submit' ? submitError || labels.errorSubmit || uiM('feedback_error_submit') : '')
|
|
71
75
|
|
|
72
76
|
const typeOptions = $derived([
|
|
73
77
|
{ value: 'bug', label: labels.types?.bug ?? uiM('feedback_type_bug') },
|
|
@@ -77,8 +81,10 @@
|
|
|
77
81
|
])
|
|
78
82
|
|
|
79
83
|
async function handleSubmit() {
|
|
84
|
+
if (loading)
|
|
85
|
+
return
|
|
80
86
|
if (!description.trim()) {
|
|
81
|
-
error =
|
|
87
|
+
error = 'empty'
|
|
82
88
|
return
|
|
83
89
|
}
|
|
84
90
|
|
|
@@ -95,7 +101,8 @@
|
|
|
95
101
|
resetForm()
|
|
96
102
|
}
|
|
97
103
|
catch (e) {
|
|
98
|
-
|
|
104
|
+
submitError = e instanceof Error ? e.message : ''
|
|
105
|
+
error = 'submit'
|
|
99
106
|
}
|
|
100
107
|
finally {
|
|
101
108
|
loading = false
|
|
@@ -107,41 +114,44 @@
|
|
|
107
114
|
description = ''
|
|
108
115
|
contact = ''
|
|
109
116
|
error = ''
|
|
117
|
+
submitError = ''
|
|
110
118
|
}
|
|
111
119
|
</script>
|
|
112
120
|
|
|
113
121
|
<Modal {...dataAttributes} class={className} bind:open title={labels.title ?? uiM('feedback_title')}>
|
|
114
122
|
<p class='text-base-content/70 mb-4'>{labels.description ?? uiM('feedback_description')}</p>
|
|
115
123
|
|
|
116
|
-
{#if error}
|
|
124
|
+
{#if error === 'submit'}
|
|
117
125
|
<div class='alert alert-error mb-4'>
|
|
118
126
|
<span class='icon-[tabler--alert-circle] size-5'></span>
|
|
119
|
-
<span>{
|
|
127
|
+
<span>{errorText}</span>
|
|
120
128
|
</div>
|
|
121
129
|
{/if}
|
|
122
130
|
|
|
123
|
-
<
|
|
124
|
-
<
|
|
125
|
-
<legend class='fieldset-legend'>{labels.typeLabel ?? uiM('feedback_type_label')}</legend>
|
|
131
|
+
<FormLayout {columns} {labelPosition} {labelWidth}>
|
|
132
|
+
<FormField for='feedback-type' label={labels.typeLabel ?? uiM('feedback_type_label')}>
|
|
126
133
|
<Select
|
|
127
134
|
id='feedback-type'
|
|
128
135
|
options={typeOptions}
|
|
129
136
|
bind:value={feedbackType}
|
|
130
137
|
/>
|
|
131
|
-
</
|
|
138
|
+
</FormField>
|
|
132
139
|
|
|
133
|
-
<
|
|
134
|
-
<legend class='fieldset-legend'>{labels.contentLabel ?? uiM('feedback_content_label')} <span class='text-error'>*</span></legend>
|
|
140
|
+
<FormField for='feedback-desc' label={labels.contentLabel ?? uiM('feedback_content_label')} required error={error === 'empty' ? errorText : ''}>
|
|
135
141
|
<Textarea
|
|
136
142
|
id='feedback-desc'
|
|
137
143
|
class='h-32'
|
|
138
144
|
placeholder={labels.contentPlaceholder ?? uiM('feedback_content_placeholder')}
|
|
139
145
|
bind:value={description}
|
|
146
|
+
disabled={loading}
|
|
147
|
+
oninput={() => {
|
|
148
|
+
if (description.trim())
|
|
149
|
+
error = ''
|
|
150
|
+
}}
|
|
140
151
|
/>
|
|
141
|
-
</
|
|
152
|
+
</FormField>
|
|
142
153
|
|
|
143
|
-
<
|
|
144
|
-
<legend class='fieldset-legend'>{labels.contactLabel ?? uiM('feedback_contact_label')}</legend>
|
|
154
|
+
<FormField for='feedback-contact' label={labels.contactLabel ?? uiM('feedback_contact_label')}>
|
|
145
155
|
<Input
|
|
146
156
|
id='feedback-contact'
|
|
147
157
|
type='text'
|
|
@@ -150,8 +160,8 @@
|
|
|
150
160
|
bind:value={contact}
|
|
151
161
|
autocomplete='email'
|
|
152
162
|
/>
|
|
153
|
-
</
|
|
154
|
-
</
|
|
163
|
+
</FormField>
|
|
164
|
+
</FormLayout>
|
|
155
165
|
|
|
156
166
|
{#snippet footer()}
|
|
157
167
|
<Button variant='ghost' onclick={() => open = false}>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DataAttributes } from '../../../types.js';
|
|
1
|
+
import type { DataAttributes, FormLayoutOptions } from '../../../types.js';
|
|
2
2
|
type FeedbackType = 'bug' | 'feature' | 'question' | 'other';
|
|
3
3
|
interface FeedbackData {
|
|
4
4
|
type: FeedbackType;
|
|
@@ -25,7 +25,7 @@ interface FeedbackLabels {
|
|
|
25
25
|
other?: string;
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
-
interface Props {
|
|
28
|
+
interface Props extends FormLayoutOptions {
|
|
29
29
|
open?: boolean;
|
|
30
30
|
labels?: FeedbackLabels;
|
|
31
31
|
onsubmit?: (data: FeedbackData) => Promise<void>;
|