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.
@@ -954,11 +954,28 @@ var ContractDiagnostics = {
954
954
 
955
955
  // ../../lib/contract/src/diagnostics/html.ts
956
956
  import { DiagnosticCategory as DiagnosticCategory3, DiagnosticCode as DiagnosticCode3 } from "../_shared/diagnostics.js";
957
+ var ATTRIBUTE_IGNORED_CODES = {
958
+ checked: DiagnosticCode3.HtmlInputCheckedIgnoredForType,
959
+ multiple: DiagnosticCode3.HtmlInputMultipleIgnoredForType,
960
+ maxLength: DiagnosticCode3.HtmlInputMaxLengthIgnoredForType,
961
+ minLength: DiagnosticCode3.HtmlInputMinLengthIgnoredForType,
962
+ pattern: DiagnosticCode3.HtmlInputPatternIgnoredForType,
963
+ min: DiagnosticCode3.HtmlInputMinIgnoredForType,
964
+ max: DiagnosticCode3.HtmlInputMaxIgnoredForType,
965
+ step: DiagnosticCode3.HtmlInputStepIgnoredForType,
966
+ accept: DiagnosticCode3.HtmlInputAcceptIgnoredForType,
967
+ capture: DiagnosticCode3.HtmlInputCaptureIgnoredForType,
968
+ size: DiagnosticCode3.HtmlInputSizeIgnoredForType,
969
+ alt: DiagnosticCode3.HtmlInputAltIgnoredForType,
970
+ height: DiagnosticCode3.HtmlInputHeightIgnoredForType,
971
+ width: DiagnosticCode3.HtmlInputWidthIgnoredForType
972
+ };
957
973
  var HtmlDiagnostics = {
958
974
  emptyRole(tag) {
959
975
  return {
960
976
  code: DiagnosticCode3.HtmlEmptyRole,
961
977
  category: DiagnosticCategory3.HTML,
978
+ severity: "warning",
962
979
  message: `<${tag}> has an explicit empty role="". Omit the attribute instead.`
963
980
  };
964
981
  },
@@ -966,6 +983,7 @@ var HtmlDiagnostics = {
966
983
  return {
967
984
  code: DiagnosticCode3.HtmlImplicitRoleRedundant,
968
985
  category: DiagnosticCategory3.HTML,
986
+ severity: "warning",
969
987
  message: `<${tag}> already has implicit role="${implicitRole}". Avoid redundant role assignment.`
970
988
  };
971
989
  },
@@ -973,6 +991,7 @@ var HtmlDiagnostics = {
973
991
  return {
974
992
  code: DiagnosticCode3.HtmlImplicitRoleOverride,
975
993
  category: DiagnosticCategory3.HTML,
994
+ severity: "error",
976
995
  message: `<${tag}> should not override its implicit role="${implicitRole}" with role="${role}".`
977
996
  };
978
997
  },
@@ -980,6 +999,7 @@ var HtmlDiagnostics = {
980
999
  return {
981
1000
  code: DiagnosticCode3.HtmlStandaloneRegionOverride,
982
1001
  category: DiagnosticCategory3.HTML,
1002
+ severity: "error",
983
1003
  message: `<${tag}> is a self-contained element with implicit role="${implicitRole}". Assigning role="region" has been removed.`
984
1004
  };
985
1005
  },
@@ -987,6 +1007,7 @@ var HtmlDiagnostics = {
987
1007
  return {
988
1008
  code: DiagnosticCode3.HtmlLandmarkRoleOverride,
989
1009
  category: DiagnosticCategory3.HTML,
1010
+ severity: "error",
990
1011
  message: `<${tag}> has a fixed landmark role="${implicitRole}". role="${role}" overrides it and confuses assistive technology. The override has been removed.`
991
1012
  };
992
1013
  },
@@ -994,34 +1015,148 @@ var HtmlDiagnostics = {
994
1015
  return {
995
1016
  code: DiagnosticCode3.HtmlInvalidChild,
996
1017
  category: DiagnosticCategory3.HTML,
1018
+ severity: "error",
997
1019
  message: `<${child}> is not a valid direct child of <${parent}>. Allowed: ${allowed}.`
998
1020
  };
1021
+ },
1022
+ roleNotPermitted(tag, role, allowedRoles) {
1023
+ const allowed = allowedRoles.length > 0 ? allowedRoles.map((r) => `"${r}"`).join(", ") : "none \u2014 no explicit role is permitted on this element";
1024
+ return {
1025
+ code: DiagnosticCode3.HtmlRoleNotPermitted,
1026
+ category: DiagnosticCategory3.HTML,
1027
+ severity: "error",
1028
+ message: `role="${role}" is not permitted on <${tag}>. Allowed alternate role(s): ${allowed}.`,
1029
+ 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.'
1030
+ };
1031
+ },
1032
+ // Reserved for <input>-specific facts (HTML3101–3199, see codes.ts) — later element families
1033
+ // (button, img, table, ...) get their own reserved block and their own namespace here.
1034
+ input: {
1035
+ unsupportedType(type) {
1036
+ return {
1037
+ code: DiagnosticCode3.HtmlInputUnsupportedType,
1038
+ category: DiagnosticCategory3.HTML,
1039
+ severity: "warning",
1040
+ message: `type="${type}" is not a value defined by the HTML specification. Browsers silently fall back to type="text".`,
1041
+ 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).',
1042
+ suggestions: [
1043
+ {
1044
+ title: "Check for a typo in the type value",
1045
+ description: `"${type}" does not match any HTML5 input type.`
1046
+ }
1047
+ ]
1048
+ };
1049
+ },
1050
+ // The user-visible problem is that the attribute is ignored — not that it "requires" a type;
1051
+ // that's the rule's internal framing, not what the browser actually does.
1052
+ attributeIgnoredForType(attribute, type, allowedTypes) {
1053
+ const allowed = allowedTypes.map((t) => `"${t}"`).join(", ");
1054
+ const code = ATTRIBUTE_IGNORED_CODES[attribute];
1055
+ if (!code) throw new Error(`No DiagnosticCode registered for input attribute "${attribute}"`);
1056
+ return {
1057
+ code,
1058
+ category: DiagnosticCategory3.HTML,
1059
+ severity: "warning",
1060
+ message: `"${attribute}" is ignored on <input type="${type}">.`,
1061
+ rationale: `"${attribute}" only has an effect when type is one of: ${allowed}. Browsers silently ignore it on other input types.`,
1062
+ suggestions: [
1063
+ {
1064
+ title: `Remove "${attribute}"`,
1065
+ description: `"${attribute}" only affects <input> when type is one of: ${allowed}.`
1066
+ }
1067
+ ]
1068
+ };
1069
+ }
999
1070
  }
1000
1071
  };
1001
1072
 
1002
- // ../../lib/contract/src/diagnostics/slot.ts
1073
+ // ../../lib/contract/src/diagnostics/input-accessibility.ts
1003
1074
  import { DiagnosticCategory as DiagnosticCategory4, DiagnosticCode as DiagnosticCode4 } from "../_shared/diagnostics.js";
