praxis-kit 6.0.0 → 6.1.1

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.
@@ -0,0 +1,62 @@
1
+ // ../../lib/primitive/src/utils/type-guards.ts
2
+ function isObject(value, excludeArrays = false) {
3
+ if (value === null || typeof value !== "object") return false;
4
+ return excludeArrays ? !Array.isArray(value) : true;
5
+ }
6
+ function isString(value) {
7
+ return typeof value === "string";
8
+ }
9
+ function isNumber(value) {
10
+ return typeof value === "number";
11
+ }
12
+
13
+ // ../../lib/primitive/src/guards/children/component-id.ts
14
+ var COMPONENT_DEFAULT_TAG = /* @__PURE__ */ Symbol.for("praxis.component-default-tag");
15
+
16
+ // ../../lib/primitive/src/guards/children/is-tag.ts
17
+ function getAsProp(child) {
18
+ if (!isObject(child) || !("props" in child)) return void 0;
19
+ const props = child.props;
20
+ if (!isObject(props)) return void 0;
21
+ const as = props.as;
22
+ return isString(as) && as !== "" ? as : void 0;
23
+ }
24
+ function getTag(child) {
25
+ if (!isObject(child) || !("type" in child)) return void 0;
26
+ const t = child.type;
27
+ if (isString(t)) return t;
28
+ if (typeof t === "function" || isObject(t)) {
29
+ const defaultTag = t[COMPONENT_DEFAULT_TAG];
30
+ if (!isString(defaultTag)) return void 0;
31
+ return getAsProp(child) ?? defaultTag;
32
+ }
33
+ return void 0;
34
+ }
35
+ function isTag(...args) {
36
+ if (isString(args[0])) {
37
+ const set2 = new Set(args);
38
+ return (child2) => {
39
+ const tag2 = getTag(child2);
40
+ return tag2 !== void 0 && set2.has(tag2);
41
+ };
42
+ }
43
+ const [child, ...tags] = args;
44
+ const set = new Set(tags);
45
+ const tag = getTag(child);
46
+ return tag !== void 0 && set.has(tag);
47
+ }
48
+ function isFlowContent(...blockedTags) {
49
+ const set = new Set(blockedTags);
50
+ return (child) => {
51
+ if (isString(child) || isNumber(child)) return true;
52
+ const tag = getTag(child);
53
+ return tag === void 0 || !set.has(tag);
54
+ };
55
+ }
56
+ export {
57
+ getTag,
58
+ isFlowContent,
59
+ isObject,
60
+ isString,
61
+ isTag
62
+ };
@@ -139,7 +139,7 @@ interface BaseClassOptions {
139
139
  baseClassName?: ClassName;
140
140
  }
141
141
 
142
- type ClassPipelineFn = (tag: unknown, props: AnyRecord, className?: ClassName, recipe?: string) => string;
142
+ type ClassPipelineFn = (tag: unknown, props: AnyRecord, className?: ClassName, recipe?: string) => string | undefined;
143
143
 
