@h-ai/ui 0.1.0-alpha.13 → 0.1.0-alpha.15

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.md CHANGED
@@ -49,7 +49,8 @@ export default defineConfig({
49
49
  @import 'tailwindcss';
50
50
  @import '@h-ai/ui/styles/global.css';
51
51
  @import '@h-ai/ui/styles/theme.css';
52
- @source "../node_modules/@h-ai/ui/dist/**/*.{svelte,ts}";
52
+ @source "../node_modules/@h-ai/ui/dist/**/*.{svelte,js,ts}";
53
+ @source "../../../node_modules/@h-ai/ui/dist/**/*.{svelte,js,ts}";
53
54
 
54
55
  @plugin "daisyui" {
55
56
  themes: light --default, dark --prefersdark, cupcake, emerald, corporate, nord, dracula, night;
@@ -61,7 +62,7 @@ export default defineConfig({
61
62
  }
62
63
  ```
63
64
 
64
- > `@source` 让 TailwindCSS 扫描 `@h-ai/ui` 组件中使用的 class 名,否则组件样式会丢失。
65
+ > `@source` 让 TailwindCSS 扫描 `@h-ai/ui` 组件中使用的 class 名,否则组件样式会丢失。嵌套应用位于 monorepo 中时,依赖包可能被提升到工作区根 `node_modules`,所以建议同时保留两条路径。
65
66
 
66
67
  ### 4. 使用组件
67
68
 
@@ -236,6 +237,8 @@ components/
236
237
  | `HashDisplay` | 哈希展示 | `value`, `algorithm`, `copyable`, `truncate` |
237
238
  | `SignatureDisplay` | 签名展示 | `signature`, `publicKey`, `algorithm`, `verified` |
238
239
 
240
+ 默认算法与 `@h-ai/crypto` 对齐:加密输入使用 `SM4`,哈希展示使用 `SM3`,签名展示使用 `SM2`。
241
+
239
242
  ## 使用示例
240
243
 
241
244
  ### Toast 通知
@@ -376,7 +379,8 @@ components/
376
379
  @import 'tailwindcss';
377
380
  @import '@h-ai/ui/styles/global.css'; /* 基础重置、滚动条、焦点样式 */
378
381
  @import '@h-ai/ui/styles/theme.css'; /* Tailwind v4 @theme Token(品牌色/阴影/动效) */
379
- @source "../node_modules/@h-ai/ui/dist/**/*.{svelte,ts}";
382
+ @source "../node_modules/@h-ai/ui/dist/**/*.{svelte,js,ts}";
383
+ @source "../../../node_modules/@h-ai/ui/dist/**/*.{svelte,js,ts}";
380
384
 
381
385
  /* 移动端项目追加(可选) */
382
386
  @import '@h-ai/ui/styles/design-tokens.css'; /* CSS 自定义属性 */
@@ -391,7 +395,19 @@ components/
391
395
  - `global.css`:基础 HTML 重置、滚动条美化、表单焦点环
392
396
  - `theme.css`:Tailwind v4 `@theme` 块,包含品牌色、阴影层级、动效曲线、字体特性
393
397
  - `design-tokens.css`:CSS 自定义属性(间距/圆角/z-index/过渡),移动端推荐
394
- - `mobile.css`:安全区域 padding、momentum 滚动、虚拟键盘适配
398
+ - `mobile.css`:安全区域 padding、momentum 滚动、虚拟键盘适配、移动端 App 壳布局工具(`hai-mobile-viewport` / `hai-mobile-shell` / `hai-mobile-main`)
399
+
400
+ 移动端页面推荐让 `BottomNav` 与页面壳宽度一致,避免桌面预览时底栏横跨整个视口:
401
+
402
+ ```svelte
403
+ <div class="hai-mobile-viewport">
404
+ <div class="hai-mobile-shell max-w-lg">
405
+ <AppBar title="Home" fixed={false} safeArea />
406
+ <main class="hai-mobile-main">...</main>
407
+ <BottomNav items={items} active="home" centered maxWidth="lg" safeArea />
408
+ </div>
409
+ </div>
410
+ ```
395
411
 
396
412
  ### 图标
397
413
 
@@ -11,6 +11,16 @@
11
11
  // eslint-disable-next-line no-import-assign -- type re-export
12
12
  export type { BottomNavItem }
13
13
 
14
+ type BottomNavMaxWidth = 'sm' | 'md' | 'lg' | 'xl' | 'none'
15
+
16
+ const MAX_WIDTH_CLASS: Record<BottomNavMaxWidth, string> = {
17
+ sm: 'max-w-sm',
18
+ md: 'max-w-md',
19
+ lg: 'max-w-lg',
20
+ xl: 'max-w-xl',
21
+ none: '',
22
+ }
23
+
14
24
  interface Props {
15
25
  /** 导航项列表 */
16
26
  items: BottomNavItem[]
@@ -20,6 +30,10 @@
20
30
  onchange?: (id: string) => void
21
31
  /** 是否适配底部安全区域 */
22
32
  safeArea?: boolean
33
+ /** 是否将 fixed 导航居中,适用于移动端仿真器/桌面预览中的窄屏壳 */
34
+ centered?: boolean
35
+ /** fixed 导航最大宽度,centered=true 时生效 */
36
+ maxWidth?: BottomNavMaxWidth
23
37
  /** 额外 CSS 类 */
24
38
  class?: string
25
39
  }
@@ -29,13 +43,19 @@
29
43
  active,
30
44
  onchange,
31
45
  safeArea = true,
46
+ centered = false,
47
+ maxWidth = 'none',
32
48
  class: className,
33
49
  }: Props = $props()
50
+
51
+ const constrained = $derived(centered && maxWidth !== 'none')
34
52
  </script>
35
53
 
36
54
  <nav
37
55
  class={cn(
38
- 'fixed bottom-0 left-0 right-0 bg-base-100 border-t border-base-200 z-40',
56
+ 'fixed bottom-0 bg-base-100 border-t border-base-200 z-40',
57
+ constrained ? 'left-1/2 right-auto w-full -translate-x-1/2' : 'left-0 right-0',
58
+ constrained && MAX_WIDTH_CLASS[maxWidth],
39
59
  safeArea && 'hai-safe-bottom',
40
60
  className,
41
61
  )}
@@ -1,4 +1,5 @@
1
1
  import type { BottomNavItem } from './bottom-nav-types.js';
2
+ type BottomNavMaxWidth = 'sm' | 'md' | 'lg' | 'xl' | 'none';
2
3
  interface Props {
3
4
  /** 导航项列表 */
4
5
  items: BottomNavItem[];
@@ -8,6 +9,10 @@ interface Props {
8
9
  onchange?: (id: string) => void;
9
10
  /** 是否适配底部安全区域 */
10
11
  safeArea?: boolean;
12
+ /** 是否将 fixed 导航居中,适用于移动端仿真器/桌面预览中的窄屏壳 */
13
+ centered?: boolean;
14
+ /** fixed 导航最大宽度,centered=true 时生效 */
15
+ maxWidth?: BottomNavMaxWidth;
11
16
  /** 额外 CSS 类 */
12
17
  class?: string;
13
18
  }
@@ -172,6 +172,7 @@
172
172
  tabindex='-1'
173
173
  onmousedown={event => event.preventDefault()}
174
174
  onclick={event => event.stopPropagation()}
175
+ onkeydown={event => event.stopPropagation()}
175
176
  >
176
177
  {#each resolvedActions as action}
177
178
  <button
@@ -249,6 +249,8 @@
249
249
  breaks = true,
250
250
  // 是否显示左侧目录。
251
251
  showOutline = true,
252
+ // 是否显示目录项的序号前缀。
253
+ showOutlineNumbering = false,
252
254
  // 是否显示顶部工具栏。
253
255
  showToolbar = true,
254
256
  // 目录面板初始是否折叠。
@@ -2567,7 +2569,7 @@ ${safeCode}
2567
2569
  style={`padding-left:${0.65 + (item.level - 1) * 0.56}rem`}
2568
2570
  onclick={() => scrollToHeading(item.id)}
2569
2571
  >
2570
- {item.numberedTitle}
2572
+ {showOutlineNumbering ? item.numberedTitle : item.text}
2571
2573
  </button>
2572
2574
  {/each}
2573
2575
  </nav>
@@ -33,9 +33,6 @@
33
33
  parseMarkdown(content, { enableHighlight, showCopyButton, breaks }),
34
34
  )
35
35
 
36
- /** 容器引用 */
37
- let containerEl: HTMLDivElement | undefined = $state()
38
-
39
36
  /**
40
37
  * 事件代理:处理代码块复制按钮点击
41
38
  */
@@ -66,7 +63,6 @@
66
63
  <!-- svelte-ignore a11y_click_events_have_key_events -->
67
64
  <!-- svelte-ignore a11y_no_static_element_interactions -->
68
65
  <div
69
- bind:this={containerEl}
70
66
  class={cn('hai-markdown', className)}
71
67
  onclick={handleClick}
72
68
  >
@@ -144,6 +144,8 @@ export interface AiDocumentEditorProps {
144
144
  breaks?: boolean;
145
145
  /** Whether to show the left outline panel. */
146
146
  showOutline?: boolean;
147
+ /** Whether to show numbering prefixes in outline items. */
148
+ showOutlineNumbering?: boolean;
147
149
  /** Whether to show the top toolbar. */
148
150
  showToolbar?: boolean;
149
151
  /** Initial outline collapsed state. */
@@ -79,7 +79,7 @@
79
79
  <Select
80
80
  class='min-w-32'
81
81
  value={String(filterValues[field.id] ?? '')}
82
- onchange={e => handleFilterChange(field.id, (e.target as HTMLSelectElement).value)}
82
+ onchange={value => handleFilterChange(field.id, value)}
83
83
  >
84
84
  <option value="">{uiM('crud_filter_all')}</option>
85
85
  {#each opts as opt}
@@ -13,8 +13,7 @@
13
13
  <script lang='ts'>
14
14
  import type { Snippet } from 'svelte'
15
15
  import type { Size } from '../../../types.js'
16
- import { goto, invalidateAll } from '$app/navigation'
17
- import { page } from '$app/state'
16
+ import type { NavAdapter } from './nav-adapter.js'
18
17
  import { uiM } from '../../../messages.js'
19
18
  import Card from '../../compounds/Card.svelte'
20
19
  import DataTable from '../../compounds/DataTable.svelte'
@@ -26,6 +25,7 @@
26
25
  import CrudDetailDrawer from './CrudDetailDrawer.svelte'
27
26
  import CrudEditDrawer from './CrudEditDrawer.svelte'
28
27
  import CrudFilterBar from './CrudFilterBar.svelte'
28
+ import { createBrowserNavAdapter } from './nav-adapter.js'
29
29
 
30
30
  // ─── 类型定义 ───
31
31
 
@@ -49,7 +49,15 @@
49
49
  order?: number
50
50
  }
51
51
 
52
- type AnyApiFunc = (...args: unknown[]) => Promise<Record<string, unknown> | unknown> | Record<string, unknown> | unknown
52
+ // 使用 method shorthand 语法(双向协变)以兼容来自 @h-ai/kit 的 CrudOperations 类型
53
+ // 详见:https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-6.html#strict-function-types
54
+ type CrudApi = {
55
+ list(params: Record<string, unknown>): Promise<{ items: Record<string, unknown>[], total: number, page: number, pageSize: number }>
56
+ get?(id: string): Promise<Record<string, unknown> | null>
57
+ create?(data: Record<string, unknown>): Promise<Record<string, unknown>>
58
+ update?(id: string, data: Record<string, unknown>): Promise<Record<string, unknown>>
59
+ remove?(id: string): Promise<void>
60
+ }
53
61
 
54
62
  type CrudDef = {
55
63
  resource: {
@@ -60,13 +68,7 @@
60
68
  defaultPageSize?: number
61
69
  searchable?: boolean
62
70
  searchPlaceholder?: string | (() => string)
63
- api: {
64
- list: AnyApiFunc
65
- get?: AnyApiFunc
66
- create?: AnyApiFunc
67
- update?: AnyApiFunc
68
- remove?: AnyApiFunc
69
- }
71
+ api: CrudApi
70
72
  }
71
73
  getListFields: () => FieldDef[]
72
74
  getFilterFields: () => FieldDef[]
@@ -75,13 +77,7 @@
75
77
  getDetailFields: () => FieldDef[]
76
78
  toTableColumns: () => Array<{ key: string, label: string, width?: string, align?: string, render?: (item: Record<string, unknown>) => string }>
77
79
  getDefaultValues: () => Record<string, unknown>
78
- api: {
79
- list: AnyApiFunc
80
- get?: AnyApiFunc
81
- create?: AnyApiFunc
82
- update?: AnyApiFunc
83
- remove?: AnyApiFunc
84
- }
80
+ api: CrudApi
85
81
  }
86
82
 
87
83
  // ─── Props ───
@@ -101,6 +97,7 @@
101
97
  onafterdelete,
102
98
  onerror,
103
99
  basePath = '',
100
+ nav = createBrowserNavAdapter(),
104
101
  class: className = '',
105
102
  }: {
106
103
  crud: CrudDef
@@ -117,6 +114,13 @@
117
114
  onafterdelete?: (item: Record<string, unknown>) => void
118
115
  onerror?: (error: string) => void
119
116
  basePath?: string
117
+ /**
118
+ * 路由适配器:用于读取当前 pathname、同步过滤参数到 URL、列表刷新。
119
+ * 默认使用 `createBrowserNavAdapter()`(基于 `globalThis.location`)。
120
+ * SvelteKit 应用应传入 `createSvelteKitNavAdapter()`(来自 `@h-ai/kit/client`),
121
+ * 以获得客户端跳转 + invalidateAll 体验。
122
+ */
123
+ nav?: NavAdapter
120
124
  class?: string
121
125
  } = $props()
122
126
 
@@ -144,7 +148,7 @@
144
148
  let filterValues = $state<Record<string, unknown>>({})
145
149
 
146
150
  // 基础路径
147
- const currentBasePath = $derived(basePath || page.url.pathname)
151
+ const currentBasePath = $derived(basePath || nav.pathname)
148
152
 
149
153
  // ─── 工具函数 ───
150
154
 
@@ -194,7 +198,8 @@
194
198
  }
195
199
 
196
200
  const qs = params.toString()
197
- goto(`${currentBasePath}${qs ? `?${qs}` : ''}`, { invalidateAll: true })
201
+ const url = `${currentBasePath}${qs ? `?${qs}` : ''}`
202
+ void Promise.resolve(nav.navigate(url)).then(() => nav.refresh?.())
198
203
  }
199
204
 
200
205
  function handleSearch(search: string) {
@@ -258,7 +263,7 @@
258
263
  const submitted = (result ?? {}) as Record<string, unknown>
259
264
  closeDrawer()
260
265
  onaftersubmit?.(submitted, 'create')
261
- await invalidateAll()
266
+ await nav.refresh?.()
262
267
  }
263
268
  else if (drawerMode === 'edit' && crud.api.update && selectedItem) {
264
269
  const id = String(selectedItem[keyField])
@@ -266,7 +271,7 @@
266
271
  const submitted = (result ?? {}) as Record<string, unknown>
267
272
  closeDrawer()
268
273
  onaftersubmit?.(submitted, 'edit')
269
- await invalidateAll()
274
+ await nav.refresh?.()
270
275
  }
271
276
  }
272
277
  catch (e) {
@@ -311,7 +316,7 @@
311
316
  closeDrawer()
312
317
  }
313
318
  onafterdelete?.(deleted)
314
- await invalidateAll()
319
+ await nav.refresh?.()
315
320
  }
316
321
  catch (e) {
317
322
  const msg = e instanceof Error ? e.message : uiM('crud_delete_failed')
@@ -1,5 +1,6 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { Size } from '../../../types.js';
3
+ import type { NavAdapter } from './nav-adapter.js';
3
4
  type FieldDef = {
4
5
  id: string;
5
6
  label: string | (() => string);
@@ -31,7 +32,18 @@ type FieldDef = {
31
32
  defaultValue?: unknown;
32
33
  order?: number;
33
34
  };
34
- type AnyApiFunc = (...args: unknown[]) => Promise<Record<string, unknown> | unknown> | Record<string, unknown> | unknown;
35
+ type CrudApi = {
36
+ list(params: Record<string, unknown>): Promise<{
37
+ items: Record<string, unknown>[];
38
+ total: number;
39
+ page: number;
40
+ pageSize: number;
41
+ }>;
42
+ get?(id: string): Promise<Record<string, unknown> | null>;
43
+ create?(data: Record<string, unknown>): Promise<Record<string, unknown>>;
44
+ update?(id: string, data: Record<string, unknown>): Promise<Record<string, unknown>>;
45
+ remove?(id: string): Promise<void>;
46
+ };
35
47
  type CrudDef = {
36
48
  resource: {
37
49
  name: string;
@@ -41,13 +53,7 @@ type CrudDef = {
41
53
  defaultPageSize?: number;
42
54
  searchable?: boolean;
43
55
  searchPlaceholder?: string | (() => string);
44
- api: {
45
- list: AnyApiFunc;
46
- get?: AnyApiFunc;
47
- create?: AnyApiFunc;
48
- update?: AnyApiFunc;
49
- remove?: AnyApiFunc;
50
- };
56
+ api: CrudApi;
51
57
  };
52
58
  getListFields: () => FieldDef[];
53
59
  getFilterFields: () => FieldDef[];
@@ -62,13 +68,7 @@ type CrudDef = {
62
68
  render?: (item: Record<string, unknown>) => string;
63
69
  }>;
64
70
  getDefaultValues: () => Record<string, unknown>;
65
- api: {
66
- list: AnyApiFunc;
67
- get?: AnyApiFunc;
68
- create?: AnyApiFunc;
69
- update?: AnyApiFunc;
70
- remove?: AnyApiFunc;
71
- };
71
+ api: CrudApi;
72
72
  };
73
73
  type $$ComponentProps = {
74
74
  crud: CrudDef;
@@ -95,6 +95,13 @@ type $$ComponentProps = {
95
95
  onafterdelete?: (item: Record<string, unknown>) => void;
96
96
  onerror?: (error: string) => void;
97
97
  basePath?: string;
98
+ /**
99
+ * 路由适配器:用于读取当前 pathname、同步过滤参数到 URL、列表刷新。
100
+ * 默认使用 `createBrowserNavAdapter()`(基于 `globalThis.location`)。
101
+ * SvelteKit 应用应传入 `createSvelteKitNavAdapter()`(来自 `@h-ai/kit/client`),
102
+ * 以获得客户端跳转 + invalidateAll 体验。
103
+ */
104
+ nav?: NavAdapter;
98
105
  class?: string;
99
106
  };
100
107
  /**
@@ -10,3 +10,5 @@ export { default as CrudDetailDrawer } from './CrudDetailDrawer.svelte';
10
10
  export { default as CrudEditDrawer } from './CrudEditDrawer.svelte';
11
11
  export { default as CrudFilterBar } from './CrudFilterBar.svelte';
12
12
  export { default as CrudPage } from './CrudPage.svelte';
13
+ export type { NavAdapter } from './nav-adapter.js';
14
+ export { createBrowserNavAdapter } from './nav-adapter.js';
@@ -9,3 +9,4 @@ export { default as CrudDetailDrawer } from './CrudDetailDrawer.svelte';
9
9
  export { default as CrudEditDrawer } from './CrudEditDrawer.svelte';
10
10
  export { default as CrudFilterBar } from './CrudFilterBar.svelte';
11
11
  export { default as CrudPage } from './CrudPage.svelte';
12
+ export { createBrowserNavAdapter } from './nav-adapter.js';
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @h-ai/ui — CRUD 导航适配器
3
+ *
4
+ * `CrudPage` 通过 `nav` prop 接入宿主路由系统,避免对任何具体框架(SvelteKit、
5
+ * 自实现 hash router 等)形成强依赖。
6
+ *
7
+ * - SvelteKit 应用:传入 `createSvelteKitNavAdapter()`(从 `@h-ai/kit/client` 导入)。
8
+ * - 纯浏览器 / Tauri / Capacitor:传入自定义实现或使用默认 `createBrowserNavAdapter()`。
9
+ *
10
+ * @module nav-adapter
11
+ */
12
+ /**
13
+ * 路由适配器接口。`CrudPage` 通过此接口完成:
14
+ * - 读取当前 pathname(作为列表 URL 的 basePath 兜底)
15
+ * - 调用 `navigate(url)` 同步搜索 / 过滤 / 分页参数到地址栏
16
+ * - 调用 `refresh()` 在写操作后重新拉取列表数据
17
+ */
18
+ export interface NavAdapter {
19
+ /** 当前页面路径(不含 query / hash),用于推导 `basePath`。 */
20
+ readonly pathname: string;
21
+ /** 跳转到指定 URL(可带 query),通常用于更新 URL 上的搜索/分页参数。 */
22
+ readonly navigate: (url: string) => void | Promise<void>;
23
+ /** 写操作完成后触发列表刷新;未提供时由调用方自行通过 `onaftersubmit` / `onafterdelete` 处理。 */
24
+ readonly refresh?: () => void | Promise<void>;
25
+ }
26
+ /**
27
+ * 创建基于浏览器 `globalThis.location` 的默认导航适配器。
28
+ *
29
+ * 适用场景:纯 SPA(无 SvelteKit)、Tauri、Capacitor 等可以直接读写 `location` 的环境。
30
+ *
31
+ * - `pathname` 读 `globalThis.location?.pathname`,SSR 下安全降级为 `''`。
32
+ * - `navigate(url)` 调 `globalThis.location.assign(url)`;如需客户端路由(hash router 等),
33
+ * 宿主应自行实现一个 `NavAdapter` 传入。
34
+ * - 不提供 `refresh`:浏览器默认导航本身会触发页面加载,调用方需在导航前/后重新拉取数据。
35
+ */
36
+ export declare function createBrowserNavAdapter(): NavAdapter;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @h-ai/ui — CRUD 导航适配器
3
+ *
4
+ * `CrudPage` 通过 `nav` prop 接入宿主路由系统,避免对任何具体框架(SvelteKit、
5
+ * 自实现 hash router 等)形成强依赖。
6
+ *
7
+ * - SvelteKit 应用:传入 `createSvelteKitNavAdapter()`(从 `@h-ai/kit/client` 导入)。
8
+ * - 纯浏览器 / Tauri / Capacitor:传入自定义实现或使用默认 `createBrowserNavAdapter()`。
9
+ *
10
+ * @module nav-adapter
11
+ */
12
+ /**
13
+ * 创建基于浏览器 `globalThis.location` 的默认导航适配器。
14
+ *
15
+ * 适用场景:纯 SPA(无 SvelteKit)、Tauri、Capacitor 等可以直接读写 `location` 的环境。
16
+ *
17
+ * - `pathname` 读 `globalThis.location?.pathname`,SSR 下安全降级为 `''`。
18
+ * - `navigate(url)` 调 `globalThis.location.assign(url)`;如需客户端路由(hash router 等),
19
+ * 宿主应自行实现一个 `NavAdapter` 传入。
20
+ * - 不提供 `refresh`:浏览器默认导航本身会触发页面加载,调用方需在导航前/后重新拉取数据。
21
+ */
22
+ export function createBrowserNavAdapter() {
23
+ return {
24
+ get pathname() {
25
+ return globalThis.location?.pathname ?? '';
26
+ },
27
+ navigate(url) {
28
+ globalThis.location?.assign(url);
29
+ },
30
+ };
31
+ }
@@ -11,9 +11,7 @@
11
11
  <script lang='ts'>
12
12
  import type { Size } from '../../../types.js'
13
13
  import { uiM } from '../../../messages.js'
14
- import BareButton from '../../primitives/BareButton.svelte'
15
- import IconButton from '../../primitives/IconButton.svelte'
16
- import Input from '../../primitives/Input.svelte'
14
+ import { BareButton, IconButton, Input } from '../../primitives/index.js'
17
15
 
18
16
  interface Props {
19
17
  /** 原始值 */
@@ -29,7 +27,7 @@
29
27
  /** 是否显示加密结果 */
30
28
  showEncrypted?: boolean
31
29
  /** 加密算法 */
32
- algorithm?: string
30
+ algorithm?: 'SM2' | 'SM4'
33
31
  /** 自定义类名 */
34
32
  class?: string
35
33
  /** 加密函数 */
@@ -13,7 +13,7 @@ interface Props {
13
13
  /** 是否显示加密结果 */
14
14
  showEncrypted?: boolean;
15
15
  /** 加密算法 */
16
- algorithm?: string;
16
+ algorithm?: 'SM2' | 'SM4';
17
17
  /** 自定义类名 */
18
18
  class?: string;
19
19
  /** 加密函数 */
@@ -12,8 +12,7 @@
12
12
  import type { HashDisplayProps } from '../types.js'
13
13
  import { uiM } from '../../../messages.js'
14
14
  import { cn } from '../../../utils.js'
15
- import Badge from '../../primitives/Badge.svelte'
16
- import IconButton from '../../primitives/IconButton.svelte'
15
+ import { Badge, IconButton } from '../../primitives/index.js'
17
16
 
18
17
  const {
19
18
  value = '',
@@ -12,9 +12,7 @@
12
12
  import type { SignatureDisplayProps } from '../types.js'
13
13
  import { uiM } from '../../../messages.js'
14
14
  import { cn } from '../../../utils.js'
15
- import Badge from '../../primitives/Badge.svelte'
16
-
17
- import IconButton from '../../primitives/IconButton.svelte'
15
+ import { Badge, IconButton } from '../../primitives/index.js'
18
16
 
19
17
  const {
20
18
  signature = '',
@@ -144,6 +144,7 @@
144
144
  const file = e.currentTarget.files?.[0]
145
145
  if (file) {
146
146
  onavatarchange?.(file)
147
+ e.currentTarget.value = ''
147
148
  }
148
149
  }
149
150
  </script>
@@ -170,9 +171,14 @@
170
171
  type='file'
171
172
  class='file-input file-input-sm w-full max-w-xs'
172
173
  accept='image/*'
174
+ disabled={loading}
173
175
  onchange={handleAvatarChange}
174
176
  />
175
- <p class='text-xs text-base-content/60 mt-1'>{uiM('user_profile_avatar_hint')}</p>
177
+ {#if errors.avatar}
178
+ <p class='text-xs text-error mt-1' role='alert'>{errors.avatar}</p>
179
+ {:else}
180
+ <p class='text-xs text-base-content/60 mt-1'>{uiM('user_profile_avatar_hint')}</p>
181
+ {/if}
176
182
  </div>
177
183
  {/if}
178
184
  </div>
@@ -499,7 +499,7 @@ export interface EncryptedInputProps {
499
499
  /** 是否显示加密结果 */
500
500
  showEncrypted?: boolean;
501
501
  /** 加密算法 */
502
- algorithm?: 'SM2' | 'SM4' | 'AES';
502
+ algorithm?: 'SM2' | 'SM4';
503
503
  /** 加密函数 */
504
504
  onencrypt?: (value: string) => Promise<string>;
505
505
  /** 自定义类名 */
@@ -520,7 +520,7 @@ export interface HashDisplayProps {
520
520
  /** 哈希值 */
521
521
  value?: string;
522
522
  /** 算法 */
523
- algorithm?: 'SM3' | 'SHA256' | 'MD5';
523
+ algorithm?: 'SM3';
524
524
  /** 标签 */
525
525
  label?: string;
526
526
  /** 是否可复制 */
@@ -560,7 +560,7 @@ export interface SignatureDisplayProps {
560
560
  /** 公钥 */
561
561
  publicKey?: string;
562
562
  /** 算法 */
563
- algorithm?: 'SM2' | 'RSA' | 'ECDSA';
563
+ algorithm?: 'SM2';
564
564
  /** 验证状态 */
565
565
  verified?: boolean;
566
566
  /** 是否显示公钥 */
@@ -103,3 +103,31 @@
103
103
  overflow-y: auto;
104
104
  -webkit-overflow-scrolling: touch;
105
105
  }
106
+
107
+ /* ─── 主流移动端 App 壳布局 ─── */
108
+ .hai-mobile-viewport {
109
+ min-height: 100dvh;
110
+ background:
111
+ radial-gradient(circle at top left, color-mix(in srgb, hsl(var(--p)) 12%, transparent), transparent 32rem),
112
+ hsl(var(--b2));
113
+ }
114
+
115
+ .hai-mobile-shell {
116
+ width: 100%;
117
+ min-height: 100dvh;
118
+ margin-inline: auto;
119
+ display: flex;
120
+ flex-direction: column;
121
+ background: hsl(var(--b1));
122
+ box-shadow:
123
+ 0 0 0 1px color-mix(in srgb, currentColor 6%, transparent),
124
+ 0 24px 72px rgb(15 23 42 / 10%);
125
+ }
126
+
127
+ .hai-mobile-main {
128
+ flex: 1 1 auto;
129
+ min-height: 0;
130
+ overflow-y: auto;
131
+ overscroll-behavior: contain;
132
+ -webkit-overflow-scrolling: touch;
133
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@h-ai/ui",
3
3
  "type": "module",
4
- "version": "0.1.0-alpha.13",
4
+ "version": "0.1.0-alpha.15",
5
5
  "description": "Hai Framework UI component library for Svelte 5 Runes.",
6
6
  "license": "Apache-2.0",
7
7
  "svelte": "./dist/index.js",
@@ -33,7 +33,7 @@
33
33
  "access": "public"
34
34
  },
35
35
  "peerDependencies": {
36
- "svelte": "^5.53.5"
36
+ "svelte": "^5.55.5"
37
37
  },
38
38
  "dependencies": {
39
39
  "@internationalized/date": "^3.8.0",
@@ -44,7 +44,7 @@
44
44
  "marked": "^18.0.0",
45
45
  "shiki": "^4.0.2",
46
46
  "tailwind-merge": "^3.5.0",
47
- "@h-ai/core": "0.1.0-alpha.13"
47
+ "@h-ai/core": "0.1.0-alpha.15"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@sveltejs/package": "^2.5.7",