@h-ai/ui 0.1.0-alpha.63 → 0.1.0-alpha.65

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 CHANGED
@@ -187,6 +187,10 @@ Labels connect to nested UI controls automatically; use `<FormField for="control
187
187
 
188
188
  `Input`, `Textarea`, `Select` and `PasswordInput` provide Chinese/English native validation messages. `validationMessage` overrides failures without keeping valid input invalid. Sync locale with the UI `setGlobalLocale` / `createLocaleStore` to update visible validation errors immediately. `Textarea` supports `minlength` / `maxlength`; `Select` supports `name`, required validation and FormData serialization.
189
189
 
190
+ Client validation appears on the first blur and updates as invalid input is corrected. Untouched fields are validated on submit. CRUD create/edit forms share this behavior, honor custom messages and reset validation when reopened. `Select` validates when focus leaves both its trigger and options. `PasswordInput.minLength` enforces length and informs strength; omitting it leaves login password length unrestricted while strength uses an eight-character baseline.
191
+
192
+ `UserProfile.fieldConstraints` accepts per-field `required`, `minlength`, `maxlength`, `pattern` and `validationMessage` (`pattern` applies to single-line inputs), so application rules such as username formats share blur validation. `validationMessage` also accepts a deferred translation function.
193
+
190
194
  `LoginForm`, `RegisterForm`, `ChangePasswordForm`, `ForgotPasswordForm`, `ResetPasswordForm`, `AuthPanel`, `UserProfile` and `FeedbackModal` accept the same layout options. Pass them through `CrudPage.form` or directly to `CrudEditPanel`. CRUD fields support `colSpan` and deferred `validation.message`. Buttons and Enter both validate before submission; corrections clear errors. Server validation remains required.
191
195
 
192
196
  ```svelte
package/README.ja.md CHANGED
@@ -187,6 +187,10 @@ components/
187
187
 
188
188
  `Input`、`Textarea`、`Select`、`PasswordInput` のネイティブ検証は中国語・英語に対応します。`validationMessage` は失敗時だけ適用され、有効な入力を無効にし続けません。UI の `setGlobalLocale` / `createLocaleStore` で言語を同期すると表示中のエラーも更新されます。`Textarea` は `minlength` / `maxlength`、`Select` は `name`・必須検証・FormData に対応します。
189
189
 
190
+ 初回のフォーカス離脱時にエラーを表示し、修正中も更新します。未操作の項目は送信時に検証します。CRUD の新規作成・編集も同じ動作とカスタムメッセージに対応し、再表示時に検証状態をリセットします。`Select` はトリガーと選択肢の両方からフォーカスが離れた時に検証します。`PasswordInput.minLength` は長さ制約と強度計算に使い、省略時はログイン用の長さ制限を設けず、強度のみ8文字を基準にします。
191
+
192
+ `UserProfile.fieldConstraints` は項目別に `required`、`minlength`、`maxlength`、`pattern`、`validationMessage` を受け取り、ユーザー名などの業務ルールをフォーカス離脱時に検証します。`pattern` は単一行の入力に適用し、`validationMessage` は翻訳関数にも対応します。
193
+
190
194
  `LoginForm`、`RegisterForm`、`ChangePasswordForm`、`ForgotPasswordForm`、`ResetPasswordForm`、`AuthPanel`、`UserProfile`、`FeedbackModal` は同じレイアウト設定に対応します。`CrudPage.form` または `CrudEditPanel` に渡します。CRUD 項目は `colSpan` と翻訳関数の `validation.message` に対応し、ボタンと Enter のどちらも送信前に検証します。入力修正でエラーが消えます。サーバー側の検証も必要です。
191
195
 
