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.
- package/README.md +48 -103
- package/dist/api/core-api.d.ts +34 -0
- package/dist/api/core-api.js +135 -0
- package/dist/api/global-css.d.ts +0 -11
- package/dist/api/global-css.js +42 -0
- package/dist/api/styled.d.ts +0 -1
- package/dist/api/styled.js +419 -0
- package/dist/api/theme-provider.d.ts +41 -0
- package/dist/api/theme-provider.js +223 -0
- package/dist/constants.d.ts +13 -4
- package/dist/constants.js +154 -0
- package/dist/core/cache.d.ts +5 -9
- package/dist/core/cache.js +68 -0
- package/dist/core/compiler.d.ts +11 -0
- package/dist/core/compiler.js +206 -0
- package/dist/core/theme-manager.d.ts +27 -4
- package/dist/core/theme-manager.js +107 -0
- package/dist/core/variants.js +38 -0
- 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 -4
- package/dist/create-stoop.js +156 -0
- package/dist/inject.d.ts +113 -0
- package/dist/inject.js +308 -0
- package/dist/types/index.d.ts +157 -17
- package/dist/types/index.js +5 -0
- package/dist/types/react-polymorphic-types.d.ts +15 -8
- 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} +36 -12
- package/dist/utils/theme-utils.js +353 -0
- package/dist/utils/theme.d.ts +17 -5
- package/dist/utils/theme.js +304 -0
- package/package.json +48 -24
- package/LICENSE.md +0 -21
- package/dist/api/create-theme.d.ts +0 -13
- package/dist/api/css.d.ts +0 -16
- package/dist/api/keyframes.d.ts +0 -16
- package/dist/api/provider.d.ts +0 -19
- package/dist/api/use-theme.d.ts +0 -13
- package/dist/index.d.ts +0 -6
- package/dist/index.js +0 -13
- package/dist/inject/browser.d.ts +0 -59
- package/dist/inject/dedup.d.ts +0 -29
- package/dist/inject/index.d.ts +0 -41
- package/dist/inject/ssr.d.ts +0 -28
- package/dist/utils/theme-map.d.ts +0 -25
- package/dist/utils/theme-validation.d.ts +0 -13
- package/dist/utils/type-guards.d.ts +0 -26
- 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
|
+
}
|