@h-ai/ui 0.1.0-alpha.62 → 0.1.0-alpha.64

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 (59) hide show
  1. package/README.en.md +37 -7
  2. package/README.ja.md +37 -7
  3. package/README.md +37 -7
  4. package/auto-import.d.ts +1 -0
  5. package/auto-import.js +1 -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/Drawer.svelte +5 -1
  11. package/dist/components/compounds/Form.svelte +10 -6
  12. package/dist/components/compounds/FormField.svelte +78 -13
  13. package/dist/components/compounds/FormLayout.svelte +59 -0
  14. package/dist/components/compounds/FormLayout.svelte.d.ts +4 -0
  15. package/dist/components/compounds/Modal.svelte +5 -0
  16. package/dist/components/compounds/TagInput.svelte +1 -1
  17. package/dist/components/compounds/index.d.ts +1 -0
  18. package/dist/components/compounds/index.js +1 -0
  19. package/dist/components/primitives/BareInput.svelte +7 -1
  20. package/dist/components/primitives/Checkbox.svelte +8 -2
  21. package/dist/components/primitives/Input.svelte +23 -11
  22. package/dist/components/primitives/Select.svelte +90 -18
  23. package/dist/components/primitives/Textarea.svelte +28 -13
  24. package/dist/components/primitives/Textarea.svelte.d.ts +1 -1
  25. package/dist/components/scenes/app/FeedbackModal.svelte +29 -19
  26. package/dist/components/scenes/app/FeedbackModal.svelte.d.ts +2 -2
  27. package/dist/components/scenes/crud/CrudEditPanel.svelte +158 -118
  28. package/dist/components/scenes/crud/CrudEditPanel.svelte.d.ts +4 -25
  29. package/dist/components/scenes/crud/CrudPage.svelte +5 -41
  30. package/dist/components/scenes/crud/CrudPage.svelte.d.ts +2 -53
  31. package/dist/components/scenes/crud/crud-controller.svelte.d.ts +4 -1
  32. package/dist/components/scenes/crud/crud-form-validation.d.ts +4 -0
  33. package/dist/components/scenes/crud/crud-form-validation.js +39 -0
  34. package/dist/components/scenes/crud/crud-types.d.ts +9 -42
  35. package/dist/components/scenes/iam/AuthPanel.svelte +55 -47
  36. package/dist/components/scenes/iam/ChangePasswordForm.svelte +66 -59
  37. package/dist/components/scenes/iam/ForgotPasswordForm.svelte +89 -84
  38. package/dist/components/scenes/iam/LoginForm.svelte +134 -132
  39. package/dist/components/scenes/iam/PasswordInput.svelte +30 -10
  40. package/dist/components/scenes/iam/PasswordInput.svelte.d.ts +3 -3
  41. package/dist/components/scenes/iam/RegisterForm.svelte +95 -97
  42. package/dist/components/scenes/iam/ResetPasswordForm.svelte +83 -85
  43. package/dist/components/scenes/iam/UserProfile.svelte +14 -7
  44. package/dist/components/scenes/storage/FileUpload.svelte +1 -1
  45. package/dist/components/scenes/storage/ImageUpload.svelte +1 -0
  46. package/dist/components/scenes/types.d.ts +12 -10
  47. package/dist/i18n.svelte.d.ts +2 -1
  48. package/dist/i18n.svelte.js +2 -1
  49. package/dist/internal/form-context.d.ts +18 -0
  50. package/dist/internal/form-context.js +13 -0
  51. package/dist/internal/form-validation.svelte.d.ts +16 -0
  52. package/dist/internal/form-validation.svelte.js +86 -0
  53. package/dist/internal/locale.d.ts +2 -0
  54. package/dist/internal/locale.js +22 -0
  55. package/dist/messages/en-US.json +13 -1
  56. package/dist/messages/zh-CN.json +13 -1
  57. package/dist/messages.js +4 -3
  58. package/dist/types.d.ts +57 -10
  59. package/package.json +7 -2
