praxis-kit 6.2.0 → 6.2.3

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,24 @@ 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
+ };
968
980
  var HtmlDiagnostics = {
969
981
  emptyRole(tag) {
970
982
  return {
971
983
  code: DiagnosticCode3.HtmlEmptyRole,
972
984
  category: DiagnosticCategory3.HTML,
985
+ severity: "warning",
973
986
  message: `<${tag}> has an explicit empty role="". Omit the attribute instead.`
974
987
  };
975
988
  },
@@ -977,6 +990,7 @@ var HtmlDiagnostics = {
977
990
  return {
978
991
  code: DiagnosticCode3.HtmlImplicitRoleRedundant,
979
992
  category: DiagnosticCategory3.HTML,
993
+ severity: "warning",
980
994
  message: `<${tag}> already has implicit role="${implicitRole}". Avoid redundant role assignment.`
981
995
  };
982
996
  },
@@ -984,6 +998,7 @@ var HtmlDiagnostics = {
984
998
  return {
985
999
  code: DiagnosticCode3.HtmlImplicitRoleOverride,
986
1000
  category: DiagnosticCategory3.HTML,
1001
+ severity: "error",
987
1002
  message: `<${tag}> should not override its implicit role="${implicitRole}" with role="${role}".`
988
1003
  };
989
1004
  },
@@ -991,6 +1006,7 @@ var HtmlDiagnostics = {
991
1006
  return {
992
1007
  code: DiagnosticCode3.HtmlStandaloneRegionOverride,
993
1008
  category: DiagnosticCategory3.HTML,
1009
+ severity: "error",
994
1010
  message: `<${tag}> is a self-contained element with implicit role="${implicitRole}". Assigning role="region" has been removed.`
995
1011
  };
996
1012
  },
@@ -998,6 +1014,7 @@ var HtmlDiagnostics = {
998
1014
  return {
999
1015
  code: DiagnosticCode3.HtmlLandmarkRoleOverride,
1000
1016
  category: DiagnosticCategory3.HTML,
1017
+ severity: "error",
1001
1018
  message: `<${tag}> has a fixed landmark role="${implicitRole}". role="${role}" overrides it and confuses assistive technology. The override has been removed.`
1002
1019
  };
1003
1020
  },
@@ -1005,34 +1022,148 @@ var HtmlDiagnostics = {
1005
1022
  return {
1006
1023
  code: DiagnosticCode3.HtmlInvalidChild,
1007
1024
  category: DiagnosticCategory3.HTML,
1025
+ severity: "error",
1008
1026
  message: `<${child}> is not a valid direct child of <${parent}>. Allowed: ${allowed}.`
1009
1027
  };
1028
+ },
1029
+ roleNotPermitted(tag, role, allowedRoles) {
1030
+ const allowed = allowedRoles.length > 0 ? allowedRoles.map((r) => `"${r}"`).join(", ") : "none \u2014 no explicit role is permitted on this element";
1031
+ return {
1032
+ code: DiagnosticCode3.HtmlRoleNotPermitted,
1033
+ category: DiagnosticCategory3.HTML,
1034
+ severity: "error",
1035
+ message: `role="${role}" is not permitted on <${tag}>. Allowed alternate role(s): ${allowed}.`,
1036
+ 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.'
1037
+ };
1038
+ },
1039
+ // Reserved for <input>-specific facts (HTML3101–3199, see codes.ts) — later element families
1040
+ // (button, img, table, ...) get their own reserved block and their own namespace here.
1041
+ input: {
1042
+ unsupportedType(type) {
1043
+ return {
1044
+ code: DiagnosticCode3.HtmlInputUnsupportedType,
1045
+ category: DiagnosticCategory3.HTML,
1046
+ severity: "warning",
1047
+ message: `type="${type}" is not a value defined by the HTML specification. Browsers silently fall back to type="text".`,
1048
+ 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).',
1049
+ suggestions: [
1050
+ {
1051
+ title: "Check for a typo in the type value",
1052
+ description: `"${type}" does not match any HTML5 input type.`
1053
+ }
1054
+ ]
1055
+ };
1056
+ },
1057
+ // The user-visible problem is that the attribute is ignored — not that it "requires" a type;
1058
+ // that's the rule's internal framing, not what the browser actually does.
1059
+ attributeIgnoredForType(attribute, type, allowedTypes) {
1060
+ const allowed = allowedTypes.map((t) => `"${t}"`).join(", ");
1061
+ const code = ATTRIBUTE_IGNORED_CODES[attribute];
1062
+ if (!code) throw new Error(`No DiagnosticCode registered for input attribute "${attribute}"`);
1063
+ return {
1064
+ code,
1065
+ category: DiagnosticCategory3.HTML,
1066
+ severity: "warning",
1067
+ message: `"${attribute}" is ignored on <input type="${type}">.`,
1068
+ rationale: `"${attribute}" only has an effect when type is one of: ${allowed}. Browsers silently ignore it on other input types.`,
1069
+ suggestions: [
1070
+ {
1071
+ title: `Remove "${attribute}"`,
1072
+ description: `"${attribute}" only affects <input> when type is one of: ${allowed}.`
1073
+ }
1074
+ ]
1075
+ };
1076
+ }
1010
1077
  }
