@h-ai/ui 0.1.0-alpha.24 → 0.1.0-alpha.26

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 (36) hide show
  1. package/README.md +41 -11
  2. package/auto-import.d.ts +5 -0
  3. package/auto-import.js +4 -0
  4. package/dist/components/compounds/DataTable.svelte +81 -4
  5. package/dist/components/compounds/DataTable.svelte.d.ts +7 -0
  6. package/dist/components/compounds/Modal.svelte +123 -63
  7. package/dist/components/compounds/Pagination.svelte +72 -119
  8. package/dist/components/scenes/ai/markdown-parse.js +12 -4
  9. package/dist/components/scenes/ai/table/AiTableEditor.svelte +4 -2
  10. package/dist/components/scenes/app/SettingsLayout.svelte +111 -0
  11. package/dist/components/scenes/app/SettingsLayout.svelte.d.ts +41 -0
  12. package/dist/components/scenes/app/ThemeColorPicker.svelte +41 -0
  13. package/dist/components/scenes/app/index.d.ts +1 -0
  14. package/dist/components/scenes/app/index.js +1 -0
  15. package/dist/components/scenes/crud/CrudFilterBar.svelte +173 -35
  16. package/dist/components/scenes/crud/CrudFilterBar.svelte.d.ts +7 -1
  17. package/dist/components/scenes/crud/CrudPage.svelte +19 -15
  18. package/dist/components/scenes/error/ErrorPage.svelte +125 -0
  19. package/dist/components/scenes/error/ErrorPage.svelte.d.ts +42 -0
  20. package/dist/components/scenes/error/error-presets.d.ts +24 -0
  21. package/dist/components/scenes/error/error-presets.js +31 -0
  22. package/dist/components/scenes/error/index.d.ts +8 -0
  23. package/dist/components/scenes/error/index.js +8 -0
  24. package/dist/components/scenes/iam/AuthShell.svelte +138 -0
  25. package/dist/components/scenes/iam/AuthShell.svelte.d.ts +45 -0
  26. package/dist/components/scenes/iam/index.d.ts +1 -0
  27. package/dist/components/scenes/iam/index.js +2 -1
  28. package/dist/components/scenes/index.d.ts +1 -0
  29. package/dist/components/scenes/index.js +2 -0
  30. package/dist/messages/en-US.json +20 -1
  31. package/dist/messages/zh-CN.json +20 -1
  32. package/dist/styles/global.css +4 -1
  33. package/dist/theme-config.d.ts +60 -4
  34. package/dist/theme-config.js +77 -6
  35. package/dist/types.d.ts +6 -0
  36. package/package.json +2 -2
@@ -6,6 +6,7 @@
6
6
  -->
7
7
  <script lang='ts'>
8
8
  import { uiM } from '../../../messages.js'
9
+ import BareButton from '../../primitives/BareButton.svelte'
9
10
  import Input from '../../primitives/Input.svelte'
10
11
  import Select from '../../primitives/Select.svelte'
11
12
 
@@ -13,6 +14,12 @@
13
14
  id: string
14
15
  label: string | (() => string)
15
16
  type: string
17
+ placeholder?: string | (() => string)
18
+ startKey?: string
19
+ endKey?: string
20
+ min?: number
21
+ max?: number
22
+ step?: number
16
23
  options?: Array<{ label: string, value: string | number | boolean }> | (() => Array<{ label: string, value: string | number | boolean }>)
17
24
  }
18
25
 
@@ -22,22 +29,62 @@
22
29
  searchValue = $bindable(''),
23
30
  filterFields = [],
24
31
  filterValues = $bindable<Record<string, unknown>>({}),
25
- total = 0,
26
32
  onsearch,
27
33
  onfilterchange,
34
+ onreset,
28
35
  }: {
29
36
  searchable?: boolean
30
37
  searchPlaceholder?: string
31
38
  searchValue?: string
32
39
  filterFields?: FilterFieldDef[]
33
40
  filterValues?: Record<string, unknown>
34
- total?: number
35
41
  onsearch?: (search: string) => void
36
42
  onfilterchange?: (filters: Record<string, unknown>) => void
43
+ onreset?: () => void
37
44
  } = $props()
38
45
 
39
46
  let searchTimer: ReturnType<typeof setTimeout> | undefined
