@codeleap/styles 6.2.3 → 6.8.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 (138) hide show
  1. package/dist/classes/Cacher.d.ts +87 -0
  2. package/dist/classes/Cacher.d.ts.map +1 -0
  3. package/dist/classes/StaleControl.d.ts +65 -0
  4. package/dist/classes/StaleControl.d.ts.map +1 -0
  5. package/dist/classes/StyleCache.d.ts +63 -0
  6. package/dist/classes/StyleCache.d.ts.map +1 -0
  7. package/dist/classes/StylePersistor.d.ts +52 -0
  8. package/dist/classes/StylePersistor.d.ts.map +1 -0
  9. package/dist/classes/StyleRegistry.d.ts +108 -0
  10. package/dist/classes/StyleRegistry.d.ts.map +1 -0
  11. package/dist/classes/index.d.ts +3 -0
  12. package/dist/classes/index.d.ts.map +1 -0
  13. package/dist/constants.d.ts +22 -0
  14. package/dist/constants.d.ts.map +1 -0
  15. package/dist/hooks/index.d.ts +5 -0
  16. package/dist/hooks/index.d.ts.map +1 -0
  17. package/dist/hooks/useCompositionStyles.d.ts +12 -0
  18. package/dist/hooks/useCompositionStyles.d.ts.map +1 -0
  19. package/dist/hooks/useNestedStylesByKey.d.ts +11 -0
  20. package/dist/hooks/useNestedStylesByKey.d.ts.map +1 -0
  21. package/dist/hooks/useStyleObserver.d.ts +9 -0
  22. package/dist/hooks/useStyleObserver.d.ts.map +1 -0
  23. package/dist/hooks/useTheme.d.ts +19 -0
  24. package/dist/hooks/useTheme.d.ts.map +1 -0
  25. package/dist/index.d.ts +12 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/lib/calc.d.ts +27 -0
  28. package/dist/lib/calc.d.ts.map +1 -0
  29. package/dist/lib/createStyles.d.ts +30 -0
  30. package/dist/lib/createStyles.d.ts.map +1 -0
  31. package/dist/lib/createTheme.d.ts +28 -0
  32. package/dist/lib/createTheme.d.ts.map +1 -0
  33. package/dist/lib/cssVariables.d.ts +35 -0
  34. package/dist/lib/cssVariables.d.ts.map +1 -0
  35. package/dist/lib/index.d.ts +5 -0
  36. package/dist/lib/index.d.ts.map +1 -0
  37. package/dist/theme/generateColorScheme.d.ts +22 -0
  38. package/dist/theme/generateColorScheme.d.ts.map +1 -0
  39. package/dist/theme/index.d.ts +4 -0
  40. package/dist/theme/index.d.ts.map +1 -0
  41. package/dist/theme/themeStore.d.ts +106 -0
  42. package/dist/theme/themeStore.d.ts.map +1 -0
  43. package/dist/theme/validateTheme.d.ts +19 -0
  44. package/dist/theme/validateTheme.d.ts.map +1 -0
  45. package/dist/tools/colors.d.ts +70 -0
  46. package/dist/tools/colors.d.ts.map +1 -0
  47. package/dist/tools/deepClone.d.ts +7 -0
  48. package/dist/tools/deepClone.d.ts.map +1 -0
  49. package/dist/tools/deepmerge.d.ts +13 -0
  50. package/dist/tools/deepmerge.d.ts.map +1 -0
  51. package/dist/tools/hashKey.d.ts +8 -0
  52. package/dist/tools/hashKey.d.ts.map +1 -0
  53. package/dist/tools/index.d.ts +7 -0
  54. package/dist/tools/index.d.ts.map +1 -0
  55. package/dist/tools/minifier.d.ts +24 -0
  56. package/dist/tools/minifier.d.ts.map +1 -0
  57. package/dist/tools/multiplierProperty.d.ts +4 -0
  58. package/dist/tools/multiplierProperty.d.ts.map +1 -0
  59. package/dist/types/cache.d.ts +12 -0
  60. package/dist/types/cache.d.ts.map +1 -0
  61. package/dist/types/component.d.ts +58 -0
  62. package/dist/types/component.d.ts.map +1 -0
  63. package/dist/types/core.d.ts +77 -0
  64. package/dist/types/core.d.ts.map +1 -0
  65. package/dist/types/icon.d.ts +15 -0
  66. package/dist/types/icon.d.ts.map +1 -0
  67. package/dist/types/index.d.ts +6 -0
  68. package/dist/types/index.d.ts.map +1 -0
  69. package/dist/types/spacing.d.ts +28 -0
  70. package/dist/types/spacing.d.ts.map +1 -0
  71. package/dist/types/store.d.ts +12 -0
  72. package/dist/types/store.d.ts.map +1 -0
  73. package/dist/types/style.d.ts +42 -0
  74. package/dist/types/style.d.ts.map +1 -0
  75. package/dist/types/theme.d.ts +109 -0
  76. package/dist/types/theme.d.ts.map +1 -0
  77. package/dist/utils.d.ts +40 -0
  78. package/dist/utils.d.ts.map +1 -0
  79. package/dist/variants/borderCreator.d.ts +22 -0
  80. package/dist/variants/borderCreator.d.ts.map +1 -0
  81. package/dist/variants/createAppVariants.d.ts +18 -0
  82. package/dist/variants/createAppVariants.d.ts.map +1 -0
  83. package/dist/variants/defaultVariants.d.ts +140 -0
  84. package/dist/variants/defaultVariants.d.ts.map +1 -0
  85. package/dist/variants/dynamicVariants.d.ts +43 -0
  86. package/dist/variants/dynamicVariants.d.ts.map +1 -0
  87. package/dist/variants/index.d.ts +7 -0
  88. package/dist/variants/index.d.ts.map +1 -0
  89. package/dist/variants/mediaQuery.d.ts +30 -0
  90. package/dist/variants/mediaQuery.d.ts.map +1 -0
  91. package/dist/variants/spacing.d.ts +26 -0
  92. package/dist/variants/spacing.d.ts.map +1 -0
  93. package/package.json +19 -5
  94. package/src/classes/Cacher.ts +9 -9
  95. package/src/classes/StaleControl.ts +1 -1
  96. package/src/classes/StyleCache.ts +9 -3
  97. package/src/classes/StylePersistor.ts +11 -0
  98. package/src/classes/StyleRegistry.ts +124 -43
  99. package/src/classes/tests/StyleRegistry.spec.ts +169 -0
  100. package/src/constants.ts +14 -0
  101. package/src/hooks/useCompositionStyles.ts +9 -7
  102. package/src/hooks/useNestedStylesByKey.ts +8 -0
  103. package/src/hooks/useStyleObserver.ts +6 -5
  104. package/src/hooks/useTheme.ts +14 -0
  105. package/src/lib/calc.ts +13 -0
  106. package/src/lib/createStyles.ts +35 -4
  107. package/src/lib/createTheme.ts +74 -25
  108. package/src/lib/cssVariables.ts +74 -0
  109. package/src/lib/index.ts +2 -1
  110. package/src/lib/tests/createStyles.spec.ts +80 -23
  111. package/src/lib/tests/createStylesWithContext.spec.ts +108 -0
  112. package/src/tests/theme.ts +6 -2
  113. package/src/theme/generateColorScheme.ts +13 -10
  114. package/src/theme/tests/themeStore.spec.ts +72 -71
  115. package/src/theme/themeStore.ts +10 -7
  116. package/src/theme/validateTheme.ts +1 -1
  117. package/src/tools/colors.ts +24 -36
  118. package/src/tools/deepClone.ts +3 -5
  119. package/src/tools/deepmerge.ts +8 -6
  120. package/src/tools/hashKey.ts +4 -5
  121. package/src/tools/minifier.ts +11 -12
  122. package/src/tools/tests/deepClone.spec.ts +2 -2
  123. package/src/types/cache.ts +10 -0
  124. package/src/types/component.ts +41 -5
  125. package/src/types/core.ts +66 -6
  126. package/src/types/icon.ts +11 -0
  127. package/src/types/spacing.ts +21 -0
  128. package/src/types/store.ts +6 -0
  129. package/src/types/style.ts +37 -10
  130. package/src/types/theme.ts +37 -1
  131. package/src/utils.ts +34 -4
  132. package/src/variants/borderCreator.ts +14 -5
  133. package/src/variants/createAppVariants.ts +11 -0
  134. package/src/variants/defaultVariants.ts +28 -8
  135. package/src/variants/dynamicVariants.ts +76 -18
  136. package/src/variants/mediaQuery.ts +18 -0
  137. package/src/variants/spacing.ts +15 -1
  138. package/package.json.bak +0 -30