package/README.en.md CHANGED
@@ -98,7 +98,7 @@ With auto-import enabled, use components directly in templates without individua
98
98
 
99
99
  > `@h-ai/ui/auto-import` injects all public Svelte components except `toast`, type imports, and `Range` (which conflicts with the DOM `Range` constructor).
100
100
 
101
- Pages using a subset of components, such as administration screens, can import public component-directory entry points directly. Import theme utilities from the lightweight `@h-ai/ui/theme` entry so theme consumers do not load the root entry's AI and chart components. Check the installed package's exports; do not import its internal `src/` or `dist/` paths.
101
+ Pages using a subset of components, such as administration screens, can import public component-directory entry points directly. Import theme utilities from the lightweight `@h-ai/ui/theme` entry so theme consumers do not load the root entry's AI and chart components. Use `@h-ai/ui/i18n` for locale utilities in stores without loading components or tour CSS. Check the installed package's exports; do not import its internal `src/` or `dist/` paths.
102
102
 
103
103
  The preprocessor recognizes semicolon-free, multiline and aliased imports without injecting components already bound locally. Type-only imports do not replace runtime components.
104
104
 
@@ -166,16 +166,46 @@ components/
166
166
 
167
167
  For `BareInput` with `type="file"`, read `event.currentTarget.files` in `onchange` and leave the file path `value` unbound. Other input types support `bind:value`.
168
168
 
169
- ### Compounds (36)
169
+ ### Compounds (37)
170
170
 
171
171
  #### Forms
172
172
 
173
173
  | Component | Description | Key props |
174
174
  | ----------- | -------------- | -------------------------------------------- |
175
- | `Form` | Form container | `loading`, `disabled`, `onsubmit` |
176
- | `FormField` | Form field | `label`, `error`, `hint`, `required` |
175
+ | `Form` | Form container | `columns`, `labelPosition`, `labelWidth`, `loading`, `disabled`, `onsubmit` |
176
+ | `FormField` | Form field | `label`, `for`, `colSpan`, `labelPosition`, `error`, `hint`, `required` |
177
+ | `FormLayout` | Form layout | `columns`, `labelPosition`, `labelWidth` |
177
178
  | `TagInput` | Tag input | `tags`, `maxTags`, `allowDuplicates`, `size` |
178
179
 
180
+ #### Form layouts and validation
181
+
182
+ `Form` and `FormLayout` (for native `<form>` elements) share `columns={1 | 2}`, `labelPosition="top" | "left"` and `labelWidth` (default `8rem`). Defaults are one column and stacked labels. Two columns collapse below a container width of `36rem`; inline labels stack below `28rem`.
183
+
184
+ `FormField.colSpan` defaults to `'auto'`: textareas, file/image uploads, TagInput and multiple Combobox controls span both columns. Use `1` or `2` to override, or mark a custom wide control with `data-form-wide`. Non-field content, including headings, error summaries and actions, spans the full row. Fields may override `labelPosition` / `labelWidth`.
185
+
186
+ Labels connect to nested UI controls automatically; use `<FormField for="control-id">` for explicit IDs or native controls. Errors and hints use `aria-describedby`, invalid controls use `aria-invalid`, and field errors appear once. `label` / `error` / `hint` accept strings or deferred translation functions.
187
+
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
+
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
+
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.
195
+
196
+ ```svelte
197
+ <!-- m、save、name、notes 为应用自己的翻译、提交回调和状态 -->
198
+ <Form columns={2} labelPosition="left" labelWidth="6rem" onsubmit={save}>
199
+ <FormField label={() => m.name()} required>
200
+ <Input name="name" bind:value={name} required />
201
+ </FormField>
202
+ <FormField label={() => m.notes()}>
203
+ <Textarea name="notes" bind:value={notes} />
204
+ </FormField>
205
+ <Button type="submit">{m.save()}</Button>
206
+ </Form>
207
+ ```
208
+
179
209
  #### Feedback
