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.
package/dist/lit/index.js CHANGED
@@ -10,11 +10,6 @@ function invariant(condition, message) {
10
10
  if (!condition) panic(message);
11
11
  }
12
12
  //#endregion
13
- //#region ../../lib/adapter-utils/src/runtime/define-component.ts
14
- function defineContractComponent(options) {
15
- return (factory) => factory(options);
16
- }
17
- //#endregion
18
13
  //#region ../../lib/adapter-utils/src/runtime/assemble-compound-component.ts
19
14
  /**
20
15
  * Assembles a compound component by attaching sub-components to a
@@ -1406,72 +1401,100 @@ var InvariantBase = class {
1406
1401
  }
1407
1402
  };
1408
1403
  //#endregion
1409
- //#region ../../lib/contract/src/aria/spec/roles/required-properties.ts
1410
- const REQUIRED_ARIA_PROPERTIES = {
1411
- combobox: ["aria-expanded"],
1412
- option: ["aria-selected"],
1413
- slider: ["aria-valuenow"],
1414
- scrollbar: ["aria-controls", "aria-valuenow"],
1415
- spinbutton: ["aria-valuenow"]
1416
- };
1417
- //#endregion
1418
- //#region ../../lib/contract/src/aria/spec/roles/name-required.ts
1419
- const NAME_REQUIRED_ROLES = /* @__PURE__ */ new Set(["img"]);
1420
- //#endregion
1421
- //#region ../../lib/contract/src/aria/spec/roles/name-prohibited.ts
1422
- const NAME_PROHIBITED_ROLES = /* @__PURE__ */ new Set([
1423
- "caption",
1424
- "code",
1425
- "deletion",
1426
- "emphasis",
1427
- "generic",
1428
- "insertion",
1429
- "none",
1430
- "paragraph",
1431
- "presentation",
1432
- "strong",
1433
- "subscript",
1434
- "superscript"
1435
- ]);
1436
- const NAME_PROHIBITED_ATTRIBUTES = ["aria-label", "aria-labelledby"];
1437
- //#endregion
1438
- //#region ../../lib/contract/src/aria/spec/validators/required-properties-validator.ts
1439
- const NO_VIOLATIONS$1 = [{ valid: true }];
1440
- function requiredAttributeByRole(roles, attribute) {
1441
- return Object.fromEntries([...roles].map((role) => [role, [attribute]]));
1442
- }
1404
+ //#region ../../lib/contract/src/aria/plan-cache.ts
1443
1405
  /**
1444
- * Reports missing attributes required by an element's effective ARIA role.
1406
+ * Validation-plan cache machinery for `AriaPolicyEngine`: computing a cache key from a
1407
+ * (tag, props) usage site, and diffing/replaying the props transformation a validation pass
1408
+ * produced (an {@link AriaPlan}'s `removals`/`updates`) without re-running the ARIA rule
1409
+ * pipeline itself. Extracted because these four functions are a cohesive subsystem in their own
1410
+ * right — a small revalidation-caching scheme — not specifically ARIA *policy*; the actual
1411
+ * cache storage (an `LRUCache` instance) stays owned by `AriaPolicyEngine` itself, since it's
1412
+ * per-engine-instance state, not something this module needs to hold.
1413
+ */
1414
+ /**
1415
+ * Cache key covering only the aria-relevant subset of props (tag + role + aria-* attrs) —
1416
+ * exactly what the built-in pipeline reads. Non-aria props (className, onClick, etc.) do
1417
+ * not affect built-in ARIA decisions and are excluded so cache hits survive re-renders
1418
+ * that only change non-aria props. This key alone is unsound for a policy engine's own
1419
+ * extra rules, which may read arbitrary props outside this set — extend it with
1420
+ * {@link extraRulesKeySuffix}, or bypass the cache, to account for that.
1445
1421
  *
1446
- * Shared by validators that differ only in their role-to-attribute mapping and diagnostic
1447
- * generation.
1422
+ * Returns `null` (meaning "don't cache") when `tag` isn't a plain intrinsic string tag —
1423
+ * `isString(tag)` inline rather than importing `AriaPolicyEngine`'s own `isIntrinsicTag`: the
1424
+ * check is a one-liner, and importing it back from the engine module would be a needless
1425
+ * dependency in the other direction for a plan-cache module the engine itself depends on.
1448
1426
  */
