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.
@@ -995,11 +995,24 @@ var ContractDiagnostics = {
995
995
 
996
996
  // ../../lib/contract/src/diagnostics/html.ts
997
997
  import { DiagnosticCategory as DiagnosticCategory3, DiagnosticCode as DiagnosticCode3 } from "../_shared/diagnostics.js";
998
+ var ATTRIBUTE_IGNORED_CODES = {
999
+ checked: DiagnosticCode3.HtmlInputCheckedIgnoredForType,
1000
+ multiple: DiagnosticCode3.HtmlInputMultipleIgnoredForType,
1001
+ maxLength: DiagnosticCode3.HtmlInputMaxLengthIgnoredForType,
1002
+ minLength: DiagnosticCode3.HtmlInputMinLengthIgnoredForType,
1003
+ pattern: DiagnosticCode3.HtmlInputPatternIgnoredForType,
1004
+ min: DiagnosticCode3.HtmlInputMinIgnoredForType,
1005
+ max: DiagnosticCode3.HtmlInputMaxIgnoredForType,
1006
+ step: DiagnosticCode3.HtmlInputStepIgnoredForType,
1007
+ accept: DiagnosticCode3.HtmlInputAcceptIgnoredForType,
1008
+ capture: DiagnosticCode3.HtmlInputCaptureIgnoredForType
1009
+ };
998
1010
  var HtmlDiagnostics = {
999
1011
  emptyRole(tag) {
1000
1012
  return {
1001
1013
  code: DiagnosticCode3.HtmlEmptyRole,
1002
1014
  category: DiagnosticCategory3.HTML,
1015
+ severity: "warning",
1003
1016
  message: `<${tag}> has an explicit empty role="". Omit the attribute instead.`
1004
1017
  };
1005
1018
  },
@@ -1007,6 +1020,7 @@ var HtmlDiagnostics = {
1007
1020
  return {
1008
1021
  code: DiagnosticCode3.HtmlImplicitRoleRedundant,
1009
1022
  category: DiagnosticCategory3.HTML,
1023
+ severity: "warning",
1010
1024
  message: `<${tag}> already has implicit role="${implicitRole}". Avoid redundant role assignment.`
1011
1025
  };
1012
1026
  },
@@ -1014,6 +1028,7 @@ var HtmlDiagnostics = {
1014
1028
  return {
1015
1029
  code: DiagnosticCode3.HtmlImplicitRoleOverride,
1016
1030
  category: DiagnosticCategory3.HTML,
1031
+ severity: "error",
1017
1032
  message: `<${tag}> should not override its implicit role="${implicitRole}" with role="${role}".`
1018
1033
  };
1019
1034
  },
@@ -1021,6 +1036,7 @@ var HtmlDiagnostics = {
1021
1036
  return {
1022
1037
  code: DiagnosticCode3.HtmlStandaloneRegionOverride,
1023
1038
  category: DiagnosticCategory3.HTML,
1039
+ severity: "error",
1024
1040
  message: `<${tag}> is a self-contained element with implicit role="${implicitRole}". Assigning role="region" has been removed.`
1025
1041
  };
1026
1042
  },
@@ -1028,6 +1044,7 @@ var HtmlDiagnostics = {
1028
1044
  return {
1029
1045
  code: DiagnosticCode3.HtmlLandmarkRoleOverride,
1030
1046
  category: DiagnosticCategory3.HTML,
1047
+ severity: "error",
1031
1048
  message: `<${tag}> has a fixed landmark role="${implicitRole}". role="${role}" overrides it and confuses assistive technology. The override has been removed.`
1032
1049
  };
1033
1050
  },
@@ -1035,34 +1052,148 @@ var HtmlDiagnostics = {
1035
1052
  return {
1036
1053
  code: DiagnosticCode3.HtmlInvalidChild,
1037
1054
  category: DiagnosticCategory3.HTML,
1055
+ severity: "error",
1038
1056
  message: `<${child}> is not a valid direct child of <${parent}>. Allowed: ${allowed}.`
1039
1057
  };
1058
+ },
1059
+ roleNotPermitted(tag, role, allowedRoles) {
1060
+ const allowed = allowedRoles.length > 0 ? allowedRoles.map((r) => `"${r}"`).join(", ") : "none \u2014 no explicit role is permitted on this element";
1061
+ return {
1062
+ code: DiagnosticCode3.HtmlRoleNotPermitted,
1063
+ category: DiagnosticCategory3.HTML,
1064
+ severity: "error",
1065
+ message: `role="${role}" is not permitted on <${tag}>. Allowed alternate role(s): ${allowed}.`,
1066
+ 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.'
1067
+ };
1068
+ },
1069
+ // Reserved for <input>-specific facts (HTML3101–3199, see codes.ts) — later element families
1070
+ // (button, img, table, ...) get their own reserved block and their own namespace here.
1071
+ input: {
1072
+ unsupportedType(type) {
1073
+ return {
1074
+ code: DiagnosticCode3.HtmlInputUnsupportedType,
1075
+ category: DiagnosticCategory3.HTML,
1076
+ severity: "warning",
1077
+ message: `type="${type}" is not a value defined by the HTML specification. Browsers silently fall back to type="text".`,
1078
+ 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).',
1079
+ suggestions: [
1080
+ {
1081
+ title: "Check for a typo in the type value",
1082
+ description: `"${type}" does not match any HTML5 input type.`
1083
+ }
1084
+ ]
1085
+ };
1086
+ },
1087
+ // The user-visible problem is that the attribute is ignored — not that it "requires" a type;
1088
+ // that's the rule's internal framing, not what the browser actually does.
1089
+ attributeIgnoredForType(attribute, type, allowedTypes) {
1090
+ const allowed = allowedTypes.map((t) => `"${t}"`).join(", ");
1091
+ const code = ATTRIBUTE_IGNORED_CODES[attribute];
1092
+ if (!code) throw new Error(`No DiagnosticCode registered for input attribute "${attribute}"`);
1093
+ return {
1094
+ code,
1095
+ category: DiagnosticCategory3.HTML,
1096
+ severity: "warning",
1097
+ message: `"${attribute}" is ignored on <input type="${type}">.`,
1098
+ rationale: `"${attribute}" only has an effect when type is one of: ${allowed}. Browsers silently ignore it on other input types.`,
1099
+ suggestions: [
1100
+ {
1101
+ title: `Remove "${attribute}"`,
1102
+ description: `"${attribute}" only affects <input> when type is one of: ${allowed}.`
1103
+ }
1104
+ ]
1105
+ };
1106
+ }
1040
1107
  }
1041
1108
  };
