praxis-kit 7.3.0 → 7.5.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.
@@ -17,8 +17,20 @@ import { RequireAtLeastOne, Simplify, ValueOf } from 'type-fest';
17
17
  */
18
18
  declare const layoutKeys: readonly ["flex", "inline-flex", "grid", "inline-grid", "block", "inline-block", "inline", "hidden", "contents", "flow-root", "list-item", "table", "inline-table", "table-caption", "table-cell", "table-column", "table-column-group", "table-footer-group", "table-header-group", "table-row-group", "table-row"];
19
19
 
20
+ /**
21
+ * A string-keyed object whose values are of type `T`.
22
+ */
20
23
  type StringMap<T = unknown> = Record<string, T>;
24
+ /**
25
+ * A string-keyed object with values of unknown type.
26
+ */
21
27
  type AnyRecord = StringMap<unknown>;
28
+ /**
29
+ * An object type with no named properties.
30
+ *
31
+ * Unlike `{}`, this excludes arbitrary properties during type operations while
32
+ * still satisfying `extends object`.
33
+ */
22
34
  type EmptyRecord = Record<never, never>;
23
35
 
24
36
  type IntrinsicTag = keyof HTMLElementTagNameMap;
@@ -2,7 +2,13 @@ import { Plugin } from 'vite';
2
2
  import { Except, Simplify } from 'type-fest';
3
3
  import ts from 'typescript';
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>;
7
13
 
