@miozu/jera 0.8.1 → 0.8.3
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/CLAUDE.md +21 -6
- package/README.md +53 -8
- package/package.json +1 -1
- package/src/components/layout/SettingCard.svelte +11 -82
- package/src/components/layout/SettingItem.svelte +116 -0
- package/src/components/navigation/TabNav.svelte +27 -1
- package/src/components/navigation/Tabs.svelte +20 -20
- package/src/components/primitives/ThemeSelect.svelte +260 -0
- package/src/components/primitives/ThemeToggle.svelte +263 -0
- package/src/index.js +3 -0
package/CLAUDE.md
CHANGED
|
@@ -58,13 +58,28 @@ Singleton pattern with `miozu-theme` storage key.
|
|
|
58
58
|
|
|
59
59
|
**Full reference:** `docs/ai-context/theme-management.md`
|
|
60
60
|
|
|
61
|
+
**ThemeToggle:** Accessible toggle with animated sun/moon icons
|
|
61
62
|
```svelte
|
|
62
|
-
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
import { ThemeToggle } from '@miozu/jera';
|
|
64
|
+
<ThemeToggle />
|
|
65
|
+
<ThemeToggle size="sm" variant="outline" />
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**ThemeSelect:** Three-option selector (light/dark/system)
|
|
69
|
+
```svelte
|
|
70
|
+
import { ThemeSelect } from '@miozu/jera';
|
|
71
|
+
<ThemeSelect />
|
|
72
|
+
<ThemeSelect variant="dropdown" />
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**ThemeState API:**
|
|
76
|
+
```javascript
|
|
77
|
+
import { getTheme } from '@miozu/jera';
|
|
78
|
+
const theme = getTheme();
|
|
79
|
+
theme.init(); // Call once in root onMount
|
|
80
|
+
theme.toggle(); // Switch dark/light
|
|
81
|
+
theme.set('system'); // Follow OS preference
|
|
82
|
+
theme.isDark; // boolean reactive property
|
|
68
83
|
```
|
|
69
84
|
|
|
70
85
|
## Svelte 5 Patterns
|
package/README.md
CHANGED
|
@@ -176,23 +176,68 @@ button({ variant: 'secondary' }); // => "inline-flex items-center bg-surface h-1
|
|
|
176
176
|
|
|
177
177
|
Dark theme is default. Uses singleton pattern with `miozu-theme` storage key.
|
|
178
178
|
|
|
179
|
-
|
|
180
|
-
// In root +layout.svelte
|
|
181
|
-
import { getTheme } from '@miozu/jera';
|
|
182
|
-
import { onMount } from 'svelte';
|
|
179
|
+
### Setup
|
|
183
180
|
|
|
184
|
-
|
|
185
|
-
|
|
181
|
+
```svelte
|
|
182
|
+
<!-- +layout.svelte -->
|
|
183
|
+
<script>
|
|
184
|
+
import { getTheme } from '@miozu/jera';
|
|
185
|
+
import { onMount } from 'svelte';
|
|
186
|
+
|
|
187
|
+
const theme = getTheme();
|
|
188
|
+
onMount(() => theme.init());
|
|
189
|
+
</script>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### ThemeToggle
|
|
193
|
+
|
|
194
|
+
Accessible toggle button with animated sun/moon icons.
|
|
195
|
+
|
|
196
|
+
```svelte
|
|
197
|
+
<script>
|
|
198
|
+
import { ThemeToggle } from '@miozu/jera';
|
|
199
|
+
</script>
|
|
200
|
+
|
|
201
|
+
<ThemeToggle />
|
|
202
|
+
<ThemeToggle size="sm" variant="outline" />
|
|
203
|
+
<ThemeToggle size="lg" variant="subtle" />
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Props: `size` (sm|md|lg), `variant` (ghost|outline|subtle), `themeState` (optional)
|
|
207
|
+
|
|
208
|
+
### ThemeSelect
|
|
209
|
+
|
|
210
|
+
Three-option selector for light/dark/system preference.
|
|
211
|
+
|
|
212
|
+
```svelte
|
|
213
|
+
<script>
|
|
214
|
+
import { ThemeSelect } from '@miozu/jera';
|
|
215
|
+
</script>
|
|
216
|
+
|
|
217
|
+
<!-- Segmented control (default) -->
|
|
218
|
+
<ThemeSelect />
|
|
219
|
+
|
|
220
|
+
<!-- Dropdown -->
|
|
221
|
+
<ThemeSelect variant="dropdown" />
|
|
222
|
+
|
|
223
|
+
<!-- Custom labels -->
|
|
224
|
+
<ThemeSelect labels={{ light: 'Light', dark: 'Dark', system: 'Auto' }} />
|
|
186
225
|
```
|
|
187
226
|
|
|
227
|
+
Props: `variant` (segmented|dropdown), `size` (sm|md|lg), `labels`, `showIcons`
|
|
228
|
+
|
|
229
|
+
### ThemeState API
|
|
230
|
+
|
|
188
231
|
```javascript
|
|
189
|
-
// Anywhere in your app
|
|
190
232
|
import { getTheme } from '@miozu/jera';
|
|
191
233
|
|
|
192
234
|
const theme = getTheme();
|
|
193
235
|
theme.toggle(); // Switch between light/dark
|
|
194
236
|
theme.set('system'); // Follow system preference
|
|
195
|
-
theme.isDark; // boolean
|
|
237
|
+
theme.isDark; // boolean - resolved dark mode
|
|
238
|
+
theme.isLight; // boolean - resolved light mode
|
|
239
|
+
theme.current; // 'light' | 'dark' | 'system'
|
|
240
|
+
theme.dataTheme; // 'miozu-light' | 'miozu-dark'
|
|
196
241
|
```
|
|
197
242
|
|
|
198
243
|
Data-theme values: `miozu-dark` (default) or `miozu-light`.
|
package/package.json
CHANGED
|
@@ -2,28 +2,24 @@
|
|
|
2
2
|
@component SettingCard
|
|
3
3
|
|
|
4
4
|
A card container for settings sections with optional danger variant.
|
|
5
|
-
|
|
5
|
+
Use with SettingItem for structured setting rows.
|
|
6
6
|
|
|
7
7
|
@example Basic settings card
|
|
8
8
|
<SettingCard title="Account Settings">
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
<Input value={name} />
|
|
15
|
-
</div>
|
|
9
|
+
<SettingItem label="Display Name" description="Your public display name">
|
|
10
|
+
{#snippet action()}
|
|
11
|
+
<Input value={name} />
|
|
12
|
+
{/snippet}
|
|
13
|
+
</SettingItem>
|
|
16
14
|
</SettingCard>
|
|
17
15
|
|
|
18
16
|
@example Danger zone
|
|
19
17
|
<SettingCard title="Danger Zone" variant="danger">
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
<Button variant="danger">Delete</Button>
|
|
26
|
-
</div>
|
|
18
|
+
<SettingItem label="Delete Account" description="This cannot be undone">
|
|
19
|
+
{#snippet action()}
|
|
20
|
+
<Button variant="danger">Delete</Button>
|
|
21
|
+
{/snippet}
|
|
22
|
+
</SettingItem>
|
|
27
23
|
</SettingCard>
|
|
28
24
|
-->
|
|
29
25
|
<script>
|
|
@@ -83,71 +79,4 @@
|
|
|
83
79
|
display: flex;
|
|
84
80
|
flex-direction: column;
|
|
85
81
|
}
|
|
86
|
-
|
|
87
|
-
/* Utility classes for setting items - exposed globally */
|
|
88
|
-
:global(.setting-item) {
|
|
89
|
-
display: flex;
|
|
90
|
-
align-items: center;
|
|
91
|
-
justify-content: space-between;
|
|
92
|
-
gap: var(--space-4);
|
|
93
|
-
padding: var(--space-4) 0;
|
|
94
|
-
border-bottom: 1px solid var(--color-base02);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
:global(.setting-item:last-child) {
|
|
98
|
-
border-bottom: none;
|
|
99
|
-
padding-bottom: 0;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
:global(.setting-item:first-child) {
|
|
103
|
-
padding-top: 0;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
:global(.setting-content) {
|
|
107
|
-
flex: 1;
|
|
108
|
-
min-width: 0;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
:global(.setting-label) {
|
|
112
|
-
margin: 0 0 var(--space-1);
|
|
113
|
-
font-size: var(--text-sm);
|
|
114
|
-
font-weight: 500;
|
|
115
|
-
color: var(--color-base06);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
:global(.setting-description) {
|
|
119
|
-
margin: 0;
|
|
120
|
-
font-size: var(--text-xs);
|
|
121
|
-
color: var(--color-base04);
|
|
122
|
-
line-height: 1.5;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
:global(.setting-item-icon) {
|
|
126
|
-
display: flex;
|
|
127
|
-
align-items: center;
|
|
128
|
-
gap: var(--space-3);
|
|
129
|
-
color: color-mix(in srgb, var(--color-base04) 80%, transparent);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
:global(.setting-item-with-icon) {
|
|
133
|
-
display: flex;
|
|
134
|
-
align-items: flex-start;
|
|
135
|
-
gap: var(--space-3);
|
|
136
|
-
padding: var(--space-4) 0;
|
|
137
|
-
border-bottom: 1px solid var(--color-base02);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
:global(.setting-item-with-icon:last-child) {
|
|
141
|
-
border-bottom: none;
|
|
142
|
-
padding-bottom: 0;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/* Responsive */
|
|
146
|
-
@media (max-width: 640px) {
|
|
147
|
-
:global(.setting-item) {
|
|
148
|
-
flex-direction: column;
|
|
149
|
-
align-items: flex-start;
|
|
150
|
-
gap: var(--space-3);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
82
|
</style>
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component SettingItem
|
|
3
|
+
|
|
4
|
+
A structured setting row for use inside SettingCard.
|
|
5
|
+
Replaces magic class names with a composable component API.
|
|
6
|
+
|
|
7
|
+
@example Basic setting
|
|
8
|
+
<SettingItem label="Display Name" description="Your public display name">
|
|
9
|
+
{#snippet action()}
|
|
10
|
+
<Input value={name} />
|
|
11
|
+
{/snippet}
|
|
12
|
+
</SettingItem>
|
|
13
|
+
|
|
14
|
+
@example With icon
|
|
15
|
+
<SettingItem label="Active Sessions" description="Manage your devices">
|
|
16
|
+
{#snippet leading()}
|
|
17
|
+
<Monitor size={16} />
|
|
18
|
+
{/snippet}
|
|
19
|
+
{#snippet action()}
|
|
20
|
+
<Button size="sm">Manage</Button>
|
|
21
|
+
{/snippet}
|
|
22
|
+
</SettingItem>
|
|
23
|
+
-->
|
|
24
|
+
<script>
|
|
25
|
+
let {
|
|
26
|
+
label = '',
|
|
27
|
+
description = '',
|
|
28
|
+
leading,
|
|
29
|
+
action,
|
|
30
|
+
class: className = ''
|
|
31
|
+
} = $props();
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<div class="setting-item {className}" class:has-leading={leading}>
|
|
35
|
+
{#if leading}
|
|
36
|
+
<div class="setting-leading">
|
|
37
|
+
{@render leading()}
|
|
38
|
+
</div>
|
|
39
|
+
{/if}
|
|
40
|
+
<div class="setting-content">
|
|
41
|
+
{#if label}
|
|
42
|
+
<h4 class="setting-label">{label}</h4>
|
|
43
|
+
{/if}
|
|
44
|
+
{#if description}
|
|
45
|
+
<p class="setting-description">{description}</p>
|
|
46
|
+
{/if}
|
|
47
|
+
</div>
|
|
48
|
+
{#if action}
|
|
49
|
+
<div class="setting-action">
|
|
50
|
+
{@render action()}
|
|
51
|
+
</div>
|
|
52
|
+
{/if}
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<style>
|
|
56
|
+
.setting-item {
|
|
57
|
+
display: flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
justify-content: space-between;
|
|
60
|
+
gap: var(--space-4);
|
|
61
|
+
padding: var(--space-4) 0;
|
|
62
|
+
border-bottom: 1px solid var(--color-base02);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.setting-item:last-child {
|
|
66
|
+
border-bottom: none;
|
|
67
|
+
padding-bottom: 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.setting-item:first-child {
|
|
71
|
+
padding-top: 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.has-leading {
|
|
75
|
+
justify-content: flex-start;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.setting-leading {
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
color: color-mix(in srgb, var(--color-base04) 80%, transparent);
|
|
82
|
+
flex-shrink: 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.setting-content {
|
|
86
|
+
flex: 1;
|
|
87
|
+
min-width: 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.setting-label {
|
|
91
|
+
margin: 0 0 var(--space-1);
|
|
92
|
+
font-size: var(--text-sm);
|
|
93
|
+
font-weight: 500;
|
|
94
|
+
color: var(--color-base06);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.setting-description {
|
|
98
|
+
margin: 0;
|
|
99
|
+
font-size: var(--text-xs);
|
|
100
|
+
color: var(--color-base04);
|
|
101
|
+
line-height: 1.5;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.setting-action {
|
|
105
|
+
flex-shrink: 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* Responsive */
|
|
109
|
+
@media (max-width: 640px) {
|
|
110
|
+
.setting-item:not(.has-leading) {
|
|
111
|
+
flex-direction: column;
|
|
112
|
+
align-items: flex-start;
|
|
113
|
+
gap: var(--space-3);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
</style>
|
|
@@ -45,10 +45,34 @@
|
|
|
45
45
|
active = tab.id;
|
|
46
46
|
onchange?.(tab);
|
|
47
47
|
}
|
|
48
|
+
|
|
49
|
+
function handleKeydown(e, index) {
|
|
50
|
+
let nextIndex = index;
|
|
51
|
+
|
|
52
|
+
if (e.key === 'ArrowRight') {
|
|
53
|
+
nextIndex = (index + 1) % tabs.length;
|
|
54
|
+
} else if (e.key === 'ArrowLeft') {
|
|
55
|
+
nextIndex = (index - 1 + tabs.length) % tabs.length;
|
|
56
|
+
} else if (e.key === 'Home') {
|
|
57
|
+
nextIndex = 0;
|
|
58
|
+
} else if (e.key === 'End') {
|
|
59
|
+
nextIndex = tabs.length - 1;
|
|
60
|
+
} else {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
const nextTab = tabs[nextIndex];
|
|
66
|
+
if (!nextTab.disabled) {
|
|
67
|
+
handleTabClick(nextTab);
|
|
68
|
+
const buttons = e.currentTarget.parentElement.querySelectorAll('[role="tab"]');
|
|
69
|
+
buttons[nextIndex]?.focus();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
48
72
|
</script>
|
|
49
73
|
|
|
50
74
|
<nav class="tab-nav tab-nav-{variant} tab-nav-{size} {className}" role="tablist">
|
|
51
|
-
{#each tabs as tab}
|
|
75
|
+
{#each tabs as tab, index}
|
|
52
76
|
<button
|
|
53
77
|
type="button"
|
|
54
78
|
class="tab-item"
|
|
@@ -57,7 +81,9 @@
|
|
|
57
81
|
role="tab"
|
|
58
82
|
aria-selected={active === tab.id}
|
|
59
83
|
aria-disabled={tab.disabled}
|
|
84
|
+
tabindex={active === tab.id ? 0 : -1}
|
|
60
85
|
onclick={() => handleTabClick(tab)}
|
|
86
|
+
onkeydown={(e) => handleKeydown(e, index)}
|
|
61
87
|
>
|
|
62
88
|
{#if tab.icon}
|
|
63
89
|
<span class="tab-icon">
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component Tabs
|
|
3
3
|
|
|
4
|
-
Tabbed navigation component.
|
|
4
|
+
Tabbed navigation component with keyboard navigation.
|
|
5
5
|
|
|
6
6
|
@example Basic usage
|
|
7
7
|
<Tabs
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
bind:active={activeTab}
|
|
14
14
|
/>
|
|
15
15
|
|
|
16
|
-
@example With icons
|
|
16
|
+
@example With icons (component reference)
|
|
17
17
|
<Tabs
|
|
18
18
|
tabs={[
|
|
19
19
|
{ id: 'home', label: 'Home', icon: HomeIcon },
|
|
@@ -64,7 +64,6 @@
|
|
|
64
64
|
const nextTab = tabs[nextIndex];
|
|
65
65
|
if (!nextTab.disabled) {
|
|
66
66
|
selectTab(nextTab);
|
|
67
|
-
// Focus the next tab button
|
|
68
67
|
const buttons = e.currentTarget.parentElement.querySelectorAll('[role="tab"]');
|
|
69
68
|
buttons[nextIndex]?.focus();
|
|
70
69
|
}
|
|
@@ -90,8 +89,9 @@
|
|
|
90
89
|
onkeydown={(e) => handleKeydown(e, tab, index)}
|
|
91
90
|
>
|
|
92
91
|
{#if tab.icon}
|
|
92
|
+
{@const Icon = tab.icon}
|
|
93
93
|
<span class="tab-icon">
|
|
94
|
-
<
|
|
94
|
+
<Icon size={16} />
|
|
95
95
|
</span>
|
|
96
96
|
{/if}
|
|
97
97
|
{#if tab.label}
|
|
@@ -107,10 +107,10 @@
|
|
|
107
107
|
<style>
|
|
108
108
|
.tabs {
|
|
109
109
|
display: inline-flex;
|
|
110
|
-
gap:
|
|
110
|
+
gap: var(--space-1);
|
|
111
111
|
background: var(--color-base01);
|
|
112
|
-
border-radius:
|
|
113
|
-
padding:
|
|
112
|
+
border-radius: var(--radius-lg);
|
|
113
|
+
padding: var(--space-1);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
.tabs-full-width {
|
|
@@ -126,12 +126,12 @@
|
|
|
126
126
|
.tab {
|
|
127
127
|
display: inline-flex;
|
|
128
128
|
align-items: center;
|
|
129
|
-
gap:
|
|
130
|
-
padding:
|
|
129
|
+
gap: var(--space-2);
|
|
130
|
+
padding: var(--space-2) var(--space-4);
|
|
131
131
|
background: transparent;
|
|
132
132
|
border: none;
|
|
133
|
-
border-radius:
|
|
134
|
-
font-size:
|
|
133
|
+
border-radius: var(--radius-md);
|
|
134
|
+
font-size: var(--text-sm);
|
|
135
135
|
font-weight: 500;
|
|
136
136
|
color: var(--color-base05);
|
|
137
137
|
cursor: pointer;
|
|
@@ -141,13 +141,13 @@
|
|
|
141
141
|
|
|
142
142
|
/* Size variants */
|
|
143
143
|
.tabs-sm .tab {
|
|
144
|
-
padding:
|
|
145
|
-
font-size:
|
|
144
|
+
padding: var(--space-1) var(--space-3);
|
|
145
|
+
font-size: var(--text-xs);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
.tabs-lg .tab {
|
|
149
|
-
padding:
|
|
150
|
-
font-size:
|
|
149
|
+
padding: var(--space-3) var(--space-5);
|
|
150
|
+
font-size: var(--text-base);
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
.tab:hover:not(.tab-disabled) {
|
|
@@ -180,12 +180,12 @@
|
|
|
180
180
|
display: inline-flex;
|
|
181
181
|
align-items: center;
|
|
182
182
|
justify-content: center;
|
|
183
|
-
min-width:
|
|
184
|
-
height:
|
|
185
|
-
padding: 0
|
|
183
|
+
min-width: var(--space-5);
|
|
184
|
+
height: var(--space-5);
|
|
185
|
+
padding: 0 var(--space-1);
|
|
186
186
|
background: var(--color-base03);
|
|
187
187
|
border-radius: 9999px;
|
|
188
|
-
font-size:
|
|
188
|
+
font-size: var(--text-xs);
|
|
189
189
|
font-weight: 600;
|
|
190
190
|
}
|
|
191
191
|
|
|
@@ -224,7 +224,7 @@
|
|
|
224
224
|
.tabs-pills {
|
|
225
225
|
background: transparent;
|
|
226
226
|
padding: 0;
|
|
227
|
-
gap:
|
|
227
|
+
gap: var(--space-2);
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
.tabs-pills .tab {
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component ThemeSelect
|
|
3
|
+
|
|
4
|
+
A dropdown/segmented selector for theme preference with light/dark/system options.
|
|
5
|
+
Uses the Jera ThemeState singleton for theme management.
|
|
6
|
+
|
|
7
|
+
@example Basic dropdown
|
|
8
|
+
<ThemeSelect />
|
|
9
|
+
|
|
10
|
+
@example Segmented control style
|
|
11
|
+
<ThemeSelect variant="segmented" />
|
|
12
|
+
|
|
13
|
+
@example With custom theme state
|
|
14
|
+
<ThemeSelect themeState={myThemeState} />
|
|
15
|
+
|
|
16
|
+
@example Custom labels
|
|
17
|
+
<ThemeSelect
|
|
18
|
+
labels={{ light: 'Light', dark: 'Dark', system: 'Auto' }}
|
|
19
|
+
/>
|
|
20
|
+
-->
|
|
21
|
+
<script>
|
|
22
|
+
import { cn } from '../../utils/cn.svelte.js';
|
|
23
|
+
import { getTheme, generateId } from '../../utils/reactive.svelte.js';
|
|
24
|
+
|
|
25
|
+
let {
|
|
26
|
+
themeState,
|
|
27
|
+
variant = 'segmented',
|
|
28
|
+
size = 'md',
|
|
29
|
+
labels = { light: 'Light', dark: 'Dark', system: 'System' },
|
|
30
|
+
showIcons = true,
|
|
31
|
+
class: className = '',
|
|
32
|
+
...rest
|
|
33
|
+
} = $props();
|
|
34
|
+
|
|
35
|
+
// Use provided themeState or fall back to singleton
|
|
36
|
+
const theme = themeState ?? getTheme();
|
|
37
|
+
const groupId = generateId();
|
|
38
|
+
|
|
39
|
+
// Current preference (not resolved)
|
|
40
|
+
const current = $derived(theme.current);
|
|
41
|
+
|
|
42
|
+
// Size mappings
|
|
43
|
+
const sizes = {
|
|
44
|
+
sm: { wrapper: 'theme-select-sm', icon: 14 },
|
|
45
|
+
md: { wrapper: 'theme-select-md', icon: 16 },
|
|
46
|
+
lg: { wrapper: 'theme-select-lg', icon: 18 }
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const currentSize = $derived(sizes[size] || sizes.md);
|
|
50
|
+
|
|
51
|
+
const options = [
|
|
52
|
+
{ value: 'light', label: labels.light, icon: 'sun' },
|
|
53
|
+
{ value: 'dark', label: labels.dark, icon: 'moon' },
|
|
54
|
+
{ value: 'system', label: labels.system, icon: 'monitor' }
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
function handleChange(value) {
|
|
58
|
+
theme.set(value);
|
|
59
|
+
}
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
{#if variant === 'segmented'}
|
|
63
|
+
<div
|
|
64
|
+
class={cn('theme-select-segmented', currentSize.wrapper, className)}
|
|
65
|
+
role="radiogroup"
|
|
66
|
+
aria-label="Theme preference"
|
|
67
|
+
{...rest}
|
|
68
|
+
>
|
|
69
|
+
{#each options as option (option.value)}
|
|
70
|
+
{@const isActive = current === option.value}
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
role="radio"
|
|
74
|
+
aria-checked={isActive}
|
|
75
|
+
class={cn('theme-select-option', isActive && 'theme-select-option-active')}
|
|
76
|
+
onclick={() => handleChange(option.value)}
|
|
77
|
+
>
|
|
78
|
+
{#if showIcons}
|
|
79
|
+
<span class="theme-select-icon" aria-hidden="true">
|
|
80
|
+
{#if option.icon === 'sun'}
|
|
81
|
+
<svg width={currentSize.icon} height={currentSize.icon} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
82
|
+
<circle cx="12" cy="12" r="4" />
|
|
83
|
+
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41" />
|
|
84
|
+
</svg>
|
|
85
|
+
{:else if option.icon === 'moon'}
|
|
86
|
+
<svg width={currentSize.icon} height={currentSize.icon} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
87
|
+
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
|
|
88
|
+
</svg>
|
|
89
|
+
{:else if option.icon === 'monitor'}
|
|
90
|
+
<svg width={currentSize.icon} height={currentSize.icon} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
91
|
+
<rect width="20" height="14" x="2" y="3" rx="2" />
|
|
92
|
+
<line x1="8" x2="16" y1="21" y2="21" />
|
|
93
|
+
<line x1="12" x2="12" y1="17" y2="21" />
|
|
94
|
+
</svg>
|
|
95
|
+
{/if}
|
|
96
|
+
</span>
|
|
97
|
+
{/if}
|
|
98
|
+
<span class="theme-select-label">{option.label}</span>
|
|
99
|
+
</button>
|
|
100
|
+
{/each}
|
|
101
|
+
</div>
|
|
102
|
+
{:else}
|
|
103
|
+
<!-- Dropdown variant -->
|
|
104
|
+
<div class={cn('theme-select-dropdown', currentSize.wrapper, className)} {...rest}>
|
|
105
|
+
<select
|
|
106
|
+
id={groupId}
|
|
107
|
+
value={current}
|
|
108
|
+
onchange={(e) => handleChange(e.target.value)}
|
|
109
|
+
aria-label="Theme preference"
|
|
110
|
+
class="theme-select-native"
|
|
111
|
+
>
|
|
112
|
+
{#each options as option (option.value)}
|
|
113
|
+
<option value={option.value}>{option.label}</option>
|
|
114
|
+
{/each}
|
|
115
|
+
</select>
|
|
116
|
+
<span class="theme-select-dropdown-icon" aria-hidden="true">
|
|
117
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
118
|
+
<path d="m6 9 6 6 6-6" />
|
|
119
|
+
</svg>
|
|
120
|
+
</span>
|
|
121
|
+
</div>
|
|
122
|
+
{/if}
|
|
123
|
+
|
|
124
|
+
<style>
|
|
125
|
+
/* ============================================
|
|
126
|
+
SEGMENTED CONTROL
|
|
127
|
+
============================================ */
|
|
128
|
+
.theme-select-segmented {
|
|
129
|
+
display: inline-flex;
|
|
130
|
+
align-items: center;
|
|
131
|
+
background-color: var(--color-base01, #282828);
|
|
132
|
+
border-radius: var(--radius-md, 0.375rem);
|
|
133
|
+
padding: 2px;
|
|
134
|
+
gap: 2px;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.theme-select-option {
|
|
138
|
+
display: inline-flex;
|
|
139
|
+
align-items: center;
|
|
140
|
+
justify-content: center;
|
|
141
|
+
gap: var(--space-1, 0.25rem);
|
|
142
|
+
padding: var(--space-1, 0.25rem) var(--space-3, 0.75rem);
|
|
143
|
+
border: none;
|
|
144
|
+
border-radius: calc(var(--radius-md, 0.375rem) - 2px);
|
|
145
|
+
background-color: transparent;
|
|
146
|
+
color: var(--color-base04, #665c54);
|
|
147
|
+
font-size: var(--text-sm, 0.875rem);
|
|
148
|
+
font-weight: 500;
|
|
149
|
+
cursor: pointer;
|
|
150
|
+
transition:
|
|
151
|
+
background-color var(--duration-fast, 150ms) var(--ease-out, ease-out),
|
|
152
|
+
color var(--duration-fast, 150ms) var(--ease-out, ease-out);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.theme-select-option:hover:not(.theme-select-option-active) {
|
|
156
|
+
color: var(--color-base05, #a89984);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.theme-select-option:focus-visible {
|
|
160
|
+
outline: 2px solid var(--color-base0D, #83a598);
|
|
161
|
+
outline-offset: -2px;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.theme-select-option-active {
|
|
165
|
+
background-color: var(--color-base02, #3c3836);
|
|
166
|
+
color: var(--color-base06, #d5c4a1);
|
|
167
|
+
box-shadow: var(--shadow-sm, 0 1px 2px rgba(0, 0, 0, 0.1));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.theme-select-icon {
|
|
171
|
+
display: flex;
|
|
172
|
+
align-items: center;
|
|
173
|
+
justify-content: center;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.theme-select-label {
|
|
177
|
+
white-space: nowrap;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* ============================================
|
|
181
|
+
SIZES - SEGMENTED
|
|
182
|
+
============================================ */
|
|
183
|
+
.theme-select-sm .theme-select-option {
|
|
184
|
+
padding: 2px var(--space-2, 0.5rem);
|
|
185
|
+
font-size: var(--text-xs, 0.75rem);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.theme-select-md .theme-select-option {
|
|
189
|
+
padding: var(--space-1, 0.25rem) var(--space-3, 0.75rem);
|
|
190
|
+
font-size: var(--text-sm, 0.875rem);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.theme-select-lg .theme-select-option {
|
|
194
|
+
padding: var(--space-2, 0.5rem) var(--space-4, 1rem);
|
|
195
|
+
font-size: var(--text-base, 1rem);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* ============================================
|
|
199
|
+
DROPDOWN
|
|
200
|
+
============================================ */
|
|
201
|
+
.theme-select-dropdown {
|
|
202
|
+
position: relative;
|
|
203
|
+
display: inline-flex;
|
|
204
|
+
align-items: center;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.theme-select-native {
|
|
208
|
+
appearance: none;
|
|
209
|
+
background-color: var(--color-base01, #282828);
|
|
210
|
+
border: 1px solid var(--color-base03, #504945);
|
|
211
|
+
border-radius: var(--radius-md, 0.375rem);
|
|
212
|
+
color: var(--color-base06, #d5c4a1);
|
|
213
|
+
font-size: var(--text-sm, 0.875rem);
|
|
214
|
+
padding: var(--space-2, 0.5rem) var(--space-8, 2rem) var(--space-2, 0.5rem) var(--space-3, 0.75rem);
|
|
215
|
+
cursor: pointer;
|
|
216
|
+
transition:
|
|
217
|
+
border-color var(--duration-fast, 150ms) var(--ease-out, ease-out),
|
|
218
|
+
background-color var(--duration-fast, 150ms) var(--ease-out, ease-out);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.theme-select-native:hover {
|
|
222
|
+
border-color: var(--color-base04, #665c54);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.theme-select-native:focus {
|
|
226
|
+
outline: none;
|
|
227
|
+
border-color: var(--color-base0D, #83a598);
|
|
228
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-base0D, #83a598) 20%, transparent);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.theme-select-dropdown-icon {
|
|
232
|
+
position: absolute;
|
|
233
|
+
right: var(--space-2, 0.5rem);
|
|
234
|
+
pointer-events: none;
|
|
235
|
+
color: var(--color-base04, #665c54);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* ============================================
|
|
239
|
+
SIZES - DROPDOWN
|
|
240
|
+
============================================ */
|
|
241
|
+
.theme-select-dropdown.theme-select-sm .theme-select-native {
|
|
242
|
+
padding: var(--space-1, 0.25rem) var(--space-6, 1.5rem) var(--space-1, 0.25rem) var(--space-2, 0.5rem);
|
|
243
|
+
font-size: var(--text-xs, 0.75rem);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.theme-select-dropdown.theme-select-lg .theme-select-native {
|
|
247
|
+
padding: var(--space-3, 0.75rem) var(--space-10, 2.5rem) var(--space-3, 0.75rem) var(--space-4, 1rem);
|
|
248
|
+
font-size: var(--text-base, 1rem);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* ============================================
|
|
252
|
+
REDUCED MOTION
|
|
253
|
+
============================================ */
|
|
254
|
+
@media (prefers-reduced-motion: reduce) {
|
|
255
|
+
.theme-select-option,
|
|
256
|
+
.theme-select-native {
|
|
257
|
+
transition: none;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
</style>
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component ThemeToggle
|
|
3
|
+
|
|
4
|
+
An accessible theme toggle button with animated sun/moon icons.
|
|
5
|
+
Uses the Jera ThemeState singleton for theme management.
|
|
6
|
+
|
|
7
|
+
@example Basic usage
|
|
8
|
+
<ThemeToggle />
|
|
9
|
+
|
|
10
|
+
@example With custom theme state
|
|
11
|
+
<ThemeToggle themeState={myThemeState} />
|
|
12
|
+
|
|
13
|
+
@example Different sizes
|
|
14
|
+
<ThemeToggle size="sm" />
|
|
15
|
+
<ThemeToggle size="lg" />
|
|
16
|
+
|
|
17
|
+
@example With label
|
|
18
|
+
<ThemeToggle>
|
|
19
|
+
{#snippet label()}Dark mode{/snippet}
|
|
20
|
+
</ThemeToggle>
|
|
21
|
+
|
|
22
|
+
@example Custom icons via snippets
|
|
23
|
+
<ThemeToggle>
|
|
24
|
+
{#snippet sunIcon()}<MyCustomSun />{/snippet}
|
|
25
|
+
{#snippet moonIcon()}<MyCustomMoon />{/snippet}
|
|
26
|
+
</ThemeToggle>
|
|
27
|
+
-->
|
|
28
|
+
<script>
|
|
29
|
+
import { cn } from '../../utils/cn.svelte.js';
|
|
30
|
+
import { getTheme } from '../../utils/reactive.svelte.js';
|
|
31
|
+
|
|
32
|
+
let {
|
|
33
|
+
themeState,
|
|
34
|
+
size = 'md',
|
|
35
|
+
variant = 'ghost',
|
|
36
|
+
class: className = '',
|
|
37
|
+
sunIcon,
|
|
38
|
+
moonIcon,
|
|
39
|
+
label,
|
|
40
|
+
...rest
|
|
41
|
+
} = $props();
|
|
42
|
+
|
|
43
|
+
// Use provided themeState or fall back to singleton
|
|
44
|
+
const theme = themeState ?? getTheme();
|
|
45
|
+
|
|
46
|
+
// Derive current state
|
|
47
|
+
const isDark = $derived(theme.isDark);
|
|
48
|
+
|
|
49
|
+
// Size mappings
|
|
50
|
+
const sizes = {
|
|
51
|
+
sm: { button: 'theme-toggle-sm', icon: 16 },
|
|
52
|
+
md: { button: 'theme-toggle-md', icon: 20 },
|
|
53
|
+
lg: { button: 'theme-toggle-lg', icon: 24 }
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const currentSize = $derived(sizes[size] || sizes.md);
|
|
57
|
+
|
|
58
|
+
function handleToggle() {
|
|
59
|
+
theme.toggle();
|
|
60
|
+
}
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<button
|
|
64
|
+
type="button"
|
|
65
|
+
onclick={handleToggle}
|
|
66
|
+
aria-pressed={isDark}
|
|
67
|
+
aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
|
|
68
|
+
class={cn('theme-toggle', `theme-toggle-${variant}`, currentSize.button, className)}
|
|
69
|
+
{...rest}
|
|
70
|
+
>
|
|
71
|
+
<span class="theme-toggle-icon-wrapper">
|
|
72
|
+
<!-- Sun icon (visible in dark mode, click to switch to light) -->
|
|
73
|
+
<span class={cn('theme-toggle-icon', 'theme-toggle-sun', isDark && 'theme-toggle-icon-active')}>
|
|
74
|
+
{#if sunIcon}
|
|
75
|
+
{@render sunIcon()}
|
|
76
|
+
{:else}
|
|
77
|
+
<svg
|
|
78
|
+
width={currentSize.icon}
|
|
79
|
+
height={currentSize.icon}
|
|
80
|
+
viewBox="0 0 24 24"
|
|
81
|
+
fill="none"
|
|
82
|
+
stroke="currentColor"
|
|
83
|
+
stroke-width="2"
|
|
84
|
+
stroke-linecap="round"
|
|
85
|
+
stroke-linejoin="round"
|
|
86
|
+
aria-hidden="true"
|
|
87
|
+
>
|
|
88
|
+
<circle cx="12" cy="12" r="4" />
|
|
89
|
+
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41" />
|
|
90
|
+
</svg>
|
|
91
|
+
{/if}
|
|
92
|
+
</span>
|
|
93
|
+
|
|
94
|
+
<!-- Moon icon (visible in light mode, click to switch to dark) -->
|
|
95
|
+
<span class={cn('theme-toggle-icon', 'theme-toggle-moon', !isDark && 'theme-toggle-icon-active')}>
|
|
96
|
+
{#if moonIcon}
|
|
97
|
+
{@render moonIcon()}
|
|
98
|
+
{:else}
|
|
99
|
+
<svg
|
|
100
|
+
width={currentSize.icon}
|
|
101
|
+
height={currentSize.icon}
|
|
102
|
+
viewBox="0 0 24 24"
|
|
103
|
+
fill="none"
|
|
104
|
+
stroke="currentColor"
|
|
105
|
+
stroke-width="2"
|
|
106
|
+
stroke-linecap="round"
|
|
107
|
+
stroke-linejoin="round"
|
|
108
|
+
aria-hidden="true"
|
|
109
|
+
>
|
|
110
|
+
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
|
|
111
|
+
</svg>
|
|
112
|
+
{/if}
|
|
113
|
+
</span>
|
|
114
|
+
</span>
|
|
115
|
+
|
|
116
|
+
{#if label}
|
|
117
|
+
<span class="theme-toggle-label">
|
|
118
|
+
{@render label()}
|
|
119
|
+
</span>
|
|
120
|
+
{/if}
|
|
121
|
+
</button>
|
|
122
|
+
|
|
123
|
+
<style>
|
|
124
|
+
/* ============================================
|
|
125
|
+
BASE STYLES
|
|
126
|
+
============================================ */
|
|
127
|
+
.theme-toggle {
|
|
128
|
+
position: relative;
|
|
129
|
+
display: inline-flex;
|
|
130
|
+
align-items: center;
|
|
131
|
+
justify-content: center;
|
|
132
|
+
gap: var(--space-2, 0.5rem);
|
|
133
|
+
border: none;
|
|
134
|
+
border-radius: var(--radius-md, 0.375rem);
|
|
135
|
+
cursor: pointer;
|
|
136
|
+
transition: background-color var(--duration-fast, 150ms) var(--ease-out, ease-out);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.theme-toggle:focus-visible {
|
|
140
|
+
outline: 2px solid var(--color-base0D, #83a598);
|
|
141
|
+
outline-offset: 2px;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* ============================================
|
|
145
|
+
VARIANTS
|
|
146
|
+
============================================ */
|
|
147
|
+
.theme-toggle-ghost {
|
|
148
|
+
background-color: transparent;
|
|
149
|
+
color: var(--color-base05, #a89984);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.theme-toggle-ghost:hover {
|
|
153
|
+
background-color: var(--color-base02, #3c3836);
|
|
154
|
+
color: var(--color-base06, #d5c4a1);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.theme-toggle-outline {
|
|
158
|
+
background-color: transparent;
|
|
159
|
+
color: var(--color-base05, #a89984);
|
|
160
|
+
border: 1px solid var(--color-base03, #504945);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.theme-toggle-outline:hover {
|
|
164
|
+
background-color: var(--color-base02, #3c3836);
|
|
165
|
+
border-color: var(--color-base04, #665c54);
|
|
166
|
+
color: var(--color-base06, #d5c4a1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.theme-toggle-subtle {
|
|
170
|
+
background-color: var(--color-base01, #282828);
|
|
171
|
+
color: var(--color-base05, #a89984);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.theme-toggle-subtle:hover {
|
|
175
|
+
background-color: var(--color-base02, #3c3836);
|
|
176
|
+
color: var(--color-base06, #d5c4a1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* ============================================
|
|
180
|
+
SIZES
|
|
181
|
+
============================================ */
|
|
182
|
+
.theme-toggle-sm {
|
|
183
|
+
padding: var(--space-1, 0.25rem);
|
|
184
|
+
min-width: 1.75rem;
|
|
185
|
+
min-height: 1.75rem;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.theme-toggle-md {
|
|
189
|
+
padding: var(--space-2, 0.5rem);
|
|
190
|
+
min-width: 2.25rem;
|
|
191
|
+
min-height: 2.25rem;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.theme-toggle-lg {
|
|
195
|
+
padding: var(--space-3, 0.75rem);
|
|
196
|
+
min-width: 2.75rem;
|
|
197
|
+
min-height: 2.75rem;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* ============================================
|
|
201
|
+
ICON ANIMATION
|
|
202
|
+
============================================ */
|
|
203
|
+
.theme-toggle-icon-wrapper {
|
|
204
|
+
position: relative;
|
|
205
|
+
display: flex;
|
|
206
|
+
align-items: center;
|
|
207
|
+
justify-content: center;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.theme-toggle-icon {
|
|
211
|
+
position: absolute;
|
|
212
|
+
display: flex;
|
|
213
|
+
align-items: center;
|
|
214
|
+
justify-content: center;
|
|
215
|
+
opacity: 0;
|
|
216
|
+
transform: rotate(-90deg) scale(0.5);
|
|
217
|
+
transition:
|
|
218
|
+
opacity var(--duration-normal, 200ms) var(--ease-out, ease-out),
|
|
219
|
+
transform var(--duration-normal, 200ms) var(--ease-out, ease-out);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.theme-toggle-icon-active {
|
|
223
|
+
position: relative;
|
|
224
|
+
opacity: 1;
|
|
225
|
+
transform: rotate(0deg) scale(1);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* Sun specific - warm color when active */
|
|
229
|
+
.theme-toggle-sun.theme-toggle-icon-active {
|
|
230
|
+
color: var(--color-base0A, #fabd2f);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/* Moon specific - cool color when active */
|
|
234
|
+
.theme-toggle-moon.theme-toggle-icon-active {
|
|
235
|
+
color: var(--color-base0D, #83a598);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* ============================================
|
|
239
|
+
REDUCED MOTION
|
|
240
|
+
============================================ */
|
|
241
|
+
@media (prefers-reduced-motion: reduce) {
|
|
242
|
+
.theme-toggle-icon {
|
|
243
|
+
transition: opacity var(--duration-fast, 150ms) var(--ease-out, ease-out);
|
|
244
|
+
transform: none;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.theme-toggle-icon-active {
|
|
248
|
+
transform: none;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* ============================================
|
|
253
|
+
LABEL
|
|
254
|
+
============================================ */
|
|
255
|
+
.theme-toggle-label {
|
|
256
|
+
font-size: var(--text-sm, 0.875rem);
|
|
257
|
+
color: var(--color-base05, #a89984);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.theme-toggle:hover .theme-toggle-label {
|
|
261
|
+
color: var(--color-base06, #d5c4a1);
|
|
262
|
+
}
|
|
263
|
+
</style>
|
package/src/index.js
CHANGED
|
@@ -40,6 +40,8 @@ export { default as FilterChip } from './components/primitives/FilterChip.svelte
|
|
|
40
40
|
export { default as StatusLine } from './components/primitives/StatusLine.svelte';
|
|
41
41
|
export { default as Tooltip } from './components/primitives/Tooltip.svelte';
|
|
42
42
|
export { default as MemberCard } from './components/primitives/MemberCard.svelte';
|
|
43
|
+
export { default as ThemeToggle } from './components/primitives/ThemeToggle.svelte';
|
|
44
|
+
export { default as ThemeSelect } from './components/primitives/ThemeSelect.svelte';
|
|
43
45
|
|
|
44
46
|
// ============================================
|
|
45
47
|
// COMPONENTS - Forms
|
|
@@ -139,6 +141,7 @@ export { default as DropdownContainer } from './components/navigation/DropdownCo
|
|
|
139
141
|
|
|
140
142
|
export { default as PageHeader } from './components/layout/PageHeader.svelte';
|
|
141
143
|
export { default as SettingCard } from './components/layout/SettingCard.svelte';
|
|
144
|
+
export { default as SettingItem } from './components/layout/SettingItem.svelte';
|
|
142
145
|
|
|
143
146
|
// ============================================
|
|
144
147
|
// COMPONENTS - Documentation
|