1075
+ function accessibilityFact(input) {
1076
+ return { category: DiagnosticCategory4.Accessibility, ...input };
1077
+ }
1078
+ var InputAccessibilityDiagnostics = {
1079
+ missingAccessibleName() {
1080
+ return accessibilityFact({
1081
+ code: DiagnosticCode4.A11yInputMissingAccessibleName,
1082
+ severity: "warning",
1083
+ message: "This input has no accessible name. Add an associated <label>, aria-label, or aria-labelledby.",
1084
+ rationale: "Assistive technology announces a form field by its accessible name; without one, users of screen readers cannot tell what the field is for.",
1085
+ suggestions: [
1086
+ { title: "Add aria-label", description: 'Set aria-label="\u2026" directly on the input.' },
1087
+ {
1088
+ title: "Add an associated <label>",
1089
+ description: 'Wrap the input in a <label>, or point a <label for="\u2026"> at its id.'
1090
+ }
1091
+ ]
1092
+ });
1093
+ },
1094
+ placeholderIsNotLabel() {
1095
+ return accessibilityFact({
1096
+ code: DiagnosticCode4.A11yInputPlaceholderNotLabel,
1097
+ severity: "warning",
1098
+ message: "Placeholder text does not provide an accessible name. Add an associated <label>, aria-label, or aria-labelledby.",
1099
+ rationale: "Placeholder text disappears as users interact with the field and is not treated as the control's accessible name by many assistive technologies.",
1100
+ suggestions: [
1101
+ { title: "Add aria-label", description: 'Set aria-label="\u2026" directly on the input.' },
1102
+ {
1103
+ title: "Add an associated <label>",
1104
+ description: 'Wrap the input in a <label>, or point a <label for="\u2026"> at its id.'
1105
+ }
1106
+ ]
1107
+ });
1108
+ },
1109
+ passwordMissingAutocomplete() {
1110
+ return accessibilityFact({
1111
+ code: DiagnosticCode4.A11yInputPasswordAutocomplete,
1112
+ severity: "warning",
1113
+ message: "Password inputs should specify an autoComplete value.",
1114
+ rationale: "Without an explicit autocomplete hint, password managers and browsers cannot reliably tell a sign-in field apart from a password-creation field.",
1115
+ suggestions: [
1116
+ {
1117
+ title: 'Set autoComplete="current-password"',
1118
+ description: "Use this for sign-in forms."
1119
+ },
1120
+ {
1121
+ title: 'Set autoComplete="new-password"',
1122
+ description: "Use this for sign-up / change-password forms."
1123
+ }
1124
+ ]
1125
+ });
1126
+ },
1127
+ requiredReadOnlyConflict() {
1128
+ return accessibilityFact({
1129
+ code: DiagnosticCode4.A11yInputRequiredReadOnlyConflict,
1130
+ severity: "warning",
1131
+ message: "The required and readOnly attributes are both present. A read-only field cannot satisfy required validation through user interaction.",
1132
+ 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."
1133
+ });
1134
+ }
1135
+ };
1136
+
1137
+ // ../../lib/contract/src/diagnostics/slot.ts
1138
+ import { DiagnosticCategory as DiagnosticCategory5, DiagnosticCode as DiagnosticCode5 } from "../_shared/diagnostics.js";
1004
1139
  var SlotDiagnostics = {
1005
1140
  exclusive(name) {
1006
1141
  return {
1007
- code: DiagnosticCode4.SlotExclusive,
1008
- category: DiagnosticCategory4.Contract,
1142
+ code: DiagnosticCode5.SlotExclusive,
1143
+ category: DiagnosticCategory5.Contract,
1009
1144
  component: name,
1010
1145
  message: `${name}: "as" and "asChild" are mutually exclusive`
1011
1146
  };
1012
1147
  },
1013
1148
  singleChildRequired(name, elementTerm) {
1014
1149
  return {
1015
- code: DiagnosticCode4.SlotSingleChild,
1016
- category: DiagnosticCategory4.Contract,
1150
+ code: DiagnosticCode5.SlotSingleChild,
1151
+ category: DiagnosticCategory5.Contract,
1017
1152
  component: name,
1018
1153
  message: `${name}: asChild requires a ${elementTerm} child`
1019
1154
  };
1020
1155
  },
1021
1156
  singleChildExceeded(name, elementTerm, count) {
1022
1157
  return {
1023
- code: DiagnosticCode4.SlotSingleChild,
1024
- category: DiagnosticCategory4.Contract,
1158
+ code: DiagnosticCode5.SlotSingleChild,
1159
+ category: DiagnosticCategory5.Contract,
1025
1160
  component: name,
1026
1161
  message: `${name}: asChild requires exactly one ${elementTerm} child, got ${count}`
1027
1162
  };
@@ -1029,16 +1164,16 @@ var SlotDiagnostics = {
1029
1164
  discardedChildren(name, elementTerm, count) {
1030
1165
  const suffix = count === 1 ? "" : "ren";
1031
1166
  return {
1032
- code: DiagnosticCode4.SlotDiscardedChildren,
1033
- category: DiagnosticCategory4.Contract,
1167
+ code: DiagnosticCode5.SlotDiscardedChildren,
1168
+ category: DiagnosticCategory5.Contract,
1034
1169
  component: name,
1035
1170
  message: `${name}: asChild discarded ${count} non-element child${suffix} \u2014 only ${elementTerm}s are valid asChild children.`
1036
1171
  };
1037
1172
  },
1038
1173
  renderFnRequired(name, received) {
1039
1174
  return {
1040
- code: DiagnosticCode4.SlotRenderFn,
1041
- category: DiagnosticCategory4.Contract,
1175
+ code: DiagnosticCode5.SlotRenderFn,
1176
+ category: DiagnosticCategory5.Contract,
1042
1177
  component: name,
1043
1178
  message: `${name}: asChild requires a render function as children, got ${received}`
1044
1179
  };
@@ -1067,8 +1202,132 @@ var InvariantBase = class {
1067
1202
  }
1068
1203
  };
1069
1204
 
1070
- // ../../lib/contract/src/aria/polymorphic-validator.ts
1205
+ // ../../lib/contract/src/aria/spec/roles/required-properties.ts
1206
+ var REQUIRED_ARIA_PROPERTIES = {
1207
+ combobox: ["aria-expanded"],
1208
+ option: ["aria-selected"],
1209
+ slider: ["aria-valuenow"],
1210
+ scrollbar: ["aria-controls", "aria-valuenow"],
1211
+ spinbutton: ["aria-valuenow"]
1212
+ };
1213
+
1214
+ // ../../lib/contract/src/aria/spec/roles/name-required.ts
1215
+ var NAME_REQUIRED_ROLES = /* @__PURE__ */ new Set(["img"]);
1216
+
1217
+ // ../../lib/contract/src/aria/spec/validators/required-properties-validator.ts
1071
1218
  var NO_VIOLATIONS = [{ valid: true }];
1219
+ function requiredAttributeByRole(roles, attribute) {
1220
+ return Object.fromEntries([...roles].map((role) => [role, [attribute]]));
1221
+ }
1222
+ function checkRequiredAttributes(requirement, { props, effectiveRole }) {
1223
+ if (!effectiveRole) return NO_VIOLATIONS;
1224
+ const requiredAttributes = requirement.attributesByRole[effectiveRole];
1225
+ if (!requiredAttributes) return NO_VIOLATIONS;
1226
+ const results = [];
1227
+ for (const attribute of requiredAttributes) {
1228
+ if (attribute in props) continue;
1229
+ results.push({
1230
+ valid: false,
1231
+ fixable: false,
1232
+ severity: "warning",
1233
+ attribute,
1234
+ diagnostic: requirement.diagnosticFor(attribute, effectiveRole)
1235
+ });
1236
+ }
1237
+ return results;
1238
+ }
1239
+
1240
+ // ../../lib/contract/src/aria/spec/roles/live-region.ts
1241
+ var LIVE_REGION_ROLES = /* @__PURE__ */ new Map([
1242
+ ["alert", "assertive"],
1243
+ ["status", "polite"],
1244
+ ["log", "polite"],
1245
+ ["timer", "off"]
1246
+ ]);
1247
+ var ATOMIC_REQUIREMENTS = requiredAttributeByRole(LIVE_REGION_ROLES.keys(), "aria-atomic");
1248
+
1249
+ // ../../lib/contract/src/aria/spec/attributes/aria-value-types.ts
1250
+ var ARIA_VALUE_TYPES = /* @__PURE__ */ new Map([
1251
+ // Boolean (true | false)
1252
+ ["aria-atomic", { kind: "boolean" }],
1253
+ ["aria-busy", { kind: "boolean" }],
1254
+ ["aria-disabled", { kind: "boolean" }],
1255
+ ["aria-expanded", { kind: "boolean" }],
1256
+ ["aria-hidden", { kind: "boolean" }],
1257
+ ["aria-modal", { kind: "boolean" }],
1258
+ ["aria-multiline", { kind: "boolean" }],
1259
+ ["aria-multiselectable", { kind: "boolean" }],
1260
+ ["aria-readonly", { kind: "boolean" }],
1261
+ ["aria-required", { kind: "boolean" }],
1262
+ ["aria-selected", { kind: "boolean" }],
1263
+ // Tristate (true | false | mixed)
1264
+ ["aria-checked", { kind: "tristate" }],
1265
+ ["aria-pressed", { kind: "tristate" }],
1266
+ // Numeric (any finite number)
1267
+ ["aria-valuenow", { kind: "number" }],
1268
+ ["aria-valuemin", { kind: "number" }],
1269
+ ["aria-valuemax", { kind: "number" }],
1270
+ // Integer with optional range
1271
+ ["aria-level", { kind: "integer", min: 1, max: 6 }],
1272
+ ["aria-posinset", { kind: "integer", min: 1 }],
1273
+ ["aria-setsize", { kind: "integer", min: -1 }],
1274
+ ["aria-rowcount", { kind: "integer", min: -1 }],
1275
+ ["aria-colcount", { kind: "integer", min: -1 }],
1276
+ ["aria-rowindex", { kind: "integer", min: 1 }],
1277
+ ["aria-colindex", { kind: "integer", min: 1 }],
1278
+ ["aria-rowspan", { kind: "integer", min: 0 }],
1279
+ ["aria-colspan", { kind: "integer", min: 0 }],
1280
+ // Enum (specific allowed tokens)
1281
+ ["aria-autocomplete", { kind: "enum", values: /* @__PURE__ */ new Set(["inline", "list", "both", "none"]) }],
1282
+ [
1283
+ "aria-current",
1284
+ {
1285
+ kind: "enum",
1286
+ values: /* @__PURE__ */ new Set(["page", "step", "location", "date", "time", "true", "false"])
1287
+ }
1288
+ ],
1289
+ [
1290
+ "aria-haspopup",
1291
+ {
1292
+ kind: "enum",
1293
+ values: /* @__PURE__ */ new Set(["false", "true", "menu", "listbox", "tree", "grid", "dialog"])
1294
+ }
1295
+ ],
1296
+ ["aria-invalid", { kind: "enum", values: /* @__PURE__ */ new Set(["grammar", "false", "spelling", "true"]) }],
1297
+ ["aria-live", { kind: "enum", values: /* @__PURE__ */ new Set(["assertive", "off", "polite"]) }],
1298
+ ["aria-orientation", { kind: "enum", values: /* @__PURE__ */ new Set(["horizontal", "vertical", "undefined"]) }],
1299
+ ["aria-sort", { kind: "enum", values: /* @__PURE__ */ new Set(["ascending", "descending", "none", "other"]) }]
1300
+ ]);
1301
+
1302
+ // ../../lib/contract/src/aria/spec/attributes/aria-relevant-tokens.ts
1303
+ var VALID_RELEVANT_TOKENS = /* @__PURE__ */ new Set([
1304
+ "additions",
1305
+ "removals",
1306
+ "text",
1307
+ "all"
1308
+ ]);
1309
+
1310
+ // ../../lib/contract/src/aria/spec/elements/heading-implicit-levels.ts
1311
+ var HEADING_IMPLICIT_LEVELS = /* @__PURE__ */ new Map([
1312
+ ["h1", 1],
1313
+ ["h2", 2],
1314
+ ["h3", 3],
1315
+ ["h4", 4],
1316
+ ["h5", 5],
1317
+ ["h6", 6]
1318
+ ]);
1319
+
1320
+ // ../../lib/contract/src/aria/spec/elements/interactive-tags.ts
1321
+ var INTERACTIVE_TAGS = /* @__PURE__ */ new Set([
1322
+ "a",
1323
+ "button",
1324
+ "input",
1325
+ "select",
1326
+ "textarea"
1327
+ ]);
1328
+
1329
+ // ../../lib/contract/src/aria/polymorphic-validator.ts
1330
+ var NO_VIOLATIONS2 = [{ valid: true }];
1072
1331
  function isIntrinsicTag(tag) {
1073
1332
  return isString(tag);
1074
1333
  }
@@ -1100,7 +1359,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1100
1359
  tag,
1101
1360
  role: "",
1102
1361
  attribute: void 0,
1103
- severity: "warning",
1362
+ severity: d.severity,
1104
1363
  phase: "evaluate"
1105
1364
  }
1106
1365
  ]
@@ -1130,6 +1389,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1130
1389
  const violations = [];
1131
1390
  const fixes = [];
1132
1391
  iterate.forEach(rules, (rule) => {
1392
+ if (isNonNull(rule.tags) && !rule.tags.includes(context.tag)) return;
1133
1393
  iterate.forEach(rule(context), (result) => {
1134
1394
  if (result.valid) return;
1135
1395
  const {
@@ -1155,7 +1415,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1155
1415
  return { violations, fixes };
1156
1416
  }
1157
1417
  static #getRules(context) {
1158
- if (_AriaPolicyEngine.#hasRole(context.props) || isNonNull(context.effectiveRole) && _AriaPolicyEngine.#LIVE_REGION_ROLES.has(context.effectiveRole)) {
1418
+ if (_AriaPolicyEngine.#hasRole(context.props) || isNonNull(context.effectiveRole) && LIVE_REGION_ROLES.has(context.effectiveRole)) {
1159
1419
  return _AriaPolicyEngine.#pipeline;
1160
1420
  }
1161
1421
  return _AriaPolicyEngine.#implicitOnlyRules;
@@ -1357,44 +1617,47 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1357
1617
  implicitRole
1358
1618
  }) {
1359
1619
  const role = props.role;
1360
- if (!implicitRole || !role || role === implicitRole) return NO_VIOLATIONS;
1620
+ if (!implicitRole || !role || role === implicitRole) return NO_VIOLATIONS2;
1361
1621
  if (isStrongImplicitRole(tag) && role === "region") {
1622
+ const diagnostic = HtmlDiagnostics.implicitRoleOverride(tag, implicitRole, role);
1362
1623
  return [
1363
1624
  {
1364
1625
  valid: false,
1365
1626
  fixable: true,
1366
- severity: "error",
1627
+ severity: diagnostic.severity,
1367
1628
  fix: _AriaPolicyEngine.#removeRole,
1368
- diagnostic: HtmlDiagnostics.implicitRoleOverride(tag, implicitRole, role)
1629
+ diagnostic
1369
1630
  }
1370
1631
  ];
1371
1632
  }
1372
- return NO_VIOLATIONS;
1633
+ return NO_VIOLATIONS2;
1373
1634
  }
1374
1635
  static #checkRedundantRole({ tag, props, implicitRole }) {
1375
1636
  const role = props.role;
1376
- if (!implicitRole || !role || role !== implicitRole) return NO_VIOLATIONS;
1637
+ if (!implicitRole || !role || role !== implicitRole) return NO_VIOLATIONS2;
1638
+ const diagnostic = HtmlDiagnostics.implicitRoleRedundant(tag, implicitRole);
1377
1639
  return [
1378
1640
  {
1379
1641
  valid: false,
1380
1642
  fixable: true,
1381
- severity: "warning",
1643
+ severity: diagnostic.severity,
1382
1644
  fix: _AriaPolicyEngine.#removeRole,
1383
- diagnostic: HtmlDiagnostics.implicitRoleRedundant(tag, implicitRole)
1645
+ diagnostic
1384
1646
  }
1385
1647
  ];
1386
1648
  }
1387
1649
  static #checkStandaloneRegion({ tag, props, implicitRole }) {
1388
1650
  const role = props.role;
1389
- if (role !== "region") return NO_VIOLATIONS;
1390
- if (!isStandaloneTag(tag)) return NO_VIOLATIONS;
1651
+ if (role !== "region") return NO_VIOLATIONS2;
1652
+ if (!isStandaloneTag(tag)) return NO_VIOLATIONS2;
1653
+ const diagnostic = HtmlDiagnostics.standaloneRegionOverride(tag, implicitRole ?? tag);
1391
1654
  return [
1392
1655
  {
1393
1656
  valid: false,
1394
1657
  fixable: true,
1395
- severity: "error",
1658
+ severity: diagnostic.severity,
1396
1659
  fix: _AriaPolicyEngine.#removeRole,
1397
- diagnostic: HtmlDiagnostics.standaloneRegionOverride(tag, implicitRole ?? tag)
1660
+ diagnostic
1398
1661
  }
1399
1662
  ];
1400
1663
  }
@@ -1403,7 +1666,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1403
1666
  props,
1404
1667
  effectiveRole
1405
1668
  }) {
1406
- if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS;
1669
+ if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS2;
1407
1670
  const results = [];
1408
1671
  iterate.forEachEntry(props, (key) => {
1409
1672
  if (!key.startsWith("aria-")) return;
@@ -1421,62 +1684,6 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1421
1684
  return results;
1422
1685
  }
1423
1686
  // ─── ARIA attribute value validation ──────────────────────────────────────
1424
- // Accepted value shapes for typed ARIA attributes.
1425
- // Attributes not in this map are unconstrained (arbitrary string values permitted).
1426
- static #ARIA_VALUE_TYPES = /* @__PURE__ */ new Map([
1427
- // Boolean (true | false)
1428
- ["aria-atomic", { kind: "boolean" }],
1429
- ["aria-busy", { kind: "boolean" }],
1430
- ["aria-disabled", { kind: "boolean" }],
1431
- ["aria-expanded", { kind: "boolean" }],
1432
- ["aria-hidden", { kind: "boolean" }],
1433
- ["aria-modal", { kind: "boolean" }],
1434
- ["aria-multiline", { kind: "boolean" }],
1435
- ["aria-multiselectable", { kind: "boolean" }],
1436
- ["aria-readonly", { kind: "boolean" }],
1437
- ["aria-required", { kind: "boolean" }],
1438
- ["aria-selected", { kind: "boolean" }],
1439
- // Tristate (true | false | mixed)
1440
- ["aria-checked", { kind: "tristate" }],
1441
- ["aria-pressed", { kind: "tristate" }],
1442
- // Numeric (any finite number)
1443
- ["aria-valuenow", { kind: "number" }],
1444
- ["aria-valuemin", { kind: "number" }],
1445
- ["aria-valuemax", { kind: "number" }],
1446
- // Integer with optional range
1447
- ["aria-level", { kind: "integer", min: 1, max: 6 }],
1448
- ["aria-posinset", { kind: "integer", min: 1 }],
1449
- ["aria-setsize", { kind: "integer", min: -1 }],
1450
- ["aria-rowcount", { kind: "integer", min: -1 }],
1451
- ["aria-colcount", { kind: "integer", min: -1 }],
1452
- ["aria-rowindex", { kind: "integer", min: 1 }],
1453
- ["aria-colindex", { kind: "integer", min: 1 }],
1454
- ["aria-rowspan", { kind: "integer", min: 0 }],
1455
- ["aria-colspan", { kind: "integer", min: 0 }],
1456
- // Enum (specific allowed tokens)
1457
- ["aria-autocomplete", { kind: "enum", values: /* @__PURE__ */ new Set(["inline", "list", "both", "none"]) }],
1458
- [
1459
- "aria-current",
1460
- {
1461
- kind: "enum",
1462
- values: /* @__PURE__ */ new Set(["page", "step", "location", "date", "time", "true", "false"])
1463
- }
1464
- ],
1465
- [
1466
- "aria-haspopup",
1467
- {
1468
- kind: "enum",
1469
- values: /* @__PURE__ */ new Set(["false", "true", "menu", "listbox", "tree", "grid", "dialog"])
1470
- }
1471
- ],
1472
- ["aria-invalid", { kind: "enum", values: /* @__PURE__ */ new Set(["grammar", "false", "spelling", "true"]) }],
1473
- ["aria-live", { kind: "enum", values: /* @__PURE__ */ new Set(["assertive", "off", "polite"]) }],
1474
- [
1475
- "aria-orientation",
1476
- { kind: "enum", values: /* @__PURE__ */ new Set(["horizontal", "vertical", "undefined"]) }
1477
- ],
1478
- ["aria-sort", { kind: "enum", values: /* @__PURE__ */ new Set(["ascending", "descending", "none", "other"]) }]
1479
- ]);
1480
1687
  static #isValidAriaValue(value, type) {