1042
1109
 
1043
- // ../../lib/contract/src/diagnostics/slot.ts
1110
+ // ../../lib/contract/src/diagnostics/input-accessibility.ts
1044
1111
  import { DiagnosticCategory as DiagnosticCategory4, DiagnosticCode as DiagnosticCode4 } from "../_shared/diagnostics.js";
1112
+ function accessibilityFact(input) {
1113
+ return { category: DiagnosticCategory4.Accessibility, ...input };
1114
+ }
1115
+ var InputAccessibilityDiagnostics = {
1116
+ missingAccessibleName() {
1117
+ return accessibilityFact({
1118
+ code: DiagnosticCode4.A11yInputMissingAccessibleName,
1119
+ severity: "warning",
1120
+ message: "This input has no accessible name. Add an associated <label>, aria-label, or aria-labelledby.",
1121
+ rationale: "Assistive technology announces a form field by its accessible name; without one, users of screen readers cannot tell what the field is for.",
1122
+ suggestions: [
1123
+ { title: "Add aria-label", description: 'Set aria-label="\u2026" directly on the input.' },
1124
+ {
1125
+ title: "Add an associated <label>",
1126
+ description: 'Wrap the input in a <label>, or point a <label for="\u2026"> at its id.'
1127
+ }
1128
+ ]
1129
+ });
1130
+ },
1131
+ placeholderIsNotLabel() {
1132
+ return accessibilityFact({
1133
+ code: DiagnosticCode4.A11yInputPlaceholderNotLabel,
1134
+ severity: "warning",
1135
+ message: "Placeholder text does not provide an accessible name. Add an associated <label>, aria-label, or aria-labelledby.",
1136
+ rationale: "Placeholder text disappears as users interact with the field and is not treated as the control's accessible name by many assistive technologies.",
1137
+ suggestions: [
1138
+ { title: "Add aria-label", description: 'Set aria-label="\u2026" directly on the input.' },
1139
+ {
1140
+ title: "Add an associated <label>",
1141
+ description: 'Wrap the input in a <label>, or point a <label for="\u2026"> at its id.'
1142
+ }
1143
+ ]
1144
+ });
1145
+ },
1146
+ passwordMissingAutocomplete() {
1147
+ return accessibilityFact({
1148
+ code: DiagnosticCode4.A11yInputPasswordAutocomplete,
1149
+ severity: "warning",
1150
+ message: "Password inputs should specify an autoComplete value.",
1151
+ rationale: "Without an explicit autocomplete hint, password managers and browsers cannot reliably tell a sign-in field apart from a password-creation field.",
1152
+ suggestions: [
1153
+ {
1154
+ title: 'Set autoComplete="current-password"',
1155
+ description: "Use this for sign-in forms."
1156
+ },
1157
+ {
1158
+ title: 'Set autoComplete="new-password"',
1159
+ description: "Use this for sign-up / change-password forms."
1160
+ }
1161
+ ]
1162
+ });
1163
+ },
1164
+ requiredReadOnlyConflict() {
1165
+ return accessibilityFact({
1166
+ code: DiagnosticCode4.A11yInputRequiredReadOnlyConflict,
1167
+ severity: "warning",
1168
+ message: "The required and readOnly attributes are both present. A read-only field cannot satisfy required validation through user interaction.",
1169
+ 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."
1170
+ });
1171
+ }
1172
+ };
1173
+
1174
+ // ../../lib/contract/src/diagnostics/slot.ts
1175
+ import { DiagnosticCategory as DiagnosticCategory5, DiagnosticCode as DiagnosticCode5 } from "../_shared/diagnostics.js";
1045
1176
  var SlotDiagnostics = {
1046
1177
  exclusive(name) {
1047
1178
  return {
1048
- code: DiagnosticCode4.SlotExclusive,
1049
- category: DiagnosticCategory4.Contract,
1179
+ code: DiagnosticCode5.SlotExclusive,
1180
+ category: DiagnosticCategory5.Contract,
1050
1181
  component: name,
1051
1182
  message: `${name}: "as" and "asChild" are mutually exclusive`
1052
1183
  };
1053
1184
  },
1054
1185
  singleChildRequired(name, elementTerm) {
1055
1186
  return {
1056
- code: DiagnosticCode4.SlotSingleChild,
1057
- category: DiagnosticCategory4.Contract,
1187
+ code: DiagnosticCode5.SlotSingleChild,
1188
+ category: DiagnosticCategory5.Contract,
1058
1189
  component: name,
1059
1190
  message: `${name}: asChild requires a ${elementTerm} child`
1060
1191
  };
1061
1192
  },
1062
1193
  singleChildExceeded(name, elementTerm, count) {
1063
1194
  return {
1064
- code: DiagnosticCode4.SlotSingleChild,
1065
- category: DiagnosticCategory4.Contract,
1195
+ code: DiagnosticCode5.SlotSingleChild,
1196
+ category: DiagnosticCategory5.Contract,
1066
1197
  component: name,
1067
1198
  message: `${name}: asChild requires exactly one ${elementTerm} child, got ${count}`
1068
1199
  };
@@ -1070,16 +1201,16 @@ var SlotDiagnostics = {
1070
1201
  discardedChildren(name, elementTerm, count) {
1071
1202
  const suffix = count === 1 ? "" : "ren";
1072
1203
  return {
1073
- code: DiagnosticCode4.SlotDiscardedChildren,
1074
- category: DiagnosticCategory4.Contract,
1204
+ code: DiagnosticCode5.SlotDiscardedChildren,
1205
+ category: DiagnosticCategory5.Contract,
1075
1206
  component: name,
1076
1207
  message: `${name}: asChild discarded ${count} non-element child${suffix} \u2014 only ${elementTerm}s are valid asChild children.`
1077
1208
  };
1078
1209
  },
1079
1210
  renderFnRequired(name, received) {
1080
1211
  return {
1081
- code: DiagnosticCode4.SlotRenderFn,
1082
- category: DiagnosticCategory4.Contract,
1212
+ code: DiagnosticCode5.SlotRenderFn,
1213
+ category: DiagnosticCategory5.Contract,
1083
1214
  component: name,
1084
1215
  message: `${name}: asChild requires a render function as children, got ${received}`
1085
1216
  };
@@ -1141,7 +1272,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1141
1272
  tag,
1142
1273
  role: "",
1143
1274
  attribute: void 0,
1144
- severity: "warning",
1275
+ severity: d.severity,
1145
1276
  phase: "evaluate"
1146
1277
  }
1147
1278
  ]
@@ -1400,13 +1531,14 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1400
1531
  const role = props.role;
1401
1532
  if (!implicitRole || !role || role === implicitRole) return NO_VIOLATIONS;
1402
1533
  if (isStrongImplicitRole(tag) && role === "region") {
1534
+ const diagnostic = HtmlDiagnostics.implicitRoleOverride(tag, implicitRole, role);
1403
1535
  return [
1404
1536
  {
1405
1537
  valid: false,
1406
1538
  fixable: true,
1407
- severity: "error",
1539
+ severity: diagnostic.severity,
1408
1540
  fix: _AriaPolicyEngine.#removeRole,
1409
- diagnostic: HtmlDiagnostics.implicitRoleOverride(tag, implicitRole, role)
1541
+ diagnostic
1410
1542
  }
1411
1543
  ];
1412
1544
  }
@@ -1415,13 +1547,14 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1415
1547
  static #checkRedundantRole({ tag, props, implicitRole }) {
1416
1548
  const role = props.role;
1417
1549
  if (!implicitRole || !role || role !== implicitRole) return NO_VIOLATIONS;
1550
+ const diagnostic = HtmlDiagnostics.implicitRoleRedundant(tag, implicitRole);
1418
1551
  return [
1419
1552
  {
1420
1553
  valid: false,
1421
1554
  fixable: true,
1422
- severity: "warning",
1555
+ severity: diagnostic.severity,
1423
1556
  fix: _AriaPolicyEngine.#removeRole,
1424
- diagnostic: HtmlDiagnostics.implicitRoleRedundant(tag, implicitRole)
1557
+ diagnostic
1425
1558
  }
1426
1559
  ];
1427
1560
  }
@@ -1429,13 +1562,14 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1429
1562
  const role = props.role;
1430
1563
  if (role !== "region") return NO_VIOLATIONS;
1431
1564
  if (!isStandaloneTag(tag)) return NO_VIOLATIONS;
1565
+ const diagnostic = HtmlDiagnostics.standaloneRegionOverride(tag, implicitRole ?? tag);
1432
1566
  return [
1433
1567
  {
1434
1568
  valid: false,
1435
1569
  fixable: true,
1436
- severity: "error",
1570
+ severity: diagnostic.severity,
1437
1571
  fix: _AriaPolicyEngine.#removeRole,
1438
- diagnostic: HtmlDiagnostics.standaloneRegionOverride(tag, implicitRole ?? tag)
1572
+ diagnostic
1439
1573
  }
1440
1574
  ];
1441
1575
  }
