praxis-kit 0.1.0 → 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.
@@ -18,11 +18,6 @@ function applyDisplayName(component, name) {
18
18
  Object.assign(component, { displayName: name ?? "PolymorphicComponent" });
19
19
  }
20
20
  //#endregion
21
- //#region ../../lib/adapter-utils/src/runtime/define-component.ts
22
- function defineContractComponent(options) {
23
- return (factory) => factory(options);
24
- }
25
- //#endregion
26
21
  //#region ../../lib/adapter-utils/src/runtime/assemble-compound-component.ts
27
22
  /**
28
23
  * Assembles a compound component by attaching sub-components to a
@@ -1590,72 +1585,100 @@ var InvariantBase = class {
1590
1585
  }
1591
1586
  };
1592
1587
  //#endregion
1593
- //#region ../../lib/contract/src/aria/spec/roles/required-properties.ts
1594
- const REQUIRED_ARIA_PROPERTIES = {
1595
- combobox: ["aria-expanded"],
1596
- option: ["aria-selected"],
1597
- slider: ["aria-valuenow"],
1598
- scrollbar: ["aria-controls", "aria-valuenow"],
1599
- spinbutton: ["aria-valuenow"]
1600
- };
1601
- //#endregion
1602
- //#region ../../lib/contract/src/aria/spec/roles/name-required.ts
1603
- const NAME_REQUIRED_ROLES = /* @__PURE__ */ new Set(["img"]);
1604
- //#endregion
1605
- //#region ../../lib/contract/src/aria/spec/roles/name-prohibited.ts
1606
- const NAME_PROHIBITED_ROLES = /* @__PURE__ */ new Set([
1607
- "caption",
1608
- "code",
1609
- "deletion",
1610
- "emphasis",
1611
- "generic",
1612
- "insertion",
1613
- "none",
1614
- "paragraph",
1615
- "presentation",
1616
- "strong",
1617
- "subscript",
1618
- "superscript"
1619
- ]);
1620
- const NAME_PROHIBITED_ATTRIBUTES = ["aria-label", "aria-labelledby"];
1621
- //#endregion
1622
- //#region ../../lib/contract/src/aria/spec/validators/required-properties-validator.ts
1623
- const NO_VIOLATIONS$1 = [{ valid: true }];
1624
- function requiredAttributeByRole(roles, attribute) {
1625
- return Object.fromEntries([...roles].map((role) => [role, [attribute]]));
1626
- }
1588
+ //#region ../../lib/contract/src/aria/plan-cache.ts
1627
1589
  /**
1628
- * Reports missing attributes required by an element's effective ARIA role.
1590
+ * Validation-plan cache machinery for `AriaPolicyEngine`: computing a cache key from a
1591
+ * (tag, props) usage site, and diffing/replaying the props transformation a validation pass
1592
+ * produced (an {@link AriaPlan}'s `removals`/`updates`) without re-running the ARIA rule
1593
+ * pipeline itself. Extracted because these four functions are a cohesive subsystem in their own
1594
+ * right — a small revalidation-caching scheme — not specifically ARIA *policy*; the actual
1595
+ * cache storage (an `LRUCache` instance) stays owned by `AriaPolicyEngine` itself, since it's
1596
+ * per-engine-instance state, not something this module needs to hold.
1597
+ */
1598
+ /**
1599
+ * Cache key covering only the aria-relevant subset of props (tag + role + aria-* attrs) —
1600
+ * exactly what the built-in pipeline reads. Non-aria props (className, onClick, etc.) do
1601
+ * not affect built-in ARIA decisions and are excluded so cache hits survive re-renders
1602
+ * that only change non-aria props. This key alone is unsound for a policy engine's own
1603
+ * extra rules, which may read arbitrary props outside this set — extend it with
1604
+ * {@link extraRulesKeySuffix}, or bypass the cache, to account for that.
1629
1605
  *
1630
- * Shared by validators that differ only in their role-to-attribute mapping and diagnostic
1631
- * generation.
1606
+ * Returns `null` (meaning "don't cache") when `tag` isn't a plain intrinsic string tag —
1607
+ * `isString(tag)` inline rather than importing `AriaPolicyEngine`'s own `isIntrinsicTag`: the
1608
+ * check is a one-liner, and importing it back from the engine module would be a needless
1609
+ * dependency in the other direction for a plan-cache module the engine itself depends on.
1632
1610
  */
