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.
@@ -11,11 +11,6 @@ function invariant(condition, message) {
11
11
  if (!condition) panic(message);
12
12
  }
13
13
  //#endregion
14
- //#region ../../lib/adapter-utils/src/runtime/define-component.ts
15
- function defineContractComponent(options) {
16
- return (factory) => factory(options);
17
- }
18
- //#endregion
19
14
  //#region ../../lib/adapter-utils/src/runtime/assemble-compound-component.ts
20
15
  /**
21
16
  * Assembles a compound component by attaching sub-components to a
@@ -1010,6 +1005,59 @@ function finalizeComponent(component, defaultTag, subComponents) {
1010
1005
  return assembleCompoundComponent(component, subComponents);
1011
1006
  }
1012
1007
  //#endregion
1008
+ //#region ../../lib/adapter-utils/src/runtime/is-factory-options-like.ts
1009
+ /**
1010
+ * Loosely validates one recognized `FactoryOptions` field's runtime shape.
1011
+ * "Loosely" because the field's declared type is itself parameterized by a
1012
+ * generic (Props, Variants, TPlugin, ...) that's erased at runtime — these
1013
+ * checks confirm the field is the right *kind* of value (object, function,
1014
+ * string), not that it satisfies its exact generic instantiation, which no
1015
+ * runtime check can ever do.
1016
+ *
1017
+ * Covers every field `FactoryOptions` itself declares — identical across
1018
+ * every adapter, since they all extend the same core type. Adapter-specific
1019
+ * additions (React's `slotComponent`/`artifact`, every adapter's own
1020
+ * `filterProps`, etc.) are passed in by the caller as extra entries.
1021
+ */
1022
+ const FACTORY_OPTIONS_FIELD_VALIDATORS = {
1023
+ tag: (v) => v === void 0 || isString(v),
1024
+ name: (v) => v === void 0 || isString(v),
1025
+ defaults: (v) => v === void 0 || isObject(v),
1026
+ normalize: (v) => v === void 0 || isFunction(v),
1027
+ styling: (v) => v === void 0 || isObject(v),
1028
+ enforcement: (v) => v === void 0 || isObject(v),
1029
+ diagnostics: (v) => v === void 0 || isObject(v),
1030
+ subComponents: (v) => v === void 0 || isObject(v),
1031
+ onElement: (v) => v === void 0 || isFunction(v)
1032
+ };
1033
+ /**
1034
+ * Type guard narrowing the generic `FactoryOptions` shape down to an
1035
+ * adapter-specific factory options type `T` — the type each adapter's
1036
+ * `buildRuntime` is declared against.
1037
+ *
1038
+ * Walks every own enumerable property on `options` and checks it against
1039
+ * `FACTORY_OPTIONS_FIELD_VALIDATORS` plus `extraFieldValidators` (the
1040
+ * adapter's own additions on top of `FactoryOptions`): an unrecognized key,
1041
+ * or a recognized key holding a value of the wrong kind, fails the guard.
1042
+ * This is a genuine structural check, not a relabeled assertion — though it
1043
+ * necessarily stops at each field's runtime *kind*, since the field's actual
1044
+ * generic instantiation is erased at runtime and no guard can validate it.
1045
+ * That part remains the caller's responsibility, same as with any other
1046
+ * generic function in TypeScript.
1047
+ */
1048
+ function isFactoryOptionsLike(options, extraFieldValidators) {
1049
+ if (!isObject(options)) return false;
1050
+ const validators = {
1051
+ ...FACTORY_OPTIONS_FIELD_VALIDATORS,
1052
+ ...extraFieldValidators
1053
+ };
1054
+ for (const [key, value] of Object.entries(options)) {
1055
+ const validate = validators[key];
1056
+ if (!validate || !validate(value)) return false;
1057
+ }
1058
+ return true;
1059
+ }
1060
+ //#endregion
1013
1061
  //#region ../../lib/contract/src/aria/aria-role-policy.ts
1014
1062
  function getImplicitRole(tag, props) {
1015
1063
  if (tag in IMPLICIT_ROLE_RECORD) return IMPLICIT_ROLE_RECORD[tag];
@@ -1555,72 +1603,100 @@ var InvariantBase = class {
1555
1603
  }
1556
1604
  };
1557
1605
  //#endregion
