@pyreon/rocketstyle 0.14.0 → 0.16.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/lib/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import * as _pyreon_core0 from "@pyreon/core";
1
+ import * as _$_pyreon_core0 from "@pyreon/core";
2
2
  import { VNodeChild } from "@pyreon/core";
3
3
  import { config, context, render } from "@pyreon/ui-core";
4
4
 
@@ -87,11 +87,47 @@ type ExtractNullableKeys<T> = { [P in keyof T as IsAny<T[P]> extends true ? P :
87
87
  type SpreadTwo<L, R> = Id<Pick<L, Exclude<keyof L, keyof R>> & R>;
88
88
  type Spread<A extends readonly [...any]> = A extends [infer L, ...infer R] ? SpreadTwo<L, Spread<R>> : unknown;
89
89
  type MergeTypes<A extends readonly [...any]> = ExtractNullableKeys<Spread<A>>;
90
- type ExtractProps<TComponentOrTProps> = TComponentOrTProps extends ComponentFn<infer TProps> ? TProps : TComponentOrTProps;
90
+ /**
91
+ * Extracts the props type from a Pyreon component function — or passes
92
+ * through the input unchanged when it's already a props type.
93
+ *
94
+ * Multi-overload aware: matches up to 4 call signatures and produces the
95
+ * UNION of their first-argument types. A single-overload function still
96
+ * works (the union of 4 copies of the same props type dedupes back to
97
+ * the single shape).
98
+ *
99
+ * **Why this shape**. `T extends (props: infer P) => any ? P : never` only
100
+ * captures the LAST overload of a multi-overload function — TS's overload-
101
+ * resolution-against-conditional-types semantics. Iterator / List / Element
102
+ * are 3-overload primitives where the LAST overload (`ChildrenProps`) is the
103
+ * loosest; without overload-aware extraction, `ExtractProps<Iterator>`
104
+ * returned just `ChildrenProps` and lost both `SimpleProps<T>` and
105
+ * `ObjectProps<T>` — wrapping Iterator through `rocketstyle()` /
106
+ * `attrs()` silently downgraded the public prop surface.
107
+ *
108
+ * The pattern-match shape `T extends { (props: infer P1, ...args: any): any;
109
+ * (props: infer P2, ...args: any): any; ... }` is the canonical TS trick
110
+ * for extracting overload sets — see also `Parameters<T>` semantics.
111
+ *
112
+ * Mirrors vitus-labs PR #222.
113
+ */
114
+ type ExtractProps<TComponentOrTProps> = TComponentOrTProps extends {
115
+ (props: infer P1, ...args: any): any;
116
+ (props: infer P2, ...args: any): any;
117
+ (props: infer P3, ...args: any): any;
118
+ (props: infer P4, ...args: any): any;
119
+ } ? P1 | P2 | P3 | P4 : TComponentOrTProps extends {
120
+ (props: infer P1, ...args: any): any;
121
+ (props: infer P2, ...args: any): any;
122
+ (props: infer P3, ...args: any): any;
123
+ } ? P1 | P2 | P3 : TComponentOrTProps extends {
124
+ (props: infer P1, ...args: any): any;
125
+ (props: infer P2, ...args: any): any;
126
+ } ? P1 | P2 : TComponentOrTProps extends ComponentFn<infer TProps> ? TProps : TComponentOrTProps;
91
127
  //#endregion
92
128
  //#region src/types/styles.d.ts
93
129
  interface StylesDefault {}
94
- type Styles<S = unknown> = StylesDefault;
130
+ type Styles<_S = unknown> = StylesDefault;
95
131
  type Css = typeof config.css;
96
132
  /**
97
133
  * Props available inside `.styles()` interpolation functions.
@@ -222,16 +258,29 @@ type Configuration<C = ElementType | unknown, D extends Dimensions = Dimensions>
222
258
  statics: Record<string, any>;
223
259
  } & Record<string, any>;
224
260
  type DefaultProps = Partial<PseudoProps> & {
225
- children?: _pyreon_core0.VNodeChild;
261
+ children?: _$_pyreon_core0.VNodeChild;
226
262
  };
227
263
  //#endregion
228
264
  //#region src/types/attrs.d.ts
229
- type AttrsCb<A, T> = (props: Partial<A>, theme: T, helpers: {
265
+ /** Helpers object passed as the 3rd arg to every `.attrs(callback)`. */
266
+ type AttrsHelpers = {
230
267
  mode?: ThemeModeKeys;
231
268
  isDark?: boolean;
232
269
  isLight?: boolean;
233
270
  createElement: typeof render;
234
- }) => Partial<A>;
271
+ };
272
+ /**
273
+ * Callback signature for `.attrs((props, theme, helpers) => …)`.
274
+ *
275
+ * `Partial<A>` on the return is for the strict-typing form when callers
276
+ * pass `AttrsCb<DFP, Theme<T>>` directly. In the rocketstyle `.attrs()`
277
+ * callback overload itself we use a different shape that decouples the
278
+ * props arg (narrow, full DFP) from the return type (loose — only the
279
+ * user's explicit `<P>` generic is checked, with `Record<string,
280
+ * unknown>` allowing runtime extras like `_documentProps`). See
281
+ * `IRocketStyleComponent.attrs` for the call-site shape.
282
+ */
283
+ type AttrsCb<A, T> = (props: Partial<A>, theme: T, helpers: AttrsHelpers) => Partial<A>;
235
284
  //#endregion
