rizzo-css 0.0.13 → 0.0.14

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.
Files changed (38) hide show
  1. package/README.md +3 -3
  2. package/bin/rizzo-css.js +75 -44
  3. package/package.json +2 -2
  4. package/scaffold/astro/ThemeSwitcher.astro +504 -0
  5. package/scaffold/astro/themes.ts +54 -0
  6. package/scaffold/astro-app/README.md +1 -1
  7. package/scaffold/astro-app/src/pages/components.astro +1 -1
  8. package/scaffold/svelte/ThemeSwitcher.svelte +315 -0
  9. package/scaffold/svelte/theme.ts +65 -0
  10. package/scaffold/svelte/themes.ts +54 -0
  11. package/scaffold/svelte-app/README.md +1 -1
  12. package/scaffold/vanilla/README.md +2 -2
  13. package/scaffold/vanilla/components/accordion.html +6 -0
  14. package/scaffold/vanilla/components/alert.html +6 -0
  15. package/scaffold/vanilla/components/avatar.html +6 -0
  16. package/scaffold/vanilla/components/badge.html +6 -0
  17. package/scaffold/vanilla/components/breadcrumb.html +6 -0
  18. package/scaffold/vanilla/components/button.html +6 -0
  19. package/scaffold/vanilla/components/cards.html +6 -0
  20. package/scaffold/vanilla/components/copy-to-clipboard.html +6 -0
  21. package/scaffold/vanilla/components/divider.html +6 -0
  22. package/scaffold/vanilla/components/dropdown.html +6 -0
  23. package/scaffold/vanilla/components/forms.html +6 -0
  24. package/scaffold/vanilla/components/icons.html +6 -0
  25. package/scaffold/vanilla/components/index.html +6 -0
  26. package/scaffold/vanilla/components/modal.html +6 -0
  27. package/scaffold/vanilla/components/navbar.html +6 -0
  28. package/scaffold/vanilla/components/pagination.html +6 -0
  29. package/scaffold/vanilla/components/progress-bar.html +6 -0
  30. package/scaffold/vanilla/components/search.html +6 -0
  31. package/scaffold/vanilla/components/settings.html +6 -0
  32. package/scaffold/vanilla/components/spinner.html +6 -0
  33. package/scaffold/vanilla/components/table.html +6 -0
  34. package/scaffold/vanilla/components/tabs.html +6 -0
  35. package/scaffold/vanilla/components/theme-switcher.html +6 -0
  36. package/scaffold/vanilla/components/toast.html +6 -0
  37. package/scaffold/vanilla/components/tooltip.html +6 -0
  38. package/scaffold/vanilla/index.html +6 -0
