praxis-kit 7.0.0 → 7.4.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.
@@ -2,9 +2,72 @@ import { RequireAtLeastOne, Simplify, ReadonlyDeep, OmitIndexSignature } from 't
2
2
  import { Diagnostics, DiagnosticInput, DiagnosticsMode } from '../_shared/diagnostics.js';
3
3
  import { ComponentChildren, VNode, ComponentType, JSX, Ref } from 'preact';
4
4
 
5
+ /**
6
+ * A string-keyed object whose values are of type `T`.
7
+ */
5
8
  type StringMap<T = unknown> = Record<string, T>;
9
+ /**
10
+ * A string-keyed object with values of unknown type.
11
+ */
6
12
  type AnyRecord = StringMap<unknown>;
13
+ /**
14
+ * An object type with no named properties.
15
+ *
16
+ * Unlike `{}`, this excludes arbitrary properties during type operations while
17
+ * still satisfying `extends object`.
18
+ */
7
19
  type EmptyRecord = Record<never, never>;
20
+ /**
21
+ * A compound component's named sub-components, for example
22
+ * `{ Header, Content, Footer }`.
23
+ */
24
+ type SubComponentMap = Readonly<AnyRecord>;
25
+ /**
26
+ * Default `Variants` type for components that declare no variants.
27
+ *
28
+ * Structurally identical to `Readonly<EmptyRecord>`, but named separately so
29
+ * editor hovers remain self-descriptive.
30
+ */
31
+ type NoVariants = Readonly<EmptyRecord>;
32
+ /**
33
+ * Default `TPreset` type for components that declare no named presets.
34
+ *
35
+ * Structurally identical to `Readonly<EmptyRecord>`, but named separately so
36
+ * editor hovers remain self-descriptive.
37
+ */
38
+ type NoPreset = Readonly<EmptyRecord>;
39
+ /**
40
+ * Fallback for `ExtractPluginProps<TPlugin>` when a plugin contributes no
41
+ * props, including the no-plugin case.
42
+ *
43
+ * Structurally identical to `EmptyRecord`, but named separately so editor
44
+ * hovers remain self-descriptive.
45
+ */
46
+ type NoPluginProps = EmptyRecord;
47
+ /**
48
+ * Determines whether an object type should be treated as empty.
49
+ *
50
+ * `keyof T` ignores call and construct signatures...
51
+ */
52
+ type IsEmptyRecord<T extends object> = T extends (...args: never[]) => unknown ? false : T extends new (...args: never[]) => unknown ? false : keyof T extends never ? true : false;
53
+ /**
54
+ * Merges two object types while eliding empty operands.
55
+ *
56
+ * If either operand is {@link EmptyRecord}, the other operand is returned
57
+ * directly instead of producing intersections such as
58
+ * `Component & EmptyRecord` in editor hovers.
59
+ *
60
+ * Unlike a homomorphic mapped type (for example `Simplify<T>`), this preserves
61
+ * call and construct signatures. Many component types are callable objects,
62
+ * and mapped types silently discard those signatures.
63
+ *
64
+ * @remarks
65
+ * Instantiate `MergeRecords` directly. Introducing an intermediate alias for
66
+ * one operand (for example `type C = PolymorphicComponent<G>`) can prevent
67
+ * `IsEmptyRecord` from evaluating eagerly, which breaks assignability under
68
+ * `exactOptionalPropertyTypes`.
69
+ */
70
+ type MergeRecords<A extends object, B extends object> = IsEmptyRecord<A> extends true ? B : IsEmptyRecord<B> extends true ? A : A & B;
8
71
 
9
72
  type IntrinsicTag = keyof HTMLElementTagNameMap;
10
73
 
@@ -187,7 +250,7 @@ type ClassPluginFactory<TProps extends AnyRecord = EmptyRecord> = <V extends Var
187
250
  * wherever a factory's concrete plugin-props shape isn't tracked (factory generics,
188
251
  * capability wiring). */
189
252
  type AnyClassPluginFactory = ClassPluginFactory<AnyRecord> | undefined;
190
- type ExtractPluginProps<TPlugin extends AnyClassPluginFactory> = TPlugin extends ClassPluginFactory<infer T> ? string extends keyof T ? EmptyRecord : T : EmptyRecord;
253
+ type ExtractPluginProps<TPlugin extends AnyClassPluginFactory> = TPlugin extends ClassPluginFactory<infer T> ? string extends keyof T ? NoPluginProps : T : NoPluginProps;
191
254
 