1449
- function checkRequiredAttributes(requirement, { props, effectiveRole }) {
1450
- if (!effectiveRole) return NO_VIOLATIONS$1;
1451
- const requiredAttributes = requirement.attributesByRole[effectiveRole];
1452
- if (!requiredAttributes) return NO_VIOLATIONS$1;
1453
- const results = [];
1454
- for (const attribute of requiredAttributes) {
1455
- if (attribute in props) continue;
1456
- results.push({
1457
- valid: false,
1458
- fixable: false,
1459
- severity: "warning",
1460
- attribute,
1461
- diagnostic: requirement.diagnosticFor(attribute, effectiveRole)
1462
- });
1427
+ function createPlanKey(tag, props) {
1428
+ if (!isString(tag)) return null;
1429
+ const parts = [tag];
1430
+ if (isString(props.role)) parts.push(`role:${props.role}`);
1431
+ if (tag === "input" && isString(props.type)) parts.push(`type:${props.type}`);
1432
+ if (tag === "img") parts.push(`alt:${props.alt === "" ? "empty" : "present"}`);
1433
+ const ariaEntries = [];
1434
+ iterate.forEachEntry(props, (k, v) => {
1435
+ if (!k.startsWith("aria-")) return;
1436
+ if (!isString(v) && !isNumber(v) && typeof v !== "boolean") return;
1437
+ ariaEntries.push(`${k}:${String(v)}`);
1438
+ });
1439
+ if (ariaEntries.length > 0) parts.push(...ariaEntries.sort());
1440
+ return parts.join("|");
1441
+ }
1442
+ /**
1443
+ * Extends the base cache key with the props each extra rule declares it reads (`readsProps`).
1444
+ * Returns `null` — meaning "don't cache" — if any extra rule omits `readsProps` (it may read
1445
+ * arbitrary props the key can't account for) or if a declared prop's value isn't a primitive
1446
+ * (object/array identity isn't stably representable in a string key).
1447
+ */
1448
+ function extraRulesKeySuffix(extraRules, props) {
1449
+ const parts = [];
1450
+ for (const rule of extraRules) {
1451
+ const readsProps = rule.readsProps;
1452
+ if (!isNonNull(readsProps)) return null;
1453
+ for (const propKey of readsProps) {
1454
+ const v = props[propKey];
1455
+ if (isDefined(v) && !isString(v) && !isNumber(v) && typeof v !== "boolean") return null;
1456
+ parts.push(`x:${propKey}:${String(v)}`);
1457
+ }
1463
1458
  }
1464
- return results;
1459
+ return parts.sort().join("|");
1460
+ }
1461
+ /** Diffs a validation pass's input/output props into a replayable plan: which keys were removed
1462
+ * entirely, and which keys were added or changed value (and to what). */
1463
+ function computePlan(inputProps, resultProps) {
1464
+ const removals = /* @__PURE__ */ new Set();
1465
+ const updates = {};
1466
+ iterate.forEachKey(inputProps, (key) => {
1467
+ if (!(key in resultProps)) removals.add(key);
1468
+ });
1469
+ iterate.forEachEntry(resultProps, (key, resultVal) => {
1470
+ if (inputProps[key] !== resultVal) updates[key] = resultVal;
1471
+ });
1472
+ return {
1473
+ removals,
1474
+ updates
1475
+ };
1476
+ }
1477
+ /** Replays a cached plan's removals/updates against a fresh `props` object, without re-running
1478
+ * the rule pipeline that originally produced the plan. */
1479
+ function applyPlan(props, removals, updates) {
1480
+ const hasRemovals = removals.size > 0;
1481
+ const hasUpdates = Object.keys(updates).length > 0;
1482
+ if (!hasRemovals && !hasUpdates) return props;
1483
+ const next = {};
1484
+ iterate.forEachEntry(props, (k, v) => {
1485
+ if (!removals.has(k)) next[k] = v;
1486
+ });
1487
+ Object.assign(next, updates);
1488
+ return next;
1465
1489
  }
1466
1490
  //#endregion
1467
- //#region ../../lib/contract/src/aria/spec/roles/live-region.ts
1468
- const LIVE_REGION_ROLES = /* @__PURE__ */ new Map([
1469
- ["alert", "assertive"],
1470
- ["status", "polite"],
1471
- ["log", "polite"],
1472
- ["timer", "off"]
1491
+ //#region ../../lib/contract/src/aria/spec/attributes/aria-relevant-tokens.ts
1492
+ const VALID_RELEVANT_TOKENS = /* @__PURE__ */ new Set([
1493
+ "additions",
1494
+ "removals",
1495
+ "text",
1496
+ "all"
1473
1497
  ]);
1474
- const ATOMIC_REQUIREMENTS = requiredAttributeByRole(LIVE_REGION_ROLES.keys(), "aria-atomic");
1475
1498
  //#endregion
1476
1499
  //#region ../../lib/contract/src/aria/spec/attributes/aria-value-types.ts
1477
1500
  const ARIA_VALUE_TYPES = /* @__PURE__ */ new Map([
@@ -1596,24 +1619,6 @@ const ARIA_VALUE_TYPES = /* @__PURE__ */ new Map([
1596
1619
  }]
1597
1620
  ]);
1598
1621
  //#endregion
1599
- //#region ../../lib/contract/src/aria/spec/attributes/aria-relevant-tokens.ts
1600
- const VALID_RELEVANT_TOKENS = /* @__PURE__ */ new Set([
1601
- "additions",
1602
- "removals",
1603
- "text",
1604
- "all"
1605
- ]);
1606
- //#endregion
1607
- //#region ../../lib/contract/src/aria/spec/elements/heading-implicit-levels.ts
1608
- const HEADING_IMPLICIT_LEVELS = /* @__PURE__ */ new Map([
1609
- ["h1", 1],
1610
- ["h2", 2],
1611
- ["h3", 3],
1612
- ["h4", 4],
1613
- ["h5", 5],
1614
- ["h6", 6]
1615
- ]);
1616
- //#endregion
1617
1622
  //#region ../../lib/contract/src/aria/spec/elements/focusable.ts
1618
1623
  function parseTabIndex(raw) {
1619
1624
  if (typeof raw === "number") return Number.isInteger(raw) ? raw : void 0;
@@ -1664,24 +1669,156 @@ function isPotentiallyFocusable(tag, props) {
1664
1669
  }
1665
1670
  }
1666
1671
  //#endregion
1667
- //#region ../../lib/contract/src/aria/aria-policy-engine.ts
1672
+ //#region ../../lib/contract/src/aria/spec/elements/heading-implicit-levels.ts
1673
+ const HEADING_IMPLICIT_LEVELS = /* @__PURE__ */ new Map([
1674
+ ["h1", 1],
1675
+ ["h2", 2],
1676
+ ["h3", 3],
1677
+ ["h4", 4],
1678
+ ["h5", 5],
1679
+ ["h6", 6]
1680
+ ]);
1681
+ //#endregion
1682
+ //#region ../../lib/contract/src/aria/spec/validators/required-properties-validator.ts
1668
1683
  const NO_VIOLATIONS = [{ valid: true }];
1669
- const NO_VARIANT_KEYS = /* @__PURE__ */ new Set();
1670
- function isIntrinsicTag(tag) {
1671
- return isString(tag);
1684
+ function requiredAttributeByRole(roles, attribute) {
1685
+ return Object.fromEntries([...roles].map((role) => [role, [attribute]]));
1672
1686
  }
1673
- function omitProp(obj, key) {
1674
- const { [key]: _, ...rest } = obj;
1675
- return rest;
1687
+ /**
1688
+ * Reports missing attributes required by an element's effective ARIA role.
1689
+ *
1690
+ * Shared by validators that differ only in their role-to-attribute mapping and diagnostic
1691
+ * generation.
1692
+ */
1693
+ function checkRequiredAttributes(requirement, { props, effectiveRole }) {
1694
+ if (!effectiveRole) return NO_VIOLATIONS;
1695
+ const requiredAttributes = requirement.attributesByRole[effectiveRole];
1696
+ if (!requiredAttributes) return NO_VIOLATIONS;
1697
+ const results = [];
1698
+ for (const attribute of requiredAttributes) {
1699
+ if (attribute in props) continue;
1700
+ results.push({
1701
+ valid: false,
1702
+ fixable: false,
1703
+ severity: "warning",
1704
+ attribute,
1705
+ diagnostic: requirement.diagnosticFor(attribute, effectiveRole)
1706
+ });
1707
+ }
1708
+ return results;
1676
1709
  }
1710
+ //#endregion
1711
+ //#region ../../lib/contract/src/aria/spec/roles/live-region.ts
1712
+ const LIVE_REGION_ROLES = /* @__PURE__ */ new Map([
1713
+ ["alert", "assertive"],
1714
+ ["status", "polite"],
1715
+ ["log", "polite"],
1716
+ ["timer", "off"]
1717
+ ]);
1718
+ const ATOMIC_REQUIREMENTS = requiredAttributeByRole(LIVE_REGION_ROLES.keys(), "aria-atomic");
1719
+ //#endregion
1720
+ //#region ../../lib/contract/src/aria/spec/roles/name-prohibited.ts
1721
+ const NAME_PROHIBITED_ROLES = /* @__PURE__ */ new Set([
1722
+ "caption",
1723
+ "code",
1724
+ "deletion",
1725
+ "emphasis",
1726
+ "generic",
1727
+ "insertion",
1728
+ "none",
1729
+ "paragraph",
1730
+ "presentation",
1731
+ "strong",
1732
+ "subscript",
1733
+ "superscript"
1734
+ ]);
1735
+ const NAME_PROHIBITED_ATTRIBUTES = ["aria-label", "aria-labelledby"];
1736
+ //#endregion
1737
+ //#region ../../lib/contract/src/aria/spec/roles/name-required.ts
1738
+ const NAME_REQUIRED_ROLES = /* @__PURE__ */ new Set(["img"]);
1739
+ //#endregion
1740
+ //#region ../../lib/contract/src/aria/spec/roles/required-properties.ts
1741
+ const REQUIRED_ARIA_PROPERTIES = {
1742
+ combobox: ["aria-expanded"],
1743
+ option: ["aria-selected"],
1744
+ slider: ["aria-valuenow"],
1745
+ scrollbar: ["aria-controls", "aria-valuenow"],
1746
+ spinbutton: ["aria-valuenow"]
1747
+ };
1748
+ //#endregion
1749
+ //#region ../../lib/contract/src/aria/value-validation.ts
1750
+ /**
1751
+ * ARIA attribute value type system: what counts as a valid value for a given
1752
+ * {@link AriaValueType}, and how to describe what was expected when it isn't. Extracted from
1753
+ * `AriaPolicyEngine` — these three functions are cohesive on their own (they're describing a
1754
+ * type system, not policy), and `#checkAriaAttributeValues` (kept in `aria-policy-engine.ts`
1755
+ * alongside every other rule implementation — see that file's own pipeline) is now just a thin
1756
+ * caller of {@link isValidAriaValue}/{@link describeExpected}.
1757
+ */
1758
+ /**
1759
+ * Strict numeric coercion for ARIA attribute values. Unlike `parseFloat`/`parseInt`, the whole
1760
+ * string must be a number — `"12abc"` is rejected, not read as `12` — and an empty / whitespace
1761
+ * string is rejected rather than coerced to `0` the way `Number("")` would.
1762
+ *
1763
+ * Exported (not local to `isValidAriaValue`) because `AriaPolicyEngine.#checkRedundantAriaLevel`
1764
+ * also needs it, independent of the general ARIA value-type check this module otherwise exists
1765
+ * for — `aria-level`'s "redundant" check is about comparing a numeric attribute value to a
1766
+ * known implicit level, not about validating the value's type.
1767
+ */
1677
1768
  function strictNumeric(value) {
1678
- if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
1679
- if (typeof value !== "string") return void 0;
1769
+ if (isNumber(value)) return Number.isFinite(value) ? value : void 0;
1770
+ if (!isString(value)) return void 0;
1680
1771
  const trimmed = value.trim();
1681
1772
  if (trimmed === "") return void 0;
1682
1773
  const n = Number(trimmed);
1683
1774
  return Number.isFinite(n) ? n : void 0;
1684
1775
  }
1776
+ /** Returns whether `value` satisfies `type`'s own value-type constraint (boolean/tristate/
1777
+ * number/integer/enum) — the WAI-ARIA attribute value type system, independent of which
1778
+ * attribute or role it's being checked for. */
1779
+ function isValidAriaValue(value, type) {
1780
+ switch (type.kind) {
1781
+ case "boolean": return value === "true" || value === "false" || value === true || value === false;
1782
+ case "tristate": return value === "true" || value === "false" || value === "mixed" || value === true || value === false;
1783
+ case "number": return strictNumeric(value) !== void 0;
1784
+ case "integer": {
1785
+ const n = strictNumeric(value);
1786
+ if (isUndefined(n) || !Number.isInteger(n)) return false;
1787
+ if (isDefined(type.min) && n < type.min) return false;
1788
+ if (isDefined(type.max) && n > type.max) return false;
1789
+ return true;
1790
+ }
1791
+ case "enum": return isString(value) && type.values.has(value);
1792
+ }
1793
+ }
1794
+ /** Describes what a valid value for `type` looks like, for use in an invalid-value diagnostic
1795
+ * message (e.g. `aria-valuenow` got `"abc"`, expected "a finite number"). */
1796
+ function describeExpected(type) {
1797
+ switch (type.kind) {
1798
+ case "boolean": return "\"true\" or \"false\"";
1799
+ case "tristate": return "\"true\", \"false\", or \"mixed\"";
1800
+ case "number": return "a finite number";
1801
+ case "integer": {
1802
+ const parts = ["an integer"];
1803
+ if (isDefined(type.min) && isDefined(type.max)) parts.push(`between ${type.min} and ${type.max}`);
1804
+ else if (isDefined(type.min)) parts.push(`≥ ${type.min}`);
1805
+ else if (isDefined(type.max)) parts.push(`≤ ${type.max}`);
1806
+ return parts.join(" ");
1807
+ }
1808
+ case "enum": return [...type.values].map((v) => `"${v}"`).join(", ");
1809
+ }
1810
+ }
1811
+ //#endregion
1812
+ //#region ../../lib/contract/src/aria/aria-policy-engine.ts
1813
+ const VALID_RESULT = [{ valid: true }];
1814
+ const NO_VARIANT_KEYS = /* @__PURE__ */ new Set();
1815
+ function isIntrinsicTag(tag) {
1816
+ return isString(tag);
1817
+ }
1818
+ function omitProp(obj, key) {
1819
+ const { [key]: _, ...rest } = obj;
1820
+ return rest;
1821
+ }
1685
1822
  var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
1686
1823
  #extraRules;
1687
1824
  #variantKeys;
@@ -1814,73 +1951,20 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
1814
1951
  else this.warn(d);
1815
1952
  });
1816
1953
  }
1817
- static #createPlanKey(tag, props) {
1818
- if (!isIntrinsicTag(tag)) return null;
1819
- const parts = [tag];
1820
- if (typeof props.role === "string") parts.push(`role:${props.role}`);
1821
- if (tag === "input" && typeof props.type === "string") parts.push(`type:${props.type}`);
1822
- if (tag === "img") parts.push(`alt:${props.alt === "" ? "empty" : "present"}`);
1823
- const ariaEntries = [];
1824
- iterate.forEachEntry(props, (k, v) => {
1825
- if (!k.startsWith("aria-")) return;
1826
- if (!isString(v) && !isNumber(v) && typeof v !== "boolean") return;
1827
- ariaEntries.push(`${k}:${String(v)}`);
1828
- });
1829
- if (ariaEntries.length > 0) parts.push(...ariaEntries.sort());
1830
- return parts.join("|");
1831
- }
1832
- static #extraRulesKeySuffix(extraRules, props) {
1833
- const parts = [];
1834
- for (const rule of extraRules) {
1835
- const readsProps = rule.readsProps;
1836
- if (!isNonNull(readsProps)) return null;
1837
- for (const propKey of readsProps) {
1838
- const v = props[propKey];
1839
- if (v !== void 0 && !isString(v) && !isNumber(v) && typeof v !== "boolean") return null;
1840
- parts.push(`x:${propKey}:${String(v)}`);
1841
- }
1842
- }
1843
- return parts.sort().join("|");
1844
- }
1845
- static #computePlan(inputProps, resultProps) {
1846
- const removals = /* @__PURE__ */ new Set();
1847
- const updates = {};
1848
- iterate.forEachKey(inputProps, (key) => {
1849
- if (!(key in resultProps)) removals.add(key);
1850
- });
1851
- iterate.forEachEntry(resultProps, (key, resultVal) => {
1852
- if (inputProps[key] !== resultVal) updates[key] = resultVal;
1853
- });
1854
- return {
1855
- removals,
1856
- updates
1857
- };
1858
- }
1859
- static #applyPlan(props, removals, updates) {
1860
- const hasRemovals = removals.size > 0;
1861
- const hasUpdates = Object.keys(updates).length > 0;
1862
- if (!hasRemovals && !hasUpdates) return props;
1863
- const next = {};
1864
- iterate.forEachEntry(props, (k, v) => {
1865
- if (!removals.has(k)) next[k] = v;
1866
- });
1867
- Object.assign(next, updates);
1868
- return next;
1869
- }
1870
1954
  validate(tag, props, extraProps) {
1871
1955
  const ruleProps = extraProps ?? props;
1872
- const baseKey = AriaPolicyEngine.#createPlanKey(tag, props);
1956
+ const baseKey = createPlanKey(tag, props);
1873
1957
  let key = baseKey;
1874
1958
  if (this.#extraRules.length > 0) {
1875
- const suffix = AriaPolicyEngine.#extraRulesKeySuffix(this.#extraRules, ruleProps);
1959
+ const suffix = extraRulesKeySuffix(this.#extraRules, ruleProps);
1876
1960
  key = isNonNull(baseKey) && isNonNull(suffix) ? `${baseKey}|${suffix}` : null;
1877
1961
  }
1878
1962
  if (!isNull(key)) {
1879
1963
  const cached = this.#planCache.get(key);
1880
- if (cached !== void 0) {
1964
+ if (isDefined(cached)) {
1881
1965
  if (cached.violations.length > 0) this.report(cached.violations);
1882
1966
  return {
1883
- props: AriaPolicyEngine.#applyPlan(props, cached.removals, cached.updates),
1967
+ props: applyPlan(props, cached.removals, cached.updates),
1884
1968
  violations: cached.violations
1885
1969
  };
1886
1970
  }
@@ -1888,7 +1972,7 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
1888
1972
  const result = this.#extraRules.length ? AriaPolicyEngine.#evaluateWithRules(tag, props, this.#extraRules, extraProps, this.#variantKeys) : AriaPolicyEngine.evaluate(tag, props);
1889
1973
  if (result.violations.length > 0) this.report(result.violations);
1890
1974
  if (!isNull(key)) {
1891
- const { removals, updates } = AriaPolicyEngine.#computePlan(props, result.props);
1975
+ const { removals, updates } = computePlan(props, result.props);
1892
1976
  const plan = {
1893
1977
  removals,
1894
1978
  updates,
@@ -1979,7 +2063,7 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
1979
2063
  ];
1980
2064
  static #checkInvalidRoleOverride({ tag, props, implicitRole }) {
1981
2065
  const role = props.role;
1982
- if (!implicitRole || !role || role === implicitRole) return NO_VIOLATIONS;
2066
+ if (!implicitRole || isUndefined(role) || role === implicitRole) return VALID_RESULT;
1983
2067
  if (isStrongImplicitRole(tag) && role === "region") {
1984
2068
  const diagnostic = HtmlDiagnostics.implicitRoleOverride(tag, implicitRole, role);
1985
2069
  return [{
@@ -1990,11 +2074,11 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
1990
2074
  diagnostic
1991
2075
  }];
1992
2076
  }
1993
- return NO_VIOLATIONS;
2077
+ return VALID_RESULT;
1994
2078
  }
1995
2079
  static #checkRedundantRole({ tag, props, implicitRole }) {
1996
2080
  const role = props.role;
1997
- if (!implicitRole || !role || role !== implicitRole) return NO_VIOLATIONS;
2081
+ if (!implicitRole || isUndefined(role) || role !== implicitRole) return VALID_RESULT;
1998
2082
  const diagnostic = HtmlDiagnostics.implicitRoleRedundant(tag, implicitRole);
1999
2083
  return [{
2000
2084
  valid: false,
@@ -2005,8 +2089,8 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2005
2089
  }];
2006
2090
  }
2007
2091
  static #checkStandaloneRegion({ tag, props, implicitRole }) {
2008
- if (props.role !== "region") return NO_VIOLATIONS;
2009
- if (!hasStandaloneRole(tag)) return NO_VIOLATIONS;
2092
+ if (props.role !== "region") return VALID_RESULT;
2093
+ if (!hasStandaloneRole(tag)) return VALID_RESULT;
2010
2094
  const diagnostic = HtmlDiagnostics.standaloneRegionOverride(tag, implicitRole ?? tag);
2011
2095
  return [{
2012
2096
  valid: false,
@@ -2017,7 +2101,7 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2017
2101
  }];
2018
2102
  }
2019
2103
  static #checkInvalidAriaAttributes({ tag, props, effectiveRole }) {
2020
- if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS;
2104
+ if (effectiveRole === "none" || effectiveRole === "presentation") return VALID_RESULT;
2021
2105
  const results = [];
2022
2106
  iterate.forEachEntry(props, (key) => {
2023
2107
  if (!key.startsWith("aria-")) return;
@@ -2034,63 +2118,33 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2034
2118
  });
2035
2119
  return results;
2036
2120
  }
2037
- static #isValidAriaValue(value, type) {
2038
- switch (type.kind) {
2039
- case "boolean": return value === "true" || value === "false" || value === true || value === false;
2040
- case "tristate": return value === "true" || value === "false" || value === "mixed" || value === true || value === false;
2041
- case "number": return strictNumeric(value) !== void 0;
2042
- case "integer": {
2043
- const n = strictNumeric(value);
2044
- if (n === void 0 || !Number.isInteger(n)) return false;
2045
- if (type.min !== void 0 && n < type.min) return false;
2046
- if (type.max !== void 0 && n > type.max) return false;
2047
- return true;
2048
- }
2049
- case "enum": return typeof value === "string" && type.values.has(value);
2050
- }
2051
- }
2052
- static #describeExpected(type) {
2053
- switch (type.kind) {
2054
- case "boolean": return "\"true\" or \"false\"";
2055
- case "tristate": return "\"true\", \"false\", or \"mixed\"";
2056
- case "number": return "a finite number";
2057
- case "integer": {
2058
- const parts = ["an integer"];
2059
- if (type.min !== void 0 && type.max !== void 0) parts.push(`between ${type.min} and ${type.max}`);
2060
- else if (type.min !== void 0) parts.push(`≥ ${type.min}`);
2061
- else if (type.max !== void 0) parts.push(`≤ ${type.max}`);
2062
- return parts.join(" ");
2063
- }
2064
- case "enum": return [...type.values].map((v) => `"${v}"`).join(", ");
2065
- }
2066
- }
2067
2121
  static #checkAriaAttributeValues({ props, effectiveRole }) {
