rizzo-css 0.0.13 → 0.0.15

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 (45) hide show
  1. package/README.md +4 -4
  2. package/bin/rizzo-css.js +75 -44
  3. package/dist/rizzo.min.css +10 -7
  4. package/package.json +2 -2
  5. package/scaffold/astro/ThemeSwitcher.astro +504 -0
  6. package/scaffold/astro/themes.ts +54 -0
  7. package/scaffold/astro-app/README.md +1 -1
  8. package/scaffold/astro-app/src/components/DocsSidebar.astro +51 -0
  9. package/scaffold/astro-app/src/components/Navbar.astro +45 -123
  10. package/scaffold/astro-app/src/layouts/DocsLayout.astro +72 -13
  11. package/scaffold/astro-app/src/pages/components/navbar.astro +3 -3
  12. package/scaffold/astro-app/src/pages/components/theme-switcher.astro +1 -0
  13. package/scaffold/astro-app/src/pages/components.astro +1 -1
  14. package/scaffold/svelte/ThemeSwitcher.svelte +315 -0
  15. package/scaffold/svelte/theme.ts +65 -0
  16. package/scaffold/svelte/themes.ts +54 -0
  17. package/scaffold/svelte-app/README.md +1 -1
  18. package/scaffold/svelte-app/src/lib/rizzo-docs/CodeBlock.svelte +1 -1
  19. package/scaffold/vanilla/README.md +2 -2
  20. package/scaffold/vanilla/components/accordion.html +20 -0
  21. package/scaffold/vanilla/components/alert.html +20 -0
  22. package/scaffold/vanilla/components/avatar.html +20 -0
  23. package/scaffold/vanilla/components/badge.html +20 -0
  24. package/scaffold/vanilla/components/breadcrumb.html +20 -0
  25. package/scaffold/vanilla/components/button.html +20 -0
  26. package/scaffold/vanilla/components/cards.html +20 -0
  27. package/scaffold/vanilla/components/copy-to-clipboard.html +20 -0
  28. package/scaffold/vanilla/components/divider.html +20 -0
  29. package/scaffold/vanilla/components/dropdown.html +20 -0
  30. package/scaffold/vanilla/components/forms.html +20 -0
  31. package/scaffold/vanilla/components/icons.html +20 -0
  32. package/scaffold/vanilla/components/index.html +20 -0
  33. package/scaffold/vanilla/components/modal.html +20 -0
  34. package/scaffold/vanilla/components/navbar.html +20 -0
  35. package/scaffold/vanilla/components/pagination.html +20 -0
  36. package/scaffold/vanilla/components/progress-bar.html +20 -0
  37. package/scaffold/vanilla/components/search.html +20 -0
  38. package/scaffold/vanilla/components/settings.html +20 -0
  39. package/scaffold/vanilla/components/spinner.html +20 -0
  40. package/scaffold/vanilla/components/table.html +20 -0
  41. package/scaffold/vanilla/components/tabs.html +20 -0
  42. package/scaffold/vanilla/components/theme-switcher.html +20 -0
  43. package/scaffold/vanilla/components/toast.html +20 -0
  44. package/scaffold/vanilla/components/tooltip.html +20 -0
  45. package/scaffold/vanilla/index.html +20 -0
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "rizzo-css",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "scripts": {
5
- "prepublishOnly": "cd ../.. && pnpm build:css && node scripts/copy-scaffold.js && node scripts/prepare-astro-scaffold.js && node scripts/prepare-vanilla-scaffold.js && node scripts/prepare-svelte-scaffold.js"
5
+ "prepublishOnly": "cd ../.. && pnpm run lint:css:fix && pnpm run build:css && node scripts/copy-scaffold.js && node scripts/prepare-astro-scaffold.js && node scripts/prepare-vanilla-scaffold.js && node scripts/prepare-svelte-scaffold.js"
6
6
  },
7
7
  "description": "A modern CSS design system with semantic theming, 14 themes, and accessible components (BEM). Use with Astro, Svelte, or any framework.",
8
8
  "style": "dist/rizzo.min.css",
