@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.
- package/dist/classes/Cacher.d.ts +87 -0
- package/dist/classes/Cacher.d.ts.map +1 -0
- package/dist/classes/StaleControl.d.ts +65 -0
- package/dist/classes/StaleControl.d.ts.map +1 -0
- package/dist/classes/StyleCache.d.ts +63 -0
- package/dist/classes/StyleCache.d.ts.map +1 -0
- package/dist/classes/StylePersistor.d.ts +52 -0
- package/dist/classes/StylePersistor.d.ts.map +1 -0
- package/dist/classes/StyleRegistry.d.ts +108 -0
- package/dist/classes/StyleRegistry.d.ts.map +1 -0
- package/dist/classes/index.d.ts +3 -0
- package/dist/classes/index.d.ts.map +1 -0
- package/dist/constants.d.ts +22 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +5 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/useCompositionStyles.d.ts +12 -0
- package/dist/hooks/useCompositionStyles.d.ts.map +1 -0
- package/dist/hooks/useNestedStylesByKey.d.ts +11 -0
- package/dist/hooks/useNestedStylesByKey.d.ts.map +1 -0
- package/dist/hooks/useStyleObserver.d.ts +9 -0
- package/dist/hooks/useStyleObserver.d.ts.map +1 -0
- package/dist/hooks/useTheme.d.ts +19 -0
- package/dist/hooks/useTheme.d.ts.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/lib/calc.d.ts +27 -0
- package/dist/lib/calc.d.ts.map +1 -0
- package/dist/lib/createStyles.d.ts +30 -0
- package/dist/lib/createStyles.d.ts.map +1 -0
- package/dist/lib/createTheme.d.ts +28 -0
- package/dist/lib/createTheme.d.ts.map +1 -0
- package/dist/lib/cssVariables.d.ts +35 -0
- package/dist/lib/cssVariables.d.ts.map +1 -0
- package/dist/lib/index.d.ts +5 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/theme/generateColorScheme.d.ts +22 -0
- package/dist/theme/generateColorScheme.d.ts.map +1 -0
- package/dist/theme/index.d.ts +4 -0
- package/dist/theme/index.d.ts.map +1 -0
- package/dist/theme/themeStore.d.ts +106 -0
- package/dist/theme/themeStore.d.ts.map +1 -0
- package/dist/theme/validateTheme.d.ts +19 -0
- package/dist/theme/validateTheme.d.ts.map +1 -0
- package/dist/tools/colors.d.ts +70 -0
- package/dist/tools/colors.d.ts.map +1 -0
- package/dist/tools/deepClone.d.ts +7 -0
- package/dist/tools/deepClone.d.ts.map +1 -0
- package/dist/tools/deepmerge.d.ts +13 -0
- package/dist/tools/deepmerge.d.ts.map +1 -0
- package/dist/tools/hashKey.d.ts +8 -0
- package/dist/tools/hashKey.d.ts.map +1 -0
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/minifier.d.ts +24 -0
- package/dist/tools/minifier.d.ts.map +1 -0
- package/dist/tools/multiplierProperty.d.ts +4 -0
- package/dist/tools/multiplierProperty.d.ts.map +1 -0
- package/dist/types/cache.d.ts +12 -0
- package/dist/types/cache.d.ts.map +1 -0
- package/dist/types/component.d.ts +58 -0
- package/dist/types/component.d.ts.map +1 -0
- package/dist/types/core.d.ts +77 -0
- package/dist/types/core.d.ts.map +1 -0
- package/dist/types/icon.d.ts +15 -0
- package/dist/types/icon.d.ts.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/spacing.d.ts +28 -0
- package/dist/types/spacing.d.ts.map +1 -0
- package/dist/types/store.d.ts +12 -0
- package/dist/types/store.d.ts.map +1 -0
- package/dist/types/style.d.ts +42 -0
- package/dist/types/style.d.ts.map +1 -0
- package/dist/types/theme.d.ts +109 -0
- package/dist/types/theme.d.ts.map +1 -0
- package/dist/utils.d.ts +40 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/variants/borderCreator.d.ts +22 -0
- package/dist/variants/borderCreator.d.ts.map +1 -0
- package/dist/variants/createAppVariants.d.ts +18 -0
- package/dist/variants/createAppVariants.d.ts.map +1 -0
- package/dist/variants/defaultVariants.d.ts +140 -0
- package/dist/variants/defaultVariants.d.ts.map +1 -0
- package/dist/variants/dynamicVariants.d.ts +43 -0
- package/dist/variants/dynamicVariants.d.ts.map +1 -0
- package/dist/variants/index.d.ts +7 -0
- package/dist/variants/index.d.ts.map +1 -0
- package/dist/variants/mediaQuery.d.ts +30 -0
- package/dist/variants/mediaQuery.d.ts.map +1 -0
- package/dist/variants/spacing.d.ts +26 -0
- package/dist/variants/spacing.d.ts.map +1 -0
- package/package.json +19 -5
- package/src/classes/Cacher.ts +9 -9
- package/src/classes/StaleControl.ts +1 -1
- package/src/classes/StyleCache.ts +9 -3
- package/src/classes/StylePersistor.ts +11 -0
- package/src/classes/StyleRegistry.ts +124 -43
- package/src/classes/tests/StyleRegistry.spec.ts +169 -0
- package/src/constants.ts +14 -0
- package/src/hooks/useCompositionStyles.ts +9 -7
- package/src/hooks/useNestedStylesByKey.ts +8 -0
- package/src/hooks/useStyleObserver.ts +6 -5
- package/src/hooks/useTheme.ts +14 -0
- package/src/lib/calc.ts +13 -0
- package/src/lib/createStyles.ts +35 -4
- package/src/lib/createTheme.ts +74 -25
- package/src/lib/cssVariables.ts +74 -0
- package/src/lib/index.ts +2 -1
- package/src/lib/tests/createStyles.spec.ts +80 -23
- package/src/lib/tests/createStylesWithContext.spec.ts +108 -0
- package/src/tests/theme.ts +6 -2
- package/src/theme/generateColorScheme.ts +13 -10
- package/src/theme/tests/themeStore.spec.ts +38 -37
- package/src/theme/themeStore.ts +10 -7
- package/src/theme/validateTheme.ts +1 -1
- package/src/tools/colors.ts +24 -36
- package/src/tools/deepClone.ts +3 -5
- package/src/tools/deepmerge.ts +8 -6
- package/src/tools/hashKey.ts +4 -5
- package/src/tools/minifier.ts +11 -12
- package/src/tools/tests/deepClone.spec.ts +2 -2
- package/src/types/cache.ts +10 -0
- package/src/types/component.ts +41 -5
- package/src/types/core.ts +66 -6
- package/src/types/icon.ts +11 -0
- package/src/types/spacing.ts +21 -0
- package/src/types/store.ts +6 -0
- package/src/types/style.ts +37 -10
- package/src/types/theme.ts +37 -1
- package/src/utils.ts +34 -4
- package/src/variants/borderCreator.ts +14 -5
- package/src/variants/createAppVariants.ts +11 -0
- package/src/variants/defaultVariants.ts +28 -8
- package/src/variants/dynamicVariants.ts +76 -18
- package/src/variants/mediaQuery.ts +18 -0
- package/src/variants/spacing.ts +15 -1
- package/package.json.bak +0 -30
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'bun:test'
|
|
2
|
+
import { createStyles, createStylesWithContext, CONTEXT_FACTORY_SYMBOL } from '../createStyles'
|
|
3
|
+
import { themeStore } from '../../theme'
|
|
4
|
+
import { mockTheme, MockedTheme } from '../../tests/theme'
|
|
5
|
+
|
|
6
|
+
describe('createStylesWithContext (web — isBrowser: true)', () => {
|
|
7
|
+
let currentTheme: MockedTheme = null as any
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
mockTheme({ isBrowser: true })
|
|
11
|
+
currentTheme = themeStore.theme as unknown as MockedTheme
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('exposes the factory function via CONTEXT_FACTORY_SYMBOL', () => {
|
|
15
|
+
const factory = (theme: any, context: { isActive: boolean }) => ({
|
|
16
|
+
wrapper: { color: context.isActive ? theme.colors.neutralSo : theme.colors.secondary },
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const styles = createStylesWithContext(factory as any)
|
|
20
|
+
|
|
21
|
+
expect((styles as any)[CONTEXT_FACTORY_SYMBOL]).toBe(factory)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('plain createStyles proxy returns undefined for CONTEXT_FACTORY_SYMBOL', () => {
|
|
25
|
+
const styles = createStyles((theme: any) => ({
|
|
26
|
+
wrapper: { color: theme.colors.neutralSo },
|
|
27
|
+
}))
|
|
28
|
+
|
|
29
|
+
expect((styles as any)[CONTEXT_FACTORY_SYMBOL]).toBeUndefined()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('returns expected styles when accessed with default (empty) context', () => {
|
|
33
|
+
const styles = createStylesWithContext((theme: any, context: { multiplier?: number }) => ({
|
|
34
|
+
wrapper: { padding: (context.multiplier ?? 1) * 8 },
|
|
35
|
+
}))
|
|
36
|
+
|
|
37
|
+
expect(styles.wrapper).toEqual({ padding: 8 })
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('re-evaluates on every property access (proxy behavior)', () => {
|
|
41
|
+
let callCount = 0
|
|
42
|
+
|
|
43
|
+
const styles = createStylesWithContext((theme: any, context: {}) => {
|
|
44
|
+
callCount++
|
|
45
|
+
return { wrapper: { color: theme.colors.neutralSo } }
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
callCount = 0
|
|
49
|
+
|
|
50
|
+
styles.wrapper
|
|
51
|
+
styles.wrapper
|
|
52
|
+
styles.wrapper
|
|
53
|
+
|
|
54
|
+
expect(callCount).toBe(3)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('reflects theme changes on subsequent accesses', () => {
|
|
58
|
+
const styles = createStylesWithContext((theme: any, _context: {}) => ({
|
|
59
|
+
wrapper: { color: theme.colors.buttonRegularPrimaryBgDefault },
|
|
60
|
+
}))
|
|
61
|
+
|
|
62
|
+
// CSS var reference is static — same regardless of active scheme.
|
|
63
|
+
// The browser CSS cascade handles the actual color switch.
|
|
64
|
+
expect(styles.wrapper).toEqual({ color: 'var(--cl-buttonRegularPrimaryBgDefault)' })
|
|
65
|
+
|
|
66
|
+
currentTheme.setColorScheme('dark')
|
|
67
|
+
|
|
68
|
+
expect(styles.wrapper).toEqual({ color: 'var(--cl-buttonRegularPrimaryBgDefault)' })
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('returns undefined for non-existent keys', () => {
|
|
72
|
+
const styles = createStylesWithContext((_theme: any, _context: {}) => ({
|
|
73
|
+
wrapper: { color: 'red' },
|
|
74
|
+
}))
|
|
75
|
+
|
|
76
|
+
expect((styles as any).nonExistent).toBeUndefined()
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('createStylesWithContext (RN — isBrowser: false)', () => {
|
|
81
|
+
let currentTheme: MockedTheme = null as any
|
|
82
|
+
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
mockTheme({ isBrowser: false })
|
|
85
|
+
currentTheme = themeStore.theme as unknown as MockedTheme
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('returns raw RGBA values when isBrowser is false', () => {
|
|
89
|
+
const styles = createStylesWithContext((_theme: any, _context: {}) => ({
|
|
90
|
+
wrapper: { color: _theme.colors.neutralSolid500 },
|
|
91
|
+
}))
|
|
92
|
+
|
|
93
|
+
expect(styles.wrapper).toEqual({ color: 'rgba(136, 136, 136, 1.00)' })
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('currentSchemeColors reflects active scheme on RN', () => {
|
|
97
|
+
const styles = createStylesWithContext((_theme: any, _context: {}) => ({
|
|
98
|
+
wrapper: { color: _theme.currentSchemeColors.buttonRegularPrimaryBgDefault },
|
|
99
|
+
}))
|
|
100
|
+
|
|
101
|
+
expect(styles.wrapper).toEqual({ color: 'rgba(43, 105, 122, 1.00)' })
|
|
102
|
+
|
|
103
|
+
currentTheme.setColorScheme('dark')
|
|
104
|
+
|
|
105
|
+
const darkValue = currentTheme.currentSchemeColors.buttonRegularPrimaryBgDefault as string
|
|
106
|
+
expect(styles.wrapper).toEqual({ color: darkValue })
|
|
107
|
+
})
|
|
108
|
+
})
|
package/src/tests/theme.ts
CHANGED
|
@@ -6,7 +6,11 @@ import darkMode from './colors/darkMode'
|
|
|
6
6
|
import { createTheme } from '../lib'
|
|
7
7
|
import { validateTheme } from '../theme'
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
type MockThemeOptions = {
|
|
10
|
+
isBrowser?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const mockTheme = ({ isBrowser = true }: MockThemeOptions = {}) => {
|
|
10
14
|
return createTheme(
|
|
11
15
|
validateTheme({
|
|
12
16
|
baseColors,
|
|
@@ -46,7 +50,7 @@ export const mockTheme = () => {
|
|
|
46
50
|
icons: {},
|
|
47
51
|
presets: {},
|
|
48
52
|
values: {},
|
|
49
|
-
isBrowser
|
|
53
|
+
isBrowser,
|
|
50
54
|
}),
|
|
51
55
|
{
|
|
52
56
|
set: (name, colorSchema) => {},
|
|
@@ -20,25 +20,28 @@ const defaultAlphasMap = Object.fromEntries(
|
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
23
|
+
* Derives a 20-token color ramp from a single anchor hex color.
|
|
24
|
+
*
|
|
25
|
+
* Produces two groups of tokens:
|
|
26
|
+
* - **10 solid tokens** (`{prefix}Solid100` … `{prefix}Solid1000`) — vary the lightness
|
|
27
|
+
* of the anchor's hue/saturation from ~95 L (lightest) to ~10 L (darkest).
|
|
28
|
+
* - **10 transparent tokens** (`{prefix}Transparent100` … `{prefix}Transparent1000`) —
|
|
29
|
+
* keep the anchor's exact RGB channels but increase alpha from 0.05 to 0.90.
|
|
30
|
+
*
|
|
31
|
+
* Default lightness steps: `[95, 85, 75, 60, 45, 30, 27, 21, 16, 10]`.
|
|
32
|
+
* Default alpha steps: `[0.05, 0.10, …, 0.90]`.
|
|
33
|
+
* Both can be overridden by passing custom maps (step → value).
|
|
31
34
|
*/
|
|
32
35
|
export function generateColorScheme(
|
|
33
36
|
anchorHex: string,
|
|
34
37
|
prefix = 'primary',
|
|
35
38
|
lightnesses:typeof defaultLightnessMap = defaultLightnessMap,
|
|
36
39
|
alphas: typeof defaultAlphasMap = defaultAlphasMap,
|
|
37
|
-
):
|
|
40
|
+
): Record<string, string> {
|
|
38
41
|
const { h, s } = colorTools.hexToHSL(anchorHex)
|
|
39
42
|
const baseRGB = colorTools.hexToRGB(anchorHex)
|
|
40
43
|
|
|
41
|
-
const scheme:
|
|
44
|
+
const scheme: Record<string, string> = {}
|
|
42
45
|
|
|
43
46
|
Object.entries(lightnesses).forEach(([step, lightness]) => {
|
|
44
47
|
const rgb = colorTools.hslToRGB(h, s, lightness)
|
|
@@ -75,13 +75,13 @@ describe('ThemeStore - Heavy Load Tests', () => {
|
|
|
75
75
|
})
|
|
76
76
|
|
|
77
77
|
it('should handle massive color schemes with deep nesting', () => {
|
|
78
|
-
const colorSchemes = {}
|
|
78
|
+
const colorSchemes: Record<string, any> = {}
|
|
79
79
|
const schemeCount = 1000
|
|
80
80
|
const colorsPerScheme = 100
|
|
81
81
|
|
|
82
82
|
// Generate massive color schemes
|
|
83
83
|
for (let scheme = 0; scheme < schemeCount; scheme++) {
|
|
84
|
-
const colors = {}
|
|
84
|
+
const colors: Record<string, any> = {}
|
|
85
85
|
for (let color = 0; color < colorsPerScheme; color++) {
|
|
86
86
|
colors[`color_${color}`] = {
|
|
87
87
|
primary: `#${(scheme * color).toString(16).padStart(6, '0')}`,
|
|
@@ -135,9 +135,10 @@ describe('ThemeStore - Heavy Load Tests', () => {
|
|
|
135
135
|
|
|
136
136
|
// Inject scheme
|
|
137
137
|
const injected = store.injectColorScheme(schemeName, colors)
|
|
138
|
-
|
|
139
|
-
expect(
|
|
140
|
-
expect(
|
|
138
|
+
const scheme = injected[schemeName] as Record<string, string>
|
|
139
|
+
expect(scheme).toBeDefined()
|
|
140
|
+
expect(scheme.primary).toBe(colors.primary)
|
|
141
|
+
expect(scheme.secondary).toBe('#111111') // inherited
|
|
141
142
|
|
|
142
143
|
// Every 10th operation, eject some schemes
|
|
143
144
|
if (i % 10 === 0 && i > 0) {
|
|
@@ -157,10 +158,10 @@ describe('ThemeStore - Heavy Load Tests', () => {
|
|
|
157
158
|
it('should handle massive variants with complex structures', () => {
|
|
158
159
|
const componentCount = 500
|
|
159
160
|
const variantsPerComponent = 50
|
|
160
|
-
const variants = {}
|
|
161
|
+
const variants: Record<string, any> = {}
|
|
161
162
|
|
|
162
163
|
for (let comp = 0; comp < componentCount; comp++) {
|
|
163
|
-
const componentVariants = {}
|
|
164
|
+
const componentVariants: Record<string, any> = {}
|
|
164
165
|
for (let variant = 0; variant < variantsPerComponent; variant++) {
|
|
165
166
|
componentVariants[`variant_${variant}`] = {
|
|
166
167
|
className: `comp-${comp}-var-${variant}`,
|
|
@@ -229,7 +230,7 @@ describe('ThemeStore - Heavy Load Tests', () => {
|
|
|
229
230
|
if (i % 200 === 0) {
|
|
230
231
|
expect(getThemeStore()?.iteration).toBe(i)
|
|
231
232
|
expect(store.colorScheme).toBe(`scheme_${i % 50}`)
|
|
232
|
-
expect(store.variants[`component_${i}`]).toBeDefined()
|
|
233
|
+
expect((store.variants as any)[`component_${i}`]).toBeDefined()
|
|
233
234
|
}
|
|
234
235
|
}
|
|
235
236
|
|
|
@@ -246,7 +247,7 @@ describe('ThemeStore - Heavy Load Tests', () => {
|
|
|
246
247
|
describe('function storage and execution stress tests', () => {
|
|
247
248
|
it('should handle thousands of stored functions with complex computations', () => {
|
|
248
249
|
const functionCount = 5000
|
|
249
|
-
const theme = {
|
|
250
|
+
const theme: Record<string, Record<string, any>> = {
|
|
250
251
|
colors: {},
|
|
251
252
|
spacing: {},
|
|
252
253
|
calculators: {},
|
|
@@ -259,10 +260,10 @@ describe('ThemeStore - Heavy Load Tests', () => {
|
|
|
259
260
|
// Store various types of functions
|
|
260
261
|
for (let i = 0; i < functionCount; i++) {
|
|
261
262
|
// Simple calculators
|
|
262
|
-
theme.calculators[`calc_${i}`] = (value) => value * (i + 1)
|
|
263
|
+
theme.calculators[`calc_${i}`] = (value: any) => value * (i + 1)
|
|
263
264
|
|
|
264
265
|
// Complex transformers
|
|
265
|
-
theme.transformers[`transform_${i}`] = (input) => {
|
|
266
|
+
theme.transformers[`transform_${i}`] = (input: any) => {
|
|
266
267
|
return {
|
|
267
268
|
scaled: input * Math.pow(2, i % 10),
|
|
268
269
|
offset: input + (i * 3.14159),
|
|
@@ -284,7 +285,7 @@ describe('ThemeStore - Heavy Load Tests', () => {
|
|
|
284
285
|
}
|
|
285
286
|
|
|
286
287
|
// Validators
|
|
287
|
-
theme.validators[`validate_${i}`] = (value) => {
|
|
288
|
+
theme.validators[`validate_${i}`] = (value: any) => {
|
|
288
289
|
return value !== null &&
|
|
289
290
|
value !== undefined &&
|
|
290
291
|
(typeof value === 'number' ? value >= i : value.length >= i % 10)
|
|
@@ -347,15 +348,15 @@ describe('ThemeStore - Heavy Load Tests', () => {
|
|
|
347
348
|
|
|
348
349
|
// Create deeply nested function chains
|
|
349
350
|
const theme = {
|
|
350
|
-
fibonacci: {},
|
|
351
|
-
factorial: {},
|
|
352
|
-
compose: {},
|
|
353
|
-
pipeline: {},
|
|
351
|
+
fibonacci: {} as Record<string, any>,
|
|
352
|
+
factorial: {} as Record<string, any>,
|
|
353
|
+
compose: {} as Record<string, any>,
|
|
354
|
+
pipeline: {} as Record<string, any>,
|
|
354
355
|
}
|
|
355
356
|
|
|
356
357
|
// Fibonacci functions
|
|
357
358
|
for (let i = 0; i < depth; i++) {
|
|
358
|
-
theme.fibonacci[`fib_${i}`] = (n) => {
|
|
359
|
+
theme.fibonacci[`fib_${i}`] = (n: any) => {
|
|
359
360
|
if (n <= 1) return n
|
|
360
361
|
if (i > 0 && theme.fibonacci[`fib_${i - 1}`]) {
|
|
361
362
|
return theme.fibonacci[`fib_${i - 1}`](n - 1) + theme.fibonacci[`fib_${i - 1}`](n - 2)
|
|
@@ -366,7 +367,7 @@ describe('ThemeStore - Heavy Load Tests', () => {
|
|
|
366
367
|
|
|
367
368
|
// Factorial chains
|
|
368
369
|
for (let i = 0; i < depth; i++) {
|
|
369
|
-
theme.factorial[`fact_${i}`] = (n) => {
|
|
370
|
+
theme.factorial[`fact_${i}`] = (n: any) => {
|
|
370
371
|
if (n <= 1) return 1
|
|
371
372
|
const prevFact = theme.factorial[`fact_${Math.max(0, i - 1)}`]
|
|
372
373
|
return prevFact ? n * prevFact(n - 1) : n
|
|
@@ -375,7 +376,7 @@ describe('ThemeStore - Heavy Load Tests', () => {
|
|
|
375
376
|
|
|
376
377
|
// Function composition
|
|
377
378
|
for (let i = 0; i < depth; i++) {
|
|
378
|
-
theme.compose[`comp_${i}`] = (value) => {
|
|
379
|
+
theme.compose[`comp_${i}`] = (value: any) => {
|
|
379
380
|
let result = value
|
|
380
381
|
for (let j = 0; j <= i && j < 10; j++) {
|
|
381
382
|
result = Math.sqrt(Math.abs(result * (j + 1)))
|
|
@@ -386,13 +387,13 @@ describe('ThemeStore - Heavy Load Tests', () => {
|
|
|
386
387
|
|
|
387
388
|
// Pipeline functions
|
|
388
389
|
for (let i = 0; i < depth; i++) {
|
|
389
|
-
theme.pipeline[`pipe_${i}`] = (input) => {
|
|
390
|
+
theme.pipeline[`pipe_${i}`] = (input: any) => {
|
|
390
391
|
const steps = [
|
|
391
|
-
(x) => x * 2,
|
|
392
|
-
(x) => x + i,
|
|
393
|
-
(x) => Math.pow(x, 1.5),
|
|
394
|
-
(x) => x % 1000,
|
|
395
|
-
(x) => theme.compose[`comp_${Math.min(i, depth - 1)}`]?.(x) || x,
|
|
392
|
+
(x: any) => x * 2,
|
|
393
|
+
(x: any) => x + i,
|
|
394
|
+
(x: any) => Math.pow(x, 1.5),
|
|
395
|
+
(x: any) => x % 1000,
|
|
396
|
+
(x: any) => theme.compose[`comp_${Math.min(i, depth - 1)}`]?.(x) || x,
|
|
396
397
|
]
|
|
397
398
|
return steps.reduce((acc, fn) => fn(acc), input)
|
|
398
399
|
}
|
|
@@ -449,7 +450,7 @@ describe('ThemeStore - Heavy Load Tests', () => {
|
|
|
449
450
|
const factoryCount = 1000
|
|
450
451
|
const generatedPerFactory = 10
|
|
451
452
|
|
|
452
|
-
const theme = {
|
|
453
|
+
const theme: Record<string, Record<string, any>> = {
|
|
453
454
|
factories: {},
|
|
454
455
|
generated: {},
|
|
455
456
|
dynamicComputed: {},
|
|
@@ -459,17 +460,17 @@ describe('ThemeStore - Heavy Load Tests', () => {
|
|
|
459
460
|
|
|
460
461
|
// Create function factories
|
|
461
462
|
for (let i = 0; i < factoryCount; i++) {
|
|
462
|
-
theme.factories[`factory_${i}`] = (config) => {
|
|
463
|
+
theme.factories[`factory_${i}`] = (config: any) => {
|
|
463
464
|
return {
|
|
464
|
-
calculator: (value) => value * (config.multiplier || 1) + (config.offset || 0),
|
|
465
|
-
validator: (value) => value >= (config.min || 0) && value <= (config.max || 1000),
|
|
466
|
-
transformer: (value) => ({
|
|
465
|
+
calculator: (value: any) => value * (config.multiplier || 1) + (config.offset || 0),
|
|
466
|
+
validator: (value: any) => value >= (config.min || 0) && value <= (config.max || 1000),
|
|
467
|
+
transformer: (value: any) => ({
|
|
467
468
|
original: value,
|
|
468
469
|
scaled: value * (config.scale || 1),
|
|
469
470
|
formatted: `${config.prefix || ''}${value}${config.suffix || ''}`,
|
|
470
471
|
computed: Math.pow(value, config.power || 1),
|
|
471
472
|
}),
|
|
472
|
-
composer: (...fns) => (value) => fns.reduce((acc, fn) => fn(acc), value),
|
|
473
|
+
composer: (...fns: any[]) => (value: any) => fns.reduce((acc, fn) => fn(acc), value),
|
|
473
474
|
}
|
|
474
475
|
}
|
|
475
476
|
|
|
@@ -490,7 +491,7 @@ describe('ThemeStore - Heavy Load Tests', () => {
|
|
|
490
491
|
theme.generated[`gen_${i}_${j}`] = generatedFunctions
|
|
491
492
|
|
|
492
493
|
// Create dynamic computed values
|
|
493
|
-
theme.dynamicComputed[`comp_${i}_${j}`] = (input) => {
|
|
494
|
+
theme.dynamicComputed[`comp_${i}_${j}`] = (input: any) => {
|
|
494
495
|
const calc = generatedFunctions.calculator(input)
|
|
495
496
|
const valid = generatedFunctions.validator(calc)
|
|
496
497
|
const transform = generatedFunctions.transformer(calc)
|
|
@@ -543,8 +544,8 @@ describe('ThemeStore - Heavy Load Tests', () => {
|
|
|
543
544
|
const generated = getThemeStore()?.generated[`gen_${i}_${j}`]
|
|
544
545
|
if (generated?.composer) {
|
|
545
546
|
const composed = generated.composer(
|
|
546
|
-
(x) => x * 2,
|
|
547
|
-
(x) => x + 10,
|
|
547
|
+
(x: any) => x * 2,
|
|
548
|
+
(x: any) => x + 10,
|
|
548
549
|
generated.calculator,
|
|
549
550
|
)(testValue)
|
|
550
551
|
if (typeof composed === 'number') executionCount++
|
|
@@ -577,7 +578,7 @@ describe('ThemeStore - Heavy Load Tests', () => {
|
|
|
577
578
|
|
|
578
579
|
for (let set = 0; set < hugeSets; set++) {
|
|
579
580
|
// Create massive theme
|
|
580
|
-
const hugeTheme = {
|
|
581
|
+
const hugeTheme: Record<string, Record<string, any>> = {
|
|
581
582
|
colors: {},
|
|
582
583
|
spacing: {},
|
|
583
584
|
typography: {},
|
|
@@ -597,9 +598,9 @@ describe('ThemeStore - Heavy Load Tests', () => {
|
|
|
597
598
|
store.setTheme(hugeTheme)
|
|
598
599
|
|
|
599
600
|
// Create massive color schemes
|
|
600
|
-
const hugeColorSchemes = {}
|
|
601
|
+
const hugeColorSchemes: Record<string, any> = {}
|
|
601
602
|
for (let i = 0; i < itemsPerSet; i++) {
|
|
602
|
-
const scheme = {}
|
|
603
|
+
const scheme: Record<string, any> = {}
|
|
603
604
|
for (let j = 0; j < 50; j++) {
|
|
604
605
|
scheme[`color_${j}`] = `#${(i * j).toString(16).padStart(6, '0')}`
|
|
605
606
|
}
|
package/src/theme/themeStore.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { map, computed, atom } from 'nanostores'
|
|
|
5
5
|
* Theme state interface containing theme and color scheme information.
|
|
6
6
|
*/
|
|
7
7
|
export type ThemeState = {
|
|
8
|
-
theme:
|
|
8
|
+
theme: ITheme | null
|
|
9
9
|
colorScheme: string | null
|
|
10
10
|
}
|
|
11
11
|
|
|
@@ -18,9 +18,9 @@ export class ThemeStore {
|
|
|
18
18
|
|
|
19
19
|
public readonly colorSchemeStore = atom<string | null>(null)
|
|
20
20
|
|
|
21
|
-
public readonly themeStore =
|
|
22
|
-
|
|
23
|
-
public readonly variantsStore = map<IAppVariants>({})
|
|
21
|
+
public readonly themeStore = atom<ITheme | null>(null)
|
|
22
|
+
|
|
23
|
+
public readonly variantsStore = map<IAppVariants>({} as IAppVariants)
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Gets the current theme.
|
|
@@ -35,7 +35,8 @@ export class ThemeStore {
|
|
|
35
35
|
* @returns {AppTheme<Theme>} Current theme cast to AppTheme type
|
|
36
36
|
*/
|
|
37
37
|
get themeTyped() {
|
|
38
|
-
|
|
38
|
+
const theme = this.themeStore.get()
|
|
39
|
+
return theme ? theme as unknown as AppTheme<Theme> : null
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
/**
|
|
@@ -159,8 +160,10 @@ export class ThemeStore {
|
|
|
159
160
|
export const themeStore = new ThemeStore()
|
|
160
161
|
|
|
161
162
|
/**
|
|
162
|
-
*
|
|
163
|
-
*
|
|
163
|
+
* Derived nanostores `computed` that combines `themeStore` and `colorSchemeStore`
|
|
164
|
+
* into a single `ThemeState` atom. Components subscribe to this via `useTheme`
|
|
165
|
+
* so they re-render only when either the theme object or the active color scheme
|
|
166
|
+
* changes — not on every `ThemeStore` method call.
|
|
164
167
|
*/
|
|
165
168
|
export const themeStoreComputed = computed([
|
|
166
169
|
themeStore.themeStore,
|
|
@@ -22,7 +22,7 @@ export function validateTheme<T extends Theme>(theme: T) {
|
|
|
22
22
|
|
|
23
23
|
const requiredColors = Object.keys(colors)
|
|
24
24
|
|
|
25
|
-
const mergedAlternateColors = {}
|
|
25
|
+
const mergedAlternateColors: Record<string, any> = {}
|
|
26
26
|
|
|
27
27
|
if (alternateColors) {
|
|
28
28
|
for (const [colorSchemeName, colorSchemeColors] of Object.entries(alternateColors)) {
|
package/src/tools/colors.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* @returns {object} HSL object with h (0-360), s (0-100), l (0-100)
|
|
2
|
+
* Expects a 7-character hex string (`#rrggbb`). Shorter forms (3-char, no hash) are
|
|
3
|
+
* not handled and will produce NaN channels. Returns h in [0, 360], s and l in [0, 100],
|
|
4
|
+
* all rounded to integers via `Math.round`.
|
|
6
5
|
*/
|
|
7
6
|
export function hexToHSL(hex: string) {
|
|
8
7
|
const r = parseInt(hex.slice(1, 3), 16) / 255
|
|
@@ -31,12 +30,10 @@ export function hexToHSL(hex: string) {
|
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* @param {number} l - Lightness (0-100)
|
|
39
|
-
* @returns {string} Hex color string
|
|
33
|
+
* Uses the HSL-to-RGB CSS Color 4 formula (single-pass, no separate hue helper).
|
|
34
|
+
* Inputs h in [0, 360], s and l in [0, 100]; channels are clamped implicitly by
|
|
35
|
+
* `Math.min`/`Math.max`. Each channel is individually rounded before hex encoding,
|
|
36
|
+
* so the round-trip `hexToHSL → hslToHex` may differ by ±1 in the last hex digit.
|
|
40
37
|
*/
|
|
41
38
|
export function hslToHex(h: number, s: number, l: number): string {
|
|
42
39
|
s /= 100
|
|
@@ -50,10 +47,8 @@ export function hslToHex(h: number, s: number, l: number): string {
|
|
|
50
47
|
}
|
|
51
48
|
|
|
52
49
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* @param {string} hex - Hex color string (e.g., '#ff5733')
|
|
56
|
-
* @returns {object} RGB object with r, g, b values (0-255)
|
|
50
|
+
* Parses only the 6-digit `#rrggbb` form via `parseInt(..., 16)`.
|
|
51
|
+
* No alpha channel is extracted. Channels are returned as integers in [0, 255].
|
|
57
52
|
*/
|
|
58
53
|
export function hexToRGB(hex: string) {
|
|
59
54
|
const r = parseInt(hex.slice(1, 3), 16)
|
|
@@ -63,12 +58,9 @@ export function hexToRGB(hex: string) {
|
|
|
63
58
|
}
|
|
64
59
|
|
|
65
60
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* @param {number} s - Saturation (0-100)
|
|
70
|
-
* @param {number} l - Lightness (0-100)
|
|
71
|
-
* @returns {object} RGB object with r, g, b values (0-255)
|
|
61
|
+
* Shares the same formula as `hslToHex` but returns separate integer r/g/b channels
|
|
62
|
+
* instead of a hex string. Useful when you need numeric channels for `rgba(...)` output.
|
|
63
|
+
* Input ranges: h [0, 360], s [0, 100], l [0, 100].
|
|
72
64
|
*/
|
|
73
65
|
export function hslToRGB(h: number, s: number, l: number) {
|
|
74
66
|
s /= 100
|
|
@@ -86,10 +78,9 @@ export function hslToRGB(h: number, s: number, l: number) {
|
|
|
86
78
|
}
|
|
87
79
|
|
|
88
80
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* @returns {object} HSL object with h (0-360), s (0-100), l (0-100)
|
|
81
|
+
* Inverse of `hslToRGB`. Normalises each channel from [0, 255] to [0, 1] before
|
|
82
|
+
* computing. When max === min (achromatic), hue is fixed at 0. All output values are
|
|
83
|
+
* rounded, so the round-trip `hslToRGB → rgbToHSL` is not guaranteed to be lossless.
|
|
93
84
|
*/
|
|
94
85
|
export function rgbToHSL(rgb: { r: number; g: number; b: number }): { h: number; s: number; l: number } {
|
|
95
86
|
const r = rgb.r / 255
|
|
@@ -129,11 +120,11 @@ export function rgbToHSL(rgb: { r: number; g: number; b: number }): { h: number;
|
|
|
129
120
|
}
|
|
130
121
|
|
|
131
122
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
123
|
+
* Implements WCAG 2.x relative luminance (https://www.w3.org/TR/WCAG20/#relativeluminancedef).
|
|
124
|
+
* Channels are linearised with the sRGB transfer function: values ≤ 0.03928 use
|
|
125
|
+
* the linear segment (C / 12.92); values above use the gamma curve ((C + 0.055) / 1.055)^2.4.
|
|
126
|
+
* The luminance coefficients are 0.2126 R + 0.7152 G + 0.0722 B, reflecting the
|
|
127
|
+
* eye's greater sensitivity to green. Returns a value in [0, 1].
|
|
137
128
|
*/
|
|
138
129
|
export function getLuminance({ r, g, b }: { r: number; g: number; b: number }): number {
|
|
139
130
|
const [R, G, B] = [r, g, b].map(c => {
|
|
@@ -147,13 +138,10 @@ export function getLuminance({ r, g, b }: { r: number; g: number; b: number }):
|
|
|
147
138
|
}
|
|
148
139
|
|
|
149
140
|
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* @param {string} darkColor - Dark text color option
|
|
155
|
-
* @param {string} lightColor - Light text color option
|
|
156
|
-
* @returns {string} Recommended text color
|
|
141
|
+
* Uses a luminance threshold of **0.5** (not the WCAG 4.5:1 contrast ratio) to pick
|
|
142
|
+
* between two text colours. Backgrounds with luminance > 0.5 are treated as light and
|
|
143
|
+
* receive `darkColor`; all others receive `lightColor`. This is a perceptual shortcut —
|
|
144
|
+
* use explicit contrast-ratio checks for accessibility-critical situations.
|
|
157
145
|
*/
|
|
158
146
|
export function getTextColor(backgroundHex: string, darkColor = 'black', lightColor = 'white'): string {
|
|
159
147
|
const rgb = hexToRGB(backgroundHex)
|
package/src/tools/deepClone.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import rfdc from 'rfdc'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* @param {T} obj - The object or value to clone
|
|
8
|
-
* @returns {T} A deep copy of the input
|
|
4
|
+
* rfdc clone function configured with default options (no circular-reference support,
|
|
5
|
+
* proto: false). Does not handle `Date`, `RegExp`, or `Buffer` fields — they are copied
|
|
6
|
+
* by reference. Use only on plain style/theme objects.
|
|
9
7
|
*/
|
|
10
8
|
export const deepClone = rfdc()
|
package/src/tools/deepmerge.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Re-exports `@fastify/deepmerge`'s factory function. Call it once with options
|
|
3
|
+
* (e.g., `deepmerge({ all: true })`) to obtain a variadic merge function.
|
|
4
|
+
* Passing `{ all: true }` merges arrays by index rather than concatenating them —
|
|
5
|
+
* this is the mode used throughout the style registry for variant merging.
|
|
6
|
+
* The returned merger is not idempotent: source properties always overwrite target.
|
|
7
|
+
*
|
|
6
8
|
* @example
|
|
7
|
-
* const
|
|
8
|
-
* const result =
|
|
9
|
+
* const merge = deepmerge({ all: true })
|
|
10
|
+
* const result = merge(base, override1, override2)
|
|
9
11
|
*/
|
|
10
12
|
export { default as deepmerge } from '@fastify/deepmerge'
|
package/src/tools/hashKey.ts
CHANGED
|
@@ -4,11 +4,10 @@ const styleKey = '@styles-version'
|
|
|
4
4
|
const version = require('../../package.json')?.version
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* @returns {string} SHA-256 hash string
|
|
7
|
+
* Mutates the input array by appending `{ '@styles-version': <pkg.version> }` before
|
|
8
|
+
* hashing — meaning callers must not rely on the array being unchanged after the call.
|
|
9
|
+
* The version injection ensures any cache key computed against an older package version
|
|
10
|
+
* is automatically invalid after a library upgrade.
|
|
12
11
|
*/
|
|
13
12
|
export const hashKey = (value: Array<any>): string => {
|
|
14
13
|
value.push({ [styleKey]: version })
|
package/src/tools/minifier.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { compressToBase64, decompressFromBase64 } from 'lz-string'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* Returns the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Serialises `value` with `JSON.stringify` then LZ-compresses to a Base64 string.
|
|
5
|
+
* Returns the value unchanged (without throwing) when it is falsy, so callers
|
|
6
|
+
* do not need to guard against `null`/`undefined`/`''` inputs.
|
|
7
|
+
* Non-serialisable values (functions, `undefined` inside objects, circular refs)
|
|
8
|
+
* will be silently dropped by `JSON.stringify`.
|
|
9
9
|
*/
|
|
10
10
|
export function compress(value: any): any {
|
|
11
11
|
if (!value) return value
|
|
@@ -14,11 +14,10 @@ export function compress(value: any): any {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
* Returns the
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* @returns {any} Original decompressed value or original falsy value
|
|
17
|
+
* Reverses `compress`: LZ-decompresses the Base64 string then parses the JSON.
|
|
18
|
+
* Returns the value unchanged when falsy. Will throw if `value` is a non-empty
|
|
19
|
+
* string that is not valid LZ-Base64, or if the decompressed payload is not
|
|
20
|
+
* valid JSON — callers should guard accordingly.
|
|
22
21
|
*/
|
|
23
22
|
export function decompress(value: any): any {
|
|
24
23
|
if (!value) return value
|
|
@@ -29,8 +28,8 @@ export function decompress(value: any): any {
|
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* Convenience namespace so callsites can import a single symbol and call
|
|
32
|
+
* `minifier.compress` / `minifier.decompress` without named imports.
|
|
34
33
|
*/
|
|
35
34
|
export const minifier = {
|
|
36
35
|
compress,
|
|
@@ -44,8 +44,8 @@ describe('deepClone', () => {
|
|
|
44
44
|
expect(cloned).not.toBe(original)
|
|
45
45
|
expect(cloned[2]).not.toBe(original[2])
|
|
46
46
|
|
|
47
|
-
cloned[2][0] = 5
|
|
48
|
-
expect(original[2][0]).toBe(3)
|
|
47
|
+
;(cloned[2] as any)[0] = 5
|
|
48
|
+
expect((original[2] as any)[0]).toBe(3)
|
|
49
49
|
})
|
|
50
50
|
|
|
51
51
|
test('should handle complex nested structures', () => {
|