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.
package/dist/lit/index.js CHANGED
@@ -858,11 +858,28 @@ var ContractDiagnostics = {
858
858
 
859
859
  // ../../lib/contract/src/diagnostics/html.ts
860
860
  import { DiagnosticCategory as DiagnosticCategory3, DiagnosticCode as DiagnosticCode3 } from "../_shared/diagnostics.js";
861
+ var ATTRIBUTE_IGNORED_CODES = {
862
+ checked: DiagnosticCode3.HtmlInputCheckedIgnoredForType,
863
+ multiple: DiagnosticCode3.HtmlInputMultipleIgnoredForType,
864
+ maxLength: DiagnosticCode3.HtmlInputMaxLengthIgnoredForType,
865
+ minLength: DiagnosticCode3.HtmlInputMinLengthIgnoredForType,
866
+ pattern: DiagnosticCode3.HtmlInputPatternIgnoredForType,
867
+ min: DiagnosticCode3.HtmlInputMinIgnoredForType,
868
+ max: DiagnosticCode3.HtmlInputMaxIgnoredForType,
869
+ step: DiagnosticCode3.HtmlInputStepIgnoredForType,
870
+ accept: DiagnosticCode3.HtmlInputAcceptIgnoredForType,
871
+ capture: DiagnosticCode3.HtmlInputCaptureIgnoredForType,
872
+ size: DiagnosticCode3.HtmlInputSizeIgnoredForType,
873
+ alt: DiagnosticCode3.HtmlInputAltIgnoredForType,
874
+ height: DiagnosticCode3.HtmlInputHeightIgnoredForType,
875
+ width: DiagnosticCode3.HtmlInputWidthIgnoredForType
876
+ };
861
877
  var HtmlDiagnostics = {
862
878
  emptyRole(tag) {
863
879
  return {
864
880
  code: DiagnosticCode3.HtmlEmptyRole,
865
881
  category: DiagnosticCategory3.HTML,
882
+ severity: "warning",
866
883
  message: `<${tag}> has an explicit empty role="". Omit the attribute instead.`
867
884
  };
868
885
  },
@@ -870,6 +887,7 @@ var HtmlDiagnostics = {
870
887
  return {
871
888
  code: DiagnosticCode3.HtmlImplicitRoleRedundant,
872
889
  category: DiagnosticCategory3.HTML,
890
+ severity: "warning",
873
891
  message: `<${tag}> already has implicit role="${implicitRole}". Avoid redundant role assignment.`
874
892
  };
875
893
  },
@@ -877,6 +895,7 @@ var HtmlDiagnostics = {
877
895
  return {
878
896
  code: DiagnosticCode3.HtmlImplicitRoleOverride,
879
897
  category: DiagnosticCategory3.HTML,
898
+ severity: "error",
880
899
  message: `<${tag}> should not override its implicit role="${implicitRole}" with role="${role}".`
881
900
  };
882
901
  },
@@ -884,6 +903,7 @@ var HtmlDiagnostics = {
884
903
  return {
885
904
  code: DiagnosticCode3.HtmlStandaloneRegionOverride,
886
905
  category: DiagnosticCategory3.HTML,
906
+ severity: "error",
887
907
  message: `<${tag}> is a self-contained element with implicit role="${implicitRole}". Assigning role="region" has been removed.`
888
908
  };
889
909
  },
@@ -891,6 +911,7 @@ var HtmlDiagnostics = {
891
911
  return {
892
912
  code: DiagnosticCode3.HtmlLandmarkRoleOverride,
893
913
  category: DiagnosticCategory3.HTML,
914
+ severity: "error",
894
915
  message: `<${tag}> has a fixed landmark role="${implicitRole}". role="${role}" overrides it and confuses assistive technology. The override has been removed.`
895
916
  };
896
917
  },
@@ -898,8 +919,122 @@ var HtmlDiagnostics = {
898
919
  return {
899
920
  code: DiagnosticCode3.HtmlInvalidChild,
900
921
  category: DiagnosticCategory3.HTML,
922
+ severity: "error",
901
923
  message: `<${child}> is not a valid direct child of <${parent}>. Allowed: ${allowed}.`
902
924
  };
925
+ },
926
+ roleNotPermitted(tag, role, allowedRoles) {
927
+ const allowed = allowedRoles.length > 0 ? allowedRoles.map((r) => `"${r}"`).join(", ") : "none \u2014 no explicit role is permitted on this element";
928
+ return {
929
+ code: DiagnosticCode3.HtmlRoleNotPermitted,
930
+ category: DiagnosticCategory3.HTML,
931
+ severity: "error",
932
+ message: `role="${role}" is not permitted on <${tag}>. Allowed alternate role(s): ${allowed}.`,
933
+ 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.'
934
+ };
935
+ },
936
+ // Reserved for <input>-specific facts (HTML3101–3199, see codes.ts) — later element families
937
+ // (button, img, table, ...) get their own reserved block and their own namespace here.
938
+ input: {
939
+ unsupportedType(type) {
940
+ return {
941
+ code: DiagnosticCode3.HtmlInputUnsupportedType,
942
+ category: DiagnosticCategory3.HTML,
943
+ severity: "warning",
944
+ message: `type="${type}" is not a value defined by the HTML specification. Browsers silently fall back to type="text".`,
945
+ 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).',
946
+ suggestions: [
947
+ {
948
+ title: "Check for a typo in the type value",
949
+ description: `"${type}" does not match any HTML5 input type.`
950
+ }
951
+ ]
952
+ };
953
+ },
954
+ // The user-visible problem is that the attribute is ignored — not that it "requires" a type;
955
+ // that's the rule's internal framing, not what the browser actually does.
956
+ attributeIgnoredForType(attribute, type, allowedTypes) {
957
+ const allowed = allowedTypes.map((t) => `"${t}"`).join(", ");
958
+ const code = ATTRIBUTE_IGNORED_CODES[attribute];
959
+ if (!code) throw new Error(`No DiagnosticCode registered for input attribute "${attribute}"`);
960
+ return {
961
+ code,
962
+ category: DiagnosticCategory3.HTML,
963
+ severity: "warning",
964
+ message: `"${attribute}" is ignored on <input type="${type}">.`,
965
+ rationale: `"${attribute}" only has an effect when type is one of: ${allowed}. Browsers silently ignore it on other input types.`,
966
+ suggestions: [
967
+ {
968
+ title: `Remove "${attribute}"`,
969
+ description: `"${attribute}" only affects <input> when type is one of: ${allowed}.`
970
+ }
971
+ ]
972
+ };
973
+ }
974
+ }
975
+ };
976
+
977
+ // ../../lib/contract/src/diagnostics/input-accessibility.ts
978
+ import { DiagnosticCategory as DiagnosticCategory4, DiagnosticCode as DiagnosticCode4 } from "../_shared/diagnostics.js";
979
+ function accessibilityFact(input) {
980
+ return { category: DiagnosticCategory4.Accessibility, ...input };
981
+ }
982
+ var InputAccessibilityDiagnostics = {
983
+ missingAccessibleName() {
984
+ return accessibilityFact({
985
+ code: DiagnosticCode4.A11yInputMissingAccessibleName,
986
+ severity: "warning",
987
+ message: "This input has no accessible name. Add an associated <label>, aria-label, or aria-labelledby.",
988
+ rationale: "Assistive technology announces a form field by its accessible name; without one, users of screen readers cannot tell what the field is for.",
989
+ suggestions: [
990
+ { title: "Add aria-label", description: 'Set aria-label="\u2026" directly on the input.' },
991
+ {
992
+ title: "Add an associated <label>",
993
+ description: 'Wrap the input in a <label>, or point a <label for="\u2026"> at its id.'
994
+ }
995
+ ]
996
+ });
997
+ },
998
+ placeholderIsNotLabel() {
999
+ return accessibilityFact({
1000
+ code: DiagnosticCode4.A11yInputPlaceholderNotLabel,
1001
+ severity: "warning",
1002
+ message: "Placeholder text does not provide an accessible name. Add an associated <label>, aria-label, or aria-labelledby.",
1003
+ rationale: "Placeholder text disappears as users interact with the field and is not treated as the control's accessible name by many assistive technologies.",
1004
+ suggestions: [
1005
+ { title: "Add aria-label", description: 'Set aria-label="\u2026" directly on the input.' },
1006
+ {
1007
+ title: "Add an associated <label>",
1008
+ description: 'Wrap the input in a <label>, or point a <label for="\u2026"> at its id.'
1009
+ }
1010
+ ]
1011
+ });
1012
+ },
1013
+ passwordMissingAutocomplete() {
1014
+ return accessibilityFact({
1015
+ code: DiagnosticCode4.A11yInputPasswordAutocomplete,
1016
+ severity: "warning",
1017
+ message: "Password inputs should specify an autoComplete value.",
1018
+ rationale: "Without an explicit autocomplete hint, password managers and browsers cannot reliably tell a sign-in field apart from a password-creation field.",
1019
+ suggestions: [
1020
+ {
1021
+ title: 'Set autoComplete="current-password"',
1022
+ description: "Use this for sign-in forms."
1023
+ },
1024
+ {
1025
+ title: 'Set autoComplete="new-password"',
1026
+ description: "Use this for sign-up / change-password forms."
1027
+ }
1028
+ ]
1029
+ });
1030
+ },
1031
+ requiredReadOnlyConflict() {
1032
+ return accessibilityFact({
1033
+ code: DiagnosticCode4.A11yInputRequiredReadOnlyConflict,
1034
+ severity: "warning",
1035
+ message: "The required and readOnly attributes are both present. A read-only field cannot satisfy required validation through user interaction.",
1036
+ 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."
1037
+ });
903
1038
  }
904
1039
  };