1481
1688
  switch (type.kind) {
1482
1689
  case "boolean":
@@ -1521,11 +1728,11 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1521
1728
  }
1522
1729
  }
1523
1730
  static #checkAriaAttributeValues({ props, effectiveRole }) {
1524
- if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS;
1731
+ if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS2;
1525
1732
  const results = [];
1526
1733
  iterate.forEachEntry(props, (key, value) => {
1527
1734
  if (!key.startsWith("aria-")) return;
1528
- const type = _AriaPolicyEngine.#ARIA_VALUE_TYPES.get(key);
1735
+ const type = ARIA_VALUE_TYPES.get(key);
1529
1736
  if (!isNonNull(type)) return;
1530
1737
  if (_AriaPolicyEngine.#isValidAriaValue(value, type)) return;
1531
1738
  results.push({
@@ -1544,26 +1751,18 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1544
1751
  return results;
1545
1752
  }
1546
1753
  // ─── Heading implicit level ────────────────────────────────────────────────
1547
- static #HEADING_IMPLICIT_LEVELS = /* @__PURE__ */ new Map([
1548
- ["h1", 1],
1549
- ["h2", 2],
1550
- ["h3", 3],
1551
- ["h4", 4],
1552
- ["h5", 5],
1553
- ["h6", 6]
1554
- ]);
1555
1754
  static #checkRedundantAriaLevel({
1556
1755
  tag,
1557
1756
  props,
1558
1757
  effectiveRole
1559
1758
  }) {
1560
- if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS;
1561
- const implicitLevel = _AriaPolicyEngine.#HEADING_IMPLICIT_LEVELS.get(tag);
1562
- if (!isNonNull(implicitLevel)) return NO_VIOLATIONS;
1759
+ if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS2;
1760
+ const implicitLevel = HEADING_IMPLICIT_LEVELS.get(tag);
1761
+ if (!isNonNull(implicitLevel)) return NO_VIOLATIONS2;
1563
1762
  const raw = props["aria-level"];
1564
- if (!isNonNull(raw)) return NO_VIOLATIONS;
1763
+ if (!isNonNull(raw)) return NO_VIOLATIONS2;
1565
1764
  const n = typeof raw === "number" ? raw : typeof raw === "string" ? parseInt(raw, 10) : NaN;
1566
- if (!Number.isFinite(n) || n !== implicitLevel) return NO_VIOLATIONS;
1765
+ if (!Number.isFinite(n) || n !== implicitLevel) return NO_VIOLATIONS2;
1567
1766
  return [
1568
1767
  {
1569
1768
  valid: false,
@@ -1576,20 +1775,14 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1576
1775
  ];
1577
1776
  }
1578
1777
  // ─── Name-required roles ───────────────────────────────────────────────────
1579
- // Roles that always require an accessible name per WAI-ARIA APG.
1580
- // Dialog and landmark names are enforced via contracts (ariaContract) rather than
1581
- // the built-in pipeline so consumers can opt in; img is built in because role=img
1582
- // on any element (including bare <img>) is definitionally useless without a name.
1583
- static #NAME_REQUIRED_ROLES = /* @__PURE__ */ new Set(["img"]);
1584
1778
  static #checkNameRequiredRoles({
1585
1779
  tag,
1586
1780
  props,
1587
1781
  effectiveRole
1588
1782
  }) {
1589
- if (!effectiveRole || !_AriaPolicyEngine.#NAME_REQUIRED_ROLES.has(effectiveRole))
1590
- return NO_VIOLATIONS;
1591
- if ("aria-label" in props || "aria-labelledby" in props) return NO_VIOLATIONS;
1592
- if (tag === "img" && typeof props.alt === "string" && props.alt.length > 0) return NO_VIOLATIONS;
1783
+ if (!effectiveRole || !NAME_REQUIRED_ROLES.has(effectiveRole)) return NO_VIOLATIONS2;
1784
+ if ("aria-label" in props || "aria-labelledby" in props) return NO_VIOLATIONS2;
1785
+ if (tag === "img" && typeof props.alt === "string" && props.alt.length > 0) return NO_VIOLATIONS2;
1593
1786
  return [
1594
1787
  {
1595
1788
  valid: false,
@@ -1599,51 +1792,21 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1599
1792
  }
1600
1793
  ];
1601
1794
  }
1602
- // WAI-ARIA 1.2 required states and properties, keyed by role.
1603
- // Source: https://www.w3.org/TR/wai-aria-1.2/#requiredState
1604
- static #REQUIRED_PROPERTIES = /* @__PURE__ */ new Map([
1605
- ["combobox", ["aria-expanded"]],
1606
- ["option", ["aria-selected"]],
1607
- ["slider", ["aria-valuenow"]],
1608
- ["scrollbar", ["aria-controls", "aria-valuenow"]],
1609
- ["spinbutton", ["aria-valuenow"]]
1610
- ]);
1611
- static #checkRequiredAriaProperties({
1612
- props,
1613
- effectiveRole
1614
- }) {
1615
- if (!effectiveRole) return NO_VIOLATIONS;
1616
- const required = _AriaPolicyEngine.#REQUIRED_PROPERTIES.get(effectiveRole);
1617
- if (!isNonNull(required)) return NO_VIOLATIONS;
1618
- const results = [];
1619
- iterate.forEach(required, (attr) => {
1620
- if (attr in props) return;
1621
- results.push({
1622
- valid: false,
1623
- fixable: false,
1624
- severity: "warning",
1625
- attribute: attr,
1626
- diagnostic: AriaDiagnostics.requiredProperty(attr, effectiveRole)
1627
- });
1628
- });
1629
- return results;
1795
+ static #requiredAriaPropertiesRule = {
1796
+ attributesByRole: REQUIRED_ARIA_PROPERTIES,
1797
+ diagnosticFor: (attribute, role) => AriaDiagnostics.requiredProperty(attribute, role)
1798
+ };
1799
+ static #checkRequiredAriaProperties(context) {
1800
+ return checkRequiredAttributes(_AriaPolicyEngine.#requiredAriaPropertiesRule, context);
1630
1801
  }