2068
- if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS;
2122
+ if (effectiveRole === "none" || effectiveRole === "presentation") return VALID_RESULT;
2069
2123
  const results = [];
2070
2124
  iterate.forEachEntry(props, (key, value) => {
2071
2125
  if (!key.startsWith("aria-")) return;
2072
2126
  const type = ARIA_VALUE_TYPES.get(key);
2073
2127
  if (!isNonNull(type)) return;
2074
- if (AriaPolicyEngine.#isValidAriaValue(value, type)) return;
2128
+ if (isValidAriaValue(value, type)) return;
2075
2129
  results.push({
2076
2130
  valid: false,
2077
2131
  fixable: true,
2078
2132
  severity: "warning",
2079
2133
  attribute: key,
2080
- diagnostic: AriaDiagnostics.invalidAttributeValue(key, value, AriaPolicyEngine.#describeExpected(type)),
2134
+ diagnostic: AriaDiagnostics.invalidAttributeValue(key, value, describeExpected(type)),
2081
2135
  fix: AriaPolicyEngine.#makeRemoveAttributeFix(key)
2082
2136
  });
2083
2137
  });
2084
2138
  return results;
2085
2139
  }
2086
2140
  static #checkRedundantAriaLevel({ tag, props, effectiveRole }) {
2087
- if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS;
2141
+ if (effectiveRole === "none" || effectiveRole === "presentation") return VALID_RESULT;
2088
2142
  const implicitLevel = HEADING_IMPLICIT_LEVELS.get(tag);
2089
- if (!isNonNull(implicitLevel)) return NO_VIOLATIONS;
2143
+ if (!isNonNull(implicitLevel)) return VALID_RESULT;
2090
2144
  const raw = props["aria-level"];
2091
- if (!isNonNull(raw)) return NO_VIOLATIONS;
2145
+ if (!isNonNull(raw)) return VALID_RESULT;
2092
2146
  const n = strictNumeric(raw);
2093
- if (n === void 0 || n !== implicitLevel) return NO_VIOLATIONS;
2147
+ if (n === void 0 || n !== implicitLevel) return VALID_RESULT;
2094
2148
  return [{
2095
2149
  valid: false,
2096
2150
  fixable: true,
@@ -2101,7 +2155,7 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2101
2155
  }];
2102
2156
  }