1633
- function checkRequiredAttributes(requirement, { props, effectiveRole }) {
1634
- if (!effectiveRole) return NO_VIOLATIONS$1;
1635
- const requiredAttributes = requirement.attributesByRole[effectiveRole];
1636
- if (!requiredAttributes) return NO_VIOLATIONS$1;
1637
- const results = [];
1638
- for (const attribute of requiredAttributes) {
1639
- if (attribute in props) continue;
1640
- results.push({
1641
- valid: false,
1642
- fixable: false,
1643
- severity: "warning",
1644
- attribute,
1645
- diagnostic: requirement.diagnosticFor(attribute, effectiveRole)
1646
- });
1611
+ function createPlanKey(tag, props) {
1612
+ if (!isString(tag)) return null;
1613
+ const parts = [tag];
1614
+ if (isString(props.role)) parts.push(`role:${props.role}`);
1615
+ if (tag === "input" && isString(props.type)) parts.push(`type:${props.type}`);
1616
+ if (tag === "img") parts.push(`alt:${props.alt === "" ? "empty" : "present"}`);
1617
+ const ariaEntries = [];
1618
+ iterate.forEachEntry(props, (k, v) => {
1619
+ if (!k.startsWith("aria-")) return;
1620
+ if (!isString(v) && !isNumber(v) && typeof v !== "boolean") return;
1621
+ ariaEntries.push(`${k}:${String(v)}`);
1622
+ });
1623
+ if (ariaEntries.length > 0) parts.push(...ariaEntries.sort());
1624
+ return parts.join("|");
1625
+ }
1626
+ /**
1627
+ * Extends the base cache key with the props each extra rule declares it reads (`readsProps`).
1628
+ * Returns `null` — meaning "don't cache" — if any extra rule omits `readsProps` (it may read
1629
+ * arbitrary props the key can't account for) or if a declared prop's value isn't a primitive
1630
+ * (object/array identity isn't stably representable in a string key).
1631
+ */
1632
+ function extraRulesKeySuffix(extraRules, props) {
1633
+ const parts = [];
1634
+ for (const rule of extraRules) {
1635
+ const readsProps = rule.readsProps;
1636
+ if (!isNonNull(readsProps)) return null;
1637
+ for (const propKey of readsProps) {
1638
+ const v = props[propKey];
1639
+ if (isDefined(v) && !isString(v) && !isNumber(v) && typeof v !== "boolean") return null;
1640
+ parts.push(`x:${propKey}:${String(v)}`);
1641
+ }
1647
1642
  }
1648
- return results;
1643
+ return parts.sort().join("|");
1644
+ }
1645
+ /** Diffs a validation pass's input/output props into a replayable plan: which keys were removed
1646
+ * entirely, and which keys were added or changed value (and to what). */
1647
+ function computePlan(inputProps, resultProps) {
1648
+ const removals = /* @__PURE__ */ new Set();
1649
+ const updates = {};
1650
+ iterate.forEachKey(inputProps, (key) => {
1651
+ if (!(key in resultProps)) removals.add(key);
1652
+ });
1653
+ iterate.forEachEntry(resultProps, (key, resultVal) => {
1654
+ if (inputProps[key] !== resultVal) updates[key] = resultVal;
1655
+ });
1656
+ return {
1657
+ removals,
1658
+ updates
1659
+ };
1660
+ }
1661
+ /** Replays a cached plan's removals/updates against a fresh `props` object, without re-running
1662
+ * the rule pipeline that originally produced the plan. */
1663
+ function applyPlan(props, removals, updates) {
1664
+ const hasRemovals = removals.size > 0;
1665
+ const hasUpdates = Object.keys(updates).length > 0;
1666
+ if (!hasRemovals && !hasUpdates) return props;
1667
+ const next = {};
1668
+ iterate.forEachEntry(props, (k, v) => {
1669
+ if (!removals.has(k)) next[k] = v;
1670
+ });
1671
+ Object.assign(next, updates);
1672
+ return next;
1649
1673
  }
1650
1674
  //#endregion
1651
- //#region ../../lib/contract/src/aria/spec/roles/live-region.ts
1652
- const LIVE_REGION_ROLES = /* @__PURE__ */ new Map([
1653
- ["alert", "assertive"],
1654
- ["status", "polite"],
1655
- ["log", "polite"],
1656
- ["timer", "off"]
1675
+ //#region ../../lib/contract/src/aria/spec/attributes/aria-relevant-tokens.ts
1676
+ const VALID_RELEVANT_TOKENS = /* @__PURE__ */ new Set([
1677
+ "additions",
1678
+ "removals",
1679
+ "text",
1680
+ "all"
1657
1681
  ]);
1658
- const ATOMIC_REQUIREMENTS = requiredAttributeByRole(LIVE_REGION_ROLES.keys(), "aria-atomic");
1659
1682
  //#endregion
1660
1683
  //#region ../../lib/contract/src/aria/spec/attributes/aria-value-types.ts
1661
1684
  const ARIA_VALUE_TYPES = /* @__PURE__ */ new Map([
@@ -1780,24 +1803,6 @@ const ARIA_VALUE_TYPES = /* @__PURE__ */ new Map([
1780
1803
  }]
1781
1804
  ]);
1782
1805
  //#endregion
1783
- //#region ../../lib/contract/src/aria/spec/attributes/aria-relevant-tokens.ts
1784
- const VALID_RELEVANT_TOKENS = /* @__PURE__ */ new Set([
1785
- "additions",
1786
- "removals",
1787
- "text",
1788
- "all"
1789
- ]);
1790
- //#endregion
1791
- //#region ../../lib/contract/src/aria/spec/elements/heading-implicit-levels.ts
1792
- const HEADING_IMPLICIT_LEVELS = /* @__PURE__ */ new Map([
1793
- ["h1", 1],
1794
- ["h2", 2],
1795
- ["h3", 3],
1796
- ["h4", 4],
1797
- ["h5", 5],
1798
- ["h6", 6]
1799
- ]);
1800
- //#endregion
1801
1806
  //#region ../../lib/contract/src/aria/spec/elements/focusable.ts
1802
1807
  function parseTabIndex(raw) {
1803
1808
  if (typeof raw === "number") return Number.isInteger(raw) ? raw : void 0;
@@ -1848,24 +1853,156 @@ function isPotentiallyFocusable(tag, props) {
1848
1853
  }
1849
1854
  }
1850
1855
  //#endregion
1851
- //#region ../../lib/contract/src/aria/aria-policy-engine.ts
1856
+ //#region ../../lib/contract/src/aria/spec/elements/heading-implicit-levels.ts
1857
+ const HEADING_IMPLICIT_LEVELS = /* @__PURE__ */ new Map([
1858
+ ["h1", 1],
1859
+ ["h2", 2],
1860
+ ["h3", 3],
1861
+ ["h4", 4],
1862
+ ["h5", 5],
1863
+ ["h6", 6]
1864
+ ]);
1865
+ //#endregion
1866
+ //#region ../../lib/contract/src/aria/spec/validators/required-properties-validator.ts
1852
1867
  const NO_VIOLATIONS = [{ valid: true }];
1853
- const NO_VARIANT_KEYS = /* @__PURE__ */ new Set();
1854
- function isIntrinsicTag(tag) {
1855
- return isString(tag);
1868
+ function requiredAttributeByRole(roles, attribute) {
1869
+ return Object.fromEntries([...roles].map((role) => [role, [attribute]]));
1856
1870
  }
1857
- function omitProp(obj, key) {
1858
- const { [key]: _, ...rest } = obj;
1859
- return rest;
1871
+ /**
1872
+ * Reports missing attributes required by an element's effective ARIA role.
1873
+ *
1874
+ * Shared by validators that differ only in their role-to-attribute mapping and diagnostic
1875
+ * generation.
1876
+ */
1877
+ function checkRequiredAttributes(requirement, { props, effectiveRole }) {
1878
+ if (!effectiveRole) return NO_VIOLATIONS;
1879
+ const requiredAttributes = requirement.attributesByRole[effectiveRole];
1880
+ if (!requiredAttributes) return NO_VIOLATIONS;
1881
+ const results = [];
1882
+ for (const attribute of requiredAttributes) {
1883
+ if (attribute in props) continue;
1884
+ results.push({
1885
+ valid: false,
1886
+ fixable: false,
1887
+ severity: "warning",
1888
+ attribute,
1889
+ diagnostic: requirement.diagnosticFor(attribute, effectiveRole)
1890
+ });
1891
+ }
1892
+ return results;
1860
1893
  }
1894
+ //#endregion
1895
+ //#region ../../lib/contract/src/aria/spec/roles/live-region.ts
1896
+ const LIVE_REGION_ROLES = /* @__PURE__ */ new Map([
1897
+ ["alert", "assertive"],
1898
+ ["status", "polite"],
1899
+ ["log", "polite"],
1900
+ ["timer", "off"]
1901
+ ]);
1902
+ const ATOMIC_REQUIREMENTS = requiredAttributeByRole(LIVE_REGION_ROLES.keys(), "aria-atomic");
1903
+ //#endregion
1904
+ //#region ../../lib/contract/src/aria/spec/roles/name-prohibited.ts
1905
+ const NAME_PROHIBITED_ROLES = /* @__PURE__ */ new Set([
1906
+ "caption",
1907
+ "code",
1908
+ "deletion",
1909
+ "emphasis",
1910
+ "generic",
1911
+ "insertion",
1912
+ "none",
1913
+ "paragraph",
1914
+ "presentation",
1915
+ "strong",
1916
+ "subscript",
1917
+ "superscript"
1918
+ ]);
1919
+ const NAME_PROHIBITED_ATTRIBUTES = ["aria-label", "aria-labelledby"];
1920
+ //#endregion
1921
+ //#region ../../lib/contract/src/aria/spec/roles/name-required.ts
1922
+ const NAME_REQUIRED_ROLES = /* @__PURE__ */ new Set(["img"]);
1923
+ //#endregion
1924
+ //#region ../../lib/contract/src/aria/spec/roles/required-properties.ts
1925
+ const REQUIRED_ARIA_PROPERTIES = {
1926
+ combobox: ["aria-expanded"],
1927
+ option: ["aria-selected"],
1928
+ slider: ["aria-valuenow"],
1929
+ scrollbar: ["aria-controls", "aria-valuenow"],
1930
+ spinbutton: ["aria-valuenow"]
1931
+ };
1932
+ //#endregion
1933
+ //#region ../../lib/contract/src/aria/value-validation.ts
1934
+ /**
1935
+ * ARIA attribute value type system: what counts as a valid value for a given
1936
+ * {@link AriaValueType}, and how to describe what was expected when it isn't. Extracted from
1937
+ * `AriaPolicyEngine` — these three functions are cohesive on their own (they're describing a
1938
+ * type system, not policy), and `#checkAriaAttributeValues` (kept in `aria-policy-engine.ts`
1939
+ * alongside every other rule implementation — see that file's own pipeline) is now just a thin
1940
+ * caller of {@link isValidAriaValue}/{@link describeExpected}.
1941
+ */
1942
+ /**
1943
+ * Strict numeric coercion for ARIA attribute values. Unlike `parseFloat`/`parseInt`, the whole
1944
+ * string must be a number — `"12abc"` is rejected, not read as `12` — and an empty / whitespace
1945
+ * string is rejected rather than coerced to `0` the way `Number("")` would.
1946
+ *
1947
+ * Exported (not local to `isValidAriaValue`) because `AriaPolicyEngine.#checkRedundantAriaLevel`
1948
+ * also needs it, independent of the general ARIA value-type check this module otherwise exists
1949
+ * for — `aria-level`'s "redundant" check is about comparing a numeric attribute value to a
1950
+ * known implicit level, not about validating the value's type.
1951
+ */
1861
1952
  function strictNumeric(value) {
1862
- if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
1863
- if (typeof value !== "string") return void 0;
1953
+ if (isNumber(value)) return Number.isFinite(value) ? value : void 0;
1954
+ if (!isString(value)) return void 0;
1864
1955
  const trimmed = value.trim();
1865
1956
  if (trimmed === "") return void 0;
1866
1957
  const n = Number(trimmed);
1867
1958
  return Number.isFinite(n) ? n : void 0;
1868
1959
  }
1960
+ /** Returns whether `value` satisfies `type`'s own value-type constraint (boolean/tristate/
1961
+ * number/integer/enum) — the WAI-ARIA attribute value type system, independent of which
1962
+ * attribute or role it's being checked for. */
1963
+ function isValidAriaValue(value, type) {
1964
+ switch (type.kind) {
1965
+ case "boolean": return value === "true" || value === "false" || value === true || value === false;
1966
+ case "tristate": return value === "true" || value === "false" || value === "mixed" || value === true || value === false;
1967
+ case "number": return strictNumeric(value) !== void 0;
1968
+ case "integer": {
1969
+ const n = strictNumeric(value);
1970
+ if (isUndefined(n) || !Number.isInteger(n)) return false;
1971
+ if (isDefined(type.min) && n < type.min) return false;
1972
+ if (isDefined(type.max) && n > type.max) return false;
1973
+ return true;
1974
+ }
1975
+ case "enum": return isString(value) && type.values.has(value);
1976
+ }
1977
+ }
1978
+ /** Describes what a valid value for `type` looks like, for use in an invalid-value diagnostic
1979
+ * message (e.g. `aria-valuenow` got `"abc"`, expected "a finite number"). */
1980
+ function describeExpected(type) {
1981
+ switch (type.kind) {
1982
+ case "boolean": return "\"true\" or \"false\"";
1983
+ case "tristate": return "\"true\", \"false\", or \"mixed\"";
1984
+ case "number": return "a finite number";
1985
+ case "integer": {
1986
+ const parts = ["an integer"];
1987
+ if (isDefined(type.min) && isDefined(type.max)) parts.push(`between ${type.min} and ${type.max}`);
1988
+ else if (isDefined(type.min)) parts.push(`≥ ${type.min}`);
1989
+ else if (isDefined(type.max)) parts.push(`≤ ${type.max}`);
1990
+ return parts.join(" ");
1991
+ }
1992
+ case "enum": return [...type.values].map((v) => `"${v}"`).join(", ");
1993
+ }
1994
+ }
1995
+ //#endregion
1996
+ //#region ../../lib/contract/src/aria/aria-policy-engine.ts
1997
+ const VALID_RESULT = [{ valid: true }];
1998
+ const NO_VARIANT_KEYS = /* @__PURE__ */ new Set();
1999
+ function isIntrinsicTag(tag) {
2000
+ return isString(tag);
2001
+ }
2002
+ function omitProp(obj, key) {
2003
+ const { [key]: _, ...rest } = obj;
2004
+ return rest;
2005
+ }
1869
2006
  var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
1870
2007
  #extraRules;
1871
2008
  #variantKeys;
@@ -1998,73 +2135,20 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
1998
2135
  else this.warn(d);
1999
2136
  });
2000
2137
  }
2001
- static #createPlanKey(tag, props) {
2002
- if (!isIntrinsicTag(tag)) return null;
2003
- const parts = [tag];
2004
- if (typeof props.role === "string") parts.push(`role:${props.role}`);
2005
- if (tag === "input" && typeof props.type === "string") parts.push(`type:${props.type}`);
2006
- if (tag === "img") parts.push(`alt:${props.alt === "" ? "empty" : "present"}`);
2007
- const ariaEntries = [];
2008
- iterate.forEachEntry(props, (k, v) => {
2009
- if (!k.startsWith("aria-")) return;
2010
- if (!isString(v) && !isNumber(v) && typeof v !== "boolean") return;
2011
- ariaEntries.push(`${k}:${String(v)}`);
2012
- });
2013
- if (ariaEntries.length > 0) parts.push(...ariaEntries.sort());
2014
- return parts.join("|");
2015
- }
2016
- static #extraRulesKeySuffix(extraRules, props) {
2017
- const parts = [];
2018
- for (const rule of extraRules) {
2019
- const readsProps = rule.readsProps;
2020
- if (!isNonNull(readsProps)) return null;
2021
- for (const propKey of readsProps) {
2022
- const v = props[propKey];
2023
- if (v !== void 0 && !isString(v) && !isNumber(v) && typeof v !== "boolean") return null;
2024
- parts.push(`x:${propKey}:${String(v)}`);
2025
- }
2026
- }
2027
- return parts.sort().join("|");
2028
- }
2029
- static #computePlan(inputProps, resultProps) {
2030
- const removals = /* @__PURE__ */ new Set();
2031
- const updates = {};
2032
- iterate.forEachKey(inputProps, (key) => {
2033
- if (!(key in resultProps)) removals.add(key);
2034
- });
2035
- iterate.forEachEntry(resultProps, (key, resultVal) => {
2036
- if (inputProps[key] !== resultVal) updates[key] = resultVal;
2037
- });
2038
- return {
2039
- removals,
2040
- updates
2041
- };
2042
- }
2043
- static #applyPlan(props, removals, updates) {
2044
- const hasRemovals = removals.size > 0;
2045
- const hasUpdates = Object.keys(updates).length > 0;
2046
- if (!hasRemovals && !hasUpdates) return props;
2047
- const next = {};
2048
- iterate.forEachEntry(props, (k, v) => {
2049
- if (!removals.has(k)) next[k] = v;
2050
- });
2051
- Object.assign(next, updates);
2052
- return next;
2053
- }
2054
2138
  validate(tag, props, extraProps) {
2055
2139
  const ruleProps = extraProps ?? props;
2056
- const baseKey = AriaPolicyEngine.#createPlanKey(tag, props);
2140
+ const baseKey = createPlanKey(tag, props);
2057
2141
  let key = baseKey;
2058
2142
  if (this.#extraRules.length > 0) {
2059
- const suffix = AriaPolicyEngine.#extraRulesKeySuffix(this.#extraRules, ruleProps);
2143
+ const suffix = extraRulesKeySuffix(this.#extraRules, ruleProps);
2060
2144
  key = isNonNull(baseKey) && isNonNull(suffix) ? `${baseKey}|${suffix}` : null;
2061
2145
  }
2062
2146
  if (!isNull(key)) {
2063
2147
  const cached = this.#planCache.get(key);
2064
- if (cached !== void 0) {
2148
+ if (isDefined(cached)) {
2065
2149
  if (cached.violations.length > 0) this.report(cached.violations);
2066
2150
  return {
2067
- props: AriaPolicyEngine.#applyPlan(props, cached.removals, cached.updates),
2151
+ props: applyPlan(props, cached.removals, cached.updates),
2068
2152
  violations: cached.violations
2069
2153
  };
2070
2154
  }
@@ -2072,7 +2156,7 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2072
2156
  const result = this.#extraRules.length ? AriaPolicyEngine.#evaluateWithRules(tag, props, this.#extraRules, extraProps, this.#variantKeys) : AriaPolicyEngine.evaluate(tag, props);
2073
2157
  if (result.violations.length > 0) this.report(result.violations);
2074
2158
  if (!isNull(key)) {
2075
- const { removals, updates } = AriaPolicyEngine.#computePlan(props, result.props);
2159
+ const { removals, updates } = computePlan(props, result.props);
2076
2160
  const plan = {
2077
2161
  removals,
2078
2162
  updates,
@@ -2163,7 +2247,7 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2163
2247
  ];
2164
2248
  static #checkInvalidRoleOverride({ tag, props, implicitRole }) {
2165
2249
  const role = props.role;
2166
- if (!implicitRole || !role || role === implicitRole) return NO_VIOLATIONS;
2250
+ if (!implicitRole || isUndefined(role) || role === implicitRole) return VALID_RESULT;
2167
2251
  if (isStrongImplicitRole(tag) && role === "region") {
2168
2252
  const diagnostic = HtmlDiagnostics.implicitRoleOverride(tag, implicitRole, role);
2169
2253
  return [{
@@ -2174,11 +2258,11 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2174
2258
  diagnostic
2175
2259
  }];
2176
2260
  }
2177
- return NO_VIOLATIONS;
2261
+ return VALID_RESULT;
2178
2262
  }
2179
2263
  static #checkRedundantRole({ tag, props, implicitRole }) {
2180
2264
  const role = props.role;
2181
- if (!implicitRole || !role || role !== implicitRole) return NO_VIOLATIONS;
2265
+ if (!implicitRole || isUndefined(role) || role !== implicitRole) return VALID_RESULT;
2182
2266
  const diagnostic = HtmlDiagnostics.implicitRoleRedundant(tag, implicitRole);
2183
2267
  return [{
2184
2268
  valid: false,
@@ -2189,8 +2273,8 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2189
2273
  }];
2190
2274
  }
2191
2275
  static #checkStandaloneRegion({ tag, props, implicitRole }) {
2192
- if (props.role !== "region") return NO_VIOLATIONS;
2193
- if (!hasStandaloneRole(tag)) return NO_VIOLATIONS;
2276
+ if (props.role !== "region") return VALID_RESULT;
2277
+ if (!hasStandaloneRole(tag)) return VALID_RESULT;
2194
2278
  const diagnostic = HtmlDiagnostics.standaloneRegionOverride(tag, implicitRole ?? tag);
2195
2279
  return [{
2196
2280
  valid: false,
@@ -2201,7 +2285,7 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2201
2285
  }];
2202
2286
  }
2203
2287
  static #checkInvalidAriaAttributes({ tag, props, effectiveRole }) {
2204
- if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS;
2288
+ if (effectiveRole === "none" || effectiveRole === "presentation") return VALID_RESULT;
2205
2289
  const results = [];
2206
2290
  iterate.forEachEntry(props, (key) => {
2207
2291
  if (!key.startsWith("aria-")) return;
@@ -2218,63 +2302,33 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2218
2302
  });
2219
2303
  return results;
2220
2304
  }
2221
- static #isValidAriaValue(value, type) {
2222
- switch (type.kind) {
2223
- case "boolean": return value === "true" || value === "false" || value === true || value === false;
2224
- case "tristate": return value === "true" || value === "false" || value === "mixed" || value === true || value === false;
2225
- case "number": return strictNumeric(value) !== void 0;
2226
- case "integer": {
2227
- const n = strictNumeric(value);
2228
- if (n === void 0 || !Number.isInteger(n)) return false;
2229
- if (type.min !== void 0 && n < type.min) return false;
2230
- if (type.max !== void 0 && n > type.max) return false;
2231
- return true;
2232
- }
2233
- case "enum": return typeof value === "string" && type.values.has(value);
2234
- }
2235
- }
2236
- static #describeExpected(type) {
2237
- switch (type.kind) {
2238
- case "boolean": return "\"true\" or \"false\"";
2239
- case "tristate": return "\"true\", \"false\", or \"mixed\"";
2240
- case "number": return "a finite number";
2241
- case "integer": {
2242
- const parts = ["an integer"];
2243
- if (type.min !== void 0 && type.max !== void 0) parts.push(`between ${type.min} and ${type.max}`);
2244
- else if (type.min !== void 0) parts.push(`≥ ${type.min}`);
2245
- else if (type.max !== void 0) parts.push(`≤ ${type.max}`);
2246
- return parts.join(" ");
2247
- }
2248
- case "enum": return [...type.values].map((v) => `"${v}"`).join(", ");
2249
- }
2250
- }
2251
2305
  static #checkAriaAttributeValues({ props, effectiveRole }) {