1631
- // Natively interactive HTML elements — always keyboard-reachable unless explicitly disabled.
1632
- static #INTERACTIVE_TAGS = /* @__PURE__ */ new Set([
1633
- "a",
1634
- "button",
1635
- "input",
1636
- "select",
1637
- "textarea"
1638
- ]);
1639
1802
  // WAI-ARIA 1.2 §6.6: aria-hidden="true" must not be placed on focusable elements.
1640
1803
  static #checkAriaHiddenOnFocusable({ tag, props }) {
1641
- if (props["aria-hidden"] !== "true" && props["aria-hidden"] !== true) return NO_VIOLATIONS;
1642
- const isInteractive = _AriaPolicyEngine.#INTERACTIVE_TAGS.has(tag);
1804
+ if (props["aria-hidden"] !== "true" && props["aria-hidden"] !== true) return NO_VIOLATIONS2;
1805
+ const isInteractive = INTERACTIVE_TAGS.has(tag);
1643
1806
  if (!isInteractive) {
1644
1807
  const tabindex = props.tabindex;
1645
1808
  const n = typeof tabindex === "number" ? tabindex : typeof tabindex === "string" ? parseInt(tabindex, 10) : NaN;
1646
- if (!Number.isFinite(n) || n < 0) return NO_VIOLATIONS;
1809
+ if (!Number.isFinite(n) || n < 0) return NO_VIOLATIONS2;
1647
1810
  }