2103
2157
  static #checkNameProhibitedRoles({ props, effectiveRole }) {
2104
- if (!effectiveRole || effectiveRole === "none" || effectiveRole === "presentation" || !NAME_PROHIBITED_ROLES.has(effectiveRole)) return NO_VIOLATIONS;
2158
+ if (!effectiveRole || effectiveRole === "none" || effectiveRole === "presentation" || !NAME_PROHIBITED_ROLES.has(effectiveRole)) return VALID_RESULT;
2105
2159
  const results = [];
2106
2160
  for (const key of NAME_PROHIBITED_ATTRIBUTES) {
2107
2161
  if (!(key in props)) continue;
@@ -2117,9 +2171,9 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2117
2171
  return results;
2118
2172
  }
2119
2173
  static #checkNameRequiredRoles({ tag, props, effectiveRole }) {
2120
- if (!effectiveRole || !NAME_REQUIRED_ROLES.has(effectiveRole)) return NO_VIOLATIONS;
2121
- if ("aria-label" in props || "aria-labelledby" in props) return NO_VIOLATIONS;
2122
- if (tag === "img" && typeof props.alt === "string" && props.alt.length > 0) return NO_VIOLATIONS;
2174
+ if (!effectiveRole || !NAME_REQUIRED_ROLES.has(effectiveRole)) return VALID_RESULT;
2175
+ if ("aria-label" in props || "aria-labelledby" in props) return VALID_RESULT;
2176
+ if (tag === "img" && isString(props.alt) && props.alt.length > 0) return VALID_RESULT;
2123
2177
  return [{
2124
2178
  valid: false,
2125
2179
  fixable: false,
@@ -2132,12 +2186,12 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2132
2186
  diagnosticFor: (attribute, role) => AriaDiagnostics.requiredProperty(attribute, role)
2133
2187
  };
2134
2188
  static #checkRequiredAriaProperties(context) {
2135
- if (!isNonNull(context.props.role)) return NO_VIOLATIONS;
2189
+ if (!isNonNull(context.props.role)) return VALID_RESULT;
2136
2190
  return checkRequiredAttributes(AriaPolicyEngine.#requiredAriaPropertiesRule, context);
2137
2191
  }
2138
2192
  static #checkAriaHiddenOnFocusable({ tag, props }) {
2139
- if (props["aria-hidden"] !== "true" && props["aria-hidden"] !== true) return NO_VIOLATIONS;
2140
- if (!isPotentiallyFocusable(tag, props)) return NO_VIOLATIONS;
2193
+ if (props["aria-hidden"] !== "true" && props["aria-hidden"] !== true) return VALID_RESULT;
2194
+ if (!isPotentiallyFocusable(tag, props)) return VALID_RESULT;
2141
2195
  return [{
2142
2196
  valid: false,
2143
2197
  fixable: false,
@@ -2147,7 +2201,7 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2147
2201
  }];
2148
2202
  }
