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.
@@ -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,34 +919,148 @@ 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
+ }
903
974
  }
904
975
  };
905
976
 
906
- // ../../lib/contract/src/diagnostics/slot.ts
977
+ // ../../lib/contract/src/diagnostics/input-accessibility.ts
907
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
+ });
1038
+ }
1039
+ };
1040
+
1041
+ // ../../lib/contract/src/diagnostics/slot.ts
1042
+ import { DiagnosticCategory as DiagnosticCategory5, DiagnosticCode as DiagnosticCode5 } from "../_shared/diagnostics.js";
908
1043
  var SlotDiagnostics = {
909
1044
  exclusive(name) {
910
1045
  return {
911
- code: DiagnosticCode4.SlotExclusive,
912
- category: DiagnosticCategory4.Contract,
1046
+ code: DiagnosticCode5.SlotExclusive,
1047
+ category: DiagnosticCategory5.Contract,
913
1048
  component: name,
914
1049
  message: `${name}: "as" and "asChild" are mutually exclusive`
915
1050
  };
916
1051
  },
917
1052
  singleChildRequired(name, elementTerm) {
918
1053
  return {
919
- code: DiagnosticCode4.SlotSingleChild,
920
- category: DiagnosticCategory4.Contract,
1054
+ code: DiagnosticCode5.SlotSingleChild,
1055
+ category: DiagnosticCategory5.Contract,
921
1056
  component: name,
922
1057
  message: `${name}: asChild requires a ${elementTerm} child`
923
1058
  };
924
1059
  },
925
1060
  singleChildExceeded(name, elementTerm, count) {
926
1061
  return {
927
- code: DiagnosticCode4.SlotSingleChild,
928
- category: DiagnosticCategory4.Contract,
1062
+ code: DiagnosticCode5.SlotSingleChild,
1063
+ category: DiagnosticCategory5.Contract,
929
1064
  component: name,
930
1065
  message: `${name}: asChild requires exactly one ${elementTerm} child, got ${count}`
931
1066
  };
@@ -933,16 +1068,16 @@ var SlotDiagnostics = {
933
1068
  discardedChildren(name, elementTerm, count) {
934
1069
  const suffix = count === 1 ? "" : "ren";
935
1070
  return {
936
- code: DiagnosticCode4.SlotDiscardedChildren,
937
- category: DiagnosticCategory4.Contract,
1071
+ code: DiagnosticCode5.SlotDiscardedChildren,
1072
+ category: DiagnosticCategory5.Contract,
938
1073
  component: name,
939
1074
  message: `${name}: asChild discarded ${count} non-element child${suffix} \u2014 only ${elementTerm}s are valid asChild children.`
940
1075
  };
941
1076
  },
942
1077
  renderFnRequired(name, received) {
943
1078
  return {
944
- code: DiagnosticCode4.SlotRenderFn,
945
- category: DiagnosticCategory4.Contract,
1079
+ code: DiagnosticCode5.SlotRenderFn,
1080
+ category: DiagnosticCategory5.Contract,
946
1081
  component: name,
947
1082
  message: `${name}: asChild requires a render function as children, got ${received}`
948
1083
  };
@@ -971,8 +1106,132 @@ var InvariantBase = class {
971
1106
  }
972
1107
  };
973
1108
 
974
- // ../../lib/contract/src/aria/polymorphic-validator.ts
1109
+ // ../../lib/contract/src/aria/spec/roles/required-properties.ts
1110
+ var REQUIRED_ARIA_PROPERTIES = {
1111
+ combobox: ["aria-expanded"],
1112
+ option: ["aria-selected"],
1113
+ slider: ["aria-valuenow"],
1114
+ scrollbar: ["aria-controls", "aria-valuenow"],
1115
+ spinbutton: ["aria-valuenow"]
1116
+ };
1117
+
1118
+ // ../../lib/contract/src/aria/spec/roles/name-required.ts
1119
+ var NAME_REQUIRED_ROLES = /* @__PURE__ */ new Set(["img"]);
1120
+
1121
+ // ../../lib/contract/src/aria/spec/validators/required-properties-validator.ts
975
1122
  var NO_VIOLATIONS = [{ valid: true }];
1123
+ function requiredAttributeByRole(roles, attribute) {
1124
+ return Object.fromEntries([...roles].map((role) => [role, [attribute]]));
1125
+ }
1126
+ function checkRequiredAttributes(requirement, { props, effectiveRole }) {
1127
+ if (!effectiveRole) return NO_VIOLATIONS;
1128
+ const requiredAttributes = requirement.attributesByRole[effectiveRole];
1129
+ if (!requiredAttributes) return NO_VIOLATIONS;
1130
+ const results = [];
1131
+ for (const attribute of requiredAttributes) {
1132
+ if (attribute in props) continue;
1133
+ results.push({
1134
+ valid: false,
1135
+ fixable: false,
1136
+ severity: "warning",
1137
+ attribute,
1138
+ diagnostic: requirement.diagnosticFor(attribute, effectiveRole)
1139
+ });
1140
+ }
1141
+ return results;
1142
+ }
1143
+
1144
+ // ../../lib/contract/src/aria/spec/roles/live-region.ts
1145
+ var LIVE_REGION_ROLES = /* @__PURE__ */ new Map([
1146
+ ["alert", "assertive"],
1147
+ ["status", "polite"],
1148
+ ["log", "polite"],
1149
+ ["timer", "off"]
1150
+ ]);
1151
+ var ATOMIC_REQUIREMENTS = requiredAttributeByRole(LIVE_REGION_ROLES.keys(), "aria-atomic");
1152
+
1153
+ // ../../lib/contract/src/aria/spec/attributes/aria-value-types.ts
1154
+ var ARIA_VALUE_TYPES = /* @__PURE__ */ new Map([
1155
+ // Boolean (true | false)
1156
+ ["aria-atomic", { kind: "boolean" }],
1157
+ ["aria-busy", { kind: "boolean" }],
1158
+ ["aria-disabled", { kind: "boolean" }],
1159
+ ["aria-expanded", { kind: "boolean" }],
1160
+ ["aria-hidden", { kind: "boolean" }],
1161
+ ["aria-modal", { kind: "boolean" }],
1162
+ ["aria-multiline", { kind: "boolean" }],
1163
+ ["aria-multiselectable", { kind: "boolean" }],
1164
+ ["aria-readonly", { kind: "boolean" }],
1165
+ ["aria-required", { kind: "boolean" }],
1166
+ ["aria-selected", { kind: "boolean" }],
1167
+ // Tristate (true | false | mixed)
1168
+ ["aria-checked", { kind: "tristate" }],
1169
+ ["aria-pressed", { kind: "tristate" }],
1170
+ // Numeric (any finite number)
1171
+ ["aria-valuenow", { kind: "number" }],
1172
+ ["aria-valuemin", { kind: "number" }],
1173
+ ["aria-valuemax", { kind: "number" }],
1174
+ // Integer with optional range
1175
+ ["aria-level", { kind: "integer", min: 1, max: 6 }],
1176
+ ["aria-posinset", { kind: "integer", min: 1 }],
1177
+ ["aria-setsize", { kind: "integer", min: -1 }],
1178
+ ["aria-rowcount", { kind: "integer", min: -1 }],
1179
+ ["aria-colcount", { kind: "integer", min: -1 }],
1180
+ ["aria-rowindex", { kind: "integer", min: 1 }],
1181
+ ["aria-colindex", { kind: "integer", min: 1 }],
1182
+ ["aria-rowspan", { kind: "integer", min: 0 }],
1183
+ ["aria-colspan", { kind: "integer", min: 0 }],
1184
+ // Enum (specific allowed tokens)
1185
+ ["aria-autocomplete", { kind: "enum", values: /* @__PURE__ */ new Set(["inline", "list", "both", "none"]) }],
1186
+ [
1187
+ "aria-current",
1188
+ {
1189
+ kind: "enum",
1190
+ values: /* @__PURE__ */ new Set(["page", "step", "location", "date", "time", "true", "false"])
1191
+ }
1192
+ ],
1193
+ [
1194
+ "aria-haspopup",
1195
+ {
1196
+ kind: "enum",
1197
+ values: /* @__PURE__ */ new Set(["false", "true", "menu", "listbox", "tree", "grid", "dialog"])
1198
+ }
1199
+ ],
1200
+ ["aria-invalid", { kind: "enum", values: /* @__PURE__ */ new Set(["grammar", "false", "spelling", "true"]) }],
1201
+ ["aria-live", { kind: "enum", values: /* @__PURE__ */ new Set(["assertive", "off", "polite"]) }],
1202
+ ["aria-orientation", { kind: "enum", values: /* @__PURE__ */ new Set(["horizontal", "vertical", "undefined"]) }],
1203
+ ["aria-sort", { kind: "enum", values: /* @__PURE__ */ new Set(["ascending", "descending", "none", "other"]) }]
1204
+ ]);
1205
+
1206
+ // ../../lib/contract/src/aria/spec/attributes/aria-relevant-tokens.ts
1207
+ var VALID_RELEVANT_TOKENS = /* @__PURE__ */ new Set([
1208
+ "additions",
1209
+ "removals",
1210
+ "text",
1211
+ "all"
1212
+ ]);
1213
+
1214
+ // ../../lib/contract/src/aria/spec/elements/heading-implicit-levels.ts
1215
+ var HEADING_IMPLICIT_LEVELS = /* @__PURE__ */ new Map([
1216
+ ["h1", 1],
1217
+ ["h2", 2],
1218
+ ["h3", 3],
1219
+ ["h4", 4],
1220
+ ["h5", 5],
1221
+ ["h6", 6]
1222
+ ]);
1223
+
1224
+ // ../../lib/contract/src/aria/spec/elements/interactive-tags.ts
1225
+ var INTERACTIVE_TAGS = /* @__PURE__ */ new Set([
1226
+ "a",
1227
+ "button",
1228
+ "input",
1229
+ "select",
1230
+ "textarea"
1231
+ ]);
1232
+
1233
+ // ../../lib/contract/src/aria/polymorphic-validator.ts
1234
+ var NO_VIOLATIONS2 = [{ valid: true }];
976
1235
  function isIntrinsicTag(tag) {
977
1236
  return isString(tag);
978
1237
  }
@@ -1004,7 +1263,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1004
1263
  tag,
1005
1264
  role: "",
1006
1265
  attribute: void 0,
1007
- severity: "warning",
1266
+ severity: d.severity,
1008
1267
  phase: "evaluate"
1009
1268
  }
1010
1269
  ]
@@ -1034,6 +1293,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1034
1293
  const violations = [];
1035
1294
  const fixes = [];
1036
1295
  iterate.forEach(rules, (rule) => {
1296
+ if (isNonNull(rule.tags) && !rule.tags.includes(context.tag)) return;
1037
1297
  iterate.forEach(rule(context), (result) => {
1038
1298
  if (result.valid) return;
1039
1299
  const {
@@ -1059,7 +1319,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1059
1319
  return { violations, fixes };
1060
1320
  }
1061
1321
  static #getRules(context) {
1062
- if (_AriaPolicyEngine.#hasRole(context.props) || isNonNull(context.effectiveRole) && _AriaPolicyEngine.#LIVE_REGION_ROLES.has(context.effectiveRole)) {
1322
+ if (_AriaPolicyEngine.#hasRole(context.props) || isNonNull(context.effectiveRole) && LIVE_REGION_ROLES.has(context.effectiveRole)) {
1063
1323
  return _AriaPolicyEngine.#pipeline;
1064
1324
  }
1065
1325
  return _AriaPolicyEngine.#implicitOnlyRules;
@@ -1261,44 +1521,47 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1261
1521
  implicitRole
1262
1522
  }) {
1263
1523
  const role = props.role;
1264
- if (!implicitRole || !role || role === implicitRole) return NO_VIOLATIONS;
1524
+ if (!implicitRole || !role || role === implicitRole) return NO_VIOLATIONS2;
1265
1525
  if (isStrongImplicitRole(tag) && role === "region") {
1526
+ const diagnostic = HtmlDiagnostics.implicitRoleOverride(tag, implicitRole, role);
1266
1527
  return [
1267
1528
  {
1268
1529
  valid: false,
1269
1530
  fixable: true,
1270
- severity: "error",
1531
+ severity: diagnostic.severity,
1271
1532
  fix: _AriaPolicyEngine.#removeRole,
1272
- diagnostic: HtmlDiagnostics.implicitRoleOverride(tag, implicitRole, role)
1533
+ diagnostic
1273
1534
  }
1274
1535
  ];
1275
1536
  }
1276
- return NO_VIOLATIONS;
1537
+ return NO_VIOLATIONS2;
1277
1538
  }
1278
1539
  static #checkRedundantRole({ tag, props, implicitRole }) {
1279
1540
  const role = props.role;
1280
- if (!implicitRole || !role || role !== implicitRole) return NO_VIOLATIONS;
1541
+ if (!implicitRole || !role || role !== implicitRole) return NO_VIOLATIONS2;
1542
+ const diagnostic = HtmlDiagnostics.implicitRoleRedundant(tag, implicitRole);
1281
1543
  return [
1282
1544
  {
1283
1545
  valid: false,
1284
1546
  fixable: true,
1285
- severity: "warning",
1547
+ severity: diagnostic.severity,
1286
1548
  fix: _AriaPolicyEngine.#removeRole,
1287
- diagnostic: HtmlDiagnostics.implicitRoleRedundant(tag, implicitRole)
1549
+ diagnostic
1288
1550
  }
1289
1551
  ];
1290
1552
  }
1291
1553
  static #checkStandaloneRegion({ tag, props, implicitRole }) {
1292
1554
  const role = props.role;
1293
- if (role !== "region") return NO_VIOLATIONS;
1294
- if (!isStandaloneTag(tag)) return NO_VIOLATIONS;
1555
+ if (role !== "region") return NO_VIOLATIONS2;
1556
+ if (!isStandaloneTag(tag)) return NO_VIOLATIONS2;
1557
+ const diagnostic = HtmlDiagnostics.standaloneRegionOverride(tag, implicitRole ?? tag);
1295
1558
  return [
1296
1559
  {
1297
1560
  valid: false,
1298
1561
  fixable: true,
1299
- severity: "error",
1562
+ severity: diagnostic.severity,
1300
1563
  fix: _AriaPolicyEngine.#removeRole,
1301
- diagnostic: HtmlDiagnostics.standaloneRegionOverride(tag, implicitRole ?? tag)
1564
+ diagnostic
1302
1565
  }
1303
1566
  ];
1304
1567
  }
@@ -1307,7 +1570,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1307
1570
  props,
1308
1571
  effectiveRole
1309
1572
  }) {
1310
- if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS;
1573
+ if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS2;
1311
1574
  const results = [];
1312
1575
  iterate.forEachEntry(props, (key) => {
1313
1576
  if (!key.startsWith("aria-")) return;
@@ -1325,62 +1588,6 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1325
1588
  return results;
1326
1589
  }
1327
1590
  // ─── ARIA attribute value validation ──────────────────────────────────────
1328
- // Accepted value shapes for typed ARIA attributes.
1329
- // Attributes not in this map are unconstrained (arbitrary string values permitted).
1330
- static #ARIA_VALUE_TYPES = /* @__PURE__ */ new Map([
1331
- // Boolean (true | false)
1332
- ["aria-atomic", { kind: "boolean" }],
1333
- ["aria-busy", { kind: "boolean" }],
1334
- ["aria-disabled", { kind: "boolean" }],
1335
- ["aria-expanded", { kind: "boolean" }],
1336
- ["aria-hidden", { kind: "boolean" }],
1337
- ["aria-modal", { kind: "boolean" }],
1338
- ["aria-multiline", { kind: "boolean" }],
1339
- ["aria-multiselectable", { kind: "boolean" }],
1340
- ["aria-readonly", { kind: "boolean" }],
1341
- ["aria-required", { kind: "boolean" }],
1342
- ["aria-selected", { kind: "boolean" }],
1343
- // Tristate (true | false | mixed)
1344
- ["aria-checked", { kind: "tristate" }],
1345
- ["aria-pressed", { kind: "tristate" }],
1346
- // Numeric (any finite number)
1347
- ["aria-valuenow", { kind: "number" }],
1348
- ["aria-valuemin", { kind: "number" }],
1349
- ["aria-valuemax", { kind: "number" }],
1350
- // Integer with optional range
1351
- ["aria-level", { kind: "integer", min: 1, max: 6 }],
1352
- ["aria-posinset", { kind: "integer", min: 1 }],
1353
- ["aria-setsize", { kind: "integer", min: -1 }],
1354
- ["aria-rowcount", { kind: "integer", min: -1 }],
1355
- ["aria-colcount", { kind: "integer", min: -1 }],
1356
- ["aria-rowindex", { kind: "integer", min: 1 }],
1357
- ["aria-colindex", { kind: "integer", min: 1 }],
1358
- ["aria-rowspan", { kind: "integer", min: 0 }],
1359
- ["aria-colspan", { kind: "integer", min: 0 }],
1360
- // Enum (specific allowed tokens)
1361
- ["aria-autocomplete", { kind: "enum", values: /* @__PURE__ */ new Set(["inline", "list", "both", "none"]) }],
1362
- [
1363
- "aria-current",
1364
- {
1365
- kind: "enum",
1366
- values: /* @__PURE__ */ new Set(["page", "step", "location", "date", "time", "true", "false"])
1367
- }
1368
- ],
1369
- [
1370
- "aria-haspopup",
1371
- {
1372
- kind: "enum",
1373
- values: /* @__PURE__ */ new Set(["false", "true", "menu", "listbox", "tree", "grid", "dialog"])
1374
- }
1375
- ],
1376
- ["aria-invalid", { kind: "enum", values: /* @__PURE__ */ new Set(["grammar", "false", "spelling", "true"]) }],
1377
- ["aria-live", { kind: "enum", values: /* @__PURE__ */ new Set(["assertive", "off", "polite"]) }],
1378
- [
1379
- "aria-orientation",
1380
- { kind: "enum", values: /* @__PURE__ */ new Set(["horizontal", "vertical", "undefined"]) }
1381
- ],
1382
- ["aria-sort", { kind: "enum", values: /* @__PURE__ */ new Set(["ascending", "descending", "none", "other"]) }]
1383
- ]);
1384
1591
  static #isValidAriaValue(value, type) {