2252
- if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS;
2306
+ if (effectiveRole === "none" || effectiveRole === "presentation") return VALID_RESULT;
2253
2307
  const results = [];
2254
2308
  iterate.forEachEntry(props, (key, value) => {
2255
2309
  if (!key.startsWith("aria-")) return;
2256
2310
  const type = ARIA_VALUE_TYPES.get(key);
2257
2311
  if (!isNonNull(type)) return;
2258
- if (AriaPolicyEngine.#isValidAriaValue(value, type)) return;
2312
+ if (isValidAriaValue(value, type)) return;
2259
2313
  results.push({
2260
2314
  valid: false,
2261
2315
  fixable: true,
2262
2316
  severity: "warning",
2263
2317
  attribute: key,
2264
- diagnostic: AriaDiagnostics.invalidAttributeValue(key, value, AriaPolicyEngine.#describeExpected(type)),
2318
+ diagnostic: AriaDiagnostics.invalidAttributeValue(key, value, describeExpected(type)),
2265
2319
  fix: AriaPolicyEngine.#makeRemoveAttributeFix(key)
2266
2320
  });
2267
2321
  });
2268
2322
  return results;
2269
2323
  }
2270
2324
  static #checkRedundantAriaLevel({ tag, props, effectiveRole }) {
2271
- if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS;
2325
+ if (effectiveRole === "none" || effectiveRole === "presentation") return VALID_RESULT;
2272
2326
  const implicitLevel = HEADING_IMPLICIT_LEVELS.get(tag);
2273
- if (!isNonNull(implicitLevel)) return NO_VIOLATIONS;
2327
+ if (!isNonNull(implicitLevel)) return VALID_RESULT;
2274
2328
  const raw = props["aria-level"];
2275
- if (!isNonNull(raw)) return NO_VIOLATIONS;
2329
+ if (!isNonNull(raw)) return VALID_RESULT;
2276
2330
  const n = strictNumeric(raw);
2277
- if (n === void 0 || n !== implicitLevel) return NO_VIOLATIONS;
2331
+ if (n === void 0 || n !== implicitLevel) return VALID_RESULT;
2278
2332
  return [{
2279
2333
  valid: false,
2280
2334
  fixable: true,
@@ -2285,7 +2339,7 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2285
2339
  }];
2286
2340
  }
