stoop 0.6.0 → 0.6.2
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/dist/api/core-api.d.ts +2 -0
- package/dist/api/core-api.js +171 -0
- package/dist/api/styled.d.ts +3 -0
- package/dist/api/styled.js +467 -0
- package/dist/api/theme-provider.d.ts +3 -0
- package/dist/api/theme-provider.js +199 -0
- package/dist/constants.js +154 -0
- package/dist/core/cache.js +66 -0
- package/dist/core/compiler.js +408 -0
- package/dist/core/stringify.js +150 -0
- package/dist/core/theme-manager.js +97 -0
- package/dist/create-stoop-internal.d.ts +2 -2
- package/dist/create-stoop-internal.js +119 -0
- package/dist/create-stoop-ssr.d.ts +2 -0
- package/dist/create-stoop-ssr.js +22 -16
- package/dist/create-stoop.d.ts +6 -26
- package/dist/create-stoop.js +48 -17
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/inject.js +293 -0
- package/dist/types/index.d.ts +4 -4
- package/dist/types/index.js +5 -0
- package/dist/utils/helpers.js +157 -0
- package/dist/utils/storage.js +130 -0
- package/dist/utils/theme-utils.js +328 -0
- package/dist/utils/theme.js +430 -0
- package/package.json +14 -28
package/dist/inject.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS injection implementation.
|
|
3
|
+
* Consolidates browser-specific and SSR-specific CSS injection logic.
|
|
4
|
+
* Handles stylesheet management, theme variable injection, deduplication, and SSR caching.
|
|
5
|
+
*/
|
|
6
|
+
import { MAX_CSS_CACHE_SIZE } from "./constants";
|
|
7
|
+
import { LRUCache, clearStyleCache } from "./core/cache";
|
|
8
|
+
import { isBrowser } from "./utils/helpers";
|
|
9
|
+
import { getRootRegex, sanitizePrefix } from "./utils/theme-utils";
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Internal Deduplication
|
|
12
|
+
// ============================================================================
|
|
13
|
+
const injectedRules = new Map();
|
|
14
|
+
/**
|
|
15
|
+
* Checks if a CSS rule has already been injected.
|
|
16
|
+
*
|
|
17
|
+
* @param key - Rule key to check
|
|
18
|
+
* @returns True if rule is already injected
|
|
19
|
+
*/
|
|
20
|
+
export function isInjectedRule(key) {
|
|
21
|
+
return injectedRules.has(key);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Marks a CSS rule as injected.
|
|
25
|
+
*
|
|
26
|
+
* @param key - Rule key
|
|
27
|
+
* @param css - CSS string
|
|
28
|
+
*/
|
|
29
|
+
export function markRuleAsInjected(key, css) {
|
|
30
|
+
injectedRules.set(key, css);
|
|
31
|
+
}
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// SSR Cache Management
|
|
34
|
+
// ============================================================================
|
|
35
|
+
const cssTextCache = new LRUCache(MAX_CSS_CACHE_SIZE);
|
|
36
|
+
/**
|
|
37
|
+
* Adds CSS to the SSR cache with LRU eviction.
|
|
38
|
+
*
|
|
39
|
+
* @param css - CSS string to cache
|
|
40
|
+
*/
|
|
41
|
+
export function addToSSRCache(css) {
|
|
42
|
+
if (!cssTextCache.has(css)) {
|
|
43
|
+
cssTextCache.set(css, true);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Gets all cached CSS text for SSR.
|
|
48
|
+
*
|
|
49
|
+
* @returns Joined CSS text string
|
|
50
|
+
*/
|
|
51
|
+
export function getSSRCacheText() {
|
|
52
|
+
return Array.from(cssTextCache.keys()).join("\n");
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Clears the SSR cache.
|
|
56
|
+
*/
|
|
57
|
+
export function clearSSRCache() {
|
|
58
|
+
cssTextCache.clear();
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Checks if CSS is already in the SSR cache.
|
|
62
|
+
*
|
|
63
|
+
* @param css - CSS string to check
|
|
64
|
+
* @returns True if CSS is cached
|
|
65
|
+
*/
|
|
66
|
+
export function isInSSRCache(css) {
|
|
67
|
+
return cssTextCache.has(css);
|
|
68
|
+
}
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Browser-Specific Injection
|
|
71
|
+
// ============================================================================
|
|
72
|
+
const stylesheetElements = new Map();
|
|
73
|
+
const lastInjectedThemes = new Map();
|
|
74
|
+
const lastInjectedCSSVars = new Map();
|
|
75
|
+
/**
|
|
76
|
+
* Gets or creates the stylesheet element for CSS injection.
|
|
77
|
+
* Reuses the SSR stylesheet if it exists to prevent FOUC.
|
|
78
|
+
*
|
|
79
|
+
* @param prefix - Optional prefix for stylesheet identification
|
|
80
|
+
* @returns HTMLStyleElement
|
|
81
|
+
* @throws Error if called in SSR context
|
|
82
|
+
*/
|
|
83
|
+
export function getStylesheet(prefix = "stoop") {
|
|
84
|
+
if (!isBrowser()) {
|
|
85
|
+
throw new Error("Cannot access document in SSR context");
|
|
86
|
+
}
|
|
87
|
+
const sanitizedPrefix = sanitizePrefix(prefix);
|
|
88
|
+
let stylesheetElement = stylesheetElements.get(sanitizedPrefix);
|
|
89
|
+
if (!stylesheetElement || !stylesheetElement.parentNode) {
|
|
90
|
+
const ssrStylesheet = document.getElementById("stoop-ssr");
|
|
91
|
+
if (ssrStylesheet) {
|
|
92
|
+
const existingPrefix = ssrStylesheet.getAttribute("data-stoop");
|
|
93
|
+
if (!existingPrefix || existingPrefix === sanitizedPrefix) {
|
|
94
|
+
stylesheetElement = ssrStylesheet;
|
|
95
|
+
stylesheetElement.setAttribute("data-stoop", sanitizedPrefix);
|
|
96
|
+
stylesheetElements.set(sanitizedPrefix, stylesheetElement);
|
|
97
|
+
return stylesheetElement;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
stylesheetElement = document.createElement("style");
|
|
101
|
+
stylesheetElement.setAttribute("data-stoop", sanitizedPrefix);
|
|
102
|
+
stylesheetElement.setAttribute("id", `stoop-${sanitizedPrefix}`);
|
|
103
|
+
document.head.appendChild(stylesheetElement);
|
|
104
|
+
stylesheetElements.set(sanitizedPrefix, stylesheetElement);
|
|
105
|
+
}
|
|
106
|
+
return stylesheetElement;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Removes all theme variable blocks (both :root and attribute selectors) from CSS.
|
|
110
|
+
*
|
|
111
|
+
* @param css - CSS string to clean
|
|
112
|
+
* @returns CSS string without theme variable blocks
|
|
113
|
+
*/
|
|
114
|
+
export function removeThemeVariableBlocks(css) {
|
|
115
|
+
let result = css;
|
|
116
|
+
const rootRegex = getRootRegex("");
|
|
117
|
+
result = result.replace(rootRegex, "").trim();
|
|
118
|
+
let startIndex = result.indexOf("[data-theme=");
|
|
119
|
+
while (startIndex !== -1) {
|
|
120
|
+
const openBrace = result.indexOf("{", startIndex);
|
|
121
|
+
if (openBrace === -1) {
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
let braceCount = 1;
|
|
125
|
+
let closeBrace = openBrace + 1;
|
|
126
|
+
while (closeBrace < result.length && braceCount > 0) {
|
|
127
|
+
if (result[closeBrace] === "{") {
|
|
128
|
+
braceCount++;
|
|
129
|
+
}
|
|
130
|
+
else if (result[closeBrace] === "}") {
|
|
131
|
+
braceCount--;
|
|
132
|
+
}
|
|
133
|
+
closeBrace++;
|
|
134
|
+
}
|
|
135
|
+
if (braceCount === 0) {
|
|
136
|
+
const before = result.substring(0, startIndex).trim();
|
|
137
|
+
const after = result.substring(closeBrace).trim();
|
|
138
|
+
result = (before + "\n" + after).trim();
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
startIndex = result.indexOf("[data-theme=");
|
|
144
|
+
}
|
|
145
|
+
return result.trim();
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Injects CSS variables for all themes using attribute selectors.
|
|
149
|
+
* All themes are available simultaneously, with theme switching handled by changing the data-theme attribute.
|
|
150
|
+
*
|
|
151
|
+
* @param allThemeVars - CSS string containing all theme CSS variables
|
|
152
|
+
* @param prefix - Optional prefix for CSS variables
|
|
153
|
+
*/
|
|
154
|
+
export function injectAllThemeVariables(allThemeVars, prefix = "stoop") {
|
|
155
|
+
if (!allThemeVars) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const sanitizedPrefix = sanitizePrefix(prefix);
|
|
159
|
+
const key = `__all_theme_vars_${sanitizedPrefix}`;
|
|
160
|
+
const lastCSSVars = lastInjectedCSSVars.get(key) ?? null;
|
|
161
|
+
if (lastCSSVars === allThemeVars) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
lastInjectedCSSVars.set(key, allThemeVars);
|
|
165
|
+
if (!isBrowser()) {
|
|
166
|
+
addToSSRCache(allThemeVars);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const sheet = getStylesheet(sanitizedPrefix);
|
|
170
|
+
const currentCSS = sheet.textContent || "";
|
|
171
|
+
const hasThemeBlocks = currentCSS.includes(":root") || currentCSS.includes("[data-theme=");
|
|
172
|
+
if (isInjectedRule(key) || hasThemeBlocks) {
|
|
173
|
+
const withoutVars = removeThemeVariableBlocks(currentCSS);
|
|
174
|
+
sheet.textContent = allThemeVars + (withoutVars ? "\n\n" + withoutVars : "");
|
|
175
|
+
markRuleAsInjected(key, allThemeVars);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
sheet.textContent = allThemeVars + (currentCSS ? "\n\n" + currentCSS : "");
|
|
179
|
+
markRuleAsInjected(key, allThemeVars);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Updates the stylesheet with new CSS rules.
|
|
184
|
+
*
|
|
185
|
+
* @param css - CSS string to inject
|
|
186
|
+
* @param ruleKey - Unique key for deduplication
|
|
187
|
+
* @param prefix - Optional prefix for CSS rules
|
|
188
|
+
*/
|
|
189
|
+
export function updateStylesheet(css, ruleKey, prefix = "stoop") {
|
|
190
|
+
if (!isBrowser()) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const sanitizedPrefix = sanitizePrefix(prefix);
|
|
194
|
+
if (isInjectedRule(ruleKey)) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const sheet = getStylesheet(sanitizedPrefix);
|
|
198
|
+
const currentCSS = sheet.textContent || "";
|
|
199
|
+
sheet.textContent = currentCSS + (currentCSS ? "\n" : "") + css;
|
|
200
|
+
markRuleAsInjected(ruleKey, css);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Injects CSS into the browser stylesheet with deduplication.
|
|
204
|
+
*
|
|
205
|
+
* @param css - CSS string to inject
|
|
206
|
+
* @param ruleKey - Unique key for deduplication
|
|
207
|
+
* @param prefix - Optional prefix for CSS rules
|
|
208
|
+
*/
|
|
209
|
+
export function injectBrowserCSS(css, ruleKey, prefix = "stoop") {
|
|
210
|
+
if (isInjectedRule(ruleKey)) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const sanitizedPrefix = sanitizePrefix(prefix);
|
|
214
|
+
updateStylesheet(css, ruleKey, sanitizedPrefix);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Gets the current stylesheet element for a given prefix.
|
|
218
|
+
*
|
|
219
|
+
* @param prefix - Optional prefix for stylesheet identification
|
|
220
|
+
* @returns HTMLStyleElement or null if not created
|
|
221
|
+
*/
|
|
222
|
+
export function getStylesheetElement(prefix = "stoop") {
|
|
223
|
+
const sanitizedPrefix = sanitizePrefix(prefix);
|
|
224
|
+
return stylesheetElements.get(sanitizedPrefix) || null;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Clears the stylesheet and all caches.
|
|
228
|
+
*/
|
|
229
|
+
function clearStylesheetInternal() {
|
|
230
|
+
for (const [, element] of stylesheetElements.entries()) {
|
|
231
|
+
if (element && element.parentNode) {
|
|
232
|
+
element.parentNode.removeChild(element);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
stylesheetElements.clear();
|
|
236
|
+
lastInjectedThemes.clear();
|
|
237
|
+
lastInjectedCSSVars.clear();
|
|
238
|
+
injectedRules.clear();
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Gets all injected rules.
|
|
242
|
+
*/
|
|
243
|
+
export function getAllInjectedRules() {
|
|
244
|
+
return new Map(injectedRules);
|
|
245
|
+
}
|
|
246
|
+
// ============================================================================
|
|
247
|
+
// Public API Functions
|
|
248
|
+
// ============================================================================
|
|
249
|
+
/**
|
|
250
|
+
* Injects CSS into the document with automatic SSR support.
|
|
251
|
+
*
|
|
252
|
+
* @param css - CSS string to inject
|
|
253
|
+
* @param prefix - Optional prefix for CSS rules
|
|
254
|
+
* @param ruleKey - Optional unique key for deduplication
|
|
255
|
+
*/
|
|
256
|
+
export function injectCSS(css, prefix = "stoop", ruleKey) {
|
|
257
|
+
const key = ruleKey || css;
|
|
258
|
+
if (!isBrowser()) {
|
|
259
|
+
if (!isInjectedRule(key)) {
|
|
260
|
+
markRuleAsInjected(key, css);
|
|
261
|
+
}
|
|
262
|
+
addToSSRCache(css);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
injectBrowserCSS(css, key, prefix);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Gets all injected CSS text (browser or SSR).
|
|
269
|
+
*
|
|
270
|
+
* @returns CSS text string
|
|
271
|
+
*/
|
|
272
|
+
export function getCssText(prefix = "stoop") {
|
|
273
|
+
if (isBrowser()) {
|
|
274
|
+
const sanitizedPrefix = sanitizePrefix(prefix);
|
|
275
|
+
const sheetElement = getStylesheetElement(sanitizedPrefix);
|
|
276
|
+
if (sheetElement && sheetElement.parentNode) {
|
|
277
|
+
const sheetCSS = sheetElement.textContent || "";
|
|
278
|
+
if (!sheetCSS && getAllInjectedRules().size > 0) {
|
|
279
|
+
return getSSRCacheText();
|
|
280
|
+
}
|
|
281
|
+
return sheetCSS;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return getSSRCacheText();
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Clears all injected CSS and caches.
|
|
288
|
+
*/
|
|
289
|
+
export function clearStylesheet() {
|
|
290
|
+
clearStylesheetInternal();
|
|
291
|
+
clearSSRCache();
|
|
292
|
+
clearStyleCache();
|
|
293
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -265,7 +265,7 @@ type Merge<T, U> = Omit<T, keyof U> & U;
|
|
|
265
265
|
* Props for a styled component without the `as` prop polymorphism.
|
|
266
266
|
* Just the base element props + our styled props.
|
|
267
267
|
*/
|
|
268
|
-
export type StyledComponentProps<DefaultElement extends ElementType, VariantsConfig extends Variants = {}> = Merge<DefaultElement extends keyof JSX.IntrinsicElements | ComponentType<
|
|
268
|
+
export type StyledComponentProps<DefaultElement extends ElementType, VariantsConfig extends Variants = {}> = Merge<DefaultElement extends keyof JSX.IntrinsicElements | ComponentType<unknown> ? ComponentPropsWithRef<DefaultElement> : {}, StyledOwnProps<VariantsConfig>>;
|
|
269
269
|
export interface ThemeContextValue {
|
|
270
270
|
theme: Theme;
|
|
271
271
|
themeName?: string;
|
|
@@ -521,7 +521,7 @@ export interface StoopInstance {
|
|
|
521
521
|
* type ButtonProps = ComponentProps<typeof Button>;
|
|
522
522
|
* ```
|
|
523
523
|
*/
|
|
524
|
-
export type ComponentProps<Component> = Component extends (...args:
|
|
524
|
+
export type ComponentProps<Component> = Component extends (...args: unknown[]) => unknown ? Parameters<Component>[0] : never;
|
|
525
525
|
/**
|
|
526
526
|
* Returns a type that extracts only the variant props from a styled component.
|
|
527
527
|
*
|
|
@@ -535,6 +535,6 @@ export type ComponentProps<Component> = Component extends (...args: any[]) => an
|
|
|
535
535
|
* ```
|
|
536
536
|
*/
|
|
537
537
|
export type VariantProps<Component extends {
|
|
538
|
-
selector:
|
|
539
|
-
}> = Component extends StyledComponent<
|
|
538
|
+
selector: StyledComponentRef;
|
|
539
|
+
}> = Component extends StyledComponent<ElementType, infer V> ? VariantPropsFromConfig<V> : never;
|
|
540
540
|
export {};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper utilities for Stoop.
|
|
3
|
+
* Consolidates environment detection, type guards, theme validation, and utility function application.
|
|
4
|
+
*/
|
|
5
|
+
import { APPROVED_THEME_SCALES } from "../constants";
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Environment Detection
|
|
8
|
+
// ============================================================================
|
|
9
|
+
/**
|
|
10
|
+
* Checks if code is running in a browser environment.
|
|
11
|
+
*
|
|
12
|
+
* @returns True if running in browser, false if in SSR/Node environment
|
|
13
|
+
*/
|
|
14
|
+
export function isBrowser() {
|
|
15
|
+
return (typeof window !== "undefined" &&
|
|
16
|
+
typeof document !== "undefined" &&
|
|
17
|
+
typeof window.document === "object" &&
|
|
18
|
+
typeof document.createElement === "function");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Checks if running in production mode.
|
|
22
|
+
*
|
|
23
|
+
* @returns True if running in production mode
|
|
24
|
+
*/
|
|
25
|
+
export function isProduction() {
|
|
26
|
+
return typeof process !== "undefined" && process.env?.NODE_ENV === "production";
|
|
27
|
+
}
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Type Guards
|
|
30
|
+
// ============================================================================
|
|
31
|
+
/**
|
|
32
|
+
* Type guard for CSS objects.
|
|
33
|
+
*
|
|
34
|
+
* @param value - Value to check
|
|
35
|
+
* @returns True if value is a CSS object
|
|
36
|
+
*/
|
|
37
|
+
export function isCSSObject(value) {
|
|
38
|
+
return typeof value === "object" && value !== null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Checks if a value is a styled component reference.
|
|
42
|
+
*
|
|
43
|
+
* @param value - Value to check
|
|
44
|
+
* @returns True if value is a styled component reference
|
|
45
|
+
*/
|
|
46
|
+
export function isStyledComponentRef(value) {
|
|
47
|
+
return (typeof value === "object" &&
|
|
48
|
+
value !== null &&
|
|
49
|
+
"__isStoopStyled" in value &&
|
|
50
|
+
"__stoopClassName" in value &&
|
|
51
|
+
value.__isStoopStyled === true);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Type guard for valid CSS objects (excludes styled component references).
|
|
55
|
+
*
|
|
56
|
+
* @param value - Value to check
|
|
57
|
+
* @returns True if value is a valid CSS object
|
|
58
|
+
*/
|
|
59
|
+
export function isValidCSSObject(value) {
|
|
60
|
+
return isCSSObject(value) && !isStyledComponentRef(value);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Type guard for theme objects.
|
|
64
|
+
*
|
|
65
|
+
* @param value - Value to check
|
|
66
|
+
* @returns True if value is a theme object
|
|
67
|
+
*/
|
|
68
|
+
export function isThemeObject(value) {
|
|
69
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
70
|
+
}
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// Theme Validation
|
|
73
|
+
// ============================================================================
|
|
74
|
+
/**
|
|
75
|
+
* Validates that a theme object only contains approved scales.
|
|
76
|
+
*
|
|
77
|
+
* @param theme - Theme object to validate
|
|
78
|
+
* @returns Validated theme as DefaultTheme
|
|
79
|
+
* @throws Error if theme contains invalid scales (development only)
|
|
80
|
+
*/
|
|
81
|
+
export function validateTheme(theme) {
|
|
82
|
+
if (!theme || typeof theme !== "object" || Array.isArray(theme)) {
|
|
83
|
+
throw new Error("[Stoop] Theme must be a non-null object");
|
|
84
|
+
}
|
|
85
|
+
// Skip all validation in production for performance
|
|
86
|
+
if (isProduction()) {
|
|
87
|
+
return theme;
|
|
88
|
+
}
|
|
89
|
+
const themeObj = theme;
|
|
90
|
+
const invalidScales = [];
|
|
91
|
+
for (const key in themeObj) {
|
|
92
|
+
if (key === "media") {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (!APPROVED_THEME_SCALES.includes(key)) {
|
|
96
|
+
invalidScales.push(key);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (invalidScales.length > 0) {
|
|
100
|
+
const errorMessage = `[Stoop] Theme contains invalid scales: ${invalidScales.join(", ")}. ` +
|
|
101
|
+
`Only these scales are allowed: ${APPROVED_THEME_SCALES.join(", ")}`;
|
|
102
|
+
throw new Error(errorMessage);
|
|
103
|
+
}
|
|
104
|
+
return theme;
|
|
105
|
+
}
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// Utility Function Application
|
|
108
|
+
// ============================================================================
|
|
109
|
+
/**
|
|
110
|
+
* Applies utility functions to transform shorthand properties into CSS.
|
|
111
|
+
*
|
|
112
|
+
* @param styles - CSS object to process
|
|
113
|
+
* @param utils - Optional utility functions object
|
|
114
|
+
* @returns CSS object with utilities applied
|
|
115
|
+
*/
|
|
116
|
+
export function applyUtilities(styles, utils) {
|
|
117
|
+
if (!utils || !styles || typeof styles !== "object") {
|
|
118
|
+
return styles;
|
|
119
|
+
}
|
|
120
|
+
const utilityKeys = Object.keys(utils);
|
|
121
|
+
// Fast path: check if any utility keys are present before creating new object
|
|
122
|
+
let hasUtilities = false;
|
|
123
|
+
for (const key in styles) {
|
|
124
|
+
if (utilityKeys.includes(key)) {
|
|
125
|
+
hasUtilities = true;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// If no utility keys found, return original object
|
|
130
|
+
if (!hasUtilities) {
|
|
131
|
+
return styles;
|
|
132
|
+
}
|
|
133
|
+
const result = {};
|
|
134
|
+
for (const key in styles) {
|
|
135
|
+
const value = styles[key];
|
|
136
|
+
if (utilityKeys.includes(key) && utils[key]) {
|
|
137
|
+
try {
|
|
138
|
+
const utilityResult = utils[key](value);
|
|
139
|
+
if (utilityResult && typeof utilityResult === "object") {
|
|
140
|
+
for (const utilKey in utilityResult) {
|
|
141
|
+
result[utilKey] = utilityResult[utilKey];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
result[key] = value;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else if (isCSSObject(value)) {
|
|
150
|
+
result[key] = applyUtilities(value, utils);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
result[key] = value;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage and theme detection utilities.
|
|
3
|
+
* Provides simplified localStorage and cookie management with SSR compatibility and error handling.
|
|
4
|
+
*/
|
|
5
|
+
import { DEFAULT_COOKIE_MAX_AGE, DEFAULT_COOKIE_PATH } from "../constants";
|
|
6
|
+
import { isBrowser } from "./helpers";
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Storage Utilities
|
|
9
|
+
// ============================================================================
|
|
10
|
+
/**
|
|
11
|
+
* Safely gets a value from localStorage.
|
|
12
|
+
*
|
|
13
|
+
* @param key - Storage key
|
|
14
|
+
* @returns Storage result
|
|
15
|
+
*/
|
|
16
|
+
export function getFromStorage(key) {
|
|
17
|
+
if (!isBrowser()) {
|
|
18
|
+
return { error: "Not in browser environment", success: false, value: null };
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const value = localStorage.getItem(key);
|
|
22
|
+
return { source: "localStorage", success: true, value };
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
return {
|
|
26
|
+
error: error instanceof Error ? error.message : "localStorage access failed",
|
|
27
|
+
success: false,
|
|
28
|
+
value: null,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Safely sets a value in localStorage.
|
|
34
|
+
*
|
|
35
|
+
* @param key - Storage key
|
|
36
|
+
* @param value - Value to store
|
|
37
|
+
* @returns Storage result
|
|
38
|
+
*/
|
|
39
|
+
export function setInStorage(key, value) {
|
|
40
|
+
if (!isBrowser()) {
|
|
41
|
+
return { error: "Not in browser environment", success: false, value: undefined };
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
localStorage.setItem(key, value);
|
|
45
|
+
return { success: true, value: undefined };
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return {
|
|
49
|
+
error: error instanceof Error ? error.message : "localStorage write failed",
|
|
50
|
+
success: false,
|
|
51
|
+
value: undefined,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Safely removes a value from localStorage.
|
|
57
|
+
*
|
|
58
|
+
* @param key - Storage key
|
|
59
|
+
* @returns Storage result
|
|
60
|
+
*/
|
|
61
|
+
export function removeFromStorage(key) {
|
|
62
|
+
if (!isBrowser()) {
|
|
63
|
+
return { error: "Not in browser environment", success: false, value: undefined };
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
localStorage.removeItem(key);
|
|
67
|
+
return { success: true, value: undefined };
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
return {
|
|
71
|
+
error: error instanceof Error ? error.message : "localStorage remove failed",
|
|
72
|
+
success: false,
|
|
73
|
+
value: undefined,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Gets a cookie value.
|
|
79
|
+
*
|
|
80
|
+
* @param name - Cookie name
|
|
81
|
+
* @returns Cookie value or null if not found
|
|
82
|
+
*/
|
|
83
|
+
export function getCookie(name) {
|
|
84
|
+
if (!isBrowser())
|
|
85
|
+
return null;
|
|
86
|
+
const value = `; ${document.cookie}`;
|
|
87
|
+
const parts = value.split(`; ${name}=`);
|
|
88
|
+
if (parts.length === 2) {
|
|
89
|
+
return parts.pop()?.split(";").shift() || null;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Sets a cookie value.
|
|
95
|
+
*
|
|
96
|
+
* @param name - Cookie name
|
|
97
|
+
* @param value - Cookie value
|
|
98
|
+
* @param options - Cookie options
|
|
99
|
+
* @returns Success status
|
|
100
|
+
*/
|
|
101
|
+
export function setCookie(name, value, options = {}) {
|
|
102
|
+
if (!isBrowser())
|
|
103
|
+
return false;
|
|
104
|
+
const { maxAge = DEFAULT_COOKIE_MAX_AGE, path = DEFAULT_COOKIE_PATH, secure = false } = options;
|
|
105
|
+
try {
|
|
106
|
+
document.cookie = `${name}=${value}; path=${path}; max-age=${maxAge}${secure ? "; secure" : ""}`;
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Removes a cookie by setting it to expire.
|
|
115
|
+
*
|
|
116
|
+
* @param name - Cookie name
|
|
117
|
+
* @param path - Cookie path
|
|
118
|
+
* @returns Success status
|
|
119
|
+
*/
|
|
120
|
+
export function removeCookie(name, path = "/") {
|
|
121
|
+
if (!isBrowser())
|
|
122
|
+
return false;
|
|
123
|
+
try {
|
|
124
|
+
document.cookie = `${name}=; path=${path}; max-age=0`;
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|