praxis-kit 6.2.1 → 6.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -993,11 +993,28 @@ var ContractDiagnostics = {
993
993
 
994
994
  // ../../lib/contract/src/diagnostics/html.ts
995
995
  import { DiagnosticCategory as DiagnosticCategory3, DiagnosticCode as DiagnosticCode3 } from "./_shared/diagnostics.js";
996
+ var ATTRIBUTE_IGNORED_CODES = {
997
+ checked: DiagnosticCode3.HtmlInputCheckedIgnoredForType,
998
+ multiple: DiagnosticCode3.HtmlInputMultipleIgnoredForType,
999
+ maxLength: DiagnosticCode3.HtmlInputMaxLengthIgnoredForType,
1000
+ minLength: DiagnosticCode3.HtmlInputMinLengthIgnoredForType,
1001
+ pattern: DiagnosticCode3.HtmlInputPatternIgnoredForType,
1002
+ min: DiagnosticCode3.HtmlInputMinIgnoredForType,
1003
+ max: DiagnosticCode3.HtmlInputMaxIgnoredForType,
1004
+ step: DiagnosticCode3.HtmlInputStepIgnoredForType,
1005
+ accept: DiagnosticCode3.HtmlInputAcceptIgnoredForType,
1006
+ capture: DiagnosticCode3.HtmlInputCaptureIgnoredForType,
1007
+ size: DiagnosticCode3.HtmlInputSizeIgnoredForType,
1008
+ alt: DiagnosticCode3.HtmlInputAltIgnoredForType,
1009
+ height: DiagnosticCode3.HtmlInputHeightIgnoredForType,
1010
+ width: DiagnosticCode3.HtmlInputWidthIgnoredForType
1011
+ };
996
1012
  var HtmlDiagnostics = {
997
1013
  emptyRole(tag) {
998
1014
  return {
999
1015
  code: DiagnosticCode3.HtmlEmptyRole,
1000
1016
  category: DiagnosticCategory3.HTML,
1017
+ severity: "warning",
1001
1018
  message: `<${tag}> has an explicit empty role="". Omit the attribute instead.`
1002
1019
  };
1003
1020
  },
@@ -1005,6 +1022,7 @@ var HtmlDiagnostics = {
1005
1022
  return {
1006
1023
  code: DiagnosticCode3.HtmlImplicitRoleRedundant,
1007
1024
  category: DiagnosticCategory3.HTML,
1025
+ severity: "warning",
1008
1026
  message: `<${tag}> already has implicit role="${implicitRole}". Avoid redundant role assignment.`
1009
1027
  };
1010
1028
  },
@@ -1012,6 +1030,7 @@ var HtmlDiagnostics = {
1012
1030
  return {
1013
1031
  code: DiagnosticCode3.HtmlImplicitRoleOverride,
1014
1032
  category: DiagnosticCategory3.HTML,
1033
+ severity: "error",
1015
1034
  message: `<${tag}> should not override its implicit role="${implicitRole}" with role="${role}".`
1016
1035
  };
1017
1036
  },
@@ -1019,6 +1038,7 @@ var HtmlDiagnostics = {
1019
1038
  return {
1020
1039
  code: DiagnosticCode3.HtmlStandaloneRegionOverride,
1021
1040
  category: DiagnosticCategory3.HTML,
1041
+ severity: "error",
1022
1042
  message: `<${tag}> is a self-contained element with implicit role="${implicitRole}". Assigning role="region" has been removed.`
1023
1043
  };
1024
1044
  },
@@ -1026,6 +1046,7 @@ var HtmlDiagnostics = {
1026
1046
  return {
1027
1047
  code: DiagnosticCode3.HtmlLandmarkRoleOverride,
1028
1048
  category: DiagnosticCategory3.HTML,
1049
+ severity: "error",
1029
1050
  message: `<${tag}> has a fixed landmark role="${implicitRole}". role="${role}" overrides it and confuses assistive technology. The override has been removed.`
1030
1051
  };
1031
1052
  },
@@ -1033,34 +1054,148 @@ var HtmlDiagnostics = {
1033
1054
  return {
1034
1055
  code: DiagnosticCode3.HtmlInvalidChild,
1035
1056
  category: DiagnosticCategory3.HTML,
1057
+ severity: "error",
1036
1058
  message: `<${child}> is not a valid direct child of <${parent}>. Allowed: ${allowed}.`
1037
1059
  };
1060
+ },
1061
+ roleNotPermitted(tag, role, allowedRoles) {
1062
+ const allowed = allowedRoles.length > 0 ? allowedRoles.map((r) => `"${r}"`).join(", ") : "none \u2014 no explicit role is permitted on this element";
1063
+ return {
1064
+ code: DiagnosticCode3.HtmlRoleNotPermitted,
1065
+ category: DiagnosticCategory3.HTML,
1066
+ severity: "error",
1067
+ message: `role="${role}" is not permitted on <${tag}>. Allowed alternate role(s): ${allowed}.`,
1068
+ rationale: 'The WAI-ARIA "ARIA in HTML" specification restricts which explicit roles a native element may take. A role outside that set is ignored or produces undefined behavior in assistive technology.'
1069
+ };
1070
+ },
1071
+ // Reserved for <input>-specific facts (HTML3101–3199, see codes.ts) — later element families
1072
+ // (button, img, table, ...) get their own reserved block and their own namespace here.
1073
+ input: {
1074
+ unsupportedType(type) {
1075
+ return {
1076
+ code: DiagnosticCode3.HtmlInputUnsupportedType,
1077
+ category: DiagnosticCategory3.HTML,
1078
+ severity: "warning",
1079
+ message: `type="${type}" is not a value defined by the HTML specification. Browsers silently fall back to type="text".`,
1080
+ rationale: 'An unrecognized input type is not invalid markup \u2014 the spec requires the "text" fallback \u2014 but it usually means a typo, since the input keeps working while silently losing the intended type-specific behavior (validation, virtual keyboard, picker UI).',
1081
+ suggestions: [
1082
+ {
1083
+ title: "Check for a typo in the type value",
1084
+ description: `"${type}" does not match any HTML5 input type.`
1085
+ }
1086
+ ]
1087
+ };
1088
+ },
1089
+ // The user-visible problem is that the attribute is ignored — not that it "requires" a type;
1090
+ // that's the rule's internal framing, not what the browser actually does.
1091
+ attributeIgnoredForType(attribute, type, allowedTypes) {
1092
+ const allowed = allowedTypes.map((t) => `"${t}"`).join(", ");
1093
+ const code = ATTRIBUTE_IGNORED_CODES[attribute];
1094
+ if (!code) throw new Error(`No DiagnosticCode registered for input attribute "${attribute}"`);
1095
+ return {
1096
+ code,
1097
+ category: DiagnosticCategory3.HTML,
1098
+ severity: "warning",
1099
+ message: `"${attribute}" is ignored on <input type="${type}">.`,
1100
+ rationale: `"${attribute}" only has an effect when type is one of: ${allowed}. Browsers silently ignore it on other input types.`,
1101
+ suggestions: [
1102
+ {
1103
+ title: `Remove "${attribute}"`,
1104
+ description: `"${attribute}" only affects <input> when type is one of: ${allowed}.`
1105
+ }
1106
+ ]
1107
+ };
1108
+ }
1038
1109
  }
1039
1110
  };
1040
1111
 
1041
- // ../../lib/contract/src/diagnostics/slot.ts
1112
+ // ../../lib/contract/src/diagnostics/input-accessibility.ts
1042
1113
  import { DiagnosticCategory as DiagnosticCategory4, DiagnosticCode as DiagnosticCode4 } from "./_shared/diagnostics.js";
1114
+ function accessibilityFact(input) {
1115
+ return { category: DiagnosticCategory4.Accessibility, ...input };
1116
+ }
1117
+ var InputAccessibilityDiagnostics = {
1118
+ missingAccessibleName() {
1119
+ return accessibilityFact({
1120
+ code: DiagnosticCode4.A11yInputMissingAccessibleName,
1121
+ severity: "warning",
1122
+ message: "This input has no accessible name. Add an associated <label>, aria-label, or aria-labelledby.",
1123
+ rationale: "Assistive technology announces a form field by its accessible name; without one, users of screen readers cannot tell what the field is for.",
1124
+ suggestions: [
1125
+ { title: "Add aria-label", description: 'Set aria-label="\u2026" directly on the input.' },
1126
+ {
1127
+ title: "Add an associated <label>",
1128
+ description: 'Wrap the input in a <label>, or point a <label for="\u2026"> at its id.'
1129
+ }
1130
+ ]
1131
+ });
1132
+ },
1133
+ placeholderIsNotLabel() {
1134
+ return accessibilityFact({
1135
+ code: DiagnosticCode4.A11yInputPlaceholderNotLabel,
1136
+ severity: "warning",
1137
+ message: "Placeholder text does not provide an accessible name. Add an associated <label>, aria-label, or aria-labelledby.",
1138
+ rationale: "Placeholder text disappears as users interact with the field and is not treated as the control's accessible name by many assistive technologies.",
1139
+ suggestions: [
1140
+ { title: "Add aria-label", description: 'Set aria-label="\u2026" directly on the input.' },
1141
+ {
1142
+ title: "Add an associated <label>",
1143
+ description: 'Wrap the input in a <label>, or point a <label for="\u2026"> at its id.'
1144
+ }
1145
+ ]
1146
+ });
1147
+ },
1148
+ passwordMissingAutocomplete() {
1149
+ return accessibilityFact({
1150
+ code: DiagnosticCode4.A11yInputPasswordAutocomplete,
1151
+ severity: "warning",
1152
+ message: "Password inputs should specify an autoComplete value.",
1153
+ rationale: "Without an explicit autocomplete hint, password managers and browsers cannot reliably tell a sign-in field apart from a password-creation field.",
1154
+ suggestions: [
1155
+ {
1156
+ title: 'Set autoComplete="current-password"',
1157
+ description: "Use this for sign-in forms."
1158
+ },
1159
+ {
1160
+ title: 'Set autoComplete="new-password"',
1161
+ description: "Use this for sign-up / change-password forms."
1162
+ }
1163
+ ]
1164
+ });
1165
+ },
1166
+ requiredReadOnlyConflict() {
1167
+ return accessibilityFact({
1168
+ code: DiagnosticCode4.A11yInputRequiredReadOnlyConflict,
1169
+ severity: "warning",
1170
+ message: "The required and readOnly attributes are both present. A read-only field cannot satisfy required validation through user interaction.",
1171
+ rationale: "This combination is valid HTML but usually indicates an unintended state. Consider using disabled instead of readOnly, or only applying required when the field is editable."
1172
+ });
1173
+ }
1174
+ };
1175
+
1176
+ // ../../lib/contract/src/diagnostics/slot.ts
1177
+ import { DiagnosticCategory as DiagnosticCategory5, DiagnosticCode as DiagnosticCode5 } from "./_shared/diagnostics.js";
1043
1178
  var SlotDiagnostics = {
1044
1179
  exclusive(name) {
1045
1180
  return {
1046
- code: DiagnosticCode4.SlotExclusive,
1047
- category: DiagnosticCategory4.Contract,
1181
+ code: DiagnosticCode5.SlotExclusive,
1182
+ category: DiagnosticCategory5.Contract,
1048
1183
  component: name,
1049
1184
  message: `${name}: "as" and "asChild" are mutually exclusive`
1050
1185
  };
1051
1186
  },
1052
1187
  singleChildRequired(name, elementTerm) {
1053
1188
  return {
1054
- code: DiagnosticCode4.SlotSingleChild,
1055
- category: DiagnosticCategory4.Contract,
1189
+ code: DiagnosticCode5.SlotSingleChild,
1190
+ category: DiagnosticCategory5.Contract,
1056
1191
  component: name,
1057
1192
  message: `${name}: asChild requires a ${elementTerm} child`
1058
1193
  };
1059
1194
  },
1060
1195
  singleChildExceeded(name, elementTerm, count) {
1061
1196
  return {
1062
- code: DiagnosticCode4.SlotSingleChild,
1063
- category: DiagnosticCategory4.Contract,
1197
+ code: DiagnosticCode5.SlotSingleChild,
1198
+ category: DiagnosticCategory5.Contract,
1064
1199
  component: name,
1065
1200
  message: `${name}: asChild requires exactly one ${elementTerm} child, got ${count}`
1066
1201
  };
@@ -1068,16 +1203,16 @@ var SlotDiagnostics = {
1068
1203
  discardedChildren(name, elementTerm, count) {
1069
1204
  const suffix = count === 1 ? "" : "ren";
1070
1205
  return {
1071
- code: DiagnosticCode4.SlotDiscardedChildren,
1072
- category: DiagnosticCategory4.Contract,
1206
+ code: DiagnosticCode5.SlotDiscardedChildren,
1207
+ category: DiagnosticCategory5.Contract,
1073
1208
  component: name,
1074
1209
  message: `${name}: asChild discarded ${count} non-element child${suffix} \u2014 only ${elementTerm}s are valid asChild children.`
1075
1210
  };
1076
1211
  },
1077
1212
  renderFnRequired(name, received) {
1078
1213
  return {
1079
- code: DiagnosticCode4.SlotRenderFn,
1080
- category: DiagnosticCategory4.Contract,
1214
+ code: DiagnosticCode5.SlotRenderFn,
1215
+ category: DiagnosticCategory5.Contract,
1081
1216
  component: name,
1082
1217
  message: `${name}: asChild requires a render function as children, got ${received}`
1083
1218
  };
@@ -1106,8 +1241,132 @@ var InvariantBase = class {
1106
1241
  }
1107
1242
  };
1108
1243
 
1109
- // ../../lib/contract/src/aria/polymorphic-validator.ts
1244
+ // ../../lib/contract/src/aria/spec/roles/required-properties.ts
1245
+ var REQUIRED_ARIA_PROPERTIES = {
1246
+ combobox: ["aria-expanded"],
1247
+ option: ["aria-selected"],
1248
+ slider: ["aria-valuenow"],
1249
+ scrollbar: ["aria-controls", "aria-valuenow"],
1250
+ spinbutton: ["aria-valuenow"]
1251
+ };
1252
+
1253
+ // ../../lib/contract/src/aria/spec/roles/name-required.ts
1254
+ var NAME_REQUIRED_ROLES = /* @__PURE__ */ new Set(["img"]);
1255
+
1256
+ // ../../lib/contract/src/aria/spec/validators/required-properties-validator.ts
1110
1257
  var NO_VIOLATIONS = [{ valid: true }];
1258
+ function requiredAttributeByRole(roles, attribute) {
1259
+ return Object.fromEntries([...roles].map((role) => [role, [attribute]]));
1260
+ }
1261
+ function checkRequiredAttributes(requirement, { props, effectiveRole }) {
1262
+ if (!effectiveRole) return NO_VIOLATIONS;
1263
+ const requiredAttributes = requirement.attributesByRole[effectiveRole];
1264
+ if (!requiredAttributes) return NO_VIOLATIONS;
1265
+ const results = [];
1266
+ for (const attribute of requiredAttributes) {
1267
+ if (attribute in props) continue;
1268
+ results.push({
1269
+ valid: false,
1270
+ fixable: false,
1271
+ severity: "warning",
1272
+ attribute,
1273
+ diagnostic: requirement.diagnosticFor(attribute, effectiveRole)
1274
+ });
1275
+ }
1276
+ return results;
1277
+ }
1278
+
1279
+ // ../../lib/contract/src/aria/spec/roles/live-region.ts
1280
+ var LIVE_REGION_ROLES = /* @__PURE__ */ new Map([
1281
+ ["alert", "assertive"],
1282
+ ["status", "polite"],
1283
+ ["log", "polite"],
1284
+ ["timer", "off"]
1285
+ ]);
1286
+ var ATOMIC_REQUIREMENTS = requiredAttributeByRole(LIVE_REGION_ROLES.keys(), "aria-atomic");
1287
+
1288
+ // ../../lib/contract/src/aria/spec/attributes/aria-value-types.ts
1289
+ var ARIA_VALUE_TYPES = /* @__PURE__ */ new Map([
1290
+ // Boolean (true | false)
1291
+ ["aria-atomic", { kind: "boolean" }],
1292
+ ["aria-busy", { kind: "boolean" }],
1293
+ ["aria-disabled", { kind: "boolean" }],
1294
+ ["aria-expanded", { kind: "boolean" }],
1295
+ ["aria-hidden", { kind: "boolean" }],
1296
+ ["aria-modal", { kind: "boolean" }],
1297
+ ["aria-multiline", { kind: "boolean" }],
1298
+ ["aria-multiselectable", { kind: "boolean" }],
1299
+ ["aria-readonly", { kind: "boolean" }],
1300
+ ["aria-required", { kind: "boolean" }],
1301
+ ["aria-selected", { kind: "boolean" }],
1302
+ // Tristate (true | false | mixed)
1303
+ ["aria-checked", { kind: "tristate" }],
1304
+ ["aria-pressed", { kind: "tristate" }],
1305
+ // Numeric (any finite number)
1306
+ ["aria-valuenow", { kind: "number" }],
1307
+ ["aria-valuemin", { kind: "number" }],
1308
+ ["aria-valuemax", { kind: "number" }],
1309
+ // Integer with optional range
1310
+ ["aria-level", { kind: "integer", min: 1, max: 6 }],
1311
+ ["aria-posinset", { kind: "integer", min: 1 }],
1312
+ ["aria-setsize", { kind: "integer", min: -1 }],
1313
+ ["aria-rowcount", { kind: "integer", min: -1 }],
1314
+ ["aria-colcount", { kind: "integer", min: -1 }],
1315
+ ["aria-rowindex", { kind: "integer", min: 1 }],
1316
+ ["aria-colindex", { kind: "integer", min: 1 }],
1317
+ ["aria-rowspan", { kind: "integer", min: 0 }],
1318
+ ["aria-colspan", { kind: "integer", min: 0 }],
1319
+ // Enum (specific allowed tokens)
1320
+ ["aria-autocomplete", { kind: "enum", values: /* @__PURE__ */ new Set(["inline", "list", "both", "none"]) }],
1321
+ [
1322
+ "aria-current",
1323
+ {
1324
+ kind: "enum",
1325
+ values: /* @__PURE__ */ new Set(["page", "step", "location", "date", "time", "true", "false"])
1326
+ }
1327
+ ],
1328
+ [
1329
+ "aria-haspopup",
1330
+ {
1331
+ kind: "enum",
1332
+ values: /* @__PURE__ */ new Set(["false", "true", "menu", "listbox", "tree", "grid", "dialog"])
1333
+ }
1334
+ ],
1335
+ ["aria-invalid", { kind: "enum", values: /* @__PURE__ */ new Set(["grammar", "false", "spelling", "true"]) }],
1336
+ ["aria-live", { kind: "enum", values: /* @__PURE__ */ new Set(["assertive", "off", "polite"]) }],
1337
+ ["aria-orientation", { kind: "enum", values: /* @__PURE__ */ new Set(["horizontal", "vertical", "undefined"]) }],
1338
+ ["aria-sort", { kind: "enum", values: /* @__PURE__ */ new Set(["ascending", "descending", "none", "other"]) }]
1339
+ ]);
1340
+
1341
+ // ../../lib/contract/src/aria/spec/attributes/aria-relevant-tokens.ts
1342
+ var VALID_RELEVANT_TOKENS = /* @__PURE__ */ new Set([
1343
+ "additions",
1344
+ "removals",
1345
+ "text",
1346
+ "all"
1347
+ ]);
1348
+
1349
+ // ../../lib/contract/src/aria/spec/elements/heading-implicit-levels.ts
1350
+ var HEADING_IMPLICIT_LEVELS = /* @__PURE__ */ new Map([
1351
+ ["h1", 1],
1352
+ ["h2", 2],
1353
+ ["h3", 3],
1354
+ ["h4", 4],
1355
+ ["h5", 5],
1356
+ ["h6", 6]
1357
+ ]);
1358
+
1359
+ // ../../lib/contract/src/aria/spec/elements/interactive-tags.ts
1360
+ var INTERACTIVE_TAGS = /* @__PURE__ */ new Set([
1361
+ "a",
1362
+ "button",
1363
+ "input",
1364
+ "select",
1365
+ "textarea"
1366
+ ]);
1367
+
1368
+ // ../../lib/contract/src/aria/polymorphic-validator.ts
1369
+ var NO_VIOLATIONS2 = [{ valid: true }];
1111
1370
  function isIntrinsicTag(tag) {
1112
1371
  return isString(tag);
1113
1372
  }
@@ -1139,7 +1398,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1139
1398
  tag,
1140
1399
  role: "",
1141
1400
  attribute: void 0,
1142
- severity: "warning",
1401
+ severity: d.severity,
1143
1402
  phase: "evaluate"
1144
1403
  }
1145
1404
  ]
@@ -1169,6 +1428,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1169
1428
  const violations = [];
1170
1429
  const fixes = [];
1171
1430
  iterate.forEach(rules, (rule) => {
1431
+ if (isNonNull(rule.tags) && !rule.tags.includes(context.tag)) return;
1172
1432
  iterate.forEach(rule(context), (result) => {
1173
1433
  if (result.valid) return;
1174
1434
  const {
@@ -1194,7 +1454,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1194
1454
  return { violations, fixes };
1195
1455
  }
1196
1456
  static #getRules(context) {
1197
- if (_AriaPolicyEngine.#hasRole(context.props) || isNonNull(context.effectiveRole) && _AriaPolicyEngine.#LIVE_REGION_ROLES.has(context.effectiveRole)) {
1457
+ if (_AriaPolicyEngine.#hasRole(context.props) || isNonNull(context.effectiveRole) && LIVE_REGION_ROLES.has(context.effectiveRole)) {
1198
1458
  return _AriaPolicyEngine.#pipeline;
1199
1459
  }
1200
1460
  return _AriaPolicyEngine.#implicitOnlyRules;
@@ -1396,44 +1656,47 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1396
1656
  implicitRole
1397
1657
  }) {
1398
1658
  const role = props.role;
1399
- if (!implicitRole || !role || role === implicitRole) return NO_VIOLATIONS;
1659
+ if (!implicitRole || !role || role === implicitRole) return NO_VIOLATIONS2;
1400
1660
  if (isStrongImplicitRole(tag) && role === "region") {
1661
+ const diagnostic = HtmlDiagnostics.implicitRoleOverride(tag, implicitRole, role);
1401
1662
  return [
1402
1663
  {
1403
1664
  valid: false,
1404
1665
  fixable: true,
1405
- severity: "error",
1666
+ severity: diagnostic.severity,
1406
1667
  fix: _AriaPolicyEngine.#removeRole,
1407
- diagnostic: HtmlDiagnostics.implicitRoleOverride(tag, implicitRole, role)
1668
+ diagnostic
1408
1669
  }
1409
1670
  ];
1410
1671
  }
1411
- return NO_VIOLATIONS;
1672
+ return NO_VIOLATIONS2;
1412
1673
  }
1413
1674
  static #checkRedundantRole({ tag, props, implicitRole }) {
1414
1675
  const role = props.role;
1415
- if (!implicitRole || !role || role !== implicitRole) return NO_VIOLATIONS;
1676
+ if (!implicitRole || !role || role !== implicitRole) return NO_VIOLATIONS2;
1677
+ const diagnostic = HtmlDiagnostics.implicitRoleRedundant(tag, implicitRole);
1416
1678
  return [
1417
1679
  {
1418
1680
  valid: false,
1419
1681
  fixable: true,
1420
- severity: "warning",
1682
+ severity: diagnostic.severity,
1421
1683
  fix: _AriaPolicyEngine.#removeRole,
1422
- diagnostic: HtmlDiagnostics.implicitRoleRedundant(tag, implicitRole)
1684
+ diagnostic
1423
1685
  }
1424
1686
  ];
1425
1687
  }
1426
1688
  static #checkStandaloneRegion({ tag, props, implicitRole }) {
1427
1689
  const role = props.role;
1428
- if (role !== "region") return NO_VIOLATIONS;
1429
- if (!isStandaloneTag(tag)) return NO_VIOLATIONS;
1690
+ if (role !== "region") return NO_VIOLATIONS2;
1691
+ if (!isStandaloneTag(tag)) return NO_VIOLATIONS2;
1692
+ const diagnostic = HtmlDiagnostics.standaloneRegionOverride(tag, implicitRole ?? tag);
1430
1693
  return [
1431
1694
  {
1432
1695
  valid: false,
1433
1696
  fixable: true,
1434
- severity: "error",
1697
+ severity: diagnostic.severity,
1435
1698
  fix: _AriaPolicyEngine.#removeRole,
1436
- diagnostic: HtmlDiagnostics.standaloneRegionOverride(tag, implicitRole ?? tag)
1699
+ diagnostic
1437
1700
  }
1438
1701
  ];
1439
1702
  }
@@ -1442,7 +1705,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1442
1705
  props,
1443
1706
  effectiveRole
1444
1707
  }) {
1445
- if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS;
1708
+ if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS2;
1446
1709
  const results = [];
1447
1710
  iterate.forEachEntry(props, (key) => {
1448
1711
  if (!key.startsWith("aria-")) return;
@@ -1460,62 +1723,6 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1460
1723
  return results;
1461
1724
  }
1462
1725
  // ─── ARIA attribute value validation ──────────────────────────────────────
1463
- // Accepted value shapes for typed ARIA attributes.
1464
- // Attributes not in this map are unconstrained (arbitrary string values permitted).
1465
- static #ARIA_VALUE_TYPES = /* @__PURE__ */ new Map([
1466
- // Boolean (true | false)
1467
- ["aria-atomic", { kind: "boolean" }],
1468
- ["aria-busy", { kind: "boolean" }],
1469
- ["aria-disabled", { kind: "boolean" }],
1470
- ["aria-expanded", { kind: "boolean" }],
1471
- ["aria-hidden", { kind: "boolean" }],
1472
- ["aria-modal", { kind: "boolean" }],
1473
- ["aria-multiline", { kind: "boolean" }],
1474
- ["aria-multiselectable", { kind: "boolean" }],
1475
- ["aria-readonly", { kind: "boolean" }],
1476
- ["aria-required", { kind: "boolean" }],
1477
- ["aria-selected", { kind: "boolean" }],
1478
- // Tristate (true | false | mixed)
1479
- ["aria-checked", { kind: "tristate" }],
1480
- ["aria-pressed", { kind: "tristate" }],
1481
- // Numeric (any finite number)
1482
- ["aria-valuenow", { kind: "number" }],
1483
- ["aria-valuemin", { kind: "number" }],
1484
- ["aria-valuemax", { kind: "number" }],
1485
- // Integer with optional range
1486
- ["aria-level", { kind: "integer", min: 1, max: 6 }],
1487
- ["aria-posinset", { kind: "integer", min: 1 }],
1488
- ["aria-setsize", { kind: "integer", min: -1 }],
1489
- ["aria-rowcount", { kind: "integer", min: -1 }],
1490
- ["aria-colcount", { kind: "integer", min: -1 }],
1491
- ["aria-rowindex", { kind: "integer", min: 1 }],
1492
- ["aria-colindex", { kind: "integer", min: 1 }],
1493
- ["aria-rowspan", { kind: "integer", min: 0 }],
1494
- ["aria-colspan", { kind: "integer", min: 0 }],
1495
- // Enum (specific allowed tokens)
1496
- ["aria-autocomplete", { kind: "enum", values: /* @__PURE__ */ new Set(["inline", "list", "both", "none"]) }],
1497
- [
1498
- "aria-current",
1499
- {
1500
- kind: "enum",
1501
- values: /* @__PURE__ */ new Set(["page", "step", "location", "date", "time", "true", "false"])
1502
- }
1503
- ],
1504
- [
1505
- "aria-haspopup",
1506
- {
1507
- kind: "enum",
1508
- values: /* @__PURE__ */ new Set(["false", "true", "menu", "listbox", "tree", "grid", "dialog"])
1509
- }
1510
- ],
1511
- ["aria-invalid", { kind: "enum", values: /* @__PURE__ */ new Set(["grammar", "false", "spelling", "true"]) }],
1512
- ["aria-live", { kind: "enum", values: /* @__PURE__ */ new Set(["assertive", "off", "polite"]) }],
1513
- [
1514
- "aria-orientation",
1515
- { kind: "enum", values: /* @__PURE__ */ new Set(["horizontal", "vertical", "undefined"]) }
1516
- ],
1517
- ["aria-sort", { kind: "enum", values: /* @__PURE__ */ new Set(["ascending", "descending", "none", "other"]) }]
1518
- ]);
1519
1726
  static #isValidAriaValue(value, type) {
1520
1727
  switch (type.kind) {
1521
1728
  case "boolean":
@@ -1560,11 +1767,11 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1560
1767
  }
1561
1768
  }