2287
2341
  static #checkNameProhibitedRoles({ props, effectiveRole }) {
2288
- if (!effectiveRole || effectiveRole === "none" || effectiveRole === "presentation" || !NAME_PROHIBITED_ROLES.has(effectiveRole)) return NO_VIOLATIONS;
2342
+ if (!effectiveRole || effectiveRole === "none" || effectiveRole === "presentation" || !NAME_PROHIBITED_ROLES.has(effectiveRole)) return VALID_RESULT;
2289
2343
  const results = [];
2290
2344
  for (const key of NAME_PROHIBITED_ATTRIBUTES) {
2291
2345
  if (!(key in props)) continue;
@@ -2301,9 +2355,9 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2301
2355
  return results;
2302
2356
  }
2303
2357
  static #checkNameRequiredRoles({ tag, props, effectiveRole }) {
2304
- if (!effectiveRole || !NAME_REQUIRED_ROLES.has(effectiveRole)) return NO_VIOLATIONS;
2305
- if ("aria-label" in props || "aria-labelledby" in props) return NO_VIOLATIONS;
2306
- if (tag === "img" && typeof props.alt === "string" && props.alt.length > 0) return NO_VIOLATIONS;
2358
+ if (!effectiveRole || !NAME_REQUIRED_ROLES.has(effectiveRole)) return VALID_RESULT;
2359
+ if ("aria-label" in props || "aria-labelledby" in props) return VALID_RESULT;
2360
+ if (tag === "img" && isString(props.alt) && props.alt.length > 0) return VALID_RESULT;
2307
2361
  return [{
2308
2362
  valid: false,
2309
2363
  fixable: false,
@@ -2316,12 +2370,12 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2316
2370
  diagnosticFor: (attribute, role) => AriaDiagnostics.requiredProperty(attribute, role)
2317
2371
  };
2318
2372
  static #checkRequiredAriaProperties(context) {
2319
- if (!isNonNull(context.props.role)) return NO_VIOLATIONS;
2373
+ if (!isNonNull(context.props.role)) return VALID_RESULT;
2320
2374
  return checkRequiredAttributes(AriaPolicyEngine.#requiredAriaPropertiesRule, context);
2321
2375
  }
2322
2376
  static #checkAriaHiddenOnFocusable({ tag, props }) {
2323
- if (props["aria-hidden"] !== "true" && props["aria-hidden"] !== true) return NO_VIOLATIONS;
2324
- if (!isPotentiallyFocusable(tag, props)) return NO_VIOLATIONS;
2377
+ if (props["aria-hidden"] !== "true" && props["aria-hidden"] !== true) return VALID_RESULT;
2378
+ if (!isPotentiallyFocusable(tag, props)) return VALID_RESULT;
2325
2379
  return [{
2326
2380
  valid: false,
2327
2381
  fixable: false,
@@ -2331,7 +2385,7 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2331
2385
  }];
2332
2386
  }