1385
1592
  switch (type.kind) {
1386
1593
  case "boolean":
@@ -1425,11 +1632,11 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1425
1632
  }
1426
1633
  }
1427
1634
  static #checkAriaAttributeValues({ props, effectiveRole }) {
1428
- if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS;
1635
+ if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS2;
1429
1636
  const results = [];
1430
1637
  iterate.forEachEntry(props, (key, value) => {
1431
1638
  if (!key.startsWith("aria-")) return;
1432
- const type = _AriaPolicyEngine.#ARIA_VALUE_TYPES.get(key);
1639
+ const type = ARIA_VALUE_TYPES.get(key);
1433
1640
  if (!isNonNull(type)) return;
1434
1641
  if (_AriaPolicyEngine.#isValidAriaValue(value, type)) return;
1435
1642
  results.push({
@@ -1448,26 +1655,18 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1448
1655
  return results;
1449
1656
  }
1450
1657
  // ─── Heading implicit level ────────────────────────────────────────────────
1451
- static #HEADING_IMPLICIT_LEVELS = /* @__PURE__ */ new Map([
1452
- ["h1", 1],
1453
- ["h2", 2],
1454
- ["h3", 3],
1455
- ["h4", 4],
1456
- ["h5", 5],
1457
- ["h6", 6]
1458
- ]);
1459
1658
  static #checkRedundantAriaLevel({
1460
1659
  tag,
1461
1660
  props,
1462
1661
  effectiveRole
1463
1662
  }) {
1464
- if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS;
1465
- const implicitLevel = _AriaPolicyEngine.#HEADING_IMPLICIT_LEVELS.get(tag);
1466
- if (!isNonNull(implicitLevel)) return NO_VIOLATIONS;
1663
+ if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS2;
1664
+ const implicitLevel = HEADING_IMPLICIT_LEVELS.get(tag);
1665
+ if (!isNonNull(implicitLevel)) return NO_VIOLATIONS2;
1467
1666
  const raw = props["aria-level"];
1468
- if (!isNonNull(raw)) return NO_VIOLATIONS;
1667
+ if (!isNonNull(raw)) return NO_VIOLATIONS2;
1469
1668
  const n = typeof raw === "number" ? raw : typeof raw === "string" ? parseInt(raw, 10) : NaN;
1470
- if (!Number.isFinite(n) || n !== implicitLevel) return NO_VIOLATIONS;
1669
+ if (!Number.isFinite(n) || n !== implicitLevel) return NO_VIOLATIONS2;
1471
1670
  return [
1472
1671
  {
1473
1672
  valid: false,
@@ -1480,20 +1679,14 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1480
1679
  ];
1481
1680
  }
1482
1681
  // ─── Name-required roles ───────────────────────────────────────────────────
1483
- // Roles that always require an accessible name per WAI-ARIA APG.
1484
- // Dialog and landmark names are enforced via contracts (ariaContract) rather than
1485
- // the built-in pipeline so consumers can opt in; img is built in because role=img
1486
- // on any element (including bare <img>) is definitionally useless without a name.
1487
- static #NAME_REQUIRED_ROLES = /* @__PURE__ */ new Set(["img"]);
1488
1682
  static #checkNameRequiredRoles({
1489
1683
  tag,
1490
1684
  props,
1491
1685
  effectiveRole
1492
1686
  }) {
1493
- if (!effectiveRole || !_AriaPolicyEngine.#NAME_REQUIRED_ROLES.has(effectiveRole))
1494
- return NO_VIOLATIONS;
1495
- if ("aria-label" in props || "aria-labelledby" in props) return NO_VIOLATIONS;
1496
- if (tag === "img" && typeof props.alt === "string" && props.alt.length > 0) return NO_VIOLATIONS;
1687
+ if (!effectiveRole || !NAME_REQUIRED_ROLES.has(effectiveRole)) return NO_VIOLATIONS2;
1688
+ if ("aria-label" in props || "aria-labelledby" in props) return NO_VIOLATIONS2;
1689
+ if (tag === "img" && typeof props.alt === "string" && props.alt.length > 0) return NO_VIOLATIONS2;
1497
1690
  return [
1498
1691
  {
1499
1692
  valid: false,
@@ -1503,51 +1696,21 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1503
1696
  }
1504
1697
  ];
1505
1698
  }
1506
- // WAI-ARIA 1.2 required states and properties, keyed by role.
1507
- // Source: https://www.w3.org/TR/wai-aria-1.2/#requiredState
1508
- static #REQUIRED_PROPERTIES = /* @__PURE__ */ new Map([
1509
- ["combobox", ["aria-expanded"]],
1510
- ["option", ["aria-selected"]],
1511
- ["slider", ["aria-valuenow"]],
1512
- ["scrollbar", ["aria-controls", "aria-valuenow"]],
1513
- ["spinbutton", ["aria-valuenow"]]
1514
- ]);
1515
- static #checkRequiredAriaProperties({
1516
- props,
1517
- effectiveRole
1518
- }) {
1519
- if (!effectiveRole) return NO_VIOLATIONS;
1520
- const required = _AriaPolicyEngine.#REQUIRED_PROPERTIES.get(effectiveRole);
1521
- if (!isNonNull(required)) return NO_VIOLATIONS;
1522
- const results = [];
1523
- iterate.forEach(required, (attr) => {
1524
- if (attr in props) return;
1525
- results.push({
1526
- valid: false,
1527
- fixable: false,
1528
- severity: "warning",
1529
- attribute: attr,
1530
- diagnostic: AriaDiagnostics.requiredProperty(attr, effectiveRole)
1531
- });
1532
- });
1533
- return results;
1699
+ static #requiredAriaPropertiesRule = {
1700
+ attributesByRole: REQUIRED_ARIA_PROPERTIES,
1701
+ diagnosticFor: (attribute, role) => AriaDiagnostics.requiredProperty(attribute, role)
1702
+ };
1703
+ static #checkRequiredAriaProperties(context) {
1704
+ return checkRequiredAttributes(_AriaPolicyEngine.#requiredAriaPropertiesRule, context);
1534
1705
  }