1562
1769
  static #checkAriaAttributeValues({ props, effectiveRole }) {
1563
- if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS;
1770
+ if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS2;
1564
1771
  const results = [];
1565
1772
  iterate.forEachEntry(props, (key, value) => {
1566
1773
  if (!key.startsWith("aria-")) return;
1567
- const type = _AriaPolicyEngine.#ARIA_VALUE_TYPES.get(key);
1774
+ const type = ARIA_VALUE_TYPES.get(key);
1568
1775
  if (!isNonNull(type)) return;
1569
1776
  if (_AriaPolicyEngine.#isValidAriaValue(value, type)) return;
1570
1777
  results.push({
@@ -1583,26 +1790,18 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1583
1790
  return results;
1584
1791
  }
1585
1792
  // ─── Heading implicit level ────────────────────────────────────────────────
1586
- static #HEADING_IMPLICIT_LEVELS = /* @__PURE__ */ new Map([
1587
- ["h1", 1],
1588
- ["h2", 2],
1589
- ["h3", 3],
1590
- ["h4", 4],
1591
- ["h5", 5],
1592
- ["h6", 6]
1593
- ]);
1594
1793
  static #checkRedundantAriaLevel({
1595
1794
  tag,
1596
1795
  props,
1597
1796
  effectiveRole
1598
1797
  }) {
1599
- if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS;
1600
- const implicitLevel = _AriaPolicyEngine.#HEADING_IMPLICIT_LEVELS.get(tag);
1601
- if (!isNonNull(implicitLevel)) return NO_VIOLATIONS;
1798
+ if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS2;
1799
+ const implicitLevel = HEADING_IMPLICIT_LEVELS.get(tag);
1800
+ if (!isNonNull(implicitLevel)) return NO_VIOLATIONS2;
1602
1801
  const raw = props["aria-level"];
1603
- if (!isNonNull(raw)) return NO_VIOLATIONS;
1802
+ if (!isNonNull(raw)) return NO_VIOLATIONS2;
1604
1803
  const n = typeof raw === "number" ? raw : typeof raw === "string" ? parseInt(raw, 10) : NaN;
1605
- if (!Number.isFinite(n) || n !== implicitLevel) return NO_VIOLATIONS;
1804
+ if (!Number.isFinite(n) || n !== implicitLevel) return NO_VIOLATIONS2;
1606
1805
  return [
1607
1806
  {
1608
1807
  valid: false,
@@ -1615,20 +1814,14 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1615
1814
  ];
1616
1815
  }
1617
1816
  // ─── Name-required roles ───────────────────────────────────────────────────
1618
- // Roles that always require an accessible name per WAI-ARIA APG.
1619
- // Dialog and landmark names are enforced via contracts (ariaContract) rather than
1620
- // the built-in pipeline so consumers can opt in; img is built in because role=img
1621
- // on any element (including bare <img>) is definitionally useless without a name.
1622
- static #NAME_REQUIRED_ROLES = /* @__PURE__ */ new Set(["img"]);
1623
1817
  static #checkNameRequiredRoles({
1624
1818
  tag,
1625
1819
  props,
1626
1820
  effectiveRole
1627
1821
  }) {
1628
- if (!effectiveRole || !_AriaPolicyEngine.#NAME_REQUIRED_ROLES.has(effectiveRole))
1629
- return NO_VIOLATIONS;
1630
- if ("aria-label" in props || "aria-labelledby" in props) return NO_VIOLATIONS;
1631
- if (tag === "img" && typeof props.alt === "string" && props.alt.length > 0) return NO_VIOLATIONS;
1822
+ if (!effectiveRole || !NAME_REQUIRED_ROLES.has(effectiveRole)) return NO_VIOLATIONS2;
1823
+ if ("aria-label" in props || "aria-labelledby" in props) return NO_VIOLATIONS2;
1824
+ if (tag === "img" && typeof props.alt === "string" && props.alt.length > 0) return NO_VIOLATIONS2;
1632
1825
  return [
1633
1826
  {
1634
1827
  valid: false,
@@ -1638,51 +1831,21 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1638
1831
  }
1639
1832
  ];
1640
1833
  }
1641
- // WAI-ARIA 1.2 required states and properties, keyed by role.
1642
- // Source: https://www.w3.org/TR/wai-aria-1.2/#requiredState
1643
- static #REQUIRED_PROPERTIES = /* @__PURE__ */ new Map([
1644
- ["combobox", ["aria-expanded"]],
1645
- ["option", ["aria-selected"]],
1646
- ["slider", ["aria-valuenow"]],
1647
- ["scrollbar", ["aria-controls", "aria-valuenow"]],
1648
- ["spinbutton", ["aria-valuenow"]]
1649
- ]);
1650
- static #checkRequiredAriaProperties({
1651
- props,
1652
- effectiveRole
1653
- }) {
1654
- if (!effectiveRole) return NO_VIOLATIONS;
1655
- const required = _AriaPolicyEngine.#REQUIRED_PROPERTIES.get(effectiveRole);
1656
- if (!isNonNull(required)) return NO_VIOLATIONS;
1657
- const results = [];
1658
- iterate.forEach(required, (attr) => {
1659
- if (attr in props) return;
1660
- results.push({
1661
- valid: false,
1662
- fixable: false,
1663
- severity: "warning",
1664
- attribute: attr,
1665
- diagnostic: AriaDiagnostics.requiredProperty(attr, effectiveRole)
1666
- });
1667
- });
1668
- return results;
1834
+ static #requiredAriaPropertiesRule = {
1835
+ attributesByRole: REQUIRED_ARIA_PROPERTIES,
1836
+ diagnosticFor: (attribute, role) => AriaDiagnostics.requiredProperty(attribute, role)
1837
+ };
1838
+ static #checkRequiredAriaProperties(context) {
1839
+ return checkRequiredAttributes(_AriaPolicyEngine.#requiredAriaPropertiesRule, context);
1669
1840
  }