2333
2387
  static #checkPresentationalAriaAttributes({ tag, props, effectiveRole }) {
2334
- if (effectiveRole !== "none" && effectiveRole !== "presentation") return NO_VIOLATIONS;
2388
+ if (effectiveRole !== "none" && effectiveRole !== "presentation") return VALID_RESULT;
2335
2389
  const results = [];
2336
2390
  iterate.forEachEntry(props, (key) => {
2337
2391
  if (!key.startsWith("aria-")) return;
@@ -2348,10 +2402,10 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2348
2402
  return results;
2349
2403
  }
2350
2404
  static #checkMissingLiveRegion({ effectiveRole, props }) {
2351
- if (!effectiveRole) return NO_VIOLATIONS;
2405
+ if (!effectiveRole) return VALID_RESULT;
2352
2406
  const impliedLive = LIVE_REGION_ROLES.get(effectiveRole);
2353
- if (!impliedLive) return NO_VIOLATIONS;
2354
- if ("aria-live" in props) return NO_VIOLATIONS;
2407
+ if (!impliedLive) return VALID_RESULT;
2408
+ if ("aria-live" in props) return VALID_RESULT;
2355
2409
  return [{
2356
2410
  valid: false,
2357
2411
  fixable: true,
@@ -2391,8 +2445,8 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2391
2445
  };
2392
2446
  static #checkInvalidAriaRelevant({ props }) {
2393
2447
  const relevant = props["aria-relevant"];
2394
- if (relevant === void 0) return NO_VIOLATIONS;
2395
- if (typeof relevant !== "string") return NO_VIOLATIONS;
2448
+ if (isUndefined(relevant)) return VALID_RESULT;
2449
+ if (!isString(relevant)) return VALID_RESULT;
2396
2450
  const tokens = relevant.trim().split(/\s+/);
2397
2451
  const invalid = tokens.filter((t) => !VALID_RELEVANT_TOKENS.has(t));
2398
2452
  if (invalid.length > 0) return [{
@@ -2411,7 +2465,7 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2411
2465
  diagnostic: AriaDiagnostics.relevantSuperseded(),
2412
2466
  fix: AriaPolicyEngine.#normalizeRelevantAllFix
2413
2467
  }];
2414
- return NO_VIOLATIONS;
2468
+ return VALID_RESULT;
2415
2469
  }
2416
2470
  };
