stoop 0.4.0 → 0.5.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.
@@ -1,150 +0,0 @@
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
- // Use document.createElement instead of requestAnimationFrame for better compatibility
19
- // requestAnimationFrame may not exist in test environments (jsdom)
20
- typeof document.createElement === "function");
21
- }
22
- /**
23
- * Checks if running in production mode.
24
- * Extracted to helper function for consistency.
25
- *
26
- * @returns True if running in production mode
27
- */
28
- export function isProduction() {
29
- return typeof process !== "undefined" && process.env?.NODE_ENV === "production";
30
- }
31
- // ============================================================================
32
- // Type Guards
33
- // ============================================================================
34
- /**
35
- * Type guard for CSS objects.
36
- *
37
- * @param value - Value to check
38
- * @returns True if value is a CSS object
39
- */
40
- export function isCSSObject(value) {
41
- return typeof value === "object" && value !== null;
42
- }
43
- /**
44
- * Checks if a value is a styled component reference.
45
- * Consolidated function used across the codebase.
46
- *
47
- * @param value - Value to check
48
- * @returns True if value is a styled component reference
49
- */
50
- export function isStyledComponentRef(value) {
51
- return (typeof value === "object" &&
52
- value !== null &&
53
- "__isStoopStyled" in value &&
54
- "__stoopClassName" in value &&
55
- value.__isStoopStyled === true);
56
- }
57
- /**
58
- * Type guard for valid CSS objects (excludes styled component references).
59
- *
60
- * @param value - Value to check
61
- * @returns True if value is a valid CSS object
62
- */
63
- export function isValidCSSObject(value) {
64
- return isCSSObject(value) && !isStyledComponentRef(value);
65
- }
66
- /**
67
- * Type guard for theme objects.
68
- *
69
- * @param value - Value to check
70
- * @returns True if value is a theme object
71
- */
72
- export function isThemeObject(value) {
73
- return typeof value === "object" && value !== null && !Array.isArray(value);
74
- }
75
- // ============================================================================
76
- // Theme Validation
77
- // ============================================================================
78
- /**
79
- * Validates that a theme object only contains approved scales.
80
- * In production, skips all validation for performance.
81
- *
82
- * @param theme - Theme object to validate
83
- * @returns Validated theme as DefaultTheme
84
- * @throws Error if theme contains invalid scales (development only)
85
- */
86
- export function validateTheme(theme) {
87
- if (!theme || typeof theme !== "object" || Array.isArray(theme)) {
88
- throw new Error("[Stoop] Theme must be a non-null object");
89
- }
90
- // Skip all validation in production for performance
91
- if (isProduction()) {
92
- return theme;
93
- }
94
- const themeObj = theme;
95
- const invalidScales = [];
96
- for (const key in themeObj) {
97
- if (key === "media") {
98
- continue;
99
- }
100
- if (!APPROVED_THEME_SCALES.includes(key)) {
101
- invalidScales.push(key);
102
- }
103
- }
104
- if (invalidScales.length > 0) {
105
- const errorMessage = `[Stoop] Theme contains invalid scales: ${invalidScales.join(", ")}. ` +
106
- `Only these scales are allowed: ${APPROVED_THEME_SCALES.join(", ")}`;
107
- throw new Error(errorMessage);
108
- }
109
- return theme;
110
- }
111
- // ============================================================================
112
- // Utility Function Application
113
- // ============================================================================
114
- /**
115
- * Applies utility functions to transform shorthand properties into CSS.
116
- *
117
- * @param styles - CSS object to process
118
- * @param utils - Optional utility functions object
119
- * @returns CSS object with utilities applied
120
- */
121
- export function applyUtilities(styles, utils) {
122
- if (!utils || !styles || typeof styles !== "object") {
123
- return styles;
124
- }
125
- const result = {};
126
- const utilityKeys = Object.keys(utils);
127
- for (const key in styles) {
128
- const value = styles[key];
129
- if (utilityKeys.includes(key) && utils[key]) {
130
- try {
131
- const utilityResult = utils[key](value);
132
- if (utilityResult && typeof utilityResult === "object") {
133
- for (const utilKey in utilityResult) {
134
- result[utilKey] = utilityResult[utilKey];
135
- }
136
- }
137
- }
138
- catch {
139
- result[key] = value;
140
- }
141
- }
142
- else if (isCSSObject(value)) {
143
- result[key] = applyUtilities(value, utils);
144
- }
145
- else {
146
- result[key] = value;
147
- }
148
- }
149
- return result;
150
- }
@@ -1,396 +0,0 @@
1
- /**
2
- * Storage and theme detection utilities.
3
- * Provides simplified localStorage and cookie management, plus automatic theme selection.
4
- * Supports SSR compatibility and error handling.
5
- */
6
- import { DEFAULT_COOKIE_MAX_AGE, DEFAULT_COOKIE_PATH } from "../constants";
7
- import { isBrowser } from "./helpers";
8
- // ============================================================================
9
- // Storage Utilities
10
- // ============================================================================
11
- /**
12
- * Parses a JSON value safely.
13
- *
14
- * @param value - String value to parse
15
- * @param fallback - Fallback value if parsing fails
16
- * @returns Parsed value or fallback
17
- */
18
- function safeJsonParse(value, fallback) {
19
- try {
20
- return JSON.parse(value);
21
- }
22
- catch {
23
- return fallback;
24
- }
25
- }
26
- /**
27
- * Safely gets a value from localStorage.
28
- *
29
- * @param key - Storage key
30
- * @returns Storage result
31
- */
32
- export function getFromStorage(key) {
33
- if (!isBrowser()) {
34
- return { error: "Not in browser environment", success: false, value: null };
35
- }
36
- try {
37
- const value = localStorage.getItem(key);
38
- return { source: "localStorage", success: true, value };
39
- }
40
- catch (error) {
41
- return {
42
- error: error instanceof Error ? error.message : "localStorage access failed",
43
- success: false,
44
- value: null,
45
- };
46
- }
47
- }
48
- /**
49
- * Safely sets a value in localStorage.
50
- *
51
- * @param key - Storage key
52
- * @param value - Value to store
53
- * @returns Storage result
54
- */
55
- export function setInStorage(key, value) {
56
- if (!isBrowser()) {
57
- return { error: "Not in browser environment", success: false, value: undefined };
58
- }
59
- try {
60
- localStorage.setItem(key, value);
61
- return { success: true, value: undefined };
62
- }
63
- catch (error) {
64
- return {
65
- error: error instanceof Error ? error.message : "localStorage write failed",
66
- success: false,
67
- value: undefined,
68
- };
69
- }
70
- }
71
- /**
72
- * Safely removes a value from localStorage.
73
- *
74
- * @param key - Storage key
75
- * @returns Storage result
76
- */
77
- export function removeFromStorage(key) {
78
- if (!isBrowser()) {
79
- return { error: "Not in browser environment", success: false, value: undefined };
80
- }
81
- try {
82
- localStorage.removeItem(key);
83
- return { success: true, value: undefined };
84
- }
85
- catch (error) {
86
- return {
87
- error: error instanceof Error ? error.message : "localStorage remove failed",
88
- success: false,
89
- value: undefined,
90
- };
91
- }
92
- }
93
- /**
94
- * Gets a cookie value.
95
- *
96
- * @param name - Cookie name
97
- * @returns Cookie value or null if not found
98
- */
99
- export function getCookie(name) {
100
- if (!isBrowser())
101
- return null;
102
- const value = `; ${document.cookie}`;
103
- const parts = value.split(`; ${name}=`);
104
- if (parts.length === 2) {
105
- return parts.pop()?.split(";").shift() || null;
106
- }
107
- return null;
108
- }
109
- /**
110
- * Sets a cookie value.
111
- *
112
- * @param name - Cookie name
113
- * @param value - Cookie value
114
- * @param options - Cookie options
115
- * @returns Success status
116
- */
117
- export function setCookie(name, value, options = {}) {
118
- if (!isBrowser())
119
- return false;
120
- const { maxAge = DEFAULT_COOKIE_MAX_AGE, path = DEFAULT_COOKIE_PATH, secure = false } = options;
121
- try {
122
- document.cookie = `${name}=${value}; path=${path}; max-age=${maxAge}${secure ? "; secure" : ""}`;
123
- return true;
124
- }
125
- catch {
126
- return false;
127
- }
128
- }
129
- /**
130
- * Removes a cookie by setting it to expire.
131
- *
132
- * @param name - Cookie name
133
- * @param path - Cookie path
134
- * @returns Success status
135
- */
136
- export function removeCookie(name, path = "/") {
137
- if (!isBrowser())
138
- return false;
139
- try {
140
- document.cookie = `${name}=; path=${path}; max-age=0`;
141
- return true;
142
- }
143
- catch {
144
- return false;
145
- }
146
- }
147
- /**
148
- * Unified storage API that works with both localStorage and cookies.
149
- *
150
- * @param key - Storage key
151
- * @param options - Storage options
152
- * @returns Storage result
153
- */
154
- export function getStorage(key, options = {}) {
155
- const { type = "localStorage" } = options;
156
- if (type === "cookie") {
157
- const value = getCookie(key);
158
- return {
159
- source: "cookie",
160
- success: value !== null,
161
- value,
162
- };
163
- }
164
- return getFromStorage(key);
165
- }
166
- /**
167
- * Unified storage API that works with both localStorage and cookies.
168
- *
169
- * @param key - Storage key
170
- * @param value - Value to store
171
- * @param options - Storage options
172
- * @returns Storage result
173
- */
174
- export function setStorage(key, value, options = {}) {
175
- const { type = "localStorage" } = options;
176
- if (type === "cookie") {
177
- const success = setCookie(key, value, options);
178
- return {
179
- error: success ? undefined : "Cookie write failed",
180
- success,
181
- value: undefined,
182
- };
183
- }
184
- return setInStorage(key, value);
185
- }
186
- /**
187
- * Unified storage API that works with both localStorage and cookies.
188
- *
189
- * @param key - Storage key
190
- * @param options - Storage options
191
- * @returns Storage result
192
- */
193
- export function removeStorage(key, options = {}) {
194
- const { type = "localStorage" } = options;
195
- if (type === "cookie") {
196
- const success = removeCookie(key, options.path);
197
- return {
198
- error: success ? undefined : "Cookie remove failed",
199
- success,
200
- value: undefined,
201
- };
202
- }
203
- return removeFromStorage(key);
204
- }
205
- /**
206
- * Gets a JSON value from storage with automatic parsing.
207
- *
208
- * @param key - Storage key
209
- * @param fallback - Fallback value if parsing fails or key not found
210
- * @param options - Storage options
211
- * @returns Parsed JSON value or fallback
212
- */
213
- export function getJsonFromStorage(key, fallback, options = {}) {
214
- const result = getStorage(key, options);
215
- if (!result.success || result.value === null) {
216
- return { ...result, value: fallback };
217
- }
218
- const parsed = safeJsonParse(result.value, fallback);
219
- return { ...result, value: parsed };
220
- }
221
- /**
222
- * Sets a JSON value in storage with automatic serialization.
223
- *
224
- * @param key - Storage key
225
- * @param value - Value to serialize and store
226
- * @param options - Storage options
227
- * @returns Storage result
228
- */
229
- export function setJsonInStorage(key, value, options = {}) {
230
- try {
231
- const serialized = JSON.stringify(value);
232
- return setStorage(key, serialized, options);
233
- }
234
- catch (error) {
235
- return {
236
- error: error instanceof Error ? error.message : "JSON serialization failed",
237
- success: false,
238
- value: undefined,
239
- };
240
- }
241
- }
242
- /**
243
- * Creates a typed storage interface for a specific key.
244
- *
245
- * @param key - Storage key
246
- * @param options - Storage options
247
- * @returns Typed storage interface
248
- */
249
- export function createStorage(key, options = {}) {
250
- return {
251
- get: () => getStorage(key, options),
252
- getJson: (fallback) => getJsonFromStorage(key, fallback, options),
253
- remove: () => removeStorage(key, options),
254
- set: (value) => setStorage(key, value, options),
255
- setJson: (value) => setJsonInStorage(key, value, options),
256
- };
257
- }
258
- // ============================================================================
259
- // Theme Detection Utilities
260
- // ============================================================================
261
- /**
262
- * Gets localStorage value safely (internal helper for theme detection).
263
- * Uses getFromStorage for consistency.
264
- *
265
- * @param key - Storage key
266
- * @returns Stored value or null if not found or access failed
267
- */
268
- function getLocalStorage(key) {
269
- const result = getFromStorage(key);
270
- return result.success ? result.value : null;
271
- }
272
- /**
273
- * Detects system color scheme preference.
274
- *
275
- * @returns 'dark' or 'light' based on system preference
276
- */
277
- function getSystemPreference() {
278
- if (!isBrowser())
279
- return null;
280
- try {
281
- return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
282
- }
283
- catch {
284
- return null;
285
- }
286
- }
287
- /**
288
- * Validates if a theme name exists in available themes.
289
- *
290
- * @param themeName - Theme name to validate
291
- * @param themes - Available themes map
292
- * @returns True if theme is valid
293
- */
294
- function isValidTheme(themeName, themes) {
295
- return !themes || !!themes[themeName];
296
- }
297
- /**
298
- * Detects the best theme to use based on multiple sources with priority.
299
- *
300
- * Priority order (highest to lowest):
301
- * 1. Explicit localStorage preference
302
- * 2. Cookie preference (for SSR compatibility)
303
- * 3. System color scheme preference
304
- * 4. Default theme
305
- *
306
- * @param options - Theme detection options
307
- * @returns Theme detection result
308
- */
309
- export function detectTheme(options = {}) {
310
- const { cookie: cookieKey, default: defaultTheme = "light", localStorage: storageKey, systemPreference = true, themes, } = options;
311
- // 1. Check localStorage (highest priority - explicit user choice)
312
- if (storageKey) {
313
- const stored = getLocalStorage(storageKey);
314
- if (stored && isValidTheme(stored, themes)) {
315
- return {
316
- confidence: 0.9, // High confidence - explicit user choice
317
- source: "localStorage",
318
- theme: stored,
319
- };
320
- }
321
- }
322
- // 2. Check cookie (for SSR compatibility)
323
- if (cookieKey) {
324
- const cookieValue = getCookie(cookieKey);
325
- if (cookieValue && isValidTheme(cookieValue, themes)) {
326
- return {
327
- confidence: 0.8, // High confidence - persisted preference
328
- source: "cookie",
329
- theme: cookieValue,
330
- };
331
- }
332
- }
333
- // 3. Check system preference
334
- if (systemPreference) {
335
- const system = getSystemPreference();
336
- if (system && isValidTheme(system, themes)) {
337
- return {
338
- confidence: 0.6, // Medium confidence - system default
339
- source: "system",
340
- theme: system,
341
- };
342
- }
343
- }
344
- // 4. Fall back to default
345
- return {
346
- confidence: 0.3, // Low confidence - fallback only
347
- source: "default",
348
- theme: defaultTheme,
349
- };
350
- }
351
- /**
352
- * Creates a theme detector function with pre-configured options.
353
- *
354
- * @param options - Theme detection options
355
- * @returns Theme detection function
356
- */
357
- export function createThemeDetector(options) {
358
- return () => detectTheme(options);
359
- }
360
- /**
361
- * Auto-detects theme for SSR contexts (server-side or during hydration).
362
- * Uses only cookie and default sources since localStorage isn't available.
363
- *
364
- * @param options - Theme detection options
365
- * @returns Theme name
366
- */
367
- export function detectThemeForSSR(options = {}) {
368
- const { cookie: cookieKey, default: defaultTheme = "light", themes } = options;
369
- // Only check cookie in SSR context
370
- if (cookieKey) {
371
- const cookieValue = getCookie(cookieKey);
372
- if (cookieValue && isValidTheme(cookieValue, themes)) {
373
- return cookieValue;
374
- }
375
- }
376
- return defaultTheme;
377
- }
378
- /**
379
- * Listens for system theme changes and calls callback when changed.
380
- *
381
- * @param callback - Function to call when system theme changes
382
- * @returns Cleanup function
383
- */
384
- export function onSystemThemeChange(callback) {
385
- if (!isBrowser()) {
386
- return () => { }; // No-op in SSR
387
- }
388
- const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
389
- const handleChange = (e) => {
390
- callback(e.matches ? "dark" : "light");
391
- };
392
- mediaQuery.addEventListener("change", handleChange);
393
- return () => {
394
- mediaQuery.removeEventListener("change", handleChange);
395
- };
396
- }