180
210
 
181
211
  | Component | Description | Key props |
@@ -531,7 +561,7 @@ Defaults match `@h-ai/crypto`: `SM4` for encrypted inputs, `SM3` for hashes, and
531
561
  crud={roleCrud}
532
562
  {data}
533
563
  permissions={{ create: true, update: true, delete: true }}
534
- form={{ variant: 'modal', modalSize: 'lg' }}
564
+ form={{ variant: 'modal', modalSize: '3xl', columns: 2, labelPosition: 'top' }}
535
565
  pagination={{ showSizeChanger: true, showJumper: true, pageSizeOptions: [10, 20, 50] }}
536
566
  density='compact'
537
567
  detailColumns={['name']}
@@ -698,7 +728,7 @@ Client locale state management, automatically synchronized with the `@h-ai/core`
698
728
 
699
729
  ```svelte
700
730
  <script>
701
- import { createLocaleStore, setGlobalLocale } from '@h-ai/ui'
731
+ import { createLocaleStore, setGlobalLocale } from '@h-ai/ui/i18n'
702
732
  import { setLocale } from '$lib/paraglide/runtime'
703
733
 
704
734
  const localeStore = createLocaleStore()
@@ -729,7 +759,7 @@ import {
729
759
  isLocaleSupported, // 检查 locale 是否支持
730
760
  resolveLocale, // 解析 locale(支持回退)
731
761
  setGlobalLocale, // 设置全局 locale(同步 @h-ai/core)
732
- } from '@h-ai/ui'
762
+ } from '@h-ai/ui/i18n'
733
763
  ```
734
764
 
735
765
  ## Configuration driven authentication
