stoop 0.3.0 → 0.4.0
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 +125 -0
- package/dist/api/core-api.d.ts +34 -0
- package/dist/api/{keyframes.js → core-api.js} +45 -5
- package/dist/api/global-css.js +11 -58
- package/dist/api/styled.d.ts +0 -1
- package/dist/api/styled.js +265 -16
- package/dist/api/theme-provider.d.ts +41 -0
- package/dist/api/theme-provider.js +223 -0
- package/dist/constants.d.ts +11 -1
- package/dist/constants.js +11 -1
- package/dist/core/compiler.d.ts +11 -0
- package/dist/core/compiler.js +15 -7
- package/dist/core/theme-manager.d.ts +34 -3
- package/dist/core/theme-manager.js +55 -45
- package/dist/core/variants.js +9 -3
- package/dist/create-stoop-internal.d.ts +30 -0
- package/dist/create-stoop-internal.js +123 -0
- package/dist/create-stoop-ssr.d.ts +10 -0
- package/dist/create-stoop-ssr.js +26 -0
- package/dist/create-stoop.d.ts +32 -1
- package/dist/create-stoop.js +78 -69
- package/dist/inject.d.ts +113 -0
- package/dist/inject.js +308 -0
- package/dist/types/index.d.ts +147 -12
- package/dist/types/react-polymorphic-types.d.ts +13 -2
- package/dist/utils/auto-preload.d.ts +45 -0
- package/dist/utils/auto-preload.js +167 -0
- package/dist/utils/helpers.d.ts +64 -0
- package/dist/utils/helpers.js +150 -0
- package/dist/utils/storage.d.ts +148 -0
- package/dist/utils/storage.js +396 -0
- package/dist/utils/{string.d.ts → theme-utils.d.ts} +20 -3
- package/dist/utils/{string.js → theme-utils.js} +109 -9
- package/dist/utils/theme.d.ts +14 -2
- package/dist/utils/theme.js +41 -16
- package/package.json +48 -23
- package/dist/api/create-theme.d.ts +0 -13
- package/dist/api/create-theme.js +0 -43
- package/dist/api/css.d.ts +0 -16
- package/dist/api/css.js +0 -20
- package/dist/api/keyframes.d.ts +0 -16
- package/dist/api/provider.d.ts +0 -19
- package/dist/api/provider.js +0 -109
- package/dist/api/use-theme.d.ts +0 -13
- package/dist/api/use-theme.js +0 -21
- package/dist/create-stoop-server.d.ts +0 -33
- package/dist/create-stoop-server.js +0 -130
- package/dist/index.d.ts +0 -6
- package/dist/index.js +0 -5
- package/dist/inject/browser.d.ts +0 -58
- package/dist/inject/browser.js +0 -149
- package/dist/inject/dedup.d.ts +0 -29
- package/dist/inject/dedup.js +0 -38
- package/dist/inject/index.d.ts +0 -40
- package/dist/inject/index.js +0 -75
- package/dist/inject/ssr.d.ts +0 -27
- package/dist/inject/ssr.js +0 -46
- package/dist/ssr.d.ts +0 -21
- package/dist/ssr.js +0 -19
- package/dist/utils/environment.d.ts +0 -6
- package/dist/utils/environment.js +0 -12
- package/dist/utils/theme-map.d.ts +0 -22
- package/dist/utils/theme-map.js +0 -97
- package/dist/utils/theme-validation.d.ts +0 -13
- package/dist/utils/theme-validation.js +0 -36
- package/dist/utils/type-guards.d.ts +0 -26
- package/dist/utils/type-guards.js +0 -38
- package/dist/utils/utilities.d.ts +0 -14
- package/dist/utils/utilities.js +0 -43
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Provider component and hook.
|
|
3
|
+
* Manages theme state, localStorage persistence, cookie sync, and centralized theme variable updates.
|
|
4
|
+
* Includes the useTheme hook for accessing theme management context.
|
|
5
|
+
*/
|
|
6
|
+
import { type ComponentType, type Context } from "react";
|
|
7
|
+
import type { ProviderProps, Theme, ThemeContextValue, ThemeManagementContextValue } from "../types";
|
|
8
|
+
/**
|
|
9
|
+
* Creates a Provider component for theme management.
|
|
10
|
+
*
|
|
11
|
+
* @param themes - Map of theme names to theme objects
|
|
12
|
+
* @param defaultTheme - Default theme object
|
|
13
|
+
* @param prefix - Optional prefix for CSS variable scoping
|
|
14
|
+
* @param globalCss - Optional global CSS object from config
|
|
15
|
+
* @param globalCssFunction - Optional globalCss function from createStoop
|
|
16
|
+
* @returns Provider component, theme context, and theme management context
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* To prevent FOUC (Flash of Unstyled Content) when a user has a non-default theme stored,
|
|
20
|
+
* call `preloadTheme()` from your stoop instance in a script tag before React hydrates:
|
|
21
|
+
*
|
|
22
|
+
* ```html
|
|
23
|
+
* <script>
|
|
24
|
+
* // Read theme from storage and preload before React renders
|
|
25
|
+
* const storedTheme = localStorage.getItem('stoop-theme') || 'light';
|
|
26
|
+
* stoopInstance.preloadTheme(storedTheme);
|
|
27
|
+
* </script>
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare function createProvider(themes: Record<string, Theme>, defaultTheme: Theme, prefix?: string, globalCss?: import("../types").CSS, globalCssFunction?: import("../types").GlobalCSSFunction): {
|
|
31
|
+
Provider: ComponentType<ProviderProps>;
|
|
32
|
+
ThemeContext: Context<ThemeContextValue | null>;
|
|
33
|
+
ThemeManagementContext: Context<ThemeManagementContextValue | null>;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Creates a useTheme hook for a specific theme management context.
|
|
37
|
+
*
|
|
38
|
+
* @param ThemeManagementContext - React context for theme management
|
|
39
|
+
* @returns Hook function that returns theme management context value
|
|
40
|
+
*/
|
|
41
|
+
export declare function createUseThemeHook(ThemeManagementContext: Context<ThemeManagementContextValue | null>): () => ThemeManagementContextValue;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* Theme Provider component and hook.
|
|
5
|
+
* Manages theme state, localStorage persistence, cookie sync, and centralized theme variable updates.
|
|
6
|
+
* Includes the useTheme hook for accessing theme management context.
|
|
7
|
+
*/
|
|
8
|
+
import { createContext, useCallback, useContext, useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
9
|
+
import { injectAllThemes } from "../core/theme-manager";
|
|
10
|
+
import { isBrowser, isProduction } from "../utils/helpers";
|
|
11
|
+
import { getCookie, setCookie, getFromStorage, setInStorage } from "../utils/storage";
|
|
12
|
+
/**
|
|
13
|
+
* Syncs a theme value between cookie and localStorage.
|
|
14
|
+
* If cookie exists, syncs to localStorage. If localStorage exists, syncs to cookie.
|
|
15
|
+
*
|
|
16
|
+
* @param value - Theme value to sync
|
|
17
|
+
* @param cookieKey - Cookie key (if undefined, cookie sync is skipped)
|
|
18
|
+
* @param storageKey - LocalStorage key
|
|
19
|
+
*/
|
|
20
|
+
function syncThemeStorage(value, cookieKey, storageKey) {
|
|
21
|
+
if (!isBrowser()) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const cookieValue = cookieKey ? getCookie(cookieKey) : null;
|
|
25
|
+
const localStorageResult = getFromStorage(storageKey);
|
|
26
|
+
const localStorageValue = localStorageResult.success ? localStorageResult.value : null;
|
|
27
|
+
// Sync cookie -> localStorage
|
|
28
|
+
if (cookieValue === value && localStorageValue !== value) {
|
|
29
|
+
setInStorage(storageKey, value);
|
|
30
|
+
}
|
|
31
|
+
// Sync localStorage -> cookie
|
|
32
|
+
if (localStorageValue === value && cookieKey && cookieValue !== value) {
|
|
33
|
+
setCookie(cookieKey, value);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Reads theme from cookie or localStorage, preferring cookie if available.
|
|
38
|
+
*
|
|
39
|
+
* @param cookieKey - Cookie key (if undefined, cookie is not checked)
|
|
40
|
+
* @param storageKey - LocalStorage key
|
|
41
|
+
* @param themes - Available themes map for validation
|
|
42
|
+
* @returns Theme name or null if not found or invalid
|
|
43
|
+
*/
|
|
44
|
+
function readThemeFromStorage(cookieKey, storageKey, themes) {
|
|
45
|
+
if (!isBrowser()) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
// Try cookie first if cookieKey is provided
|
|
49
|
+
if (cookieKey !== undefined) {
|
|
50
|
+
const cookieValue = getCookie(cookieKey);
|
|
51
|
+
if (cookieValue && themes[cookieValue]) {
|
|
52
|
+
return cookieValue;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Fall back to localStorage
|
|
56
|
+
const storageResult = getFromStorage(storageKey);
|
|
57
|
+
const stored = storageResult.success ? storageResult.value : null;
|
|
58
|
+
if (stored && themes[stored]) {
|
|
59
|
+
return stored;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Creates a Provider component for theme management.
|
|
65
|
+
*
|
|
66
|
+
* @param themes - Map of theme names to theme objects
|
|
67
|
+
* @param defaultTheme - Default theme object
|
|
68
|
+
* @param prefix - Optional prefix for CSS variable scoping
|
|
69
|
+
* @param globalCss - Optional global CSS object from config
|
|
70
|
+
* @param globalCssFunction - Optional globalCss function from createStoop
|
|
71
|
+
* @returns Provider component, theme context, and theme management context
|
|
72
|
+
*
|
|
73
|
+
* @remarks
|
|
74
|
+
* To prevent FOUC (Flash of Unstyled Content) when a user has a non-default theme stored,
|
|
75
|
+
* call `preloadTheme()` from your stoop instance in a script tag before React hydrates:
|
|
76
|
+
*
|
|
77
|
+
* ```html
|
|
78
|
+
* <script>
|
|
79
|
+
* // Read theme from storage and preload before React renders
|
|
80
|
+
* const storedTheme = localStorage.getItem('stoop-theme') || 'light';
|
|
81
|
+
* stoopInstance.preloadTheme(storedTheme);
|
|
82
|
+
* </script>
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export function createProvider(themes, defaultTheme, prefix = "stoop", globalCss, globalCssFunction) {
|
|
86
|
+
const ThemeContext = createContext(null);
|
|
87
|
+
const ThemeManagementContext = createContext(null);
|
|
88
|
+
const availableThemeNames = Object.keys(themes);
|
|
89
|
+
const firstThemeName = availableThemeNames[0] || "default";
|
|
90
|
+
// Create global styles function from config if provided
|
|
91
|
+
const configGlobalStyles = globalCss && globalCssFunction ? globalCssFunction(globalCss) : undefined;
|
|
92
|
+
function Provider({ attribute = "data-theme", children, cookieKey, defaultTheme: defaultThemeProp, storageKey = "stoop-theme", }) {
|
|
93
|
+
// SSR-safe initialization: always start with default theme to match SSR
|
|
94
|
+
// This prevents hydration mismatch - server always renders with default theme
|
|
95
|
+
// Hydration will happen in useLayoutEffect to update to stored theme
|
|
96
|
+
const [themeName, setThemeNameState] = useState(defaultThemeProp || firstThemeName);
|
|
97
|
+
// Track if hydration has occurred to prevent re-running hydration effect
|
|
98
|
+
const hasHydratedRef = useRef(false);
|
|
99
|
+
// Hydrate from cookie/localStorage after mount to prevent hydration mismatch
|
|
100
|
+
// Only run once on mount - storage changes are handled by the storage event listener
|
|
101
|
+
useLayoutEffect(() => {
|
|
102
|
+
if (!isBrowser() || hasHydratedRef.current) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const stored = readThemeFromStorage(cookieKey, storageKey, themes);
|
|
106
|
+
if (stored) {
|
|
107
|
+
// Sync between cookie and localStorage
|
|
108
|
+
syncThemeStorage(stored, cookieKey, storageKey);
|
|
109
|
+
// Only update if different from initial state to avoid unnecessary re-render
|
|
110
|
+
if (stored !== themeName) {
|
|
111
|
+
setThemeNameState(stored);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
hasHydratedRef.current = true;
|
|
115
|
+
}, [cookieKey, storageKey, themes]); // Removed themeName from deps - only run once on mount
|
|
116
|
+
// Listen for storage changes from other tabs/windows
|
|
117
|
+
useLayoutEffect(() => {
|
|
118
|
+
if (!isBrowser()) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const handleStorageChange = (e) => {
|
|
122
|
+
if (e.key === storageKey && e.newValue && themes[e.newValue] && e.newValue !== themeName) {
|
|
123
|
+
setThemeNameState(e.newValue);
|
|
124
|
+
// Sync to cookie if cookieKey is provided
|
|
125
|
+
syncThemeStorage(e.newValue, cookieKey, storageKey);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
window.addEventListener("storage", handleStorageChange);
|
|
129
|
+
return () => {
|
|
130
|
+
window.removeEventListener("storage", handleStorageChange);
|
|
131
|
+
};
|
|
132
|
+
}, [storageKey, cookieKey, themeName, themes]);
|
|
133
|
+
const currentTheme = useMemo(() => {
|
|
134
|
+
return themes[themeName] || themes[defaultThemeProp || firstThemeName] || defaultTheme;
|
|
135
|
+
}, [themeName, defaultThemeProp, firstThemeName, themes, defaultTheme]);
|
|
136
|
+
// Track if themes and global styles have been injected
|
|
137
|
+
const themesInjectedRef = useRef(false);
|
|
138
|
+
const globalStylesInjectedRef = useRef(false);
|
|
139
|
+
// Inject all theme CSS variables once on mount (before global styles and theme switching)
|
|
140
|
+
// This ensures all themes are available simultaneously via attribute selectors
|
|
141
|
+
useLayoutEffect(() => {
|
|
142
|
+
if (!isBrowser() || themesInjectedRef.current) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Inject all themes using attribute selectors
|
|
146
|
+
// This allows instant theme switching by only changing the data-theme attribute
|
|
147
|
+
injectAllThemes(themes, prefix, attribute);
|
|
148
|
+
themesInjectedRef.current = true;
|
|
149
|
+
}, [themes, prefix, attribute]);
|
|
150
|
+
// Inject global styles once on mount (after themes are injected)
|
|
151
|
+
useLayoutEffect(() => {
|
|
152
|
+
if (!isBrowser() || globalStylesInjectedRef.current) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// Inject global styles from config
|
|
156
|
+
// These use CSS variables, so they'll automatically work with all themes
|
|
157
|
+
if (configGlobalStyles) {
|
|
158
|
+
configGlobalStyles();
|
|
159
|
+
globalStylesInjectedRef.current = true;
|
|
160
|
+
}
|
|
161
|
+
}, [configGlobalStyles]);
|
|
162
|
+
// Update data-theme attribute when theme changes
|
|
163
|
+
// No need to update CSS variables since all themes are already injected
|
|
164
|
+
useLayoutEffect(() => {
|
|
165
|
+
if (!isBrowser()) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// Simply update the data-theme attribute - CSS variables are already available
|
|
169
|
+
if (attribute) {
|
|
170
|
+
document.documentElement.setAttribute(attribute, themeName);
|
|
171
|
+
}
|
|
172
|
+
}, [themeName, attribute]);
|
|
173
|
+
const setTheme = useCallback((newThemeName) => {
|
|
174
|
+
if (themes[newThemeName]) {
|
|
175
|
+
setThemeNameState(newThemeName);
|
|
176
|
+
setInStorage(storageKey, newThemeName);
|
|
177
|
+
syncThemeStorage(newThemeName, cookieKey, storageKey);
|
|
178
|
+
}
|
|
179
|
+
else if (!isProduction()) {
|
|
180
|
+
// eslint-disable-next-line no-console
|
|
181
|
+
console.warn(`[Stoop] Theme "${newThemeName}" not found. Available themes: ${availableThemeNames.join(", ")}`);
|
|
182
|
+
}
|
|
183
|
+
}, [storageKey, cookieKey, themes, availableThemeNames, themeName]);
|
|
184
|
+
const themeContextValue = useMemo(() => ({
|
|
185
|
+
theme: currentTheme,
|
|
186
|
+
themeName,
|
|
187
|
+
}), [currentTheme, themeName]);
|
|
188
|
+
const toggleTheme = useCallback(() => {
|
|
189
|
+
const currentIndex = availableThemeNames.indexOf(themeName);
|
|
190
|
+
const nextIndex = (currentIndex + 1) % availableThemeNames.length;
|
|
191
|
+
const newTheme = availableThemeNames[nextIndex];
|
|
192
|
+
setTheme(newTheme);
|
|
193
|
+
}, [themeName, setTheme, availableThemeNames]);
|
|
194
|
+
const managementContextValue = useMemo(() => ({
|
|
195
|
+
availableThemes: availableThemeNames,
|
|
196
|
+
setTheme,
|
|
197
|
+
theme: currentTheme,
|
|
198
|
+
themeName,
|
|
199
|
+
toggleTheme,
|
|
200
|
+
}), [currentTheme, themeName, setTheme, toggleTheme]);
|
|
201
|
+
return (_jsx(ThemeContext.Provider, { value: themeContextValue, children: _jsx(ThemeManagementContext.Provider, { value: managementContextValue, children: children }) }));
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
Provider,
|
|
205
|
+
ThemeContext,
|
|
206
|
+
ThemeManagementContext,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Creates a useTheme hook for a specific theme management context.
|
|
211
|
+
*
|
|
212
|
+
* @param ThemeManagementContext - React context for theme management
|
|
213
|
+
* @returns Hook function that returns theme management context value
|
|
214
|
+
*/
|
|
215
|
+
export function createUseThemeHook(ThemeManagementContext) {
|
|
216
|
+
return function useTheme() {
|
|
217
|
+
const context = useContext(ThemeManagementContext);
|
|
218
|
+
if (!context) {
|
|
219
|
+
throw new Error("useTheme must be used within a Provider");
|
|
220
|
+
}
|
|
221
|
+
return context;
|
|
222
|
+
};
|
|
223
|
+
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -7,13 +7,23 @@ export declare const EMPTY_CSS: CSS;
|
|
|
7
7
|
export declare const MAX_CSS_CACHE_SIZE = 10000;
|
|
8
8
|
export declare const MAX_CLASS_NAME_CACHE_SIZE = 5000;
|
|
9
9
|
export declare const MAX_CSS_NESTING_DEPTH = 10;
|
|
10
|
+
/**
|
|
11
|
+
* Cache size limits for various LRU caches.
|
|
12
|
+
*/
|
|
13
|
+
export declare const KEYFRAME_CACHE_LIMIT = 500;
|
|
14
|
+
export declare const SANITIZE_CACHE_SIZE_LIMIT = 1000;
|
|
15
|
+
/**
|
|
16
|
+
* Cookie defaults.
|
|
17
|
+
*/
|
|
18
|
+
export declare const DEFAULT_COOKIE_MAX_AGE = 31536000;
|
|
19
|
+
export declare const DEFAULT_COOKIE_PATH = "/";
|
|
10
20
|
/**
|
|
11
21
|
* Approved theme scales - only these scales are allowed in theme objects.
|
|
12
22
|
*/
|
|
13
23
|
export declare const APPROVED_THEME_SCALES: ReadonlyArray<ThemeScale>;
|
|
14
24
|
/**
|
|
15
25
|
* Default themeMap mapping CSS properties to theme scales.
|
|
16
|
-
* Covers
|
|
26
|
+
* Covers common CSS properties for zero-config experience.
|
|
17
27
|
* Missing properties gracefully fallback to pattern-based auto-detection.
|
|
18
28
|
*/
|
|
19
29
|
export declare const DEFAULT_THEME_MAP: Record<string, ThemeScale>;
|
package/dist/constants.js
CHANGED
|
@@ -6,6 +6,16 @@ export const EMPTY_CSS = Object.freeze({});
|
|
|
6
6
|
export const MAX_CSS_CACHE_SIZE = 10000;
|
|
7
7
|
export const MAX_CLASS_NAME_CACHE_SIZE = 5000;
|
|
8
8
|
export const MAX_CSS_NESTING_DEPTH = 10;
|
|
9
|
+
/**
|
|
10
|
+
* Cache size limits for various LRU caches.
|
|
11
|
+
*/
|
|
12
|
+
export const KEYFRAME_CACHE_LIMIT = 500;
|
|
13
|
+
export const SANITIZE_CACHE_SIZE_LIMIT = 1000;
|
|
14
|
+
/**
|
|
15
|
+
* Cookie defaults.
|
|
16
|
+
*/
|
|
17
|
+
export const DEFAULT_COOKIE_MAX_AGE = 31536000; // 1 year in seconds
|
|
18
|
+
export const DEFAULT_COOKIE_PATH = "/";
|
|
9
19
|
/**
|
|
10
20
|
* Approved theme scales - only these scales are allowed in theme objects.
|
|
11
21
|
*/
|
|
@@ -26,7 +36,7 @@ export const APPROVED_THEME_SCALES = [
|
|
|
26
36
|
];
|
|
27
37
|
/**
|
|
28
38
|
* Default themeMap mapping CSS properties to theme scales.
|
|
29
|
-
* Covers
|
|
39
|
+
* Covers common CSS properties for zero-config experience.
|
|
30
40
|
* Missing properties gracefully fallback to pattern-based auto-detection.
|
|
31
41
|
*/
|
|
32
42
|
export const DEFAULT_THEME_MAP = {
|
package/dist/core/compiler.d.ts
CHANGED
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
* Handles nested selectors, media queries, styled component targeting, and theme tokens.
|
|
5
5
|
*/
|
|
6
6
|
import type { CSS, Theme, ThemeScale, UtilityFunction } from "../types";
|
|
7
|
+
/**
|
|
8
|
+
* Converts a CSS object to a CSS string with proper nesting and selectors.
|
|
9
|
+
* Handles pseudo-selectors, media queries, combinators, and styled component targeting.
|
|
10
|
+
*
|
|
11
|
+
* @param obj - CSS object to convert
|
|
12
|
+
* @param selector - Current selector context
|
|
13
|
+
* @param depth - Current nesting depth
|
|
14
|
+
* @param media - Media query breakpoints
|
|
15
|
+
* @returns CSS string
|
|
16
|
+
*/
|
|
17
|
+
export declare function cssObjectToString(obj: CSS, selector?: string, depth?: number, media?: Record<string, string>): string;
|
|
7
18
|
/**
|
|
8
19
|
* Compiles CSS objects into CSS strings and generates unique class names.
|
|
9
20
|
* Handles nested selectors, media queries, styled component targeting, and theme tokens.
|
package/dist/core/compiler.js
CHANGED
|
@@ -5,25 +5,28 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { MAX_CSS_NESTING_DEPTH, STOOP_COMPONENT_SYMBOL } from "../constants";
|
|
7
7
|
import { injectCSS } from "../inject";
|
|
8
|
-
import {
|
|
8
|
+
import { isValidCSSObject, applyUtilities, isStyledComponentRef } from "../utils/helpers";
|
|
9
9
|
import { replaceThemeTokensWithVars } from "../utils/theme";
|
|
10
|
-
import {
|
|
11
|
-
import { applyUtilities } from "../utils/utilities";
|
|
10
|
+
import { escapeCSSValue, hash, sanitizeCSSPropertyName, sanitizeCSSSelector, sanitizeMediaQuery, sanitizePrefix, } from "../utils/theme-utils";
|
|
12
11
|
import { classNameCache, cssStringCache } from "./cache";
|
|
13
12
|
/**
|
|
14
13
|
* Checks if a key/value pair represents a styled component reference.
|
|
14
|
+
* Uses shared isStyledComponentRef helper for consistency.
|
|
15
15
|
*
|
|
16
16
|
* @param key - Property key to check
|
|
17
17
|
* @param value - Property value to check
|
|
18
18
|
* @returns True if key/value represents a styled component reference
|
|
19
19
|
*/
|
|
20
20
|
function isStyledComponentKey(key, value) {
|
|
21
|
+
// Check symbol key
|
|
21
22
|
if (typeof key === "symbol" && key === STOOP_COMPONENT_SYMBOL) {
|
|
22
23
|
return true;
|
|
23
24
|
}
|
|
24
|
-
if
|
|
25
|
+
// Check if value is a styled component ref (consolidated check)
|
|
26
|
+
if (isStyledComponentRef(value)) {
|
|
25
27
|
return true;
|
|
26
28
|
}
|
|
29
|
+
// Check string key prefix
|
|
27
30
|
if (typeof key === "string" && key.startsWith("__STOOP_COMPONENT_")) {
|
|
28
31
|
return true;
|
|
29
32
|
}
|
|
@@ -58,7 +61,7 @@ function getClassNameFromKeyOrValue(key, value) {
|
|
|
58
61
|
* @param media - Media query breakpoints
|
|
59
62
|
* @returns CSS string
|
|
60
63
|
*/
|
|
61
|
-
function cssObjectToString(obj, selector = "", depth = 0, media) {
|
|
64
|
+
export function cssObjectToString(obj, selector = "", depth = 0, media) {
|
|
62
65
|
if (!obj || typeof obj !== "object") {
|
|
63
66
|
return "";
|
|
64
67
|
}
|
|
@@ -162,8 +165,13 @@ function cssObjectToString(obj, selector = "", depth = 0, media) {
|
|
|
162
165
|
}
|
|
163
166
|
}
|
|
164
167
|
}
|
|
165
|
-
|
|
166
|
-
|
|
168
|
+
// Use array join pattern for better performance with large stylesheets
|
|
169
|
+
const parts = [];
|
|
170
|
+
if (cssProperties.length > 0) {
|
|
171
|
+
parts.push(`${selector} { ${cssProperties.join(" ")} }`);
|
|
172
|
+
}
|
|
173
|
+
parts.push(...nestedRulesList);
|
|
174
|
+
return parts.join("");
|
|
167
175
|
}
|
|
168
176
|
/**
|
|
169
177
|
* Compiles CSS objects into CSS strings and generates unique class names.
|
|
@@ -14,9 +14,40 @@ import type { Theme } from "../types";
|
|
|
14
14
|
*/
|
|
15
15
|
export declare function registerDefaultTheme(theme: Theme, prefix?: string): void;
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
17
|
+
* Gets the default theme for a given prefix.
|
|
18
|
+
*
|
|
19
|
+
* @param prefix - Optional prefix for theme scoping
|
|
20
|
+
* @returns Default theme or null if not registered
|
|
21
|
+
*/
|
|
22
|
+
export declare function getDefaultTheme(prefix?: string): Theme | null;
|
|
23
|
+
/**
|
|
24
|
+
* Core theme merging logic.
|
|
25
|
+
* Merges source theme into target theme, handling nested objects.
|
|
26
|
+
* Shared implementation used by both mergeWithDefaultTheme and createTheme.
|
|
27
|
+
*
|
|
28
|
+
* @param target - Target theme to merge into
|
|
29
|
+
* @param source - Source theme to merge from
|
|
30
|
+
* @returns Merged theme
|
|
31
|
+
*/
|
|
32
|
+
export declare function mergeThemes(target: Theme, source: Theme | Partial<Theme>): Theme;
|
|
33
|
+
/**
|
|
34
|
+
* Merges a theme with the default theme if it's not already the default theme.
|
|
35
|
+
* This ensures all themes extend the default theme, keeping all original properties.
|
|
36
|
+
* Uses caching to avoid repeated merging of the same theme objects.
|
|
37
|
+
*
|
|
38
|
+
* @param theme - Theme to merge
|
|
39
|
+
* @param prefix - Optional prefix for theme scoping
|
|
40
|
+
* @returns Merged theme (or original if it's the default theme)
|
|
41
|
+
*/
|
|
42
|
+
export declare function mergeWithDefaultTheme(theme: Theme, prefix?: string): Theme;
|
|
43
|
+
/**
|
|
44
|
+
* Injects CSS variables for all themes using attribute selectors.
|
|
45
|
+
* This allows all themes to be available simultaneously, with theme switching
|
|
46
|
+
* handled by changing the data-theme attribute. This prevents layout shifts
|
|
47
|
+
* and eliminates the need to replace CSS variables on theme change.
|
|
18
48
|
*
|
|
19
|
-
* @param
|
|
49
|
+
* @param themes - Map of theme names to theme objects
|
|
20
50
|
* @param prefix - Optional prefix for CSS variable names
|
|
51
|
+
* @param attribute - Attribute name for theme selection (defaults to 'data-theme')
|
|
21
52
|
*/
|
|
22
|
-
export declare function
|
|
53
|
+
export declare function injectAllThemes(themes: Record<string, Theme>, prefix?: string, attribute?: string): void;
|
|
@@ -4,11 +4,10 @@
|
|
|
4
4
|
* Ensures CSS variables are injected and kept in sync with theme updates.
|
|
5
5
|
* Automatically merges themes with the default theme when applied.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import { isBrowser } from "../utils/
|
|
9
|
-
import {
|
|
7
|
+
import { injectAllThemeVariables } from "../inject";
|
|
8
|
+
import { isBrowser } from "../utils/helpers";
|
|
9
|
+
import { generateAllThemeVariables, themesAreEqual } from "../utils/theme";
|
|
10
10
|
const defaultThemes = new Map();
|
|
11
|
-
const mergedThemeCache = new WeakMap();
|
|
12
11
|
/**
|
|
13
12
|
* Registers the default theme for a given prefix.
|
|
14
13
|
* Called automatically by createStoop.
|
|
@@ -26,10 +25,44 @@ export function registerDefaultTheme(theme, prefix = "stoop") {
|
|
|
26
25
|
* @param prefix - Optional prefix for theme scoping
|
|
27
26
|
* @returns Default theme or null if not registered
|
|
28
27
|
*/
|
|
29
|
-
function getDefaultTheme(prefix = "stoop") {
|
|
28
|
+
export function getDefaultTheme(prefix = "stoop") {
|
|
30
29
|
const sanitizedPrefix = prefix || "";
|
|
31
30
|
return defaultThemes.get(sanitizedPrefix) || null;
|
|
32
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Core theme merging logic.
|
|
34
|
+
* Merges source theme into target theme, handling nested objects.
|
|
35
|
+
* Shared implementation used by both mergeWithDefaultTheme and createTheme.
|
|
36
|
+
*
|
|
37
|
+
* @param target - Target theme to merge into
|
|
38
|
+
* @param source - Source theme to merge from
|
|
39
|
+
* @returns Merged theme
|
|
40
|
+
*/
|
|
41
|
+
export function mergeThemes(target, source) {
|
|
42
|
+
const merged = { ...target };
|
|
43
|
+
const sourceKeys = Object.keys(source);
|
|
44
|
+
for (const key of sourceKeys) {
|
|
45
|
+
if (key === "media") {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const sourceValue = source[key];
|
|
49
|
+
const targetValue = target[key];
|
|
50
|
+
if (sourceValue &&
|
|
51
|
+
typeof sourceValue === "object" &&
|
|
52
|
+
!Array.isArray(sourceValue) &&
|
|
53
|
+
targetValue &&
|
|
54
|
+
typeof targetValue === "object" &&
|
|
55
|
+
!Array.isArray(targetValue)) {
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
57
|
+
merged[key] = { ...targetValue, ...sourceValue };
|
|
58
|
+
}
|
|
59
|
+
else if (sourceValue !== undefined) {
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
61
|
+
merged[key] = sourceValue;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return merged;
|
|
65
|
+
}
|
|
33
66
|
/**
|
|
34
67
|
* Merges a theme with the default theme if it's not already the default theme.
|
|
35
68
|
* This ensures all themes extend the default theme, keeping all original properties.
|
|
@@ -39,7 +72,7 @@ function getDefaultTheme(prefix = "stoop") {
|
|
|
39
72
|
* @param prefix - Optional prefix for theme scoping
|
|
40
73
|
* @returns Merged theme (or original if it's the default theme)
|
|
41
74
|
*/
|
|
42
|
-
function mergeWithDefaultTheme(theme, prefix = "stoop") {
|
|
75
|
+
export function mergeWithDefaultTheme(theme, prefix = "stoop") {
|
|
43
76
|
const defaultTheme = getDefaultTheme(prefix);
|
|
44
77
|
if (!defaultTheme) {
|
|
45
78
|
return theme;
|
|
@@ -47,51 +80,28 @@ function mergeWithDefaultTheme(theme, prefix = "stoop") {
|
|
|
47
80
|
if (themesAreEqual(theme, defaultTheme)) {
|
|
48
81
|
return theme;
|
|
49
82
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
prefixCache = new Map();
|
|
53
|
-
mergedThemeCache.set(theme, prefixCache);
|
|
54
|
-
}
|
|
55
|
-
const cached = prefixCache.get(prefix);
|
|
56
|
-
if (cached) {
|
|
57
|
-
return cached;
|
|
58
|
-
}
|
|
59
|
-
const merged = { ...defaultTheme };
|
|
60
|
-
const allThemeKeys = Object.keys(theme);
|
|
61
|
-
for (const key of allThemeKeys) {
|
|
62
|
-
if (key === "media") {
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
const themeValue = theme[key];
|
|
66
|
-
const defaultValue = defaultTheme[key];
|
|
67
|
-
if (themeValue &&
|
|
68
|
-
typeof themeValue === "object" &&
|
|
69
|
-
!Array.isArray(themeValue) &&
|
|
70
|
-
defaultValue &&
|
|
71
|
-
typeof defaultValue === "object" &&
|
|
72
|
-
!Array.isArray(defaultValue)) {
|
|
73
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
74
|
-
merged[key] = { ...defaultValue, ...themeValue };
|
|
75
|
-
}
|
|
76
|
-
else if (themeValue !== undefined) {
|
|
77
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
78
|
-
merged[key] = themeValue;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
prefixCache.set(prefix, merged);
|
|
82
|
-
return merged;
|
|
83
|
+
// Merge theme with default theme
|
|
84
|
+
return mergeThemes(defaultTheme, theme);
|
|
83
85
|
}
|
|
84
86
|
/**
|
|
85
|
-
*
|
|
87
|
+
* Injects CSS variables for all themes using attribute selectors.
|
|
88
|
+
* This allows all themes to be available simultaneously, with theme switching
|
|
89
|
+
* handled by changing the data-theme attribute. This prevents layout shifts
|
|
90
|
+
* and eliminates the need to replace CSS variables on theme change.
|
|
86
91
|
*
|
|
87
|
-
* @param
|
|
92
|
+
* @param themes - Map of theme names to theme objects
|
|
88
93
|
* @param prefix - Optional prefix for CSS variable names
|
|
94
|
+
* @param attribute - Attribute name for theme selection (defaults to 'data-theme')
|
|
89
95
|
*/
|
|
90
|
-
export function
|
|
96
|
+
export function injectAllThemes(themes, prefix = "stoop", attribute = "data-theme") {
|
|
91
97
|
if (!isBrowser()) {
|
|
92
98
|
return;
|
|
93
99
|
}
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
100
|
+
// Merge all themes with default theme
|
|
101
|
+
const mergedThemes = {};
|
|
102
|
+
for (const [themeName, theme] of Object.entries(themes)) {
|
|
103
|
+
mergedThemes[themeName] = mergeWithDefaultTheme(theme, prefix);
|
|
104
|
+
}
|
|
105
|
+
const allThemeVars = generateAllThemeVariables(mergedThemes, prefix, attribute);
|
|
106
|
+
injectAllThemeVariables(allThemeVars, prefix);
|
|
97
107
|
}
|
package/dist/core/variants.js
CHANGED
|
@@ -26,7 +26,13 @@ export function applyVariants(variants, props, baseStyles) {
|
|
|
26
26
|
hasVariants = true;
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
if (!hasVariants) {
|
|
30
|
+
return baseStyles;
|
|
31
|
+
}
|
|
32
|
+
// Merge all variant styles into a single object using spread for consistency
|
|
33
|
+
const mergedVariants = { ...appliedVariantStyles[0] };
|
|
34
|
+
for (let i = 1; i < appliedVariantStyles.length; i++) {
|
|
35
|
+
Object.assign(mergedVariants, appliedVariantStyles[i]);
|
|
36
|
+
}
|
|
37
|
+
return { ...baseStyles, ...mergedVariants };
|
|
32
38
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal implementation for creating Stoop instances.
|
|
3
|
+
* This file is used by the SSR entry point and does NOT import React types at module level.
|
|
4
|
+
* React types are only imported conditionally when creating client instances.
|
|
5
|
+
*/
|
|
6
|
+
import type { CSS, StoopConfig, Theme, ThemeScale } from "./types";
|
|
7
|
+
import { createCSSFunction, createKeyframesFunction } from "./api/core-api";
|
|
8
|
+
import { createGlobalCSSFunction } from "./api/global-css";
|
|
9
|
+
/**
|
|
10
|
+
* Shared implementation for creating Stoop instances.
|
|
11
|
+
* Handles common setup logic for both client and server instances.
|
|
12
|
+
* Exported for use in SSR entry point.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createStoopBase(config: StoopConfig): {
|
|
15
|
+
config: StoopConfig;
|
|
16
|
+
createTheme: (overrides?: Partial<Theme>) => Theme;
|
|
17
|
+
css: ReturnType<typeof createCSSFunction>;
|
|
18
|
+
getCssText: (theme?: string | Theme) => string;
|
|
19
|
+
globalCss: ReturnType<typeof createGlobalCSSFunction>;
|
|
20
|
+
globalCssConfig: StoopConfig["globalCss"];
|
|
21
|
+
keyframes: ReturnType<typeof createKeyframesFunction>;
|
|
22
|
+
media: StoopConfig["media"];
|
|
23
|
+
mergedThemeMap: Record<string, ThemeScale>;
|
|
24
|
+
preloadTheme: (theme: string | Theme) => void;
|
|
25
|
+
sanitizedPrefix: string;
|
|
26
|
+
theme: Theme;
|
|
27
|
+
utils: StoopConfig["utils"];
|
|
28
|
+
validatedTheme: Theme;
|
|
29
|
+
warmCache: (styles: CSS[]) => void;
|
|
30
|
+
};
|