40
47
 
48
+ // 是否存在已激活的搜索/过滤条件(用于显示重置按钮)
49
+ const hasFilterValue = $derived(Object.values(filterValues).some(v => v !== undefined && v !== '' && v !== null))
50
+ const hasActiveFilters = $derived(searchValue.trim() !== '' || hasFilterValue)
51
+
52
+ const dateLikeTypes = new Set(['date', 'datetime-local', 'datetime', 'month', 'week'])
53
+ const numberLikeTypes = new Set(['number', 'integer'])
54
+
55
+ function resolveText(text: string | (() => string) | undefined): string {
56
+ if (!text)
57
+ return ''
58
+ return typeof text === 'function' ? text() : text
59
+ }
60
+
61
+ function normalizeType(type: string): string {
62
+ return type.trim().toLowerCase()
63
+ }
64
+
65
+ function isBooleanField(field: FilterFieldDef): boolean {
66
+ const t = normalizeType(field.type)
67
+ return t === 'boolean' || t === 'switch' || t === 'toggle'
68
+ }
69
+
70
+ function isDateRangeField(field: FilterFieldDef): boolean {
71
+ const t = normalizeType(field.type)
72
+ return t === 'date-range' || t === 'daterange'
73
+ }
74
+
75
+ function isDateField(field: FilterFieldDef): boolean {
76
+ return dateLikeTypes.has(normalizeType(field.type))
77
+ }
78
+
79
+ function isNumberField(field: FilterFieldDef): boolean {
80
+ return numberLikeTypes.has(normalizeType(field.type))
81
+ }
82
+
83
+ function isTextField(field: FilterFieldDef): boolean {
84
+ const t = normalizeType(field.type)
85
+ return t === 'text' || t === 'search' || t === 'keyword'
86
+ }
87
+
41
88
  function resolveOptions(options?: FilterFieldDef['options']): Array<{ label: string, value: string | number | boolean }> {
42
89
  if (!options)
43
90
  return []
@@ -55,46 +102,137 @@
55
102
  filterValues = { ...filterValues, [fieldId]: value }
56
103
  onfilterchange?.(filterValues)
57
104
  }
105
+
106
+ function handleFilterPatch(partial: Record<string, unknown>) {
107
+ filterValues = { ...filterValues, ...partial }
108
+ onfilterchange?.(filterValues)
109
+ }
110
+
111
+ function handleReset() {
112
+ clearTimeout(searchTimer)
113
+ searchValue = ''
114
+ filterValues = {}
115
+ onreset?.()
116
+ }
58
117
  </script>
59
118
 