8
14
  declare enum DiagnosticCategory {
@@ -3,15 +3,89 @@ import { Diagnostics, DiagnosticInput, DiagnosticsMode } from '../_shared/diagno
3
3
  import * as vue from 'vue';
4
4
  import { AllowedComponentProps } from 'vue';
5
5
 
6
+ /**
7
+ * A string-keyed object whose values are of type `T`.
8
+ */
6
9
  type StringMap<T = unknown> = Record<string, T>;
10
+ /**
11
+ * A string-keyed object with values of unknown type.
12
+ */
7
13
  type AnyRecord = StringMap<unknown>;
14
+ /**
15
+ * An object type with no named properties.
16
+ *
17
+ * Unlike `{}`, this excludes arbitrary properties during type operations while
18
+ * still satisfying `extends object`.
19
+ */
8
20
  type EmptyRecord = Record<never, never>;
9
- /** A compound component's named sub-components, e.g. `{ Header, Content, Footer }`. */
21
+ /**
22
+ * A compound component's named sub-components, for example
23
+ * `{ Header, Content, Footer }`.
24
+ */
10
25
  type SubComponentMap = Readonly<AnyRecord>;
26
+ /**
27
+ * Default `Variants` type for components that declare no variants.
28
+ *
29
+ * Structurally identical to `Readonly<EmptyRecord>`, but named separately so
30
+ * editor hovers remain self-descriptive.
31
+ */
32
+ type NoVariants = Readonly<EmptyRecord>;
33
+ /**
34
+ * Default `TPreset` type for components that declare no named presets.
35
+ *
36
+ * Structurally identical to `Readonly<EmptyRecord>`, but named separately so
37
+ * editor hovers remain self-descriptive.
38
+ */
39
+ type NoPreset = Readonly<EmptyRecord>;
40
+ /**
41
+ * Fallback for `ExtractPluginProps<TPlugin>` when a plugin contributes no
42
+ * props, including the no-plugin case.
43
+ *
44
+ * Structurally identical to `EmptyRecord`, but named separately so editor
45
+ * hovers remain self-descriptive.
46
+ */
47
+ type NoPluginProps = EmptyRecord;
48
+ /**
49
+ * Determines whether an object type should be treated as empty.
50
+ *
51
+ * `keyof T` ignores call and construct signatures...
52
+ */
53
+ type IsEmptyRecord<T extends object> = T extends (...args: never[]) => unknown ? false : T extends new (...args: never[]) => unknown ? false : keyof T extends never ? true : false;
54
+ /**
55
+ * Merges two object types while eliding empty operands.
56
+ *
57
+ * If either operand is {@link EmptyRecord}, the other operand is returned
58
+ * directly instead of producing intersections such as
59
+ * `Component & EmptyRecord` in editor hovers.
60
+ *
61
+ * Unlike a homomorphic mapped type (for example `Simplify<T>`), this preserves
62
+ * call and construct signatures. Many component types are callable objects,
63
+ * and mapped types silently discard those signatures.
64
+ *
65
+ * @remarks
66
+ * Instantiate `MergeRecords` directly. Introducing an intermediate alias for
67
+ * one operand (for example `type C = PolymorphicComponent<G>`) can prevent
68
+ * `IsEmptyRecord` from evaluating eagerly, which breaks assignability under
69
+ * `exactOptionalPropertyTypes`.
70
+ */
71
+ type MergeRecords<A extends object, B extends object> = IsEmptyRecord<A> extends true ? B : IsEmptyRecord<B> extends true ? A : A & B;
11
72
 
12
73
  type IntrinsicTag = keyof HTMLElementTagNameMap;
13
74
 
14
75
  type ElementType = IntrinsicTag | (string & {});
76
+ /**
77
+ * Resolves a component's default tag to its real DOM interface — `HTMLDialogElement` for
78
+ * `'dialog'`, `HTMLDetailsElement` for `'details'`, and so on — falling back to `HTMLElement`
79
+ * for custom-element tags or anything not in `HTMLElementTagNameMap`. Used to type
80
+ * `FactoryOptions.onElement`'s `element` param so component authors get direct, correctly-typed
81
+ * access to tag-specific native members (`dialogEl.showModal()`) without an unsafe cast.
82
+ *
83
+ * The fallback is `HTMLElement`, not the more generic `Element` — every tag reachable through
84
+ * `IntrinsicTag` extends it, and so does every custom element per spec, so members `HTMLElement`
85
+ * itself declares (`showPopover()`/`hidePopover()`/`togglePopover()`, the `popover` attribute)
86
+ * stay directly accessible even for tags with no dedicated entry in `HTMLElementTagNameMap`.
87
+ */
88
+ type ElementForTag<TDefault extends ElementType> = TDefault extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[TDefault] : HTMLElement;
15
89
 
16
90
  declare const KNOWN_ARIA_ROLES: readonly ["alert", "alertdialog", "application", "article", "banner", "blockquote", "button", "caption", "cell", "checkbox", "code", "columnheader", "combobox", "complementary", "contentinfo", "definition", "deletion", "dialog", "document", "emphasis", "feed", "figure", "form", "generic", "grid", "gridcell", "group", "heading", "img", "insertion", "link", "list", "listbox", "listitem", "log", "main", "marquee", "math", "menu", "menubar", "menuitem", "menuitemcheckbox", "menuitemradio", "meter", "navigation", "none", "note", "option", "paragraph", "presentation", "progressbar", "radio", "radiogroup", "region", "row", "rowgroup", "rowheader", "scrollbar", "search", "searchbox", "separator", "slider", "spinbutton", "status", "strong", "subscript", "superscript", "switch", "tab", "table", "tablist", "tabpanel", "term", "textbox", "time", "timer", "toolbar", "tooltip", "tree", "treegrid", "treeitem"];
17
91
  type KnownAriaRole = (typeof KNOWN_ARIA_ROLES)[number];
@@ -190,7 +264,7 @@ type ClassPluginFactory<TProps extends AnyRecord = EmptyRecord> = <V extends Var
190
264
  * wherever a factory's concrete plugin-props shape isn't tracked (factory generics,
191
265
  * capability wiring). */
192
266
  type AnyClassPluginFactory = ClassPluginFactory<AnyRecord> | undefined;
193
- type ExtractPluginProps<TPlugin extends AnyClassPluginFactory> = TPlugin extends ClassPluginFactory<infer T> ? string extends keyof T ? EmptyRecord : T : EmptyRecord;
267
+ type ExtractPluginProps<TPlugin extends AnyClassPluginFactory> = TPlugin extends ClassPluginFactory<infer T> ? string extends keyof T ? NoPluginProps : T : NoPluginProps;
194
268
 
195
269
  type AriaContext = {
196
270
  readonly tag: IntrinsicTag;
@@ -254,6 +328,12 @@ type EnforcementOptions<TAllowed extends ElementType = ElementType> = {
254
328
  * `@praxis-kit/diagnostics`.
255
329
  */
256
330
  readonly diagnostics?: Diagnostics | DiagnosticsMode;
331
+ /**
332
+ * ARIA/accessibility rules evaluated against the resolved tag and props on every render.
333
+ * Each rule is a function receiving the current context and returning zero or more
334
+ * violations, some of which can carry an auto-applicable fix (see `createRemoveAttributeRule`
335
+ * and friends in `praxis-kit/contract`).
336
+ */
257
337
  readonly aria?: readonly AriaRule[];
258
338
  /**
259
339
  * Rules that need `AriaPolicyEngine`'s fix-application/caching machinery
@@ -265,6 +345,11 @@ type EnforcementOptions<TAllowed extends ElementType = ElementType> = {
265
345
  * misleading `aria` name to get the machinery it needs.
266
346
  */
267
347
  readonly rules?: readonly AriaRule[];
348
+ /**
349
+ * Declares which children are valid, by name, match predicate, and cardinality (e.g. "at
350
+ * least 1, at most 4 `Button` children"). Open by default — children matching no rule are
351
+ * still allowed unless `exclusiveChildren` is set.
352
+ */
268
353
  readonly children?: readonly ChildRuleInput[];
269
354
  /**
270
355
  * When true, only children matching a `children` rule (or text, per `allowText`)
@@ -277,19 +362,49 @@ type EnforcementOptions<TAllowed extends ElementType = ElementType> = {
277
362
  * or any listed rule. Default: true.
278
363
  */
279
364
  readonly allowText?: boolean;
365
+ /**
366
+ * Prop transforms composed with the component's own `normalize` (from `FactoryOptions`) and
367
+ * run before it. Unlike `normalize`, these live in the enforcement bucket because they
368
+ * typically encode a built-in HTML/ARIA fact rather than component-specific behavior.
369
+ */
280
370
  readonly props?: readonly PropNormalizer[];
281
371
  /** Restricts the `as` prop to this set of tags. Violations route through diagnostics. */
282
372
  readonly allowedAs?: readonly TAllowed[];
283
373
  };
284
374
 
285
375
  type StylingOptions<V extends Readonly<VariantMap> = Readonly<EmptyRecord>, TPreset extends RecipeMap<V> = Readonly<EmptyRecord>, TPlugin extends AnyClassPluginFactory = AnyClassPluginFactory> = {
376
+ /** Class applied to every instance regardless of variant selection. */
286
377
  readonly base?: ClassName;
378
+ /**
379
+ * Named variant groups (e.g. `intent`, `size`), each mapping its possible values to a
380
+ * class string. A consumer selects a value per group as a prop (`<Button intent="primary">`).
381
+ */
287
382
  readonly variants?: V;
383
+ /** Value used for a variant group when the consumer doesn't pass one explicitly. */
288
384
  readonly defaults?: Partial<DefaultVariants<V>>;
385
+ /**
386
+ * Applies an extra class only when a specific *combination* of variant selections matches —
387
+ * for cases `variants` alone can't express (e.g. `intent: 'primary'` + `size: 'lg'` together
388
+ * need a class neither variant would add on its own).
389
+ */
289
390
  readonly compounds?: readonly CompoundVariant<V>[];
391
+ /**
392
+ * Named bundles of variant values, selectable as a single unit via the `recipe` prop (e.g.
393
+ * `<Button recipe="cta">` instead of setting `intent`/`size` individually).
394
+ */
290
395
  readonly presets?: TPreset;
396
+ /** Maps a resolved tag directly to a raw class string, independent of the variant system. */
291
397
  readonly tags?: Readonly<TagMap>;
398
+ /**
399
+ * A `ClassPluginFactory` (e.g. the Tailwind layout pipeline) that extends class resolution
400
+ * with its own owned props, layered on top of `variants`/`presets`/`tags`.
401
+ */
292
402
  readonly plugin?: TPlugin;
403
+ /**
404
+ * A cache-key → resolved-class-string lookup for every statically-known variant
405
+ * combination, skipping runtime class computation entirely when a match is found. Normally
406
+ * generated by a build-time class-extraction plugin rather than hand-authored.
407
+ */
293
408
  readonly precomputedClasses?: Readonly<Record<string, string>>;
294
409
  };
295
410
 
@@ -298,11 +413,21 @@ type NormalizeFn<Props extends AnyRecord = AnyRecord> = {
298
413
  }['normalize'];
299
414
  type AnyFactoryOptions = FactoryOptions<ElementType, AnyRecord, VariantMap, RecipeMap<VariantMap>, AnyClassPluginFactory>;
300
415
  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> = {
416
+ /** The intrinsic tag the component renders by default. Overridable per instance via `as`. */
301
417
  readonly tag?: TDefault;
418
+ /** Display name used in diagnostics, dev tools, and generated component naming. */
302
419
  readonly name?: string;
420
+ /** Values used for the component's own (non-variant) props when the consumer omits them. */
303
421
  readonly defaults?: Partial<NoInfer<Props>>;
422
+ /**
423
+ * A pure `(props) => props` transform run on every render, after `enforcement.props`'s
424
+ * normalizers see the same input. Use this for component-specific prop shaping — anything
425
+ * that depends on live instance state or the real DOM element belongs in `onElement` instead.
426
+ */
304
427
  readonly normalize?: NormalizeFn<NoInfer<Props>>;
428
+ /** Variant groups, base classes, presets, and the optional class-resolution plugin. */
305
429
  readonly styling?: StylingOptions<V, TPreset, TPlugin>;
430
+ /** ARIA rules, child-content contracts, and other runtime validation for this component. */
306
431
  readonly enforcement?: EnforcementOptions<TAllowed>;
307
432
  /**
308
433
  * Adapter-resolved diagnostics default, spread in by `resolveAdapterCommonOptions`. Not meant to
@@ -326,13 +451,23 @@ type FactoryOptions<TDefault extends ElementType = ElementType, Props extends An
326
451
  * no prop-based equivalent), not for anything expressible as a plain
327
452
  * prop.
328
453
  *
454
+ * `element` is typed to the real DOM interface of every tag the rendered
455
+ * element could actually be — `TDefault` plus whatever `enforcement.allowed`
456
+ * permits via `as` (`HTMLDialogElement` for `tag: 'dialog'`,
457
+ * `HTMLDetailsElement` for `tag: 'details'`, and so on) — no cast needed to
458
+ * reach tag-specific members. A component that leaves `allowed`
459
+ * unconstrained (any tag reachable via `as`) falls back to `HTMLElement`,
460
+ * which still covers members every element shares (`showPopover()` and
461
+ * friends); restrict `enforcement.allowed` to the tags `onElement`
462
+ * actually knows how to handle to get real narrowing.
463
+ *
329
464
  * `getProps` returns the instance's *current* resolved props at call
330
465
  * time — read it from inside a listener registered once at mount, rather
331
466
  * than re-subscribing on every prop change.
332
467
  *
333
468
  * Return a cleanup function to run when the instance unmounts.
334
469
  */
335
- readonly onElement?: (element: Element, getProps: () => Readonly<Props>) => void | (() => void);
470
+ readonly onElement?: (element: ElementForTag<TDefault | TAllowed>, getProps: () => Readonly<Props>) => void | (() => void);
336
471
  };
337
472
 
338
473
  declare function defineContractComponent<O extends FactoryOptions>(options: O): <R>(factory: (options: O) => R) => R;
@@ -350,7 +485,7 @@ declare const Slottable: vue.DefineComponent<{}, () => vue.VNode<vue.RendererNod
350
485
 
351
486
  type UnknownProps = AnyRecord;
352
487
 
353
- type VueFactoryOptions<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> & {
488
+ type VueFactoryOptions<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> & {
354
489
  /**
355
490
  * Return true for any prop key that should be consumed but not forwarded to
356
491
  * the DOM. Variant keys are always stripped automatically.
@@ -396,8 +531,31 @@ type PolymorphicComponent<G extends PolymorphicGenerics> = {
396
531
  displayName?: string;
397
532
  };
398
533
 
399
- 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, TSubComponents extends Readonly<AnyRecord> = EmptyRecord>(options: VueFactoryOptions<TDefault, Props, Variants, TPreset, TPlugin> & {
534
+ /**
535
+ * Creates a polymorphic Vue component with praxis-kit contracts applied.
536
+ *
537
+ * ```ts
538
+ * const Button = createContractComponent({
539
+ * tag: 'button',
540
+ * name: 'Button',
541
+ * styling: {
542
+ * base: 'btn',
543
+ * variants: { intent: { primary: 'btn--primary', ghost: 'btn--ghost' } },
544
+ * defaults: { intent: 'primary' },
545
+ * },
546
+ * })
547
+ * ```
548
+ *
549
+ * ```vue
550
+ * <Button intent="ghost" as="a" href="/home">Home</Button>
551
+ * ```
552
+ *
553
+ * Pass `subComponents` to attach named sub-components (`Card.Header`) and `onElement` to run
554
+ * setup once the real DOM element exists — both purely additive on top of the generated
555
+ * component.
556
+ */
557
+ 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: VueFactoryOptions<TDefault, Props, Variants, TPreset, TPlugin> & {
400
558
  readonly subComponents?: TSubComponents;
401
- }): PolymorphicComponent<PolymorphicGenerics<TDefault, Props & ExtractPluginProps<TPlugin>, Variants, TPreset>> & TSubComponents;
559
+ }): MergeRecords<PolymorphicComponent<PolymorphicGenerics<TDefault, MergeRecords<Props, ExtractPluginProps<TPlugin>>, Variants, TPreset>>, TSubComponents>;
402
560
 
403
561
  export { type AnyFactoryOptions, type ElementType, type EmptyRecord, type PolymorphicComponent, type PolymorphicGenerics, type PolymorphicProps, type PolymorphicWithAsChild, Slottable, type SlottableProps, type VueFactoryOptions, createContractComponent, defineContractComponent };
package/dist/vue/index.js CHANGED
@@ -3811,7 +3811,10 @@ function createContractComponent(options) {
3811
3811
  }
3812
3812
  boundElement = element;
3813
3813
  if (element) {
3814
- cleanup = onElement(element, () => attrs) ?? void 0;
3814
+ cleanup = onElement(
3815
+ element,
3816
+ () => attrs
3817
+ ) ?? void 0;
3815
3818
  }
3816
3819
  } : void 0;
