@h-ai/cli 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 CHANGED
@@ -275,12 +275,16 @@ Admin and fullstack gates also verify public types and runtime behavior from ins
275
275
 
276
276
  Successful screenshots default to the CLI's `test-results/scaffold-evidence/`. Set `HAI_AUTH_EVIDENCE_DIR` to retain them in a chosen directory after generated projects are cleaned up. Fullstack E2E starts every selected Web/App/Desktop browser preview and checks login, tours, help and profile navigation. Miniapp and native shells require separate platform acceptance.
277
277
 
278
- ## License
279
-
280
- Apache-2.0
281
-
282
278
  Failures and degraded-state messages must also be localized: website APIs use the request locale for AI fallback and email delivery status; undelivered messages must not be described as received. CLI deployment status uses cliM, and Gallery encryption limitations use bilingual messages. Verify unconfigured, failed, and successful paths in Chinese and English without paid service calls.
283
279
 
284
280
  Provisioned configuration: Neon emits HAI_DB (PostgreSQL JSON), Upstash HAI_CACHE (Redis rediss URL JSON), and R2 HAI_STORAGE (flat S3 JSON). Email/SMS emit HAI_REACH_PROVIDERS; deployApp merges both channels while preserving YAML message templates, replacing the original providers array. Core supports whole-object/array overrides, with parents preceding leaves and schema validation retained. Build subprocesses and target runtimes share provisioned values. R2 needs existing S3 accessKeyId/secretAccessKey, Resend a verified-domain from address, and Alibaba Cloud approved signName/templates. Applications need matching `_db.yml` / `_cache.yml` / `_storage.yml` / `_reach.yml` loaded at startup; network clients require Node.
285
281
 
286
282
  Deployment recovery: `ProvisionResult.resourceStatus` marks created/reused. `getDeployRecovery(result.error)` reads projectName, stage, platformProjectId, and resource lists; unknown means an uncertain request result that must first be checked in the provider console. Preserve returned IDs for failed resources. Recovery excludes envVars, raw exceptions, and credentials; the CLI shows resources and advice. Same-name retries reuse resources. Reused resources are never deleted automatically; clean up newly created resources only after manually confirming they are unused.
283
+
284
+ ## Generated form layouts
285
+
286
+ Website contact forms and fullstack management screens reuse UI FormLayout/FormField: one or two columns, stacked or inline labels, automatic spans and narrow-container fallback. User, profile and password forms share the same options and built-in Chinese/English validation. The generated hai-ui skill documents the complete contract.
287
+
288
+ ## License
289
+
290
+ Apache-2.0
package/README.ja.md CHANGED
@@ -275,12 +275,16 @@ Admin/fullstack の門禁では実際にインストールしたローカル tar
275
275
 
276
276
  成功時のスクリーンショットは CLI の `test-results/scaffold-evidence/` に保存します。`HAI_AUTH_EVIDENCE_DIR` で保存先を指定でき、生成プロジェクトの削除後も残ります。fullstack E2E は選択した全 Web/App/Desktop のブラウザープレビューでログイン、ガイド、ヘルプ、個人設定を検証します。小程序とネイティブシェルは各プラットフォームで別途検証します。
277
277
 
278
- ## License
279
-
280
- Apache-2.0
281
-
282
278
  失敗・縮退時の表示も国際化します。サイト API はリクエストの locale で AI 縮退・メール到達状態を表示し、未到達を受信済みと表現しません。CLI のデプロイ状態は cliM、Gallery の暗号化制限は二言語メッセージを使います。中国語・英語で未設定・失敗・成功を検証し、有料サービスの呼び出しは不要です。
283
279
 