905
1040
 
@@ -925,8 +1060,132 @@ var InvariantBase = class {
925
1060
  }
926
1061
  };
927
1062
 
928
- // ../../lib/contract/src/aria/polymorphic-validator.ts
1063
+ // ../../lib/contract/src/aria/spec/roles/required-properties.ts
1064
+ var REQUIRED_ARIA_PROPERTIES = {
1065
+ combobox: ["aria-expanded"],
1066
+ option: ["aria-selected"],
1067
+ slider: ["aria-valuenow"],
1068
+ scrollbar: ["aria-controls", "aria-valuenow"],
1069
+ spinbutton: ["aria-valuenow"]
1070
+ };
1071
+
1072
+ // ../../lib/contract/src/aria/spec/roles/name-required.ts
1073
+ var NAME_REQUIRED_ROLES = /* @__PURE__ */ new Set(["img"]);
1074
+
1075
+ // ../../lib/contract/src/aria/spec/validators/required-properties-validator.ts
929
1076
  var NO_VIOLATIONS = [{ valid: true }];
1077
+ function requiredAttributeByRole(roles, attribute) {
1078
+ return Object.fromEntries([...roles].map((role) => [role, [attribute]]));
1079
+ }
1080
+ function checkRequiredAttributes(requirement, { props, effectiveRole }) {
1081
+ if (!effectiveRole) return NO_VIOLATIONS;
1082
+ const requiredAttributes = requirement.attributesByRole[effectiveRole];
1083
+ if (!requiredAttributes) return NO_VIOLATIONS;
1084
+ const results = [];
1085
+ for (const attribute of requiredAttributes) {
1086
+ if (attribute in props) continue;
1087
+ results.push({
1088
+ valid: false,
1089
+ fixable: false,
1090
+ severity: "warning",
1091
+ attribute,
1092
+ diagnostic: requirement.diagnosticFor(attribute, effectiveRole)
1093
+ });
1094
+ }
1095
+ return results;
1096
+ }
1097
+
1098
+ // ../../lib/contract/src/aria/spec/roles/live-region.ts
1099
+ var LIVE_REGION_ROLES = /* @__PURE__ */ new Map([
1100
+ ["alert", "assertive"],
1101
+ ["status", "polite"],
1102
+ ["log", "polite"],
1103
+ ["timer", "off"]
1104
+ ]);
1105
+ var ATOMIC_REQUIREMENTS = requiredAttributeByRole(LIVE_REGION_ROLES.keys(), "aria-atomic");
1106
+
1107
+ // ../../lib/contract/src/aria/spec/attributes/aria-value-types.ts
1108
+ var ARIA_VALUE_TYPES = /* @__PURE__ */ new Map([
1109
+ // Boolean (true | false)
1110
+ ["aria-atomic", { kind: "boolean" }],
1111
+ ["aria-busy", { kind: "boolean" }],
1112
+ ["aria-disabled", { kind: "boolean" }],
1113
+ ["aria-expanded", { kind: "boolean" }],
1114
+ ["aria-hidden", { kind: "boolean" }],
1115
+ ["aria-modal", { kind: "boolean" }],
1116
+ ["aria-multiline", { kind: "boolean" }],
1117
+ ["aria-multiselectable", { kind: "boolean" }],
1118
+ ["aria-readonly", { kind: "boolean" }],
1119
+ ["aria-required", { kind: "boolean" }],
1120
+ ["aria-selected", { kind: "boolean" }],
1121
+ // Tristate (true | false | mixed)
1122
+ ["aria-checked", { kind: "tristate" }],
1123
+ ["aria-pressed", { kind: "tristate" }],
1124
+ // Numeric (any finite number)
1125
+ ["aria-valuenow", { kind: "number" }],
1126
+ ["aria-valuemin", { kind: "number" }],
1127
+ ["aria-valuemax", { kind: "number" }],
1128
+ // Integer with optional range
1129
+ ["aria-level", { kind: "integer", min: 1, max: 6 }],
1130
+ ["aria-posinset", { kind: "integer", min: 1 }],
1131
+ ["aria-setsize", { kind: "integer", min: -1 }],
1132
+ ["aria-rowcount", { kind: "integer", min: -1 }],
1133
+ ["aria-colcount", { kind: "integer", min: -1 }],
1134
+ ["aria-rowindex", { kind: "integer", min: 1 }],
1135
+ ["aria-colindex", { kind: "integer", min: 1 }],
1136
+ ["aria-rowspan", { kind: "integer", min: 0 }],
1137
+ ["aria-colspan", { kind: "integer", min: 0 }],
1138
+ // Enum (specific allowed tokens)
1139
+ ["aria-autocomplete", { kind: "enum", values: /* @__PURE__ */ new Set(["inline", "list", "both", "none"]) }],
1140
+ [
1141
+ "aria-current",
1142
+ {
1143
+ kind: "enum",
1144
+ values: /* @__PURE__ */ new Set(["page", "step", "location", "date", "time", "true", "false"])
1145
+ }
1146
+ ],
1147
+ [
1148
+ "aria-haspopup",
1149
+ {
1150
+ kind: "enum",
1151
+ values: /* @__PURE__ */ new Set(["false", "true", "menu", "listbox", "tree", "grid", "dialog"])
1152
+ }
1153
+ ],
1154
+ ["aria-invalid", { kind: "enum", values: /* @__PURE__ */ new Set(["grammar", "false", "spelling", "true"]) }],
1155
+ ["aria-live", { kind: "enum", values: /* @__PURE__ */ new Set(["assertive", "off", "polite"]) }],
1156
+ ["aria-orientation", { kind: "enum", values: /* @__PURE__ */ new Set(["horizontal", "vertical", "undefined"]) }],
1157
+ ["aria-sort", { kind: "enum", values: /* @__PURE__ */ new Set(["ascending", "descending", "none", "other"]) }]
1158
+ ]);
1159
+
1160
+ // ../../lib/contract/src/aria/spec/attributes/aria-relevant-tokens.ts
1161
+ var VALID_RELEVANT_TOKENS = /* @__PURE__ */ new Set([
1162
+ "additions",
1163
+ "removals",
1164
+ "text",
1165
+ "all"
1166
+ ]);
1167
+
1168
+ // ../../lib/contract/src/aria/spec/elements/heading-implicit-levels.ts
1169
+ var HEADING_IMPLICIT_LEVELS = /* @__PURE__ */ new Map([
1170
+ ["h1", 1],
1171
+ ["h2", 2],
1172
+ ["h3", 3],
1173
+ ["h4", 4],
1174
+ ["h5", 5],
1175
+ ["h6", 6]
1176
+ ]);
1177
+
1178
+ // ../../lib/contract/src/aria/spec/elements/interactive-tags.ts
1179
+ var INTERACTIVE_TAGS = /* @__PURE__ */ new Set([
1180
+ "a",
1181
+ "button",
1182
+ "input",
1183
+ "select",
1184
+ "textarea"
1185
+ ]);
1186
+
1187
+ // ../../lib/contract/src/aria/polymorphic-validator.ts
1188
+ var NO_VIOLATIONS2 = [{ valid: true }];
930
1189
  function isIntrinsicTag(tag) {
931
1190
  return isString(tag);
932
1191
  }