1558
- //#region ../../lib/contract/src/aria/spec/roles/required-properties.ts
1559
- const REQUIRED_ARIA_PROPERTIES = {
1560
- combobox: ["aria-expanded"],
1561
- option: ["aria-selected"],
1562
- slider: ["aria-valuenow"],
1563
- scrollbar: ["aria-controls", "aria-valuenow"],
1564
- spinbutton: ["aria-valuenow"]
1565
- };
1566
- //#endregion
1567
- //#region ../../lib/contract/src/aria/spec/roles/name-required.ts
1568
- const NAME_REQUIRED_ROLES = /* @__PURE__ */ new Set(["img"]);
1569
- //#endregion
1570
- //#region ../../lib/contract/src/aria/spec/roles/name-prohibited.ts
1571
- const NAME_PROHIBITED_ROLES = /* @__PURE__ */ new Set([
1572
- "caption",
1573
- "code",
1574
- "deletion",
1575
- "emphasis",
1576
- "generic",
1577
- "insertion",
1578
- "none",
1579
- "paragraph",
1580
- "presentation",
1581
- "strong",
1582
- "subscript",
1583
- "superscript"
1584
- ]);
1585
- const NAME_PROHIBITED_ATTRIBUTES = ["aria-label", "aria-labelledby"];
1586
- //#endregion
1587
- //#region ../../lib/contract/src/aria/spec/validators/required-properties-validator.ts
1588
- const NO_VIOLATIONS$1 = [{ valid: true }];
1589
- function requiredAttributeByRole(roles, attribute) {
1590
- return Object.fromEntries([...roles].map((role) => [role, [attribute]]));
1591
- }
1606
+ //#region ../../lib/contract/src/aria/plan-cache.ts
1592
1607
  /**
1593
- * Reports missing attributes required by an element's effective ARIA role.
1608
+ * Validation-plan cache machinery for `AriaPolicyEngine`: computing a cache key from a
1609
+ * (tag, props) usage site, and diffing/replaying the props transformation a validation pass
1610
+ * produced (an {@link AriaPlan}'s `removals`/`updates`) without re-running the ARIA rule
1611
+ * pipeline itself. Extracted because these four functions are a cohesive subsystem in their own
1612
+ * right — a small revalidation-caching scheme — not specifically ARIA *policy*; the actual
1613
+ * cache storage (an `LRUCache` instance) stays owned by `AriaPolicyEngine` itself, since it's
1614
+ * per-engine-instance state, not something this module needs to hold.
1615
+ */
1616
+ /**
1617
+ * Cache key covering only the aria-relevant subset of props (tag + role + aria-* attrs) —
1618
+ * exactly what the built-in pipeline reads. Non-aria props (className, onClick, etc.) do
1619
+ * not affect built-in ARIA decisions and are excluded so cache hits survive re-renders
1620
+ * that only change non-aria props. This key alone is unsound for a policy engine's own
1621
+ * extra rules, which may read arbitrary props outside this set — extend it with
1622
+ * {@link extraRulesKeySuffix}, or bypass the cache, to account for that.
1594
1623
  *
1595
- * Shared by validators that differ only in their role-to-attribute mapping and diagnostic
1596
- * generation.
1624
+ * Returns `null` (meaning "don't cache") when `tag` isn't a plain intrinsic string tag —
1625
+ * `isString(tag)` inline rather than importing `AriaPolicyEngine`'s own `isIntrinsicTag`: the
1626
+ * check is a one-liner, and importing it back from the engine module would be a needless
1627
+ * dependency in the other direction for a plan-cache module the engine itself depends on.
1597
1628
  */