3817
3820
  return () => render({ ...bundle, state: state.value, slots, elementRef: onElementRef });
@@ -1,15 +1,65 @@
1
1
  import { RequireAtLeastOne, Simplify, ReadonlyDeep } from 'type-fest';
2
2
  import { Diagnostics, DiagnosticInput, DiagnosticsMode } from '../_shared/diagnostics.js';
3
3
 
4
+ /**
5
+ * A string-keyed object whose values are of type `T`.
6
+ */
4
7
  type StringMap<T = unknown> = Record<string, T>;
8
+ /**
9
+ * A string-keyed object with values of unknown type.
10
+ */
5
11
  type AnyRecord = StringMap<unknown>;
12
+ /**
13
+ * An object type with no named properties.
14
+ *
15
+ * Unlike `{}`, this excludes arbitrary properties during type operations while
16
+ * still satisfying `extends object`.
17
+ */
6
18
  type EmptyRecord = Record<never, never>;
7
- /** A compound component's named sub-components, e.g. `{ Header, Content, Footer }`. */
19
+ /**
20
+ * A compound component's named sub-components, for example
21
+ * `{ Header, Content, Footer }`.
22
+ */
8
23
  type SubComponentMap = Readonly<AnyRecord>;
24
+ /**
25
+ * Default `Variants` type for components that declare no variants.
26
+ *
27
+ * Structurally identical to `Readonly<EmptyRecord>`, but named separately so
28
+ * editor hovers remain self-descriptive.
29
+ */
30
+ type NoVariants = Readonly<EmptyRecord>;
31
+ /**
32
+ * Default `TPreset` type for components that declare no named presets.
33
+ *
34
+ * Structurally identical to `Readonly<EmptyRecord>`, but named separately so
35
+ * editor hovers remain self-descriptive.
36
+ */
37
+ type NoPreset = Readonly<EmptyRecord>;
38
+ /**
39
+ * Fallback for `ExtractPluginProps<TPlugin>` when a plugin contributes no
40
+ * props, including the no-plugin case.
41
+ *
42
+ * Structurally identical to `EmptyRecord`, but named separately so editor
43
+ * hovers remain self-descriptive.
44
+ */
45
+ type NoPluginProps = EmptyRecord;
9
46
 
