@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.
Files changed (57) hide show
  1. package/README.en.md +33 -7
  2. package/README.ja.md +33 -7
  3. package/README.md +33 -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/Form.svelte +10 -6
  11. package/dist/components/compounds/FormField.svelte +78 -13
  12. package/dist/components/compounds/FormLayout.svelte +59 -0
  13. package/dist/components/compounds/FormLayout.svelte.d.ts +4 -0
  14. package/dist/components/compounds/TagInput.svelte +1 -1
  15. package/dist/components/compounds/index.d.ts +1 -0
  16. package/dist/components/compounds/index.js +1 -0
  17. package/dist/components/primitives/BareInput.svelte +7 -1
  18. package/dist/components/primitives/Checkbox.svelte +8 -2
  19. package/dist/components/primitives/Input.svelte +22 -11
  20. package/dist/components/primitives/Select.svelte +77 -18
  21. package/dist/components/primitives/Textarea.svelte +27 -13
  22. package/dist/components/primitives/Textarea.svelte.d.ts +1 -1
  23. package/dist/components/scenes/app/FeedbackModal.svelte +29 -19
  24. package/dist/components/scenes/app/FeedbackModal.svelte.d.ts +2 -2
  25. package/dist/components/scenes/crud/CrudEditPanel.svelte +148 -118
  26. package/dist/components/scenes/crud/CrudEditPanel.svelte.d.ts +4 -25
  27. package/dist/components/scenes/crud/CrudPage.svelte +5 -41
  28. package/dist/components/scenes/crud/CrudPage.svelte.d.ts +2 -53
  29. package/dist/components/scenes/crud/crud-controller.svelte.d.ts +4 -1
  30. package/dist/components/scenes/crud/crud-form-validation.d.ts +4 -0
  31. package/dist/components/scenes/crud/crud-form-validation.js +39 -0
  32. package/dist/components/scenes/crud/crud-types.d.ts +9 -42
  33. package/dist/components/scenes/iam/AuthPanel.svelte +55 -47
  34. package/dist/components/scenes/iam/ChangePasswordForm.svelte +57 -59
  35. package/dist/components/scenes/iam/ForgotPasswordForm.svelte +89 -84
  36. package/dist/components/scenes/iam/LoginForm.svelte +134 -132
  37. package/dist/components/scenes/iam/PasswordInput.svelte +25 -7
  38. package/dist/components/scenes/iam/PasswordInput.svelte.d.ts +2 -2
  39. package/dist/components/scenes/iam/RegisterForm.svelte +95 -97
  40. package/dist/components/scenes/iam/ResetPasswordForm.svelte +83 -85
  41. package/dist/components/scenes/iam/UserProfile.svelte +9 -5
  42. package/dist/components/scenes/storage/FileUpload.svelte +1 -1
  43. package/dist/components/scenes/storage/ImageUpload.svelte +1 -0
  44. package/dist/components/scenes/types.d.ts +9 -9
  45. package/dist/i18n.svelte.d.ts +2 -1
  46. package/dist/i18n.svelte.js +2 -1
  47. package/dist/internal/form-context.d.ts +12 -0
  48. package/dist/internal/form-context.js +9 -0
  49. package/dist/internal/form-validation.svelte.d.ts +16 -0
  50. package/dist/internal/form-validation.svelte.js +76 -0
  51. package/dist/internal/locale.d.ts +2 -0
  52. package/dist/internal/locale.js +22 -0
  53. package/dist/messages/en-US.json +13 -1
  54. package/dist/messages/zh-CN.json +13 -1
  55. package/dist/messages.js +4 -3
  56. package/dist/types.d.ts +57 -10
  57. package/package.json +7 -2
@@ -1,60 +1,9 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { DataAttributes } from '../../../types.js';
3
+ import type { CrudFieldDef } from './crud-controller.svelte.js';
3
4
  import type { CrudDensity, CrudFormConfig, CrudPaginationConfig, CrudToolbarStyle } from './crud-types.js';