192
196
  ```svelte
package/README.md CHANGED
@@ -187,6 +187,10 @@ components/
187
187
 
188
188
  `Input`、`Textarea`、`Select` 和 `PasswordInput` 的原生校验使用内置中英文提示;`validationMessage` 只覆盖失败提示,不让有效输入一直无效。应用通过 UI 的 `setGlobalLocale` / `createLocaleStore` 同步语言,已显示的校验错误即时更新。`Textarea` 支持 `minlength` / `maxlength`,`Select` 支持 `name`、必填校验和 FormData 序列化。
189
189
 
190
+ 字段首次失焦后显示前端校验错误,修正输入时即时更新;未访问的字段在提交时统一校验。CRUD 新增/编辑复用该行为,支持自定义校验文案,关闭重开清空校验状态。`Select` 在焦点离开触发器和选项后校验。`PasswordInput.minLength` 同时约束长度并用于强度计算;省略时不限制登录密码长度,强度计算仍以 8 位为基准。
191
+
192
+ `UserProfile.fieldConstraints` 按字段接收 `required`、`minlength`、`maxlength`、`pattern` 和 `validationMessage`(`pattern` 用于单行输入),让应用的用户名等业务规则复用失焦校验;`validationMessage` 支持延迟翻译函数。
193
+
190
194
  `LoginForm`、`RegisterForm`、`ChangePasswordForm`、`ForgotPasswordForm`、`ResetPasswordForm`、`AuthPanel`、`UserProfile` 和 `FeedbackModal` 接受同一组布局参数。`CrudPage` 通过 `form` 传入;`CrudEditPanel` 直接接收。CRUD 字段支持 `colSpan`,`validation.message` 支持翻译函数;按钮与回车均先校验,修正后错误消失,客户端校验不能替代服务端校验。
191
195
 
192
196
  ```svelte
@@ -10,8 +10,9 @@
10
10
  -->
11
11
  <script lang='ts'>
12
12
  import type { DataAttributes, DrawerProps } from '../../types.js'
13
- import { onDestroy, onMount } from 'svelte'
13
+ import { onDestroy, onMount, setContext } from 'svelte'
14
14
  import { readStoredValue, writeStoredValue } from '../../internal/browser-safety.js'
15
+ import { FORM_VISIBILITY_CONTEXT } from '../../internal/form-context.js'
15
16
  import { uiM } from '../../messages.js'
16
17
  import { cn, generateId, getDataAttributes } from '../../utils.js'
17
18
  import IconButton from '../primitives/IconButton.svelte'
@@ -34,6 +35,9 @@
34
35
  }: DrawerProps & DataAttributes = $props()
35
36
 
36
37
  const dataAttributes = $derived(getDataAttributes(restProps))
38
+ setContext(FORM_VISIBILITY_CONTEXT, {
39
+ get open() { return open },
40
+ })
37
41
  const id = generateId('drawer')
38
42
  /** 用户拖动后的像素宽度;优先于 width 与 size。 */
39
43
  let resizedWidth = $state<number | null>(null)
@@ -4,6 +4,8 @@
4
4
  -->
5
5
  <script lang='ts'>
6
6
  import type { DataAttributes, ModalProps } from '../../types.js'
7
+ import { setContext } from 'svelte'
8
+ import { FORM_VISIBILITY_CONTEXT } from '../../internal/form-context.js'
7
9
  import { uiM } from '../../messages.js'
8
10
  import { cn, getDataAttributes } from '../../utils.js'
9
11
 
@@ -28,6 +30,9 @@
28
30
  }: ModalProps & DataAttributes = $props()
29
31
 
30
32
  const dataAttributes = $derived(getDataAttributes(restProps))
33
+ setContext(FORM_VISIBILITY_CONTEXT, {
34
+ get open() { return open },
35
+ })
31
36
  const modalId = $props.id()
32
37
  const titleId = `${modalId}-title`
33
38
  let panel = $state<HTMLDivElement>()
@@ -89,6 +89,7 @@
89
89
  }
90
90
 
91
91
  function handleBlur(e: FocusEvent & { currentTarget: HTMLInputElement }) {
92
+ validation.validate(e.currentTarget)
92
93
  onblur?.(e)
93
94
  }
94
95
 
@@ -97,7 +98,7 @@
97
98
  }
98
99
 
99
100
  function handleInvalid(e: Event & { currentTarget: HTMLInputElement }) {
100
- validation.invalid(e.currentTarget)
101
+ validation.validate(e.currentTarget)
101
102
  oninvalid?.(e)
102
103
  }
103
104
  </script>
@@ -258,6 +258,17 @@
258
258
  onchange?.(undefined as T)
259
259
  }
260
260
 
261
+ function handleFocusOut(event: FocusEvent) {
262
+ const next = event.relatedTarget
263
+ // 选项通过 portal 渲染,触发器与选项之间的焦点移动仍属于同一字段。
264
+ if (next instanceof Node && (dropdownRef?.contains(next) || listboxRef?.contains(next)))
265
+ return
266
+ if (nativeSelect)
267
+ validation.validate(nativeSelect)
268
+ isDropdownOpen = false
269
+ filterText = ''
270
+ }
271
+
261
272
  // 判断是否显示清除按钮
262
273
  const showClear = $derived(clearable && value !== undefined && value !== null && value !== '' && isHovered)