@@ -2124,6 +2258,331 @@ var readonlyProps = ({
2124
2258
  // ../core/src/html/evaluators.ts
2125
2259
  import { warnDiagnostics as warnDiagnostics2 } from "../_shared/diagnostics.js";
2126
2260
 
2261
+ // ../core/src/html/input-rules.ts
2262
+ var DEFAULT_INPUT_TYPE = "text";
2263
+ function omit(props, key) {
2264
+ const next = { ...props };
2265
+ delete next[key];
2266
+ return next;
2267
+ }
2268
+ function removeAttributeFix(attribute) {
2269
+ return {
2270
+ kind: `removeAttribute:${attribute}`,
2271
+ apply: ({ props }) => {
2272
+ if (!(attribute in props)) return { applied: false, next: props };
2273
+ return { applied: true, next: omit(props, attribute), previous: props };
2274
+ }
2275
+ };
2276
+ }
2277
+ function inputAttributeRequiresType(attribute, allowedTypes) {
2278
+ const rule = ({ tag, props }) => {
2279
+ if (tag !== "input" || !(attribute in props)) return [];
2280
+ const type = typeof props.type === "string" ? props.type : DEFAULT_INPUT_TYPE;
2281
+ if (allowedTypes.includes(type)) return [];
2282
+ const diagnostic = HtmlDiagnostics.input.attributeIgnoredForType(attribute, type, allowedTypes);
2283
+ return [
2284
+ {
2285
+ valid: false,
2286
+ fixable: true,
2287
+ severity: diagnostic.severity,
2288
+ fix: removeAttributeFix(attribute),
2289
+ diagnostic
2290
+ }
2291
+ ];
2292
+ };
2293
+ return Object.assign(rule, { readsProps: ["type", attribute] });
2294
+ }
2295
+ var TEXT_INPUT_TYPES = ["text", "search", "url", "tel", "email", "password"];
2296
+ var NUMERIC_INPUT_TYPES = [
2297
+ "number",
2298
+ "range",
2299
+ "date",
2300
+ "month",
2301
+ "week",
2302
+ "time",
2303
+ "datetime-local"
2304
+ ];
2305
+ var HTML_INPUT_TYPES = /* @__PURE__ */ new Set([
2306
+ ...TEXT_INPUT_TYPES,
2307
+ ...NUMERIC_INPUT_TYPES,
2308
+ "checkbox",
2309
+ "radio",
2310
+ "file",
2311
+ "color",
2312
+ "hidden",
2313
+ "button",
2314
+ "submit",
2315
+ "reset",
2316
+ "image"
2317
+ ]);
2318
+ var supportedInputTypeRule = Object.assign(
2319
+ ({ tag, props }) => {
2320
+ if (tag !== "input" || typeof props.type !== "string") return [];
2321
+ const type = props.type;
2322
+ if (HTML_INPUT_TYPES.has(type)) return [];
2323
+ const diagnostic = HtmlDiagnostics.input.unsupportedType(type);
2324
+ return [
2325
+ {
2326
+ valid: false,
2327
+ fixable: false,
2328
+ severity: diagnostic.severity,
2329
+ diagnostic
2330
+ }
2331
+ ];
2332
+ },
2333
+ { readsProps: ["type"] }
2334
+ );
2335
+ var checkedRequiresCheckableTypeRule = inputAttributeRequiresType("checked", [
2336
+ "checkbox",
2337
+ "radio"
2338
+ ]);
2339
+ var multipleRequiresSupportedTypeRule = inputAttributeRequiresType("multiple", [
2340
+ "email",
2341
+ "file"
2342
+ ]);
2343
+ var maxLengthRequiresTextTypeRule = inputAttributeRequiresType(
2344
+ "maxLength",
2345
+ TEXT_INPUT_TYPES
2346
+ );
2347
+ var minLengthRequiresTextTypeRule = inputAttributeRequiresType(
2348
+ "minLength",
2349
+ TEXT_INPUT_TYPES
2350
+ );
2351
+ var patternRequiresTextTypeRule = inputAttributeRequiresType("pattern", TEXT_INPUT_TYPES);
2352
+ var minRequiresNumericTypeRule = inputAttributeRequiresType("min", NUMERIC_INPUT_TYPES);
2353
+ var maxRequiresNumericTypeRule = inputAttributeRequiresType("max", NUMERIC_INPUT_TYPES);
2354
+ var stepRequiresNumericTypeRule = inputAttributeRequiresType("step", NUMERIC_INPUT_TYPES);
2355
+ var acceptRequiresFileTypeRule = inputAttributeRequiresType("accept", ["file"]);
2356
+ var captureRequiresFileTypeRule = inputAttributeRequiresType("capture", ["file"]);
2357
+ var inputAccessibleNameRule = Object.assign(
2358
+ ({ tag, props }) => {
2359
+ if (tag !== "input" || props.type === "hidden") return [];
2360
+ if ("aria-label" in props || "aria-labelledby" in props) return [];
2361
+ const hasPlaceholder = typeof props.placeholder === "string" && props.placeholder.length > 0;
2362
+ const diagnostic = hasPlaceholder ? InputAccessibilityDiagnostics.placeholderIsNotLabel() : InputAccessibilityDiagnostics.missingAccessibleName();
2363
+ return [{ valid: false, fixable: false, severity: diagnostic.severity, diagnostic }];
2364
+ },
2365
+ { readsProps: ["type", "aria-label", "aria-labelledby", "placeholder"] }
2366
+ );
2367
+ var PASSWORD_AUTOCOMPLETE_VALUES = ["current-password", "new-password"];
2368
+ var passwordAutocompleteRule = Object.assign(
2369
+ ({ tag, props }) => {
2370
+ if (tag !== "input" || props.type !== "password") return [];
2371
+ const autoComplete = props.autoComplete;
2372
+ const tokens = typeof autoComplete === "string" ? autoComplete.split(" ") : [];
2373
+ if (PASSWORD_AUTOCOMPLETE_VALUES.some((value) => tokens.includes(value))) return [];
2374
+ const diagnostic = InputAccessibilityDiagnostics.passwordMissingAutocomplete();
2375
+ return [{ valid: false, fixable: false, severity: diagnostic.severity, diagnostic }];
2376
+ },
2377
+ { readsProps: ["type", "autoComplete"] }
2378
+ );
2379
+ var requiredReadOnlyConflictRule = Object.assign(
2380
+ ({ tag, props }) => {
2381
+ if (tag !== "input" || !props.required || !props.readOnly) return [];
2382
+ const diagnostic = InputAccessibilityDiagnostics.requiredReadOnlyConflict();
2383
+ return [{ valid: false, fixable: false, severity: diagnostic.severity, diagnostic }];
2384
+ },
2385
+ { readsProps: ["required", "readOnly"] }
2386
+ );
2387
+ var INPUT_RULES = [
2388
+ supportedInputTypeRule,
2389
+ checkedRequiresCheckableTypeRule,
2390
+ multipleRequiresSupportedTypeRule,
2391
+ maxLengthRequiresTextTypeRule,
2392
+ minLengthRequiresTextTypeRule,
2393
+ patternRequiresTextTypeRule,
2394
+ minRequiresNumericTypeRule,
2395
+ maxRequiresNumericTypeRule,
2396
+ stepRequiresNumericTypeRule,
2397
+ acceptRequiresFileTypeRule,
2398
+ captureRequiresFileTypeRule,
2399
+ inputAccessibleNameRule,
2400
+ passwordAutocompleteRule,
2401
+ requiredReadOnlyConflictRule
2402
+ ];
2403
+
2404
+ // ../core/src/html/role-restrictions.ts
2405
+ var ALLOWED_ROLES = {
2406
+ article: ["application", "document", "feed", "main", "none", "presentation", "region"],
2407
+ aside: ["feed", "none", "presentation", "region", "search"],
2408
+ footer: ["group", "none", "presentation"],
2409
+ header: ["group", "none", "presentation"],
2410
+ main: [],
2411
+ nav: [],
2412
+ a: [
2413
+ "button",
2414
+ "checkbox",
2415
+ "menuitem",
2416
+ "menuitemcheckbox",
2417
+ "menuitemradio",
2418
+ "option",
2419
+ "radio",
2420
+ "switch",
2421
+ "tab",
2422
+ "treeitem"
2423
+ ],
2424
+ button: [
2425
+ "checkbox",
2426
+ "link",
2427
+ "menuitem",
2428
+ "menuitemcheckbox",
2429
+ "menuitemradio",
2430
+ "option",
2431
+ "radio",
2432
+ "switch",
2433
+ "tab"
2434
+ ],
2435
+ select: ["menu"],
2436
+ h1: ["tab", "presentation", "none"],
2437
+ h2: ["tab", "presentation", "none"],
2438
+ h3: ["tab", "presentation", "none"],
2439
+ h4: ["tab", "presentation", "none"],
2440
+ h5: ["tab", "presentation", "none"],
2441
+ h6: ["tab", "presentation", "none"],
2442
+ ul: [
2443
+ "directory",
2444
+ "group",
2445
+ "listbox",
2446
+ "menu",
2447
+ "menubar",
2448
+ "radiogroup",
2449
+ "tablist",
2450
+ "toolbar",
2451
+ "tree"
2452
+ ],
2453
+ ol: [
2454
+ "directory",
2455
+ "group",
2456
+ "listbox",
2457
+ "menu",
2458
+ "menubar",
2459
+ "radiogroup",
2460
+ "tablist",
2461
+ "toolbar",
2462
+ "tree"
2463
+ ],
2464
+ li: [
2465
+ "menuitem",
2466
+ "menuitemcheckbox",
2467
+ "menuitemradio",
2468
+ "option",
2469
+ "none",
2470
+ "presentation",
2471
+ "radio",
2472
+ "separator",
2473
+ "tab",
2474
+ "treeitem"
2475
+ ],
2476
+ table: ["grid", "treegrid"],
2477
+ dialog: ["alertdialog"],
2478
+ fieldset: ["none", "presentation", "radiogroup"]
2479
+ };
2480
+ var IMG_NAMED_ROLES = [
2481
+ "button",
2482
+ "checkbox",
2483
+ "link",
2484
+ "menuitem",
2485
+ "menuitemcheckbox",
2486
+ "menuitemradio",
2487
+ "option",
2488
+ "progressbar",
2489
+ "scrollbar",
2490
+ "separator",
2491
+ "slider",
2492
+ "switch",
2493
+ "tab",
2494
+ "treeitem"
2495
+ ];
2496
+ var ALLOWED_INPUT_ROLES = {
2497
+ checkbox: ["menuitemcheckbox", "option", "switch", "button"],
2498
+ radio: ["menuitemradio"],
2499
+ range: [],
2500
+ number: [],
2501
+ search: ["combobox"],
2502
+ text: ["combobox", "searchbox", "spinbutton"],
2503
+ email: ["combobox"],
2504
+ tel: ["combobox"],
2505
+ url: ["combobox"],
2506
+ button: [
2507
+ "link",
2508
+ "menuitem",
2509
+ "menuitemcheckbox",
2510
+ "menuitemradio",
2511
+ "option",
2512
+ "radio",
2513
+ "switch",
2514
+ "tab"
2515
+ ],
2516
+ submit: [
2517
+ "link",
2518
+ "menuitem",
2519
+ "menuitemcheckbox",
2520
+ "menuitemradio",
2521
+ "option",
2522
+ "radio",
2523
+ "switch",
2524
+ "tab"
2525
+ ],
2526
+ reset: [
2527
+ "link",
2528
+ "menuitem",
2529
+ "menuitemcheckbox",
2530
+ "menuitemradio",
2531
+ "option",
2532
+ "radio",
2533
+ "switch",
2534
+ "tab"
2535
+ ],
2536
+ image: [
2537
+ "link",
2538
+ "menuitem",
2539
+ "menuitemcheckbox",
2540
+ "menuitemradio",
2541
+ "option",
2542
+ "radio",
2543
+ "switch",
2544
+ "tab"
2545
+ ],
2546
+ hidden: []
2547
+ };
2548
+ function getAllowedRoles(tag, props) {
2549
+ if (tag === "input") {
2550
+ const type = typeof props.type === "string" ? props.type : "text";
2551
+ return ALLOWED_INPUT_ROLES[type];
2552
+ }
2553
+ if (tag === "img") {
2554
+ return props.alt === "" ? [] : IMG_NAMED_ROLES;
2555
+ }
2556
+ return ALLOWED_ROLES[tag];
2557
+ }
2558
+ var removeRoleFix = {
2559
+ kind: "removeRole",
2560
+ apply: ({ props }) => {
2561
+ if (!("role" in props)) return { applied: false, next: props };
2562
+ const { role: _role, ...rest } = props;
2563
+ return { applied: true, next: rest, previous: props };
2564
+ }
2565
+ };
2566
+ var roleNotPermittedRule = Object.assign(
2567
+ ({ tag, props, implicitRole }) => {
2568
+ const role = props.role;
2569
+ if (typeof role !== "string" || role.length === 0 || role === implicitRole) return [];
2570
+ const allowed = getAllowedRoles(tag, props);
2571
+ if (allowed === void 0 || allowed.includes(role)) return [];
2572
+ const diagnostic = HtmlDiagnostics.roleNotPermitted(tag, role, allowed);
2573
+ return [
2574
+ {
2575
+ valid: false,
2576
+ fixable: true,
2577
+ severity: diagnostic.severity,
2578
+ fix: removeRoleFix,
2579
+ diagnostic
2580
+ }
2581
+ ];
2582
+ },
2583
+ { readsProps: ["role", "type", "alt"] }
2584
+ );
2585
+
2127
2586
  // ../core/src/html/aria-rules.ts
2128
2587
  var LANDMARK_TAG_SET = /* @__PURE__ */ new Set(["article", "aside", "footer", "header", "main", "nav"]);
2129
2588
  var removeLandmarkRoleOverride = {
@@ -2138,13 +2597,14 @@ function landmarkRoleRule({ tag, props, implicitRole }) {
2138
2597
  if (!LANDMARK_TAG_SET.has(tag) || !implicitRole) return [];
2139
2598
  const role = props.role;
2140
2599
  if (!role || role === implicitRole) return [];
2600
+ const diagnostic = HtmlDiagnostics.landmarkRoleOverride(tag, implicitRole, role);
2141
2601
  return [
2142
2602
  {
2143
2603
  valid: false,
2144
2604
  fixable: true,
2145
- severity: "error",
2605
+ severity: diagnostic.severity,
2146
2606
  fix: removeLandmarkRoleOverride,
2147
- diagnostic: HtmlDiagnostics.landmarkRoleOverride(tag, implicitRole, role)
2607
+ diagnostic
2148
2608
  }
2149
2609
  ];
2150
2610
  }
@@ -2164,7 +2624,12 @@ function landmarkNameAdvisory(ctx) {
2164
2624
  if (!ctx.implicitRole || !NAMED_LANDMARK_TAGS.has(ctx.tag)) return [];
2165
2625
  return requireAccessibleName(ctx);
2166
2626
  }
2167
- var HTML_ARIA_RULES = [landmarkRoleRule, landmarkNameAdvisory];
2627
+ var HTML_ARIA_RULES = [
2628
+ landmarkRoleRule,
2629
+ landmarkNameAdvisory,
2630
+ roleNotPermittedRule,
2631
+ ...INPUT_RULES
2632
+ ];
2168
2633
 
2169
2634
  // ../core/src/html/contracts.ts
2170
2635
  import { warnDiagnostics } from "../_shared/diagnostics.js";
@@ -2625,21 +3090,21 @@ function validateRenderProps(diagnostics, options, props, recipeKey) {
2625
3090
  import { throwDiagnostics } from "../_shared/diagnostics.js";
2626
3091
 
2627
3092
  // ../core/src/factory/plugin-diagnostics.ts
2628
- import { DiagnosticCategory as DiagnosticCategory5, DiagnosticCode as DiagnosticCode5 } from "../_shared/diagnostics.js";
3093
+ import { DiagnosticCategory as DiagnosticCategory6, DiagnosticCode as DiagnosticCode6 } from "../_shared/diagnostics.js";
2629
3094
  var PluginDiagnostics = {
2630
3095
  invalidShape(received) {
2631
3096
  const got = received === null ? "null" : typeof received;
2632
3097
  return {
2633
- code: DiagnosticCode5.PluginInvalidShape,
2634
- category: DiagnosticCategory5.Internal,
3098
+ code: DiagnosticCode6.PluginInvalidShape,
3099
+ category: DiagnosticCategory6.Internal,
2635
3100
  message: `[praxis-kit] Plugin factory must return an object with a 'pipeline' function. Got: ${got}.`
2636
3101
  };
2637
3102
  },
2638
3103
  pipelineReturnType(received) {
2639
3104
  const got = received === null ? "null" : Array.isArray(received) ? "array" : typeof received;
2640
3105
  return {
2641
- code: DiagnosticCode5.PluginPipelineReturnType,
2642
- category: DiagnosticCategory5.Internal,
3106
+ code: DiagnosticCode6.PluginPipelineReturnType,
3107
+ category: DiagnosticCategory6.Internal,
2643
3108
  message: `[praxis-kit] Plugin pipeline must return a string. Got: ${got}.`
2644
3109
  };
2645
3110
  }
@@ -12,7 +12,7 @@ import {
12
12
  makeCloneSlotChild,
13
13
  mergeRefs,
14
14
  render
15
- } from "../chunk-4YVNOMUS.js";
15
+ } from "../chunk-H3D2JA3J.js";
16
16
 
17
17
  // ../../adapters/react/src/current/slot/composeRefs.ts
18
18
  function getChildRef(element) {