1535
- // Natively interactive HTML elements — always keyboard-reachable unless explicitly disabled.
1536
- static #INTERACTIVE_TAGS = /* @__PURE__ */ new Set([
1537
- "a",
1538
- "button",
1539
- "input",
1540
- "select",
1541
- "textarea"
1542
- ]);
1543
1706
  // WAI-ARIA 1.2 §6.6: aria-hidden="true" must not be placed on focusable elements.
1544
1707
  static #checkAriaHiddenOnFocusable({ tag, props }) {
1545
- if (props["aria-hidden"] !== "true" && props["aria-hidden"] !== true) return NO_VIOLATIONS;
1546
- const isInteractive = _AriaPolicyEngine.#INTERACTIVE_TAGS.has(tag);
1708
+ if (props["aria-hidden"] !== "true" && props["aria-hidden"] !== true) return NO_VIOLATIONS2;
1709
+ const isInteractive = INTERACTIVE_TAGS.has(tag);
1547
1710
  if (!isInteractive) {
1548
1711
  const tabindex = props.tabindex;
1549
1712
  const n = typeof tabindex === "number" ? tabindex : typeof tabindex === "string" ? parseInt(tabindex, 10) : NaN;
1550
- if (!Number.isFinite(n) || n < 0) return NO_VIOLATIONS;
1713
+ if (!Number.isFinite(n) || n < 0) return NO_VIOLATIONS2;
1551
1714
  }