60
- <div class='grid gap-3 md:grid-cols-[minmax(0,1fr)_auto] md:items-center'>
61
- <div class='flex min-w-0 flex-wrap items-center gap-3 [&_.fieldset]:m-0 [&_.fieldset]:min-w-0'>
62
- {#if searchable}
63
- <div class='relative min-w-[15rem] flex-[1_1_20rem]'>
64
- <span class='icon-[tabler--search] pointer-events-none absolute left-3.5 top-1/2 z-10 size-4 -translate-y-1/2 text-base-content/35'></span>
119
+ <div class='flex flex-wrap items-center gap-2 [&_.fieldset]:m-0 [&_.fieldset]:min-w-0'>
120
+ {#if searchable}
121
+ <div class='relative w-full sm:w-64 md:w-72'>
122
+ <span class='icon-[tabler--search] pointer-events-none absolute left-3 top-1/2 z-10 size-4 -translate-y-1/2 text-base-content/35'></span>
123
+ <Input
124
+ type='text'
125
+ size='sm'
126
+ placeholder={searchPlaceholder || uiM('crud_search_placeholder')}
127
+ class='h-8 pl-9 shadow-none'
128
+ bind:value={searchValue}
129
+ oninput={handleSearchInput}
130
+ autocomplete='off'
131
+ />
132
+ </div>
133
+ {/if}
134
+
135
+ {#each filterFields as field (field.id)}
136
+ {@const fieldPlaceholder = resolveText(field.placeholder)}
137
+ {@const opts = resolveOptions(field.options)}
138
+
139
+ {#if isDateRangeField(field)}
140
+ {@const startKey = field.startKey ?? `${field.id}Start`}
141
+ {@const endKey = field.endKey ?? `${field.id}End`}
142
+ <div class='flex items-center gap-1.5'>
143
+ <div class='w-32 shrink-0'>
144
+ <input
145
+ type='date'
146
+ class='h-8 w-full rounded-lg border border-base-content/15 bg-base-100 px-2.5 text-sm outline-none transition-[border-color,box-shadow] duration-150 focus:border-primary/50 focus:ring-2 focus:ring-primary/10'
147
+ aria-label={fieldPlaceholder || uiM('crud_filter_from')}
148
+ value={String(filterValues[startKey] ?? '')}
149
+ onchange={(e: Event & { currentTarget: HTMLInputElement }) => handleFilterPatch({ [startKey]: e.currentTarget.value })}
150
+ />
151
+ </div>
152
+ <span class='text-sm text-base-content/40'>~</span>
153
+ <div class='w-32 shrink-0'>
154
+ <input
155
+ type='date'
156
+ class='h-8 w-full rounded-lg border border-base-content/15 bg-base-100 px-2.5 text-sm outline-none transition-[border-color,box-shadow] duration-150 focus:border-primary/50 focus:ring-2 focus:ring-primary/10'
157
+ aria-label={uiM('crud_filter_to')}
158
+ value={String(filterValues[endKey] ?? '')}
159
+ onchange={(e: Event & { currentTarget: HTMLInputElement }) => handleFilterPatch({ [endKey]: e.currentTarget.value })}
160
+ />
161
+ </div>
162
+ </div>
163
+ {:else if isBooleanField(field)}
164
+ <div class='w-30 shrink-0'>
165
+ <Select
166
+ size='sm'
167
+ class='h-8 border-base-content/20 bg-base-100 shadow-none'
168
+ value={String(filterValues[field.id] ?? '')}
169
+ onchange={(value: string) => handleFilterChange(field.id, value)}
170
+ >
171
+ <option value="">{uiM('crud_filter_all')}</option>
172
+ <option value='true'>{uiM('crud_filter_yes')}</option>
173
+ <option value='false'>{uiM('crud_filter_no')}</option>
174
+ </Select>
175
+ </div>
176
+ {:else if isDateField(field)}
177
+ <div class='w-36 shrink-0'>
178
+ <input
179
+ type='date'
180
+ class='h-8 w-full rounded-lg border border-base-content/15 bg-base-100 px-2.5 text-sm outline-none transition-[border-color,box-shadow] duration-150 focus:border-primary/50 focus:ring-2 focus:ring-primary/10'
181
+ aria-label={fieldPlaceholder || resolveText(field.label)}
182
+ value={String(filterValues[field.id] ?? '')}
183
+ onchange={(e: Event & { currentTarget: HTMLInputElement }) => handleFilterChange(field.id, e.currentTarget.value)}
184
+ />
185
+ </div>
186
+ {:else if isNumberField(field)}
187
+ <div class='w-28 shrink-0'>
188
+ <Input
189
+ type='number'
190
+ size='sm'
191
+ min={field.min}
192
+ max={field.max}
193
+ step={field.step}
194
+ class='h-8 shadow-none'
195
+ placeholder={fieldPlaceholder || resolveText(field.label)}
196
+ value={String(filterValues[field.id] ?? '')}
197
+ onchange={(e: Event & { currentTarget: HTMLInputElement }) => handleFilterChange(field.id, e.currentTarget.value)}
198
+ />
199
+ </div>
200
+ {:else if opts.length > 0}
201
+ <div class='w-34 shrink-0'>
202
+ <Select
203
+ size='sm'
204
+ class='h-8 border-base-content/20 bg-base-100 shadow-none'
205
+ value={String(filterValues[field.id] ?? '')}
206
+ onchange={(value: string) => handleFilterChange(field.id, value)}
207
+ >
208
+ <option value="">{uiM('crud_filter_all')}</option>
209
+ {#each opts as opt (String(opt.value))}
210
+ <option value={String(opt.value)}>{opt.label}</option>
211
+ {/each}
212
+ </Select>
213
+ </div>
214
+ {:else if isTextField(field)}
215
+ <div class='w-44 shrink-0'>
65
216
  <Input
66
217
  type='text'
67
218
  size='sm'
68
- placeholder={searchPlaceholder || uiM('crud_search_placeholder')}
69
- class='pl-10 shadow-none'
70
- bind:value={searchValue}
71
- oninput={handleSearchInput}
72
- autocomplete='off'
219
+ class='h-8 shadow-none'
220
+ placeholder={fieldPlaceholder || resolveText(field.label)}
221
+ value={String(filterValues[field.id] ?? '')}
222
+ onchange={(e: Event & { currentTarget: HTMLInputElement }) => handleFilterChange(field.id, e.currentTarget.value)}
73
223
  />
74
224
  </div>
75
225
  {/if}
226
+ {/each}
76
227
 
77
- {#each filterFields as field (field.id)}
78
- {@const opts = resolveOptions(field.options)}
79
- {#if opts.length > 0}
80
- <div class='w-full sm:w-[11rem] sm:flex-none'>
81
- <Select
82
- size='sm'
83
- class='border-base-content/10 bg-base-100 shadow-none'
84
- value={String(filterValues[field.id] ?? '')}
85
- onchange={value => handleFilterChange(field.id, value)}
86
- >
87
- <option value="">{uiM('crud_filter_all')}</option>
88
- {#each opts as opt (String(opt.value))}
89
- <option value={String(opt.value)}>{opt.label}</option>
90
- {/each}
91
- </Select>
92
- </div>
93
- {/if}
94
- {/each}
95
- </div>
96
-
97
- <div class='inline-flex items-center justify-self-start rounded-full border border-base-content/8 bg-base-200/60 px-3 py-1 text-xs font-medium text-base-content/60 md:justify-self-end'>
98
- {uiM('crud_total', { count: total })}
99
- </div>
228
+ {#if hasActiveFilters}
229
+ <BareButton
230
+ type='button'
231
+ class='inline-flex h-8 items-center gap-1 rounded-md px-2.5 text-sm font-medium text-base-content/60 transition-colors hover:bg-base-content/5 hover:text-base-content'
232
+ onclick={handleReset}
233
+ >
234
+ {uiM('crud_reset')}
235
+ <span class='icon-[tabler--x] size-3.5'></span>
236
+ </BareButton>
237
+ {/if}
100
238
  </div>
@@ -2,6 +2,12 @@ type FilterFieldDef = {
2
2
  id: string;
3
3
  label: string | (() => string);
4
4
  type: string;
5
+ placeholder?: string | (() => string);
6
+ startKey?: string;
7
+ endKey?: string;
8
+ min?: number;
9
+ max?: number;
10
+ step?: number;
5
11
  options?: Array<{
6
12
  label: string;
7
13
  value: string | number | boolean;
@@ -16,9 +22,9 @@ type $$ComponentProps = {
16
22
  searchValue?: string;
17
23
  filterFields?: FilterFieldDef[];
18
24
  filterValues?: Record<string, unknown>;
19
- total?: number;
20
25
  onsearch?: (search: string) => void;
21
26
  onfilterchange?: (filters: Record<string, unknown>) => void;
27
+ onreset?: () => void;
22
28
  };
23
29
  /**
24
30
  * CrudFilterBar
@@ -142,9 +142,8 @@
142
142
  const actionButtonSize = $derived(isCompact ? 'xs' : 'sm')
143
143
  const actionIconClass = $derived(isCompact ? 'size-3.5' : 'size-4')
144
144
  const paginationSize = $derived(isCompact ? 'xs' : 'sm')
145
- const filterCardClass = 'overflow-hidden rounded-[1.25rem] border-base-content/7 shadow-sm'
146
145
  const listCardClass = 'overflow-hidden rounded-[1.25rem] border-base-content/7 shadow-sm'
147
- const paginationBarClass = 'flex justify-center border-t border-base-content/5 px-4 py-2'
146
+ const paginationBarClass = 'border-t border-base-content/5 px-4 py-3'
148
147
 
149
148
  // 表单展示配置(抽屉 / 弹窗)
150
149
  const formVariant = $derived(form.variant ?? 'drawer')
@@ -249,6 +248,12 @@
249
248
  navigateWithParams({ pageSize: newPageSize, page: 1 })
250
249
  }
251
250
 
251
+ function handleResetFilters() {
252
+ searchValue = ''
253
+ filterValues = {}
254
+ void Promise.resolve(nav.navigate(currentBasePath)).then(() => nav.refresh?.())
255
+ }
256
+
252
257
  // ─── 打开/关闭抽屉 ───
253
258
 
254
259
  function openDetail(item: Record<string, unknown>) {
@@ -396,6 +401,7 @@
396
401
  label: col.label,
397
402
  width: col.width,
398
403
  align: col.align as 'left' | 'center' | 'right' | undefined,
404
+ sortable: true,
399
405
  render: col.render
400
406
  ? (item: Record<string, unknown>) => col.render!(item)
401
407
  : undefined,
@@ -421,18 +427,16 @@
421
427
 
422
428
  <!-- 搜索 + 过滤栏 -->
423
429
  {#if searchable || filterFields.length > 0}
424
- <Card padding='sm' class={filterCardClass}>
425
- <CrudFilterBar
426
- {searchable}
427
- searchPlaceholder={searchPlaceholderText}
428
- bind:searchValue
429
- {filterFields}
430
- bind:filterValues
431
- total={data.total}
432
- onsearch={handleSearch}
433
- onfilterchange={handleFilterChange}
434
- />
435
- </Card>
430
+ <CrudFilterBar
431
+ {searchable}
432
+ searchPlaceholder={searchPlaceholderText}
433
+ bind:searchValue
434
+ {filterFields}
435
+ bind:filterValues
436
+ onsearch={handleSearch}
437
+ onfilterchange={handleFilterChange}
438
+ onreset={handleResetFilters}
439
+ />
436
440
  {/if}
437
441
 
438
442
  <!-- 数据列表 -->
@@ -482,7 +486,7 @@
482
486
  {/snippet}
483
487
  </DataTable>
484
488
 
485
- <!-- 分页栏:始终显示,支持每页条数选择与跳页 -->
489
+ <!-- 分页栏:始终显示,支持每页条数选择与跳页(shadcn table 风格) -->
486
490
  <div class={paginationBarClass}>
487
491
  <Pagination
488
492
  page={data.page}
@@ -0,0 +1,125 @@
1
+ <!--
2
+ @component ErrorPage
3
+ 通用错误页场景组件
4
+
5
+ 展示大号状态码 + 标题 + 描述 + 操作按钮,内置 401/403/404/500/503 预设。
6
+ 借鉴 shadcn-admin 错误页布局,复用现有主题 token。
7
+
8
+ 使用 Svelte 5 Runes ($props, $derived)
9
+
10
+ @example
11
+ <ErrorPage status={403} homeUrl="/admin" onback={() => history.back()} />
12
+ -->
13
+ <script lang='ts'>
14
+ import type { Snippet } from 'svelte'
15
+ import type { ErrorPreset } from './error-presets.js'
16
+ import { uiM } from '../../../messages.js'
17
+ import { cn } from '../../../utils.js'
18
+ import Button from '../../primitives/Button.svelte'
19
+ import { ERROR_PRESETS, resolveErrorPreset } from './error-presets.js'
20
+
21
+ const {
22
+ status = 404,
23
+ code,
24
+ title,
25
+ description,
26
+ showHome = true,
27
+ showBack = true,
28
+ homeUrl = '/',
29
+ onhome,
30
+ onback,
31
+ icon,
32
+ actions,
33
+ class: className = '',
34
+ }: {
35
+ /** HTTP 状态码或预设标识;未命中预设时回退到最接近的预设 */
36
+ status?: number | string | ErrorPreset
37
+ /** 自定义大号展示码(覆盖预设) */
38
+ code?: string
39
+ /** 自定义标题(覆盖预设文案) */
40
+ title?: string
41
+ /** 自定义描述(覆盖预设文案) */
42
+ description?: string
43
+ /** 是否显示「返回首页」按钮 */
44
+ showHome?: boolean
45
+ /** 是否显示「返回上一页」按钮 */
46
+ showBack?: boolean
47
+ /** 首页地址(未提供 onhome 时作为链接跳转) */
48
+ homeUrl?: string
49
+ /** 自定义「返回首页」处理(优先于 homeUrl 链接) */
50
+ onhome?: () => void
51
+ /** 自定义「返回上一页」处理(默认 history.back()) */
52
+ onback?: () => void
53
+ /** 自定义图标插槽 */
54
+ icon?: Snippet
55
+ /** 自定义操作区插槽(提供时覆盖默认按钮) */
56
+ actions?: Snippet
57
+ class?: string
58
+ } = $props()
59
+
60
+ const preset = $derived(ERROR_PRESETS[resolveErrorPreset(status)])
61
+ const displayCode = $derived(code ?? preset.code)
62
+ const displayTitle = $derived(title ?? uiM(preset.titleKey))
63
+ const displayDesc = $derived(description ?? uiM(preset.descKey))
64
+
65
+ function handleBack() {
66
+ if (onback) {
67
+ onback()
68
+ return
69
+ }
70
+ if (typeof history !== 'undefined') {
71
+ history.back()
72
+ }
73
+ }
74
+
75
+ function handleHome() {
76
+ onhome?.()
77
+ }
78
+ </script>
79
+
80
+ <div class={cn('flex min-h-[60vh] w-full flex-col items-center justify-center px-6 py-12 text-center', className)}>
81
+ <!-- 图标 -->
82
+ <div class='mb-6 flex size-16 items-center justify-center rounded-2xl bg-base-content/5 text-base-content/40'>
83
+ {#if icon}
84
+ {@render icon()}
85
+ {:else}
86
+ <span class='{preset.icon} size-8'></span>
87
+ {/if}
88
+ </div>
89
+
90
+ <!-- 大号状态码 -->
91
+ <p class='text-6xl font-bold tracking-tight text-base-content/85 sm:text-7xl'>{displayCode}</p>
92
+
93
+ <!-- 标题 -->
94
+ <h1 class='mt-4 text-xl font-semibold text-base-content sm:text-2xl'>{displayTitle}</h1>
95
+
96
+ <!-- 描述 -->
97
+ <p class='mt-2 max-w-md text-sm text-base-content/55'>{displayDesc}</p>
98
+
99
+ <!-- 操作区 -->
100
+ <div class='mt-8 flex flex-wrap items-center justify-center gap-3'>
101
+ {#if actions}
102
+ {@render actions()}
103
+ {:else}
104
+ {#if showBack}
105
+ <Button variant='outline' size='sm' onclick={handleBack}>
106
+ <span class='icon-[tabler--arrow-left] mr-1.5 size-4'></span>
107
+ {uiM('error_go_back')}
108
+ </Button>
109
+ {/if}
110
+ {#if showHome}
111
+ {#if onhome}
112
+ <Button variant='primary' size='sm' onclick={handleHome}>
113
+ <span class='icon-[tabler--home] mr-1.5 size-4'></span>
114
+ {uiM('error_back_home')}
115
+ </Button>
116
+ {:else}
117
+ <a href={homeUrl} class='btn btn-primary btn-sm'>
118
+ <span class='icon-[tabler--home] mr-1.5 size-4'></span>
119
+ {uiM('error_back_home')}
120
+ </a>
121
+ {/if}
122
+ {/if}
123
+ {/if}
124
+ </div>
125
+ </div>
@@ -0,0 +1,42 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { ErrorPreset } from './error-presets.js';
3
+ type $$ComponentProps = {
4
+ /** HTTP 状态码或预设标识;未命中预设时回退到最接近的预设 */
5
+ status?: number | string | ErrorPreset;
6
+ /** 自定义大号展示码(覆盖预设) */
7
+ code?: string;
8
+ /** 自定义标题(覆盖预设文案) */
9
+ title?: string;
10
+ /** 自定义描述(覆盖预设文案) */
11
+ description?: string;
12
+ /** 是否显示「返回首页」按钮 */
13
+ showHome?: boolean;
14
+ /** 是否显示「返回上一页」按钮 */
15
+ showBack?: boolean;
16
+ /** 首页地址(未提供 onhome 时作为链接跳转) */
17
+ homeUrl?: string;
18
+ /** 自定义「返回首页」处理(优先于 homeUrl 链接) */
19
+ onhome?: () => void;
20
+ /** 自定义「返回上一页」处理(默认 history.back()) */
21
+ onback?: () => void;
22
+ /** 自定义图标插槽 */
23
+ icon?: Snippet;
24
+ /** 自定义操作区插槽(提供时覆盖默认按钮) */
25
+ actions?: Snippet;
26
+ class?: string;
27
+ };
28
+ /**
29
+ * ErrorPage
30
+ * 通用错误页场景组件
31
+ *
32
+ * 展示大号状态码 + 标题 + 描述 + 操作按钮,内置 401/403/404/500/503 预设。
33
+ * 借鉴 shadcn-admin 错误页布局,复用现有主题 token。
34
+ *
35
+ * 使用 Svelte 5 Runes ($props, $derived)
36
+ *
37
+ * @example
38
+ * <ErrorPage status={403} homeUrl="/admin" onback={() => history.back()} />
39
+ */
40
+ declare const ErrorPage: import("svelte").Component<$$ComponentProps, {}, "">;
41
+ type ErrorPage = ReturnType<typeof ErrorPage>;
42
+ export default ErrorPage;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @h-ai/ui — 错误页预设
3
+ *
4
+ * 为 ErrorPage 提供常见 HTTP 错误的内置文案 key 与图标。
5
+ * @module error-presets
6
+ */
7
+ import type { UIMessageKey } from '../../../messages.js';
8
+ /** 内置错误预设标识 */
9
+ export type ErrorPreset = '401' | '403' | '404' | '500' | '503';
10
+ /** 单个错误预设的展示信息 */
11
+ export interface ErrorPresetInfo {
12
+ /** HTTP 状态码(用于大号展示) */
13
+ code: string;
14
+ /** 标题文案 key */
15
+ titleKey: UIMessageKey;
16
+ /** 描述文案 key */
17
+ descKey: UIMessageKey;
18
+ /** Tabler 图标类名 */
19
+ icon: string;
20
+ }
21
+ /** 错误预设映射表 */
22
+ export declare const ERROR_PRESETS: Record<ErrorPreset, ErrorPresetInfo>;
23
+ /** 根据 HTTP 状态码解析到最接近的预设 */
24
+ export declare function resolveErrorPreset(status: number | string | undefined): ErrorPreset;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @h-ai/ui — 错误页预设
3
+ *
4
+ * 为 ErrorPage 提供常见 HTTP 错误的内置文案 key 与图标。
5
+ * @module error-presets
6
+ */
7
+ /** 错误预设映射表 */
8
+ export const ERROR_PRESETS = {
9
+ 401: { code: '401', titleKey: 'error_401_title', descKey: 'error_401_desc', icon: 'icon-[tabler--lock]' },
10
+ 403: { code: '403', titleKey: 'error_403_title', descKey: 'error_403_desc', icon: 'icon-[tabler--shield-lock]' },
11
+ 404: { code: '404', titleKey: 'error_404_title', descKey: 'error_404_desc', icon: 'icon-[tabler--map-search]' },
12
+ 500: { code: '500', titleKey: 'error_500_title', descKey: 'error_500_desc', icon: 'icon-[tabler--server-off]' },
13
+ 503: { code: '503', titleKey: 'error_503_title', descKey: 'error_503_desc', icon: 'icon-[tabler--tool]' },
14
+ };
15
+ /** 根据 HTTP 状态码解析到最接近的预设 */
16
+ export function resolveErrorPreset(status) {
17
+ const code = String(status ?? '');
18
+ if (code in ERROR_PRESETS) {
19
+ return code;
20
+ }
21
+ const num = Number(code);
22
+ if (num === 401)
23
+ return '401';
24
+ if (num === 403)
25
+ return '403';
26
+ if (num === 503)
27
+ return '503';
28
+ if (num >= 500)
29
+ return '500';
30
+ return '404';
31
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @h-ai/ui — 错误页场景组件 (Error Scenes)
3
+ *
4
+ * 通用错误页展示,内置常见 HTTP 错误预设。
5
+ * @module index
6
+ */
7
+ export * from './error-presets.js';
8
+ export { default as ErrorPage } from './ErrorPage.svelte';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @h-ai/ui — 错误页场景组件 (Error Scenes)
3
+ *
4
+ * 通用错误页展示,内置常见 HTTP 错误预设。
5
+ * @module index
6
+ */
7
+ export * from './error-presets.js';
8
+ export { default as ErrorPage } from './ErrorPage.svelte';