@h-ai/ui 0.1.0-alpha5
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/LICENSE +202 -0
- package/README.md +488 -0
- package/auto-import.d.ts +96 -0
- package/auto-import.js +259 -0
- package/dist/components/compounds/Accordion.svelte +142 -0
- package/dist/components/compounds/Accordion.svelte.d.ts +25 -0
- package/dist/components/compounds/ActionSheet.svelte +116 -0
- package/dist/components/compounds/ActionSheet.svelte.d.ts +22 -0
- package/dist/components/compounds/Alert.svelte +67 -0
- package/dist/components/compounds/Alert.svelte.d.ts +8 -0
- package/dist/components/compounds/AppBar.svelte +77 -0
- package/dist/components/compounds/AppBar.svelte.d.ts +20 -0
- package/dist/components/compounds/BottomNav.svelte +75 -0
- package/dist/components/compounds/BottomNav.svelte.d.ts +18 -0
- package/dist/components/compounds/Breadcrumb.svelte +49 -0
- package/dist/components/compounds/Breadcrumb.svelte.d.ts +4 -0
- package/dist/components/compounds/Calendar.svelte +116 -0
- package/dist/components/compounds/Calendar.svelte.d.ts +47 -0
- package/dist/components/compounds/Card.svelte +77 -0
- package/dist/components/compounds/Card.svelte.d.ts +8 -0
- package/dist/components/compounds/Combobox.svelte +327 -0
- package/dist/components/compounds/Combobox.svelte.d.ts +69 -0
- package/dist/components/compounds/Confirm.svelte +90 -0
- package/dist/components/compounds/Confirm.svelte.d.ts +4 -0
- package/dist/components/compounds/DataTable.svelte +133 -0
- package/dist/components/compounds/DataTable.svelte.d.ts +69 -0
- package/dist/components/compounds/DatePicker.svelte +135 -0
- package/dist/components/compounds/DatePicker.svelte.d.ts +46 -0
- package/dist/components/compounds/Drawer.svelte +113 -0
- package/dist/components/compounds/Drawer.svelte.d.ts +4 -0
- package/dist/components/compounds/Dropdown.svelte +91 -0
- package/dist/components/compounds/Dropdown.svelte.d.ts +8 -0
- package/dist/components/compounds/Empty.svelte +83 -0
- package/dist/components/compounds/Empty.svelte.d.ts +4 -0
- package/dist/components/compounds/Form.svelte +96 -0
- package/dist/components/compounds/Form.svelte.d.ts +7 -0
- package/dist/components/compounds/FormField.svelte +56 -0
- package/dist/components/compounds/FormField.svelte.d.ts +4 -0
- package/dist/components/compounds/InfiniteScroll.svelte +82 -0
- package/dist/components/compounds/InfiniteScroll.svelte.d.ts +22 -0
- package/dist/components/compounds/Modal.svelte +123 -0
- package/dist/components/compounds/Modal.svelte.d.ts +8 -0
- package/dist/components/compounds/PageHeader.svelte +44 -0
- package/dist/components/compounds/PageHeader.svelte.d.ts +28 -0
- package/dist/components/compounds/Pagination.svelte +157 -0
- package/dist/components/compounds/Pagination.svelte.d.ts +4 -0
- package/dist/components/compounds/Popover.svelte +137 -0
- package/dist/components/compounds/Popover.svelte.d.ts +4 -0
- package/dist/components/compounds/PullRefresh.svelte +98 -0
- package/dist/components/compounds/PullRefresh.svelte.d.ts +16 -0
- package/dist/components/compounds/Result.svelte +84 -0
- package/dist/components/compounds/Result.svelte.d.ts +4 -0
- package/dist/components/compounds/SafeArea.svelte +37 -0
- package/dist/components/compounds/SafeArea.svelte.d.ts +12 -0
- package/dist/components/compounds/Skeleton.svelte +75 -0
- package/dist/components/compounds/Skeleton.svelte.d.ts +8 -0
- package/dist/components/compounds/Steps.svelte +111 -0
- package/dist/components/compounds/Steps.svelte.d.ts +4 -0
- package/dist/components/compounds/SwipeCell.svelte +115 -0
- package/dist/components/compounds/SwipeCell.svelte.d.ts +19 -0
- package/dist/components/compounds/Tabs.svelte +79 -0
- package/dist/components/compounds/Tabs.svelte.d.ts +8 -0
- package/dist/components/compounds/TagInput.svelte +108 -0
- package/dist/components/compounds/TagInput.svelte.d.ts +4 -0
- package/dist/components/compounds/Timeline.svelte +167 -0
- package/dist/components/compounds/Timeline.svelte.d.ts +25 -0
- package/dist/components/compounds/ToastContainer.svelte +59 -0
- package/dist/components/compounds/ToastContainer.svelte.d.ts +3 -0
- package/dist/components/compounds/Tooltip.svelte +41 -0
- package/dist/components/compounds/Tooltip.svelte.d.ts +4 -0
- package/dist/components/compounds/accordion-types.d.ts +16 -0
- package/dist/components/compounds/accordion-types.js +5 -0
- package/dist/components/compounds/action-sheet-types.d.ts +14 -0
- package/dist/components/compounds/action-sheet-types.js +5 -0
- package/dist/components/compounds/bottom-nav-types.d.ts +17 -0
- package/dist/components/compounds/bottom-nav-types.js +5 -0
- package/dist/components/compounds/index.d.ts +44 -0
- package/dist/components/compounds/index.js +54 -0
- package/dist/components/compounds/swipe-cell-types.d.ts +14 -0
- package/dist/components/compounds/swipe-cell-types.js +5 -0
- package/dist/components/compounds/timeline-types.d.ts +20 -0
- package/dist/components/compounds/timeline-types.js +5 -0
- package/dist/components/index.d.ts +9 -0
- package/dist/components/index.js +12 -0
- package/dist/components/primitives/Avatar.svelte +102 -0
- package/dist/components/primitives/Avatar.svelte.d.ts +8 -0
- package/dist/components/primitives/Badge.svelte +33 -0
- package/dist/components/primitives/Badge.svelte.d.ts +8 -0
- package/dist/components/primitives/BareButton.svelte +45 -0
- package/dist/components/primitives/BareButton.svelte.d.ts +4 -0
- package/dist/components/primitives/BareInput.svelte +101 -0
- package/dist/components/primitives/BareInput.svelte.d.ts +4 -0
- package/dist/components/primitives/Button.svelte +60 -0
- package/dist/components/primitives/Button.svelte.d.ts +8 -0
- package/dist/components/primitives/Checkbox.svelte +58 -0
- package/dist/components/primitives/Checkbox.svelte.d.ts +4 -0
- package/dist/components/primitives/IconButton.svelte +105 -0
- package/dist/components/primitives/IconButton.svelte.d.ts +4 -0
- package/dist/components/primitives/Input.svelte +130 -0
- package/dist/components/primitives/Input.svelte.d.ts +8 -0
- package/dist/components/primitives/Progress.svelte +58 -0
- package/dist/components/primitives/Progress.svelte.d.ts +4 -0
- package/dist/components/primitives/Radio.svelte +60 -0
- package/dist/components/primitives/Radio.svelte.d.ts +25 -0
- package/dist/components/primitives/Range.svelte +117 -0
- package/dist/components/primitives/Range.svelte.d.ts +27 -0
- package/dist/components/primitives/Rating.svelte +148 -0
- package/dist/components/primitives/Rating.svelte.d.ts +27 -0
- package/dist/components/primitives/Select.svelte +95 -0
- package/dist/components/primitives/Select.svelte.d.ts +25 -0
- package/dist/components/primitives/Spinner.svelte +54 -0
- package/dist/components/primitives/Spinner.svelte.d.ts +4 -0
- package/dist/components/primitives/Switch.svelte +53 -0
- package/dist/components/primitives/Switch.svelte.d.ts +4 -0
- package/dist/components/primitives/Tag.svelte +64 -0
- package/dist/components/primitives/Tag.svelte.d.ts +4 -0
- package/dist/components/primitives/Textarea.svelte +92 -0
- package/dist/components/primitives/Textarea.svelte.d.ts +4 -0
- package/dist/components/primitives/ToggleCheckbox.svelte +36 -0
- package/dist/components/primitives/ToggleCheckbox.svelte.d.ts +4 -0
- package/dist/components/primitives/ToggleInput.svelte +36 -0
- package/dist/components/primitives/ToggleInput.svelte.d.ts +4 -0
- package/dist/components/primitives/ToggleRadio.svelte +36 -0
- package/dist/components/primitives/ToggleRadio.svelte.d.ts +4 -0
- package/dist/components/primitives/index.d.ts +26 -0
- package/dist/components/primitives/index.js +30 -0
- package/dist/components/scenes/ai/AiDocumentDownloadMenu.svelte +387 -0
- package/dist/components/scenes/ai/AiDocumentDownloadMenu.svelte.d.ts +38 -0
- package/dist/components/scenes/ai/AiDocumentEditor.svelte +2259 -0
- package/dist/components/scenes/ai/AiDocumentEditor.svelte.d.ts +4 -0
- package/dist/components/scenes/ai/MarkdownRenderer.svelte +460 -0
- package/dist/components/scenes/ai/MarkdownRenderer.svelte.d.ts +4 -0
- package/dist/components/scenes/ai/document-download.d.ts +40 -0
- package/dist/components/scenes/ai/document-download.js +300 -0
- package/dist/components/scenes/ai/document-parse.d.ts +28 -0
- package/dist/components/scenes/ai/document-parse.js +243 -0
- package/dist/components/scenes/ai/document-types.d.ts +164 -0
- package/dist/components/scenes/ai/document-types.js +1 -0
- package/dist/components/scenes/ai/index.d.ts +17 -0
- package/dist/components/scenes/ai/index.js +13 -0
- package/dist/components/scenes/ai/markdown-parse.d.ts +32 -0
- package/dist/components/scenes/ai/markdown-parse.js +161 -0
- package/dist/components/scenes/app/FeedbackModal.svelte +160 -0
- package/dist/components/scenes/app/FeedbackModal.svelte.d.ts +47 -0
- package/dist/components/scenes/app/LanguageSwitch.svelte +136 -0
- package/dist/components/scenes/app/LanguageSwitch.svelte.d.ts +50 -0
- package/dist/components/scenes/app/SettingsModal.svelte +174 -0
- package/dist/components/scenes/app/SettingsModal.svelte.d.ts +31 -0
- package/dist/components/scenes/app/ThemeSelector.svelte +176 -0
- package/dist/components/scenes/app/ThemeSelector.svelte.d.ts +31 -0
- package/dist/components/scenes/app/ThemeToggle.svelte +76 -0
- package/dist/components/scenes/app/ThemeToggle.svelte.d.ts +29 -0
- package/dist/components/scenes/app/index.d.ts +11 -0
- package/dist/components/scenes/app/index.js +11 -0
- package/dist/components/scenes/crud/CrudDeleteConfirm.svelte +39 -0
- package/dist/components/scenes/crud/CrudDeleteConfirm.svelte.d.ts +18 -0
- package/dist/components/scenes/crud/CrudDetailDrawer.svelte +155 -0
- package/dist/components/scenes/crud/CrudDetailDrawer.svelte.d.ts +38 -0
- package/dist/components/scenes/crud/CrudEditDrawer.svelte +222 -0
- package/dist/components/scenes/crud/CrudEditDrawer.svelte.d.ts +48 -0
- package/dist/components/scenes/crud/CrudFilterBar.svelte +96 -0
- package/dist/components/scenes/crud/CrudFilterBar.svelte.d.ts +31 -0
- package/dist/components/scenes/crud/CrudPage.svelte +499 -0
- package/dist/components/scenes/crud/CrudPage.svelte.d.ts +114 -0
- package/dist/components/scenes/crud/crud-types.d.ts +135 -0
- package/dist/components/scenes/crud/crud-types.js +7 -0
- package/dist/components/scenes/crud/index.d.ts +12 -0
- package/dist/components/scenes/crud/index.js +11 -0
- package/dist/components/scenes/crypto/EncryptedInput.svelte +156 -0
- package/dist/components/scenes/crypto/EncryptedInput.svelte.d.ts +32 -0
- package/dist/components/scenes/crypto/HashDisplay.svelte +97 -0
- package/dist/components/scenes/crypto/HashDisplay.svelte.d.ts +4 -0
- package/dist/components/scenes/crypto/SignatureDisplay.svelte +161 -0
- package/dist/components/scenes/crypto/SignatureDisplay.svelte.d.ts +4 -0
- package/dist/components/scenes/crypto/index.d.ts +9 -0
- package/dist/components/scenes/crypto/index.js +11 -0
- package/dist/components/scenes/iam/ChangePasswordForm.svelte +134 -0
- package/dist/components/scenes/iam/ChangePasswordForm.svelte.d.ts +8 -0
- package/dist/components/scenes/iam/ForgotPasswordForm.svelte +147 -0
- package/dist/components/scenes/iam/ForgotPasswordForm.svelte.d.ts +4 -0
- package/dist/components/scenes/iam/LoginForm.svelte +191 -0
- package/dist/components/scenes/iam/LoginForm.svelte.d.ts +8 -0
- package/dist/components/scenes/iam/PasswordInput.svelte +203 -0
- package/dist/components/scenes/iam/PasswordInput.svelte.d.ts +49 -0
- package/dist/components/scenes/iam/PermGuard.svelte +69 -0
- package/dist/components/scenes/iam/PermGuard.svelte.d.ts +16 -0
- package/dist/components/scenes/iam/RegisterForm.svelte +269 -0
- package/dist/components/scenes/iam/RegisterForm.svelte.d.ts +8 -0
- package/dist/components/scenes/iam/ResetPasswordForm.svelte +153 -0
- package/dist/components/scenes/iam/ResetPasswordForm.svelte.d.ts +8 -0
- package/dist/components/scenes/iam/UserProfile.svelte +251 -0
- package/dist/components/scenes/iam/UserProfile.svelte.d.ts +4 -0
- package/dist/components/scenes/iam/index.d.ts +16 -0
- package/dist/components/scenes/iam/index.js +18 -0
- package/dist/components/scenes/iam/password-utils.d.ts +15 -0
- package/dist/components/scenes/iam/password-utils.js +19 -0
- package/dist/components/scenes/iam/permission-context.svelte.d.ts +90 -0
- package/dist/components/scenes/iam/permission-context.svelte.js +114 -0
- package/dist/components/scenes/index.d.ts +13 -0
- package/dist/components/scenes/index.js +20 -0
- package/dist/components/scenes/storage/AvatarUpload.svelte +218 -0
- package/dist/components/scenes/storage/AvatarUpload.svelte.d.ts +4 -0
- package/dist/components/scenes/storage/FileList.svelte +260 -0
- package/dist/components/scenes/storage/FileList.svelte.d.ts +4 -0
- package/dist/components/scenes/storage/FileUpload.svelte +400 -0
- package/dist/components/scenes/storage/FileUpload.svelte.d.ts +7 -0
- package/dist/components/scenes/storage/ImageUpload.svelte +285 -0
- package/dist/components/scenes/storage/ImageUpload.svelte.d.ts +4 -0
- package/dist/components/scenes/storage/index.d.ts +10 -0
- package/dist/components/scenes/storage/index.js +12 -0
- package/dist/components/scenes/types.d.ts +583 -0
- package/dist/components/scenes/types.js +7 -0
- package/dist/i18n.svelte.d.ts +44 -0
- package/dist/i18n.svelte.js +108 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +19 -0
- package/dist/messages/en-US.json +242 -0
- package/dist/messages/zh-CN.json +242 -0
- package/dist/messages.d.ts +29 -0
- package/dist/messages.js +42 -0
- package/dist/styles/design-tokens.css +58 -0
- package/dist/styles/global.css +218 -0
- package/dist/styles/mobile.css +105 -0
- package/dist/theme-config.d.ts +92 -0
- package/dist/theme-config.js +122 -0
- package/dist/toast.svelte.d.ts +45 -0
- package/dist/toast.svelte.js +77 -0
- package/dist/types.d.ts +944 -0
- package/dist/types.js +7 -0
- package/dist/utils/platform.svelte.d.ts +54 -0
- package/dist/utils/platform.svelte.js +87 -0
- package/dist/utils.d.ts +46 -0
- package/dist/utils.js +100 -0
- package/package.json +69 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component LanguageSwitch
|
|
3
|
+
语言切换下拉组件,支持多语言切换。
|
|
4
|
+
|
|
5
|
+
@prop {string} currentLanguage - 当前语言代码(BCP 47 格式,如 'zh-CN', 'en-US')
|
|
6
|
+
@prop {Language[]} languages - 可选语言列表
|
|
7
|
+
@prop {function} onchange - 语言变更回调,传入选中的语言代码
|
|
8
|
+
|
|
9
|
+
@example
|
|
10
|
+
```svelte
|
|
11
|
+
<script>
|
|
12
|
+
import { LanguageSwitch, createLocaleStore, setGlobalLocale } from '@h-ai/ui'
|
|
13
|
+
import { setLocale, getLocale } from '../../../paraglide/runtime.js'
|
|
14
|
+
|
|
15
|
+
const localeStore = createLocaleStore()
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<LanguageSwitch
|
|
19
|
+
currentLanguage={localeStore.current}
|
|
20
|
+
languages={[
|
|
21
|
+
{ value: 'zh-CN', label: '简体中文' },
|
|
22
|
+
{ value: 'en-US', label: 'English' }
|
|
23
|
+
]}
|
|
24
|
+
onchange={(lang) => {
|
|
25
|
+
localeStore.set(lang) // 同步到 @h-ai/core 全局 locale
|
|
26
|
+
setLocale(lang) // 同步到 Paraglide
|
|
27
|
+
}}
|
|
28
|
+
/>
|
|
29
|
+
```
|
|
30
|
+
-->
|
|
31
|
+
<script lang='ts'>
|
|
32
|
+
import { uiM } from '../../../messages.js'
|
|
33
|
+
import BareButton from '../../primitives/BareButton.svelte'
|
|
34
|
+
|
|
35
|
+
interface Language {
|
|
36
|
+
value: string
|
|
37
|
+
label: string
|
|
38
|
+
flag?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface Props {
|
|
42
|
+
/** 当前语言代码(BCP 47 格式) */
|
|
43
|
+
currentLanguage?: string
|
|
44
|
+
/** 可选语言列表 */
|
|
45
|
+
languages?: Language[]
|
|
46
|
+
/** 语言变更回调 */
|
|
47
|
+
onchange?: (lang: string) => void
|
|
48
|
+
/** 紧凑模式:仅显示图标,适用于移动端头部 */
|
|
49
|
+
compact?: boolean
|
|
50
|
+
/** 自定义类名 */
|
|
51
|
+
class?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const {
|
|
55
|
+
currentLanguage = 'zh-CN',
|
|
56
|
+
languages = [
|
|
57
|
+
{ value: 'zh-CN', label: '简体中文', flag: 'CN' },
|
|
58
|
+
{ value: 'en-US', label: 'English', flag: 'US' },
|
|
59
|
+
],
|
|
60
|
+
onchange,
|
|
61
|
+
compact = false,
|
|
62
|
+
class: className = '',
|
|
63
|
+
}: Props = $props()
|
|
64
|
+
|
|
65
|
+
let open = $state(false)
|
|
66
|
+
let containerRef = $state<HTMLDivElement | null>(null)
|
|
67
|
+
|
|
68
|
+
const currentLangInfo = $derived(
|
|
69
|
+
languages.find(l => l.value === currentLanguage),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
function selectLanguage(lang: string) {
|
|
73
|
+
if (lang !== currentLanguage) {
|
|
74
|
+
onchange?.(lang)
|
|
75
|
+
}
|
|
76
|
+
open = false
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function handleClickOutside(event: MouseEvent) {
|
|
80
|
+
if (containerRef && !containerRef.contains(event.target as Node)) {
|
|
81
|
+
open = false
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
$effect(() => {
|
|
86
|
+
if (open) {
|
|
87
|
+
document.addEventListener('click', handleClickOutside)
|
|
88
|
+
return () => document.removeEventListener('click', handleClickOutside)
|
|
89
|
+
}
|
|
90
|
+
return undefined
|
|
91
|
+
})
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<div bind:this={containerRef} class='dropdown dropdown-end {open ? 'dropdown-open' : ''} {className}'>
|
|
95
|
+
<BareButton
|
|
96
|
+
type='button'
|
|
97
|
+
class='btn btn-ghost {compact ? 'btn-sm btn-square' : 'btn-sm gap-2 min-w-fit'}'
|
|
98
|
+
onclick={() => (open = !open)}
|
|
99
|
+
ariaLabel={uiM('language_switch_label')}
|
|
100
|
+
>
|
|
101
|
+
<svg xmlns='http://www.w3.org/2000/svg' class='h-4 w-4 shrink-0' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'>
|
|
102
|
+
<path d='M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129' />
|
|
103
|
+
</svg>
|
|
104
|
+
{#if !compact}
|
|
105
|
+
{#if currentLangInfo?.flag}
|
|
106
|
+
<span class='shrink-0 text-xs'>{currentLangInfo.flag}</span>
|
|
107
|
+
{/if}
|
|
108
|
+
<span class='text-sm whitespace-nowrap'>{currentLangInfo?.label ?? currentLanguage}</span>
|
|
109
|
+
<svg xmlns='http://www.w3.org/2000/svg' class='h-4 w-4 shrink-0 opacity-50' viewBox='0 0 20 20' fill='currentColor'>
|
|
110
|
+
<path fill-rule='evenodd' d='M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z' clip-rule='evenodd' />
|
|
111
|
+
</svg>
|
|
112
|
+
{/if}
|
|
113
|
+
</BareButton>
|
|
114
|
+
|
|
115
|
+
{#if open}
|
|
116
|
+
<div class='dropdown-content bg-base-100 rounded-box shadow-lg border border-base-content/10 z-50 p-2'>
|
|
117
|
+
{#each languages as lang (lang.value)}
|
|
118
|
+
<BareButton
|
|
119
|
+
type='button'
|
|
120
|
+
class='flex items-center gap-2 w-full px-3 py-2 rounded-lg text-left transition-colors whitespace-nowrap {lang.value === currentLanguage ? 'bg-primary/10 text-primary' : 'hover:bg-base-200'}'
|
|
121
|
+
onclick={() => selectLanguage(lang.value)}
|
|
122
|
+
>
|
|
123
|
+
{#if lang.flag}
|
|
124
|
+
<span class='shrink-0 text-xs font-medium text-base-content/60'>{lang.flag}</span>
|
|
125
|
+
{/if}
|
|
126
|
+
<span class='whitespace-nowrap'>{lang.label}</span>
|
|
127
|
+
{#if lang.value === currentLanguage}
|
|
128
|
+
<svg xmlns='http://www.w3.org/2000/svg' class='h-4 w-4 ml-auto shrink-0' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'>
|
|
129
|
+
<path d='M5 13l4 4L19 7' />
|
|
130
|
+
</svg>
|
|
131
|
+
{/if}
|
|
132
|
+
</BareButton>
|
|
133
|
+
{/each}
|
|
134
|
+
</div>
|
|
135
|
+
{/if}
|
|
136
|
+
</div>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
interface Language {
|
|
2
|
+
value: string;
|
|
3
|
+
label: string;
|
|
4
|
+
flag?: string;
|
|
5
|
+
}
|
|
6
|
+
interface Props {
|
|
7
|
+
/** 当前语言代码(BCP 47 格式) */
|
|
8
|
+
currentLanguage?: string;
|
|
9
|
+
/** 可选语言列表 */
|
|
10
|
+
languages?: Language[];
|
|
11
|
+
/** 语言变更回调 */
|
|
12
|
+
onchange?: (lang: string) => void;
|
|
13
|
+
/** 紧凑模式:仅显示图标,适用于移动端头部 */
|
|
14
|
+
compact?: boolean;
|
|
15
|
+
/** 自定义类名 */
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* LanguageSwitch
|
|
20
|
+
* 语言切换下拉组件,支持多语言切换。
|
|
21
|
+
*
|
|
22
|
+
* @prop {string} currentLanguage - 当前语言代码(BCP 47 格式,如 'zh-CN', 'en-US')
|
|
23
|
+
* @prop {Language[]} languages - 可选语言列表
|
|
24
|
+
* @prop {function} onchange - 语言变更回调,传入选中的语言代码
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```svelte
|
|
28
|
+
* <script>
|
|
29
|
+
* import { LanguageSwitch, createLocaleStore, setGlobalLocale } from '@h-ai/ui'
|
|
30
|
+
* import { setLocale, getLocale } from '../../../paraglide/runtime.js'
|
|
31
|
+
*
|
|
32
|
+
* const localeStore = createLocaleStore()
|
|
33
|
+
* </script>
|
|
34
|
+
*
|
|
35
|
+
* <LanguageSwitch
|
|
36
|
+
* currentLanguage={localeStore.current}
|
|
37
|
+
* languages={[
|
|
38
|
+
* { value: 'zh-CN', label: '简体中文' },
|
|
39
|
+
* { value: 'en-US', label: 'English' }
|
|
40
|
+
* ]}
|
|
41
|
+
* onchange={(lang) => {
|
|
42
|
+
* localeStore.set(lang) // 同步到 @h-ai/core 全局 locale
|
|
43
|
+
* setLocale(lang) // 同步到 Paraglide
|
|
44
|
+
* }}
|
|
45
|
+
* />
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
declare const LanguageSwitch: import("svelte").Component<Props, {}, "">;
|
|
49
|
+
type LanguageSwitch = ReturnType<typeof LanguageSwitch>;
|
|
50
|
+
export default LanguageSwitch;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component SettingsModal
|
|
3
|
+
应用设置模态框,包含语言和主题切换功能。
|
|
4
|
+
内置多语言支持,自动跟随全局 locale
|
|
5
|
+
|
|
6
|
+
@prop {boolean} open - 是否显示模态框(双向绑定)
|
|
7
|
+
@prop {string} currentLanguage - 当前语言
|
|
8
|
+
@prop {string} currentTheme - 当前主题
|
|
9
|
+
@prop {function} onlanguagechange - 语言变更回调
|
|
10
|
+
@prop {function} onthemechange - 主题变更回调
|
|
11
|
+
-->
|
|
12
|
+
<script lang='ts'>
|
|
13
|
+
import { uiM } from '../../../messages.js'
|
|
14
|
+
import BareButton from '../../primitives/BareButton.svelte'
|
|
15
|
+
import IconButton from '../../primitives/IconButton.svelte'
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
open?: boolean
|
|
19
|
+
currentLanguage?: string
|
|
20
|
+
currentTheme?: string
|
|
21
|
+
languages?: { value: string, label: string, icon?: string }[]
|
|
22
|
+
themes?: { value: string, label: string }[]
|
|
23
|
+
onlanguagechange?: (lang: string) => void
|
|
24
|
+
onthemechange?: (theme: string) => void
|
|
25
|
+
onclose?: () => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let {
|
|
29
|
+
open = $bindable(false),
|
|
30
|
+
currentLanguage = 'zh-cn',
|
|
31
|
+
currentTheme = 'light',
|
|
32
|
+
languages = [
|
|
33
|
+
{ value: 'zh-cn', label: '简体中文', icon: '🇨🇳' },
|
|
34
|
+
{ value: 'en-us', label: 'English', icon: '🇺🇸' },
|
|
35
|
+
],
|
|
36
|
+
themes = [
|
|
37
|
+
{ value: 'light', label: 'Light' },
|
|
38
|
+
{ value: 'dark', label: 'Dark' },
|
|
39
|
+
{ value: 'corporate', label: 'Corporate' },
|
|
40
|
+
{ value: 'luxury', label: 'Luxury' },
|
|
41
|
+
{ value: 'pastel', label: 'Pastel' },
|
|
42
|
+
],
|
|
43
|
+
onlanguagechange,
|
|
44
|
+
onthemechange,
|
|
45
|
+
onclose,
|
|
46
|
+
}: Props = $props()
|
|
47
|
+
|
|
48
|
+
function handleClose() {
|
|
49
|
+
open = false
|
|
50
|
+
onclose?.()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function handleBackdropClick(e: MouseEvent) {
|
|
54
|
+
if (e.target === e.currentTarget) {
|
|
55
|
+
handleClose()
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
60
|
+
if (e.key === 'Escape') {
|
|
61
|
+
handleClose()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<svelte:window onkeydown={handleKeydown} />
|
|
67
|
+
|
|
68
|
+
{#if open}
|
|
69
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
70
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
71
|
+
<div
|
|
72
|
+
class='fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm transition-opacity'
|
|
73
|
+
onclick={handleBackdropClick}
|
|
74
|
+
>
|
|
75
|
+
<div
|
|
76
|
+
class='relative w-full max-w-lg mx-4 transform overflow-hidden rounded-2xl bg-base-100 shadow-xl transition-all'
|
|
77
|
+
role='dialog'
|
|
78
|
+
aria-modal='true'
|
|
79
|
+
>
|
|
80
|
+
<!-- 标题栏 -->
|
|
81
|
+
<div class='flex items-center justify-between border-b border-base-content/10 px-6 py-4'>
|
|
82
|
+
<h3 class='text-lg font-semibold text-base-content'>{uiM('settings_title')}</h3>
|
|
83
|
+
<IconButton
|
|
84
|
+
size='sm'
|
|
85
|
+
variant='ghost'
|
|
86
|
+
label={uiM('settings_close')}
|
|
87
|
+
onclick={handleClose}
|
|
88
|
+
class='hover:bg-base-content/10'
|
|
89
|
+
>
|
|
90
|
+
{#snippet children()}
|
|
91
|
+
<span class='icon-[tabler--x] size-5'></span>
|
|
92
|
+
{/snippet}
|
|
93
|
+
</IconButton>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<!-- 内容区 -->
|
|
97
|
+
<div class='overflow-y-auto p-6' style='max-height: calc(85vh - 4rem);'>
|
|
98
|
+
<!-- 语言设置 -->
|
|
99
|
+
<section class='mb-8'>
|
|
100
|
+
<h4 class='mb-4 flex items-center gap-2 text-sm font-semibold uppercase tracking-wider text-base-content/60'>
|
|
101
|
+
<span class='icon-[tabler--world] size-4'></span>
|
|
102
|
+
{uiM('settings_language')}
|
|
103
|
+
</h4>
|
|
104
|
+
<div class='flex gap-3'>
|
|
105
|
+
{#each languages as lang (lang.value)}
|
|
106
|
+
<BareButton
|
|
107
|
+
type='button'
|
|
108
|
+
class='group flex flex-1 items-center justify-center gap-3 rounded-xl border-2 px-6 py-4 text-base font-medium transition-all duration-200
|
|
109
|
+
hover:border-primary/60 hover:bg-primary/5 hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40
|
|
110
|
+
{currentLanguage === lang.value
|
|
111
|
+
? 'border-primary bg-primary/10 text-primary shadow-sm'
|
|
112
|
+
: 'border-base-content/15 bg-base-100 text-base-content/80'}'
|
|
113
|
+
onclick={() => onlanguagechange?.(lang.value)}
|
|
114
|
+
role='option'
|
|
115
|
+
ariaSelected={currentLanguage === lang.value}
|
|
116
|
+
>
|
|
117
|
+
{#if lang.icon}
|
|
118
|
+
<span class='text-2xl'>{lang.icon}</span>
|
|
119
|
+
{/if}
|
|
120
|
+
<span>{lang.label}</span>
|
|
121
|
+
{#if currentLanguage === lang.value}
|
|
122
|
+
<span class='icon-[tabler--check] size-5 text-primary'></span>
|
|
123
|
+
{/if}
|
|
124
|
+
</BareButton>
|
|
125
|
+
{/each}
|
|
126
|
+
</div>
|
|
127
|
+
</section>
|
|
128
|
+
|
|
129
|
+
<!-- 主题设置 -->
|
|
130
|
+
<section>
|
|
131
|
+
<h4 class='mb-4 flex items-center gap-2 text-sm font-semibold uppercase tracking-wider text-base-content/60'>
|
|
132
|
+
<span class='icon-[tabler--sun] size-4'></span>
|
|
133
|
+
{uiM('settings_theme')}
|
|
134
|
+
</h4>
|
|
135
|
+
<div class='grid gap-2.5 grid-cols-2 sm:grid-cols-3'>
|
|
136
|
+
{#each themes as theme (theme.value)}
|
|
137
|
+
<BareButton
|
|
138
|
+
type='button'
|
|
139
|
+
class='group relative flex flex-col gap-2 rounded-xl border-2 p-3 text-left transition-all duration-200
|
|
140
|
+
hover:border-primary/50 hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40
|
|
141
|
+
{currentTheme === theme.value
|
|
142
|
+
? 'border-primary bg-primary/5 shadow-sm'
|
|
143
|
+
: 'border-base-content/10 bg-base-100 hover:bg-base-50'}'
|
|
144
|
+
onclick={() => onthemechange?.(theme.value)}
|
|
145
|
+
role='option'
|
|
146
|
+
ariaSelected={currentTheme === theme.value}
|
|
147
|
+
>
|
|
148
|
+
{#if currentTheme === theme.value}
|
|
149
|
+
<div class='absolute -right-1 -top-1 rounded-full bg-primary p-0.5 text-primary-content shadow'>
|
|
150
|
+
<span class='icon-[tabler--check] size-3'></span>
|
|
151
|
+
</div>
|
|
152
|
+
{/if}
|
|
153
|
+
|
|
154
|
+
<!-- 主题预览 -->
|
|
155
|
+
<div data-theme={theme.value} class='flex items-center gap-2 rounded-lg bg-base-100 p-2 ring-1 ring-base-content/10' aria-hidden='true'>
|
|
156
|
+
<span class='text-sm font-bold text-base-content'>Aa</span>
|
|
157
|
+
<div class='flex flex-1 gap-1'>
|
|
158
|
+
<span class='h-3 flex-1 rounded bg-primary'></span>
|
|
159
|
+
<span class='h-3 flex-1 rounded bg-secondary'></span>
|
|
160
|
+
<span class='h-3 flex-1 rounded bg-accent'></span>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<span class='text-xs font-medium text-base-content/80 group-hover:text-base-content'>
|
|
165
|
+
{theme.label}
|
|
166
|
+
</span>
|
|
167
|
+
</BareButton>
|
|
168
|
+
{/each}
|
|
169
|
+
</div>
|
|
170
|
+
</section>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
{/if}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
open?: boolean;
|
|
3
|
+
currentLanguage?: string;
|
|
4
|
+
currentTheme?: string;
|
|
5
|
+
languages?: {
|
|
6
|
+
value: string;
|
|
7
|
+
label: string;
|
|
8
|
+
icon?: string;
|
|
9
|
+
}[];
|
|
10
|
+
themes?: {
|
|
11
|
+
value: string;
|
|
12
|
+
label: string;
|
|
13
|
+
}[];
|
|
14
|
+
onlanguagechange?: (lang: string) => void;
|
|
15
|
+
onthemechange?: (theme: string) => void;
|
|
16
|
+
onclose?: () => void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* SettingsModal
|
|
20
|
+
* 应用设置模态框,包含语言和主题切换功能。
|
|
21
|
+
* 内置多语言支持,自动跟随全局 locale
|
|
22
|
+
*
|
|
23
|
+
* @prop {boolean} open - 是否显示模态框(双向绑定)
|
|
24
|
+
* @prop {string} currentLanguage - 当前语言
|
|
25
|
+
* @prop {string} currentTheme - 当前主题
|
|
26
|
+
* @prop {function} onlanguagechange - 语言变更回调
|
|
27
|
+
* @prop {function} onthemechange - 主题变更回调
|
|
28
|
+
*/
|
|
29
|
+
declare const SettingsModal: import("svelte").Component<Props, {}, "open">;
|
|
30
|
+
type SettingsModal = ReturnType<typeof SettingsModal>;
|
|
31
|
+
export default SettingsModal;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component ThemeSelector
|
|
3
|
+
主题选择器组件,支持内置主题集合的选择和预览。
|
|
4
|
+
|
|
5
|
+
@prop {string} currentTheme - 当前主题
|
|
6
|
+
@prop {function} onchange - 主题变更回调
|
|
7
|
+
@prop {boolean} showPreview - 是否显示主题预览
|
|
8
|
+
@prop {boolean} grouped - 是否按分组显示
|
|
9
|
+
|
|
10
|
+
@example
|
|
11
|
+
<ThemeSelector
|
|
12
|
+
currentTheme={$theme}
|
|
13
|
+
onchange={(theme) => setTheme(theme)}
|
|
14
|
+
showPreview
|
|
15
|
+
grouped
|
|
16
|
+
/>
|
|
17
|
+
-->
|
|
18
|
+
<script lang='ts'>
|
|
19
|
+
import type { ThemeInfo } from '../../../theme-config.js'
|
|
20
|
+
import { uiM } from '../../../messages.js'
|
|
21
|
+
import { THEME_GROUPS, THEMES } from '../../../theme-config.js'
|
|
22
|
+
import { cn } from '../../../utils.js'
|
|
23
|
+
import BareButton from '../../primitives/BareButton.svelte'
|
|
24
|
+
|
|
25
|
+
interface Props {
|
|
26
|
+
currentTheme?: string
|
|
27
|
+
/** 选择主题按钮的 aria-label */
|
|
28
|
+
selectLabel?: string
|
|
29
|
+
onchange?: (theme: string) => void
|
|
30
|
+
showPreview?: boolean
|
|
31
|
+
/** 紧凑模式:仅显示预览色块,适用于移动端头部 */
|
|
32
|
+
compact?: boolean
|
|
33
|
+
grouped?: boolean
|
|
34
|
+
class?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const {
|
|
38
|
+
currentTheme = 'light',
|
|
39
|
+
selectLabel,
|
|
40
|
+
onchange,
|
|
41
|
+
showPreview = true,
|
|
42
|
+
compact = false,
|
|
43
|
+
grouped = true,
|
|
44
|
+
class: className = '',
|
|
45
|
+
}: Props = $props()
|
|
46
|
+
|
|
47
|
+
const displaySelectLabel = $derived(selectLabel ?? uiM('theme_selector_label'))
|
|
48
|
+
|
|
49
|
+
let open = $state(false)
|
|
50
|
+
let containerRef = $state<HTMLDivElement | null>(null)
|
|
51
|
+
|
|
52
|
+
const currentInfo = $derived(
|
|
53
|
+
THEMES.find(t => t.id === currentTheme) ?? THEMES[0],
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
function selectTheme(themeId: string) {
|
|
57
|
+
onchange?.(themeId)
|
|
58
|
+
open = false
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function renderThemePreview(theme: ThemeInfo) {
|
|
62
|
+
return `
|
|
63
|
+
background: ${theme.bgColor};
|
|
64
|
+
border-color: ${theme.primaryColor};
|
|
65
|
+
`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function handleClickOutside(event: MouseEvent) {
|
|
69
|
+
if (containerRef && !containerRef.contains(event.target as Node)) {
|
|
70
|
+
open = false
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
$effect(() => {
|
|
75
|
+
if (open) {
|
|
76
|
+
document.addEventListener('click', handleClickOutside)
|
|
77
|
+
return () => document.removeEventListener('click', handleClickOutside)
|
|
78
|
+
}
|
|
79
|
+
return undefined
|
|
80
|
+
})
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<div bind:this={containerRef} class='dropdown dropdown-end {open ? 'dropdown-open' : ''} {className}'>
|
|
84
|
+
<BareButton
|
|
85
|
+
type='button'
|
|
86
|
+
class='btn btn-ghost {compact ? 'btn-sm btn-square' : 'gap-2'}'
|
|
87
|
+
onclick={() => (open = !open)}
|
|
88
|
+
ariaLabel={displaySelectLabel}
|
|
89
|
+
>
|
|
90
|
+
<!-- 当前主题预览 -->
|
|
91
|
+
{#if showPreview}
|
|
92
|
+
<div
|
|
93
|
+
class='w-5 h-5 rounded-full border-2 shrink-0'
|
|
94
|
+
style={renderThemePreview(currentInfo)}
|
|
95
|
+
></div>
|
|
96
|
+
{/if}
|
|
97
|
+
{#if !compact}
|
|
98
|
+
<span class='text-sm'>{currentInfo.name}</span>
|
|
99
|
+
<!-- 下拉箭头 -->
|
|
100
|
+
<svg xmlns='http://www.w3.org/2000/svg' class='h-4 w-4' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'>
|
|
101
|
+
<path stroke-linecap='round' stroke-linejoin='round' d='M19 9l-7 7-7-7' />
|
|
102
|
+
</svg>
|
|
103
|
+
{/if}
|
|
104
|
+
</BareButton>
|
|
105
|
+
|
|
106
|
+
{#if open}
|
|
107
|
+
<!-- 主题列表 -->
|
|
108
|
+
<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
|
+
{#if grouped}
|
|
110
|
+
<!-- 分组显示 -->
|
|
111
|
+
{#each THEME_GROUPS as group}
|
|
112
|
+
<div class='mb-4 last:mb-0'>
|
|
113
|
+
<div class='text-xs font-semibold text-base-content/50 uppercase tracking-wider px-2 mb-2'>
|
|
114
|
+
{uiM(group.nameKey)}
|
|
115
|
+
</div>
|
|
116
|
+
<div class='flex flex-wrap gap-2'>
|
|
117
|
+
{#each group.themes as theme (theme.id)}
|
|
118
|
+
<BareButton
|
|
119
|
+
type='button'
|
|
120
|
+
class={cn(
|
|
121
|
+
'flex items-center gap-2 px-3 py-1.5 rounded-lg text-left transition-colors w-28',
|
|
122
|
+
theme.id === currentTheme
|
|
123
|
+
? 'bg-primary/10 text-primary ring-1 ring-primary/30'
|
|
124
|
+
: 'hover:bg-base-200',
|
|
125
|
+
)}
|
|
126
|
+
onclick={() => selectTheme(theme.id)}
|
|
127
|
+
>
|
|
128
|
+
<!-- 主题预览色块 -->
|
|
129
|
+
<div
|
|
130
|
+
class='w-4 h-4 rounded border shrink-0'
|
|
131
|
+
style={renderThemePreview(theme)}
|
|
132
|
+
></div>
|
|
133
|
+
<!-- 主题名称 -->
|
|
134
|
+
<span class='text-sm whitespace-nowrap'>{theme.name}</span>
|
|
135
|
+
<!-- 选中标记 -->
|
|
136
|
+
{#if theme.id === currentTheme}
|
|
137
|
+
<svg xmlns='http://www.w3.org/2000/svg' class='h-4 w-4 text-primary shrink-0' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'>
|
|
138
|
+
<path stroke-linecap='round' stroke-linejoin='round' d='M5 13l4 4L19 7' />
|
|
139
|
+
</svg>
|
|
140
|
+
{/if}
|
|
141
|
+
</BareButton>
|
|
142
|
+
{/each}
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
{/each}
|
|
146
|
+
{:else}
|
|
147
|
+
<!-- 平铺显示 -->
|
|
148
|
+
<div class='flex flex-wrap gap-2'>
|
|
149
|
+
{#each THEMES as theme (theme.id)}
|
|
150
|
+
<BareButton
|
|
151
|
+
type='button'
|
|
152
|
+
class={cn(
|
|
153
|
+
'flex items-center gap-2 px-3 py-1.5 rounded-lg text-left transition-colors w-28',
|
|
154
|
+
theme.id === currentTheme
|
|
155
|
+
? 'bg-primary/10 text-primary ring-1 ring-primary/30'
|
|
156
|
+
: 'hover:bg-base-200',
|
|
157
|
+
)}
|
|
158
|
+
onclick={() => selectTheme(theme.id)}
|
|
159
|
+
>
|
|
160
|
+
<div
|
|
161
|
+
class='w-4 h-4 rounded border shrink-0'
|
|
162
|
+
style={renderThemePreview(theme)}
|
|
163
|
+
></div>
|
|
164
|
+
<span class='text-sm whitespace-nowrap'>{theme.name}</span>
|
|
165
|
+
{#if theme.id === currentTheme}
|
|
166
|
+
<svg xmlns='http://www.w3.org/2000/svg' class='h-4 w-4 text-primary shrink-0' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'>
|
|
167
|
+
<path stroke-linecap='round' stroke-linejoin='round' d='M5 13l4 4L19 7' />
|
|
168
|
+
</svg>
|
|
169
|
+
{/if}
|
|
170
|
+
</BareButton>
|
|
171
|
+
{/each}
|
|
172
|
+
</div>
|
|
173
|
+
{/if}
|
|
174
|
+
</div>
|
|
175
|
+
{/if}
|
|
176
|
+
</div>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
currentTheme?: string;
|
|
3
|
+
/** 选择主题按钮的 aria-label */
|
|
4
|
+
selectLabel?: string;
|
|
5
|
+
onchange?: (theme: string) => void;
|
|
6
|
+
showPreview?: boolean;
|
|
7
|
+
/** 紧凑模式:仅显示预览色块,适用于移动端头部 */
|
|
8
|
+
compact?: boolean;
|
|
9
|
+
grouped?: boolean;
|
|
10
|
+
class?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* ThemeSelector
|
|
14
|
+
* 主题选择器组件,支持内置主题集合的选择和预览。
|
|
15
|
+
*
|
|
16
|
+
* @prop {string} currentTheme - 当前主题
|
|
17
|
+
* @prop {function} onchange - 主题变更回调
|
|
18
|
+
* @prop {boolean} showPreview - 是否显示主题预览
|
|
19
|
+
* @prop {boolean} grouped - 是否按分组显示
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* <ThemeSelector
|
|
23
|
+
* currentTheme={$theme}
|
|
24
|
+
* onchange={(theme) => setTheme(theme)}
|
|
25
|
+
* showPreview
|
|
26
|
+
* grouped
|
|
27
|
+
* />
|
|
28
|
+
*/
|
|
29
|
+
declare const ThemeSelector: import("svelte").Component<Props, {}, "">;
|
|
30
|
+
type ThemeSelector = ReturnType<typeof ThemeSelector>;
|
|
31
|
+
export default ThemeSelector;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component ThemeToggle
|
|
3
|
+
主题切换按钮组件,支持明/暗主题快速切换。
|
|
4
|
+
|
|
5
|
+
@prop {string} currentTheme - 当前主题
|
|
6
|
+
@prop {function} onchange - 主题变更回调
|
|
7
|
+
@prop {string} lightTheme - 亮色主题名称
|
|
8
|
+
@prop {string} darkTheme - 暗色主题名称
|
|
9
|
+
|
|
10
|
+
@example
|
|
11
|
+
<ThemeToggle
|
|
12
|
+
currentTheme={$theme}
|
|
13
|
+
onchange={(theme) => themeStore.set(theme)}
|
|
14
|
+
/>
|
|
15
|
+
-->
|
|
16
|
+
<script lang='ts'>
|
|
17
|
+
import { uiM } from '../../../messages.js'
|
|
18
|
+
import { isDarkTheme } from '../../../theme-config.js'
|
|
19
|
+
import IconButton from '../../primitives/IconButton.svelte'
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
currentTheme?: string
|
|
23
|
+
onchange?: (theme: string) => void
|
|
24
|
+
lightTheme?: string
|
|
25
|
+
darkTheme?: string
|
|
26
|
+
labels?: {
|
|
27
|
+
switchToLight?: string
|
|
28
|
+
switchToDark?: string
|
|
29
|
+
}
|
|
30
|
+
class?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
currentTheme = 'light',
|
|
35
|
+
onchange,
|
|
36
|
+
lightTheme = 'light',
|
|
37
|
+
darkTheme = 'dark',
|
|
38
|
+
labels = {},
|
|
39
|
+
class: className = '',
|
|
40
|
+
}: Props = $props()
|
|
41
|
+
|
|
42
|
+
// labels 优先,缺省回退到内置文案
|
|
43
|
+
const isDark = $derived(isDarkTheme(currentTheme))
|
|
44
|
+
|
|
45
|
+
function toggleTheme() {
|
|
46
|
+
const newTheme = isDark ? lightTheme : darkTheme
|
|
47
|
+
onchange?.(newTheme)
|
|
48
|
+
}
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<div class='tooltip tooltip-bottom {className}' data-tip={isDark ? (labels.switchToLight ?? uiM('theme_switch_to_light')) : (labels.switchToDark ?? uiM('theme_switch_to_dark'))}>
|
|
52
|
+
<IconButton
|
|
53
|
+
variant='ghost'
|
|
54
|
+
size='sm'
|
|
55
|
+
onclick={toggleTheme}
|
|
56
|
+
ariaLabel={isDark ? (labels.switchToLight ?? uiM('theme_switch_to_light')) : (labels.switchToDark ?? uiM('theme_switch_to_dark'))}
|
|
57
|
+
>
|
|
58
|
+
{#if isDark}
|
|
59
|
+
<svg xmlns='http://www.w3.org/2000/svg' class='h-5 w-5' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'>
|
|
60
|
+
<circle cx='12' cy='12' r='5' />
|
|
61
|
+
<line x1='12' y1='1' x2='12' y2='3' />
|
|
62
|
+
<line x1='12' y1='21' x2='12' y2='23' />
|
|
63
|
+
<line x1='4.22' y1='4.22' x2='5.64' y2='5.64' />
|
|
64
|
+
<line x1='18.36' y1='18.36' x2='19.78' y2='19.78' />
|
|
65
|
+
<line x1='1' y1='12' x2='3' y2='12' />
|
|
66
|
+
<line x1='21' y1='12' x2='23' y2='12' />
|
|
67
|
+
<line x1='4.22' y1='19.78' x2='5.64' y2='18.36' />
|
|
68
|
+
<line x1='18.36' y1='5.64' x2='19.78' y2='4.22' />
|
|
69
|
+
</svg>
|
|
70
|
+
{:else}
|
|
71
|
+
<svg xmlns='http://www.w3.org/2000/svg' class='h-5 w-5' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'>
|
|
72
|
+
<path d='M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z' />
|
|
73
|
+
</svg>
|
|
74
|
+
{/if}
|
|
75
|
+
</IconButton>
|
|
76
|
+
</div>
|