@h-ai/ui 0.1.0-alpha.13 → 0.1.0-alpha.16
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 +20 -4
- package/dist/components/compounds/BottomNav.svelte +21 -1
- package/dist/components/compounds/BottomNav.svelte.d.ts +5 -0
- package/dist/components/scenes/ai/AiDocumentDownloadMenu.svelte +1 -0
- package/dist/components/scenes/ai/AiDocumentEditor.svelte +3 -1
- package/dist/components/scenes/ai/MarkdownRenderer.svelte +0 -4
- package/dist/components/scenes/ai/document-types.d.ts +2 -0
- package/dist/components/scenes/crud/CrudFilterBar.svelte +1 -1
- package/dist/components/scenes/crud/CrudPage.svelte +27 -22
- package/dist/components/scenes/crud/CrudPage.svelte.d.ts +22 -15
- package/dist/components/scenes/crud/index.d.ts +2 -0
- package/dist/components/scenes/crud/index.js +1 -0
- package/dist/components/scenes/crud/nav-adapter.d.ts +36 -0
- package/dist/components/scenes/crud/nav-adapter.js +31 -0
- package/dist/components/scenes/crypto/EncryptedInput.svelte +2 -4
- package/dist/components/scenes/crypto/EncryptedInput.svelte.d.ts +1 -1
- package/dist/components/scenes/crypto/HashDisplay.svelte +1 -2
- package/dist/components/scenes/crypto/SignatureDisplay.svelte +1 -3
- package/dist/components/scenes/iam/LoginForm.svelte +2 -0
- package/dist/components/scenes/iam/PasswordInput.svelte +4 -0
- package/dist/components/scenes/iam/PasswordInput.svelte.d.ts +2 -0
- package/dist/components/scenes/iam/RegisterForm.svelte +2 -0
- package/dist/components/scenes/iam/UserProfile.svelte +7 -1
- package/dist/components/scenes/types.d.ts +5 -3
- package/dist/styles/mobile.css +28 -0
- package/package.json +3 -3
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
|
|
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
|
}
|
|
@@ -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={
|
|
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 {
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
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?:
|
|
30
|
+
algorithm?: 'SM2' | 'SM4'
|
|
33
31
|
/** 自定义类名 */
|
|
34
32
|
class?: string
|
|
35
33
|
/** 加密函数 */
|
|
@@ -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/
|
|
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/
|
|
16
|
-
|
|
17
|
-
import IconButton from '../../primitives/IconButton.svelte'
|
|
15
|
+
import { Badge, IconButton } from '../../primitives/index.js'
|
|
18
16
|
|
|
19
17
|
const {
|
|
20
18
|
signature = '',
|
|
@@ -90,6 +90,7 @@
|
|
|
90
90
|
type='text'
|
|
91
91
|
placeholder={uiM('login_username_placeholder')}
|
|
92
92
|
bind:value={username}
|
|
93
|
+
autocomplete='username'
|
|
93
94
|
{disabled}
|
|
94
95
|
required
|
|
95
96
|
error={errors.username}
|
|
@@ -104,6 +105,7 @@
|
|
|
104
105
|
<PasswordInput
|
|
105
106
|
bind:value={password}
|
|
106
107
|
placeholder={uiM('login_password_placeholder')}
|
|
108
|
+
autocomplete='current-password'
|
|
107
109
|
{disabled}
|
|
108
110
|
required
|
|
109
111
|
error={errors.password}
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
id?: string
|
|
22
22
|
/** 值 */
|
|
23
23
|
value?: string
|
|
24
|
+
/** autocomplete */
|
|
25
|
+
autocomplete?: string
|
|
24
26
|
/** 占位符 */
|
|
25
27
|
placeholder?: string
|
|
26
28
|
/** 尺寸 */
|
|
@@ -54,6 +56,7 @@
|
|
|
54
56
|
let {
|
|
55
57
|
id,
|
|
56
58
|
value = $bindable(''),
|
|
59
|
+
autocomplete,
|
|
57
60
|
placeholder = '',
|
|
58
61
|
size = 'md',
|
|
59
62
|
disabled = false,
|
|
@@ -162,6 +165,7 @@
|
|
|
162
165
|
bind:value
|
|
163
166
|
bind:inputRef={inputRef}
|
|
164
167
|
{placeholder}
|
|
168
|
+
{autocomplete}
|
|
165
169
|
{disabled}
|
|
166
170
|
{readonly}
|
|
167
171
|
{required}
|
|
@@ -175,6 +175,7 @@
|
|
|
175
175
|
id='register-password'
|
|
176
176
|
bind:value={password}
|
|
177
177
|
placeholder={getFieldPlaceholder('password')}
|
|
178
|
+
autocomplete='new-password'
|
|
178
179
|
{disabled}
|
|
179
180
|
required
|
|
180
181
|
showStrength={showPasswordStrength}
|
|
@@ -193,6 +194,7 @@
|
|
|
193
194
|
id='register-confirm-password'
|
|
194
195
|
bind:value={confirmPassword}
|
|
195
196
|
placeholder={getFieldPlaceholder('confirmPassword')}
|
|
197
|
+
autocomplete='new-password'
|
|
196
198
|
{disabled}
|
|
197
199
|
required
|
|
198
200
|
showStrength={false}
|
|
@@ -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
|
-
|
|
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>
|
|
@@ -14,6 +14,8 @@ export interface PasswordInputProps {
|
|
|
14
14
|
id?: string;
|
|
15
15
|
/** 值 */
|
|
16
16
|
value?: string;
|
|
17
|
+
/** autocomplete */
|
|
18
|
+
autocomplete?: string;
|
|
17
19
|
/** 占位符 */
|
|
18
20
|
placeholder?: string;
|
|
19
21
|
/** 尺寸 */
|
|
@@ -499,7 +501,7 @@ export interface EncryptedInputProps {
|
|
|
499
501
|
/** 是否显示加密结果 */
|
|
500
502
|
showEncrypted?: boolean;
|
|
501
503
|
/** 加密算法 */
|
|
502
|
-
algorithm?: 'SM2' | 'SM4'
|
|
504
|
+
algorithm?: 'SM2' | 'SM4';
|
|
503
505
|
/** 加密函数 */
|
|
504
506
|
onencrypt?: (value: string) => Promise<string>;
|
|
505
507
|
/** 自定义类名 */
|
|
@@ -520,7 +522,7 @@ export interface HashDisplayProps {
|
|
|
520
522
|
/** 哈希值 */
|
|
521
523
|
value?: string;
|
|
522
524
|
/** 算法 */
|
|
523
|
-
algorithm?: 'SM3'
|
|
525
|
+
algorithm?: 'SM3';
|
|
524
526
|
/** 标签 */
|
|
525
527
|
label?: string;
|
|
526
528
|
/** 是否可复制 */
|
|
@@ -560,7 +562,7 @@ export interface SignatureDisplayProps {
|
|
|
560
562
|
/** 公钥 */
|
|
561
563
|
publicKey?: string;
|
|
562
564
|
/** 算法 */
|
|
563
|
-
algorithm?: 'SM2'
|
|
565
|
+
algorithm?: 'SM2';
|
|
564
566
|
/** 验证状态 */
|
|
565
567
|
verified?: boolean;
|
|
566
568
|
/** 是否显示公钥 */
|
package/dist/styles/mobile.css
CHANGED
|
@@ -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.
|
|
4
|
+
"version": "0.1.0-alpha.16",
|
|
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.
|
|
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.
|
|
47
|
+
"@h-ai/core": "0.1.0-alpha.16"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@sveltejs/package": "^2.5.7",
|