@h-ai/ui 0.1.0-alpha.62 → 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 +33 -7
- package/README.ja.md +33 -7
- package/README.md +33 -7
- package/auto-import.d.ts +1 -0
- package/auto-import.js +1 -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/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/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/i18n.svelte.d.ts +2 -1
- package/dist/i18n.svelte.js +2 -1
- 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 +13 -1
- package/dist/messages/zh-CN.json +13 -1
- package/dist/messages.js +4 -3
- package/dist/types.d.ts +57 -10
- 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,42 @@ 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 (
|
|
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
|
+
`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
|
+
|
|
192
|
+
```svelte
|
|
193
|
+
<!-- m、save、name、notes 为应用自己的翻译、提交回调和状态 -->
|
|
194
|
+
<Form columns={2} labelPosition="left" labelWidth="6rem" onsubmit={save}>
|
|
195
|
+
<FormField label={() => m.name()} required>
|
|
196
|
+
<Input name="name" bind:value={name} required />
|
|
197
|
+
</FormField>
|
|
198
|
+
<FormField label={() => m.notes()}>
|
|
199
|
+
<Textarea name="notes" bind:value={notes} />
|
|
200
|
+
</FormField>
|
|
201
|
+
<Button type="submit">{m.save()}</Button>
|
|
202
|
+
</Form>
|
|
203
|
+
```
|
|
204
|
+
|
|
179
205
|
#### Feedback
|
|
180
206
|
|
|
181
207
|
| Component | Description | Key props |
|
|
@@ -531,7 +557,7 @@ Defaults match `@h-ai/crypto`: `SM4` for encrypted inputs, `SM3` for hashes, and
|
|
|
531
557
|
crud={roleCrud}
|
|
532
558
|
{data}
|
|
533
559
|
permissions={{ create: true, update: true, delete: true }}
|
|
534
|
-
form={{ variant: 'modal', modalSize: '
|
|
560
|
+
form={{ variant: 'modal', modalSize: '3xl', columns: 2, labelPosition: 'top' }}
|
|
535
561
|
pagination={{ showSizeChanger: true, showJumper: true, pageSizeOptions: [10, 20, 50] }}
|
|
536
562
|
density='compact'
|
|
537
563
|
detailColumns={['name']}
|
|
@@ -698,7 +724,7 @@ Client locale state management, automatically synchronized with the `@h-ai/core`
|
|
|
698
724
|
|
|
699
725
|
```svelte
|
|
700
726
|
<script>
|
|
701
|
-
import { createLocaleStore, setGlobalLocale } from '@h-ai/ui'
|
|
727
|
+
import { createLocaleStore, setGlobalLocale } from '@h-ai/ui/i18n'
|
|
702
728
|
import { setLocale } from '$lib/paraglide/runtime'
|
|
703
729
|
|
|
704
730
|
const localeStore = createLocaleStore()
|
|
@@ -729,7 +755,7 @@ import {
|
|
|
729
755
|
isLocaleSupported, // 检查 locale 是否支持
|
|
730
756
|
resolveLocale, // 解析 locale(支持回退)
|
|
731
757
|
setGlobalLocale, // 设置全局 locale(同步 @h-ai/core)
|
|
732
|
-
} from '@h-ai/ui'
|
|
758
|
+
} from '@h-ai/ui/i18n'
|
|
733
759
|
```
|
|
734
760
|
|
|
735
761
|
## 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
|
|
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,42 @@ components/
|
|
|
166
166
|
|
|
167
167
|
`BareInput` の `type="file"` では `onchange` の `event.currentTarget.files` を読み取り、ファイルパスの `value` はバインドしません。他の入力型では `bind:value` を使用できます。
|
|
168
168
|
|
|
169
|
-
### Compounds:複合コンポーネント(
|
|
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
|
+
`LoginForm`、`RegisterForm`、`ChangePasswordForm`、`ForgotPasswordForm`、`ResetPasswordForm`、`AuthPanel`、`UserProfile`、`FeedbackModal` は同じレイアウト設定に対応します。`CrudPage.form` または `CrudEditPanel` に渡します。CRUD 項目は `colSpan` と翻訳関数の `validation.message` に対応し、ボタンと Enter のどちらも送信前に検証します。入力修正でエラーが消えます。サーバー側の検証も必要です。
|
|
191
|
+
|
|
192
|
+
```svelte
|
|
193
|
+
<!-- m、save、name、notes 为应用自己的翻译、提交回调和状态 -->
|
|
194
|
+
<Form columns={2} labelPosition="left" labelWidth="6rem" onsubmit={save}>
|
|
195
|
+
<FormField label={() => m.name()} required>
|
|
196
|
+
<Input name="name" bind:value={name} required />
|
|
197
|
+
</FormField>
|
|
198
|
+
<FormField label={() => m.notes()}>
|
|
199
|
+
<Textarea name="notes" bind:value={notes} />
|
|
200
|
+
</FormField>
|
|
201
|
+
<Button type="submit">{m.save()}</Button>
|
|
202
|
+
</Form>
|
|
203
|
+
```
|
|
204
|
+
|
|
179
205
|
#### フィードバック
|
|
180
206
|
|
|
181
207
|
| コンポーネント | 説明 | 主なプロパティ |
|
|
@@ -531,7 +557,7 @@ components/
|
|
|
531
557
|
crud={roleCrud}
|
|
532
558
|
{data}
|
|
533
559
|
permissions={{ create: true, update: true, delete: true }}
|
|
534
|
-
form={{ variant: 'modal', modalSize: '
|
|
560
|
+
form={{ variant: 'modal', modalSize: '3xl', columns: 2, labelPosition: 'top' }}
|
|
535
561
|
pagination={{ showSizeChanger: true, showJumper: true, pageSizeOptions: [10, 20, 50] }}
|
|
536
562
|
density='compact'
|
|
537
563
|
detailColumns={['name']}
|
|
@@ -698,7 +724,7 @@ transformPageChunk: ({ html }) => html.replace('%theme_bootstrap%', themeBootstr
|
|
|
698
724
|
|
|
699
725
|
```svelte
|
|
700
726
|
<script>
|
|
701
|
-
import { createLocaleStore, setGlobalLocale } from '@h-ai/ui'
|
|
727
|
+
import { createLocaleStore, setGlobalLocale } from '@h-ai/ui/i18n'
|
|
702
728
|
import { setLocale } from '$lib/paraglide/runtime'
|
|
703
729
|
|
|
704
730
|
const localeStore = createLocaleStore()
|
|
@@ -729,7 +755,7 @@ import {
|
|
|
729
755
|
isLocaleSupported, // 检查 locale 是否支持
|
|
730
756
|
resolveLocale, // 解析 locale(支持回退)
|
|
731
757
|
setGlobalLocale, // 设置全局 locale(同步 @h-ai/core)
|
|
732
|
-
} from '@h-ai/ui'
|
|
758
|
+
} from '@h-ai/ui/i18n'
|
|
733
759
|
```
|
|
734
760
|
|
|
735
761
|
## 設定による認証画面
|
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
|
|
101
|
+
管理后台等只使用部分组件的页面,可直接使用公开组件目录入口;主题工具通过轻量 `@h-ai/ui/theme` 导入,避免主题消费加载根入口中的 AI、图表等组件。状态层的语言工具使用 `@h-ai/ui/i18n`,不加载组件与引导 CSS。组件路径以已安装版本的 exports 为准,不导入包内 `src/` 或 `dist/`。
|
|
102
102
|
|
|
103
103
|
自动导入器识别无分号、跨行及带别名的显式导入,已绑定的组件名称不会重复注入;仅类型导入不会替代运行时组件。
|
|
104
104
|
|
|
@@ -166,16 +166,42 @@ components/
|
|
|
166
166
|
|
|
167
167
|
`BareInput` 的 `file` 类型通过 `onchange` 读取 `event.currentTarget.files`,不绑定文件路径 `value`;其他输入类型可使用 `bind:value`。
|
|
168
168
|
|
|
169
|
-
### 组合组件 Compounds(
|
|
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
|
+
`LoginForm`、`RegisterForm`、`ChangePasswordForm`、`ForgotPasswordForm`、`ResetPasswordForm`、`AuthPanel`、`UserProfile` 和 `FeedbackModal` 接受同一组布局参数。`CrudPage` 通过 `form` 传入;`CrudEditPanel` 直接接收。CRUD 字段支持 `colSpan`,`validation.message` 支持翻译函数;按钮与回车均先校验,修正后错误消失,客户端校验不能替代服务端校验。
|
|
191
|
+
|
|
192
|
+
```svelte
|
|
193
|
+
<!-- m、save、name、notes 为应用自己的翻译、提交回调和状态 -->
|
|
194
|
+
<Form columns={2} labelPosition="left" labelWidth="6rem" onsubmit={save}>
|
|
195
|
+
<FormField label={() => m.name()} required>
|
|
196
|
+
<Input name="name" bind:value={name} required />
|
|
197
|
+
</FormField>
|
|
198
|
+
<FormField label={() => m.notes()}>
|
|
199
|
+
<Textarea name="notes" bind:value={notes} />
|
|
200
|
+
</FormField>
|
|
201
|
+
<Button type="submit">{m.save()}</Button>
|
|
202
|
+
</Form>
|
|
203
|
+
```
|
|
204
|
+
|
|
179
205
|
#### 反馈
|
|
180
206
|
|
|
181
207
|
| 组件 | 描述 | 主要属性 |
|
|
@@ -530,7 +556,7 @@ components/
|
|
|
530
556
|
crud={roleCrud}
|
|
531
557
|
{data}
|
|
532
558
|
permissions={{ create: true, update: true, delete: true }}
|
|
533
|
-
form={{ variant: 'modal', modalSize: '
|
|
559
|
+
form={{ variant: 'modal', modalSize: '3xl', columns: 2, labelPosition: 'top' }}
|
|
534
560
|
pagination={{ showSizeChanger: true, showJumper: true, pageSizeOptions: [10, 20, 50] }}
|
|
535
561
|
density='compact'
|
|
536
562
|
detailColumns={['name']}
|
|
@@ -728,7 +754,7 @@ AuthPanel 在 SSR 阶段禁用原生表单控件,客户端挂载并绑定事
|
|
|
728
754
|
|
|
729
755
|
```svelte
|
|
730
756
|
<script>
|
|
731
|
-
import { createLocaleStore, setGlobalLocale } from '@h-ai/ui'
|
|
757
|
+
import { createLocaleStore, setGlobalLocale } from '@h-ai/ui/i18n'
|
|
732
758
|
import { setLocale } from '$lib/paraglide/runtime'
|
|
733
759
|
|
|
734
760
|
const localeStore = createLocaleStore()
|
|
@@ -759,7 +785,7 @@ import {
|
|
|
759
785
|
isLocaleSupported, // 检查 locale 是否支持
|
|
760
786
|
resolveLocale, // 解析 locale(支持回退)
|
|
761
787
|
setGlobalLocale, // 设置全局 locale(同步 @h-ai/core)
|
|
762
|
-
} from '@h-ai/ui'
|
|
788
|
+
} from '@h-ai/ui/i18n'
|
|
763
789
|
```
|
|
764
790
|
|
|
765
791
|
|
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
|
@@ -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?:
|
|
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
|
-
|
|
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
|
-
<
|
|
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?:
|
|
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?:
|
|
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>
|