@invana/styling 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +165 -0
- package/package.json +103 -0
- package/src/base.css +41 -0
- package/src/index.css +151 -0
- package/src/theme.css +80 -0
- package/src/themes/default.css +96 -0
- package/src/themes/index.ts +6 -0
- package/src/themes/presets-base.css +97 -0
- package/src/themes/presets.css +1946 -0
- package/src/themes/tailwind.css +105 -0
- package/src/themes/vite.css +107 -0
- package/src/themes.config.ts +212 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tailwind CSS Theme
|
|
3
|
+
* Inspired by tailwindcss.com design
|
|
4
|
+
* Features: Sky/cyan accents, dark buttons, docs use violet for example buttons
|
|
5
|
+
* Based on research from https://github.com/tailwindlabs/tailwindcss.com
|
|
6
|
+
*
|
|
7
|
+
* Key colors from Tailwind website:
|
|
8
|
+
* - Primary buttons: bg-black (light mode), bg-gray-700 (dark mode)
|
|
9
|
+
* - Accent/links: sky-400/sky-500 (#38bdf8/#0ea5e9)
|
|
10
|
+
* - Example buttons in docs: violet-500 (#8b5cf6)
|
|
11
|
+
* - Dark mode background: oklch(.13 .028 261.692) ≈ hsl(262 18% 13%)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/* Light mode - Tailwind theme */
|
|
15
|
+
[data-theme="tailwind-light"],
|
|
16
|
+
.theme-tailwind-light {
|
|
17
|
+
/* Raw HSL values for opacity modifiers */
|
|
18
|
+
--background: 0 0% 100%;
|
|
19
|
+
--foreground: 222 47% 11%;
|
|
20
|
+
--muted: 210 40% 96%;
|
|
21
|
+
--muted-foreground: 215 16% 47%;
|
|
22
|
+
--accent: 199 89% 48%; /* sky-500: #0ea5e9 */
|
|
23
|
+
--accent-foreground: 0 0% 100%;
|
|
24
|
+
--popover: 0 0% 100%;
|
|
25
|
+
--popover-foreground: 222 47% 11%;
|
|
26
|
+
--border: 214 32% 91%;
|
|
27
|
+
--input: 214 32% 91%;
|
|
28
|
+
--card: 0 0% 100%;
|
|
29
|
+
--card-foreground: 222 47% 11%;
|
|
30
|
+
--primary: 0 0% 0%; /* black - main buttons */
|
|
31
|
+
--primary-foreground: 0 0% 100%;
|
|
32
|
+
--secondary: 210 40% 96%;
|
|
33
|
+
--secondary-foreground: 222 47% 11%;
|
|
34
|
+
--destructive: 0 84% 60%;
|
|
35
|
+
--destructive-foreground: 0 0% 100%;
|
|
36
|
+
--ring: 199 89% 48%; /* sky-500 */
|
|
37
|
+
|
|
38
|
+
/* Full HSL colors for direct use */
|
|
39
|
+
--color-background: hsl(0 0% 100%);
|
|
40
|
+
--color-foreground: hsl(222 47% 11%);
|
|
41
|
+
--color-card: hsl(0 0% 100%);
|
|
42
|
+
--color-card-foreground: hsl(222 47% 11%);
|
|
43
|
+
--color-popover: hsl(0 0% 100%);
|
|
44
|
+
--color-popover-foreground: hsl(222 47% 11%);
|
|
45
|
+
--color-primary: hsl(0 0% 0%);
|
|
46
|
+
--color-primary-foreground: hsl(0 0% 100%);
|
|
47
|
+
--color-secondary: hsl(210 40% 96%);
|
|
48
|
+
--color-secondary-foreground: hsl(222 47% 11%);
|
|
49
|
+
--color-muted: hsl(210 40% 96%);
|
|
50
|
+
--color-muted-foreground: hsl(215 16% 47%);
|
|
51
|
+
--color-accent: hsl(199 89% 48%);
|
|
52
|
+
--color-accent-foreground: hsl(0 0% 100%);
|
|
53
|
+
--color-destructive: hsl(0 84% 60%);
|
|
54
|
+
--color-destructive-foreground: hsl(0 0% 100%);
|
|
55
|
+
--color-border: hsl(214 32% 91%);
|
|
56
|
+
--color-input: hsl(214 32% 91%);
|
|
57
|
+
--color-ring: hsl(199 89% 48%);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Dark mode - Tailwind theme */
|
|
61
|
+
[data-theme="tailwind-dark"],
|
|
62
|
+
.theme-tailwind-dark {
|
|
63
|
+
/* Raw HSL values for opacity modifiers */
|
|
64
|
+
/* Dark background from tailwindcss.com - very dark blue-gray, almost black */
|
|
65
|
+
--background: 222 47% 5%;
|
|
66
|
+
--foreground: 210 40% 98%;
|
|
67
|
+
--muted: 217 33% 12%;
|
|
68
|
+
--muted-foreground: 215 20% 65%;
|
|
69
|
+
--accent: 198 93% 60%; /* sky-400: #38bdf8 */
|
|
70
|
+
--accent-foreground: 222 47% 11%;
|
|
71
|
+
--popover: 222 47% 7%;
|
|
72
|
+
--popover-foreground: 210 40% 98%;
|
|
73
|
+
--border: 217 19% 20%;
|
|
74
|
+
--input: 217 19% 20%;
|
|
75
|
+
--card: 222 47% 8%;
|
|
76
|
+
--card-foreground: 210 40% 98%;
|
|
77
|
+
--primary: 220 9% 46%; /* gray-700: #374151 - lighter button for dark mode */
|
|
78
|
+
--primary-foreground: 0 0% 100%;
|
|
79
|
+
--secondary: 217 33% 12%;
|
|
80
|
+
--secondary-foreground: 210 40% 98%;
|
|
81
|
+
--destructive: 0 63% 31%;
|
|
82
|
+
--destructive-foreground: 210 40% 98%;
|
|
83
|
+
--ring: 198 93% 60%; /* sky-400 */
|
|
84
|
+
|
|
85
|
+
/* Full HSL colors for direct use */
|
|
86
|
+
--color-background: hsl(222 47% 5%);
|
|
87
|
+
--color-foreground: hsl(210 40% 98%);
|
|
88
|
+
--color-card: hsl(222 47% 8%);
|
|
89
|
+
--color-card-foreground: hsl(210 40% 98%);
|
|
90
|
+
--color-popover: hsl(222 47% 7%);
|
|
91
|
+
--color-popover-foreground: hsl(210 40% 98%);
|
|
92
|
+
--color-primary: hsl(220 9% 46%);
|
|
93
|
+
--color-primary-foreground: hsl(0 0% 100%);
|
|
94
|
+
--color-secondary: hsl(217 33% 12%);
|
|
95
|
+
--color-secondary-foreground: hsl(210 40% 98%);
|
|
96
|
+
--color-muted: hsl(217 33% 12%);
|
|
97
|
+
--color-muted-foreground: hsl(215 20% 65%);
|
|
98
|
+
--color-accent: hsl(198 93% 60%);
|
|
99
|
+
--color-accent-foreground: hsl(222 47% 11%);
|
|
100
|
+
--color-destructive: hsl(0 63% 31%);
|
|
101
|
+
--color-destructive-foreground: hsl(210 40% 98%);
|
|
102
|
+
--color-border: hsl(217 19% 27%);
|
|
103
|
+
--color-input: hsl(217 19% 27%);
|
|
104
|
+
--color-ring: hsl(198 93% 60%);
|
|
105
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vite Theme
|
|
3
|
+
* Inspired by vite.dev design
|
|
4
|
+
* Based on research from https://github.com/vitejs/vite
|
|
5
|
+
*
|
|
6
|
+
* Key colors from Vite website:
|
|
7
|
+
* - Brand primary: #646cff (indigo/violet)
|
|
8
|
+
* - Brand hover: #535bf2
|
|
9
|
+
* - Brand lighter: #747bff, #9499ff
|
|
10
|
+
* - Link color: #646cff (dark mode), #747bff (light mode hover)
|
|
11
|
+
* - Button gradient: radial-gradient with #bd34fe (purple) and #41d1ff (cyan)
|
|
12
|
+
* - Background dark: #242424
|
|
13
|
+
* - Glow effects: purple #bd34fe, cyan #41d1ff
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/* Light mode - Vite theme */
|
|
17
|
+
[data-theme="vite-light"],
|
|
18
|
+
.theme-vite-light {
|
|
19
|
+
/* Raw HSL values for opacity modifiers */
|
|
20
|
+
--background: 0 0% 100%;
|
|
21
|
+
--foreground: 213 27% 27%; /* #213547 - Vite's light mode text */
|
|
22
|
+
--muted: 240 5% 96%;
|
|
23
|
+
--muted-foreground: 0 0% 53%; /* #888 - .read-the-docs color */
|
|
24
|
+
--accent: 235 85% 70%; /* #646cff - Vite brand */
|
|
25
|
+
--accent-foreground: 0 0% 100%;
|
|
26
|
+
--popover: 0 0% 100%;
|
|
27
|
+
--popover-foreground: 213 27% 27%;
|
|
28
|
+
--border: 240 6% 90%;
|
|
29
|
+
--input: 240 6% 90%;
|
|
30
|
+
--card: 0 0% 100%;
|
|
31
|
+
--card-foreground: 213 27% 27%;
|
|
32
|
+
--primary: 235 85% 70%; /* #646cff - Vite brand */
|
|
33
|
+
--primary-foreground: 0 0% 100%;
|
|
34
|
+
--secondary: 235 50% 95%;
|
|
35
|
+
--secondary-foreground: 235 85% 47%;
|
|
36
|
+
--destructive: 0 84% 60%;
|
|
37
|
+
--destructive-foreground: 0 0% 100%;
|
|
38
|
+
--ring: 235 85% 70%;
|
|
39
|
+
|
|
40
|
+
/* Full HSL colors for direct use */
|
|
41
|
+
--color-background: hsl(0 0% 100%);
|
|
42
|
+
--color-foreground: hsl(213 27% 27%);
|
|
43
|
+
--color-card: hsl(0 0% 100%);
|
|
44
|
+
--color-card-foreground: hsl(213 27% 27%);
|
|
45
|
+
--color-popover: hsl(0 0% 100%);
|
|
46
|
+
--color-popover-foreground: hsl(213 27% 27%);
|
|
47
|
+
--color-primary: hsl(235 85% 70%);
|
|
48
|
+
--color-primary-foreground: hsl(0 0% 100%);
|
|
49
|
+
--color-secondary: hsl(235 50% 95%);
|
|
50
|
+
--color-secondary-foreground: hsl(235 85% 47%);
|
|
51
|
+
--color-muted: hsl(240 5% 96%);
|
|
52
|
+
--color-muted-foreground: hsl(0 0% 53%);
|
|
53
|
+
--color-accent: hsl(235 85% 70%);
|
|
54
|
+
--color-accent-foreground: hsl(0 0% 100%);
|
|
55
|
+
--color-destructive: hsl(0 84% 60%);
|
|
56
|
+
--color-destructive-foreground: hsl(0 0% 100%);
|
|
57
|
+
--color-border: hsl(240 6% 90%);
|
|
58
|
+
--color-input: hsl(240 6% 90%);
|
|
59
|
+
--color-ring: hsl(235 85% 70%);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Dark mode - Vite theme */
|
|
63
|
+
[data-theme="vite-dark"],
|
|
64
|
+
.theme-vite-dark {
|
|
65
|
+
/* Raw HSL values for opacity modifiers */
|
|
66
|
+
/* Background: #242424 */
|
|
67
|
+
--background: 0 0% 14%;
|
|
68
|
+
--foreground: 0 0% 100%; /* white text with 87% opacity visually */
|
|
69
|
+
--muted: 0 0% 20%; /* #1a1a1a - button bg */
|
|
70
|
+
--muted-foreground: 0 0% 53%; /* #888 */
|
|
71
|
+
--accent: 235 85% 70%; /* #646cff */
|
|
72
|
+
--accent-foreground: 0 0% 100%;
|
|
73
|
+
--popover: 0 0% 10%;
|
|
74
|
+
--popover-foreground: 0 0% 100%;
|
|
75
|
+
--border: 235 85% 70%; /* button hover border: #646cff */
|
|
76
|
+
--input: 0 0% 20%;
|
|
77
|
+
--card: 0 0% 10%;
|
|
78
|
+
--card-foreground: 0 0% 100%;
|
|
79
|
+
--primary: 235 85% 70%; /* #646cff - Vite brand */
|
|
80
|
+
--primary-foreground: 0 0% 100%;
|
|
81
|
+
--secondary: 0 0% 20%;
|
|
82
|
+
--secondary-foreground: 0 0% 100%;
|
|
83
|
+
--destructive: 0 63% 50%;
|
|
84
|
+
--destructive-foreground: 0 0% 100%;
|
|
85
|
+
--ring: 235 85% 70%;
|
|
86
|
+
|
|
87
|
+
/* Full HSL colors for direct use */
|
|
88
|
+
--color-background: hsl(0 0% 14%);
|
|
89
|
+
--color-foreground: hsl(0 0% 100%);
|
|
90
|
+
--color-card: hsl(0 0% 10%);
|
|
91
|
+
--color-card-foreground: hsl(0 0% 100%);
|
|
92
|
+
--color-popover: hsl(0 0% 10%);
|
|
93
|
+
--color-popover-foreground: hsl(0 0% 100%);
|
|
94
|
+
--color-primary: hsl(235 85% 70%);
|
|
95
|
+
--color-primary-foreground: hsl(0 0% 100%);
|
|
96
|
+
--color-secondary: hsl(0 0% 20%);
|
|
97
|
+
--color-secondary-foreground: hsl(0 0% 100%);
|
|
98
|
+
--color-muted: hsl(0 0% 20%);
|
|
99
|
+
--color-muted-foreground: hsl(0 0% 53%);
|
|
100
|
+
--color-accent: hsl(235 85% 70%);
|
|
101
|
+
--color-accent-foreground: hsl(0 0% 100%);
|
|
102
|
+
--color-destructive: hsl(0 63% 50%);
|
|
103
|
+
--color-destructive-foreground: hsl(0 0% 100%);
|
|
104
|
+
--color-border: hsl(0 0% 20%);
|
|
105
|
+
--color-input: hsl(0 0% 20%);
|
|
106
|
+
--color-ring: hsl(235 85% 70%);
|
|
107
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Configuration System
|
|
3
|
+
* Central configuration for all available themes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ThemeVariant {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
mode: 'light' | 'dark' | 'system';
|
|
10
|
+
icon?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Theme {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
variants: ThemeVariant[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Theme Presets contributed as first-class themes. Unlike the classic themes
|
|
22
|
+
* (which vary by light/dark/system), each preset is a single mode and its
|
|
23
|
+
* selectable accent swatches become the "variant" axis — e.g. theme `dark-gold`
|
|
24
|
+
* with variants `dark-gold-gold`, `dark-gold-ocean`, … The colours for every
|
|
25
|
+
* combination are plain CSS in `themes/presets.css`; only the ids/names/modes
|
|
26
|
+
* are listed here so they register with the toolbar and `applyTheme()`.
|
|
27
|
+
*/
|
|
28
|
+
const DARK_ACCENTS: [string, string][] = [
|
|
29
|
+
['gold', 'Gold'], ['ocean', 'Ocean'], ['emerald', 'Emerald'], ['rose', 'Rose'],
|
|
30
|
+
['purple', 'Purple'], ['amber', 'Amber'], ['teal', 'Teal'], ['silver', 'Silver'],
|
|
31
|
+
];
|
|
32
|
+
const LIGHT_ACCENTS: [string, string][] = [
|
|
33
|
+
['indigo', 'Indigo'], ['ocean', 'Ocean'], ['emerald', 'Emerald'], ['rose', 'Rose'],
|
|
34
|
+
['purple', 'Purple'], ['amber', 'Amber'], ['teal', 'Teal'], ['slate', 'Slate'],
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
// `default` is each preset's signature accent. It is
|
|
38
|
+
// placed FIRST so it becomes the theme's default variant (variants[0]) — the value
|
|
39
|
+
// consumers fall back to when no accent is specified.
|
|
40
|
+
const PRESET_THEMES: { id: string; name: string; dark: boolean; accents: [string, string][]; default: string }[] = [
|
|
41
|
+
{ id: 'dark-gold', name: 'Dark Gold', dark: true, accents: DARK_ACCENTS, default: 'gold' },
|
|
42
|
+
{ id: 'dark-ocean', name: 'Dark Ocean', dark: true, accents: DARK_ACCENTS, default: 'ocean' },
|
|
43
|
+
{ id: 'dark-forest', name: 'Dark Forest', dark: true, accents: DARK_ACCENTS, default: 'emerald' },
|
|
44
|
+
{ id: 'dark-rose', name: 'Dark Rose', dark: true, accents: DARK_ACCENTS, default: 'rose' },
|
|
45
|
+
{ id: 'light-minimal', name: 'Light Minimal', dark: false, accents: LIGHT_ACCENTS, default: 'indigo' },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
function buildPresetThemes(): Theme[] {
|
|
49
|
+
return PRESET_THEMES.map((p) => {
|
|
50
|
+
// Signature accent first → it becomes variants[0] (the theme's default).
|
|
51
|
+
const ordered = [
|
|
52
|
+
...p.accents.filter(([id]) => id === p.default),
|
|
53
|
+
...p.accents.filter(([id]) => id !== p.default),
|
|
54
|
+
];
|
|
55
|
+
return {
|
|
56
|
+
id: p.id,
|
|
57
|
+
name: p.name,
|
|
58
|
+
description: `${p.name} preset — pick an accent swatch (${p.dark ? 'dark' : 'light'} mode)`,
|
|
59
|
+
variants: ordered.map(([accentId, accentName]) => ({
|
|
60
|
+
id: `${p.id}-${accentId}`,
|
|
61
|
+
name: accentName,
|
|
62
|
+
mode: (p.dark ? 'dark' : 'light') as ThemeVariant['mode'],
|
|
63
|
+
icon: 'circle',
|
|
64
|
+
})),
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const themes: Theme[] = [
|
|
70
|
+
{
|
|
71
|
+
id: 'default',
|
|
72
|
+
name: 'Invana',
|
|
73
|
+
description: 'Default Invana brand theme with green accents',
|
|
74
|
+
variants: [
|
|
75
|
+
{ id: 'default-light', name: 'Invana Light', mode: 'light', icon: 'sun' },
|
|
76
|
+
{ id: 'default-dark', name: 'Invana Dark', mode: 'dark', icon: 'moon' },
|
|
77
|
+
{ id: 'default-system', name: 'Invana System', mode: 'system', icon: 'circle' }
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'tailwind',
|
|
82
|
+
name: 'Tailwind',
|
|
83
|
+
description: 'Inspired by tailwindcss.com - black primary buttons, sky/cyan accents',
|
|
84
|
+
variants: [
|
|
85
|
+
{ id: 'tailwind-light', name: 'Tailwind Light', mode: 'light', icon: 'sun' },
|
|
86
|
+
{ id: 'tailwind-dark', name: 'Tailwind Dark', mode: 'dark', icon: 'moon' },
|
|
87
|
+
{ id: 'tailwind-system', name: 'Tailwind System', mode: 'system', icon: 'circle' }
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 'vite',
|
|
92
|
+
name: 'Vite',
|
|
93
|
+
description: 'Inspired by vite.dev - indigo/violet brand (#646cff)',
|
|
94
|
+
variants: [
|
|
95
|
+
{ id: 'vite-light', name: 'Vite Light', mode: 'light', icon: 'sun' },
|
|
96
|
+
{ id: 'vite-dark', name: 'Vite Dark', mode: 'dark', icon: 'moon' },
|
|
97
|
+
{ id: 'vite-system', name: 'Vite System', mode: 'system', icon: 'circle' }
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
...buildPresetThemes()
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get all theme variants as a flat list for selection
|
|
105
|
+
*/
|
|
106
|
+
export function getAllThemeVariants(): ThemeVariant[] {
|
|
107
|
+
return themes.flatMap(theme => theme.variants);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get a theme by its ID
|
|
112
|
+
*/
|
|
113
|
+
export function getThemeById(id: string): Theme | undefined {
|
|
114
|
+
return themes.find(theme => theme.id === id);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get a theme variant by its ID
|
|
119
|
+
*/
|
|
120
|
+
export function getThemeVariantById(id: string): { theme: Theme; variant: ThemeVariant } | undefined {
|
|
121
|
+
for (const theme of themes) {
|
|
122
|
+
const variant = theme.variants.find(v => v.id === id);
|
|
123
|
+
if (variant) {
|
|
124
|
+
return { theme, variant };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get the default theme variant
|
|
132
|
+
*/
|
|
133
|
+
export function getDefaultThemeVariant(): ThemeVariant {
|
|
134
|
+
return themes[0].variants[0];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Apply a theme to the document
|
|
139
|
+
*/
|
|
140
|
+
export function applyTheme(variantId: string): void {
|
|
141
|
+
const root = document.documentElement;
|
|
142
|
+
const result = getThemeVariantById(variantId);
|
|
143
|
+
|
|
144
|
+
if (!result) {
|
|
145
|
+
console.warn(`Theme variant "${variantId}" not found`);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const { variant } = result;
|
|
150
|
+
|
|
151
|
+
// Remove all existing theme classes
|
|
152
|
+
getAllThemeVariants().forEach(v => {
|
|
153
|
+
root.classList.remove(`theme-${v.id}`);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Handle system mode with prefers-color-scheme detection
|
|
157
|
+
if (variant.mode === 'system') {
|
|
158
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
159
|
+
const effectiveMode = prefersDark ? 'dark' : 'light';
|
|
160
|
+
const themeBase = variantId.replace('-system', '');
|
|
161
|
+
const effectiveVariantId = `${themeBase}-${effectiveMode}`;
|
|
162
|
+
|
|
163
|
+
// Apply the effective theme
|
|
164
|
+
root.setAttribute('data-theme', effectiveVariantId);
|
|
165
|
+
root.classList.add(`theme-${effectiveVariantId}`, effectiveMode);
|
|
166
|
+
root.classList.remove('light', 'dark');
|
|
167
|
+
root.classList.add(effectiveMode);
|
|
168
|
+
|
|
169
|
+
// Listen for system theme changes
|
|
170
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
171
|
+
const handleChange = (e: MediaQueryListEvent) => {
|
|
172
|
+
const newMode = e.matches ? 'dark' : 'light';
|
|
173
|
+
const newVariantId = `${themeBase}-${newMode}`;
|
|
174
|
+
const oldVariantId = `${themeBase}-${e.matches ? 'light' : 'dark'}`;
|
|
175
|
+
|
|
176
|
+
root.setAttribute('data-theme', newVariantId);
|
|
177
|
+
root.classList.remove(`theme-${oldVariantId}`, 'light', 'dark');
|
|
178
|
+
root.classList.add(`theme-${newVariantId}`, newMode);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// Clean up previous listener and add new one
|
|
182
|
+
if ((window as any).__themeMediaQueryHandler) {
|
|
183
|
+
mediaQuery.removeEventListener('change', (window as any).__themeMediaQueryHandler);
|
|
184
|
+
}
|
|
185
|
+
(window as any).__themeMediaQueryHandler = handleChange;
|
|
186
|
+
mediaQuery.addEventListener('change', handleChange);
|
|
187
|
+
} else {
|
|
188
|
+
// Apply light or dark mode directly
|
|
189
|
+
root.setAttribute('data-theme', variantId);
|
|
190
|
+
root.classList.add(`theme-${variantId}`, variant.mode);
|
|
191
|
+
root.classList.remove('light', 'dark');
|
|
192
|
+
root.classList.add(variant.mode);
|
|
193
|
+
|
|
194
|
+
// Clean up system mode listener if switching away from system mode
|
|
195
|
+
if ((window as any).__themeMediaQueryHandler) {
|
|
196
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
197
|
+
mediaQuery.removeEventListener('change', (window as any).__themeMediaQueryHandler);
|
|
198
|
+
delete (window as any).__themeMediaQueryHandler;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get theme variant for Storybook toolbar items
|
|
205
|
+
*/
|
|
206
|
+
export function getStorybookThemeItems() {
|
|
207
|
+
return getAllThemeVariants().map(variant => ({
|
|
208
|
+
value: variant.id,
|
|
209
|
+
title: variant.name,
|
|
210
|
+
icon: variant.icon || (variant.mode === 'system' ? 'circle' : variant.mode === 'light' ? 'sun' : 'moon')
|
|
211
|
+
}));
|
|
212
|
+
}
|