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.
@@ -13,7 +13,7 @@ import {
13
13
  makeCloneSlotChild,
14
14
  mergeRefs,
15
15
  render
16
- } from "../chunk-4YVNOMUS.js";
16
+ } from "../chunk-H3D2JA3J.js";
17
17
 
18
18
  // ../../adapters/react/src/legacy/create-contract-component.ts
19
19
  import { forwardRef as forwardRef2 } from "react";
@@ -954,11 +954,24 @@ var ContractDiagnostics = {
954
954
 
955
955
  // ../../lib/contract/src/diagnostics/html.ts
956
956
  import { DiagnosticCategory as DiagnosticCategory3, DiagnosticCode as DiagnosticCode3 } from "../_shared/diagnostics.js";
957
+ var ATTRIBUTE_IGNORED_CODES = {
958
+ checked: DiagnosticCode3.HtmlInputCheckedIgnoredForType,
959
+ multiple: DiagnosticCode3.HtmlInputMultipleIgnoredForType,
960
+ maxLength: DiagnosticCode3.HtmlInputMaxLengthIgnoredForType,
961
+ minLength: DiagnosticCode3.HtmlInputMinLengthIgnoredForType,
962
+ pattern: DiagnosticCode3.HtmlInputPatternIgnoredForType,
963
+ min: DiagnosticCode3.HtmlInputMinIgnoredForType,
964
+ max: DiagnosticCode3.HtmlInputMaxIgnoredForType,
965
+ step: DiagnosticCode3.HtmlInputStepIgnoredForType,
966
+ accept: DiagnosticCode3.HtmlInputAcceptIgnoredForType,
967
+ capture: DiagnosticCode3.HtmlInputCaptureIgnoredForType
968
+ };
957
969
  var HtmlDiagnostics = {
958
970
  emptyRole(tag) {
959
971
  return {
960
972
  code: DiagnosticCode3.HtmlEmptyRole,
961
973
  category: DiagnosticCategory3.HTML,
974
+ severity: "warning",
962
975
  message: `<${tag}> has an explicit empty role="". Omit the attribute instead.`
963
976
  };
964
977
  },
@@ -966,6 +979,7 @@ var HtmlDiagnostics = {
966
979
  return {
967
980
  code: DiagnosticCode3.HtmlImplicitRoleRedundant,
968
981
  category: DiagnosticCategory3.HTML,
982
+ severity: "warning",
969
983
  message: `<${tag}> already has implicit role="${implicitRole}". Avoid redundant role assignment.`
970
984
  };
971
985
  },
@@ -973,6 +987,7 @@ var HtmlDiagnostics = {
973
987
  return {
974
988
  code: DiagnosticCode3.HtmlImplicitRoleOverride,
975
989
  category: DiagnosticCategory3.HTML,
990
+ severity: "error",
976
991
  message: `<${tag}> should not override its implicit role="${implicitRole}" with role="${role}".`
977
992
  };
978
993
  },
@@ -980,6 +995,7 @@ var HtmlDiagnostics = {
980
995
  return {
981
996
  code: DiagnosticCode3.HtmlStandaloneRegionOverride,
982
997
  category: DiagnosticCategory3.HTML,
998
+ severity: "error",
983
999
  message: `<${tag}> is a self-contained element with implicit role="${implicitRole}". Assigning role="region" has been removed.`
984
1000
  };
985
1001
  },
@@ -987,6 +1003,7 @@ var HtmlDiagnostics = {
987
1003
  return {
988
1004
  code: DiagnosticCode3.HtmlLandmarkRoleOverride,
989
1005
  category: DiagnosticCategory3.HTML,
1006
+ severity: "error",
990
1007
  message: `<${tag}> has a fixed landmark role="${implicitRole}". role="${role}" overrides it and confuses assistive technology. The override has been removed.`
991
1008
  };
992
1009
  },
@@ -994,34 +1011,148 @@ var HtmlDiagnostics = {
994
1011
  return {
995
1012
  code: DiagnosticCode3.HtmlInvalidChild,
996
1013
  category: DiagnosticCategory3.HTML,
1014
+ severity: "error",
997
1015
  message: `<${child}> is not a valid direct child of <${parent}>. Allowed: ${allowed}.`
998
1016
  };
1017
+ },
1018
+ roleNotPermitted(tag, role, allowedRoles) {
1019
+ const allowed = allowedRoles.length > 0 ? allowedRoles.map((r) => `"${r}"`).join(", ") : "none \u2014 no explicit role is permitted on this element";
1020
+ return {
1021
+ code: DiagnosticCode3.HtmlRoleNotPermitted,
1022
+ category: DiagnosticCategory3.HTML,
1023
+ severity: "error",
1024
+ message: `role="${role}" is not permitted on <${tag}>. Allowed alternate role(s): ${allowed}.`,
1025
+ 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.'
1026
+ };
1027
+ },
1028
+ // Reserved for <input>-specific facts (HTML3101–3199, see codes.ts) — later element families
1029
+ // (button, img, table, ...) get their own reserved block and their own namespace here.
1030
+ input: {
1031
+ unsupportedType(type) {
1032
+ return {
1033
+ code: DiagnosticCode3.HtmlInputUnsupportedType,
1034
+ category: DiagnosticCategory3.HTML,
1035
+ severity: "warning",
1036
+ message: `type="${type}" is not a value defined by the HTML specification. Browsers silently fall back to type="text".`,
1037
+ 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).',
1038
+ suggestions: [
1039
+ {
1040
+ title: "Check for a typo in the type value",
1041
+ description: `"${type}" does not match any HTML5 input type.`
1042
+ }
1043
+ ]
1044
+ };
1045
+ },
1046
+ // The user-visible problem is that the attribute is ignored — not that it "requires" a type;
1047
+ // that's the rule's internal framing, not what the browser actually does.
1048
+ attributeIgnoredForType(attribute, type, allowedTypes) {
1049
+ const allowed = allowedTypes.map((t) => `"${t}"`).join(", ");
1050
+ const code = ATTRIBUTE_IGNORED_CODES[attribute];
1051
+ if (!code) throw new Error(`No DiagnosticCode registered for input attribute "${attribute}"`);
1052
+ return {
1053
+ code,
1054
+ category: DiagnosticCategory3.HTML,
1055
+ severity: "warning",
1056
+ message: `"${attribute}" is ignored on <input type="${type}">.`,
1057
+ rationale: `"${attribute}" only has an effect when type is one of: ${allowed}. Browsers silently ignore it on other input types.`,
1058
+ suggestions: [
1059
+ {
1060
+ title: `Remove "${attribute}"`,
1061
+ description: `"${attribute}" only affects <input> when type is one of: ${allowed}.`
1062
+ }
1063
+ ]
1064
+ };
1065
+ }
999
1066
  }
1000
1067
  };
