@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
|
@@ -23,8 +23,10 @@
|
|
|
23
23
|
-->
|
|
24
24
|
<script lang='ts'>
|
|
25
25
|
import type { DateValue } from '@internationalized/date'
|
|
26
|
-
import type { DataAttributes, Size } from '../../types.js'
|
|
26
|
+
import type { DataAttributes, FormText, Size } from '../../types.js'
|
|
27
27
|
import { DatePicker as BitsDatePicker } from 'bits-ui'
|
|
28
|
+
import { getFormFieldContext } from '../../internal/form-context.js'
|
|
29
|
+
import { resolveFormText } from '../../internal/form-validation.svelte.js'
|
|
28
30
|
import { cn, getDataAttributes } from '../../utils.js'
|
|
29
31
|
import { getFormControlSizeClasses } from '../control-size.js'
|
|
30
32
|
|
|
@@ -44,7 +46,7 @@
|
|
|
44
46
|
/** 尺寸 */
|
|
45
47
|
size?: Size
|
|
46
48
|
/** 错误消息 */
|
|
47
|
-
error?:
|
|
49
|
+
error?: FormText
|
|
48
50
|
/** 自定义类名 */
|
|
49
51
|
class?: string
|
|
50
52
|
/** 日期变更回调 */
|
|
@@ -66,6 +68,11 @@
|
|
|
66
68
|
}: Props & DataAttributes = $props()
|
|
67
69
|
|
|
68
70
|
const dataAttributes = $derived(getDataAttributes(restProps))
|
|
71
|
+
const field = getFormFieldContext()
|
|
72
|
+
const uniqueId = $props.id()
|
|
73
|
+
const messageId = `${uniqueId}-message`
|
|
74
|
+
const localError = $derived(resolveFormText(error))
|
|
75
|
+
const effectiveError = $derived(field?.error || localError)
|
|
69
76
|
const sizeClasses = $derived(getFormControlSizeClasses(size))
|
|
70
77
|
</script>
|
|
71
78
|
|
|
@@ -80,10 +87,12 @@
|
|
|
80
87
|
onValueChange={v => onchange?.(v)}
|
|
81
88
|
>
|
|
82
89
|
<BitsDatePicker.Input
|
|
90
|
+
aria-labelledby={field?.labelId}
|
|
91
|
+
aria-describedby={[field?.messageId, localError && !field?.error ? messageId : ''].filter(Boolean).join(' ') || undefined}
|
|
83
92
|
class={cn(
|
|
84
93
|
'flex w-full items-center gap-0.5 rounded-lg border bg-base-100 pr-2 transition-[border-color,box-shadow] duration-150',
|
|
85
94
|
sizeClasses.control,
|
|
86
|
-
|
|
95
|
+
effectiveError
|
|
87
96
|
? 'border-error/60 focus-within:ring-2 focus-within:ring-error/15'
|
|
88
97
|
: 'border-base-content/15 hover:border-base-content/25 focus-within:border-primary/50 focus-within:ring-2 focus-within:ring-primary/10',
|
|
89
98
|
disabled && 'cursor-not-allowed opacity-50',
|
|
@@ -149,7 +158,7 @@
|
|
|
149
158
|
</BitsDatePicker.Content>
|
|
150
159
|
</BitsDatePicker.Root>
|
|
151
160
|
|
|
152
|
-
{#if error}
|
|
153
|
-
<
|
|
161
|
+
{#if localError && !field?.error}
|
|
162
|
+
<p id={messageId} class='mt-1.5 text-xs text-error leading-relaxed break-words' aria-live='polite'>{localError}</p>
|
|
154
163
|
{/if}
|
|
155
164
|
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DateValue } from '@internationalized/date';
|
|
2
|
-
import type { DataAttributes, Size } from '../../types.js';
|
|
2
|
+
import type { DataAttributes, FormText, Size } from '../../types.js';
|
|
3
3
|
interface Props {
|
|
4
4
|
/** 选中日期(双向绑定) */
|
|
5
5
|
value?: DateValue;
|
|
@@ -16,7 +16,7 @@ interface Props {
|
|
|
16
16
|
/** 尺寸 */
|
|
17
17
|
size?: Size;
|
|
18
18
|
/** 错误消息 */
|
|
19
|
-
error?:
|
|
19
|
+
error?: FormText;
|
|
20
20
|
/** 自定义类名 */
|
|
21
21
|
class?: string;
|
|
22
22
|
/** 日期变更回调 */
|
|
@@ -10,12 +10,16 @@
|
|
|
10
10
|
<script lang='ts'>
|
|
11
11
|
import type { DataAttributes, FormProps } from '../../types.js'
|
|
12
12
|
import { cn, getDataAttributes } from '../../utils.js'
|
|
13
|
+
import FormLayout from './FormLayout.svelte'
|
|
13
14
|
|
|
14
15
|
const {
|
|
15
16
|
id = '',
|
|
16
17
|
class: className = '',
|
|
17
18
|
loading = false,
|
|
18
19
|
disabled = false,
|
|
20
|
+
columns = 1,
|
|
21
|
+
labelPosition = 'top',
|
|
22
|
+
labelWidth = '8rem',
|
|
19
23
|
onsubmit,
|
|
20
24
|
onreset,
|
|
21
25
|
onerror,
|
|
@@ -65,9 +69,7 @@
|
|
|
65
69
|
}
|
|
66
70
|
}
|
|
67
71
|
|
|
68
|
-
function handleReset(
|
|
69
|
-
e.preventDefault()
|
|
70
|
-
formElement?.reset()
|
|
72
|
+
function handleReset() {
|
|
71
73
|
onreset?.()
|
|
72
74
|
}
|
|
73
75
|
|
|
@@ -94,7 +96,9 @@
|
|
|
94
96
|
onsubmit={handleSubmit}
|
|
95
97
|
onreset={handleReset}
|
|
96
98
|
>
|
|
97
|
-
{
|
|
98
|
-
{
|
|
99
|
-
|
|
99
|
+
<FormLayout {columns} {labelPosition} {labelWidth}>
|
|
100
|
+
{#if children}
|
|
101
|
+
{@render children()}
|
|
102
|
+
{/if}
|
|
103
|
+
</FormLayout>
|
|
100
104
|
</form>
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
-->
|
|
10
10
|
<script lang='ts'>
|
|
11
11
|
import type { DataAttributes, FormFieldProps } from '../../types.js'
|
|
12
|
+
import { setContext } from 'svelte'
|
|
13
|
+
import { FORM_FIELD_CONTEXT, getFormLayoutContext } from '../../internal/form-context.js'
|
|
14
|
+
import { resolveFormText } from '../../internal/form-validation.svelte.js'
|
|
12
15
|
import { cn, getDataAttributes } from '../../utils.js'
|
|
13
16
|
|
|
14
17
|
const {
|
|
@@ -16,43 +19,105 @@
|
|
|
16
19
|
error = '',
|
|
17
20
|
hint = '',
|
|
18
21
|
required = false,
|
|
22
|
+
for: providedControlId,
|
|
23
|
+
labelPosition,
|
|
24
|
+
labelWidth,
|
|
25
|
+
colSpan = 'auto',
|
|
19
26
|
class: className = '',
|
|
20
27
|
children,
|
|
21
28
|
...restProps
|
|
22
29
|
}: FormFieldProps & DataAttributes = $props()
|
|
23
30
|
|
|
24
31
|
const dataAttributes = $derived(getDataAttributes(restProps))
|
|
32
|
+
const layout = getFormLayoutContext()
|
|
33
|
+
const uniqueId = $props.id()
|
|
34
|
+
const controlId = $derived(providedControlId ?? `${uniqueId}-control`)
|
|
35
|
+
const messageId = `${uniqueId}-message`
|
|
36
|
+
const labelId = `${uniqueId}-label`
|
|
37
|
+
const resolvedLabel = $derived(resolveFormText(label))
|
|
38
|
+
const resolvedError = $derived(resolveFormText(error))
|
|
39
|
+
const resolvedHint = $derived(resolveFormText(hint))
|
|
40
|
+
const position = $derived(labelPosition ?? layout?.labelPosition ?? 'top')
|
|
41
|
+
const width = $derived(labelWidth ?? layout?.labelWidth ?? '8rem')
|
|
42
|
+
setContext(FORM_FIELD_CONTEXT, {
|
|
43
|
+
get controlId() { return controlId },
|
|
44
|
+
get labelId() { return labelId },
|
|
45
|
+
get messageId() { return resolvedError || resolvedHint ? messageId : undefined },
|
|
46
|
+
get error() { return resolvedError },
|
|
47
|
+
})
|
|
25
48
|
const fieldClass = $derived(
|
|
26
49
|
cn(
|
|
27
|
-
'
|
|
50
|
+
'hai-form-field w-full min-w-0',
|
|
28
51
|
className,
|
|
29
52
|
),
|
|
30
53
|
)
|
|
31
54
|
</script>
|
|
32
55
|
|
|
33
|
-
<div {...dataAttributes} class={fieldClass}>
|
|
34
|
-
{#if
|
|
35
|
-
<
|
|
36
|
-
{
|
|
56
|
+
<div {...dataAttributes} class={fieldClass} data-col-span={colSpan} data-label-position={position} style:--hai-form-label-width={width}>
|
|
57
|
+
{#if resolvedLabel}
|
|
58
|
+
<label id={labelId} for={controlId} class='field-label text-sm font-medium text-base-content/70'>
|
|
59
|
+
{resolvedLabel}
|
|
37
60
|
{#if required}
|
|
38
61
|
<span class='text-error'>*</span>
|
|
39
62
|
{/if}
|
|
40
|
-
</
|
|
63
|
+
</label>
|
|
41
64
|
{/if}
|
|
42
65
|
|
|
43
|
-
<div class='field-content'>
|
|
66
|
+
<div class='field-content min-w-0'>
|
|
44
67
|
{#if children}
|
|
45
68
|
{@render children()}
|
|
46
69
|
{/if}
|
|
47
70
|
</div>
|
|
48
71
|
|
|
49
|
-
{#if
|
|
50
|
-
<div>
|
|
51
|
-
{#if
|
|
52
|
-
<span class='
|
|
53
|
-
{:else if
|
|
54
|
-
<span class='
|
|
72
|
+
{#if resolvedError || resolvedHint}
|
|
73
|
+
<div id={messageId} class='field-message text-xs leading-relaxed break-words' aria-live='polite'>
|
|
74
|
+
{#if resolvedError}
|
|
75
|
+
<span class='text-error'>{resolvedError}</span>
|
|
76
|
+
{:else if resolvedHint}
|
|
77
|
+
<span class='text-base-content/60'>{resolvedHint}</span>
|
|
55
78
|
{/if}
|
|
56
79
|
</div>
|
|
57
80
|
{/if}
|
|
58
81
|
</div>
|
|
82
|
+
|
|
83
|
+
<style>
|
|
84
|
+
.hai-form-field {
|
|
85
|
+
display: grid;
|
|
86
|
+
grid-template-columns: minmax(0, 1fr);
|
|
87
|
+
gap: 0.375rem 1rem;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.field-label {
|
|
91
|
+
overflow-wrap: anywhere;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.field-content > :global(.fieldset) {
|
|
95
|
+
padding-block: 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.hai-form-field[data-label-position='left']:has(.field-label) {
|
|
99
|
+
grid-template-columns: minmax(0, var(--hai-form-label-width)) minmax(0, 1fr);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.hai-form-field[data-label-position='left'] .field-label {
|
|
103
|
+
padding-top: 0.5rem;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.hai-form-field[data-label-position='left']:has(.field-label) .field-message {
|
|
107
|
+
grid-column: 2;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@container hai-form (max-width: 27.99rem) {
|
|
111
|
+
.hai-form-field[data-label-position='left']:has(.field-label) {
|
|
112
|
+
grid-template-columns: minmax(0, 1fr);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.hai-form-field[data-label-position='left'] .field-label {
|
|
116
|
+
padding-top: 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.hai-form-field[data-label-position='left']:has(.field-label) .field-message {
|
|
120
|
+
grid-column: 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
</style>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<!-- 表单字段布局,供 Form 和原生表单/场景组件共同复用。 -->
|
|
2
|
+
<script lang='ts'>
|
|
3
|
+
import type { FormLayoutProps } from '../../types.js'
|
|
4
|
+
import { setContext } from 'svelte'
|
|
5
|
+
import { FORM_LAYOUT_CONTEXT } from '../../internal/form-context.js'
|
|
6
|
+
import { cn, getDataAttributes } from '../../utils.js'
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
columns = 1,
|
|
10
|
+
labelPosition = 'top',
|
|
11
|
+
labelWidth = '8rem',
|
|
12
|
+
class: className = '',
|
|
13
|
+
children,
|
|
14
|
+
...restProps
|
|
15
|
+
}: FormLayoutProps = $props()
|
|
16
|
+
|
|
17
|
+
const dataAttributes = $derived(getDataAttributes(restProps))
|
|
18
|
+
setContext(FORM_LAYOUT_CONTEXT, {
|
|
19
|
+
get labelPosition() { return labelPosition },
|
|
20
|
+
get labelWidth() { return labelWidth },
|
|
21
|
+
})
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<div {...dataAttributes} class={cn('hai-form-layout', className)}>
|
|
25
|
+
<div class='hai-form-grid' data-columns={columns}>
|
|
26
|
+
{#if children}{@render children()}{/if}
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<style>
|
|
31
|
+
.hai-form-layout {
|
|
32
|
+
container: hai-form / inline-size;
|
|
33
|
+
width: 100%;
|
|
34
|
+
min-width: 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.hai-form-grid {
|
|
38
|
+
display: grid;
|
|
39
|
+
grid-template-columns: minmax(0, 1fr);
|
|
40
|
+
gap: var(--hai-form-gap, 1rem);
|
|
41
|
+
align-items: start;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.hai-form-grid > :global(*) {
|
|
45
|
+
min-width: 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@container hai-form (min-width: 36rem) {
|
|
49
|
+
.hai-form-grid[data-columns='2'] {
|
|
50
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.hai-form-grid[data-columns='2'] > :global(:not(.hai-form-field)),
|
|
54
|
+
.hai-form-grid[data-columns='2'] > :global(.hai-form-field[data-col-span='2']),
|
|
55
|
+
.hai-form-grid[data-columns='2'] > :global(.hai-form-field[data-col-span='auto']:has(textarea, input[type='file'], [data-form-wide], [aria-multiselectable='true'])) {
|
|
56
|
+
grid-column: 1 / -1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
</style>
|
|
@@ -24,6 +24,7 @@ export { default as Dropdown } from './Dropdown.svelte';
|
|
|
24
24
|
export { default as Empty } from './Empty.svelte';
|
|
25
25
|
export { default as Form } from './Form.svelte';
|
|
26
26
|
export { default as FormField } from './FormField.svelte';
|
|
27
|
+
export { default as FormLayout } from './FormLayout.svelte';
|
|
27
28
|
export { default as InfiniteScroll } from './InfiniteScroll.svelte';
|
|
28
29
|
export { default as MessageBoxContainer } from './MessageBoxContainer.svelte';
|
|
29
30
|
export { default as MessageBoxDialog } from './MessageBoxDialog.svelte';
|
|
@@ -31,6 +31,7 @@ export { default as Empty } from './Empty.svelte';
|
|
|
31
31
|
// 表单组合
|
|
32
32
|
export { default as Form } from './Form.svelte';
|
|
33
33
|
export { default as FormField } from './FormField.svelte';
|
|
34
|
+
export { default as FormLayout } from './FormLayout.svelte';
|
|
34
35
|
// 无限滚动 / 上拉加载
|
|
35
36
|
export { default as InfiniteScroll } from './InfiniteScroll.svelte';
|
|
36
37
|
// MessageBox 命令式弹框(类似 ElementUI MessageBox)
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
-->
|
|
10
10
|
<script lang='ts'>
|
|
11
11
|
import type { BareInputProps, DataAttributes } from '../../types.js'
|
|
12
|
+
import { getFormFieldContext } from '../../internal/form-context.js'
|
|
12
13
|
import { getDataAttributes } from '../../utils.js'
|
|
13
14
|
|
|
14
15
|
let {
|
|
@@ -37,6 +38,8 @@
|
|
|
37
38
|
ariaExpanded,
|
|
38
39
|
ariaControls,
|
|
39
40
|
ariaAutocomplete,
|
|
41
|
+
ariaInvalid,
|
|
42
|
+
ariaDescribedby,
|
|
40
43
|
oninput,
|
|
41
44
|
onchange,
|
|
42
45
|
onkeydown,
|
|
@@ -47,6 +50,7 @@
|
|
|
47
50
|
}: BareInputProps & DataAttributes = $props()
|
|
48
51
|
|
|
49
52
|
const dataAttributes = $derived(getDataAttributes(restProps))
|
|
53
|
+
const field = getFormFieldContext()
|
|
50
54
|
function handleInput(e: Event & { currentTarget: HTMLInputElement }) {
|
|
51
55
|
// 文件输入的 value 由浏览器管理,不能把 fakepath 写回原生控件。
|
|
52
56
|
if (type !== 'file')
|
|
@@ -73,7 +77,9 @@
|
|
|
73
77
|
|
|
74
78
|
<input {...dataAttributes}
|
|
75
79
|
bind:this={inputRef}
|
|
76
|
-
{id}
|
|
80
|
+
id={id ?? field?.controlId}
|
|
81
|
+
aria-invalid={ariaInvalid ?? (field?.error ? true : undefined)}
|
|
82
|
+
aria-describedby={ariaDescribedby ?? field?.messageId}
|
|
77
83
|
{name}
|
|
78
84
|
{type}
|
|
79
85
|
{placeholder}
|
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
-->
|
|
10
10
|
<script lang='ts'>
|
|
11
11
|
import type { CheckboxProps, DataAttributes } from '../../types.js'
|
|
12
|
-
import {
|
|
12
|
+
import { getFormFieldContext } from '../../internal/form-context.js'
|
|
13
|
+
import { cn, getDataAttributes, getSizeClass } from '../../utils.js'
|
|
13
14
|
|
|
14
15
|
let {
|
|
15
16
|
checked = $bindable(false),
|
|
17
|
+
id: providedId,
|
|
16
18
|
label = '',
|
|
17
19
|
size = 'md',
|
|
18
20
|
disabled = false,
|
|
@@ -24,7 +26,9 @@
|
|
|
24
26
|
}: CheckboxProps & DataAttributes = $props()
|
|
25
27
|
|
|
26
28
|
const dataAttributes = $derived(getDataAttributes(restProps))
|
|
27
|
-
const
|
|
29
|
+
const field = getFormFieldContext()
|
|
30
|
+
const uniqueId = $props.id()
|
|
31
|
+
const id = $derived(providedId ?? uniqueId)
|
|
28
32
|
|
|
29
33
|
const checkboxClass = $derived(
|
|
30
34
|
cn(
|
|
@@ -46,6 +50,8 @@
|
|
|
46
50
|
<label class='label cursor-pointer gap-2' for={id}>
|
|
47
51
|
<input
|
|
48
52
|
{id}
|
|
53
|
+
aria-invalid={field?.error ? true : undefined}
|
|
54
|
+
aria-describedby={field?.messageId}
|
|
49
55
|
type='checkbox'
|
|
50
56
|
class={checkboxClass}
|
|
51
57
|
disabled={disabled || readonly}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
-->
|
|
5
5
|
<script lang='ts'>
|
|
6
6
|
import type { DataAttributes, InputProps } from '../../types.js'
|
|
7
|
+
import { getFormFieldContext } from '../../internal/form-context.js'
|
|
8
|
+
import { createControlValidation, resolveFormText } from '../../internal/form-validation.svelte.js'
|
|
7
9
|
import { cn, getDataAttributes } from '../../utils.js'
|
|
8
10
|
import { getFormControlSizeClasses } from '../control-size.js'
|
|
9
11
|
|
|
@@ -40,6 +42,17 @@
|
|
|
40
42
|
}: InputProps & DataAttributes = $props()
|
|
41
43
|
|
|
42
44
|
const dataAttributes = $derived(getDataAttributes(restProps))
|
|
45
|
+
const field = getFormFieldContext()
|
|
46
|
+
const validation = createControlValidation(() => validationMessage, () => [value, required, readonly, disabled, pattern, min, max, minlength, maxlength, step])
|
|
47
|
+
const uniqueId = $props.id()
|
|
48
|
+
const messageId = `${uniqueId}-error`
|
|
49
|
+
const localError = $derived(resolveFormText(error) || validation.message)
|
|
50
|
+
const effectiveError = $derived(field?.error || localError)
|
|
51
|
+
$effect(() => {
|
|
52
|
+
// 修正输入后清除旧的 customError,切换语言时同步原生提示。
|
|
53
|
+
if (inputRef)
|
|
54
|
+
inputRef.setCustomValidity(validation.message)
|
|
55
|
+
})
|
|
43
56
|
const sizeClasses = $derived(getFormControlSizeClasses(size))
|
|
44
57
|
|
|
45
58
|
const inputTypeClass = $derived(
|
|
@@ -52,7 +65,7 @@
|
|
|
52
65
|
cn(
|
|
53
66
|
'flex items-center w-full rounded-lg border bg-base-100',
|
|
54
67
|
sizeClasses.height,
|
|
55
|
-
|
|
68
|
+
effectiveError
|
|
56
69
|
? 'border-error/60 focus-within:ring-2 focus-within:ring-error/15'
|
|
57
70
|
: 'border-base-content/15 focus-within:border-primary/50 focus-within:ring-2 focus-within:ring-primary/10',
|
|
58
71
|
'transition-[border-color,box-shadow] duration-150',
|
|
@@ -62,9 +75,7 @@
|
|
|
62
75
|
)
|
|
63
76
|
|
|
64
77
|
function handleInput(e: Event & { currentTarget: HTMLInputElement }) {
|
|
65
|
-
|
|
66
|
-
e.currentTarget.setCustomValidity('')
|
|
67
|
-
}
|
|
78
|
+
validation.input(e.currentTarget)
|
|
68
79
|
value = e.currentTarget.value
|
|
69
80
|
oninput?.(e)
|
|
70
81
|
}
|
|
@@ -86,9 +97,7 @@
|
|
|
86
97
|
}
|
|
87
98
|
|
|
88
99
|
function handleInvalid(e: Event & { currentTarget: HTMLInputElement }) {
|
|
89
|
-
|
|
90
|
-
e.currentTarget.setCustomValidity(validationMessage)
|
|
91
|
-
}
|
|
100
|
+
validation.invalid(e.currentTarget)
|
|
92
101
|
oninvalid?.(e)
|
|
93
102
|
}
|
|
94
103
|
</script>
|
|
@@ -97,7 +106,9 @@
|
|
|
97
106
|
<div class={containerClass}>
|
|
98
107
|
<input
|
|
99
108
|
bind:this={inputRef}
|
|
100
|
-
{id}
|
|
109
|
+
id={id ?? field?.controlId}
|
|
110
|
+
aria-invalid={effectiveError ? true : undefined}
|
|
111
|
+
aria-describedby={[field?.messageId, localError && !field?.error ? messageId : ''].filter(Boolean).join(' ') || undefined}
|
|
101
112
|
{name}
|
|
102
113
|
{type}
|
|
103
114
|
{placeholder}
|
|
@@ -114,7 +125,7 @@
|
|
|
114
125
|
inputmode={inputmode}
|
|
115
126
|
autocomplete={autocomplete as HTMLInputElement['autocomplete']}
|
|
116
127
|
class={cn(
|
|
117
|
-
'flex-1 h-full bg-transparent border-none outline-none placeholder:text-base-content/35',
|
|
128
|
+
'flex-1 min-w-0 h-full bg-transparent border-none outline-none placeholder:text-base-content/35',
|
|
118
129
|
sizeClasses.spacing,
|
|
119
130
|
inputTypeClass,
|
|
120
131
|
)}
|
|
@@ -127,7 +138,7 @@
|
|
|
127
138
|
oninvalid={handleInvalid}
|
|
128
139
|
/>
|
|
129
140
|
</div>
|
|
130
|
-
{#if error}
|
|
131
|
-
<p class='mt-1.5 text-xs text-error
|
|
141
|
+
{#if localError && !field?.error}
|
|
142
|
+
<p id={messageId} class='mt-1.5 text-xs text-error leading-relaxed break-words' aria-live='polite'>{localError}</p>
|
|
132
143
|
{/if}
|
|
133
144
|
</div>
|