praxis-kit 0.1.1 → 1.0.2

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.
@@ -31,13 +31,6 @@ type SubComponentMap = Readonly<AnyRecord>;
31
31
  * editor hovers remain self-descriptive.
32
32
  */
33
33
  type NoVariants = Readonly<EmptyRecord>;
34
- /**
35
- * Default `TPreset` type for components that declare no named presets.
36
- *
37
- * Structurally identical to `Readonly<EmptyRecord>`, but named separately so
38
- * editor hovers remain self-descriptive.
39
- */
40
- type NoPreset = Readonly<EmptyRecord>;
41
34
  /**
42
35
  * Fallback for `ExtractPluginProps<TPlugin>` when a plugin contributes no
43
36
  * props, including the no-plugin case.
@@ -46,6 +39,30 @@ type NoPreset = Readonly<EmptyRecord>;
46
39
  * hovers remain self-descriptive.
47
40
  */
48
41
  type NoPluginProps = EmptyRecord;
42
+ /**
43
+ * Determines whether an object type should be treated as empty.
44
+ *
45
+ * `keyof T` ignores call and construct signatures...
46
+ */
47
+ type IsEmptyRecord<T extends object> = T extends ((...args: never[]) => unknown) ? false : T extends (new (...args: never[]) => unknown) ? false : keyof T extends never ? true : false;
48
+ /**
49
+ * Merges two object types while eliding empty operands.
50
+ *
51
+ * If either operand is {@link EmptyRecord}, the other operand is returned
52
+ * directly instead of producing intersections such as
53
+ * `Component & EmptyRecord` in editor hovers.
54
+ *
55
+ * Unlike a homomorphic mapped type (for example `Simplify<T>`), this preserves
56
+ * call and construct signatures. Many component types are callable objects,
57
+ * and mapped types silently discard those signatures.
58
+ *
59
+ * @remarks
60
+ * Instantiate `MergeRecords` directly. Introducing an intermediate alias for
61
+ * one operand (for example `type C = PolymorphicComponent<G>`) can prevent
62
+ * `IsEmptyRecord` from evaluating eagerly, which breaks assignability under
63
+ * `exactOptionalPropertyTypes`.
64
+ */
65
+ type MergeRecords<A extends object, B extends object> = IsEmptyRecord<A> extends true ? B : IsEmptyRecord<B> extends true ? A : A & B;
49
66
  //#endregion
50
67
  //#region ../../lib/primitive/src/types/intrinsic-tag.d.ts
51
68
  type IntrinsicTag = keyof HTMLElementTagNameMap;
@@ -509,16 +526,6 @@ type StylingOptions<V extends Readonly<VariantMap> = Readonly<EmptyRecord>, TPre
509
526
  type NormalizeFn<Props extends AnyRecord = AnyRecord> = {
510
527
  normalize(props: Readonly<Props & IntrinsicProps>): Props & IntrinsicProps;
511
528
  }['normalize'];
512
- /**
513
- * The type-erased shape of {@link FactoryOptions} — every generic parameter widened to its bound.
514
- *
515
- * Use it for a value that must hold *any* factory config (a registry, a generic wrapper). It
516
- * cannot check `styling.compounds` conditions against the real variant keys/values, because it
517
- * has forgotten what they are — for that, annotate against `FactoryOptions<...>` with the concrete
518
- * generics (or `satisfies FactoryOptions<'button', Props, typeof variants>`), which keeps an
519
- * invalid compound condition a type error rather than a silent no-op.
520
- */
521
- type AnyFactoryOptions = FactoryOptions<ElementType, AnyRecord, VariantMap, RecipeMap<VariantMap>, AnyClassPluginFactory>;
522
529
  /**
523
530
  * The framework-neutral component-authoring config passed to `createContractComponent` in every
524
531
  * adapter: default tag + name, own-prop defaults, a `normalize` transform, `styling` (variants,
@@ -528,7 +535,9 @@ type AnyFactoryOptions = FactoryOptions<ElementType, AnyRecord, VariantMap, Reci
528
535
  * `satisfies FactoryOptions<TDefault, Props, typeof variants, ...>` on a config object narrows
529
536
  * `styling.compounds` conditions to the real per-variant-key shape — including resolving a
530
537
  * boolean-shaped axis (`{ true, false }`) to a real `boolean` — so a condition naming a variant or
531
- * value that does not exist is a compile error. `AnyFactoryOptions` cannot do this.
538
+ * value that does not exist is a compile error. Leaving `V` at the bare `VariantMap` instead
539
+ * (whether via `FactoryOptions`'s own default or an explicit erased instantiation) forgets that
540
+ * shape entirely, so the same invalid condition becomes a silent no-op instead.
532
541
  */