@@ -958,7 +1217,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
958
1217
  tag,
959
1218
  role: "",
960
1219
  attribute: void 0,
961
- severity: "warning",
1220
+ severity: d.severity,
962
1221
  phase: "evaluate"
963
1222
  }
964
1223
  ]
@@ -988,6 +1247,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
988
1247
  const violations = [];
989
1248
  const fixes = [];
990
1249
  iterate.forEach(rules, (rule) => {
1250
+ if (isNonNull(rule.tags) && !rule.tags.includes(context.tag)) return;
991
1251
  iterate.forEach(rule(context), (result) => {
992
1252
  if (result.valid) return;
993
1253
  const {
@@ -1013,7 +1273,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1013
1273
  return { violations, fixes };
1014
1274
  }
1015
1275
  static #getRules(context) {
1016
- if (_AriaPolicyEngine.#hasRole(context.props) || isNonNull(context.effectiveRole) && _AriaPolicyEngine.#LIVE_REGION_ROLES.has(context.effectiveRole)) {
1276
+ if (_AriaPolicyEngine.#hasRole(context.props) || isNonNull(context.effectiveRole) && LIVE_REGION_ROLES.has(context.effectiveRole)) {
1017
1277
  return _AriaPolicyEngine.#pipeline;
1018
1278
  }
1019
1279
  return _AriaPolicyEngine.#implicitOnlyRules;
@@ -1215,44 +1475,47 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1215
1475
  implicitRole
1216
1476
  }) {
1217
1477
  const role = props.role;
1218
- if (!implicitRole || !role || role === implicitRole) return NO_VIOLATIONS;
1478
+ if (!implicitRole || !role || role === implicitRole) return NO_VIOLATIONS2;
1219
1479
  if (isStrongImplicitRole(tag) && role === "region") {
1480
+ const diagnostic = HtmlDiagnostics.implicitRoleOverride(tag, implicitRole, role);
1220
1481
  return [
1221
1482
  {
1222
1483
  valid: false,
1223
1484
  fixable: true,
1224
- severity: "error",
1485
+ severity: diagnostic.severity,
1225
1486
  fix: _AriaPolicyEngine.#removeRole,
1226
- diagnostic: HtmlDiagnostics.implicitRoleOverride(tag, implicitRole, role)
1487
+ diagnostic
1227
1488
  }
1228
1489
  ];
1229
1490
  }
1230
- return NO_VIOLATIONS;
1491
+ return NO_VIOLATIONS2;
1231
1492
  }
1232
1493
  static #checkRedundantRole({ tag, props, implicitRole }) {
1233
1494
  const role = props.role;
1234
- if (!implicitRole || !role || role !== implicitRole) return NO_VIOLATIONS;
1495
+ if (!implicitRole || !role || role !== implicitRole) return NO_VIOLATIONS2;
1496
+ const diagnostic = HtmlDiagnostics.implicitRoleRedundant(tag, implicitRole);
1235
1497
  return [
1236
1498
  {
1237
1499
  valid: false,
1238
1500
  fixable: true,
1239
- severity: "warning",
1501
+ severity: diagnostic.severity,
1240
1502
  fix: _AriaPolicyEngine.#removeRole,
1241
- diagnostic: HtmlDiagnostics.implicitRoleRedundant(tag, implicitRole)
1503
+ diagnostic
1242
1504
  }
1243
1505
  ];
1244
1506
  }
1245
1507
  static #checkStandaloneRegion({ tag, props, implicitRole }) {
1246
1508
  const role = props.role;
1247
- if (role !== "region") return NO_VIOLATIONS;
1248
- if (!isStandaloneTag(tag)) return NO_VIOLATIONS;
1509
+ if (role !== "region") return NO_VIOLATIONS2;
1510
+ if (!isStandaloneTag(tag)) return NO_VIOLATIONS2;
1511
+ const diagnostic = HtmlDiagnostics.standaloneRegionOverride(tag, implicitRole ?? tag);
1249
1512
  return [
1250
1513
  {
1251
1514
  valid: false,
1252
1515
  fixable: true,
1253
- severity: "error",
1516
+ severity: diagnostic.severity,
1254
1517
  fix: _AriaPolicyEngine.#removeRole,
1255
- diagnostic: HtmlDiagnostics.standaloneRegionOverride(tag, implicitRole ?? tag)
1518
+ diagnostic
1256
1519
  }
1257
1520
  ];
1258
1521
  }
@@ -1261,7 +1524,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1261
1524
  props,
1262
1525
  effectiveRole
1263
1526
  }) {
1264
- if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS;
1527
+ if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS2;
1265
1528
  const results = [];
1266
1529
  iterate.forEachEntry(props, (key) => {
1267
1530
  if (!key.startsWith("aria-")) return;
@@ -1279,62 +1542,6 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1279
1542
  return results;
1280
1543
  }
1281
1544
  // ─── ARIA attribute value validation ──────────────────────────────────────
1282
- // Accepted value shapes for typed ARIA attributes.
1283
- // Attributes not in this map are unconstrained (arbitrary string values permitted).
1284
- static #ARIA_VALUE_TYPES = /* @__PURE__ */ new Map([
1285
- // Boolean (true | false)
1286
- ["aria-atomic", { kind: "boolean" }],
1287
- ["aria-busy", { kind: "boolean" }],
1288
- ["aria-disabled", { kind: "boolean" }],
1289
- ["aria-expanded", { kind: "boolean" }],
1290
- ["aria-hidden", { kind: "boolean" }],
1291
- ["aria-modal", { kind: "boolean" }],
1292
- ["aria-multiline", { kind: "boolean" }],
1293
- ["aria-multiselectable", { kind: "boolean" }],
1294
- ["aria-readonly", { kind: "boolean" }],
1295
- ["aria-required", { kind: "boolean" }],
1296
- ["aria-selected", { kind: "boolean" }],
1297
- // Tristate (true | false | mixed)
1298
- ["aria-checked", { kind: "tristate" }],
1299
- ["aria-pressed", { kind: "tristate" }],
1300
- // Numeric (any finite number)
1301
- ["aria-valuenow", { kind: "number" }],
1302
- ["aria-valuemin", { kind: "number" }],
1303
- ["aria-valuemax", { kind: "number" }],
1304
- // Integer with optional range
1305
- ["aria-level", { kind: "integer", min: 1, max: 6 }],
1306
- ["aria-posinset", { kind: "integer", min: 1 }],
1307
- ["aria-setsize", { kind: "integer", min: -1 }],
1308
- ["aria-rowcount", { kind: "integer", min: -1 }],
1309
- ["aria-colcount", { kind: "integer", min: -1 }],
1310
- ["aria-rowindex", { kind: "integer", min: 1 }],
1311
- ["aria-colindex", { kind: "integer", min: 1 }],
1312
- ["aria-rowspan", { kind: "integer", min: 0 }],
1313
- ["aria-colspan", { kind: "integer", min: 0 }],
1314
- // Enum (specific allowed tokens)
1315
- ["aria-autocomplete", { kind: "enum", values: /* @__PURE__ */ new Set(["inline", "list", "both", "none"]) }],
1316
- [
1317
- "aria-current",
1318
- {
1319
- kind: "enum",
1320
- values: /* @__PURE__ */ new Set(["page", "step", "location", "date", "time", "true", "false"])
1321
- }
1322
- ],
1323
- [
1324
- "aria-haspopup",
1325
- {
1326
- kind: "enum",
1327
- values: /* @__PURE__ */ new Set(["false", "true", "menu", "listbox", "tree", "grid", "dialog"])
1328
- }
1329
- ],
1330
- ["aria-invalid", { kind: "enum", values: /* @__PURE__ */ new Set(["grammar", "false", "spelling", "true"]) }],
1331
- ["aria-live", { kind: "enum", values: /* @__PURE__ */ new Set(["assertive", "off", "polite"]) }],
1332
- [
1333
- "aria-orientation",
1334
- { kind: "enum", values: /* @__PURE__ */ new Set(["horizontal", "vertical", "undefined"]) }
1335
- ],
1336
- ["aria-sort", { kind: "enum", values: /* @__PURE__ */ new Set(["ascending", "descending", "none", "other"]) }]
1337
- ]);
1338
1545
  static #isValidAriaValue(value, type) {
