@pyreon/rocketstyle 0.11.6 → 0.11.8

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/lib/index.js CHANGED
@@ -175,10 +175,11 @@ const createLocalProvider = (WrappedComponent) => {
175
175
  if (onBlur) onBlur(e);
176
176
  }
177
177
  };
178
+ const resolvedState = typeof $rocketstate === "function" ? $rocketstate() : $rocketstate;
178
179
  const updatedState = {
179
- ...$rocketstate,
180
+ ...resolvedState,
180
181
  pseudo: {
181
- ...$rocketstate?.pseudo,
182
+ ...resolvedState?.pseudo,
182
183
  get hover() {
183
184
  return hover();
184
185
  },
@@ -497,14 +498,6 @@ const rocketComponent = (options) => {
497
498
  const EnhancedComponent = (props) => {
498
499
  const localCtx = useLocalContext(options.consumer);
499
500
  const themeAttrs = useThemeAttrs(options);
500
- const { pseudo, ...mergeProps } = {
501
- ...localCtx,
502
- ...props
503
- };
504
- const pseudoRocketstate = {
505
- ...pseudo,
506
- ...pick(props, [...PSEUDO_KEYS, ...PSEUDO_META_KEYS])
507
- };
508
501
  const theme = themeAttrs.theme;
509
502
  const baseThemeHelper = ThemeManager$1.baseTheme;
510
503
  if (!baseThemeHelper.has(theme)) baseThemeHelper.set(theme, getThemeFromChain(options.theme, theme));
@@ -517,16 +510,12 @@ const rocketComponent = (options) => {
517
510
  useBooleans: options.useBooleans
518
511
  });
519
512
  const RESERVED_STYLING_PROPS_KEYS = Object.keys(reservedPropNames);
520
- const rocketstate = _calculateStylingAttrs({
521
- props: pickStyledAttrs(mergeProps, reservedPropNames),
522
- dimensions
523
- });
524
- const finalRocketstate = {
525
- ...rocketstate,
526
- pseudo: pseudoRocketstate
527
- };
528
513
  const $rocketstyleAccessor = () => {
529
514
  const mode = themeAttrs.mode;
515
+ const rocketstate = _calculateStylingAttrs({
516
+ props: pickStyledAttrs(props, reservedPropNames),
517
+ dimensions
518
+ });
530
519
  const modeBaseHelper = ThemeManager$1.modeBaseTheme[mode];
531
520
  if (!modeBaseHelper.has(baseTheme)) modeBaseHelper.set(baseTheme, getThemeByMode(baseTheme, mode));
532
521
  const currentModeBaseTheme = modeBaseHelper.get(baseTheme);
@@ -540,6 +529,24 @@ const rocketComponent = (options) => {
540
529
  appTheme: theme
541
530
  });
542
531
  };
532
+ const localPseudo = localCtx?.pseudo;
533
+ const propPseudo = pick(props, [...PSEUDO_KEYS, ...PSEUDO_META_KEYS]);
534
+ const $rocketstateAccessor = () => {
535
+ return {
536
+ ..._calculateStylingAttrs({
537
+ props: pickStyledAttrs(props, reservedPropNames),
538
+ dimensions
539
+ }),
540
+ pseudo: {
541
+ ...localPseudo,
542
+ ...propPseudo
543
+ }
544
+ };
545
+ };
546
+ const { pseudo: _pseudo, ...mergeProps } = {
547
+ ...localCtx,
548
+ ...props
549
+ };
543
550
  const finalProps = {
544
551
  ...omit(mergeProps, [
545
552
  ...RESERVED_STYLING_PROPS_KEYS,
@@ -549,14 +556,14 @@ const rocketComponent = (options) => {
549
556
  ...options.passProps ? pick(mergeProps, options.passProps) : {},
550
557
  ref: props.ref,
551
558
  $rocketstyle: $rocketstyleAccessor,
552
- $rocketstate: finalRocketstate
559
+ $rocketstate: $rocketstateAccessor
553
560
  };
554
561
  if (process.env.NODE_ENV !== "production") {
555
562
  finalProps["data-rocketstyle"] = componentName;
556
563
  if (options.DEBUG) {
557
564
  const debugPayload = {
558
565
  component: componentName,
559
- rocketstate: finalRocketstate,
566
+ rocketstate: $rocketstateAccessor(),
560
567
  rocketstyle: $rocketstyleAccessor(),
561
568
  dimensions,
562
569
  mode: themeAttrs.mode,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/rocketstyle",
3
- "version": "0.11.6",
3
+ "version": "0.11.8",
4
4
  "description": "Multi-dimensional style composition for Pyreon components",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -41,15 +41,15 @@
41
41
  "typecheck": "tsc --noEmit"
42
42
  },
43
43
  "devDependencies": {
44
- "@pyreon/test-utils": "^0.11.6",
45
- "@pyreon/typescript": "^0.11.6",
44
+ "@pyreon/test-utils": "^0.11.8",
45
+ "@pyreon/typescript": "^0.11.8",
46
46
  "@vitus-labs/tools-rolldown": "^1.15.3"
47
47
  },
48
48
  "peerDependencies": {
49
- "@pyreon/core": "^0.11.6",
50
- "@pyreon/reactivity": "^0.11.6",
51
- "@pyreon/styler": "^0.11.6",
52
- "@pyreon/ui-core": "^0.11.6"
49
+ "@pyreon/core": "^0.11.8",
50
+ "@pyreon/reactivity": "^0.11.8",
51
+ "@pyreon/styler": "^0.11.8",
52
+ "@pyreon/ui-core": "^0.11.8"
53
53
  },
54
54
  "engines": {
55
55
  "node": ">= 22"
@@ -7,7 +7,7 @@
7
7
  * Unlike the React version which tested CSS injection in the DOM,
8
8
  * this Pyreon version tests the computed $rocketstyle output directly.
9
9
  */
10
- import { ThemeCapture, getComputedTheme, initTestConfig } from '@pyreon/test-utils'
10
+ import { ThemeCapture, getComputedTheme, initTestConfig, withThemeContext } from '@pyreon/test-utils'
11
11
  import rocketstyle from '../init'
12
12
 
13
13
  let cleanup: () => void
@@ -234,3 +234,67 @@ describe('e2e: rocketstyle theme computation', () => {
234
234
  expect(theme.sawStep).toBe('one')
235
235
  })
236
236
  })
237
+
238
+ // ── Reactive dimension props ──────────────────────────────────────────────────
239
+
240
+ describe('reactive $rocketstyle accessor', () => {
241
+ it('$rocketstyleAccessor resolves different themes for different dimension props', () => {
242
+ const Comp: any = rocketstyle()({
243
+ name: 'ReactiveComp',
244
+ component: ThemeCapture,
245
+ })
246
+ .theme({ color: 'black', bg: 'white' })
247
+ .states({
248
+ primary: { color: 'blue' },
249
+ secondary: { color: 'green' },
250
+ })
251
+
252
+ // First call with state=primary
253
+ const theme1 = getComputedTheme(Comp, { state: 'primary' })
254
+ expect(theme1.color).toBe('blue')
255
+
256
+ // Second call with state=secondary — should produce different theme
257
+ const theme2 = getComputedTheme(Comp, { state: 'secondary' })
258
+ expect(theme2.color).toBe('green')
259
+ })
260
+
261
+ it('$rocketstyleAccessor is a function, not a plain object', () => {
262
+ const Comp: any = rocketstyle()({
263
+ name: 'AccessorComp',
264
+ component: ThemeCapture,
265
+ }).theme({ color: 'red' })
266
+
267
+ const vnode = withThemeContext(() => Comp({}))
268
+ // ThemeCapture resolves the accessor — result should be the theme object
269
+ expect(vnode.$rocketstyle).toBeDefined()
270
+ expect(vnode.$rocketstyle.color).toBe('red')
271
+ })
272
+
273
+ it('$rocketstateAccessor resolves active dimensions', () => {
274
+ const Comp: any = rocketstyle()({
275
+ name: 'StateAccessorComp',
276
+ component: ThemeCapture,
277
+ }).states({
278
+ primary: { color: 'blue' },
279
+ })
280
+
281
+ const vnode = withThemeContext(() => Comp({ state: 'primary' }))
282
+ expect(vnode.$rocketstate).toBeDefined()
283
+ expect(vnode.$rocketstate.state).toBe('primary')
284
+ })
285
+
286
+ it('mode change produces different theme via accessor', () => {
287
+ const Comp: any = rocketstyle()({
288
+ name: 'ModeReactiveComp',
289
+ component: ThemeCapture,
290
+ }).theme((t: any, m: any) => ({
291
+ color: m('light-color', 'dark-color'),
292
+ }))
293
+
294
+ const lightTheme = getComputedTheme(Comp, {}, { mode: 'light' })
295
+ expect(lightTheme.color).toBe('light-color')
296
+
297
+ const darkTheme = getComputedTheme(Comp, {}, { mode: 'dark' })
298
+ expect(darkTheme.color).toBe('dark-color')
299
+ })
300
+ })
@@ -23,8 +23,8 @@ const BaseComponent: any = ({ children, $rocketstyle, $rocketstate, ...rest }: a
23
23
  props: rest,
24
24
  children,
25
25
  key: null,
26
- $rocketstyle,
27
- $rocketstate,
26
+ $rocketstyle: typeof $rocketstyle === 'function' ? $rocketstyle() : $rocketstyle,
27
+ $rocketstate: typeof $rocketstate === 'function' ? $rocketstate() : $rocketstate,
28
28
  })
29
29
  BaseComponent.displayName = 'BaseComponent'
30
30
 
@@ -62,10 +62,13 @@ const createLocalProvider = (WrappedComponent: ComponentFn<any>) => {
62
62
  // Without getters, hover()/focus()/pressed() reads here would register
63
63
  // as dependencies of any parent effect, causing cascading re-renders
64
64
  // on every mouse event.
65
+ // Resolve $rocketstate if it's a function accessor (from EnhancedComponent)
66
+ const resolvedState =
67
+ typeof $rocketstate === 'function' ? $rocketstate() : $rocketstate
65
68
  const updatedState = {
66
- ...$rocketstate,
69
+ ...resolvedState,
67
70
  pseudo: {
68
- ...$rocketstate?.pseudo,
71
+ ...resolvedState?.pseudo,
69
72
  get hover() {
70
73
  return hover()
71
74
  },
@@ -110,41 +110,24 @@ const rocketComponent: RocketComponent = (options) => {
110
110
  const themeAttrs = useTheme(options)
111
111
 
112
112
  // --------------------------------------------------
113
- // Static setupruns once at component mount
114
- // --------------------------------------------------
115
- const { pseudo, ...mergeProps } = {
116
- ...localCtx,
117
- ...props,
118
- }
119
-
120
- const pseudoRocketstate = {
121
- ...pseudo,
122
- ...pick(props, [...PSEUDO_KEYS, ...PSEUDO_META_KEYS]),
123
- }
124
-
125
- // --------------------------------------------------
126
- // Static theme structure — computed once at mount, doesn't change with mode.
127
- // Only the mode-dependent resolution is reactive (via $rocketstyle accessor).
113
+ // Theme structurecached by theme object identity.
114
+ // Theme object itself doesn't change (enrichTheme produces a stable ref),
115
+ // only mode switches change which mode-variant is resolved.
128
116
  // --------------------------------------------------
129
117
  const theme = themeAttrs.theme
130
118
 
131
- // BASE / DEFAULT THEME Object (cached by theme identity)
132
119
  const baseThemeHelper = ThemeManager.baseTheme
133
120
  if (!baseThemeHelper.has(theme)) {
134
121
  baseThemeHelper.set(theme, getThemeFromChain(options.theme, theme))
135
122
  }
136
123
  const baseTheme = baseThemeHelper.get(theme)
137
124
 
138
- // DIMENSION(S) THEMES Object (cached by theme identity)
139
125
  const dimHelper = ThemeManager.dimensionsThemes
140
126
  if (!dimHelper.has(theme)) {
141
127
  dimHelper.set(theme, getDimensionThemes(theme, options))
142
128
  }
143
129
  const themes = dimHelper.get(theme)
144
130
 
145
- // --------------------------------------------------
146
- // dimension map & reserved prop names
147
- // --------------------------------------------------
148
131
  const { keysMap: dimensions, keywords: reservedPropNames } = getDimensionsMap({
149
132
  themes,
150
133
  useBooleans: options.useBooleans,
@@ -153,23 +136,26 @@ const rocketComponent: RocketComponent = (options) => {
153
136
  const RESERVED_STYLING_PROPS_KEYS = Object.keys(reservedPropNames)
154
137
 
155
138
  // --------------------------------------------------
156
- // rocketstateactive dimension values
157
- // --------------------------------------------------
158
- const rocketstate = _calculateStylingAttrs({
159
- props: pickStyledAttrs(mergeProps, reservedPropNames),
160
- dimensions,
161
- })
162
-
163
- const finalRocketstate = { ...rocketstate, pseudo: pseudoRocketstate }
164
-
165
- // --------------------------------------------------
166
- // $rocketstyle as a FUNCTION ACCESSOR — reactive on mode changes.
167
- // The styled component calls this inside its own effect() to track
168
- // the mode dependency. Only the CSS class swaps — no VNode remount.
139
+ // $rocketstyle as a FUNCTION ACCESSOR fully reactive.
140
+ // Re-evaluates when mode OR dimension props change.
141
+ // Props are resolved fresh each call so reactive prop accessors
142
+ // (signals, getters) produce updated dimension values.
169
143
  // --------------------------------------------------
170
144
  const $rocketstyleAccessor = () => {
171
- const mode = themeAttrs.mode // reactive read via getter
145
+ // Only read mode and dimension props NOT pseudo state.
146
+ // Pseudo state (hover, focus, pressed) is read by .styles()
147
+ // via $rocketstate inside runUntracked(). Reading pseudo signals
148
+ // here would subscribe this accessor to hover/focus/pressed,
149
+ // causing CSS recomputation on every mouse event.
150
+ const mode = themeAttrs.mode // reactive: tracks mode signal
151
+
152
+ // Resolve active dimensions from props (not localCtx which has pseudo getters)
153
+ const rocketstate = _calculateStylingAttrs({
154
+ props: pickStyledAttrs(props as Record<string, unknown>, reservedPropNames),
155
+ dimensions,
156
+ })
172
157
 
158
+ // Resolve mode-specific theme
173
159
  const modeBaseHelper = ThemeManager.modeBaseTheme[mode]
174
160
  if (!modeBaseHelper.has(baseTheme)) {
175
161
  modeBaseHelper.set(baseTheme, getThemeByMode(baseTheme, mode))
@@ -191,6 +177,38 @@ const rocketComponent: RocketComponent = (options) => {
191
177
  })
192
178
  }
193
179
 
180
+ // --------------------------------------------------
181
+ // $rocketstate as a FUNCTION ACCESSOR — reactive on prop changes.
182
+ // Re-evaluates active dimensions + pseudo state from current props.
183
+ // --------------------------------------------------
184
+ // Capture pseudo from localCtx once at setup — pseudo properties are
185
+ // getters (from createLocalProvider) that read signals lazily.
186
+ // Passing them through preserves reactivity without subscribing here.
187
+ const localPseudo = localCtx?.pseudo
188
+ const propPseudo = pick(props, [...PSEUDO_KEYS, ...PSEUDO_META_KEYS])
189
+
190
+ const $rocketstateAccessor = () => {
191
+ const rocketstate = _calculateStylingAttrs({
192
+ props: pickStyledAttrs(props as Record<string, unknown>, reservedPropNames),
193
+ dimensions,
194
+ })
195
+
196
+ // Pseudo state uses getter properties — they're evaluated lazily
197
+ // by .styles() inside runUntracked(), not here.
198
+ return {
199
+ ...rocketstate,
200
+ pseudo: { ...localPseudo, ...propPseudo },
201
+ }
202
+ }
203
+
204
+ // --------------------------------------------------
205
+ // Static mergeProps for final prop filtering (non-dimension props)
206
+ // --------------------------------------------------
207
+ const { pseudo: _pseudo, ...mergeProps } = {
208
+ ...localCtx,
209
+ ...props,
210
+ }
211
+
194
212
  // --------------------------------------------------
195
213
  // final props passed to WrappedComponent
196
214
  // --------------------------------------------------
@@ -202,9 +220,9 @@ const rocketComponent: RocketComponent = (options) => {
202
220
  ]),
203
221
  ...(options.passProps ? pick(mergeProps, options.passProps) : {}),
204
222
  ref: props.ref,
205
- // FUNCTION accessor — styled component resolves it reactively
223
+ // FUNCTION accessors — styled component resolves them reactively
206
224
  $rocketstyle: $rocketstyleAccessor,
207
- $rocketstate: finalRocketstate,
225
+ $rocketstate: $rocketstateAccessor,
208
226
  }
209
227
 
210
228
  // development debugging
@@ -214,7 +232,7 @@ const rocketComponent: RocketComponent = (options) => {
214
232
  if (options.DEBUG) {
215
233
  const debugPayload = {
216
234
  component: componentName,
217
- rocketstate: finalRocketstate,
235
+ rocketstate: $rocketstateAccessor(),
218
236
  rocketstyle: $rocketstyleAccessor(),
219
237
  dimensions,
220
238
  mode: themeAttrs.mode,