@vitus-labs/rocketstyle 2.6.1 → 2.7.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.
Files changed (2) hide show
  1. package/lib/index.js +176 -125
  2. package/package.json +11 -6
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Provider as Provider$1, compose, config, context, get, hoistNonReactStatics, isEmpty, merge, omit, pick, render, set, useStableValue } from "@vitus-labs/core";
2
- import { createContext, useCallback, useContext, useImperativeHandle, useMemo, useRef, useState } from "react";
2
+ import { createContext, memo, useContext, useImperativeHandle, useMemo, useRef, useState } from "react";
3
3
  import { jsx } from "react/jsx-runtime";
4
4
 
5
5
  //#region src/constants/index.ts
@@ -14,6 +14,12 @@ const PSEUDO_KEYS = [
14
14
  ];
15
15
  /** Meta pseudo-state keys representing non-interactive states (disabled, readOnly). */
16
16
  const PSEUDO_META_KEYS = ["disabled", "readOnly"];
17
+ /**
18
+ * Pre-merged interaction + meta keys. Hoisted from `rocketstyle.tsx`'s render
19
+ * body so the `pick(props, [...PSEUDO_KEYS, ...PSEUDO_META_KEYS])` call no
20
+ * longer allocates a fresh 6-element array on every render.
21
+ */
22
+ const PSEUDO_AND_META_KEYS = [...PSEUDO_KEYS, ...PSEUDO_META_KEYS];
17
23
  /** Supported theme mode flags. */
