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