144
144
  interface RecipeOptions<TVariants extends VariantMap = VariantMap> {
145
145
  recipeMap?: Record<string, RecipeTarget<TVariants>>;
@@ -217,7 +217,9 @@ type AriaInvalidWithoutFix<M extends string = string> = AriaInvalidBase<M> & {
217
217
  type AriaInvalidResult<M extends string = string> = AriaInvalidWithFix<M> | AriaInvalidWithoutFix<M>;
218
218
  type AriaResult = ValidResult | AriaInvalidResult;
219
219
 
220
- type AriaRule<C extends AriaContext = AriaContext> = (context: C) => readonly AriaResult[];
220
+ type AriaRule<C extends AriaContext = AriaContext> = ((context: C) => readonly AriaResult[]) & {
221
+ readonly readsProps?: readonly string[];
222
+ };
221
223
 
222
224
  type PropNormalizer = (props: Readonly<AnyRecord & IntrinsicProps>) => Partial<AnyRecord & IntrinsicProps>;
223
225
 
package/dist/lit/index.js CHANGED
@@ -12,8 +12,11 @@ function isUndefined(value) {
12
12
  function isNull(value) {
13
13
  return value === null;
14
14
  }
15
+ function isNonNull(value) {
16
+ return value != null;
17
+ }
15
18
 
16
- // ../../lib/primitive/src/utils/is-object.ts
19
+ // ../../lib/primitive/src/utils/type-guards.ts
17
20
  function isObject(value, excludeArrays = false) {
18
21
  if (value === null || typeof value !== "object") return false;
19
22
  return excludeArrays ? !Array.isArray(value) : true;
@@ -507,6 +510,45 @@ var iterate = Object.freeze({
507
510
  values
508
511
  });
509
512
 
513
+ // ../../lib/primitive/src/utils/lru-cache.ts
514
+ var LRUCache = class {
515
+ #maxSize;
516
+ #store = /* @__PURE__ */ new Map();
517
+ constructor(maxSize) {
518
+ if (!Number.isInteger(maxSize) || maxSize < 1) {
519
+ throw new RangeError("LRUCache maxSize must be a positive integer.");
520
+ }
521
+ this.#maxSize = maxSize;
522
+ }
523
+ get(key) {
524
+ if (!this.#store.has(key)) return void 0;
525
+ const value = this.#store.get(key);
526
+ this.#store.delete(key);
527
+ this.#store.set(key, value);
528
+ return value;
529
+ }
530
+ set(key, value) {
531
+ this.#store.delete(key);
532
+ this.#store.set(key, value);
533
+ if (this.#store.size > this.#maxSize) {
534
+ const lru = this.#store.keys().next().value;
535
+ if (lru !== void 0) this.#store.delete(lru);
536
+ }
537
+ }
538
+ has(key) {
539
+ return this.#store.has(key);
540
+ }
541
+ delete(key) {
542
+ return this.#store.delete(key);
543
+ }
544
+ get size() {
545
+ return this.#store.size;
546
+ }
547
+ clear() {
548
+ this.#store.clear();
549
+ }
550
+ };
551
+
510
552
  // ../../lib/primitive/src/utils/merge-props.ts
511
553
  function mergeProps(defaultProps, props) {
512
554
  return {
@@ -551,28 +593,6 @@ function isTag(...args) {
551
593
  return tag !== void 0 && set2.has(tag);
552
594
  }
553
595
 
554
- // ../../lib/contract/src/strict/invariant-base.ts
555
- var InvariantBase = class {
556
- diagnostics;
557
- constructor(diagnostics) {
558
- this.diagnostics = diagnostics;
559
- }
560
- get active() {
561
- return this.diagnostics.active;
562
- }
563
- violate(input) {
564
- this.diagnostics.error(input);
565
- }
566
- // Always caps at warn — never throws. ARIA 'warning' violations route here
567
- // so they surface even in strict='throw' mode without aborting a render.
568
- warn(input) {
569
- this.diagnostics.warn(input);
570
- }
571
- invariant(condition, input) {
572
- if (!condition) this.violate(input);
573
- }
574
- };
575
-
576
596
  // ../../lib/contract/src/diagnostics/aria.ts
577
597
  import { DiagnosticCategory, DiagnosticCode } from "../_shared/diagnostics.js";
578
598
  var AriaDiagnostics = {
@@ -883,8 +903,30 @@ var HtmlDiagnostics = {
883
903
  }
884
904
  };
885
905
 
906
+ // ../../lib/contract/src/strict/invariant-base.ts
907
+ var InvariantBase = class {
908
+ diagnostics;
909
+ constructor(diagnostics) {
910
+ this.diagnostics = diagnostics;
911
+ }
912
+ get active() {
913
+ return this.diagnostics.active;
914
+ }
915
+ violate(input) {
916
+ this.diagnostics.error(input);
917
+ }
918
+ // Always caps at warn — never throws. ARIA 'warning' violations route here
919
+ // so they surface even in strict='throw' mode without aborting a render.
920
+ warn(input) {
921
+ this.diagnostics.warn(input);
922
+ }
923
+ invariant(condition, input) {
924
+ if (!condition) this.violate(input);
925
+ }
926
+ };
927
+
886
928
  // ../../lib/contract/src/aria/polymorphic-validator.ts
887
- var VALID = [{ valid: true }];
929
+ var NO_VIOLATIONS = [{ valid: true }];
888
930
  function isIntrinsicTag(tag) {
889
931
  return isString(tag);
890
932
  }
@@ -894,8 +936,7 @@ function omitProp(obj, key) {
894
936
  }
895
937
  var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
896
938
  #extraRules;
897
- #planCache = /* @__PURE__ */ new Map();
898
- static #MAX_CACHE = 100;
939
+ #planCache = new LRUCache(100);
899
940
  // Memoized AriaFix objects keyed by attribute name — the ARIA attribute set is
900
941
  // finite so this Map is bounded and avoids recreating closures on every cache miss.
901
942
  static #removeAttributeFixCache = /* @__PURE__ */ new Map();
@@ -927,17 +968,20 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
927
968
  static #deriveContext(tag, props) {
928
969
  if (!isIntrinsicTag(tag)) return { proceed: false, result: { props, violations: [] } };
929
970
  const implicitRole = getImplicitRole(tag, props);
930
- const hasRole = implicitRole != null || isString(props.role) && props.role.length > 0;
971
+ const hasRole = isNonNull(implicitRole) || isString(props.role) && props.role.length > 0;
931
972
  if (!hasRole) return { proceed: false, result: { props, violations: [] } };
932
973
  const normalized = _AriaPolicyEngine.#normalizeEmptyRole(tag, props);
933
- if (normalized.normalized) return { proceed: false, result: normalized.result };
934
- const effectiveRole = props.role ?? implicitRole;
974
+ const workingProps = normalized.normalized ? normalized.result.props : props;
975
+ const preExistingViolations = normalized.normalized ? normalized.result.violations : [];
976
+ const effectiveRole = workingProps.role ?? implicitRole;
935
977
  return {
936
978
  proceed: true,
937
979
  tag,
938
980
  implicitRole,
939
981
  effectiveRole,
940
- context: { tag, props, implicitRole, effectiveRole }
982
+ props: workingProps,
983
+ preExistingViolations,
984
+ context: { tag, props: workingProps, implicitRole, effectiveRole }
941
985
  };
942
986
  }
943
987
  static #runRules(rules, context) {
@@ -952,7 +996,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
952
996
  } = context;
953
997
  const { message, attribute, severity } = result;
954
998
  const resolvedMessage = message ?? result.diagnostic?.message;
955
- const fallbackDiag = resolvedMessage == null ? AriaDiagnostics.invalidRole(role, tag) : void 0;
999
+ const fallbackDiag = isNonNull(resolvedMessage) ? void 0 : AriaDiagnostics.invalidRole(role, tag);
956
1000
  violations.push({
957
1001
  message: resolvedMessage ?? fallbackDiag.message,
958
1002
  tag,
@@ -960,8 +1004,8 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
960
1004
  attribute,
961
1005
  severity,
962
1006
  phase: "evaluate",
963
- ...result.diagnostic != null && { diagnostic: result.diagnostic },
964
- ...fallbackDiag != null && { diagnostic: fallbackDiag }
1007
+ ...isNonNull(result.diagnostic) && { diagnostic: result.diagnostic },
1008
+ ...isNonNull(fallbackDiag) && { diagnostic: fallbackDiag }
965
1009
  });
966
1010
  if (result.fixable) fixes.push(result.fix);
967
1011
  });
@@ -969,7 +1013,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
969
1013
  return { violations, fixes };
970
1014
  }
971
1015
  static #getRules(context) {
972
- if (_AriaPolicyEngine.#hasRole(context.props) || context.effectiveRole != null && _AriaPolicyEngine.#LIVE_REGION_ROLES.has(context.effectiveRole)) {
1016
+ if (_AriaPolicyEngine.#hasRole(context.props) || isNonNull(context.effectiveRole) && _AriaPolicyEngine.#LIVE_REGION_ROLES.has(context.effectiveRole)) {
973
1017
  return _AriaPolicyEngine.#pipeline;
974
1018
  }
975
1019
  return _AriaPolicyEngine.#implicitOnlyRules;
@@ -977,24 +1021,36 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
977
1021
  static evaluate(tag, props) {
978
1022
  const derived = _AriaPolicyEngine.#deriveContext(tag, props);
979
1023
  if (!derived.proceed) return derived.result;
980
- const { tag: narrowedTag, implicitRole, context } = derived;
1024
+ const {
1025
+ tag: narrowedTag,
1026
+ implicitRole,
1027
+ context,
1028
+ props: workingProps,
1029
+ preExistingViolations
1030
+ } = derived;
981
1031
  const { violations, fixes } = _AriaPolicyEngine.#runRules(
982
1032
  _AriaPolicyEngine.#getRules(context),
983
1033
  context
984
1034
  );
985
- const next = _AriaPolicyEngine.#applyFixes(narrowedTag, implicitRole, props, fixes);
986
- return { props: next, violations };
1035
+ const next = _AriaPolicyEngine.#applyFixes(narrowedTag, implicitRole, workingProps, fixes);
1036
+ return { props: next, violations: [...preExistingViolations, ...violations] };
987
1037
  }
988
1038
  static #evaluateWithRules(tag, props, extraRules) {
989
1039
  const derived = _AriaPolicyEngine.#deriveContext(tag, props);
990
1040
  if (!derived.proceed) return derived.result;
991
- const { tag: narrowedTag, implicitRole, context } = derived;
1041
+ const {
1042
+ tag: narrowedTag,
1043
+ implicitRole,
1044
+ context,
1045
+ props: workingProps,
1046
+ preExistingViolations
1047
+ } = derived;
992
1048
  const { violations, fixes } = _AriaPolicyEngine.#runRules(
993
1049
  [..._AriaPolicyEngine.#getRules(context), ...extraRules],
994
1050
  context
995
1051
  );
996
- const next = _AriaPolicyEngine.#applyFixes(narrowedTag, implicitRole, props, fixes);
997
- return { props: next, violations };
1052
+ const next = _AriaPolicyEngine.#applyFixes(narrowedTag, implicitRole, workingProps, fixes);
1053
+ return { props: next, violations: [...preExistingViolations, ...violations] };
998
1054
  }
999
1055
  report(violations) {
1000
1056
  iterate.forEach(violations, (v) => {
@@ -1003,12 +1059,12 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1003
1059
  else this.warn(d);
1004
1060
  });
1005
1061
  }
1006
- // Cache key covers only the aria-relevant subset of props (tag + role + aria-* attrs).
1007
- // Non-aria props (className, onClick, etc.) do not affect ARIA decisions and are excluded
1008
- // so cache hits survive re-renders that only change non-aria props.
1009
- // Note: #extraRules are NOT included in the key each engine instance has its own Map,
1010
- // so two engines with different rules never share cache entries. If caching ever becomes
1011
- // static/shared, rule identity would need to be folded into the key.
1062
+ // Cache key covers only the aria-relevant subset of props (tag + role + aria-* attrs)
1063
+ // exactly what the built-in pipeline reads. Non-aria props (className, onClick, etc.) do
1064
+ // not affect built-in ARIA decisions and are excluded so cache hits survive re-renders
1065
+ // that only change non-aria props. This key alone is unsound for #extraRules, which may
1066
+ // read arbitrary props outside this set validate() extends it with #extraRulesKeySuffix,
1067
+ // or bypasses the cache, to account for that.
1012
1068
  static #createPlanKey(tag, props) {
1013
1069
  if (!isIntrinsicTag(tag)) return null;
1014
1070
  const parts = [tag];
@@ -1024,6 +1080,23 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1024
1080
  if (ariaEntries.length > 0) parts.push(...ariaEntries.sort());
1025
1081
  return parts.join("|");
1026
1082
  }