533
542
  type FactoryOptions<TDefault extends ElementType = ElementType, Props extends AnyRecord = EmptyRecord, V extends Readonly<VariantMap> = Readonly<EmptyRecord>, TPreset extends RecipeMap<V> = Readonly<EmptyRecord>, TPlugin extends AnyClassPluginFactory = AnyClassPluginFactory, TAllowed extends ElementType = ElementType> = {
534
543
  /** The intrinsic tag the component renders by default. Overridable per instance via `as`. */
@@ -537,6 +546,33 @@ type FactoryOptions<TDefault extends ElementType = ElementType, Props extends An
537
546
  readonly name?: string;
538
547
  /** Values used for the component's own (non-variant) props when the consumer omits them. */
539
548
  readonly defaults?: Partial<NoInfer<Props>>;
549
+ /**
550
+ * Optional, type-only declaration of this component's complete own-prop shape — present purely
551
+ * for type recovery, never read at runtime (see `declareProps` in `@praxis-kit/adapter-utils`).
552
+ *
553
+ * `defaults` alone can only prove a prop *has a default*, not that it's the complete prop model
554
+ * a component accepts — a `defaults: { size: 'md' }` component may still take `onClick`,
555
+ * `disabled`, and other props with no default at all, none of which a `defaults`-only recovery
556
+ * can see (see `ContractPropsFrom`'s own doc comment for the general shape of this problem).
557
+ * `props` closes that gap: when present, `ContractPropsOf<C>` / `ContractProps<typeof Component>`
558
+ * recover this declared type directly and exactly, in place of the necessarily-partial,
559
+ * literal-widened recovery `defaults` alone allows.
560
+ *
561
+ * Typed as `object | undefined`, not `NoInfer<Props> | undefined` like `defaults`/`onElement` —
562
+ * deliberately decoupled from this interface's own `Props` generic, unlike those two fields.
563
+ * `defaults`/`onElement` are tied to `Props` because real runtime code reads them against a
564
+ * concretely-resolved `Props` for a real call; `props` is never read at runtime at all (see
565
+ * above), so it has no such need, and tying it to `Props extends AnyRecord` would force every
566
+ * hand-declared prop `interface`/`type` an author passes through `declareProps<Props>()` to
567
+ * structurally satisfy `Record<string, unknown>` — a real TypeScript limitation (a named type
568
+ * without an index signature never satisfies that, even via plain assignment, only a fresh
569
+ * object literal does) that would make this field far more awkward to use for its one real job:
570
+ * carrying an author's own already-precise prop type through untouched. `ContractPropsFrom`
571
+ * (`contract-model.ts`) recovers the real value here structurally, straight off `O`'s own
572
+ * literal type — independent of this field's declared type, same as every other `Contract*From`
573
+ * derivation in that file.
574
+ */
575
+ readonly props?: object | undefined;
540
576
  /**
541
577
  * A pure `(props) => props` transform run on every render, after `enforcement.props`'s
542
578
  * normalizers see the same input. Use this for component-specific prop shaping — anything
@@ -591,11 +627,158 @@ type FactoryOptions<TDefault extends ElementType = ElementType, Props extends An
591
627
  *
592
628
  * Return a cleanup function to run when the instance unmounts.
593
629
  */
594
- readonly onElement?: (element: ElementForTag<TDefault | TAllowed>, getProps: () => Readonly<Props>) => void | (() => void);
630
+ readonly onElement?: {
631
+ onElement(element: ElementForTag<TDefault | TAllowed>, getProps: () => Readonly<Props>): void | (() => void);
632
+ }['onElement'];
595
633
  };
596
634
  //#endregion