10
47
  type IntrinsicTag = keyof HTMLElementTagNameMap;
11
48
 
12
49
  type ElementType = IntrinsicTag | (string & {});
50
+ /**
51
+ * Resolves a component's default tag to its real DOM interface — `HTMLDialogElement` for
52
+ * `'dialog'`, `HTMLDetailsElement` for `'details'`, and so on — falling back to `HTMLElement`
53
+ * for custom-element tags or anything not in `HTMLElementTagNameMap`. Used to type
54
+ * `FactoryOptions.onElement`'s `element` param so component authors get direct, correctly-typed
55
+ * access to tag-specific native members (`dialogEl.showModal()`) without an unsafe cast.
56
+ *
57
+ * The fallback is `HTMLElement`, not the more generic `Element` — every tag reachable through
58
+ * `IntrinsicTag` extends it, and so does every custom element per spec, so members `HTMLElement`
59
+ * itself declares (`showPopover()`/`hidePopover()`/`togglePopover()`, the `popover` attribute)
60
+ * stay directly accessible even for tags with no dedicated entry in `HTMLElementTagNameMap`.
61
+ */
62
+ type ElementForTag<TDefault extends ElementType> = TDefault extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[TDefault] : HTMLElement;
13
63
 
14
64
  declare const KNOWN_ARIA_ROLES: readonly ["alert", "alertdialog", "application", "article", "banner", "blockquote", "button", "caption", "cell", "checkbox", "code", "columnheader", "combobox", "complementary", "contentinfo", "definition", "deletion", "dialog", "document", "emphasis", "feed", "figure", "form", "generic", "grid", "gridcell", "group", "heading", "img", "insertion", "link", "list", "listbox", "listitem", "log", "main", "marquee", "math", "menu", "menubar", "menuitem", "menuitemcheckbox", "menuitemradio", "meter", "navigation", "none", "note", "option", "paragraph", "presentation", "progressbar", "radio", "radiogroup", "region", "row", "rowgroup", "rowheader", "scrollbar", "search", "searchbox", "separator", "slider", "spinbutton", "status", "strong", "subscript", "superscript", "switch", "tab", "table", "tablist", "tabpanel", "term", "textbox", "time", "timer", "toolbar", "tooltip", "tree", "treegrid", "treeitem"];