192
255
  type AriaContext = {
193
256
  readonly tag: IntrinsicTag;
@@ -251,6 +314,12 @@ type EnforcementOptions<TAllowed extends ElementType = ElementType> = {
251
314
  * `@praxis-kit/diagnostics`.
252
315
  */
253
316
  readonly diagnostics?: Diagnostics | DiagnosticsMode;
317
+ /**
318
+ * ARIA/accessibility rules evaluated against the resolved tag and props on every render.
319
+ * Each rule is a function receiving the current context and returning zero or more
320
+ * violations, some of which can carry an auto-applicable fix (see `createRemoveAttributeRule`
321
+ * and friends in `praxis-kit/contract`).
322
+ */
254
323
  readonly aria?: readonly AriaRule[];
255
324
  /**
256
325
  * Rules that need `AriaPolicyEngine`'s fix-application/caching machinery
@@ -262,6 +331,11 @@ type EnforcementOptions<TAllowed extends ElementType = ElementType> = {
262
331
  * misleading `aria` name to get the machinery it needs.
263
332
  */
264
333
  readonly rules?: readonly AriaRule[];
334
+ /**
335
+ * Declares which children are valid, by name, match predicate, and cardinality (e.g. "at
336
+ * least 1, at most 4 `Button` children"). Open by default — children matching no rule are
337
+ * still allowed unless `exclusiveChildren` is set.
338
+ */
265
339
  readonly children?: readonly ChildRuleInput[];
266
340
  /**
267
341
  * When true, only children matching a `children` rule (or text, per `allowText`)
@@ -274,19 +348,49 @@ type EnforcementOptions<TAllowed extends ElementType = ElementType> = {
274
348
  * or any listed rule. Default: true.
275
349
  */
276
350
  readonly allowText?: boolean;
351
+ /**
352
+ * Prop transforms composed with the component's own `normalize` (from `FactoryOptions`) and
353
+ * run before it. Unlike `normalize`, these live in the enforcement bucket because they
354
+ * typically encode a built-in HTML/ARIA fact rather than component-specific behavior.
355
+ */
277
356
  readonly props?: readonly PropNormalizer[];
278
357
  /** Restricts the `as` prop to this set of tags. Violations route through diagnostics. */
279
358
  readonly allowedAs?: readonly TAllowed[];
280
359
  };
281
360
 
282
361
  type StylingOptions<V extends Readonly<VariantMap> = Readonly<EmptyRecord>, TPreset extends RecipeMap<V> = Readonly<EmptyRecord>, TPlugin extends AnyClassPluginFactory = AnyClassPluginFactory> = {
362
+ /** Class applied to every instance regardless of variant selection. */
283
363
  readonly base?: ClassName;
364
+ /**
365
+ * Named variant groups (e.g. `intent`, `size`), each mapping its possible values to a
366
+ * class string. A consumer selects a value per group as a prop (`<Button intent="primary">`).
367
+ */
284
368
  readonly variants?: V;
369
+ /** Value used for a variant group when the consumer doesn't pass one explicitly. */
285
370
  readonly defaults?: Partial<DefaultVariants<V>>;
371
+ /**
372
+ * Applies an extra class only when a specific *combination* of variant selections matches —
373
+ * for cases `variants` alone can't express (e.g. `intent: 'primary'` + `size: 'lg'` together
374
+ * need a class neither variant would add on its own).
375
+ */
286
376
  readonly compounds?: readonly CompoundVariant<V>[];
377
+ /**
378
+ * Named bundles of variant values, selectable as a single unit via the `recipe` prop (e.g.
379
+ * `<Button recipe="cta">` instead of setting `intent`/`size` individually).
380
+ */
287
381
  readonly presets?: TPreset;
382
+ /** Maps a resolved tag directly to a raw class string, independent of the variant system. */
288
383
  readonly tags?: Readonly<TagMap>;
384
+ /**
385
+ * A `ClassPluginFactory` (e.g. the Tailwind layout pipeline) that extends class resolution
386
+ * with its own owned props, layered on top of `variants`/`presets`/`tags`.
387
+ */
289
388
  readonly plugin?: TPlugin;
389
+ /**
390
+ * A cache-key → resolved-class-string lookup for every statically-known variant
391
+ * combination, skipping runtime class computation entirely when a match is found. Normally
392
+ * generated by a build-time class-extraction plugin rather than hand-authored.
393
+ */
290
394
  readonly precomputedClasses?: Readonly<Record<string, string>>;
291
395
  };
292
396
 
@@ -295,17 +399,51 @@ type NormalizeFn<Props extends AnyRecord = AnyRecord> = {
295
399
  }['normalize'];
296
400
  type AnyFactoryOptions = FactoryOptions<ElementType, AnyRecord, VariantMap, RecipeMap<VariantMap>, AnyClassPluginFactory>;
297
401
  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> = {
402
+ /** The intrinsic tag the component renders by default. Overridable per instance via `as`. */
298
403
  readonly tag?: TDefault;
404
+ /** Display name used in diagnostics, dev tools, and generated component naming. */
299
405
  readonly name?: string;
406
+ /** Values used for the component's own (non-variant) props when the consumer omits them. */
300
407
  readonly defaults?: Partial<NoInfer<Props>>;
408
+ /**
409
+ * A pure `(props) => props` transform run on every render, after `enforcement.props`'s
410
+ * normalizers see the same input. Use this for component-specific prop shaping — anything
411
+ * that depends on live instance state or the real DOM element belongs in `onElement` instead.
412
+ */
301
413
  readonly normalize?: NormalizeFn<NoInfer<Props>>;
414
+ /** Variant groups, base classes, presets, and the optional class-resolution plugin. */
302
415
  readonly styling?: StylingOptions<V, TPreset, TPlugin>;
416
+ /** ARIA rules, child-content contracts, and other runtime validation for this component. */
303
417
  readonly enforcement?: EnforcementOptions<TAllowed>;
304
418
  /**
305
419
  * Adapter-resolved diagnostics default, spread in by `resolveAdapterCommonOptions`. Not meant to
306
420
  * be set directly by component authors — use `enforcement.diagnostics` to override per component.
307
421
  */
308
422
  readonly diagnostics?: Diagnostics;
423
+ /**
424
+ * Sub-components to attach to the generated root component, producing a
425
+ * compound component API (for example, `Card.Header`, `Card.Content`,
426
+ * and `Card.Footer`). Purely additive — has no effect on
427
+ * `enforcement.children`; author child rules explicitly if the component
428
+ * needs to validate its children.
429
+ */
430
+ readonly subComponents?: SubComponentMap;
431
+ /**
432
+ * Called once per instance, when the real underlying DOM element first
433
+ * exists, in every adapter — via that adapter's own native mount
434
+ * lifecycle, never through the props/attribute pipeline. Use this for
435
+ * wiring that needs the actual element (native imperative methods like
436
+ * `dialogEl.showModal()`, native events like `close`/`cancel` that have
437
+ * no prop-based equivalent), not for anything expressible as a plain
438
+ * prop.
439
+ *
440
+ * `getProps` returns the instance's *current* resolved props at call
441
+ * time — read it from inside a listener registered once at mount, rather
442
+ * than re-subscribing on every prop change.
443
+ *
444
+ * Return a cleanup function to run when the instance unmounts.
445
+ */
446
+ readonly onElement?: (element: Element, getProps: () => Readonly<Props>) => void | (() => void);
309
447
  };
310
448
 
311
449
  declare function defineContractComponent<O extends FactoryOptions>(options: O): <R>(factory: (options: O) => R) => R;
@@ -319,7 +457,7 @@ type UnknownProps = AnyRecord;
319
457
  type SlotComponent = ComponentType<UnknownProps>;
320
458
  type AnyVNode = VNode<any>;
321
459
 
322
- type PreactFactoryOptions<TDefault extends ElementType, Props extends UnknownProps, Variants extends Readonly<VariantMap>, TPreset extends RecipeMap<Variants> = Readonly<EmptyRecord>, TPlugin extends AnyClassPluginFactory = AnyClassPluginFactory> = FactoryOptions<TDefault, Props, Variants, TPreset, TPlugin> & {
460
+ type PreactFactoryOptions<TDefault extends ElementType, Props extends UnknownProps, Variants extends Readonly<VariantMap>, TPreset extends RecipeMap<Variants> = NoPreset, TPlugin extends AnyClassPluginFactory = AnyClassPluginFactory> = FactoryOptions<TDefault, Props, Variants, TPreset, TPlugin> & {
323
461
  /** Component used to render the asChild slot. Defaults to the built-in Slot. */
324
462
  slotComponent?: SlotComponent;
325
463
  /**
@@ -362,6 +500,29 @@ type PolymorphicComponent<G extends PolymorphicGenerics> = {
362
500
  displayName?: string;
363
501
  };
364
502
 
365
- declare function createContractComponent<TDefault extends ElementType, Props extends UnknownProps = EmptyRecord, Variants extends Readonly<VariantMap> = Readonly<EmptyRecord>, TPreset extends RecipeMap<Variants> = Readonly<EmptyRecord>, TPlugin extends AnyClassPluginFactory = AnyClassPluginFactory>(options: PreactFactoryOptions<TDefault, Props, Variants, TPreset, TPlugin>): PolymorphicComponent<PolymorphicGenerics<TDefault, Props & ExtractPluginProps<TPlugin>, Variants, TPreset>>;
503
+ /**
504
+ * Creates a polymorphic Preact component with praxis-kit contracts applied.
505
+ *
506
+ * ```tsx
507
+ * const Button = createContractComponent({
508
+ * tag: 'button',
509
+ * name: 'Button',
510
+ * styling: {
511
+ * base: 'btn',
512
+ * variants: { intent: { primary: 'btn--primary', ghost: 'btn--ghost' } },
513
+ * defaults: { intent: 'primary' },
514
+ * },
515
+ * })
516
+ *
517
+ * <Button intent="ghost" as="a" href="/home">Home</Button>
518
+ * ```
519
+ *
520
+ * Returns a `forwardRef` component — `ref` is forwarded to the rendered host element. Pass
521
+ * `subComponents` to attach named sub-components (`Card.Header`) and `onElement` to run setup
522
+ * once the real DOM element exists.
523
+ */
524
+ declare function createContractComponent<TDefault extends ElementType, Props extends UnknownProps = EmptyRecord, Variants extends Readonly<VariantMap> = NoVariants, TPreset extends RecipeMap<Variants> = NoPreset, TPlugin extends AnyClassPluginFactory = AnyClassPluginFactory, TSubComponents extends Readonly<AnyRecord> = EmptyRecord>(options: PreactFactoryOptions<TDefault, Props, Variants, TPreset, TPlugin> & {
525
+ readonly subComponents?: TSubComponents;
526
+ }): MergeRecords<PolymorphicComponent<PolymorphicGenerics<TDefault, MergeRecords<Props, ExtractPluginProps<TPlugin>>, Variants, TPreset>>, TSubComponents>;
366
527
 
367
528
  export { type AnyFactoryOptions, type ElementRef, type ElementType, type EmptyRecord, type PolymorphicComponent, type PolymorphicGenerics, type PolymorphicProps, type PolymorphicWithAsChild, type PreactFactoryOptions, Slottable, createContractComponent, defineContractComponent };
@@ -1,3 +1,27 @@
1
+ // ../../lib/adapter-utils/src/invariant.ts
2
+ function panic(message) {
3
+ throw new Error(message);
4
+ }
5
+ function invariant(condition, message) {
6
+ if (!condition) panic(message);
7
+ }
8
+
9
+ // ../../lib/adapter-utils/src/runtime/apply-display-name.ts
10
+ function applyDisplayName(component, name) {
11
+ Object.assign(component, { displayName: name ?? "PolymorphicComponent" });
12
+ }
13
+
14
+ // ../../lib/adapter-utils/src/runtime/define-component.ts
15
+ function defineContractComponent(options) {
16
+ return (factory) => factory(options);
17
+ }
18
+
19
+ // ../../lib/adapter-utils/src/runtime/assemble-compound-component.ts
20
+ function assembleCompoundComponent(root, subComponents) {
21
+ if (!subComponents) return root;
22
+ return Object.assign(root, subComponents);
23
+ }
24
+
1
25
  // ../../lib/primitive/src/tag/resolve-tag.ts
2
26
  function makeResolveTag(defaultTag) {
3
27
  return function tag(as) {
@@ -54,7 +78,7 @@ function isPlainObject(value) {
54
78
 
55
79
  // ../../lib/primitive/src/rule/is-dynamic-rule.ts
56
80
  function isDynamicRule(rule) {
57
- return isObject(rule, true) && rule[RULE_BRAND] === true;
81
+ return isObject(rule, true) && Reflect.get(rule, RULE_BRAND) === true;
58
82
  }
59
83
 
60
84
  // ../../lib/primitive/src/rule/resolve-rule.ts
@@ -703,17 +727,17 @@ var COMPONENT_DEFAULT_TAG = /* @__PURE__ */ Symbol.for("praxis.component-default
703
727
  // ../../lib/primitive/src/guards/children/is-tag.ts
704
728
  function getAsProp(child) {
705
729
  if (!isObject(child) || !("props" in child)) return void 0;
706
- const props = child.props;
730
+ const { props } = child;
707
731
  if (!isObject(props)) return void 0;
708
- const as = props.as;
732
+ const as = Reflect.get(props, "as");
709
733
  return isString(as) && as !== "" ? as : void 0;
710
734
  }
711
735
  function getTag(child) {
712
736
  if (!isObject(child) || !("type" in child)) return void 0;
713
- const t = child.type;
737
+ const { type: t } = child;
714
738
  if (isString(t)) return t;
715
739
  if (typeof t === "function" || isObject(t)) {
716
- const defaultTag = t[COMPONENT_DEFAULT_TAG];
740
+ const defaultTag = Reflect.get(t, COMPONENT_DEFAULT_TAG);
717
741
  if (!isString(defaultTag)) return void 0;
718
742
  return getAsProp(child) ?? defaultTag;
719
743
  }
@@ -733,25 +757,34 @@ function isTag(...args) {
733
757
  return tag !== void 0 && set2.has(tag);
734
758
  }
735
759
 
736
- // ../../adapters/preact/src/create-contract-component.ts
737
- import { forwardRef as forwardRef2 } from "preact/compat";
738
-
739
- // ../../lib/adapter-utils/src/invariant.ts
740
- function panic(message) {
741
- throw new Error(message);
742
- }
743
- function invariant(condition, message) {
744
- if (!condition) panic(message);
745
- }
746
-
747
- // ../../lib/adapter-utils/src/runtime/apply-display-name.ts
748
- function applyDisplayName(component, name) {
749
- Object.assign(component, { displayName: name ?? "PolymorphicComponent" });
750
- }
751
-
752
- // ../../lib/adapter-utils/src/runtime/define-component.ts
753
- function defineContractComponent(options) {
754
- return (factory) => factory(options);
760
+ // ../../lib/adapter-utils/src/runtime/finalize-component.ts
761
+ function finalizeComponent(component, defaultTag, subComponents) {
762
+ if (typeof defaultTag === "string") {
763
+ Object.assign(component, { [COMPONENT_DEFAULT_TAG]: defaultTag });
764
+ }
765
+ return assembleCompoundComponent(component, subComponents);
766
+ }
767
+
768
+ // ../../lib/adapter-utils/src/runtime/is-factory-options-like.ts
769
+ var FACTORY_OPTIONS_FIELD_VALIDATORS = {
770
+ tag: (v) => v === void 0 || isString(v),
771
+ name: (v) => v === void 0 || isString(v),
772
+ defaults: (v) => v === void 0 || isObject(v),
773
+ normalize: (v) => v === void 0 || isFunction(v),
774
+ styling: (v) => v === void 0 || isObject(v),
775
+ enforcement: (v) => v === void 0 || isObject(v),
776
+ diagnostics: (v) => v === void 0 || isObject(v),
777
+ subComponents: (v) => v === void 0 || isObject(v),
778
+ onElement: (v) => v === void 0 || isFunction(v)
779
+ };
780
+ function isFactoryOptionsLike(options, extraFieldValidators) {
781
+ if (!isObject(options)) return false;
782
+ const validators = { ...FACTORY_OPTIONS_FIELD_VALIDATORS, ...extraFieldValidators };
783
+ for (const [key, value] of Object.entries(options)) {
784
+ const validate = validators[key];
785
+ if (!validate || !validate(value)) return false;
786
+ }
787
+ return true;
755
788
  }
756
789
 
757
790
  // ../../lib/contract/src/aria/aria-role-policy.ts
@@ -2904,7 +2937,7 @@ function getChildProp(child, key) {
2904
2937
  if (!isVNodeLike(child)) return void 0;
2905
2938
  const { props } = child;
2906
2939
  if (!isObject(props)) return void 0;
2907
- return props[key];
2940
+ return Reflect.get(props, key);
2908
2941
  }
2909
2942
 
2910
2943
  // ../core/src/html/contracts/aria/landmarks.ts
@@ -3619,6 +3652,10 @@ function applyMergePolicy(key, slotVal, childVal) {
3619
3652
  return policyHandlers[classifyProp(key, slotVal, childVal)](slotVal, childVal);
3620
3653
  }
3621
3654
 
3655
+ // ../../adapters/preact/src/create-contract-component.ts
3656
+ import { forwardRef as forwardRef2 } from "preact/compat";
3657
+ import { useCallback, useRef } from "preact/hooks";
3658
+
3622
3659
  // ../../adapters/preact/src/render.tsx
3623
3660
  import { h as h2 } from "preact";
3624
3661
 
@@ -3881,18 +3918,54 @@ function buildRuntime(options) {
3881
3918
  return built;
3882
3919
  }
3883
3920
 
3921
+ // ../../adapters/preact/src/is-polymorphic-component.ts
3922
+ function isPolymorphicComponent(value) {
3923
+ if (!isFunction(value)) return false;
3924
+ if (!("displayName" in value)) return true;
3925
+ return value.displayName === void 0 || isString(value.displayName);
3926
+ }
3927
+
3928
+ // ../../adapters/preact/src/to-preact-factory-options.ts
3929
+ var PREACT_FIELD_VALIDATORS = {
3930
+ slotComponent: (v) => v === void 0 || isFunction(v) || isObject(v),
3931
+ filterProps: (v) => v === void 0 || isFunction(v)
3932
+ };
3933
+ function isPreactFactoryOptions(options) {
3934
+ return isFactoryOptionsLike(options, PREACT_FIELD_VALIDATORS);
3935
+ }
3936
+
3884
3937
  // ../../adapters/preact/src/create-contract-component.ts
3885
3938
  function createContractComponent(options) {
3939
+ invariant(isPreactFactoryOptions(options), "options is not a valid PreactFactoryOptions object");
3886
3940
  const bundle = buildRuntime(options);
3941
+ const { onElement } = options;
3887
3942
  const Component = forwardRef2(function Component2(props, ref) {
3888
- return render({ ...bundle, props, ref: ref ?? null });
3943
+ const propsRef = useRef(props);
3944
+ propsRef.current = props;
3945
+ const cleanupRef = useRef(void 0);
3946
+ const onElementRef = useCallback((el) => {
3947
+ if (!onElement) return;
3948
+ if (el) {
3949
+ cleanupRef.current = onElement(el, () => propsRef.current) ?? void 0;
3950
+ } else {
3951
+ cleanupRef.current?.();
3952
+ cleanupRef.current = void 0;
3953
+ }
3954
+ }, []);
3955
+ const mergedRef = onElement ? mergeRefs(ref, onElementRef) : ref;
3956
+ return render({ ...bundle, props, ref: mergedRef });
3889
3957
  });
3890
3958
  applyDisplayName(Component, options.name);
3891
- const defaultTag = bundle.runtime.options.defaultTag;
3892
- if (typeof defaultTag === "string") {
3893
- Object.assign(Component, { [COMPONENT_DEFAULT_TAG]: defaultTag });
3894
- }
3895
- return Component;
3959
+ const assembled = finalizeComponent(
3960
+ Component,
3961
+ bundle.runtime.options.defaultTag,
3962
+ options.subComponents
3963
+ );
3964
+ invariant(
3965
+ isPolymorphicComponent(assembled),
3966
+ "Generated component failed to satisfy the PolymorphicComponent shape"
3967
+ );
3968
+ return assembled;
3896
3969
  }
3897
3970
  export {
3898
3971
  Slottable,
@@ -1,5 +1,5 @@
1
- import { U as UnknownProps, E as ElementType, a as EmptyRecord, V as VariantMap, R as RecipeMap, A as AnyClassPluginFactory, b as ReactFactoryOptions, P as PolymorphicComponent, c as PolymorphicGenerics, d as ExtractPluginProps } from '../react-options-Cm99IE5J.js';
2
- export { e as AnyFactoryOptions, f as ElementRef, F as FactoryOptions, g as PolymorphicProps, h as PolymorphicWithAsChild, i as PolymorphicWithRender, j as RenderCallbackProps, S as Slottable, k as SlottableProps, m as composeRefs, l as defineContractComponent, m as mergeRefs } from '../react-options-Cm99IE5J.js';
1
+ import { U as UnknownProps, E as ElementType, a as EmptyRecord, V as VariantMap, N as NoVariants, R as RecipeMap, b as NoPreset, A as AnyClassPluginFactory, c as AnyRecord, d as ReactFactoryOptions, M as MergeRecords, P as PolymorphicComponent, e as PolymorphicGenerics, f as ExtractPluginProps } from '../react-options-BnjZpdVh.js';
2
+ export { g as AnyFactoryOptions, h as ElementRef, F as FactoryOptions, i as PolymorphicProps, j as PolymorphicWithAsChild, k as PolymorphicWithRender, l as RenderCallbackProps, S as Slottable, m as SlottableProps, n as composeRefs, o as defineContractComponent, n as mergeRefs } from '../react-options-BnjZpdVh.js';
3
3
  import * as react from 'react';
4
4
  import { ReactElement, Ref } from 'react';
5
5
  import 'type-fest';
@@ -22,7 +22,30 @@ type CloneInput = {
22
22
  ref: NormalizedRef;
23
23
  };
24
24
 
25
- declare function createContractComponent<TDefault extends ElementType, Props extends UnknownProps = EmptyRecord, Variants extends Readonly<VariantMap> = Readonly<EmptyRecord>, TPreset extends RecipeMap<Variants> = Readonly<EmptyRecord>, TPlugin extends AnyClassPluginFactory = AnyClassPluginFactory, TAllowed extends ElementType = ElementType>(options: ReactFactoryOptions<TDefault, Props, Variants, TPreset, TPlugin, TAllowed>): PolymorphicComponent<PolymorphicGenerics<TDefault, Props & ExtractPluginProps<TPlugin>, Variants, TPreset, TAllowed>>;
25
+ /**
26
+ * Creates a polymorphic React 19 component with praxis-kit contracts applied.
27
+ *
28
+ * ```tsx
29
+ * const Button = createContractComponent({
30
+ * tag: 'button',
31
+ * name: 'Button',
32
+ * styling: {
33
+ * base: 'btn',
34
+ * variants: { intent: { primary: 'btn--primary', ghost: 'btn--ghost' } },
35
+ * defaults: { intent: 'primary' },
36
+ * },
37
+ * })
38
+ *
39
+ * <Button intent="ghost" as="a" href="/home">Home</Button>
40
+ * ```
41
+ *
42
+ * `ref` is accepted as a plain prop (React 19) and forwarded to the rendered host element or,
43
+ * with `asChild`, to the consumer's own element. Pass `subComponents` to attach named
44
+ * sub-components (`Card.Header`) and `onElement` to run setup once the real DOM element exists.
45
+ */
46
+ declare function createContractComponent<TDefault extends ElementType, Props extends UnknownProps = EmptyRecord, Variants extends Readonly<VariantMap> = NoVariants, TPreset extends RecipeMap<Variants> = NoPreset, TPlugin extends AnyClassPluginFactory = AnyClassPluginFactory, TAllowed extends ElementType = ElementType, TSubComponents extends Readonly<AnyRecord> = EmptyRecord>(options: ReactFactoryOptions<TDefault, Props, Variants, TPreset, TPlugin, TAllowed> & {
47
+ readonly subComponents?: TSubComponents;
48
+ }): MergeRecords<PolymorphicComponent<PolymorphicGenerics<TDefault, MergeRecords<Props, ExtractPluginProps<TPlugin>>, Variants, TPreset, TAllowed>>, TSubComponents>;
26
49
 
27
50
  type SlotProps = {
28
51
  ref?: Ref<unknown> | null;
@@ -1,18 +1,24 @@
1
1
  import {
2
- COMPONENT_DEFAULT_TAG,
3
2
  SLOT_NAME,
4
3
  Slottable,
5
4
  applyDisplayName,
6
5
  applySlot,
7
6
  buildRuntime,
8
7
  defineContractComponent,
8
+ finalizeComponent,
9
9
  getElementRef,
10
10
  getPropsRef,
11
11
  hasWarningGetter,
12
+ invariant,
13
+ isPolymorphicComponent,
14
+ isReactFactoryOptions,
12
15
  makeCloneSlotChild,
13
16
  mergeRefs,
14
17
  render
15
- } from "../chunk-35CJAOJW.js";
18
+ } from "../chunk-EIKPSL26.js";
19
+
20
+ // ../../adapters/react/src/current/create-contract-component.ts
21
+ import { useCallback, useRef } from "react";
16
22
 
17
23
  // ../../adapters/react/src/current/slot/composeRefs.ts
18
24
  function getChildRef(element) {
@@ -43,16 +49,36 @@ function buildRuntime2(options) {
43
49
 
44
50
  // ../../adapters/react/src/current/create-contract-component.ts
45
51
  function createContractComponent(options) {
52
+ invariant(isReactFactoryOptions(options), "options is not a valid ReactFactoryOptions object");
46
53
  const bundle = buildRuntime2(options);
54
+ const { onElement } = options;
47
55
  function Component({ ref, ...props }) {
48
- return render({ ...bundle, props, ref: ref ?? null });
56
+ const propsRef = useRef(props);
57
+ propsRef.current = props;
58
+ const cleanupRef = useRef(void 0);
59
+ const onElementRef = useCallback((el) => {
60
+ if (!onElement) return;
61
+ if (el) {
62
+ cleanupRef.current = onElement(el, () => propsRef.current) ?? void 0;
63
+ } else {
64
+ cleanupRef.current?.();
65
+ cleanupRef.current = void 0;
66
+ }
67
+ }, []);
68
+ const mergedRef = onElement ? mergeRefs(ref, onElementRef) : ref;
69
+ return render({ ...bundle, props, ref: mergedRef ?? null });
49
70
  }
50
71
  applyDisplayName(Component, options.name);
51
- const defaultTag = bundle.runtime.options.defaultTag;
52
- if (typeof defaultTag === "string") {
53
- Object.assign(Component, { [COMPONENT_DEFAULT_TAG]: defaultTag });
54
- }
55
- return Component;
72
+ const assembled = finalizeComponent(
73
+ Component,
74
+ bundle.runtime.options.defaultTag,
75
+ options.subComponents
76
+ );
77
+ invariant(
78
+ isPolymorphicComponent(assembled),
79
+ "Generated component failed to satisfy the PolymorphicComponent shape"
80
+ );
81
+ return assembled;
56
82
  }
57
83
  export {
58
84
  Slot,
@@ -1,10 +1,32 @@
1
- import { E as ElementType, U as UnknownProps, a as EmptyRecord, V as VariantMap, R as RecipeMap, A as AnyClassPluginFactory, b as ReactFactoryOptions, P as PolymorphicComponent, c as PolymorphicGenerics, d as ExtractPluginProps } from '../react-options-Cm99IE5J.js';
2
- export { e as AnyFactoryOptions, f as ElementRef, F as FactoryOptions, g as PolymorphicProps, h as PolymorphicWithAsChild, i as PolymorphicWithRender, j as RenderCallbackProps, S as Slottable, k as SlottableProps, l as defineContractComponent, m as mergeRefs } from '../react-options-Cm99IE5J.js';
1
+ import { E as ElementType, U as UnknownProps, a as EmptyRecord, V as VariantMap, N as NoVariants, R as RecipeMap, b as NoPreset, A as AnyClassPluginFactory, d as ReactFactoryOptions, P as PolymorphicComponent, e as PolymorphicGenerics, M as MergeRecords, f as ExtractPluginProps } from '../react-options-BnjZpdVh.js';
2
+ export { g as AnyFactoryOptions, h as ElementRef, F as FactoryOptions, i as PolymorphicProps, j as PolymorphicWithAsChild, k as PolymorphicWithRender, l as RenderCallbackProps, S as Slottable, m as SlottableProps, o as defineContractComponent, n as mergeRefs } from '../react-options-BnjZpdVh.js';
3
3
  import * as react from 'react';
4
4
  import 'type-fest';
5
5
  import '../_shared/diagnostics.js';
6
6
 
7
- declare function createContractComponent<TDefault extends ElementType, Props extends UnknownProps = EmptyRecord, Variants extends Readonly<VariantMap> = Readonly<EmptyRecord>, TPreset extends RecipeMap<Variants> = Readonly<EmptyRecord>, TPlugin extends AnyClassPluginFactory = AnyClassPluginFactory, TAllowed extends ElementType = ElementType>(options: ReactFactoryOptions<TDefault, Props, Variants, TPreset, TPlugin, TAllowed>): PolymorphicComponent<PolymorphicGenerics<TDefault, Props & ExtractPluginProps<TPlugin>, Variants, TPreset, TAllowed>>;
7
+ /**
8
+ * Creates a polymorphic React component with praxis-kit contracts applied, for React 18 and
9
+ * earlier (use `praxis-kit/react` instead on React 19, which accepts `ref` as a plain prop).
10
+ *
11
+ * ```tsx
12
+ * const Button = createContractComponent({
13
+ * tag: 'button',
14
+ * name: 'Button',
15
+ * styling: {
16
+ * base: 'btn',
17
+ * variants: { intent: { primary: 'btn--primary', ghost: 'btn--ghost' } },
18
+ * defaults: { intent: 'primary' },
19
+ * },
20
+ * })
21
+ *
22
+ * <Button intent="ghost" as="a" href="/home">Home</Button>
23
+ * ```
24
+ *
25
+ * Returns a `forwardRef` component — `ref` is forwarded to the rendered host element the same
26
+ * way it works in `praxis-kit/react`. Pass `onElement` to run setup once the real DOM element
27
+ * exists; this adapter doesn't support `subComponents`.
28
+ */
29
+ declare function createContractComponent<TDefault extends ElementType, Props extends UnknownProps = EmptyRecord, Variants extends Readonly<VariantMap> = NoVariants, TPreset extends RecipeMap<Variants> = NoPreset, TPlugin extends AnyClassPluginFactory = AnyClassPluginFactory, TAllowed extends ElementType = ElementType>(options: ReactFactoryOptions<TDefault, Props, Variants, TPreset, TPlugin, TAllowed>): PolymorphicComponent<PolymorphicGenerics<TDefault, MergeRecords<Props, ExtractPluginProps<TPlugin>>, Variants, TPreset, TAllowed>>;
8
30
 
9
31
  type SlotProps = {
10
32
  [key: string]: unknown;
@@ -13,10 +13,10 @@ import {
13
13
  makeCloneSlotChild,
14
14
  mergeRefs,
15
15
  render
16
- } from "../chunk-35CJAOJW.js";
16
+ } from "../chunk-EIKPSL26.js";
17
17
 
18
18
  // ../../adapters/react/src/legacy/create-contract-component.ts
19
- import { forwardRef as forwardRef2 } from "react";
19
+ import { forwardRef as forwardRef2, useCallback, useRef } from "react";
20
20
 
21
21
  // ../../adapters/react/src/legacy/slot/Slot.tsx
22
22
  import { forwardRef } from "react";
@@ -49,8 +49,22 @@ function buildRuntime2(options) {
49
49
  // ../../adapters/react/src/legacy/create-contract-component.ts
50
50
  function createContractComponent(options) {
51
51
  const bundle = buildRuntime2(options);
52
+ const { onElement } = options;
52
53
  const Component = forwardRef2(function Component2(props, ref) {
53
- return render({ ...bundle, props, ref });
54
+ const propsRef = useRef(props);
55
+ propsRef.current = props;
56
+ const cleanupRef = useRef(void 0);
57
+ const onElementRef = useCallback((el) => {
58
+ if (!onElement) return;
59
+ if (el) {
60
+ cleanupRef.current = onElement(el, () => propsRef.current) ?? void 0;
61
+ } else {
62
+ cleanupRef.current?.();
63
+ cleanupRef.current = void 0;
64
+ }
65
+ }, []);
66
+ const mergedRef = onElement ? mergeRefs(ref, onElementRef) : ref;
67
+ return render({ ...bundle, props, ref: mergedRef });
54
68
  });
55
69
  applyDisplayName(Component, options.name);
56
70
  const defaultTag = bundle.runtime.options.defaultTag;