1001
1068
 
1002
- // ../../lib/contract/src/diagnostics/slot.ts
1069
+ // ../../lib/contract/src/diagnostics/input-accessibility.ts
1003
1070
  import { DiagnosticCategory as DiagnosticCategory4, DiagnosticCode as DiagnosticCode4 } from "../_shared/diagnostics.js";
1071
+ function accessibilityFact(input) {
1072
+ return { category: DiagnosticCategory4.Accessibility, ...input };
1073
+ }
1074
+ var InputAccessibilityDiagnostics = {
1075
+ missingAccessibleName() {
1076
+ return accessibilityFact({
1077
+ code: DiagnosticCode4.A11yInputMissingAccessibleName,
1078
+ severity: "warning",
1079
+ message: "This input has no accessible name. Add an associated <label>, aria-label, or aria-labelledby.",
1080
+ rationale: "Assistive technology announces a form field by its accessible name; without one, users of screen readers cannot tell what the field is for.",
1081
+ suggestions: [
1082
+ { title: "Add aria-label", description: 'Set aria-label="\u2026" directly on the input.' },
1083
+ {
1084
+ title: "Add an associated <label>",
1085
+ description: 'Wrap the input in a <label>, or point a <label for="\u2026"> at its id.'
1086
+ }
1087
+ ]
1088
+ });
1089
+ },
1090
+ placeholderIsNotLabel() {
1091
+ return accessibilityFact({
1092
+ code: DiagnosticCode4.A11yInputPlaceholderNotLabel,
1093
+ severity: "warning",
1094
+ message: "Placeholder text does not provide an accessible name. Add an associated <label>, aria-label, or aria-labelledby.",
1095
+ rationale: "Placeholder text disappears as users interact with the field and is not treated as the control's accessible name by many assistive technologies.",
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
+ passwordMissingAutocomplete() {
1106
+ return accessibilityFact({
1107
+ code: DiagnosticCode4.A11yInputPasswordAutocomplete,
1108
+ severity: "warning",
1109
+ message: "Password inputs should specify an autoComplete value.",
1110
+ rationale: "Without an explicit autocomplete hint, password managers and browsers cannot reliably tell a sign-in field apart from a password-creation field.",
1111
+ suggestions: [
1112
+ {
1113
+ title: 'Set autoComplete="current-password"',
1114
+ description: "Use this for sign-in forms."
1115
+ },
1116
+ {
1117
+ title: 'Set autoComplete="new-password"',
1118
+ description: "Use this for sign-up / change-password forms."
1119
+ }
1120
+ ]
1121
+ });
1122
+ },
1123
+ requiredReadOnlyConflict() {
1124
+ return accessibilityFact({
1125
+ code: DiagnosticCode4.A11yInputRequiredReadOnlyConflict,
1126
+ severity: "warning",
1127
+ message: "The required and readOnly attributes are both present. A read-only field cannot satisfy required validation through user interaction.",
1128
+ 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."
1129
+ });
1130
+ }
1131
+ };
1132
+
1133
+ // ../../lib/contract/src/diagnostics/slot.ts
1134
+ import { DiagnosticCategory as DiagnosticCategory5, DiagnosticCode as DiagnosticCode5 } from "../_shared/diagnostics.js";
1004
1135
  var SlotDiagnostics = {
1005
1136
  exclusive(name) {
1006
1137
  return {
1007
- code: DiagnosticCode4.SlotExclusive,
1008
- category: DiagnosticCategory4.Contract,
1138
+ code: DiagnosticCode5.SlotExclusive,
1139
+ category: DiagnosticCategory5.Contract,
1009
1140
  component: name,
1010
1141
  message: `${name}: "as" and "asChild" are mutually exclusive`
1011
1142
  };
1012
1143
  },
1013
1144
  singleChildRequired(name, elementTerm) {
1014
1145
  return {
1015
- code: DiagnosticCode4.SlotSingleChild,
1016
- category: DiagnosticCategory4.Contract,
1146
+ code: DiagnosticCode5.SlotSingleChild,
1147
+ category: DiagnosticCategory5.Contract,
1017
1148
  component: name,
1018
1149
  message: `${name}: asChild requires a ${elementTerm} child`
1019
1150
  };
1020
1151
  },
1021
1152
  singleChildExceeded(name, elementTerm, count) {
1022
1153
  return {
1023
- code: DiagnosticCode4.SlotSingleChild,
1024
- category: DiagnosticCategory4.Contract,
1154
+ code: DiagnosticCode5.SlotSingleChild,
1155
+ category: DiagnosticCategory5.Contract,
1025
1156
  component: name,
1026
1157
  message: `${name}: asChild requires exactly one ${elementTerm} child, got ${count}`
1027
1158
  };
@@ -1029,16 +1160,16 @@ var SlotDiagnostics = {
1029
1160
  discardedChildren(name, elementTerm, count) {
1030
1161
  const suffix = count === 1 ? "" : "ren";
1031
1162
  return {
1032
- code: DiagnosticCode4.SlotDiscardedChildren,
1033
- category: DiagnosticCategory4.Contract,
1163
+ code: DiagnosticCode5.SlotDiscardedChildren,
1164
+ category: DiagnosticCategory5.Contract,
1034
1165
  component: name,
1035
1166
  message: `${name}: asChild discarded ${count} non-element child${suffix} \u2014 only ${elementTerm}s are valid asChild children.`
1036
1167
  };
1037
1168
  },
1038
1169
  renderFnRequired(name, received) {
1039
1170
  return {
1040
- code: DiagnosticCode4.SlotRenderFn,
1041
- category: DiagnosticCategory4.Contract,
1171
+ code: DiagnosticCode5.SlotRenderFn,
1172
+ category: DiagnosticCategory5.Contract,
1042
1173
  component: name,
1043
1174
  message: `${name}: asChild requires a render function as children, got ${received}`
1044
1175
  };
@@ -1100,7 +1231,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1100
1231
  tag,
1101
1232
  role: "",
1102
1233
  attribute: void 0,
1103
- severity: "warning",
1234
+ severity: d.severity,
1104
1235
  phase: "evaluate"
1105
1236
  }
1106
1237
  ]
@@ -1359,13 +1490,14 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1359
1490
  const role = props.role;
1360
1491
  if (!implicitRole || !role || role === implicitRole) return NO_VIOLATIONS;
1361
1492
  if (isStrongImplicitRole(tag) && role === "region") {
1493
+ const diagnostic = HtmlDiagnostics.implicitRoleOverride(tag, implicitRole, role);
1362
1494
  return [
1363
1495
  {
1364
1496
  valid: false,
1365
1497
  fixable: true,
1366
- severity: "error",
1498
+ severity: diagnostic.severity,
1367
1499
  fix: _AriaPolicyEngine.#removeRole,
1368
- diagnostic: HtmlDiagnostics.implicitRoleOverride(tag, implicitRole, role)
1500
+ diagnostic
1369
1501
  }
1370
1502
  ];
1371
1503
  }
@@ -1374,13 +1506,14 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1374
1506
  static #checkRedundantRole({ tag, props, implicitRole }) {
1375
1507
  const role = props.role;
1376
1508
  if (!implicitRole || !role || role !== implicitRole) return NO_VIOLATIONS;
1509
+ const diagnostic = HtmlDiagnostics.implicitRoleRedundant(tag, implicitRole);
1377
1510
  return [
1378
1511
  {
1379
1512
  valid: false,
1380
1513
  fixable: true,
1381
- severity: "warning",
1514
+ severity: diagnostic.severity,
1382
1515
  fix: _AriaPolicyEngine.#removeRole,
1383
- diagnostic: HtmlDiagnostics.implicitRoleRedundant(tag, implicitRole)
1516
+ diagnostic
1384
1517
  }
1385
1518
  ];
1386
1519
  }
@@ -1388,13 +1521,14 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1388
1521
  const role = props.role;
1389
1522
  if (role !== "region") return NO_VIOLATIONS;
1390
1523
  if (!isStandaloneTag(tag)) return NO_VIOLATIONS;
1524
+ const diagnostic = HtmlDiagnostics.standaloneRegionOverride(tag, implicitRole ?? tag);
1391
1525
  return [
1392
1526
  {
1393
1527
  valid: false,
1394
1528
  fixable: true,
1395
- severity: "error",
1529
+ severity: diagnostic.severity,
1396
1530
  fix: _AriaPolicyEngine.#removeRole,
1397
- diagnostic: HtmlDiagnostics.standaloneRegionOverride(tag, implicitRole ?? tag)
1531
+ diagnostic
1398
1532
  }
1399
1533
  ];
1400
1534
  }
@@ -2083,6 +2217,331 @@ var readonlyProps = ({
2083
2217
  // ../core/src/html/evaluators.ts
2084
2218
  import { warnDiagnostics as warnDiagnostics2 } from "../_shared/diagnostics.js";
2085
2219
 
2220
+ // ../core/src/html/input-rules.ts
2221
+ var DEFAULT_INPUT_TYPE = "text";
2222
+ function omit(props, key) {
2223
+ const next = { ...props };
2224
+ delete next[key];
2225
+ return next;
2226
+ }
2227
+ function removeAttributeFix(attribute) {
2228
+ return {
2229
+ kind: `removeAttribute:${attribute}`,
2230
+ apply: ({ props }) => {
2231
+ if (!(attribute in props)) return { applied: false, next: props };
2232
+ return { applied: true, next: omit(props, attribute), previous: props };
2233
+ }
2234
+ };
2235
+ }
2236
+ function inputAttributeRequiresType(attribute, allowedTypes) {
2237
+ const rule = ({ tag, props }) => {
2238
+ if (tag !== "input" || !(attribute in props)) return [];
2239
+ const type = typeof props.type === "string" ? props.type : DEFAULT_INPUT_TYPE;
2240
+ if (allowedTypes.includes(type)) return [];
2241
+ const diagnostic = HtmlDiagnostics.input.attributeIgnoredForType(attribute, type, allowedTypes);
2242
+ return [
2243
+ {
2244
+ valid: false,
2245
+ fixable: true,
2246
+ severity: diagnostic.severity,
2247
+ fix: removeAttributeFix(attribute),
2248
+ diagnostic
2249
+ }
2250
+ ];
2251
+ };
2252
+ return Object.assign(rule, { readsProps: ["type", attribute] });
2253
+ }
2254
+ var TEXT_INPUT_TYPES = ["text", "search", "url", "tel", "email", "password"];
2255
+ var NUMERIC_INPUT_TYPES = [
2256
+ "number",
2257
+ "range",
2258
+ "date",
2259
+ "month",
2260
+ "week",
2261
+ "time",
2262
+ "datetime-local"
2263
+ ];
2264
+ var HTML_INPUT_TYPES = /* @__PURE__ */ new Set([
2265
+ ...TEXT_INPUT_TYPES,
2266
+ ...NUMERIC_INPUT_TYPES,
2267
+ "checkbox",
2268
+ "radio",
2269
+ "file",
2270
+ "color",
2271
+ "hidden",
2272
+ "button",
2273
+ "submit",
2274
+ "reset",
2275
+ "image"
2276
+ ]);
2277
+ var supportedInputTypeRule = Object.assign(
2278
+ ({ tag, props }) => {
2279
+ if (tag !== "input" || typeof props.type !== "string") return [];
2280
+ const type = props.type;
2281
+ if (HTML_INPUT_TYPES.has(type)) return [];
2282
+ const diagnostic = HtmlDiagnostics.input.unsupportedType(type);
2283
+ return [
2284
+ {
2285
+ valid: false,
2286
+ fixable: false,
2287
+ severity: diagnostic.severity,
2288
+ diagnostic
2289
+ }
2290
+ ];
2291
+ },
2292
+ { readsProps: ["type"] }
2293
+ );
2294
+ var checkedRequiresCheckableTypeRule = inputAttributeRequiresType("checked", [
2295
+ "checkbox",
2296
+ "radio"
2297
+ ]);
2298
+ var multipleRequiresSupportedTypeRule = inputAttributeRequiresType("multiple", [
2299
+ "email",
2300
+ "file"
2301
+ ]);
2302
+ var maxLengthRequiresTextTypeRule = inputAttributeRequiresType(
2303
+ "maxLength",
2304
+ TEXT_INPUT_TYPES
2305
+ );
2306
+ var minLengthRequiresTextTypeRule = inputAttributeRequiresType(
2307
+ "minLength",
2308
+ TEXT_INPUT_TYPES
2309
+ );
2310
+ var patternRequiresTextTypeRule = inputAttributeRequiresType("pattern", TEXT_INPUT_TYPES);
2311
+ var minRequiresNumericTypeRule = inputAttributeRequiresType("min", NUMERIC_INPUT_TYPES);
2312
+ var maxRequiresNumericTypeRule = inputAttributeRequiresType("max", NUMERIC_INPUT_TYPES);
2313
+ var stepRequiresNumericTypeRule = inputAttributeRequiresType("step", NUMERIC_INPUT_TYPES);
2314
+ var acceptRequiresFileTypeRule = inputAttributeRequiresType("accept", ["file"]);
2315
+ var captureRequiresFileTypeRule = inputAttributeRequiresType("capture", ["file"]);
2316
+ var inputAccessibleNameRule = Object.assign(
2317
+ ({ tag, props }) => {
2318
+ if (tag !== "input" || props.type === "hidden") return [];
2319
+ if ("aria-label" in props || "aria-labelledby" in props) return [];
2320
+ const hasPlaceholder = typeof props.placeholder === "string" && props.placeholder.length > 0;
2321
+ const diagnostic = hasPlaceholder ? InputAccessibilityDiagnostics.placeholderIsNotLabel() : InputAccessibilityDiagnostics.missingAccessibleName();
2322
+ return [{ valid: false, fixable: false, severity: diagnostic.severity, diagnostic }];
2323
+ },
2324
+ { readsProps: ["type", "aria-label", "aria-labelledby", "placeholder"] }
2325
+ );
2326
+ var PASSWORD_AUTOCOMPLETE_VALUES = ["current-password", "new-password"];
2327
+ var passwordAutocompleteRule = Object.assign(
2328
+ ({ tag, props }) => {
2329
+ if (tag !== "input" || props.type !== "password") return [];
2330
+ const autoComplete = props.autoComplete;
2331
+ const tokens = typeof autoComplete === "string" ? autoComplete.split(" ") : [];
2332
+ if (PASSWORD_AUTOCOMPLETE_VALUES.some((value) => tokens.includes(value))) return [];
2333
+ const diagnostic = InputAccessibilityDiagnostics.passwordMissingAutocomplete();
2334
+ return [{ valid: false, fixable: false, severity: diagnostic.severity, diagnostic }];
2335
+ },
2336
+ { readsProps: ["type", "autoComplete"] }
2337
+ );
2338
+ var requiredReadOnlyConflictRule = Object.assign(
2339
+ ({ tag, props }) => {
2340
+ if (tag !== "input" || !props.required || !props.readOnly) return [];
2341
+ const diagnostic = InputAccessibilityDiagnostics.requiredReadOnlyConflict();
2342
+ return [{ valid: false, fixable: false, severity: diagnostic.severity, diagnostic }];
2343
+ },
2344
+ { readsProps: ["required", "readOnly"] }
2345
+ );
2346
+ var INPUT_RULES = [
2347
+ supportedInputTypeRule,
2348
+ checkedRequiresCheckableTypeRule,
2349
+ multipleRequiresSupportedTypeRule,
2350
+ maxLengthRequiresTextTypeRule,
2351
+ minLengthRequiresTextTypeRule,
2352
+ patternRequiresTextTypeRule,
2353
+ minRequiresNumericTypeRule,
2354
+ maxRequiresNumericTypeRule,
2355
+ stepRequiresNumericTypeRule,
2356
+ acceptRequiresFileTypeRule,
2357
+ captureRequiresFileTypeRule,
2358
+ inputAccessibleNameRule,
2359
+ passwordAutocompleteRule,
2360
+ requiredReadOnlyConflictRule
2361
+ ];
2362
+
2363
+ // ../core/src/html/role-restrictions.ts
2364
+ var ALLOWED_ROLES = {
2365
+ article: ["application", "document", "feed", "main", "none", "presentation", "region"],
2366
+ aside: ["feed", "none", "presentation", "region", "search"],
2367
+ footer: ["group", "none", "presentation"],
2368
+ header: ["group", "none", "presentation"],
2369
+ main: [],
2370
+ nav: [],
2371
+ a: [
2372
+ "button",
2373
+ "checkbox",
2374
+ "menuitem",
2375
+ "menuitemcheckbox",
2376
+ "menuitemradio",
2377
+ "option",
2378
+ "radio",
2379
+ "switch",
2380
+ "tab",
2381
+ "treeitem"
2382
+ ],
2383
+ button: [
2384
+ "checkbox",
2385
+ "link",
2386
+ "menuitem",
2387
+ "menuitemcheckbox",
2388
+ "menuitemradio",
2389
+ "option",
2390
+ "radio",
2391
+ "switch",
2392
+ "tab"
2393
+ ],
2394
+ select: ["menu"],
2395
+ h1: ["tab", "presentation", "none"],
2396
+ h2: ["tab", "presentation", "none"],
2397
+ h3: ["tab", "presentation", "none"],
2398
+ h4: ["tab", "presentation", "none"],
2399
+ h5: ["tab", "presentation", "none"],
2400
+ h6: ["tab", "presentation", "none"],
2401
+ ul: [
2402
+ "directory",
2403
+ "group",
2404
+ "listbox",
2405
+ "menu",
2406
+ "menubar",
2407
+ "radiogroup",
2408
+ "tablist",
2409
+ "toolbar",
2410
+ "tree"
2411
+ ],
2412
+ ol: [
2413
+ "directory",
2414
+ "group",
2415
+ "listbox",
2416
+ "menu",
2417
+ "menubar",
2418
+ "radiogroup",
2419
+ "tablist",
2420
+ "toolbar",
2421
+ "tree"
2422
+ ],
2423
+ li: [
2424
+ "menuitem",
2425
+ "menuitemcheckbox",
2426
+ "menuitemradio",
2427
+ "option",
2428
+ "none",
2429
+ "presentation",
2430
+ "radio",
2431
+ "separator",
2432
+ "tab",
2433
+ "treeitem"
2434
+ ],
2435
+ table: ["grid", "treegrid"],
2436
+ dialog: ["alertdialog"],
2437
+ fieldset: ["none", "presentation", "radiogroup"]
2438
+ };
2439
+ var IMG_NAMED_ROLES = [
2440
+ "button",
2441
+ "checkbox",
2442
+ "link",
2443
+ "menuitem",
2444
+ "menuitemcheckbox",
2445
+ "menuitemradio",
2446
+ "option",
2447
+ "progressbar",
2448
+ "scrollbar",
2449
+ "separator",
2450
+ "slider",
2451
+ "switch",
2452
+ "tab",
2453
+ "treeitem"
2454
+ ];
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
+ function getAllowedRoles(tag, props) {
2508
+ if (tag === "input") {
2509
+ const type = typeof props.type === "string" ? props.type : "text";
2510
+ return ALLOWED_INPUT_ROLES[type];
2511
+ }
2512
+ if (tag === "img") {
2513
+ return props.alt === "" ? [] : IMG_NAMED_ROLES;
2514
+ }
2515
+ return ALLOWED_ROLES[tag];
2516
+ }
2517
+ var removeRoleFix = {
2518
+ kind: "removeRole",
2519
+ apply: ({ props }) => {
2520
+ if (!("role" in props)) return { applied: false, next: props };
2521
+ const { role: _role, ...rest } = props;
2522
+ return { applied: true, next: rest, previous: props };
2523
+ }
2524
+ };
2525
+ var roleNotPermittedRule = Object.assign(
2526
+ ({ tag, props, implicitRole }) => {
2527
+ const role = props.role;
2528
+ if (typeof role !== "string" || role.length === 0 || role === implicitRole) return [];
2529
+ const allowed = getAllowedRoles(tag, props);
2530
+ if (allowed === void 0 || allowed.includes(role)) return [];
2531
+ const diagnostic = HtmlDiagnostics.roleNotPermitted(tag, role, allowed);
2532
+ return [
2533
+ {
2534
+ valid: false,
2535
+ fixable: true,
2536
+ severity: diagnostic.severity,
2537
+ fix: removeRoleFix,
2538
+ diagnostic
2539
+ }
2540
+ ];
2541
+ },
2542
+ { readsProps: ["role", "type", "alt"] }
2543
+ );
2544
+
2086
2545
  // ../core/src/html/aria-rules.ts
2087
2546
  var LANDMARK_TAG_SET = /* @__PURE__ */ new Set(["article", "aside", "footer", "header", "main", "nav"]);
2088
2547
  var removeLandmarkRoleOverride = {
@@ -2097,13 +2556,14 @@ function landmarkRoleRule({ tag, props, implicitRole }) {
2097
2556
  if (!LANDMARK_TAG_SET.has(tag) || !implicitRole) return [];
2098
2557
  const role = props.role;
2099
2558
  if (!role || role === implicitRole) return [];
2559
+ const diagnostic = HtmlDiagnostics.landmarkRoleOverride(tag, implicitRole, role);
2100
2560
  return [
2101
2561
  {
2102
2562
  valid: false,
2103
2563
  fixable: true,
2104
- severity: "error",
2564
+ severity: diagnostic.severity,
2105
2565
  fix: removeLandmarkRoleOverride,
2106
- diagnostic: HtmlDiagnostics.landmarkRoleOverride(tag, implicitRole, role)
2566
+ diagnostic
2107
2567
  }
2108
2568
  ];
2109
2569
  }
@@ -2123,7 +2583,12 @@ function landmarkNameAdvisory(ctx) {
2123
2583
  if (!ctx.implicitRole || !NAMED_LANDMARK_TAGS.has(ctx.tag)) return [];
2124
2584
  return requireAccessibleName(ctx);
2125
2585
  }
2126
- var HTML_ARIA_RULES = [landmarkRoleRule, landmarkNameAdvisory];
2586
+ var HTML_ARIA_RULES = [
2587
+ landmarkRoleRule,
2588
+ landmarkNameAdvisory,
2589
+ roleNotPermittedRule,
2590
+ ...INPUT_RULES
2591
+ ];
2127
2592
 
2128
2593
  // ../core/src/html/contracts.ts
2129
2594
  import { warnDiagnostics } from "../_shared/diagnostics.js";
@@ -2584,21 +3049,21 @@ function validateRenderProps(diagnostics, options, props, recipeKey) {
2584
3049
  import { throwDiagnostics } from "../_shared/diagnostics.js";
2585
3050
 
2586
3051
  // ../core/src/factory/plugin-diagnostics.ts
2587
- import { DiagnosticCategory as DiagnosticCategory5, DiagnosticCode as DiagnosticCode5 } from "../_shared/diagnostics.js";
3052
+ import { DiagnosticCategory as DiagnosticCategory6, DiagnosticCode as DiagnosticCode6 } from "../_shared/diagnostics.js";
2588
3053
  var PluginDiagnostics = {
2589
3054
  invalidShape(received) {
2590
3055
  const got = received === null ? "null" : typeof received;
2591
3056
  return {
2592
- code: DiagnosticCode5.PluginInvalidShape,
2593
- category: DiagnosticCategory5.Internal,
3057
+ code: DiagnosticCode6.PluginInvalidShape,
3058
+ category: DiagnosticCategory6.Internal,
2594
3059
  message: `[praxis-kit] Plugin factory must return an object with a 'pipeline' function. Got: ${got}.`
2595
3060
  };
2596
3061
  },
2597
3062
  pipelineReturnType(received) {
2598
3063
  const got = received === null ? "null" : Array.isArray(received) ? "array" : typeof received;
2599
3064
  return {
2600
- code: DiagnosticCode5.PluginPipelineReturnType,
2601
- category: DiagnosticCategory5.Internal,
3065
+ code: DiagnosticCode6.PluginPipelineReturnType,
3066
+ category: DiagnosticCategory6.Internal,
2602
3067
  message: `[praxis-kit] Plugin pipeline must return a string. Got: ${got}.`
2603
3068
  };
2604
3069
  }