1648
1811
  return [
1649
1812
  {
@@ -1662,7 +1825,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1662
1825
  props,
1663
1826
  effectiveRole
1664
1827
  }) {
1665
- if (effectiveRole !== "none" && effectiveRole !== "presentation") return NO_VIOLATIONS;
1828
+ if (effectiveRole !== "none" && effectiveRole !== "presentation") return NO_VIOLATIONS2;
1666
1829
  const results = [];
1667
1830
  iterate.forEachEntry(props, (key) => {
1668
1831
  if (!key.startsWith("aria-")) return;
@@ -1678,18 +1841,11 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1678
1841
  });
1679
1842
  return results;
1680
1843
  }
1681
- // WAI-ARIA live region roles and their implied aria-live politeness values.
1682
- static #LIVE_REGION_ROLES = /* @__PURE__ */ new Map([
1683
- ["alert", "assertive"],
1684
- ["status", "polite"],
1685
- ["log", "polite"],
1686
- ["timer", "off"]
1687
- ]);
1688
1844
  static #checkMissingLiveRegion({ effectiveRole, props }) {
1689
- if (!effectiveRole) return NO_VIOLATIONS;
1690
- const impliedLive = _AriaPolicyEngine.#LIVE_REGION_ROLES.get(effectiveRole);
1691
- if (!impliedLive) return NO_VIOLATIONS;
1692
- if ("aria-live" in props) return NO_VIOLATIONS;
1845
+ if (!effectiveRole) return NO_VIOLATIONS2;
1846
+ const impliedLive = LIVE_REGION_ROLES.get(effectiveRole);
1847
+ if (!impliedLive) return NO_VIOLATIONS2;
1848
+ if ("aria-live" in props) return NO_VIOLATIONS2;
1693
1849
  const injectLive = {
1694
1850
  kind: `injectLive:${effectiveRole}`,
1695
1851
  apply: (ctx) => ({
@@ -1708,20 +1864,13 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1708
1864
  }
1709
1865
  ];
1710
1866
  }
1711
- static #checkMissingAtomic({ effectiveRole, props }) {
1712
- if (!effectiveRole || !_AriaPolicyEngine.#LIVE_REGION_ROLES.has(effectiveRole))
1713
- return NO_VIOLATIONS;
1714
- if ("aria-atomic" in props) return NO_VIOLATIONS;
1715
- return [
1716
- {
1717
- valid: false,
1718
- fixable: false,
1719
- severity: "warning",
1720
- diagnostic: AriaDiagnostics.missingAtomic(effectiveRole)
1721
- }
1722
- ];
1867
+ static #missingAtomicRule = {
1868
+ attributesByRole: ATOMIC_REQUIREMENTS,
1869
+ diagnosticFor: (_attribute, role) => AriaDiagnostics.missingAtomic(role)
1870
+ };
1871
+ static #checkMissingAtomic(context) {
1872
+ return checkRequiredAttributes(_AriaPolicyEngine.#missingAtomicRule, context);
1723
1873
  }
1724
- static #VALID_RELEVANT_TOKENS = /* @__PURE__ */ new Set(["additions", "removals", "text", "all"]);
1725
1874
  // Custom fix rules passed via `options.rules` must be pure functions of (tag, props) — the cache
1726
1875
  // replays stored fixes against new prop objects, so fixes that close over external state will
1727
1876
  // produce inconsistent results on cache hits.
@@ -1735,10 +1884,10 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1735
1884
  };
1736
1885
  static #checkInvalidAriaRelevant({ props }) {
1737
1886
  const relevant = props["aria-relevant"];
1738
- if (relevant === void 0) return NO_VIOLATIONS;
1739
- if (typeof relevant !== "string") return NO_VIOLATIONS;
1887
+ if (relevant === void 0) return NO_VIOLATIONS2;
1888
+ if (typeof relevant !== "string") return NO_VIOLATIONS2;
1740
1889
  const tokens = relevant.trim().split(/\s+/);
1741
- const invalid = tokens.filter((t) => !_AriaPolicyEngine.#VALID_RELEVANT_TOKENS.has(t));
1890
+ const invalid = tokens.filter((t) => !VALID_RELEVANT_TOKENS.has(t));
1742
1891
  if (invalid.length > 0) {
1743
1892
  return [
1744
1893
  {
@@ -1763,7 +1912,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1763
1912
  }
1764
1913
  ];
1765
1914
  }
1766
- return NO_VIOLATIONS;
1915
+ return NO_VIOLATIONS2;
1767
1916
  }
1768
1917
  };
1769
1918
 