1011
1078
  };
1012
1079
 
1013
- // ../../lib/contract/src/diagnostics/slot.ts
1080
+ // ../../lib/contract/src/diagnostics/input-accessibility.ts
1014
1081
  import { DiagnosticCategory as DiagnosticCategory4, DiagnosticCode as DiagnosticCode4 } from "../_shared/diagnostics.js";
1082
+ function accessibilityFact(input) {
1083
+ return { category: DiagnosticCategory4.Accessibility, ...input };
1084
+ }
1085
+ var InputAccessibilityDiagnostics = {
1086
+ missingAccessibleName() {
1087
+ return accessibilityFact({
1088
+ code: DiagnosticCode4.A11yInputMissingAccessibleName,
1089
+ severity: "warning",
1090
+ message: "This input has no accessible name. Add an associated <label>, aria-label, or aria-labelledby.",
1091
+ rationale: "Assistive technology announces a form field by its accessible name; without one, users of screen readers cannot tell what the field is for.",
1092
+ suggestions: [
1093
+ { title: "Add aria-label", description: 'Set aria-label="\u2026" directly on the input.' },
1094
+ {
1095
+ title: "Add an associated <label>",
1096
+ description: 'Wrap the input in a <label>, or point a <label for="\u2026"> at its id.'
1097
+ }
1098
+ ]
1099
+ });
1100
+ },
1101
+ placeholderIsNotLabel() {
1102
+ return accessibilityFact({
1103
+ code: DiagnosticCode4.A11yInputPlaceholderNotLabel,
1104
+ severity: "warning",
1105
+ message: "Placeholder text does not provide an accessible name. Add an associated <label>, aria-label, or aria-labelledby.",
1106
+ rationale: "Placeholder text disappears as users interact with the field and is not treated as the control's accessible name by many assistive technologies.",
1107
+ suggestions: [
1108
+ { title: "Add aria-label", description: 'Set aria-label="\u2026" directly on the input.' },
1109
+ {
1110
+ title: "Add an associated <label>",
1111
+ description: 'Wrap the input in a <label>, or point a <label for="\u2026"> at its id.'
1112
+ }
1113
+ ]
1114
+ });
1115
+ },
1116
+ passwordMissingAutocomplete() {
1117
+ return accessibilityFact({
1118
+ code: DiagnosticCode4.A11yInputPasswordAutocomplete,
1119
+ severity: "warning",
1120
+ message: "Password inputs should specify an autoComplete value.",
1121
+ rationale: "Without an explicit autocomplete hint, password managers and browsers cannot reliably tell a sign-in field apart from a password-creation field.",
1122
+ suggestions: [
1123
+ {
1124
+ title: 'Set autoComplete="current-password"',
1125
+ description: "Use this for sign-in forms."
1126
+ },
1127
+ {
1128
+ title: 'Set autoComplete="new-password"',
1129
+ description: "Use this for sign-up / change-password forms."
1130
+ }
1131
+ ]
1132
+ });
1133
+ },
1134
+ requiredReadOnlyConflict() {
1135
+ return accessibilityFact({
1136
+ code: DiagnosticCode4.A11yInputRequiredReadOnlyConflict,
1137
+ severity: "warning",
1138
+ message: "The required and readOnly attributes are both present. A read-only field cannot satisfy required validation through user interaction.",
1139
+ 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."
1140
+ });
1141
+ }
1142
+ };
1143
+
1144
+ // ../../lib/contract/src/diagnostics/slot.ts
1145
+ import { DiagnosticCategory as DiagnosticCategory5, DiagnosticCode as DiagnosticCode5 } from "../_shared/diagnostics.js";
1015
1146
  var SlotDiagnostics = {
1016
1147
  exclusive(name) {
1017
1148
  return {
1018
- code: DiagnosticCode4.SlotExclusive,
1019
- category: DiagnosticCategory4.Contract,
1149
+ code: DiagnosticCode5.SlotExclusive,
1150
+ category: DiagnosticCategory5.Contract,
1020
1151
  component: name,
1021
1152
  message: `${name}: "as" and "asChild" are mutually exclusive`
1022
1153
  };
1023
1154
  },
1024
1155
  singleChildRequired(name, elementTerm) {
1025
1156
  return {
1026
- code: DiagnosticCode4.SlotSingleChild,
1027
- category: DiagnosticCategory4.Contract,
1157
+ code: DiagnosticCode5.SlotSingleChild,
1158
+ category: DiagnosticCategory5.Contract,
1028
1159
  component: name,
1029
1160
  message: `${name}: asChild requires a ${elementTerm} child`
1030
1161
  };
1031
1162
  },
1032
1163
  singleChildExceeded(name, elementTerm, count) {
1033
1164
  return {
1034
- code: DiagnosticCode4.SlotSingleChild,
1035
- category: DiagnosticCategory4.Contract,
1165
+ code: DiagnosticCode5.SlotSingleChild,
1166
+ category: DiagnosticCategory5.Contract,
1036
1167
  component: name,
1037
1168
  message: `${name}: asChild requires exactly one ${elementTerm} child, got ${count}`
1038
1169
  };
@@ -1040,16 +1171,16 @@ var SlotDiagnostics = {
1040
1171
  discardedChildren(name, elementTerm, count) {
1041
1172
  const suffix = count === 1 ? "" : "ren";
1042
1173
  return {
1043
- code: DiagnosticCode4.SlotDiscardedChildren,
1044
- category: DiagnosticCategory4.Contract,
1174
+ code: DiagnosticCode5.SlotDiscardedChildren,
1175
+ category: DiagnosticCategory5.Contract,
1045
1176
  component: name,
1046
1177
  message: `${name}: asChild discarded ${count} non-element child${suffix} \u2014 only ${elementTerm}s are valid asChild children.`
1047
1178
  };
1048
1179
  },
1049
1180
  renderFnRequired(name, received) {
1050
1181
  return {
1051
- code: DiagnosticCode4.SlotRenderFn,
1052
- category: DiagnosticCategory4.Contract,
1182
+ code: DiagnosticCode5.SlotRenderFn,
1183
+ category: DiagnosticCategory5.Contract,
1053
1184
  component: name,
1054
1185
  message: `${name}: asChild requires a render function as children, got ${received}`
1055
1186
  };
@@ -1111,7 +1242,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1111
1242
  tag,
1112
1243
  role: "",
1113
1244
  attribute: void 0,
1114
- severity: "warning",
1245
+ severity: d.severity,
1115
1246
  phase: "evaluate"
1116
1247
  }
1117
1248
  ]
@@ -1370,13 +1501,14 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1370
1501
  const role = props.role;
1371
1502
  if (!implicitRole || !role || role === implicitRole) return NO_VIOLATIONS;
1372
1503
  if (isStrongImplicitRole(tag) && role === "region") {
1504
+ const diagnostic = HtmlDiagnostics.implicitRoleOverride(tag, implicitRole, role);
1373
1505
  return [
1374
1506
  {
1375
1507
  valid: false,
1376
1508
  fixable: true,
1377
- severity: "error",
1509
+ severity: diagnostic.severity,
1378
1510
  fix: _AriaPolicyEngine.#removeRole,
1379
- diagnostic: HtmlDiagnostics.implicitRoleOverride(tag, implicitRole, role)
1511
+ diagnostic
1380
1512
  }
1381
1513
  ];
1382
1514
  }
@@ -1385,13 +1517,14 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1385
1517
  static #checkRedundantRole({ tag, props, implicitRole }) {
1386
1518
  const role = props.role;
1387
1519
  if (!implicitRole || !role || role !== implicitRole) return NO_VIOLATIONS;
1520
+ const diagnostic = HtmlDiagnostics.implicitRoleRedundant(tag, implicitRole);
1388
1521
  return [
1389
1522
  {
1390
1523
  valid: false,
1391
1524
  fixable: true,
1392
- severity: "warning",
1525
+ severity: diagnostic.severity,
1393
1526
  fix: _AriaPolicyEngine.#removeRole,
1394
- diagnostic: HtmlDiagnostics.implicitRoleRedundant(tag, implicitRole)
1527
+ diagnostic
1395
1528
  }
1396
1529
  ];
1397
1530
  }
@@ -1399,13 +1532,14 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1399
1532
  const role = props.role;
1400
1533
  if (role !== "region") return NO_VIOLATIONS;
1401
1534
  if (!isStandaloneTag(tag)) return NO_VIOLATIONS;
1535
+ const diagnostic = HtmlDiagnostics.standaloneRegionOverride(tag, implicitRole ?? tag);
1402
1536
  return [
1403
1537
  {
1404
1538
  valid: false,
1405
1539
  fixable: true,
1406
- severity: "error",
1540
+ severity: diagnostic.severity,
1407
1541
  fix: _AriaPolicyEngine.#removeRole,
1408
- diagnostic: HtmlDiagnostics.standaloneRegionOverride(tag, implicitRole ?? tag)
1542
+ diagnostic
1409
1543
  }
1410
1544
  ];
1411
1545
  }