597
- //#region ../../adapters/web/src/types/generics.d.ts
598
- type RuntimeG<TDefault extends ElementType, Props extends AnyRecord, Variants extends Readonly<VariantMap>, TPreset extends RecipeMap<Variants>> = PolymorphicGenerics<TDefault, Props, Variants, TPreset>;
635
+ //#region ../../lib/primitive/src/types/factory/contract-model.d.ts
636
+ /**
637
+ * The pipeline this file implements:
638
+ *
639
+ * ```text
640
+ * O (a raw contract literal)
641
+ * → Contract*From<O> one derivation per dimension, straight off O's own shape
642
+ * → ContractDimensions<O> the six derivations, assembled
643
+ * → ContractModel<...> the same six values, as a required-field carrier
644
+ * → ContractModelOf<C> resolves an already-`defineContract`-ed C's real model,
645
+ * or reconstructs one from a raw C via ContractModelFrom
646
+ * ```
647
+ *
648
+ * See `DECISIONS.md`'s `defineContract` entry for the TypeScript limitations that shaped the
649
+ * derivations below.
650
+ */
651
+ /**
652
+ * Canonical type-level representation of a contract's dimensions — the contract-level counterpart
653
+ * to `PolymorphicGenerics`. Every field is required, so a field's presence is never ambiguous the
654
+ * way it is on an optional `FactoryOptions` field.
655
+ */
656
+ interface ContractModel<TDefault extends ElementType = ElementType, Props extends AnyRecord = AnyRecord, V extends Readonly<VariantMap> = Readonly<VariantMap>, TPreset extends RecipeMap<VariantMap> = RecipeMap<VariantMap>, TPlugin extends AnyClassPluginFactory = AnyClassPluginFactory, TAllowed extends ElementType = ElementType> {
657
+ readonly tag: TDefault;
658
+ readonly props: Props;
659
+ readonly variants: V;
660
+ readonly preset: TPreset;
661
+ readonly plugin: TPlugin;
662
+ readonly allowed: TAllowed;
663
+ }
664
+ /**
665
+ * The phantom-marker shape read back via `T extends HasContractModel<infer M> ? M : ...` — the
666
+ * contract-level counterpart to `lib/contract-props`'s `HasGenerics<G>`. Type-only: never assigned
667
+ * at runtime.
668
+ *
669
+ * `__model` is **required**, not optional like `HasGenerics<G>`'s `__generics?` — an optional field
670
+ * would make `C extends HasContractModel<infer M>` trivially succeed for any `C`, defeating the
671
+ * one thing this marker exists to answer: did `C` really go through `defineContract`?
672
+ */
673
+ interface HasContractModel<M extends ContractModel = ContractModel> {
674
+ readonly __model: M;
675
+ }
676
+ type ContractTagFrom<O> = O extends {
677
+ tag: infer TDefault extends ElementType;
678
+ } ? TDefault : ElementType;
679
+ /** `'img'` → `string`, `1` → `number`, `true` → `boolean` — undoes `defineContract`'s `const O`
680
+ * literal narrowing so a `defaults` value doesn't become the only value a caller may pass. */
681
+ type WidenLiteral<T> = T extends string ? string : T extends number ? number : T extends boolean ? boolean : T;
682
+ type WidenShallow<T> = { [K in keyof T]: WidenLiteral<T[K]>; };
683
+ /** What the author explicitly declared, via `props: declareProps<Props>()`
684
+ * (`@praxis-kit/adapter-utils`) — used exactly as declared, no `Partial`/widening/`data-*`
685
+ * stripping (see `DefaultPropsFrom` for why those exist there but not here). */
686
+ type DeclaredPropsFrom<O> = O extends {
687
+ props: infer Props extends object | undefined;
688
+ } ? [NonNullable<Props>] extends [never] ? never : NonNullable<Props> & AnyRecord : never;
689
+ /** What can be inferred from `defaults` — partial at best, since a default only proves a prop
690
+ * *has* one, not that it's the complete prop set. `Partial`: a defaulted prop is optional to the
691
+ * caller by definition. `data-*` keys are dropped — every adapter has its own passthrough for
692
+ * those already (finding #43). */
693
+ type DefaultPropsFrom<O> = O extends {
694
+ defaults: infer Props extends AnyRecord;
695
+ } ? Partial<WidenShallow<Omit<Props, Extract<keyof Props, `data-${string}`>>>> : never;
696
+ /** Declared beats defaulted beats nothing. `[X] extends [never]`, not bare `X extends never` —
697
+ * tuple-wrapped so the check doesn't distribute if `X` is ever a union containing `never`. */
698
+ type ContractPropsFrom<O> = [DeclaredPropsFrom<O>] extends [never] ? [DefaultPropsFrom<O>] extends [never] ? EmptyRecord : DefaultPropsFrom<O> : DeclaredPropsFrom<O>;
699
+ type ContractVariantsFrom<O> = O extends {
700
+ styling: {
701
+ variants: infer V extends Readonly<VariantMap>;
702
+ };
703
+ } ? V : Readonly<EmptyRecord>;
704
+ type ContractPresetFrom<O> = O extends {
705
+ styling: {
706
+ presets: infer TPreset extends RecipeMap<VariantMap>;
707
+ };
708
+ } ? TPreset : Readonly<EmptyRecord>;
709
+ type ContractPluginFrom<O> = O extends {
710
+ styling: {
711
+ plugin: infer TPlugin extends AnyClassPluginFactory;
712
+ };
713
+ } ? TPlugin : AnyClassPluginFactory;
714
+ type ContractAllowedFrom<O> = O extends {
715
+ enforcement: {
716
+ allowedAs: readonly (infer TAllowed extends ElementType)[];
717
+ };
718
+ } ? TAllowed : ElementType;
719
+ /** The six derivations above, assembled — the direct input to `ContractModel`. */
720
+ type ContractDimensions<O> = {
721
+ readonly tag: ContractTagFrom<O>;
722
+ readonly props: ContractPropsFrom<O>;
723
+ readonly variants: ContractVariantsFrom<O>;
724
+ readonly preset: ContractPresetFrom<O>;
725
+ readonly plugin: ContractPluginFrom<O>;
726
+ readonly allowed: ContractAllowedFrom<O>;
727
+ };
728
+ /**
729
+ * Builds a `ContractModel` from a raw literal `O` — the fallback path `ContractModelOf<C>` uses
730
+ * when `C` never went through `defineContract` (no `__model` marker to read directly), and what
731
+ * `defineContract` itself attaches as that marker for a `C` that did.
732
+ */
733
+ type ContractModelFrom<O> = ContractDimensions<O> extends (infer D extends ContractDimensions<O>) ? ContractModel<D['tag'], D['props'], D['variants'], D['preset'], D['plugin'], D['allowed']> : never;
734
+ /**
735
+ * Resolves "does `C` carry a real `ContractModel` already, or do we need to build one from its raw
736
+ * shape" — every `Contract*Of` accessor (`contract-of.ts`) is a one-line projection off this.
737
+ */
738
+ type ContractModelOf<C> = C extends HasContractModel<infer M> ? M : ContractModelFrom<C>;
739
+ //#endregion
740
+ //#region ../../lib/primitive/src/types/factory/contract-of.d.ts
741
+ /**
742
+ * `FactoryOptions`-level accessor family, mirroring `polymorphic-generics.ts`'s `*Of<T>`
743
+ * convention (`DefaultOf<G>`, `PropsOf<G>`, etc.) but applied one layer up, to a contract itself
744
+ * rather than to the `PolymorphicGenerics` an adapter derives from it.
745
+ *
746
+ * Named with a `Contract` prefix specifically to avoid colliding with `polymorphic-generics.ts`'s
747
+ * own `PropsOf`/`VariantsOf`/`RecipeOf`/`AllowedOf`/`DefaultOf` — both families are re-exported
748
+ * from `@praxis-kit/core`, so a name clash would be a real conflict, not a style nit.
749
+ *
750
+ * Each accessor is a trivial projection off `ContractModelOf<C>` (`contract-model.ts`) — the one
751
+ * place that resolves "does `C` carry a real `ContractModel` (via `defineContract`) or does one
752
+ * need reconstructing from `C`'s raw shape," so every accessor shares one implementation of that
753
+ * resolution rather than repeating it. See `contract-model.ts`'s own doc comments for the
754
+ * `ContractXFrom<O>` derivation each of these ultimately reads through, and `DECISIONS.md`'s
755
+ * `defineContract` entry for the design history behind the required-pattern-match technique.
756
+ */
757
+ type ContractTagOf<C extends FactoryOptions> = ContractModelOf<C>['tag'];
758
+ /** See this file's own doc comment. Best-effort when `C` has no `ContractModel` marker — see
759
+ * `ContractPropsFrom`'s own doc comment for why. */
760
+ type ContractPropsOf<C extends FactoryOptions> = ContractModelOf<C>['props'];
761
+ /** See this file's own doc comment. */
762
+ type ContractVariantsOf<C extends FactoryOptions> = ContractModelOf<C>['variants'];
763
+ /** See this file's own doc comment. */
764
+ type ContractPresetOf<C extends FactoryOptions> = ContractModelOf<C>['preset'];
765
+ /** See this file's own doc comment. */
766
+ type ContractPluginOf<C extends FactoryOptions> = ContractModelOf<C>['plugin'];
767
+ //#endregion
768
+ //#region ../../lib/primitive/src/types/factory/contract-generics.d.ts
769
+ /**
770
+ * The canonical projection from a defined contract `C` to the `PolymorphicGenerics` shape every
771
+ * adapter's prop types are built from — the one place "G" gets computed, so it can no longer
772
+ * silently drift per adapter the way today's hand-assembled `PolymorphicGenerics<...>` instantiation
773
+ * at each `createContractComponent` call site can (see `ContractGenericsWithAllowedOf` below for
774
+ * the one confirmed instance of that drift).
775
+ *
776
+ * Folds the class-resolution plugin's own contributed props (`ExtractPluginProps<TPlugin>`) into
777
+ * `props` here, once, rather than leaving each adapter's `ContractProps` to re-derive that merge
778
+ * via its own distributive conditional type on every read — the root cause of the ~22-member
779
+ * layout-union bug PR #95 patched per-adapter (see `DECISIONS.md`'s `defineContract` entry).
780
+ */
781
+ type ContractGenericsOf<C extends FactoryOptions> = PolymorphicGenerics<ContractTagOf<C>, MergeRecords<ContractPropsOf<C>, ExtractPluginProps<ContractPluginOf<C>>>, ContractVariantsOf<C>, ContractPresetOf<C>>;
599
782
  //#endregion