2149
2203
  static #checkPresentationalAriaAttributes({ tag, props, effectiveRole }) {
2150
- if (effectiveRole !== "none" && effectiveRole !== "presentation") return NO_VIOLATIONS;
2204
+ if (effectiveRole !== "none" && effectiveRole !== "presentation") return VALID_RESULT;
2151
2205
  const results = [];
2152
2206
  iterate.forEachEntry(props, (key) => {
2153
2207
  if (!key.startsWith("aria-")) return;
@@ -2164,10 +2218,10 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2164
2218
  return results;
2165
2219
  }
2166
2220
  static #checkMissingLiveRegion({ effectiveRole, props }) {
2167
- if (!effectiveRole) return NO_VIOLATIONS;
2221
+ if (!effectiveRole) return VALID_RESULT;
2168
2222
  const impliedLive = LIVE_REGION_ROLES.get(effectiveRole);
2169
- if (!impliedLive) return NO_VIOLATIONS;
2170
- if ("aria-live" in props) return NO_VIOLATIONS;
2223
+ if (!impliedLive) return VALID_RESULT;
2224
+ if ("aria-live" in props) return VALID_RESULT;
2171
2225
  return [{
2172
2226
  valid: false,
2173
2227
  fixable: true,
@@ -2207,8 +2261,8 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2207
2261
  };
2208
2262
  static #checkInvalidAriaRelevant({ props }) {
2209
2263
  const relevant = props["aria-relevant"];
2210
- if (relevant === void 0) return NO_VIOLATIONS;
2211
- if (typeof relevant !== "string") return NO_VIOLATIONS;
2264
+ if (isUndefined(relevant)) return VALID_RESULT;
2265
+ if (!isString(relevant)) return VALID_RESULT;
2212
2266
  const tokens = relevant.trim().split(/\s+/);
2213
2267
  const invalid = tokens.filter((t) => !VALID_RELEVANT_TOKENS.has(t));
2214
2268
  if (invalid.length > 0) return [{
@@ -2227,7 +2281,7 @@ var AriaPolicyEngine = class AriaPolicyEngine extends InvariantBase {
2227
2281
  diagnostic: AriaDiagnostics.relevantSuperseded(),
2228
2282
  fix: AriaPolicyEngine.#normalizeRelevantAllFix
2229
2283
  }];
2230
- return NO_VIOLATIONS;
2284
+ return VALID_RESULT;
2231
2285
  }
2232
2286
  };
2233
2287
  //#endregion
@@ -4646,11 +4700,11 @@ function buildRuntime(options) {
4646
4700
  * boundaries: the value is a function whose prototype chain includes
4647
4701
  * `LitElement`, exactly what `createContractComponent` always produces.
4648
4702
  *
4649
- * `LitContractComponent`'s third parameter, `G` (the phantom `__generics`
4650
- * marker `GenericsOf`/`ContractProps` read back — `./types/contract-props`),
4651
- * is left at its default here: it carries no runtime signal for this guard to
4652
- * check, and `createContractComponent` supplies the real `G` itself when it
4653
- * casts this function's narrowed result to its public return type.
4703
+ * `LitContractComponent`'s third and fourth parameters, `G` and `C` (the phantom `__generics`/
4704
+ * `__contract` markers `GenericsOf`/`ContractProps` read back — `./types/contract-props`), are
4705
+ * left at their defaults here: neither carries a runtime signal for this guard to check, and
4706
+ * `createContractComponent` supplies the real `G`/`C` itself when it casts this function's
4707
+ * narrowed result to its public return type.
4654
4708
  */