1598
- function checkRequiredAttributes(requirement, { props, effectiveRole }) {
1599
- if (!effectiveRole) return NO_VIOLATIONS$1;
1600
- const requiredAttributes = requirement.attributesByRole[effectiveRole];
1601
- if (!requiredAttributes) return NO_VIOLATIONS$1;
1602
- const results = [];
1603
- for (const attribute of requiredAttributes) {
1604
- if (attribute in props) continue;
1605
- results.push({
1606
- valid: false,
1607
- fixable: false,
1608
- severity: "warning",
1609
- attribute,
1610
- diagnostic: requirement.diagnosticFor(attribute, effectiveRole)
1611
- });
1629
+ function createPlanKey(tag, props) {
1630
+ if (!isString(tag)) return null;
1631
+ const parts = [tag];
1632
+ if (isString(props.role)) parts.push(`role:${props.role}`);
1633
+ if (tag === "input" && isString(props.type)) parts.push(`type:${props.type}`);
1634
+ if (tag === "img") parts.push(`alt:${props.alt === "" ? "empty" : "present"}`);
1635
+ const ariaEntries = [];
1636
+ iterate.forEachEntry(props, (k, v) => {
1637
+ if (!k.startsWith("aria-")) return;
1638
+ if (!isString(v) && !isNumber(v) && typeof v !== "boolean") return;
1639
+ ariaEntries.push(`${k}:${String(v)}`);
1640
+ });
1641
+ if (ariaEntries.length > 0) parts.push(...ariaEntries.sort());
1642
+ return parts.join("|");
1643
+ }
1644
+ /**
1645
+ * Extends the base cache key with the props each extra rule declares it reads (`readsProps`).
1646
+ * Returns `null` — meaning "don't cache" — if any extra rule omits `readsProps` (it may read
1647
+ * arbitrary props the key can't account for) or if a declared prop's value isn't a primitive
1648
+ * (object/array identity isn't stably representable in a string key).
1649
+ */
1650
+ function extraRulesKeySuffix(extraRules, props) {
1651
+ const parts = [];
1652
+ for (const rule of extraRules) {
1653
+ const readsProps = rule.readsProps;
1654
+ if (!isNonNull(readsProps)) return null;
1655
+ for (const propKey of readsProps) {
1656
+ const v = props[propKey];
1657
+ if (isDefined(v) && !isString(v) && !isNumber(v) && typeof v !== "boolean") return null;
1658
+ parts.push(`x:${propKey}:${String(v)}`);
1659
+ }
1612
1660
  }
1613
- return results;
1661
+ return parts.sort().join("|");
1662
+ }
1663
+ /** Diffs a validation pass's input/output props into a replayable plan: which keys were removed
1664
+ * entirely, and which keys were added or changed value (and to what). */
1665
+ function computePlan(inputProps, resultProps) {
1666
+ const removals = /* @__PURE__ */ new Set();
1667
+ const updates = {};
1668
+ iterate.forEachKey(inputProps, (key) => {
1669
+ if (!(key in resultProps)) removals.add(key);
1670
+ });
1671
+ iterate.forEachEntry(resultProps, (key, resultVal) => {
1672
+ if (inputProps[key] !== resultVal) updates[key] = resultVal;
1673
+ });
1674
+ return {
1675
+ removals,
1676
+ updates
1677
+ };
1678
+ }
1679
+ /** Replays a cached plan's removals/updates against a fresh `props` object, without re-running
1680
+ * the rule pipeline that originally produced the plan. */
1681
+ function applyPlan(props, removals, updates) {
1682
+ const hasRemovals = removals.size > 0;
1683
+ const hasUpdates = Object.keys(updates).length > 0;
1684
+ if (!hasRemovals && !hasUpdates) return props;
1685
+ const next = {};
1686
+ iterate.forEachEntry(props, (k, v) => {
1687
+ if (!removals.has(k)) next[k] = v;
1688
+ });
1689
+ Object.assign(next, updates);
1690
+ return next;
1614
1691
  }
1615
1692
  //#endregion
1616
- //#region ../../lib/contract/src/aria/spec/roles/live-region.ts
1617
- const LIVE_REGION_ROLES = /* @__PURE__ */ new Map([
1618
- ["alert", "assertive"],
1619
- ["status", "polite"],
1620
- ["log", "polite"],
1621
- ["timer", "off"]
1693
+ //#region ../../lib/contract/src/aria/spec/attributes/aria-relevant-tokens.ts
1694
+ const VALID_RELEVANT_TOKENS = /* @__PURE__ */ new Set([
1695
+ "additions",
1696
+ "removals",
1697
+ "text",
1698
+ "all"
1622
1699
  ]);
1623
- const ATOMIC_REQUIREMENTS = requiredAttributeByRole(LIVE_REGION_ROLES.keys(), "aria-atomic");
1624
1700
  //#endregion
1625
1701
  //#region ../../lib/contract/src/aria/spec/attributes/aria-value-types.ts
1626
1702
  const ARIA_VALUE_TYPES = /* @__PURE__ */ new Map([
@@ -1745,24 +1821,6 @@ const ARIA_VALUE_TYPES = /* @__PURE__ */ new Map([
1745
1821
  }]
1746
1822
  ]);
1747
1823
  //#endregion
1748
- //#region ../../lib/contract/src/aria/spec/attributes/aria-relevant-tokens.ts
1749
- const VALID_RELEVANT_TOKENS = /* @__PURE__ */ new Set([
1750
- "additions",
1751
- "removals",
1752
- "text",
1753
- "all"
1754
- ]);
1755
- //#endregion
1756
- //#region ../../lib/contract/src/aria/spec/elements/heading-implicit-levels.ts
1757
- const HEADING_IMPLICIT_LEVELS = /* @__PURE__ */ new Map([
1758
- ["h1", 1],
1759
- ["h2", 2],
1760
- ["h3", 3],
1761
- ["h4", 4],
1762
- ["h5", 5],
1763
- ["h6", 6]
1764
- ]);
1765
- //#endregion
1766
1824
  //#region ../../lib/contract/src/aria/spec/elements/focusable.ts
1767
1825
  function parseTabIndex(raw) {
1768
1826
  if (typeof raw === "number") return Number.isInteger(raw) ? raw : void 0;
@@ -1813,24 +1871,156 @@ function isPotentiallyFocusable(tag, props) {
1813
1871
  }
1814
1872
  }
1815
1873
  //#endregion
1816
- //#region ../../lib/contract/src/aria/aria-policy-engine.ts
1874
+ //#region ../../lib/contract/src/aria/spec/elements/heading-implicit-levels.ts
1875
+ const HEADING_IMPLICIT_LEVELS = /* @__PURE__ */ new Map([
1876
+ ["h1", 1],
1877
+ ["h2", 2],
1878
+ ["h3", 3],
1879
+ ["h4", 4],
1880
+ ["h5", 5],
1881
+ ["h6", 6]
1882
+ ]);
1883
+ //#endregion
1884
+ //#region ../../lib/contract/src/aria/spec/validators/required-properties-validator.ts
1817
1885
  const NO_VIOLATIONS = [{ valid: true }];
1818
- const NO_VARIANT_KEYS = /* @__PURE__ */ new Set();
1819
- function isIntrinsicTag(tag) {
1820
- return isString(tag);
1886
+ function requiredAttributeByRole(roles, attribute) {
1887
+ return Object.fromEntries([...roles].map((role) => [role, [attribute]]));
1821
1888
  }
1822
- function omitProp(obj, key) {
1823
- const { [key]: _, ...rest } = obj;
1824
- return rest;
1889
+ /**
1890
+ * Reports missing attributes required by an element's effective ARIA role.
1891
+ *
1892
+ * Shared by validators that differ only in their role-to-attribute mapping and diagnostic
1893
+ * generation.
1894
+ */
1895
+ function checkRequiredAttributes(requirement, { props, effectiveRole }) {
1896
+ if (!effectiveRole) return NO_VIOLATIONS;
1897
+ const requiredAttributes = requirement.attributesByRole[effectiveRole];
1898
+ if (!requiredAttributes) return NO_VIOLATIONS;
1899
+ const results = [];
1900
+ for (const attribute of requiredAttributes) {
1901
+ if (attribute in props) continue;
1902
+ results.push({
1903
+ valid: false,
1904
+ fixable: false,
1905
+ severity: "warning",
1906
+ attribute,
1907
+ diagnostic: requirement.diagnosticFor(attribute, effectiveRole)
1908
+ });
1909
+ }
1910
+ return results;
1825
1911
  }
1912
+ //#endregion
1913
+ //#region ../../lib/contract/src/aria/spec/roles/live-region.ts
1914
+ const LIVE_REGION_ROLES = /* @__PURE__ */ new Map([
1915
+ ["alert", "assertive"],
1916
+ ["status", "polite"],
1917
+ ["log", "polite"],
1918
+ ["timer", "off"]
1919
+ ]);
1920
+ const ATOMIC_REQUIREMENTS = requiredAttributeByRole(LIVE_REGION_ROLES.keys(), "aria-atomic");
1921
+ //#endregion
1922
+ //#region ../../lib/contract/src/aria/spec/roles/name-prohibited.ts
1923
+ const NAME_PROHIBITED_ROLES = /* @__PURE__ */ new Set([
1924
+ "caption",
1925
+ "code",
1926
+ "deletion",
1927
+ "emphasis",
1928
+ "generic",
1929
+ "insertion",
1930
+ "none",
1931
+ "paragraph",
1932
+ "presentation",
1933
+ "strong",
1934
+ "subscript",
1935
+ "superscript"
1936
+ ]);
1937
+ const NAME_PROHIBITED_ATTRIBUTES = ["aria-label", "aria-labelledby"];
1938
+ //#endregion
1939
+ //#region ../../lib/contract/src/aria/spec/roles/name-required.ts
1940
+ const NAME_REQUIRED_ROLES = /* @__PURE__ */ new Set(["img"]);
1941
+ //#endregion
1942
+ //#region ../../lib/contract/src/aria/spec/roles/required-properties.ts
1943
+ const REQUIRED_ARIA_PROPERTIES = {
1944
+ combobox: ["aria-expanded"],
1945
+ option: ["aria-selected"],
1946
+ slider: ["aria-valuenow"],
1947
+ scrollbar: ["aria-controls", "aria-valuenow"],
1948
+ spinbutton: ["aria-valuenow"]
1949
+ };
1950
+ //#endregion
1951
+ //#region ../../lib/contract/src/aria/value-validation.ts
1952
+ /**
1953
+ * ARIA attribute value type system: what counts as a valid value for a given
1954
+ * {@link AriaValueType}, and how to describe what was expected when it isn't. Extracted from
1955
+ * `AriaPolicyEngine` — these three functions are cohesive on their own (they're describing a
1956
+ * type system, not policy), and `#checkAriaAttributeValues` (kept in `aria-policy-engine.ts`
1957
+ * alongside every other rule implementation — see that file's own pipeline) is now just a thin
1958
+ * caller of {@link isValidAriaValue}/{@link describeExpected}.
1959
+ */
1960
+ /**
1961
+ * Strict numeric coercion for ARIA attribute values. Unlike `parseFloat`/`parseInt`, the whole
1962
+ * string must be a number — `"12abc"` is rejected, not read as `12` — and an empty / whitespace
1963
+ * string is rejected rather than coerced to `0` the way `Number("")` would.
1964
+ *
1965
+ * Exported (not local to `isValidAriaValue`) because `AriaPolicyEngine.#checkRedundantAriaLevel`
1966
+ * also needs it, independent of the general ARIA value-type check this module otherwise exists
1967
+ * for — `aria-level`'s "redundant" check is about comparing a numeric attribute value to a
1968
+ * known implicit level, not about validating the value's type.
1969
+ */
1826
1970
  function strictNumeric(value) {
1827
- if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
1828
- if (typeof value !== "string") return void 0;
1971
+ if (isNumber(value)) return Number.isFinite(value) ? value : void 0;
1972
+ if (!isString(value)) return void 0;
1829
1973
  const trimmed = value.trim();
1830
1974
  if (trimmed === "") return void 0;
1831
1975
  const n = Number(trimmed);
1832
1976
  return Number.isFinite(n) ? n : void 0;
1833
1977
  }
1978
+ /** Returns whether `value` satisfies `type`'s own value-type constraint (boolean/tristate/
1979
+ * number/integer/enum) — the WAI-ARIA attribute value type system, independent of which
1980
+ * attribute or role it's being checked for. */
1981
+ function isValidAriaValue(value, type) {
1982
+ switch (type.kind) {
1983
+ case "boolean": return value === "true" || value === "false" || value === true || value === false;
1984
+ case "tristate": return value === "true" || value === "false" || value === "mixed" || value === true || value === false;
1985
+ case "number": return strictNumeric(value) !== void 0;
1986
+ case "integer": {
1987
+ const n = strictNumeric(value);
1988
+ if (isUndefined(n) || !Number.isInteger(n)) return false;
1989
+ if (isDefined(type.min) && n < type.min) return false;
1990
+ if (isDefined(type.max) && n > type.max) return false;
1991
+ return true;
1992
+ }
1993
+ case "enum": return isString(value) && type.values.has(value);
1994
+ }
1995
+ }
1996
+ /** Describes what a valid value for `type` looks like, for use in an invalid-value diagnostic
1997
+ * message (e.g. `aria-valuenow` got `"abc"`, expected "a finite number"). */
1998
+ function describeExpected(type) {
1999
+ switch (type.kind) {
2000
+ case "boolean": return "\"true\" or \"false\"";
2001
+ case "tristate": return "\"true\", \"false\", or \"mixed\"";
2002
+ case "number": return "a finite number";
2003
+ case "integer": {
2004
+ const parts = ["an integer"];
2005
+ if (isDefined(type.min) && isDefined(type.max)) parts.push(`between ${type.min} and ${type.max}`);
2006
+ else if (isDefined(type.min)) parts.push(`≥ ${type.min}`);
2007
+ else if (isDefined(type.max)) parts.push(`≤ ${type.max}`);
2008
+ return parts.join(" ");
2009
+ }
2010
+ case "enum": return [...type.values].map((v) => `"${v}"`).join(", ");
2011
+ }
2012
+ }
2013
+ //#endregion
2014
+ //#region ../../lib/contract/src/aria/aria-policy-engine.ts
2015
+ const VALID_RESULT = [{ valid: true }];
2016
+ const NO_VARIANT_KEYS = /* @__PURE__ */ new Set();
2017
+ function isIntrinsicTag(tag) {
2018
+ return isString(tag);
2019
+ }
2020
+ function omitProp(obj, key) {
2021
+ const { [key]: _, ...rest } = obj;
2022
+ return rest;
2023
+ }
1834
2024
  var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
1835
2025
  #extraRules;
1836
2026
  #variantKeys;
@@ -1963,73 +2153,20 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
1963
2153
  else this.warn(d);
1964
2154
  });
1965
2155
  }
1966
- static #createPlanKey(tag, props) {
1967
- if (!isIntrinsicTag(tag)) return null;
1968
- const parts = [tag];
1969
- if (typeof props.role === "string") parts.push(`role:${props.role}`);
1970
- if (tag === "input" && typeof props.type === "string") parts.push(`type:${props.type}`);
1971
- if (tag === "img") parts.push(`alt:${props.alt === "" ? "empty" : "present"}`);
1972
- const ariaEntries = [];
1973
- iterate.forEachEntry(props, (k, v) => {
1974
- if (!k.startsWith("aria-")) return;
1975
- if (!isString(v) && !isNumber(v) && typeof v !== "boolean") return;
1976
- ariaEntries.push(`${k}:${String(v)}`);
1977
- });
1978
- if (ariaEntries.length > 0) parts.push(...ariaEntries.sort());
1979
- return parts.join("|");
1980
- }
1981
- static #extraRulesKeySuffix(extraRules, props) {
1982
- const parts = [];
1983
- for (const rule of extraRules) {
1984
- const readsProps = rule.readsProps;
1985
- if (!isNonNull(readsProps)) return null;
1986
- for (const propKey of readsProps) {
1987
- const v = props[propKey];
1988
- if (v !== void 0 && !isString(v) && !isNumber(v) && typeof v !== "boolean") return null;
1989
- parts.push(`x:${propKey}:${String(v)}`);
1990
- }
1991
- }
1992
- return parts.sort().join("|");
1993
- }
1994
- static #computePlan(inputProps, resultProps) {
1995
- const removals = /* @__PURE__ */ new Set();
1996
- const updates = {};
1997
- iterate.forEachKey(inputProps, (key) => {
1998
- if (!(key in resultProps)) removals.add(key);
1999
- });
2000
- iterate.forEachEntry(resultProps, (key, resultVal) => {
2001
- if (inputProps[key] !== resultVal) updates[key] = resultVal;
2002
- });
2003
- return {
2004
- removals,
2005
- updates
2006
- };
2007
- }
2008
- static #applyPlan(props, removals, updates) {
2009
- const hasRemovals = removals.size > 0;
2010
- const hasUpdates = Object.keys(updates).length > 0;
2011
- if (!hasRemovals && !hasUpdates) return props;
2012
- const next = {};
2013
- iterate.forEachEntry(props, (k, v) => {
2014
- if (!removals.has(k)) next[k] = v;
2015
- });
2016
- Object.assign(next, updates);
2017
- return next;
2018
- }
2019
2156
  validate(tag, props, extraProps) {
2020
2157
  const ruleProps = extraProps ?? props;
2021
- const baseKey = AriaPolicyEngine.#createPlanKey(tag, props);
2158
+ const baseKey = createPlanKey(tag, props);
2022
2159
  let key = baseKey;
2023
2160
  if (this.#extraRules.length > 0) {
2024
- const suffix = AriaPolicyEngine.#extraRulesKeySuffix(this.#extraRules, ruleProps);
2161
+ const suffix = extraRulesKeySuffix(this.#extraRules, ruleProps);
2025
2162
  key = isNonNull(baseKey) && isNonNull(suffix) ? `${baseKey}|${suffix}` : null;
2026
2163
  }
2027
2164
  if (!isNull(key)) {
2028
2165
  const cached = this.#planCache.get(key);
2029
- if (cached !== void 0) {
2166
+ if (isDefined(cached)) {
2030
2167
  if (cached.violations.length > 0) this.report(cached.violations);
2031
2168
  return {
2032
- props: AriaPolicyEngine.#applyPlan(props, cached.removals, cached.updates),
2169
+ props: applyPlan(props, cached.removals, cached.updates),
2033
2170
  violations: cached.violations
2034
2171
  };
2035
2172
  }
@@ -2037,7 +2174,7 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2037
2174
  const result = this.#extraRules.length ? AriaPolicyEngine.#evaluateWithRules(tag, props, this.#extraRules, extraProps, this.#variantKeys) : AriaPolicyEngine.evaluate(tag, props);
2038
2175
  if (result.violations.length > 0) this.report(result.violations);
2039
2176
  if (!isNull(key)) {
2040
- const { removals, updates } = AriaPolicyEngine.#computePlan(props, result.props);
2177
+ const { removals, updates } = computePlan(props, result.props);
2041
2178
  const plan = {
2042
2179
  removals,
2043
2180
  updates,
@@ -2128,7 +2265,7 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2128
2265
  ];
2129
2266
  static #checkInvalidRoleOverride({ tag, props, implicitRole }) {
2130
2267
  const role = props.role;
2131
- if (!implicitRole || !role || role === implicitRole) return NO_VIOLATIONS;
2268
+ if (!implicitRole || isUndefined(role) || role === implicitRole) return VALID_RESULT;
2132
2269
  if (isStrongImplicitRole(tag) && role === "region") {
2133
2270
  const diagnostic = HtmlDiagnostics.implicitRoleOverride(tag, implicitRole, role);
2134
2271
  return [{
@@ -2139,11 +2276,11 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2139
2276
  diagnostic
2140
2277
  }];
2141
2278
  }
2142
- return NO_VIOLATIONS;
2279
+ return VALID_RESULT;
2143
2280
  }
2144
2281
  static #checkRedundantRole({ tag, props, implicitRole }) {
2145
2282
  const role = props.role;
2146
- if (!implicitRole || !role || role !== implicitRole) return NO_VIOLATIONS;
2283
+ if (!implicitRole || isUndefined(role) || role !== implicitRole) return VALID_RESULT;
2147
2284
  const diagnostic = HtmlDiagnostics.implicitRoleRedundant(tag, implicitRole);
2148
2285
  return [{
2149
2286
  valid: false,
@@ -2154,8 +2291,8 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2154
2291
  }];
2155
2292
  }
2156
2293
  static #checkStandaloneRegion({ tag, props, implicitRole }) {
2157
- if (props.role !== "region") return NO_VIOLATIONS;
2158
- if (!hasStandaloneRole(tag)) return NO_VIOLATIONS;
2294
+ if (props.role !== "region") return VALID_RESULT;
2295
+ if (!hasStandaloneRole(tag)) return VALID_RESULT;
2159
2296
  const diagnostic = HtmlDiagnostics.standaloneRegionOverride(tag, implicitRole ?? tag);
2160
2297
  return [{
2161
2298
  valid: false,
@@ -2166,7 +2303,7 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2166
2303
  }];
2167
2304
  }
2168
2305
  static #checkInvalidAriaAttributes({ tag, props, effectiveRole }) {
2169
- if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS;
2306
+ if (effectiveRole === "none" || effectiveRole === "presentation") return VALID_RESULT;
2170
2307
  const results = [];
2171
2308
  iterate.forEachEntry(props, (key) => {
2172
2309
  if (!key.startsWith("aria-")) return;
@@ -2183,63 +2320,33 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2183
2320
  });
2184
2321
  return results;
2185
2322
  }
2186
- static #isValidAriaValue(value, type) {
2187
- switch (type.kind) {
2188
- case "boolean": return value === "true" || value === "false" || value === true || value === false;
2189
- case "tristate": return value === "true" || value === "false" || value === "mixed" || value === true || value === false;
2190
- case "number": return strictNumeric(value) !== void 0;
2191
- case "integer": {
2192
- const n = strictNumeric(value);
2193
- if (n === void 0 || !Number.isInteger(n)) return false;
2194
- if (type.min !== void 0 && n < type.min) return false;
2195
- if (type.max !== void 0 && n > type.max) return false;
2196
- return true;
2197
- }
2198
- case "enum": return typeof value === "string" && type.values.has(value);
2199
- }
2200
- }
2201
- static #describeExpected(type) {
2202
- switch (type.kind) {
2203
- case "boolean": return "\"true\" or \"false\"";
2204
- case "tristate": return "\"true\", \"false\", or \"mixed\"";
2205
- case "number": return "a finite number";
2206
- case "integer": {
2207
- const parts = ["an integer"];
2208
- if (type.min !== void 0 && type.max !== void 0) parts.push(`between ${type.min} and ${type.max}`);
2209
- else if (type.min !== void 0) parts.push(`≥ ${type.min}`);
2210
- else if (type.max !== void 0) parts.push(`≤ ${type.max}`);
2211
- return parts.join(" ");
2212
- }
2213
- case "enum": return [...type.values].map((v) => `"${v}"`).join(", ");
2214
- }
2215
- }
2216
2323
  static #checkAriaAttributeValues({ props, effectiveRole }) {
