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