@@ -0,0 +1,315 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import ChevronDown from './icons/ChevronDown.svelte';
4
+ import { THEMES_DARK, THEMES_LIGHT } from './themes';
5
+ import {
6
+ applyTheme,
7
+ getStoredTheme,
8
+ getThemeLabel,
9
+ resolveSystemTheme,
10
+ THEME_SYSTEM,
11
+ } from './theme';
12
+
13
+ const DEFAULT_THEME_DARK = 'github-dark-classic';
14
+ const DEFAULT_THEME_LIGHT = 'github-light';
15
+
16
+ const THEME_ICON_SVG: Record<string, string> = {
17
+ system:
18
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" /><circle cx="12" cy="12" r="3" /></svg>',
19
+ 'github-dark-classic':
20
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 7h.01" /><path d="M3.4 18H12a8 8 0 0 0 8-8V7a4 4 0 0 0-7.28-2.3L2 20" /><path d="m20 7 2 .5-2 .5" /><path d="M10 18v3" /><path d="M14 17.75V21" /><path d="M7 18a6 6 0 0 0 3.84-10.61" /></svg>',
21
+ 'github-light':
22
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4" /><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" /></svg>',
23
+ 'red-velvet-cupcake':
24
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 21v-8a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8" /><path d="M4 16s1-1 4-1 5 2 8 2 4-1 4-1V4" /><path d="M2 16v4M22 16v4M8 8h.01M16 8h.01M8 12h.01M16 12h.01" /></svg>',
25
+ 'orangy-one-light':
26
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15.5 6.5c.5-2.5 2.5-4 5-4 1.5 0 2.5.5 3 1" /><path d="M12 12c-2 2-3 4-3 6 0 3 2 5 5 5 2 0 4-1 6-3" /><path d="M18 12c2 2 3 4 3 6 0 3-2 5-5 5-2 0-4-1-6-3" /></svg>',
27
+ sunflower:
28
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 17a10 10 0 0 0-20 0" /><path d="M6 17a6 6 0 0 1 12 0" /><path d="M10 17a2 2 0 0 1 4 0" /></svg>',
29
+ 'shades-of-purple':
30
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 21a9 9 0 0 1 0 -18c4.97 0 9 3.582 9 8c0 1.06 -.474 2.078 -1.318 2.828c-.844 .75 -1.989 1.172 -3.182 1.172h-2.5a2 2 0 0 0 -1 3.75a1.3 1.3 0 0 1 -1 2.25"/><path d="M7.5 10.5a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"/><path d="M11.5 7.5a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"/><path d="M15.5 10.5a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"/></svg>',
31
+ 'sandstorm-classic':
32
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z" /></svg>',
33
+ 'rocky-blood-orange':
34
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 10V2M4.93 10.93l1.41 1.41M2 18h2M20 18h2M17.66 10.93l1.41-1.41M22 22H2M8 6l4-4 4 4M16 18a4 4 0 0 0-8 0" /><path d="M12 22v-4" /></svg>',
35
+ 'minimal-dark-neon-yellow':
36
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z" /></svg>',
37
+ 'hack-the-box':
38
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" /></svg>',
39
+ 'green-breeze-light':
40
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10Z" /><path d="M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12" /></svg>',
41
+ 'pink-cat-boo':
42
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19.5 12.572l-7.5 7.428l-7.5 -7.428a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572" /></svg>',
43
+ 'cute-pink':
44
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 18c0-2 1.5-4 4-4s4 2 4 4c0 2-1.5 4-4 4s-4-2-4-4Z" /><path d="M15 18c0-2 1.5-4 4-4s4 2 4 4c0 2-1.5 4-4 4s-4-2-4-4Z" /><path d="M12 8v4M10 10l-2 2M14 10l2 2" /></svg>',
45
+ 'semi-light-purple':
46
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9.06 11.9 8.07-8.06a2.85 2.85 0 1 1 4.03 4.03l-8.06 8.08" /><path d="M7.87 14.14c-.32.32-.67.68-1.06 1.06a5.04 5.04 0 0 1-2.17 1.22c-.47.15-.85.2-1.15.2-.16 0-.3-.02-.41-.03a1 1 0 0 1-.63-.97c-.01-.14.02-.31.07-.51.1-.41.3-.95.6-1.59.3-.64.67-1.33 1.08-2.05" /></svg>',
47
+ };
48
+
49
+ let open = $state(false);
50
+ let stored = $state(DEFAULT_THEME_DARK);
51
+ let currentTheme = $state(DEFAULT_THEME_DARK);
52
+ let previewOption = $state<{ value: string; label: string; bg: string; accent: string } | null>(null);
53
+ let menuEl: HTMLElement | null = $state(null);
54
+ let triggerEl: HTMLElement | null = $state(null);
55
+
56
+ const menuId = 'theme-switcher-menu';
57
+ const triggerId = 'theme-switcher-trigger';
58
+
59
+ function getStored(): string {
60
+ if (typeof localStorage === 'undefined') return DEFAULT_THEME_DARK;
61
+ return localStorage.getItem('theme') || document.documentElement.getAttribute('data-theme') || DEFAULT_THEME_DARK;
62
+ }
63
+ function getCurrent(): string {
64
+ if (typeof document === 'undefined') return DEFAULT_THEME_DARK;
65
+ return document.documentElement.getAttribute('data-theme') || DEFAULT_THEME_DARK;
66
+ }
67
+
68
+ function syncFromStorage() {
69
+ stored = getStored();
70
+ currentTheme = getCurrent();
71
+ }
72
+
73
+ const currentLabel = $derived(
74
+ stored === THEME_SYSTEM ? 'System' : getThemeLabel(currentTheme)
75
+ );
76
+ const currentIconSvg = $derived(
77
+ THEME_ICON_SVG[stored === THEME_SYSTEM ? THEME_SYSTEM : currentTheme || DEFAULT_THEME_DARK] || THEME_ICON_SVG[DEFAULT_THEME_DARK] || ''
78
+ );
79
+ const resolvedThemeInfo = $derived(THEMES_DARK.concat(THEMES_LIGHT).find((t) => t.value === currentTheme));
80
+ const preview = $derived(
81
+ previewOption ?? (stored === THEME_SYSTEM ? resolvedThemeInfo ?? { value: THEME_SYSTEM, label: 'System', bg: 'oklch(55% 0.02 270deg)', accent: '' } : resolvedThemeInfo)
82
+ );
83
+ const previewBg = $derived(preview?.bg ?? '');
84
+ const previewAccent = $derived(preview?.accent ?? '');
85
+ const previewLabel = $derived(preview?.label ?? '');
86
+ const previewStyle = $derived(previewAccent ? `--preview-accent: ${previewAccent}` : '');
87
+
88
+ function isOptionActive(value: string): boolean {
89
+ if (stored === value) return true;
90
+ if (stored === THEME_SYSTEM && value === currentTheme) return true;
91
+ return false;
92
+ }
93
+
94
+ function selectTheme(value: string) {
95
+ applyTheme(value);
96
+ syncFromStorage();
97
+ open = false;
98
+ triggerEl?.focus();
99
+ }
100
+
101
+ function getVisibleOptions(): HTMLElement[] {
102
+ if (!menuEl) return [];
103
+ return Array.from(menuEl.querySelectorAll<HTMLElement>('.theme-switcher__option'));
104
+ }
105
+
106
+ function handleKeydown(e: KeyboardEvent) {
107
+ if (e.target !== triggerEl && !menuEl?.contains(e.target as Node)) return;
108
+ const visible = getVisibleOptions();
109
+ if (e.key === 'Escape') {
110
+ open = false;
111
+ triggerEl?.focus();
112
+ e.preventDefault();
113
+ return;
114
+ }
115
+ if (e.key === 'ArrowDown' && !open) {
116
+ open = true;
117
+ e.preventDefault();
118
+ setTimeout(() => visible[0]?.focus(), 0);
119
+ return;
120
+ }
121
+ if (e.key === 'ArrowUp' && !open) {
122
+ open = true;
123
+ e.preventDefault();
124
+ setTimeout(() => visible[visible.length - 1]?.focus(), 0);
125
+ return;
126
+ }
127
+ if (!open) {
128
+ if (e.key === 'Enter' || e.key === ' ') {
129
+ e.preventDefault();
130
+ open = true;
131
+ setTimeout(() => visible[0]?.focus(), 0);
132
+ }
133
+ return;
134
+ }
135
+ const i = visible.indexOf(e.target as HTMLElement);
136
+ if (e.key === 'ArrowDown') {
137
+ e.preventDefault();
138
+ visible[(i + 1) % visible.length]?.focus();
139
+ } else if (e.key === 'ArrowUp') {
140
+ e.preventDefault();
141
+ visible[i <= 0 ? visible.length - 1 : i - 1]?.focus();
142
+ } else if (e.key === 'Home') {
143
+ e.preventDefault();
144
+ visible[0]?.focus();
145
+ } else if (e.key === 'End') {
146
+ e.preventDefault();
147
+ visible[visible.length - 1]?.focus();
148
+ } else if ((e.key === 'Enter' || e.key === ' ') && i >= 0) {
149
+ e.preventDefault();
150
+ const value = visible[i]?.getAttribute('data-theme-value');
151
+ if (value) selectTheme(value);
152
+ } else if (e.key === 'Tab') {
153
+ open = false;
154
+ }
155
+ }
156
+
157
+ function handleClickOutside(e: MouseEvent) {
158
+ if (menuEl && triggerEl && !menuEl.contains(e.target as Node) && !triggerEl.contains(e.target as Node)) {
159
+ open = false;
160
+ }
161
+ }
162
+
163
+ $effect(() => {
164
+ if (open) {
165
+ document.addEventListener('click', handleClickOutside);
166
+ return () => document.removeEventListener('click', handleClickOutside);
167
+ }
168
+ });
169
+
170
+ onMount(() => {
171
+ syncFromStorage();
172
+ const handler = () => syncFromStorage();
173
+ window.addEventListener('rizzo-theme-change', handler);
174
+ return () => window.removeEventListener('rizzo-theme-change', handler);
175
+ });
176
+
177
+ $effect(() => {
178
+ if (typeof window === 'undefined') return;
179
+ syncFromStorage();
180
+ });
181
+ </script>
182
+
183
+ <svelte:window onkeydown={handleKeydown} />
184
+
185
+ <div class="theme-switcher" data-theme-switcher>
186
+ <button
187
+ id={triggerId}
188
+ type="button"
189
+ class="theme-switcher__trigger"
190
+ aria-expanded={open}
191
+ aria-haspopup="true"
192
+ aria-controls={menuId}
193
+ aria-label="Select theme"
194
+ bind:this={triggerEl}
195
+ onclick={() => (open = !open)}
196
+ >
197
+ <span class="theme-switcher__label-wrapper" data-theme-label-wrapper>
198
+ {#if currentIconSvg}
199
+ <span class="theme-switcher__label-icon" data-theme-label-icon>{@html currentIconSvg}</span>
200
+ {/if}
201
+ <span class="theme-switcher__label" data-theme-label>{currentLabel}</span>
202
+ </span>
203
+ <ChevronDown width={16} height={16} class="theme-switcher__icon" />
204
+ </button>
205
+
206
+ <div
207
+ id={menuId}
208
+ class="theme-switcher__menu"
209
+ class:theme-switcher__menu--open={open}
210
+ role="menu"
211
+ aria-labelledby={triggerId}
212
+ aria-label="Theme selection menu"
213
+ aria-hidden={!open}
214
+ tabindex="-1"
215
+ bind:this={menuEl}
216
+ onmouseleave={() => (previewOption = null)}
217
+ onfocusout={(e) => {
218
+ const related = e.relatedTarget as Node | null;
219
+ if (!related || !menuEl?.contains(related)) previewOption = null;
220
+ }}
221
+ >
222
+ <div class="theme-switcher__menu-options">
223
+ <div class="theme-switcher__group" role="group" aria-label="Preference">
224
+ <div class="theme-switcher__group-label" role="presentation">Preference</div>
225
+ <div
226
+ class="theme-switcher__option"
227
+ class:theme-switcher__option--active={isOptionActive(THEME_SYSTEM)}
228
+ role="menuitemradio"
229
+ aria-checked={stored === THEME_SYSTEM}
230
+ tabindex={open ? 0 : -1}
231
+ data-theme-value="system"
232
+ data-theme-type="system"
233
+ data-theme-bg="oklch(55% 0.02 270deg)"
234
+ data-theme-accent=""
235
+ data-theme-label="System"
236
+ onclick={() => selectTheme(THEME_SYSTEM)}
237
+ onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); selectTheme(THEME_SYSTEM); } }}
238
+ onmouseenter={() => (previewOption = { value: THEME_SYSTEM, label: 'System', bg: 'oklch(55% 0.02 270deg)', accent: '' })}
239
+ onfocus={() => (previewOption = { value: THEME_SYSTEM, label: 'System', bg: 'oklch(55% 0.02 270deg)', accent: '' })}
240
+ >
241
+ <span class="theme-switcher__option-icon">{@html THEME_ICON_SVG['system'] || ''}</span>
242
+ <span class="sr-only">Preference: </span>
243
+ System
244
+ </div>
245
+ </div>
246
+ <div class="theme-switcher__group" role="group" aria-label="Dark themes">
247
+ <div class="theme-switcher__group-label" role="presentation">Dark</div>
248
+ {#each THEMES_DARK as theme}
249
+ <div
250
+ class="theme-switcher__option"
251
+ class:theme-switcher__option--active={isOptionActive(theme.value)}
252
+ role="menuitemradio"
253
+ aria-checked={stored === theme.value}
254
+ tabindex={open ? 0 : -1}
255
+ data-theme-value={theme.value}
256
+ data-theme-type="dark"
257
+ data-theme-bg={theme.bg}
258
+ data-theme-accent={theme.accent}
259
+ data-theme-label={theme.label}
260
+ onclick={() => selectTheme(theme.value)}
261
+ onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); selectTheme(theme.value); } }}
262
+ onmouseenter={() => (previewOption = { value: theme.value, label: theme.label, bg: theme.bg, accent: theme.accent })}
263
+ onfocus={() => (previewOption = { value: theme.value, label: theme.label, bg: theme.bg, accent: theme.accent })}
264
+ >
265
+ <span class="theme-switcher__option-icon">{@html THEME_ICON_SVG[theme.value] || ''}</span>
266
+ <span class="sr-only">Dark theme: </span>
267
+ {theme.label}
268
+ </div>
269
+ {/each}
270
+ </div>
271
+ <div class="theme-switcher__group" role="group" aria-label="Light themes">
272
+ <div class="theme-switcher__group-label" role="presentation">Light</div>
273
+ {#each THEMES_LIGHT as theme}
274
+ <div
275
+ class="theme-switcher__option"
276
+ class:theme-switcher__option--active={isOptionActive(theme.value)}
277
+ role="menuitemradio"
278
+ aria-checked={stored === theme.value}
279
+ tabindex={open ? 0 : -1}
280
+ data-theme-value={theme.value}
281
+ data-theme-type="light"
282
+ data-theme-bg={theme.bg}
283
+ data-theme-accent={theme.accent}
284
+ data-theme-label={theme.label}
285
+ onclick={() => selectTheme(theme.value)}
286
+ onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); selectTheme(theme.value); } }}
287
+ onmouseenter={() => (previewOption = { value: theme.value, label: theme.label, bg: theme.bg, accent: theme.accent })}
288
+ onfocus={() => (previewOption = { value: theme.value, label: theme.label, bg: theme.bg, accent: theme.accent })}
289
+ >
290
+ <span class="theme-switcher__option-icon">{@html THEME_ICON_SVG[theme.value] || ''}</span>
291
+ <span class="sr-only">Light theme: </span>
292
+ {theme.label}
293
+ </div>
294
+ {/each}
295
+ </div>
296
+ </div>
297
+ <div
298
+ class="theme-switcher__preview"
299
+ data-theme-preview
300
+ aria-hidden={!open}
301
+ style={previewStyle}
302
+ >
303
+ <div class="theme-switcher__preview-title">Preview</div>
304
+ <div class="theme-switcher__preview-header" data-theme-preview-label>{previewLabel}</div>
305
+ <div class="theme-switcher__preview-swatch-wrap">
306
+ <div
307
+ class="theme-switcher__preview-swatch"
308
+ data-theme-preview-swatch
309
+ style={previewBg ? `background-color: ${previewBg}` : ''}
310
+ ></div>
311
+ </div>
312
+ <div class="theme-switcher__preview-accent" data-theme-preview-accent></div>
313
+ </div>
314
+ </div>
315
+ </div>
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Theme utilities — apply theme, resolve system preference, get/set stored theme.
3
+ * Used by ThemeSwitcher, Layout (flash prevention), and any consumer that sets data-theme.
4
+ */
5
+ import { ALL_THEMES } from './themes';
6
+
7
+ export const THEME_SYSTEM = 'system';
8
+ export const DEFAULT_THEME_DARK = 'github-dark-classic';
9
+ export const DEFAULT_THEME_LIGHT = 'github-light';
10
+
11
+ /** Current theme id from the DOM (data-theme on html). */
12
+ export function getCurrentTheme(): string {
13
+ if (typeof document === 'undefined') return DEFAULT_THEME_DARK;
14
+ return document.documentElement.getAttribute('data-theme') || DEFAULT_THEME_DARK;
15
+ }
16
+
17
+ /** Stored theme from localStorage (may be 'system' or a theme id). */
18
+ export function getStoredTheme(): string {
19
+ if (typeof localStorage === 'undefined') return getCurrentTheme();
20
+ return localStorage.getItem('theme') || getCurrentTheme();
21
+ }
22
+
23
+ /** Resolve system preference to a concrete theme id. */
24
+ export function resolveSystemTheme(): string {
25
+ if (typeof window === 'undefined') return DEFAULT_THEME_DARK;
26
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? DEFAULT_THEME_DARK : DEFAULT_THEME_LIGHT;
27
+ }
28
+
29
+ /** Resolve stored theme to the effective theme id (for 'system', returns resolved dark/light). */
30
+ export function getResolvedTheme(): string {
31
+ const stored = getStoredTheme();
32
+ if (!stored || stored === THEME_SYSTEM) return resolveSystemTheme();
33
+ return stored;
34
+ }
35
+
36
+ /** Get { value, label } for a theme (for UI display). */
37
+ export function getThemeInfo(themeValue: string): { value: string; label: string } {
38
+ if (themeValue === THEME_SYSTEM) return { value: THEME_SYSTEM, label: 'System' };
39
+ const entry = ALL_THEMES.find((t) => t.value === themeValue);
40
+ return entry ? { value: entry.value, label: entry.label } : { value: themeValue, label: 'Theme' };
41
+ }
42
+
43
+ /** Get display label for a theme value (from config). */
44
+ export function getThemeLabel(themeValue: string): string {
45
+ return getThemeInfo(themeValue).label;
46
+ }
47
+
48
+ /** Apply a theme: set data-theme and persist to localStorage. Use for ThemeSwitcher and programmatic changes. */
49
+ export function applyTheme(themeValue: string): void {
50
+ if (typeof document === 'undefined' || typeof localStorage === 'undefined') return;
51
+ let effective: string;
52
+ if (themeValue === THEME_SYSTEM) {
53
+ effective = resolveSystemTheme();
54
+ document.documentElement.setAttribute('data-theme', effective);
55
+ localStorage.setItem('theme', THEME_SYSTEM);
56
+ } else {
57
+ document.documentElement.setAttribute('data-theme', themeValue);
58
+ localStorage.setItem('theme', themeValue);
59
+ effective = themeValue;
60
+ }
61
+ // Allow listeners to sync UI (e.g. ThemeSwitcher)
62
+ try {
63
+ window.dispatchEvent(new CustomEvent('rizzo-theme-change', { detail: { themeValue, effective } }));
64
+ } catch (_) {}
65
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Single source of truth for theme id, label, type, icon, and preview colors.
3
+ * Used by ThemeSwitcher and Navbar so theme icons and labels stay consistent.
4
+ */
5
+
6
+ export type ThemeIconKey =
7
+ | 'gear'
8
+ | 'owl'
9
+ | 'palette'
10
+ | 'flame'
11
+ | 'sunset'
12
+ | 'zap'
13
+ | 'shield'
14
+ | 'heart'
15
+ | 'sun'
16
+ | 'cake'
17
+ | 'lemon'
18
+ | 'rainbow'
19
+ | 'leaf'
20
+ | 'cherry'
21
+ | 'brush';
22
+
23
+ export interface ThemeEntry {
24
+ value: string;
25
+ label: string;
26
+ type: 'dark' | 'light';
27
+ iconKey: ThemeIconKey;
28
+ /** OKLCH background for ThemeSwitcher preview */
29
+ bg: string;
30
+ /** OKLCH accent for ThemeSwitcher preview */
31
+ accent: string;
32
+ }
33
+
34
+ export const THEMES_DARK: ThemeEntry[] = [
35
+ { value: 'github-dark-classic', label: 'GitHub Dark Classic', type: 'dark', iconKey: 'owl', bg: 'oklch(18% 0.012 264deg)', accent: 'oklch(72% 0.12 250deg)' },
36
+ { value: 'shades-of-purple', label: 'Shades of Purple', type: 'dark', iconKey: 'palette', bg: 'oklch(18% 0.08 290deg)', accent: 'oklch(65% 0.25 290deg)' },
37
+ { value: 'sandstorm-classic', label: 'Sandstorm Classic', type: 'dark', iconKey: 'flame', bg: 'oklch(16% 0.025 25deg)', accent: 'oklch(58% 0.18 25deg)' },
38
+ { value: 'rocky-blood-orange', label: 'Rocky Blood Orange', type: 'dark', iconKey: 'sunset', bg: 'oklch(16% 0.03 45deg)', accent: 'oklch(62% 0.16 55deg)' },
39
+ { value: 'minimal-dark-neon-yellow', label: 'Minimal Dark Neon Yellow', type: 'dark', iconKey: 'zap', bg: 'oklch(14% 0.01 95deg)', accent: 'oklch(88% 0.18 95deg)' },
40
+ { value: 'hack-the-box', label: 'Hack The Box', type: 'dark', iconKey: 'shield', bg: 'oklch(16% 0.03 255deg)', accent: 'oklch(88% 0.22 130deg)' },
41
+ { value: 'pink-cat-boo', label: 'Pink Cat Boo', type: 'dark', iconKey: 'heart', bg: 'oklch(18% 0.03 280deg)', accent: 'oklch(78% 0.12 350deg)' },
42
+ ];
43
+
44
+ export const THEMES_LIGHT: ThemeEntry[] = [
45
+ { value: 'github-light', label: 'GitHub Light', type: 'light', iconKey: 'sun', bg: 'oklch(100% 0 0deg)', accent: 'oklch(55% 0.18 255deg)' },
46
+ { value: 'red-velvet-cupcake', label: 'Red Velvet Cupcake', type: 'light', iconKey: 'cake', bg: 'oklch(99% 0.005 25deg)', accent: 'oklch(55% 0.17 25deg)' },
47
+ { value: 'orangy-one-light', label: 'Orangy One Light', type: 'light', iconKey: 'lemon', bg: 'oklch(99% 0.008 70deg)', accent: 'oklch(58% 0.16 55deg)' },
48
+ { value: 'sunflower', label: 'Sunflower', type: 'light', iconKey: 'rainbow', bg: 'oklch(98% 0.03 95deg)', accent: 'oklch(75% 0.16 95deg)' },
49
+ { value: 'green-breeze-light', label: 'Green Breeze Light', type: 'light', iconKey: 'leaf', bg: 'oklch(98% 0.008 140deg)', accent: 'oklch(48% 0.16 155deg)' },
50
+ { value: 'cute-pink', label: 'Cute Pink', type: 'light', iconKey: 'cherry', bg: 'oklch(98% 0.025 350deg)', accent: 'oklch(62% 0.22 350deg)' },
51
+ { value: 'semi-light-purple', label: 'Semi Light Purple', type: 'light', iconKey: 'brush', bg: 'oklch(96% 0.02 290deg)', accent: 'oklch(52% 0.2 290deg)' },
52
+ ];
53
+
54
+ export const ALL_THEMES = [...THEMES_DARK, ...THEMES_LIGHT];
@@ -1,6 +1,6 @@
1
1
  # SvelteKit + Rizzo CSS
2
2
 
3
- This project was scaffolded with `npx rizzo-css init` when you chose **Create new project** and Svelte. This full clone (home, component showcase at `/components`) is only generated for new projects; **Add to existing project** only adds the CSS and optional components.
3
+ This project was scaffolded with `npx rizzo-css init` when you chose **Create new project** and Svelte. This full clone (home, component showcase at `/components`) is only generated for new projects; **Add to existing project** only adds the CSS and optional components (you must add the stylesheet `<link>` to your layout yourself; the CLI prints the exact tag).
4
4
 
5
5
  ## First-time setup
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Vanilla JS + Rizzo CSS
2
2
 
3
- This project was scaffolded with `npx rizzo-css init` when you chose **Create new project** and Vanilla JS. This full clone (home, component showcase) is only generated for new projects; **Add to existing project** only adds the CSS and optional files.
3
+ This project was scaffolded with `npx rizzo-css init` when you chose **Create new project** and Vanilla JS. This full clone (home, component showcase) is only generated for new projects; **Add to existing project** only adds the CSS and optional files (you must add the stylesheet `<link>` to your HTML/layout yourself; the CLI prints the exact tag).
4
4
 
5
5
  ## First-time setup
6
6
 
@@ -13,7 +13,7 @@ If you prefer to load CSS from a CDN instead of the local file, replace the `<li
13
13
  - `<link rel="stylesheet" href="https://unpkg.com/rizzo-css@latest/dist/rizzo.min.css" />`
14
14
  - Or jsDelivr: `https://cdn.jsdelivr.net/npm/rizzo-css@latest/dist/rizzo.min.css`
15
15
 
16
- (Replace `@latest` with a specific version, e.g. `@0.0.13`, in production.)
16
+ (Replace `@latest` with a specific version, e.g. `@0.0.14`, in production.)
17
17
 
18
18
  The CLI replaces placeholders in `index.html` (e.g. `{{DATA_THEME}}`, `{{TITLE}}`) when you run `rizzo-css init`. The theme selected during init is used on first load when you have no saved preference in the browser.
19
19
 
@@ -170,6 +170,9 @@
170
170
 
171
171
 
172
172
 
173
+
174
+
175
+
173
176
  <main id="main-content" class="flex flex-col items-center justify-center text-center min-h-screen" style="padding: var(--spacing-12) var(--spacing-4); min-height: calc(100vh - 4rem);">
174
177
  <h1 style="font-size: var(--font-size-3xl); margin: 0 0 var(--spacing-4) 0; color: var(--text);">Accordion</h1>
175
178
  <p style="color: var(--text-dim); margin-bottom: var(--spacing-4);">Read the full docs for this component on the main site:</p>
@@ -182,6 +185,9 @@
182
185
 
183
186
 
184
187
 
188
+
189
+
190
+
185
191
  <script src="../js/main.js"></script>
186
192
  </body>
187
193
  </html>
@@ -170,6 +170,9 @@
170
170
 
171
171
 
172
172
 
173
+
174
+
175
+
173
176
  <main id="main-content" class="flex flex-col items-center justify-center text-center min-h-screen" style="padding: var(--spacing-12) var(--spacing-4); min-height: calc(100vh - 4rem);">
174
177
  <h1 style="font-size: var(--font-size-3xl); margin: 0 0 var(--spacing-4) 0; color: var(--text);">Alert</h1>
175
178
  <p style="color: var(--text-dim); margin-bottom: var(--spacing-4);">Read the full docs for this component on the main site:</p>
@@ -182,6 +185,9 @@
182
185
 
183
186
 
184
187
 
188
+
189
+
190
+
185
191
  <script src="../js/main.js"></script>
186
192
  </body>
187
193
  </html>
@@ -170,6 +170,9 @@
170
170
 
171
171
 
172
172
 
173
+
174
+
175
+
173
176
  <main id="main-content" class="flex flex-col items-center justify-center text-center min-h-screen" style="padding: var(--spacing-12) var(--spacing-4); min-height: calc(100vh - 4rem);">
174
177
  <h1 style="font-size: var(--font-size-3xl); margin: 0 0 var(--spacing-4) 0; color: var(--text);">Avatar</h1>
175
178
  <p style="color: var(--text-dim); margin-bottom: var(--spacing-4);">Read the full docs for this component on the main site:</p>
@@ -182,6 +185,9 @@
182
185
 
183
186
 
184
187
 
188
+
189
+
190
+
185
191
  <script src="../js/main.js"></script>
186
192
  </body>
187
193
  </html>
@@ -170,6 +170,9 @@
170
170
 
171
171
 
172
172
 
173
+
174
+
175
+
173
176
  <main id="main-content" class="flex flex-col items-center justify-center text-center min-h-screen" style="padding: var(--spacing-12) var(--spacing-4); min-height: calc(100vh - 4rem);">
174
177
  <h1 style="font-size: var(--font-size-3xl); margin: 0 0 var(--spacing-4) 0; color: var(--text);">Badge</h1>
175
178
  <p style="color: var(--text-dim); margin-bottom: var(--spacing-4);">Read the full docs for this component on the main site:</p>
@@ -182,6 +185,9 @@
182
185
 
183
186
 
184
187
 
188
+
189
+
190
+
185
191
  <script src="../js/main.js"></script>
186
192
  </body>
187
193
  </html>
@@ -170,6 +170,9 @@
170
170
 
171
171
 
172
172
 
173
+
174
+
175
+
173
176
  <main id="main-content" class="flex flex-col items-center justify-center text-center min-h-screen" style="padding: var(--spacing-12) var(--spacing-4); min-height: calc(100vh - 4rem);">
174
177
  <h1 style="font-size: var(--font-size-3xl); margin: 0 0 var(--spacing-4) 0; color: var(--text);">Breadcrumb</h1>
175
178
  <p style="color: var(--text-dim); margin-bottom: var(--spacing-4);">Read the full docs for this component on the main site:</p>
@@ -182,6 +185,9 @@
182
185
 
183
186
 
184
187
 
188
+
189
+
190
+
185
191
  <script src="../js/main.js"></script>
186
192
  </body>
187
193
  </html>
@@ -170,6 +170,9 @@
170
170
 
171
171
 
172
172
 
173
+
174
+
175
+
173
176
  <main id="main-content" class="flex flex-col items-center justify-center text-center min-h-screen" style="padding: var(--spacing-12) var(--spacing-4); min-height: calc(100vh - 4rem);">
174
177
  <h1 style="font-size: var(--font-size-3xl); margin: 0 0 var(--spacing-4) 0; color: var(--text);">Button</h1>
175
178
  <p style="color: var(--text-dim); margin-bottom: var(--spacing-4);">Read the full docs for this component on the main site:</p>
@@ -182,6 +185,9 @@
182
185
 
183
186
 
184
187
 
188
+
189
+
190
+
185
191
  <script src="../js/main.js"></script>
186
192
  </body>
187
193
  </html>
@@ -170,6 +170,9 @@
170
170
 
171
171
 
172
172
 
173
+
174
+
175
+
173
176
  <main id="main-content" class="flex flex-col items-center justify-center text-center min-h-screen" style="padding: var(--spacing-12) var(--spacing-4); min-height: calc(100vh - 4rem);">
174
177
  <h1 style="font-size: var(--font-size-3xl); margin: 0 0 var(--spacing-4) 0; color: var(--text);">Cards</h1>
175
178
  <p style="color: var(--text-dim); margin-bottom: var(--spacing-4);">Read the full docs for this component on the main site:</p>
@@ -182,6 +185,9 @@
182
185
 
183
186
 
184
187
 
188
+
189
+
190
+
185
191
  <script src="../js/main.js"></script>
186
192
  </body>
187
193
  </html>
@@ -170,6 +170,9 @@
170
170
 
171
171
 
172
172
 
173
+
174
+
175
+
173
176
  <main id="main-content" class="flex flex-col items-center justify-center text-center min-h-screen" style="padding: var(--spacing-12) var(--spacing-4); min-height: calc(100vh - 4rem);">
174
177
  <h1 style="font-size: var(--font-size-3xl); margin: 0 0 var(--spacing-4) 0; color: var(--text);">Copy to Clipboard</h1>
175
178
  <p style="color: var(--text-dim); margin-bottom: var(--spacing-4);">Read the full docs for this component on the main site:</p>
@@ -182,6 +185,9 @@
182
185
 
183
186
 
184
187
 
188
+
189
+
190
+
185
191
  <script src="../js/main.js"></script>
186
192
  </body>
187
193
  </html>
@@ -170,6 +170,9 @@
170
170
 
171
171
 
172
172
 
173
+
174
+
175
+
173
176
  <main id="main-content" class="flex flex-col items-center justify-center text-center min-h-screen" style="padding: var(--spacing-12) var(--spacing-4); min-height: calc(100vh - 4rem);">
174
177
  <h1 style="font-size: var(--font-size-3xl); margin: 0 0 var(--spacing-4) 0; color: var(--text);">Divider</h1>
175
178
  <p style="color: var(--text-dim); margin-bottom: var(--spacing-4);">Read the full docs for this component on the main site:</p>
@@ -182,6 +185,9 @@
182
185
 
183
186
 
184
187
 
188
+
189
+
190
+
185
191
  <script src="../js/main.js"></script>
186
192
  </body>
187
193
  </html>