263
274
  </script>
@@ -268,6 +279,7 @@
268
279
  bind:this={dropdownRef}
269
280
  onmouseenter={() => isHovered = true}
270
281
  onmouseleave={() => isHovered = false}
282
+ onfocusout={handleFocusOut}
271
283
  >
272
284
  <select
273
285
  bind:this={nativeSelect}
@@ -280,7 +292,7 @@
280
292
  value={nativeValue}
281
293
  oninvalid={(e) => {
282
294
  e.preventDefault()
283
- validation.invalid(e.currentTarget)
295
+ validation.validate(e.currentTarget)
284
296
  inputRef?.focus()
285
297
  }}
286
298
  >
@@ -387,6 +399,7 @@
387
399
  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'
388
400
  style={dropdownStyle}
389
401
  role='listbox'
402
+ onfocusout={handleFocusOut}
390
403
  >
391
404
  {#each filteredOptions() as option (String(option.value))}
392
405
  <li
@@ -88,7 +88,7 @@
88
88
  }
89
89
 
90
90
  function handleInvalid(e: Event & { currentTarget: HTMLTextAreaElement }) {
91
- validation.invalid(e.currentTarget)
91
+ validation.validate(e.currentTarget)
92
92
  oninvalid?.(e)
93
93
  }
94
94
  </script>
@@ -112,6 +112,7 @@
112
112
  class={textareaClass}
113
113
  bind:value
114
114
  oninput={handleInput}
115
+ onblur={e => validation.validate(e.currentTarget)}
115
116
  oninvalid={handleInvalid}
116
117
  ></textarea>
117
118
  </div>
@@ -79,9 +79,12 @@
79
79
  const panelId = $props.id()
80
80
  let formElement: HTMLFormElement | undefined = $state()
81
81
  let attempted = $state(false)
82
+ let touched = $state<Record<string, boolean>>({})
82
83
  $effect(() => {
83
- if (!open)
84
+ if (!open) {
84
85
  attempted = false
86
+ touched = {}
87
+ }
85
88
  })
86
89
  function resolveOptions(options?: CrudFieldDef['options']): Array<{ label: string, value: string | number | boolean }> {
87
90
  if (!options)
@@ -94,10 +97,8 @@
94
97
  )