1552
1715
  return [
1553
1716
  {
@@ -1566,7 +1729,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1566
1729
  props,
1567
1730
  effectiveRole
1568
1731
  }) {
1569
- if (effectiveRole !== "none" && effectiveRole !== "presentation") return NO_VIOLATIONS;
1732
+ if (effectiveRole !== "none" && effectiveRole !== "presentation") return NO_VIOLATIONS2;
1570
1733
  const results = [];
1571
1734
  iterate.forEachEntry(props, (key) => {
1572
1735
  if (!key.startsWith("aria-")) return;
@@ -1582,18 +1745,11 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1582
1745
  });
1583
1746
  return results;
1584
1747
  }
1585
- // WAI-ARIA live region roles and their implied aria-live politeness values.
1586
- static #LIVE_REGION_ROLES = /* @__PURE__ */ new Map([
1587
- ["alert", "assertive"],
1588
- ["status", "polite"],
1589
- ["log", "polite"],
1590
- ["timer", "off"]
1591
- ]);
1592
1748
  static #checkMissingLiveRegion({ effectiveRole, props }) {
1593
- if (!effectiveRole) return NO_VIOLATIONS;
1594
- const impliedLive = _AriaPolicyEngine.#LIVE_REGION_ROLES.get(effectiveRole);
1595
- if (!impliedLive) return NO_VIOLATIONS;
1596
- if ("aria-live" in props) return NO_VIOLATIONS;
1749
+ if (!effectiveRole) return NO_VIOLATIONS2;
1750
+ const impliedLive = LIVE_REGION_ROLES.get(effectiveRole);
1751
+ if (!impliedLive) return NO_VIOLATIONS2;
1752
+ if ("aria-live" in props) return NO_VIOLATIONS2;
1597
1753
  const injectLive = {
1598
1754
  kind: `injectLive:${effectiveRole}`,
1599
1755
  apply: (ctx) => ({
@@ -1612,20 +1768,13 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1612
1768
  }
1613
1769
  ];
1614
1770
  }
1615
- static #checkMissingAtomic({ effectiveRole, props }) {
1616
- if (!effectiveRole || !_AriaPolicyEngine.#LIVE_REGION_ROLES.has(effectiveRole))
1617
- return NO_VIOLATIONS;
1618
- if ("aria-atomic" in props) return NO_VIOLATIONS;
1619
- return [
1620
- {
1621
- valid: false,
1622
- fixable: false,
1623
- severity: "warning",
1624
- diagnostic: AriaDiagnostics.missingAtomic(effectiveRole)
1625
- }
1626
- ];
1771
+ static #missingAtomicRule = {
1772
+ attributesByRole: ATOMIC_REQUIREMENTS,
1773
+ diagnosticFor: (_attribute, role) => AriaDiagnostics.missingAtomic(role)
1774
+ };
1775
+ static #checkMissingAtomic(context) {
1776
+ return checkRequiredAttributes(_AriaPolicyEngine.#missingAtomicRule, context);
1627
1777
  }