1670
- // Natively interactive HTML elements — always keyboard-reachable unless explicitly disabled.
1671
- static #INTERACTIVE_TAGS = /* @__PURE__ */ new Set([
1672
- "a",
1673
- "button",
1674
- "input",
1675
- "select",
1676
- "textarea"
1677
- ]);
1678
1841
  // WAI-ARIA 1.2 §6.6: aria-hidden="true" must not be placed on focusable elements.
1679
1842
  static #checkAriaHiddenOnFocusable({ tag, props }) {
1680
- if (props["aria-hidden"] !== "true" && props["aria-hidden"] !== true) return NO_VIOLATIONS;
1681
- const isInteractive = _AriaPolicyEngine.#INTERACTIVE_TAGS.has(tag);
1843
+ if (props["aria-hidden"] !== "true" && props["aria-hidden"] !== true) return NO_VIOLATIONS2;
1844
+ const isInteractive = INTERACTIVE_TAGS.has(tag);
1682
1845
  if (!isInteractive) {
1683
1846
  const tabindex = props.tabindex;
1684
1847
  const n = typeof tabindex === "number" ? tabindex : typeof tabindex === "string" ? parseInt(tabindex, 10) : NaN;
1685
- if (!Number.isFinite(n) || n < 0) return NO_VIOLATIONS;
1848
+ if (!Number.isFinite(n) || n < 0) return NO_VIOLATIONS2;
1686
1849
  }