284
280
  クラウド設定の利用:Neon は HAI_DB(PostgreSQL JSON)、Upstash は HAI_CACHE(Redis rediss URL JSON)、R2 は HAI_STORAGE(フラットな S3 JSON)を出力します。メール・SMS は HAI_REACH_PROVIDERS を出力し、deployApp が両チャネルと YAML のメッセージテンプレートを統合して元の providers 配列を置換します。Core はオブジェクト・配列全体の上書きをサポートし、親ノードを末端より優先して Schema 検証を維持します。ビルド子プロセスと対象環境は同じ値を使います。R2 は既存の S3 accessKeyId/secretAccessKey、Resend は検証済みドメインの from、Alibaba Cloud は承認済み signName・テンプレートが必要です。アプリは対応する `_db.yml` / `_cache.yml` / `_storage.yml` / `_reach.yml` を起動時に読み込み、ネットワーククライアントには Node が必要です。
285
281
 
286
282
  デプロイ失敗からの復旧:`ProvisionResult.resourceStatus` は created/reused を示します。`getDeployRecovery(result.error)` で projectName、stage、platformProjectId、リソース一覧を取得し、unknown は結果不明なので先にサービス側の管理画面で確認します。失敗リソースの ID が返った場合は保持します。復旧情報に envVars・元の例外・接続認証情報は含まれず、CLI が一覧と提案を表示します。同名の再試行は既存リソースを再利用し、自動削除しません。新規作成分も未使用を人が確認した後にのみ削除します。
283
+
284
+ ## 生成フォームのレイアウト
285
+
286
+ website の連絡フォームと fullstack 管理画面は UI の FormLayout/FormField を使い、1列・2列、ラベルの縦・横配置、自動列結合と狭いコンテナーへの対応を共有します。ユーザー編集・プロフィール・パスワードは同じ設定と中国語・英語の検証メッセージを使用します。生成された hai-ui skill に設定を記載しています。
287
+
288
+ ## License
289
+
290
+ Apache-2.0
package/README.md CHANGED
@@ -275,12 +275,16 @@ Admin 与 fullstack 门禁还用实际安装的本地 tarball 验证公开类型
275
275
 
276
276
  成功截图默认保存在 CLI 的 `test-results/scaffold-evidence/`,可通过 `HAI_AUTH_EVIDENCE_DIR` 指定独立目录,生成项目清理后仍保留。fullstack E2E 启动所有已选 Web/App/Desktop 浏览器预览,并验证各端的登录、导览、帮助与个人设置;小程序和原生壳仍需要对应平台验收。
277
277
 
278
- ## License
279
-
280
- Apache-2.0
281
-
282
278
  失败与降级提示也必须国际化:官网 API 按请求 locale 生成 AI 降级/邮件送达状态;未送达不能称为已接收。CLI 部署状态使用 cliM,Gallery 加密能力限制使用双语消息。中文与英文分别验证未配置、失败和成功路径,无需调用付费服务。
283
279
 
284
280
  云资源配置消费:Neon 输出 HAI_DB(PostgreSQL JSON),Upstash 输出 HAI_CACHE(Redis rediss URL JSON),R2 输出 HAI_STORAGE(扁平 S3 JSON)。邮件/短信输出 HAI_REACH_PROVIDERS 数组,deployApp 合并两个渠道并保留 YAML 中的消息模板;此覆盖替换原 providers 数组。Core 支持整体对象/数组覆盖,父节点优先于叶子变量,仍执行 Schema 校验;构建子进程与目标运行环境使用相同开通结果。R2 必须提供已有 S3 accessKeyId/secretAccessKey;Resend 必须提供已验证域名的 from;阿里云必须提供已审核 signName/模板。应用须已有相应\_db/\_cache/\_storage/\_reach.yml 并在启动时加载,网络客户端需要 Node 运行时。
285
281
 
286
282
  部署失败恢复:`ProvisionResult.resourceStatus` 标记 created/reused;`getDeployRecovery(result.error)` 读取 projectName、stage、platformProjectId 和资源清单,unknown 表示请求结果不确定,必须先在服务商控制台核对。失败资源已返回 ID 时保留该 ID。恢复结果不含 envVars、原始异常或连接凭证,CLI 会展示清单和建议。同名重试复用现有资源;不自动删除复用资源,新建资源仅在人工确认未被使用后清理。