95
98
  const fieldErrors = $derived.by(() => {
96
99
  const errors: Record<string, string> = {}
97
- if (!attempted)
98
- return errors
99
100
  for (const field of sortedFields) {
100
- if (mode === 'edit' && field.readonly)
101
+ if ((!attempted && !touched[field.id]) || (mode === 'edit' && field.readonly))
101
102
  continue
102
103
  const issue = getCrudValidationIssue(field, formData[field.id])
103
104
  if (issue)
@@ -148,6 +149,15 @@
148
149
  formData = { ...formData, [fieldId]: value }
149
150
  }
150
151
 
152
+ function handleFieldBlur(event: FocusEvent) {
153
+ if (!(event.target instanceof HTMLElement))
154
+ return
155
+ const field = event.target.closest<HTMLElement>('[data-field]')
156
+ if (!field?.dataset.field || (event.relatedTarget instanceof Node && field.contains(event.relatedTarget)))
157
+ return
158
+ touched = { ...touched, [field.dataset.field]: true }
159
+ }
160
+
151
161
  function toggleMultiSelect(fieldId: string, optValue: string | number | boolean) {
152
162
  const current = (formData[fieldId] as Array<string | number | boolean>) ?? []
153
163
  const strVal = String(optValue)
@@ -295,7 +305,7 @@
295
305
  closeOnBackdrop={false}
296
306
  onclose={handleClose}
297
307
  >
298
- <form bind:this={formElement} novalidate onsubmit={handleSubmit}>
308
+ <form bind:this={formElement} novalidate onsubmit={handleSubmit} onfocusout={handleFieldBlur}>
299
309
  <!-- 底部按钮在表单外;保留原生提交控件,使多字段表单支持回车提交。 -->
300
310
  <input type='submit' class='hidden' tabindex='-1' aria-hidden='true' value={uiM('crud_save')} />
301
311
  {@render formFields()}
@@ -318,7 +328,7 @@
318
328
  onclose={handleClose}
319
329
  closeOnBackdrop={false}
320
330
  >
321
- <form bind:this={formElement} novalidate onsubmit={handleSubmit} class={drawerFormClass}>
331
+ <form bind:this={formElement} novalidate onsubmit={handleSubmit} onfocusout={handleFieldBlur} class={drawerFormClass}>
322
332
  <input type='submit' class='hidden' tabindex='-1' aria-hidden='true' value={uiM('crud_save')} />
323
333
  {@render formFields()}
324
334
  </form>
@@ -83,6 +83,9 @@
83
83
  <FormField for='old-password' label={uiM('change_password_old')}>
84
84
  <PasswordInput
85
85
  value={oldPassword}
86
+ id='old-password'
87
+ autocomplete='current-password'
88
+ required
86
89
  oninput={(e) => { oldPassword = e.currentTarget.value }}
87
90
  placeholder={uiM('change_password_old_placeholder')}
88
91
  {disabled}
@@ -95,6 +98,9 @@
95
98
  <FormField for='new-password' label={uiM('change_password_new')}>
96
99
  <PasswordInput
97
100
  value={newPassword}
101
+ id='new-password'
102
+ autocomplete='new-password'
103
+ required
98
104
  oninput={(e) => { newPassword = e.currentTarget.value }}
99
105
  placeholder={uiM('change_password_new_placeholder')}
100
106
  {disabled}
@@ -107,6 +113,9 @@
107
113
  <FormField for='confirm-password' label={uiM('change_password_confirm')}>
108
114
  <PasswordInput
109
115
  value={confirmPassword}
116
+ id='confirm-password'
117
+ autocomplete='new-password'
118
+ required
110
119
  oninput={(e) => { confirmPassword = e.currentTarget.value }}
111
120
  placeholder={uiM('change_password_confirm_placeholder')}
112
121
  {disabled}
@@ -42,7 +42,7 @@
42
42
  showToggle?: boolean
43
43
  /** 是否显示密码强度 */
44
44
  showStrength?: boolean
45
- /** 最小长度 */
45
+ /** 最小长度;提供时校验,省略时仅以 8 位为基准计算强度。 */
46
46
  minLength?: number
47
47
  /** 自定义类名 */
48
48
  class?: string
@@ -68,7 +68,7 @@
68
68
  error = '',
69
69
  showToggle = true,
70
70
  showStrength = false,
71
- minLength = 8,
71
+ minLength,
72
72
  class: className = '',
73
73
  inputRef = $bindable<HTMLInputElement | undefined>(),
74
74
  oninput,
@@ -79,7 +79,7 @@
79
79
 
80
80
  const dataAttributes = $derived(getDataAttributes(restProps))
81
81
  const field = getFormFieldContext()
82
- const validation = createControlValidation(undefined, () => [value, required, readonly, disabled])
82
+ const validation = createControlValidation(undefined, () => [value, required, readonly, disabled, minLength])
83
83
  const uniqueId = $props.id()
84
84
  const messageId = `${uniqueId}-error`
85
85
  const localError = $derived(resolveFormText(error) || validation.message)
@@ -110,7 +110,7 @@
110
110
  let score = 0
111
111
 
112
112
  // 长度检查
113
- if (value.length >= minLength)
113
+ if (value.length >= (minLength ?? 8))
114
114
  score += 1
115
115
  if (value.length >= 12)
116
116
  score += 1
@@ -173,12 +173,14 @@
173
173
  {disabled}
174
174
  {readonly}
175
175
  {required}
176
+ minlength={minLength}
176
177
  ariaInvalid={effectiveError ? true : undefined}
177
178
  ariaDescribedby={[field?.messageId, localError && !field?.error ? messageId : ''].filter(Boolean).join(' ') || undefined}
178
179
  oninput={handleInput}
179
180
  onchange={handleChange}
181
+ onblur={e => validation.validate(e.currentTarget)}
180
182
  oninvalid={(e) => {
181
- validation.invalid(e.currentTarget)
183
+ validation.validate(e.currentTarget)
182
184
  oninvalid?.(e)
183
185
  }}
184
186
  />
@@ -22,7 +22,7 @@ interface Props {
22
22
  showToggle?: boolean;
23
23
  /** 是否显示密码强度 */
24
24
  showStrength?: boolean;
25
- /** 最小长度 */
25
+ /** 最小长度;提供时校验,省略时仅以 8 位为基准计算强度。 */
26
26
  minLength?: number;
27
27
  /** 自定义类名 */
28
28
  class?: string;
@@ -31,6 +31,7 @@
31
31
  labelPosition = 'top',
32
32
  labelWidth = '8rem',
33
33
  fields = ['avatar', 'username', 'email', 'displayName', 'phone'],
34
+ fieldConstraints = {},
34
35
  avatarAccept = 'image/*',
35
36
  avatarHint,
36
37
  class: className = '',
@@ -167,7 +168,7 @@
167
168
  {@const modelKey = getModelKey(field)}
168
169
  {#if field === 'avatar'}
169
170
  <!-- 头像 -->
170
- <div class='flex items-center gap-4'>
171
+ <div class='flex flex-wrap items-center gap-4 border-b border-base-content/8 pb-5'>
171
172
  <Avatar
172
173
  src={user?.avatarUrl ?? user?.avatar}
173
174
  alt={uiM('user_profile_avatar')}
@@ -176,7 +177,7 @@
176
177
  class='shadow-sm'
177
178
  />
178
179
  {#if editable && (editMode || alwaysEditable)}
179
- <div>
180
+ <div class='min-w-0 flex-1 basis-48'>
180
181
  <BareInput
181
182
  id={avatarInputId}
182
183
  type='file'
@@ -196,10 +197,11 @@
196
197
  </div>
197
198
  {:else}
198
199
  <!-- 其他字段 -->
199
- <FormField label={getFieldLabel(field)} colSpan={field === 'bio' ? 2 : 'auto'}>
200
+ <FormField label={getFieldLabel(field)} required={fieldConstraints[field]?.required} colSpan={field === 'bio' ? 2 : 'auto'}>
200
201
  {#if editable && (editMode || alwaysEditable)}
201
202
  {#if field === 'bio'}
202
203
  <Textarea
204
+ {...fieldConstraints[field]}
203
205
  name={modelKey}
204
206
  rows={3}
205
207
  bind:value={formData[modelKey]}
@@ -208,6 +210,7 @@
208
210
  />
209
211
  {:else}
210
212
  <Input
213
+ {...fieldConstraints[field]}
211
214
  type={field === 'email' ? 'email' : field === 'phone' ? 'tel' : 'text'}
212
215
  name={modelKey}
213
216
  bind:value={formData[modelKey]}
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import type { HaiResult } from '@h-ai/core';
8
8
  import type { Snippet } from 'svelte';
9
- import type { DataAttributes, FormLayoutOptions, FormText, Size } from '../../types.js';
9
+ import type { DataAttributes, FormLayoutOptions, FormText, InputProps, Size } from '../../types.js';
10
10
  /**
11
11
  * 密码输入框属性
12
12
  */
@@ -33,7 +33,7 @@ export interface PasswordInputProps extends DataAttributes {
33
33
  showToggle?: boolean;
34
34
  /** 是否显示密码强度 */
35
35
  showStrength?: boolean;
36
- /** 最小长度 */
36
+ /** 最小长度;提供时校验,省略时仅以 8 位为基准计算强度。 */
37
37
  minLength?: number;
38
38
  /** 自定义类名 */
39
39
  class?: string;
@@ -418,6 +418,8 @@ export interface UserProfileProps extends FormLayoutOptions, DataAttributes {
418
418
  loading?: boolean;
419
419
  /** 显示的字段 */
420
420
  fields?: UserProfileField[];
421
+ /** 按业务规则声明字段的原生约束,复用控件失焦校验。 */
422
+ fieldConstraints?: Partial<Record<Exclude<UserProfileField, 'avatar'>, Pick<InputProps, 'required' | 'minlength' | 'maxlength' | 'pattern' | 'validationMessage'>>>;
421
423
  /** 头像上传地址 */
422
424
  avatarUploadUrl?: string;
423
425
  /** 文件选择器允许的头像类型;最终校验由业务上传服务负责。 */
@@ -2,6 +2,11 @@
2
2
  import type { FormLayoutOptions } from '../types.js';
3
3
  export declare const FORM_LAYOUT_CONTEXT: unique symbol;
4
4
  export declare const FORM_FIELD_CONTEXT: unique symbol;
5
+ export declare const FORM_VISIBILITY_CONTEXT: unique symbol;
6
+ interface FormVisibilityContext {
7
+ readonly open: boolean;
8
+ }
9
+ export declare function getFormVisibilityContext(): FormVisibilityContext | undefined;
5
10
  export interface FormFieldContext {
6
11
  readonly controlId: string;
7
12
  readonly labelId: string;
@@ -10,3 +15,4 @@ export interface FormFieldContext {
10
15
  }
11
16
  export declare function getFormLayoutContext(): FormLayoutOptions | undefined;
12
17
  export declare function getFormFieldContext(): FormFieldContext | undefined;
18
+ export {};
@@ -1,6 +1,10 @@
1
1
  import { getContext } from 'svelte';
2
2
  export const FORM_LAYOUT_CONTEXT = Symbol('hai-form-layout');
3
3
  export const FORM_FIELD_CONTEXT = Symbol('hai-form-field');
4
+ export const FORM_VISIBILITY_CONTEXT = Symbol('hai-form-visibility');
5
+ export function getFormVisibilityContext() {
6
+ return getContext(FORM_VISIBILITY_CONTEXT);
7
+ }
4
8
  export function getFormLayoutContext() {
5
9
  return getContext(FORM_LAYOUT_CONTEXT);
6
10
  }
@@ -9,7 +9,7 @@ type FormControl = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
9
9
  export declare function resolveFormText(text?: FormText): string;
10
10
  export declare function createControlValidation(getOverride?: () => FormText | undefined, getValue?: () => unknown): {
11
11
  readonly message: string;
12
- invalid(control: FormControl): void;
12
+ validate(control: FormControl): void;
13
13
  input(control: FormControl): void;
14
14
  reset: () => void;
15
15
  };
@@ -1,5 +1,6 @@
1
1
  import { untrack } from 'svelte';
2
2
  import { getUILocale, uiM } from '../messages.js';
3
+ import { getFormVisibilityContext } from './form-context.js';
3
4
  export function resolveFormText(text) {
4
5
  if (typeof text !== 'function')
5
6
  return text ?? '';
@@ -15,9 +16,12 @@ function readIssue(control) {
15
16
  return { key: 'form_required' };
16
17
  if (validity.typeMismatch)
17
18
  return { key: control.getAttribute('type') === 'email' ? 'form_email' : 'form_url' };
18
- if (validity.tooShort)
19
+ // 程序回填的值也需校验长度,原生 tooShort/tooLong 只覆盖用户输入。
20
+ const minLength = control.getAttribute('minlength');
21
+ const maxLength = control.getAttribute('maxlength');
22
+ if (validity.tooShort || (control.value && minLength !== null && control.value.length < Number(minLength)))
19
23
  return { key: 'form_min_length', params: { min: control.getAttribute('minlength') ?? '' } };
20
- if (validity.tooLong)
24
+ if (validity.tooLong || (control.value && maxLength !== null && control.value.length > Number(maxLength)))
21
25
  return { key: 'form_max_length', params: { max: control.getAttribute('maxlength') ?? '' } };
22
26
  if (validity.rangeUnderflow)
23
27
  return { key: 'form_min', params: { min: control.getAttribute('min') ?? '' } };
@@ -32,12 +36,18 @@ function readIssue(control) {
32
36
  return undefined;
33
37
  }
34
38
  export function createControlValidation(getOverride = () => undefined, getValue) {
39
+ const visibility = getFormVisibilityContext();
35
40
  let issue = $state();
36
41
  let currentControl = $state();
37
42
  function reset() {
38
43
  issue = undefined;
39
44
  currentControl?.setCustomValidity('');
40
45
  }
46
+ $effect(() => {
47
+ // 弹窗与抽屉保留子树;关闭时清理原生错误,重开不能沿用上次的触碰状态。
48
+ if (visibility && !visibility.open)
49
+ reset();
50
+ });
41
51
  $effect(() => {
42
52
  const form = currentControl?.form;
43
53
  form?.addEventListener('reset', reset);
@@ -59,7 +69,7 @@ export function createControlValidation(getOverride = () => undefined, getValue)
59
69
  get message() {
60
70
  return issue ? resolveFormText(getOverride()) || uiM(issue.key, issue.params) : '';
61
71
  },
62
- invalid(control) {
72
+ validate(control) {
63
73
  currentControl = control;
64
74
  control.setCustomValidity('');
65
75
  issue = readIssue(control);
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.63",
4
+ "version": "0.1.0-alpha.65",
5
5
  "description": "Hai Framework UI component library for Svelte 5 Runes.",
6
6
  "license": "Apache-2.0",
7
7
  "svelte": "./dist/index.js",
@@ -59,7 +59,7 @@
59
59
  "svelte": "^5.57.2"
60
60
  },
61
61
  "dependencies": {
62
- "@h-ai/core": "0.1.0-alpha.63",
62
+ "@h-ai/core": "0.1.0-alpha.65",
63
63
  "@internationalized/date": "^3.12.4",
64
64
  "@shikijs/langs": "^4.5.0",
65
65
  "bits-ui": "^2.19.5",