1628
- static #VALID_RELEVANT_TOKENS = /* @__PURE__ */ new Set(["additions", "removals", "text", "all"]);
1629
1778
  // Custom fix rules passed via `options.rules` must be pure functions of (tag, props) — the cache
1630
1779
  // replays stored fixes against new prop objects, so fixes that close over external state will
1631
1780
  // produce inconsistent results on cache hits.
@@ -1639,10 +1788,10 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1639
1788
  };
1640
1789
  static #checkInvalidAriaRelevant({ props }) {
1641
1790
  const relevant = props["aria-relevant"];
1642
- if (relevant === void 0) return NO_VIOLATIONS;
1643
- if (typeof relevant !== "string") return NO_VIOLATIONS;
1791
+ if (relevant === void 0) return NO_VIOLATIONS2;
1792
+ if (typeof relevant !== "string") return NO_VIOLATIONS2;
1644
1793
  const tokens = relevant.trim().split(/\s+/);
1645
- const invalid = tokens.filter((t) => !_AriaPolicyEngine.#VALID_RELEVANT_TOKENS.has(t));
1794
+ const invalid = tokens.filter((t) => !VALID_RELEVANT_TOKENS.has(t));
1646
1795
  if (invalid.length > 0) {
1647
1796
  return [
1648
1797
  {
@@ -1667,7 +1816,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1667
1816
  }
1668
1817
  ];
1669
1818
  }
1670
- return NO_VIOLATIONS;
1819
+ return NO_VIOLATIONS2;
1671
1820
  }
1672
1821
  };
1673
1822
 
