stoop 0.2.1 → 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.
Files changed (56) hide show
  1. package/README.md +48 -103
  2. package/dist/api/core-api.d.ts +34 -0
  3. package/dist/api/core-api.js +135 -0
  4. package/dist/api/global-css.d.ts +0 -11
  5. package/dist/api/global-css.js +42 -0
  6. package/dist/api/styled.d.ts +0 -1
  7. package/dist/api/styled.js +419 -0
  8. package/dist/api/theme-provider.d.ts +41 -0
  9. package/dist/api/theme-provider.js +223 -0
  10. package/dist/constants.d.ts +13 -4
  11. package/dist/constants.js +154 -0
  12. package/dist/core/cache.d.ts +5 -9
  13. package/dist/core/cache.js +68 -0
  14. package/dist/core/compiler.d.ts +11 -0
  15. package/dist/core/compiler.js +206 -0
  16. package/dist/core/theme-manager.d.ts +27 -4
  17. package/dist/core/theme-manager.js +107 -0
  18. package/dist/core/variants.js +38 -0
  19. package/dist/create-stoop-internal.d.ts +30 -0
  20. package/dist/create-stoop-internal.js +123 -0
  21. package/dist/create-stoop-ssr.d.ts +10 -0
  22. package/dist/create-stoop-ssr.js +26 -0
  23. package/dist/create-stoop.d.ts +32 -4
  24. package/dist/create-stoop.js +156 -0
  25. package/dist/inject.d.ts +113 -0
  26. package/dist/inject.js +308 -0
  27. package/dist/types/index.d.ts +157 -17
  28. package/dist/types/index.js +5 -0
  29. package/dist/types/react-polymorphic-types.d.ts +15 -8
  30. package/dist/utils/auto-preload.d.ts +45 -0
  31. package/dist/utils/auto-preload.js +167 -0
  32. package/dist/utils/helpers.d.ts +64 -0
  33. package/dist/utils/helpers.js +150 -0
  34. package/dist/utils/storage.d.ts +148 -0
  35. package/dist/utils/storage.js +396 -0
  36. package/dist/utils/{string.d.ts → theme-utils.d.ts} +36 -12
  37. package/dist/utils/theme-utils.js +353 -0
  38. package/dist/utils/theme.d.ts +17 -5
  39. package/dist/utils/theme.js +304 -0
  40. package/package.json +48 -24
  41. package/LICENSE.md +0 -21
  42. package/dist/api/create-theme.d.ts +0 -13
  43. package/dist/api/css.d.ts +0 -16
  44. package/dist/api/keyframes.d.ts +0 -16
  45. package/dist/api/provider.d.ts +0 -19
  46. package/dist/api/use-theme.d.ts +0 -13
  47. package/dist/index.d.ts +0 -6
  48. package/dist/index.js +0 -13
  49. package/dist/inject/browser.d.ts +0 -59
  50. package/dist/inject/dedup.d.ts +0 -29
  51. package/dist/inject/index.d.ts +0 -41
  52. package/dist/inject/ssr.d.ts +0 -28
  53. package/dist/utils/theme-map.d.ts +0 -25
  54. package/dist/utils/theme-validation.d.ts +0 -13
  55. package/dist/utils/type-guards.d.ts +0 -26
  56. package/dist/utils/utilities.d.ts +0 -14