1687
1850
  return [
1688
1851
  {
@@ -1701,7 +1864,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1701
1864
  props,
1702
1865
  effectiveRole
1703
1866
  }) {
1704
- if (effectiveRole !== "none" && effectiveRole !== "presentation") return NO_VIOLATIONS;
1867
+ if (effectiveRole !== "none" && effectiveRole !== "presentation") return NO_VIOLATIONS2;
1705
1868
  const results = [];
1706
1869
  iterate.forEachEntry(props, (key) => {
1707
1870
  if (!key.startsWith("aria-")) return;
@@ -1717,18 +1880,11 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1717
1880
  });
1718
1881
  return results;
1719
1882
  }
1720
- // WAI-ARIA live region roles and their implied aria-live politeness values.
1721
- static #LIVE_REGION_ROLES = /* @__PURE__ */ new Map([
1722
- ["alert", "assertive"],
1723
- ["status", "polite"],
1724
- ["log", "polite"],
1725
- ["timer", "off"]
1726
- ]);
1727
1883
  static #checkMissingLiveRegion({ effectiveRole, props }) {
1728
- if (!effectiveRole) return NO_VIOLATIONS;
1729
- const impliedLive = _AriaPolicyEngine.#LIVE_REGION_ROLES.get(effectiveRole);
1730
- if (!impliedLive) return NO_VIOLATIONS;
1731
- if ("aria-live" in props) return NO_VIOLATIONS;
1884
+ if (!effectiveRole) return NO_VIOLATIONS2;
1885
+ const impliedLive = LIVE_REGION_ROLES.get(effectiveRole);
1886
+ if (!impliedLive) return NO_VIOLATIONS2;
1887
+ if ("aria-live" in props) return NO_VIOLATIONS2;
1732
1888
  const injectLive = {
1733
1889
  kind: `injectLive:${effectiveRole}`,
1734
1890
  apply: (ctx) => ({
@@ -1747,20 +1903,13 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1747
1903
  }
1748
1904
  ];
1749
1905
  }
