@codeleap/styles 6.3.0 → 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 +38 -37
  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
@@ -6,7 +6,7 @@
6
6
  * Developed for potential future need of time-based cache invalidation.
7
7
  */
8
8
  export class StaleControl {
9
- private wiperId: Timer | null = null
9
+ private wiperId: ReturnType<typeof setInterval> | null = null
10
10
 
11
11
  /**
12
12
  * @param staleTime - Expiration time in minutes (default: 60 minutes)
@@ -5,12 +5,18 @@ import { CacheType } from '../types/cache'
5
5
  import { StateStorage } from '../types/store'
6
6
 
7
7
  /**
8
- * StyleCache manages multiple cache instances for different style-related data.
9
- * Provides centralized caching functionality with compression and key generation.
8
+ * Aggregates the six typed `Cache` buckets used by the style registry.
9
+ * Three buckets (`styles`, `compositions`, `responsive`) are in-memory only.
10
+ * Three buckets (`variants`, `common`, `components`) are backed by the
11
+ * provided `StateStorage` and survive page reloads until the stale window expires.
12
+ *
13
+ * All keys are derived from a `baseKey` that encodes the active color scheme,
14
+ * theme snapshot, and common variants — so switching color scheme automatically
15
+ * invalidates all cached values without an explicit `wipeCache` call.
10
16
  */
11
17
  export class StyleCache {
12
18
  /** Base key used for cache key generation */
13
- baseKey: string
19
+ baseKey!: string
14
20
 
15
21
  /** Cache for style data */
16
22
  styles = new Cache('styles')
@@ -2,12 +2,23 @@ import { StateStorage } from '../types/store'
2
2
  import { minifier } from '../tools'
3
3
  import { StyleConstants } from '../constants'
4
4
 
5
+ /**
6
+ * Platform-specific storage adapter required by `StylePersistor`.
7
+ * Implementations exist for MMKV (mobile) and localStorage (web).
8
+ * Note the method names (`set`/`get`/`del`) differ from the `StateStorage` interface
9
+ * (`setItem`/`getItem`/`removeItem`) — `StylePersistor` bridges the two.
10
+ */
5
11
  export type StoragePersistor = {
6
12
  set: (key: string, value: any) => void
7
13
  get: (key: string) => any
8
14
  del: (key: string) => void
9
15
  }
10
16
 
17
+ /**
18
+ * Adapts a platform `StoragePersistor` to the `StateStorage` interface expected by
19
+ * `StyleCache`. All values are LZ-compressed on write and decompressed on read,
20
+ * substantially reducing storage footprint for large serialised style caches.
21
+ */
11
22
  export class StylePersistor implements StateStorage {
12
23
  /**
13
24
  * Creates a new StylePersistor instance with compression capabilities
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable dot-notation */
2
- import type { AnyRecord, AnyStyledComponent, ICSS, ITheme, StyleAggregator, StyleProp, VariantStyleSheet } from '../types'
2
+ import type { AnyStyledComponent, ComponentContext, ICSS, ITheme, StyleAggregator, StyleProp, VariantStyleSheet } from '../types'
3
3
  import type { StateStorage } from '../types/store'
4
4
  import type { MultiplierFunction } from '../variants'
5
5
  import { themeStore } from '../theme'
@@ -7,7 +7,22 @@ import { defaultVariants, dynamicVariants } from '../variants'
7
7
  import { capitalize, ignoredStyleKeys, isSpacingKey } from '../utils'
8
8
  import { StyleCache } from './StyleCache'
9
9
  import { minifier, deepClone, deepmerge } from '../tools'
10
-
10
+ import { CONTEXT_FACTORY_SYMBOL } from '../lib/createStyles'
11
+
12
+ /**
13
+ * Central style resolution engine for the `@codeleap/styles` system.
14
+ *
15
+ * Responsibilities:
16
+ * - Stores compressed variant stylesheets per component (`stylesheets`).
17
+ * - Maintains the merged common-variant map (`defaultVariants + appVariants + dynamicVariants + spacing`).
18
+ * - Resolves `StyleProp` values into flat `ICSS` objects for each composition element.
19
+ * - Caches every resolution step in the six `StyleCache` buckets for performance.
20
+ * - Provides `createStyle` as an abstract hook for platform-specific style flattening
21
+ * (web passes through; RN uses `StyleSheet.flatten`).
22
+ *
23
+ * Instantiated once per platform registry (not per component). Platform packages
24
+ * subclass or configure this to provide the appropriate `createStyle` implementation.
25
+ */
11
26
  export class CodeleapStyleRegistry {
12
27
  stylesheets: Record<string, VariantStyleSheet> = {}
13
28
 
@@ -17,6 +32,9 @@ export class CodeleapStyleRegistry {
17
32
 
18
33
  components: Record<string, AnyStyledComponent> = {}
19
34
 
35
+ /** Per-component map of variant names to context-aware factory functions. Populated during `registerVariants`. */
36
+ variantFactories: Record<string, Record<string, (theme: ITheme, context: ComponentContext) => any>> = {}
37
+
20
38
  private styleCache: StyleCache
21
39
 
22
40
  constructor(storage: StateStorage) {
@@ -24,42 +42,47 @@ export class CodeleapStyleRegistry {
24
42
 
25
43
  this.registerCommonVariants()
26
44
 
27
- const currentColorScheme = themeStore.theme?.['currentColorScheme']?.() ?? themeStore.colorScheme ?? 'default'
45
+ const currentColorScheme = themeStore.themeTyped?.currentColorScheme?.() ?? themeStore.colorScheme ?? 'default'
28
46
 
29
47
  this.styleCache.registerBaseKey([currentColorScheme, themeStore.theme, this.commonVariants])
30
48
  }
31
49
 
32
- computeCommonVariantStyle(componentName: string, variant: string, component = null) {
50
+ /**
51
+ * Resolves a single common-variant string (e.g., `"flex"`, `"padding:2"`, `"xl:flex"`)
52
+ * into a composition-keyed style object. Handles breakpoint-prefixed variants by wrapping
53
+ * the result in a `@media` key. Returns `null` if the variant is not found in `commonVariants`.
54
+ */
55
+ computeCommonVariantStyle(componentName: string, variant: string, component: string | undefined = undefined) {
33
56
  const cache = this.styleCache.keyFor('common', variant)
34
57
 
35
58
  if (!!cache.value) {
36
59
  return {
37
- [component]: this.createStyle(cache.value),
60
+ [component as string]: this.createStyle(cache.value),
38
61
  }
39
62
  }
40
63
 
41
64
  const theme = themeStore.theme
42
65
 
43
- let mediaQuery = null
66
+ let mediaQuery: string | null = null
44
67
 
45
- let [variantName, value] = variant?.includes(':') ? variant?.split(':') : [variant, null]
68
+ let [variantName, value] = variant?.includes(':') ? variant?.split(':') : [variant, null as string | null]
46
69
 
47
- // @ts-expect-error
70
+ // @ts-ignore the not yet augmented/defined
48
71
  if (!!theme?.breakpoints[variantName]) {
49
72
  const [breakpoint, _variantName, _value] = variant?.split(':')?.length == 2 ? [...variant?.split(':'), null] : variant?.split(':')
50
73
 
51
74
  // @ts-expect-error
52
75
  mediaQuery = theme.media.down(breakpoint)
53
- value = _value
54
- variantName = _variantName
76
+ value = _value as string | null
77
+ variantName = _variantName as string
55
78
  }
56
79
 
57
80
  const variantStyle = this.commonVariants[variantName] ?? this.commonVariants[variant]
58
81
 
59
- let style = null
82
+ let style: any = null
60
83
 
61
84
  if (typeof variantStyle == 'function') {
62
- style = isSpacingKey(variantName) ? variantStyle(value) : variantStyle(theme, value)
85
+ style = isSpacingKey(variantName) ? variantStyle(value as any) : (variantStyle as any)(theme, value)
63
86
  } else {
64
87
  style = variantStyle
65
88
  }
@@ -73,7 +96,7 @@ export class CodeleapStyleRegistry {
73
96
  }
74
97
 
75
98
  const commonStyles = {
76
- [component]: this.createStyle(style),
99
+ [component as string]: this.createStyle(style),
77
100
  }
78
101
 
79
102
  this.styleCache.cacheFor('common', cache.key, style)
@@ -81,15 +104,23 @@ export class CodeleapStyleRegistry {
81
104
  return commonStyles
82
105
  }
83
106
 
84
- computeVariantStyle(componentName: string, variants: string[], _component = null): ICSS {
107
+ /**
108
+ * Resolves an array of variant strings for a component into a single merged `ICSS`.
109
+ * Checks component-level variant stylesheet first, then falls back to common variants.
110
+ * Context-aware factories (registered via `CONTEXT_FACTORY_SYMBOL`) are called with
111
+ * the current theme and the provided `context` object.
112
+ */
113
+ computeVariantStyle(componentName: string, variants: string[], _component: string | undefined = undefined, context?: ComponentContext): ICSS {
85
114
  const { rootElement } = this.getRegisteredComponent(componentName)
86
115
 
87
116
  const component = _component ?? rootElement
88
117
 
89
- const stylesheet = minifier.decompress(this.stylesheets[componentName])
118
+ const stylesheet = minifier.decompress(this.stylesheets[componentName]) ?? {}
119
+
120
+
90
121
  const aggregator = this.aggregators[componentName]
91
122
 
92
- const cache = this.styleCache.keyFor('variants', { componentName, component, stylesheet, variants, aggregator })
123
+ const cache = this.styleCache.keyFor('variants', { componentName, component, stylesheet, variants, aggregator, context })
93
124
 
94
125
  if (!!cache.value) {
95
126
  return cache.value
@@ -98,6 +129,12 @@ export class CodeleapStyleRegistry {
98
129
  const theme = themeStore.theme
99
130
 
100
131
  const variantStyles = variants.map((variant) => {
132
+ const factory = context && theme && this.variantFactories?.[componentName]?.[variant]
133
+
134
+ if (factory) {
135
+ return factory(theme, context)
136
+ }
137
+
101
138
  if (!!stylesheet[variant]) {
102
139
 
103
140
  return stylesheet[variant]
@@ -123,7 +160,7 @@ export class CodeleapStyleRegistry {
123
160
  let variantStyle = deepmerge({ all: true })(...variantStyles)
124
161
 
125
162
  if (!!aggregator) {
126
- variantStyle = aggregator(theme, variantStyle)
163
+ variantStyle = aggregator(theme as ITheme, variantStyle)
127
164
  }
128
165
 
129
166
  this.styleCache.cacheFor('variants', cache.key, variantStyle)
@@ -131,8 +168,14 @@ export class CodeleapStyleRegistry {
131
168
  return variantStyle
132
169
  }
133
170
 
171
+ /**
172
+ * Determines whether a style object contains composition keys for a given component.
173
+ * A key is a composition key if it starts with one of the component's `elements` strings
174
+ * and is not in `ignoredStyleKeys`. Returns both a boolean flag and the extracted
175
+ * composition sub-record so callers can avoid a second pass.
176
+ */
134
177
  isCompositionStyle(component: AnyStyledComponent, style: any) {
135
- const composition = {}
178
+ const composition: Record<string, any> = {}
136
179
 
137
180
  if (!style) {
138
181
  return {
@@ -143,9 +186,9 @@ export class CodeleapStyleRegistry {
143
186
 
144
187
  const styleKeys = Object.keys(style)
145
188
 
146
- let elements = []
189
+ let elements: string[] = []
147
190
 
148
- for (const element of component?.elements) {
191
+ for (const element of component?.elements ?? []) {
149
192
  const componentElements = styleKeys?.filter(k => k?.startsWith(element) && !ignoredStyleKeys?.includes(k))
150
193
 
151
194
  if (componentElements?.length >= 1) {
@@ -163,6 +206,12 @@ export class CodeleapStyleRegistry {
163
206
  }
164
207
  }
165
208
 
209
+ /**
210
+ * Checks whether a style object contains a `breakpoints` key, indicating it uses
211
+ * the responsive style API. Returns the key name alongside the boolean so the caller
212
+ * can use it to read and delete the breakpoints entry without a repeated `'breakpoints'`
213
+ * string literal.
214
+ */
166
215
  isResponsiveStyle(style: any) {
167
216
  const responsiveStyleKey = 'breakpoints'
168
217
 
@@ -214,7 +263,7 @@ export class CodeleapStyleRegistry {
214
263
  }
215
264
  }
216
265
 
217
- getResponsiveStyle(componentName: string, responsiveStyleKey: string, style: object) {
266
+ getResponsiveStyle(componentName: string, responsiveStyleKey: string, style: Record<string, any>) {
218
267
  const responsiveStyles = style[responsiveStyleKey]
219
268
 
220
269
  if (!responsiveStyles) return {}
@@ -227,14 +276,14 @@ export class CodeleapStyleRegistry {
227
276
  return cache.value
228
277
  }
229
278
 
230
- const styles = {}
279
+ const styles: Record<string, any> = {}
231
280
 
232
281
  for (const responsiveStyle in responsiveStyles) {
233
282
  const mediaQuery = this.getMediaQuery(responsiveStyle)
234
283
 
235
284
  const breakpointStyle = responsiveStyles[responsiveStyle]
236
285
 
237
- const componentStyles = this.styleFor(componentName, breakpointStyle, false)
286
+ const componentStyles = this.styleFor(componentName, breakpointStyle, false) as Record<string, any>
238
287
 
239
288
  // @ts-ignore
240
289
  for (const composition in componentStyles) {
@@ -272,7 +321,7 @@ export class CodeleapStyleRegistry {
272
321
  component,
273
322
  )
274
323
 
275
- styles = deepmerge({ all: true })(styles, computedVariantStyle[component])
324
+ styles = deepmerge({ all: true })(styles, (computedVariantStyle as any)[component])
276
325
  }
277
326
  } else if (typeof style === 'object') {
278
327
  styles = !!predicateObj ? predicateObj(style) : style
@@ -283,8 +332,7 @@ export class CodeleapStyleRegistry {
283
332
 
284
333
  private getMediaQuery(responsiveKey: string) {
285
334
  const [breakpoint, query] = responsiveKey?.includes(':') ? responsiveKey?.split(':') : [responsiveKey, 'down']
286
-
287
- // @ts-expect-error - media not has type
335
+ // @ts-ignore
288
336
  const mediaQuery = themeStore.theme?.media?.[query]?.(breakpoint)
289
337
 
290
338
  return mediaQuery
@@ -345,8 +393,8 @@ export class CodeleapStyleRegistry {
345
393
  return styles
346
394
  }
347
395
 
348
- styleFor<T = unknown>(componentName: string, componentStyle: StyleProp<T>, mergeWithDefaultStyle = true): T {
349
- const cache = this.styleCache.keyFor('components', { componentName, componentStyle, stylesheet: this.stylesheets[componentName] })
396
+ styleFor<T = unknown>(componentName: string, componentStyle: StyleProp<T & string>, mergeWithDefaultStyle = true, context?: ComponentContext): T {
397
+ const cache = this.styleCache.keyFor('components', { componentName, componentStyle, stylesheet: this.stylesheets[componentName], context })
350
398
 
351
399
  if (!!cache.value) {
352
400
  return cache.value as T
@@ -364,7 +412,7 @@ export class CodeleapStyleRegistry {
364
412
  }
365
413
 
366
414
  if (typeof style === 'string') {
367
- const computedVariantStyle = this.computeVariantStyle(componentName, [style])
415
+ const computedVariantStyle = this.computeVariantStyle(componentName, [style], undefined, context)
368
416
 
369
417
  return this.mergeStylesWithCache(
370
418
  [defaultStyle, computedVariantStyle],
@@ -418,7 +466,7 @@ export class CodeleapStyleRegistry {
418
466
  const isObj = typeof s === 'object'
419
467
 
420
468
  if (variants.length > 0 && (idx === filteredStyle.length - 1 || isObj)) {
421
- const computedVariantStyle = this.computeVariantStyle(componentName, variants)
469
+ const computedVariantStyle = this.computeVariantStyle(componentName, variants, undefined, context)
422
470
 
423
471
  styles.push(computedVariantStyle)
424
472
 
@@ -461,10 +509,15 @@ export class CodeleapStyleRegistry {
461
509
  return {} as T
462
510
  }
463
511
 
512
+ /**
513
+ * Rebuilds `commonVariants` by merging (in ascending priority order):
514
+ * `defaultVariants` → `appVariants` → `dynamicVariants` → `spacingVariants` → `insetVariants`.
515
+ * Called once in the constructor; must be re-called (via `update`) if app variants change.
516
+ */
464
517
  registerCommonVariants() {
465
- const spacingVariants = themeStore.theme?.['spacing']
518
+ const spacingVariants = themeStore.themeTyped?.spacing
466
519
 
467
- const insetVariants = themeStore.theme?.['inset']
520
+ const insetVariants = themeStore.themeTyped?.inset
468
521
 
469
522
  const appVariants = themeStore.variants
470
523
 
@@ -476,18 +529,41 @@ export class CodeleapStyleRegistry {
476
529
  insetVariants,
477
530
  )
478
531
 
479
- this.commonVariants = commonVariants
532
+ this.commonVariants = commonVariants as Record<string, ICSS | MultiplierFunction>
480
533
  }
481
534
 
535
+ /**
536
+ * Registers a component's variant stylesheet. No-ops if the component is already
537
+ * registered (variants are immutable after first registration). Extracts any
538
+ * `CONTEXT_FACTORY_SYMBOL`-tagged entries into `variantFactories` for context-aware
539
+ * resolution, then compresses and stores the remaining variants.
540
+ */
482
541
  registerVariants<Composition extends string = any>(componentName: string, variants: VariantStyleSheet, aggregator?: StyleAggregator<Composition>) {
483
542
  if (this.stylesheets[componentName]) {
484
- throw new Error(`Variants for ${componentName} already registered`)
543
+ return
544
+ }
545
+
546
+ const factories: Record<string, (theme: ITheme, context: ComponentContext) => any> = {}
547
+ for (const [variantName, variant] of Object.entries(variants)) {
548
+ const factory = (variant as any)?.[CONTEXT_FACTORY_SYMBOL]
549
+ if (typeof factory === 'function') factories[variantName] = factory
550
+ }
551
+ if (Object.keys(factories).length > 0) {
552
+ this.variantFactories[componentName] = factories
553
+ }
554
+
555
+ if (aggregator) {
556
+ this.aggregators[componentName] = aggregator as StyleAggregator
485
557
  }
486
- this.aggregators[componentName] = aggregator
487
558
 
488
559
  this.stylesheets[componentName] = minifier.compress(variants)
489
560
  }
490
561
 
562
+ /**
563
+ * Records the minimal component metadata (`styleRegistryName`, `elements`,
564
+ * `rootElement`) needed for composition and style resolution. Only the metadata
565
+ * is stored — the component function itself is not retained.
566
+ */
491
567
  registerComponent(component: AnyStyledComponent) {
492
568
  const componentData = {
493
569
  styleRegistryName: component?.styleRegistryName,
@@ -495,7 +571,7 @@ export class CodeleapStyleRegistry {
495
571
  rootElement: component?.rootElement,
496
572
  }
497
573
 
498
- this.components[component.styleRegistryName] = componentData as any
574
+ this.components[component.styleRegistryName!] = componentData as any
499
575
  }
500
576
 
501
577
  /**
@@ -506,8 +582,13 @@ export class CodeleapStyleRegistry {
506
582
  throw new Error('createStyle: Not implemented')
507
583
  }
508
584
 
585
+ /**
586
+ * Must be called after the active color scheme changes so that the `StyleCache`
587
+ * base key is regenerated. Without this call, cached values from the previous
588
+ * scheme would be returned for the new scheme.
589
+ */
509
590
  update() {
510
- const currentColorScheme = themeStore.theme?.['currentColorScheme']?.() ?? themeStore.colorScheme ?? 'default'
591
+ const currentColorScheme = themeStore.themeTyped?.currentColorScheme?.() ?? themeStore.colorScheme ?? 'default'
511
592
 
512
593
  this.styleCache.registerBaseKey([currentColorScheme, themeStore.theme, this.commonVariants])
513
594
  }
@@ -516,11 +597,11 @@ export class CodeleapStyleRegistry {
516
597
  return deepClone(style)
517
598
  }
518
599
 
519
- createStyles<K extends string = string>(styles: Record<K, StyleProp<AnyRecord, ''>> | ((theme: ITheme) => Record<K, StyleProp<AnyRecord, ''>>)): Record<K, ICSS> {
600
+ createStyles<K extends string = string>(styles: Record<K, StyleProp<string, ''>> | ((theme: ITheme) => Record<K, StyleProp<string, ''>>)): Record<K, ICSS> {
520
601
  const compute = () => {
521
602
  const current = themeStore.theme
522
603
 
523
- const stylesObj = typeof styles === 'function' ? styles(current) : styles
604
+ const stylesObj = typeof styles === 'function' ? styles(current as ITheme) : styles
524
605
 
525
606
  const cache = this.styleCache.keyFor('styles', stylesObj)
526
607
 
@@ -531,7 +612,7 @@ export class CodeleapStyleRegistry {
531
612
  const createdStyles = {} as Record<K, any>
532
613
 
533
614
  for (const key in stylesObj) {
534
- const style = this.styleFor('MyComponent', stylesObj[key], false)
615
+ const style = this.styleFor('MyComponent', stylesObj[key], false) as any
535
616
 
536
617
  createdStyles[key] = style?.wrapper ?? style
537
618
  }
@@ -551,12 +632,12 @@ export class CodeleapStyleRegistry {
551
632
  prefixStyle(prefix: string, style: any) {
552
633
  const entries = Object.entries(style).map(e => {
553
634
  const [key, value] = e
554
-
635
+
555
636
  const elementKey = capitalize(key)
556
-
637
+
557
638
  return [`${prefix}${elementKey}`, value]
558
639
  })
559
-
640
+
560
641
  return Object.fromEntries(entries)
561
642
  }
562
643
  }
@@ -0,0 +1,169 @@
1
+ import { describe, it, expect, beforeEach } from 'bun:test'
2
+ import { CodeleapStyleRegistry } from '../StyleRegistry'
3
+ import { createStyles, createStylesWithContext, CONTEXT_FACTORY_SYMBOL } from '../../lib/createStyles'
4
+ import { mockTheme, MockedTheme } from '../../tests/theme'
5
+ import { themeStore } from '../../theme'
6
+ import type { ICSS, ITheme } from '../../types'
7
+
8
+ class MockStateStorage {
9
+ private storage = new Map<string, any>()
10
+
11
+ getItem(key: string): any {
12
+ return this.storage.get(key) || null
13
+ }
14
+
15
+ setItem(key: string, value: any): void {
16
+ this.storage.set(key, value)
17
+ }
18
+
19
+ removeItem(key: string): void {
20
+ this.storage.delete(key)
21
+ }
22
+ }
23
+
24
+ class TestStyleRegistry extends CodeleapStyleRegistry {
25
+ constructor() {
26
+ super(new MockStateStorage() as any)
27
+ }
28
+
29
+ createStyle(css: ICSS): ICSS {
30
+ return css
31
+ }
32
+ }
33
+
34
+ const TEST_COMPONENT = 'TestComponent'
35
+
36
+ const testComponent = {
37
+ styleRegistryName: TEST_COMPONENT,
38
+ elements: ['wrapper', 'text'],
39
+ rootElement: 'wrapper',
40
+ }
41
+
42
+ describe('StyleRegistry — context threading', () => {
43
+ let registry: TestStyleRegistry
44
+ let currentTheme: MockedTheme
45
+
46
+ beforeEach(() => {
47
+ mockTheme()
48
+ currentTheme = themeStore.theme as unknown as MockedTheme
49
+ registry = new TestStyleRegistry()
50
+ registry.registerComponent(testComponent as any)
51
+ })
52
+
53
+ describe('registerVariants — factory extraction', () => {
54
+ it('stores factory for variants created with createStylesWithContext', () => {
55
+ const factory = (_theme: ITheme, context: { isActive: boolean }) => ({
56
+ wrapper: { opacity: context.isActive ? 1 : 0.5 },
57
+ })
58
+
59
+ const variants = {
60
+ active: createStylesWithContext(factory as any),
61
+ }
62
+
63
+ registry.registerVariants(TEST_COMPONENT, variants)
64
+
65
+ expect(registry.variantFactories[TEST_COMPONENT]?.active).toBe(factory as any)
66
+ })
67
+
68
+ it('does not store factory for plain createStyles variants', () => {
69
+ const variants = {
70
+ default: createStyles((_theme: ITheme) => ({
71
+ wrapper: { opacity: 1 },
72
+ })),
73
+ }
74
+
75
+ registry.registerVariants(TEST_COMPONENT, variants)
76
+
77
+ expect(registry.variantFactories[TEST_COMPONENT]).toBeUndefined()
78
+ })
79
+
80
+ it('stores only context-aware factories in mixed registrations', () => {
81
+ const factory = (_theme: ITheme, context: { isActive: boolean }) => ({
82
+ wrapper: { opacity: context.isActive ? 1 : 0.5 },
83
+ })
84
+
85
+ const variants = {
86
+ default: createStyles((_theme: ITheme) => ({ wrapper: { opacity: 1 } })),
87
+ active: createStylesWithContext(factory as any),
88
+ }
89
+
90
+ registry.registerVariants(TEST_COMPONENT, variants)
91
+
92
+ const factories = registry.variantFactories[TEST_COMPONENT]
93
+ expect(factories?.default).toBeUndefined()
94
+ expect(factories?.active).toBe(factory as any)
95
+ })
96
+ })
97
+
98
+ describe('computeVariantStyle — context path', () => {
99
+ beforeEach(() => {
100
+ const factory = (_theme: ITheme, context: { isActive: boolean }) => ({
101
+ wrapper: { opacity: context.isActive ? 1 : 0.5 },
102
+ })
103
+
104
+ const variants = {
105
+ default: createStyles((_theme: ITheme) => ({ wrapper: { color: 'black' } })),
106
+ active: createStylesWithContext(factory as any),
107
+ }
108
+
109
+ registry.registerVariants(TEST_COMPONENT, variants)
110
+ })
111
+
112
+ it('uses pre-computed stylesheet when no context is provided', () => {
113
+ const result = registry.computeVariantStyle(TEST_COMPONENT, ['default']) as any
114
+ expect(result?.['wrapper']?.['color']).toBe('black')
115
+ })
116
+
117
+ it('calls factory with context when context is provided', () => {
118
+ const result = registry.computeVariantStyle(TEST_COMPONENT, ['active'], undefined, { isActive: 1 }) as any
119
+ expect(result?.['wrapper']?.['opacity']).toBe(1)
120
+ })
121
+
122
+ it('produces different output for different context values', () => {
123
+ const active = registry.computeVariantStyle(TEST_COMPONENT, ['active'], undefined, { isActive: 1 }) as any
124
+ const inactive = registry.computeVariantStyle(TEST_COMPONENT, ['active'], undefined, { isActive: 0 }) as any
125
+
126
+ expect(active?.['wrapper']?.['opacity']).toBe(1)
127
+ expect(inactive?.['wrapper']?.['opacity']).toBe(0.5)
128
+ })
129
+
130
+ it('caches separately for different context values', () => {
131
+ const resultA = registry.computeVariantStyle(TEST_COMPONENT, ['active'], undefined, { isActive: 1 })
132
+ const resultB = registry.computeVariantStyle(TEST_COMPONENT, ['active'], undefined, { isActive: 0 })
133
+
134
+ expect(resultA).not.toEqual(resultB)
135
+ })
136
+ })
137
+
138
+ describe('styleFor — context threading', () => {
139
+ beforeEach(() => {
140
+ const factory = (_theme: ITheme, context: { isActive: boolean }) => ({
141
+ wrapper: { opacity: context.isActive ? 1 : 0.5 },
142
+ })
143
+
144
+ const variants = {
145
+ default: createStyles((_theme: ITheme) => ({ wrapper: { color: 'black' } })),
146
+ active: createStylesWithContext(factory as any),
147
+ }
148
+
149
+ registry.registerVariants(TEST_COMPONENT, variants)
150
+ })
151
+
152
+ it('threads context into computeVariantStyle when style is a string', () => {
153
+ const result = registry.styleFor(TEST_COMPONENT, 'active', true, { isActive: 1 }) as any
154
+ expect(result?.['wrapper']?.['opacity']).toBe(1)
155
+ })
156
+
157
+ it('threads context into computeVariantStyle when style is an array', () => {
158
+ const result = registry.styleFor(TEST_COMPONENT, ['active'], true, { isActive: 0 }) as any
159
+ expect(result?.['wrapper']?.['opacity']).toBe(0.5)
160
+ })
161
+
162
+ it('produces different results for different contexts via styleFor', () => {
163
+ const active = registry.styleFor(TEST_COMPONENT, 'active', true, { isActive: 1 }) as any
164
+ const inactive = registry.styleFor(TEST_COMPONENT, 'active', true, { isActive: 0 }) as any
165
+
166
+ expect(active?.['wrapper']?.['opacity']).not.toBe(inactive?.['wrapper']?.['opacity'])
167
+ })
168
+ })
169
+ })
package/src/constants.ts CHANGED
@@ -1,6 +1,20 @@
1
1
  // @ts-ignore
2
2
  const isBrowser = ():boolean => typeof document !== 'undefined'
3
3
 
4
+ /**
5
+ * Package-wide feature flags and runtime constants. These are read once at module
6
+ * load time and are not reactive.
7
+ *
8
+ * - `STORES_PERSIST_VERSION` — bumping this value invalidates all persisted caches
9
+ * on the next load (because it is included in every `hashKey` call via `registerBaseKey`).
10
+ * - `STORE_CACHE_ENABLED` — when `false`, the persistent `Cache` instances skip
11
+ * loading from and writing to `StateStorage`.
12
+ * - `CACHE_ENABLED` — when `false`, `StyleCache.cacheFor` returns values without
13
+ * writing to any cache bucket, effectively disabling all in-memory caching.
14
+ * - `IS_BROWSER` — sniffed at load time via `typeof document`; used as a fast guard
15
+ * before any DOM access.
16
+ * - `LOG` — enables verbose `console.log` tracing inside `Cache` and `StylePersistor`.
17
+ */
4
18
  export const StyleConstants = {
5
19
  STORES_PERSIST_VERSION: 1,
6
20
  STORE_CACHE_ENABLED: true,
@@ -3,11 +3,13 @@ import { ICSS } from '../types'
3
3
  import { getNestedStylesByKey } from '../utils'
4
4
 
5
5
  /**
6
- * Hook that processes composition styles based on component styles.
7
- *
8
- * @param {T | Array<T>} composition - Composition element(s)
9
- * @param {Partial<Record<C, ICSS>>} componentStyles - Component styles object
10
- * @returns {Partial<Record<T, ICSS>>} Mapped styles for each composition element
6
+ * Slices a flat component-style record into per-element sub-records, memoised to
7
+ * avoid unnecessary re-renders. For each element name in `composition`, it calls
8
+ * `getNestedStylesByKey(element, componentStyles)` to collect all keys that start
9
+ * with that element name. Accepts either a single element string or an array.
10
+ *
11
+ * Useful when a parent component receives a merged `componentStyles` object and
12
+ * needs to distribute the correct slice to each child element.
11
13
  */
12
14
  export function useCompositionStyles<T extends string, C extends string>(
13
15
  composition: (T | Array<T>),
@@ -18,7 +20,7 @@ export function useCompositionStyles<T extends string, C extends string>(
18
20
  }
19
21
 
20
22
  return useMemo(() => {
21
- const compositionStyles = {}
23
+ const compositionStyles: Record<string, ICSS> = {}
22
24
 
23
25
  if (Array.isArray(composition)) {
24
26
  for (const element of composition) {
@@ -28,6 +30,6 @@ export function useCompositionStyles<T extends string, C extends string>(
28
30
  compositionStyles[composition as string] = getNestedStylesByKey(composition, styles)
29
31
  }
30
32
 
31
- return compositionStyles
33
+ return compositionStyles as Partial<Record<T, ICSS>>
32
34
  }, [styles])
33
35
  }