@pyreon/rocketstyle 0.13.0 → 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.
@@ -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 (process.env.NODE_ENV !== 'production') {
76
+ if (__DEV__) {
77
77
  validateInit(name, component, dimensions)
78
78
  }
79
79
 
@@ -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
- const { keysMap: dimensions, keywords: reservedPropNames } = getDimensionsMap({
144
- themes: initialDimensionThemes,
145
- useBooleans: options.useBooleans,
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
- const RESERVED_STYLING_PROPS_KEYS = Object.keys(reservedPropNames)
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 (!baseThemeHelper.has(theme)) {
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 (!dimHelper.has(theme)) {
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 (!modeBaseHelper.has(baseTheme)) {
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 (!modeDimHelper.has(themes)) {
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, [...PSEUDO_KEYS, ...PSEUDO_META_KEYS])
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
- // Static mergeProps for final prop filtering (non-dimension props)
296
+ // final props passed to WrappedComponent
241
297
  // --------------------------------------------------
242
- const { pseudo: _pseudo, ...mergeProps } = {
243
- ...localCtx,
244
- ...props,
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
- // final props passed to WrappedComponent
249
- // --------------------------------------------------
250
- const finalProps: Record<string, any> = {
251
- ...omit(mergeProps, [
252
- ...RESERVED_STYLING_PROPS_KEYS,
253
- ...PSEUDO_KEYS,
254
- ...options.filterAttrs,
255
- ]),
256
- ...(options.passProps ? pick(mergeProps, options.passProps) : {}),
257
- ref: props.ref,
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
- // development debugging
267
- if (process.env.NODE_ENV !== 'production') {
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> = {}
@@ -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
  }
@@ -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
- // object with possible options
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 propsKeys = Object.keys(props)
101
+ for (const key in result) {
102
+ if (result[key]) continue // already assigned
104
103
 
105
- Object.entries(result).forEach(([key, value]) => {
104
+ const dimensionMap = dimensions[key] as Record<string, unknown>
106
105
  const isMultiKey = multiKeys?.[key]
106
+ let newDimensionValue: string | string[] | undefined
107
107
 
108
- // when value in result is not assigned yet
109
- if (!value) {
110
- let newDimensionValue: string | string[] | undefined
111
- const keywordSet = new Set(Object.keys(dimensions[key] as Record<string, unknown>))
112
-
113
- if (isMultiKey) {
114
- newDimensionValue = propsKeys.filter((propKey) => keywordSet.has(propKey))
115
- } else {
116
- // iterate backwards to guarantee the last one will have
117
- // a priority over previous ones
118
- for (let i = propsKeys.length - 1; i >= 0; i--) {
119
- const k = propsKeys[i] as string
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
@@ -60,26 +60,21 @@ type GetDimensionThemes = (
60
60
  ) => Record<string, any>
61
61
 
62
62
  export const getDimensionThemes: GetDimensionThemes = (theme, options) => {
63
- const result = {}
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
- const helper = options[key]
66
+ const result: Record<string, any> = {}
72
67
 
73
- if (Array.isArray(helper) && helper.length > 0) {
74
- const finalDimensionThemes = getThemeFromChain(helper, theme)
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
- acc[dimension] = removeNullableValues(finalDimensionThemes)
77
- }
72
+ if (Array.isArray(helper) && helper.length > 0) {
73
+ result[dimension] = removeNullableValues(getThemeFromChain(helper, theme))
74
+ }
75
+ }
78
76
 
79
- return acc
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
- let finalTheme = { ...baseTheme }
124
- const deferredTransforms: Array<
125
- (
126
- currentTheme: Record<string, any>,
127
- currentAppTheme: Record<string, any>,
128
- mode: typeof themeModeCallback,
129
- cssFn: typeof config.css,
130
- ) => Record<string, any>
131
- > = []
132
-
133
- Object.entries(rocketstate).forEach(([key, value]: [string, string | string[]]) => {
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 = merge({}, finalTheme, val)
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.forEach(mergeValue)
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 (const transform of deferredTransforms) {
155
- finalTheme = merge(
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
- finalTheme.hover ??= {}
165
- finalTheme.focus ??= {}
166
- finalTheme.active ??= {}
167
- finalTheme.disabled ??= {}
168
- finalTheme.pressed ??= {}
169
- finalTheme.readOnly ??= {}
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
  }