@@ -0,0 +1,504 @@
1
+ ---
2
+ import ChevronDown from './icons/ChevronDown.astro';
3
+ import Gear from './icons/Gear.astro';
4
+ import Palette from './icons/Palette.astro';
5
+ import Owl from './icons/Owl.astro';
6
+ import Sun from './icons/Sun.astro';
7
+ import Flame from './icons/Flame.astro';
8
+ import Heart from './icons/Heart.astro';
9
+ import Leaf from './icons/Leaf.astro';
10
+ import Shield from './icons/Shield.astro';
11
+ import Zap from './icons/Zap.astro';
12
+ import Cake from './icons/Cake.astro';
13
+ import Sunset from './icons/Sunset.astro';
14
+ import Cherry from './icons/Cherry.astro';
15
+ import Brush from './icons/Brush.astro';
16
+ import Lemon from './icons/Lemon.astro';
17
+ import Rainbow from './icons/Rainbow.astro';
18
+ import type { ThemeIconKey } from './themes';
19
+ import { THEMES_DARK, THEMES_LIGHT } from './themes';
20
+
21
+ const iconMap: Record<ThemeIconKey, typeof Owl> = {
22
+ gear: Gear,
23
+ owl: Owl,
24
+ palette: Palette,
25
+ flame: Flame,
26
+ sunset: Sunset,
27
+ zap: Zap,
28
+ shield: Shield,
29
+ heart: Heart,
30
+ sun: Sun,
31
+ cake: Cake,
32
+ lemon: Lemon,
33
+ rainbow: Rainbow,
34
+ leaf: Leaf,
35
+ cherry: Cherry,
36
+ brush: Brush,
37
+ };
38
+
39
+ const themes = {
40
+ dark: THEMES_DARK.map((t) => ({ ...t, icon: iconMap[t.iconKey] })),
41
+ light: THEMES_LIGHT.map((t) => ({ ...t, icon: iconMap[t.iconKey] })),
42
+ };
43
+
44
+
45
+ // Get current theme from HTML data attribute or default
46
+ const getCurrentTheme = () => {
47
+ if (typeof document !== 'undefined') {
48
+ return document.documentElement.getAttribute('data-theme') || 'github-dark-classic';
49
+ }
50
+ return 'github-dark-classic';
51
+ };
52
+
53
+ // System preference theme id (stored in localStorage; data-theme gets resolved to dark/light default)
54
+ const THEME_SYSTEM = 'system';
55
+ const DEFAULT_THEME_DARK = 'github-dark-classic';
56
+ const DEFAULT_THEME_LIGHT = 'github-light';
57
+
58
+ // Get theme info helper
59
+ const getThemeInfo = (themeValue: string) => {
60
+ if (themeValue === THEME_SYSTEM) return { value: THEME_SYSTEM, label: 'System', icon: Gear };
61
+ const allThemes = [...themes.dark, ...themes.light];
62
+ return allThemes.find(t => t.value === themeValue) || null;
63
+ };
64
+
65
+ // Get initial theme info (will be updated by script, but set initial value)
66
+ const currentTheme = getCurrentTheme();
67
+ const initialTheme = getThemeInfo(currentTheme);
68
+ const initialLabel = initialTheme ? initialTheme.label : 'Theme';
69
+ const InitialIcon = initialTheme ? initialTheme.icon : null;
70
+
71
+ ---
72
+
73
+ <div class="theme-switcher" data-theme-switcher>
74
+ <button
75
+ class="theme-switcher__trigger"
76
+ type="button"
77
+ aria-expanded="false"
78
+ aria-haspopup="true"
79
+ aria-controls="theme-menu"
80
+ aria-label="Select theme"
81
+ id="theme-trigger"
82
+ >
83
+ <span class="theme-switcher__label-wrapper" data-theme-label-wrapper>
84
+ <span class="theme-switcher__label" data-theme-label>{initialLabel}</span>
85
+ </span>
86
+ <ChevronDown class="theme-switcher__icon" width={16} height={16} />
87
+ </button>
88
+
89
+ <div
90
+ class="theme-switcher__menu"
91
+ id="theme-menu"
92
+ role="menu"
93
+ aria-labelledby="theme-trigger"
94
+ aria-label="Theme selection menu"
95
+ aria-orientation="vertical"
96
+ aria-hidden="true"
97
+ tabindex="-1"
98
+ >
99
+ <div class="theme-switcher__menu-options">
100
+ <div class="theme-switcher__group" role="group" aria-label="Preference">
101
+ <div class="theme-switcher__group-label" role="presentation">Preference</div>
102
+ <div
103
+ class="theme-switcher__option"
104
+ role="menuitemradio"
105
+ aria-checked="false"
106
+ tabindex={-1}
107
+ data-theme-value="system"
108
+ data-theme-type="system"
109
+ data-theme-bg="oklch(55% 0.02 270deg)"
110
+ data-theme-accent=""
111
+ >
112
+ <Gear width={16} height={16} class="theme-switcher__option-icon" />
113
+ <span class="sr-only">Preference: </span>
114
+ System
115
+ </div>
116
+ </div>
117
+ <div class="theme-switcher__group" role="group" aria-label="Dark themes">
118
+ <div class="theme-switcher__group-label" role="presentation">Dark</div>
119
+ {themes.dark.map((theme) => {
120
+ const IconComponent = theme.icon;
121
+ return (
122
+ <div
123
+ class="theme-switcher__option"
124
+ role="menuitemradio"
125
+ aria-checked="false"
126
+ tabindex={-1}
127
+ data-theme-value={theme.value}
128
+ data-theme-type="dark"
129
+ data-theme-bg={theme.bg}
130
+ data-theme-accent={theme.accent}
131
+ data-theme-label={theme.label}
132
+ >
133
+ <IconComponent width={16} height={16} class="theme-switcher__option-icon" />
134
+ <span class="sr-only">Dark theme: </span>
135
+ {theme.label}
136
+ </div>
137
+ );
138
+ })}
139
+ </div>
140
+
141
+ <div class="theme-switcher__group" role="group" aria-label="Light themes">
142
+ <div class="theme-switcher__group-label" role="presentation">Light</div>
143
+ {themes.light.map((theme) => {
144
+ const IconComponent = theme.icon;
145
+ return (
146
+ <div
147
+ class="theme-switcher__option"
148
+ role="menuitemradio"
149
+ aria-checked="false"
150
+ tabindex={-1}
151
+ data-theme-value={theme.value}
152
+ data-theme-type="light"
153
+ data-theme-bg={theme.bg}
154
+ data-theme-accent={theme.accent}
155
+ data-theme-label={theme.label}
156
+ >
157
+ <IconComponent width={16} height={16} class="theme-switcher__option-icon" />
158
+ <span class="sr-only">Light theme: </span>
159
+ {theme.label}
160
+ </div>
161
+ );
162
+ })}
163
+ </div>
164
+ </div>
165
+ <div class="theme-switcher__preview" data-theme-preview aria-hidden="true">
166
+ <div class="theme-switcher__preview-title">Preview</div>
167
+ <div class="theme-switcher__preview-header" data-theme-preview-label></div>
168
+ <div class="theme-switcher__preview-swatch-wrap">
169
+ <div class="theme-switcher__preview-swatch" data-theme-preview-swatch></div>
170
+ </div>
171
+ <div class="theme-switcher__preview-accent" data-theme-preview-accent></div>
172
+ </div>
173
+ </div>
174
+ </div>
175
+
176
+ <script>
177
+ import {
178
+ applyTheme,
179
+ getStoredTheme,
180
+ getCurrentTheme,
181
+ resolveSystemTheme,
182
+ getThemeLabel,
183
+ THEME_SYSTEM,
184
+ } from '../utils/theme';
185
+
186
+ (function initThemeSwitcher() {
187
+ const getIconSVG = (themeValue: string) => {
188
+ const svgMap: Record<string, string> = {
189
+ 'system': '<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>',
190
+ 'github-dark-classic': '<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>',
191
+ 'github-light': '<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>',
192
+ 'red-velvet-cupcake': '<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>',
193
+ 'orangy-one-light': '<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>',
194
+ 'sunflower': '<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>',
195
+ 'shades-of-purple': '<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>',
196
+ 'sandstorm-classic': '<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>',
197
+ 'rocky-blood-orange': '<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>',
198
+ 'minimal-dark-neon-yellow': '<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>',
199
+ 'hack-the-box': '<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>',
200
+ 'green-breeze-light': '<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>',
201
+ 'pink-cat-boo': '<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>',
202
+ 'cute-pink': '<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>',
203
+ 'semi-light-purple': '<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>',
204
+ };
205
+ return svgMap[themeValue] || '';
206
+ };
207
+
208
+ // Update all switchers on the page
209
+ // When called from applyTheme, pass stored + currentTheme so we don't rely on DOM/localStorage being updated yet
210
+ const updateAllSwitchers = (overrideStored?: string | null, overrideCurrentTheme?: string | null) => {
211
+ const stored = (overrideStored != null ? String(overrideStored) : getStoredTheme() || '').trim();
212
+ const currentTheme = (overrideCurrentTheme != null ? String(overrideCurrentTheme) : getCurrentTheme() || '').trim();
213
+ document.querySelectorAll('[data-theme-switcher]').forEach((switcher) => {
214
+ const label = switcher.querySelector('[data-theme-label]');
215
+ const labelWrapper = switcher.querySelector('[data-theme-label-wrapper]');
216
+ const options = switcher.querySelectorAll('.theme-switcher__option');
217
+
218
+ if (label && labelWrapper) {
219
+ const themeLabel = stored === THEME_SYSTEM ? 'System' : getThemeLabel(currentTheme);
220
+ label.textContent = themeLabel;
221
+
222
+ // Update icon
223
+ const existingIcon = labelWrapper.querySelector('[data-theme-label-icon]');
224
+ if (existingIcon) {
225
+ existingIcon.remove();
226
+ }
227
+ const iconTheme = stored === THEME_SYSTEM ? THEME_SYSTEM : currentTheme;
228
+ const iconSvg = getIconSVG(iconTheme);
229
+ if (iconSvg) {
230
+ const iconElement = document.createElement('span');
231
+ iconElement.setAttribute('data-theme-label-icon', '');
232
+ iconElement.className = 'theme-switcher__label-icon';
233
+ iconElement.innerHTML = iconSvg;
234
+ labelWrapper.insertBefore(iconElement, label);
235
+ }
236
+ }
237
+
238
+ // Update options: when System is selected, both System and the resolved theme show active styles
239
+ const resolvedThemeBg = stored === THEME_SYSTEM
240
+ ? (Array.from(options).find((opt) => (opt.getAttribute('data-theme-value') || '').trim() === currentTheme)?.getAttribute('data-theme-bg') || null)
241
+ : null;
242
+ options.forEach((option) => {
243
+ const el = option as HTMLElement;
244
+ const optionValue = (el.getAttribute('data-theme-value') || '').trim();
245
+ const isStoredChoice = optionValue === stored;
246
+ const isResolvedTheme = stored === THEME_SYSTEM && optionValue === currentTheme;
247
+ const isActive = isStoredChoice || isResolvedTheme;
248
+ el.setAttribute('aria-checked', isStoredChoice.toString());
249
+ if (isActive) {
250
+ el.classList.add('theme-switcher__option--active');
251
+ const themeBg = optionValue === THEME_SYSTEM && resolvedThemeBg
252
+ ? resolvedThemeBg
253
+ : el.getAttribute('data-theme-bg');
254
+ if (themeBg) {
255
+ el.style.setProperty('--theme-bg', themeBg);
256
+ }
257
+ } else {
258
+ el.classList.remove('theme-switcher__option--active');
259
+ el.style.removeProperty('--theme-bg');
260
+ }
261
+ });
262
+ });
263
+ };
264
+
265
+ // Sync UI when theme changes (from util or other tabs)
266
+ window.addEventListener('rizzo-theme-change', () => updateAllSwitchers());
267
+
268
+ // Initialize all theme switcher instances on the page
269
+ const switchers = document.querySelectorAll('[data-theme-switcher]');
270
+ if (!switchers.length) return;
271
+
272
+ switchers.forEach((switcher) => {
273
+ const trigger = switcher.querySelector('.theme-switcher__trigger') as HTMLButtonElement | null;
274
+ const menu = switcher.querySelector('.theme-switcher__menu') as HTMLElement | null;
275
+ const options = switcher.querySelectorAll('.theme-switcher__option') as NodeListOf<HTMLElement>;
276
+
277
+ if (!trigger || !menu) return;
278
+
279
+ // Toggle menu
280
+ const toggleMenu = (open?: boolean) => {
281
+ const isOpen = open !== undefined ? open : menu.classList.contains('theme-switcher__menu--open');
282
+ const willBeOpen = !isOpen;
283
+
284
+ menu.classList.toggle('theme-switcher__menu--open', willBeOpen);
285
+ trigger.setAttribute('aria-expanded', willBeOpen.toString());
286
+ menu.setAttribute('aria-hidden', (!willBeOpen).toString());
287
+
288
+ options.forEach((option) => {
289
+ option.setAttribute('tabindex', willBeOpen ? '0' : '-1');
290
+ });
291
+
292
+ if (!willBeOpen) {
293
+ trigger.focus();
294
+ } else {
295
+ updateAllSwitchers();
296
+ updatePreview(null);
297
+ const firstOption = options[0];
298
+ if (firstOption) {
299
+ setTimeout(() => (firstOption as HTMLElement).focus(), 0);
300
+ }
301
+ }
302
+ };
303
+
304
+ // Close menu
305
+ const closeMenu = () => {
306
+ menu.classList.remove('theme-switcher__menu--open');
307
+ trigger.setAttribute('aria-expanded', 'false');
308
+ menu.setAttribute('aria-hidden', 'true');
309
+ options.forEach((option) => {
310
+ option.setAttribute('tabindex', '-1');
311
+ });
312
+ };
313
+
314
+ // Close on outside click handler
315
+ const handleOutsideClick = (e: MouseEvent) => {
316
+ const target = e.target as Node | null;
317
+ if (target && !switcher.contains(target)) {
318
+ closeMenu();
319
+ document.removeEventListener('click', handleOutsideClick);
320
+ }
321
+ };
322
+
323
+ // Wrapped toggle menu that handles outside clicks
324
+ const wrappedToggleMenu = (open?: boolean) => {
325
+ const wasOpen = menu.classList.contains('theme-switcher__menu--open');
326
+ toggleMenu(open);
327
+ const isNowOpen = menu.classList.contains('theme-switcher__menu--open');
328
+
329
+ if (isNowOpen && !wasOpen) {
330
+ setTimeout(() => document.addEventListener('click', handleOutsideClick), 0);
331
+ } else if (!isNowOpen && wasOpen) {
332
+ document.removeEventListener('click', handleOutsideClick);
333
+ }
334
+ };
335
+
336
+ // Theme preview: show on hover/focus of an option that has accent
337
+ const preview = switcher.querySelector('[data-theme-preview]') as HTMLElement | null;
338
+ const previewSwatch = switcher.querySelector('[data-theme-preview-swatch]') as HTMLElement | null;
339
+ const previewAccent = switcher.querySelector('[data-theme-preview-accent]') as HTMLElement | null;
340
+
341
+ const previewLabel = switcher.querySelector('[data-theme-preview-label]') as HTMLElement | null;
342
+ const getActiveOption = (): HTMLElement | null => {
343
+ const active = Array.from(options).filter((opt) => (opt as HTMLElement).classList.contains('theme-switcher__option--active'));
344
+ const withTheme = active.find((opt) => (opt as HTMLElement).getAttribute('data-theme-value') !== THEME_SYSTEM) as HTMLElement | undefined;
345
+ return (withTheme ?? active[0] ?? null) as HTMLElement | null;
346
+ };
347
+ const getResolvedOption = (): HTMLElement | null => {
348
+ return (Array.from(options).find((opt) => (opt as HTMLElement).getAttribute('data-theme-value') !== THEME_SYSTEM && (opt as HTMLElement).classList.contains('theme-switcher__option--active')) as HTMLElement) ?? null;
349
+ };
350
+ const updatePreview = (option: HTMLElement | null) => {
351
+ if (!preview || !previewSwatch || !previewAccent) return;
352
+ const target = option ?? getActiveOption();
353
+ if (!target) {
354
+ preview.style.removeProperty('--preview-accent');
355
+ previewSwatch.style.backgroundColor = '';
356
+ if (previewLabel) previewLabel.textContent = '';
357
+ preview.setAttribute('aria-hidden', 'true');
358
+ return;
359
+ }
360
+ const value = target.getAttribute('data-theme-value') || '';
361
+ const isSystem = value === THEME_SYSTEM;
362
+ const resolved = getResolvedOption();
363
+ const bg = isSystem
364
+ ? (getComputedStyle(target).getPropertyValue('--theme-bg').trim() || resolved?.getAttribute('data-theme-bg'))
365
+ : target.getAttribute('data-theme-bg');
366
+ const accent = isSystem ? (resolved?.getAttribute('data-theme-accent') || '') : (target.getAttribute('data-theme-accent') || '');
367
+ const label = isSystem ? 'System' : (target.getAttribute('data-theme-label') || '');
368
+ previewSwatch.style.backgroundColor = bg || '';
369
+ preview.style.setProperty('--preview-accent', accent);
370
+ if (previewLabel) previewLabel.textContent = label;
371
+ preview.setAttribute('aria-hidden', 'false');
372
+ };
373
+
374
+ options.forEach((option) => {
375
+ const el = option as HTMLElement;
376
+ el.addEventListener('mouseenter', () => updatePreview(el));
377
+ el.addEventListener('focus', () => updatePreview(el));
378
+ });
379
+ menu.addEventListener('mouseleave', () => {
380
+ const focused = menu.contains(document.activeElement);
381
+ if (!focused) updatePreview(null);
382
+ });
383
+ menu.addEventListener('focusout', (e) => {
384
+ const related = (e as FocusEvent).relatedTarget as Node | null;
385
+ if (!related || !menu.contains(related)) updatePreview(null);
386
+ });
387
+
388
+ // Event listeners
389
+ trigger.addEventListener('click', () => wrappedToggleMenu(undefined));
390
+
391
+ menu.addEventListener('click', (e) => {
392
+ const target = e.target as HTMLElement | null;
393
+ if (!target) return;
394
+ const option = target.closest('.theme-switcher__option');
395
+ if (option) {
396
+ const themeValue = (option as HTMLElement).getAttribute('data-theme-value');
397
+ if (themeValue) {
398
+ applyTheme(themeValue);
399
+ updateAllSwitchers();
400
+ closeMenu();
401
+ }
402
+ }
403
+ });
404
+
405
+ // Keyboard navigation
406
+ let currentIndex = -1;
407
+
408
+ const getVisibleOptions = () => {
409
+ return Array.from(options).filter(opt => {
410
+ return opt.offsetParent !== null || opt.closest('.theme-switcher__menu--open');
411
+ });
412
+ };
413
+
414
+ trigger.addEventListener('keydown', (e) => {
415
+ if (e.key === 'Enter' || e.key === ' ') {
416
+ e.preventDefault();
417
+ wrappedToggleMenu();
418
+ } else if (e.key === 'ArrowDown') {
419
+ e.preventDefault();
420
+ const visibleOptions = getVisibleOptions();
421
+ if (visibleOptions.length > 0) {
422
+ currentIndex = 0;
423
+ wrappedToggleMenu(true);
424
+ setTimeout(() => visibleOptions[0].focus(), 0);
425
+ }
426
+ } else if (e.key === 'ArrowUp') {
427
+ e.preventDefault();
428
+ const visibleOptions = getVisibleOptions();
429
+ if (visibleOptions.length > 0) {
430
+ currentIndex = visibleOptions.length - 1;
431
+ wrappedToggleMenu(true);
432
+ setTimeout(() => visibleOptions[currentIndex].focus(), 0);
433
+ }
434
+ } else if (e.key === 'Escape') {
435
+ e.preventDefault();
436
+ closeMenu();
437
+ }
438
+ });
439
+
440
+ menu.addEventListener('keydown', (e) => {
441
+ const visibleOptions = getVisibleOptions();
442
+
443
+ if (e.key === 'Escape') {
444
+ e.preventDefault();
445
+ closeMenu();
446
+ trigger.focus();
447
+ currentIndex = -1;
448
+ } else if (e.key === 'ArrowDown') {
449
+ e.preventDefault();
450
+ currentIndex = (currentIndex + 1) % visibleOptions.length;
451
+ visibleOptions[currentIndex].focus();
452
+ } else if (e.key === 'ArrowUp') {
453
+ e.preventDefault();
454
+ currentIndex = currentIndex <= 0 ? visibleOptions.length - 1 : currentIndex - 1;
455
+ visibleOptions[currentIndex].focus();
456
+ } else if (e.key === 'Home') {
457
+ e.preventDefault();
458
+ currentIndex = 0;
459
+ visibleOptions[0].focus();
460
+ } else if (e.key === 'End') {
461
+ e.preventDefault();
462
+ currentIndex = visibleOptions.length - 1;
463
+ visibleOptions[currentIndex].focus();
464
+ } else if (e.key === 'Enter' || e.key === ' ') {
465
+ e.preventDefault();
466
+ const target = e.target as HTMLElement | null;
467
+ if (!target) return;
468
+ const themeValue = target.getAttribute('data-theme-value');
469
+ if (themeValue) {
470
+ applyTheme(themeValue);
471
+ updateAllSwitchers();
472
+ closeMenu();
473
+ trigger.focus(); // Return focus to trigger after selection
474
+ }
475
+ } else if (e.key === 'Tab') {
476
+ // Close menu when Tab is pressed (allows natural tab flow)
477
+ closeMenu();
478
+ // Don't prevent default - allow Tab to continue naturally
479
+ }
480
+ });
481
+
482
+ });
483
+
484
+ // Initialize all switchers
485
+ setTimeout(() => {
486
+ const savedTheme = localStorage.getItem('theme');
487
+ if (savedTheme) {
488
+ applyTheme(savedTheme);
489
+ updateAllSwitchers();
490
+ } else {
491
+ updateAllSwitchers();
492
+ }
493
+ }, 0);
494
+
495
+ // When "System" is selected, update theme when OS preference changes
496
+ const systemMedia = window.matchMedia('(prefers-color-scheme: dark)');
497
+ systemMedia.addEventListener('change', () => {
498
+ if (getStoredTheme() === THEME_SYSTEM) {
499
+ applyTheme(THEME_SYSTEM);
500
+ updateAllSwitchers();
501
+ }
502
+ });
503
+ })();
504
+ </script>
@@ -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
  # Astro + Rizzo CSS