1339
1546
  switch (type.kind) {
1340
1547
  case "boolean":
@@ -1379,11 +1586,11 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1379
1586
  }
1380
1587
  }
1381
1588
  static #checkAriaAttributeValues({ props, effectiveRole }) {
1382
- if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS;
1589
+ if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS2;
1383
1590
  const results = [];
1384
1591
  iterate.forEachEntry(props, (key, value) => {
1385
1592
  if (!key.startsWith("aria-")) return;
1386
- const type = _AriaPolicyEngine.#ARIA_VALUE_TYPES.get(key);
1593
+ const type = ARIA_VALUE_TYPES.get(key);
1387
1594
  if (!isNonNull(type)) return;
1388
1595
  if (_AriaPolicyEngine.#isValidAriaValue(value, type)) return;
1389
1596
  results.push({
@@ -1402,26 +1609,18 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1402
1609
  return results;
1403
1610
  }
1404
1611
  // ─── Heading implicit level ────────────────────────────────────────────────
1405
- static #HEADING_IMPLICIT_LEVELS = /* @__PURE__ */ new Map([
1406
- ["h1", 1],
1407
- ["h2", 2],
1408
- ["h3", 3],
1409
- ["h4", 4],
1410
- ["h5", 5],
1411
- ["h6", 6]
1412
- ]);
1413
1612
  static #checkRedundantAriaLevel({
1414
1613
  tag,
1415
1614
  props,
1416
1615
  effectiveRole
1417
1616
  }) {
1418
- if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS;
1419
- const implicitLevel = _AriaPolicyEngine.#HEADING_IMPLICIT_LEVELS.get(tag);
1420
- if (!isNonNull(implicitLevel)) return NO_VIOLATIONS;
1617
+ if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS2;
1618
+ const implicitLevel = HEADING_IMPLICIT_LEVELS.get(tag);
1619
+ if (!isNonNull(implicitLevel)) return NO_VIOLATIONS2;
1421
1620
  const raw = props["aria-level"];
1422
- if (!isNonNull(raw)) return NO_VIOLATIONS;
1621
+ if (!isNonNull(raw)) return NO_VIOLATIONS2;
1423
1622
  const n = typeof raw === "number" ? raw : typeof raw === "string" ? parseInt(raw, 10) : NaN;
1424
- if (!Number.isFinite(n) || n !== implicitLevel) return NO_VIOLATIONS;
1623
+ if (!Number.isFinite(n) || n !== implicitLevel) return NO_VIOLATIONS2;
1425
1624
  return [
1426
1625
  {
1427
1626
  valid: false,
@@ -1434,20 +1633,14 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1434
1633
  ];
1435
1634
  }
1436
1635
  // ─── Name-required roles ───────────────────────────────────────────────────
1437
- // Roles that always require an accessible name per WAI-ARIA APG.
1438
- // Dialog and landmark names are enforced via contracts (ariaContract) rather than
1439
- // the built-in pipeline so consumers can opt in; img is built in because role=img
1440
- // on any element (including bare <img>) is definitionally useless without a name.
1441
- static #NAME_REQUIRED_ROLES = /* @__PURE__ */ new Set(["img"]);
1442
1636
  static #checkNameRequiredRoles({
1443
1637
  tag,
1444
1638
  props,
1445
1639
  effectiveRole
1446
1640
  }) {
1447
- if (!effectiveRole || !_AriaPolicyEngine.#NAME_REQUIRED_ROLES.has(effectiveRole))
1448
- return NO_VIOLATIONS;
1449
- if ("aria-label" in props || "aria-labelledby" in props) return NO_VIOLATIONS;
1450
- if (tag === "img" && typeof props.alt === "string" && props.alt.length > 0) return NO_VIOLATIONS;
1641
+ if (!effectiveRole || !NAME_REQUIRED_ROLES.has(effectiveRole)) return NO_VIOLATIONS2;
1642
+ if ("aria-label" in props || "aria-labelledby" in props) return NO_VIOLATIONS2;
1643
+ if (tag === "img" && typeof props.alt === "string" && props.alt.length > 0) return NO_VIOLATIONS2;
1451
1644
  return [
1452
1645
  {
1453
1646
  valid: false,
@@ -1457,51 +1650,21 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1457
1650
  }
1458
1651
  ];
1459
1652
  }
1460
- // WAI-ARIA 1.2 required states and properties, keyed by role.
1461
- // Source: https://www.w3.org/TR/wai-aria-1.2/#requiredState
1462
- static #REQUIRED_PROPERTIES = /* @__PURE__ */ new Map([
1463
- ["combobox", ["aria-expanded"]],
1464
- ["option", ["aria-selected"]],
1465
- ["slider", ["aria-valuenow"]],
1466
- ["scrollbar", ["aria-controls", "aria-valuenow"]],
1467
- ["spinbutton", ["aria-valuenow"]]
1468
- ]);
1469
- static #checkRequiredAriaProperties({
1470
- props,
1471
- effectiveRole
1472
- }) {
1473
- if (!effectiveRole) return NO_VIOLATIONS;
1474
- const required = _AriaPolicyEngine.#REQUIRED_PROPERTIES.get(effectiveRole);
1475
- if (!isNonNull(required)) return NO_VIOLATIONS;
1476
- const results = [];
1477
- iterate.forEach(required, (attr) => {
1478
- if (attr in props) return;
1479
- results.push({
1480
- valid: false,
1481
- fixable: false,
1482
- severity: "warning",
1483
- attribute: attr,
1484
- diagnostic: AriaDiagnostics.requiredProperty(attr, effectiveRole)
1485
- });
1486
- });
1487
- return results;
1653
+ static #requiredAriaPropertiesRule = {
1654
+ attributesByRole: REQUIRED_ARIA_PROPERTIES,
1655
+ diagnosticFor: (attribute, role) => AriaDiagnostics.requiredProperty(attribute, role)
1656
+ };
1657
+ static #checkRequiredAriaProperties(context) {
1658
+ return checkRequiredAttributes(_AriaPolicyEngine.#requiredAriaPropertiesRule, context);
1488
1659
  }
1489
- // Natively interactive HTML elements — always keyboard-reachable unless explicitly disabled.
1490
- static #INTERACTIVE_TAGS = /* @__PURE__ */ new Set([
1491
- "a",
1492
- "button",
1493
- "input",
1494
- "select",
1495
- "textarea"
1496
- ]);
1497
1660
  // WAI-ARIA 1.2 §6.6: aria-hidden="true" must not be placed on focusable elements.
1498
1661
  static #checkAriaHiddenOnFocusable({ tag, props }) {