2217
- if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS;
2324
+ if (effectiveRole === "none" || effectiveRole === "presentation") return VALID_RESULT;
2218
2325
  const results = [];
2219
2326
  iterate.forEachEntry(props, (key, value) => {
2220
2327
  if (!key.startsWith("aria-")) return;
2221
2328
  const type = ARIA_VALUE_TYPES.get(key);
2222
2329
  if (!isNonNull(type)) return;
2223
- if (AriaPolicyEngine.#isValidAriaValue(value, type)) return;
2330
+ if (isValidAriaValue(value, type)) return;
2224
2331
  results.push({
2225
2332
  valid: false,
2226
2333
  fixable: true,
2227
2334
  severity: "warning",
2228
2335
  attribute: key,
2229
- diagnostic: AriaDiagnostics.invalidAttributeValue(key, value, AriaPolicyEngine.#describeExpected(type)),
2336
+ diagnostic: AriaDiagnostics.invalidAttributeValue(key, value, describeExpected(type)),
2230
2337
  fix: AriaPolicyEngine.#makeRemoveAttributeFix(key)
2231
2338
  });
2232
2339
  });
2233
2340
  return results;
2234
2341
  }
2235
2342
  static #checkRedundantAriaLevel({ tag, props, effectiveRole }) {
2236
- if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS;
2343
+ if (effectiveRole === "none" || effectiveRole === "presentation") return VALID_RESULT;
2237
2344
  const implicitLevel = HEADING_IMPLICIT_LEVELS.get(tag);
2238
- if (!isNonNull(implicitLevel)) return NO_VIOLATIONS;
2345
+ if (!isNonNull(implicitLevel)) return VALID_RESULT;
2239
2346
  const raw = props["aria-level"];
2240
- if (!isNonNull(raw)) return NO_VIOLATIONS;
2347
+ if (!isNonNull(raw)) return VALID_RESULT;
2241
2348
  const n = strictNumeric(raw);
2242
- if (n === void 0 || n !== implicitLevel) return NO_VIOLATIONS;
2349
+ if (n === void 0 || n !== implicitLevel) return VALID_RESULT;
2243
2350
  return [{
2244
2351
  valid: false,
2245
2352
  fixable: true,
@@ -2250,7 +2357,7 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2250
2357
  }];
2251
2358
  }