1750
- static #checkMissingAtomic({ effectiveRole, props }) {
1751
- if (!effectiveRole || !_AriaPolicyEngine.#LIVE_REGION_ROLES.has(effectiveRole))
1752
- return NO_VIOLATIONS;
1753
- if ("aria-atomic" in props) return NO_VIOLATIONS;
1754
- return [
1755
- {
1756
- valid: false,
1757
- fixable: false,
1758
- severity: "warning",
1759
- diagnostic: AriaDiagnostics.missingAtomic(effectiveRole)
1760
- }
1761
- ];
1906
+ static #missingAtomicRule = {
1907
+ attributesByRole: ATOMIC_REQUIREMENTS,
1908
+ diagnosticFor: (_attribute, role) => AriaDiagnostics.missingAtomic(role)
1909
+ };
1910
+ static #checkMissingAtomic(context) {
1911
+ return checkRequiredAttributes(_AriaPolicyEngine.#missingAtomicRule, context);
1762
1912
  }
1763
- static #VALID_RELEVANT_TOKENS = /* @__PURE__ */ new Set(["additions", "removals", "text", "all"]);
1764
1913
  // Custom fix rules passed via `options.rules` must be pure functions of (tag, props) — the cache
1765
1914
  // replays stored fixes against new prop objects, so fixes that close over external state will
1766
1915
  // produce inconsistent results on cache hits.
@@ -1774,10 +1923,10 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1774
1923
  };
1775
1924
  static #checkInvalidAriaRelevant({ props }) {
1776
1925
  const relevant = props["aria-relevant"];
1777
- if (relevant === void 0) return NO_VIOLATIONS;
1778
- if (typeof relevant !== "string") return NO_VIOLATIONS;
1926
+ if (relevant === void 0) return NO_VIOLATIONS2;
1927
+ if (typeof relevant !== "string") return NO_VIOLATIONS2;
1779
1928
  const tokens = relevant.trim().split(/\s+/);
1780
- const invalid = tokens.filter((t) => !_AriaPolicyEngine.#VALID_RELEVANT_TOKENS.has(t));
1929
+ const invalid = tokens.filter((t) => !VALID_RELEVANT_TOKENS.has(t));
1781
1930
  if (invalid.length > 0) {
1782
1931
  return [
1783
1932
  {
@@ -1802,7 +1951,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1802
1951
  }
1803
1952
  ];
1804
1953
  }
1805
- return NO_VIOLATIONS;
1954
+ return NO_VIOLATIONS2;
1806
1955
  }
1807
1956
  };
1808
1957
 
