stoop 0.2.0 → 0.3.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 (48) hide show
  1. package/dist/api/create-theme.js +43 -0
  2. package/dist/api/css.js +20 -0
  3. package/dist/api/global-css.d.ts +0 -11
  4. package/dist/api/global-css.js +89 -0
  5. package/dist/api/keyframes.js +95 -0
  6. package/dist/api/provider.d.ts +3 -3
  7. package/dist/api/provider.js +109 -0
  8. package/dist/api/styled.js +170 -0
  9. package/dist/api/use-theme.js +21 -0
  10. package/dist/constants.d.ts +2 -3
  11. package/dist/constants.js +144 -0
  12. package/dist/core/cache.d.ts +5 -9
  13. package/dist/core/cache.js +68 -0
  14. package/dist/core/compiler.js +198 -0
  15. package/dist/core/theme-manager.d.ts +0 -8
  16. package/dist/core/theme-manager.js +97 -0
  17. package/dist/core/variants.js +32 -0
  18. package/dist/create-stoop-server.d.ts +33 -0
  19. package/dist/create-stoop-server.js +130 -0
  20. package/dist/create-stoop.d.ts +2 -5
  21. package/dist/create-stoop.js +147 -0
  22. package/dist/index.js +5 -13
  23. package/dist/inject/browser.d.ts +2 -3
  24. package/dist/inject/browser.js +149 -0
  25. package/dist/inject/dedup.js +38 -0
  26. package/dist/inject/index.d.ts +0 -1
  27. package/dist/inject/index.js +75 -0
  28. package/dist/inject/ssr.d.ts +0 -1
  29. package/dist/inject/ssr.js +46 -0
  30. package/dist/ssr.d.ts +21 -0
  31. package/dist/ssr.js +19 -0
  32. package/dist/types/index.d.ts +10 -5
  33. package/dist/types/index.js +5 -0
  34. package/dist/types/react-polymorphic-types.d.ts +4 -8
  35. package/dist/utils/environment.d.ts +6 -0
  36. package/dist/utils/environment.js +12 -0
  37. package/dist/utils/string.d.ts +16 -9
  38. package/dist/utils/string.js +253 -0
  39. package/dist/utils/theme-map.d.ts +0 -3
  40. package/dist/utils/theme-map.js +97 -0
  41. package/dist/utils/theme-validation.js +36 -0
  42. package/dist/utils/theme.d.ts +3 -3
  43. package/dist/utils/theme.js +279 -0
  44. package/dist/utils/type-guards.js +38 -0
  45. package/dist/utils/utilities.js +43 -0
  46. package/package.json +26 -27
  47. package/LICENSE.md +0 -21
  48. package/README.md +0 -180
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Server-safe factory function that creates a Stoop instance.
3
+ * Only includes APIs that work without React: css, globalCss, keyframes, getCssText, etc.
4
+ * Does NOT include: styled, Provider, useTheme
5
+ *
6
+ * This is the entry point for Server Components in Next.js App Router.
7
+ */
8
+ import { createTheme as createThemeFactory } from "./api/create-theme";
9
+ import { createCSSFunction } from "./api/css";
10
+ import { createGlobalCSSFunction } from "./api/global-css";
11
+ import { createKeyframesFunction } from "./api/keyframes";
12
+ import { DEFAULT_THEME_MAP } from "./constants";
13
+ import { compileCSS } from "./core/compiler";
14
+ import { registerDefaultTheme, updateThemeVariables } from "./core/theme-manager";
15
+ import { getCssText as getCssTextBase, registerTheme } from "./inject";
16
+ import { getRootRegex, sanitizePrefix } from "./utils/string";
17
+ import { generateCSSVariables } from "./utils/theme";
18
+ import { validateTheme } from "./utils/theme-validation";
19
+ /**
20
+ * Creates a server-safe Stoop instance with the provided configuration.
21
+ * Only includes APIs that work without React: css, globalCss, keyframes, getCssText, etc.
22
+ *
23
+ * @param config - Configuration object containing theme, media queries, utilities, and optional prefix/themeMap
24
+ * @returns StoopServerInstance with server-safe API functions
25
+ */
26
+ export function createStoop(config) {
27
+ const { media: configMedia, prefix = "stoop", theme, themeMap: userThemeMap, utils } = config;
28
+ const sanitizedPrefix = sanitizePrefix(prefix);
29
+ const validatedTheme = validateTheme(theme);
30
+ const media = validatedTheme.media || configMedia;
31
+ const mergedThemeMap = {
32
+ ...DEFAULT_THEME_MAP,
33
+ ...userThemeMap,
34
+ };
35
+ registerDefaultTheme(validatedTheme, sanitizedPrefix);
36
+ registerTheme(validatedTheme, sanitizedPrefix);
37
+ const css = createCSSFunction(validatedTheme, sanitizedPrefix, media, utils, mergedThemeMap);
38
+ const createTheme = createThemeFactory(validatedTheme);
39
+ const globalCss = createGlobalCSSFunction(validatedTheme, sanitizedPrefix, media, utils, mergedThemeMap);
40
+ const keyframes = createKeyframesFunction(sanitizedPrefix, validatedTheme, mergedThemeMap);
41
+ const themeObject = Object.freeze({ ...validatedTheme });
42
+ /**
43
+ * Pre-compiles CSS objects to warm the cache.
44
+ * Useful for eliminating FOUC by pre-compiling common styles.
45
+ *
46
+ * @param styles - Array of CSS objects to pre-compile
47
+ */
48
+ function warmCache(styles) {
49
+ for (const style of styles) {
50
+ try {
51
+ compileCSS(style, validatedTheme, sanitizedPrefix, media, utils, mergedThemeMap);
52
+ }
53
+ catch {
54
+ // Skip invalid styles
55
+ }
56
+ }
57
+ }
58
+ /**
59
+ * Preloads a theme by injecting its CSS variables before React renders.
60
+ * Useful for preventing FOUC when loading a non-default theme from localStorage.
61
+ *
62
+ * @param theme - Theme to preload (can be theme name string or Theme object)
63
+ */
64
+ function preloadTheme(theme) {
65
+ let themeToInject;
66
+ if (typeof theme === "string") {
67
+ if (config.themes && config.themes[theme]) {
68
+ themeToInject = config.themes[theme];
69
+ }
70
+ else {
71
+ if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
72
+ // eslint-disable-next-line no-console
73
+ console.warn(`[Stoop] Theme "${theme}" not found. Available themes: ${config.themes ? Object.keys(config.themes).join(", ") : "none"}`);
74
+ }
75
+ return;
76
+ }
77
+ }
78
+ else {
79
+ themeToInject = theme;
80
+ }
81
+ updateThemeVariables(themeToInject, sanitizedPrefix);
82
+ }
83
+ /**
84
+ * Gets all injected CSS text for server-side rendering.
85
+ * Always includes theme CSS variables.
86
+ *
87
+ * @param theme - Optional theme (name or object) to include vars for (defaults to default theme)
88
+ * @returns CSS text string with theme variables and component styles
89
+ */
90
+ function getCssText(theme) {
91
+ let themeToUse = validatedTheme;
92
+ if (theme) {
93
+ if (typeof theme === "string") {
94
+ if (config.themes && config.themes[theme]) {
95
+ themeToUse = config.themes[theme];
96
+ }
97
+ else if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
98
+ // eslint-disable-next-line no-console
99
+ console.warn(`[Stoop] Theme "${theme}" not found. Using default theme. Available: ${config.themes ? Object.keys(config.themes).join(", ") : "none"}`);
100
+ }
101
+ }
102
+ else {
103
+ themeToUse = theme;
104
+ }
105
+ }
106
+ let result = "";
107
+ const themeVars = generateCSSVariables(themeToUse, sanitizedPrefix);
108
+ if (themeVars) {
109
+ result += themeVars + "\n";
110
+ }
111
+ const baseCss = getCssTextBase();
112
+ const rootRegex = getRootRegex(sanitizedPrefix);
113
+ const cssWithoutThemeVars = baseCss.replace(rootRegex, "").trim();
114
+ if (cssWithoutThemeVars) {
115
+ result += (result ? "\n" : "") + cssWithoutThemeVars;
116
+ }
117
+ return result;
118
+ }
119
+ return {
120
+ config: { ...config, prefix: sanitizedPrefix },
121
+ createTheme,
122
+ css,
123
+ getCssText,
124
+ globalCss,
125
+ keyframes,
126
+ preloadTheme,
127
+ theme: themeObject,
128
+ warmCache,
129
+ };
130
+ }
@@ -1,12 +1,9 @@
1
- /**
2
- * Main factory function that creates a Stoop instance.
3
- * Configures theme, media queries, utilities, and returns all API functions.
4
- */
5
1
  import type { StoopConfig, StoopInstance } from "./types";