1083
+ // Extends the base cache key with the props each extra rule declares it reads (`readsProps`).
1084
+ // Returns null — meaning "don't cache" — if any extra rule omits `readsProps` (it may read
1085
+ // arbitrary props the key can't account for) or if a declared prop's value isn't a primitive
1086
+ // (object/array identity isn't stably representable in a string key).
1087
+ static #extraRulesKeySuffix(extraRules, props) {
1088
+ const parts = [];
1089
+ for (const rule of extraRules) {
1090
+ const readsProps = rule.readsProps;
1091
+ if (!isNonNull(readsProps)) return null;
1092
+ for (const propKey of readsProps) {
1093
+ const v = props[propKey];
1094
+ if (v !== void 0 && !isString(v) && !isNumber(v) && typeof v !== "boolean") return null;
1095
+ parts.push(`x:${propKey}:${String(v)}`);
1096
+ }
1097
+ }
1098
+ return parts.sort().join("|");
1099
+ }
1027
1100
  static #computePlan(inputProps, resultProps) {
1028
1101
  const removals = /* @__PURE__ */ new Set();
1029
1102
  const updates = {};
@@ -1047,12 +1120,15 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1047
1120
  return next;
1048
1121
  }
1049
1122
  validate(tag, props) {
1050
- const key = _AriaPolicyEngine.#createPlanKey(tag, props);
1123
+ const baseKey = _AriaPolicyEngine.#createPlanKey(tag, props);
1124
+ let key = baseKey;
1125
+ if (this.#extraRules.length > 0) {
1126
+ const suffix = _AriaPolicyEngine.#extraRulesKeySuffix(this.#extraRules, props);
1127
+ key = isNonNull(baseKey) && isNonNull(suffix) ? `${baseKey}|${suffix}` : null;
1128
+ }
1051
1129
  if (!isNull(key)) {
1052
1130
  const cached = this.#planCache.get(key);
1053
1131
  if (cached !== void 0) {
1054
- this.#planCache.delete(key);
1055
- this.#planCache.set(key, cached);
1056
1132
  if (cached.violations.length > 0) this.report(cached.violations);
1057
1133
  return {
1058
1134
  props: _AriaPolicyEngine.#applyPlan(props, cached.removals, cached.updates),
@@ -1069,10 +1145,6 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1069
1145
  );
1070
1146
  const plan = { removals, updates, violations: result.violations };
1071
1147
  this.#planCache.set(key, plan);
1072
- if (this.#planCache.size > _AriaPolicyEngine.#MAX_CACHE) {
1073
- const lru = this.#planCache.keys().next().value;
1074
- if (lru !== void 0) this.#planCache.delete(lru);
1075
- }
1076
1148
  }
1077
1149
  return result;
1078
1150
  }
@@ -1143,7 +1215,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1143
1215
  implicitRole
1144
1216
  }) {
1145
1217
  const role = props.role;
1146
- if (!implicitRole || !role || role === implicitRole) return VALID;
1218
+ if (!implicitRole || !role || role === implicitRole) return NO_VIOLATIONS;
1147
1219
  if (isStrongImplicitRole(tag) && role === "region") {
1148
1220
  return [
1149
1221
  {
@@ -1155,11 +1227,11 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1155
1227
  }
1156
1228
  ];
1157
1229
  }
1158
- return VALID;
1230
+ return NO_VIOLATIONS;
1159
1231
  }