@@ -2,6 +2,14 @@ import { useMemo } from 'react'
2
2
  import { ICSS } from '../types'
3
3
  import { getNestedStylesByKey } from '../utils'
4
4
 
5
+ /**
6
+ * Memoised wrapper around `getNestedStylesByKey`. Returns the sub-record of
7
+ * `componentStyles` whose keys start with `match`, with the prefix stripped and the
8
+ * remainder lowercased. Re-computes only when `componentStyles` changes by reference.
9
+ *
10
+ * Use instead of `useCompositionStyles` when you need the slice for a single element
11
+ * and don't want the overhead of iterating over an array.
12
+ */
5
13
  export function useNestedStylesByKey<T extends string>(match: string, componentStyles: Partial<Record<T, ICSS>>) {
6
14
  const styles = {
7
15
  ...componentStyles
@@ -1,12 +1,13 @@
1
1
  import { useMemo } from 'react'
2
2
 
3
3
  /**
4
- * Hook that observes style changes by creating a memoized string representation.
5
- *
6
- * @param {any} style - Style value to observe (array, object, or primitive)
7
- * @returns {string} Serialized string representation of the style for comparison
4
+ * Produces a stable string that changes only when the style value changes, suitable
5
+ * for use as a `useEffect`/`useMemo` dependency when the style prop is an object or
6
+ * array (which would otherwise trigger on every render due to referential inequality).
7
+ * Falsy entries are stripped from arrays before serialisation, so `[null, style]` and
8
+ * `[style]` yield the same string.
8
9
  */
9
- export const useStyleObserver = (style) => {
10
+ export const useStyleObserver = (style: any) => {
10
11
  return useMemo(() => {
11
12
  if (Array.isArray(style)) {
12
13
  return JSON.stringify(style?.filter(v => !!v))
@@ -3,6 +3,20 @@ import { useStore } from '@nanostores/react'
3
3
 
4
4
  type ThemeSelector<T> = (state: ThemeState) => T
5
5
 
6
+ /**
7
+ * Subscribes to the global theme state and returns either the full `ThemeState`
8
+ * (when called without arguments) or a derived value (when called with a selector).
9
+ * Uses nanostores' `useStore` under the hood, so re-renders are triggered only when
10
+ * the selected slice changes by reference.
11
+ *
12
+ * @example
13
+ * // Full state
14
+ * const { theme, colorScheme } = useTheme()
15
+ *
16
+ * @example
17
+ * // Derived slice — component only re-renders when colorScheme changes
18
+ * const colorScheme = useTheme(s => s.colorScheme)
19
+ */
6
20
  export const useTheme = <T = ThemeState>(
7
21
  selector?: ThemeSelector<T>
8
22
  ): T => {
package/src/lib/calc.ts CHANGED
@@ -1,5 +1,13 @@
1
1
  type Unit = 'px' | 'vh' | 'dvh' | 'vw' | 'dvw' | '%' | 'lvh' | 'svh'
2
2
 
3
+ /**
4
+ * Chainable builder for CSS `calc()` expressions. Each arithmetic method appends
5
+ * to the expression and returns `this`, so calls can be chained. Call `build()`
6
+ * at the end to get the final `calc(...)` string.
7
+ *
8
+ * @example
9
+ * calc(100, '%').sub(16).build() // → 'calc((100%) - (16px))'
10
+ */
3
11
  class CalcBuilder {
4
12
  private expression: string
5
13
 
@@ -38,4 +46,9 @@ class CalcBuilder {
38
46
  }
39
47
  }
40
48
 
49
+ /**
50
+ * Entry point for building CSS `calc()` expressions. Pass an initial numeric value
51
+ * and optional unit (default `'px'`), then chain `.add()`, `.sub()`, `.mult()`, `.div()`,
52
+ * and finally call `.build()` to get the complete string.
53
+ */
41
54
  export const calc = (base: number, unit: Unit = 'px') => new CalcBuilder(base, unit)
@@ -1,14 +1,22 @@
1
- import { AnyRecord, ICSS, ITheme } from '../types'
1
+ import { AnyRecord, ComponentContext, ICSS, ITheme } from '../types'
2
2
  import { themeStore } from '../theme'
3
3
 
4
4
  type Value = AnyRecord
5
5
 
6
6
  type StylesShape<K extends string, V extends Value> = Partial<Record<K, ICSS & Partial<Omit<V, keyof ICSS>>>>
7
7
 
8
+ /**
9
+ * Symbol used to attach a context-aware factory function to a styles proxy.
10
+ * `CodeleapStyleRegistry.registerVariants` reads this symbol from each entry to
11
+ * identify which variants require a `ComponentContext` at resolution time.
12
+ * Not intended for direct use by consumers.
13
+ */
14
+ export const CONTEXT_FACTORY_SYMBOL = Symbol('contextFactory')
15
+
8
16
  /**
9
17
  * Creates a reactive styles object that automatically updates when theme changes.
10
18
  * Uses a proxy to re-compute styles on each access, ensuring theme changes are reflected.
11
- *
19
+ *
12
20
  * @template K - Style keys (extends string)
13
21
  * @template V - Additional value type (extends AnyRecord)
14
22
  * @param {StylesShape<K, V> | ((theme: ITheme) => StylesShape<K, V>)} styles - Static styles object or function that receives theme
@@ -26,14 +34,37 @@ export function createStyles<K extends string, V extends Value = {}>(
26
34
  return styles
27
35
  }
28
36
  }
29
-
37
+
30
38
  // We use a proxy here so that the color scheme is recomputed every time the
31
39
  // theme changes. This is necessary because the theme is a singleton which does not cause
32
40
  // a re-render when it changes. The end-user will only have to worry about remounting the root component
33
41
  // when the theme changes in order to get the new color scheme due to this proxy.
34
42
  return new Proxy(compute() as StylesShape<K, V>, {
35
43
  get(target, prop) {
36
- return compute()[prop as string]
44
+ return (compute() as Record<string, any>)[prop as string]
45
+ },
46
+ })
47
+ }
48
+
49
+ /**
50
+ * Like `createStyles`, but the factory also receives a `ComponentContext` so that
51
+ * individual style values can vary based on runtime boolean/numeric state (e.g.,
52
+ * `isDisabled`, `isSelected`). The resulting proxy exposes a `CONTEXT_FACTORY_SYMBOL`
53
+ * getter so the registry can invoke the factory with the actual context at render time.
54
+ * Use this instead of `createStyles` when variant styles depend on component state.
55
+ */
56
+ export function createStylesWithContext<K extends string, V extends Value = {}, C extends ComponentContext = {}>(
57
+ styles: (theme: ITheme, context: C) => StylesShape<K, V>,
58
+ ) {
59
+ const compute = (context: C = {} as C) => {
60
+ const current = themeStore.theme
61
+ return !current ? {} as StylesShape<K, V> : styles(current, context)
62
+ }
63
+
64
+ return new Proxy(compute() as StylesShape<K, V>, {
65
+ get(target, prop) {
66
+ if (prop === CONTEXT_FACTORY_SYMBOL) return styles
67
+ return (compute() as Record<string, any>)[prop as string]
37
68
  },
38
69
  })
39
70
  }
@@ -1,16 +1,37 @@
1
- import { AppTheme, Theme } from '../types'
1
+ import { AppTheme, Theme, ITheme } from '../types'
2
2
  import { borderCreator, createMediaQueries, defaultVariants, spacingFactory } from '../variants'
3
3
  import { minifier, multiplierProperty } from '../tools'
4
4
  import { themeStore } from '../theme'
5
+ import { applyColorSchemeToDOM, buildCssVarProxy, flattenColorMap, DOM_COLOR_SCHEME_KEY } from './cssVariables'
5
6
 
6
7
  type ThemePersistor = {
7
8
  get: (name: string) => any
8
9
  set: (name: string, value: any) => void
10
+ getNoCompress?: (name: string) => any
11
+ setNoCompress?: (name: string, value: any) => void
12
+ getSystemColorScheme?: () => string | null
9
13
  }
10
14
 
11
15
  const colorSchemeKey = '@styles.theme.colorScheme'
12
16
  const alternateColorsKey = '@styles.theme.alternateColors'
13
17
 
18
+ /**
19
+ * Builds and registers the runtime `AppTheme<T>` object from a raw `Theme` definition.
20
+ *
21
+ * Key behaviours:
22
+ * - On web (`theme.isBrowser = true`), `theme.colors` is replaced with a CSS-var proxy
23
+ * so all color references become `var(--cl-<token>)` strings. The active scheme's
24
+ * real values are still accessible via `theme.currentSchemeColors`.
25
+ * - Persists the selected color scheme to storage and restores it on next load.
26
+ * Falls back to `getSystemColorScheme()` if no persisted value exists.
27
+ * - `alternateColors` from storage are merged with those in the theme definition
28
+ * (runtime-injected schemes take priority).
29
+ * - The constructed theme is immediately set on `themeStore` so hooks and the registry
30
+ * can read it synchronously during the same render pass.
31
+ *
32
+ * @param theme - Raw theme object (output of `validateTheme` or a compliant literal).
33
+ * @param themePersistor - Storage adapter for persisting color-scheme selection and injected schemes.
34
+ */
14
35
  export const createTheme = <T extends Theme>(theme: T, themePersistor: ThemePersistor): AppTheme<T> => {
15
36
  const {
16
37
  colors,
@@ -35,10 +56,19 @@ export const createTheme = <T extends Theme>(theme: T, themePersistor: ThemePers
35
56
  },
36
57
  set: (key: string, value: any) => {
37
58
  return themePersistor.set(key, !value ? '' : minifier.compress(value))
38
- }
59
+ },
60
+ getNoCompress: (key: string) => {
61
+ if (themePersistor.getNoCompress) return themePersistor.getNoCompress(key)
62
+ if (typeof localStorage !== 'undefined') return localStorage.getItem(key)
63
+ return null
64
+ },
65
+ setNoCompress: (key: string, value: any) => {
66
+ if (themePersistor.setNoCompress) return themePersistor.setNoCompress(key, value)
67
+ if (typeof localStorage !== 'undefined') localStorage.setItem(key, value)
68
+ },
39
69
  }
40
70
 
41
- themeStore.setColorScheme(persistor.get(colorSchemeKey) ?? 'default')
71
+ themeStore.setColorScheme(persistor.get(colorSchemeKey) ?? themePersistor.getSystemColorScheme?.() ?? 'default')
42
72
 
43
73
  const persistedAlternateColors = persistor.get(alternateColorsKey)
44
74
 
@@ -49,6 +79,15 @@ export const createTheme = <T extends Theme>(theme: T, themePersistor: ThemePers
49
79
 
50
80
  themeStore.setAlternateColorsScheme(alternateColors)
51
81
 
82
+ // On web: build CSS var proxy once — theme.colors returns var(--cl-X) strings.
83
+ // On RN (isBrowser falsy): keep raw RGBA values as before.
84
+ const cssVarColors = theme.isBrowser ? buildCssVarProxy(colors) : colors
85
+
86
+ // Apply persisted scheme to DOM immediately (browser-only, no-op on server/RN)
87
+ applyColorSchemeToDOM(persistor.getNoCompress(DOM_COLOR_SCHEME_KEY) ?? 'default')
88
+
89
+ const baseSpacing = theme.baseSpacing ?? 1
90
+
52
91
  const themeObj: AppTheme<T> = {
53
92
  ...otherThemeValues,
54
93
 
@@ -64,18 +103,26 @@ export const createTheme = <T extends Theme>(theme: T, themePersistor: ThemePers
64
103
 
65
104
  breakpoints: breakpoints ?? {},
66
105
 
67
- get colors() {
68
- const colorScheme = themeStore.colorScheme ?? 'default'
69
-
70
- if (colorScheme === 'default') return colors
106
+ // On web: var(--cl-X) strings — browser CSS handles color switching.
107
+ // On RN: raw RGBA values, scheme-reactive as before.
108
+ colors: cssVarColors as T['colors'],
71
109
 
110
+ // Always the active scheme's real RGBA values — use when you need the actual color in JS.
111
+ get currentSchemeColors(): T['colors'] {
112
+ const colorScheme = themeStore.colorScheme ?? 'default'
113
+ if (colorScheme === 'default') return colors as T['colors']
72
114
  const scheme = themeStore.alternateColorsScheme?.[colorScheme]
73
-
74
115
  if (!scheme) {
75
116
  console.warn(`Color scheme ${colorScheme} not found in theme`)
76
117
  }
118
+ return (scheme ?? colors) as T['colors']
119
+ },
77
120
 
78
- return scheme ?? colors
121
+ getCssVariables(schemeName?: string): Record<string, string> {
122
+ const map = !schemeName || schemeName === 'default'
123
+ ? colors
124
+ : themeStore.alternateColorsScheme?.[schemeName] ?? colors
125
+ return flattenColorMap(map as Record<string, any>)
79
126
  },
80
127
 
81
128
  setColorScheme(colorScheme: string) {
@@ -87,8 +134,10 @@ export const createTheme = <T extends Theme>(theme: T, themePersistor: ThemePers
87
134
  }
88
135
 
89
136
  themeStore.setColorScheme(colorScheme)
90
-
91
137
  persistor.set(colorSchemeKey, colorScheme)
138
+ // Store uncompressed for the FOUC prevention script (lz can't run inline)
139
+ persistor.setNoCompress(DOM_COLOR_SCHEME_KEY, colorScheme)
140
+ applyColorSchemeToDOM(colorScheme)
92
141
  },
93
142
 
94
143
  injectColorScheme(name, colorMap) {
@@ -116,24 +165,24 @@ export const createTheme = <T extends Theme>(theme: T, themePersistor: ThemePers
116
165
  persistor.set(alternateColorsKey, persistedAlternateColors)
117
166
  },
118
167
 
119
- baseSpacing: theme.baseSpacing,
168
+ baseSpacing,
120
169
 
121
- value: (n = 1) => theme.baseSpacing * n,
170
+ value: (n = 1) => baseSpacing * n,
122
171
 
123
172
  spacing: {
124
- value: (n = 1) => theme.baseSpacing * n,
125
- gap: multiplierProperty(theme.baseSpacing, 'gap'),
126
- ...spacingFactory(theme.baseSpacing, 'padding'),
127
- ...spacingFactory(theme.baseSpacing, 'margin'),
128
- ...spacingFactory(theme.baseSpacing, 'p', true),
129
- ...spacingFactory(theme.baseSpacing, 'm', true),
173
+ value: (n = 1) => baseSpacing * n,
174
+ gap: multiplierProperty(baseSpacing, 'gap'),
175
+ ...spacingFactory(baseSpacing, 'padding'),
176
+ ...spacingFactory(baseSpacing, 'margin'),
177
+ ...spacingFactory(baseSpacing, 'p', true),
178
+ ...spacingFactory(baseSpacing, 'm', true),
130
179
  },
131
180
 
132
181
  inset: {
133
- top: multiplierProperty(theme.baseSpacing, 'top'),
134
- bottom: multiplierProperty(theme.baseSpacing, 'bottom'),
135
- left: multiplierProperty(theme.baseSpacing, 'left'),
136
- right: multiplierProperty(theme.baseSpacing, 'right'),
182
+ top: multiplierProperty(baseSpacing, 'top'),
183
+ bottom: multiplierProperty(baseSpacing, 'bottom'),
184
+ left: multiplierProperty(baseSpacing, 'left'),
185
+ right: multiplierProperty(baseSpacing, 'right'),
137
186
  },
138
187
 
139
188
  presets: {
@@ -160,16 +209,16 @@ export const createTheme = <T extends Theme>(theme: T, themePersistor: ThemePers
160
209
  values: values ?? {},
161
210
 
162
211
  sized: (size) => {
163
- const value = typeof size == 'number' ? size * theme.baseSpacing : size
212
+ const value = typeof size == 'number' ? size * baseSpacing : size
164
213
 
165
214
  return {
166
215
  width: value,
167
216
  height: value,
168
217
  }
169
218
  },
170
- }
219
+ } as AppTheme<T>
171
220
 
172
- themeStore.setTheme(themeObj)
221
+ themeStore.setTheme(themeObj as unknown as ITheme)
173
222
 
174
223
  return themeObj
175
224
  }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * CSS custom-property prefix used for all codeleap design-token variables.
3
+ * Color tokens are injected as `--cl-<tokenPath>` on `:root` (or a scheme-specific
4
+ * `[data-color-scheme]` selector). Changing this value after tokens are injected
5
+ * will break existing CSS that references `var(--cl-*)`.
6
+ */
7
+ export const CSS_VAR_PREFIX = '--cl-'
8
+
9
+ /**
10
+ * Storage key used to persist the active color scheme name in an uncompressed format
11
+ * (plain string, not LZ-encoded). Stored separately so a lightweight inline script
12
+ * can read it before React hydrates and set `data-color-scheme` on `<html>` to prevent
13
+ * a flash of the wrong color scheme (FOUC).
14
+ */
15
+ export const DOM_COLOR_SCHEME_KEY = '@styles.dom.colorScheme'
16
+
17
+ /**
18
+ * Recursively flattens a nested color token object into a flat map of CSS custom-property
19
+ * names to their values. Nesting levels are joined with `-` (e.g., `{ primary: { solid: '#fff' } }`
20
+ * → `{ '--cl-primary-solid': '#fff' }`). Non-string leaf values are silently skipped.
21
+ */
22
+ export function flattenColorMap(
23
+ obj: Record<string, any>,
24
+ path = '',
25
+ prefix = CSS_VAR_PREFIX,
26
+ ): Record<string, string> {
27
+ const result: Record<string, string> = {}
28
+ for (const [key, value] of Object.entries(obj)) {
29
+ const cssPath = path ? `${path}-${key}` : key
30
+ if (typeof value === 'string') {
31
+ result[`${prefix}${cssPath}`] = value
32
+ } else if (value && typeof value === 'object') {
33
+ Object.assign(result, flattenColorMap(value, cssPath, prefix))
34
+ }
35
+ }
36
+ return result
37
+ }
38
+
39
+ /**
40
+ * Recursively replaces every string leaf in `obj` with a `var(--cl-<path>)` reference,
41
+ * preserving the original object shape. Used on web to replace raw color values with
42
+ * CSS variable references so that color-scheme switching via `data-color-scheme` CSS
43
+ * selectors works without JavaScript re-renders.
44
+ */
45
+ export function buildCssVarProxy<T>(obj: T, path = '', prefix = CSS_VAR_PREFIX): T {
46
+ if (typeof obj !== 'object' || obj === null) return obj
47
+ const result: Record<string, any> = {}
48
+ for (const [key, value] of Object.entries(obj as Record<string, any>)) {
49
+ const cssPath = path ? `${path}-${key}` : key
50
+ if (typeof value === 'string') {
51
+ result[key] = `var(${prefix}${cssPath})`
52
+ } else if (value && typeof value === 'object') {
53
+ result[key] = buildCssVarProxy(value, cssPath, prefix)
54
+ } else {
55
+ result[key] = value
56
+ }
57
+ }
58
+ return result as T
59
+ }
60
+
61
+ /**
62
+ * Sets or removes `data-color-scheme` on `document.documentElement` to trigger the
63
+ * matching CSS selector that applies alternate-scheme custom-property overrides.
64
+ * The `'default'` scheme removes the attribute entirely so the `:root` base variables
65
+ * apply. No-ops on non-browser environments (SSR, React Native).
66
+ */
67
+ export function applyColorSchemeToDOM(colorScheme: string) {
68
+ if (typeof document === 'undefined') return
69
+ if (colorScheme === 'default') {
70
+ delete document.documentElement.dataset.colorScheme
71
+ } else {
72
+ document.documentElement.dataset.colorScheme = colorScheme
73
+ }
74
+ }
package/src/lib/index.ts CHANGED
@@ -1,3 +1,4 @@
1
- export { createStyles } from './createStyles'
1
+ export { createStyles, createStylesWithContext, CONTEXT_FACTORY_SYMBOL } from './createStyles'
2
2
  export { createTheme } from './createTheme'
3
3
  export { calc } from './calc'
4
+ export { flattenColorMap, buildCssVarProxy, applyColorSchemeToDOM, CSS_VAR_PREFIX, DOM_COLOR_SCHEME_KEY } from './cssVariables'
@@ -3,11 +3,11 @@ import { createStyles } from '../createStyles'
3
3
  import { themeStore } from '../../theme'
4
4
  import { mockTheme, MockedTheme } from '../../tests/theme'
5
5
 
6
- describe('createStyles', () => {
7
- let currentTheme: MockedTheme = null
6
+ describe('createStyles (web — isBrowser: true)', () => {
7
+ let currentTheme: MockedTheme = null as any
8
8
 
9
9
  beforeEach(() => {
10
- mockTheme()
10
+ mockTheme({ isBrowser: true })
11
11
 
12
12
  currentTheme = themeStore.theme as unknown as MockedTheme
13
13
  })
@@ -30,10 +30,10 @@ describe('createStyles', () => {
30
30
  })
31
31
 
32
32
  it('should compute styles from function when theme is available', () => {
33
- const functionStyles = (theme: MockedTheme) => ({
33
+ const functionStyles = (theme: any) => ({
34
34
  button: {
35
- color: theme.colors.neutralSo,
36
- backgroundColor: theme.colors.secondary
35
+ color: theme.colors.neutralSolid500,
36
+ backgroundColor: theme.colors.buttonRegularPrimaryBgDefault,
37
37
  },
38
38
  container: {
39
39
  padding: theme.spacing.value(2)
@@ -42,33 +42,34 @@ describe('createStyles', () => {
42
42
 
43
43
  const styles = createStyles(functionStyles)
44
44
 
45
+ // On web (isBrowser: true), theme.colors returns CSS var references
45
46
  expect(styles.button).toEqual({
46
- color: currentTheme.colors.neutralSo,
47
- backgroundColor: currentTheme.colors.secondary
47
+ color: 'var(--cl-neutralSolid500)',
48
+ backgroundColor: 'var(--cl-buttonRegularPrimaryBgDefault)',
48
49
  })
49
50
  expect(styles.container).toEqual({
50
51
  padding: 16
51
52
  })
52
53
  })
53
54
 
54
- it('should recompute styles when theme changes (proxy behavior)', () => {
55
- const functionStyles = (theme: MockedTheme) => ({
55
+ it('should return CSS var references regardless of active color scheme (proxy behavior)', () => {
56
+ const functionStyles = (theme: any) => ({
56
57
  button: { color: theme.colors.buttonRegularPrimaryBgDefault }
57
58
  })
58
59
 
59
60
  const styles = createStyles(functionStyles)
60
61
 
61
- // Initial theme
62
- expect(styles.button).toEqual({ color: currentTheme.colors.primarySolid800 })
62
+ // CSS var reference is static — same string before and after scheme change.
63
+ // The browser CSS cascade (via data-color-scheme attribute) handles the actual color switch.
64
+ expect(styles.button).toEqual({ color: 'var(--cl-buttonRegularPrimaryBgDefault)' })
63
65
 
64
66
  currentTheme.setColorScheme('dark')
65
67
 
66
- // Should reflect new theme due to proxy
67
- expect(styles.button).toEqual({ color: currentTheme.colors.primarySolid500 })
68
+ expect(styles.button).toEqual({ color: 'var(--cl-buttonRegularPrimaryBgDefault)' })
68
69
  })
69
70
 
70
71
  it('should handle complex theme-based styles', () => {
71
- const functionStyles = (theme: MockedTheme) => ({
72
+ const functionStyles = (theme: any) => ({
72
73
  button: {
73
74
  color: theme.colors.redSolid100,
74
75
  border: `1px solid ${theme.colors.neutralSolid1000}`,
@@ -84,14 +85,14 @@ describe('createStyles', () => {
84
85
  const styles = createStyles(functionStyles)
85
86
 
86
87
  expect(styles.button).toEqual({
87
- color: currentTheme.colors.redSolid100,
88
- border: `1px solid ${currentTheme.colors.neutralSolid1000}`,
88
+ color: 'var(--cl-redSolid100)',
89
+ border: '1px solid var(--cl-neutralSolid1000)',
89
90
  '@media (max-width: 768px)': {
90
91
  fontSize: '14px'
91
92
  }
92
93
  })
93
94
  expect(styles.alert).toEqual({
94
- backgroundColor: currentTheme.colors.redSolid600
95
+ backgroundColor: 'var(--cl-redSolid600)',
95
96
  })
96
97
  })
97
98
 
@@ -106,12 +107,10 @@ describe('createStyles', () => {
106
107
  })
107
108
 
108
109
  it('should work with mixed ICSS and custom properties', () => {
109
- const functionStyles = (theme: MockedTheme) => ({
110
+ const functionStyles = (theme: any) => ({
110
111
  component: {
111
- // ICSS properties
112
112
  color: theme.colors.blueSolid900,
113
113
  padding: '10px',
114
- // Custom properties (merged with ICSS)
115
114
  customProp: 'custom-value',
116
115
  dataAttribute: 'test'
117
116
  }
@@ -120,7 +119,7 @@ describe('createStyles', () => {
120
119
  const styles = createStyles(functionStyles)
121
120
 
122
121
  expect(styles.component).toEqual({
123
- color: currentTheme.colors.blueSolid900,
122
+ color: 'var(--cl-blueSolid900)',
124
123
  padding: '10px',
125
124
  customProp: 'custom-value',
126
125
  dataAttribute: 'test'
@@ -130,7 +129,7 @@ describe('createStyles', () => {
130
129
  it('should maintain proxy behavior across multiple accesses', () => {
131
130
  let computeCount = 0
132
131
 
133
- const functionStyles = (theme: MockedTheme) => {
132
+ const functionStyles = (theme: any) => {
134
133
  computeCount++
135
134
  return {
136
135
  button: { color: theme.colors.blueSolid100 }
@@ -149,3 +148,61 @@ describe('createStyles', () => {
149
148
  expect(computeCount).toBe(3)
150
149
  })
151
150
  })
151
+
152
+ describe('createStyles (RN — isBrowser: false)', () => {
153
+ let currentTheme: MockedTheme = null as any
154
+
155
+ beforeEach(() => {
156
+ mockTheme({ isBrowser: false })
157
+ currentTheme = themeStore.theme as unknown as MockedTheme
158
+ })
159
+
160
+ it('should compute styles from function with raw RGBA values', () => {
161
+ const functionStyles = (theme: any) => ({
162
+ button: {
163
+ color: theme.colors.neutralSolid500,
164
+ backgroundColor: theme.colors.buttonRegularPrimaryBgDefault,
165
+ },
166
+ })
167
+
168
+ const styles = createStyles(functionStyles)
169
+
170
+ expect(styles.button).toEqual({
171
+ color: 'rgba(136, 136, 136, 1.00)',
172
+ backgroundColor: 'rgba(43, 105, 122, 1.00)',
173
+ })
174
+ })
175
+
176
+ it('should return raw RGBA for active scheme after setColorScheme', () => {
177
+ const functionStyles = (theme: any) => ({
178
+ button: { color: theme.colors.buttonRegularPrimaryBgDefault },
179
+ })
180
+
181
+ const styles = createStyles(functionStyles)
182
+
183
+ // On RN, colors is raw — before scheme change it's the light value
184
+ expect(styles.button).toEqual({ color: 'rgba(43, 105, 122, 1.00)' })
185
+
186
+ // After switching, same key in the proxy still returns the light value
187
+ // because theme.colors is the static raw colors object (not scheme-reactive on RN).
188
+ // Use theme.currentSchemeColors for the active scheme's values.
189
+ currentTheme.setColorScheme('dark')
190
+
191
+ expect(styles.button).toEqual({ color: 'rgba(43, 105, 122, 1.00)' })
192
+ })
193
+
194
+ it('currentSchemeColors returns the active scheme raw values', () => {
195
+ const styles = createStyles((theme: any) => ({
196
+ button: { color: theme.currentSchemeColors.buttonRegularPrimaryBgDefault },
197
+ }))
198
+
199
+ // Light mode
200
+ expect(styles.button).toEqual({ color: 'rgba(43, 105, 122, 1.00)' })
201
+
202
+ currentTheme.setColorScheme('dark')
203
+
204
+ // Dark mode colors — dark scheme overrides buttonRegularPrimaryBgDefault
205
+ const darkValue = currentTheme.currentSchemeColors.buttonRegularPrimaryBgDefault as string
206
+ expect(styles.button).toEqual({ color: darkValue })
207
+ })
208
+ })