@@ -1987,6 +2136,425 @@ var readonlyProps = ({
1987
2136
  // ../core/src/html/evaluators.ts
1988
2137
  import { warnDiagnostics as warnDiagnostics2 } from "../_shared/diagnostics.js";
1989
2138
 
2139
+ // ../core/src/html/spec/vocabulary/input.ts
2140
+ var TEXT_INPUT_TYPES = ["text", "search", "url", "tel", "email", "password"];
2141
+ var NUMERIC_INPUT_TYPES = [
2142
+ "number",
2143
+ "range",
2144
+ "date",
2145
+ "month",
2146
+ "week",
2147
+ "time",
2148
+ "datetime-local"
2149
+ ];
2150
+ var HTML_INPUT_TYPES = /* @__PURE__ */ new Set([
2151
+ ...TEXT_INPUT_TYPES,
2152
+ ...NUMERIC_INPUT_TYPES,
2153
+ "checkbox",
2154
+ "radio",
2155
+ "file",
2156
+ "color",
2157
+ "hidden",
2158
+ "button",
2159
+ "submit",
2160
+ "reset",
2161
+ "image"
2162
+ ]);
2163
+
2164
+ // ../core/src/html/spec/attributes/input.ts
2165
+ var INPUT_ATTRIBUTE_TYPE_POLICIES = [
2166
+ { attribute: "checked", allowedTypes: ["checkbox", "radio"] },
2167
+ { attribute: "multiple", allowedTypes: ["email", "file"] },
2168
+ { attribute: "maxLength", allowedTypes: TEXT_INPUT_TYPES },
2169
+ { attribute: "minLength", allowedTypes: TEXT_INPUT_TYPES },
2170
+ { attribute: "pattern", allowedTypes: TEXT_INPUT_TYPES },
2171
+ { attribute: "min", allowedTypes: NUMERIC_INPUT_TYPES },
2172
+ { attribute: "max", allowedTypes: NUMERIC_INPUT_TYPES },
2173
+ { attribute: "step", allowedTypes: NUMERIC_INPUT_TYPES },
2174
+ { attribute: "accept", allowedTypes: ["file"] },
2175
+ { attribute: "capture", allowedTypes: ["file"] },
2176
+ { attribute: "size", allowedTypes: TEXT_INPUT_TYPES },
2177
+ { attribute: "alt", allowedTypes: ["image"] },
2178
+ { attribute: "height", allowedTypes: ["image"] },
2179
+ { attribute: "width", allowedTypes: ["image"] }
2180
+ ];
2181
+
2182
+ // ../core/src/html/spec/constraints/input.ts
2183
+ var REQUIRED_READONLY_CONFLICT = {
2184
+ props: ["required", "readOnly"],
2185
+ diagnostic: () => InputAccessibilityDiagnostics.requiredReadOnlyConflict()
2186
+ };
2187
+ var INPUT_MUTUALLY_EXCLUSIVE_POLICIES = [
2188
+ REQUIRED_READONLY_CONFLICT
2189
+ ];
2190
+
2191
+ // ../core/src/html/spec/validators/attribute-type-validator.ts
2192
+ var DEFAULT_INPUT_TYPE = "text";
2193
+ function omit(props, key) {
2194
+ const next = { ...props };
2195
+ delete next[key];
2196
+ return next;
2197
+ }
2198
+ function removeAttributeFix(attribute) {
2199
+ return {
2200
+ kind: `removeAttribute:${attribute}`,
2201
+ apply: ({ props }) => {
2202
+ if (!(attribute in props)) return { applied: false, next: props };
2203
+ return { applied: true, next: omit(props, attribute), previous: props };
2204
+ }
2205
+ };
2206
+ }
2207
+ function createInputAttributeTypeRule({
2208
+ attribute,
2209
+ allowedTypes
2210
+ }) {
2211
+ const rule = ({ tag, props }) => {
2212
+ if (tag !== "input" || !(attribute in props)) return [];
2213
+ const type = typeof props.type === "string" ? props.type : DEFAULT_INPUT_TYPE;
2214
+ if (allowedTypes.includes(type)) return [];
2215
+ const diagnostic = HtmlDiagnostics.input.attributeIgnoredForType(attribute, type, allowedTypes);
2216
+ return [
2217
+ {
2218
+ valid: false,
2219
+ fixable: true,
2220
+ severity: diagnostic.severity,
2221
+ fix: removeAttributeFix(attribute),
2222
+ diagnostic
2223
+ }
2224
+ ];
2225
+ };
2226
+ return Object.assign(rule, { readsProps: ["type", attribute], tags: ["input"] });
2227
+ }
2228
+
2229
+ // ../core/src/html/spec/validators/mutually-exclusive-validator.ts
2230
+ function createMutuallyExclusiveRule({
2231
+ props: conflictingProps,
2232
+ diagnostic: createDiagnostic
2233
+ }) {
2234
+ const [first, second] = conflictingProps;
2235
+ const rule = ({ tag, props }) => {
2236
+ if (tag !== "input" || !props[first] || !props[second]) return [];
2237
+ const diagnostic = createDiagnostic();
2238
+ return [{ valid: false, fixable: false, severity: diagnostic.severity, diagnostic }];
2239
+ };
2240
+ return Object.assign(rule, { readsProps: conflictingProps, tags: ["input"] });
2241
+ }
2242
+
2243
+ // ../core/src/html/input-rules.ts
2244
+ var policyByAttribute = Object.fromEntries(
2245
+ INPUT_ATTRIBUTE_TYPE_POLICIES.map((policy) => [policy.attribute, policy])
2246
+ );
2247
+ function policyFor(attribute) {
2248
+ return policyByAttribute[attribute];
2249
+ }
2250
+ var supportedInputTypeRule = Object.assign(
2251
+ ({ tag, props }) => {
2252
+ if (tag !== "input" || typeof props.type !== "string") return [];
2253
+ const type = props.type;
2254
+ if (HTML_INPUT_TYPES.has(type)) return [];
2255
+ const diagnostic = HtmlDiagnostics.input.unsupportedType(type);
2256
+ return [
2257
+ {
2258
+ valid: false,
2259
+ fixable: false,
2260
+ severity: diagnostic.severity,
2261
+ diagnostic
2262
+ }
2263
+ ];
2264
+ },
2265
+ { readsProps: ["type"], tags: ["input"] }
2266
+ );
2267
+ var checkedRequiresCheckableTypeRule = createInputAttributeTypeRule(policyFor("checked"));
2268
+ var multipleRequiresSupportedTypeRule = createInputAttributeTypeRule(policyFor("multiple"));
2269
+ var maxLengthRequiresTextTypeRule = createInputAttributeTypeRule(policyFor("maxLength"));
2270
+ var minLengthRequiresTextTypeRule = createInputAttributeTypeRule(policyFor("minLength"));
2271
+ var patternRequiresTextTypeRule = createInputAttributeTypeRule(policyFor("pattern"));
2272
+ var minRequiresNumericTypeRule = createInputAttributeTypeRule(policyFor("min"));
2273
+ var maxRequiresNumericTypeRule = createInputAttributeTypeRule(policyFor("max"));
2274
+ var stepRequiresNumericTypeRule = createInputAttributeTypeRule(policyFor("step"));
2275
+ var acceptRequiresFileTypeRule = createInputAttributeTypeRule(policyFor("accept"));
2276
+ var captureRequiresFileTypeRule = createInputAttributeTypeRule(policyFor("capture"));
2277
+ var sizeRequiresTextTypeRule = createInputAttributeTypeRule(policyFor("size"));
2278
+ var altRequiresImageTypeRule = createInputAttributeTypeRule(policyFor("alt"));
2279
+ var heightRequiresImageTypeRule = createInputAttributeTypeRule(policyFor("height"));
2280
+ var widthRequiresImageTypeRule = createInputAttributeTypeRule(policyFor("width"));
2281
+ var inputAccessibleNameRule = Object.assign(
2282
+ ({ tag, props }) => {
2283
+ if (tag !== "input" || props.type === "hidden") return [];
2284
+ if ("aria-label" in props || "aria-labelledby" in props) return [];
2285
+ const hasPlaceholder = typeof props.placeholder === "string" && props.placeholder.length > 0;
2286
+ const diagnostic = hasPlaceholder ? InputAccessibilityDiagnostics.placeholderIsNotLabel() : InputAccessibilityDiagnostics.missingAccessibleName();
2287
+ return [{ valid: false, fixable: false, severity: diagnostic.severity, diagnostic }];
2288
+ },
2289
+ {
2290
+ readsProps: ["type", "aria-label", "aria-labelledby", "placeholder"],
2291
+ tags: ["input"]
2292
+ }
2293
+ );
2294
+ var PASSWORD_AUTOCOMPLETE_VALUES = ["current-password", "new-password"];
2295
+ var passwordAutocompleteRule = Object.assign(
2296
+ ({ tag, props }) => {
2297
+ if (tag !== "input" || props.type !== "password") return [];
2298
+ const autoComplete = props.autoComplete;
2299
+ const tokens = typeof autoComplete === "string" ? autoComplete.split(" ") : [];
2300
+ if (PASSWORD_AUTOCOMPLETE_VALUES.some((value) => tokens.includes(value))) return [];
2301
+ const diagnostic = InputAccessibilityDiagnostics.passwordMissingAutocomplete();
2302
+ return [{ valid: false, fixable: false, severity: diagnostic.severity, diagnostic }];
2303
+ },
2304
+ { readsProps: ["type", "autoComplete"], tags: ["input"] }
2305
+ );
2306
+ var requiredReadOnlyConflictRule = createMutuallyExclusiveRule(REQUIRED_READONLY_CONFLICT);
2307
+ var INPUT_RULES = [
2308
+ supportedInputTypeRule,
2309
+ checkedRequiresCheckableTypeRule,
2310
+ multipleRequiresSupportedTypeRule,
2311
+ maxLengthRequiresTextTypeRule,
2312
+ minLengthRequiresTextTypeRule,
2313
+ patternRequiresTextTypeRule,
2314
+ minRequiresNumericTypeRule,
2315
+ maxRequiresNumericTypeRule,
2316
+ stepRequiresNumericTypeRule,
2317
+ acceptRequiresFileTypeRule,
2318
+ captureRequiresFileTypeRule,
2319
+ sizeRequiresTextTypeRule,
2320
+ altRequiresImageTypeRule,
2321
+ heightRequiresImageTypeRule,
2322
+ widthRequiresImageTypeRule,
2323
+ inputAccessibleNameRule,
2324
+ passwordAutocompleteRule,
2325
+ requiredReadOnlyConflictRule
2326
+ ];
2327
+
2328
+ // ../core/src/html/spec/types.ts
2329
+ function definePropRolePolicy(prop, map2, fallback) {
2330
+ return { kind: "byProp", prop, map: map2, fallback };
2331
+ }
2332
+ function resolveAllowedRoles(spec, props) {
2333
+ const policy = spec.allowedRoles;
2334
+ if (!policy) return void 0;
2335
+ switch (policy.kind) {
2336
+ case "fixed":
2337
+ return policy.roles;
2338
+ case "byProp": {
2339
+ const value = typeof props[policy.prop] === "string" ? props[policy.prop] : policy.fallback;
2340
+ return policy.map[value];
2341
+ }
2342
+ case "dynamic":
2343
+ return policy.resolve({ props });
2344
+ }
2345
+ }
2346
+
2347
+ // ../core/src/html/spec/roles/input.ts
2348
+ var ALLOWED_INPUT_ROLES = {
2349
+ checkbox: ["menuitemcheckbox", "option", "switch", "button"],
2350
+ radio: ["menuitemradio"],
2351
+ range: [],
2352
+ number: [],
2353
+ search: ["combobox"],
2354
+ text: ["combobox", "searchbox", "spinbutton"],
2355
+ email: ["combobox"],
2356
+ tel: ["combobox"],
2357
+ url: ["combobox"],
2358
+ button: [
2359
+ "link",
2360
+ "menuitem",
2361
+ "menuitemcheckbox",
2362
+ "menuitemradio",
2363
+ "option",
2364
+ "radio",
2365
+ "switch",
2366
+ "tab"
2367
+ ],
2368
+ submit: [
2369
+ "link",
2370
+ "menuitem",
2371
+ "menuitemcheckbox",
2372
+ "menuitemradio",
2373
+ "option",
2374
+ "radio",
2375
+ "switch",
2376
+ "tab"
2377
+ ],
2378
+ reset: [
2379
+ "link",
2380
+ "menuitem",
2381
+ "menuitemcheckbox",
2382
+ "menuitemradio",
2383
+ "option",
2384
+ "radio",
2385
+ "switch",
2386
+ "tab"
2387
+ ],
2388
+ image: [
2389
+ "link",
2390
+ "menuitem",
2391
+ "menuitemcheckbox",
2392
+ "menuitemradio",
2393
+ "option",
2394
+ "radio",
2395
+ "switch",
2396
+ "tab"
2397
+ ],
2398
+ hidden: []
2399
+ };
2400
+
2401
+ // ../core/src/html/spec/elements/input.ts
2402
+ var inputElementSpec = {
2403
+ tag: "input",
2404
+ allowedRoles: definePropRolePolicy("type", ALLOWED_INPUT_ROLES, "text"),
2405
+ attributes: INPUT_ATTRIBUTE_TYPE_POLICIES,
2406
+ mutuallyExclusive: INPUT_MUTUALLY_EXCLUSIVE_POLICIES
2407
+ };
2408
+
2409
+ // ../core/src/html/spec/roles/img.ts
2410
+ var IMG_NAMED_ROLES = [
2411
+ "button",
2412
+ "checkbox",
2413
+ "link",
2414
+ "menuitem",
2415
+ "menuitemcheckbox",
2416
+ "menuitemradio",
2417
+ "option",
2418
+ "progressbar",
2419
+ "scrollbar",
2420
+ "separator",
2421
+ "slider",
2422
+ "switch",
2423
+ "tab",
2424
+ "treeitem"
2425
+ ];
2426
+
2427
+ // ../core/src/html/spec/elements/img.ts
2428
+ var imgElementSpec = {
2429
+ tag: "img",
2430
+ allowedRoles: {
2431
+ kind: "dynamic",
2432
+ resolve: ({ props }) => props.alt === "" ? [] : IMG_NAMED_ROLES
2433
+ }
2434
+ };
2435
+
2436
+ // ../core/src/html/spec/roles/table.ts
2437
+ var ALLOWED_TABLE_ROLES = ["grid", "treegrid"];
2438
+
2439
+ // ../core/src/html/spec/elements/table.ts
2440
+ var tableElementSpec = {
2441
+ tag: "table",
2442
+ allowedRoles: { kind: "fixed", roles: ALLOWED_TABLE_ROLES }
2443
+ };
2444
+
2445
+ // ../core/src/html/role-restrictions.ts
2446
+ var ALLOWED_ROLES = {
2447
+ article: ["application", "document", "feed", "main", "none", "presentation", "region"],
2448
+ aside: ["feed", "none", "presentation", "region", "search"],
2449
+ footer: ["group", "none", "presentation"],
2450
+ header: ["group", "none", "presentation"],
2451
+ main: [],
2452
+ nav: [],
2453
+ a: [
2454
+ "button",
2455
+ "checkbox",
2456
+ "menuitem",
2457
+ "menuitemcheckbox",
2458
+ "menuitemradio",
2459
+ "option",
2460
+ "radio",
2461
+ "switch",
2462
+ "tab",
2463
+ "treeitem"
2464
+ ],
2465
+ button: [
2466
+ "checkbox",
2467
+ "link",
2468
+ "menuitem",
2469
+ "menuitemcheckbox",
2470
+ "menuitemradio",
2471
+ "option",
2472
+ "radio",
2473
+ "switch",
2474
+ "tab"
2475
+ ],
2476
+ select: ["menu"],
2477
+ h1: ["tab", "presentation", "none"],
2478
+ h2: ["tab", "presentation", "none"],
2479
+ h3: ["tab", "presentation", "none"],
2480
+ h4: ["tab", "presentation", "none"],
2481
+ h5: ["tab", "presentation", "none"],
2482
+ h6: ["tab", "presentation", "none"],
2483
+ ul: [
2484
+ "directory",
2485
+ "group",
2486
+ "listbox",
2487
+ "menu",
2488
+ "menubar",
2489
+ "radiogroup",
2490
+ "tablist",
2491
+ "toolbar",
2492
+ "tree"
2493
+ ],
2494
+ ol: [
2495
+ "directory",
2496
+ "group",
2497
+ "listbox",
2498
+ "menu",
2499
+ "menubar",
2500
+ "radiogroup",
2501
+ "tablist",
2502
+ "toolbar",
2503
+ "tree"
2504
+ ],
2505
+ li: [
2506
+ "menuitem",
2507
+ "menuitemcheckbox",
2508
+ "menuitemradio",
2509
+ "option",
2510
+ "none",
2511
+ "presentation",
2512
+ "radio",
2513
+ "separator",
2514
+ "tab",
2515
+ "treeitem"
2516
+ ],
2517
+ dialog: ["alertdialog"],
2518
+ fieldset: ["none", "presentation", "radiogroup"]
2519
+ };
2520
+ var ELEMENT_SPECS = {
2521
+ input: inputElementSpec,
2522
+ img: imgElementSpec,
2523
+ table: tableElementSpec
2524
+ };
2525
+ function getAllowedRoles(tag, props) {
2526
+ const spec = ELEMENT_SPECS[tag];
2527
+ if (spec) return resolveAllowedRoles(spec, props);
2528
+ return ALLOWED_ROLES[tag];
2529
+ }
2530
+ var removeRoleFix = {
2531
+ kind: "removeRole",
2532
+ apply: ({ props }) => {
2533
+ if (!("role" in props)) return { applied: false, next: props };
2534
+ const { role: _role, ...rest } = props;
2535
+ return { applied: true, next: rest, previous: props };
2536
+ }
2537
+ };
2538
+ var roleNotPermittedRule = Object.assign(
2539
+ ({ tag, props, implicitRole }) => {
2540
+ const role = props.role;
2541
+ if (typeof role !== "string" || role.length === 0 || role === implicitRole) return [];
2542
+ const allowed = getAllowedRoles(tag, props);
2543
+ if (allowed === void 0 || allowed.includes(role)) return [];
2544
+ const diagnostic = HtmlDiagnostics.roleNotPermitted(tag, role, allowed);
2545
+ return [
2546
+ {
2547
+ valid: false,
2548
+ fixable: true,
2549
+ severity: diagnostic.severity,
2550
+ fix: removeRoleFix,
2551
+ diagnostic
2552
+ }
2553
+ ];
2554
+ },
2555
+ { readsProps: ["role", "type", "alt"] }
2556
+ );
2557
+
1990
2558
  // ../core/src/html/aria-rules.ts
1991
2559
  var LANDMARK_TAG_SET = /* @__PURE__ */ new Set(["article", "aside", "footer", "header", "main", "nav"]);
1992
2560
  var removeLandmarkRoleOverride = {
@@ -1997,20 +2565,24 @@ var removeLandmarkRoleOverride = {
1997
2565
  return { applied: true, next: rest, previous: props };
1998
2566
  }
1999
2567
  };
2000
- function landmarkRoleRule({ tag, props, implicitRole }) {
2001
- if (!LANDMARK_TAG_SET.has(tag) || !implicitRole) return [];
2002
- const role = props.role;
2003
- if (!role || role === implicitRole) return [];
2004
- return [
2005
- {
2006
- valid: false,
2007
- fixable: true,
2008
- severity: "error",
2009
- fix: removeLandmarkRoleOverride,
2010
- diagnostic: HtmlDiagnostics.landmarkRoleOverride(tag, implicitRole, role)
2011
- }
2012
- ];
2013
- }
2568
+ var landmarkRoleRule = Object.assign(
2569
+ ({ tag, props, implicitRole }) => {
2570
+ if (!LANDMARK_TAG_SET.has(tag) || !implicitRole) return [];
2571
+ const role = props.role;
2572
+ if (!role || role === implicitRole) return [];
2573
+ const diagnostic = HtmlDiagnostics.landmarkRoleOverride(tag, implicitRole, role);
2574
+ return [
2575
+ {
2576
+ valid: false,
2577
+ fixable: true,
2578
+ severity: diagnostic.severity,
2579
+ fix: removeLandmarkRoleOverride,
2580
+ diagnostic
2581
+ }
2582
+ ];
2583
+ },
2584
+ { tags: [...LANDMARK_TAG_SET] }
2585
+ );
2014
2586
  function requireAccessibleName({ tag, props }) {
2015
2587
  if ("aria-label" in props || "aria-labelledby" in props) return [];
2016
2588
  return [
@@ -2023,11 +2595,19 @@ function requireAccessibleName({ tag, props }) {
2023
2595
  ];
2024
2596
  }
2025
2597
  var NAMED_LANDMARK_TAGS = /* @__PURE__ */ new Set(["nav", "aside"]);
2026
- function landmarkNameAdvisory(ctx) {
2027
- if (!ctx.implicitRole || !NAMED_LANDMARK_TAGS.has(ctx.tag)) return [];
2028
- return requireAccessibleName(ctx);
2029
- }
2030
- var HTML_ARIA_RULES = [landmarkRoleRule, landmarkNameAdvisory];
2598
+ var landmarkNameAdvisory = Object.assign(
2599
+ (ctx) => {
2600
+ if (!ctx.implicitRole || !NAMED_LANDMARK_TAGS.has(ctx.tag)) return [];
2601
+ return requireAccessibleName(ctx);
2602
+ },
2603
+ { tags: [...NAMED_LANDMARK_TAGS] }
2604
+ );
2605
+ var HTML_ARIA_RULES = [
2606
+ landmarkRoleRule,
2607
+ landmarkNameAdvisory,
2608
+ roleNotPermittedRule,
2609
+ ...INPUT_RULES
2610
+ ];
2031
2611
 
2032
2612
  // ../core/src/html/contracts.ts
2033
2613
  import { warnDiagnostics } from "../_shared/diagnostics.js";
@@ -2122,6 +2702,62 @@ var figureContract = contract([
2122
2702
  ]);
2123
2703
  var detailsContract = firstChildContract("summary", "summary");
2124
2704
  var fieldsetContract = firstChildContract("legend", "legend");
2705
+ var objectContract = contract([
2706
+ { name: "param", match: isTag("param") },
2707
+ { name: "content", match: isOpenContent("param") }
2708
+ ]);
2709
+ var INTERACTIVE_CONTENT_TAGS = ["a", "button", "input", "select", "textarea", "label"];
2710
+ var buttonContract = closedContract([
2711
+ { name: "content", match: isOpenContent(...INTERACTIVE_CONTENT_TAGS) }
2712
+ ]);
2713
+ var anchorContract = closedContract([
2714
+ { name: "content", match: isOpenContent(...INTERACTIVE_CONTENT_TAGS) }
2715
+ ]);
2716
+ var LABELABLE_TAGS = [
2717
+ "button",
2718
+ "input",
2719
+ "meter",
2720
+ "output",
2721
+ "progress",
2722
+ "select",
2723
+ "textarea"
2724
+ ];
2725
+ var labelContract = contract([
2726
+ { name: "control", match: isTag(...LABELABLE_TAGS), cardinality: { max: 1 } }
2727
+ ]);
2728
+ var P_BLOCKED_TAGS = [
2729
+ "address",
2730
+ "article",
2731
+ "aside",
2732
+ "blockquote",
2733
+ "details",
2734
+ "dialog",
2735
+ "div",
2736
+ "dl",
2737
+ "fieldset",
2738
+ "figure",
2739
+ "footer",
2740
+ "form",
2741
+ "h1",
2742
+ "h2",
2743
+ "h3",
2744
+ "h4",
2745
+ "h5",
2746
+ "h6",
2747
+ "header",
2748
+ "hr",
2749
+ "main",
2750
+ "nav",
2751
+ "ol",
2752
+ "p",
2753
+ "pre",
2754
+ "section",
2755
+ "table",
2756
+ "ul"
2757
+ ];
2758
+ var pContract = closedContract([
2759
+ { name: "content", match: isOpenContent(...P_BLOCKED_TAGS) }
2760
+ ]);
2125
2761
  var mediaContract = contract([
2126
2762
  { name: "source", match: isTag("source") },
2127
2763
  { name: "track", match: isTag("track") },
@@ -2176,6 +2812,11 @@ var htmlContracts = {
2176
2812
  details: detailsContract,
2177
2813
  fieldset: fieldsetContract,
2178
2814
  dialog: dialogContract,
2815
+ object: objectContract,
2816
+ button: buttonContract,
2817
+ a: anchorContract,
2818
+ label: labelContract,
2819
+ p: pContract,
2179
2820
  head: headContract,
2180
2821
  html: htmlContract
2181
2822
  };
@@ -2488,21 +3129,21 @@ function validateRenderProps(diagnostics, options, props, recipeKey) {
2488
3129
  import { throwDiagnostics } from "../_shared/diagnostics.js";
2489
3130
 
2490
3131
  // ../core/src/factory/plugin-diagnostics.ts
2491
- import { DiagnosticCategory as DiagnosticCategory5, DiagnosticCode as DiagnosticCode5 } from "../_shared/diagnostics.js";
3132
+ import { DiagnosticCategory as DiagnosticCategory6, DiagnosticCode as DiagnosticCode6 } from "../_shared/diagnostics.js";
2492
3133
  var PluginDiagnostics = {
2493
3134
  invalidShape(received) {
2494
3135
  const got = received === null ? "null" : typeof received;
2495
3136
  return {
2496
- code: DiagnosticCode5.PluginInvalidShape,
2497
- category: DiagnosticCategory5.Internal,
3137
+ code: DiagnosticCode6.PluginInvalidShape,
3138
+ category: DiagnosticCategory6.Internal,
2498
3139
  message: `[praxis-kit] Plugin factory must return an object with a 'pipeline' function. Got: ${got}.`
2499
3140
  };
2500
3141
  },
2501
3142
  pipelineReturnType(received) {
2502
3143
  const got = received === null ? "null" : Array.isArray(received) ? "array" : typeof received;
2503
3144
  return {
2504
- code: DiagnosticCode5.PluginPipelineReturnType,
2505
- category: DiagnosticCategory5.Internal,
3145
+ code: DiagnosticCode6.PluginPipelineReturnType,
3146
+ category: DiagnosticCategory6.Internal,
2506
3147
  message: `[praxis-kit] Plugin pipeline must return a string. Got: ${got}.`
2507
3148
  };
2508
3149
  }