@@ -2094,6 +2228,331 @@ var readonlyProps = ({
2094
2228
  // ../core/src/html/evaluators.ts
2095
2229
  import { warnDiagnostics as warnDiagnostics2 } from "../_shared/diagnostics.js";
2096
2230
 
2231
+ // ../core/src/html/input-rules.ts
2232
+ var DEFAULT_INPUT_TYPE = "text";
2233
+ function omit(props, key) {
2234
+ const next = { ...props };
2235
+ delete next[key];
2236
+ return next;
2237
+ }
2238
+ function removeAttributeFix(attribute) {
2239
+ return {
2240
+ kind: `removeAttribute:${attribute}`,
2241
+ apply: ({ props }) => {
2242
+ if (!(attribute in props)) return { applied: false, next: props };
2243
+ return { applied: true, next: omit(props, attribute), previous: props };
2244
+ }
2245
+ };
2246
+ }
2247
+ function inputAttributeRequiresType(attribute, allowedTypes) {
2248
+ const rule = ({ tag, props }) => {
2249
+ if (tag !== "input" || !(attribute in props)) return [];
2250
+ const type = typeof props.type === "string" ? props.type : DEFAULT_INPUT_TYPE;
2251
+ if (allowedTypes.includes(type)) return [];
2252
+ const diagnostic = HtmlDiagnostics.input.attributeIgnoredForType(attribute, type, allowedTypes);
2253
+ return [
2254
+ {
2255
+ valid: false,
2256
+ fixable: true,
2257
+ severity: diagnostic.severity,
2258
+ fix: removeAttributeFix(attribute),
2259
+ diagnostic
2260
+ }
2261
+ ];
2262
+ };
2263
+ return Object.assign(rule, { readsProps: ["type", attribute] });
2264
+ }
2265
+ var TEXT_INPUT_TYPES = ["text", "search", "url", "tel", "email", "password"];
2266
+ var NUMERIC_INPUT_TYPES = [
2267
+ "number",
2268
+ "range",
2269
+ "date",
2270
+ "month",
2271
+ "week",
2272
+ "time",
2273
+ "datetime-local"
2274
+ ];
2275
+ var HTML_INPUT_TYPES = /* @__PURE__ */ new Set([
2276
+ ...TEXT_INPUT_TYPES,
2277
+ ...NUMERIC_INPUT_TYPES,
2278
+ "checkbox",
2279
+ "radio",
2280
+ "file",
2281
+ "color",
2282
+ "hidden",
2283
+ "button",
2284
+ "submit",
2285
+ "reset",
2286
+ "image"
2287
+ ]);
2288
+ var supportedInputTypeRule = Object.assign(
2289
+ ({ tag, props }) => {
2290
+ if (tag !== "input" || typeof props.type !== "string") return [];
2291
+ const type = props.type;
2292
+ if (HTML_INPUT_TYPES.has(type)) return [];
2293
+ const diagnostic = HtmlDiagnostics.input.unsupportedType(type);
2294
+ return [
2295
+ {
2296
+ valid: false,
2297
+ fixable: false,
2298
+ severity: diagnostic.severity,
2299
+ diagnostic
2300
+ }
2301
+ ];
2302
+ },
2303
+ { readsProps: ["type"] }
2304
+ );
2305
+ var checkedRequiresCheckableTypeRule = inputAttributeRequiresType("checked", [
2306
+ "checkbox",
2307
+ "radio"
2308
+ ]);
2309
+ var multipleRequiresSupportedTypeRule = inputAttributeRequiresType("multiple", [
2310
+ "email",
2311
+ "file"
2312
+ ]);
2313
+ var maxLengthRequiresTextTypeRule = inputAttributeRequiresType(
2314
+ "maxLength",
2315
+ TEXT_INPUT_TYPES
2316
+ );
2317
+ var minLengthRequiresTextTypeRule = inputAttributeRequiresType(
2318
+ "minLength",
2319
+ TEXT_INPUT_TYPES
2320
+ );
2321
+ var patternRequiresTextTypeRule = inputAttributeRequiresType("pattern", TEXT_INPUT_TYPES);
2322
+ var minRequiresNumericTypeRule = inputAttributeRequiresType("min", NUMERIC_INPUT_TYPES);
2323
+ var maxRequiresNumericTypeRule = inputAttributeRequiresType("max", NUMERIC_INPUT_TYPES);
2324
+ var stepRequiresNumericTypeRule = inputAttributeRequiresType("step", NUMERIC_INPUT_TYPES);
2325
+ var acceptRequiresFileTypeRule = inputAttributeRequiresType("accept", ["file"]);
2326
+ var captureRequiresFileTypeRule = inputAttributeRequiresType("capture", ["file"]);
2327
+ var inputAccessibleNameRule = Object.assign(
2328
+ ({ tag, props }) => {
2329
+ if (tag !== "input" || props.type === "hidden") return [];
2330
+ if ("aria-label" in props || "aria-labelledby" in props) return [];
2331
+ const hasPlaceholder = typeof props.placeholder === "string" && props.placeholder.length > 0;
2332
+ const diagnostic = hasPlaceholder ? InputAccessibilityDiagnostics.placeholderIsNotLabel() : InputAccessibilityDiagnostics.missingAccessibleName();
2333
+ return [{ valid: false, fixable: false, severity: diagnostic.severity, diagnostic }];
2334
+ },
2335
+ { readsProps: ["type", "aria-label", "aria-labelledby", "placeholder"] }
2336
+ );
2337
+ var PASSWORD_AUTOCOMPLETE_VALUES = ["current-password", "new-password"];
2338
+ var passwordAutocompleteRule = Object.assign(
2339
+ ({ tag, props }) => {
2340
+ if (tag !== "input" || props.type !== "password") return [];
2341
+ const autoComplete = props.autoComplete;
2342
+ const tokens = typeof autoComplete === "string" ? autoComplete.split(" ") : [];
2343
+ if (PASSWORD_AUTOCOMPLETE_VALUES.some((value) => tokens.includes(value))) return [];
2344
+ const diagnostic = InputAccessibilityDiagnostics.passwordMissingAutocomplete();
2345
+ return [{ valid: false, fixable: false, severity: diagnostic.severity, diagnostic }];
2346
+ },
2347
+ { readsProps: ["type", "autoComplete"] }
2348
+ );
2349
+ var requiredReadOnlyConflictRule = Object.assign(
2350
+ ({ tag, props }) => {
2351
+ if (tag !== "input" || !props.required || !props.readOnly) return [];
2352
+ const diagnostic = InputAccessibilityDiagnostics.requiredReadOnlyConflict();
2353
+ return [{ valid: false, fixable: false, severity: diagnostic.severity, diagnostic }];
2354
+ },
2355
+ { readsProps: ["required", "readOnly"] }
2356
+ );
2357
+ var INPUT_RULES = [
2358
+ supportedInputTypeRule,
2359
+ checkedRequiresCheckableTypeRule,
2360
+ multipleRequiresSupportedTypeRule,
2361
+ maxLengthRequiresTextTypeRule,
2362
+ minLengthRequiresTextTypeRule,
2363
+ patternRequiresTextTypeRule,
2364
+ minRequiresNumericTypeRule,
2365
+ maxRequiresNumericTypeRule,
2366
+ stepRequiresNumericTypeRule,
2367
+ acceptRequiresFileTypeRule,
2368
+ captureRequiresFileTypeRule,
2369
+ inputAccessibleNameRule,
2370
+ passwordAutocompleteRule,
2371
+ requiredReadOnlyConflictRule
2372
+ ];
2373
+
2374
+ // ../core/src/html/role-restrictions.ts
2375
+ var ALLOWED_ROLES = {
2376
+ article: ["application", "document", "feed", "main", "none", "presentation", "region"],
2377
+ aside: ["feed", "none", "presentation", "region", "search"],
2378
+ footer: ["group", "none", "presentation"],
2379
+ header: ["group", "none", "presentation"],
2380
+ main: [],
2381
+ nav: [],
2382
+ a: [
2383
+ "button",
2384
+ "checkbox",
2385
+ "menuitem",
2386
+ "menuitemcheckbox",
2387
+ "menuitemradio",
2388
+ "option",
2389
+ "radio",
2390
+ "switch",
2391
+ "tab",
2392
+ "treeitem"
2393
+ ],
2394
+ button: [
2395
+ "checkbox",
2396
+ "link",
2397
+ "menuitem",
2398
+ "menuitemcheckbox",
2399
+ "menuitemradio",
2400
+ "option",
2401
+ "radio",
2402
+ "switch",
2403
+ "tab"
2404
+ ],
2405
+ select: ["menu"],
2406
+ h1: ["tab", "presentation", "none"],
2407
+ h2: ["tab", "presentation", "none"],
2408
+ h3: ["tab", "presentation", "none"],
2409
+ h4: ["tab", "presentation", "none"],
2410
+ h5: ["tab", "presentation", "none"],
2411
+ h6: ["tab", "presentation", "none"],
2412
+ ul: [
2413
+ "directory",
2414
+ "group",
2415
+ "listbox",
2416
+ "menu",
2417
+ "menubar",
2418
+ "radiogroup",
2419
+ "tablist",
2420
+ "toolbar",
2421
+ "tree"
2422
+ ],
2423
+ ol: [
2424
+ "directory",
2425
+ "group",
2426
+ "listbox",
2427
+ "menu",
2428
+ "menubar",
2429
+ "radiogroup",
2430
+ "tablist",
2431
+ "toolbar",
2432
+ "tree"
2433
+ ],
2434
+ li: [
2435
+ "menuitem",
2436
+ "menuitemcheckbox",
2437
+ "menuitemradio",
2438
+ "option",
2439
+ "none",
2440
+ "presentation",
2441
+ "radio",
2442
+ "separator",
2443
+ "tab",
2444
+ "treeitem"
2445
+ ],
2446
+ table: ["grid", "treegrid"],
2447
+ dialog: ["alertdialog"],
2448
+ fieldset: ["none", "presentation", "radiogroup"]
2449
+ };
2450
+ var IMG_NAMED_ROLES = [
2451
+ "button",
2452
+ "checkbox",
2453
+ "link",
2454
+ "menuitem",
2455
+ "menuitemcheckbox",
2456
+ "menuitemradio",
2457
+ "option",
2458
+ "progressbar",
2459
+ "scrollbar",
2460
+ "separator",
2461
+ "slider",
2462
+ "switch",
2463
+ "tab",
2464
+ "treeitem"
2465
+ ];
2466
+ var ALLOWED_INPUT_ROLES = {
2467
+ checkbox: ["menuitemcheckbox", "option", "switch", "button"],
2468
+ radio: ["menuitemradio"],
2469
+ range: [],
2470
+ number: [],
2471
+ search: ["combobox"],
2472
+ text: ["combobox", "searchbox", "spinbutton"],
2473
+ email: ["combobox"],
2474
+ tel: ["combobox"],
2475
+ url: ["combobox"],
2476
+ button: [
2477
+ "link",
2478
+ "menuitem",
2479
+ "menuitemcheckbox",
2480
+ "menuitemradio",
2481
+ "option",
2482
+ "radio",
2483
+ "switch",
2484
+ "tab"
2485
+ ],
2486
+ submit: [
2487
+ "link",
2488
+ "menuitem",
2489
+ "menuitemcheckbox",
2490
+ "menuitemradio",
2491
+ "option",
2492
+ "radio",
2493
+ "switch",
2494
+ "tab"
2495
+ ],
2496
+ reset: [
2497
+ "link",
2498
+ "menuitem",
2499
+ "menuitemcheckbox",
2500
+ "menuitemradio",
2501
+ "option",
2502
+ "radio",
2503
+ "switch",
2504
+ "tab"
2505
+ ],
2506
+ image: [
2507
+ "link",
2508
+ "menuitem",
2509
+ "menuitemcheckbox",
2510
+ "menuitemradio",
2511
+ "option",
2512
+ "radio",
2513
+ "switch",
2514
+ "tab"
2515
+ ],
2516
+ hidden: []
2517
+ };
2518
+ function getAllowedRoles(tag, props) {
2519
+ if (tag === "input") {
2520
+ const type = typeof props.type === "string" ? props.type : "text";
2521
+ return ALLOWED_INPUT_ROLES[type];
2522
+ }
2523
+ if (tag === "img") {
2524
+ return props.alt === "" ? [] : IMG_NAMED_ROLES;
2525
+ }
2526
+ return ALLOWED_ROLES[tag];
2527
+ }
2528
+ var removeRoleFix = {
2529
+ kind: "removeRole",
2530
+ apply: ({ props }) => {
2531
+ if (!("role" in props)) return { applied: false, next: props };
2532
+ const { role: _role, ...rest } = props;
2533
+ return { applied: true, next: rest, previous: props };
2534
+ }
2535
+ };
2536
+ var roleNotPermittedRule = Object.assign(
2537
+ ({ tag, props, implicitRole }) => {
2538
+ const role = props.role;
2539
+ if (typeof role !== "string" || role.length === 0 || role === implicitRole) return [];
2540
+ const allowed = getAllowedRoles(tag, props);
2541
+ if (allowed === void 0 || allowed.includes(role)) return [];
2542
+ const diagnostic = HtmlDiagnostics.roleNotPermitted(tag, role, allowed);
2543
+ return [
2544
+ {
2545
+ valid: false,
2546
+ fixable: true,
2547
+ severity: diagnostic.severity,
2548
+ fix: removeRoleFix,
2549
+ diagnostic
2550
+ }
2551
+ ];
2552
+ },
2553
+ { readsProps: ["role", "type", "alt"] }
2554
+ );
2555
+
2097
2556
  // ../core/src/html/aria-rules.ts
2098
2557
  var LANDMARK_TAG_SET = /* @__PURE__ */ new Set(["article", "aside", "footer", "header", "main", "nav"]);
2099
2558
  var removeLandmarkRoleOverride = {
@@ -2108,13 +2567,14 @@ function landmarkRoleRule({ tag, props, implicitRole }) {
2108
2567
  if (!LANDMARK_TAG_SET.has(tag) || !implicitRole) return [];
2109
2568
  const role = props.role;
2110
2569
  if (!role || role === implicitRole) return [];
2570
+ const diagnostic = HtmlDiagnostics.landmarkRoleOverride(tag, implicitRole, role);
2111
2571
  return [
2112
2572
  {
2113
2573
  valid: false,
2114
2574
  fixable: true,
2115
- severity: "error",
2575
+ severity: diagnostic.severity,
2116
2576
  fix: removeLandmarkRoleOverride,
2117
- diagnostic: HtmlDiagnostics.landmarkRoleOverride(tag, implicitRole, role)
2577
+ diagnostic
2118
2578
  }
2119
2579
  ];
2120
2580
  }
@@ -2134,7 +2594,12 @@ function landmarkNameAdvisory(ctx) {
2134
2594
  if (!ctx.implicitRole || !NAMED_LANDMARK_TAGS.has(ctx.tag)) return [];
2135
2595
  return requireAccessibleName(ctx);
2136
2596
  }
2137
- var HTML_ARIA_RULES = [landmarkRoleRule, landmarkNameAdvisory];
2597
+ var HTML_ARIA_RULES = [
2598
+ landmarkRoleRule,
2599
+ landmarkNameAdvisory,
2600
+ roleNotPermittedRule,
2601
+ ...INPUT_RULES
2602
+ ];
2138
2603
 
2139
2604
  // ../core/src/html/contracts.ts
2140
2605
  import { warnDiagnostics } from "../_shared/diagnostics.js";
@@ -2595,21 +3060,21 @@ function validateRenderProps(diagnostics, options, props, recipeKey) {
2595
3060
  import { throwDiagnostics } from "../_shared/diagnostics.js";
2596
3061
 
2597
3062
  // ../core/src/factory/plugin-diagnostics.ts
2598
- import { DiagnosticCategory as DiagnosticCategory5, DiagnosticCode as DiagnosticCode5 } from "../_shared/diagnostics.js";
3063
+ import { DiagnosticCategory as DiagnosticCategory6, DiagnosticCode as DiagnosticCode6 } from "../_shared/diagnostics.js";
2599
3064
  var PluginDiagnostics = {
2600
3065
  invalidShape(received) {
2601
3066
  const got = received === null ? "null" : typeof received;
2602
3067
  return {
2603
- code: DiagnosticCode5.PluginInvalidShape,
2604
- category: DiagnosticCategory5.Internal,
3068
+ code: DiagnosticCode6.PluginInvalidShape,
3069
+ category: DiagnosticCategory6.Internal,
2605
3070
  message: `[praxis-kit] Plugin factory must return an object with a 'pipeline' function. Got: ${got}.`
2606
3071
  };
2607
3072
  },
2608
3073
  pipelineReturnType(received) {
2609
3074
  const got = received === null ? "null" : Array.isArray(received) ? "array" : typeof received;
2610
3075
  return {
2611
- code: DiagnosticCode5.PluginPipelineReturnType,
2612
- category: DiagnosticCategory5.Internal,
3076
+ code: DiagnosticCode6.PluginPipelineReturnType,
3077
+ category: DiagnosticCategory6.Internal,
2613
3078
  message: `[praxis-kit] Plugin pipeline must return a string. Got: ${got}.`
2614
3079
  };
2615
3080
  }