15
65
  type KnownAriaRole = (typeof KNOWN_ARIA_ROLES)[number];
@@ -171,7 +221,7 @@ type ClassPluginFactory<TProps extends AnyRecord = EmptyRecord> = <V extends Var
171
221
  * wherever a factory's concrete plugin-props shape isn't tracked (factory generics,
172
222
  * capability wiring). */
173
223
  type AnyClassPluginFactory = ClassPluginFactory<AnyRecord> | undefined;
174
- type ExtractPluginProps<TPlugin extends AnyClassPluginFactory> = TPlugin extends ClassPluginFactory<infer T> ? string extends keyof T ? EmptyRecord : T : EmptyRecord;
224
+ type ExtractPluginProps<TPlugin extends AnyClassPluginFactory> = TPlugin extends ClassPluginFactory<infer T> ? string extends keyof T ? NoPluginProps : T : NoPluginProps;
175
225
 
176
226
  type AriaContext = {
177
227
  readonly tag: IntrinsicTag;
@@ -235,6 +285,12 @@ type EnforcementOptions<TAllowed extends ElementType = ElementType> = {
235
285
  * `@praxis-kit/diagnostics`.
236
286
  */
237
287
  readonly diagnostics?: Diagnostics | DiagnosticsMode;
288
+ /**
289
+ * ARIA/accessibility rules evaluated against the resolved tag and props on every render.
290
+ * Each rule is a function receiving the current context and returning zero or more
291
+ * violations, some of which can carry an auto-applicable fix (see `createRemoveAttributeRule`
292
+ * and friends in `praxis-kit/contract`).
293
+ */
238
294
  readonly aria?: readonly AriaRule[];
239
295
  /**
240
296
  * Rules that need `AriaPolicyEngine`'s fix-application/caching machinery
@@ -246,6 +302,11 @@ type EnforcementOptions<TAllowed extends ElementType = ElementType> = {
246
302
  * misleading `aria` name to get the machinery it needs.
247
303
  */
248
304
  readonly rules?: readonly AriaRule[];
305
+ /**
306
+ * Declares which children are valid, by name, match predicate, and cardinality (e.g. "at
307
+ * least 1, at most 4 `Button` children"). Open by default — children matching no rule are
308
+ * still allowed unless `exclusiveChildren` is set.
309
+ */
249
310
  readonly children?: readonly ChildRuleInput[];
250
311
  /**
251
312
  * When true, only children matching a `children` rule (or text, per `allowText`)
@@ -258,19 +319,49 @@ type EnforcementOptions<TAllowed extends ElementType = ElementType> = {
258
319
  * or any listed rule. Default: true.
259
320
  */
260
321
  readonly allowText?: boolean;
322
+ /**
323
+ * Prop transforms composed with the component's own `normalize` (from `FactoryOptions`) and
324
+ * run before it. Unlike `normalize`, these live in the enforcement bucket because they
325
+ * typically encode a built-in HTML/ARIA fact rather than component-specific behavior.
326
+ */
261
327
  readonly props?: readonly PropNormalizer[];
262
328
  /** Restricts the `as` prop to this set of tags. Violations route through diagnostics. */
263
329
  readonly allowedAs?: readonly TAllowed[];
264
330
  };
265
331
 
266
332
  type StylingOptions<V extends Readonly<VariantMap> = Readonly<EmptyRecord>, TPreset extends RecipeMap<V> = Readonly<EmptyRecord>, TPlugin extends AnyClassPluginFactory = AnyClassPluginFactory> = {
333
+ /** Class applied to every instance regardless of variant selection. */
267
334
  readonly base?: ClassName;
335
+ /**
336
+ * Named variant groups (e.g. `intent`, `size`), each mapping its possible values to a
337
+ * class string. A consumer selects a value per group as a prop (`<Button intent="primary">`).
338
+ */
268
339
  readonly variants?: V;
340
+ /** Value used for a variant group when the consumer doesn't pass one explicitly. */
269
341
  readonly defaults?: Partial<DefaultVariants<V>>;
342
+ /**
343
+ * Applies an extra class only when a specific *combination* of variant selections matches —
344
+ * for cases `variants` alone can't express (e.g. `intent: 'primary'` + `size: 'lg'` together
345
+ * need a class neither variant would add on its own).
346
+ */
270
347
  readonly compounds?: readonly CompoundVariant<V>[];
348
+ /**
349
+ * Named bundles of variant values, selectable as a single unit via the `recipe` prop (e.g.
350
+ * `<Button recipe="cta">` instead of setting `intent`/`size` individually).
351
+ */
271
352
  readonly presets?: TPreset;
353
+ /** Maps a resolved tag directly to a raw class string, independent of the variant system. */
272
354
  readonly tags?: Readonly<TagMap>;
355
+ /**
356
+ * A `ClassPluginFactory` (e.g. the Tailwind layout pipeline) that extends class resolution
357
+ * with its own owned props, layered on top of `variants`/`presets`/`tags`.
358
+ */
273
359
  readonly plugin?: TPlugin;
360
+ /**
361
+ * A cache-key → resolved-class-string lookup for every statically-known variant
362
+ * combination, skipping runtime class computation entirely when a match is found. Normally
363
+ * generated by a build-time class-extraction plugin rather than hand-authored.
364
+ */
274
365
  readonly precomputedClasses?: Readonly<Record<string, string>>;
275
366
  };
276
367
 
@@ -279,11 +370,21 @@ type NormalizeFn<Props extends AnyRecord = AnyRecord> = {
279
370
  }['normalize'];
280
371
  type AnyFactoryOptions = FactoryOptions<ElementType, AnyRecord, VariantMap, RecipeMap<VariantMap>, AnyClassPluginFactory>;
281
372
  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> = {
373
+ /** The intrinsic tag the component renders by default. Overridable per instance via `as`. */
282
374
  readonly tag?: TDefault;
375
+ /** Display name used in diagnostics, dev tools, and generated component naming. */
283
376
  readonly name?: string;
377
+ /** Values used for the component's own (non-variant) props when the consumer omits them. */
284
378
  readonly defaults?: Partial<NoInfer<Props>>;
379
+ /**
380
+ * A pure `(props) => props` transform run on every render, after `enforcement.props`'s
381
+ * normalizers see the same input. Use this for component-specific prop shaping — anything
382
+ * that depends on live instance state or the real DOM element belongs in `onElement` instead.
383
+ */
285
384
  readonly normalize?: NormalizeFn<NoInfer<Props>>;
385
+ /** Variant groups, base classes, presets, and the optional class-resolution plugin. */
286
386
  readonly styling?: StylingOptions<V, TPreset, TPlugin>;
387
+ /** ARIA rules, child-content contracts, and other runtime validation for this component. */
287
388
  readonly enforcement?: EnforcementOptions<TAllowed>;
288
389
  /**
289
390
  * Adapter-resolved diagnostics default, spread in by `resolveAdapterCommonOptions`. Not meant to
@@ -307,15 +408,37 @@ type FactoryOptions<TDefault extends ElementType = ElementType, Props extends An
307
408
  * no prop-based equivalent), not for anything expressible as a plain
308
409
  * prop.
309
410
  *
411
+ * `element` is typed to the real DOM interface of every tag the rendered
412
+ * element could actually be — `TDefault` plus whatever `enforcement.allowed`
413
+ * permits via `as` (`HTMLDialogElement` for `tag: 'dialog'`,
414
+ * `HTMLDetailsElement` for `tag: 'details'`, and so on) — no cast needed to
415
+ * reach tag-specific members. A component that leaves `allowed`
416
+ * unconstrained (any tag reachable via `as`) falls back to `HTMLElement`,
417
+ * which still covers members every element shares (`showPopover()` and
418
+ * friends); restrict `enforcement.allowed` to the tags `onElement`
419
+ * actually knows how to handle to get real narrowing.
420
+ *
310
421
  * `getProps` returns the instance's *current* resolved props at call
311
422
  * time — read it from inside a listener registered once at mount, rather
312
423
  * than re-subscribing on every prop change.
313
424
  *
314
425
  * Return a cleanup function to run when the instance unmounts.
315
426
  */
316
- readonly onElement?: (element: Element, getProps: () => Readonly<Props>) => void | (() => void);
427
+ readonly onElement?: (element: ElementForTag<TDefault | TAllowed>, getProps: () => Readonly<Props>) => void | (() => void);
317
428
  };
318
429
 
430
+ /**
431
+ * Determines whether a prop should be stripped before forwarding to the
432
+ * rendered element.
433
+ *
434
+ * Returning `true` excludes the prop from the output; returning `false`
435
+ * keeps it. This is the inverse polarity of `shouldForwardProp`-style
436
+ * predicates (Emotion/styled-components), where `true` means include.
437
+ *
438
+ * @param key - The prop name being evaluated.
439
+ * @param variantKeys - The set of configured variant prop names.
440
+ * @returns `true` to strip the prop; `false` to forward it.
441
+ */
319
442
  type FilterPredicate = (key: string, variantKeys: ReadonlySet<string>) => boolean;
320
443
 
321
444
  declare function defineContractComponent<O extends FactoryOptions>(options: O): <R>(factory: (options: O) => R) => R;
@@ -326,7 +449,7 @@ declare function defineContractComponent<O extends FactoryOptions>(options: O):
326
449
  * Identical shape to LitFactoryOptions — a plain HTMLElement subclass with
327
450
  * no framework dependency. Light DOM only; Shadow DOM is out of scope.
328
451
  */
329
- type WebFactoryOptions<TDefault extends ElementType = ElementType, TProps extends AnyRecord = EmptyRecord, TVariants extends Readonly<VariantMap> = Readonly<EmptyRecord>, TPreset extends RecipeMap<TVariants> = Readonly<EmptyRecord>, TPlugin extends AnyClassPluginFactory = AnyClassPluginFactory> = FactoryOptions<TDefault, TProps, TVariants, TPreset, TPlugin> & {
452
+ 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> & {
330
453
  readonly filterProps?: FilterPredicate;
331
454
  };
332
455
 
@@ -337,7 +460,7 @@ type UnknownProps = AnyRecord;
337
460
  * Describes the public contract without exposing HTMLElement's internal members.
338
461
  * Variant key instance properties are typed via TVariants.
339
462
  */
340
- type WebContractComponent<TVariants extends Readonly<VariantMap> = Readonly<EmptyRecord>, TPluginProps extends AnyRecord = EmptyRecord> = {
463
+ type WebContractComponent<TVariants extends Readonly<VariantMap> = NoVariants, TPluginProps extends AnyRecord = EmptyRecord> = {
341
464
  new (): HTMLElement & {
342
465
  as: string | undefined;
343
466
  recipe: string | undefined;
@@ -378,7 +501,7 @@ type WebContractComponent<TVariants extends Readonly<VariantMap> = Readonly<Empt
378
501
  * For non-reactive attributes (`aria-*`, `role`, `data-*`) call `element.update()`
379
502
  * after setting them to trigger an explicit pipeline re-run.
380
503
  */
381
- declare function createContractComponent<TDefault extends ElementType, TProps extends UnknownProps = EmptyRecord, TVariants extends Readonly<VariantMap> = Readonly<EmptyRecord>, TPreset extends RecipeMap<TVariants> = Readonly<EmptyRecord>, TPlugin extends AnyClassPluginFactory = AnyClassPluginFactory, TSubComponents extends Readonly<AnyRecord> = EmptyRecord>(options: WebFactoryOptions<TDefault, TProps, TVariants, TPreset, TPlugin> & {
504
+ 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> & {
382
505
  readonly subComponents?: TSubComponents;
383
506
  }): WebContractComponent<TVariants, ExtractPluginProps<TPlugin>> & TSubComponents;
384
507
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-kit",
3
- "version": "7.3.0",
3
+ "version": "7.5.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./react": {
@@ -203,11 +203,11 @@
203
203
  "typescript": "^6.0.3",
204
204
  "vite": "^8.1.5",
205
205
  "vue": "^3.5.40",
206
- "@praxis-kit/adapter-utils": "0.0.0",
207
206
  "@praxis-kit/core": "0.0.0",
207
+ "@praxis-kit/adapter-utils": "0.0.0",
208
+ "@praxis-kit/primitive": "0.0.0",
208
209
  "@praxis-kit/diagnostics": "0.0.0",
209
210
  "@praxis-kit/pipeline": "0.0.0",
210
- "@praxis-kit/primitive": "0.0.0",
211
211
  "@praxis-kit/vite-plugin": "0.0.0"
212
212
  },
213
213
  "publishConfig": {
File without changes