2
2
 
3
- This project was scaffolded with `npx rizzo-css init` when you chose **Create new project** and Astro. This full clone (navbar, docs pages, component showcase) 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 Astro. This full clone (navbar, docs pages, component showcase) 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
 
@@ -0,0 +1,51 @@
1
+ ---
2
+ import { DOCS_NAV } from '../config/docsNav';
3
+ import type { Framework } from '../config/frameworks';
4
+
5
+ interface Props {
6
+ currentPath: string;
7
+ framework: Framework;
8
+ }
9
+
10
+ const { currentPath, framework } = Astro.props;
11
+ const pathPrefix = framework.pathPrefix; // /docs or /docs/svelte or /docs/vanilla
12
+
13
+ function fullHref(link: { href: string; frameworkOnly?: boolean }) {
14
+ const base = link.frameworkOnly ? pathPrefix : '/docs';
15
+ return `${base}/${link.href}`;
16
+ }
17
+
18
+ function isActive(link: { href: string; frameworkOnly?: boolean }): boolean {
19
+ const path = currentPath.replace(/\/$/, '');
20
+ const href = fullHref(link);
21
+ if (path === href) return true;
22
+ if (path.startsWith(href + '/')) return true;
23
+ return false;
24
+ }
25
+ ---
26
+
27
+ <aside id="docs-sidebar" class="docs-sidebar" aria-label="Documentation navigation">
28
+ <nav class="docs-sidebar__nav">
29
+ {DOCS_NAV.map((group) => (
30
+ <div class="docs-sidebar__group">
31
+ <h2 class="docs-sidebar__group-label">{group.label}</h2>
32
+ <ul class="docs-sidebar__list">
33
+ {group.links.map((link) => {
34
+ const href = fullHref(link);
35
+ const active = isActive(link);
36
+ return (
37
+ <li class="docs-sidebar__item">
38
+ <a
39
+ href={href}
40
+ class={`docs-sidebar__link ${active ? 'docs-sidebar__link--active' : ''}`}
41
+ >
42
+ {link.label}
43
+ </a>
44
+ </li>
45
+ );
46
+ })}
47
+ </ul>
48
+ </div>
49
+ ))}
50
+ </nav>
51
+ </aside>