@@ -0,0 +1,419 @@
1
+ "use client";
2
+ /**
3
+ * Styled component API.
4
+ * Creates polymorphic styled components with variant support, theme awareness,
5
+ * and CSS prop merging. Supports component targeting via selector references.
6
+ */
7
+ import { useMemo, forwardRef, createElement, useContext, createContext, } from "react";
8
+ import { EMPTY_CSS, STOOP_COMPONENT_SYMBOL } from "../constants";
9
+ import { compileCSS } from "../core/compiler";
10
+ import { applyVariants } from "../core/variants";
11
+ import { isStyledComponentRef } from "../utils/helpers";
12
+ import { hash, sanitizeClassName } from "../utils/theme-utils";
13
+ let defaultThemeContext = null;
14
+ function getDefaultThemeContext() {
15
+ if (!defaultThemeContext) {
16
+ defaultThemeContext = createContext(null);
17
+ }
18
+ return defaultThemeContext;
19
+ }
20
+ /**
21
+ * Creates a styled component reference for selector targeting.
22
+ *
23
+ * @param className - Class name to reference
24
+ * @returns StyledComponentRef for use in CSS selectors
25
+ */
26
+ export function createStyledComponentRef(className) {
27
+ const ref = {
28
+ __isStoopStyled: true,
29
+ __stoopClassName: className,
30
+ [STOOP_COMPONENT_SYMBOL]: className,
31
+ toString: () => `__STOOP_COMPONENT_${className}`,
32
+ };
33
+ return ref;
34
+ }
35
+ /**
36
+ * Type guard for styled component references.
37
+ * Uses shared isStyledComponentRef helper for consistency.
38
+ *
39
+ * @param value - Value to check
40
+ * @returns True if value is a styled component reference
41
+ */
42
+ function isStyledComponent(value) {
43
+ return isStyledComponentRef(value);
44
+ }
45
+ /**
46
+ * Set of common CSS properties that should not be passed to DOM elements.
47
+ * These are camelCase CSS properties that React doesn't recognize as valid DOM attributes.
48
+ */
49
+ const CSS_PROPERTIES = new Set([
50
+ "alignContent",
51
+ "alignItems",
52
+ "alignSelf",
53
+ "animation",
54
+ "animationDelay",
55
+ "animationDirection",
56
+ "animationDuration",
57
+ "animationFillMode",
58
+ "animationIterationCount",
59
+ "animationName",
60
+ "animationPlayState",
61
+ "animationTimingFunction",
62
+ "aspectRatio",
63
+ "backdropFilter",
64
+ "backfaceVisibility",
65
+ "background",
66
+ "backgroundAttachment",
67
+ "backgroundBlendMode",
68
+ "backgroundClip",
69
+ "backgroundColor",
70
+ "backgroundImage",
71
+ "backgroundOrigin",
72
+ "backgroundPosition",
73
+ "backgroundRepeat",
74
+ "backgroundSize",
75
+ "border",
76
+ "borderBottom",
77
+ "borderBottomColor",
78
+ "borderBottomLeftRadius",
79
+ "borderBottomRightRadius",
80
+ "borderBottomStyle",
81
+ "borderBottomWidth",
82
+ "borderCollapse",
83
+ "borderColor",
84
+ "borderImage",
85
+ "borderImageOutset",
86
+ "borderImageRepeat",
87
+ "borderImageSlice",
88
+ "borderImageSource",
89
+ "borderImageWidth",
90
+ "borderLeft",
91
+ "borderLeftColor",
92
+ "borderLeftStyle",
93
+ "borderLeftWidth",
94
+ "borderRadius",
95
+ "borderRight",
96
+ "borderRightColor",
97
+ "borderRightStyle",
98
+ "borderRightWidth",
99
+ "borderSpacing",
100
+ "borderStyle",
101
+ "borderTop",
102
+ "borderTopColor",
103
+ "borderTopLeftRadius",
104
+ "borderTopRightRadius",
105
+ "borderTopStyle",
106
+ "borderTopWidth",
107
+ "borderWidth",
108
+ "bottom",
109
+ "boxShadow",
110
+ "boxSizing",
111
+ "captionSide",
112
+ "caretColor",
113
+ "clear",
114
+ "clip",
115
+ "clipPath",
116
+ "color",
117
+ "columnCount",
118
+ "columnFill",
119
+ "columnGap",
120
+ "columnRule",
121
+ "columnRuleColor",
122
+ "columnRuleStyle",
123
+ "columnRuleWidth",
124
+ "columnSpan",
125
+ "columnWidth",
126
+ "columns",
127
+ "content",
128
+ "counterIncrement",
129
+ "counterReset",
130
+ "cursor",
131
+ "direction",
132
+ "display",
133
+ "emptyCells",
134
+ "filter",
135
+ "flex",
136
+ "flexBasis",
137
+ "flexDirection",
138
+ "flexFlow",
139
+ "flexGrow",
140
+ "flexShrink",
141
+ "flexWrap",
142
+ "float",
143
+ "font",
144
+ "fontFamily",
145
+ "fontFeatureSettings",
146
+ "fontKerning",
147
+ "fontLanguageOverride",
148
+ "fontSize",
149
+ "fontSizeAdjust",
150
+ "fontStretch",
151
+ "fontStyle",
152
+ "fontSynthesis",
153
+ "fontVariant",
154
+ "fontVariantAlternates",
155
+ "fontVariantCaps",
156
+ "fontVariantEastAsian",
157
+ "fontVariantLigatures",
158
+ "fontVariantNumeric",
159
+ "fontVariantPosition",
160
+ "fontWeight",
161
+ "gap",
162
+ "grid",
163
+ "gridArea",
164
+ "gridAutoColumns",
165
+ "gridAutoFlow",
166
+ "gridAutoRows",
167
+ "gridColumn",
168
+ "gridColumnEnd",
169
+ "gridColumnGap",
170
+ "gridColumnStart",
171
+ "gridGap",
172
+ "gridRow",
173
+ "gridRowEnd",
174
+ "gridRowGap",
175
+ "gridRowStart",
176
+ "gridTemplate",
177
+ "gridTemplateAreas",
178
+ "gridTemplateColumns",
179
+ "gridTemplateRows",
180
+ "height",
181
+ "hyphens",
182
+ "imageOrientation",
183
+ "imageRendering",
184
+ "imageResolution",
185
+ "imeMode",
186
+ "inlineSize",
187
+ "isolation",
188
+ "justifyContent",
189
+ "justifyItems",
190
+ "justifySelf",
191
+ "left",
192
+ "letterSpacing",
193
+ "lineHeight",
194
+ "listStyle",
195
+ "listStyleImage",
196
+ "listStylePosition",
197
+ "listStyleType",
198
+ "margin",
199
+ "marginBottom",
200
+ "marginLeft",
201
+ "marginRight",
202
+ "marginTop",
203
+ "maxHeight",
204
+ "maxWidth",
205
+ "minHeight",
206
+ "minWidth",
207
+ "objectFit",
208
+ "objectPosition",
209
+ "opacity",
210
+ "order",
211
+ "orphans",
212
+ "outline",
213
+ "outlineColor",
214
+ "outlineOffset",
215
+ "outlineStyle",
216
+ "outlineWidth",
217
+ "overflow",
218
+ "overflowWrap",
219
+ "overflowX",
220
+ "overflowY",
221
+ "padding",
222
+ "paddingBottom",
223
+ "paddingLeft",
224
+ "paddingRight",
225
+ "paddingTop",
226
+ "pageBreakAfter",
227
+ "pageBreakBefore",
228
+ "pageBreakInside",
229
+ "perspective",
230
+ "perspectiveOrigin",
231
+ "placeContent",
232
+ "placeItems",
233
+ "placeSelf",
234
+ "pointerEvents",
235
+ "position",
236
+ "quotes",
237
+ "resize",
238
+ "right",
239
+ "rowGap",
240
+ "scrollBehavior",
241
+ "tabSize",
242
+ "tableLayout",
243
+ "textAlign",
244
+ "textAlignLast",
245
+ "textDecoration",
246
+ "textDecorationColor",
247
+ "textDecorationLine",
248
+ "textDecorationStyle",
249
+ "textIndent",
250
+ "textJustify",
251
+ "textOverflow",
252
+ "textShadow",
253
+ "textTransform",
254
+ "textUnderlinePosition",
255
+ "top",
256
+ "transform",
257
+ "transformOrigin",
258
+ "transformStyle",
259
+ "transition",
260
+ "transitionDelay",
261
+ "transitionDuration",
262
+ "transitionProperty",
263
+ "transitionTimingFunction",
264
+ "unicodeBidi",
265
+ "userSelect",
266
+ "verticalAlign",
267
+ "visibility",
268
+ "whiteSpace",
269
+ "width",
270
+ "wordBreak",
271
+ "wordSpacing",
272
+ "wordWrap",
273
+ "writingMode",
274
+ "zIndex",
275
+ ]);
276
+ /**
277
+ * Checks if a prop name is a CSS property that should be converted to CSS styles.
278
+ *
279
+ * @param propName - The prop name to check
280
+ * @returns True if the prop is a CSS property
281
+ */
282
+ function isCSSProperty(propName) {
283
+ return CSS_PROPERTIES.has(propName);
284
+ }
285
+ /**
286
+ * Separates component props into variant props, CSS props, and element props.
287
+ * Variant props are used for style variants.
288
+ * CSS props are converted to CSS styles.
289
+ * Element props are passed to the DOM element.
290
+ *
291
+ * @param props - All component props
292
+ * @param variants - Variant configuration
293
+ * @returns Object with separated elementProps, variantProps, and cssProps
294
+ */
295
+ function extractVariantProps(props, variants) {
296
+ const variantKeys = variants ? new Set(Object.keys(variants)) : new Set();
297
+ const variantProps = {};
298
+ const cssProps = {};
299
+ const elementProps = {};
300
+ for (const key in props) {
301
+ if (variantKeys.has(key)) {
302
+ variantProps[key] = props[key];
303
+ }
304
+ else if (isCSSProperty(key)) {
305
+ // Convert CSS properties to CSS styles
306
+ cssProps[key] = props[key];
307
+ }
308
+ else {
309
+ // Only include non-CSS, non-variant properties in elementProps
310
+ elementProps[key] = props[key];
311
+ }
312
+ }
313
+ return { cssProps, elementProps, variantProps };
314
+ }
315
+ /**
316
+ * Creates a styled component factory function.
317
+ * Supports polymorphic components, variants, theme awareness, and CSS prop merging.
318
+ *
319
+ * @param defaultTheme - Default theme for token resolution
320
+ * @param prefix - Optional prefix for generated class names
321
+ * @param media - Optional media query breakpoints
322
+ * @param utils - Optional utility functions
323
+ * @param themeMap - Optional theme scale mappings
324
+ * @param themeContext - React context for theme values (instance-specific)
325
+ * @returns Styled component factory function
326
+ */
327
+ export function createStyledFunction(defaultTheme, prefix = "stoop", media, utils, themeMap, themeContext) {
328
+ return function styled(defaultElement, baseStylesOrVariants, variantsParam) {
329
+ let actualBaseStyles = (baseStylesOrVariants || EMPTY_CSS);
330
+ let actualVariants = variantsParam;
331
+ if (baseStylesOrVariants &&
332
+ "variants" in baseStylesOrVariants &&
333
+ typeof baseStylesOrVariants.variants === "object") {
334
+ actualVariants = baseStylesOrVariants.variants;
335
+ const { variants: _, ...rest } = baseStylesOrVariants;
336
+ actualBaseStyles = rest;
337
+ }
338
+ let baseElementClassName;
339
+ if (typeof defaultElement !== "string" && isStyledComponent(defaultElement)) {
340
+ baseElementClassName = defaultElement.__stoopClassName;
341
+ }
342
+ const StyledComponent = forwardRef(function StyledComponent(propsWithBase, ref) {
343
+ const { as, className, css: cssStyles, ...restProps } = propsWithBase;
344
+ const element = (as || defaultElement);
345
+ const cssObject = useMemo(() => cssStyles && typeof cssStyles === "object" && cssStyles !== null
346
+ ? cssStyles
347
+ : EMPTY_CSS, [cssStyles]);
348
+ const { cssProps, elementProps, variantProps } = extractVariantProps(restProps, actualVariants);
349
+ const contextValue = useContext(themeContext || getDefaultThemeContext());
350
+ const currentTheme = contextValue?.theme || defaultTheme;
351
+ const currentMedia = currentTheme.media ? { ...media, ...currentTheme.media } : media;
352
+ const variantKey = useMemo(() => {
353
+ if (!actualVariants) {
354
+ return "";
355
+ }
356
+ const variantEntries = Object.entries(variantProps);
357
+ if (variantEntries.length === 0) {
358
+ return "";
359
+ }
360
+ return variantEntries
361
+ .sort(([a], [b]) => a.localeCompare(b))
362
+ .map(([key, value]) => `${key}:${String(value)}`)
363
+ .join("|");
364
+ }, [variantProps]);
365
+ const finalStyles = useMemo(() => {
366
+ let componentStyles = actualBaseStyles;
367
+ if (actualVariants && variantKey) {
368
+ componentStyles = applyVariants(actualVariants, variantProps, actualBaseStyles);
369
+ }
370
+ // Merge CSS props (CSS properties passed as props) into styles
371
+ if (Object.keys(cssProps).length > 0) {
372
+ componentStyles = Object.assign({}, componentStyles, cssProps);
373
+ }
374
+ // Merge explicit css prop into styles
375
+ if (cssObject !== EMPTY_CSS) {
376
+ componentStyles = Object.assign({}, componentStyles, cssObject);
377
+ }
378
+ return componentStyles;
379
+ }, [variantKey, cssObject, cssProps, actualBaseStyles, actualVariants, variantProps]);
380
+ const finalClassName = useMemo(() => {
381
+ const classNames = [];
382
+ if (baseElementClassName) {
383
+ classNames.push(baseElementClassName);
384
+ }
385
+ const mergedClass = compileCSS(finalStyles, currentTheme, prefix, currentMedia, utils, themeMap);
386
+ if (mergedClass) {
387
+ classNames.push(mergedClass);
388
+ }
389
+ if (className) {
390
+ const classNameStr = typeof className === "string" ? className : String(className);
391
+ const sanitizedClassName = sanitizeClassName(classNameStr);
392
+ if (sanitizedClassName) {
393
+ classNames.push(sanitizedClassName);
394
+ }
395
+ }
396
+ return classNames.length > 0 ? classNames.join(" ") : undefined;
397
+ }, [
398
+ finalStyles,
399
+ className,
400
+ baseElementClassName,
401
+ currentTheme,
402
+ prefix,
403
+ currentMedia,
404
+ utils,
405
+ themeMap,
406
+ ]);
407
+ return createElement(element, {
408
+ ...elementProps,
409
+ className: finalClassName,
410
+ ref,
411
+ });
412
+ });
413
+ const selectorHash = hash(JSON.stringify(actualBaseStyles));
414
+ const selectorClassName = `${prefix}-${selectorHash}`;
415
+ const componentWithSelector = StyledComponent;
416
+ componentWithSelector.selector = createStyledComponentRef(selectorClassName);
417
+ return componentWithSelector;
418
+ };
419
+ }
@@ -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
+ }