283
+
284
+ ## 生成项目的表单布局
285
+
286
+ website 联系页与 fullstack 管理界面复用 UI 的 FormLayout/FormField:支持一列/双列、标签独立成行/同行、自动跨列和窄容器回落。用户编辑、资料与密码表单使用同组参数,校验提示由 UI 内置中英文处理;完整配置随生成项目的 hai-ui skill 提供。
287
+
288
+ ## License
289
+
290
+ Apache-2.0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@h-ai/cli",
3
3
  "type": "module",
4
- "version": "0.1.0-alpha.62",
4
+ "version": "0.1.0-alpha.63",
5
5
  "description": "Hai Framework CLI for project scaffolding and code generation.",
6
6
  "license": "Apache-2.0",
7
7
  "keywords": [
@@ -30,7 +30,7 @@
30
30
  "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
31
31
  },
32
32
  "dependencies": {
33
- "@h-ai/core": "0.1.0-alpha.62",
33
+ "@h-ai/core": "0.1.0-alpha.63",
34
34
  "cac": "^7.0.1",
35
35
  "chalk": "^6.0.1",
36
36
  "fast-wrap-ansi": "^0.2.2",
@@ -41,11 +41,11 @@
41
41
  "yaml": "^2.9.1"
42
42
  },
43
43
  "devDependencies": {
44
- "@h-ai/cache": "0.1.0-alpha.62",
45
- "@h-ai/deploy": "0.1.0-alpha.62",
46
- "@h-ai/reach": "0.1.0-alpha.62",
47
- "@h-ai/reldb": "0.1.0-alpha.62",
48
- "@h-ai/storage": "0.1.0-alpha.62",
44
+ "@h-ai/cache": "0.1.0-alpha.63",
45
+ "@h-ai/deploy": "0.1.0-alpha.63",
46
+ "@h-ai/reach": "0.1.0-alpha.63",
47
+ "@h-ai/reldb": "0.1.0-alpha.63",
48
+ "@h-ai/storage": "0.1.0-alpha.63",
49
49
  "@types/fs-extra": "^11.0.4",
50
50
  "@types/node": "^26.6.5",
51
51
  "@types/prompts": "^2.4.9",
@@ -157,3 +157,7 @@ import { iam } from '@h-ai/iam'
157
157
  选用 IAM 的 admin/h5/website 模板提供 AuthPanel 和标准认证路由;mobile-app 与 fullstack Web/App/Desktop 使用 API 客户端共享动作。修改服务端 config/_iam.yml 即可控制登录方式、注册、重置、协议和品牌,无需改表单。配置 Reach 模板和真实发送 Provider 后启用 OTP/重置。分布式部署使用共享数据库与 Redis。
158
158
 
159
159
  LDAP/Reach 是可选 peer;IAM 不强制启用 Reach。默认 LDAP 客户端由应用选择安装 ldapts。自定义通知回调、协议弹窗、表单 snippet 用法随 hai-iam/hai-ui skills 完整复制到生成项目,不能要求应用读取本仓库私有路径。
160
+
161
+ ## 表单布局
162
+
163
+ 新增/编辑复用 `@h-ai/ui` 的 Form/FormLayout/FormField:可选一列或双列、标签独立成行或与控件同行,多行文本与多选自动跨列,窄容器自动收为一列。CRUD 使用 `form` 配置,字段可用 `colSpan` 覆盖。错误显示在字段附近并随 UI 语言更新。website 联系页采用 FormLayout + FormField;fullstack 共享用户编辑页采用双列同行标签,个人资料与密码页复用 UI 场景组件。没有数据表单的模板保留现有骨架,新增表单按 hai-ui skill 接入。
@@ -48,3 +48,7 @@ LDAP and Reach are optional. Install ldapts in the IAM service workspace only wh
48
48
  协议支持配置中的 userAgreementMarkdown/privacyPolicyMarkdown 正文及 URL;同主题优先 Markdown,由安全渲染器在弹窗显示。
49
49
 
50
50
  See `.agents/skills/hai-iam/reference.md` in the generated application for the complete contract.
51
+
52
+ ## Form layouts
53
+
54
+ The user editor uses `FormLayout columns={2} labelPosition="left"`; profile and password forms reuse the same UI options. Narrow containers automatically stack fields and labels. Field validation errors follow the current UI language. The management store syncs locale through the lightweight `@h-ai/ui/i18n` entry, without loading components or tour CSS. Configure `colSpan` on FormField to override automatic spans for textareas, uploads and multiple choices.
@@ -33,7 +33,7 @@
33
33
  <p class='mt-4 text-xs leading-6 text-base-content/50'>{m.avatar_hint()}</p>
34
34
  </Card>
35
35
  <Card title={m.profile_title()} bordered shadow={false} data-guide='admin-profile-form'>
36
- <UserProfile user={store.user} editable alwaysEditable fields={['avatar', 'displayName', 'email']} avatarAccept='image/png,image/jpeg,image/webp' avatarHint={m.avatar_hint()} loading={store.saving} onsubmit={data => store.saveProfile(data)} onavatarchange={file => void store.uploadAvatar(file)} data-testid='profile-form' />
36
+ <UserProfile columns={2} labelPosition='left' user={store.user} editable alwaysEditable fields={['avatar', 'displayName', 'email']} avatarAccept='image/png,image/jpeg,image/webp' avatarHint={m.avatar_hint()} loading={store.saving} onsubmit={data => store.saveProfile(data)} onavatarchange={file => void store.uploadAvatar(file)} data-testid='profile-form' />
37
37
  </Card>
38
38
  </div>
39
39
  <Card title={m.preferences_title()} bordered shadow={false} data-guide='admin-preferences'>
@@ -51,7 +51,7 @@
51
51
  </Card>
52
52
  <Card title={m.password_title()} bordered shadow={false} data-guide='admin-password'>
53
53
  <p class='mb-5 text-sm text-base-content/50'>{m.password_rules()}</p>
54
- <div class='max-w-xl'><ChangePasswordForm loading={store.saving} passwordPolicy={store.authConfig?.password} minPasswordLength={store.authConfig?.password.minLength ?? 8} onsubmit={data => store.changePassword(data)} data-testid='password-form' /></div>
54
+ <div class='max-w-xl'><ChangePasswordForm labelPosition='left' loading={store.saving} passwordPolicy={store.authConfig?.password} minPasswordLength={store.authConfig?.password.minLength ?? 8} onsubmit={data => store.changePassword(data)} data-testid='password-form' /></div>
55
55
  </Card>
56
56
  {/if}
57
57
  </section>
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { AdminRoleCode, AdminUser } from '{{fullstack.contractPackageName}}'
3
3
  import type { ManagementStore } from '../stores/management-store.svelte.js'
4
- import { Card, DataTable, FormField, Modal, Pagination, Skeleton } from '@h-ai/ui/components/compounds/index'
4
+ import { Card, DataTable, FormField, FormLayout, Modal, Pagination, Skeleton } from '@h-ai/ui/components/compounds/index'
5
5
  import { Avatar, Badge, BareInput, Button, Checkbox, Input, Select } from '@h-ai/ui/components/primitives/index'
6
6
  import { PasswordInput } from '@h-ai/ui/components/scenes/iam/index'
7
7
  import * as m from '../paraglide/messages.js'
@@ -133,23 +133,25 @@
133
133
  </div>
134
134
  </section>
135
135
 
136
- <Modal bind:open={editorOpen} title={editing ? m.user_edit() : m.user_create()} closeOnBackdrop={!store.saving} closeOnEscape={!store.saving} data-testid="user-editor">
137
- <form class="space-y-4" onsubmit={submit}>
138
- <FormField label={m.user_username()} required>
139
- <Input bind:value={username} name="username" readonly={!!editing} required minlength={3} maxlength={64} pattern="[\w\-]+" autocomplete="off" data-testid="user-username" />
140
- </FormField>
141
- <FormField label={m.user_display_name()}><Input bind:value={displayName} maxlength={100} data-testid="user-display-name" /></FormField>
142
- <FormField label={m.user_email()}><Input type="email" bind:value={email} data-testid="user-email" /></FormField>
143
- {#if !editing}
144
- <FormField label={m.user_password()} hint={m.initial_password_rules({ min: store.authConfig?.password.minLength ?? 8 })} required><PasswordInput bind:value={password} autocomplete="new-password" minLength={store.authConfig?.password.minLength ?? 8} data-testid="user-password" /></FormField>
145
- {/if}
146
- <FormField label={m.user_role()}><Select bind:value={role} options={roleOptions} disabled={editing?.id === store.user?.id} data-testid="user-role" /></FormField>
147
- <Checkbox bind:checked={enabled} label={m.account_enabled()} disabled={editing?.id === store.user?.id} />
148
- {#if store.error}<p class="text-sm text-error" role="alert">{store.error}</p>{/if}
149
- <div class="flex justify-end gap-2 border-t border-base-content/8 pt-4">
150
- <Button variant="ghost" disabled={store.saving} onclick={() => { editorOpen = false }}>{m.cancel()}</Button>
151
- <Button type="submit" variant="primary" loading={store.saving} disabled={!editing && password.length < 8} data-testid="user-save">{m.save()}</Button>
152
- </div>
136
+ <Modal bind:open={editorOpen} title={editing ? m.user_edit() : m.user_create()} closeOnBackdrop={!store.saving} closeOnEscape={!store.saving} size="3xl" data-testid="user-editor">
137
+ <form onsubmit={submit}>
138
+ <FormLayout columns={2} labelPosition="left" labelWidth="6rem">
139
+ <FormField label={m.user_username()} required>
140
+ <Input bind:value={username} name="username" readonly={!!editing} required minlength={3} maxlength={64} pattern="[\w\-]+" autocomplete="off" data-testid="user-username" />
141
+ </FormField>
142
+ <FormField label={m.user_display_name()}><Input bind:value={displayName} maxlength={100} data-testid="user-display-name" /></FormField>
143
+ <FormField label={m.user_email()}><Input type="email" bind:value={email} data-testid="user-email" /></FormField>
144
+ {#if !editing}
145
+ <FormField label={m.user_password()} hint={m.initial_password_rules({ min: store.authConfig?.password.minLength ?? 8 })} required><PasswordInput required bind:value={password} autocomplete="new-password" minLength={store.authConfig?.password.minLength ?? 8} data-testid="user-password" /></FormField>
146
+ {/if}
147
+ <FormField label={m.user_role()}><Select bind:value={role} options={roleOptions} disabled={editing?.id === store.user?.id} data-testid="user-role" /></FormField>
148
+ <Checkbox bind:checked={enabled} label={m.account_enabled()} disabled={editing?.id === store.user?.id} />
149
+ {#if store.error}<p class="text-sm text-error" role="alert">{store.error}</p>{/if}
150
+ <div class="flex justify-end gap-2 border-t border-base-content/8 pt-4">
151
+ <Button variant="ghost" disabled={store.saving} onclick={() => { editorOpen = false }}>{m.cancel()}</Button>
152
+ <Button type="submit" variant="primary" loading={store.saving} disabled={!editing && password.length < 8} data-testid="user-save">{m.save()}</Button>
153
+ </div>
154
+ </FormLayout>
153
155
  </form>
154
156
  </Modal>
155
157
 
@@ -4,7 +4,7 @@ import type { ChangePasswordFormData, UserProfileSubmitData } from '@h-ai/ui'
4
4
  import type { AdminAuditLog, AdminCreateUser, AdminRole, AdminUpdateUser, AdminUser, AvatarUpload, ProfileUpdate } from '{{fullstack.contractPackageName}}'
5
5
  import type { AppInfo, ManagementApiOptions } from '../api/api-client.js'
6
6
  import { HaiApiClientError } from '@h-ai/api-client'
7
- import { core } from '@h-ai/core'
7
+ import { setGlobalLocale } from '@h-ai/ui/i18n'
8
8
  import { SvelteDate } from 'svelte/reactivity'
9
9
  import { initializeManagementApi, managementApi, resolveAvatarUrl } from '../api/api-client.js'
10
10
  import * as m from '../paraglide/messages.js'
@@ -305,7 +305,7 @@ export class ManagementStore {
305
305
  return
306
306
  this.locale = locale
307
307
  setLocale(locale, { reload: false })
308
- core.i18n.setGlobalLocale(locale)
308
+ setGlobalLocale(locale)
309
309
  if (typeof document !== 'undefined')
310
310
  document.documentElement.lang = locale
311
311
  this.onLocaleChange?.(locale)
@@ -6,7 +6,7 @@
6
6
  {{#if hasI18n}}
7
7
  import * as m from '$lib/paraglide/messages'
8
8
  {{/if}}
9
- import { Button, Card, FormField, Input, PageHeader } from '@h-ai/ui'
9
+ import { Button, Card, FormField, FormLayout, Input, PageHeader, Textarea } from '@h-ai/ui'
10
10
 
11
11
  let name = $state('')
12
12
  let email = $state('')
@@ -33,30 +33,32 @@
33
33
  {{/if}}
34
34
 
35
35
  <Card shadow='sm' class='bg-base-100'>
36
- <form class='space-y-4' onsubmit={handleSubmit}>
36
+ <form onsubmit={handleSubmit}>
37
+ <FormLayout columns={2} labelPosition='top'>
37
38
  {{#if hasI18n}}
38
- <FormField label={m.page_contact_name()} required>
39
- <Input id='name' type='text' bind:value={name} required />
40
- </FormField>
41
- <FormField label={m.page_contact_email()} required>
42
- <Input id='email' type='email' bind:value={email} required />
43
- </FormField>
44
- <FormField label={m.page_contact_message()} required>
45
- <textarea id='message' class='textarea textarea-bordered w-full' rows='4' bind:value={message}></textarea>
46
- </FormField>
47
- <Button type='submit' variant='primary' class='w-full'>{m.page_contact_submit()}</Button>
39
+ <FormField for='name' label={m.page_contact_name()} required>
40
+ <Input id='name' type='text' bind:value={name} required />
41
+ </FormField>
42
+ <FormField for='email' label={m.page_contact_email()} required>
43
+ <Input id='email' type='email' bind:value={email} required />
44
+ </FormField>
45
+ <FormField for='message' label={m.page_contact_message()} required>
46
+ <Textarea id='message' rows={4} bind:value={message} required />
47
+ </FormField>
48
+ <Button type='submit' variant='primary' class='w-full'>{m.page_contact_submit()}</Button>
48
49
  {{else}}
49
- <FormField label='姓名' required>
50
- <Input id='name' type='text' bind:value={name} required />
51
- </FormField>
52
- <FormField label='邮箱' required>
53
- <Input id='email' type='email' bind:value={email} required />
54
- </FormField>
55
- <FormField label='留言' required>
56
- <textarea id='message' class='textarea textarea-bordered w-full' rows='4' bind:value={message}></textarea>
57
- </FormField>
58
- <Button type='submit' variant='primary' class='w-full'>发送</Button>
50
+ <FormField for='name' label='姓名' required>
51
+ <Input id='name' type='text' bind:value={name} required />
52
+ </FormField>
53
+ <FormField for='email' label='邮箱' required>
54
+ <Input id='email' type='email' bind:value={email} required />
55
+ </FormField>
56
+ <FormField for='message' label='留言' required>
57
+ <Textarea id='message' rows={4} bind:value={message} required />
58
+ </FormField>
59
+ <Button type='submit' variant='primary' class='w-full'>发送</Button>
59
60
  {{/if}}
61
+ </FormLayout>
60
62
  </form>
61
63
  </Card>
62
64
  </section>
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "engines": { "node": "^22.22.2 || ^24.15.0 || >=26.0.0" },
3
3
  "name": "{{projectName}}",
4
- "version": "0.1.0-alpha.62",
4
+ "version": "0.1.0-alpha.63",
5
5
  "private": true,
6
6
  "packageManager": "pnpm@12.11.2",
7
7
  "description": "{{#if_eq appType 'admin'}}Enterprise management system built with hai Agent Framework{{/if_eq}}{{#if_eq appType 'website'}}Enterprise website built with hai Agent Framework{{/if_eq}}{{#if_eq appType 'h5'}}Mobile H5 app built with hai Agent Framework{{/if_eq}}{{#if_eq appType 'api'}}REST API service built with hai Agent Framework{{/if_eq}}{{#if_eq appType 'mobile-app'}}Mobile app built with hai Agent Framework{{/if_eq}}",
@@ -181,3 +181,7 @@ export const appKitConfig = resolveKitConfig(parse(rawKitConfig) ?? {})
181
181
  - 传输加密只通过 `crypto.transport` 间接装配,不在 kit 中新增本地加密工厂或鸭子类型。
182
182
  - 服务端业务模块直接调用模块 API,不通过 HTTP 自环。
183
183
  - API endpoint 必须用 Zod 校验用户输入,并设置认证/权限边界。
184
+
185
+ ## 声明式 CRUD 表单字段
186
+
187
+ `kit.crud.define` 的字段支持 `colSpan: 'auto' | 1 | 2`,以及 `validation.message: string | (() => string)`。布局通过 UI 的 `CrudPage.form` 配置;客户端校验用于交互提示,服务端仍使用 Zod 验证和现有权限守卫。UI 公共契约见相邻 [hai-ui skill](../hai-ui/SKILL.md)。
@@ -45,3 +45,13 @@ description: "使用 @h-ai/ui 构建 Svelte 5 表单、表格、弹窗、新手
45
45
  - 重要约定
46
46
  - 导出路径
47
47
  - 相关 Skills
48
+
49
+ ## 表单布局与错误提示
50
+
51
+ 状态层通过轻量入口 `@h-ai/ui/i18n` 导入 `setGlobalLocale` / `createLocaleStore`,不加载组件或引导 CSS。
52
+
53
+ 数据新增/修改统一使用 `Form`(托管提交)或原生 `<form>` + `FormLayout`,不要在应用复制字段 grid。两者支持 `columns={1 | 2}`、`labelPosition='top' | 'left'`、`labelWidth`;默认单列/标签在上,窄容器自动收拢。`FormField.colSpan='auto'` 自动让多行文本、上传和多选占两列,`1` / `2` 可覆盖;自定义宽控件标注 `data-form-wide`。已有控件 ID 或原生控件给 FormField 配 `for`,避免重复或失效的标签。
54
+
55
+ IAM 表单、AuthPanel、UserProfile、FeedbackModal 直接接收同组参数;CRUD 使用 `form={{ columns: 2, labelPosition: 'left', labelWidth: '6rem' }}`,字段支持 `colSpan` 和延迟翻译的 `validation.message: () => m.required()`。通用字段的 `label/error/hint`、Input/Textarea/Select 的 `validationMessage` 支持翻译函数。内置必填、类型、长度和范围错误由 UI 提供;应用只提供业务文案。通过 UI 的 `setGlobalLocale` / `createLocaleStore` 同步语言,已显示的错误随之更新;自定义提示只用于失败,不能把固定提示直接设成持续的 customValidity。字段错误只在 FormField 或控件其中一处展示。
56
+
57
+ 验证一列/双列与两种标签位置、自动/显式跨列、窄抽屉/手机、无效提交与修正、切换语言后的错误、按钮和回车提交,以及关闭重开后的状态。
@@ -255,8 +255,8 @@ const p = usePlatform()
255
255
 
256
256
  | 组件 | Props 要点 | 说明 |
257
257
  | ------------ | ------------------------------------------------------------------ | ------------- |
258
- | `Form` | `loading`, `disabled`, `onsubmit` | 表单容器 |
259
- | `FormField` | `label`, `name`, `error`, `hint`, `required` | 表单字段 |
258
+ | `Form` | `columns`, `labelPosition`, `labelWidth`, `loading`, `disabled`, `onsubmit` | 表单容器 |
259
+ | `FormField` | `label`, `for`, `colSpan`, `labelPosition`, `error`, `hint`, `required` | 表单字段 |
260
260
  | `Modal` | `open`, `title`, `size`, `closeOnBackdrop` | 模态框,内置对话框语义、焦点约束和关闭后焦点恢复 |
261
261
  | `Drawer` | `open`, `position`, `size`, `width`, `resizable`, `widthStorageKey` | 可选拖动并记忆宽度的抽屉 |
262
262
  | `DataTable` | `data`, `columns`, `keyField`, `sortKey`, `sortDir`, `onsort`, snippet slots | 数据表格(列定义 `sortable` 可排序) |
@@ -643,6 +643,7 @@ AuthPanel 在 SSR 阶段禁用原生表单控件,客户端挂载并绑定事
643
643
  | --------------------------- | -------------------------------------- |
644
644
  | `@h-ai/ui` | 主入口:所有组件、工具函数、类型 |
645
645
  | `@h-ai/ui/theme` | 轻量主题元数据与工具,不经过组件根入口 |
646
+ | `@h-ai/ui/i18n` | 轻量语言工具,供状态层使用,不加载组件或引导 CSS |
646
647
  | `@h-ai/ui/auto-import` | Svelte 预处理器:组件自动导入 |
647
648
  | `@h-ai/ui/components/*` | 公开组件目录的 JS 入口,按实际 exports/产物选择 |
648
649
  | `@h-ai/ui/components/primitives/index` | 公开原子组件目录入口,如 Button/Input |
@@ -657,3 +658,25 @@ AuthPanel 在 SSR 阶段禁用原生表单控件,客户端挂载并绑定事
657
658
  - `hai-iam`:IAM 模块 API(与 LoginForm/RegisterForm 配合)
658
659
  - `hai-capacitor`:原生 App 开发(与 SafeArea/AppBar 配合)
659
660
  - `hai-api-client`:客户端数据获取
661
+
662
+ ## 表单布局与错误提示
663
+
664
+ 数据新增/修改统一使用 `Form`(托管提交)或原生 `<form>` + `FormLayout`,不要在应用复制字段 grid。两者支持 `columns={1 | 2}`、`labelPosition='top' | 'left'`、`labelWidth`;默认单列/标签在上,窄容器自动收拢。`FormField.colSpan='auto'` 自动让多行文本、上传和多选占两列,`1` / `2` 可覆盖;自定义宽控件标注 `data-form-wide`。已有控件 ID 或原生控件给 FormField 配 `for`,避免重复或失效的标签。
665
+
666
+ IAM 表单、AuthPanel、UserProfile、FeedbackModal 直接接收同组参数;CRUD 使用 `form={{ columns: 2, labelPosition: 'left', labelWidth: '6rem' }}`,字段支持 `colSpan` 和延迟翻译的 `validation.message: () => m.required()`。通用字段的 `label/error/hint`、Input/Textarea/Select 的 `validationMessage` 支持翻译函数。内置必填、类型、长度和范围错误由 UI 提供;应用只提供业务文案。通过 UI 的 `setGlobalLocale` / `createLocaleStore` 同步语言,已显示的错误随之更新;自定义提示只用于失败,不能把固定提示直接设成持续的 customValidity。字段错误只在 FormField 或控件其中一处展示。
667
+
668
+ 验证一列/双列与两种标签位置、自动/显式跨列、窄抽屉/手机、无效提交与修正、切换语言后的错误、按钮和回车提交,以及关闭重开后的状态。
669
+
670
+
671
+ ```svelte
672
+ <!-- m、save、name、notes 为应用自己的翻译、提交回调和状态 -->
673
+ <Form columns={2} labelPosition="left" labelWidth="6rem" onsubmit={save}>
674
+ <FormField label={() => m.name()} required>
675
+ <Input name="name" bind:value={name} required />
676
+ </FormField>
677
+ <FormField label={() => m.notes()}>
678
+ <Textarea name="notes" bind:value={notes} />
679
+ </FormField>
680
+ <Button type="submit">{m.save()}</Button>
681
+ </Form>
682
+ ```