@pyreon/rocketstyle 0.13.1 → 0.14.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/README.md +17 -1
- package/lib/index.d.ts +14 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +103 -67
- package/lib/index.js.map +1 -1
- package/package.json +8 -8
- package/src/__tests__/context.test.ts +15 -15
- package/src/__tests__/createLocalProvider.test.ts +33 -1
- package/src/__tests__/rocketstyle.browser.test.tsx +36 -0
- package/src/constants/index.ts +10 -0
- package/src/init.ts +2 -2
- package/src/rocketstyle.ts +118 -34
- package/src/types/rocketstyle.ts +15 -0
- package/src/utils/attrs.ts +25 -29
- package/src/utils/theme.ts +47 -44
package/src/constants/index.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
// `import.meta.env.DEV` is provided by Vite/Rolldown at build time and
|
|
2
|
+
// literal-replaced so prod bundles tree-shake the dev branch to zero bytes.
|
|
3
|
+
// Typed through a narrowing interface so downstream packages don't need
|
|
4
|
+
// `vite/client` in their tsconfigs to type-check this file transitively.
|
|
5
|
+
interface ViteMeta {
|
|
6
|
+
readonly env?: { readonly DEV?: boolean }
|
|
7
|
+
}
|
|
8
|
+
/** Tree-shakeable dev-mode flag. `true` in dev, `false` (dead code eliminated) in prod. */
|
|
9
|
+
export const __DEV__: boolean = (import.meta as ViteMeta).env?.DEV === true
|
|
10
|
+
|
|
1
11
|
/** Default theme mode used when no mode is provided via context. */
|
|
2
12
|
export const MODE_DEFAULT = 'light'
|
|
3
13
|
|
package/src/init.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isEmpty } from '@pyreon/ui-core'
|
|
2
|
-
import { ALL_RESERVED_KEYS } from './constants'
|
|
2
|
+
import { ALL_RESERVED_KEYS, __DEV__ } from './constants'
|
|
3
3
|
import defaultDimensions from './constants/defaultDimensions'
|
|
4
4
|
import rocketComponent from './rocketstyle'
|
|
5
5
|
import type { DefaultDimensions, Dimensions } from './types/dimensions'
|
|
@@ -73,7 +73,7 @@ const validateInit = (name: string, component: unknown, dimensions: Dimensions)
|
|
|
73
73
|
|
|
74
74
|
const rocketstyle = (({ dimensions = defaultDimensions, useBooleans = false } = {}) =>
|
|
75
75
|
({ name, component }: { name: string; component: any }) => {
|
|
76
|
-
if (
|
|
76
|
+
if (__DEV__) {
|
|
77
77
|
validateInit(name, component, dimensions)
|
|
78
78
|
}
|
|
79
79
|
|
package/src/rocketstyle.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { compose, config, hoistNonReactStatics, omit, pick, render } from '@pyreon/ui-core'
|
|
2
2
|
import { LocalThemeManager } from './cache'
|
|
3
|
-
import { CONFIG_KEYS, PSEUDO_KEYS, PSEUDO_META_KEYS, STYLING_KEYS } from './constants'
|
|
3
|
+
import { CONFIG_KEYS, PSEUDO_KEYS, PSEUDO_META_KEYS, STYLING_KEYS, __DEV__ } from './constants'
|
|
4
4
|
import createLocalProvider from './context/createLocalProvider'
|
|
5
5
|
import { useLocalContext } from './context/localContext'
|
|
6
6
|
import { rocketstyleAttrsHoc } from './hoc'
|
|
@@ -17,6 +17,12 @@ import { createStaticsChainingEnhancers, createStaticsEnhancers } from './utils/
|
|
|
17
17
|
import { calculateStyles } from './utils/styles'
|
|
18
18
|
import { getDimensionThemes, getTheme, getThemeByMode, getThemeFromChain } from './utils/theme'
|
|
19
19
|
|
|
20
|
+
// Dev-time counter sink — see packages/internals/perf-harness for contract.
|
|
21
|
+
interface ViteMeta {
|
|
22
|
+
readonly env?: { readonly DEV?: boolean }
|
|
23
|
+
}
|
|
24
|
+
const _countSink = globalThis as { __pyreon_count__?: (name: string, n?: number) => void }
|
|
25
|
+
|
|
20
26
|
/**
|
|
21
27
|
* Core rocketstyle component factory. Creates a fully-featured Pyreon component
|
|
22
28
|
* that integrates theme management (with light/dark mode support), multi-tier
|
|
@@ -87,6 +93,27 @@ const rocketComponent: RocketComponent = (options) => {
|
|
|
87
93
|
// --------------------------------------------------------
|
|
88
94
|
const ThemeManager = new LocalThemeManager()
|
|
89
95
|
|
|
96
|
+
// ── Per-definition caches (shared across all instances) ──────────────
|
|
97
|
+
// getDimensionsMap + Object.keys(reservedPropNames) are theme-independent
|
|
98
|
+
// (dimension structure comes from .sizes()/.states()/.variants() chain,
|
|
99
|
+
// not from runtime theme values). Cache them so 50 instances of the same
|
|
100
|
+
// component definition skip the rebuild entirely.
|
|
101
|
+
const _dimensionsCache = new WeakMap<
|
|
102
|
+
object,
|
|
103
|
+
{ keysMap: Record<string, unknown>; keywords: Record<string, true | undefined> }
|
|
104
|
+
>()
|
|
105
|
+
const _reservedKeysCache = new WeakMap<object, string[]>()
|
|
106
|
+
|
|
107
|
+
// Pre-compute merged key arrays once per definition (not per mount)
|
|
108
|
+
const ALL_PSEUDO_KEYS = [...PSEUDO_KEYS, ...PSEUDO_META_KEYS]
|
|
109
|
+
// Static portion of omit keys — PSEUDO_KEYS + filterAttrs + 'pseudo' are definition-scoped.
|
|
110
|
+
// RESERVED_STYLING_PROPS_KEYS is dimension-dependent but also cached per definition.
|
|
111
|
+
// 'pseudo' is included here so we can skip the destructuring spread of mergeProps.
|
|
112
|
+
const STATIC_OMIT_KEYS = ['pseudo', ...PSEUDO_KEYS, ...(options.filterAttrs ?? [])]
|
|
113
|
+
// Pre-built Set for omit() — avoids per-call Set allocation. Built once the
|
|
114
|
+
// dimension-dependent reserved keys are known (first mount), then reused.
|
|
115
|
+
const _omitSetCache = new WeakMap<string[], Set<string>>()
|
|
116
|
+
|
|
90
117
|
// --------------------------------------------------------
|
|
91
118
|
// COMPOSE - high-order components
|
|
92
119
|
// --------------------------------------------------------
|
|
@@ -140,12 +167,27 @@ const rocketComponent: RocketComponent = (options) => {
|
|
|
140
167
|
return helper.get(initialTheme)
|
|
141
168
|
})()
|
|
142
169
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
170
|
+
// Cache getDimensionsMap per dimension-themes identity — all instances
|
|
171
|
+
// of the same component definition share the same dimension structure.
|
|
172
|
+
let dimResult = _dimensionsCache.get(initialDimensionThemes as object)
|
|
173
|
+
if (dimResult) {
|
|
174
|
+
if ((import.meta as ViteMeta).env?.DEV === true)
|
|
175
|
+
_countSink.__pyreon_count__?.('rocketstyle.dimensionsMap.hit')
|
|
176
|
+
} else {
|
|
177
|
+
dimResult = getDimensionsMap({
|
|
178
|
+
themes: initialDimensionThemes,
|
|
179
|
+
useBooleans: options.useBooleans,
|
|
180
|
+
})
|
|
181
|
+
_dimensionsCache.set(initialDimensionThemes as object, dimResult)
|
|
182
|
+
}
|
|
183
|
+
const { keysMap: dimensions, keywords: reservedPropNames } = dimResult
|
|
147
184
|
|
|
148
|
-
|
|
185
|
+
// Cache Object.keys() result — same dimension structure = same keys
|
|
186
|
+
let RESERVED_STYLING_PROPS_KEYS = _reservedKeysCache.get(reservedPropNames as object)
|
|
187
|
+
if (!RESERVED_STYLING_PROPS_KEYS) {
|
|
188
|
+
RESERVED_STYLING_PROPS_KEYS = Object.keys(reservedPropNames)
|
|
189
|
+
_reservedKeysCache.set(reservedPropNames as object, RESERVED_STYLING_PROPS_KEYS)
|
|
190
|
+
}
|
|
149
191
|
|
|
150
192
|
// --------------------------------------------------
|
|
151
193
|
// $rocketstyle as a FUNCTION ACCESSOR — fully reactive.
|
|
@@ -154,6 +196,8 @@ const rocketComponent: RocketComponent = (options) => {
|
|
|
154
196
|
// (signals, getters) produce updated dimension values.
|
|
155
197
|
// --------------------------------------------------
|
|
156
198
|
const $rocketstyleAccessor = () => {
|
|
199
|
+
if ((import.meta as ViteMeta).env?.DEV === true)
|
|
200
|
+
_countSink.__pyreon_count__?.('rocketstyle.getTheme')
|
|
157
201
|
// Read theme + mode LAZILY via the getter-backed themeAttrs object.
|
|
158
202
|
// Both reads are tracked when this accessor runs inside a reactive
|
|
159
203
|
// scope (styler's effect), so theme swap / mode toggle re-runs the
|
|
@@ -165,13 +209,19 @@ const rocketComponent: RocketComponent = (options) => {
|
|
|
165
209
|
// keyed on theme identity — stable-theme renders hit cache in O(1),
|
|
166
210
|
// theme swaps fall through to recompute (once per new theme).
|
|
167
211
|
const baseThemeHelper = ThemeManager.baseTheme
|
|
168
|
-
if (
|
|
212
|
+
if (baseThemeHelper.has(theme)) {
|
|
213
|
+
if ((import.meta as ViteMeta).env?.DEV === true)
|
|
214
|
+
_countSink.__pyreon_count__?.('rocketstyle.localThemeManager.hit')
|
|
215
|
+
} else {
|
|
169
216
|
baseThemeHelper.set(theme, getThemeFromChain(options.theme, theme))
|
|
170
217
|
}
|
|
171
218
|
const baseTheme = baseThemeHelper.get(theme)
|
|
172
219
|
|
|
173
220
|
const dimHelper = ThemeManager.dimensionsThemes
|
|
174
|
-
if (
|
|
221
|
+
if (dimHelper.has(theme)) {
|
|
222
|
+
if ((import.meta as ViteMeta).env?.DEV === true)
|
|
223
|
+
_countSink.__pyreon_count__?.('rocketstyle.localThemeManager.hit')
|
|
224
|
+
} else {
|
|
175
225
|
dimHelper.set(theme, getDimensionThemes(theme, options))
|
|
176
226
|
}
|
|
177
227
|
const themes = dimHelper.get(theme)
|
|
@@ -184,13 +234,19 @@ const rocketComponent: RocketComponent = (options) => {
|
|
|
184
234
|
|
|
185
235
|
// Resolve mode-specific theme
|
|
186
236
|
const modeBaseHelper = ThemeManager.modeBaseTheme[mode]
|
|
187
|
-
if (
|
|
237
|
+
if (modeBaseHelper.has(baseTheme)) {
|
|
238
|
+
if ((import.meta as ViteMeta).env?.DEV === true)
|
|
239
|
+
_countSink.__pyreon_count__?.('rocketstyle.localThemeManager.hit')
|
|
240
|
+
} else {
|
|
188
241
|
modeBaseHelper.set(baseTheme, getThemeByMode(baseTheme, mode))
|
|
189
242
|
}
|
|
190
243
|
const currentModeBaseTheme = modeBaseHelper.get(baseTheme)
|
|
191
244
|
|
|
192
245
|
const modeDimHelper = ThemeManager.modeDimensionTheme[mode]
|
|
193
|
-
if (
|
|
246
|
+
if (modeDimHelper.has(themes)) {
|
|
247
|
+
if ((import.meta as ViteMeta).env?.DEV === true)
|
|
248
|
+
_countSink.__pyreon_count__?.('rocketstyle.localThemeManager.hit')
|
|
249
|
+
} else {
|
|
194
250
|
modeDimHelper.set(themes, getThemeByMode(themes, mode))
|
|
195
251
|
}
|
|
196
252
|
const currentModeThemes = modeDimHelper.get(themes)
|
|
@@ -228,7 +284,7 @@ const rocketComponent: RocketComponent = (options) => {
|
|
|
228
284
|
// Read pseudo props fresh each call — props may have reactive getters
|
|
229
285
|
// from _rp() wrapping. Reading inside the accessor (which runs in an
|
|
230
286
|
// effect) ensures changes to pseudo props like active={isDark()} are tracked.
|
|
231
|
-
const propPseudo = pick(props,
|
|
287
|
+
const propPseudo = pick(props, ALL_PSEUDO_KEYS)
|
|
232
288
|
|
|
233
289
|
return {
|
|
234
290
|
...rocketstate,
|
|
@@ -237,34 +293,42 @@ const rocketComponent: RocketComponent = (options) => {
|
|
|
237
293
|
}
|
|
238
294
|
|
|
239
295
|
// --------------------------------------------------
|
|
240
|
-
//
|
|
296
|
+
// final props passed to WrappedComponent
|
|
241
297
|
// --------------------------------------------------
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
298
|
+
// Cache a pre-built Set for omit() — avoids building a new Set from
|
|
299
|
+
// the key array on every mount. Same dimension structure = same Set.
|
|
300
|
+
let omitSet = _omitSetCache.get(RESERVED_STYLING_PROPS_KEYS)
|
|
301
|
+
if (omitSet) {
|
|
302
|
+
if ((import.meta as ViteMeta).env?.DEV === true)
|
|
303
|
+
_countSink.__pyreon_count__?.('rocketstyle.omitSet.hit')
|
|
304
|
+
} else {
|
|
305
|
+
omitSet = new Set([...RESERVED_STYLING_PROPS_KEYS, ...STATIC_OMIT_KEYS])
|
|
306
|
+
_omitSetCache.set(RESERVED_STYLING_PROPS_KEYS, omitSet)
|
|
245
307
|
}
|
|
246
308
|
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
// Function accessors — DynamicStyled wraps them in a computed() so
|
|
259
|
-
// mode/dimension changes produce a new CSS class reactively. The
|
|
260
|
-
// computed tracks only these two accessors; the resolve itself runs
|
|
261
|
-
// untracked to prevent exponential cascade from theme deep-reads.
|
|
262
|
-
$rocketstyle: $rocketstyleAccessor,
|
|
263
|
-
$rocketstate: $rocketstateAccessor,
|
|
309
|
+
// Merge localCtx + props without an intermediate spread object.
|
|
310
|
+
// omit() handles 'pseudo' removal (included in STATIC_OMIT_KEYS).
|
|
311
|
+
const mergeProps = localCtx ? { ...localCtx, ...props } : props
|
|
312
|
+
|
|
313
|
+
// omit() already returns a fresh object — assign directly onto it
|
|
314
|
+
// instead of spreading into another {} (saves one object allocation).
|
|
315
|
+
const finalProps = omit(mergeProps as Record<string, unknown>, omitSet) as Record<string, any>
|
|
316
|
+
|
|
317
|
+
if (options.passProps) {
|
|
318
|
+
const passed = pick(mergeProps, options.passProps)
|
|
319
|
+
for (const k in passed) finalProps[k] = passed[k]
|
|
264
320
|
}
|
|
265
321
|
|
|
266
|
-
|
|
267
|
-
|
|
322
|
+
finalProps.ref = props.ref
|
|
323
|
+
// Function accessors — DynamicStyled wraps them in a computed() so
|
|
324
|
+
// mode/dimension changes produce a new CSS class reactively. The
|
|
325
|
+
// computed tracks only these two accessors; the resolve itself runs
|
|
326
|
+
// untracked to prevent exponential cascade from theme deep-reads.
|
|
327
|
+
finalProps.$rocketstyle = $rocketstyleAccessor
|
|
328
|
+
finalProps.$rocketstate = $rocketstateAccessor
|
|
329
|
+
|
|
330
|
+
// development debugging — tree-shaken in production via import.meta.env.DEV
|
|
331
|
+
if (__DEV__) {
|
|
268
332
|
finalProps['data-rocketstyle'] = componentName
|
|
269
333
|
|
|
270
334
|
if (options.DEBUG) {
|
|
@@ -326,6 +390,26 @@ const rocketComponent: RocketComponent = (options) => {
|
|
|
326
390
|
options: options.statics,
|
|
327
391
|
})
|
|
328
392
|
|
|
393
|
+
// ─── Hoisted attrs chain (T3.1) ──────────────────────────────────────
|
|
394
|
+
//
|
|
395
|
+
// Expose the accumulated `.attrs()` callback chain on the component so
|
|
396
|
+
// external inspectors (notably `extractDocumentTree` from
|
|
397
|
+
// `@pyreon/connector-document`) can compute the post-attrs props
|
|
398
|
+
// without invoking the full component. The previous Path B workaround
|
|
399
|
+
// had to run the entire styled wrapper — JSX tree creation, dimension
|
|
400
|
+
// resolution, the lot — just to read `_documentProps` off the result.
|
|
401
|
+
//
|
|
402
|
+
// Typed surface: `RocketStyleComponent.__rs_attrs` is a `readonly
|
|
403
|
+
// ReadonlyArray<(props) => Record<string, unknown>>`. Empty when
|
|
404
|
+
// no `.attrs()` was ever called. `chain.reduce(Object.assign, {})`
|
|
405
|
+
// produces the post-attrs result for a given props bag.
|
|
406
|
+
//
|
|
407
|
+
// The `readonly` modifier guards external CONSUMERS — internal
|
|
408
|
+
// assignment from the factory itself is the only legitimate write,
|
|
409
|
+
// hence the cast. Do not drop the readonly on the type.
|
|
410
|
+
;(FinalComponent as unknown as { __rs_attrs: typeof options.attrs }).__rs_attrs =
|
|
411
|
+
options.attrs ?? []
|
|
412
|
+
|
|
329
413
|
Object.assign(FinalComponent, {
|
|
330
414
|
attrs: (attrs: any, { priority, filter }: any = {}) => {
|
|
331
415
|
const result: Record<string, any> = {}
|
package/src/types/rocketstyle.ts
CHANGED
|
@@ -153,4 +153,19 @@ export interface IRocketStyleComponent<
|
|
|
153
153
|
|
|
154
154
|
IS_ROCKETSTYLE: true
|
|
155
155
|
displayName: string
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The accumulated `.attrs()` callback chain — hoisted at component-creation
|
|
159
|
+
* time so external inspectors (notably `extractDocumentTree` from
|
|
160
|
+
* `@pyreon/connector-document`) can compute post-attrs props without
|
|
161
|
+
* invoking the full styled wrapper. Each callback maps user props to a
|
|
162
|
+
* partial props object; `chain.reduce(Object.assign, {})` produces the
|
|
163
|
+
* post-attrs result.
|
|
164
|
+
*
|
|
165
|
+
* Stable contract — consumers can rely on this property being present on
|
|
166
|
+
* every rocketstyle-wrapped component. Empty array when no `.attrs()`
|
|
167
|
+
* was ever called on the chain. Treat as read-only; mutating breaks
|
|
168
|
+
* `extractDocumentTree` and any other inspector consuming the hoist.
|
|
169
|
+
*/
|
|
170
|
+
readonly __rs_attrs: ReadonlyArray<(props: Record<string, unknown>) => Record<string, unknown>>
|
|
156
171
|
}
|
package/src/utils/attrs.ts
CHANGED
|
@@ -80,53 +80,49 @@ export const calculateStylingAttrs: CalculateStylingAttrs =
|
|
|
80
80
|
const result: Record<string, any> = {}
|
|
81
81
|
|
|
82
82
|
// (1) find dimension keys values & initialize
|
|
83
|
-
|
|
84
|
-
Object.keys(dimensions).forEach((item) => {
|
|
83
|
+
for (const item in dimensions) {
|
|
85
84
|
const pickedProp = props[item]
|
|
86
85
|
const t = typeof pickedProp
|
|
87
86
|
|
|
88
|
-
// if the property is multi key, allow assign array as well
|
|
89
87
|
if (multiKeys?.[item] && Array.isArray(pickedProp)) {
|
|
90
88
|
result[item] = pickedProp
|
|
91
|
-
}
|
|
92
|
-
// assign when it's only a string or number otherwise it's considered
|
|
93
|
-
// as invalid param
|
|
94
|
-
else if (t === 'string' || t === 'number') {
|
|
89
|
+
} else if (t === 'string' || t === 'number') {
|
|
95
90
|
result[item] = pickedProp
|
|
96
91
|
} else {
|
|
97
92
|
result[item] = undefined
|
|
98
93
|
}
|
|
99
|
-
}
|
|
94
|
+
}
|
|
100
95
|
|
|
101
96
|
// (2) if booleans are being used let's find the rest
|
|
97
|
+
// Use `in` operator on the dimension map instead of allocating
|
|
98
|
+
// a new Set per dimension — the map is already an object with
|
|
99
|
+
// the keywords as keys.
|
|
102
100
|
if (useBooleans) {
|
|
103
|
-
const
|
|
101
|
+
for (const key in result) {
|
|
102
|
+
if (result[key]) continue // already assigned
|
|
104
103
|
|
|
105
|
-
|
|
104
|
+
const dimensionMap = dimensions[key] as Record<string, unknown>
|
|
106
105
|
const isMultiKey = multiKeys?.[key]
|
|
106
|
+
let newDimensionValue: string | string[] | undefined
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (keywordSet.has(k) && props[k]) {
|
|
121
|
-
newDimensionValue = k
|
|
122
|
-
break
|
|
123
|
-
}
|
|
108
|
+
if (isMultiKey) {
|
|
109
|
+
const matches: string[] = []
|
|
110
|
+
for (const propKey in props) {
|
|
111
|
+
if (propKey in dimensionMap) matches.push(propKey)
|
|
112
|
+
}
|
|
113
|
+
newDimensionValue = matches.length > 0 ? matches : undefined
|
|
114
|
+
} else {
|
|
115
|
+
// Iterate props to find last matching keyword
|
|
116
|
+
// (last wins for priority)
|
|
117
|
+
for (const k in props) {
|
|
118
|
+
if (k in dimensionMap && props[k]) {
|
|
119
|
+
newDimensionValue = k
|
|
124
120
|
}
|
|
125
121
|
}
|
|
126
|
-
|
|
127
|
-
result[key] = newDimensionValue
|
|
128
122
|
}
|
|
129
|
-
|
|
123
|
+
|
|
124
|
+
result[key] = newDimensionValue
|
|
125
|
+
}
|
|
130
126
|
}
|
|
131
127
|
|
|
132
128
|
return result
|
package/src/utils/theme.ts
CHANGED
|
@@ -60,26 +60,21 @@ type GetDimensionThemes = (
|
|
|
60
60
|
) => Record<string, any>
|
|
61
61
|
|
|
62
62
|
export const getDimensionThemes: GetDimensionThemes = (theme, options) => {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
if (isEmpty(options.dimensions)) return result
|
|
66
|
-
|
|
67
|
-
return Object.entries(options.dimensions).reduce(
|
|
68
|
-
(acc, [key, value]) => {
|
|
69
|
-
const [, dimension] = isMultiKey(value as string | Record<string, unknown>)
|
|
63
|
+
const dims = options.dimensions
|
|
64
|
+
if (isEmpty(dims)) return {}
|
|
70
65
|
|
|
71
|
-
|
|
66
|
+
const result: Record<string, any> = {}
|
|
72
67
|
|
|
73
|
-
|
|
74
|
-
|
|
68
|
+
for (const key in dims) {
|
|
69
|
+
const [, dimension] = isMultiKey(dims[key] as string | Record<string, unknown>)
|
|
70
|
+
const helper = options[key]
|
|
75
71
|
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
if (Array.isArray(helper) && helper.length > 0) {
|
|
73
|
+
result[dimension] = removeNullableValues(getThemeFromChain(helper, theme))
|
|
74
|
+
}
|
|
75
|
+
}
|
|
78
76
|
|
|
79
|
-
|
|
80
|
-
},
|
|
81
|
-
result as Record<string, any>,
|
|
82
|
-
)
|
|
77
|
+
return result
|
|
83
78
|
}
|
|
84
79
|
|
|
85
80
|
// --------------------------------------------------------
|
|
@@ -119,54 +114,62 @@ export type GetTheme = (params: {
|
|
|
119
114
|
appTheme?: Record<string, any>
|
|
120
115
|
}) => Record<string, unknown>
|
|
121
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
|
+
|
|
122
121
|
export const getTheme: GetTheme = ({ rocketstate, themes, baseTheme, transformKeys, appTheme }) => {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
134
137
|
const keyTheme: Record<string, any> = themes[key] ?? {}
|
|
135
138
|
const isTransform = transformKeys?.[key]
|
|
136
139
|
|
|
137
140
|
const mergeValue = (item: string) => {
|
|
138
141
|
const val = keyTheme[item]
|
|
142
|
+
if (val == null) return
|
|
139
143
|
if (isTransform && typeof val === 'function') {
|
|
140
|
-
deferredTransforms.push(val)
|
|
144
|
+
deferredTransforms.push(val as TransformFn)
|
|
141
145
|
} else {
|
|
142
|
-
finalTheme
|
|
146
|
+
// Merge in-place onto finalTheme — avoids allocating a fresh {}
|
|
147
|
+
// as merge target on every dimension slice.
|
|
148
|
+
merge(finalTheme, val)
|
|
143
149
|
}
|
|
144
150
|
}
|
|
145
151
|
|
|
146
152
|
if (Array.isArray(value)) {
|
|
147
|
-
value.
|
|
153
|
+
for (let i = 0; i < value.length; i++) mergeValue(value[i] as string)
|
|
148
154
|
} else {
|
|
149
|
-
mergeValue(value)
|
|
155
|
+
mergeValue(value as string)
|
|
150
156
|
}
|
|
151
|
-
}
|
|
157
|
+
}
|
|
152
158
|
|
|
153
159
|
// Apply transform dimension values last with the fully accumulated theme
|
|
154
|
-
for (
|
|
155
|
-
finalTheme
|
|
156
|
-
{},
|
|
157
|
-
finalTheme,
|
|
158
|
-
transform(finalTheme, appTheme ?? {}, themeModeCallback, config.css),
|
|
159
|
-
)
|
|
160
|
+
for (let i = 0; i < deferredTransforms.length; i++) {
|
|
161
|
+
merge(finalTheme, deferredTransforms[i]!(finalTheme, appTheme ?? {}, themeModeCallback, config.css))
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
// Ensure pseudo-state keys always exist as objects so .styles() can
|
|
163
165
|
// destructure without defaults: const { hover, focus, ... } = $rocketstyle
|
|
164
|
-
|
|
165
|
-
finalTheme.
|
|
166
|
-
finalTheme.
|
|
167
|
-
finalTheme.
|
|
168
|
-
finalTheme.
|
|
169
|
-
finalTheme.
|
|
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
|
|
170
173
|
|
|
171
174
|
return finalTheme
|
|
172
175
|
}
|