2252
2359
  static #checkNameProhibitedRoles({ props, effectiveRole }) {
2253
- if (!effectiveRole || effectiveRole === "none" || effectiveRole === "presentation" || !NAME_PROHIBITED_ROLES.has(effectiveRole)) return NO_VIOLATIONS;
2360
+ if (!effectiveRole || effectiveRole === "none" || effectiveRole === "presentation" || !NAME_PROHIBITED_ROLES.has(effectiveRole)) return VALID_RESULT;
2254
2361
  const results = [];
2255
2362
  for (const key of NAME_PROHIBITED_ATTRIBUTES) {
2256
2363
  if (!(key in props)) continue;
@@ -2266,9 +2373,9 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2266
2373
  return results;
2267
2374
  }
2268
2375
  static #checkNameRequiredRoles({ tag, props, effectiveRole }) {
2269
- if (!effectiveRole || !NAME_REQUIRED_ROLES.has(effectiveRole)) return NO_VIOLATIONS;
2270
- if ("aria-label" in props || "aria-labelledby" in props) return NO_VIOLATIONS;
2271
- if (tag === "img" && typeof props.alt === "string" && props.alt.length > 0) return NO_VIOLATIONS;
2376
+ if (!effectiveRole || !NAME_REQUIRED_ROLES.has(effectiveRole)) return VALID_RESULT;
2377
+ if ("aria-label" in props || "aria-labelledby" in props) return VALID_RESULT;
2378
+ if (tag === "img" && isString(props.alt) && props.alt.length > 0) return VALID_RESULT;
2272
2379
  return [{
2273
2380
  valid: false,
2274
2381
  fixable: false,
@@ -2281,12 +2388,12 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2281
2388
  diagnosticFor: (attribute, role) => AriaDiagnostics.requiredProperty(attribute, role)
2282
2389
  };
2283
2390
  static #checkRequiredAriaProperties(context) {
2284
- if (!isNonNull(context.props.role)) return NO_VIOLATIONS;
2391
+ if (!isNonNull(context.props.role)) return VALID_RESULT;
2285
2392
  return checkRequiredAttributes(AriaPolicyEngine.#requiredAriaPropertiesRule, context);
2286
2393
  }
2287
2394
  static #checkAriaHiddenOnFocusable({ tag, props }) {
2288
- if (props["aria-hidden"] !== "true" && props["aria-hidden"] !== true) return NO_VIOLATIONS;
2289
- if (!isPotentiallyFocusable(tag, props)) return NO_VIOLATIONS;
2395
+ if (props["aria-hidden"] !== "true" && props["aria-hidden"] !== true) return VALID_RESULT;
2396
+ if (!isPotentiallyFocusable(tag, props)) return VALID_RESULT;
2290
2397
  return [{
2291
2398
  valid: false,
2292
2399
  fixable: false,
@@ -2296,7 +2403,7 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2296
2403
  }];
2297
2404
  }
