@h-ai/ui 0.1.0-alpha.16 → 0.1.0-alpha.19
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 +3 -2
- package/auto-import.d.ts +1 -0
- package/auto-import.js +1 -0
- package/dist/components/compounds/Alert.svelte +1 -3
- package/dist/components/compounds/Breadcrumb.svelte +1 -1
- package/dist/components/compounds/Calendar.svelte +4 -4
- package/dist/components/compounds/DataTable.svelte +2 -2
- package/dist/components/compounds/DatePicker.svelte +5 -5
- package/dist/components/compounds/Drawer.svelte +3 -5
- package/dist/components/compounds/Dropdown.svelte +1 -1
- package/dist/components/compounds/Modal.svelte +11 -2
- package/dist/components/compounds/Pagination.svelte +1 -1
- package/dist/components/compounds/Tabs.svelte +1 -1
- package/dist/components/compounds/ToastContainer.svelte +1 -1
- package/dist/components/primitives/Range.svelte +1 -1
- package/dist/components/primitives/Rating.svelte +2 -2
- package/dist/components/primitives/Select.svelte +1 -1
- package/dist/components/scenes/ai/AiDocumentDownloadMenu.svelte +1 -1
- package/dist/components/scenes/ai/AiDocumentEditor.svelte +6 -6
- package/dist/components/scenes/ai/table/AiTableEditor.svelte +4 -3
- package/dist/components/scenes/app/LanguageSwitch.svelte +4 -1
- package/dist/components/scenes/app/LanguageSwitch.svelte.d.ts +2 -0
- package/dist/components/scenes/app/SettingsModal.svelte +1 -3
- package/dist/components/scenes/app/ThemeColorPicker.svelte +165 -0
- package/dist/components/scenes/app/ThemeColorPicker.svelte.d.ts +24 -0
- package/dist/components/scenes/app/ThemeSelector.svelte +5 -2
- package/dist/components/scenes/app/ThemeSelector.svelte.d.ts +2 -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/CrudDetailDrawer.svelte +2 -2
- package/dist/components/scenes/crud/CrudEditDrawer.svelte +3 -3
- package/dist/components/scenes/crud/CrudFilterBar.svelte +2 -2
- package/dist/components/scenes/crud/CrudPage.svelte +2 -1
- package/dist/components/scenes/crypto/EncryptedInput.svelte +3 -5
- package/dist/components/scenes/crypto/HashDisplay.svelte +9 -11
- package/dist/components/scenes/crypto/SignatureDisplay.svelte +18 -22
- package/dist/components/scenes/iam/ForgotPasswordForm.svelte +11 -2
- package/dist/components/scenes/iam/RegisterForm.svelte +13 -4
- package/dist/components/scenes/storage/FileUpload.svelte +6 -10
- package/dist/components/scenes/storage/ImageUpload.svelte +7 -16
- package/dist/messages/en-US.json +5 -0
- package/dist/messages/zh-CN.json +5 -0
- package/dist/styles/theme.css +8 -1
- package/dist/theme-config.d.ts +21 -0
- package/dist/theme-config.js +16 -0
- package/dist/types.d.ts +2 -0
- package/package.json +11 -11
package/README.md
CHANGED
|
@@ -196,15 +196,16 @@ components/
|
|
|
196
196
|
| ------------ | -------- | ------------------------------------------- |
|
|
197
197
|
| `PageHeader` | 页面头部 | `title`, `description`,支持 `actions` 插槽 |
|
|
198
198
|
|
|
199
|
-
### 场景组件 Scenes(
|
|
199
|
+
### 场景组件 Scenes(20 个)
|
|
200
200
|
|
|
201
|
-
#### App 应用级(
|
|
201
|
+
#### App 应用级(6 个)
|
|
202
202
|
|
|
203
203
|
| 组件 | 描述 | 主要属性 |
|
|
204
204
|
| ---------------- | ---------------- | ------------------ |
|
|
205
205
|
| `FeedbackModal` | 反馈模态框 | `open`, `onsubmit` |
|
|
206
206
|
| `SettingsModal` | 设置模态框 | `open`, `onclose` |
|
|
207
207
|
| `LanguageSwitch` | 语言切换 | 无需 Props |
|
|
208
|
+
| `ThemeColorPicker` | 主题色选择器 | `value`, `presets`, `onchange` |
|
|
208
209
|
| `ThemeSelector` | 完整主题选择面板 | 无需 Props |
|
|
209
210
|
| `ThemeToggle` | 明/暗主题切换 | 无需 Props |
|
|
210
211
|
|
package/auto-import.d.ts
CHANGED
|
@@ -69,6 +69,7 @@ declare global {
|
|
|
69
69
|
const FeedbackModal: typeof import('@h-ai/ui')['FeedbackModal']
|
|
70
70
|
const LanguageSwitch: typeof import('@h-ai/ui')['LanguageSwitch']
|
|
71
71
|
const SettingsModal: typeof import('@h-ai/ui')['SettingsModal']
|
|
72
|
+
const ThemeColorPicker: typeof import('@h-ai/ui')['ThemeColorPicker']
|
|
72
73
|
const ThemeSelector: typeof import('@h-ai/ui')['ThemeSelector']
|
|
73
74
|
const ThemeToggle: typeof import('@h-ai/ui')['ThemeToggle']
|
|
74
75
|
|
package/auto-import.js
CHANGED
|
@@ -58,9 +58,7 @@
|
|
|
58
58
|
|
|
59
59
|
{#if dismissible}
|
|
60
60
|
<IconButton size='sm' variant='ghost' label={uiM('common_close')} onclick={handleClose}>
|
|
61
|
-
|
|
62
|
-
<span class='icon-[tabler--x] size-4'></span>
|
|
63
|
-
{/snippet}
|
|
61
|
+
<span class='icon-[tabler--x] size-4'></span>
|
|
64
62
|
</IconButton>
|
|
65
63
|
{/if}
|
|
66
64
|
</div>
|
|
@@ -84,11 +84,11 @@
|
|
|
84
84
|
</BitsCalendar.NextButton>
|
|
85
85
|
</BitsCalendar.Header>
|
|
86
86
|
|
|
87
|
-
{#each months as month}
|
|
87
|
+
{#each months as month (month.value.toString())}
|
|
88
88
|
<BitsCalendar.Grid class='w-full border-collapse'>
|
|
89
89
|
<BitsCalendar.GridHead>
|
|
90
90
|
<BitsCalendar.GridRow class='flex w-full'>
|
|
91
|
-
{#each weekdays as day}
|
|
91
|
+
{#each weekdays as day (day)}
|
|
92
92
|
<BitsCalendar.HeadCell class='w-9 text-center text-xs font-medium text-base-content/50'>
|
|
93
93
|
{day}
|
|
94
94
|
</BitsCalendar.HeadCell>
|
|
@@ -97,9 +97,9 @@
|
|
|
97
97
|
</BitsCalendar.GridHead>
|
|
98
98
|
|
|
99
99
|
<BitsCalendar.GridBody>
|
|
100
|
-
{#each month.weeks as weekDates}
|
|
100
|
+
{#each month.weeks as weekDates, weekIndex (weekIndex)}
|
|
101
101
|
<BitsCalendar.GridRow class='flex w-full'>
|
|
102
|
-
{#each weekDates as date}
|
|
102
|
+
{#each weekDates as date (date.toString())}
|
|
103
103
|
<BitsCalendar.Cell {date} month={month.value} class='p-0'>
|
|
104
104
|
<BitsCalendar.Day class='inline-flex size-9 items-center justify-center rounded-lg text-sm transition-colors hover:bg-base-200 data-[selected]:bg-primary data-[selected]:text-primary-content data-[disabled]:opacity-30 data-[unavailable]:line-through data-[unavailable]:opacity-30 data-[outside-month]:text-base-content/30'>
|
|
105
105
|
{date.day}
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
<table class='table w-full text-[13px]' class:table-zebra={striped}>
|
|
83
83
|
<thead>
|
|
84
84
|
<tr class='text-xs text-base-content/50 border-b border-base-content/6'>
|
|
85
|
-
{#each columns as col}
|
|
85
|
+
{#each columns as col (String(col.key))}
|
|
86
86
|
<th
|
|
87
87
|
style={col.width ? `width: ${col.width}` : ''}
|
|
88
88
|
class='{getAlignClass(col.align)} font-medium'
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
{:else}
|
|
116
116
|
{#each data as item (item[keyField])}
|
|
117
117
|
<tr class={hoverable ? 'hover:bg-base-content/3 transition-colors' : ''}>
|
|
118
|
-
{#each columns as col}
|
|
118
|
+
{#each columns as col (String(col.key))}
|
|
119
119
|
<td class={getAlignClass(col.align)}>{getValue(item, col)}</td>
|
|
120
120
|
{/each}
|
|
121
121
|
{#if actions}
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
>
|
|
71
71
|
<BitsDatePicker.Input class="input flex items-center gap-0.5 pr-2 {error ? 'input-error' : ''}">
|
|
72
72
|
{#snippet children({ segments })}
|
|
73
|
-
{#each segments as { part, value: segValue }}
|
|
73
|
+
{#each segments as { part, value: segValue }, index (`${part}-${index}`)}
|
|
74
74
|
<BitsDatePicker.Segment
|
|
75
75
|
{part}
|
|
76
76
|
class='rounded px-0.5 py-0.5 text-sm tabular-nums focus:bg-primary focus:text-primary-content focus:outline-none data-[type=literal]:px-0 data-[type=literal]:text-base-content/50'
|
|
@@ -97,11 +97,11 @@
|
|
|
97
97
|
</BitsDatePicker.NextButton>
|
|
98
98
|
</BitsDatePicker.Header>
|
|
99
99
|
|
|
100
|
-
{#each months as month}
|
|
100
|
+
{#each months as month (month.value.toString())}
|
|
101
101
|
<BitsDatePicker.Grid class='w-full border-collapse'>
|
|
102
102
|
<BitsDatePicker.GridHead>
|
|
103
103
|
<BitsDatePicker.GridRow class='flex w-full'>
|
|
104
|
-
{#each weekdays as day}
|
|
104
|
+
{#each weekdays as day (day)}
|
|
105
105
|
<BitsDatePicker.HeadCell class='w-9 text-center text-xs font-medium text-base-content/50'>
|
|
106
106
|
{day}
|
|
107
107
|
</BitsDatePicker.HeadCell>
|
|
@@ -110,9 +110,9 @@
|
|
|
110
110
|
</BitsDatePicker.GridHead>
|
|
111
111
|
|
|
112
112
|
<BitsDatePicker.GridBody>
|
|
113
|
-
{#each month.weeks as weekDates}
|
|
113
|
+
{#each month.weeks as weekDates, weekIndex (weekIndex)}
|
|
114
114
|
<BitsDatePicker.GridRow class='flex w-full'>
|
|
115
|
-
{#each weekDates as date}
|
|
115
|
+
{#each weekDates as date (date.toString())}
|
|
116
116
|
<BitsDatePicker.Cell {date} month={month.value} class='p-0'>
|
|
117
117
|
<BitsDatePicker.Day class='inline-flex size-9 items-center justify-center rounded-lg text-sm transition-colors hover:bg-base-200 data-[selected]:bg-primary data-[selected]:text-primary-content data-[disabled]:opacity-30 data-[unavailable]:line-through data-[unavailable]:opacity-30 data-[outside-month]:text-base-content/30'>
|
|
118
118
|
{date.day}
|
|
@@ -96,11 +96,9 @@
|
|
|
96
96
|
|
|
97
97
|
{#if showClose}
|
|
98
98
|
<IconButton size='sm' variant='ghost' label={uiM('common_close')} onclick={handleClose}>
|
|
99
|
-
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
</svg>
|
|
103
|
-
{/snippet}
|
|
99
|
+
<svg xmlns='http://www.w3.org/2000/svg' class='h-5 w-5' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
|
|
100
|
+
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M6 18L18 6M6 6l12 12' />
|
|
101
|
+
</svg>
|
|
104
102
|
</IconButton>
|
|
105
103
|
{/if}
|
|
106
104
|
</div>
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
|
|
70
70
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
71
71
|
<ul tabindex='0' class='dropdown-content menu bg-base-100 rounded-xl z-[1] w-52 p-1.5 shadow-lg border border-base-content/6 animate-[hai-scale-in_0.15s_cubic-bezier(0.16,1,0.3,1)]'>
|
|
72
|
-
{#each items as item}
|
|
72
|
+
{#each items as item, index (item.key || `divider-${index}`)}
|
|
73
73
|
{#if item.divider}
|
|
74
74
|
<li class='divider'></li>
|
|
75
75
|
{:else}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
showClose = true,
|
|
17
17
|
width,
|
|
18
18
|
height,
|
|
19
|
+
bodyOverflow = 'auto',
|
|
19
20
|
bodyClass = '',
|
|
20
21
|
class: className = '',
|
|
21
22
|
onclose,
|
|
@@ -59,13 +60,21 @@
|
|
|
59
60
|
|
|
60
61
|
const modalBoxClass = $derived(
|
|
61
62
|
cn(
|
|
62
|
-
'hai-modal__panel flex min-h-0 flex-col
|
|
63
|
+
'hai-modal__panel flex min-h-0 flex-col',
|
|
63
64
|
'bg-base-100 p-0',
|
|
64
65
|
sizeMap[size],
|
|
65
66
|
className,
|
|
66
67
|
),
|
|
67
68
|
)
|
|
68
69
|
|
|
70
|
+
const bodyOverflowClass = $derived(
|
|
71
|
+
bodyOverflow === 'visible'
|
|
72
|
+
? 'overflow-visible'
|
|
73
|
+
: bodyOverflow === 'hidden'
|
|
74
|
+
? 'overflow-hidden'
|
|
75
|
+
: 'overflow-y-auto',
|
|
76
|
+
)
|
|
77
|
+
|
|
69
78
|
function handleClose() {
|
|
70
79
|
open = false
|
|
71
80
|
onclose?.()
|
|
@@ -148,7 +157,7 @@
|
|
|
148
157
|
{/if}
|
|
149
158
|
|
|
150
159
|
<!-- 主体区域独立滚动,避免长内容把头部和底部一起顶走。 -->
|
|
151
|
-
<div class={cn('min-h-0 flex-1
|
|
160
|
+
<div class={cn('min-h-0 flex-1 px-6 py-5 sm:px-7', bodyOverflowClass, bodyClass)}>
|
|
152
161
|
{#if children}
|
|
153
162
|
{@render children()}
|
|
154
163
|
{/if}
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
})
|
|
39
39
|
</script>
|
|
40
40
|
|
|
41
|
-
{#each Object.entries(groupedItems()) as [position, items]}
|
|
41
|
+
{#each Object.entries(groupedItems()) as [position, items] (position)}
|
|
42
42
|
<div class={cn('toast z-50', positionMap[position as keyof typeof positionMap])}>
|
|
43
43
|
{#each items as item (item.id)}
|
|
44
44
|
<div class={cn('alert', getAlertVariantClass(item.variant ?? 'info'))}>
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
|
|
110
110
|
{#if showSteps && stepCount > 0}
|
|
111
111
|
<div class='flex w-full justify-between px-2 text-xs text-base-content/60'>
|
|
112
|
-
{#each Array.from({ length: stepCount }) as _}
|
|
112
|
+
{#each Array.from({ length: stepCount }) as _, index (index)}
|
|
113
113
|
<span>|</span>
|
|
114
114
|
{/each}
|
|
115
115
|
</div>
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
{#if readonly}
|
|
112
112
|
<!-- 只读模式 - 使用 div 而非 input -->
|
|
113
113
|
<div class={cn('rating', sizeClass, className)}>
|
|
114
|
-
{#each Array.from({ length: max }) as _, i}
|
|
114
|
+
{#each Array.from({ length: max }) as _, i (i)}
|
|
115
115
|
<div
|
|
116
116
|
class={cn(
|
|
117
117
|
'mask mask-star-2',
|
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
{:else}
|
|
129
129
|
<!-- 交互模式 - 使用 radio input -->
|
|
130
130
|
<div class={ratingClass}>
|
|
131
|
-
{#each items as item}
|
|
131
|
+
{#each items as item (item.value)}
|
|
132
132
|
<input
|
|
133
133
|
type='radio'
|
|
134
134
|
{name}
|
|
@@ -174,7 +174,7 @@
|
|
|
174
174
|
onclick={event => event.stopPropagation()}
|
|
175
175
|
onkeydown={event => event.stopPropagation()}
|
|
176
176
|
>
|
|
177
|
-
{#each resolvedActions as action}
|
|
177
|
+
{#each resolvedActions as action (action.id)}
|
|
178
178
|
<button
|
|
179
179
|
type='button'
|
|
180
180
|
class={cn('hai-ai-download-menu__item', itemClass)}
|
|
@@ -2561,7 +2561,7 @@ ${safeCode}
|
|
|
2561
2561
|
|
|
2562
2562
|
{#if outlineHasContent}
|
|
2563
2563
|
<nav class='hai-ai-doc-outline-list'>
|
|
2564
|
-
{#each outline as item}
|
|
2564
|
+
{#each outline as item (item.id)}
|
|
2565
2565
|
<button
|
|
2566
2566
|
type='button'
|
|
2567
2567
|
class:active={activeHeadingId === item.id}
|
|
@@ -2917,7 +2917,7 @@ ${safeCode}
|
|
|
2917
2917
|
style={`--hai-ai-doc-selection-menu-left:${selectionMenuLeft}px;`}
|
|
2918
2918
|
onmousedown={handleSelectionPanelMouseDown}
|
|
2919
2919
|
>
|
|
2920
|
-
{#each resolvedRewriteActions as action}
|
|
2920
|
+
{#each resolvedRewriteActions as action (action.id)}
|
|
2921
2921
|
<button
|
|
2922
2922
|
type='button'
|
|
2923
2923
|
class='hai-ai-doc-rewrite-menu-btn'
|
|
@@ -2937,7 +2937,7 @@ ${safeCode}
|
|
|
2937
2937
|
style={`--hai-ai-doc-selection-menu-left:${selectionMenuLeft}px;`}
|
|
2938
2938
|
onmousedown={handleSelectionPanelMouseDown}
|
|
2939
2939
|
>
|
|
2940
|
-
{#each BLOCK_FORMAT_OPTIONS as option}
|
|
2940
|
+
{#each BLOCK_FORMAT_OPTIONS as option (option.value)}
|
|
2941
2941
|
<button
|
|
2942
2942
|
type='button'
|
|
2943
2943
|
class={cn(
|
|
@@ -2969,7 +2969,7 @@ ${safeCode}
|
|
|
2969
2969
|
style={`--hai-ai-doc-selection-menu-left:${selectionMenuLeft}px;`}
|
|
2970
2970
|
onmousedown={handleSelectionPanelMouseDown}
|
|
2971
2971
|
>
|
|
2972
|
-
{#each ALIGN_OPTIONS as option}
|
|
2972
|
+
{#each ALIGN_OPTIONS as option (option.value)}
|
|
2973
2973
|
<button
|
|
2974
2974
|
type='button'
|
|
2975
2975
|
class={cn(
|
|
@@ -3043,7 +3043,7 @@ ${safeCode}
|
|
|
3043
3043
|
{uiM('markdown_text_color')}
|
|
3044
3044
|
</div>
|
|
3045
3045
|
<div class='hai-ai-doc-color-grid'>
|
|
3046
|
-
{#each TEXT_COLOR_PRESETS as color}
|
|
3046
|
+
{#each TEXT_COLOR_PRESETS as color, index (`${color}-${index}`)}
|
|
3047
3047
|
<button
|
|
3048
3048
|
type='button'
|
|
3049
3049
|
class={cn(
|
|
@@ -3066,7 +3066,7 @@ ${safeCode}
|
|
|
3066
3066
|
{uiM('markdown_background_color')}
|
|
3067
3067
|
</div>
|
|
3068
3068
|
<div class='hai-ai-doc-color-grid'>
|
|
3069
|
-
{#each BACKGROUND_COLOR_OPTIONS as colorOption}
|
|
3069
|
+
{#each BACKGROUND_COLOR_OPTIONS as colorOption, index (resolveColorOptionValue(colorOption) ?? `empty-${index}`)}
|
|
3070
3070
|
<button
|
|
3071
3071
|
type='button'
|
|
3072
3072
|
class={cn(
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
AiTableEditorChangePayload,
|
|
22
22
|
AiTableEditorProps,
|
|
23
23
|
} from './table-types.js'
|
|
24
|
+
import { SvelteSet } from 'svelte/reactivity'
|
|
24
25
|
import { uiM } from '../../../../messages.js'
|
|
25
26
|
import { cn } from '../../../../utils.js'
|
|
26
27
|
import AiDocumentDownloadMenu from '../AiDocumentDownloadMenu.svelte'
|
|
@@ -215,7 +216,7 @@
|
|
|
215
216
|
* - 基于 `key` 去重,避免流式增量里重复列导致渲染抖动
|
|
216
217
|
*/
|
|
217
218
|
function normalizeTableColumns(items: unknown[]): AiTableColumn[] {
|
|
218
|
-
const usedKeys = new
|
|
219
|
+
const usedKeys = new SvelteSet<string>()
|
|
219
220
|
const columns: AiTableColumn[] = []
|
|
220
221
|
|
|
221
222
|
for (const item of items) {
|
|
@@ -1095,7 +1096,7 @@
|
|
|
1095
1096
|
<table class='hai-ai-table-grid'>
|
|
1096
1097
|
<thead>
|
|
1097
1098
|
<tr>
|
|
1098
|
-
{#each tableColumns as column}
|
|
1099
|
+
{#each tableColumns as column (column.key)}
|
|
1099
1100
|
<th>{column.label}</th>
|
|
1100
1101
|
{/each}
|
|
1101
1102
|
{#if editable}
|
|
@@ -1120,7 +1121,7 @@
|
|
|
1120
1121
|
ondragover={event => handleRowDragOver(row.row_id, event)}
|
|
1121
1122
|
ondrop={event => handleRowDrop(row.row_id, event)}
|
|
1122
1123
|
>
|
|
1123
|
-
{#each tableColumns as column}
|
|
1124
|
+
{#each tableColumns as column (column.key)}
|
|
1124
1125
|
<td>
|
|
1125
1126
|
<input
|
|
1126
1127
|
class={resolveCellInputClass(column.type)}
|
|
@@ -45,6 +45,8 @@
|
|
|
45
45
|
languages?: Language[]
|
|
46
46
|
/** 语言变更回调 */
|
|
47
47
|
onchange?: (lang: string) => void
|
|
48
|
+
/** 下拉菜单水平对齐方式 */
|
|
49
|
+
align?: 'start' | 'end'
|
|
48
50
|
/** 紧凑模式:仅显示图标,适用于移动端头部 */
|
|
49
51
|
compact?: boolean
|
|
50
52
|
/** 自定义类名 */
|
|
@@ -58,6 +60,7 @@
|
|
|
58
60
|
{ value: 'en-US', label: 'English', flag: 'US' },
|
|
59
61
|
],
|
|
60
62
|
onchange,
|
|
63
|
+
align = 'end',
|
|
61
64
|
compact = false,
|
|
62
65
|
class: className = '',
|
|
63
66
|
}: Props = $props()
|
|
@@ -91,7 +94,7 @@
|
|
|
91
94
|
})
|
|
92
95
|
</script>
|
|
93
96
|
|
|
94
|
-
<div bind:this={containerRef} class='dropdown dropdown-end {open ? 'dropdown-open' : ''} {className}'>
|
|
97
|
+
<div bind:this={containerRef} class='dropdown {align === 'end' ? 'dropdown-end' : ''} {open ? 'dropdown-open' : ''} {className}'>
|
|
95
98
|
<BareButton
|
|
96
99
|
type='button'
|
|
97
100
|
class='btn btn-ghost {compact ? 'btn-sm btn-square' : 'btn-sm gap-2 min-w-fit'}'
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component ThemeColorPicker
|
|
3
|
+
主题色选择器,提供预置主题色与原生取色盘能力。
|
|
4
|
+
|
|
5
|
+
@prop {string} value - 当前选中的颜色值(Hex)
|
|
6
|
+
@prop {Array<{ value: string, label: string }>} presets - 预置主题色列表
|
|
7
|
+
@prop {function} onchange - 颜色变更回调
|
|
8
|
+
-->
|
|
9
|
+
<script lang='ts'>
|
|
10
|
+
import { uiM } from '../../../messages.js'
|
|
11
|
+
import { DEFAULT_THEME_COLOR, THEME_COLOR_PRESETS } from '../../../theme-config.js'
|
|
12
|
+
import { cn } from '../../../utils.js'
|
|
13
|
+
|
|
14
|
+
const HEX_COLOR_REGEX = /^#[0-9a-f]{6}$/
|
|
15
|
+
const SHORT_HEX_COLOR_REGEX = /^#[0-9a-f]{3}$/
|
|
16
|
+
|
|
17
|
+
interface ThemeColorPreset {
|
|
18
|
+
value: string
|
|
19
|
+
label: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface Props {
|
|
23
|
+
value?: string
|
|
24
|
+
presets?: ThemeColorPreset[]
|
|
25
|
+
disabled?: boolean
|
|
26
|
+
onchange?: (color: string) => void
|
|
27
|
+
pickerLabel?: string
|
|
28
|
+
customLabel?: string
|
|
29
|
+
class?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const {
|
|
33
|
+
value = DEFAULT_THEME_COLOR,
|
|
34
|
+
presets,
|
|
35
|
+
disabled = false,
|
|
36
|
+
onchange,
|
|
37
|
+
pickerLabel,
|
|
38
|
+
customLabel,
|
|
39
|
+
class: className = '',
|
|
40
|
+
}: Props = $props()
|
|
41
|
+
|
|
42
|
+
let colorInputRef = $state<HTMLInputElement | null>(null)
|
|
43
|
+
|
|
44
|
+
const defaultPresets = $derived(
|
|
45
|
+
THEME_COLOR_PRESETS.map(preset => ({
|
|
46
|
+
value: preset.value,
|
|
47
|
+
label: uiM(preset.labelKey),
|
|
48
|
+
})),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
const resolvedPresets = $derived(presets?.length ? presets : defaultPresets)
|
|
52
|
+
const displayPickerLabel = $derived(pickerLabel ?? uiM('theme_color_picker_label'))
|
|
53
|
+
const displayCustomLabel = $derived(customLabel ?? uiM('theme_color_custom'))
|
|
54
|
+
|
|
55
|
+
function normalizeHexColor(input: string | undefined): string | null {
|
|
56
|
+
const normalized = input?.trim().toLowerCase() ?? ''
|
|
57
|
+
|
|
58
|
+
if (HEX_COLOR_REGEX.test(normalized)) {
|
|
59
|
+
return normalized
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (SHORT_HEX_COLOR_REGEX.test(normalized)) {
|
|
63
|
+
return `#${normalized[1]}${normalized[1]}${normalized[2]}${normalized[2]}${normalized[3]}${normalized[3]}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const normalizedPresets = $derived(
|
|
70
|
+
resolvedPresets
|
|
71
|
+
.map((preset) => {
|
|
72
|
+
const normalizedValue = normalizeHexColor(preset.value)
|
|
73
|
+
if (!normalizedValue) {
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
...preset,
|
|
79
|
+
value: normalizedValue,
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
.filter((preset): preset is ThemeColorPreset => Boolean(preset)),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
const currentColor = $derived(normalizeHexColor(value) ?? DEFAULT_THEME_COLOR)
|
|
86
|
+
const activePreset = $derived(
|
|
87
|
+
normalizedPresets.find(preset => preset.value === currentColor) ?? null,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
function updateColor(nextColor: string): void {
|
|
91
|
+
const normalized = normalizeHexColor(nextColor)
|
|
92
|
+
if (!normalized || normalized === currentColor) {
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
onchange?.(normalized)
|
|
97
|
+
}
|
|
98
|
+
</script>
|
|
99
|
+
|
|
100
|
+
<div class={cn('', className)}>
|
|
101
|
+
<div class='flex flex-wrap items-center gap-2'>
|
|
102
|
+
{#each normalizedPresets as preset (preset.value)}
|
|
103
|
+
<button
|
|
104
|
+
type='button'
|
|
105
|
+
class={cn(
|
|
106
|
+
'inline-flex items-center gap-2 rounded-full border px-3 py-2 text-sm transition-colors',
|
|
107
|
+
preset.value === currentColor
|
|
108
|
+
? 'border-primary bg-primary/10 text-primary shadow-sm'
|
|
109
|
+
: 'border-base-content/12 bg-base-100 text-base-content hover:border-primary/25 hover:bg-base-200/80',
|
|
110
|
+
disabled && 'cursor-not-allowed opacity-60',
|
|
111
|
+
)}
|
|
112
|
+
aria-pressed={preset.value === currentColor}
|
|
113
|
+
disabled={disabled}
|
|
114
|
+
onclick={() => updateColor(preset.value)}
|
|
115
|
+
>
|
|
116
|
+
<span
|
|
117
|
+
class='size-4 shrink-0 rounded-full border border-black/10'
|
|
118
|
+
style={`background:${preset.value};`}
|
|
119
|
+
aria-hidden='true'
|
|
120
|
+
></span>
|
|
121
|
+
<span>{preset.label}</span>
|
|
122
|
+
</button>
|
|
123
|
+
{/each}
|
|
124
|
+
|
|
125
|
+
<button
|
|
126
|
+
type='button'
|
|
127
|
+
class={cn(
|
|
128
|
+
'group inline-flex items-center gap-2 rounded-full border px-3 py-2 text-left text-sm transition-colors',
|
|
129
|
+
activePreset
|
|
130
|
+
? 'border-base-content/12 bg-base-100 text-base-content hover:border-primary/25 hover:bg-base-200/80'
|
|
131
|
+
: 'border-primary bg-primary/10 text-primary shadow-sm',
|
|
132
|
+
disabled && 'cursor-not-allowed opacity-60',
|
|
133
|
+
)}
|
|
134
|
+
aria-label={displayPickerLabel}
|
|
135
|
+
aria-pressed={!activePreset}
|
|
136
|
+
disabled={disabled}
|
|
137
|
+
onclick={() => colorInputRef?.click()}
|
|
138
|
+
>
|
|
139
|
+
<span
|
|
140
|
+
class='size-4 shrink-0 rounded-full border border-black/10 shadow-sm transition-transform group-hover:scale-[1.03]'
|
|
141
|
+
style={`background:${currentColor};`}
|
|
142
|
+
aria-hidden='true'
|
|
143
|
+
></span>
|
|
144
|
+
|
|
145
|
+
<span class='inline-flex items-center gap-2 min-w-0'>
|
|
146
|
+
<span>{displayCustomLabel}</span>
|
|
147
|
+
{#if !activePreset}
|
|
148
|
+
<span class='text-xs uppercase tracking-[0.14em] text-current/70'>
|
|
149
|
+
{currentColor}
|
|
150
|
+
</span>
|
|
151
|
+
{/if}
|
|
152
|
+
</span>
|
|
153
|
+
</button>
|
|
154
|
+
|
|
155
|
+
<input
|
|
156
|
+
bind:this={colorInputRef}
|
|
157
|
+
class='sr-only'
|
|
158
|
+
type='color'
|
|
159
|
+
value={currentColor}
|
|
160
|
+
aria-label={displayPickerLabel}
|
|
161
|
+
disabled={disabled}
|
|
162
|
+
onchange={event => updateColor(event.currentTarget.value)}
|
|
163
|
+
/>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
interface ThemeColorPreset {
|
|
2
|
+
value: string;
|
|
3
|
+
label: string;
|
|
4
|
+
}
|
|
5
|
+
interface Props {
|
|
6
|
+
value?: string;
|
|
7
|
+
presets?: ThemeColorPreset[];
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
onchange?: (color: string) => void;
|
|
10
|
+
pickerLabel?: string;
|
|
11
|
+
customLabel?: string;
|
|
12
|
+
class?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* ThemeColorPicker
|
|
16
|
+
* 主题色选择器,提供预置主题色与原生取色盘能力。
|
|
17
|
+
*
|
|
18
|
+
* @prop {string} value - 当前选中的颜色值(Hex)
|
|
19
|
+
* @prop {Array<{ value: string, label: string }>} presets - 预置主题色列表
|
|
20
|
+
* @prop {function} onchange - 颜色变更回调
|
|
21
|
+
*/
|
|
22
|
+
declare const ThemeColorPicker: import("svelte").Component<Props, {}, "">;
|
|
23
|
+
type ThemeColorPicker = ReturnType<typeof ThemeColorPicker>;
|
|
24
|
+
export default ThemeColorPicker;
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
selectLabel?: string
|
|
29
29
|
onchange?: (theme: string) => void
|
|
30
30
|
showPreview?: boolean
|
|
31
|
+
/** 下拉菜单水平对齐方式 */
|
|
32
|
+
align?: 'start' | 'end'
|
|
31
33
|
/** 紧凑模式:仅显示预览色块,适用于移动端头部 */
|
|
32
34
|
compact?: boolean
|
|
33
35
|
grouped?: boolean
|
|
@@ -39,6 +41,7 @@
|
|
|
39
41
|
selectLabel,
|
|
40
42
|
onchange,
|
|
41
43
|
showPreview = true,
|
|
44
|
+
align = 'end',
|
|
42
45
|
compact = false,
|
|
43
46
|
grouped = true,
|
|
44
47
|
class: className = '',
|
|
@@ -80,7 +83,7 @@
|
|
|
80
83
|
})
|
|
81
84
|
</script>
|
|
82
85
|
|
|
83
|
-
<div bind:this={containerRef} class='dropdown dropdown-end
|
|
86
|
+
<div bind:this={containerRef} class={cn('dropdown', align === 'end' ? 'dropdown-end' : '', open ? 'dropdown-open' : '', className)}>
|
|
84
87
|
<BareButton
|
|
85
88
|
type='button'
|
|
86
89
|
class='btn btn-ghost {compact ? 'btn-sm btn-square' : 'gap-2'}'
|
|
@@ -108,7 +111,7 @@
|
|
|
108
111
|
<div class='dropdown-content bg-base-100 rounded-box shadow-xl border border-base-content/10 z-50 p-4 max-h-[80vh] overflow-y-auto {compact ? 'w-56 right-0' : ''}'>
|
|
109
112
|
{#if grouped}
|
|
110
113
|
<!-- 分组显示 -->
|
|
111
|
-
{#each THEME_GROUPS as group}
|
|
114
|
+
{#each THEME_GROUPS as group (group.nameKey)}
|
|
112
115
|
<div class='mb-4 last:mb-0'>
|
|
113
116
|
<div class='text-xs font-semibold text-base-content/50 uppercase tracking-wider px-2 mb-2'>
|
|
114
117
|
{uiM(group.nameKey)}
|
|
@@ -7,5 +7,6 @@
|
|
|
7
7
|
export { default as FeedbackModal } from './FeedbackModal.svelte';
|
|
8
8
|
export { default as LanguageSwitch } from './LanguageSwitch.svelte';
|
|
9
9
|
export { default as SettingsModal } from './SettingsModal.svelte';
|
|
10
|
+
export { default as ThemeColorPicker } from './ThemeColorPicker.svelte';
|
|
10
11
|
export { default as ThemeSelector } from './ThemeSelector.svelte';
|
|
11
12
|
export { default as ThemeToggle } from './ThemeToggle.svelte';
|
|
@@ -7,5 +7,6 @@
|
|
|
7
7
|
export { default as FeedbackModal } from './FeedbackModal.svelte';
|
|
8
8
|
export { default as LanguageSwitch } from './LanguageSwitch.svelte';
|
|
9
9
|
export { default as SettingsModal } from './SettingsModal.svelte';
|
|
10
|
+
export { default as ThemeColorPicker } from './ThemeColorPicker.svelte';
|
|
10
11
|
export { default as ThemeSelector } from './ThemeSelector.svelte';
|
|
11
12
|
export { default as ThemeToggle } from './ThemeToggle.svelte';
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
<Drawer bind:open {title} position='right' {size} onclose={handleClose}>
|
|
111
111
|
{#if item}
|
|
112
112
|
<div class='space-y-4 pb-20'>
|
|
113
|
-
{#each detailFields as field}
|
|
113
|
+
{#each detailFields as field (field.id)}
|
|
114
114
|
{@const value = item[field.id]}
|
|
115
115
|
<div>
|
|
116
116
|
<div class='text-xs font-medium text-base-content/50 mb-1'>
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
<div class='text-sm text-base-content'>
|
|
120
120
|
{#if field.type === 'multi-select' && Array.isArray(value) && value.length > 0}
|
|
121
121
|
<div class='flex flex-wrap gap-1'>
|
|
122
|
-
{#each value as v}
|
|
122
|
+
{#each value as v, index (`${String(v)}:${index}`)}
|
|
123
123
|
{@const opts = resolveOptions(field.options)}
|
|
124
124
|
{@const found = opts.find(o => String(o.value) === String(v))}
|
|
125
125
|
<Badge variant='ghost' size='sm'>{found?.label ?? String(v)}</Badge>
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
</div>
|
|
119
119
|
{/if}
|
|
120
120
|
|
|
121
|
-
{#each sortedFields as field}
|
|
121
|
+
{#each sortedFields as field (field.id)}
|
|
122
122
|
{@const isReadonly = field.readonly && mode === 'edit'}
|
|
123
123
|
{@const isRequired = field.validation?.required}
|
|
124
124
|
{@const fieldValue = formData[field.id]}
|
|
@@ -146,7 +146,7 @@
|
|
|
146
146
|
onchange={value => updateField(field.id, value)}
|
|
147
147
|
>
|
|
148
148
|
<option value="">{placeholder || uiM('crud_filter_all')}</option>
|
|
149
|
-
{#each opts as opt}
|
|
149
|
+
{#each opts as opt (String(opt.value))}
|
|
150
150
|
<option value={String(opt.value)}>{opt.label}</option>
|
|
151
151
|
{/each}
|
|
152
152
|
</Select>
|
|
@@ -157,7 +157,7 @@
|
|
|
157
157
|
{@const selectedValues = ((fieldValue ?? []) as Array<string | number | boolean>).map(String)}
|
|
158
158
|
<FormField label={resolveText(field.label)} required={isRequired}>
|
|
159
159
|
<div class='flex flex-wrap gap-3 p-3 bg-base-200 rounded-lg max-h-48 overflow-y-auto'>
|
|
160
|
-
{#each opts as opt}
|
|
160
|
+
{#each opts as opt (String(opt.value))}
|
|
161
161
|
<label class='inline-flex items-center gap-2 cursor-pointer'>
|
|
162
162
|
<Checkbox
|
|
163
163
|
size='sm'
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
</div>
|
|
74
74
|
{/if}
|
|
75
75
|
|
|
76
|
-
{#each filterFields as field}
|
|
76
|
+
{#each filterFields as field (field.id)}
|
|
77
77
|
{@const opts = resolveOptions(field.options)}
|
|
78
78
|
{#if opts.length > 0}
|
|
79
79
|
<Select
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
onchange={value => handleFilterChange(field.id, value)}
|
|
83
83
|
>
|
|
84
84
|
<option value="">{uiM('crud_filter_all')}</option>
|
|
85
|
-
{#each opts as opt}
|
|
85
|
+
{#each opts as opt (String(opt.value))}
|
|
86
86
|
<option value={String(opt.value)}>{opt.label}</option>
|
|
87
87
|
{/each}
|
|
88
88
|
</Select>
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import type { Snippet } from 'svelte'
|
|
15
15
|
import type { Size } from '../../../types.js'
|
|
16
16
|
import type { NavAdapter } from './nav-adapter.js'
|
|
17
|
+
import { SvelteURLSearchParams } from 'svelte/reactivity'
|
|
17
18
|
import { uiM } from '../../../messages.js'
|
|
18
19
|
import Card from '../../compounds/Card.svelte'
|
|
19
20
|
import DataTable from '../../compounds/DataTable.svelte'
|
|
@@ -177,7 +178,7 @@
|
|
|
177
178
|
// ─── 导航 ───
|
|
178
179
|
|
|
179
180
|
function navigateWithParams(overrides: Record<string, string | number>) {
|
|
180
|
-
const params = new
|
|
181
|
+
const params = new SvelteURLSearchParams()
|
|
181
182
|
const merged: Record<string, string | number> = {
|
|
182
183
|
search: searchValue,
|
|
183
184
|
page: data.page,
|
|
@@ -139,11 +139,9 @@
|
|
|
139
139
|
label={uiM('encrypted_input_copy')}
|
|
140
140
|
onclick={copyEncrypted}
|
|
141
141
|
>
|
|
142
|
-
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
</svg>
|
|
146
|
-
{/snippet}
|
|
142
|
+
<svg xmlns='http://www.w3.org/2000/svg' class='h-4 w-4' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
|
|
143
|
+
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z' />
|
|
144
|
+
</svg>
|
|
147
145
|
</IconButton>
|
|
148
146
|
</div>
|
|
149
147
|
<code class='text-xs break-all text-base-content/80'>
|
|
@@ -79,17 +79,15 @@
|
|
|
79
79
|
label={labels.copyHash ?? uiM('hash_copy')}
|
|
80
80
|
onclick={copyToClipboard}
|
|
81
81
|
>
|
|
82
|
-
{#
|
|
83
|
-
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
{/if}
|
|
92
|
-
{/snippet}
|
|
82
|
+
{#if copied}
|
|
83
|
+
<svg xmlns='http://www.w3.org/2000/svg' class='h-4 w-4 text-success' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
|
|
84
|
+
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 13l4 4L19 7' />
|
|
85
|
+
</svg>
|
|
86
|
+
{:else}
|
|
87
|
+
<svg xmlns='http://www.w3.org/2000/svg' class='h-4 w-4' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
|
|
88
|
+
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z' />
|
|
89
|
+
</svg>
|
|
90
|
+
{/if}
|
|
93
91
|
</IconButton>
|
|
94
92
|
{/if}
|
|
95
93
|
</div>
|
|
@@ -107,17 +107,15 @@
|
|
|
107
107
|
label={labels.copySignature ?? uiM('signature_copy_signature')}
|
|
108
108
|
onclick={copySignature}
|
|
109
109
|
>
|
|
110
|
-
{#
|
|
111
|
-
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
<
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
{/if}
|
|
120
|
-
{/snippet}
|
|
110
|
+
{#if copiedSig}
|
|
111
|
+
<svg xmlns='http://www.w3.org/2000/svg' class='h-4 w-4 text-success' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
|
|
112
|
+
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 13l4 4L19 7' />
|
|
113
|
+
</svg>
|
|
114
|
+
{:else}
|
|
115
|
+
<svg xmlns='http://www.w3.org/2000/svg' class='h-4 w-4' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
|
|
116
|
+
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z' />
|
|
117
|
+
</svg>
|
|
118
|
+
{/if}
|
|
121
119
|
</IconButton>
|
|
122
120
|
{/if}
|
|
123
121
|
</div>
|
|
@@ -139,17 +137,15 @@
|
|
|
139
137
|
label={labels.copyPublicKey ?? uiM('signature_copy_public_key')}
|
|
140
138
|
onclick={copyPublicKey}
|
|
141
139
|
>
|
|
142
|
-
{#
|
|
143
|
-
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
<
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
{/if}
|
|
152
|
-
{/snippet}
|
|
140
|
+
{#if copiedKey}
|
|
141
|
+
<svg xmlns='http://www.w3.org/2000/svg' class='h-4 w-4 text-success' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
|
|
142
|
+
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 13l4 4L19 7' />
|
|
143
|
+
</svg>
|
|
144
|
+
{:else}
|
|
145
|
+
<svg xmlns='http://www.w3.org/2000/svg' class='h-4 w-4' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
|
|
146
|
+
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z' />
|
|
147
|
+
</svg>
|
|
148
|
+
{/if}
|
|
153
149
|
</IconButton>
|
|
154
150
|
{/if}
|
|
155
151
|
</div>
|
|
@@ -42,14 +42,23 @@
|
|
|
42
42
|
),
|
|
43
43
|
)
|
|
44
44
|
|
|
45
|
+
function getSubmittedValue(formData: FormData, key: string, fallback = ''): string | undefined {
|
|
46
|
+
const submitted = formData.get(key)
|
|
47
|
+
const value = typeof submitted === 'string' ? submitted : fallback
|
|
48
|
+
const trimmed = value.trim()
|
|
49
|
+
return trimmed || undefined
|
|
50
|
+
}
|
|
51
|
+
|
|
45
52
|
async function handleSubmit(e: SubmitEvent) {
|
|
46
53
|
e.preventDefault()
|
|
47
54
|
if (loading || disabled)
|
|
48
55
|
return
|
|
49
56
|
|
|
57
|
+
const submittedFormData = new FormData(e.currentTarget as HTMLFormElement)
|
|
58
|
+
|
|
50
59
|
const data: ForgotPasswordFormData = {
|
|
51
|
-
email: mode === 'email' ? email : undefined,
|
|
52
|
-
phone: mode === 'phone' ? phone : undefined,
|
|
60
|
+
email: mode === 'email' ? getSubmittedValue(submittedFormData, 'email', email) : undefined,
|
|
61
|
+
phone: mode === 'phone' ? getSubmittedValue(submittedFormData, 'phone', phone) : undefined,
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
await onsubmit?.(data)
|
|
@@ -101,6 +101,13 @@
|
|
|
101
101
|
: ''),
|
|
102
102
|
)
|
|
103
103
|
|
|
104
|
+
function getSubmittedValue(formData: FormData, key: string, fallback = ''): string | undefined {
|
|
105
|
+
const submitted = formData.get(key)
|
|
106
|
+
const value = typeof submitted === 'string' ? submitted : fallback
|
|
107
|
+
const trimmed = value.trim()
|
|
108
|
+
return trimmed || undefined
|
|
109
|
+
}
|
|
110
|
+
|
|
104
111
|
async function handleSubmit(e: SubmitEvent) {
|
|
105
112
|
e.preventDefault()
|
|
106
113
|
if (loading || disabled)
|
|
@@ -108,13 +115,15 @@
|
|
|
108
115
|
if (!passwordsMatch)
|
|
109
116
|
return
|
|
110
117
|
|
|
118
|
+
const submittedFormData = new FormData(e.currentTarget as HTMLFormElement)
|
|
119
|
+
|
|
111
120
|
const data: RegisterFormData = {
|
|
112
|
-
username,
|
|
113
|
-
email,
|
|
114
|
-
phone,
|
|
121
|
+
username: getSubmittedValue(submittedFormData, 'username', username),
|
|
122
|
+
email: getSubmittedValue(submittedFormData, 'email', email),
|
|
123
|
+
phone: getSubmittedValue(submittedFormData, 'phone', phone),
|
|
115
124
|
password,
|
|
116
125
|
confirmPassword: requireConfirmPassword ? confirmPassword : undefined,
|
|
117
|
-
nickname,
|
|
126
|
+
nickname: getSubmittedValue(submittedFormData, 'nickname', nickname),
|
|
118
127
|
}
|
|
119
128
|
|
|
120
129
|
await onsubmit?.(data)
|
|
@@ -375,11 +375,9 @@
|
|
|
375
375
|
label={uiM('file_upload_retry')}
|
|
376
376
|
onclick={() => retryUpload(file.id)}
|
|
377
377
|
>
|
|
378
|
-
|
|
379
|
-
<
|
|
380
|
-
|
|
381
|
-
</svg>
|
|
382
|
-
{/snippet}
|
|
378
|
+
<svg xmlns='http://www.w3.org/2000/svg' class='h-4 w-4' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
|
|
379
|
+
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15' />
|
|
380
|
+
</svg>
|
|
383
381
|
</IconButton>
|
|
384
382
|
{/if}
|
|
385
383
|
<IconButton
|
|
@@ -388,11 +386,9 @@
|
|
|
388
386
|
label={uiM('file_upload_remove')}
|
|
389
387
|
onclick={() => removeFile(file.id)}
|
|
390
388
|
>
|
|
391
|
-
|
|
392
|
-
<
|
|
393
|
-
|
|
394
|
-
</svg>
|
|
395
|
-
{/snippet}
|
|
389
|
+
<svg xmlns='http://www.w3.org/2000/svg' class='h-4 w-4' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
|
|
390
|
+
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M6 18L18 6M6 6l12 12' />
|
|
391
|
+
</svg>
|
|
396
392
|
</IconButton>
|
|
397
393
|
</div>
|
|
398
394
|
</div>
|
|
@@ -37,12 +37,7 @@
|
|
|
37
37
|
|
|
38
38
|
let loading = $state(false)
|
|
39
39
|
let inputElement = $state<HTMLInputElement | undefined>(undefined)
|
|
40
|
-
let previewUrl = $
|
|
41
|
-
|
|
42
|
-
// 当 value 变化时更新预览
|
|
43
|
-
$effect(() => {
|
|
44
|
-
previewUrl = value
|
|
45
|
-
})
|
|
40
|
+
let previewUrl = $derived(value)
|
|
46
41
|
|
|
47
42
|
const containerClass = $derived(
|
|
48
43
|
cn(
|
|
@@ -248,11 +243,9 @@
|
|
|
248
243
|
onclick={handleClick}
|
|
249
244
|
class='text-white'
|
|
250
245
|
>
|
|
251
|
-
|
|
252
|
-
<
|
|
253
|
-
|
|
254
|
-
</svg>
|
|
255
|
-
{/snippet}
|
|
246
|
+
<svg xmlns='http://www.w3.org/2000/svg' class='h-5 w-5' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
|
|
247
|
+
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z' />
|
|
248
|
+
</svg>
|
|
256
249
|
</IconButton>
|
|
257
250
|
<IconButton
|
|
258
251
|
size='sm'
|
|
@@ -261,11 +254,9 @@
|
|
|
261
254
|
onclick={handleRemove}
|
|
262
255
|
class='text-white'
|
|
263
256
|
>
|
|
264
|
-
|
|
265
|
-
<
|
|
266
|
-
|
|
267
|
-
</svg>
|
|
268
|
-
{/snippet}
|
|
257
|
+
<svg xmlns='http://www.w3.org/2000/svg' class='h-5 w-5' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
|
|
258
|
+
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16' />
|
|
259
|
+
</svg>
|
|
269
260
|
</IconButton>
|
|
270
261
|
{/if}
|
|
271
262
|
</div>
|
package/dist/messages/en-US.json
CHANGED
|
@@ -165,6 +165,11 @@
|
|
|
165
165
|
"severity_low": "Low",
|
|
166
166
|
"theme_switch_to_light": "Switch to light theme",
|
|
167
167
|
"theme_switch_to_dark": "Switch to dark theme",
|
|
168
|
+
"theme_color_picker_label": "Choose a custom theme color",
|
|
169
|
+
"theme_color_custom": "Custom",
|
|
170
|
+
"theme_color_tech_purple": "Tech Purple",
|
|
171
|
+
"theme_color_grid_green": "Grid Green",
|
|
172
|
+
"theme_color_ocean": "Ocean",
|
|
168
173
|
"hash_copy": "Copy hash",
|
|
169
174
|
"signature_label": "Signature",
|
|
170
175
|
"signature_public_key": "Public Key",
|
package/dist/messages/zh-CN.json
CHANGED
|
@@ -165,6 +165,11 @@
|
|
|
165
165
|
"severity_low": "低",
|
|
166
166
|
"theme_switch_to_light": "切换到亮色主题",
|
|
167
167
|
"theme_switch_to_dark": "切换到暗色主题",
|
|
168
|
+
"theme_color_picker_label": "选择自定义主题色",
|
|
169
|
+
"theme_color_custom": "自定义",
|
|
170
|
+
"theme_color_tech_purple": "科技紫",
|
|
171
|
+
"theme_color_grid_green": "低碳绿",
|
|
172
|
+
"theme_color_ocean": "海岸蓝",
|
|
168
173
|
"hash_copy": "复制哈希",
|
|
169
174
|
"signature_label": "签名",
|
|
170
175
|
"signature_public_key": "公钥",
|
package/dist/styles/theme.css
CHANGED
|
@@ -11,9 +11,16 @@
|
|
|
11
11
|
* =============================================================================
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
:root {
|
|
15
|
+
--hai-theme-color-default: #5765f0;
|
|
16
|
+
--hai-theme-color-tech-purple: #5765f0;
|
|
17
|
+
--hai-theme-color-grid-green: #13b981;
|
|
18
|
+
--hai-theme-color-ocean: #1f5eff;
|
|
19
|
+
}
|
|
20
|
+
|
|
14
21
|
@theme {
|
|
15
22
|
/* ─── 品牌色 ─── */
|
|
16
|
-
--color-brand:
|
|
23
|
+
--color-brand: var(--hai-theme-color-default);
|
|
17
24
|
|
|
18
25
|
/* ─── 阴影层级 ─── */
|
|
19
26
|
--shadow-xs: 0 1px 2px oklch(0 0 0 / 0.04);
|
package/dist/theme-config.d.ts
CHANGED
|
@@ -20,6 +20,15 @@ export interface ThemeInfo {
|
|
|
20
20
|
/** 背景色(用于预览) */
|
|
21
21
|
bgColor: string;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* 主题色预设定义
|
|
25
|
+
*/
|
|
26
|
+
export interface ThemeColorPreset {
|
|
27
|
+
/** 颜色值(Hex) */
|
|
28
|
+
value: string;
|
|
29
|
+
/** 预设标签 i18n key */
|
|
30
|
+
labelKey: UIMessageKey;
|
|
31
|
+
}
|
|
23
32
|
/**
|
|
24
33
|
* 主题分组
|
|
25
34
|
*/
|
|
@@ -43,6 +52,18 @@ export declare const THEME_GROUPS: ThemeGroup[];
|
|
|
43
52
|
* 暗色主题列表
|
|
44
53
|
*/
|
|
45
54
|
export declare const DARK_THEMES: string[];
|
|
55
|
+
/**
|
|
56
|
+
* 默认主题色
|
|
57
|
+
*/
|
|
58
|
+
export declare const DEFAULT_THEME_COLOR = "#5765f0";
|
|
59
|
+
/**
|
|
60
|
+
* 主题色 CSS 变量名
|
|
61
|
+
*/
|
|
62
|
+
export declare const DEFAULT_THEME_COLOR_CSS_VAR = "--hai-theme-color-default";
|
|
63
|
+
/**
|
|
64
|
+
* 内置主题色预设
|
|
65
|
+
*/
|
|
66
|
+
export declare const THEME_COLOR_PRESETS: ThemeColorPreset[];
|
|
46
67
|
/**
|
|
47
68
|
* 获取主题信息
|
|
48
69
|
*/
|
package/dist/theme-config.js
CHANGED
|
@@ -45,6 +45,22 @@ export const THEME_GROUPS = [
|
|
|
45
45
|
* 暗色主题列表
|
|
46
46
|
*/
|
|
47
47
|
export const DARK_THEMES = THEMES.filter(t => t.dark).map(t => t.id);
|
|
48
|
+
/**
|
|
49
|
+
* 默认主题色
|
|
50
|
+
*/
|
|
51
|
+
export const DEFAULT_THEME_COLOR = '#5765f0';
|
|
52
|
+
/**
|
|
53
|
+
* 主题色 CSS 变量名
|
|
54
|
+
*/
|
|
55
|
+
export const DEFAULT_THEME_COLOR_CSS_VAR = '--hai-theme-color-default';
|
|
56
|
+
/**
|
|
57
|
+
* 内置主题色预设
|
|
58
|
+
*/
|
|
59
|
+
export const THEME_COLOR_PRESETS = [
|
|
60
|
+
{ value: DEFAULT_THEME_COLOR, labelKey: 'theme_color_tech_purple' },
|
|
61
|
+
{ value: '#13b981', labelKey: 'theme_color_grid_green' },
|
|
62
|
+
{ value: '#1f5eff', labelKey: 'theme_color_ocean' },
|
|
63
|
+
];
|
|
48
64
|
/**
|
|
49
65
|
* 获取主题信息
|
|
50
66
|
*/
|
package/dist/types.d.ts
CHANGED
|
@@ -448,6 +448,8 @@ export interface ModalProps {
|
|
|
448
448
|
width?: string;
|
|
449
449
|
/** 自定义高度(任意合法 CSS 高度值) */
|
|
450
450
|
height?: string;
|
|
451
|
+
/** 主体区域溢出策略。默认 auto,适合长内容;visible 适合内部浮层需要越界显示的场景。 */
|
|
452
|
+
bodyOverflow?: 'auto' | 'visible' | 'hidden';
|
|
451
453
|
/** 主体区域自定义类名 */
|
|
452
454
|
bodyClass?: string;
|
|
453
455
|
/** 自定义类名 */
|
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.19",
|
|
5
5
|
"description": "Hai Framework UI component library for Svelte 5 Runes.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"svelte": "./dist/index.js",
|
|
@@ -38,25 +38,25 @@
|
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@internationalized/date": "^3.8.0",
|
|
40
40
|
"@shikijs/langs": "^4.0.2",
|
|
41
|
-
"bits-ui": "^2.
|
|
41
|
+
"bits-ui": "^2.18.1",
|
|
42
42
|
"html2canvas": "^1.4.1",
|
|
43
43
|
"jspdf": "^4.2.1",
|
|
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.19"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@sveltejs/package": "^2.5.7",
|
|
51
|
-
"@sveltejs/vite-plugin-svelte": "^7.
|
|
52
|
-
"@types/node": "^25.1
|
|
51
|
+
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
|
52
|
+
"@types/node": "^25.9.1",
|
|
53
53
|
"jsdom": "^29.0.2",
|
|
54
|
-
"rimraf": "^6.
|
|
55
|
-
"svelte": "^5.
|
|
56
|
-
"svelte-check": "^4.4.
|
|
57
|
-
"typescript": "^6.0.
|
|
58
|
-
"vite": "^8.0.
|
|
59
|
-
"vitest": "^4.
|
|
54
|
+
"rimraf": "^6.1.3",
|
|
55
|
+
"svelte": "^5.55.9",
|
|
56
|
+
"svelte-check": "^4.4.8",
|
|
57
|
+
"typescript": "^6.0.3",
|
|
58
|
+
"vite": "^8.0.14",
|
|
59
|
+
"vitest": "^4.1.7"
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
62
|
"build": "svelte-package -i src/lib",
|