600
783
  //#region ../../lib/adapter-utils/src/types/filter-predicate.d.ts
601
784
  /**
@@ -612,17 +795,20 @@ type RuntimeG<TDefault extends ElementType, Props extends AnyRecord, Variants ex
612
795
  */
613
796
  type FilterPredicate = (key: string, variantKeys: ReadonlySet<string>) => boolean;
614
797
  //#endregion
615
- //#region ../../lib/adapter-utils/src/runtime/define-component.d.ts
616
- export declare function defineContractComponent<O extends FactoryOptions>(options: O): <R>(factory: (options: O) => R) => R;
617
- //#endregion
618
798
  //#region ../../adapters/web/src/types/web-options.d.ts
619
799
  /**
620
800
  * Options accepted by createContractComponent in the web adapter.
621
801
  *
622
802
  * Identical shape to LitFactoryOptions — a plain HTMLElement subclass with
623
803
  * no framework dependency. Light DOM only; Shadow DOM is out of scope.
804
+ *
805
+ * Every generic parameter has a default (widened to that parameter's own *bound*, matching
806
+ * a wide-bound, type-erased philosophy, not `FactoryOptions`'s own narrower `EmptyRecord`-style
807
+ * defaults) so `WebFactoryOptions` can be used bare, as `createContractComponent`'s single
808
+ * `C extends WebFactoryOptions` constraint — see `ReactFactoryOptions`'s identical fix for the
809
+ * same reason.
624
810
  */