2298
2405
  static #checkPresentationalAriaAttributes({ tag, props, effectiveRole }) {
2299
- if (effectiveRole !== "none" && effectiveRole !== "presentation") return NO_VIOLATIONS;
2406
+ if (effectiveRole !== "none" && effectiveRole !== "presentation") return VALID_RESULT;
2300
2407
  const results = [];
2301
2408
  iterate.forEachEntry(props, (key) => {
2302
2409
  if (!key.startsWith("aria-")) return;
@@ -2313,10 +2420,10 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2313
2420
  return results;
2314
2421
  }
2315
2422
  static #checkMissingLiveRegion({ effectiveRole, props }) {
2316
- if (!effectiveRole) return NO_VIOLATIONS;
2423
+ if (!effectiveRole) return VALID_RESULT;
2317
2424
  const impliedLive = LIVE_REGION_ROLES.get(effectiveRole);
2318
- if (!impliedLive) return NO_VIOLATIONS;
2319
- if ("aria-live" in props) return NO_VIOLATIONS;
2425
+ if (!impliedLive) return VALID_RESULT;
2426
+ if ("aria-live" in props) return VALID_RESULT;
2320
2427
  return [{
2321
2428
  valid: false,
2322
2429
  fixable: true,
@@ -2356,8 +2463,8 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2356
2463
  };
2357
2464
  static #checkInvalidAriaRelevant({ props }) {
2358
2465
  const relevant = props["aria-relevant"];
2359
- if (relevant === void 0) return NO_VIOLATIONS;
2360
- if (typeof relevant !== "string") return NO_VIOLATIONS;
2466
+ if (isUndefined(relevant)) return VALID_RESULT;
2467
+ if (!isString(relevant)) return VALID_RESULT;
2361
2468
  const tokens = relevant.trim().split(/\s+/);
2362
2469
  const invalid = tokens.filter((t) => !VALID_RELEVANT_TOKENS.has(t));
2363
2470
  if (invalid.length > 0) return [{
@@ -2376,7 +2483,7 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2376
2483
  diagnostic: AriaDiagnostics.relevantSuperseded(),
2377
2484
  fix: AriaPolicyEngine.#normalizeRelevantAllFix
2378
2485
  }];
2379
- return NO_VIOLATIONS;
2486
+ return VALID_RESULT;
2380
2487
  }
2381
2488
  };