4
5
  import type { NavAdapter } from './nav-adapter.js';
5
- /** CRUD 字段定义,决定列表、筛选器和表单的字段行为。 */
6
- type FieldDef = {
7
- /** 字段稳定标识,同时对应数据记录中的属性名。 */
8
- id: string;
9
- /** 字段显示文案,允许通过函数延迟读取当前语言。 */
10
- label: string | (() => string);
11
- /** 字段控件类型,最终由表单或筛选器解释。 */
12
- type: string;
13
- /** 是否出现在列表中;未设置时默认显示。 */
14
- inList?: boolean;
15
- /** 是否出现在筛选器中;只有显式为 true 时显示。 */
16
- filterable?: boolean;
17
- /** 编辑时是否禁止修改。 */
18
- readonly?: boolean;
19
- /** 是否出现在新建表单中;未设置时默认显示。 */
20
- inCreate?: boolean;
21
- /** 是否出现在编辑表单中;未设置时默认显示。 */
22
- inEdit?: boolean;
23
- /** 是否出现在详情面板中;未设置时默认显示。 */
24
- inDetail?: boolean;
25
- /** 选择类字段的静态选项或动态选项工厂。 */
26
- options?: Array<{
27
- label: string;
28
- value: string | number | boolean;
29
- }> | (() => Array<{
30
- label: string;
31
- value: string | number | boolean;
32
- }>);
33
- /** 表单校验规则。 */
34
- validation?: {
35
- required?: boolean;
36
- min?: number;
37
- max?: number;
38
- pattern?: string;
39
- message?: string;
40
- };
41
- /** 列宽 CSS 值。 */
42
- width?: string;
43
- /** 单元格对齐方式。 */
44
- align?: 'left' | 'center' | 'right';
45
- /** 列表单元格的自定义渲染函数。 */
46
- render?: (value: unknown, item: Record<string, unknown>) => string;
47
- /** 输入控件占位文案。 */
48
- placeholder?: string | (() => string);
49
- /** 日期范围起始值 key。 */
50
- startKey?: string;
51
- /** 日期范围结束值 key。 */
52
- endKey?: string;
53
- /** 新建记录时使用的默认值。 */
54
- defaultValue?: unknown;
55
- /** 字段排序权重,数值越小越靠前。 */
56
- order?: number;
57
- };
6
+ type FieldDef = CrudFieldDef;
58
7
  /** CrudPage 使用的异步资源操作集合。 */
