@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.
- package/README.md +41 -11
- package/auto-import.d.ts +5 -0
- package/auto-import.js +4 -0
- package/dist/components/compounds/DataTable.svelte +81 -4
- package/dist/components/compounds/DataTable.svelte.d.ts +7 -0
- package/dist/components/compounds/Modal.svelte +123 -63
- package/dist/components/compounds/Pagination.svelte +72 -119
- package/dist/components/scenes/ai/markdown-parse.js +12 -4
- package/dist/components/scenes/ai/table/AiTableEditor.svelte +4 -2
- package/dist/components/scenes/app/SettingsLayout.svelte +111 -0
- package/dist/components/scenes/app/SettingsLayout.svelte.d.ts +41 -0
- package/dist/components/scenes/app/ThemeColorPicker.svelte +41 -0
- package/dist/components/scenes/app/index.d.ts +1 -0
- package/dist/components/scenes/app/index.js +1 -0
- package/dist/components/scenes/crud/CrudFilterBar.svelte +173 -35
- package/dist/components/scenes/crud/CrudFilterBar.svelte.d.ts +7 -1
- package/dist/components/scenes/crud/CrudPage.svelte +19 -15
- package/dist/components/scenes/error/ErrorPage.svelte +125 -0
- package/dist/components/scenes/error/ErrorPage.svelte.d.ts +42 -0
- package/dist/components/scenes/error/error-presets.d.ts +24 -0
- package/dist/components/scenes/error/error-presets.js +31 -0
- package/dist/components/scenes/error/index.d.ts +8 -0
- package/dist/components/scenes/error/index.js +8 -0
- package/dist/components/scenes/iam/AuthShell.svelte +138 -0
- package/dist/components/scenes/iam/AuthShell.svelte.d.ts +45 -0
- package/dist/components/scenes/iam/index.d.ts +1 -0
- package/dist/components/scenes/iam/index.js +2 -1
- package/dist/components/scenes/index.d.ts +1 -0
- package/dist/components/scenes/index.js +2 -0
- package/dist/messages/en-US.json +20 -1
- package/dist/messages/zh-CN.json +20 -1
- package/dist/styles/global.css +4 -1
- package/dist/theme-config.d.ts +60 -4
- package/dist/theme-config.js +77 -6
- package/dist/types.d.ts +6 -0
- package/package.json +2 -2
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<script lang='ts'>
|
|
11
11
|
import type { PaginationProps } from '../../types.js'
|
|
12
12
|
import { uiM } from '../../messages.js'
|
|
13
|
-
import { cn
|
|
13
|
+
import { cn } from '../../utils.js'
|
|
14
14
|
import BareButton from '../primitives/BareButton.svelte'
|
|
15
15
|
|
|
16
16
|
import Input from '../primitives/Input.svelte'
|
|
@@ -36,63 +36,10 @@
|
|
|
36
36
|
// 计算总页数(至少 1 页,保证分页栏在空数据时仍可渲染)
|
|
37
37
|
const totalPages = $derived(Math.max(1, Math.ceil(total / pageSize)))
|
|
38
38
|
|
|
39
|
-
// 生成页码列表
|
|
40
|
-
const pages = $derived(() => {
|
|
41
|
-
const result: (number | string)[] = []
|
|
42
|
-
const maxVisible = 7
|
|
43
|
-
|
|
44
|
-
if (totalPages <= maxVisible) {
|
|
45
|
-
for (let i = 1; i <= totalPages; i++) {
|
|
46
|
-
result.push(i)
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
// 始终显示第一页
|
|
51
|
-
result.push(1)
|
|
52
|
-
|
|
53
|
-
if (page > 3) {
|
|
54
|
-
result.push('...')
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// 中间页码
|
|
58
|
-
const start = Math.max(2, page - 1)
|
|
59
|
-
const end = Math.min(totalPages - 1, page + 1)
|
|
60
|
-
|
|
61
|
-
for (let i = start; i <= end; i++) {
|
|
62
|
-
result.push(i)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (page < totalPages - 2) {
|
|
66
|
-
result.push('...')
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// 始终显示最后一页
|
|
70
|
-
result.push(totalPages)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return result
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
const joinClass = $derived(
|
|
77
|
-
cn(
|
|
78
|
-
'join',
|
|
79
|
-
className,
|
|
80
|
-
),
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
const btnClass = $derived(
|
|
84
|
-
cn(
|
|
85
|
-
'join-item btn',
|
|
86
|
-
getSizeClass(size),
|
|
87
|
-
),
|
|
88
|
-
)
|
|
89
|
-
|
|
90
39
|
const controlSize = $derived(size === 'xs' ? 'xs' : 'sm')
|
|
91
40
|
const jumperControlSize = 'xs'
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
const sizeChangerClass = $derived(size === 'xs' ? 'w-22 shrink-0' : 'w-24 shrink-0')
|
|
95
|
-
const jumperWrapClass = $derived(size === 'xs' ? 'flex min-w-fit items-center gap-1.5 whitespace-nowrap' : 'flex min-w-fit items-center gap-1.5 whitespace-nowrap')
|
|
41
|
+
const sizeChangerClass = $derived(size === 'xs' ? 'w-17 shrink-0' : 'w-18 shrink-0')
|
|
42
|
+
const jumperWrapClass = $derived(size === 'xs' ? 'flex min-w-fit items-center gap-1.5 whitespace-nowrap' : 'flex min-w-fit items-center gap-2 whitespace-nowrap')
|
|
96
43
|
const jumperInputClass = $derived(
|
|
97
44
|
size === 'xs'
|
|
98
45
|
? 'w-[2.25rem] shrink-0 [&>.fieldset]:m-0 [&>.fieldset]:min-w-0'
|
|
@@ -122,77 +69,83 @@
|
|
|
122
69
|
onpagesizechange?.(next)
|
|
123
70
|
}
|
|
124
71
|
}
|
|
72
|
+
|
|
73
|
+
// 统一 table 风格:首/上/下/末 导航按钮样式(等比例缩小)
|
|
74
|
+
const navBtnClass = $derived(
|
|
75
|
+
size === 'xs'
|
|
76
|
+
? 'inline-flex h-6 w-6 items-center justify-center rounded-md border border-base-content/12 text-base-content/55 transition-colors hover:bg-base-content/5 hover:text-base-content disabled:pointer-events-none disabled:opacity-40'
|
|
77
|
+
: 'inline-flex h-7 w-7 items-center justify-center rounded-md border border-base-content/12 text-base-content/55 transition-colors hover:bg-base-content/5 hover:text-base-content disabled:pointer-events-none disabled:opacity-40',
|
|
78
|
+
)
|
|
79
|
+
const navIconClass = $derived(size === 'xs' ? 'size-3.5' : 'size-3.5')
|
|
80
|
+
const tableTextClass = $derived(size === 'xs' ? 'text-xs text-base-content/60' : 'text-sm text-base-content/60')
|
|
81
|
+
const rowGapClass = $derived(size === 'xs' ? 'flex flex-wrap items-center gap-3 sm:gap-4' : 'flex flex-wrap items-center gap-4 sm:gap-6')
|
|
82
|
+
const pageInfoText = $derived(
|
|
83
|
+
(labels.pageInfo ?? uiM('pagination_page_info'))
|
|
84
|
+
.replace('{page}', String(page))
|
|
85
|
+
.replace('{total}', String(totalPages)),
|
|
86
|
+
)
|
|
125
87
|
</script>
|
|
126
88
|
|
|
127
|
-
<div class={
|
|
89
|
+
<div class={cn('flex flex-wrap items-center justify-between gap-4', className)}>
|
|
128
90
|
{#if showTotal}
|
|
129
|
-
<span class={
|
|
91
|
+
<span class={tableTextClass}>
|
|
130
92
|
{(labels.total ?? uiM('pagination_total')).replace('{count}', String(total))}
|
|
131
93
|
</span>
|
|
94
|
+
{:else}
|
|
95
|
+
<span></span>
|
|
132
96
|
{/if}
|
|
133
97
|
|
|
134
|
-
{
|
|
135
|
-
|
|
136
|
-
<
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
98
|
+
<div class={rowGapClass}>
|
|
99
|
+
{#if showSizeChanger}
|
|
100
|
+
<div class='flex items-center gap-2'>
|
|
101
|
+
<span class={tableTextClass}>{labels.rowsPerPage ?? uiM('pagination_rows_per_page')}</span>
|
|
102
|
+
<div class={sizeChangerClass}>
|
|
103
|
+
<Select
|
|
104
|
+
size={controlSize}
|
|
105
|
+
value={String(pageSize)}
|
|
106
|
+
onchange={handlePageSizeChange}
|
|
107
|
+
>
|
|
108
|
+
{#each pageSizeOptions as opt (opt)}
|
|
109
|
+
<option value={String(opt)}>{opt}</option>
|
|
110
|
+
{/each}
|
|
111
|
+
</Select>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
{/if}
|
|
115
|
+
|
|
116
|
+
<span class={tableTextClass}>{pageInfoText}</span>
|
|
117
|
+
|
|
118
|
+
<div class='flex items-center gap-1'>
|
|
119
|
+
<BareButton class={navBtnClass} disabled={page === 1} onclick={() => goToPage(1)} ariaLabel='First page'>
|
|
120
|
+
<span class={cn('icon-[tabler--chevrons-left]', navIconClass)}></span>
|
|
121
|
+
</BareButton>
|
|
122
|
+
<BareButton class={navBtnClass} disabled={page === 1} onclick={() => goToPage(page - 1)} ariaLabel='Previous page'>
|
|
123
|
+
<span class={cn('icon-[tabler--chevron-left]', navIconClass)}></span>
|
|
124
|
+
</BareButton>
|
|
125
|
+
<BareButton class={navBtnClass} disabled={page === totalPages} onclick={() => goToPage(page + 1)} ariaLabel='Next page'>
|
|
126
|
+
<span class={cn('icon-[tabler--chevron-right]', navIconClass)}></span>
|
|
127
|
+
</BareButton>
|
|
128
|
+
<BareButton class={navBtnClass} disabled={page === totalPages} onclick={() => goToPage(totalPages)} ariaLabel='Last page'>
|
|
129
|
+
<span class={cn('icon-[tabler--chevrons-right]', navIconClass)}></span>
|
|
130
|
+
</BareButton>
|
|
147
131
|
</div>
|
|
148
|
-
{/if}
|
|
149
|
-
|
|
150
|
-
<div class={joinClass}>
|
|
151
|
-
<BareButton
|
|
152
|
-
class={btnClass}
|
|
153
|
-
disabled={page === 1}
|
|
154
|
-
onclick={() => goToPage(page - 1)}
|
|
155
|
-
>
|
|
156
|
-
«
|
|
157
|
-
</BareButton>
|
|
158
|
-
|
|
159
|
-
{#each pages() as p, index (typeof p === 'number' ? p : `ellipsis-${index}`)}
|
|
160
|
-
{#if typeof p === 'number'}
|
|
161
|
-
<BareButton
|
|
162
|
-
class={cn(btnClass, page === p && 'btn-active')}
|
|
163
|
-
onclick={() => goToPage(p)}
|
|
164
|
-
>
|
|
165
|
-
{p}
|
|
166
|
-
</BareButton>
|
|
167
|
-
{:else}
|
|
168
|
-
<BareButton class={cn(btnClass, 'btn-disabled')} disabled>...</BareButton>
|
|
169
|
-
{/if}
|
|
170
|
-
{/each}
|
|
171
|
-
|
|
172
|
-
<BareButton
|
|
173
|
-
class={btnClass}
|
|
174
|
-
disabled={page === totalPages}
|
|
175
|
-
onclick={() => goToPage(page + 1)}
|
|
176
|
-
>
|
|
177
|
-
»
|
|
178
|
-
</BareButton>
|
|
179
|
-
</div>
|
|
180
132
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
133
|
+
{#if showJumper}
|
|
134
|
+
<div class={jumperWrapClass}>
|
|
135
|
+
<span class={tableTextClass}>{labels.jumpTo ?? uiM('pagination_jump_to')}</span>
|
|
136
|
+
<div class={jumperInputClass}>
|
|
137
|
+
<Input
|
|
138
|
+
type='number'
|
|
139
|
+
size={jumperControlSize}
|
|
140
|
+
class='h-7 w-full rounded-md [&_input]:px-1.5 [&_input]:text-center [&_input]:text-[12px]'
|
|
141
|
+
min={1}
|
|
142
|
+
max={totalPages}
|
|
143
|
+
bind:value={jumperValue}
|
|
144
|
+
onkeydown={(e: KeyboardEvent & { currentTarget: HTMLInputElement }) => e.key === 'Enter' && handleJump()}
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
<span class={tableTextClass}>{labels.page ?? uiM('pagination_page')}</span>
|
|
194
148
|
</div>
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
{/if}
|
|
149
|
+
{/if}
|
|
150
|
+
</div>
|
|
198
151
|
</div>
|
|
@@ -24,6 +24,16 @@ function escapeHtml(text) {
|
|
|
24
24
|
.replace(/"/g, '"')
|
|
25
25
|
.replace(/'/g, ''');
|
|
26
26
|
}
|
|
27
|
+
function createTableCellAttrs(cell) {
|
|
28
|
+
const attrs = [];
|
|
29
|
+
if (cell.align) {
|
|
30
|
+
attrs.push(`style="text-align:${cell.align}"`);
|
|
31
|
+
}
|
|
32
|
+
if (cell.text.trim()) {
|
|
33
|
+
attrs.push(`title="${escapeHtml(cell.text)}"`);
|
|
34
|
+
}
|
|
35
|
+
return attrs.length > 0 ? ` ${attrs.join(' ')}` : '';
|
|
36
|
+
}
|
|
27
37
|
/**
|
|
28
38
|
* 创建自定义渲染器对象
|
|
29
39
|
*
|
|
@@ -91,15 +101,13 @@ function createRendererObject(options) {
|
|
|
91
101
|
table(token) {
|
|
92
102
|
let headerHtml = '';
|
|
93
103
|
for (const cell of token.header) {
|
|
94
|
-
|
|
95
|
-
headerHtml += `<th${align}>${this.parser.parseInline(cell.tokens)}</th>`;
|
|
104
|
+
headerHtml += `<th${createTableCellAttrs(cell)}>${this.parser.parseInline(cell.tokens)}</th>`;
|
|
96
105
|
}
|
|
97
106
|
let bodyHtml = '';
|
|
98
107
|
for (const row of token.rows) {
|
|
99
108
|
let rowHtml = '';
|
|
100
109
|
for (const cell of row) {
|
|
101
|
-
|
|
102
|
-
rowHtml += `<td${align}>${this.parser.parseInline(cell.tokens)}</td>`;
|
|
110
|
+
rowHtml += `<td${createTableCellAttrs(cell)}>${this.parser.parseInline(cell.tokens)}</td>`;
|
|
103
111
|
}
|
|
104
112
|
bodyHtml += `<tr>${rowHtml}</tr>`;
|
|
105
113
|
}
|
|
@@ -1097,7 +1097,7 @@
|
|
|
1097
1097
|
<thead>
|
|
1098
1098
|
<tr>
|
|
1099
1099
|
{#each tableColumns as column (column.key)}
|
|
1100
|
-
<th>{column.label}</th>
|
|
1100
|
+
<th title={column.label}>{column.label}</th>
|
|
1101
1101
|
{/each}
|
|
1102
1102
|
{#if editable}
|
|
1103
1103
|
<th class='hai-ai-table-action-head'>{uiM('data_table_actions')}</th>
|
|
@@ -1122,11 +1122,13 @@
|
|
|
1122
1122
|
ondrop={event => handleRowDrop(row.row_id, event)}
|
|
1123
1123
|
>
|
|
1124
1124
|
{#each tableColumns as column (column.key)}
|
|
1125
|
+
{@const cellInputValue = getCellInputValue(row, column.key)}
|
|
1125
1126
|
<td>
|
|
1126
1127
|
<input
|
|
1127
1128
|
class={resolveCellInputClass(column.type)}
|
|
1128
1129
|
type={column.type === 'number' ? 'number' : 'text'}
|
|
1129
|
-
value={
|
|
1130
|
+
value={cellInputValue}
|
|
1131
|
+
title={cellInputValue}
|
|
1130
1132
|
disabled={!editable}
|
|
1131
1133
|
oninput={event => handleCellInput(row.row_id, column, event)}
|
|
1132
1134
|
/>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component SettingsLayout
|
|
3
|
+
设置页布局场景组件
|
|
4
|
+
|
|
5
|
+
借鉴 shadcn-admin 设置页:顶部标题/描述 + 左侧分区导航 + 右侧内容区。
|
|
6
|
+
分区内容由使用方通过 children 提供,导航高亮由 active 控制。
|
|
7
|
+
|
|
8
|
+
使用 Svelte 5 Runes ($props, $derived)
|
|
9
|
+
|
|
10
|
+
@example
|
|
11
|
+
<SettingsLayout
|
|
12
|
+
title="设置"
|
|
13
|
+
description="管理你的账户与偏好"
|
|
14
|
+
sections={[{ id: 'appearance', label: '外观', icon: 'icon-[tabler--palette]' }]}
|
|
15
|
+
active={active}
|
|
16
|
+
onselect={(id) => active = id}
|
|
17
|
+
>
|
|
18
|
+
{#if active === 'appearance'}<AppearanceSection />{/if}
|
|
19
|
+
</SettingsLayout>
|
|
20
|
+
-->
|
|
21
|
+
<script lang='ts'>
|
|
22
|
+
import type { Snippet } from 'svelte'
|
|
23
|
+
import { cn } from '../../../utils.js'
|
|
24
|
+
import BareButton from '../../primitives/BareButton.svelte'
|
|
25
|
+
|
|
26
|
+
type SettingsSection = {
|
|
27
|
+
id: string
|
|
28
|
+
label: string
|
|
29
|
+
icon?: string
|
|
30
|
+
/** 提供时渲染为链接(SSR 友好),否则触发 onselect */
|
|
31
|
+
href?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const {
|
|
35
|
+
title = '',
|
|
36
|
+
description = '',
|
|
37
|
+
sections = [],
|
|
38
|
+
active = '',
|
|
39
|
+
onselect,
|
|
40
|
+
children,
|
|
41
|
+
headerActions,
|
|
42
|
+
class: className = '',
|
|
43
|
+
}: {
|
|
44
|
+
title?: string
|
|
45
|
+
description?: string
|
|
46
|
+
sections?: SettingsSection[]
|
|
47
|
+
active?: string
|
|
48
|
+
onselect?: (id: string) => void
|
|
49
|
+
children?: Snippet
|
|
50
|
+
headerActions?: Snippet
|
|
51
|
+
class?: string
|
|
52
|
+
} = $props()
|
|
53
|
+
|
|
54
|
+
function navItemClass(id: string): string {
|
|
55
|
+
return cn(
|
|
56
|
+
'flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
|
57
|
+
id === active
|
|
58
|
+
? 'bg-base-content/8 text-base-content'
|
|
59
|
+
: 'text-base-content/60 hover:bg-base-content/5 hover:text-base-content',
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<div class={cn('space-y-5', className)}>
|
|
65
|
+
<!-- 标题区 -->
|
|
66
|
+
{#if title || description || headerActions}
|
|
67
|
+
<div class='flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between'>
|
|
68
|
+
<div>
|
|
69
|
+
{#if title}
|
|
70
|
+
<h1 class='text-xl font-semibold tracking-tight'>{title}</h1>
|
|
71
|
+
{/if}
|
|
72
|
+
{#if description}
|
|
73
|
+
<p class='mt-0.5 text-sm text-base-content/45'>{description}</p>
|
|
74
|
+
{/if}
|
|
75
|
+
</div>
|
|
76
|
+
{#if headerActions}
|
|
77
|
+
<div class='flex items-center gap-2'>{@render headerActions()}</div>
|
|
78
|
+
{/if}
|
|
79
|
+
</div>
|
|
80
|
+
{/if}
|
|
81
|
+
|
|
82
|
+
<div class='grid gap-6 lg:grid-cols-[13rem_minmax(0,1fr)]'>
|
|
83
|
+
<!-- 左侧分区导航 -->
|
|
84
|
+
{#if sections.length > 0}
|
|
85
|
+
<aside class='lg:sticky lg:top-4 lg:self-start'>
|
|
86
|
+
<nav class='flex gap-1 overflow-x-auto lg:flex-col lg:overflow-visible'>
|
|
87
|
+
{#each sections as section (section.id)}
|
|
88
|
+
{#if section.href}
|
|
89
|
+
<a href={section.href} class={navItemClass(section.id)} aria-current={section.id === active ? 'page' : undefined}>
|
|
90
|
+
{#if section.icon}<span class='{section.icon} size-4 shrink-0'></span>{/if}
|
|
91
|
+
<span class='whitespace-nowrap'>{section.label}</span>
|
|
92
|
+
</a>
|
|
93
|
+
{:else}
|
|
94
|
+
<BareButton type='button' class={navItemClass(section.id)} onclick={() => onselect?.(section.id)}>
|
|
95
|
+
{#if section.icon}<span class='{section.icon} size-4 shrink-0'></span>{/if}
|
|
96
|
+
<span class='whitespace-nowrap'>{section.label}</span>
|
|
97
|
+
</BareButton>
|
|
98
|
+
{/if}
|
|
99
|
+
{/each}
|
|
100
|
+
</nav>
|
|
101
|
+
</aside>
|
|
102
|
+
{/if}
|
|
103
|
+
|
|
104
|
+
<!-- 内容区 -->
|
|
105
|
+
<div class='min-w-0 space-y-5'>
|
|
106
|
+
{#if children}
|
|
107
|
+
{@render children()}
|
|
108
|
+
{/if}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type SettingsSection = {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
icon?: string;
|
|
6
|
+
/** 提供时渲染为链接(SSR 友好),否则触发 onselect */
|
|
7
|
+
href?: string;
|
|
8
|
+
};
|
|
9
|
+
type $$ComponentProps = {
|
|
10
|
+
title?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
sections?: SettingsSection[];
|
|
13
|
+
active?: string;
|
|
14
|
+
onselect?: (id: string) => void;
|
|
15
|
+
children?: Snippet;
|
|
16
|
+
headerActions?: Snippet;
|
|
17
|
+
class?: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* SettingsLayout
|
|
21
|
+
* 设置页布局场景组件
|
|
22
|
+
*
|
|
23
|
+
* 借鉴 shadcn-admin 设置页:顶部标题/描述 + 左侧分区导航 + 右侧内容区。
|
|
24
|
+
* 分区内容由使用方通过 children 提供,导航高亮由 active 控制。
|
|
25
|
+
*
|
|
26
|
+
* 使用 Svelte 5 Runes ($props, $derived)
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* <SettingsLayout
|
|
30
|
+
* title="设置"
|
|
31
|
+
* description="管理你的账户与偏好"
|
|
32
|
+
* sections={[{ id: 'appearance', label: '外观', icon: 'icon-[tabler--palette]' }]}
|
|
33
|
+
* active={active}
|
|
34
|
+
* onselect={(id) => active = id}
|
|
35
|
+
* >
|
|
36
|
+
* {#if active === 'appearance'}<AppearanceSection />{/if}
|
|
37
|
+
* </SettingsLayout>
|
|
38
|
+
*/
|
|
39
|
+
declare const SettingsLayout: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
40
|
+
type SettingsLayout = ReturnType<typeof SettingsLayout>;
|
|
41
|
+
export default SettingsLayout;
|
|
@@ -163,3 +163,44 @@
|
|
|
163
163
|
/>
|
|
164
164
|
</div>
|
|
165
165
|
</div>
|
|
166
|
+
|
|
167
|
+
<style>
|
|
168
|
+
button {
|
|
169
|
+
border-color: var(--soft-border, color-mix(in oklab, var(--color-base-content) 12%, transparent));
|
|
170
|
+
background: var(--soft-surface, var(--color-base-100));
|
|
171
|
+
color: var(--soft-text-body, var(--color-base-content));
|
|
172
|
+
box-shadow: none;
|
|
173
|
+
font-weight: 500;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
button:hover:not(:disabled) {
|
|
177
|
+
border-color: color-mix(in oklab, var(--color-primary) 28%, var(--soft-border, transparent) 72%);
|
|
178
|
+
background: color-mix(
|
|
179
|
+
in oklab,
|
|
180
|
+
var(--soft-surface, var(--color-base-100)) 88%,
|
|
181
|
+
var(--color-base-200) 12%
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
button[aria-pressed='true'] {
|
|
186
|
+
border-color: color-mix(in oklab, var(--color-primary) 42%, var(--soft-border, transparent) 58%);
|
|
187
|
+
background: color-mix(
|
|
188
|
+
in oklab,
|
|
189
|
+
var(--color-primary) 10%,
|
|
190
|
+
var(--soft-surface, var(--color-base-100)) 90%
|
|
191
|
+
);
|
|
192
|
+
color: var(--color-primary);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
button:focus,
|
|
196
|
+
button:focus-visible {
|
|
197
|
+
outline: none;
|
|
198
|
+
border-color: color-mix(in oklab, var(--color-primary) 48%, var(--soft-border, transparent) 52%);
|
|
199
|
+
box-shadow: 0 0 0 4px color-mix(in oklab, var(--color-primary) 12%, transparent 88%);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
button > span:first-child {
|
|
203
|
+
border-color: color-mix(in oklab, var(--color-base-content) 10%, transparent 90%);
|
|
204
|
+
box-shadow: none;
|
|
205
|
+
}
|
|
206
|
+
</style>
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { default as FeedbackModal } from './FeedbackModal.svelte';
|
|
8
8
|
export { default as LanguageSwitch } from './LanguageSwitch.svelte';
|
|
9
|
+
export { default as SettingsLayout } from './SettingsLayout.svelte';
|
|
9
10
|
export { default as SettingsModal } from './SettingsModal.svelte';
|
|
10
11
|
export { default as ThemeColorPicker } from './ThemeColorPicker.svelte';
|
|
11
12
|
export { default as ThemeSelector } from './ThemeSelector.svelte';
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { default as FeedbackModal } from './FeedbackModal.svelte';
|
|
8
8
|
export { default as LanguageSwitch } from './LanguageSwitch.svelte';
|
|
9
|
+
export { default as SettingsLayout } from './SettingsLayout.svelte';
|
|
9
10
|
export { default as SettingsModal } from './SettingsModal.svelte';
|
|
10
11
|
export { default as ThemeColorPicker } from './ThemeColorPicker.svelte';
|
|
11
12
|
export { default as ThemeSelector } from './ThemeSelector.svelte';
|