236
285
  //#region src/types/hoc.d.ts
237
286
  type GenericHoc = (component: ElementType) => ElementType;
@@ -250,7 +299,7 @@ type RocketStyleComponent<OA extends TObj = {}, EA extends TObj = {}, T extends
250
299
  * @param DKP Dimensions key props.
251
300
  * @param DFP Calculated final component props
252
301
  */
253
- interface IRocketStyleComponent<OA extends TObj = {}, EA extends TObj = {}, T extends TObj = {}, CSS extends TObj = {}, S extends TObj = {}, HOC extends TObj = {}, D extends Dimensions = Dimensions, UB extends boolean = boolean, DKP extends TDKP = TDKP, DFP = MergeTypes<[OA, EA, DefaultProps, ExtractDimensionProps<D, DKP, UB>]>> {
302
+ interface IRocketStyleComponent<OA extends TObj = {}, EA extends TObj = {}, T extends TObj = {}, CSS extends TObj = {}, S extends TObj = {}, HOC extends TObj = {}, D extends Dimensions = Dimensions, UB extends boolean = boolean, DKP extends TDKP = TDKP, DFP = (OA extends infer O ? Omit<O, keyof EA & keyof O> & Partial<Pick<O, keyof EA & keyof O>> & MergeTypes<[Partial<Omit<EA, keyof O>>, DefaultProps, ExtractDimensionProps<D, DKP, UB>]> : never)> {
254
303
  (props: DFP): VNodeChild;
255
304
  config: <NC extends ElementType | unknown = unknown>({
256
305
  name,
@@ -261,10 +310,16 @@ interface IRocketStyleComponent<OA extends TObj = {}, EA extends TObj = {}, T ex
261
310
  inversed,
262
311
  passProps
263
312
  }: ConfigAttrs<NC, D, DKP, UB>) => NC extends ElementType ? RocketStyleComponent<ExtractProps<NC>, EA, T, CSS, S, HOC, D, UB, DKP> : RocketStyleComponent<OA, EA, T, CSS, S, HOC, D, UB, DKP>;
264
- attrs: <P extends TObj | unknown = unknown>(param: P extends TObj ? Partial<DFP & P> | ((props: Partial<DFP & P>, theme: Theme<T>, helpers: any) => Partial<P> & Record<string, unknown>) : Partial<DFP> | AttrsCb<DFP, Theme<T>>, config?: Partial<{
265
- priority: boolean;
266
- filter: P extends TObj ? Partial<keyof (EA & P)>[] : Partial<keyof EA>[];
267
- }>) => P extends TObj ? RocketStyleComponent<OA, MergeTypes<[EA, P]>, T, CSS, S, HOC, D, UB, DKP> : RocketStyleComponent<OA, EA, T, CSS, S, HOC, D, UB, DKP>;
313
+ attrs: {
314
+ <P extends TObj = {}>(param: (props: Partial<DFP & P>, theme: Theme<T>, helpers: AttrsHelpers) => Partial<P> & Record<string, unknown>, config?: Partial<{
315
+ priority: boolean;
316
+ filter: (keyof MergeTypes<[EA, P]>)[];
317
+ }>): RocketStyleComponent<OA, MergeTypes<[EA, P]>, T, CSS, S, HOC, D, UB, DKP>;
318
+ <P extends TObj = {}>(param: P & Partial<NoInfer<DFP>>, config?: Partial<{
319
+ priority: boolean;
320
+ filter: (keyof MergeTypes<[EA, P]>)[];
321
+ }>): RocketStyleComponent<OA, MergeTypes<[EA, P]>, T, CSS, S, HOC, D, UB, DKP>;
322
+ };
268
323
  theme: <P extends TObj = TObj>(param: Partial<P> | Partial<Styles<CSS>> | ThemeCb<P, Theme<T>>) => RocketStyleComponent<OA, EA, T, MergeTypes<[CSS, P]>, S, HOC, D, UB, DKP>;
269
324
  styles: (param: StylesCb<CSS>) => RocketStyleComponent<OA, EA, T, CSS, S, HOC, D, UB, DKP>;
270
325
  compose: <P extends ComposeParam>(param: P) => P extends TObj ? RocketStyleComponent<OA, EA, T, CSS, S, MergeTypes<[HOC, P]>, D, UB, DKP> : RocketStyleComponent<OA, EA, T, CSS, S, HOC, D, UB, DKP>;
@@ -337,5 +392,5 @@ declare const isRocketComponent: IsRocketComponent;
337
392
  */
338
393
  declare function resolveTheme<T = Record<string, unknown>>(value: (() => T) | T): T;
339
394
  //#endregion
340
- export { type AttrsCb, type ComponentFn, type ComposeParam, type ConfigAttrs, type ConsumerCb, type ConsumerCtxCBValue, type ConsumerCtxCb, type DefaultProps, type DimensionCallbackParam, type DimensionProps, type DimensionValue, type Dimensions, type ElementType, type ExtractDimensionProps, type ExtractDimensions, type ExtractProps, type GenericHoc, type IRocketStyleComponent, type IsRocketComponent, type MergeTypes, Provider, type RocketComponentType, type RocketProviderState, type RocketStyleComponent, type RocketStyleInterpolationProps, type Rocketstyle, type StylesCb, type StylesDefault, type TDKP, type TObj, type TProvider, type ThemeCb, type ThemeDefault, type ThemeMode, type ThemeModeCallback, type ThemeModeKeys, context, rocketstyle as default, rocketstyle, isRocketComponent, resolveTheme };
395
+ export { type AttrsCb, type AttrsHelpers, type ComponentFn, type ComposeParam, type ConfigAttrs, type ConsumerCb, type ConsumerCtxCBValue, type ConsumerCtxCb, type DefaultProps, type DimensionCallbackParam, type DimensionProps, type DimensionValue, type Dimensions, type ElementType, type ExtractDimensionProps, type ExtractDimensions, type ExtractProps, type GenericHoc, type IRocketStyleComponent, type IsRocketComponent, type MergeTypes, Provider, type RocketComponentType, type RocketProviderState, type RocketStyleComponent, type RocketStyleInterpolationProps, type Rocketstyle, type StylesCb, type StylesDefault, type TDKP, type TObj, type TProvider, type ThemeCb, type ThemeDefault, type ThemeMode, type ThemeModeCallback, type ThemeModeKeys, context, rocketstyle as default, rocketstyle, isRocketComponent, resolveTheme };
341
396
  //# sourceMappingURL=index2.d.ts.map
package/lib/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import { createContext, provide, useContext } from "@pyreon/core";
1
+ import { createContext, nativeCompat, provide, useContext } from "@pyreon/core";
2
2
  import { Provider as Provider$1, compose, config, context, get, hoistNonReactStatics, isEmpty, merge, omit, pick, render, set } from "@pyreon/ui-core";
3
3
  import { signal } from "@pyreon/reactivity";
4
4
 
5
5
  //#region src/constants/index.ts
6
6
  /** Tree-shakeable dev-mode flag. `true` in dev, `false` (dead code eliminated) in prod. */
7
- const __DEV__ = import.meta.env?.DEV === true;
7
+ const __DEV__ = process.env.NODE_ENV !== "production";
8
8
  /** Default theme mode used when no mode is provided via context. */
9
9
  const MODE_DEFAULT = "light";
10
10
  /** Pseudo-state interaction keys tracked for styling (hover, active, focus, pressed). */
@@ -74,6 +74,7 @@ const Provider = ({ provider = Provider$1, inversed, ...props }) => {
74
74
  children
75
75
  }) ?? null;
76
76
  };
