@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.
Files changed (79) hide show
  1. package/README.en.md +93 -8
  2. package/README.ja.md +93 -8
  3. package/README.md +94 -8
  4. package/auto-import.d.ts +3 -0
  5. package/auto-import.js +3 -0
  6. package/dist/components/compounds/Combobox.svelte +19 -6
  7. package/dist/components/compounds/Combobox.svelte.d.ts +2 -2
  8. package/dist/components/compounds/DatePicker.svelte +14 -5
  9. package/dist/components/compounds/DatePicker.svelte.d.ts +2 -2
  10. package/dist/components/compounds/Form.svelte +10 -6
  11. package/dist/components/compounds/FormField.svelte +78 -13
  12. package/dist/components/compounds/FormLayout.svelte +59 -0
  13. package/dist/components/compounds/FormLayout.svelte.d.ts +4 -0
  14. package/dist/components/compounds/TagInput.svelte +1 -1
  15. package/dist/components/compounds/index.d.ts +1 -0
  16. package/dist/components/compounds/index.js +1 -0
  17. package/dist/components/primitives/BareInput.svelte +7 -1
  18. package/dist/components/primitives/Checkbox.svelte +8 -2
  19. package/dist/components/primitives/Input.svelte +22 -11
  20. package/dist/components/primitives/Select.svelte +77 -18
  21. package/dist/components/primitives/Textarea.svelte +27 -13
  22. package/dist/components/primitives/Textarea.svelte.d.ts +1 -1
  23. package/dist/components/scenes/app/FeedbackModal.svelte +29 -19
  24. package/dist/components/scenes/app/FeedbackModal.svelte.d.ts +2 -2
  25. package/dist/components/scenes/crud/CrudEditPanel.svelte +148 -118
  26. package/dist/components/scenes/crud/CrudEditPanel.svelte.d.ts +4 -25
  27. package/dist/components/scenes/crud/CrudPage.svelte +5 -41
  28. package/dist/components/scenes/crud/CrudPage.svelte.d.ts +2 -53
  29. package/dist/components/scenes/crud/crud-controller.svelte.d.ts +4 -1
  30. package/dist/components/scenes/crud/crud-form-validation.d.ts +4 -0
  31. package/dist/components/scenes/crud/crud-form-validation.js +39 -0
  32. package/dist/components/scenes/crud/crud-types.d.ts +9 -42
  33. package/dist/components/scenes/guide/GuideHost.svelte +111 -0
  34. package/dist/components/scenes/guide/GuideHost.svelte.d.ts +16 -0
  35. package/dist/components/scenes/guide/HelpCenter.svelte +94 -0
  36. package/dist/components/scenes/guide/HelpCenter.svelte.d.ts +13 -0
  37. package/dist/components/scenes/guide/index.d.ts +2 -0
  38. package/dist/components/scenes/guide/index.js +2 -0
  39. package/dist/components/scenes/iam/AuthPanel.svelte +55 -47
  40. package/dist/components/scenes/iam/ChangePasswordForm.svelte +57 -59
  41. package/dist/components/scenes/iam/ForgotPasswordForm.svelte +89 -84
  42. package/dist/components/scenes/iam/LoginForm.svelte +134 -132
  43. package/dist/components/scenes/iam/PasswordInput.svelte +25 -7
  44. package/dist/components/scenes/iam/PasswordInput.svelte.d.ts +2 -2
  45. package/dist/components/scenes/iam/RegisterForm.svelte +95 -97
  46. package/dist/components/scenes/iam/ResetPasswordForm.svelte +83 -85
  47. package/dist/components/scenes/iam/UserProfile.svelte +9 -5
  48. package/dist/components/scenes/iam/auth-error-message.d.ts +2 -4
  49. package/dist/components/scenes/index.d.ts +1 -0
  50. package/dist/components/scenes/index.js +1 -0
  51. package/dist/components/scenes/storage/FileUpload.svelte +1 -1
  52. package/dist/components/scenes/storage/ImageUpload.svelte +1 -0
  53. package/dist/components/scenes/types.d.ts +9 -9
  54. package/dist/guide-storage.d.ts +6 -0
  55. package/dist/guide-storage.js +67 -0
  56. package/dist/guide-types.d.ts +78 -0
  57. package/dist/guide-types.js +1 -0
  58. package/dist/guide.d.ts +3 -0
  59. package/dist/guide.js +3 -0
  60. package/dist/guide.svelte.d.ts +8 -0
  61. package/dist/guide.svelte.js +450 -0
  62. package/dist/i18n.svelte.d.ts +2 -1
  63. package/dist/i18n.svelte.js +2 -1
  64. package/dist/index.d.ts +2 -0
  65. package/dist/index.js +2 -0
  66. package/dist/internal/form-context.d.ts +12 -0
  67. package/dist/internal/form-context.js +9 -0
  68. package/dist/internal/form-validation.svelte.d.ts +16 -0
  69. package/dist/internal/form-validation.svelte.js +76 -0
  70. package/dist/internal/locale.d.ts +2 -0
  71. package/dist/internal/locale.js +22 -0
  72. package/dist/messages/en-US.json +30 -1
  73. package/dist/messages/zh-CN.json +30 -1
  74. package/dist/messages.d.ts +2 -1
  75. package/dist/messages.js +6 -4
  76. package/dist/styles/guide.css +88 -0
  77. package/dist/types.d.ts +57 -10
  78. package/package.json +13 -2
  79. 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?: string
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
- error
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
- <span class='fieldset-label text-error'>{error}</span>
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?: string;
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(e: Event) {
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
- {#if children}
98
- {@render children()}
99
- {/if}
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
- 'fieldset w-full',
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 label}
35
- <legend class='fieldset-legend'>
36
- {label}
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
- </legend>
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 error || hint}
50
- <div>
51
- {#if error}
52
- <span class='fieldset-label text-error'>{error}</span>
53
- {:else if hint}
54
- <span class='fieldset-label text-base-content/60'>{hint}</span>
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>
@@ -0,0 +1,4 @@
1
+ import type { FormLayoutProps } from '../../types.js';
2
+ declare const FormLayout: import("svelte").Component<FormLayoutProps, {}, "">;
3
+ type FormLayout = ReturnType<typeof FormLayout>;
4
+ export default FormLayout;
@@ -82,7 +82,7 @@
82
82
  }
83
83
  </script>
84
84
 
85
- <div {...dataAttributes} class='fieldset w-full'>
85
+ <div {...dataAttributes} class='fieldset w-full' data-form-wide>
86
86
  <div
87
87
  class={containerClass}
88
88
  onclick={() => inputElement?.focus()}
@@ -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 { cn, generateId, getDataAttributes, getSizeClass } from '../../utils.js'
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 id = generateId('checkbox')
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
- error
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
- if (validationMessage) {
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
- if (validationMessage) {
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/80 leading-tight'>{error}</p>
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>