@@ -2083,6 +2232,425 @@ var readonlyProps = ({
2083
2232
  // ../core/src/html/evaluators.ts
2084
2233
  import { warnDiagnostics as warnDiagnostics2 } from "../_shared/diagnostics.js";
2085
2234
 
2235
+ // ../core/src/html/spec/vocabulary/input.ts
2236
+ var TEXT_INPUT_TYPES = ["text", "search", "url", "tel", "email", "password"];
2237
+ var NUMERIC_INPUT_TYPES = [
2238
+ "number",
2239
+ "range",
2240
+ "date",
2241
+ "month",
2242
+ "week",
2243
+ "time",
2244
+ "datetime-local"
2245
+ ];
2246
+ var HTML_INPUT_TYPES = /* @__PURE__ */ new Set([
2247
+ ...TEXT_INPUT_TYPES,
2248
+ ...NUMERIC_INPUT_TYPES,
2249
+ "checkbox",
2250
+ "radio",
2251
+ "file",
2252
+ "color",
2253
+ "hidden",
2254
+ "button",
2255
+ "submit",
2256
+ "reset",
2257
+ "image"
2258
+ ]);
2259
+
2260
+ // ../core/src/html/spec/attributes/input.ts
2261
+ var INPUT_ATTRIBUTE_TYPE_POLICIES = [
2262
+ { attribute: "checked", allowedTypes: ["checkbox", "radio"] },
2263
+ { attribute: "multiple", allowedTypes: ["email", "file"] },
2264
+ { attribute: "maxLength", allowedTypes: TEXT_INPUT_TYPES },
2265
+ { attribute: "minLength", allowedTypes: TEXT_INPUT_TYPES },
2266
+ { attribute: "pattern", allowedTypes: TEXT_INPUT_TYPES },
2267
+ { attribute: "min", allowedTypes: NUMERIC_INPUT_TYPES },
2268
+ { attribute: "max", allowedTypes: NUMERIC_INPUT_TYPES },
2269
+ { attribute: "step", allowedTypes: NUMERIC_INPUT_TYPES },
2270
+ { attribute: "accept", allowedTypes: ["file"] },
2271
+ { attribute: "capture", allowedTypes: ["file"] },
2272
+ { attribute: "size", allowedTypes: TEXT_INPUT_TYPES },
2273
+ { attribute: "alt", allowedTypes: ["image"] },
2274
+ { attribute: "height", allowedTypes: ["image"] },
2275
+ { attribute: "width", allowedTypes: ["image"] }
2276
+ ];
2277
+
2278
+ // ../core/src/html/spec/constraints/input.ts
2279
+ var REQUIRED_READONLY_CONFLICT = {
2280
+ props: ["required", "readOnly"],
2281
+ diagnostic: () => InputAccessibilityDiagnostics.requiredReadOnlyConflict()
2282
+ };
2283
+ var INPUT_MUTUALLY_EXCLUSIVE_POLICIES = [
2284
+ REQUIRED_READONLY_CONFLICT
2285
+ ];
2286
+
2287
+ // ../core/src/html/spec/validators/attribute-type-validator.ts
2288
+ var DEFAULT_INPUT_TYPE = "text";
2289
+ function omit(props, key) {
2290
+ const next = { ...props };
2291
+ delete next[key];
2292
+ return next;
2293
+ }
2294
+ function removeAttributeFix(attribute) {
2295
+ return {
2296
+ kind: `removeAttribute:${attribute}`,
2297
+ apply: ({ props }) => {
2298
+ if (!(attribute in props)) return { applied: false, next: props };
2299
+ return { applied: true, next: omit(props, attribute), previous: props };
2300
+ }
2301
+ };
2302
+ }
2303
+ function createInputAttributeTypeRule({
2304
+ attribute,
2305
+ allowedTypes
2306
+ }) {
2307
+ const rule = ({ tag, props }) => {
2308
+ if (tag !== "input" || !(attribute in props)) return [];
2309
+ const type = typeof props.type === "string" ? props.type : DEFAULT_INPUT_TYPE;
2310
+ if (allowedTypes.includes(type)) return [];
2311
+ const diagnostic = HtmlDiagnostics.input.attributeIgnoredForType(attribute, type, allowedTypes);
2312
+ return [
2313
+ {
2314
+ valid: false,
2315
+ fixable: true,
2316
+ severity: diagnostic.severity,
2317
+ fix: removeAttributeFix(attribute),
2318
+ diagnostic
2319
+ }
2320
+ ];
2321
+ };
2322
+ return Object.assign(rule, { readsProps: ["type", attribute], tags: ["input"] });
2323
+ }
2324
+
2325
+ // ../core/src/html/spec/validators/mutually-exclusive-validator.ts
2326
+ function createMutuallyExclusiveRule({
2327
+ props: conflictingProps,
2328
+ diagnostic: createDiagnostic
2329
+ }) {
2330
+ const [first, second] = conflictingProps;
2331
+ const rule = ({ tag, props }) => {
2332
+ if (tag !== "input" || !props[first] || !props[second]) return [];
2333
+ const diagnostic = createDiagnostic();
2334
+ return [{ valid: false, fixable: false, severity: diagnostic.severity, diagnostic }];
2335
+ };
2336
+ return Object.assign(rule, { readsProps: conflictingProps, tags: ["input"] });
2337
+ }
2338
+
2339
+ // ../core/src/html/input-rules.ts
2340
+ var policyByAttribute = Object.fromEntries(
2341
+ INPUT_ATTRIBUTE_TYPE_POLICIES.map((policy) => [policy.attribute, policy])
2342
+ );
2343
+ function policyFor(attribute) {
2344
+ return policyByAttribute[attribute];
2345
+ }
2346
+ var supportedInputTypeRule = Object.assign(
2347
+ ({ tag, props }) => {
2348
+ if (tag !== "input" || typeof props.type !== "string") return [];
2349
+ const type = props.type;
2350
+ if (HTML_INPUT_TYPES.has(type)) return [];
2351
+ const diagnostic = HtmlDiagnostics.input.unsupportedType(type);
2352
+ return [
2353
+ {
2354
+ valid: false,
2355
+ fixable: false,
2356
+ severity: diagnostic.severity,
2357
+ diagnostic
2358
+ }
2359
+ ];
2360
+ },
2361
+ { readsProps: ["type"], tags: ["input"] }
2362
+ );
2363
+ var checkedRequiresCheckableTypeRule = createInputAttributeTypeRule(policyFor("checked"));
2364
+ var multipleRequiresSupportedTypeRule = createInputAttributeTypeRule(policyFor("multiple"));
2365
+ var maxLengthRequiresTextTypeRule = createInputAttributeTypeRule(policyFor("maxLength"));
2366
+ var minLengthRequiresTextTypeRule = createInputAttributeTypeRule(policyFor("minLength"));
2367
+ var patternRequiresTextTypeRule = createInputAttributeTypeRule(policyFor("pattern"));
2368
+ var minRequiresNumericTypeRule = createInputAttributeTypeRule(policyFor("min"));
2369
+ var maxRequiresNumericTypeRule = createInputAttributeTypeRule(policyFor("max"));
2370
+ var stepRequiresNumericTypeRule = createInputAttributeTypeRule(policyFor("step"));
2371
+ var acceptRequiresFileTypeRule = createInputAttributeTypeRule(policyFor("accept"));
2372
+ var captureRequiresFileTypeRule = createInputAttributeTypeRule(policyFor("capture"));
2373
+ var sizeRequiresTextTypeRule = createInputAttributeTypeRule(policyFor("size"));
2374
+ var altRequiresImageTypeRule = createInputAttributeTypeRule(policyFor("alt"));
2375
+ var heightRequiresImageTypeRule = createInputAttributeTypeRule(policyFor("height"));
2376
+ var widthRequiresImageTypeRule = createInputAttributeTypeRule(policyFor("width"));
2377
+ var inputAccessibleNameRule = Object.assign(
2378
+ ({ tag, props }) => {
2379
+ if (tag !== "input" || props.type === "hidden") return [];
2380
+ if ("aria-label" in props || "aria-labelledby" in props) return [];
2381
+ const hasPlaceholder = typeof props.placeholder === "string" && props.placeholder.length > 0;
2382
+ const diagnostic = hasPlaceholder ? InputAccessibilityDiagnostics.placeholderIsNotLabel() : InputAccessibilityDiagnostics.missingAccessibleName();
2383
+ return [{ valid: false, fixable: false, severity: diagnostic.severity, diagnostic }];
2384
+ },
2385
+ {
2386
+ readsProps: ["type", "aria-label", "aria-labelledby", "placeholder"],
2387
+ tags: ["input"]
2388
+ }
2389
+ );
2390
+ var PASSWORD_AUTOCOMPLETE_VALUES = ["current-password", "new-password"];
2391
+ var passwordAutocompleteRule = Object.assign(
2392
+ ({ tag, props }) => {
2393
+ if (tag !== "input" || props.type !== "password") return [];
2394
+ const autoComplete = props.autoComplete;
2395
+ const tokens = typeof autoComplete === "string" ? autoComplete.split(" ") : [];
2396
+ if (PASSWORD_AUTOCOMPLETE_VALUES.some((value) => tokens.includes(value))) return [];
2397
+ const diagnostic = InputAccessibilityDiagnostics.passwordMissingAutocomplete();
2398
+ return [{ valid: false, fixable: false, severity: diagnostic.severity, diagnostic }];
2399
+ },
2400
+ { readsProps: ["type", "autoComplete"], tags: ["input"] }
2401
+ );
2402
+ var requiredReadOnlyConflictRule = createMutuallyExclusiveRule(REQUIRED_READONLY_CONFLICT);
2403
+ var INPUT_RULES = [
2404
+ supportedInputTypeRule,
2405
+ checkedRequiresCheckableTypeRule,
2406
+ multipleRequiresSupportedTypeRule,
2407
+ maxLengthRequiresTextTypeRule,
2408
+ minLengthRequiresTextTypeRule,
2409
+ patternRequiresTextTypeRule,
2410
+ minRequiresNumericTypeRule,
2411
+ maxRequiresNumericTypeRule,
2412
+ stepRequiresNumericTypeRule,
2413
+ acceptRequiresFileTypeRule,
2414
+ captureRequiresFileTypeRule,
2415
+ sizeRequiresTextTypeRule,
2416
+ altRequiresImageTypeRule,
2417
+ heightRequiresImageTypeRule,
2418
+ widthRequiresImageTypeRule,
2419
+ inputAccessibleNameRule,
2420
+ passwordAutocompleteRule,
2421
+ requiredReadOnlyConflictRule
2422
+ ];
2423
+
2424
+ // ../core/src/html/spec/types.ts
2425
+ function definePropRolePolicy(prop, map2, fallback) {
2426
+ return { kind: "byProp", prop, map: map2, fallback };
2427
+ }
2428
+ function resolveAllowedRoles(spec, props) {
2429
+ const policy = spec.allowedRoles;
2430
+ if (!policy) return void 0;
2431
+ switch (policy.kind) {
2432
+ case "fixed":
2433
+ return policy.roles;
2434
+ case "byProp": {
2435
+ const value = typeof props[policy.prop] === "string" ? props[policy.prop] : policy.fallback;
2436
+ return policy.map[value];
2437
+ }
2438
+ case "dynamic":
2439
+ return policy.resolve({ props });
2440
+ }
2441
+ }
2442
+
2443
+ // ../core/src/html/spec/roles/input.ts
2444
+ var ALLOWED_INPUT_ROLES = {
2445
+ checkbox: ["menuitemcheckbox", "option", "switch", "button"],
2446
+ radio: ["menuitemradio"],
2447
+ range: [],
2448
+ number: [],
2449
+ search: ["combobox"],
2450
+ text: ["combobox", "searchbox", "spinbutton"],
2451
+ email: ["combobox"],
2452
+ tel: ["combobox"],
2453
+ url: ["combobox"],
2454
+ button: [
2455
+ "link",
2456
+ "menuitem",
2457
+ "menuitemcheckbox",
2458
+ "menuitemradio",
2459
+ "option",
2460
+ "radio",
2461
+ "switch",
2462
+ "tab"
2463
+ ],
2464
+ submit: [
2465
+ "link",
2466
+ "menuitem",
2467
+ "menuitemcheckbox",
2468
+ "menuitemradio",
2469
+ "option",
2470
+ "radio",
2471
+ "switch",
2472
+ "tab"
2473
+ ],
2474
+ reset: [
2475
+ "link",
2476
+ "menuitem",
2477
+ "menuitemcheckbox",
2478
+ "menuitemradio",
2479
+ "option",
2480
+ "radio",
2481
+ "switch",
2482
+ "tab"
2483
+ ],
2484
+ image: [
2485
+ "link",
2486
+ "menuitem",
2487
+ "menuitemcheckbox",
2488
+ "menuitemradio",
2489
+ "option",
2490
+ "radio",
2491
+ "switch",
2492
+ "tab"
2493
+ ],
2494
+ hidden: []
2495
+ };
2496
+
2497
+ // ../core/src/html/spec/elements/input.ts
2498
+ var inputElementSpec = {
2499
+ tag: "input",
2500
+ allowedRoles: definePropRolePolicy("type", ALLOWED_INPUT_ROLES, "text"),
2501
+ attributes: INPUT_ATTRIBUTE_TYPE_POLICIES,
2502
+ mutuallyExclusive: INPUT_MUTUALLY_EXCLUSIVE_POLICIES
2503
+ };
2504
+
2505
+ // ../core/src/html/spec/roles/img.ts
2506
+ var IMG_NAMED_ROLES = [
2507
+ "button",
2508
+ "checkbox",
2509
+ "link",
2510
+ "menuitem",
2511
+ "menuitemcheckbox",
2512
+ "menuitemradio",
2513
+ "option",
2514
+ "progressbar",
2515
+ "scrollbar",
2516
+ "separator",
2517
+ "slider",
2518
+ "switch",
2519
+ "tab",
2520
+ "treeitem"
2521
+ ];
2522
+
2523
+ // ../core/src/html/spec/elements/img.ts
2524
+ var imgElementSpec = {
2525
+ tag: "img",
2526
+ allowedRoles: {
2527
+ kind: "dynamic",
2528
+ resolve: ({ props }) => props.alt === "" ? [] : IMG_NAMED_ROLES
2529
+ }
2530
+ };
2531
+
2532
+ // ../core/src/html/spec/roles/table.ts
2533
+ var ALLOWED_TABLE_ROLES = ["grid", "treegrid"];
2534
+
2535
+ // ../core/src/html/spec/elements/table.ts
2536
+ var tableElementSpec = {
2537
+ tag: "table",
2538
+ allowedRoles: { kind: "fixed", roles: ALLOWED_TABLE_ROLES }
2539
+ };
2540
+
2541
+ // ../core/src/html/role-restrictions.ts
2542
+ var ALLOWED_ROLES = {
2543
+ article: ["application", "document", "feed", "main", "none", "presentation", "region"],
2544
+ aside: ["feed", "none", "presentation", "region", "search"],
2545
+ footer: ["group", "none", "presentation"],
2546
+ header: ["group", "none", "presentation"],
2547
+ main: [],
2548
+ nav: [],
2549
+ a: [
2550
+ "button",
2551
+ "checkbox",
2552
+ "menuitem",
2553
+ "menuitemcheckbox",
2554
+ "menuitemradio",
2555
+ "option",
2556
+ "radio",
2557
+ "switch",
2558
+ "tab",
2559
+ "treeitem"
2560
+ ],
2561
+ button: [
2562
+ "checkbox",
2563
+ "link",
2564
+ "menuitem",
2565
+ "menuitemcheckbox",
2566
+ "menuitemradio",
2567
+ "option",
2568
+ "radio",
2569
+ "switch",
2570
+ "tab"
2571
+ ],
2572
+ select: ["menu"],
2573
+ h1: ["tab", "presentation", "none"],
2574
+ h2: ["tab", "presentation", "none"],
2575
+ h3: ["tab", "presentation", "none"],
2576
+ h4: ["tab", "presentation", "none"],
2577
+ h5: ["tab", "presentation", "none"],
2578
+ h6: ["tab", "presentation", "none"],
2579
+ ul: [
2580
+ "directory",
2581
+ "group",
2582
+ "listbox",
2583
+ "menu",
2584
+ "menubar",
2585
+ "radiogroup",
2586
+ "tablist",
2587
+ "toolbar",
2588
+ "tree"
2589
+ ],
2590
+ ol: [
2591
+ "directory",
2592
+ "group",
2593
+ "listbox",
2594
+ "menu",
2595
+ "menubar",
2596
+ "radiogroup",
2597
+ "tablist",
2598
+ "toolbar",
2599
+ "tree"
2600
+ ],
2601
+ li: [
2602
+ "menuitem",
2603
+ "menuitemcheckbox",
2604
+ "menuitemradio",
2605
+ "option",
2606
+ "none",
2607
+ "presentation",
2608
+ "radio",
2609
+ "separator",
2610
+ "tab",
2611
+ "treeitem"
2612
+ ],
2613
+ dialog: ["alertdialog"],
2614
+ fieldset: ["none", "presentation", "radiogroup"]
2615
+ };
2616
+ var ELEMENT_SPECS = {
2617
+ input: inputElementSpec,
2618
+ img: imgElementSpec,
2619
+ table: tableElementSpec
2620
+ };
2621
+ function getAllowedRoles(tag, props) {
2622
+ const spec = ELEMENT_SPECS[tag];
2623
+ if (spec) return resolveAllowedRoles(spec, props);
2624
+ return ALLOWED_ROLES[tag];
2625
+ }
2626
+ var removeRoleFix = {
2627
+ kind: "removeRole",
2628
+ apply: ({ props }) => {
2629
+ if (!("role" in props)) return { applied: false, next: props };
2630
+ const { role: _role, ...rest } = props;
2631
+ return { applied: true, next: rest, previous: props };
2632
+ }
2633
+ };
2634
+ var roleNotPermittedRule = Object.assign(
2635
+ ({ tag, props, implicitRole }) => {
2636
+ const role = props.role;
2637
+ if (typeof role !== "string" || role.length === 0 || role === implicitRole) return [];
2638
+ const allowed = getAllowedRoles(tag, props);
2639
+ if (allowed === void 0 || allowed.includes(role)) return [];
2640
+ const diagnostic = HtmlDiagnostics.roleNotPermitted(tag, role, allowed);
2641
+ return [
2642
+ {
2643
+ valid: false,
2644
+ fixable: true,
2645
+ severity: diagnostic.severity,
2646
+ fix: removeRoleFix,
2647
+ diagnostic
2648
+ }
2649
+ ];
2650
+ },
2651
+ { readsProps: ["role", "type", "alt"] }
2652
+ );
2653
+
2086
2654
  // ../core/src/html/aria-rules.ts
2087
2655
  var LANDMARK_TAG_SET = /* @__PURE__ */ new Set(["article", "aside", "footer", "header", "main", "nav"]);
2088
2656
  var removeLandmarkRoleOverride = {
@@ -2093,20 +2661,24 @@ var removeLandmarkRoleOverride = {
2093
2661
  return { applied: true, next: rest, previous: props };
2094
2662
  }
2095
2663
  };
2096
- function landmarkRoleRule({ tag, props, implicitRole }) {
2097
- if (!LANDMARK_TAG_SET.has(tag) || !implicitRole) return [];
2098
- const role = props.role;
2099
- if (!role || role === implicitRole) return [];
2100
- return [
2101
- {
2102
- valid: false,
2103
- fixable: true,
2104
- severity: "error",
2105
- fix: removeLandmarkRoleOverride,
2106
- diagnostic: HtmlDiagnostics.landmarkRoleOverride(tag, implicitRole, role)
2107
- }
2108
- ];
2109
- }
2664
+ var landmarkRoleRule = Object.assign(
2665
+ ({ tag, props, implicitRole }) => {
2666
+ if (!LANDMARK_TAG_SET.has(tag) || !implicitRole) return [];
2667
+ const role = props.role;
2668
+ if (!role || role === implicitRole) return [];
2669
+ const diagnostic = HtmlDiagnostics.landmarkRoleOverride(tag, implicitRole, role);
2670
+ return [
2671
+ {
2672
+ valid: false,
2673
+ fixable: true,
2674
+ severity: diagnostic.severity,
2675
+ fix: removeLandmarkRoleOverride,
2676
+ diagnostic
2677
+ }
2678
+ ];
2679
+ },
2680
+ { tags: [...LANDMARK_TAG_SET] }
2681
+ );
2110
2682
  function requireAccessibleName({ tag, props }) {
2111
2683
  if ("aria-label" in props || "aria-labelledby" in props) return [];
2112
2684
  return [
@@ -2119,11 +2691,19 @@ function requireAccessibleName({ tag, props }) {
2119
2691
  ];
2120
2692
  }
2121
2693
  var NAMED_LANDMARK_TAGS = /* @__PURE__ */ new Set(["nav", "aside"]);
2122
- function landmarkNameAdvisory(ctx) {
2123
- if (!ctx.implicitRole || !NAMED_LANDMARK_TAGS.has(ctx.tag)) return [];
2124
- return requireAccessibleName(ctx);
2125
- }
2126
- var HTML_ARIA_RULES = [landmarkRoleRule, landmarkNameAdvisory];
2694
+ var landmarkNameAdvisory = Object.assign(
2695
+ (ctx) => {
2696
+ if (!ctx.implicitRole || !NAMED_LANDMARK_TAGS.has(ctx.tag)) return [];
2697
+ return requireAccessibleName(ctx);
2698
+ },
2699
+ { tags: [...NAMED_LANDMARK_TAGS] }
2700
+ );
2701
+ var HTML_ARIA_RULES = [
2702
+ landmarkRoleRule,
2703
+ landmarkNameAdvisory,
2704
+ roleNotPermittedRule,
2705
+ ...INPUT_RULES
2706
+ ];
2127
2707
 
2128
2708
  // ../core/src/html/contracts.ts
2129
2709
  import { warnDiagnostics } from "../_shared/diagnostics.js";
@@ -2218,6 +2798,62 @@ var figureContract = contract([
2218
2798
  ]);
2219
2799
  var detailsContract = firstChildContract("summary", "summary");
2220
2800
  var fieldsetContract = firstChildContract("legend", "legend");
2801
+ var objectContract = contract([
2802
+ { name: "param", match: isTag("param") },
2803
+ { name: "content", match: isOpenContent("param") }
2804
+ ]);
2805
+ var INTERACTIVE_CONTENT_TAGS = ["a", "button", "input", "select", "textarea", "label"];
2806
+ var buttonContract = closedContract([
2807
+ { name: "content", match: isOpenContent(...INTERACTIVE_CONTENT_TAGS) }
2808
+ ]);
2809
+ var anchorContract = closedContract([
2810
+ { name: "content", match: isOpenContent(...INTERACTIVE_CONTENT_TAGS) }
2811
+ ]);
2812
+ var LABELABLE_TAGS = [
2813
+ "button",
2814
+ "input",
2815
+ "meter",
2816
+ "output",
2817
+ "progress",
2818
+ "select",
2819
+ "textarea"
2820
+ ];
2821
+ var labelContract = contract([
2822
+ { name: "control", match: isTag(...LABELABLE_TAGS), cardinality: { max: 1 } }
2823
+ ]);
2824
+ var P_BLOCKED_TAGS = [
2825
+ "address",
2826
+ "article",
2827
+ "aside",
2828
+ "blockquote",
2829
+ "details",
2830
+ "dialog",
2831
+ "div",
2832
+ "dl",
2833
+ "fieldset",
2834
+ "figure",
2835
+ "footer",
2836
+ "form",
2837
+ "h1",
2838
+ "h2",
2839
+ "h3",
2840
+ "h4",
2841
+ "h5",
2842
+ "h6",
2843
+ "header",
2844
+ "hr",
2845
+ "main",
2846
+ "nav",
2847
+ "ol",
2848
+ "p",
2849
+ "pre",
2850
+ "section",
2851
+ "table",
2852
+ "ul"
2853
+ ];
2854
+ var pContract = closedContract([
2855
+ { name: "content", match: isOpenContent(...P_BLOCKED_TAGS) }
2856
+ ]);
2221
2857
  var mediaContract = contract([
2222
2858
  { name: "source", match: isTag("source") },
2223
2859
  { name: "track", match: isTag("track") },
@@ -2272,6 +2908,11 @@ var htmlContracts = {
2272
2908
  details: detailsContract,
2273
2909
  fieldset: fieldsetContract,
2274
2910
  dialog: dialogContract,
2911
+ object: objectContract,
2912
+ button: buttonContract,
2913
+ a: anchorContract,
2914
+ label: labelContract,
2915
+ p: pContract,
2275
2916
  head: headContract,
2276
2917
  html: htmlContract
2277
2918
  };
@@ -2584,21 +3225,21 @@ function validateRenderProps(diagnostics, options, props, recipeKey) {
2584
3225
  import { throwDiagnostics } from "../_shared/diagnostics.js";
2585
3226
 
2586
3227
  // ../core/src/factory/plugin-diagnostics.ts
2587
- import { DiagnosticCategory as DiagnosticCategory5, DiagnosticCode as DiagnosticCode5 } from "../_shared/diagnostics.js";
3228
+ import { DiagnosticCategory as DiagnosticCategory6, DiagnosticCode as DiagnosticCode6 } from "../_shared/diagnostics.js";
2588
3229
  var PluginDiagnostics = {
2589
3230
  invalidShape(received) {
2590
3231
  const got = received === null ? "null" : typeof received;
2591
3232
  return {
2592
- code: DiagnosticCode5.PluginInvalidShape,
2593
- category: DiagnosticCategory5.Internal,
3233
+ code: DiagnosticCode6.PluginInvalidShape,
3234
+ category: DiagnosticCategory6.Internal,
2594
3235
  message: `[praxis-kit] Plugin factory must return an object with a 'pipeline' function. Got: ${got}.`
2595
3236
  };
2596
3237
  },
2597
3238
  pipelineReturnType(received) {
2598
3239
  const got = received === null ? "null" : Array.isArray(received) ? "array" : typeof received;
2599
3240
  return {
2600
- code: DiagnosticCode5.PluginPipelineReturnType,
2601
- category: DiagnosticCategory5.Internal,
3241
+ code: DiagnosticCode6.PluginPipelineReturnType,
3242
+ category: DiagnosticCategory6.Internal,
2602
3243
  message: `[praxis-kit] Plugin pipeline must return a string. Got: ${got}.`
2603
3244
  };
2604
3245
  }