77
+ nativeCompat(Provider);
77
78
 
78
79
  //#endregion
79
80
  //#region src/constants/defaultDimensions.ts
@@ -471,23 +472,38 @@ const getThemeByMode = (object, mode) => Object.keys(object).reduce((acc, key) =
471
472
  //#endregion
472
473
  //#region src/rocketstyle.ts
473
474
  const _countSink = globalThis;
474
- /** Clones the current configuration and merges new options, returning a fresh rocketComponent. */
475
- const cloneAndEnhance = (defaultOpts, opts) => rocketComponent({
476
- ...defaultOpts,
477
- attrs: chainOptions(opts.attrs, defaultOpts.attrs),
478
- filterAttrs: [...defaultOpts.filterAttrs ?? [], ...opts.filterAttrs ?? []],
479
- priorityAttrs: chainOptions(opts.priorityAttrs, defaultOpts.priorityAttrs),
480
- statics: {
481
- ...defaultOpts.statics,
482
- ...opts.statics
483
- },
484
- compose: {
485
- ...defaultOpts.compose,
486
- ...opts.compose
487
- },
488
- ...chainOrOptions(CONFIG_KEYS, opts, defaultOpts),
489
- ...chainReservedKeyOptions([...defaultOpts.dimensionKeys, ...STYLING_KEYS], opts, defaultOpts)
490
- });
475
+ /**
476
+ * Clones the current configuration and merges new options, returning a fresh
477
+ * rocketComponent.
478
+ *
479
+ * Component-swap reset: when `opts.component` is set AND differs from the
480
+ * current `defaultOpts.component`, the prior `attrs`, `priorityAttrs`,
481
+ * `filterAttrs`, and `compose` chains are dropped — they were tailored to the
482
+ * previous component's prop shape, and applying them to a different component
483
+ * silently leaks invalid props through to the DOM (e.g. `disabled` on an
484
+ * `<a>`). Callers who want to preserve them must re-chain explicitly:
485
+ *
486
+ * const NewBtn = Button.config({ component: 'a' }).attrs(sharedAttrs)
487
+ */
488
+ const cloneAndEnhance = (defaultOpts, opts) => {
489
+ const componentChanged = opts.component != null && opts.component !== defaultOpts.component;
490
+ return rocketComponent({
491
+ ...defaultOpts,
492
+ attrs: componentChanged ? chainOptions(opts.attrs, []) : chainOptions(opts.attrs, defaultOpts.attrs),
493
+ filterAttrs: componentChanged ? [...opts.filterAttrs ?? []] : [...defaultOpts.filterAttrs ?? [], ...opts.filterAttrs ?? []],
494
+ priorityAttrs: componentChanged ? chainOptions(opts.priorityAttrs, []) : chainOptions(opts.priorityAttrs, defaultOpts.priorityAttrs),
495
+ statics: {
496
+ ...defaultOpts.statics,
497
+ ...opts.statics
498
+ },
499
+ compose: componentChanged ? { ...opts.compose } : {
500
+ ...defaultOpts.compose,
501
+ ...opts.compose
502
+ },
503
+ ...chainOrOptions(CONFIG_KEYS, opts, defaultOpts),
504
+ ...chainReservedKeyOptions([...defaultOpts.dimensionKeys, ...STYLING_KEYS], opts, defaultOpts)
505
+ });
506
+ };
491
507
  const rocketComponent = (options) => {
492
508
  const { component, styles } = options;
493
509
  const { styled } = config;
@@ -510,6 +526,8 @@ const rocketComponent = (options) => {
510
526
  ...options.filterAttrs ?? []
511
527
  ];
512
528
  const _omitSetCache = /* @__PURE__ */ new WeakMap();
529
+ const _rsMemo = /* @__PURE__ */ new WeakMap();
530
+ const RS_MEMO_CAP = 32;
513
531
  const hocsFuncs = [rocketStyleHOC(options), ...calculateHocsFuncs(options.compose)];
514
532
  const EnhancedComponent = (props) => {
515
533
  const localCtx = useLocalContext(options.consumer);
@@ -527,7 +545,7 @@ const rocketComponent = (options) => {
527
545
  })();
528
546
  let dimResult = _dimensionsCache.get(initialDimensionThemes);
529
547
  if (dimResult) {
530
- if (import.meta.env?.DEV === true) _countSink.__pyreon_count__?.("rocketstyle.dimensionsMap.hit");
548
+ if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("rocketstyle.dimensionsMap.hit");
531
549
  } else {
532
550
  dimResult = getDimensionsMap({
533
551
  themes: initialDimensionThemes,
@@ -541,59 +559,90 @@ const rocketComponent = (options) => {
541
559
  RESERVED_STYLING_PROPS_KEYS = Object.keys(reservedPropNames);
542
560
  _reservedKeysCache.set(reservedPropNames, RESERVED_STYLING_PROPS_KEYS);
543
561
  }
544
- const $rocketstyleAccessor = () => {
545
- if (import.meta.env?.DEV === true) _countSink.__pyreon_count__?.("rocketstyle.getTheme");
562
+ const localPseudo = localCtx?.pseudo;
563
+ const _resolveRsEntry = () => {
546
564
  const theme = themeAttrs.theme;
547
565
  const mode = themeAttrs.mode;
566
+ const propsRec = props;
567
+ const rocketstateRaw = _calculateStylingAttrs({
568
+ props: pickStyledAttrs(propsRec, reservedPropNames),
569
+ dimensions
570
+ });
571
+ let key = mode;
572
+ for (const dimName in dimensions) {
573
+ const v = rocketstateRaw[dimName];
574
+ if (Array.isArray(v)) key += "|" + (v.length === 0 ? "" : v.slice().sort().join(","));
575
+ else key += "|" + (typeof v === "string" || typeof v === "number" || typeof v === "boolean" ? String(v) : v === void 0 ? "" : "~" + typeof v);
576
+ }
577
+ for (const k of ALL_PSEUDO_KEYS) {
578
+ const propV = propsRec[k];
579
+ const localV = localPseudo?.[k];
580
+ const v = propV !== void 0 ? propV : localV;
581
+ key += "|" + (v === void 0 ? "" : v ? "1" : "0");
582
+ }
583
+ let themeMemo = _rsMemo.get(theme);
584
+ if (!themeMemo) {
585
+ themeMemo = /* @__PURE__ */ new Map();
586
+ _rsMemo.set(theme, themeMemo);
587
+ }
588
+ const cached = themeMemo.get(key);
589
+ if (cached) {
590
+ if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("rocketstyle.dimensionMemo.hit");
591
+ themeMemo.delete(key);
592
+ themeMemo.set(key, cached);
593
+ return cached;
594
+ }
595
+ if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("rocketstyle.getTheme");
548
596
  const baseThemeHelper = ThemeManager$1.baseTheme;
549
597
  if (baseThemeHelper.has(theme)) {
550
- if (import.meta.env?.DEV === true) _countSink.__pyreon_count__?.("rocketstyle.localThemeManager.hit");
598
+ if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("rocketstyle.localThemeManager.hit");
551
599
  } else baseThemeHelper.set(theme, getThemeFromChain(options.theme, theme));
552
600
  const baseTheme = baseThemeHelper.get(theme);
553
601
  const dimHelper = ThemeManager$1.dimensionsThemes;
554
602
  if (dimHelper.has(theme)) {
555
- if (import.meta.env?.DEV === true) _countSink.__pyreon_count__?.("rocketstyle.localThemeManager.hit");
603
+ if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("rocketstyle.localThemeManager.hit");
556
604
  } else dimHelper.set(theme, getDimensionThemes(theme, options));
557
605
  const themes = dimHelper.get(theme);
558
- const rocketstate = _calculateStylingAttrs({
559
- props: pickStyledAttrs(props, reservedPropNames),
560
- dimensions
561
- });
562
606
  const modeBaseHelper = ThemeManager$1.modeBaseTheme[mode];
563
607
  if (modeBaseHelper.has(baseTheme)) {
564
- if (import.meta.env?.DEV === true) _countSink.__pyreon_count__?.("rocketstyle.localThemeManager.hit");
608
+ if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("rocketstyle.localThemeManager.hit");
565
609
  } else modeBaseHelper.set(baseTheme, getThemeByMode(baseTheme, mode));
566
610
  const currentModeBaseTheme = modeBaseHelper.get(baseTheme);
567
611
  const modeDimHelper = ThemeManager$1.modeDimensionTheme[mode];
568
612
  if (modeDimHelper.has(themes)) {
569
- if (import.meta.env?.DEV === true) _countSink.__pyreon_count__?.("rocketstyle.localThemeManager.hit");
613
+ if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("rocketstyle.localThemeManager.hit");
570
614
  } else modeDimHelper.set(themes, getThemeByMode(themes, mode));
571
- return getTheme({
572
- rocketstate,
615
+ const rocketstyle = getTheme({
616
+ rocketstate: rocketstateRaw,
573
617
  themes: modeDimHelper.get(themes),
574
618
  baseTheme: currentModeBaseTheme,
575
619
  transformKeys: options.transformKeys,
576
620
  appTheme: theme
577
621
  });
578
- };
579
- const localPseudo = localCtx?.pseudo;
580
- const $rocketstateAccessor = () => {
581
- const rocketstate = _calculateStylingAttrs({
582
- props: pickStyledAttrs(props, reservedPropNames),
583
- dimensions
584
- });
585
- const propPseudo = pick(props, ALL_PSEUDO_KEYS);
586
- return {
587
- ...rocketstate,
622
+ const propPseudo = pick(propsRec, ALL_PSEUDO_KEYS);
623
+ const rocketstate = {
624
+ ...rocketstateRaw,
588
625
  pseudo: {
589
626
  ...localPseudo,
590
627
  ...propPseudo
591
628
  }
592
629
  };
630
+ if (themeMemo.size >= RS_MEMO_CAP) {
631
+ const oldestKey = themeMemo.keys().next().value;
632
+ if (oldestKey !== void 0) themeMemo.delete(oldestKey);
633
+ }
634
+ const entry = {
635
+ rocketstyle,
636
+ rocketstate
637
+ };
638
+ themeMemo.set(key, entry);
639
+ return entry;
593
640
  };
641
+ const $rocketstyleAccessor = () => _resolveRsEntry().rocketstyle;
642
+ const $rocketstateAccessor = () => _resolveRsEntry().rocketstate;
594
643
  let omitSet = _omitSetCache.get(RESERVED_STYLING_PROPS_KEYS);
595
644
  if (omitSet) {
596
- if (import.meta.env?.DEV === true) _countSink.__pyreon_count__?.("rocketstyle.omitSet.hit");
645
+ if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("rocketstyle.omitSet.hit");
597
646
  } else {
598
647
  omitSet = new Set([...RESERVED_STYLING_PROPS_KEYS, ...STATIC_OMIT_KEYS]);
599
648
  _omitSetCache.set(RESERVED_STYLING_PROPS_KEYS, omitSet);
@@ -682,8 +731,8 @@ const rocketComponent = (options) => {
682
731
  {
683
732
  render,
684
733
  mode,
685
- isDark: mode === "light",
686
- isLight: mode === "dark"
734
+ isDark: mode === "dark",
735
+ isLight: mode === "light"
687
736
  }
688
737
  ])
689
738
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/rocketstyle",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "Multi-dimensional style composition for Pyreon components",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -10,6 +10,7 @@
10
10
  },
11
11
  "files": [
12
12
  "lib",
13
+ "!lib/**/*.map",
13
14
  "!lib/analysis",
14
15
  "README.md",
15
16
  "LICENSE",
@@ -41,19 +42,19 @@
41
42
  "typecheck": "tsc --noEmit"
42
43
  },
43
44
  "devDependencies": {
44
- "@pyreon/test-utils": "^0.13.2",
45
- "@pyreon/typescript": "^0.14.0",
46
- "@pyreon/ui-core": "^0.14.0",
45
+ "@pyreon/test-utils": "^0.13.3",
46
+ "@pyreon/typescript": "^0.16.0",
47
+ "@pyreon/ui-core": "^0.16.0",
47
48
  "@vitest/browser-playwright": "^4.1.4",
48
- "@vitus-labs/tools-rolldown": "^1.15.3"
49
- },
50
- "peerDependencies": {
51
- "@pyreon/core": "^0.14.0",
52
- "@pyreon/reactivity": "^0.14.0",
53
- "@pyreon/styler": "^0.14.0",
54
- "@pyreon/ui-core": "^0.14.0"
49
+ "@vitus-labs/tools-rolldown": "^2.3.0"
55
50
  },
56
51
  "engines": {
57
52
  "node": ">= 22"
53
+ },
54
+ "dependencies": {
55
+ "@pyreon/core": "^0.16.0",
56
+ "@pyreon/reactivity": "^0.16.0",
57
+ "@pyreon/styler": "^0.16.0",
58
+ "@pyreon/ui-core": "^0.16.0"
58
59
  }
59
60
  }
@@ -0,0 +1,97 @@
1
+ import type { ComponentFn } from '@pyreon/core'
2
+ import rocketstyle from '../init'
3
+
4
+ // Type-level regression tests for the post-#225/#227 `.attrs()` overload
5
+ // split: (a) DFP widening makes `.attrs(obj)` keys optional at JSX call site,
6
+ // (b) callback overload preserves Pyreon's loose-return convention so
7
+ // `_documentProps` / `tag: 'a'` runtime extras still typecheck without
8
+ // per-callsite `as any` casts.
9
+ //
10
+ // These are not bisect-load-bearing at runtime — they're type-level
11
+ // assertions exercised by `tsc --noEmit`. Including them in the suite
12
+ // makes failures show up in the test report (vitest treats type errors
13
+ // as compile failures).
14
+ describe('attrs overloads — type-level contract', () => {
15
+ // A minimal base component standing in for Text / Button / etc.
16
+ // We only care about the type-level surface here.
17
+ type BaseProps = {
18
+ tag?: 'div' | 'span' | 'p' | 'h1' | 'h2' | 'h3'
19
+ role?: string
20
+ }
21
+ const Base: ComponentFn<BaseProps> = () => null
22
+
23
+ describe('object overload — keys become optional at JSX site (PR #225)', () => {
24
+ it('accepts object with default values', () => {
25
+ const Comp = rocketstyle()({ name: 'Comp', component: Base }).attrs({
26
+ tag: 'div',
27
+ })
28
+ // The component is callable — at the JSX call site, `tag` is now
29
+ // optional because `.attrs({ tag: 'div' })` provides a default.
30
+ // Pre-#225 the type would have required `tag` at the JSX site.
31
+ // We can't directly assert via `expectTypeOf` without the dep, but
32
+ // the smoke is: the chain compiles without errors.
33
+ expect(typeof Comp).toBe('function')
34
+ })
35
+
36
+ it('accepts new keys in attrs object', () => {
37
+ // The attrs object can introduce new keys beyond the base component's
38
+ // props (here: `customField`). The keys flow into the returned
39
+ // component's extended-attrs `EA` and become typed props.
40
+ const Comp = rocketstyle()({ name: 'Comp', component: Base }).attrs({
41
+ customField: 'hello',
42
+ })
43
+ expect(typeof Comp).toBe('function')
44
+ })
45
+ })
46
+
47
+ describe('callback overload — Pyreon convention for runtime extras', () => {
48
+ it('accepts callback returning fields outside the base prop union (no as-cast needed)', () => {
49
+ // This is the canonical document-primitive pattern: a Text-based
50
+ // rocketstyle that overrides `tag` to a value outside Text's strict
51
+ // `tag` union AND adds a runtime-only `_documentProps` marker. The
52
+ // callback's return type intentionally allows `Record<string, unknown>`
53
+ // for keys outside the user's explicit `<P>` generic, matching
54
+ // Pyreon's pre-#225 convention.
55
+ const Comp = rocketstyle()({ name: 'DocLink', component: Base }).attrs<{
56
+ href?: string
57
+ }>((props) => ({
58
+ tag: 'a', // 'a' is NOT in BaseProps['tag'] — falls through Record<string, unknown>
59
+ _documentProps: { href: props.href ?? '#' },
60
+ }))
61
+ expect(typeof Comp).toBe('function')
62
+ })
63
+
64
+ it('accepts callback returning literal values (contextual narrowing via <P>)', () => {
65
+ // When the user passes an explicit `<P>` generic, the callback's
66
+ // return is contextually typed against `Partial<P>`. Writing
67
+ // `tag: 'h1'` stays narrow at literal `'h1'` — no `as const` needed.
68
+ // Note: tag is in BaseProps['tag'] union so this typechecks against
69
+ // BOTH the wildcard arm AND the explicit P-key arm.
70
+ const Comp = rocketstyle()({ name: 'Heading', component: Base }).attrs<{
71
+ level?: number
72
+ }>((props) => ({
73
+ tag: `h${props.level ?? 1}` as 'h1' | 'h2' | 'h3',
74
+ }))
75
+ expect(typeof Comp).toBe('function')
76
+ })
77
+
78
+ it('callback receives full DFP-typed props for narrow reads', () => {
79
+ // The `props` arg passed to the callback IS strictly typed as
80
+ // `Partial<DFP & P>` — so reading `props.tag` narrows against the
81
+ // wrapped component's full surface. This is the "props narrow,
82
+ // return loose" asymmetry Pyreon settled on.
83
+ const Comp = rocketstyle()({ name: 'Probe', component: Base }).attrs<{
84
+ scale?: number
85
+ }>((props) => {
86
+ // Type check: `props.scale` is `number | undefined`, `props.tag`
87
+ // is the narrow BaseProps['tag'] union | undefined.
88
+ const _scale: number | undefined = props.scale
89
+ const _tag: 'div' | 'span' | 'p' | 'h1' | 'h2' | 'h3' | undefined = props.tag
90
+ expect(_scale).toBeUndefined()
91
+ expect(_tag).toBeUndefined()
92
+ return { scale: 1 }
93
+ })
94
+ expect(typeof Comp).toBe('function')
95
+ })
96
+ })
97
+ })
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Bug reproduction: under `useBooleans: true`, `_resolveRsEntry`'s cache
3
+ * key reads `propsRec[dimName]` directly (e.g. `propsRec.state`). Boolean
4
+ * shorthand props like `<X primary />` populate `propsRec.primary` (NOT
5
+ * `propsRec.state`), so the cache key for the `state` slot is `undefined`
6
+ * → `''` regardless of which boolean variant was passed.
7
+ *
8
+ * Result: `<X primary />` and `<X secondary />` produce identical cache
9
+ * keys and share the cached entry. The first-resolved variant's
10
+ * `$rocketstyle` wins for all subsequent renders.
11
+ */
12
+ import { initTestConfig, withThemeContext } from '@pyreon/test-utils'
13
+ import rocketstyle from '../init'
14
+
15
+ let cleanup: () => void
16
+ beforeAll(() => {
17
+ cleanup = initTestConfig()
18
+ })
19
+ afterAll(() => cleanup())
20
+
21
+ const ThemeCapture: any = ({ $rocketstyle, $rocketstate, ...rest }: any) => ({
22
+ type: 'div',
23
+ props: rest,
24
+ $rocketstyle: typeof $rocketstyle === 'function' ? $rocketstyle() : $rocketstyle,
25
+ $rocketstate: typeof $rocketstate === 'function' ? $rocketstate() : $rocketstate,
26
+ })
27
+ ThemeCapture.displayName = 'ThemeCapture'
28
+
29
+ describe('rocketstyle — cache-key collision under useBooleans:true', () => {
30
+ it('different boolean variants produce different $rocketstyle (NOT collide)', () => {
31
+ const Button: any = rocketstyle({ useBooleans: true })({
32
+ name: 'BoolButton',
33
+ component: ThemeCapture,
34
+ }).states(() => ({
35
+ primary: { color: 'red' },
36
+ secondary: { color: 'blue' },
37
+ }))
38
+
39
+ // Render with primary=true. Captures the $rocketstyle resolved
40
+ // for state='primary'.
41
+ const a = withThemeContext(() => Button({ primary: true }))
42
+
43
+ // Render with secondary=true. Should resolve to state='secondary'
44
+ // and produce DIFFERENT $rocketstyle.
45
+ const b = withThemeContext(() => Button({ secondary: true }))
46
+
47
+ // Bug: a.$rocketstyle === b.$rocketstyle (same cached entry).
48
+ // Fix: a.$rocketstyle.color === 'red', b.$rocketstyle.color === 'blue'.
49
+ expect(a.$rocketstate.state).toBe('primary')
50
+ expect(b.$rocketstate.state).toBe('secondary')
51
+ expect(a.$rocketstyle.color).toBe('red')
52
+ expect(b.$rocketstyle.color).toBe('blue')
53
+ })
54
+ })
@@ -0,0 +1,9 @@
1
+ import { isNativeCompat } from '@pyreon/core'
2
+ import { describe, expect, it } from 'vitest'
3
+ import RocketstyleProvider from '../context/context'
4
+
5
+ describe('native-compat marker — @pyreon/rocketstyle', () => {
6
+ it('Provider is marked native', () => {
7
+ expect(isNativeCompat(RocketstyleProvider)).toBe(true)
8
+ })
9
+ })