2382
2489
  //#endregion
@@ -4680,6 +4787,23 @@ function applyMergePolicy(key, slotVal, childVal) {
4680
4787
  return policyHandlers[classifyProp(key, slotVal, childVal)](slotVal, childVal);
4681
4788
  }
4682
4789
  //#endregion
4790
+ //#region ../../adapters/react/src/shared/to-react-factory-options.ts
4791
+ /** React-specific additions on top of `FactoryOptions`. */
4792
+ const REACT_FIELD_VALIDATORS = {
4793
+ slotComponent: (v) => v === void 0 || isFunction(v) || isObject(v),
4794
+ filterProps: (v) => v === void 0 || isFunction(v),
4795
+ artifact: (v) => v === void 0 || isObject(v)
4796
+ };
4797
+ /**
4798
+ * Type guard narrowing a concrete contract `C` down to `C & ReactFactoryOptions` — the type
4799
+ * `buildRuntime` is declared against. Single-generic, matching `createContractComponent`'s own
4800
+ * `C extends ReactFactoryOptions` (Phase 2 of the `defineContract` refactor — was 6 independent
4801
+ * generics). See `isFactoryOptionsLike` for what this does and doesn't validate.
4802
+ */
4803
+ function isReactFactoryOptions(options) {
4804
+ return isFactoryOptionsLike(options, REACT_FIELD_VALIDATORS);
4805
+ }
4806
+ //#endregion
4683
4807
  //#region ../../adapters/react/src/shared/merge-refs.ts
4684
4808
  function mergeRefs(...refs) {
4685
4809
  return mergeRefsCore(...refs);
@@ -5062,4 +5186,4 @@ function buildRuntime(options, defaultSlotComponent, normalizeChildren) {
5062
5186
  };
5063
5187
  }
5064
5188
  //#endregion
5065
- export { invariant as _, getElementRef as a, Slottable as c, finalizeComponent as d, SLOT_NAME as f, defineContractComponent as g, isString as h, makeCloneSlotChild as i, applyDisplayName as l, isObject as m, render as n, getPropsRef as o, isFunction as p, applySlot as r, hasWarningGetter as s, buildRuntime as t, mergeRefs as u };
5189
+ export { getElementRef as a, Slottable as c, isReactFactoryOptions as d, finalizeComponent as f, invariant as g, isString as h, makeCloneSlotChild as i, applyDisplayName as l, isFunction as m, render as n, getPropsRef as o, SLOT_NAME as p, applySlot as r, hasWarningGetter as s, buildRuntime as t, mergeRefs as u };