1160
1232
  static #checkRedundantRole({ tag, props, implicitRole }) {
1161
1233
  const role = props.role;
1162
- if (!implicitRole || !role || role !== implicitRole) return VALID;
1234
+ if (!implicitRole || !role || role !== implicitRole) return NO_VIOLATIONS;
1163
1235
  return [
1164
1236
  {
1165
1237
  valid: false,
@@ -1172,8 +1244,8 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1172
1244
  }
1173
1245
  static #checkStandaloneRegion({ tag, props, implicitRole }) {
1174
1246
  const role = props.role;
1175
- if (role !== "region") return VALID;
1176
- if (!isStandaloneTag(tag)) return VALID;
1247
+ if (role !== "region") return NO_VIOLATIONS;
1248
+ if (!isStandaloneTag(tag)) return NO_VIOLATIONS;
1177
1249
  return [
1178
1250
  {
1179
1251
  valid: false,
@@ -1189,7 +1261,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1189
1261
  props,
1190
1262
  effectiveRole
1191
1263
  }) {
1192
- if (effectiveRole === "none" || effectiveRole === "presentation") return VALID;
1264
+ if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS;
1193
1265
  const results = [];
1194
1266
  iterate.forEachEntry(props, (key) => {
1195
1267
  if (!key.startsWith("aria-")) return;
@@ -1307,12 +1379,12 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1307
1379
  }
1308
1380
  }
1309
1381
  static #checkAriaAttributeValues({ props, effectiveRole }) {
1310
- if (effectiveRole === "none" || effectiveRole === "presentation") return VALID;
1382
+ if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS;
1311
1383
  const results = [];
1312
1384
  iterate.forEachEntry(props, (key, value) => {
1313
1385
  if (!key.startsWith("aria-")) return;
1314
1386
  const type = _AriaPolicyEngine.#ARIA_VALUE_TYPES.get(key);
1315
- if (type == null) return;
1387
+ if (!isNonNull(type)) return;
1316
1388
  if (_AriaPolicyEngine.#isValidAriaValue(value, type)) return;
1317
1389
  results.push({
1318
1390
  valid: false,
@@ -1343,13 +1415,13 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1343
1415
  props,
1344
1416
  effectiveRole
1345
1417
  }) {
1346
- if (effectiveRole === "none" || effectiveRole === "presentation") return VALID;
1418
+ if (effectiveRole === "none" || effectiveRole === "presentation") return NO_VIOLATIONS;
1347
1419
  const implicitLevel = _AriaPolicyEngine.#HEADING_IMPLICIT_LEVELS.get(tag);
1348
- if (implicitLevel == null) return VALID;
1420
+ if (!isNonNull(implicitLevel)) return NO_VIOLATIONS;
1349
1421
  const raw = props["aria-level"];
1350
- if (raw == null) return VALID;
1422
+ if (!isNonNull(raw)) return NO_VIOLATIONS;
1351
1423
  const n = typeof raw === "number" ? raw : typeof raw === "string" ? parseInt(raw, 10) : NaN;
1352
- if (!Number.isFinite(n) || n !== implicitLevel) return VALID;
1424
+ if (!Number.isFinite(n) || n !== implicitLevel) return NO_VIOLATIONS;
1353
1425
  return [
1354
1426
  {
1355
1427
  valid: false,
@@ -1372,9 +1444,10 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1372
1444
  props,
1373
1445
  effectiveRole
1374
1446
  }) {
1375
- if (!effectiveRole || !_AriaPolicyEngine.#NAME_REQUIRED_ROLES.has(effectiveRole)) return VALID;
1376
- if ("aria-label" in props || "aria-labelledby" in props) return VALID;
1377
- if (tag === "img" && typeof props.alt === "string" && props.alt.length > 0) return VALID;
1447
+ if (!effectiveRole || !_AriaPolicyEngine.#NAME_REQUIRED_ROLES.has(effectiveRole))
1448
+ return NO_VIOLATIONS;
1449
+ if ("aria-label" in props || "aria-labelledby" in props) return NO_VIOLATIONS;
1450
+ if (tag === "img" && typeof props.alt === "string" && props.alt.length > 0) return NO_VIOLATIONS;
1378
1451
  return [
1379
1452
  {
1380
1453
  valid: false,
@@ -1397,9 +1470,9 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1397
1470
  props,
1398
1471
  effectiveRole
1399
1472
  }) {
1400
- if (!effectiveRole) return VALID;
1473
+ if (!effectiveRole) return NO_VIOLATIONS;
1401
1474
  const required = _AriaPolicyEngine.#REQUIRED_PROPERTIES.get(effectiveRole);
1402
- if (required == null) return VALID;
1475
+ if (!isNonNull(required)) return NO_VIOLATIONS;
1403
1476
  const results = [];
1404
1477
  iterate.forEach(required, (attr) => {
1405
1478
  if (attr in props) return;
@@ -1423,12 +1496,12 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1423
1496
  ]);
1424
1497
  // WAI-ARIA 1.2 §6.6: aria-hidden="true" must not be placed on focusable elements.
1425
1498
  static #checkAriaHiddenOnFocusable({ tag, props }) {
1426
- if (props["aria-hidden"] !== "true" && props["aria-hidden"] !== true) return VALID;
1499
+ if (props["aria-hidden"] !== "true" && props["aria-hidden"] !== true) return NO_VIOLATIONS;
1427
1500
  const isInteractive = _AriaPolicyEngine.#INTERACTIVE_TAGS.has(tag);
1428
1501
  if (!isInteractive) {
1429
1502
  const tabindex = props.tabindex;
1430
1503
  const n = typeof tabindex === "number" ? tabindex : typeof tabindex === "string" ? parseInt(tabindex, 10) : NaN;
1431
- if (!Number.isFinite(n) || n < 0) return VALID;
1504
+ if (!Number.isFinite(n) || n < 0) return NO_VIOLATIONS;
1432
1505
  }
1433
1506
  return [
1434
1507
  {
@@ -1447,7 +1520,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1447
1520
  props,
1448
1521
  effectiveRole
1449
1522
  }) {
1450
- if (effectiveRole !== "none" && effectiveRole !== "presentation") return VALID;
1523
+ if (effectiveRole !== "none" && effectiveRole !== "presentation") return NO_VIOLATIONS;
1451
1524
  const results = [];
1452
1525
  iterate.forEachEntry(props, (key) => {
1453
1526
  if (!key.startsWith("aria-")) return;
@@ -1471,10 +1544,10 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1471
1544
  ["timer", "off"]
1472
1545
  ]);
1473
1546
  static #checkMissingLiveRegion({ effectiveRole, props }) {
1474
- if (!effectiveRole) return VALID;
1547
+ if (!effectiveRole) return NO_VIOLATIONS;
1475
1548
  const impliedLive = _AriaPolicyEngine.#LIVE_REGION_ROLES.get(effectiveRole);
1476
- if (!impliedLive) return VALID;
1477
- if ("aria-live" in props) return VALID;
1549
+ if (!impliedLive) return NO_VIOLATIONS;
1550
+ if ("aria-live" in props) return NO_VIOLATIONS;
1478
1551
  const injectLive = {
1479
1552
  kind: `injectLive:${effectiveRole}`,
1480
1553
  apply: (ctx) => ({
@@ -1494,8 +1567,9 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1494
1567
  ];
1495
1568
  }
1496
1569
  static #checkMissingAtomic({ effectiveRole, props }) {
1497
- if (!effectiveRole || !_AriaPolicyEngine.#LIVE_REGION_ROLES.has(effectiveRole)) return VALID;
1498
- if ("aria-atomic" in props) return VALID;
1570
+ if (!effectiveRole || !_AriaPolicyEngine.#LIVE_REGION_ROLES.has(effectiveRole))
1571
+ return NO_VIOLATIONS;
1572
+ if ("aria-atomic" in props) return NO_VIOLATIONS;
1499
1573
  return [
1500
1574
  {
1501
1575
  valid: false,
@@ -1519,8 +1593,8 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1519
1593
  };
1520
1594
  static #checkInvalidAriaRelevant({ props }) {
1521
1595
  const relevant = props["aria-relevant"];
1522
- if (relevant === void 0) return VALID;
1523
- if (typeof relevant !== "string") return VALID;
1596
+ if (relevant === void 0) return NO_VIOLATIONS;
1597
+ if (typeof relevant !== "string") return NO_VIOLATIONS;
1524
1598
  const tokens = relevant.trim().split(/\s+/);
1525
1599
  const invalid = tokens.filter((t) => !_AriaPolicyEngine.#VALID_RELEVANT_TOKENS.has(t));
1526
1600
  if (invalid.length > 0) {
@@ -1547,7 +1621,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1547
1621
  }
1548
1622
  ];
1549
1623
  }
1550
- return VALID;
1624
+ return NO_VIOLATIONS;
1551
1625
  }
1552
1626
  };
1553
1627
 
@@ -2147,7 +2221,7 @@ function cva2(base, config) {
2147
2221
  // ../../lib/styling/src/static-class-resolver.ts
2148
2222
  var StaticClassResolver = class {
2149
2223
  #baseClass;
2150
- #cache = /* @__PURE__ */ new Map();
2224
+ #cache = new LRUCache(200);
2151
2225
  #resolveTag;
2152
2226
  constructor(baseClass, tagMap) {
2153
2227
  this.#baseClass = Array.isArray(baseClass) ? baseClass.join(" ") : baseClass;
@@ -2161,17 +2235,9 @@ var StaticClassResolver = class {
2161
2235
  resolve(tag, skipTagMap = false) {
2162
2236
  if (typeof tag !== "string" || skipTagMap) return this.#baseClass;
2163
2237
  const cached = this.#cache.get(tag);
2164
- if (cached !== void 0) {
2165
- this.#cache.delete(tag);
2166
- this.#cache.set(tag, cached);
2167
- return cached;
2168
- }
2238
+ if (cached !== void 0) return cached;
2169
2239
  const result = this.#resolveTag(tag);
2170
2240
  this.#cache.set(tag, result);
2171
- if (this.#cache.size > 200) {
2172
- const lru = this.#cache.keys().next().value;
2173
- if (lru !== void 0) this.#cache.delete(lru);
2174
- }
2175
2241
  return result;
2176
2242
  }
2177
2243
  };
@@ -2182,7 +2248,7 @@ var VariantClassResolver = class _VariantClassResolver {
2182
2248
  #recipeMap;
2183
2249
  #variantKeys;
2184
2250
  #precomputedClasses;
2185
- #cache = /* @__PURE__ */ new Map();
2251
+ #cache = new LRUCache(1e3);
2186
2252
  constructor(cvaFn, recipeMap, variantKeys, precomputedClasses) {
2187
2253
  this.#cvaFn = cvaFn ?? null;
2188
2254
  this.#recipeMap = Object.freeze(recipeMap ?? {});
@@ -2197,17 +2263,9 @@ var VariantClassResolver = class _VariantClassResolver {
2197
2263
  if (precomputed !== void 0) return precomputed;
2198
2264
  }
2199
2265
  const cached = this.#cache.get(cacheKey);
2200
- if (cached !== void 0) {
2201
- this.#cache.delete(cacheKey);
2202
- this.#cache.set(cacheKey, cached);
2203
- return cached;
2204
- }
2266
+ if (cached !== void 0) return cached;
2205
2267
  const result = this.#compute(props, recipe);
2206
2268
  this.#cache.set(cacheKey, result);
2207
- if (this.#cache.size > 1e3) {
2208
- const lru = this.#cache.keys().next().value;
2209
- if (lru !== void 0) this.#cache.delete(lru);
2210
- }
2211
2269
  return result;
2212
2270
  }
2213
2271
  #compute(props, recipe) {
@@ -2264,7 +2322,7 @@ function createClassPipeline(resolved) {
2264
2322
  const staticClasses = staticResolver.resolve(tag, recipe !== void 0);
2265
2323
  const variantClasses = variantResolver.resolve({ props, recipe });
2266
2324
  if (!className)
2267
- return staticClasses && variantClasses ? `${staticClasses} ${variantClasses}` : staticClasses || variantClasses;
2325
+ return (staticClasses && variantClasses ? `${staticClasses} ${variantClasses}` : staticClasses || variantClasses) || void 0;
2268
2326
  return cn(staticClasses, variantClasses, className);
2269
2327
  };
2270
2328
  }
@@ -2475,7 +2533,7 @@ function createPolymorphic2(options = {}) {
2475
2533
  if (process.env.NODE_ENV !== "production") {
2476
2534
  validateRenderProps(resolved.diagnostics, resolved, props, recipe);
2477
2535
  }
2478
- return classPipeline(tag, props, className, recipe);
2536
+ return classPipeline(tag, props, className, recipe) || void 0;
2479
2537
  },
2480
2538
  resolveAria(tag, props) {
2481
2539
  return resolveAriaFn(tag, props);
@@ -2645,7 +2703,11 @@ function resolveHostState(bundle, props) {
2645
2703
  function diffAndApplyAttributes(host, state, prevPipelineAttrs, incomingProps) {
2646
2704
  const hasOwn2 = Object.hasOwn;
2647
2705
  const attrs = state.attributes;
2648
- host.className = state.className;
2706
+ if (state.className) {
2707
+ host.className = state.className;
2708
+ } else {
2709
+ host.removeAttribute("class");
2710
+ }
2649
2711
  for (const key of prevPipelineAttrs) {
2650
2712
  if (!hasOwn2(attrs, key)) host.removeAttribute(key);
2651
2713
  }
@@ -156,7 +156,7 @@ interface BaseClassOptions {
156
156
  baseClassName?: ClassName;
157
157
  }
158
158
 
159
- type ClassPipelineFn = (tag: unknown, props: AnyRecord, className?: ClassName, recipe?: string) => string;
159
+ type ClassPipelineFn = (tag: unknown, props: AnyRecord, className?: ClassName, recipe?: string) => string | undefined;
160
160
 
161
161
  interface RecipeOptions<TVariants extends VariantMap = VariantMap> {
162
162
  recipeMap?: Record<string, RecipeTarget<TVariants>>;
@@ -234,7 +234,9 @@ type AriaInvalidWithoutFix<M extends string = string> = AriaInvalidBase<M> & {
234
234
  type AriaInvalidResult<M extends string = string> = AriaInvalidWithFix<M> | AriaInvalidWithoutFix<M>;
235
235
  type AriaResult = ValidResult | AriaInvalidResult;
236
236
 
237
- type AriaRule<C extends AriaContext = AriaContext> = (context: C) => readonly AriaResult[];
237
+ type AriaRule<C extends AriaContext = AriaContext> = ((context: C) => readonly AriaResult[]) & {
238
+ readonly readsProps?: readonly string[];
239
+ };
238
240
 
239
241
  type PropNormalizer = (props: Readonly<AnyRecord & IntrinsicProps>) => Partial<AnyRecord & IntrinsicProps>;
240
242