18
24
  const THEME_MODES = {
19
25
  light: true,
@@ -119,36 +125,59 @@ var ThemeManager = class {
119
125
  * Tracks hover, focus, and pressed pseudo-states via mouse and focus
120
126
  * event handlers. Returns the current state flags and wrapped event
121
127
  * callbacks that preserve any user-provided handlers.
128
+ *
129
+ * Consumer handlers are captured in a ref so the wrapped event callbacks
130
+ * keep stable identity across re-renders — otherwise inline arrow
131
+ * handlers (`onClick={() => …}`) would re-create the wrappers every
132
+ * render and defeat downstream memoization.
122
133
  */
123
134
  const usePseudoState = ({ onBlur, onFocus, onMouseDown, onMouseEnter, onMouseLeave, onMouseUp }) => {
124
135
  const [hover, setHover] = useState(false);
125
136
  const [focus, setFocus] = useState(false);
126
137
  const [pressed, setPressed] = useState(false);
127
- const handleOnMouseEnter = useCallback((e) => {
128
- setHover(true);
129
- if (onMouseEnter) onMouseEnter(e);
130
- }, [onMouseEnter]);
131
- const handleOnMouseLeave = useCallback((e) => {
132
- setHover(false);
133
- setPressed(false);
134
- if (onMouseLeave) onMouseLeave(e);
135
- }, [onMouseLeave]);
136
- const handleOnMouseDown = useCallback((e) => {
137
- setPressed(true);
138
- if (onMouseDown) onMouseDown(e);
139
- }, [onMouseDown]);
140
- const handleOnMouseUp = useCallback((e) => {
141
- setPressed(false);
142
- if (onMouseUp) onMouseUp(e);
143
- }, [onMouseUp]);
144
- const handleOnFocus = useCallback((e) => {
145
- setFocus(true);
146
- if (onFocus) onFocus(e);
147
- }, [onFocus]);
148
- const handleOnBlur = useCallback((e) => {
149
- setFocus(false);
150
- if (onBlur) onBlur(e);
151
- }, [onBlur]);
138
+ const latest = useRef({
139
+ onBlur,
140
+ onFocus,
141
+ onMouseDown,
142
+ onMouseEnter,
143
+ onMouseLeave,
144
+ onMouseUp
145
+ });
146
+ latest.current = {
147
+ onBlur,
148
+ onFocus,
149
+ onMouseDown,
150
+ onMouseEnter,
151
+ onMouseLeave,
152
+ onMouseUp
153
+ };
154
+ const events = useMemo(() => ({
155
+ onMouseEnter: (e) => {
156
+ setHover(true);
157
+ latest.current.onMouseEnter?.(e);
158
+ },
159
+ onMouseLeave: (e) => {
160
+ setHover(false);
161
+ setPressed(false);
162
+ latest.current.onMouseLeave?.(e);
163
+ },
164
+ onMouseDown: (e) => {
165
+ setPressed(true);
166
+ latest.current.onMouseDown?.(e);
167
+ },
168
+ onMouseUp: (e) => {
169
+ setPressed(false);
170
+ latest.current.onMouseUp?.(e);
171
+ },
172
+ onFocus: (e) => {
173
+ setFocus(true);
174
+ latest.current.onFocus?.(e);
175
+ },
176
+ onBlur: (e) => {
177
+ setFocus(false);
178
+ latest.current.onBlur?.(e);
179
+ }
180
+ }), []);
152
181
  return {
153
182
  state: useMemo(() => ({
154
183
  hover,
@@ -159,21 +188,7 @@ const usePseudoState = ({ onBlur, onFocus, onMouseDown, onMouseEnter, onMouseLea
159
188
  focus,
160
189
  pressed
161
190
  ]),
162
- events: useMemo(() => ({
163
- onMouseEnter: handleOnMouseEnter,
164
- onMouseLeave: handleOnMouseLeave,
165
- onMouseDown: handleOnMouseDown,
166
- onMouseUp: handleOnMouseUp,
167
- onFocus: handleOnFocus,
168
- onBlur: handleOnBlur
169
- }), [
170
- handleOnMouseEnter,
171
- handleOnMouseLeave,
172
- handleOnMouseDown,
173
- handleOnMouseUp,
174
- handleOnFocus,
175
- handleOnBlur
176
- ])
191
+ events
177
192
  };
178
193
  };
179
194
 
@@ -186,8 +201,8 @@ const usePseudoState = ({ onBlur, onFocus, onMouseDown, onMouseEnter, onMouseLea
186
201
  */
187
202
  const useRocketstyleRef = ({ $rocketstyleRef, ref }) => {
188
203
  const internalRef = useRef(null);
189
- useImperativeHandle($rocketstyleRef, () => internalRef.current);
190
- useImperativeHandle(ref, () => internalRef.current);
204
+ useImperativeHandle($rocketstyleRef, () => internalRef.current, []);
205
+ useImperativeHandle(ref, () => internalRef.current, []);
191
206
  return internalRef;
192
207
  };
193
208
 
@@ -202,18 +217,12 @@ const useThemeAttrs = ({ inversed }) => {
202
217
  const { theme = {}, mode: ctxMode = "light", isDark: ctxDark } = useContext(context) || {};
203
218
  const mode = inversed ? THEME_MODES_INVERSED[ctxMode] : ctxMode;
204
219
  const isDark = inversed ? !ctxDark : ctxDark;
205
- const isLight = !isDark;
206
- return useMemo(() => ({
207
- theme,
208
- mode,
209
- isDark,
210
- isLight
211
- }), [
220
+ return {
212
221
  theme,
213
222
  mode,
214
223
  isDark,
215
- isLight
216
- ]);
224
+ isLight: !isDark
225
+ };
217
226
  };
218
227
 
219
228
  //#endregion
@@ -281,42 +290,40 @@ const removeUndefinedProps = (props) => {
281
290
  for (const key in props) if (props[key] !== void 0) result[key] = props[key];
282
291
  return result;
283
292
  };
284
- const pickStyledAttrs = (props, keywords) => Object.keys(props).reduce((acc, key) => {
285
- if (keywords[key] && props[key]) acc[key] = props[key];
286
- return acc;
287
- }, {});
288
- const calculateChainOptions = (options) => (args) => {
293
+ const pickStyledAttrs = (props, keywords) => {
289
294
  const result = {};
290
- if (isEmpty(options)) return result;
295
+ for (const key in props) if (keywords[key] && props[key]) result[key] = props[key];
296
+ return result;
297
+ };
298
+ const calculateChainOptions = (options) => (args) => {
299
+ if (isEmpty(options)) return {};
291
300
  return options.reduce((acc, item) => Object.assign(acc, item(...args)), {});
292
301
  };
293
302
  const calculateStylingAttrs = ({ useBooleans, multiKeys }) => ({ props, dimensions }) => {
294
303
  const result = {};
295
- Object.keys(dimensions).forEach((item) => {
304
+ for (const item in dimensions) {
296
305
  const pickedProp = props[item];
297
306
  const t = typeof pickedProp;
298
307
  if (multiKeys?.[item] && Array.isArray(pickedProp)) result[item] = pickedProp;
299
308
  else if (t === "string" || t === "number") result[item] = pickedProp;
300
309
  else result[item] = void 0;
301
- });
310
+ }
302
311
  if (useBooleans) {
303
312
  const propsKeys = Object.keys(props);
304
- Object.entries(result).forEach(([key, value]) => {
305
- const isMultiKey = multiKeys[key];
306
- if (!value) {
307
- let newDimensionValue;
308
- const keywordSet = new Set(Object.keys(dimensions[key]));
309
- if (isMultiKey) newDimensionValue = propsKeys.filter((key) => keywordSet.has(key));
310
- else for (let i = propsKeys.length - 1; i >= 0; i--) {
311
- const k = propsKeys[i];
312
- if (keywordSet.has(k) && props[k]) {
313
- newDimensionValue = k;
314
- break;
315
- }
313
+ for (const key in result) if (!result[key]) {
314
+ const isMultiKey = multiKeys?.[key];
315
+ let newDimensionValue;
316
+ const dimObj = dimensions[key];
317
+ if (isMultiKey) newDimensionValue = propsKeys.filter((k) => Object.hasOwn(dimObj, k));
318
+ else for (let i = propsKeys.length - 1; i >= 0; i--) {
319
+ const k = propsKeys[i];
320
+ if (Object.hasOwn(dimObj, k) && props[k]) {
321
+ newDimensionValue = k;
322
+ break;
316
323
  }
317
- result[key] = newDimensionValue;
318
324
  }
319
- });
325
+ result[key] = newDimensionValue;
326
+ }
320
327
  }
321
328
  return result;
322
329
  };
@@ -326,33 +333,66 @@ const calculateStylingAttrs = ({ useBooleans, multiKeys }) => ({ props, dimensio
326
333
  /**
327
334
  * HOC that resolves the `.attrs()` chain before the inner component renders.
328
335
  * Evaluates both regular and priority attrs callbacks with the current theme
329
- * and mode, then merges the results with explicit props (priority attrs
330
- * are applied first, regular attrs can be overridden by direct props).
336
+ * and mode, then merges the results with explicit props (priority attrs are
337
+ * applied first, regular attrs can be overridden by direct props).
338
+ *
339
+ * Fast path: when no chain is configured (the common case for rocketstyle
340
+ * components built with .theme()/dimensions only), skip the deep-equal
341
+ * stabilization + memo dance and forward props directly. Mirrors the
342
+ * pattern in @vitus-labs/attrs' attrsHoc.
331
343
  */
332
344
  const rocketStyleHOC = ({ inversed, attrs, priorityAttrs }) => {
333
345
  const calculateAttrs = calculateChainOptions(attrs);
334
346
  const calculatePriorityAttrs = calculateChainOptions(priorityAttrs);
347
+ const hasAttrs = (attrs?.length ?? 0) > 0;
348
+ const hasPriorityAttrs = (priorityAttrs?.length ?? 0) > 0;
349
+ if (!(hasAttrs || hasPriorityAttrs)) {
350
+ const Enhanced = (WrappedComponent) => {
351
+ const HOC = ({ ref, ...props }) => {
352
+ useThemeAttrs({ inversed });
353
+ return /* @__PURE__ */ jsx(WrappedComponent, {
354
+ $rocketstyleRef: ref,
355
+ ...props
356
+ });
357
+ };
358
+ return HOC;
359
+ };
360
+ return Enhanced;
361
+ }
335
362
  const Enhanced = (WrappedComponent) => {
336
363
  const HOC = ({ ref, ...props }) => {
337
364
  const { theme, mode, isDark, isLight } = useThemeAttrs({ inversed });
338
- const callbackParams = [theme, {
365
+ const stableProps = useStableValue(props);
366
+ const filteredProps = useMemo(() => removeUndefinedProps(stableProps), [stableProps]);
367
+ const themeBag = useMemo(() => ({
339
368
  render,
340
369
  mode,
341
370
  isDark,
342
371
  isLight
343
- }];
344
- const filteredProps = removeUndefinedProps(props);
345
- const prioritizedAttrs = calculatePriorityAttrs([filteredProps, ...callbackParams]);
346
- const finalAttrs = calculateAttrs([{
347
- ...prioritizedAttrs,
348
- ...filteredProps
349
- }, ...callbackParams]);
350
- return /* @__PURE__ */ jsx(WrappedComponent, {
351
- $rocketstyleRef: ref,
352
- ...prioritizedAttrs,
353
- ...finalAttrs,
354
- ...filteredProps
355
- });
372
+ }), [
373
+ mode,
374
+ isDark,
375
+ isLight
376
+ ]);
377
+ return /* @__PURE__ */ jsx(WrappedComponent, { ...useMemo(() => {
378
+ const callbackParams = [theme, themeBag];
379
+ const prioritizedAttrs = hasPriorityAttrs ? calculatePriorityAttrs([filteredProps, ...callbackParams]) : null;
380
+ const finalAttrs = hasAttrs ? calculateAttrs([prioritizedAttrs ? {
381
+ ...prioritizedAttrs,
382
+ ...filteredProps
383
+ } : filteredProps, ...callbackParams]) : null;
384
+ return {
385
+ $rocketstyleRef: ref,
386
+ ...prioritizedAttrs,
387
+ ...finalAttrs,
388
+ ...filteredProps
389
+ };
390
+ }, [
391
+ filteredProps,
392
+ ref,
393
+ theme,
394
+ themeBag
395
+ ]) });
356
396
  };
357
397
  return HOC;
358
398
  };
@@ -367,14 +407,22 @@ const chainOptions = (opts, defaultOpts = []) => {
367
407
  else if (typeof opts === "object") result.push(() => opts);
368
408
  return result;
369
409
  };
370
- const chainOrOptions = (keys, opts, defaultOpts) => keys.reduce((acc, item) => ({
371
- ...acc,
372
- [item]: opts[item] || defaultOpts[item]
373
- }), {});
374
- const chainReservedKeyOptions = (keys, opts, defaultOpts) => keys.reduce((acc, item) => ({
375
- ...acc,
376
- [item]: chainOptions(opts[item], defaultOpts[item])
377
- }), {});
410
+ const chainOrOptions = (keys, opts, defaultOpts) => {
411
+ const result = {};
412
+ for (let i = 0; i < keys.length; i++) {
413
+ const item = keys[i];
414
+ result[item] = opts[item] || defaultOpts[item];
415
+ }
416
+ return result;
417
+ };
418
+ const chainReservedKeyOptions = (keys, opts, defaultOpts) => {
419
+ const result = {};
420
+ for (let i = 0; i < keys.length; i++) {
421
+ const item = keys[i];
422
+ result[item] = chainOptions(opts[item], defaultOpts[item]);
423
+ }
424
+ return result;
425
+ };
378
426
 
379
427
  //#endregion
380
428
  //#region src/utils/compose.ts
@@ -443,10 +491,11 @@ const calculateStyles = (styles) => {
443
491
 
444
492
  //#endregion
445
493
  //#region src/utils/collection.ts
446
- const removeNullableValues = (obj) => Object.entries(obj).filter(([, v]) => v != null && v !== false).reduce((acc, [k, v]) => ({
447
- ...acc,
448
- [k]: v
449
- }), {});
494
+ const removeNullableValues = (obj) => {
495
+ const result = {};
496
+ for (const [k, v] of Object.entries(obj)) if (v != null && v !== false) result[k] = v;
497
+ return result;
498
+ };
450
499
 
451
500
  //#endregion
452
501
  //#region src/utils/theme.ts
@@ -494,13 +543,16 @@ const getTheme = ({ rocketstate, themes, baseTheme, transformKeys, appTheme }) =
494
543
  for (const transform of deferredTransforms) merge(finalTheme, transform(finalTheme, appTheme ?? {}, themeModeCallback, config.css));
495
544
  return finalTheme;
496
545
  };
497
- const getThemeByMode = (object, mode) => Object.keys(object).reduce((acc, key) => {
498
- const value = object[key];
499
- if (typeof value === "object" && value !== null) acc[key] = getThemeByMode(value, mode);
500
- else if (isModeCallback(value)) acc[key] = value(mode);
501
- else acc[key] = value;
546
+ const getThemeByMode = (object, mode) => {
547
+ const acc = {};
548
+ for (const key in object) {
549
+ const value = object[key];
550
+ if (typeof value === "object" && value !== null) acc[key] = getThemeByMode(value, mode);
551
+ else if (isModeCallback(value)) acc[key] = value(mode);
552
+ else acc[key] = value;
553
+ }
502
554
  return acc;
503
- }, {});
555
+ };
504
556
 
505
557
  //#endregion
506
558
  //#region src/rocketstyle.tsx
@@ -519,16 +571,18 @@ const arraysEqual = (a, b) => {
519
571
  const isShallowEqualRocketstate = (a, b) => {
520
572
  if (a === b) return true;
521
573
  if (!a || !b) return false;
522
- const aKeys = Object.keys(a);
523
- if (aKeys.length !== Object.keys(b).length) return false;
524
- for (const k of aKeys) {
574
+ let aCount = 0;
575
+ for (const k in a) {
576
+ aCount++;
525
577
  const av = a[k];
526
578
  const bv = b[k];
527
579
  if (av === bv) continue;
528
580
  if (Array.isArray(av) && Array.isArray(bv) && arraysEqual(av, bv)) continue;
529
581
  return false;
530
582
  }
531
- return true;
583
+ let bCount = 0;
584
+ for (const _ in b) bCount++;
585
+ return aCount === bCount;
532
586
  };
533
587
  /**
534
588
  * Clones the current configuration and merges new options, returning a fresh
@@ -613,13 +667,18 @@ const rocketComponent = (options) => {
613
667
  useBooleans: options.useBooleans
614
668
  }), [themes]);
615
669
  const RESERVED_STYLING_PROPS_KEYS = useMemo(() => Object.keys(reservedPropNames), [reservedPropNames]);
670
+ const omitKeysSet = useMemo(() => new Set([
671
+ ...RESERVED_STYLING_PROPS_KEYS,
672
+ ...PSEUDO_KEYS,
673
+ ...options.filterAttrs
674
+ ]), [RESERVED_STYLING_PROPS_KEYS]);
616
675
  const { pseudo, ...mergeProps } = {
617
676
  ...localCtx,
618
677
  ...props
619
678
  };
620
679
  const pseudoRocketstate = {
621
680
  ...pseudo,
622
- ...pick(props, [...PSEUDO_KEYS, ...PSEUDO_META_KEYS])
681
+ ...pick(props, PSEUDO_AND_META_KEYS)
623
682
  };
624
683
  const rocketstate = _calculateStylingAttrs({
625
684
  props: pickStyledAttrs(mergeProps, reservedPropNames),
@@ -645,11 +704,7 @@ const rocketComponent = (options) => {
645
704
  theme
646
705
  ]);
647
706
  const finalProps = {
648
- ...omit(mergeProps, [
649
- ...RESERVED_STYLING_PROPS_KEYS,
650
- ...PSEUDO_KEYS,
651
- ...options.filterAttrs
652
- ]),
707
+ ...omit(mergeProps, omitKeysSet),
653
708
  ...options.passProps ? pick(mergeProps, options.passProps) : {},
654
709
  ref: ref ?? $rocketstyleRef ? internalRef : void 0,
655
710
  $rocketstyle: rocketstyle,
@@ -672,7 +727,7 @@ const rocketComponent = (options) => {
672
727
  }
673
728
  return /* @__PURE__ */ jsx(RenderComponent, { ...finalProps });
674
729
  };
675
- const RocketComponent = compose(...hocsFuncs)(EnhancedComponent);
730
+ const RocketComponent = compose(...hocsFuncs)(memo(EnhancedComponent));
676
731
  RocketComponent.IS_ROCKETSTYLE = true;
677
732
  RocketComponent.displayName = componentName;
678
733
  hoistNonReactStatics(RocketComponent, options.component);
@@ -682,8 +737,6 @@ const rocketComponent = (options) => {
682
737
  func: cloneAndEnhance,
683
738
  options
684
739
  });
685
- RocketComponent.IS_ROCKETSTYLE = true;
686
- RocketComponent.displayName = componentName;
687
740
  RocketComponent.meta = {};
688
741
  createStaticsEnhancers({
689
742
  context: RocketComponent.meta,
@@ -732,16 +785,14 @@ const rocketComponent = (options) => {
732
785
 
733
786
  //#endregion
734
787
  //#region src/init.ts
788
+ const RESERVED_KEYS_SET = new Set(ALL_RESERVED_KEYS);
735
789
  const validateInit = (name, component, dimensions) => {
736
790
  const errors = {};
737
791
  if (!component) errors.component = "Parameter `component` is missing in params!";
738
792
  if (!name) errors.name = "Parameter `name` is missing in params!";
739
793
  if (isEmpty(dimensions)) errors.dimensions = "Parameter `dimensions` is missing in params!";
740
- else {
741
- const definedDimensions = getKeys(dimensions);
742
- if (ALL_RESERVED_KEYS.some((item) => definedDimensions.some((d) => d === item))) errors.invalidDimensions = `Some of your \`dimensions\` is invalid and uses reserved static keys which are
794
+ else if (getKeys(dimensions).some((d) => RESERVED_KEYS_SET.has(d))) errors.invalidDimensions = `Some of your \`dimensions\` is invalid and uses reserved static keys which are
743
795
  ${DEFAULT_DIMENSIONS.toString()}`;
744
- }
745
796
  if (!isEmpty(errors)) throw Error(JSON.stringify(errors));
746
797
  };
747
798
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitus-labs/rocketstyle",
3
- "version": "2.6.1",
3
+ "version": "2.7.0",
4
4
  "license": "MIT",
5
5
  "author": "Vit Bokisch <vit@bokisch.cz>",
6
6
  "maintainers": [
@@ -56,6 +56,7 @@
56
56
  "prepublish": "bun run build",
57
57
  "build": "bun run vl_rolldown_build",
58
58
  "build:watch": "bun run vl_rolldown_build-watch",
59
+ "bench": "bun benchmarks/render-bench.tsx",
59
60
  "lint": "biome check src/",
60
61
  "test": "vitest run",
61
62
  "test:coverage": "vitest run --coverage",
@@ -64,15 +65,19 @@
64
65
  "typecheck": "tsc --noEmit"
65
66
  },
66
67
  "peerDependencies": {
67
- "@vitus-labs/core": "^2.6.1",
68
+ "@vitus-labs/core": "^2.7.0",
68
69
  "react": ">= 19"
69
70
  },
70
71
  "devDependencies": {
72
+ "@vitus-labs/connector-styler": "workspace:*",
71
73
  "@vitus-labs/core": "workspace:*",
72
74
  "@vitus-labs/elements": "workspace:*",
73
- "@vitus-labs/tools-rolldown": "2.3.1",
74
- "@vitus-labs/tools-storybook": "2.3.1",
75
- "@vitus-labs/tools-typescript": "2.3.1",
76
- "@vitus-labs/unistyle": "workspace:*"
75
+ "@vitus-labs/styler": "workspace:*",
76
+ "@vitus-labs/tools-rolldown": "2.5.0",
77
+ "@vitus-labs/tools-storybook": "2.5.0",
78
+ "@vitus-labs/tools-typescript": "2.5.0",
79
+ "@vitus-labs/unistyle": "workspace:*",
80
+ "jsdom": "^29.1.1",
81
+ "tinybench": "^6.0.1"
77
82
  }
78
83
  }