@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
@@ -8,6 +8,8 @@
8
8
  import { uiM } from '../../../messages.js'
9
9
  import { cn, getDataAttributes } from '../../../utils.js'
10
10
  import Alert from '../../compounds/Alert.svelte'
11
+ import FormField from '../../compounds/FormField.svelte'
12
+ import FormLayout from '../../compounds/FormLayout.svelte'
11
13
  import Button from '../../primitives/Button.svelte'
12
14
  import Input from '../../primitives/Input.svelte'
13
15
  import AgreementNotice from './AgreementNotice.svelte'
@@ -16,6 +18,9 @@
16
18
 
17
19
  const {
18
20
  loading = false,
21
+ columns = 1,
22
+ labelPosition = 'top',
23
+ labelWidth = '8rem',
19
24
  disabled = false,
20
25
  showTitle = false,
21
26
  showPasswordStrength = true,
@@ -165,114 +170,107 @@
165
170
  </script>
166
171
 
167
172
  <form {...dataAttributes} class={formClass} onsubmit={handleSubmit}>
168
- <!-- 标题 -->
169
- {#if showTitle}
170
- <h2 class='text-xl font-semibold text-center mb-5'>{uiM('register_title')}</h2>
171
- {/if}
172
-
173
- <!-- 自定义头部 -->
174
- {#if header}
175
- <div class='register-form-header'>
176
- {@render header()}
177
- </div>
178
- {/if}
179
-
180
- {#if passwordPolicy}<p class='text-xs text-base-content/60'>{passwordPolicyHint(passwordPolicy)}</p>{/if}
181
- {#if policyError}<p role='alert' class='text-sm text-error'>{policyError}</p>{/if}
173
+ <FormLayout {columns} {labelPosition} {labelWidth}>
174
+ <!-- 标题 -->
175
+ {#if showTitle}
176
+ <h2 class='text-xl font-semibold text-center mb-5'>{uiM('register_title')}</h2>
177
+ {/if}
182
178
 
183
- <!-- 动态字段 -->
184
- {#each fields as field (field)}
185
- {#if field === 'password'}
186
- <!-- 密码字段 -->
187
- <div class='space-y-1.5'>
188
- <label class='text-sm font-medium text-base-content/70' for='register-password'>
189
- {getFieldLabel('password')}
190
- </label>
191
- <PasswordInput
192
- id='register-password'
193
- bind:value={password}
194
- placeholder={getFieldPlaceholder('password')}
195
- autocomplete='new-password'
196
- {disabled}
197
- required
198
- showStrength={showPasswordStrength}
199
- minLength={minPasswordLength}
200
- error={errors.password}
201
- />
179
+ <!-- 自定义头部 -->
180
+ {#if header}
181
+ <div class='register-form-header'>
182
+ {@render header()}
202
183
  </div>
184
+ {/if}
203
185
 
204
- <!-- 确认密码字段 -->
205
- {#if requireConfirmPassword}
206
- <div class='space-y-1.5'>
207
- <label class='text-sm font-medium text-base-content/70' for='register-confirm-password'>
208
- {getFieldLabel('confirmPassword')}
209
- </label>
186
+ {#if passwordPolicy}<p class='text-xs text-base-content/60'>{passwordPolicyHint(passwordPolicy)}</p>{/if}
187
+ {#if policyError}<p role='alert' class='text-sm text-error'>{policyError}</p>{/if}
188
+
189
+ <!-- 动态字段 -->
190
+ {#each fields as field (field)}
191
+ {#if field === 'password'}
192
+ <!-- 密码字段 -->
193
+ <FormField for='register-password' label={getFieldLabel('password')}>
210
194
  <PasswordInput
211
- id='register-confirm-password'
212
- bind:value={confirmPassword}
213
- placeholder={getFieldPlaceholder('confirmPassword')}
195
+ id='register-password'
196
+ bind:value={password}
197
+ placeholder={getFieldPlaceholder('password')}
214
198
  autocomplete='new-password'
215
199
  {disabled}
216
200
  required
217
- showStrength={false}
218
- error={confirmPasswordError}
201
+ showStrength={showPasswordStrength}
202
+ minLength={minPasswordLength}
203
+ error={errors.password}
204
+ />
205
+ </FormField>
206
+
207
+ <!-- 确认密码字段 -->
208
+ {#if requireConfirmPassword}
209
+ <FormField for='register-confirm-password' label={getFieldLabel('confirmPassword')}>
210
+ <PasswordInput
211
+ id='register-confirm-password'
212
+ bind:value={confirmPassword}
213
+ placeholder={getFieldPlaceholder('confirmPassword')}
214
+ autocomplete='new-password'
215
+ {disabled}
216
+ required
217
+ showStrength={false}
218
+ error={confirmPasswordError}
219
+ />
220
+ </FormField>
221
+ {/if}
222
+ {:else if field !== 'confirmPassword'}
223
+ <FormField for='register-{field}' label={getFieldLabel(field)}>
224
+ <Input
225
+ id={`register-${field}`}
226
+ type={getFieldType(field)}
227
+ name={field}
228
+ placeholder={getFieldPlaceholder(field)}
229
+ value={getFieldValue(field)}
230
+ oninput={(e: Event & { currentTarget: HTMLInputElement }) => setFieldValue(field, e.currentTarget.value)}
231
+ {disabled}
232
+ required={field === 'username' || field === 'email'}
233
+ autocomplete={field === 'username' ? 'username' : field === 'email' ? 'email' : undefined}
234
+ error={errors[field]}
219
235
  />
220
- </div>
236
+ </FormField>
221
237
  {/if}
222
- {:else if field !== 'confirmPassword'}
223
- <div class='space-y-1.5'>
224
- <label class='text-sm font-medium text-base-content/70' for='register-{field}'>
225
- {getFieldLabel(field)}
226
- </label>
227
- <Input
228
- id={`register-${field}`}
229
- type={getFieldType(field)}
230
- name={field}
231
- placeholder={getFieldPlaceholder(field)}
232
- value={getFieldValue(field)}
233
- oninput={(e: Event & { currentTarget: HTMLInputElement }) => setFieldValue(field, e.currentTarget.value)}
234
- {disabled}
235
- required={field === 'username' || field === 'email'}
236
- autocomplete={field === 'username' ? 'username' : field === 'email' ? 'email' : undefined}
237
- error={errors[field]}
238
- />
239
- </div>
240
- {/if}
241
- {/each}
238
+ {/each}
242
239
 
243
- <!-- 通用错误 -->
244
- {#if extraFields}
245
- <div class='register-form-extra-fields'>{@render extraFields()}</div>
246
- {/if}
247
- {#if errors.general}
248
- <Alert variant='error'>
249
- {errors.general}
250
- </Alert>
251
- {/if}
240
+ <!-- 通用错误 -->
241
+ {#if extraFields}
242
+ <div class='register-form-extra-fields'>{@render extraFields()}</div>
243
+ {/if}
244
+ {#if errors.general}
245
+ <Alert variant='error'>
246
+ {errors.general}
247
+ </Alert>
248
+ {/if}
252
249
 
253
- <!-- 提交按钮 -->
254
- <Button
255
- type='submit'
256
- variant='primary'
257
- class='w-full'
258
- disabled={loading || disabled || !passwordsMatch || Boolean(policyError)}
259
- loading={loading}
260
- >
261
- {submitText || uiM('register_submit')}
262
- </Button>
250
+ <!-- 提交按钮 -->
251
+ <Button
252
+ type='submit'
253
+ variant='primary'
254
+ class='w-full'
255
+ disabled={loading || disabled || !passwordsMatch || Boolean(policyError)}
256
+ loading={loading}
257
+ >
258
+ {submitText || uiM('register_submit')}
259
+ </Button>
263
260
 
264
- <!-- 协议提示 -->
265
- <AgreementNotice {agreements} />
261
+ <!-- 协议提示 -->
262
+ <AgreementNotice {agreements} />
266
263
 
267
- <!-- 自定义底部 -->
268
- {#if footer}
269
- <div class='register-form-footer'>
270
- {@render footer()}
271
- </div>
272
- {:else if showLoginLink}
273
- <div class='text-center mt-4 text-sm text-base-content/60'>
274
- <span>{uiM('register_has_account')}</span>
275
- <a href={loginUrl} class='link link-primary ml-1'>{uiM('register_login_now')}</a>
276
- </div>
277
- {/if}
264
+ <!-- 自定义底部 -->
265
+ {#if footer}
266
+ <div class='register-form-footer'>
267
+ {@render footer()}
268
+ </div>
269
+ {:else if showLoginLink}
270
+ <div class='text-center mt-4 text-sm text-base-content/60'>
271
+ <span>{uiM('register_has_account')}</span>
272
+ <a href={loginUrl} class='link link-primary ml-1'>{uiM('register_login_now')}</a>
273
+ </div>
274
+ {/if}
275
+ </FormLayout>
278
276
  </form>
@@ -8,6 +8,8 @@
8
8
  import { uiM } from '../../../messages.js'
9
9
  import { cn, getDataAttributes } from '../../../utils.js'
10
10
  import Alert from '../../compounds/Alert.svelte'
11
+ import FormField from '../../compounds/FormField.svelte'
12
+ import FormLayout from '../../compounds/FormLayout.svelte'
11
13
  import Button from '../../primitives/Button.svelte'
12
14
  import Input from '../../primitives/Input.svelte'
13
15
  import { arePasswordsEqual, passwordPolicyError, passwordPolicyHint } from './password-utils.js'
@@ -15,6 +17,9 @@
15
17
 
16
18
  const {
17
19
  loading = false,
20
+ columns = 1,
21
+ labelPosition = 'top',
22
+ labelWidth = '8rem',
18
23
  disabled = false,
19
24
  showTitle = false,
20
25
  showDescription = false,
@@ -72,91 +77,84 @@
72
77
  </script>
73
78
 
74
79
  <form {...dataAttributes} class={formClass} onsubmit={handleSubmit}>
75
- <!-- 标题 -->
76
- {#if showTitle}
77
- <h2 class='text-xl font-semibold text-center mb-4'>{uiM('reset_password_title')}</h2>
78
- {/if}
79
-
80
- <!-- 描述 -->
81
- {#if showDescription}
82
- <p class='text-center text-base-content/45 text-sm mb-5'>
83
- {uiM('reset_password_desc')}
84
- </p>
85
- {/if}
86
-
87
- <!-- 验证码 -->
88
- {#if showCode}
89
- <div class='space-y-1.5'>
90
- <label class='text-sm font-medium text-base-content/70' for='reset-code'>
91
- {uiM('reset_password_code_label')}
92
- </label>
93
- <Input
94
- id='reset-code'
95
- name='code'
96
- type='text'
97
- placeholder={uiM('reset_password_code_placeholder')}
98
- bind:value={code}
80
+ <FormLayout {columns} {labelPosition} {labelWidth}>
81
+ <!-- 标题 -->
82
+ {#if showTitle}
83
+ <h2 class='text-xl font-semibold text-center mb-4'>{uiM('reset_password_title')}</h2>
84
+ {/if}
85
+
86
+ <!-- 描述 -->
87
+ {#if showDescription}
88
+ <p class='text-center text-base-content/45 text-sm mb-5'>
89
+ {uiM('reset_password_desc')}
90
+ </p>
91
+ {/if}
92
+
93
+ <!-- 验证码 -->
94
+ {#if showCode}
95
+ <FormField for='reset-code' label={uiM('reset_password_code_label')}>
96
+ <Input
97
+ id='reset-code'
98
+ name='code'
99
+ type='text'
100
+ placeholder={uiM('reset_password_code_placeholder')}
101
+ bind:value={code}
102
+ {disabled}
103
+ required
104
+ error={errors.code}
105
+ />
106
+ </FormField>
107
+ {/if}
108
+
109
+ <!-- 新密码 -->
110
+ <FormField for='reset-new-password' label={uiM('reset_password_new_label')}>
111
+ <PasswordInput
112
+ bind:value={newPassword}
113
+ placeholder={uiM('reset_password_new_placeholder')}
99
114
  {disabled}
100
- required
101
- error={errors.code}
115
+ error={errors.newPassword}
116
+ showStrength={showPasswordStrength}
117
+ minLength={minPasswordLength}
102
118
  />
103
- </div>
104
- {/if}
105
-
106
- <!-- 新密码 -->
107
- <div class='space-y-1.5'>
108
- <label class='text-sm font-medium text-base-content/70' for='reset-new-password'>
109
- {uiM('reset_password_new_label')}
110
- </label>
111
- <PasswordInput
112
- bind:value={newPassword}
113
- placeholder={uiM('reset_password_new_placeholder')}
114
- {disabled}
115
- error={errors.newPassword}
116
- showStrength={showPasswordStrength}
117
- minLength={minPasswordLength}
118
- />
119
- </div>
120
-
121
- <!-- 确认新密码 -->
122
- <div class='space-y-1.5'>
123
- <label class='text-sm font-medium text-base-content/70' for='reset-confirm-password'>
124
- {uiM('reset_password_confirm_label')}
125
- </label>
126
- <PasswordInput
127
- bind:value={confirmPassword}
128
- placeholder={uiM('reset_password_confirm_placeholder')}
129
- {disabled}
130
- error={errors.confirmPassword || (!passwordsMatch && confirmPassword ? uiM('reset_password_mismatch') : '')}
131
- showStrength={false}
132
- />
133
- </div>
134
-
135
- <!-- 通用错误 -->
136
- {#if errors.general}
137
- <Alert variant='error'>
138
- {errors.general}
139
- </Alert>
140
- {/if}
141
-
142
- {#if passwordPolicy}<p class='text-xs text-base-content/60'>{passwordPolicyHint(passwordPolicy)}</p>{/if}
143
- {#if policyError}<p role='alert' class='text-sm text-error'>{policyError}</p>{/if}
144
-
145
- <!-- 提交按钮 -->
146
- <Button
147
- type='submit'
148
- variant='primary'
149
- {loading}
150
- disabled={loading || disabled || !canSubmit || Boolean(policyError)}
151
- class='w-full'
152
- >
153
- {submitText || uiM('reset_password_submit')}
154
- </Button>
155
-
156
- <!-- 返回链接 -->
157
- {#if showBackLink}
158
- <div class='text-center mt-4 text-sm'>
159
- <a href={loginUrl} class='link link-primary'>← {uiM('reset_password_back')}</a>
160
- </div>
161
- {/if}
119
+ </FormField>
120
+
121
+ <!-- 确认新密码 -->
122
+ <FormField for='reset-confirm-password' label={uiM('reset_password_confirm_label')}>
123
+ <PasswordInput
124
+ bind:value={confirmPassword}
125
+ placeholder={uiM('reset_password_confirm_placeholder')}
126
+ {disabled}
127
+ error={errors.confirmPassword || (!passwordsMatch && confirmPassword ? uiM('reset_password_mismatch') : '')}
128
+ showStrength={false}
129
+ />
130
+ </FormField>
131
+
132
+ <!-- 通用错误 -->
133
+ {#if errors.general}
134
+ <Alert variant='error'>
135
+ {errors.general}
136
+ </Alert>
137
+ {/if}
138
+
139
+ {#if passwordPolicy}<p class='text-xs text-base-content/60'>{passwordPolicyHint(passwordPolicy)}</p>{/if}
140
+ {#if policyError}<p role='alert' class='text-sm text-error'>{policyError}</p>{/if}
141
+
142
+ <!-- 提交按钮 -->
143
+ <Button
144
+ type='submit'
145
+ variant='primary'
146
+ {loading}
147
+ disabled={loading || disabled || !canSubmit || Boolean(policyError)}
148
+ class='w-full'
149
+ >
150
+ {submitText || uiM('reset_password_submit')}
151
+ </Button>
152
+
153
+ <!-- 返回链接 -->
154
+ {#if showBackLink}
155
+ <div class='text-center mt-4 text-sm'>
156
+ <a href={loginUrl} class='link link-primary'>← {uiM('reset_password_back')}</a>
157
+ </div>
158
+ {/if}
159
+ </FormLayout>
162
160
  </form>
@@ -14,6 +14,8 @@
14
14
  import { uiM } from '../../../messages.js'
15
15
  import { cn, getDataAttributes } from '../../../utils.js'
16
16
  import Alert from '../../compounds/Alert.svelte'
17
+ import FormField from '../../compounds/FormField.svelte'
18
+ import FormLayout from '../../compounds/FormLayout.svelte'
17
19
  import Avatar from '../../primitives/Avatar.svelte'
18
20
  import BareInput from '../../primitives/BareInput.svelte'
19
21
  import Button from '../../primitives/Button.svelte'
@@ -25,7 +27,11 @@
25
27
  editable = false,
26
28
  alwaysEditable = false,
27
29
  loading = false,
30
+ columns = 1,
31
+ labelPosition = 'top',
32
+ labelWidth = '8rem',
28
33
  fields = ['avatar', 'username', 'email', 'displayName', 'phone'],
34
+ fieldConstraints = {},
29
35
  avatarAccept = 'image/*',
30
36
  avatarHint,
31
37
  class: className = '',
@@ -157,12 +163,12 @@
157
163
 
158
164
  <div {...dataAttributes} class={containerClass}>
159
165
  <form onsubmit={handleSubmit}>
160
- <div class='space-y-6'>
166
+ <FormLayout {columns} {labelPosition} {labelWidth}>
161
167
  {#each fields as field (field)}
162
168
  {@const modelKey = getModelKey(field)}
163
169
  {#if field === 'avatar'}
164
170
  <!-- 头像 -->
165
- <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'>
166
172
  <Avatar
167
173
  src={user?.avatarUrl ?? user?.avatar}
168
174
  alt={uiM('user_profile_avatar')}
@@ -171,7 +177,7 @@
171
177
  class='shadow-sm'
172
178
  />
173
179
  {#if editable && (editMode || alwaysEditable)}
174
- <div>
180
+ <div class='min-w-0 flex-1 basis-48'>
175
181
  <BareInput
176
182
  id={avatarInputId}
177
183
  type='file'
@@ -191,11 +197,11 @@
191
197
  </div>
192
198
  {:else}
193
199
  <!-- 其他字段 -->
194
- <div class='fieldset'>
195
- <legend class='fieldset-legend font-medium'>{getFieldLabel(field)}</legend>
200
+ <FormField label={getFieldLabel(field)} required={fieldConstraints[field]?.required} colSpan={field === 'bio' ? 2 : 'auto'}>
196
201
  {#if editable && (editMode || alwaysEditable)}
197
202
  {#if field === 'bio'}
198
203
  <Textarea
204
+ {...fieldConstraints[field]}
199
205
  name={modelKey}
200
206
  rows={3}
201
207
  bind:value={formData[modelKey]}
@@ -204,6 +210,7 @@
204
210
  />
205
211
  {:else}
206
212
  <Input
213
+ {...fieldConstraints[field]}
207
214
  type={field === 'email' ? 'email' : field === 'phone' ? 'tel' : 'text'}
208
215
  name={modelKey}
209
216
  bind:value={formData[modelKey]}
@@ -216,7 +223,7 @@
216
223
  {formData[modelKey] || '-'}
217
224
  </p>
218
225
  {/if}
219
- </div>
226
+ </FormField>
220
227
  {/if}
221
228
  {/each}
222
229
 
@@ -259,6 +266,6 @@
259
266
  {/if}
260
267
  </div>
261
268
  {/if}
262
- </div>
269
+ </FormLayout>
263
270
  </form>
264
271
  </div>
@@ -300,7 +300,7 @@
300
300
  }
301
301
  </script>
302
302
 
303
- <div {...dataAttributes} class={containerClass}>
303
+ <div {...dataAttributes} class={containerClass} data-form-wide>
304
304
  <!-- 上传区域 -->
305
305
  <div
306
306
  class={dropzoneClass}
@@ -195,6 +195,7 @@
195
195
  </script>
196
196
 
197
197
  <div {...dataAttributes}
198
+ data-form-wide
198
199
  class={containerClass}
199
200
  style={containerStyle}
200
201
  role='button'
@@ -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, Size } from '../../types.js';
9
+ import type { DataAttributes, FormLayoutOptions, FormText, InputProps, Size } from '../../types.js';
10
10
  /**
11
11
  * 密码输入框属性
12
12
  */
@@ -28,12 +28,12 @@ export interface PasswordInputProps extends DataAttributes {
28
28
  /** 是否必填 */
29
29
  required?: boolean;
30
30
  /** 错误消息 */
31
- error?: string;
31
+ error?: FormText;
32
32
  /** 是否显示切换按钮 */
33
33
  showToggle?: boolean;
34
34
  /** 是否显示密码强度 */
35
35
  showStrength?: boolean;
36
- /** 最小长度 */
36
+ /** 最小长度;提供时校验,省略时仅以 8 位为基准计算强度。 */
37
37
  minLength?: number;
38
38
  /** 自定义类名 */
39
39
  class?: string;
@@ -74,7 +74,7 @@ export interface LoginFormData {
74
74
  /**
75
75
  * 登录表单属性
76
76
  */
77
- export interface LoginFormProps extends DataAttributes {
77
+ export interface LoginFormProps extends FormLayoutOptions, DataAttributes {
78
78
  /** 实际可用的方式;空数组表示无可用登录方式。 */
79
79
  methods?: AuthLoginMethod[];
80
80
  /** 验证码长度 */
@@ -136,7 +136,7 @@ export interface RegisterFormData {
136
136
  /**
137
137
  * 注册表单属性
138
138
  */
139
- export interface RegisterFormProps extends DataAttributes {
139
+ export interface RegisterFormProps extends FormLayoutOptions, DataAttributes {
140
140
  passwordPolicy?: PasswordPolicy;
141
141
  /** 加载状态 */
142
142
  loading?: boolean;
@@ -184,7 +184,7 @@ export interface ChangePasswordFormData {
184
184
  /**
185
185
  * 修改密码表单属性
186
186
  */
187
- export interface ChangePasswordFormProps extends DataAttributes {
187
+ export interface ChangePasswordFormProps extends FormLayoutOptions, DataAttributes {
188
188
  passwordPolicy?: PasswordPolicy;
189
189
  /** 加载状态 */
190
190
  loading?: boolean;
@@ -215,7 +215,7 @@ export interface ForgotPasswordFormData {
215
215
  /**
216
216
  * 找回密码表单属性
217
217
  */
218
- export interface ForgotPasswordFormProps extends DataAttributes {
218
+ export interface ForgotPasswordFormProps extends FormLayoutOptions, DataAttributes {
219
219
  /** 加载状态 */
220
220
  loading?: boolean;
221
221
  /** 禁用状态 */
@@ -254,7 +254,7 @@ export interface ResetPasswordFormData {
254
254
  /**
255
255
  * 重置密码表单属性
256
256
  */
257
- export interface ResetPasswordFormProps extends DataAttributes {
257
+ export interface ResetPasswordFormProps extends FormLayoutOptions, DataAttributes {
258
258
  passwordPolicy?: PasswordPolicy;
259
259
  /** 加载状态 */
260
260
  loading?: boolean;
@@ -355,7 +355,7 @@ export interface AuthPanelActions {
355
355
  /** AuthPanel 的初始页面;未传独立路由时由组件内部导航。 */
356
356
  export type AuthPanelMode = 'login' | 'register' | 'forgot' | 'reset';
357
357
  /** 配置驱动认证场景;网络、通知和令牌存储由应用注入 actions。 */
358
- export interface AuthPanelProps extends DataAttributes {
358
+ export interface AuthPanelProps extends FormLayoutOptions, DataAttributes {
359
359
  /** 未取得配置时显示加载状态,只消费 IAM 的公开配置。 */
360
360
  config: AuthPanelConfig | null;
361
361
  /** 由 Kit 或 API client 提供标准动作,组件不处理底层传输。 */
@@ -408,7 +408,7 @@ export interface UserProfileSubmitData {
408
408
  /**
409
409
  * 用户个人信息属性
410
410
  */
411
- export interface UserProfileProps extends DataAttributes {
411
+ export interface UserProfileProps extends FormLayoutOptions, DataAttributes {
412
412
  /** 用户信息 */
413
413
  user?: UserProfileData;
414
414
  /** 是否可编辑 */
@@ -418,6 +418,8 @@ export interface UserProfileProps extends 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
  /** 文件选择器允许的头像类型;最终校验由业务上传服务负责。 */
@@ -5,7 +5,8 @@
5
5
  * @module i18n.svelte
6
6
  */
7
7
  import type { Locale, LocaleInfo } from '@h-ai/core';
8
- declare const DEFAULT_LOCALE: string, DEFAULT_LOCALES: LocaleInfo[], setGlobalLocale: (locale: Locale) => void;
8
+ import { changeUILocale as setGlobalLocale } from './internal/locale.js';
9
+ declare const DEFAULT_LOCALE: string, DEFAULT_LOCALES: LocaleInfo[];
9
10
  declare function isLocaleSupported(locale: Locale, supportedLocales?: LocaleInfo[]): boolean;
10
11
  declare function resolveLocale(locale: Locale, supportedLocales?: LocaleInfo[], defaultLocale?: Locale): Locale;
11
12
  declare function detectBrowserLocale(supportedLocales?: LocaleInfo[]): Locale | null;
@@ -6,8 +6,9 @@
6
6
  */
7
7
  import { core } from '@h-ai/core';
8
8
  import { readStoredValue, writeStoredValue } from './internal/browser-safety.js';
9
+ import { changeUILocale as setGlobalLocale } from './internal/locale.js';
9
10
  // 从 core.i18n 解构常用函数
10
- const { DEFAULT_LOCALE, DEFAULT_LOCALES, setGlobalLocale, } = core.i18n;
11
+ const { DEFAULT_LOCALE, DEFAULT_LOCALES, } = core.i18n;
11
12
  function isLocaleSupported(locale, supportedLocales = DEFAULT_LOCALES) {
12
13
  const normalized = locale === 'en' ? 'en-US' : locale === 'zh' ? 'zh-CN' : locale;
13
14
  return supportedLocales.some(item => item.code === normalized);
@@ -0,0 +1,18 @@
1
+ /** 表单布局和字段提示的内部上下文,不要求应用转发控件属性。 */
2
+ import type { FormLayoutOptions } from '../types.js';
3
+ export declare const FORM_LAYOUT_CONTEXT: unique symbol;
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;
10
+ export interface FormFieldContext {
11
+ readonly controlId: string;
12
+ readonly labelId: string;
13
+ readonly messageId: string | undefined;
14
+ readonly error: string;
15
+ }
16
+ export declare function getFormLayoutContext(): FormLayoutOptions | undefined;
17
+ export declare function getFormFieldContext(): FormFieldContext | undefined;
18
+ export {};
@@ -0,0 +1,13 @@
1
+ import { getContext } from 'svelte';
2
+ export const FORM_LAYOUT_CONTEXT = Symbol('hai-form-layout');
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
+ }
8
+ export function getFormLayoutContext() {
9
+ return getContext(FORM_LAYOUT_CONTEXT);
10
+ }
11
+ export function getFormFieldContext() {
12
+ return getContext(FORM_FIELD_CONTEXT);
13
+ }