package/README.ja.md CHANGED
@@ -98,7 +98,7 @@ export default defineConfig({
98
98
 
99
99
  > `toast`・型 import・DOM の `Range` と衝突する `Range` を除き、公開 Svelte コンポーネントは `@h-ai/ui/auto-import` で自動注入できます。
100
100
 
101
- 管理画面など一部のコンポーネントを使うページは、公開コンポーネントディレクトリーの入口から直接 import できます。テーマツールは軽量な `@h-ai/ui/theme` から読み込み、テーマの利用だけでルート入口の AI・グラフコンポーネントを読み込まないようにします。インストール済みパッケージの exports を確認し、内部 `src/`・`dist/` は import しないでください。
101
+ 管理画面など一部のコンポーネントを使うページは、公開コンポーネントディレクトリーの入口から直接 import できます。テーマツールは軽量な `@h-ai/ui/theme` から読み込み、テーマの利用だけでルート入口の AI・グラフコンポーネントを読み込まないようにします。ストアの言語ツールは `@h-ai/ui/i18n` から読み込み、コンポーネントやガイド CSS を読み込みません。インストール済みパッケージの exports を確認し、内部 `src/`・`dist/` は import しないでください。
102
102
 
103
103
  自動 import はセミコロンなし、複数行、別名の import を認識し、既存のローカル名を重複注入しません。型のみの import は実行時コンポーネントの代わりにはなりません。
104
104
 
@@ -166,16 +166,46 @@ components/
166
166
 
167
167
  `BareInput` の `type="file"` では `onchange` の `event.currentTarget.files` を読み取り、ファイルパスの `value` はバインドしません。他の入力型では `bind:value` を使用できます。
168
168
 
169
- ### Compounds:複合コンポーネント(36)
169
+ ### Compounds:複合コンポーネント(37)
170
170
 
171
171
  #### フォーム
172
172
 
173
173
  | コンポーネント | 説明 | 主なプロパティ |
174
174
  | -------------- | ------------------ | -------------------------------------------- |
175
- | `Form` | フォームコンテナー | `loading`, `disabled`, `onsubmit` |
176
- | `FormField` | フォーム項目 | `label`, `error`, `hint`, `required` |
175
+ | `Form` | フォームコンテナー | `columns`, `labelPosition`, `labelWidth`, `loading`, `disabled`, `onsubmit` |
176
+ | `FormField` | フォーム項目 | `label`, `for`, `colSpan`, `labelPosition`, `error`, `hint`, `required` |
177
+ | `FormLayout` | フォーム配置 | `columns`, `labelPosition`, `labelWidth` |
177
178
  | `TagInput` | タグ入力 | `tags`, `maxTags`, `allowDuplicates`, `size` |
178
179
 
180
+ #### フォームのレイアウトと検証
181
+
182
+ `Form` とネイティブ `<form>` 内で使う `FormLayout` は、`columns={1 | 2}`、`labelPosition="top" | "left"`、`labelWidth`(既定 `8rem`)を共有します。既定は1列・ラベルを上に配置します。コンテナー幅が `36rem` 未満では1列、`28rem` 未満では横並びラベルも縦並びになります。
183
+
184
+ `FormField.colSpan` の既定は `'auto'`。Textarea、ファイル・画像アップロード、TagInput、複数選択 Combobox は2列を占めます。`1` / `2` で指定でき、独自の幅広コントロールには `data-form-wide` を付けます。見出し、エラー概要、操作ボタンなどの非 FormField 要素は行全体を使用します。項目ごとに `labelPosition` / `labelWidth` を上書きできます。
185
+
186
+ ラベルは内部 UI コントロールに自動で関連付けられます。明示 ID やネイティブ要素は `<FormField for="control-id">` を使います。エラーとヒントは `aria-describedby`、無効な要素は `aria-invalid` で通知し、エラーは重複表示しません。`label` / `error` / `hint` は文字列または翻訳関数を受け取ります。
187
+
188
+ `Input`、`Textarea`、`Select`、`PasswordInput` のネイティブ検証は中国語・英語に対応します。`validationMessage` は失敗時だけ適用され、有効な入力を無効にし続けません。UI の `setGlobalLocale` / `createLocaleStore` で言語を同期すると表示中のエラーも更新されます。`Textarea` は `minlength` / `maxlength`、`Select` は `name`・必須検証・FormData に対応します。
189
+
190
+ 初回のフォーカス離脱時にエラーを表示し、修正中も更新します。未操作の項目は送信時に検証します。CRUD の新規作成・編集も同じ動作とカスタムメッセージに対応し、再表示時に検証状態をリセットします。`Select` はトリガーと選択肢の両方からフォーカスが離れた時に検証します。`PasswordInput.minLength` は長さ制約と強度計算に使い、省略時はログイン用の長さ制限を設けず、強度のみ8文字を基準にします。
191
+
192
+ `UserProfile.fieldConstraints` は項目別に `required`、`minlength`、`maxlength`、`pattern`、`validationMessage` を受け取り、ユーザー名などの業務ルールをフォーカス離脱時に検証します。`pattern` は単一行の入力に適用し、`validationMessage` は翻訳関数にも対応します。
193
+
194
+ `LoginForm`、`RegisterForm`、`ChangePasswordForm`、`ForgotPasswordForm`、`ResetPasswordForm`、`AuthPanel`、`UserProfile`、`FeedbackModal` は同じレイアウト設定に対応します。`CrudPage.form` または `CrudEditPanel` に渡します。CRUD 項目は `colSpan` と翻訳関数の `validation.message` に対応し、ボタンと Enter のどちらも送信前に検証します。入力修正でエラーが消えます。サーバー側の検証も必要です。
195
+
196
+ ```svelte
197
+ <!-- m、save、name、notes 为应用自己的翻译、提交回调和状态 -->
198
+ <Form columns={2} labelPosition="left" labelWidth="6rem" onsubmit={save}>
199
+ <FormField label={() => m.name()} required>
200
+ <Input name="name" bind:value={name} required />
201
+ </FormField>
202
+ <FormField label={() => m.notes()}>
203
+ <Textarea name="notes" bind:value={notes} />
204
+ </FormField>
205
+ <Button type="submit">{m.save()}</Button>
206
+ </Form>
207
+ ```
208
+
179
209
  #### フィードバック
180
210
 
181
211
  | コンポーネント | 説明 | 主なプロパティ |
@@ -531,7 +561,7 @@ components/
531
561
  crud={roleCrud}
532
562
  {data}
533
563
  permissions={{ create: true, update: true, delete: true }}
534
- form={{ variant: 'modal', modalSize: 'lg' }}
564
+ form={{ variant: 'modal', modalSize: '3xl', columns: 2, labelPosition: 'top' }}
535
565
  pagination={{ showSizeChanger: true, showJumper: true, pageSizeOptions: [10, 20, 50] }}
536
566
  density='compact'
537
567
  detailColumns={['name']}
@@ -698,7 +728,7 @@ transformPageChunk: ({ html }) => html.replace('%theme_bootstrap%', themeBootstr
698
728
 
699
729
  ```svelte
700
730
  <script>
701
- import { createLocaleStore, setGlobalLocale } from '@h-ai/ui'
731
+ import { createLocaleStore, setGlobalLocale } from '@h-ai/ui/i18n'
702
732
  import { setLocale } from '$lib/paraglide/runtime'
703
733
 
704
734
  const localeStore = createLocaleStore()
@@ -729,7 +759,7 @@ import {
729
759
  isLocaleSupported, // 检查 locale 是否支持
730
760
  resolveLocale, // 解析 locale(支持回退)
731
761
  setGlobalLocale, // 设置全局 locale(同步 @h-ai/core)
732
- } from '@h-ai/ui'
762
+ } from '@h-ai/ui/i18n'
733
763
  ```
734
764
 
735
765
  ## 設定による認証画面
package/README.md CHANGED
@@ -98,7 +98,7 @@ export default defineConfig({
98
98
 
99
99
  > 除 `toast`、类型导入与 `Range`(会和 DOM `Range` 构造器冲突)外,其余公开 Svelte 组件都可通过 `@h-ai/ui/auto-import` 自动注入。
100
100
 
101
- 管理后台等只使用部分组件的页面,可直接使用公开组件目录入口;主题工具通过轻量 `@h-ai/ui/theme` 导入,避免主题消费加载根入口中的 AI、图表等组件。组件路径以已安装版本的 exports 为准,不导入包内 `src/` 或 `dist/`。
101
+ 管理后台等只使用部分组件的页面,可直接使用公开组件目录入口;主题工具通过轻量 `@h-ai/ui/theme` 导入,避免主题消费加载根入口中的 AI、图表等组件。状态层的语言工具使用 `@h-ai/ui/i18n`,不加载组件与引导 CSS。组件路径以已安装版本的 exports 为准,不导入包内 `src/` 或 `dist/`。
102
102
 
103
103
  自动导入器识别无分号、跨行及带别名的显式导入,已绑定的组件名称不会重复注入;仅类型导入不会替代运行时组件。
104
104
 
@@ -166,16 +166,46 @@ components/
166
166
 
167
167
  `BareInput` 的 `file` 类型通过 `onchange` 读取 `event.currentTarget.files`,不绑定文件路径 `value`;其他输入类型可使用 `bind:value`。
168
168
 
169
- ### 组合组件 Compounds(36 个)
169
+ ### 组合组件 Compounds(37 个)
170
170
 
171
171
  #### 表单
172
172
 
173
173
  | 组件 | 描述 | 主要属性 |
174
174
  | ----------- | -------- | -------------------------------------------- |
175
- | `Form` | 表单容器 | `loading`, `disabled`, `onsubmit` |
176
- | `FormField` | 表单字段 | `label`, `error`, `hint`, `required` |
175
+ | `Form` | 表单容器 | `columns`, `labelPosition`, `labelWidth`, `loading`, `disabled`, `onsubmit` |
176
+ | `FormField` | 表单字段 | `label`, `for`, `colSpan`, `labelPosition`, `error`, `hint`, `required` |
177
+ | `FormLayout` | 表单布局 | `columns`, `labelPosition`, `labelWidth` |
177
178
  | `TagInput` | 标签输入 | `tags`, `maxTags`, `allowDuplicates`, `size` |
178
179
 
180
+ #### 表单布局与校验
181
+
182
+ `Form` 和可嵌入原生 `<form>` 的 `FormLayout` 共用 `columns={1 | 2}`、`labelPosition="top" | "left"`、`labelWidth`(默认 `8rem`)。默认单列、标签独立成行;双列在容器不足 `36rem` 时收为单列,同行标签在容器不足 `28rem` 时改为上下排列。
183
+
184
+ `FormField.colSpan` 默认 `'auto'`:多行文本、文件/图片上传、TagInput 和多选 Combobox 自动占两列;`1` / `2` 显式覆盖。自定义宽控件标注 `data-form-wide`。标题、错误摘要、按钮及其他非 FormField 内容自动独占一行。字段可单独覆盖 `labelPosition` / `labelWidth`。
185
+
186
+ 标签自动关联内部 UI 控件;已有控件 ID 或原生控件使用 `<FormField for="control-id">`。错误/提示通过 `aria-describedby` 关联,错误控件带 `aria-invalid`,只显示一份字段提示。`label` / `error` / `hint` 支持字符串或延迟翻译函数。
187
+
188
+ `Input`、`Textarea`、`Select` 和 `PasswordInput` 的原生校验使用内置中英文提示;`validationMessage` 只覆盖失败提示,不让有效输入一直无效。应用通过 UI 的 `setGlobalLocale` / `createLocaleStore` 同步语言,已显示的校验错误即时更新。`Textarea` 支持 `minlength` / `maxlength`,`Select` 支持 `name`、必填校验和 FormData 序列化。
189
+
190
+ 字段首次失焦后显示前端校验错误,修正输入时即时更新;未访问的字段在提交时统一校验。CRUD 新增/编辑复用该行为,支持自定义校验文案,关闭重开清空校验状态。`Select` 在焦点离开触发器和选项后校验。`PasswordInput.minLength` 同时约束长度并用于强度计算;省略时不限制登录密码长度,强度计算仍以 8 位为基准。
191
+
192
+ `UserProfile.fieldConstraints` 按字段接收 `required`、`minlength`、`maxlength`、`pattern` 和 `validationMessage`(`pattern` 用于单行输入),让应用的用户名等业务规则复用失焦校验;`validationMessage` 支持延迟翻译函数。
193
+
194
+ `LoginForm`、`RegisterForm`、`ChangePasswordForm`、`ForgotPasswordForm`、`ResetPasswordForm`、`AuthPanel`、`UserProfile` 和 `FeedbackModal` 接受同一组布局参数。`CrudPage` 通过 `form` 传入;`CrudEditPanel` 直接接收。CRUD 字段支持 `colSpan`,`validation.message` 支持翻译函数;按钮与回车均先校验,修正后错误消失,客户端校验不能替代服务端校验。
195
+
196
+ ```svelte
197
+ <!-- m、save、name、notes 为应用自己的翻译、提交回调和状态 -->
198
+ <Form columns={2} labelPosition="left" labelWidth="6rem" onsubmit={save}>
199
+ <FormField label={() => m.name()} required>
200
+ <Input name="name" bind:value={name} required />
201
+ </FormField>
202
+ <FormField label={() => m.notes()}>
203
+ <Textarea name="notes" bind:value={notes} />
204
+ </FormField>
205
+ <Button type="submit">{m.save()}</Button>
206
+ </Form>
207
+ ```
208
+
179
209
  #### 反馈
180
210
 
181
211
  | 组件 | 描述 | 主要属性 |
@@ -530,7 +560,7 @@ components/
530
560
  crud={roleCrud}
531
561
  {data}
532
562
  permissions={{ create: true, update: true, delete: true }}
533
- form={{ variant: 'modal', modalSize: 'lg' }}
563
+ form={{ variant: 'modal', modalSize: '3xl', columns: 2, labelPosition: 'top' }}
534
564
  pagination={{ showSizeChanger: true, showJumper: true, pageSizeOptions: [10, 20, 50] }}
535
565
  density='compact'
536
566
  detailColumns={['name']}
@@ -728,7 +758,7 @@ AuthPanel 在 SSR 阶段禁用原生表单控件,客户端挂载并绑定事
728
758
 
729
759
  ```svelte
730
760
  <script>
731
- import { createLocaleStore, setGlobalLocale } from '@h-ai/ui'
761
+ import { createLocaleStore, setGlobalLocale } from '@h-ai/ui/i18n'
732
762
  import { setLocale } from '$lib/paraglide/runtime'
733
763
 
734
764
  const localeStore = createLocaleStore()
@@ -759,7 +789,7 @@ import {
759
789
  isLocaleSupported, // 检查 locale 是否支持
760
790
  resolveLocale, // 解析 locale(支持回退)
761
791
  setGlobalLocale, // 设置全局 locale(同步 @h-ai/core)
762
- } from '@h-ai/ui'
792
+ } from '@h-ai/ui/i18n'
763
793
  ```
764
794
 
765
795
 
package/auto-import.d.ts CHANGED
@@ -58,6 +58,7 @@ declare global {
58
58
  const Empty: typeof import('@h-ai/ui')['Empty']
59
59
  const Form: typeof import('@h-ai/ui')['Form']
60
60
  const FormField: typeof import('@h-ai/ui')['FormField']
61
+ const FormLayout: typeof import('@h-ai/ui')['FormLayout']
61
62
  const InfiniteScroll: typeof import('@h-ai/ui')['InfiniteScroll']
62
63
  const MessageContainer: typeof import('@h-ai/ui')['MessageContainer']
63
64
  const MessageBoxContainer: typeof import('@h-ai/ui')['MessageBoxContainer']
package/auto-import.js CHANGED
@@ -49,6 +49,7 @@ const UI_COMPONENTS = new Set([
49
49
  'Empty',
50
50
  'Form',
51
51
  'FormField',
52
+ 'FormLayout',
52
53
  'InfiniteScroll',
53
54
  'MessageContainer',
54
55
  'MessageBoxContainer',
@@ -34,8 +34,10 @@
34
34
  />
35
35
  -->
36
36
  <script lang='ts'>
37
- import type { DataAttributes, Size } from '../../types.js'
37
+ import type { DataAttributes, FormText, Size } from '../../types.js'
38
38
  import { Combobox } from 'bits-ui'
39
+ import { getFormFieldContext } from '../../internal/form-context.js'
40
+ import { resolveFormText } from '../../internal/form-validation.svelte.js'
39
41
  import { uiM } from '../../messages.js'
40
42
  import { cn, getDataAttributes } from '../../utils.js'
41
43
  import { getFormControlSizeClasses } from '../control-size.js'
@@ -67,7 +69,7 @@
67
69
  /** 是否禁用 */
68
70
  disabled?: boolean
69
71
  /** 错误消息 */
70
- error?: string
72
+ error?: FormText
71
73
  /** 表单标签 */
72
74
  label?: string
73
75
  /** 自定义类名 */
@@ -91,6 +93,11 @@
91
93
  }: Props & DataAttributes = $props()
92
94
 
93
95
  const dataAttributes = $derived(getDataAttributes(restProps))
96
+ const field = getFormFieldContext()
97
+ const uniqueId = $props.id()
98
+ const messageId = `${uniqueId}-message`
99
+ const localError = $derived(resolveFormText(error))
100
+ const effectiveError = $derived(field?.error || localError)
94
101
  const sizeClasses = $derived(getFormControlSizeClasses(size))
95
102
  // 为未提供 value 时设置初始默认值(仅在初始化时执行一次)
96
103
  // 统一以空字符串初始化,避免在初始化阶段捕获 multiple 的初始值。
@@ -101,7 +108,7 @@
101
108
 
102
109
  // ─── 统一视觉样式(与 Select 对齐:rounded-lg / 柔和边框 / focus ring) ───
103
110
  const controlStateClass = $derived(
104
- error
111
+ effectiveError
105
112
  ? 'border-error/60 focus-within:ring-2 focus-within:ring-error/15'
106
113
  : 'border-base-content/15 hover:border-base-content/25 focus-within:border-primary/50 focus-within:ring-2 focus-within:ring-primary/10',
107
114
  )
@@ -200,7 +207,7 @@
200
207
  }
201
208
  </script>
202
209
 
203
- <div {...dataAttributes} class='fieldset w-full {className}'>
210
+ <div {...dataAttributes} class='fieldset w-full {className}' data-form-wide={multiple ? '' : undefined}>
204
211
  {#if fieldLabel}
205
212
  <legend class='fieldset-legend font-medium'>{fieldLabel}</legend>
206
213
  {/if}
@@ -243,6 +250,9 @@
243
250
  {/each}
244
251
 
245
252
  <Combobox.Input
253
+ id={field?.controlId}
254
+ aria-invalid={effectiveError ? true : undefined}
255
+ aria-describedby={[field?.messageId, localError && !field?.error ? messageId : ''].filter(Boolean).join(' ') || undefined}
246
256
  bind:ref={inputRef}
247
257
  class={cn('flex-1 min-w-20 bg-transparent py-0.5 outline-none placeholder:text-base-content/40', sizeClasses.spacing)}
248
258
  placeholder={selectedOptions.length === 0 ? placeholder : ''}
@@ -312,6 +322,9 @@
312
322
  >
313
323
  <div class='relative'>
314
324
  <Combobox.Input
325
+ id={field?.controlId}
326
+ aria-invalid={effectiveError ? true : undefined}
327
+ aria-describedby={[field?.messageId, localError && !field?.error ? messageId : ''].filter(Boolean).join(' ') || undefined}
315
328
  bind:ref={inputRef}
316
329
  class={cn(
317
330
  'w-full rounded-lg border bg-base-100 pr-9 outline-none transition-[border-color,box-shadow] duration-150 placeholder:text-base-content/40',
@@ -372,7 +385,7 @@
372
385
  </Combobox.Root>
373
386
  {/if}
374
387
 
375
- {#if error}
376
- <span class='fieldset-label text-error'>{error}</span>
388
+ {#if localError && !field?.error}
389
+ <p id={messageId} class='mt-1.5 text-xs text-error leading-relaxed break-words' aria-live='polite'>{localError}</p>
377
390
  {/if}
378
391
  </div>
@@ -1,4 +1,4 @@
1
- import type { DataAttributes, Size } from '../../types.js';
1
+ import type { DataAttributes, FormText, Size } from '../../types.js';
2
2
  import { Combobox } from 'bits-ui';
3
3
  /** 选项定义 */
4
4
  interface Option {
@@ -25,7 +25,7 @@ interface Props {
25
25
  /** 是否禁用 */
26
26
  disabled?: boolean;
27
27
  /** 错误消息 */
28
- error?: string;
28
+ error?: FormText;
29
29
  /** 表单标签 */
30
30
  label?: string;
31
31
  /** 自定义类名 */
@@ -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,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)
@@ -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>