4655
4709
  function isLitContractComponent(value) {
4656
4710
  return isFunction(value) && value.prototype instanceof LitElement;
@@ -4704,9 +4758,10 @@ function renderContractToString(component, props = {}, innerHTML = "") {
4704
4758
  /** Lit-specific addition on top of `FactoryOptions`. */
4705
4759
  const LIT_FIELD_VALIDATORS = { filterProps: (v) => v === void 0 || isFunction(v) };
4706
4760
  /**
4707
- * Type guard narrowing the generic `FactoryOptions` shape down to
4708
- * `LitFactoryOptions` — the type `buildRuntime` is declared against. See
4709
- * `isFactoryOptionsLike` for what this does and doesn't validate.
4761
+ * Type guard narrowing a concrete contract `C` down to `C & LitFactoryOptions` — the type
4762
+ * `buildRuntime` is declared against. Single-generic, matching `createContractComponent`'s own
4763
+ * `C extends LitFactoryOptions` (Phase 2 of the `defineContract` refactor — was 4 independent
4764
+ * generics). See `isFactoryOptionsLike` for what this does and doesn't validate.
4710
4765
  */
4711
4766
  function isLitFactoryOptions(options) {
4712
4767
  return isFactoryOptionsLike(options, LIT_FIELD_VALIDATORS);
@@ -4782,6 +4837,11 @@ function isLitFactoryOptions(options) {
4782
4837
  * tag polymorphism, but SSR quietly provided a fake, DOM-inconsistent form of it until now. Need
4783
4838
  * different semantics for one instance? Register a second component with a different `tag`, or
4784
4839
  * set `role` directly — both already work today, unaffected by this.
4840
+ *
4841
+ * `TDefault`/`TProps`/`TVariants`/`TPreset`/`TPlugin` are each `ContractXOf<C>`-derived *defaults*
4842
+ * on this function's own type parameter list, mirroring every other adapter's identical fix — see
4843
+ * `@praxis-kit/react`'s own doc comment for why (computed as function type-parameter defaults, not
4844
+ * inline body computations, which doesn't resolve for a still-abstract `C`).
4785
4845
  */
4786
4846
  function createContractComponent(options) {
4787
4847
  invariant(isLitFactoryOptions(options), "options is not a valid LitFactoryOptions object");
@@ -4890,4 +4950,4 @@ function createContractComponent(options) {
4890
4950
  return assembleCompoundComponent(PolymorphicLitElement, options.subComponents);
4891
4951
  }
4892
4952
  //#endregion
4893
- export { createContractComponent, defineContractComponent, renderContractToString };
4953
+ export { createContractComponent, renderContractToString };