625
- type WebFactoryOptions<TDefault extends ElementType = ElementType, TProps extends AnyRecord = EmptyRecord, TVariants extends Readonly<VariantMap> = NoVariants, TPreset extends RecipeMap<TVariants> = NoPreset, TPlugin extends AnyClassPluginFactory = AnyClassPluginFactory> = FactoryOptions<TDefault, TProps, TVariants, TPreset, TPlugin> & {
811
+ type WebFactoryOptions<TDefault extends ElementType = ElementType, TProps extends AnyRecord = AnyRecord, TVariants extends Readonly<VariantMap> = Readonly<VariantMap>, TPreset extends RecipeMap<TVariants> = RecipeMap<TVariants>, TPlugin extends AnyClassPluginFactory = AnyClassPluginFactory> = FactoryOptions<TDefault, TProps, TVariants, TPreset, TPlugin> & {
626
812
  readonly filterProps?: FilterPredicate;
627
813
  };
628
814
  //#endregion
@@ -641,8 +827,12 @@ type UnknownProps = AnyRecord;
641
827
  * `G` is a phantom marker only — see `__generics` below — and defaults to the
642
828
  * widest `PolymorphicGenerics` so existing two-argument usages of this type keep
643
829
  * resolving exactly as before. Mirrors the Lit adapter's `LitContractComponent`.
830
+ *
831
+ * `C` is a second phantom marker — see `__contract` below — added alongside `G`, not replacing
832
+ * it (Phase 3 of the `defineContract` refactor), defaulting to the widest `FactoryOptions` for the
833
+ * same reason. Mirrors `LitContractComponent`'s identical addition.
644
834
  */
645
- type WebContractComponent<TVariants extends Readonly<VariantMap> = NoVariants, TPluginProps extends AnyRecord = EmptyRecord, G extends PolymorphicGenerics = PolymorphicGenerics> = {
835
+ type WebContractComponent<TVariants extends Readonly<VariantMap> = NoVariants, TPluginProps extends AnyRecord = EmptyRecord, G extends PolymorphicGenerics = PolymorphicGenerics, C extends FactoryOptions = FactoryOptions> = {
646
836
  new (): HTMLElement & {
647
837
  recipe: string | undefined;
648
838
  praxisClass: string | undefined;
@@ -662,28 +852,45 @@ type WebContractComponent<TVariants extends Readonly<VariantMap> = NoVariants, T
662
852
  * the file that built it — identical to `LitContractComponent.__generics`.
663
853
  */
664
854
  readonly __generics?: G;
855
+ /**
856
+ * Type-only; never assigned at runtime — same rationale as `__generics` above and
857
+ * React's/Preact's `__contract` (see `HasContract<C>`, `@praxis-kit/contract-props`). Carries
858
+ * the *complete* contract this component was built from (`C`, the argument
859
+ * `createContractComponent<C extends WebFactoryOptions>` was actually called with), not just
860
+ * its `PolymorphicGenerics` projection. `GenericsOf`/`ContractProps` (./contract-props) recover
861
+ * `G` *through* this field now (`ContractGenericsOf<C>`), rather than from a separately-computed
862
+ * `G` that never carried plugin-contributed props — see that file's doc comment for the bug this
863
+ * closes. Mirrors `LitContractComponent.__contract`.
864
+ */
865
+ readonly __contract?: C;
665
866
  };
666
867
  //#endregion
667
- //#region ../../lib/contract-props/src/has-generics.d.ts
868
+ //#region ../../lib/contract-props/src/has-contract.d.ts
668
869
  /**
669
- * The phantom-marker shape read back via `T extends HasGenerics<infer G> ? G : never`. See the
670
- * README for why this exists.
870
+ * The phantom-marker shape read back via `T extends HasContract<infer C> ? C : never` — the
871
+ * contract-retention counterpart to `HasGenerics<G>` in this same package. Where `HasGenerics<G>`
872
+ * lets a built component recover the `PolymorphicGenerics` an adapter derived for it,
873
+ * `HasContract<C>` lets it recover the *complete, authoritative contract* (`C`, the argument
874
+ * `createContractComponent<C extends XFactoryOptions>` was actually called with) it was derived
875
+ * from — the two are deliberately separate markers, not one broadened to do both jobs: `G` is
876
+ * "what the adapter needs to implement the component," `C` is "what the component was configured
877
+ * with" (see `DECISIONS.md`'s `defineContract` entry). A component carries both.
671
878
  *
672
- * Type-only: never assigned at runtime. In React's `PolymorphicComponent<G>`, declaring `readonly
673
- * __generics?: G` inline (structurally matching this shape) rather than writing `HasGenerics<G> &
674
- * { ...call signatures... }` was necessary to keep `PolymorphicComponent<any>`-typed test helpers
675
- * assignable — confirmed directly against that adapter's own real component type (see
676
- * `adapters/react/src/shared/types/polymorphic-props.test.ts`), not reproducible in an isolated
677
- * minimal mock (see this file's own `has-generics.test.ts`), so treat "inline, not intersected"
678
- * as an adapter-level implementation detail this package's marker shape must stay compatible
679
- * with, not a property `HasGenerics<G>` itself enforces.
879
+ * Type-only: never assigned at runtime, same rationale as `HasGenerics<G>` (see that type's own
880
+ * doc comment) — a `createContractComponent` return value gets this shape via a type assertion,
881
+ * not a real property write.
680
882
  *
681
- * Do **not** "harden" this with a `unique symbol` or other nominal brand: the whole point is that
682
- * an adapter's real callable component type structurally satisfies this shape by declaring the
683
- * same optional string-keyed field inline. Nominal purity here would break that compatibility.
883
+ * Unconstrained (no `C extends FactoryOptions` bound), matching `HasGenerics<G>`'s own choice —
884
+ * this package has no dependency on `@praxis-kit/core`/`@praxis-kit/primitive`, and adding one
885
+ * just to write a bound here isn't worth it: the accessor types that actually consume `C`
886
+ * (`ContractTagOf<C>`, etc., in `@praxis-kit/primitive`) already declare their own constraint.
887
+ *
888
+ * Do **not** "harden" this with a `unique symbol` or other nominal brand, for the same reason
889
+ * `HasGenerics<G>` doesn't: a real component's callable type needs to structurally satisfy this
890
+ * shape by declaring the same inline optional field, not by importing a nominal brand.
684
891
  */
685
- interface HasGenerics<G> {
686
- readonly __generics?: G;
892
+ interface HasContract<C> {
893
+ readonly __contract?: C;
687
894
  }
688
895
  //#endregion
689
896
  //#region ../../adapters/web/src/types/contract-props.d.ts
@@ -691,14 +898,20 @@ interface HasGenerics<G> {
691
898
  * Recovers a `WebContractComponent`'s `PolymorphicGenerics` descriptor from its own value type —
692
899
  * identical to the Lit adapter's `GenericsOf<T>` (`adapters/lit/src/types/contract-props.ts`),
693
900
  * since both adapters build a fixed-identity custom element with the same erased return type.
694
- * Needs the phantom `__generics` marker (unlike Svelte's `GenericsOf<T>`) because
695
- * `createContractComponent` here returns `WebContractComponent<TVariants, TPluginProps, G>`, not a
696
- * `BuiltRuntime<G, TOptions>` — `TDefault`/`TProps`/`TPreset` are genuinely erased from the return
697
- * type, so there is no ordinary type parameter left to `infer` them back out of.
698
- * `WebContractComponent`'s own `__generics` field (`./primitives`) exists purely to make this
699
- * recovery possible. Falls back to the widest `PolymorphicGenerics` for any non-praxis-kit value.
901
+ * Needs a marker (unlike Svelte's `GenericsOf<T>`) because `createContractComponent` here returns
902
+ * `WebContractComponent<TVariants, TPluginProps, G, C>`, not a `BuiltRuntime<G, TOptions>` —
903
+ * `TDefault`/`TProps`/`TPreset` are genuinely erased from the return type, so there is no ordinary
904
+ * type parameter left to `infer` them back out of.
905
+ *
906
+ * Re-pointed at `__contract` (Phase 4 of the `defineContract` refactor) rather than the
907
+ * separately-computed `__generics` field it used to read — see the Lit adapter's identical doc
908
+ * comment for the full reasoning: `__generics`'s own `G` never folded in plugin-contributed props
909
+ * (`findings.md` #44), and `ContractGenericsOf<C>` fixes that for free by folding
910
+ * `ExtractPluginProps<TPlugin>` into `props` once, at the canonical projection point.
911
+ *
912
+ * Falls back to the widest `PolymorphicGenerics` for any non-praxis-kit value.
700
913
  */
701
- type GenericsOf<T extends HasGenerics<PolymorphicGenerics>> = T extends HasGenerics<infer G extends PolymorphicGenerics> ? G : PolymorphicGenerics;
914
+ type GenericsOf<T extends HasContract<FactoryOptions>> = T extends HasContract<infer C extends FactoryOptions> ? ContractGenericsOf<C> extends (infer G extends PolymorphicGenerics) ? G : PolymorphicGenerics : PolymorphicGenerics;
702
915
  /**
703
916
  * A component's full prop contract — the attributes a caller can set on the custom element,
704
917
  * recovered from outside the file that built it. The Web adapter has exactly one render mode (no
@@ -728,7 +941,7 @@ type GenericsOf<T extends HasGenerics<PolymorphicGenerics>> = T extends HasGener
728
941
  * type ButtonProps = ContractProps<typeof Button>
729
942
  * ```
730
943
  */
731
- type ContractProps<T extends HasGenerics<PolymorphicGenerics>> = Simplify<OmitIndexSignature<PropsOf<GenericsOf<T>>> & OmitIndexSignature<VariantProps<VariantsOf<GenericsOf<T>>>> & DataAttributes & {
944
+ type ContractProps<T extends HasContract<FactoryOptions>> = Simplify<OmitIndexSignature<PropsOf<GenericsOf<T>>> & OmitIndexSignature<VariantProps<VariantsOf<GenericsOf<T>>>> & DataAttributes & {
732
945
  as?: never;
733
946
  recipe?: keyof RecipeOf<GenericsOf<T>>;
734
947
  }>;
@@ -815,10 +1028,15 @@ type DataAttributes = {
815
1028
  * resolve to `options.tag` here, on both the client and SSR paths. Need different semantics for one
816
1029
  * instance? Register a second component with a different `tag`, or set `role` directly — both
817
1030
  * already work today, unaffected by this.
1031
+ *
1032
+ * `TDefault`/`TProps`/`TVariants`/`TPreset`/`TPlugin` are each `ContractXOf<C>`-derived *defaults*
1033
+ * on this function's own type parameter list, mirroring every other adapter's identical fix — see
1034
+ * `@praxis-kit/react`'s own doc comment for why (computed as function type-parameter defaults, not
1035
+ * inline body computations, which doesn't resolve for a still-abstract `C`).
818
1036
  */
819
- export declare function createContractComponent<TDefault extends ElementType, TProps extends UnknownProps = EmptyRecord, TVariants extends Readonly<VariantMap> = NoVariants, TPreset extends RecipeMap<TVariants> = NoPreset, TPlugin extends AnyClassPluginFactory = AnyClassPluginFactory, TSubComponents extends Readonly<AnyRecord> = EmptyRecord>(options: WebFactoryOptions<TDefault, TProps, TVariants, TPreset, TPlugin> & {
1037
+ export declare function createContractComponent<C extends WebFactoryOptions, TDefault extends ElementType = ContractTagOf<C>, TProps extends UnknownProps = ContractPropsOf<C>, TVariants extends Readonly<VariantMap> = ContractVariantsOf<C>, TPreset extends RecipeMap<VariantMap> = ContractPresetOf<C>, TPlugin extends AnyClassPluginFactory = ContractPluginOf<C>, TSubComponents extends Readonly<AnyRecord> = EmptyRecord>(options: C & {
820
1038
  readonly subComponents?: TSubComponents;
821
- }): WebContractComponent<TVariants, ExtractPluginProps<TPlugin>, RuntimeG<TDefault, TProps, TVariants, TPreset>> & TSubComponents;
1039
+ }): WebContractComponent<TVariants, ExtractPluginProps<TPlugin>, ContractGenericsOf<C>, C> & TSubComponents;
822
1040
  //#endregion
823
1041
  //#region ../../adapters/web/src/render-to-string.d.ts
824
1042
  /**
@@ -845,4 +1063,4 @@ export declare function createContractComponent<TDefault extends ElementType, TP
845
1063
  */
846
1064
  export declare function renderContractToString(component: WebContractComponent, props?: UnknownProps, innerHTML?: string): string;
847
1065
  //#endregion
848
- export type { AnyFactoryOptions, ContractProps, ElementType, EmptyRecord, FactoryOptions, GenericsOf, PolymorphicGenerics, WebContractComponent, WebFactoryOptions };
1066
+ export type { ContractProps, ElementType, EmptyRecord, FactoryOptions, GenericsOf, PolymorphicGenerics, WebContractComponent, WebFactoryOptions };