2417
2471
  //#endregion
@@ -4978,9 +5032,10 @@ const PREACT_FIELD_VALIDATORS = {
4978
5032
  filterProps: (v) => v === void 0 || isFunction(v)
4979
5033
  };
4980
5034
  /**
4981
- * Type guard narrowing the generic `FactoryOptions` shape down to
4982
- * `PreactFactoryOptions` — the type `buildRuntime` is declared against. See
4983
- * `isFactoryOptionsLike` for what this does and doesn't validate.
5035
+ * Type guard narrowing a concrete contract `C` down to `C & PreactFactoryOptions` — the type
5036
+ * `buildRuntime` is declared against. Single-generic, matching `createContractComponent`'s own
5037
+ * `C extends PreactFactoryOptions` (Phase 2 of the `defineContract` refactor — was 5 independent
5038
+ * generics). See `isFactoryOptionsLike` for what this does and doesn't validate.
4984
5039
  */
4985
5040
  function isPreactFactoryOptions(options) {
4986
5041
  return isFactoryOptionsLike(options, PREACT_FIELD_VALIDATORS);
@@ -5007,9 +5062,21 @@ function isPreactFactoryOptions(options) {
5007
5062
  * Returns a `forwardRef` component — `ref` is forwarded to the rendered host element. Pass
5008
5063
  * `subComponents` to attach named sub-components (`Card.Header`) and `onElement` to run setup
5009
5064
  * once the real DOM element exists.
5065
+ *
5066
+ * `TDefault`/`Props`/`Variants`/`TPreset`/`TPlugin` are each `ContractXOf<C>`-derived *defaults*
5067
+ * on this function's own type parameter list, mirroring `@praxis-kit/react`'s identical fix — see
5068
+ * that adapter's own doc comment for why (computed as function type-parameter defaults, not inline
5069
+ * body computations, which doesn't resolve for a still-abstract `C`). No `TAllowed` here, matching
5070
+ * this adapter's pre-refactor behavior — Preact never threaded it as its own generic.
5010
5071
  */
5011
5072
  function createContractComponent(options) {
5012
5073
  invariant(isPreactFactoryOptions(options), "options is not a valid PreactFactoryOptions object");
5074
+ /**
5075
+ * `C` and `TDefault`/`Props`/`Variants`/`TPreset` are formally independent type parameters to
5076
+ * the checker — see `@praxis-kit/react`'s identical comment for the full explanation. The
5077
+ * `invariant` above is what actually guarantees `options` is `PreactFactoryOptions` shaped; this
5078
+ * assertion bridges the gap in the compiler's reasoning.
5079
+ */
5013
5080
  const bundle = buildRuntime(options);
5014
5081
  const { onElement } = options;
5015
5082
  const Component = forwardRef(function Component(props, ref) {
@@ -5040,4 +5107,4 @@ function createContractComponent(options) {
5040
5107
  return assembled;
5041
5108
  }
5042
5109
  //#endregion
5043
- export { Slottable, createContractComponent, defineContractComponent };
5110
+ export { Slottable, createContractComponent };