1499
- if (props["aria-hidden"] !== "true" && props["aria-hidden"] !== true) return NO_VIOLATIONS;
1500
- const isInteractive = _AriaPolicyEngine.#INTERACTIVE_TAGS.has(tag);
1662
+ if (props["aria-hidden"] !== "true" && props["aria-hidden"] !== true) return NO_VIOLATIONS2;
1663
+ const isInteractive = INTERACTIVE_TAGS.has(tag);
1501
1664
  if (!isInteractive) {
1502
1665
  const tabindex = props.tabindex;
1503
1666
  const n = typeof tabindex === "number" ? tabindex : typeof tabindex === "string" ? parseInt(tabindex, 10) : NaN;
1504
- if (!Number.isFinite(n) || n < 0) return NO_VIOLATIONS;
1667
+ if (!Number.isFinite(n) || n < 0) return NO_VIOLATIONS2;
1505
1668
  }
1506
1669
  return [
1507
1670
  {
@@ -1520,7 +1683,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1520
1683
  props,
1521
1684
  effectiveRole
1522
1685
  }) {
1523
- if (effectiveRole !== "none" && effectiveRole !== "presentation") return NO_VIOLATIONS;
1686
+ if (effectiveRole !== "none" && effectiveRole !== "presentation") return NO_VIOLATIONS2;
1524
1687
  const results = [];
1525
1688
  iterate.forEachEntry(props, (key) => {
1526
1689
  if (!key.startsWith("aria-")) return;
@@ -1536,18 +1699,11 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1536
1699
  });
1537
1700
  return results;
1538
1701
  }
1539
- // WAI-ARIA live region roles and their implied aria-live politeness values.
1540
- static #LIVE_REGION_ROLES = /* @__PURE__ */ new Map([
1541
- ["alert", "assertive"],
1542
- ["status", "polite"],
1543
- ["log", "polite"],
1544
- ["timer", "off"]
1545
- ]);
1546
1702
  static #checkMissingLiveRegion({ effectiveRole, props }) {
1547
- if (!effectiveRole) return NO_VIOLATIONS;
1548
- const impliedLive = _AriaPolicyEngine.#LIVE_REGION_ROLES.get(effectiveRole);
1549
- if (!impliedLive) return NO_VIOLATIONS;
1550
- if ("aria-live" in props) return NO_VIOLATIONS;
1703
+ if (!effectiveRole) return NO_VIOLATIONS2;
1704
+ const impliedLive = LIVE_REGION_ROLES.get(effectiveRole);
1705
+ if (!impliedLive) return NO_VIOLATIONS2;
1706
+ if ("aria-live" in props) return NO_VIOLATIONS2;
1551
1707
  const injectLive = {
1552
1708
  kind: `injectLive:${effectiveRole}`,
1553
1709
  apply: (ctx) => ({
@@ -1566,20 +1722,13 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1566
1722
  }
1567
1723
  ];
1568
1724
  }
1569
- static #checkMissingAtomic({ effectiveRole, props }) {
1570
- if (!effectiveRole || !_AriaPolicyEngine.#LIVE_REGION_ROLES.has(effectiveRole))
1571
- return NO_VIOLATIONS;
1572
- if ("aria-atomic" in props) return NO_VIOLATIONS;
1573
- return [
1574
- {
1575
- valid: false,
1576
- fixable: false,
1577
- severity: "warning",
1578
- diagnostic: AriaDiagnostics.missingAtomic(effectiveRole)
1579
- }
1580
- ];
1725
+ static #missingAtomicRule = {
1726
+ attributesByRole: ATOMIC_REQUIREMENTS,
1727
+ diagnosticFor: (_attribute, role) => AriaDiagnostics.missingAtomic(role)
1728
+ };
1729
+ static #checkMissingAtomic(context) {
1730
+ return checkRequiredAttributes(_AriaPolicyEngine.#missingAtomicRule, context);
1581
1731
  }
1582
- static #VALID_RELEVANT_TOKENS = /* @__PURE__ */ new Set(["additions", "removals", "text", "all"]);
1583
1732
  // Custom fix rules passed via `options.rules` must be pure functions of (tag, props) — the cache
1584
1733
  // replays stored fixes against new prop objects, so fixes that close over external state will
1585
1734
  // produce inconsistent results on cache hits.
@@ -1593,10 +1742,10 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1593
1742
  };
1594
1743
  static #checkInvalidAriaRelevant({ props }) {
1595
1744
  const relevant = props["aria-relevant"];
1596
- if (relevant === void 0) return NO_VIOLATIONS;
1597
- if (typeof relevant !== "string") return NO_VIOLATIONS;
1745
+ if (relevant === void 0) return NO_VIOLATIONS2;
1746
+ if (typeof relevant !== "string") return NO_VIOLATIONS2;
1598
1747
  const tokens = relevant.trim().split(/\s+/);
1599
- const invalid = tokens.filter((t) => !_AriaPolicyEngine.#VALID_RELEVANT_TOKENS.has(t));
1748
+ const invalid = tokens.filter((t) => !VALID_RELEVANT_TOKENS.has(t));
1600
1749
  if (invalid.length > 0) {
1601
1750
  return [
1602
1751
  {
@@ -1621,7 +1770,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1621
1770
  }
1622
1771
  ];
1623
1772
  }
1624
- return NO_VIOLATIONS;
1773
+ return NO_VIOLATIONS2;
1625
1774
  }
1626
1775
  };
1627
1776
 
