@pyreon/rocketstyle 0.24.5 → 0.24.6

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 (60) hide show
  1. package/package.json +8 -10
  2. package/src/__tests__/attrs-overloads.test.ts +0 -97
  3. package/src/__tests__/attrs.test.ts +0 -190
  4. package/src/__tests__/cache-key-boolean-collision.test.ts +0 -54
  5. package/src/__tests__/chaining.test.ts +0 -86
  6. package/src/__tests__/collection.test.ts +0 -35
  7. package/src/__tests__/compose.test.ts +0 -36
  8. package/src/__tests__/context.test.ts +0 -200
  9. package/src/__tests__/createLocalProvider.test.ts +0 -280
  10. package/src/__tests__/dimensions.test.ts +0 -183
  11. package/src/__tests__/e2e-styler.test.ts +0 -299
  12. package/src/__tests__/hooks.test.ts +0 -178
  13. package/src/__tests__/isRocketComponent.test.ts +0 -48
  14. package/src/__tests__/memo-cap.test.ts +0 -174
  15. package/src/__tests__/minimal-theme.test.ts +0 -62
  16. package/src/__tests__/misc.test.ts +0 -204
  17. package/src/__tests__/native-marker.test.ts +0 -9
  18. package/src/__tests__/providerConsumer.test.ts +0 -183
  19. package/src/__tests__/reactive-props-preservation.test.ts +0 -195
  20. package/src/__tests__/rocketstyle.browser.test.tsx +0 -481
  21. package/src/__tests__/rocketstyleIntegration.test.ts +0 -711
  22. package/src/__tests__/theme-integration.test.tsx +0 -254
  23. package/src/__tests__/themeUtils.test.ts +0 -463
  24. package/src/cache/LocalThemeManager.ts +0 -14
  25. package/src/cache/index.ts +0 -3
  26. package/src/constants/booleanTags.ts +0 -32
  27. package/src/constants/defaultDimensions.ts +0 -23
  28. package/src/constants/index.ts +0 -59
  29. package/src/context/context.ts +0 -70
  30. package/src/context/createLocalProvider.ts +0 -97
  31. package/src/context/localContext.ts +0 -37
  32. package/src/env.d.ts +0 -6
  33. package/src/hoc/index.ts +0 -3
  34. package/src/hoc/rocketstyleAttrsHoc.ts +0 -76
  35. package/src/hooks/index.ts +0 -4
  36. package/src/hooks/usePseudoState.ts +0 -79
  37. package/src/hooks/useTheme.ts +0 -48
  38. package/src/index.ts +0 -95
  39. package/src/init.ts +0 -93
  40. package/src/isRocketComponent.ts +0 -16
  41. package/src/rocketstyle.ts +0 -640
  42. package/src/types/attrs.ts +0 -23
  43. package/src/types/config.ts +0 -48
  44. package/src/types/configuration.ts +0 -69
  45. package/src/types/dimensions.ts +0 -109
  46. package/src/types/hoc.ts +0 -5
  47. package/src/types/pseudo.ts +0 -19
  48. package/src/types/rocketComponent.ts +0 -24
  49. package/src/types/rocketstyle.ts +0 -220
  50. package/src/types/styles.ts +0 -61
  51. package/src/types/theme.ts +0 -18
  52. package/src/types/utils.ts +0 -98
  53. package/src/utils/attrs.ts +0 -181
  54. package/src/utils/chaining.ts +0 -58
  55. package/src/utils/collection.ts +0 -9
  56. package/src/utils/compose.ts +0 -11
  57. package/src/utils/dimensions.ts +0 -126
  58. package/src/utils/statics.ts +0 -44
  59. package/src/utils/styles.ts +0 -18
  60. package/src/utils/theme.ts +0 -211