59
8
  type CrudApi = {
60
9
  /** 获取当前分页、筛选和排序结果。 */
@@ -13,6 +13,7 @@
13
13
  * @module crud-controller
14
14
  */
15
15
  import type { PaginatedResult } from '@h-ai/core';
16
+ import type { FormColSpan } from '../../../types.js';
16
17
  import type { NavAdapter } from './nav-adapter.js';
17
18
  /** 字段展示形式;与 `CrudEditPanel` / `CrudFilterBar` 支持的类型对齐,保留 `string` 以兼容 `@h-ai/kit`。 */
18
19
  export type CrudFieldType = 'text' | 'string' | 'textarea' | 'select' | 'radio' | 'multi-select' | 'boolean' | 'checkbox' | 'email' | 'url' | 'tel' | 'password' | 'number' | 'date' | 'datetime' | 'date-range' | 'custom';
@@ -51,8 +52,10 @@ export interface CrudFieldDef {
51
52
  min?: number;
52
53
  max?: number;
53
54
  pattern?: string;
54
- message?: string;
55
+ message?: CrudText;
55
56
  };
57
+ /** 表单占用列数,默认 auto;多行文本、多选等自动占两列。 */
58
+ colSpan?: FormColSpan;
56
59
  /** 列宽(CSS)。 */
57
60
  width?: string;
58
61
  /** 列对齐。 */
@@ -0,0 +1,4 @@
1
+ /** CRUD 客户端字段校验;仅返回消息键,避免错误文案锁定在提交时的语言。 */
2
+ import type { FormValidationIssue } from '../../../internal/form-validation.svelte.js';
3
+ import type { CrudFieldDef } from './crud-controller.svelte.js';
4
+ export declare function getCrudValidationIssue(field: CrudFieldDef, value: unknown): FormValidationIssue | undefined;
@@ -0,0 +1,39 @@
1
+ export function getCrudValidationIssue(field, value) {
2
+ const rules = field.validation;
3
+ const empty = value === undefined || value === null || (typeof value === 'string' && !value.trim()) || (Array.isArray(value) && value.length === 0);
4
+ if (rules?.required && (empty || (value === false && (field.type === 'boolean' || field.type === 'checkbox'))))
5
+ return { key: 'form_required' };
6
+ if (empty)
7
+ return undefined;
8
+ if (field.type === 'number') {
9
+ const number = typeof value === 'number' ? value : Number(value);
10
+ if (!Number.isFinite(number))
11
+ return { key: 'form_number' };
12
+ if (rules?.min !== undefined && number < rules.min)
13
+ return { key: 'form_min', params: { min: rules.min } };
14
+ if (rules?.max !== undefined && number > rules.max)
15
+ return { key: 'form_max', params: { max: rules.max } };
16
+ }
17
+ else if (typeof value === 'string') {
18
+ if (rules?.min !== undefined && value.length < rules.min)
19
+ return { key: 'form_min_length', params: { min: rules.min } };
20
+ if (rules?.max !== undefined && value.length > rules.max)
21
+ return { key: 'form_max_length', params: { max: rules.max } };
22
+ }
23
+ if (field.type === 'email' && !/^[\w.!#$%&'*+/=?^`{|}~-]+@[a-z\d](?:[a-z\d-]*[a-z\d])?(?:\.[a-z\d](?:[a-z\d-]*[a-z\d])?)*$/i.test(String(value)))
24
+ return { key: 'form_email' };
25
+ if (field.type === 'url') {
26
+ if (!URL.canParse(String(value)))
27
+ return { key: 'form_url' };
28
+ }
29
+ if (rules?.pattern) {
30
+ try {
31
+ if (!new RegExp(`^(?:${rules.pattern})$`, 'v').test(String(value)))
32
+ return { key: 'form_pattern' };
33
+ }
34
+ catch {
35
+ return { key: 'form_pattern' };
36
+ }
37
+ }
38
+ return undefined;
39
+ }
@@ -5,7 +5,8 @@
5
5
  * @module crud-types
6
6
  */
7
7
  import type { Snippet } from 'svelte';
8
- import type { DataAttributes, Size } from '../../../types.js';
8
+ import type { DataAttributes, FormLayoutOptions, Size } from '../../../types.js';
9
+ import type { CrudFieldDef } from './crud-controller.svelte.js';
9
10
  /** CrudPage 权限控制 */
10
11
  export interface CrudPermissions {
11
12
  /** 允许新建 */
@@ -20,7 +21,7 @@ export type CrudFormVariant = 'drawer' | 'modal';
20
21
  /** 列表显示密度 */
21
22
  export type CrudDensity = 'normal' | 'compact';
22
23
  /** 新建/编辑表单展示配置 */
23
- export interface CrudFormConfig {
24
+ export interface CrudFormConfig extends FormLayoutOptions {
24
25
  /** 展示形式:'drawer'(抽屉,默认)或 'modal'(弹出窗口) */
25
26
  variant?: CrudFormVariant;
26
27
  /** 抽屉尺寸预设(variant='drawer',默认 '2xl') */
@@ -69,41 +70,7 @@ export interface CrudPageProps<T = Record<string, unknown>> extends DataAttribut
69
70
  name: string;
70
71
  label: string | (() => string);
71
72
  keyField?: string;
72
- fields: Array<{
73
- id: string;
74
- label: string | (() => string);
75
- type: string;
76
- inList?: boolean;
77
- filterable?: boolean;
78
- readonly?: boolean;
79
- inCreate?: boolean;
80
- inEdit?: boolean;
81
- inDetail?: boolean;
82
- options?: Array<{
83
- label: string;
84
- value: string | number | boolean;
85
- }> | (() => Array<{
86
- label: string;
87
- value: string | number | boolean;
88
- }>);
89
- validation?: {
90
- required?: boolean;
91
- min?: number;
92
- max?: number;
93
- pattern?: string;
94
- message?: string;
95
- };
96
- width?: string;
97
- align?: 'left' | 'center' | 'right';
98
- render?: (value: unknown, item: Record<string, unknown>) => string;
99
- placeholder?: string | (() => string);
100
- /** 日期范围起始值 key;默认 `${id}Start`。 */
101
- startKey?: string;
102
- /** 日期范围结束值 key;默认 `${id}End`。 */
103
- endKey?: string;
104
- defaultValue?: unknown;
105
- order?: number;
106
- }>;
73
+ fields: CrudFieldDef[];
107
74
  defaultPageSize?: number;
108
75
  searchable?: boolean;
109
76
  searchPlaceholder?: string | (() => string);
@@ -120,11 +87,11 @@ export interface CrudPageProps<T = Record<string, unknown>> extends DataAttribut
120
87
  remove?: (id: string) => Promise<void>;
121
88
  };
122
89
  };
123
- getListFields: () => Array<Record<string, unknown>>;
124
- getFilterFields: () => Array<Record<string, unknown>>;
125
- getCreateFields: () => Array<Record<string, unknown>>;
126
- getEditFields: () => Array<Record<string, unknown>>;
127
- getDetailFields: () => Array<Record<string, unknown>>;
90
+ getListFields: () => CrudFieldDef[];
91
+ getFilterFields: () => CrudFieldDef[];
92
+ getCreateFields: () => CrudFieldDef[];
93
+ getEditFields: () => CrudFieldDef[];
94
+ getDetailFields: () => CrudFieldDef[];
128
95
  toTableColumns: () => Array<{
129
96
  key: string;
130
97
  label: string;
@@ -1,6 +1,7 @@
1
1
  <!-- 配置驱动认证场景;网络、通知及 Token 存储由 actions 注入。 -->
2
2
  <script lang='ts'>
3
- import type { HaiResult } from '@h-ai/core'
3
+ import type { HaiError, HaiResult } from '@h-ai/core'
4
+ import type { UIMessageKey } from '../../../messages.js'
4
5
  import type { AuthLoginMethod, AuthPanelMode, AuthPanelProps } from '../types.js'
5
6
  import { onMount, untrack } from 'svelte'
6
7
  import { uiM } from '../../../messages.js'
@@ -16,12 +17,14 @@
16
17
  import RegisterForm from './RegisterForm.svelte'
17
18
  import ResetPasswordForm from './ResetPasswordForm.svelte'
18
19
 
19
- const { config, actions, initialMode = 'login', resetToken = '', loginUrl, registerUrl, forgotPasswordUrl, onauthenticated, error = '', onretry, loginHeader, loginFields, loginFooter, registerHeader, registerFields, registerFooter, ...restProps }: AuthPanelProps = $props()
20
+ const { config, actions, columns = 1, labelPosition = 'top', labelWidth = '8rem', initialMode = 'login', resetToken = '', loginUrl, registerUrl, forgotPasswordUrl, onauthenticated, error = '', onretry, loginHeader, loginFields, loginFooter, registerHeader, registerFields, registerFooter, ...restProps }: AuthPanelProps = $props()
20
21
  const dataAttributes = $derived(getDataAttributes(restProps))
21
22
  let mode = $state<AuthPanelMode>(untrack(() => initialMode))
22
23
  let loading = $state(false)
23
- let errors = $state<Record<string, string>>({})
24
- let notice = $state('')
24
+ let actionError = $state<Pick<HaiError, 'code' | 'message'>>()
25
+ let networkFailed = $state(false)
26
+ const errors = $derived<Record<string, string>>(actionError ? { general: authErrorMessage(actionError) } : networkFailed ? { general: uiM('auth_network_error') } : {})
27
+ let notice = $state<UIMessageKey | ''>('')
25
28
  // SSR 表单先显示但不可编辑;客户端绑定事件后开放输入,避免接管时丢失早到的输入。
26
29
  let interactive = $state(false)
27
30
  onMount(() => {
@@ -33,13 +36,15 @@
33
36
 
34
37
  function switchMode(next: AuthPanelMode) {
35
38
  mode = next
36
- errors = {}
39
+ actionError = undefined
40
+ networkFailed = false
37
41
  notice = ''
38
42
  }
39
- async function run(operation: () => Promise<HaiResult<unknown>>, authenticate = false, successNotice = '') {
43
+ async function run(operation: () => Promise<HaiResult<unknown>>, authenticate = false, successNotice: UIMessageKey | '' = '') {
40
44
  if (loading)
41
45
  return
42
- errors = {}
46
+ actionError = undefined
47
+ networkFailed = false
43
48
  notice = ''
44
49
  loading = true
45
50
  try {
@@ -48,10 +53,10 @@
48
53
  if (mode === 'register' && result.error.code === 'hai:iam:004') {
49
54
  // 默认禁用的新账户已完成注册,提示等待启用,避免用户重复注册。
50
55
  switchMode('login')
51
- notice = uiM('auth_registration_pending')
56
+ notice = 'auth_registration_pending'
52
57
  }
53
58
  else {
54
- errors = { general: authErrorMessage(result.error) }
59
+ actionError = result.error
55
60
  }
56
61
  }
57
62
  else if (authenticate) {
@@ -61,7 +66,7 @@
61
66
  notice = successNotice
62
67
  }
63
68
  }
64
- catch { errors = { general: uiM('auth_network_error') } }
69
+ catch { networkFailed = true }
65
70
  finally { loading = false }
66
71
  }
67
72
  </script>
@@ -80,44 +85,47 @@
80
85
 
81
86
  <AuthShell {...dataAttributes} {...config?.ui} title={config?.ui.title || title} brandTitle={config?.ui.brandTitle || uiM('auth_brand')}>
82
87
  <fieldset class='m-0 min-w-0 border-0 p-0' disabled={!interactive}>
83
- {#if error}
84
- <Alert variant='error'>{error}</Alert>
85
- {#if onretry}<Button class='mt-4 w-full' onclick={onretry}>{uiM('crud_retry')}</Button>{/if}
86
- {:else if !config}
87
- <div class='flex justify-center' role='status'><Spinner /></div>
88
- {:else if !available}
89
- <Alert variant='warning'>{uiM('auth_unavailable')}</Alert>
90
- <div class='mt-4 text-center'>{@render link('login', uiM('login_submit'), loginUrl)}</div>
91
- {:else}
92
- {#if notice}<Alert variant='success' class='mb-4'>{notice}</Alert>{/if}
93
- {#if mode === 'login'}
94
- <LoginForm {methods} {loading} {errors} header={loginHeader} extraFields={loginFields} showRememberMe={false} showForgotPassword={config.passwordReset.enabled} {forgotPasswordUrl}
95
- onforgotpassword={forgotPasswordUrl ? undefined : () => switchMode('forgot')}
96
- otpLength={config.otp.length} resendInterval={config.otp.resendInterval} onsendotp={actions.sendOtp}
97
- agreements={config.agreements.showOnLogin ? config.agreements : undefined}
98
- onsubmit={data => run(() => actions.login({ identifier: data.username, password: data.password, method: data.method, code: data.code }), true)}>
99
- {#snippet footer()}
100
- {#if loginFooter}{@render loginFooter()}{/if}
101
- {#if config?.register.enabled}<div class='text-center text-sm'>{uiM('login_no_account')} {@render link('register', uiM('login_register_now'), registerUrl)}</div>{/if}
102
- {/snippet}
103
- </LoginForm>
104
- {:else if mode === 'register'}
105
- <RegisterForm {loading} {errors} header={registerHeader} extraFields={registerFields} fields={config.passwordReset.mode === 'phone' ? ['username', 'phone', 'password'] : ['username', 'email', 'password']} passwordPolicy={config.password} minPasswordLength={config.password.minLength}
106
- agreements={config.agreements.showOnRegister ? config.agreements : undefined} onsubmit={data => run(() => actions.register({ username: data.username || '', password: data.password, email: data.email, phone: data.phone, displayName: data.nickname }), true)}>
107
- {#snippet footer()}
108
- {#if registerFooter}{@render registerFooter()}{/if}
109
- <div class='text-center'>{@render link('login', uiM('register_login_now'), loginUrl)}</div>
110
- {/snippet}
111
- </RegisterForm>
112
- {:else if mode === 'forgot'}
113
- <ForgotPasswordForm {loading} {errors} mode={config.passwordReset.mode ?? 'email'} onsubmit={data => run(() => actions.requestPasswordReset(data.email || data.phone || ''), false, uiM('auth_reset_sent'))}>
114
- {#snippet footer()}<div class='flex justify-between gap-3'>{@render link('login', uiM('login_submit'), loginUrl)}{@render link('reset', uiM('auth_have_token'))}</div>{/snippet}
115
- </ForgotPasswordForm>
116
- {:else}
117
- <ResetPasswordForm {loading} {errors} showCode={!resetToken} passwordPolicy={config.password} minPasswordLength={config.password.minLength}
118
- onsubmit={data => run(() => actions.confirmPasswordReset(resetToken || data.code || '', data.newPassword), false, uiM('auth_reset_done'))} />
88
+ <!-- 固定内容包装节点,避免 fieldset 插入通知时丢失容器查询子树的布局。 -->
89
+ <div class='min-w-0'>
90
+ {#if error}
91
+ <Alert variant='error'>{error}</Alert>
92
+ {#if onretry}<Button class='mt-4 w-full' onclick={onretry}>{uiM('crud_retry')}</Button>{/if}
93
+ {:else if !config}
94
+ <div class='flex justify-center' role='status'><Spinner /></div>
95
+ {:else if !available}
96
+ <Alert variant='warning'>{uiM('auth_unavailable')}</Alert>
119
97
  <div class='mt-4 text-center'>{@render link('login', uiM('login_submit'), loginUrl)}</div>
98
+ {:else}
99
+ {#if notice}<Alert variant='success' class='mb-4'>{uiM(notice)}</Alert>{/if}
100
+ {#if mode === 'login'}
101
+ <LoginForm {columns} {labelPosition} {labelWidth} {methods} {loading} {errors} header={loginHeader} extraFields={loginFields} showRememberMe={false} showForgotPassword={config.passwordReset.enabled} {forgotPasswordUrl}
102
+ onforgotpassword={forgotPasswordUrl ? undefined : () => switchMode('forgot')}
103
+ otpLength={config.otp.length} resendInterval={config.otp.resendInterval} onsendotp={actions.sendOtp}
104
+ agreements={config.agreements.showOnLogin ? config.agreements : undefined}
105
+ onsubmit={data => run(() => actions.login({ identifier: data.username, password: data.password, method: data.method, code: data.code }), true)}>
106
+ {#snippet footer()}
107
+ {#if loginFooter}{@render loginFooter()}{/if}
108
+ {#if config?.register.enabled}<div class='text-center text-sm'>{uiM('login_no_account')} {@render link('register', uiM('login_register_now'), registerUrl)}</div>{/if}
109
+ {/snippet}
110
+ </LoginForm>
111
+ {:else if mode === 'register'}
112
+ <RegisterForm {columns} {labelPosition} {labelWidth} {loading} {errors} header={registerHeader} extraFields={registerFields} fields={config.passwordReset.mode === 'phone' ? ['username', 'phone', 'password'] : ['username', 'email', 'password']} passwordPolicy={config.password} minPasswordLength={config.password.minLength}
113
+ agreements={config.agreements.showOnRegister ? config.agreements : undefined} onsubmit={data => run(() => actions.register({ username: data.username || '', password: data.password, email: data.email, phone: data.phone, displayName: data.nickname }), true)}>
114
+ {#snippet footer()}
115
+ {#if registerFooter}{@render registerFooter()}{/if}
116
+ <div class='text-center'>{@render link('login', uiM('register_login_now'), loginUrl)}</div>
117
+ {/snippet}
118
+ </RegisterForm>
119
+ {:else if mode === 'forgot'}
120
+ <ForgotPasswordForm {columns} {labelPosition} {labelWidth} {loading} {errors} mode={config.passwordReset.mode ?? 'email'} onsubmit={data => run(() => actions.requestPasswordReset(data.email || data.phone || ''), false, 'auth_reset_sent')}>
121
+ {#snippet footer()}<div class='flex justify-between gap-3'>{@render link('login', uiM('login_submit'), loginUrl)}{@render link('reset', uiM('auth_have_token'))}</div>{/snippet}
122
+ </ForgotPasswordForm>
123
+ {:else}
124
+ <ResetPasswordForm {columns} {labelPosition} {labelWidth} {loading} {errors} showCode={!resetToken} passwordPolicy={config.password} minPasswordLength={config.password.minLength}
125
+ onsubmit={data => run(() => actions.confirmPasswordReset(resetToken || data.code || '', data.newPassword), false, 'auth_reset_done')} />
126
+ <div class='mt-4 text-center'>{@render link('login', uiM('login_submit'), loginUrl)}</div>
127
+ {/if}
120
128
  {/if}
121
- {/if}
129
+ </div>
122
130
  </fieldset>
123
131
  </AuthShell>
@@ -8,12 +8,17 @@
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 { arePasswordsEqual, passwordPolicyError, passwordPolicyHint } from './password-utils.js'
13
15
  import PasswordInput from './PasswordInput.svelte'
14
16
 
15
17
  const {
16
18
  loading = false,
19
+ columns = 1,
20
+ labelPosition = 'top',
21
+ labelWidth = '8rem',
17
22
  disabled = false,
18
23
  requireOldPassword = true,
19
24
  showPasswordStrength = true,
@@ -73,71 +78,64 @@
73
78
  </script>
74
79
 
75
80
  <form {...dataAttributes} class={formClass} onsubmit={handleSubmit}>
76
- {#if requireOldPassword}
77
- <div class='space-y-1.5'>
78
- <label class='text-sm font-medium text-base-content/70' for='old-password'>
79
- {uiM('change_password_old')}
80
- </label>
81
+ <FormLayout {columns} {labelPosition} {labelWidth}>
82
+ {#if requireOldPassword}
83
+ <FormField for='old-password' label={uiM('change_password_old')}>
84
+ <PasswordInput
85
+ value={oldPassword}
86
+ oninput={(e) => { oldPassword = e.currentTarget.value }}
87
+ placeholder={uiM('change_password_old_placeholder')}
88
+ {disabled}
89
+ error={errors.oldPassword}
90
+ showStrength={false}
91
+ />
92
+ </FormField>
93
+ {/if}
94
+
95
+ <FormField for='new-password' label={uiM('change_password_new')}>
81
96
  <PasswordInput
82
- value={oldPassword}
83
- oninput={(e) => { oldPassword = e.currentTarget.value }}
84
- placeholder={uiM('change_password_old_placeholder')}
97
+ value={newPassword}
98
+ oninput={(e) => { newPassword = e.currentTarget.value }}
99
+ placeholder={uiM('change_password_new_placeholder')}
85
100
  {disabled}
86
- error={errors.oldPassword}
87
- showStrength={false}
101
+ error={errors.newPassword}
102
+ showStrength={showPasswordStrength}
103
+ minLength={minPasswordLength}
88
104
  />
89
- </div>
90
- {/if}
105
+ </FormField>
91
106
 
92
- <div class='space-y-1.5'>
93
- <label class='text-sm font-medium text-base-content/70' for='new-password'>
94
- {uiM('change_password_new')}
95
- </label>
96
- <PasswordInput
97
- value={newPassword}
98
- oninput={(e) => { newPassword = e.currentTarget.value }}
99
- placeholder={uiM('change_password_new_placeholder')}
100
- {disabled}
101
- error={errors.newPassword}
102
- showStrength={showPasswordStrength}
103
- minLength={minPasswordLength}
104
- />
105
- </div>
106
-
107
- <div class='space-y-1.5'>
108
- <label class='text-sm font-medium text-base-content/70' for='confirm-password'>
109
- {uiM('change_password_confirm')}
110
- </label>
111
- <PasswordInput
112
- value={confirmPassword}
113
- oninput={(e) => { confirmPassword = e.currentTarget.value }}
114
- placeholder={uiM('change_password_confirm_placeholder')}
115
- {disabled}
116
- error={errors.confirmPassword || (!passwordsMatch && confirmPassword ? uiM('change_password_mismatch') : '')}
117
- showStrength={false}
118
- />
119
- </div>
107
+ <FormField for='confirm-password' label={uiM('change_password_confirm')}>
108
+ <PasswordInput
109
+ value={confirmPassword}
110
+ oninput={(e) => { confirmPassword = e.currentTarget.value }}
111
+ placeholder={uiM('change_password_confirm_placeholder')}
112
+ {disabled}
113
+ error={errors.confirmPassword || (!passwordsMatch && confirmPassword ? uiM('change_password_mismatch') : '')}
114
+ showStrength={false}
115
+ />
116
+ </FormField>
120
117
 
121
- <div class='rounded-lg bg-base-content/3 px-3.5 py-2.5 text-xs text-base-content/50'>
122
- {uiM('change_password_relogin_hint')}
123
- </div>
118
+ <div class='rounded-lg bg-base-content/3 px-3.5 py-2.5 text-xs text-base-content/50'>
119
+ {uiM('change_password_relogin_hint')}
120
+ </div>
124
121
 
125
- {#if errors.general}
126
- <Alert variant='error'>
127
- {errors.general}
128
- </Alert>
129
- {/if}
122
+ {#if errors.general}
123
+ <Alert variant='error'>
124
+ {errors.general}
125
+ </Alert>
126
+ {/if}
130
127
 
131
- {#if passwordPolicy}<p class='text-xs text-base-content/60'>{passwordPolicyHint(passwordPolicy)}</p>{/if}
132
- {#if policyError}<p role='alert' class='text-sm text-error'>{policyError}</p>{/if}
128
+ {#if passwordPolicy}<p class='text-xs text-base-content/60'>{passwordPolicyHint(passwordPolicy)}</p>{/if}
129
+ {#if policyError}<p role='alert' class='text-sm text-error'>{policyError}</p>{/if}
133
130
 
134
- <Button
135
- type='submit'
136
- variant='primary'
137
- class='w-full sm:w-auto sm:min-w-40'
138
- disabled={loading || disabled || !canSubmit || Boolean(policyError)}
139
- {loading}
140
- >
141
- {submitText || uiM('change_password_submit')}
142
- </Button>
131
+ <Button
132
+ type='submit'
133
+ variant='primary'
134
+ class='w-full sm:w-auto sm:min-w-40'
135
+ disabled={loading || disabled || !canSubmit || Boolean(policyError)}
136
+ {loading}
137
+ >
138
+ {submitText || uiM('change_password_submit')}
139
+ </Button>
140
+ </FormLayout>
143
141
  </form>