praxis-kit 7.4.0 → 7.8.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.
@@ -1,18 +1,72 @@
1
- // ../../lib/contract/src/diagnostics/aria.ts
1
+ // ../../lib/primitive/src/guards/foundational/is-defined.ts
2
+ function isDefined(value) {
3
+ return value !== void 0;
4
+ }
5
+
6
+ // ../../lib/primitive/src/utils/type-guards.ts
7
+ function isString(value) {
8
+ return typeof value === "string";
9
+ }
10
+
11
+ // ../../lib/contract/src/diagnostics/anchor-accessibility.ts
2
12
  import { DiagnosticCategory, DiagnosticCode } from "../_shared/diagnostics.js";
13
+ function createDiagnostic(input) {
14
+ return { category: DiagnosticCategory.Accessibility, ...input };
15
+ }
16
+ var AnchorAccessibilityDiagnostics = {
17
+ roleButtonWithHref() {
18
+ return createDiagnostic({
19
+ code: DiagnosticCode.A11yAnchorRoleButtonWithHref,
20
+ severity: "warning",
21
+ message: 'role="button" on an <a> with an href overrides its navigation semantics for assistive technology; use a real <button> element, or remove href, if this element should not navigate.',
22
+ rationale: 'Assistive technology announces role="button" as a button, not a link \u2014 but the element still follows the link when activated by mouse or keyboard, which is confusing and inconsistent with how a button is expected to behave.',
23
+ suggestions: [
24
+ {
25
+ title: "Use a real <button>",
26
+ description: "If this element should not navigate, render a <button> instead of an <a>."
27
+ },
28
+ {
29
+ title: 'Remove role="button"',
30
+ description: "If this element should navigate, keep the default link role."
31
+ }
32
+ ]
33
+ });
34
+ },
35
+ ariaDisabledInert() {
36
+ return createDiagnostic({
37
+ code: DiagnosticCode.A11yAnchorAriaDisabledInert,
38
+ severity: "warning",
39
+ message: "aria-disabled does not prevent an <a> from being focused, clicked, or navigated via keyboard; remove href, or prevent navigation yourself, if this link should be inert.",
40
+ rationale: "Unlike a native form control, an <a> has no disabled state the browser enforces \u2014 aria-disabled only changes what assistive technology announces, not what actually happens when the link is activated by mouse or keyboard.",
41
+ suggestions: [
42
+ {
43
+ title: "Remove href",
44
+ description: "If this element is temporarily unavailable, remove href until it can be activated."
45
+ },
46
+ {
47
+ title: "Prevent navigation when disabled",
48
+ description: "Prevent the click and keyboard activation when the link is disabled so it behaves consistently for all users."
49
+ }
50
+ ]
51
+ });
52
+ }
53
+ };
54
+
55
+ // ../../lib/contract/src/diagnostics/aria.ts
56
+ import { DiagnosticCategory as DiagnosticCategory2, DiagnosticCode as DiagnosticCode2 } from "../_shared/diagnostics.js";
3
57
  var AriaDiagnostics = {
4
58
  /** Generic bridge for violations produced by external AriaRule functions. */
5
59
  fromViolation(v) {
6
60
  return {
7
- code: DiagnosticCode.AriaViolation,
8
- category: DiagnosticCategory.ARIA,
61
+ code: DiagnosticCode2.AriaViolation,
62
+ category: DiagnosticCategory2.ARIA,
9
63
  message: v.message
10
64
  };
11
65
  },
12
66
  attributeInvalid(key, role) {
13
67
  return {
14
- code: DiagnosticCode.AriaAttributeInvalid,
15
- category: DiagnosticCategory.ARIA,
68
+ code: DiagnosticCode2.AriaAttributeInvalid,
69
+ category: DiagnosticCategory2.ARIA,
16
70
  message: `"${key}" is not valid on role="${role}". It will be removed.`,
17
71
  rationale: "Invalid ARIA attributes are ignored by assistive technology and may trigger accessibility-tree warnings in browser devtools.",
18
72
  suggestions: [
@@ -25,8 +79,8 @@ var AriaDiagnostics = {
25
79
  },
26
80
  missingLiveRegion(role, impliedLive) {
27
81
  return {
28
- code: DiagnosticCode.AriaMissingLiveRegion,
29
- category: DiagnosticCategory.ARIA,
82
+ code: DiagnosticCode2.AriaMissingLiveRegion,
83
+ category: DiagnosticCategory2.ARIA,
30
84
  message: `role="${role}" implies aria-live="${impliedLive}" but it is missing. It has been injected.`,
31
85
  rationale: "Live-region roles announce dynamic content changes to screen readers. Without aria-live the politeness level is unspecified and announcements may be silent.",
32
86
  suggestions: [
@@ -40,8 +94,8 @@ var AriaDiagnostics = {
40
94
  },
41
95
  missingAtomic(role) {
42
96
  return {
43
- code: DiagnosticCode.AriaMissingAtomic,
44
- category: DiagnosticCategory.ARIA,
97
+ code: DiagnosticCode2.AriaMissingAtomic,
98
+ category: DiagnosticCategory2.ARIA,
45
99
  message: `role="${role}" is a live region. Consider setting aria-atomic="true" if the full region should be announced as a unit, or aria-atomic="false" if only changed nodes should be read.`,
46
100
  rationale: "aria-atomic controls whether assistive technology announces the entire live region or only the changed nodes. Omitting it leaves the behaviour browser-defined."
47
101
  };
@@ -49,8 +103,8 @@ var AriaDiagnostics = {
49
103
  relevantInvalidTokens(invalid) {
50
104
  const quoted = invalid.map((t) => `"${t}"`).join(", ");
51
105
  return {
52
- code: DiagnosticCode.AriaRelevantInvalidToken,
53
- category: DiagnosticCategory.ARIA,
106
+ code: DiagnosticCode2.AriaRelevantInvalidToken,
107
+ category: DiagnosticCategory2.ARIA,
54
108
  message: `aria-relevant contains invalid token(s): ${quoted}. Valid tokens are: additions, removals, text, all.`,
55
109
  rationale: "aria-relevant accepts a space-separated list of change types. Unrecognised tokens are silently ignored by assistive technology, making the attribute ineffective.",
56
110
  suggestions: [
@@ -63,8 +117,8 @@ var AriaDiagnostics = {
63
117
  },
64
118
  relevantSuperseded() {
65
119
  return {
66
- code: DiagnosticCode.AriaRelevantSuperseded,
67
- category: DiagnosticCategory.ARIA,
120
+ code: DiagnosticCode2.AriaRelevantSuperseded,
121
+ category: DiagnosticCategory2.ARIA,
68
122
  message: 'aria-relevant includes "all" alongside other tokens. "all" supersedes additions, removals, and text \u2014 use aria-relevant="all" alone.',
69
123
  rationale: '"all" is equivalent to "additions removals text". Combining it with other tokens is redundant and may confuse readers of the markup.',
70
124
  suggestions: [
@@ -77,8 +131,8 @@ var AriaDiagnostics = {
77
131
  },
78
132
  missingAccessibleName(tag) {
79
133
  return {
80
- code: DiagnosticCode.AriaMissingAccessibleName,
81
- category: DiagnosticCategory.ARIA,
134
+ code: DiagnosticCode2.AriaMissingAccessibleName,
135
+ category: DiagnosticCategory2.ARIA,
82
136
  message: `<${tag}> has no accessible name. Add aria-label or aria-labelledby.`,
83
137
  rationale: "Elements with a landmark or interactive role must have an accessible name so that assistive technology can identify them when presenting the page outline.",
84
138
  suggestions: [
@@ -95,8 +149,8 @@ var AriaDiagnostics = {
95
149
  },
96
150
  attributeOnPresentational(attr, tag) {
97
151
  return {
98
- code: DiagnosticCode.AriaAttributeOnPresentational,
99
- category: DiagnosticCategory.ARIA,
152
+ code: DiagnosticCode2.AriaAttributeOnPresentational,
153
+ category: DiagnosticCategory2.ARIA,
100
154
  message: `"${attr}" is not allowed on a presentational <${tag}>. Presentational elements are invisible to assistive technology.`,
101
155
  rationale: 'role="none" and role="presentation" (including <img alt="">) remove an element from the accessibility tree. ARIA attributes on such elements are ignored by assistive technology.',
102
156
  suggestions: [
@@ -109,8 +163,8 @@ var AriaDiagnostics = {
109
163
  },
110
164
  ariaHiddenOnFocusable(tag) {
111
165
  return {
112
- code: DiagnosticCode.AriaHiddenOnFocusable,
113
- category: DiagnosticCategory.ARIA,
166
+ code: DiagnosticCode2.AriaHiddenOnFocusable,
167
+ category: DiagnosticCategory2.ARIA,
114
168
  message: `aria-hidden="true" must not be used on focusable <${tag}> elements. Screen reader users who navigate by keyboard will encounter the element but receive no information about it.`,
115
169
  rationale: 'aria-hidden removes an element from the accessibility tree while leaving it keyboard-reachable. This creates a "ghost" \u2014 a focusable element assistive technology cannot describe.',
116
170
  suggestions: [
@@ -128,8 +182,8 @@ var AriaDiagnostics = {
128
182
  invalidAttributeValue(attr, value, expected) {
129
183
  const got = value === null ? "null" : value === void 0 ? "undefined" : typeof value === "string" ? `"${value}"` : String(value);
130
184
  return {
131
- code: DiagnosticCode.AriaInvalidAttributeValue,
132
- category: DiagnosticCategory.ARIA,
185
+ code: DiagnosticCode2.AriaInvalidAttributeValue,
186
+ category: DiagnosticCategory2.ARIA,
133
187
  message: `"${attr}" has an invalid value (${got}). Expected: ${expected}.`,
134
188
  rationale: "ARIA attributes with invalid values are silently ignored by assistive technology, making the markup semantically inert.",
135
189
  suggestions: [
@@ -142,8 +196,8 @@ var AriaDiagnostics = {
142
196
  },
143
197
  redundantAriaLevel(tag, level) {
144
198
  return {
145
- code: DiagnosticCode.AriaRedundantLevelAttribute,
146
- category: DiagnosticCategory.ARIA,
199
+ code: DiagnosticCode2.AriaRedundantLevelAttribute,
200
+ category: DiagnosticCategory2.ARIA,
147
201
  message: `aria-level="${level}" is redundant on <${tag}>: the element already has an implicit heading level of ${level}. Remove the attribute.`,
148
202
  rationale: 'Restating the implicit aria-level adds noise without semantic value. Use aria-level only to override the native heading level (e.g. aria-level="3" on <h2>).',
149
203
  suggestions: [
@@ -156,8 +210,8 @@ var AriaDiagnostics = {
156
210
  },
157
211
  requiredProperty(attr, role) {
158
212
  return {
159
- code: DiagnosticCode.AriaRequiredProperty,
160
- category: DiagnosticCategory.ARIA,
213
+ code: DiagnosticCode2.AriaRequiredProperty,
214
+ category: DiagnosticCategory2.ARIA,
161
215
  message: `"${attr}" is required for role="${role}" but is missing.`,
162
216
  rationale: `WAI-ARIA 1.2 specifies required states and properties for certain roles. Without "${attr}", assistive technology cannot correctly communicate the element's state to users.`,
163
217
  suggestions: [
@@ -170,8 +224,8 @@ var AriaDiagnostics = {
170
224
  },
171
225
  invalidRole(role, tag) {
172
226
  return {
173
- code: DiagnosticCode.AriaInvalidRole,
174
- category: DiagnosticCategory.ARIA,
227
+ code: DiagnosticCode2.AriaInvalidRole,
228
+ category: DiagnosticCategory2.ARIA,
175
229
  message: `Invalid role "${role ?? ""}" on <${tag}>.`,
176
230
  rationale: "An unrecognised or misapplied ARIA role is ignored by assistive technology and may degrade the accessibility of the element."
177
231
  };
@@ -179,68 +233,68 @@ var AriaDiagnostics = {
179
233
  };
180
234
 
181
235
  // ../../lib/contract/src/diagnostics/html.ts
182
- import { DiagnosticCategory as DiagnosticCategory2, DiagnosticCode as DiagnosticCode2 } from "../_shared/diagnostics.js";
236
+ import { DiagnosticCategory as DiagnosticCategory3, DiagnosticCode as DiagnosticCode3 } from "../_shared/diagnostics.js";
183
237
  var ATTRIBUTE_IGNORED_CODES = {
184
- checked: DiagnosticCode2.HtmlInputCheckedIgnoredForType,
185
- multiple: DiagnosticCode2.HtmlInputMultipleIgnoredForType,
186
- maxLength: DiagnosticCode2.HtmlInputMaxLengthIgnoredForType,
187
- minLength: DiagnosticCode2.HtmlInputMinLengthIgnoredForType,
188
- pattern: DiagnosticCode2.HtmlInputPatternIgnoredForType,
189
- min: DiagnosticCode2.HtmlInputMinIgnoredForType,
190
- max: DiagnosticCode2.HtmlInputMaxIgnoredForType,
191
- step: DiagnosticCode2.HtmlInputStepIgnoredForType,
192
- accept: DiagnosticCode2.HtmlInputAcceptIgnoredForType,
193
- capture: DiagnosticCode2.HtmlInputCaptureIgnoredForType,
194
- size: DiagnosticCode2.HtmlInputSizeIgnoredForType,
195
- alt: DiagnosticCode2.HtmlInputAltIgnoredForType,
196
- height: DiagnosticCode2.HtmlInputHeightIgnoredForType,
197
- width: DiagnosticCode2.HtmlInputWidthIgnoredForType
238
+ checked: DiagnosticCode3.HtmlInputCheckedIgnoredForType,
239
+ multiple: DiagnosticCode3.HtmlInputMultipleIgnoredForType,
240
+ maxLength: DiagnosticCode3.HtmlInputMaxLengthIgnoredForType,
241
+ minLength: DiagnosticCode3.HtmlInputMinLengthIgnoredForType,
242
+ pattern: DiagnosticCode3.HtmlInputPatternIgnoredForType,
243
+ min: DiagnosticCode3.HtmlInputMinIgnoredForType,
244
+ max: DiagnosticCode3.HtmlInputMaxIgnoredForType,
245
+ step: DiagnosticCode3.HtmlInputStepIgnoredForType,
246
+ accept: DiagnosticCode3.HtmlInputAcceptIgnoredForType,
247
+ capture: DiagnosticCode3.HtmlInputCaptureIgnoredForType,
248
+ size: DiagnosticCode3.HtmlInputSizeIgnoredForType,
249
+ alt: DiagnosticCode3.HtmlInputAltIgnoredForType,
250
+ height: DiagnosticCode3.HtmlInputHeightIgnoredForType,
251
+ width: DiagnosticCode3.HtmlInputWidthIgnoredForType
198
252
  };
199
253
  var HtmlDiagnostics = {
200
254
  emptyRole(tag) {
201
255
  return {
202
- code: DiagnosticCode2.HtmlEmptyRole,
203
- category: DiagnosticCategory2.HTML,
256
+ code: DiagnosticCode3.HtmlEmptyRole,
257
+ category: DiagnosticCategory3.HTML,
204
258
  severity: "warning",
205
259
  message: `<${tag}> has an explicit empty role="". Omit the attribute instead.`
206
260
  };
207
261
  },
208
262
  implicitRoleRedundant(tag, implicitRole) {
209
263
  return {
210
- code: DiagnosticCode2.HtmlImplicitRoleRedundant,
211
- category: DiagnosticCategory2.HTML,
264
+ code: DiagnosticCode3.HtmlImplicitRoleRedundant,
265
+ category: DiagnosticCategory3.HTML,
212
266
  severity: "warning",
213
267
  message: `<${tag}> already has implicit role="${implicitRole}". Avoid redundant role assignment.`
214
268
  };
215
269
  },
216
270
  implicitRoleOverride(tag, implicitRole, role) {
217
271
  return {
218
- code: DiagnosticCode2.HtmlImplicitRoleOverride,
219
- category: DiagnosticCategory2.HTML,
272
+ code: DiagnosticCode3.HtmlImplicitRoleOverride,
273
+ category: DiagnosticCategory3.HTML,
220
274
  severity: "error",
221
275
  message: `<${tag}> should not override its implicit role="${implicitRole}" with role="${role}".`
222
276
  };
223
277
  },
224
278
  standaloneRegionOverride(tag, implicitRole) {
225
279
  return {
226
- code: DiagnosticCode2.HtmlStandaloneRegionOverride,
227
- category: DiagnosticCategory2.HTML,
280
+ code: DiagnosticCode3.HtmlStandaloneRegionOverride,
281
+ category: DiagnosticCategory3.HTML,
228
282
  severity: "error",
229
283
  message: `<${tag}> is a self-contained element with implicit role="${implicitRole}". Assigning role="region" has been removed.`
230
284
  };
231
285
  },
232
286
  landmarkRoleOverride(tag, implicitRole, role) {
233
287
  return {
234
- code: DiagnosticCode2.HtmlLandmarkRoleOverride,
235
- category: DiagnosticCategory2.HTML,
288
+ code: DiagnosticCode3.HtmlLandmarkRoleOverride,
289
+ category: DiagnosticCategory3.HTML,
236
290
  severity: "error",
237
291
  message: `<${tag}> has a fixed landmark role="${implicitRole}". role="${role}" overrides it and confuses assistive technology. The override has been removed.`
238
292
  };
239
293
  },
240
294
  invalidChild(child, parent, allowed) {
241
295
  return {
242
- code: DiagnosticCode2.HtmlInvalidChild,
243
- category: DiagnosticCategory2.HTML,
296
+ code: DiagnosticCode3.HtmlInvalidChild,
297
+ category: DiagnosticCategory3.HTML,
244
298
  severity: "error",
245
299
  message: `<${child}> is not a valid direct child of <${parent}>. Allowed: ${allowed}.`
246
300
  };
@@ -248,8 +302,8 @@ var HtmlDiagnostics = {
248
302
  roleNotPermitted(tag, role, allowedRoles) {
249
303
  const allowed = allowedRoles.length > 0 ? allowedRoles.map((r) => `"${r}"`).join(", ") : "none \u2014 no explicit role is permitted on this element";
250
304
  return {
251
- code: DiagnosticCode2.HtmlRoleNotPermitted,
252
- category: DiagnosticCategory2.HTML,
305
+ code: DiagnosticCode3.HtmlRoleNotPermitted,
306
+ category: DiagnosticCategory3.HTML,
253
307
  severity: "error",
254
308
  message: `role="${role}" is not permitted on <${tag}>. Allowed alternate role(s): ${allowed}.`,
255
309
  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.'
@@ -260,8 +314,8 @@ var HtmlDiagnostics = {
260
314
  input: {
261
315
  unsupportedType(type) {
262
316
  return {
263
- code: DiagnosticCode2.HtmlInputUnsupportedType,
264
- category: DiagnosticCategory2.HTML,
317
+ code: DiagnosticCode3.HtmlInputUnsupportedType,
318
+ category: DiagnosticCategory3.HTML,
265
319
  severity: "warning",
266
320
  message: `type="${type}" is not a value defined by the HTML specification. Browsers silently fall back to type="text".`,
267
321
  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).',
@@ -281,7 +335,7 @@ var HtmlDiagnostics = {
281
335
  if (!code) throw new Error(`No DiagnosticCode registered for input attribute "${attribute}"`);
282
336
  return {
283
337
  code,
284
- category: DiagnosticCategory2.HTML,
338
+ category: DiagnosticCategory3.HTML,
285
339
  severity: "warning",
286
340
  message: `"${attribute}" is ignored on <input type="${type}">.`,
287
341
  rationale: `"${attribute}" only has an effect when type is one of: ${allowed}. Browsers silently ignore it on other input types.`,
@@ -293,18 +347,30 @@ var HtmlDiagnostics = {
293
347
  ]
294
348
  };
295
349
  }
350
+ },
351
+ // Reserved for <a>-specific facts (HTML3201–3299, see codes.ts).
352
+ anchor: {
353
+ dangerousHref(href) {
354
+ return {
355
+ code: DiagnosticCode3.HtmlAnchorDangerousHref,
356
+ category: DiagnosticCategory3.HTML,
357
+ severity: "warning",
358
+ message: `href="${href}" uses a scheme that executes attacker-controlled content when navigated to. It has been removed.`,
359
+ rationale: '"javascript:", "data:", and "vbscript:" URLs are a common XSS vector when href comes from untrusted input \u2014 none of the three are needed for legitimate navigation.'
360
+ };
361
+ }
296
362
  }
297
363
  };
298
364
 
299
365
  // ../../lib/contract/src/diagnostics/input-accessibility.ts
300
- import { DiagnosticCategory as DiagnosticCategory3, DiagnosticCode as DiagnosticCode3 } from "../_shared/diagnostics.js";
366
+ import { DiagnosticCategory as DiagnosticCategory4, DiagnosticCode as DiagnosticCode4 } from "../_shared/diagnostics.js";
301
367
  function accessibilityFact(input) {
302
- return { category: DiagnosticCategory3.Accessibility, ...input };
368
+ return { category: DiagnosticCategory4.Accessibility, ...input };
303
369
  }
304
370
  var InputAccessibilityDiagnostics = {
305
371
  missingAccessibleName() {
306
372
  return accessibilityFact({
307
- code: DiagnosticCode3.A11yInputMissingAccessibleName,
373
+ code: DiagnosticCode4.A11yInputMissingAccessibleName,
308
374
  severity: "warning",
309
375
  message: "This input has no accessible name. Add an associated <label>, aria-label, or aria-labelledby.",
310
376
  rationale: "Assistive technology announces a form field by its accessible name; without one, users of screen readers cannot tell what the field is for.",
@@ -319,7 +385,7 @@ var InputAccessibilityDiagnostics = {
319
385
  },
320
386
  placeholderIsNotLabel() {
321
387
  return accessibilityFact({
322
- code: DiagnosticCode3.A11yInputPlaceholderNotLabel,
388
+ code: DiagnosticCode4.A11yInputPlaceholderNotLabel,
323
389
  severity: "warning",
324
390
  message: "Placeholder text does not provide an accessible name. Add an associated <label>, aria-label, or aria-labelledby.",
325
391
  rationale: "Placeholder text disappears as users interact with the field and is not treated as the control's accessible name by many assistive technologies.",
@@ -334,7 +400,7 @@ var InputAccessibilityDiagnostics = {
334
400
  },
335
401
  passwordMissingAutocomplete() {
336
402
  return accessibilityFact({
337
- code: DiagnosticCode3.A11yInputPasswordAutocomplete,
403
+ code: DiagnosticCode4.A11yInputPasswordAutocomplete,
338
404
  severity: "warning",
339
405
  message: "Password inputs should specify an autoComplete value.",
340
406
  rationale: "Without an explicit autocomplete hint, password managers and browsers cannot reliably tell a sign-in field apart from a password-creation field.",
@@ -352,7 +418,7 @@ var InputAccessibilityDiagnostics = {
352
418
  },
353
419
  requiredReadOnlyConflict() {
354
420
  return accessibilityFact({
355
- code: DiagnosticCode3.A11yInputRequiredReadOnlyConflict,
421
+ code: DiagnosticCode4.A11yInputRequiredReadOnlyConflict,
356
422
  severity: "warning",
357
423
  message: "The required and readOnly attributes are both present. A read-only field cannot satisfy required validation through user interaction.",
358
424
  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."
@@ -361,6 +427,27 @@ var InputAccessibilityDiagnostics = {
361
427
  };
362
428
 
363
429
  // ../../lib/contract/src/aria/factories.ts
430
+ function invalidWithoutFix(input) {
431
+ return {
432
+ valid: false,
433
+ fixable: false,
434
+ severity: input.severity,
435
+ ...isDefined(input.attribute) && { attribute: input.attribute },
436
+ ...isDefined(input.message) && { message: input.message },
437
+ ...isDefined(input.diagnostic) && { diagnostic: input.diagnostic }
438
+ };
439
+ }
440
+ function invalidWithFix(input) {
441
+ return {
442
+ valid: false,
443
+ fixable: true,
444
+ severity: input.severity,
445
+ ...isDefined(input.attribute) && { attribute: input.attribute },
446
+ ...isDefined(input.message) && { message: input.message },
447
+ ...isDefined(input.diagnostic) && { diagnostic: input.diagnostic },
448
+ fix: input.fix
449
+ };
450
+ }
364
451
  function removeProp(props, key) {
365
452
  const next = { ...props };
366
453
  delete next[key];
@@ -376,6 +463,34 @@ function removeAttributeFix(attribute) {
376
463
  }
377
464
  });
378
465
  }
466
+ function defineRuleMetadata(rule, metadata) {
467
+ const descriptors = {};
468
+ for (const key of Object.keys(metadata)) {
469
+ descriptors[key] = { value: metadata[key], enumerable: false };
470
+ }
471
+ Object.defineProperties(rule, descriptors);
472
+ return rule;
473
+ }
474
+ function createRemoveAttributeRule(attribute, options) {
475
+ const { when, severity = "warning", message, diagnostic, readsProps, tags } = options;
476
+ const fix = removeAttributeFix(attribute);
477
+ const rule = (context) => {
478
+ if (!when(context)) return [];
479
+ return [
480
+ invalidWithFix({
481
+ severity,
482
+ attribute,
483
+ ...isDefined(message) && { message },
484
+ ...isDefined(diagnostic) && { diagnostic: diagnostic(context) },
485
+ fix
486
+ })
487
+ ];
488
+ };
489
+ return defineRuleMetadata(rule, {
490
+ ...isDefined(readsProps) && { readsProps },
491
+ ...isDefined(tags) && { tags }
492
+ });
493
+ }
379
494
 
380
495
  // ../core/src/html/contracts/categories.ts
381
496
  var LANDMARK_TAGS = [
@@ -387,6 +502,52 @@ var LANDMARK_TAGS = [
387
502
  "nav"
388
503
  ];
389
504
 
505
+ // ../core/src/html/anchor-rules.ts
506
+ var DANGEROUS_URL_SCHEMES = ["javascript:", "data:", "vbscript:"];
507
+ function normalizeForSchemeCheck(href) {
508
+ return href.replace(/[\x00-\x20]/g, "").toLowerCase();
509
+ }
510
+ function isDangerousUrl(href) {
511
+ if (!isString(href)) return false;
512
+ const normalized = normalizeForSchemeCheck(href);
513
+ return DANGEROUS_URL_SCHEMES.some((scheme) => normalized.startsWith(scheme));
514
+ }
515
+ var dangerousHrefRule = createRemoveAttributeRule("href", {
516
+ when: ({ props }) => isDangerousUrl(props.href),
517
+ severity: "warning",
518
+ diagnostic: ({ props }) => HtmlDiagnostics.anchor.dangerousHref(props.href),
519
+ readsProps: ["href"],
520
+ tags: ["a"]
521
+ });
522
+ var roleButtonWithHrefRule = Object.assign(
523
+ ({ props }) => {
524
+ if (props.role !== "button" || !isString(props.href) || props.href.length === 0) {
525
+ return [];
526
+ }
527
+ const diagnostic = AnchorAccessibilityDiagnostics.roleButtonWithHref();
528
+ return [invalidWithoutFix({ severity: diagnostic.severity, attribute: "role", diagnostic })];
529
+ },
530
+ { readsProps: ["role", "href"], tags: ["a"] }
531
+ );
532
+ var ariaDisabledInertRule = Object.assign(
533
+ ({ props }) => {
534
+ const ariaDisabled = props["aria-disabled"];
535
+ const isDisabled = ariaDisabled === true || ariaDisabled === "true";
536
+ const hasHref = isString(props.href) && props.href.length > 0;
537
+ if (!isDisabled || !hasHref) return [];
538
+ const diagnostic = AnchorAccessibilityDiagnostics.ariaDisabledInert();
539
+ return [
540
+ invalidWithoutFix({ severity: diagnostic.severity, attribute: "aria-disabled", diagnostic })
541
+ ];
542
+ },
543
+ { readsProps: ["aria-disabled", "href"], tags: ["a"] }
544
+ );
545
+ var ANCHOR_RULES = [
546
+ dangerousHrefRule,
547
+ roleButtonWithHrefRule,
548
+ ariaDisabledInertRule
549
+ ];
550
+
390
551
  // ../core/src/html/spec/vocabulary/input.ts
391
552
  var TEXT_INPUT_TYPES = ["text", "search", "url", "tel", "email", "password"];
392
553
  var NUMERIC_INPUT_TYPES = [
@@ -466,12 +627,12 @@ function createInputAttributeTypeRule({
466
627
  // ../core/src/html/spec/validators/mutually-exclusive-validator.ts
467
628
  function createMutuallyExclusiveRule({
468
629
  props: conflictingProps,
469
- diagnostic: createDiagnostic
630
+ diagnostic: createDiagnostic2
470
631
  }) {
471
632
  const [first, second] = conflictingProps;
472
633
  const rule = ({ tag, props }) => {
473
634
  if (tag !== "input" || !props[first] || !props[second]) return [];
474
- const diagnostic = createDiagnostic();
635
+ const diagnostic = createDiagnostic2();
475
636
  return [{ valid: false, fixable: false, severity: diagnostic.severity, diagnostic }];
476
637
  };
477
638
  return Object.assign(rule, { readsProps: conflictingProps, tags: ["input"] });
@@ -851,15 +1012,19 @@ var HTML_ARIA_RULES = [
851
1012
  landmarkRoleRule,
852
1013
  landmarkAccessibleNameRule,
853
1014
  roleNotPermittedRule,
854
- ...INPUT_RULES
1015
+ ...INPUT_RULES,
1016
+ ...ANCHOR_RULES
855
1017
  ];
856
1018
  export {
1019
+ ANCHOR_RULES,
857
1020
  HTML_ARIA_RULES,
858
1021
  INPUT_RULES,
859
1022
  acceptRequiresFileTypeRule,
860
1023
  altRequiresImageTypeRule,
1024
+ ariaDisabledInertRule,
861
1025
  captureRequiresFileTypeRule,
862
1026
  checkedRequiresCheckableTypeRule,
1027
+ dangerousHrefRule,
863
1028
  heightRequiresImageTypeRule,
864
1029
  inputAccessibleNameRule,
865
1030
  landmarkAccessibleNameRule,
@@ -873,6 +1038,7 @@ export {
873
1038
  patternRequiresTextTypeRule,
874
1039
  requireAccessibleName,
875
1040
  requiredReadOnlyConflictRule,
1041
+ roleButtonWithHrefRule,
876
1042
  roleNotPermittedRule,
877
1043
  sizeRequiresTextTypeRule,
878
1044
  stepRequiresNumericTypeRule,
@@ -72,6 +72,19 @@ type MergeRecords<A extends object, B extends object> = IsEmptyRecord<A> extends
72
72
  type IntrinsicTag = keyof HTMLElementTagNameMap;
73
73
 
74
74
  type ElementType = IntrinsicTag | (string & {});
75
+ /**
76
+ * Resolves a component's default tag to its real DOM interface — `HTMLDialogElement` for
77
+ * `'dialog'`, `HTMLDetailsElement` for `'details'`, and so on — falling back to `HTMLElement`
78
+ * for custom-element tags or anything not in `HTMLElementTagNameMap`. Used to type
79
+ * `FactoryOptions.onElement`'s `element` param so component authors get direct, correctly-typed
80
+ * access to tag-specific native members (`dialogEl.showModal()`) without an unsafe cast.
81
+ *
82
+ * The fallback is `HTMLElement`, not the more generic `Element` — every tag reachable through
83
+ * `IntrinsicTag` extends it, and so does every custom element per spec, so members `HTMLElement`
84
+ * itself declares (`showPopover()`/`hidePopover()`/`togglePopover()`, the `popover` attribute)
85
+ * stay directly accessible even for tags with no dedicated entry in `HTMLElementTagNameMap`.
86
+ */
87
+ type ElementForTag<TDefault extends ElementType> = TDefault extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[TDefault] : HTMLElement;
75
88
 
76
89
  declare const KNOWN_ARIA_ROLES: readonly ["alert", "alertdialog", "application", "article", "banner", "blockquote", "button", "caption", "cell", "checkbox", "code", "columnheader", "combobox", "complementary", "contentinfo", "definition", "deletion", "dialog", "document", "emphasis", "feed", "figure", "form", "generic", "grid", "gridcell", "group", "heading", "img", "insertion", "link", "list", "listbox", "listitem", "log", "main", "marquee", "math", "menu", "menubar", "menuitem", "menuitemcheckbox", "menuitemradio", "meter", "navigation", "none", "note", "option", "paragraph", "presentation", "progressbar", "radio", "radiogroup", "region", "row", "rowgroup", "rowheader", "scrollbar", "search", "searchbox", "separator", "slider", "spinbutton", "status", "strong", "subscript", "superscript", "switch", "tab", "table", "tablist", "tabpanel", "term", "textbox", "time", "timer", "toolbar", "tooltip", "tree", "treegrid", "treeitem"];
77
90
  type KnownAriaRole = (typeof KNOWN_ARIA_ROLES)[number];
@@ -171,7 +184,7 @@ type DefaultVariants<V extends VariantMap> = {
171
184
  * Presets are named bundles of variant props that callers activate by key,
172
185
  * avoiding the need to repeat variant combinations at each call site.
173
186
  */
174
- type RecipeMap<V extends VariantMap = VariantMap> = Readonly<Record<string, VariantSelection<V>>>;
187
+ type RecipeMap<V extends VariantMap = VariantMap> = Readonly<StringMap<VariantSelection<V>>>;
175
188
 
176
189
  type RecipeTarget<TVariants extends VariantMap = VariantMap> = VariantSelection<TVariants>;
177
190
 
@@ -205,7 +218,7 @@ interface BaseClassOptions {
205
218
  type ClassPipelineFn = (tag: unknown, props: AnyRecord, className?: ClassName, recipe?: string) => string | undefined;
206
219
 
207
220
  interface RecipeOptions<TVariants extends VariantMap = VariantMap> {
208
- recipeMap?: Record<string, RecipeTarget<TVariants>>;
221
+ recipeMap?: StringMap<RecipeTarget<TVariants>>;
209
222
  }
210
223
 
211
224
  interface TagMapOptions {
@@ -374,7 +387,7 @@ type StylingOptions<V extends Readonly<VariantMap> = Readonly<EmptyRecord>, TPre
374
387
  * combination, skipping runtime class computation entirely when a match is found. Normally
375
388
  * generated by a build-time class-extraction plugin rather than hand-authored.
376
389
  */
377
- readonly precomputedClasses?: Readonly<Record<string, string>>;
390
+ readonly precomputedClasses?: Readonly<StringMap<string>>;
378
391
  };
379
392
 
380
393
  type NormalizeFn<Props extends AnyRecord = AnyRecord> = {
@@ -420,13 +433,23 @@ type FactoryOptions<TDefault extends ElementType = ElementType, Props extends An
420
433
  * no prop-based equivalent), not for anything expressible as a plain
421
434
  * prop.
422
435
  *
436
+ * `element` is typed to the real DOM interface of every tag the rendered
437
+ * element could actually be — `TDefault` plus whatever `enforcement.allowed`
438
+ * permits via `as` (`HTMLDialogElement` for `tag: 'dialog'`,
439
+ * `HTMLDetailsElement` for `tag: 'details'`, and so on) — no cast needed to
440
+ * reach tag-specific members. A component that leaves `allowed`
441
+ * unconstrained (any tag reachable via `as`) falls back to `HTMLElement`,
442
+ * which still covers members every element shares (`showPopover()` and
443
+ * friends); restrict `enforcement.allowed` to the tags `onElement`
444
+ * actually knows how to handle to get real narrowing.
445
+ *
423
446
  * `getProps` returns the instance's *current* resolved props at call
424
447
  * time — read it from inside a listener registered once at mount, rather
425
448
  * than re-subscribing on every prop change.
426
449
  *
427
450
  * Return a cleanup function to run when the instance unmounts.
428
451
  */
429
- readonly onElement?: (element: Element, getProps: () => Readonly<Props>) => void | (() => void);
452
+ readonly onElement?: (element: ElementForTag<TDefault | TAllowed>, getProps: () => Readonly<Props>) => void | (() => void);
430
453
  };
431
454
 
432
455
  /**