@@ -1,181 +0,0 @@
1
- import type { MultiKeys } from '../types/dimensions'
2
-
3
- // --------------------------------------------------------
4
- // remove undefined props
5
- // --------------------------------------------------------
6
- /**
7
- * Strips keys with `undefined` values so they don't shadow default props during merging.
8
- *
9
- * Copies own property DESCRIPTORS rather than values so that reactive
10
- * getter-shaped props (compiler-emitted `_rp(() => signal())` converted
11
- * to getters by `makeReactiveProps`) survive the pipeline with their
12
- * subscription intact. Reading `props[key]` here would fire the getter
13
- * at HOC setup time (outside any tracking scope) and collapse the prop
14
- * to a static value — every downstream JSX accessor that reads
15
- * `props.x` would see the captured-once value, not the live signal.
16
- *
17
- * For getter descriptors we keep the descriptor as-is (the
18
- * undefined-filter doesn't apply — we can't peek into the getter
19
- * without firing it). For data descriptors we drop entries whose
20
- * value is `undefined` to preserve the original merge semantics.
21
- */
22
- type RemoveUndefinedProps = <T extends Record<string, any>>(props: T) => Partial<T>
23
-
24
- export const removeUndefinedProps: RemoveUndefinedProps = (props) => {
25
- const result: Partial<typeof props> = {}
26
- const descriptors = Object.getOwnPropertyDescriptors(props)
27
- for (const key of Object.keys(descriptors)) {
28
- const d = descriptors[key]!
29
- if (d.get || d.value !== undefined) {
30
- Object.defineProperty(result, key, d)
31
- }
32
- }
33
- return result
34
- }
35
-
36
- // --------------------------------------------------------
37
- // merge descriptors
38
- // --------------------------------------------------------
39
- /**
40
- * Like `Object.assign(target, ...sources)` but copies own property
41
- * DESCRIPTORS instead of reading + writing values. Later sources
42
- * override earlier ones (same semantics as spread / Object.assign).
43
- *
44
- * Required for reactive-prop preservation through the rocketstyle
45
- * pipeline: a plain `{ ...A, ...B }` spread fires every getter on A
46
- * and B and stores the resolved value, breaking the reactive
47
- * subscription. This helper copies descriptors so getters survive
48
- * the merge.
49
- */
50
- export const mergeDescriptors = (
51
- ...sources: ReadonlyArray<Record<string, any> | null | undefined>
52
- ): Record<string, any> => {
53
- const result: Record<string, any> = {}
54
- for (const source of sources) {
55
- if (!source) continue
56
- const descriptors = Object.getOwnPropertyDescriptors(source)
57
- for (const key of Object.keys(descriptors)) {
58
- Object.defineProperty(result, key, descriptors[key]!)
59
- }
60
- }
61
- return result
62
- }
63
-
64
- // --------------------------------------------------------
65
- // pick styled props
66
- // --------------------------------------------------------
67
- /** Picks only the props whose keys exist in the dimension keywords lookup and have truthy values. */
68
- export const pickStyledAttrs = <
69
- T extends Record<string, any>,
70
- K extends Record<string, true | undefined>,
71
- >(
72
- props: T,
73
- keywords: K,
74
- ): { [I in keyof K & keyof T]: T[I] } => {
75
- // Direct `for...in` avoids the `Object.keys(props)` array allocation
76
- // that `for (const key of Object.keys(props))` paid on every render.
77
- // The hot path is rocketstyle's `EnhancedComponent` body — fires once
78
- // per render of every rocketstyle-wrapped component. Ported from
79
- // vitus-labs `00fdadc2`.
80
- const result: Record<string, unknown> = {}
81
- for (const key in props) {
82
- if (keywords[key] && props[key]) result[key] = props[key]
83
- }
84
- return result as { [I in keyof K & keyof T]: T[I] }
85
- }
86
-
87
- // --------------------------------------------------------
88
- // combine values
89
- // --------------------------------------------------------
90
- /**
91
- * Returns a curried function that evaluates an array of `.attrs()` callbacks,
92
- * spreading each result into a single merged props object via `Object.assign`.
93
- */
94
- type OptionFunc<A> = (...arg: A[]) => Record<string, unknown>
95
- type CalculateChainOptions = <A>(
96
- options?: OptionFunc<A>[],
97
- ) => (args: A[]) => ReturnType<OptionFunc<A>>
98
-
99
- export const calculateChainOptions: CalculateChainOptions = (options) => (args) => {
100
- if (!options || options.length === 0) return {}
101
-
102
- return options.reduce<Record<string, unknown>>(
103
- (acc, item) => Object.assign(acc, item(...args)),
104
- {},
105
- )
106
- }
107
-
108
- // --------------------------------------------------------
109
- // get style attributes
110
- // --------------------------------------------------------
111
- /**
112
- * Resolves the active value for each styling dimension from component props.
113
- * First checks for explicit prop values (string, number, or array for multi-keys),
114
- * then falls back to boolean shorthand props when `useBooleans` is enabled.
115
- */
116
- type CalculateStylingAttrs = ({
117
- useBooleans,
118
- multiKeys,
119
- }: {
120
- useBooleans?: boolean
121
- multiKeys?: MultiKeys
122
- }) => ({
123
- props,
124
- dimensions,
125
- }: {
126
- props: Record<string, unknown>
127
- dimensions: Record<string, unknown>
128
- }) => Record<string, any>
129
- export const calculateStylingAttrs: CalculateStylingAttrs =
130
- ({ useBooleans, multiKeys }) =>
131
- ({ props, dimensions }) => {
132
- const result: Record<string, any> = {}
133
-
134
- // (1) find dimension keys values & initialize
135
- for (const item in dimensions) {
136
- const pickedProp = props[item]
137
- const t = typeof pickedProp
138
-
139
- if (multiKeys?.[item] && Array.isArray(pickedProp)) {
140
- result[item] = pickedProp
141
- } else if (t === 'string' || t === 'number') {
142
- result[item] = pickedProp
143
- } else {
144
- result[item] = undefined
145
- }
146
- }
147
-
148
- // (2) if booleans are being used let's find the rest
149
- // Use `in` operator on the dimension map instead of allocating
150
- // a new Set per dimension — the map is already an object with
151
- // the keywords as keys.
152
- if (useBooleans) {
153
- for (const key in result) {
154
- if (result[key]) continue // already assigned
155
-
156
- const dimensionMap = dimensions[key] as Record<string, unknown>
157
- const isMultiKey = multiKeys?.[key]
158
- let newDimensionValue: string | string[] | undefined
159
-
160
- if (isMultiKey) {
161
- const matches: string[] = []
162
- for (const propKey in props) {
163
- if (propKey in dimensionMap) matches.push(propKey)
164
- }
165
- newDimensionValue = matches.length > 0 ? matches : undefined
166
- } else {
167
- // Iterate props to find last matching keyword
168
- // (last wins for priority)
169
- for (const k in props) {
170
- if (k in dimensionMap && props[k]) {
171
- newDimensionValue = k
172
- }
173
- }
174
- }
175
-
176
- result[key] = newDimensionValue
177
- }
178
- }
179
-
180
- return result
181
- }
@@ -1,58 +0,0 @@
1
- type Func = (...args: any) => Record<string, unknown>
2
- type Obj = Record<string, unknown>
3
-
4
- // --------------------------------------------------------
5
- // Chain Options
6
- // --------------------------------------------------------
7
- /**
8
- * Appends a new option (function or plain object) to an existing chain
9
- * of option callbacks. Objects are wrapped in a thunk for uniform handling.
10
- */
11
- type ChainOptions = (opts: Obj | Func | undefined, defaultOpts: Func[]) => Func[]
12
-
13
- export const chainOptions: ChainOptions = (opts, defaultOpts = []) => {
14
- const result = [...defaultOpts]
15
-
16
- if (typeof opts === 'function') result.push(opts)
17
- else if (typeof opts === 'object') result.push(() => opts)
18
-
19
- return result
20
- }
21
-
22
- // --------------------------------------------------------
23
- // Chain Or Options
24
- // --------------------------------------------------------
25
- /**
26
- * For each key, picks the new value if truthy, otherwise falls back
27
- * to the default. Used for config keys that replace rather than merge.
28
- */
29
- type ChainOrOptions = (
30
- keys: readonly string[],
31
- opts: Obj,
32
- defaultOpts: Obj,
33
- ) => Record<string, unknown>
34
-
35
- export const chainOrOptions: ChainOrOptions = (keys, opts, defaultOpts) =>
36
- keys.reduce((acc, item) => ({ ...acc, [item]: opts[item] || defaultOpts[item] }), {})
37
-
38
- // --------------------------------------------------------
39
- // Chain Reserved Options
40
- // --------------------------------------------------------
41
- /**
42
- * Chains option callbacks for reserved dimension and styling keys,
43
- * delegating to `chainOptions` for each key individually.
44
- */
45
- type ChainReservedKeyOptions = (
46
- keys: readonly string[],
47
- opts: Record<string, Obj | Func>,
48
- defaultOpts: Record<string, Func[]>,
49
- ) => Record<string, ReturnType<typeof chainOptions>>
50
-
51
- export const chainReservedKeyOptions: ChainReservedKeyOptions = (keys, opts, defaultOpts) =>
52
- keys.reduce(
53
- (acc, item) => ({
54
- ...acc,
55
- [item]: chainOptions(opts[item], defaultOpts[item] ?? []),
56
- }),
57
- {},
58
- )
@@ -1,9 +0,0 @@
1
- // --------------------------------------------------------
2
- // Remove Nullable values
3
- // --------------------------------------------------------
4
- /** Filters out entries with `null`, `undefined`, or `false` values from an object. */
5
- type RemoveNullableValues = (obj: Record<string, any>) => Record<string, any>
6
- export const removeNullableValues: RemoveNullableValues = (obj) =>
7
- Object.entries(obj)
8
- .filter(([, v]) => v != null && v !== false)
9
- .reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {})
@@ -1,11 +0,0 @@
1
- /**
2
- * Extracts HOC functions from the compose configuration object,
3
- * filters out non-function entries, and reverses them so the
4
- * outermost HOC in the chain wraps first (inside-out composition).
5
- */
6
- type CalculateHocsFuncs = (options: Record<string, any>) => ((arg: any) => any)[]
7
-
8
- export const calculateHocsFuncs: CalculateHocsFuncs = (options = {}) =>
9
- Object.values(options)
10
- .filter((item) => typeof item === 'function')
11
- .reverse()
@@ -1,126 +0,0 @@
1
- import { get, isEmpty, set } from '@pyreon/ui-core'
2
- import type { Dimensions, DimensionValue, MultiKeys } from '../types/dimensions'
3
-
4
- // --------------------------------------------------------
5
- // Is value a valid key
6
- // --------------------------------------------------------
7
- /** Checks whether a dimension value is defined and not explicitly disabled (false). */
8
- type IsValidKey = (value: any) => boolean
9
- export const isValidKey: IsValidKey = (value) =>
10
- value !== undefined && value !== null && value !== false
11
-
12
- // --------------------------------------------------------
13
- // Is value a multi key
14
- // --------------------------------------------------------
15
- /** Determines if a dimension value is a multi-key config object, returning [isMulti, propName]. */
16
- type IsMultiKey = (value: string | Record<string, unknown>) => [boolean, string]
17
- export const isMultiKey: IsMultiKey = (value) => {
18
- if (typeof value === 'object' && value !== null) return [true, get(value, 'propName') as string]
19
- return [false, value]
20
- }
21
-
22
- // --------------------------------------------------------
23
- // calculate dimensions map
24
- // --------------------------------------------------------
25
- /**
26
- * Builds a two-level map (`keysMap`) of dimension -> option -> true,
27
- * and a flat `keywords` lookup of all prop names reserved by dimensions
28
- * (including boolean shorthand keys when `useBooleans` is enabled).
29
- */
30
- type GetDimensionsMap = <T extends Record<string, any>>({
31
- themes,
32
- useBooleans,
33
- }: {
34
- themes: T
35
- useBooleans?: boolean
36
- }) => { keysMap: Record<string, any>; keywords: Record<string, any> }
37
-
38
- export const getDimensionsMap: GetDimensionsMap = ({ themes, useBooleans }) => {
39
- const result = {
40
- keysMap: {} as Record<string, any>,
41
- keywords: {} as Record<string, any>,
42
- }
43
-
44
- if (isEmpty(themes)) return result
45
-
46
- return Object.entries(themes).reduce((accumulator, [key, value]) => {
47
- const { keysMap, keywords } = accumulator
48
- keywords[key] = true
49
-
50
- Object.entries(value).forEach(([itemKey, itemValue]) => {
51
- if (!isValidKey(itemValue)) return
52
-
53
- if (useBooleans) {
54
- keywords[itemKey] = true
55
- }
56
-
57
- set(keysMap, [key, itemKey], true)
58
- })
59
-
60
- return accumulator
61
- }, result)
62
- }
63
-
64
- // --------------------------------------------------------
65
- // simple object getters
66
- // --------------------------------------------------------
67
- /** Returns the keys of an object with proper typing. */
68
- type GetKeys = <T extends Record<string, unknown>>(obj: T) => Array<keyof T>
69
- export const getKeys: GetKeys = (obj) => Object.keys(obj)
70
-
71
- type GetValues = <T extends Record<string, unknown>, K extends keyof T>(obj: T) => T[K][]
72
- export const getValues = (<T extends Record<string, unknown>>(obj: T) =>
73
- Object.values(obj)) as GetValues
74
-
75
- // --------------------------------------------------------
76
- // get dimensions values array
77
- // --------------------------------------------------------
78
- /** Extracts the prop name from each dimension value, unwrapping multi-key config objects. */
79
- type ValueType<T> = T extends string ? T : T[][0]
80
- type GetDimensionsValues = <T extends Dimensions, K extends keyof T>(obj: T) => ValueType<T[K]>[]
81
-
82
- export const getDimensionsValues = (<T extends Dimensions>(obj: T) =>
83
- getValues(obj).map((item: DimensionValue) => {
84
- if (typeof item === 'object') {
85
- return item.propName as string
86
- }
87
-
88
- return item
89
- })) as GetDimensionsValues
90
-
91
- // --------------------------------------------------------
92
- // get multiple dimensions map
93
- // --------------------------------------------------------
94
- /** Builds a lookup of dimension prop names that accept multiple simultaneous values. */
95
- type GetMultipleDimensions = <T extends Dimensions>(obj: T) => MultiKeys<T>
96
-
97
- export const getMultipleDimensions: GetMultipleDimensions = (obj) =>
98
- getValues(obj).reduce(
99
- (accumulator, value: DimensionValue) => {
100
- if (typeof value === 'object') {
101
- if (value.multi === true) accumulator[value.propName] = true
102
- }
103
-
104
- return accumulator
105
- },
106
- {} as Record<string, true>,
107
- )
108
-
109
- // --------------------------------------------------------
110
- // get transform dimensions map
111
- // --------------------------------------------------------
112
- /** Builds a lookup of dimension prop names that are transform dimensions (evaluated last with function values). */
113
- type TransformKeys = Partial<Record<string, true>>
114
- type GetTransformDimensions = <T extends Dimensions>(obj: T) => TransformKeys
115
-
116
- export const getTransformDimensions: GetTransformDimensions = (obj) =>
117
- getValues(obj).reduce(
118
- (accumulator, value: DimensionValue) => {
119
- if (typeof value === 'object') {
120
- if (value.transform === true) accumulator[value.propName] = true
121
- }
122
-
123
- return accumulator
124
- },
125
- {} as Record<string, true>,
126
- )
@@ -1,44 +0,0 @@
1
- import { isEmpty } from '@pyreon/ui-core'
2
- import { STATIC_KEYS } from '../constants'
3
-
4
- // --------------------------------------------------------
5
- // helpers for create statics chaining methods on component
6
- // --------------------------------------------------------
7
- /**
8
- * Attaches chaining static methods (e.g. `.states()`, `.sizes()`, `.theme()`)
9
- * to a component. Each method calls `cloneAndEnhance` with the corresponding key.
10
- */
11
- type CreateStaticsChainingEnhancers = <O extends Record<string, any>, DK extends string[]>(props: {
12
- context: Record<string, any>
13
- dimensionKeys: DK
14
- func: (defaultOpts: O, opts: any) => void
15
- options: O
16
- }) => void
17
-
18
- export const createStaticsChainingEnhancers: CreateStaticsChainingEnhancers = ({
19
- context,
20
- dimensionKeys,
21
- func,
22
- options,
23
- }) => {
24
- const keys = [...dimensionKeys, ...STATIC_KEYS]
25
-
26
- keys.forEach((item) => {
27
- context[item] = (props: any) => func(options, { [item]: props })
28
- })
29
- }
30
-
31
- // --------------------------------------------------------
32
- // helpers for create statics on component
33
- // --------------------------------------------------------
34
- /** Copies user-defined static properties onto the component's `meta` object. */
35
- type CreateStaticsEnhancers = (params: {
36
- context: Record<string, any>
37
- options: Record<string, any>
38
- }) => void
39
-
40
- export const createStaticsEnhancers: CreateStaticsEnhancers = ({ context, options }) => {
41
- if (!isEmpty(options)) {
42
- Object.assign(context, options)
43
- }
44
- }
@@ -1,18 +0,0 @@
1
- import { config } from '@pyreon/ui-core'
2
- import type { StylesCbArray } from '../types/styles'
3
-
4
- // --------------------------------------------------------
5
- // Calculate styles
6
- // --------------------------------------------------------
7
- /**
8
- * Evaluates an array of style callback functions with the configured
9
- * `css` tagged-template helper, producing the final CSS interpolations
10
- * to be passed into the styled-component template literal.
11
- */
12
- type CalculateStyles = (styles: StylesCbArray | undefined) => ReturnType<StylesCbArray[number]>[]
13
-
14
- export const calculateStyles: CalculateStyles = (styles) => {
15
- if (!styles) return []
16
-
17
- return styles.map((item) => item(config.css as Parameters<typeof item>[0]))
18
- }
@@ -1,211 +0,0 @@
1
- import { config, isEmpty, merge } from '@pyreon/ui-core'
2
- import type { ThemeModeCallback } from '../types/theme'
3
- import { removeNullableValues } from './collection'
4
- import { isMultiKey } from './dimensions'
5
-
6
- // --------------------------------------------------------
7
- // Theme Mode Callback
8
- // --------------------------------------------------------
9
- const MODE_CALLBACK_BRAND = Symbol.for('pyreon.themeModeCallback')
10
-
11
- /** Creates a mode-switching function that returns the light or dark value based on the active mode. */
12
- export const themeModeCallback: ThemeModeCallback = (light, dark) => {
13
- const fn = (mode: string) => {
14
- if (!mode || mode === 'light') return light
15
- return dark
16
- }
17
- ;(fn as unknown as Record<string, unknown>).__brand = MODE_CALLBACK_BRAND
18
- return fn
19
- }
20
-
21
- // --------------------------------------------------------
22
- // Theme Mode Callback Check
23
- // --------------------------------------------------------
24
- /** Detects whether a value is a `themeModeCallback` function via Symbol brand. */
25
- type IsModeCallback = (value: unknown) => boolean
26
- const isModeCallback: IsModeCallback = (value: unknown) =>
27
- typeof value === 'function' &&
28
- (value as unknown as Record<string, unknown>).__brand === MODE_CALLBACK_BRAND
29
-
30
- // --------------------------------------------------------
31
- // Get Theme From Chain
32
- // --------------------------------------------------------
33
- /** Reduces an array of chained `.theme()` callbacks into a single merged theme object. */
34
- type OptionFunc = (...arg: any) => Record<string, unknown>
35
- type GetThemeFromChain = (
36
- options: OptionFunc[] | undefined | null,
37
- theme: Record<string, any>,
38
- ) => ReturnType<OptionFunc>
39
-
40
- export const getThemeFromChain: GetThemeFromChain = (options, theme) => {
41
- const result = {}
42
- if (!options || isEmpty(options)) return result
43
-
44
- return options.reduce(
45
- (acc, item) => merge(acc, item(theme, themeModeCallback, config.css)),
46
- result,
47
- )
48
- }
49
-
50
- // --------------------------------------------------------
51
- // calculate dimension themes
52
- // --------------------------------------------------------
53
- /**
54
- * Computes the theme object for each dimension by evaluating its
55
- * chained callbacks against the global theme, then strips nullable values.
56
- */
57
- type GetDimensionThemes = (
58
- theme: Record<string, any>,
59
- options: Record<string, any>,
60
- ) => Record<string, any>
61
-
62
- export const getDimensionThemes: GetDimensionThemes = (theme, options) => {
63
- const dims = options.dimensions
64
- if (isEmpty(dims)) return {}
65
-
66
- const result: Record<string, any> = {}
67
-
68
- for (const key in dims) {
69
- const [, dimension] = isMultiKey(dims[key] as string | Record<string, unknown>)
70
- const helper = options[key]
71
-
72
- if (Array.isArray(helper) && helper.length > 0) {
73
- result[dimension] = removeNullableValues(getThemeFromChain(helper, theme))
74
- }
75
- }
76
-
77
- return result
78
- }
79
-
80
- // --------------------------------------------------------
81
- // combine values
82
- // --------------------------------------------------------
83
- /** Reduces an array of option callbacks by calling each with the given args and deep-merging results. */
84
- type CalculateChainOptions = (
85
- options: OptionFunc[] | undefined | null,
86
- args: any[],
87
- ) => Record<string, any>
88
-
89
- export const calculateChainOptions: CalculateChainOptions = (options, args) => {
90
- const result = {}
91
- if (!options || isEmpty(options)) return result
92
-
93
- return options.reduce((acc, item) => merge(acc, item(...args)), result)
94
- }
95
-
96
- // --------------------------------------------------------
97
- // generate theme
98
- // --------------------------------------------------------
99
- /**
100
- * Generates the final theme object by starting with the base theme
101
- * and merging in dimension-specific theme slices based on the current
102
- * rocketstate (active dimension values). Supports multi-key dimensions.
103
- *
104
- * Transform dimensions (marked with `transform: true`) are evaluated last.
105
- * Their values are functions that receive the fully accumulated theme and
106
- * return overrides — enabling derived styles like "outlined" or "inversed".
107
- */
108
- export type GetTheme = (params: {
109
- rocketstate: Record<string, string | string[]>
110
- themes: Record<string, Record<string, any>>
111
- baseTheme: Record<string, any>
112
- transformKeys?: Partial<Record<string, true>>
113
- /** App theme from context — passed to transform dimension callbacks. */
114
- appTheme?: Record<string, any>
115
- }) => Record<string, unknown>
116
-
117
- // Shared empty object for pseudo-state defaults — allocated once, reused by
118
- // every getTheme call. Frozen to prevent accidental mutation.
119
- const EMPTY_PSEUDO: Record<string, never> = Object.freeze({}) as Record<string, never>
120
-
121
- export const getTheme: GetTheme = ({ rocketstate, themes, baseTheme, transformKeys, appTheme }) => {
122
- // Spread baseTheme into result — this is unavoidable (we must not mutate
123
- // the cached baseTheme). But we merge dimension slices in-place onto
124
- // finalTheme instead of creating a new {} target each merge() call.
125
- const finalTheme: Record<string, any> = { ...baseTheme }
126
- type TransformFn = (
127
- currentTheme: Record<string, any>,
128
- currentAppTheme: Record<string, any>,
129
- mode: typeof themeModeCallback,
130
- cssFn: typeof config.css,
131
- ) => Record<string, any>
132
- const deferredTransforms: TransformFn[] = []
133
-
134
- for (const key in rocketstate) {
135
- const value = rocketstate[key]
136
- if (value == null) continue
137
- const keyTheme: Record<string, any> = themes[key] ?? {}
138
- const isTransform = transformKeys?.[key]
139
-
140
- const mergeValue = (item: string) => {
141
- const val = keyTheme[item]
142
- if (val == null) return
143
- if (isTransform && typeof val === 'function') {
144
- deferredTransforms.push(val as TransformFn)
145
- } else {
146
- // Merge in-place onto finalTheme — avoids allocating a fresh {}
147
- // as merge target on every dimension slice.
148
- merge(finalTheme, val)
149
- }
150
- }
151
-
152
- if (Array.isArray(value)) {
153
- for (let i = 0; i < value.length; i++) mergeValue(value[i] as string)
154
- } else {
155
- mergeValue(value as string)
156
- }
157
- }
158
-
159
- // Apply transform dimension values last with the fully accumulated theme
160
- for (let i = 0; i < deferredTransforms.length; i++) {
161
- merge(finalTheme, deferredTransforms[i]!(finalTheme, appTheme ?? {}, themeModeCallback, config.css))
162
- }
163
-
164
- // Ensure pseudo-state keys always exist as objects so .styles() can
165
- // destructure without defaults: const { hover, focus, ... } = $rocketstyle
166
- // Uses a frozen shared empty object instead of allocating 6 new {} per call.
167
- finalTheme.hover ??= EMPTY_PSEUDO
168
- finalTheme.focus ??= EMPTY_PSEUDO
169
- finalTheme.active ??= EMPTY_PSEUDO
170
- finalTheme.disabled ??= EMPTY_PSEUDO
171
- finalTheme.pressed ??= EMPTY_PSEUDO
172
- finalTheme.readOnly ??= EMPTY_PSEUDO
173
-
174
- return finalTheme
175
- }
176
-
177
- // --------------------------------------------------------
178
- // resolve theme by mode
179
- // --------------------------------------------------------
180
- /**
181
- * Recursively traverses a theme object and resolves any `themeModeCallback`
182
- * functions to their concrete light or dark values for the given mode.
183
- */
184
- export type GetThemeByMode = (
185
- object: Record<string, any>,
186
- mode: 'light' | 'dark',
187
- ) => Partial<{
188
- baseTheme: Record<string, unknown>
189
- themes: Record<string, unknown>
190
- }>
191
-
192
- export const getThemeByMode: GetThemeByMode = (object, mode) => {
193
- // Recursive theme walker — `for...in` avoids the per-node
194
- // `Object.keys` array allocation that the prior reduce paid. Called
195
- // from inside cached `LocalThemeManager` WeakMap tiers (one per
196
- // theme/mode transition), so the win is smaller than per-render
197
- // helpers but the pattern is consistent. Ported from vitus-labs
198
- // `00fdadc2`.
199
- const acc: Record<string, any> = {}
200
- for (const key in object) {
201
- const value = object[key]
202
- if (typeof value === 'object' && value !== null) {
203
- acc[key] = getThemeByMode(value, mode)
204
- } else if (isModeCallback(value)) {
205
- acc[key] = value(mode)
206
- } else {
207
- acc[key] = value
208
- }
209
- }
210
- return acc
211
- }