@@ -1941,6 +2090,425 @@ var readonlyProps = ({
1941
2090
  // ../core/src/html/evaluators.ts
1942
2091
  import { warnDiagnostics as warnDiagnostics2 } from "../_shared/diagnostics.js";
1943
2092
 
2093
+ // ../core/src/html/spec/vocabulary/input.ts
2094
+ var TEXT_INPUT_TYPES = ["text", "search", "url", "tel", "email", "password"];
2095
+ var NUMERIC_INPUT_TYPES = [
2096
+ "number",
2097
+ "range",
2098
+ "date",
2099
+ "month",
2100
+ "week",
2101
+ "time",
2102
+ "datetime-local"
2103
+ ];
2104
+ var HTML_INPUT_TYPES = /* @__PURE__ */ new Set([
2105
+ ...TEXT_INPUT_TYPES,
2106
+ ...NUMERIC_INPUT_TYPES,
2107
+ "checkbox",
2108
+ "radio",
2109
+ "file",
2110
+ "color",
2111
+ "hidden",
2112
+ "button",
2113
+ "submit",
2114
+ "reset",
2115
+ "image"
2116
+ ]);
2117
+
2118
+ // ../core/src/html/spec/attributes/input.ts
2119
+ var INPUT_ATTRIBUTE_TYPE_POLICIES = [
2120
+ { attribute: "checked", allowedTypes: ["checkbox", "radio"] },
2121
+ { attribute: "multiple", allowedTypes: ["email", "file"] },
2122
+ { attribute: "maxLength", allowedTypes: TEXT_INPUT_TYPES },
2123
+ { attribute: "minLength", allowedTypes: TEXT_INPUT_TYPES },
2124
+ { attribute: "pattern", allowedTypes: TEXT_INPUT_TYPES },
2125
+ { attribute: "min", allowedTypes: NUMERIC_INPUT_TYPES },
2126
+ { attribute: "max", allowedTypes: NUMERIC_INPUT_TYPES },
2127
+ { attribute: "step", allowedTypes: NUMERIC_INPUT_TYPES },
2128
+ { attribute: "accept", allowedTypes: ["file"] },
2129
+ { attribute: "capture", allowedTypes: ["file"] },
2130
+ { attribute: "size", allowedTypes: TEXT_INPUT_TYPES },
2131
+ { attribute: "alt", allowedTypes: ["image"] },
2132
+ { attribute: "height", allowedTypes: ["image"] },
2133
+ { attribute: "width", allowedTypes: ["image"] }
2134
+ ];
2135
+
2136
+ // ../core/src/html/spec/constraints/input.ts
2137
+ var REQUIRED_READONLY_CONFLICT = {
2138
+ props: ["required", "readOnly"],
2139
+ diagnostic: () => InputAccessibilityDiagnostics.requiredReadOnlyConflict()
2140
+ };
2141
+ var INPUT_MUTUALLY_EXCLUSIVE_POLICIES = [
2142
+ REQUIRED_READONLY_CONFLICT
2143
+ ];
2144
+
2145
+ // ../core/src/html/spec/validators/attribute-type-validator.ts
2146
+ var DEFAULT_INPUT_TYPE = "text";
2147
+ function omit(props, key) {
2148
+ const next = { ...props };
2149
+ delete next[key];
2150
+ return next;
2151
+ }
2152
+ function removeAttributeFix(attribute) {
2153
+ return {
2154
+ kind: `removeAttribute:${attribute}`,
2155
+ apply: ({ props }) => {
2156
+ if (!(attribute in props)) return { applied: false, next: props };
2157
+ return { applied: true, next: omit(props, attribute), previous: props };
2158
+ }
2159
+ };
2160
+ }
2161
+ function createInputAttributeTypeRule({
2162
+ attribute,
2163
+ allowedTypes
2164
+ }) {
2165
+ const rule = ({ tag, props }) => {
2166
+ if (tag !== "input" || !(attribute in props)) return [];
2167
+ const type = typeof props.type === "string" ? props.type : DEFAULT_INPUT_TYPE;
2168
+ if (allowedTypes.includes(type)) return [];
2169
+ const diagnostic = HtmlDiagnostics.input.attributeIgnoredForType(attribute, type, allowedTypes);
2170
+ return [
2171
+ {
2172
+ valid: false,
2173
+ fixable: true,
2174
+ severity: diagnostic.severity,
2175
+ fix: removeAttributeFix(attribute),
2176
+ diagnostic
2177
+ }
2178
+ ];
2179
+ };
2180
+ return Object.assign(rule, { readsProps: ["type", attribute], tags: ["input"] });
2181
+ }
2182
+
2183
+ // ../core/src/html/spec/validators/mutually-exclusive-validator.ts
2184
+ function createMutuallyExclusiveRule({
2185
+ props: conflictingProps,
2186
+ diagnostic: createDiagnostic
2187
+ }) {
2188
+ const [first, second] = conflictingProps;
2189
+ const rule = ({ tag, props }) => {
2190
+ if (tag !== "input" || !props[first] || !props[second]) return [];
2191
+ const diagnostic = createDiagnostic();
2192
+ return [{ valid: false, fixable: false, severity: diagnostic.severity, diagnostic }];
2193
+ };
2194
+ return Object.assign(rule, { readsProps: conflictingProps, tags: ["input"] });
2195
+ }
2196
+
2197
+ // ../core/src/html/input-rules.ts
2198
+ var policyByAttribute = Object.fromEntries(
2199
+ INPUT_ATTRIBUTE_TYPE_POLICIES.map((policy) => [policy.attribute, policy])
2200
+ );
2201
+ function policyFor(attribute) {
2202
+ return policyByAttribute[attribute];
2203
+ }
2204
+ var supportedInputTypeRule = Object.assign(
2205
+ ({ tag, props }) => {
2206
+ if (tag !== "input" || typeof props.type !== "string") return [];
2207
+ const type = props.type;
2208
+ if (HTML_INPUT_TYPES.has(type)) return [];
2209
+ const diagnostic = HtmlDiagnostics.input.unsupportedType(type);
2210
+ return [
2211
+ {
2212
+ valid: false,
2213
+ fixable: false,
2214
+ severity: diagnostic.severity,
2215
+ diagnostic
2216
+ }
2217
+ ];
2218
+ },
2219
+ { readsProps: ["type"], tags: ["input"] }
2220
+ );
2221
+ var checkedRequiresCheckableTypeRule = createInputAttributeTypeRule(policyFor("checked"));
2222
+ var multipleRequiresSupportedTypeRule = createInputAttributeTypeRule(policyFor("multiple"));
2223
+ var maxLengthRequiresTextTypeRule = createInputAttributeTypeRule(policyFor("maxLength"));
2224
+ var minLengthRequiresTextTypeRule = createInputAttributeTypeRule(policyFor("minLength"));
2225
+ var patternRequiresTextTypeRule = createInputAttributeTypeRule(policyFor("pattern"));
2226
+ var minRequiresNumericTypeRule = createInputAttributeTypeRule(policyFor("min"));
2227
+ var maxRequiresNumericTypeRule = createInputAttributeTypeRule(policyFor("max"));
2228
+ var stepRequiresNumericTypeRule = createInputAttributeTypeRule(policyFor("step"));
2229
+ var acceptRequiresFileTypeRule = createInputAttributeTypeRule(policyFor("accept"));
2230
+ var captureRequiresFileTypeRule = createInputAttributeTypeRule(policyFor("capture"));
2231
+ var sizeRequiresTextTypeRule = createInputAttributeTypeRule(policyFor("size"));
2232
+ var altRequiresImageTypeRule = createInputAttributeTypeRule(policyFor("alt"));
2233
+ var heightRequiresImageTypeRule = createInputAttributeTypeRule(policyFor("height"));
2234
+ var widthRequiresImageTypeRule = createInputAttributeTypeRule(policyFor("width"));
2235
+ var inputAccessibleNameRule = Object.assign(
2236
+ ({ tag, props }) => {
2237
+ if (tag !== "input" || props.type === "hidden") return [];
2238
+ if ("aria-label" in props || "aria-labelledby" in props) return [];
2239
+ const hasPlaceholder = typeof props.placeholder === "string" && props.placeholder.length > 0;
2240
+ const diagnostic = hasPlaceholder ? InputAccessibilityDiagnostics.placeholderIsNotLabel() : InputAccessibilityDiagnostics.missingAccessibleName();
2241
+ return [{ valid: false, fixable: false, severity: diagnostic.severity, diagnostic }];
2242
+ },
2243
+ {
2244
+ readsProps: ["type", "aria-label", "aria-labelledby", "placeholder"],
2245
+ tags: ["input"]
2246
+ }
2247
+ );
2248
+ var PASSWORD_AUTOCOMPLETE_VALUES = ["current-password", "new-password"];
2249
+ var passwordAutocompleteRule = Object.assign(
2250
+ ({ tag, props }) => {
2251
+ if (tag !== "input" || props.type !== "password") return [];
2252
+ const autoComplete = props.autoComplete;
2253
+ const tokens = typeof autoComplete === "string" ? autoComplete.split(" ") : [];
2254
+ if (PASSWORD_AUTOCOMPLETE_VALUES.some((value) => tokens.includes(value))) return [];
2255
+ const diagnostic = InputAccessibilityDiagnostics.passwordMissingAutocomplete();
2256
+ return [{ valid: false, fixable: false, severity: diagnostic.severity, diagnostic }];
2257
+ },
2258
+ { readsProps: ["type", "autoComplete"], tags: ["input"] }
2259
+ );
2260
+ var requiredReadOnlyConflictRule = createMutuallyExclusiveRule(REQUIRED_READONLY_CONFLICT);
2261
+ var INPUT_RULES = [
2262
+ supportedInputTypeRule,
2263
+ checkedRequiresCheckableTypeRule,
2264
+ multipleRequiresSupportedTypeRule,
2265
+ maxLengthRequiresTextTypeRule,
2266
+ minLengthRequiresTextTypeRule,
2267
+ patternRequiresTextTypeRule,
2268
+ minRequiresNumericTypeRule,
2269
+ maxRequiresNumericTypeRule,
2270
+ stepRequiresNumericTypeRule,
2271
+ acceptRequiresFileTypeRule,
2272
+ captureRequiresFileTypeRule,
2273
+ sizeRequiresTextTypeRule,
2274
+ altRequiresImageTypeRule,
2275
+ heightRequiresImageTypeRule,
2276
+ widthRequiresImageTypeRule,
2277
+ inputAccessibleNameRule,
2278
+ passwordAutocompleteRule,
2279
+ requiredReadOnlyConflictRule
2280
+ ];
2281
+
2282
+ // ../core/src/html/spec/types.ts
2283
+ function definePropRolePolicy(prop, map2, fallback) {
2284
+ return { kind: "byProp", prop, map: map2, fallback };
2285
+ }
2286
+ function resolveAllowedRoles(spec, props) {
2287
+ const policy = spec.allowedRoles;
2288
+ if (!policy) return void 0;
2289
+ switch (policy.kind) {
2290
+ case "fixed":
2291
+ return policy.roles;
2292
+ case "byProp": {
2293
+ const value = typeof props[policy.prop] === "string" ? props[policy.prop] : policy.fallback;
2294
+ return policy.map[value];
2295
+ }
2296
+ case "dynamic":
2297
+ return policy.resolve({ props });
2298
+ }
2299
+ }
2300
+
2301
+ // ../core/src/html/spec/roles/input.ts
2302
+ var ALLOWED_INPUT_ROLES = {
2303
+ checkbox: ["menuitemcheckbox", "option", "switch", "button"],
2304
+ radio: ["menuitemradio"],
2305
+ range: [],
2306
+ number: [],
2307
+ search: ["combobox"],
2308
+ text: ["combobox", "searchbox", "spinbutton"],
2309
+ email: ["combobox"],
2310
+ tel: ["combobox"],
2311
+ url: ["combobox"],
2312
+ button: [
2313
+ "link",
2314
+ "menuitem",
2315
+ "menuitemcheckbox",
2316
+ "menuitemradio",
2317
+ "option",
2318
+ "radio",
2319
+ "switch",
2320
+ "tab"
2321
+ ],
2322
+ submit: [
2323
+ "link",
2324
+ "menuitem",
2325
+ "menuitemcheckbox",
2326
+ "menuitemradio",
2327
+ "option",
2328
+ "radio",
2329
+ "switch",
2330
+ "tab"
2331
+ ],
2332
+ reset: [
2333
+ "link",
2334
+ "menuitem",
2335
+ "menuitemcheckbox",
2336
+ "menuitemradio",
2337
+ "option",
2338
+ "radio",
2339
+ "switch",
2340
+ "tab"
2341
+ ],
2342
+ image: [
2343
+ "link",
2344
+ "menuitem",
2345
+ "menuitemcheckbox",
2346
+ "menuitemradio",
2347
+ "option",
2348
+ "radio",
2349
+ "switch",
2350
+ "tab"
2351
+ ],
2352
+ hidden: []
2353
+ };
2354
+
2355
+ // ../core/src/html/spec/elements/input.ts
2356
+ var inputElementSpec = {
2357
+ tag: "input",
2358
+ allowedRoles: definePropRolePolicy("type", ALLOWED_INPUT_ROLES, "text"),
2359
+ attributes: INPUT_ATTRIBUTE_TYPE_POLICIES,
2360
+ mutuallyExclusive: INPUT_MUTUALLY_EXCLUSIVE_POLICIES
2361
+ };
2362
+
2363
+ // ../core/src/html/spec/roles/img.ts
2364
+ var IMG_NAMED_ROLES = [
2365
+ "button",
2366
+ "checkbox",
2367
+ "link",
2368
+ "menuitem",
2369
+ "menuitemcheckbox",
2370
+ "menuitemradio",
2371
+ "option",
2372
+ "progressbar",
2373
+ "scrollbar",
2374
+ "separator",
2375
+ "slider",
2376
+ "switch",
2377
+ "tab",
2378
+ "treeitem"
2379
+ ];
2380
+
2381
+ // ../core/src/html/spec/elements/img.ts
2382
+ var imgElementSpec = {
2383
+ tag: "img",
2384
+ allowedRoles: {
2385
+ kind: "dynamic",
2386
+ resolve: ({ props }) => props.alt === "" ? [] : IMG_NAMED_ROLES
2387
+ }
2388
+ };
2389
+
2390
+ // ../core/src/html/spec/roles/table.ts
2391
+ var ALLOWED_TABLE_ROLES = ["grid", "treegrid"];
2392
+
2393
+ // ../core/src/html/spec/elements/table.ts
2394
+ var tableElementSpec = {
2395
+ tag: "table",
2396
+ allowedRoles: { kind: "fixed", roles: ALLOWED_TABLE_ROLES }
2397
+ };
2398
+
2399
+ // ../core/src/html/role-restrictions.ts
2400
+ var ALLOWED_ROLES = {
2401
+ article: ["application", "document", "feed", "main", "none", "presentation", "region"],
2402
+ aside: ["feed", "none", "presentation", "region", "search"],
2403
+ footer: ["group", "none", "presentation"],
2404
+ header: ["group", "none", "presentation"],
2405
+ main: [],
2406
+ nav: [],
2407
+ a: [
2408
+ "button",
2409
+ "checkbox",
2410
+ "menuitem",
2411
+ "menuitemcheckbox",
2412
+ "menuitemradio",
2413
+ "option",
2414
+ "radio",
2415
+ "switch",
2416
+ "tab",
2417
+ "treeitem"
2418
+ ],
2419
+ button: [
2420
+ "checkbox",
2421
+ "link",
2422
+ "menuitem",
2423
+ "menuitemcheckbox",
2424
+ "menuitemradio",
2425
+ "option",
2426
+ "radio",
2427
+ "switch",
2428
+ "tab"
2429
+ ],
2430
+ select: ["menu"],
2431
+ h1: ["tab", "presentation", "none"],
2432
+ h2: ["tab", "presentation", "none"],
2433
+ h3: ["tab", "presentation", "none"],
2434
+ h4: ["tab", "presentation", "none"],
2435
+ h5: ["tab", "presentation", "none"],
2436
+ h6: ["tab", "presentation", "none"],
2437
+ ul: [
2438
+ "directory",
2439
+ "group",
2440
+ "listbox",
2441
+ "menu",
2442
+ "menubar",
2443
+ "radiogroup",
2444
+ "tablist",
2445
+ "toolbar",
2446
+ "tree"
2447
+ ],
2448
+ ol: [
2449
+ "directory",
2450
+ "group",
2451
+ "listbox",
2452
+ "menu",
2453
+ "menubar",
2454
+ "radiogroup",
2455
+ "tablist",
2456
+ "toolbar",
2457
+ "tree"
2458
+ ],
2459
+ li: [
2460
+ "menuitem",
2461
+ "menuitemcheckbox",
2462
+ "menuitemradio",
2463
+ "option",
2464
+ "none",
2465
+ "presentation",
2466
+ "radio",
2467
+ "separator",
2468
+ "tab",
2469
+ "treeitem"
2470
+ ],
2471
+ dialog: ["alertdialog"],
2472
+ fieldset: ["none", "presentation", "radiogroup"]
2473
+ };
2474
+ var ELEMENT_SPECS = {
2475
+ input: inputElementSpec,
2476
+ img: imgElementSpec,
2477
+ table: tableElementSpec
2478
+ };
2479
+ function getAllowedRoles(tag, props) {
2480
+ const spec = ELEMENT_SPECS[tag];
2481
+ if (spec) return resolveAllowedRoles(spec, props);
2482
+ return ALLOWED_ROLES[tag];
2483
+ }
2484
+ var removeRoleFix = {
2485
+ kind: "removeRole",
2486
+ apply: ({ props }) => {
2487
+ if (!("role" in props)) return { applied: false, next: props };
2488
+ const { role: _role, ...rest } = props;
2489
+ return { applied: true, next: rest, previous: props };
2490
+ }
2491
+ };
2492
+ var roleNotPermittedRule = Object.assign(
2493
+ ({ tag, props, implicitRole }) => {
2494
+ const role = props.role;
2495
+ if (typeof role !== "string" || role.length === 0 || role === implicitRole) return [];
2496
+ const allowed = getAllowedRoles(tag, props);
2497
+ if (allowed === void 0 || allowed.includes(role)) return [];
2498
+ const diagnostic = HtmlDiagnostics.roleNotPermitted(tag, role, allowed);
2499
+ return [
2500
+ {
2501
+ valid: false,
2502
+ fixable: true,
2503
+ severity: diagnostic.severity,
2504
+ fix: removeRoleFix,
2505
+ diagnostic
2506
+ }
2507
+ ];
2508
+ },
2509
+ { readsProps: ["role", "type", "alt"] }
2510
+ );
2511
+
1944
2512
  // ../core/src/html/aria-rules.ts
1945
2513
  var LANDMARK_TAG_SET = /* @__PURE__ */ new Set(["article", "aside", "footer", "header", "main", "nav"]);
1946
2514
  var removeLandmarkRoleOverride = {
@@ -1951,20 +2519,24 @@ var removeLandmarkRoleOverride = {
1951
2519
  return { applied: true, next: rest, previous: props };
1952
2520
  }
1953
2521
  };
1954
- function landmarkRoleRule({ tag, props, implicitRole }) {
1955
- if (!LANDMARK_TAG_SET.has(tag) || !implicitRole) return [];
1956
- const role = props.role;
1957
- if (!role || role === implicitRole) return [];
1958
- return [
1959
- {
1960
- valid: false,
1961
- fixable: true,
1962
- severity: "error",
1963
- fix: removeLandmarkRoleOverride,
1964
- diagnostic: HtmlDiagnostics.landmarkRoleOverride(tag, implicitRole, role)
1965
- }
1966
- ];
1967
- }
2522
+ var landmarkRoleRule = Object.assign(
2523
+ ({ tag, props, implicitRole }) => {
2524
+ if (!LANDMARK_TAG_SET.has(tag) || !implicitRole) return [];
2525
+ const role = props.role;
2526
+ if (!role || role === implicitRole) return [];
2527
+ const diagnostic = HtmlDiagnostics.landmarkRoleOverride(tag, implicitRole, role);
2528
+ return [
2529
+ {
2530
+ valid: false,
2531
+ fixable: true,
2532
+ severity: diagnostic.severity,
2533
+ fix: removeLandmarkRoleOverride,
2534
+ diagnostic
2535
+ }
2536
+ ];
2537
+ },
2538
+ { tags: [...LANDMARK_TAG_SET] }
2539
+ );
1968
2540
  function requireAccessibleName({ tag, props }) {
1969
2541
  if ("aria-label" in props || "aria-labelledby" in props) return [];
1970
2542
  return [
@@ -1977,11 +2549,19 @@ function requireAccessibleName({ tag, props }) {
1977
2549
  ];
1978
2550
  }
1979
2551
  var NAMED_LANDMARK_TAGS = /* @__PURE__ */ new Set(["nav", "aside"]);
1980
- function landmarkNameAdvisory(ctx) {
1981
- if (!ctx.implicitRole || !NAMED_LANDMARK_TAGS.has(ctx.tag)) return [];
1982
- return requireAccessibleName(ctx);
1983
- }
1984
- var HTML_ARIA_RULES = [landmarkRoleRule, landmarkNameAdvisory];
2552
+ var landmarkNameAdvisory = Object.assign(
2553
+ (ctx) => {
2554
+ if (!ctx.implicitRole || !NAMED_LANDMARK_TAGS.has(ctx.tag)) return [];
2555
+ return requireAccessibleName(ctx);
2556
+ },
2557
+ { tags: [...NAMED_LANDMARK_TAGS] }
2558
+ );
2559
+ var HTML_ARIA_RULES = [
2560
+ landmarkRoleRule,
2561
+ landmarkNameAdvisory,
2562
+ roleNotPermittedRule,
2563
+ ...INPUT_RULES
2564
+ ];
1985
2565
 
1986
2566
  // ../core/src/html/contracts.ts
1987
2567
  import { warnDiagnostics } from "../_shared/diagnostics.js";
@@ -2076,6 +2656,62 @@ var figureContract = contract([
2076
2656
  ]);
2077
2657
  var detailsContract = firstChildContract("summary", "summary");
2078
2658
  var fieldsetContract = firstChildContract("legend", "legend");
2659
+ var objectContract = contract([
2660
+ { name: "param", match: isTag("param") },
2661
+ { name: "content", match: isOpenContent("param") }
2662
+ ]);
2663
+ var INTERACTIVE_CONTENT_TAGS = ["a", "button", "input", "select", "textarea", "label"];
2664
+ var buttonContract = closedContract([
2665
+ { name: "content", match: isOpenContent(...INTERACTIVE_CONTENT_TAGS) }
2666
+ ]);
2667
+ var anchorContract = closedContract([
2668
+ { name: "content", match: isOpenContent(...INTERACTIVE_CONTENT_TAGS) }
2669
+ ]);
2670
+ var LABELABLE_TAGS = [
2671
+ "button",
2672
+ "input",
2673
+ "meter",
2674
+ "output",
2675
+ "progress",
2676
+ "select",
2677
+ "textarea"
2678
+ ];
2679
+ var labelContract = contract([
2680
+ { name: "control", match: isTag(...LABELABLE_TAGS), cardinality: { max: 1 } }
2681
+ ]);
2682
+ var P_BLOCKED_TAGS = [
2683
+ "address",
2684
+ "article",
2685
+ "aside",
2686
+ "blockquote",
2687
+ "details",
2688
+ "dialog",
2689
+ "div",
2690
+ "dl",
2691
+ "fieldset",
2692
+ "figure",
2693
+ "footer",
2694
+ "form",
2695
+ "h1",
2696
+ "h2",
2697
+ "h3",
2698
+ "h4",
2699
+ "h5",
2700
+ "h6",
2701
+ "header",
2702
+ "hr",
2703
+ "main",
2704
+ "nav",
2705
+ "ol",
2706
+ "p",
2707
+ "pre",
2708
+ "section",
2709
+ "table",
2710
+ "ul"
2711
+ ];
2712
+ var pContract = closedContract([
2713
+ { name: "content", match: isOpenContent(...P_BLOCKED_TAGS) }
2714
+ ]);
2079
2715
  var mediaContract = contract([
2080
2716
  { name: "source", match: isTag("source") },
2081
2717
  { name: "track", match: isTag("track") },
@@ -2130,6 +2766,11 @@ var htmlContracts = {
2130
2766
  details: detailsContract,
2131
2767
  fieldset: fieldsetContract,
2132
2768
  dialog: dialogContract,
2769
+ object: objectContract,
2770
+ button: buttonContract,
2771
+ a: anchorContract,
2772
+ label: labelContract,
2773
+ p: pContract,
2133
2774
  head: headContract,
2134
2775
  html: htmlContract
2135
2776
  };
@@ -2442,21 +3083,21 @@ function validateRenderProps(diagnostics, options, props, recipeKey) {
2442
3083
  import { throwDiagnostics } from "../_shared/diagnostics.js";
2443
3084
 
2444
3085
  // ../core/src/factory/plugin-diagnostics.ts
2445
- import { DiagnosticCategory as DiagnosticCategory4, DiagnosticCode as DiagnosticCode4 } from "../_shared/diagnostics.js";
3086
+ import { DiagnosticCategory as DiagnosticCategory5, DiagnosticCode as DiagnosticCode5 } from "../_shared/diagnostics.js";
2446
3087
  var PluginDiagnostics = {
2447
3088
  invalidShape(received) {
2448
3089
  const got = received === null ? "null" : typeof received;
2449
3090
  return {
2450
- code: DiagnosticCode4.PluginInvalidShape,
2451
- category: DiagnosticCategory4.Internal,
3091
+ code: DiagnosticCode5.PluginInvalidShape,
3092
+ category: DiagnosticCategory5.Internal,
2452
3093
  message: `[praxis-kit] Plugin factory must return an object with a 'pipeline' function. Got: ${got}.`
2453
3094
  };
2454
3095
  },
2455
3096
  pipelineReturnType(received) {
2456
3097
  const got = received === null ? "null" : Array.isArray(received) ? "array" : typeof received;
2457
3098
  return {
2458
- code: DiagnosticCode4.PluginPipelineReturnType,
2459
- category: DiagnosticCategory4.Internal,
3099
+ code: DiagnosticCode5.PluginPipelineReturnType,
3100
+ category: DiagnosticCategory5.Internal,
2460
3101
  message: `[praxis-kit] Plugin pipeline must return a string. Got: ${got}.`
2461
3102
  };
2462
3103
  }