6
2
  /**
7
3
  * Creates a Stoop instance with the provided configuration.
4
+ * Includes all APIs: styled, Provider, useTheme, etc.
8
5
  *
9
6
  * @param config - Configuration object containing theme, media queries, utilities, and optional prefix/themeMap
10
- * @returns StoopInstance with all API functions (styled, css, globalCss, keyframes, createTheme, etc.)
7
+ * @returns StoopInstance with all API functions
11
8
  */
12
9
  export declare function createStoop(config: StoopConfig): StoopInstance;
@@ -0,0 +1,147 @@
1
+ "use client";
2
+ import { createTheme as createThemeFactory } from "./api/create-theme";
3
+ import { createCSSFunction } from "./api/css";
4
+ import { createGlobalCSSFunction } from "./api/global-css";
5
+ import { createKeyframesFunction } from "./api/keyframes";
6
+ import { createProvider } from "./api/provider";
7
+ import { createStyledFunction } from "./api/styled";
8
+ import { createUseThemeHook } from "./api/use-theme";
9
+ import { DEFAULT_THEME_MAP } from "./constants";
10
+ import { compileCSS } from "./core/compiler";
11
+ import { registerDefaultTheme, updateThemeVariables } from "./core/theme-manager";
12
+ import { getCssText as getCssTextBase, registerTheme } from "./inject";
13
+ import { getRootRegex, sanitizePrefix } from "./utils/string";
14
+ import { generateCSSVariables } from "./utils/theme";
15
+ import { validateTheme } from "./utils/theme-validation";
16
+ /**
17
+ * Creates a Stoop instance with the provided configuration.
18
+ * Includes all APIs: styled, Provider, useTheme, etc.
19
+ *
20
+ * @param config - Configuration object containing theme, media queries, utilities, and optional prefix/themeMap
21
+ * @returns StoopInstance with all API functions
22
+ */
23
+ export function createStoop(config) {
24
+ const { media: configMedia, prefix = "stoop", theme, themeMap: userThemeMap, utils } = config;
25
+ const sanitizedPrefix = sanitizePrefix(prefix);
26
+ const validatedTheme = validateTheme(theme);
27
+ const media = validatedTheme.media || configMedia;
28
+ const mergedThemeMap = {
29
+ ...DEFAULT_THEME_MAP,
30
+ ...userThemeMap,
31
+ };
32
+ registerDefaultTheme(validatedTheme, sanitizedPrefix);
33
+ registerTheme(validatedTheme, sanitizedPrefix);
34
+ const css = createCSSFunction(validatedTheme, sanitizedPrefix, media, utils, mergedThemeMap);
35
+ const createTheme = createThemeFactory(validatedTheme);
36
+ const globalCss = createGlobalCSSFunction(validatedTheme, sanitizedPrefix, media, utils, mergedThemeMap);
37
+ const keyframes = createKeyframesFunction(sanitizedPrefix, validatedTheme, mergedThemeMap);
38
+ const themeObject = Object.freeze({ ...validatedTheme });
39
+ /**
40
+ * Pre-compiles CSS objects to warm the cache.
41
+ * Useful for eliminating FOUC by pre-compiling common styles.
42
+ *
43
+ * @param styles - Array of CSS objects to pre-compile
44
+ */
45
+ function warmCache(styles) {
46
+ for (const style of styles) {
47
+ try {
48
+ compileCSS(style, validatedTheme, sanitizedPrefix, media, utils, mergedThemeMap);
49
+ }
50
+ catch {
51
+ // Skip invalid styles
52
+ }
53
+ }
54
+ }
55
+ /**
56
+ * Preloads a theme by injecting its CSS variables before React renders.
57
+ * Useful for preventing FOUC when loading a non-default theme from localStorage.
58
+ *
59
+ * @param theme - Theme to preload (can be theme name string or Theme object)
60
+ */
61
+ function preloadTheme(theme) {
62
+ let themeToInject;
63
+ if (typeof theme === "string") {
64
+ if (config.themes && config.themes[theme]) {
65
+ themeToInject = config.themes[theme];
66
+ }
67
+ else {
68
+ if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
69
+ // eslint-disable-next-line no-console
70
+ console.warn(`[Stoop] Theme "${theme}" not found. Available themes: ${config.themes ? Object.keys(config.themes).join(", ") : "none"}`);
71
+ }
72
+ return;
73
+ }
74
+ }
75
+ else {
76
+ themeToInject = theme;
77
+ }
78
+ updateThemeVariables(themeToInject, sanitizedPrefix);
79
+ }
80
+ /**
81
+ * Gets all injected CSS text for server-side rendering.
82
+ * Always includes theme CSS variables.
83
+ *
84
+ * @param theme - Optional theme (name or object) to include vars for (defaults to default theme)
85
+ * @returns CSS text string with theme variables and component styles
86
+ */
87
+ function getCssText(theme) {
88
+ let themeToUse = validatedTheme;
89
+ if (theme) {
90
+ if (typeof theme === "string") {
91
+ if (config.themes && config.themes[theme]) {
92
+ themeToUse = config.themes[theme];
93
+ }
94
+ else if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
95
+ // eslint-disable-next-line no-console
96
+ console.warn(`[Stoop] Theme "${theme}" not found. Using default theme. Available: ${config.themes ? Object.keys(config.themes).join(", ") : "none"}`);
97
+ }
98
+ }
99
+ else {
100
+ themeToUse = theme;
101
+ }
102
+ }
103
+ let result = "";
104
+ const themeVars = generateCSSVariables(themeToUse, sanitizedPrefix);
105
+ if (themeVars) {
106
+ result += themeVars + "\n";
107
+ }
108
+ const baseCss = getCssTextBase();
109
+ const rootRegex = getRootRegex(sanitizedPrefix);
110
+ const cssWithoutThemeVars = baseCss.replace(rootRegex, "").trim();
111
+ if (cssWithoutThemeVars) {
112
+ result += (result ? "\n" : "") + cssWithoutThemeVars;
113
+ }
114
+ return result;
115
+ }
116
+ // Create Provider and useTheme if themes are configured
117
+ let Provider;
118
+ let useTheme;
119
+ let themeContext;
120
+ if (config.themes) {
121
+ const mergedThemesForProvider = {};
122
+ for (const [themeName, themeOverride] of Object.entries(config.themes)) {
123
+ mergedThemesForProvider[themeName] = createTheme(themeOverride);
124
+ }
125
+ const { Provider: ProviderComponent, ThemeContext, ThemeManagementContext, } = createProvider(mergedThemesForProvider, validatedTheme, sanitizedPrefix);
126
+ themeContext = ThemeContext;
127
+ Provider = ProviderComponent;
128
+ useTheme = createUseThemeHook(ThemeManagementContext);
129
+ }
130
+ // Create styled function
131
+ const styled = createStyledFunction(validatedTheme, sanitizedPrefix, media, utils, mergedThemeMap, themeContext);
132
+ // Return instance with all APIs
133
+ return {
134
+ config: { ...config, prefix: sanitizedPrefix },
135
+ createTheme,
136
+ css,
137
+ getCssText,
138
+ globalCss,
139
+ keyframes,
140
+ preloadTheme,
141
+ Provider,
142
+ styled,
143
+ theme: themeObject,
144
+ useTheme,
145
+ warmCache,
146
+ };
147
+ }
package/dist/index.js CHANGED
@@ -1,13 +1,5 @@
1
- import{createContext as C4}from"react";import{createContext as W4}from"react";var YJ=Object.freeze({}),PJ=1e4,XJ=5000,GJ=10,NJ=W4(null),QJ=["colors","opacities","space","radii","sizes","fonts","fontWeights","fontSizes","letterSpacings","shadows","zIndices","transitions"],a={accentColor:"colors",animation:"transitions",animationDelay:"transitions",animationDuration:"transitions",animationTimingFunction:"transitions",backdropFilter:"shadows",background:"colors",backgroundColor:"colors",blockSize:"sizes",border:"colors",borderBlockColor:"colors",borderBlockEndColor:"colors",borderBlockStartColor:"colors",borderBottomColor:"colors",borderBottomLeftRadius:"radii",borderBottomRightRadius:"radii",borderColor:"colors",borderEndEndRadius:"radii",borderEndStartRadius:"radii",borderInlineColor:"colors",borderInlineEndColor:"colors",borderInlineStartColor:"colors",borderLeftColor:"colors",borderRadius:"radii",borderRightColor:"colors",borderStartEndRadius:"radii",borderStartStartRadius:"radii",borderTopColor:"colors",borderTopLeftRadius:"radii",borderTopRightRadius:"radii",bottom:"space",boxShadow:"shadows",caretColor:"colors",color:"colors",columnGap:"space",columnRuleColor:"colors",fill:"colors",filter:"shadows",flexBasis:"sizes",floodColor:"colors",font:"fontSizes",fontFamily:"fonts",fontSize:"fontSizes",fontWeight:"fontWeights",gap:"space",gridColumnGap:"space",gridGap:"space",gridRowGap:"space",height:"sizes",inlineSize:"sizes",inset:"space",insetBlock:"space",insetBlockEnd:"space",insetBlockStart:"space",insetInline:"space",insetInlineEnd:"space",insetInlineStart:"space",left:"space",letterSpacing:"letterSpacings",lightingColor:"colors",margin:"space",marginBlock:"space",marginBlockEnd:"space",marginBlockStart:"space",marginBottom:"space",marginInline:"space",marginInlineEnd:"space",marginInlineStart:"space",marginLeft:"space",marginRight:"space",marginTop:"space",maxBlockSize:"sizes",maxHeight:"sizes",maxInlineSize:"sizes",maxWidth:"sizes",minBlockSize:"sizes",minHeight:"sizes",minInlineSize:"sizes",minWidth:"sizes",opacity:"opacities",outline:"colors",outlineColor:"colors",padding:"space",paddingBlock:"space",paddingBlockEnd:"space",paddingBlockStart:"space",paddingBottom:"space",paddingInline:"space",paddingInlineEnd:"space",paddingInlineStart:"space",paddingLeft:"space",paddingRight:"space",paddingTop:"space",right:"space",rowGap:"space",size:"sizes",stopColor:"colors",stroke:"colors",textDecorationColor:"colors",textEmphasisColor:"colors",textShadow:"shadows",top:"space",transition:"transitions",transitionDelay:"transitions",transitionDuration:"transitions",transitionProperty:"transitions",transitionTimingFunction:"transitions",width:"sizes",zIndex:"zIndices"},t=Symbol.for("stoop.component");function BJ(J){if(!J||typeof J!=="object"||Array.isArray(J))throw new Error("[Stoop] Theme must be a non-null object");let $=J,Z=[];for(let X in $){if(X==="media")continue;if(!QJ.includes(X))Z.push(X)}if(Z.length>0){let X=`[Stoop] Theme contains invalid scales: ${Z.join(", ")}. Only these scales are allowed: ${QJ.join(", ")}`;if(typeof process!=="undefined"&&process.env?.NODE_ENV!=="production")throw new Error(X);console.warn(X)}return J}function C(J){return typeof J==="object"&&J!==null}function D4(J){return typeof J==="object"&&J!==null&&"__isStoopStyled"in J&&"__stoopClassName"in J&&J.__isStoopStyled===!0}function wJ(J){return C(J)&&!D4(J)}function o(J){return typeof J==="object"&&J!==null&&!Array.isArray(J)}function xJ(J){return function $(Z){let X=BJ(Z);function q(Y,Q){let G={...Y},W=Object.keys(Q);for(let B of W){let D=Q[B],H=Y[B];if(o(D)&&o(H))G[B]={...H,...D};else if(D!==void 0)G[B]=D}let F=Object.keys(Y);for(let B of F)if(!(B in G))G[B]=Y[B];return G}return q(J,X)}}var gJ=new Map;class UJ extends Map{maxSize;constructor(J){super();this.maxSize=J}get(J){let $=super.get(J);if($!==void 0)super.delete(J),super.set(J,$);return $}set(J,$){if(super.has(J))super.delete(J);else if(this.size>=this.maxSize){let Z=this.keys().next().value;if(Z!==void 0)super.delete(Z)}return super.set(J,$),this}}var p=new UJ(XJ),e=new UJ(XJ);function bJ(J){return gJ.has(J)}function vJ(J){gJ.set(J,J)}function TJ(){if(p.size>XJ){let J=p.keys().next().value;if(J)p.delete(J),e.delete(J)}}function FJ(J){let $=0;for(let Z=0;Z<J.length;Z++){let X=J.charCodeAt(Z);$=($<<5)-$+X,$=$&$}return Math.abs($).toString(36)}function WJ(J){try{return FJ(JSON.stringify(J))}catch{return FJ(String(J))}}function H4(J){return J.replace(/([A-Z])/g,"-$1").toLowerCase()}function h(J){return String(J).replace(/\\/g,"\\\\").replace(/"/g,"\\\"").replace(/'/g,"\\'").replace(/;/g,"\\;").replace(/\n/g,"\\A ").replace(/\r/g,"").replace(/\f/g,"\\C ")}function P(J){let $=J.replace(/[^a-zA-Z0-9\s\-_>+~:.#[\]&@()]/g,"");if(!$.trim()||/^[>+~:.#[\]&@()\s]+$/.test($))return"";return $}function f(J){return J.replace(/[^a-zA-Z0-9-_]/g,"-").replace(/^[\d-]+/,"").replace(/^-+/,"")||"invalid"}function kJ(J){return String(J).replace(/\\/g,"\\\\").replace(/"/g,"\\\"").replace(/'/g,"\\'").replace(/;/g,"\\;").replace(/\n/g,"\\A ").replace(/\r/g,"").replace(/\f/g,"\\C ").replace(/\{/g,"\\7B ").replace(/\}/g,"\\7D ")}function M(J){if(!J)return"";return J.replace(/[^a-zA-Z0-9-_]/g,"").replace(/^[\d-]+/,"").replace(/^-+/,"")||""}function CJ(J){if(!J)return"";return J.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function DJ(J){if(!J||typeof J!=="string")return"";let $=J.replace(/[^a-zA-Z0-9\s():,<>=\-@]/g,"");if(!$.trim()||!/[a-zA-Z]/.test($))return"";return $}function fJ(J){if(!J||typeof J!=="string")return"";let $=J.trim().split(/\s+/),Z=[];for(let X of $){if(!X)continue;let Y=X.replace(/[^a-zA-Z0-9-_]/g,"").replace(/^\d+/,"");if(Y&&/^[a-zA-Z-_]/.test(Y))Z.push(Y)}return Z.join(" ")}function c(J){if(!J||typeof J!=="string")return"";return H4(J).replace(/[^a-zA-Z0-9-]/g,"").replace(/^-+|-+$/g,"").replace(/^\d+/,"")||""}function SJ(J){if(!J||typeof J!=="string")return!1;if(J==="from"||J==="to")return!0;if(/^\d+(\.\d+)?%$/.test(J)){let Z=parseFloat(J);return Z>=0&&Z<=100}return!1}function L4(J){if(J.includes("Color")||J==="fill"||J==="stroke"||J==="accentColor"||J==="caretColor"||J==="border"||J==="outline"||J.includes("background")&&!J.includes("Size")&&!J.includes("Image"))return"colors";if(/^(margin|padding|gap|inset|top|right|bottom|left|rowGap|columnGap|gridGap|gridRowGap|gridColumnGap)/.test(J)||J.includes("Block")||J.includes("Inline"))return"space";if(/(width|height|size|basis)$/i.test(J)||J.includes("BlockSize")||J.includes("InlineSize"))return"sizes";if(J==="fontSize"||J==="font"&&!J.includes("Family"))return"fontSizes";if(J==="fontFamily"||J.includes("FontFamily"))return"fonts";if(J==="fontWeight"||J.includes("FontWeight"))return"fontWeights";if(J==="letterSpacing"||J.includes("LetterSpacing"))return"letterSpacings";if(J.includes("Radius")||J.includes("radius"))return"radii";if(J.includes("Shadow")||J.includes("shadow")||J==="filter"||J==="backdropFilter")return"shadows";if(J==="zIndex"||J.includes("ZIndex")||J.includes("z-index"))return"zIndices";if(J==="opacity"||J.includes("Opacity"))return"opacities";if(J.startsWith("transition")||J.startsWith("animation")||J.includes("Transition")||J.includes("Animation"))return"transitions";return}function dJ(J,$){if($&&J in $)return $[J];if(J in a)return a[J];return L4(J)}var yJ=new WeakMap;function w4(J){let $=yJ.get(J);if($)return $;let Z=new Map;function X(q,Y=[]){let Q=Object.keys(q).sort();for(let G of Q){let W=q[G],F=[...Y,G];if(o(W))X(W,F);else{let B=Z.get(G);if(B)B.push(F);else Z.set(G,[F])}}}X(J);for(let[q,Y]of Z.entries())if(Y.length>1){if(Y.sort((Q,G)=>{let W=Q.length-G.length;if(W!==0)return W;let F=Q.join("."),B=G.join(".");return F.localeCompare(B)}),typeof process!=="undefined"&&process.env?.NODE_ENV!=="production")console.warn(`[Stoop] Ambiguous token "$${q}" found in multiple categories: ${Y.map((Q)=>Q.join(".")).join(", ")}. Using "${Y[0].join(".")}" (deterministic: shorter paths first, then alphabetical). Use full path "$${Y[0].join(".")}" to be explicit.`)}return yJ.set(J,Z),Z}function U4(J,$){let Z=Object.keys(J).filter((q)=>q!=="media"),X=Object.keys($).filter((q)=>q!=="media");if(Z.length!==X.length)return!1;for(let q of Z)if(!(q in $))return!1;return!0}function uJ(J,$){if(J===$)return!0;if(!J||!$)return!1;if(!U4(J,$))return!1;let Z={...J},X={...$};return delete Z.media,delete X.media,JSON.stringify(Z)===JSON.stringify(X)}function RJ(J,$,Z){if(Z&&Z in J){let Y=J[Z];if(Y&&typeof Y==="object"&&!Array.isArray(Y)&&$ in Y)return[Z,$]}let q=w4(J).get($);if(!q||q.length===0)return null;return q[0]}function R4(J,$,Z,X){if(!J.startsWith("$"))return J;let q=J.slice(1);if(q.includes("$")||q.includes("."))return`var(${`--${(q.includes("$")?q.split("$"):q.split(".")).map((B)=>f(B)).join("-")}`})`;if($&&Z){let G=dJ(Z,X);if(G){let F=RJ($,q,G);if(F)return`var(${`--${F.map((H)=>f(H)).join("-")}`})`}let W=RJ($,q);if(W)return`var(${`--${W.map((D)=>f(D)).join("-")}`})`}else if($){let G=RJ($,q);if(G)return`var(${`--${G.map((B)=>f(B)).join("-")}`})`}return`var(${`--${f(q)}`})`}function n(J,$=""){let Z=CJ($),X=Z?`:root[data-stoop="${Z}"]`:":root",q=[];function Y(Q,G=[]){let W=Object.keys(Q).sort();for(let F of W){if(F==="media")continue;let B=Q[F],D=[...G,F];if(o(B))Y(B,D);else{let L=`--${D.map((R)=>f(R)).join("-")}`,O=typeof B==="string"||typeof B==="number"?kJ(B):String(B);q.push(` ${L}: ${O};`)}}}if(Y(J),q.length===0)return"";return`${X} {
2
- ${q.join(`
3
- `)}
4
- }`}function S(J,$,Z,X){if(!J||typeof J!=="object")return J;let q={};for(let Y in J){let Q=J[Y];if(C(Q))q[Y]=S(Q,$,Z,X);else if(typeof Q==="string"&&Q.includes("$")){let G=X||Y;q[Y]=Q.replace(/(calc\()?(-?)(\$[a-zA-Z][a-zA-Z0-9]*(?:\$[a-zA-Z][a-zA-Z0-9]*)?(?:\.[a-zA-Z][a-zA-Z0-9]*)?)/g,(W,F,B,D,H,L)=>{let O=R4(D,$,G,Z),R=L.slice(Math.max(0,H-10),H),A=R.includes("calc(")&&!R.includes(")");if(B==="-"){if(A||F==="calc(")return`${F||""}-${O}`;return`calc(-1 * ${O})`}if(F==="calc(")return`${F}${O}`;return O})}else q[Y]=Q}return q}var AJ=new Map;function g(J){return AJ.has(J)}function N(J,$){AJ.set(J,$)}function HJ(){return new Map(AJ)}var m=[];function JJ(J){if(m.includes(J))return;if(m.length>=PJ)m.shift();m.push(J)}function IJ(){return m.join(`
5
- `)}function pJ(J){return m.includes(J)}var U=null,_J=new Map,cJ=new Map,A4=new Map,l=new Map,d=null;if(typeof document!=="undefined")(function J(){if(!U)U=document.createElement("style"),U.setAttribute("data-stoop","stoop"),document.head.appendChild(U)})();function nJ(){if(l.size===0||!U||!U.parentNode){d=null;return}let J=Array.from(l.values()).join(`
6
- `);if(!J){l.clear(),d=null;return}let{sheet:$}=U;if($&&$.insertRule&&!J.includes("@")&&!J.includes("&"))try{let X=J.split(/(?<=})\s*(?=\S)/),q=!0;for(let Y of X){let Q=Y.trim();if(Q)try{$.insertRule(Q,$.cssRules.length)}catch{q=!1;break}}if(q){l.clear(),d=null;return}}catch{}let Z=U.textContent||"";U.textContent=Z+(Z?`
7
- `:"")+J,l.clear(),d=null}function I4(J,$){if(l.set($,J),!d&&typeof requestAnimationFrame!=="undefined")d=requestAnimationFrame(nJ);else if(!d)nJ()}function KJ(J=""){if(typeof document==="undefined")throw new Error("Cannot access document in SSR context");let $=M(J);if(U){if(U.parentNode)return U;if(U.textContent)try{return document.head.appendChild(U),U}catch{}U=null}return U=document.createElement("style"),U.setAttribute("data-stoop",$||"stoop"),document.head.appendChild(U),U}function OJ(J,$,Z=""){if(!J)return;let X=M(Z),q=`__theme_vars_${X}`;if((cJ.get(q)??null)===J){if((_J.get(q)??null)!==$)_J.set(q,$);return}if(_J.set(q,$),cJ.set(q,J),typeof document==="undefined"){JJ(J);return}let Q=KJ(X),G=Q.textContent||"";if(g(q)){let W=X?`:root[data-stoop="${X}"]`:":root",F=new RegExp(`${W.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}\\s*\\{[\\s\\S]*?\\}`,"g"),B=G.replace(F,"").trim();Q.textContent=J+(B?`
8
- `+B:""),N(q,J)}else Q.textContent=J+(G?`
9
- `+G:""),N(q,J)}function mJ(J,$=""){let Z=M($);if(A4.set(Z,J),typeof document!=="undefined"){KJ(Z);let X=n(J,Z);OJ(X,J,Z)}}function _4(J,$,Z=""){if(typeof document==="undefined")return;let X=M(Z);if(U&&U.parentNode){let q=U.textContent||"",Y=J.replace(/\s+/g," ").trim();if(q.replace(/\s+/g," ").trim().includes(Y)){if(!g($))N($,J);return}}try{let q=KJ(X),Y=q.textContent||"",Q=J.replace(/\s+/g," ").trim();if(Y.replace(/\s+/g," ").trim().includes(Q)){if(!g($))N($,J);return}if(!Y){let W=HJ();if(W.size>0){let F=Array.from(W.entries()).filter(([B])=>!B.startsWith("__theme_vars_")&&B!==$).map(([,B])=>B).join(`
10
- `);q.textContent=F+(F?`
11
- `:"")+J}else q.textContent=J}else I4(J,$);if(!g($))N($,J)}catch{if(!g($))N($,J)}if(!pJ(J))JJ(J)}function lJ(J,$,Z=""){if(g($))return;if(N($,J),bJ(J))return;vJ(J);let X=M(Z);if(U&&U.parentNode)try{let q=U.textContent||"",Y=J.replace(/\s+/g," ").trim();if(q.replace(/\s+/g," ").trim().includes(Y))return}catch{}_4(J,$,X)}function iJ(){return U}function T(J,$="",Z){let X=Z||J;if(typeof document==="undefined"){if(!g(X))N(X,J);JJ(J);return}lJ(J,X,$)}function rJ(J,$,Z=""){OJ(J,$,Z)}function sJ(J,$=""){mJ(J,$)}function aJ(){if(typeof document!=="undefined"){let J=iJ();if(J&&J.parentNode){let $=J.textContent||"";if(!$&&HJ().size>0)return IJ();return $}}return IJ()}function $J(J,$){if(!$||!J||typeof J!=="object")return J;let Z={},X=Object.keys($);for(let q in J){let Y=J[q];if(X.includes(q)&&$[q])try{let Q=$[q](Y);if(Q&&typeof Q==="object")Object.assign(Z,Q)}catch{Z[q]=Y}else if(C(Y))Z[q]=$J(Y,$);else Z[q]=Y}return Z}function O4(J,$){if(typeof J==="symbol"&&J===t)return!0;if(typeof $==="object"&&$!==null&&t in $)return!0;if(typeof J==="string"&&J.startsWith("__STOOP_COMPONENT_"))return!0;return!1}function E4(J,$){if(typeof $==="object"&&$!==null&&"__stoopClassName"in $&&typeof $.__stoopClassName==="string")return $.__stoopClassName;if(typeof J==="string"&&J.startsWith("__STOOP_COMPONENT_"))return J.replace("__STOOP_COMPONENT_","");return""}function x(J,$="",Z=0,X){if(!J||typeof J!=="object")return"";if(Z>GJ)return"";let q="",Y="";for(let G in J){let W=J[G];if(O4(G,W)){let F=E4(G,W);if(!F)continue;let B=P(F);if(!B)continue;let D=`.${B}`,H=wJ(W)?x(W,D,Z+1,X):"";Y+=H;continue}if(wJ(W))if(X&&G in X){let F=DJ(X[G]);if(F){let B=x(W,$,Z+1,X);Y+=`${F} { ${B} }`}}else if(G.startsWith("@")){let F=P(G);if(F){let B=x(W,$,Z+1,X);Y+=`${F} { ${B} }`}}else if(G.includes("&")){let F=P(G);if(F){let B=F.replace(/&/g,$),D=x(W,B,Z+1,X);Y+=D}}else if(G.startsWith(":")){let F=P(G);if(F){let B=`${$}${F}`,D=x(W,B,Z+1,X);Y+=D}}else if(G.includes(" ")||G.includes(">")||G.includes("+")||G.includes("~")){let F=P(G);if(F){let B=`${$} ${F}`,D=x(W,B,Z+1,X);Y+=D}}else{let F=P(G);if(F){let B=$?`${$} ${F}`:F,D=x(W,B,Z+1,X);Y+=D}}else if(W!==void 0){let F=c(G);if(F&&(typeof W==="string"||typeof W==="number")){let B=h(W);q+=`${F}: ${B}; `}}}return(q?`${$} { ${q.trim()}}`:"")+Y}function y(J,$,Z="",X,q,Y){let Q=M(Z),G=$J(J,q),W=S(G,$,Y),F=x(W,"",0,X),B=FJ(F),D=`${Q}:${B}`,H=p.get(D);if(H){let R=e.get(D);if(R)return T(R,Q,D),H;let A=x(W,`.${H}`,0,X);return e.set(D,A),T(A,Q,D),H}let L=Q?`${Q}-${B}`:`css-${B}`,O=x(W,`.${L}`,0,X);return p.set(D,L),e.set(D,O),TJ(),T(O,Q,D),L}function tJ(J,$="",Z,X,q){return function Y(Q){return y(Q,J,$,Z,X,q)}}function eJ(J,$="",Z,X,q){let Y=new Set;return function Q(G){if(typeof document==="undefined")return()=>{};let W=WJ(G);if(Y.has(W))return()=>{};Y.add(W);function F(O,R=0){if(R>GJ)return"";let A="";return Object.entries(O).forEach(([z,_])=>{if(C(_))if(Z&&z in Z){let K=DJ(Z[z]);if(K){let w=F(_,R+1);A+=`${K} { ${w} }`}}else if(z.startsWith("@")){let K=P(z);if(K){let w=F(_,R+1);A+=`${K} { ${w} }`}}else{let K=P(z);if(K){let w=F(_,R+1);A+=`${K} { ${w} }`}}else if(_!==void 0){let K=c(z);if(K&&(typeof _==="string"||typeof _==="number")){let w=h(_);A+=`${K}: ${w}; `}}}),A}let B=M($),D=$J(G,X),H=S(D,J,q),L=F(H);return T(L,B,`__global_${W}`),()=>{Y.delete(W)}}}function M4(J,$,Z,X){let q=`@keyframes ${$} {`,Y=Object.keys(J).sort((Q,G)=>{let W=parseFloat(Q.replace("%","")),F=parseFloat(G.replace("%",""));if(Q==="from")return-1;if(G==="from")return 1;if(Q==="to")return 1;if(G==="to")return-1;return W-F});for(let Q of Y){if(!SJ(Q))continue;let G=J[Q];if(!G||typeof G!=="object")continue;q+=` ${Q} {`;let W=S(G,Z,X);for(let F in W){let B=W[F];if(B!==void 0&&(typeof B==="string"||typeof B==="number")){let D=c(F);if(D){let H=h(B);q+=` ${D}: ${H};`}}}q+=" }"}return q+=" }",q}function J4(J="",$,Z){let X=M(J),q=new Map;return function Y(Q){let G=WJ(Q),W=q.get(G);if(W)return W;let F=G.slice(0,8),B=X?`${X}-${F}`:`stoop-${F}`,D=M4(Q,B,$,Z),H=`__keyframes_${B}`;return T(D,X,H),q.set(G,B),B}}import{createContext as V4,useCallback as EJ,useLayoutEffect as q4,useMemo as MJ,useState as P4}from"react";var $4=new Map;function Z4(J,$=""){let Z=$||"";$4.set(Z,J)}function z4(J=""){let $=J||"";return $4.get($)||null}function j4(J,$=""){let Z=z4($);if(!Z)return J;if(uJ(J,Z))return J;let X={...Z},q=Object.keys(J);for(let Y of q){if(Y==="media")continue;let Q=J[Y],G=Z[Y];if(Q&&typeof Q==="object"&&!Array.isArray(Q)&&G&&typeof G==="object"&&!Array.isArray(G))X[Y]={...G,...Q};else if(Q!==void 0)X[Y]=Q}return X}function LJ(J,$=""){if(typeof document==="undefined")return;let Z=j4(J,$),X=n(Z,$);rJ(X,Z,$)}import{jsxDEV as Y4}from"react/jsx-dev-runtime";function X4(J,$,Z,X=""){let q=V4(null),Y=Object.keys($),Q=Y[0]||"default";function G({attribute:W="data-theme",children:F,defaultTheme:B,storageKey:D="stoop-theme"}){let H=EJ(()=>{if(typeof window==="undefined")return B||Q;try{let w=localStorage.getItem(D);if(w&&$[w])return w}catch{}return B||Q},[B,D]),[L,O]=P4(H),R=MJ(()=>{return $[L]||$[B||Q]||Z},[L,B]);q4(()=>{if(R)LJ(R,X)},[R]),q4(()=>{if(typeof document!=="undefined"&&W)document.documentElement.setAttribute(W,L)},[L,W]);let A=EJ((w)=>{if($[w]){O(w);try{localStorage.setItem(D,w)}catch{}}else if(typeof process!=="undefined"&&process.env?.NODE_ENV!=="production")console.warn(`[Stoop] Theme "${w}" not found. Available themes: ${Y.join(", ")}`)},[D]),z=MJ(()=>({theme:R,themeName:L}),[R,L]),_=EJ(()=>{let V=(Y.indexOf(L)+1)%Y.length,k=Y[V];A(k)},[L,A]),K=MJ(()=>({availableThemes:Y,setTheme:A,theme:R,themeName:L,toggleTheme:_}),[R,L,A,_]);return Y4(J.Provider,{value:z,children:Y4(q.Provider,{value:K,children:F},void 0,!1,void 0,this)},void 0,!1,void 0,this)}return{Provider:G,ThemeManagementContext:q}}import{useMemo as zJ,forwardRef as N4,createElement as x4,useContext as g4}from"react";function G4(J,$,Z){let X=!1,q=[];for(let Y in J){let Q=$[Y];if(Q===void 0)continue;let G=J[Y],W=Q===!0?"true":Q===!1?"false":String(Q);if(G[W])q.push(G[W]),X=!0}return X?Object.assign({},Z,...q):Z}function b4(J){return{__isStoopStyled:!0,__stoopClassName:J,[t]:J,toString:()=>`__STOOP_COMPONENT_${J}`}}function v4(J){return typeof J==="object"&&J!==null&&"__isStoopStyled"in J&&J.__isStoopStyled===!0}function T4(J,$){if(!$)return{elementProps:J,variantProps:{}};let Z=new Set(Object.keys($)),X={},q={};for(let Y in J)if(Z.has(Y))X[Y]=J[Y];else q[Y]=J[Y];return{elementProps:q,variantProps:X}}function Q4(J,$="",Z,X,q,Y){return function Q(G,W,F){let B=W||YJ,D=F;if(W&&"variants"in W&&typeof W.variants==="object"){D=W.variants;let{compoundVariants:A,variants:z,..._}=W;B=_}let H=y(B,J,$,Z,X,q),L;if(typeof G!=="string"&&v4(G))L=G.__stoopClassName;let R=N4(function A(z,_){let{as:K,className:w,css:V,...k}=z,I=K||G,E=V&&typeof V==="object"&&V!==null?V:YJ,{elementProps:b,variantProps:v}=T4(k,D),i=g4(Y||NJ)?.theme||J,qJ=i.media||Z,r=zJ(()=>{if(!D)return"";let j=Object.entries(v);if(j.length===0)return"";return j.sort(([u],[s])=>u.localeCompare(s)).map(([u,s])=>`${u}:${String(s)}`).join("|")},[D,...Object.values(v)]),jJ=zJ(()=>{let j=B;if(D&&r)j=G4(D,v,B);if(E!==YJ)j=Object.assign({},j,E);return j},[r,E]),F4=zJ(()=>{let j=[];if(L)j.push(L);let u=y(jJ,i,$,qJ,X,q);if(u)j.push(u);if(w){let s=typeof w==="string"?w:String(w),VJ=fJ(s);if(VJ)j.push(VJ)}return j.length>0?j.join(" "):void 0},[jJ,i,$,qJ,X,q,w,L]);return x4(I,{...b,className:F4,ref:_})});return R.selector=b4(H),R}}import{useContext as k4}from"react";function B4(J){return function $(){let Z=k4(J);if(!Z)throw new Error("useTheme must be used within a Provider");return Z}}function f4(J){let{media:$,prefix:Z="",theme:X,themeMap:q,utils:Y}=J,Q=M(Z),G=BJ(X),W=G.media||$,F={...a,...q},B=C4(null);Z4(G,Q),sJ(G,Q);let D=tJ(G,Q,W,Y,F),H=xJ(G),L=eJ(G,Q,W,Y,F),O=J4(Q,G,F),R=Q4(G,Q,W,Y,F,B),A=Object.freeze({...G});function z(I){for(let E of I)try{y(E,G,Q,W,Y,F)}catch{}}function _(I){let E;if(typeof I==="string")if(J.themes&&J.themes[I])E=J.themes[I];else{if(typeof process!=="undefined"&&process.env?.NODE_ENV!=="production")console.warn(`[Stoop] Theme "${I}" not found. Available themes: ${J.themes?Object.keys(J.themes).join(", "):"none"}`);return}else E=I;LJ(E,Q)}function K(I){let E=G;if(I)if(typeof I==="string"){if(J.themes&&J.themes[I])E=J.themes[I];else if(typeof process!=="undefined"&&process.env?.NODE_ENV!=="production")console.warn(`[Stoop] Theme "${I}" not found. Using default theme. Available: ${J.themes?Object.keys(J.themes).join(", "):"none"}`)}else E=I;let b="",v=n(E,Q);if(v)b+=v+`
12
- `;let ZJ=aJ(),i=Q?`:root[data-stoop="${Q}"]`:":root",qJ=new RegExp(`${i.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}\\s*\\{[^}]*\\}`,"g"),r=ZJ.replace(qJ,"").trim();if(r)b+=(b?`
13
- `:"")+r;return b}let w,V;if(J.themes){let I={};for(let[v,ZJ]of Object.entries(J.themes))I[v]=H(ZJ);let{Provider:E,ThemeManagementContext:b}=X4(B,I,G,Q);w=E,V=B4(b)}let k={config:{...J,prefix:Q},createTheme:H,css:D,getCssText:K,globalCss:L,keyframes:O,preloadTheme:_,styled:R,theme:A,warmCache:z};if(w)k.Provider=w;if(V)k.useTheme=V;return k}export{f4 as createStoop};
1
+ /**
2
+ * Main entry point for Stoop CSS-in-JS library.
3
+ * Exports the createStoop factory function and essential public types.
4
+ */
5
+ export { createStoop } from "./create-stoop";
@@ -6,6 +6,7 @@
6
6
  import type { Theme } from "../types";
7
7
  /**
8
8
  * Gets or creates the stylesheet element for CSS injection.
9
+ * Reuses the SSR stylesheet if it exists to prevent FOUC.
9
10
  *
10
11
  * @param prefix - Optional prefix for stylesheet identification
11
12
  * @returns HTMLStyleElement
@@ -24,8 +25,6 @@ export declare function injectThemeVariables(cssVars: string, theme: Theme, pref
24
25
  /**
25
26
  * Registers a theme for injection (browser-specific).
26
27
  * Automatically ensures stylesheet exists and injects theme variables.
27
- * Note: This is typically called with the default theme from createStoop.
28
- * Additional themes should use updateThemeVariables which handles merging.
29
28
  *
30
29
  * @param theme - Theme object to register
31
30
  * @param prefix - Optional prefix for CSS variables
@@ -54,6 +53,6 @@ export declare function injectBrowserCSS(css: string, ruleKey: string, prefix?:
54
53
  */
55
54
  export declare function getStylesheetElement(): HTMLStyleElement | null;
56
55
  /**
57
- * Clears the stylesheet and all pending updates.
56
+ * Clears the stylesheet and all caches.
58
57
  */
59
58
  export declare function clearStylesheet(): void;
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Browser-specific CSS injection.
3
+ * Manages a single stylesheet element that gets updated with new CSS rules.
4
+ * Handles theme variable injection, deduplication, and stylesheet lifecycle.
5
+ */
6
+ import { isBrowser } from "../utils/environment";
7
+ import { getRootRegex, sanitizePrefix } from "../utils/string";
8
+ import { generateCSSVariables } from "../utils/theme";
9
+ import * as dedup from "./dedup";
10
+ import * as ssr from "./ssr";
11
+ let stylesheetElement = null;
12
+ const lastInjectedThemes = new Map();
13
+ const lastInjectedCSSVars = new Map();
14
+ /**
15
+ * Gets or creates the stylesheet element for CSS injection.
16
+ * Reuses the SSR stylesheet if it exists to prevent FOUC.
17
+ *
18
+ * @param prefix - Optional prefix for stylesheet identification
19
+ * @returns HTMLStyleElement
20
+ * @throws Error if called in SSR context
21
+ */
22
+ export function getStylesheet(prefix = "stoop") {
23
+ if (!isBrowser()) {
24
+ throw new Error("Cannot access document in SSR context");
25
+ }
26
+ const sanitizedPrefix = sanitizePrefix(prefix);
27
+ if (!stylesheetElement || !stylesheetElement.parentNode) {
28
+ const ssrStylesheet = document.getElementById("stoop-ssr");
29
+ if (ssrStylesheet) {
30
+ stylesheetElement = ssrStylesheet;
31
+ stylesheetElement.setAttribute("data-stoop", sanitizedPrefix || "stoop");
32
+ }
33
+ else {
34
+ stylesheetElement = document.createElement("style");
35
+ stylesheetElement.setAttribute("data-stoop", sanitizedPrefix || "stoop");
36
+ document.head.appendChild(stylesheetElement);
37
+ }
38
+ }
39
+ return stylesheetElement;
40
+ }
41
+ /**
42
+ * Injects theme CSS variables into the stylesheet.
43
+ * Automatically ensures stylesheet exists before injection.
44
+ *
45
+ * @param cssVars - CSS variables string
46
+ * @param theme - Theme object
47
+ * @param prefix - Optional prefix for CSS variables
48
+ */
49
+ export function injectThemeVariables(cssVars, theme, prefix = "stoop") {
50
+ if (!cssVars) {
51
+ return;
52
+ }
53
+ const sanitizedPrefix = sanitizePrefix(prefix);
54
+ const key = `__theme_vars_${sanitizedPrefix}`;
55
+ const lastCSSVars = lastInjectedCSSVars.get(key) ?? null;
56
+ if (lastCSSVars === cssVars) {
57
+ const lastTheme = lastInjectedThemes.get(key) ?? null;
58
+ if (lastTheme !== theme) {
59
+ lastInjectedThemes.set(key, theme);
60
+ }
61
+ return;
62
+ }
63
+ lastInjectedThemes.set(key, theme);
64
+ lastInjectedCSSVars.set(key, cssVars);
65
+ if (!isBrowser()) {
66
+ ssr.addToSSRCache(cssVars);
67
+ return;
68
+ }
69
+ const sheet = getStylesheet(sanitizedPrefix);
70
+ const currentCSS = sheet.textContent || "";
71
+ if (dedup.isInjectedRule(key)) {
72
+ const rootRegex = getRootRegex(sanitizedPrefix);
73
+ const withoutVars = currentCSS.replace(rootRegex, "").trim();
74
+ sheet.textContent = cssVars + (withoutVars ? "\n" + withoutVars : "");
75
+ dedup.markRuleAsInjected(key, cssVars);
76
+ }
77
+ else {
78
+ sheet.textContent = cssVars + (currentCSS ? "\n" + currentCSS : "");
79
+ dedup.markRuleAsInjected(key, cssVars);
80
+ }
81
+ }
82
+ /**
83
+ * Registers a theme for injection (browser-specific).
84
+ * Automatically ensures stylesheet exists and injects theme variables.
85
+ *
86
+ * @param theme - Theme object to register
87
+ * @param prefix - Optional prefix for CSS variables
88
+ */
89
+ export function registerTheme(theme, prefix = "stoop") {
90
+ const sanitizedPrefix = sanitizePrefix(prefix);
91
+ if (isBrowser()) {
92
+ getStylesheet(sanitizedPrefix);
93
+ const cssVars = generateCSSVariables(theme, sanitizedPrefix);
94
+ injectThemeVariables(cssVars, theme, sanitizedPrefix);
95
+ }
96
+ }
97
+ /**
98
+ * Updates the stylesheet with new CSS rules.
99
+ *
100
+ * @param css - CSS string to inject
101
+ * @param ruleKey - Unique key for deduplication
102
+ * @param prefix - Optional prefix for CSS rules
103
+ */
104
+ export function updateStylesheet(css, ruleKey, prefix = "stoop") {
105
+ if (!isBrowser()) {
106
+ return;
107
+ }
108
+ const sanitizedPrefix = sanitizePrefix(prefix);
109
+ if (dedup.isInjectedRule(ruleKey)) {
110
+ return;
111
+ }
112
+ const sheet = getStylesheet(sanitizedPrefix);
113
+ const currentCSS = sheet.textContent || "";
114
+ sheet.textContent = currentCSS + (currentCSS ? "\n" : "") + css;
115
+ dedup.markRuleAsInjected(ruleKey, css);
116
+ }
117
+ /**
118
+ * Injects CSS into the browser stylesheet with deduplication.
119
+ *
120
+ * @param css - CSS string to inject
121
+ * @param ruleKey - Unique key for deduplication
122
+ * @param prefix - Optional prefix for CSS rules
123
+ */
124
+ export function injectBrowserCSS(css, ruleKey, prefix = "stoop") {
125
+ if (dedup.isInjectedRule(ruleKey)) {
126
+ return;
127
+ }
128
+ const sanitizedPrefix = sanitizePrefix(prefix);
129
+ updateStylesheet(css, ruleKey, sanitizedPrefix);
130
+ }
131
+ /**
132
+ * Gets the current stylesheet element.
133
+ *
134
+ * @returns HTMLStyleElement or null if not created
135
+ */
136
+ export function getStylesheetElement() {
137
+ return stylesheetElement;
138
+ }
139
+ /**
140
+ * Clears the stylesheet and all caches.
141
+ */
142
+ export function clearStylesheet() {
143
+ if (stylesheetElement && stylesheetElement.parentNode) {
144
+ stylesheetElement.parentNode.removeChild(stylesheetElement);
145
+ }
146
+ stylesheetElement = null;
147
+ lastInjectedThemes.clear();
148
+ lastInjectedCSSVars.clear();
149
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * CSS injection deduplication.
3
+ * Tracks which CSS rules have been injected to prevent duplicates.
4
+ * Used by both browser and SSR injection systems.
5
+ */
6
+ const injectedRules = new Map();
7
+ /**
8
+ * Checks if a CSS rule has already been injected.
9
+ *
10
+ * @param key - Rule key to check
11
+ * @returns True if rule is already injected
12
+ */
13
+ export function isInjectedRule(key) {
14
+ return injectedRules.has(key);
15
+ }
16
+ /**
17
+ * Marks a CSS rule as injected.
18
+ *
19
+ * @param key - Rule key
20
+ * @param css - CSS string
21
+ */
22
+ export function markRuleAsInjected(key, css) {
23
+ injectedRules.set(key, css);
24
+ }
25
+ /**
26
+ * Gets all injected rules as a new Map.
27
+ *
28
+ * @returns Map of all injected rules
29
+ */
30
+ export function getAllInjectedRules() {
31
+ return new Map(injectedRules);
32
+ }
33
+ /**
34
+ * Clears all injected rule tracking.
35
+ */
36
+ export function clearInjectedRules() {
37
+ injectedRules.clear();
38
+ }
@@ -23,7 +23,6 @@ export declare function injectCSS(css: string, prefix?: string, ruleKey?: string
23
23
  export declare function injectThemeVariables(cssVars: string, theme: Theme, prefix?: string): void;
24
24
  /**
25
25
  * Registers a theme for injection.
26
- * Automatically ensures stylesheet exists and injects theme variables.
27
26
  *
28
27
  * @param theme - Theme object to register
29
28
  * @param prefix - Optional prefix for CSS variables
@@ -0,0 +1,75 @@
1
+ /**
2
+ * CSS injection public API.
3
+ * Composes browser, SSR, and deduplication modules into a unified interface.
4
+ * Provides single stylesheet injection with automatic SSR support.
5
+ */
6
+ import { clearStyleCache } from "../core/cache";
7
+ import { isBrowser } from "../utils/environment";
8
+ import * as browser from "./browser";
9
+ import * as dedup from "./dedup";
10
+ import * as ssr from "./ssr";
11
+ export { isInjectedRule } from "./dedup";
12
+ /**
13
+ * Injects CSS into the document with automatic SSR support.
14
+ *
15
+ * @param css - CSS string to inject
16
+ * @param prefix - Optional prefix for CSS rules
17
+ * @param ruleKey - Optional unique key for deduplication
18
+ */
19
+ export function injectCSS(css, prefix = "stoop", ruleKey) {
20
+ const key = ruleKey || css;
21
+ if (!isBrowser()) {
22
+ if (!dedup.isInjectedRule(key)) {
23
+ dedup.markRuleAsInjected(key, css);
24
+ }
25
+ ssr.addToSSRCache(css);
26
+ return;
27
+ }
28
+ browser.injectBrowserCSS(css, key, prefix);
29
+ }
30
+ /**
31
+ * Injects theme CSS variables into the document.
32
+ *
33
+ * @param cssVars - CSS variables string
34
+ * @param theme - Theme object
35
+ * @param prefix - Optional prefix for CSS variables
36
+ */
37
+ export function injectThemeVariables(cssVars, theme, prefix = "stoop") {
38
+ browser.injectThemeVariables(cssVars, theme, prefix);
39
+ }
40
+ /**
41
+ * Registers a theme for injection.
42
+ *
43
+ * @param theme - Theme object to register
44
+ * @param prefix - Optional prefix for CSS variables
45
+ */
46
+ export function registerTheme(theme, prefix = "stoop") {
47
+ browser.registerTheme(theme, prefix);
48
+ }
49
+ /**
50
+ * Gets all injected CSS text (browser or SSR).
51
+ *
52
+ * @returns CSS text string
53
+ */
54
+ export function getCssText() {
55
+ if (isBrowser()) {
56
+ const sheetElement = browser.getStylesheetElement();
57
+ if (sheetElement && sheetElement.parentNode) {
58
+ const sheetCSS = sheetElement.textContent || "";
59
+ if (!sheetCSS && dedup.getAllInjectedRules().size > 0) {
60
+ return ssr.getSSRCacheText();
61
+ }
62
+ return sheetCSS;
63
+ }
64
+ }
65
+ return ssr.getSSRCacheText();
66
+ }
67
+ /**
68
+ * Clears all injected CSS and caches.
69
+ */
70
+ export function clearStylesheet() {
71
+ browser.clearStylesheet();
72
+ dedup.clearInjectedRules();
73
+ ssr.clearSSRCache();
74
+ clearStyleCache();
75
+ }
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * SSR cache management for CSS injection.
3
3
  * Maintains a cache of CSS text for server-side rendering.
4
- * Implements FIFO eviction to prevent memory leaks.
5
4
  */
6
5
  /**
7
6
  * Adds CSS to the SSR cache with FIFO eviction.