@@ -2122,6 +2271,425 @@ var readonlyProps = ({
2122
2271
  // ../core/src/html/evaluators.ts
2123
2272
  import { warnDiagnostics as warnDiagnostics2 } from "./_shared/diagnostics.js";
2124
2273
 
2274
+ // ../core/src/html/spec/vocabulary/input.ts
2275
+ var TEXT_INPUT_TYPES = ["text", "search", "url", "tel", "email", "password"];
2276
+ var NUMERIC_INPUT_TYPES = [
2277
+ "number",
2278
+ "range",
2279
+ "date",
2280
+ "month",
2281
+ "week",
2282
+ "time",
2283
+ "datetime-local"
2284
+ ];
2285
+ var HTML_INPUT_TYPES = /* @__PURE__ */ new Set([
2286
+ ...TEXT_INPUT_TYPES,
2287
+ ...NUMERIC_INPUT_TYPES,
2288
+ "checkbox",
2289
+ "radio",
2290
+ "file",
2291
+ "color",
2292
+ "hidden",
2293
+ "button",
2294
+ "submit",
2295
+ "reset",
2296
+ "image"
2297
+ ]);
2298
+
2299
+ // ../core/src/html/spec/attributes/input.ts
2300
+ var INPUT_ATTRIBUTE_TYPE_POLICIES = [
2301
+ { attribute: "checked", allowedTypes: ["checkbox", "radio"] },
2302
+ { attribute: "multiple", allowedTypes: ["email", "file"] },
2303
+ { attribute: "maxLength", allowedTypes: TEXT_INPUT_TYPES },
2304
+ { attribute: "minLength", allowedTypes: TEXT_INPUT_TYPES },
2305
+ { attribute: "pattern", allowedTypes: TEXT_INPUT_TYPES },
2306
+ { attribute: "min", allowedTypes: NUMERIC_INPUT_TYPES },
2307
+ { attribute: "max", allowedTypes: NUMERIC_INPUT_TYPES },
2308
+ { attribute: "step", allowedTypes: NUMERIC_INPUT_TYPES },
2309
+ { attribute: "accept", allowedTypes: ["file"] },
2310
+ { attribute: "capture", allowedTypes: ["file"] },
2311
+ { attribute: "size", allowedTypes: TEXT_INPUT_TYPES },
2312
+ { attribute: "alt", allowedTypes: ["image"] },
2313
+ { attribute: "height", allowedTypes: ["image"] },
2314
+ { attribute: "width", allowedTypes: ["image"] }
2315
+ ];
2316
+
2317
+ // ../core/src/html/spec/constraints/input.ts
2318
+ var REQUIRED_READONLY_CONFLICT = {
2319
+ props: ["required", "readOnly"],
2320
+ diagnostic: () => InputAccessibilityDiagnostics.requiredReadOnlyConflict()
2321
+ };
2322
+ var INPUT_MUTUALLY_EXCLUSIVE_POLICIES = [
2323
+ REQUIRED_READONLY_CONFLICT
2324
+ ];
2325
+
2326
+ // ../core/src/html/spec/validators/attribute-type-validator.ts
2327
+ var DEFAULT_INPUT_TYPE = "text";
2328
+ function omit(props, key) {
2329
+ const next = { ...props };
2330
+ delete next[key];
2331
+ return next;
2332
+ }
2333
+ function removeAttributeFix(attribute) {
2334
+ return {
2335
+ kind: `removeAttribute:${attribute}`,
2336
+ apply: ({ props }) => {
2337
+ if (!(attribute in props)) return { applied: false, next: props };
2338
+ return { applied: true, next: omit(props, attribute), previous: props };
2339
+ }
2340
+ };
2341
+ }
2342
+ function createInputAttributeTypeRule({
2343
+ attribute,
2344
+ allowedTypes
2345
+ }) {
2346
+ const rule = ({ tag, props }) => {
2347
+ if (tag !== "input" || !(attribute in props)) return [];
2348
+ const type = typeof props.type === "string" ? props.type : DEFAULT_INPUT_TYPE;
2349
+ if (allowedTypes.includes(type)) return [];
2350
+ const diagnostic = HtmlDiagnostics.input.attributeIgnoredForType(attribute, type, allowedTypes);
2351
+ return [
2352
+ {
2353
+ valid: false,
2354
+ fixable: true,
2355
+ severity: diagnostic.severity,
2356
+ fix: removeAttributeFix(attribute),
2357
+ diagnostic
2358
+ }
2359
+ ];
2360
+ };
2361
+ return Object.assign(rule, { readsProps: ["type", attribute], tags: ["input"] });
2362
+ }
2363
+
2364
+ // ../core/src/html/spec/validators/mutually-exclusive-validator.ts
2365
+ function createMutuallyExclusiveRule({
2366
+ props: conflictingProps,
2367
+ diagnostic: createDiagnostic
2368
+ }) {
2369
+ const [first, second] = conflictingProps;
2370
+ const rule = ({ tag, props }) => {
2371
+ if (tag !== "input" || !props[first] || !props[second]) return [];
2372
+ const diagnostic = createDiagnostic();
2373
+ return [{ valid: false, fixable: false, severity: diagnostic.severity, diagnostic }];
2374
+ };
2375
+ return Object.assign(rule, { readsProps: conflictingProps, tags: ["input"] });
2376
+ }
2377
+
2378
+ // ../core/src/html/input-rules.ts
2379
+ var policyByAttribute = Object.fromEntries(
2380
+ INPUT_ATTRIBUTE_TYPE_POLICIES.map((policy) => [policy.attribute, policy])
2381
+ );
2382
+ function policyFor(attribute) {
2383
+ return policyByAttribute[attribute];
2384
+ }
2385
+ var supportedInputTypeRule = Object.assign(
2386
+ ({ tag, props }) => {
2387
+ if (tag !== "input" || typeof props.type !== "string") return [];
2388
+ const type = props.type;
2389
+ if (HTML_INPUT_TYPES.has(type)) return [];
2390
+ const diagnostic = HtmlDiagnostics.input.unsupportedType(type);
2391
+ return [
2392
+ {
2393
+ valid: false,
2394
+ fixable: false,
2395
+ severity: diagnostic.severity,
2396
+ diagnostic
2397
+ }
2398
+ ];
2399
+ },
2400
+ { readsProps: ["type"], tags: ["input"] }
2401
+ );
2402
+ var checkedRequiresCheckableTypeRule = createInputAttributeTypeRule(policyFor("checked"));
2403
+ var multipleRequiresSupportedTypeRule = createInputAttributeTypeRule(policyFor("multiple"));
2404
+ var maxLengthRequiresTextTypeRule = createInputAttributeTypeRule(policyFor("maxLength"));
2405
+ var minLengthRequiresTextTypeRule = createInputAttributeTypeRule(policyFor("minLength"));
2406
+ var patternRequiresTextTypeRule = createInputAttributeTypeRule(policyFor("pattern"));
2407
+ var minRequiresNumericTypeRule = createInputAttributeTypeRule(policyFor("min"));
2408
+ var maxRequiresNumericTypeRule = createInputAttributeTypeRule(policyFor("max"));
2409
+ var stepRequiresNumericTypeRule = createInputAttributeTypeRule(policyFor("step"));
2410
+ var acceptRequiresFileTypeRule = createInputAttributeTypeRule(policyFor("accept"));
2411
+ var captureRequiresFileTypeRule = createInputAttributeTypeRule(policyFor("capture"));
2412
+ var sizeRequiresTextTypeRule = createInputAttributeTypeRule(policyFor("size"));
2413
+ var altRequiresImageTypeRule = createInputAttributeTypeRule(policyFor("alt"));
2414
+ var heightRequiresImageTypeRule = createInputAttributeTypeRule(policyFor("height"));
2415
+ var widthRequiresImageTypeRule = createInputAttributeTypeRule(policyFor("width"));
2416
+ var inputAccessibleNameRule = Object.assign(
2417
+ ({ tag, props }) => {
2418
+ if (tag !== "input" || props.type === "hidden") return [];
2419
+ if ("aria-label" in props || "aria-labelledby" in props) return [];
2420
+ const hasPlaceholder = typeof props.placeholder === "string" && props.placeholder.length > 0;
2421
+ const diagnostic = hasPlaceholder ? InputAccessibilityDiagnostics.placeholderIsNotLabel() : InputAccessibilityDiagnostics.missingAccessibleName();
2422
+ return [{ valid: false, fixable: false, severity: diagnostic.severity, diagnostic }];
2423
+ },
2424
+ {
2425
+ readsProps: ["type", "aria-label", "aria-labelledby", "placeholder"],
2426
+ tags: ["input"]
2427
+ }
2428
+ );
2429
+ var PASSWORD_AUTOCOMPLETE_VALUES = ["current-password", "new-password"];
2430
+ var passwordAutocompleteRule = Object.assign(
2431
+ ({ tag, props }) => {
2432
+ if (tag !== "input" || props.type !== "password") return [];
2433
+ const autoComplete = props.autoComplete;
2434
+ const tokens = typeof autoComplete === "string" ? autoComplete.split(" ") : [];
2435
+ if (PASSWORD_AUTOCOMPLETE_VALUES.some((value) => tokens.includes(value))) return [];
2436
+ const diagnostic = InputAccessibilityDiagnostics.passwordMissingAutocomplete();
2437
+ return [{ valid: false, fixable: false, severity: diagnostic.severity, diagnostic }];
2438
+ },
2439
+ { readsProps: ["type", "autoComplete"], tags: ["input"] }
2440
+ );
2441
+ var requiredReadOnlyConflictRule = createMutuallyExclusiveRule(REQUIRED_READONLY_CONFLICT);
2442
+ var INPUT_RULES = [
2443
+ supportedInputTypeRule,
2444
+ checkedRequiresCheckableTypeRule,
2445
+ multipleRequiresSupportedTypeRule,
2446
+ maxLengthRequiresTextTypeRule,
2447
+ minLengthRequiresTextTypeRule,
2448
+ patternRequiresTextTypeRule,
2449
+ minRequiresNumericTypeRule,
2450
+ maxRequiresNumericTypeRule,
2451
+ stepRequiresNumericTypeRule,
2452
+ acceptRequiresFileTypeRule,
2453
+ captureRequiresFileTypeRule,
2454
+ sizeRequiresTextTypeRule,
2455
+ altRequiresImageTypeRule,
2456
+ heightRequiresImageTypeRule,
2457
+ widthRequiresImageTypeRule,
2458
+ inputAccessibleNameRule,
2459
+ passwordAutocompleteRule,
2460
+ requiredReadOnlyConflictRule
2461
+ ];
2462
+
2463
+ // ../core/src/html/spec/types.ts
2464
+ function definePropRolePolicy(prop, map2, fallback) {
2465
+ return { kind: "byProp", prop, map: map2, fallback };
2466
+ }
2467
+ function resolveAllowedRoles(spec, props) {
2468
+ const policy = spec.allowedRoles;
2469
+ if (!policy) return void 0;
2470
+ switch (policy.kind) {
2471
+ case "fixed":
2472
+ return policy.roles;
2473
+ case "byProp": {
2474
+ const value = typeof props[policy.prop] === "string" ? props[policy.prop] : policy.fallback;
2475
+ return policy.map[value];
2476
+ }
2477
+ case "dynamic":
2478
+ return policy.resolve({ props });
2479
+ }
2480
+ }
2481
+
2482
+ // ../core/src/html/spec/roles/input.ts
2483
+ var ALLOWED_INPUT_ROLES = {
2484
+ checkbox: ["menuitemcheckbox", "option", "switch", "button"],
2485
+ radio: ["menuitemradio"],
2486
+ range: [],
2487
+ number: [],
2488
+ search: ["combobox"],
2489
+ text: ["combobox", "searchbox", "spinbutton"],
2490
+ email: ["combobox"],
2491
+ tel: ["combobox"],
2492
+ url: ["combobox"],
2493
+ button: [
2494
+ "link",
2495
+ "menuitem",
2496
+ "menuitemcheckbox",
2497
+ "menuitemradio",
2498
+ "option",
2499
+ "radio",
2500
+ "switch",
2501
+ "tab"
2502
+ ],
2503
+ submit: [
2504
+ "link",
2505
+ "menuitem",
2506
+ "menuitemcheckbox",
2507
+ "menuitemradio",
2508
+ "option",
2509
+ "radio",
2510
+ "switch",
2511
+ "tab"
2512
+ ],
2513
+ reset: [
2514
+ "link",
2515
+ "menuitem",
2516
+ "menuitemcheckbox",
2517
+ "menuitemradio",
2518
+ "option",
2519
+ "radio",
2520
+ "switch",
2521
+ "tab"
2522
+ ],
2523
+ image: [
2524
+ "link",
2525
+ "menuitem",
2526
+ "menuitemcheckbox",
2527
+ "menuitemradio",
2528
+ "option",
2529
+ "radio",
2530
+ "switch",
2531
+ "tab"
2532
+ ],
2533
+ hidden: []
2534
+ };
2535
+
2536
+ // ../core/src/html/spec/elements/input.ts
2537
+ var inputElementSpec = {
2538
+ tag: "input",
2539
+ allowedRoles: definePropRolePolicy("type", ALLOWED_INPUT_ROLES, "text"),
2540
+ attributes: INPUT_ATTRIBUTE_TYPE_POLICIES,
2541
+ mutuallyExclusive: INPUT_MUTUALLY_EXCLUSIVE_POLICIES
2542
+ };
2543
+
2544
+ // ../core/src/html/spec/roles/img.ts
2545
+ var IMG_NAMED_ROLES = [
2546
+ "button",
2547
+ "checkbox",
2548
+ "link",
2549
+ "menuitem",
2550
+ "menuitemcheckbox",
2551
+ "menuitemradio",
2552
+ "option",
2553
+ "progressbar",
2554
+ "scrollbar",
2555
+ "separator",
2556
+ "slider",
2557
+ "switch",
2558
+ "tab",
2559
+ "treeitem"
2560
+ ];
2561
+
2562
+ // ../core/src/html/spec/elements/img.ts
2563
+ var imgElementSpec = {
2564
+ tag: "img",
2565
+ allowedRoles: {
2566
+ kind: "dynamic",
2567
+ resolve: ({ props }) => props.alt === "" ? [] : IMG_NAMED_ROLES
2568
+ }
2569
+ };
2570
+
2571
+ // ../core/src/html/spec/roles/table.ts
2572
+ var ALLOWED_TABLE_ROLES = ["grid", "treegrid"];
2573
+
2574
+ // ../core/src/html/spec/elements/table.ts
2575
+ var tableElementSpec = {
2576
+ tag: "table",
2577
+ allowedRoles: { kind: "fixed", roles: ALLOWED_TABLE_ROLES }
2578
+ };
2579
+
2580
+ // ../core/src/html/role-restrictions.ts
2581
+ var ALLOWED_ROLES = {
2582
+ article: ["application", "document", "feed", "main", "none", "presentation", "region"],
2583
+ aside: ["feed", "none", "presentation", "region", "search"],
2584
+ footer: ["group", "none", "presentation"],
2585
+ header: ["group", "none", "presentation"],
2586
+ main: [],
2587
+ nav: [],
2588
+ a: [
2589
+ "button",
2590
+ "checkbox",
2591
+ "menuitem",
2592
+ "menuitemcheckbox",
2593
+ "menuitemradio",
2594
+ "option",
2595
+ "radio",
2596
+ "switch",
2597
+ "tab",
2598
+ "treeitem"
2599
+ ],
2600
+ button: [
2601
+ "checkbox",
2602
+ "link",
2603
+ "menuitem",
2604
+ "menuitemcheckbox",
2605
+ "menuitemradio",
2606
+ "option",
2607
+ "radio",
2608
+ "switch",
2609
+ "tab"
2610
+ ],
2611
+ select: ["menu"],
2612
+ h1: ["tab", "presentation", "none"],
2613
+ h2: ["tab", "presentation", "none"],
2614
+ h3: ["tab", "presentation", "none"],
2615
+ h4: ["tab", "presentation", "none"],
2616
+ h5: ["tab", "presentation", "none"],
2617
+ h6: ["tab", "presentation", "none"],
2618
+ ul: [
2619
+ "directory",
2620
+ "group",
2621
+ "listbox",
2622
+ "menu",
2623
+ "menubar",
2624
+ "radiogroup",
2625
+ "tablist",
2626
+ "toolbar",
2627
+ "tree"
2628
+ ],
2629
+ ol: [
2630
+ "directory",
2631
+ "group",
2632
+ "listbox",
2633
+ "menu",
2634
+ "menubar",
2635
+ "radiogroup",
2636
+ "tablist",
2637
+ "toolbar",
2638
+ "tree"
2639
+ ],
2640
+ li: [
2641
+ "menuitem",
2642
+ "menuitemcheckbox",
2643
+ "menuitemradio",
2644
+ "option",
2645
+ "none",
2646
+ "presentation",
2647
+ "radio",
2648
+ "separator",
2649
+ "tab",
2650
+ "treeitem"
2651
+ ],
2652
+ dialog: ["alertdialog"],
2653
+ fieldset: ["none", "presentation", "radiogroup"]
2654
+ };
2655
+ var ELEMENT_SPECS = {
2656
+ input: inputElementSpec,
2657
+ img: imgElementSpec,
2658
+ table: tableElementSpec
2659
+ };
2660
+ function getAllowedRoles(tag, props) {
2661
+ const spec = ELEMENT_SPECS[tag];
2662
+ if (spec) return resolveAllowedRoles(spec, props);
2663
+ return ALLOWED_ROLES[tag];
2664
+ }
2665
+ var removeRoleFix = {
2666
+ kind: "removeRole",
2667
+ apply: ({ props }) => {
2668
+ if (!("role" in props)) return { applied: false, next: props };
2669
+ const { role: _role, ...rest } = props;
2670
+ return { applied: true, next: rest, previous: props };
2671
+ }
2672
+ };
2673
+ var roleNotPermittedRule = Object.assign(
2674
+ ({ tag, props, implicitRole }) => {
2675
+ const role = props.role;
2676
+ if (typeof role !== "string" || role.length === 0 || role === implicitRole) return [];
2677
+ const allowed = getAllowedRoles(tag, props);
2678
+ if (allowed === void 0 || allowed.includes(role)) return [];
2679
+ const diagnostic = HtmlDiagnostics.roleNotPermitted(tag, role, allowed);
2680
+ return [
2681
+ {
2682
+ valid: false,
2683
+ fixable: true,
2684
+ severity: diagnostic.severity,
2685
+ fix: removeRoleFix,
2686
+ diagnostic
2687
+ }
2688
+ ];
2689
+ },
2690
+ { readsProps: ["role", "type", "alt"] }
2691
+ );
2692
+
2125
2693
  // ../core/src/html/aria-rules.ts
2126
2694
  var LANDMARK_TAG_SET = /* @__PURE__ */ new Set(["article", "aside", "footer", "header", "main", "nav"]);
2127
2695
  var removeLandmarkRoleOverride = {
@@ -2132,20 +2700,24 @@ var removeLandmarkRoleOverride = {
2132
2700
  return { applied: true, next: rest, previous: props };
2133
2701
  }
2134
2702
  };
2135
- function landmarkRoleRule({ tag, props, implicitRole }) {
2136
- if (!LANDMARK_TAG_SET.has(tag) || !implicitRole) return [];
2137
- const role = props.role;
2138
- if (!role || role === implicitRole) return [];
2139
- return [
2140
- {
2141
- valid: false,
2142
- fixable: true,
2143
- severity: "error",
2144
- fix: removeLandmarkRoleOverride,
2145
- diagnostic: HtmlDiagnostics.landmarkRoleOverride(tag, implicitRole, role)
2146
- }
2147
- ];
2148
- }
2703
+ var landmarkRoleRule = Object.assign(
2704
+ ({ tag, props, implicitRole }) => {
2705
+ if (!LANDMARK_TAG_SET.has(tag) || !implicitRole) return [];
2706
+ const role = props.role;
2707
+ if (!role || role === implicitRole) return [];
2708
+ const diagnostic = HtmlDiagnostics.landmarkRoleOverride(tag, implicitRole, role);
2709
+ return [
2710
+ {
2711
+ valid: false,
2712
+ fixable: true,
2713
+ severity: diagnostic.severity,
2714
+ fix: removeLandmarkRoleOverride,
2715
+ diagnostic
2716
+ }
2717
+ ];
2718
+ },
2719
+ { tags: [...LANDMARK_TAG_SET] }
2720
+ );
2149
2721
  function requireAccessibleName({ tag, props }) {
2150
2722
  if ("aria-label" in props || "aria-labelledby" in props) return [];
2151
2723
  return [
@@ -2158,11 +2730,19 @@ function requireAccessibleName({ tag, props }) {
2158
2730
  ];
2159
2731
  }
2160
2732
  var NAMED_LANDMARK_TAGS = /* @__PURE__ */ new Set(["nav", "aside"]);
2161
- function landmarkNameAdvisory(ctx) {
2162
- if (!ctx.implicitRole || !NAMED_LANDMARK_TAGS.has(ctx.tag)) return [];
2163
- return requireAccessibleName(ctx);
2164
- }
2165
- var HTML_ARIA_RULES = [landmarkRoleRule, landmarkNameAdvisory];
2733
+ var landmarkNameAdvisory = Object.assign(
2734
+ (ctx) => {
2735
+ if (!ctx.implicitRole || !NAMED_LANDMARK_TAGS.has(ctx.tag)) return [];
2736
+ return requireAccessibleName(ctx);
2737
+ },
2738
+ { tags: [...NAMED_LANDMARK_TAGS] }
2739
+ );
2740
+ var HTML_ARIA_RULES = [
2741
+ landmarkRoleRule,
2742
+ landmarkNameAdvisory,
2743
+ roleNotPermittedRule,
2744
+ ...INPUT_RULES
2745
+ ];
2166
2746
 
2167
2747
  // ../core/src/html/contracts.ts
2168
2748
  import { warnDiagnostics } from "./_shared/diagnostics.js";
@@ -2257,6 +2837,62 @@ var figureContract = contract([
2257
2837
  ]);
2258
2838
  var detailsContract = firstChildContract("summary", "summary");
2259
2839
  var fieldsetContract = firstChildContract("legend", "legend");
2840
+ var objectContract = contract([
2841
+ { name: "param", match: isTag("param") },
2842
+ { name: "content", match: isOpenContent("param") }
2843
+ ]);
2844
+ var INTERACTIVE_CONTENT_TAGS = ["a", "button", "input", "select", "textarea", "label"];
2845
+ var buttonContract = closedContract([
2846
+ { name: "content", match: isOpenContent(...INTERACTIVE_CONTENT_TAGS) }
2847
+ ]);
2848
+ var anchorContract = closedContract([
2849
+ { name: "content", match: isOpenContent(...INTERACTIVE_CONTENT_TAGS) }
2850
+ ]);
2851
+ var LABELABLE_TAGS = [
2852
+ "button",
2853
+ "input",
2854
+ "meter",
2855
+ "output",
2856
+ "progress",
2857
+ "select",
2858
+ "textarea"
2859
+ ];
2860
+ var labelContract = contract([
2861
+ { name: "control", match: isTag(...LABELABLE_TAGS), cardinality: { max: 1 } }
2862
+ ]);
2863
+ var P_BLOCKED_TAGS = [
2864
+ "address",
2865
+ "article",
2866
+ "aside",
2867
+ "blockquote",
2868
+ "details",
2869
+ "dialog",
2870
+ "div",
2871
+ "dl",
2872
+ "fieldset",
2873
+ "figure",
2874
+ "footer",
2875
+ "form",
2876
+ "h1",
2877
+ "h2",
2878
+ "h3",
2879
+ "h4",
2880
+ "h5",
2881
+ "h6",
2882
+ "header",
2883
+ "hr",
2884
+ "main",
2885
+ "nav",
2886
+ "ol",
2887
+ "p",
2888
+ "pre",
2889
+ "section",
2890
+ "table",
2891
+ "ul"
2892
+ ];
2893
+ var pContract = closedContract([
2894
+ { name: "content", match: isOpenContent(...P_BLOCKED_TAGS) }
2895
+ ]);
2260
2896
  var mediaContract = contract([
2261
2897
  { name: "source", match: isTag("source") },
2262
2898
  { name: "track", match: isTag("track") },
@@ -2311,6 +2947,11 @@ var htmlContracts = {
2311
2947
  details: detailsContract,
2312
2948
  fieldset: fieldsetContract,
2313
2949
  dialog: dialogContract,
2950
+ object: objectContract,
2951
+ button: buttonContract,
2952
+ a: anchorContract,
2953
+ label: labelContract,
2954
+ p: pContract,
2314
2955
  head: headContract,
2315
2956
  html: htmlContract
2316
2957
  };
@@ -2623,21 +3264,21 @@ function validateRenderProps(diagnostics, options, props, recipeKey) {
2623
3264
  import { throwDiagnostics } from "./_shared/diagnostics.js";
2624
3265
 
2625
3266
  // ../core/src/factory/plugin-diagnostics.ts
2626
- import { DiagnosticCategory as DiagnosticCategory5, DiagnosticCode as DiagnosticCode5 } from "./_shared/diagnostics.js";
3267
+ import { DiagnosticCategory as DiagnosticCategory6, DiagnosticCode as DiagnosticCode6 } from "./_shared/diagnostics.js";
2627
3268
  var PluginDiagnostics = {
2628
3269
  invalidShape(received) {
2629
3270
  const got = received === null ? "null" : typeof received;
2630
3271
  return {
2631
- code: DiagnosticCode5.PluginInvalidShape,
2632
- category: DiagnosticCategory5.Internal,
3272
+ code: DiagnosticCode6.PluginInvalidShape,
3273
+ category: DiagnosticCategory6.Internal,
2633
3274
  message: `[praxis-kit] Plugin factory must return an object with a 'pipeline' function. Got: ${got}.`
2634
3275
  };
2635
3276
  },
2636
3277
  pipelineReturnType(received) {
2637
3278
  const got = received === null ? "null" : Array.isArray(received) ? "array" : typeof received;
2638
3279
  return {
2639
- code: DiagnosticCode5.PluginPipelineReturnType,
2640
- category: DiagnosticCategory5.Internal,
3280
+ code: DiagnosticCode6.PluginPipelineReturnType,
3281
+ category: DiagnosticCategory6.Internal,
2641
3282
  message: `[praxis-kit] Plugin pipeline must return a string. Got: ${got}.`
2642
3283
  };
2643
3284
  }