stimeo-ui 0.2.1 → 0.3.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.
Files changed (184) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/dist/cable/index.d.ts +16 -16
  3. package/dist/cable/index.js.map +1 -1
  4. package/dist/controllers/accordion_controller.js +10 -0
  5. package/dist/controllers/accordion_controller.js.map +1 -1
  6. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  7. package/dist/controllers/breadcrumb_controller.d.ts +97 -10
  8. package/dist/controllers/breadcrumb_controller.js +225 -13
  9. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  10. package/dist/controllers/calendar_controller.d.ts +11 -1
  11. package/dist/controllers/calendar_controller.js +89 -22
  12. package/dist/controllers/calendar_controller.js.map +1 -1
  13. package/dist/controllers/carousel_controller.d.ts +16 -1
  14. package/dist/controllers/carousel_controller.js +47 -6
  15. package/dist/controllers/carousel_controller.js.map +1 -1
  16. package/dist/controllers/collapsible_controller.js +2 -2
  17. package/dist/controllers/collapsible_controller.js.map +1 -1
  18. package/dist/controllers/color_picker_controller.d.ts +12 -1
  19. package/dist/controllers/color_picker_controller.js +46 -7
  20. package/dist/controllers/color_picker_controller.js.map +1 -1
  21. package/dist/controllers/combobox_controller.d.ts +14 -1
  22. package/dist/controllers/combobox_controller.js +162 -23
  23. package/dist/controllers/combobox_controller.js.map +1 -1
  24. package/dist/controllers/command_palette_controller.d.ts +2 -0
  25. package/dist/controllers/command_palette_controller.js +194 -17
  26. package/dist/controllers/command_palette_controller.js.map +1 -1
  27. package/dist/controllers/confirm_controller.js.map +1 -1
  28. package/dist/controllers/context_menu_controller.d.ts +7 -6
  29. package/dist/controllers/context_menu_controller.js +32 -10
  30. package/dist/controllers/context_menu_controller.js.map +1 -1
  31. package/dist/controllers/count_up_controller.js.map +1 -1
  32. package/dist/controllers/data_grid_controller.d.ts +19 -1
  33. package/dist/controllers/data_grid_controller.js +82 -4
  34. package/dist/controllers/data_grid_controller.js.map +1 -1
  35. package/dist/controllers/date_range_picker_controller.d.ts +1 -0
  36. package/dist/controllers/date_range_picker_controller.js +27 -3
  37. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  38. package/dist/controllers/dialog_controller.js.map +1 -1
  39. package/dist/controllers/dismissible_controller.js.map +1 -1
  40. package/dist/controllers/drawer_controller.js.map +1 -1
  41. package/dist/controllers/dropdown_controller.d.ts +1 -2
  42. package/dist/controllers/dropdown_controller.js.map +1 -1
  43. package/dist/controllers/editable_controller.js +1 -0
  44. package/dist/controllers/editable_controller.js.map +1 -1
  45. package/dist/controllers/focus_controller.d.ts +1 -1
  46. package/dist/controllers/focus_controller.js.map +1 -1
  47. package/dist/controllers/form_field_controller.js.map +1 -1
  48. package/dist/controllers/form_validation_controller.js +1 -1
  49. package/dist/controllers/form_validation_controller.js.map +1 -1
  50. package/dist/controllers/highlight_controller.js.map +1 -1
  51. package/dist/controllers/hover_card_controller.d.ts +1 -1
  52. package/dist/controllers/hover_card_controller.js.map +1 -1
  53. package/dist/controllers/intersection_controller.js +36 -11
  54. package/dist/controllers/intersection_controller.js.map +1 -1
  55. package/dist/controllers/lazy_frame_controller.js +31 -10
  56. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  57. package/dist/controllers/listbox_controller.d.ts +9 -2
  58. package/dist/controllers/listbox_controller.js +257 -53
  59. package/dist/controllers/listbox_controller.js.map +1 -1
  60. package/dist/controllers/local_time_controller.js +2 -2
  61. package/dist/controllers/local_time_controller.js.map +1 -1
  62. package/dist/controllers/menu_controller.d.ts +29 -11
  63. package/dist/controllers/menu_controller.js +104 -17
  64. package/dist/controllers/menu_controller.js.map +1 -1
  65. package/dist/controllers/menubar_controller.d.ts +91 -11
  66. package/dist/controllers/menubar_controller.js +415 -63
  67. package/dist/controllers/menubar_controller.js.map +1 -1
  68. package/dist/controllers/meter_controller.js.map +1 -1
  69. package/dist/controllers/multi_select_controller.d.ts +31 -5
  70. package/dist/controllers/multi_select_controller.js +312 -29
  71. package/dist/controllers/multi_select_controller.js.map +1 -1
  72. package/dist/controllers/navigation_menu_controller.d.ts +65 -18
  73. package/dist/controllers/navigation_menu_controller.js +154 -27
  74. package/dist/controllers/navigation_menu_controller.js.map +1 -1
  75. package/dist/controllers/nested_form_controller.d.ts +4 -5
  76. package/dist/controllers/nested_form_controller.js.map +1 -1
  77. package/dist/controllers/number_input_controller.d.ts +1 -1
  78. package/dist/controllers/number_input_controller.js +7 -0
  79. package/dist/controllers/number_input_controller.js.map +1 -1
  80. package/dist/controllers/otp_controller.js +18 -1
  81. package/dist/controllers/otp_controller.js.map +1 -1
  82. package/dist/controllers/overflow_indicator_controller.d.ts +3 -2
  83. package/dist/controllers/overflow_indicator_controller.js +81 -13
  84. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  85. package/dist/controllers/overflow_menu_controller.d.ts +78 -19
  86. package/dist/controllers/overflow_menu_controller.js +381 -57
  87. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  88. package/dist/controllers/pagination_controller.d.ts +36 -5
  89. package/dist/controllers/pagination_controller.js +163 -32
  90. package/dist/controllers/pagination_controller.js.map +1 -1
  91. package/dist/controllers/persist_controller.js +6 -6
  92. package/dist/controllers/persist_controller.js.map +1 -1
  93. package/dist/controllers/pointer_drag_controller.d.ts +2 -3
  94. package/dist/controllers/pointer_drag_controller.js +9 -1
  95. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  96. package/dist/controllers/popover_controller.d.ts +1 -2
  97. package/dist/controllers/popover_controller.js +2 -2
  98. package/dist/controllers/popover_controller.js.map +1 -1
  99. package/dist/controllers/portal_controller.d.ts +4 -4
  100. package/dist/controllers/portal_controller.js.map +1 -1
  101. package/dist/controllers/progress_controller.js.map +1 -1
  102. package/dist/controllers/radio_group_controller.js +22 -3
  103. package/dist/controllers/radio_group_controller.js.map +1 -1
  104. package/dist/controllers/range_slider_controller.d.ts +14 -4
  105. package/dist/controllers/range_slider_controller.js +32 -6
  106. package/dist/controllers/range_slider_controller.js.map +1 -1
  107. package/dist/controllers/rating_controller.js +16 -2
  108. package/dist/controllers/rating_controller.js.map +1 -1
  109. package/dist/controllers/read_more_controller.js +63 -19
  110. package/dist/controllers/read_more_controller.js.map +1 -1
  111. package/dist/controllers/reading_progress_controller.js.map +1 -1
  112. package/dist/controllers/resizable_controller.d.ts +3 -1
  113. package/dist/controllers/resizable_controller.js +65 -1
  114. package/dist/controllers/resizable_controller.js.map +1 -1
  115. package/dist/controllers/roving_controller.js +17 -2
  116. package/dist/controllers/roving_controller.js.map +1 -1
  117. package/dist/controllers/scroll_area_controller.js +86 -12
  118. package/dist/controllers/scroll_area_controller.js.map +1 -1
  119. package/dist/controllers/scroll_restore_controller.js +1 -1
  120. package/dist/controllers/scroll_restore_controller.js.map +1 -1
  121. package/dist/controllers/scroll_visibility_controller.d.ts +6 -3
  122. package/dist/controllers/scroll_visibility_controller.js +33 -3
  123. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  124. package/dist/controllers/scrollspy_controller.d.ts +64 -15
  125. package/dist/controllers/scrollspy_controller.js +346 -73
  126. package/dist/controllers/scrollspy_controller.js.map +1 -1
  127. package/dist/controllers/separator_controller.d.ts +20 -5
  128. package/dist/controllers/separator_controller.js +9 -0
  129. package/dist/controllers/separator_controller.js.map +1 -1
  130. package/dist/controllers/sidebar_controller.d.ts +1 -1
  131. package/dist/controllers/sidebar_controller.js.map +1 -1
  132. package/dist/controllers/skeleton_controller.js +1 -1
  133. package/dist/controllers/skeleton_controller.js.map +1 -1
  134. package/dist/controllers/slider_controller.d.ts +11 -1
  135. package/dist/controllers/slider_controller.js +32 -6
  136. package/dist/controllers/slider_controller.js.map +1 -1
  137. package/dist/controllers/sortable_controller.d.ts +3 -1
  138. package/dist/controllers/sortable_controller.js +34 -3
  139. package/dist/controllers/sortable_controller.js.map +1 -1
  140. package/dist/controllers/spinner_controller.js +1 -1
  141. package/dist/controllers/spinner_controller.js.map +1 -1
  142. package/dist/controllers/stick_to_bottom_controller.d.ts +5 -6
  143. package/dist/controllers/stick_to_bottom_controller.js +2 -1
  144. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  145. package/dist/controllers/sticky_observer_controller.js +32 -11
  146. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  147. package/dist/controllers/submit_once_controller.d.ts +2 -2
  148. package/dist/controllers/submit_once_controller.js.map +1 -1
  149. package/dist/controllers/switch_controller.js +1 -0
  150. package/dist/controllers/switch_controller.js.map +1 -1
  151. package/dist/controllers/tabs_controller.d.ts +11 -3
  152. package/dist/controllers/tabs_controller.js +26 -3
  153. package/dist/controllers/tabs_controller.js.map +1 -1
  154. package/dist/controllers/tags_input_controller.js +22 -2
  155. package/dist/controllers/tags_input_controller.js.map +1 -1
  156. package/dist/controllers/theme_controller.d.ts +1 -1
  157. package/dist/controllers/theme_controller.js +22 -3
  158. package/dist/controllers/theme_controller.js.map +1 -1
  159. package/dist/controllers/time_picker_controller.js +20 -1
  160. package/dist/controllers/time_picker_controller.js.map +1 -1
  161. package/dist/controllers/toast_controller.d.ts +4 -5
  162. package/dist/controllers/toast_controller.js +4 -5
  163. package/dist/controllers/toast_controller.js.map +1 -1
  164. package/dist/controllers/toggle_group_controller.js +23 -2
  165. package/dist/controllers/toggle_group_controller.js.map +1 -1
  166. package/dist/controllers/toolbar_controller.d.ts +53 -13
  167. package/dist/controllers/toolbar_controller.js +230 -31
  168. package/dist/controllers/toolbar_controller.js.map +1 -1
  169. package/dist/controllers/tooltip_controller.d.ts +1 -1
  170. package/dist/controllers/tooltip_controller.js.map +1 -1
  171. package/dist/controllers/transition_controller.js.map +1 -1
  172. package/dist/controllers/tree_view_controller.d.ts +87 -6
  173. package/dist/controllers/tree_view_controller.js +467 -51
  174. package/dist/controllers/tree_view_controller.js.map +1 -1
  175. package/dist/index.js +3514 -689
  176. package/dist/index.js.map +1 -1
  177. package/dist/inspector/cli.d.ts +375 -39
  178. package/dist/inspector/cli.js +228 -14
  179. package/dist/inspector/cli.js.map +1 -1
  180. package/dist/inspector/cli_bin.js +419 -48
  181. package/dist/inspector/cli_bin.js.map +1 -1
  182. package/dist/inspector/examples.json +24 -24
  183. package/dist/inspector/manifest.json +1040 -125
  184. package/package.json +5 -6
@@ -269,12 +269,17 @@ var DIAGNOSTIC_CODES = [
269
269
  "unknown-action-method",
270
270
  "orphan-target",
271
271
  "missing-required-target",
272
+ "missing-conditional-target",
272
273
  "missing-aria",
273
274
  "invalid-aria-value",
274
275
  "keyboard-inaccessible",
275
276
  "unresolved-idref",
276
277
  "managed-aria",
277
278
  "composition-mismatch",
279
+ "missing-companion",
280
+ "undeclared-target",
281
+ "cardinality-violation",
282
+ "forbidden-aria",
278
283
  "unknown-ignore-code"
279
284
  ];
280
285
 
@@ -292,6 +297,14 @@ function checkSource(source, manifest) {
292
297
  if (valueStart === void 0 || valueEnd === void 0) return false;
293
298
  return erbRanges.some(([start, end]) => start < valueEnd && end > valueStart);
294
299
  };
300
+ const conditionHolds = (scope, identifier, when) => {
301
+ if (!when) return true;
302
+ const hostAttr = scope.attrs.find(
303
+ (a) => a.name === `data-${identifier}-${dasherize(when.value)}-value`
304
+ );
305
+ if (hostAttr && isDynamicValue(hostAttr)) return null;
306
+ return when.equals.includes(hostAttr ? hostAttr.value.trim() : when.default);
307
+ };
295
308
  const suppressed = (node, code) => {
296
309
  if (code === "unknown-ignore-code") return false;
297
310
  let current = node;
@@ -339,7 +352,40 @@ function checkSource(source, manifest) {
339
352
  if (found) found.push(el);
340
353
  else byName.set(target, [el]);
341
354
  };
355
+ const contentHolds = (scope, identifier, element, condition) => {
356
+ if (!condition) return true;
357
+ const nodes = targetNodes.get(scope)?.get(identifier)?.get(condition.target) ?? [];
358
+ let held = 0;
359
+ for (const node of nodes) if (isWithin(node, element)) held += 1;
360
+ if (condition.min !== void 0 && held < condition.min) return false;
361
+ if (condition.max !== void 0 && held > condition.max) return false;
362
+ return true;
363
+ };
364
+ const elementHolds = (element, condition) => !condition?.exceptTags.includes(element.tag);
365
+ const roleTally = /* @__PURE__ */ new Map();
366
+ const focusableRoleTally = /* @__PURE__ */ new Map();
367
+ const documentHolds = (condition) => {
368
+ if (condition === void 0) return false;
369
+ const tally = condition.focusable ? focusableRoleTally : roleTally;
370
+ return (tally.get(condition.role) ?? 0) >= condition.atLeast;
371
+ };
372
+ const declarationAttrs = /* @__PURE__ */ new Set();
373
+ for (const controller of Object.values(known)) {
374
+ for (const rule of controller.targetDeclarations) declarationAttrs.add(rule.attr);
375
+ }
376
+ const declarationCandidates = [];
342
377
  walk(tree, (node) => {
378
+ if (declarationAttrs.size > 0 && node.attrs.some((a) => declarationAttrs.has(a.name))) {
379
+ declarationCandidates.push(node);
380
+ }
381
+ const roleAttr = node.attrs.find((a) => a.name === "role");
382
+ if (roleAttr && !isDynamicValue(roleAttr)) {
383
+ const role = roleAttr.value.trim();
384
+ roleTally.set(role, (roleTally.get(role) ?? 0) + 1);
385
+ if (isFocusable(node, "tab")) {
386
+ focusableRoleTally.set(role, (focusableRoleTally.get(role) ?? 0) + 1);
387
+ }
388
+ }
343
389
  for (const attr of node.attrs) {
344
390
  if (attr.name === IGNORE_ATTR) {
345
391
  for (const token of attr.value.split(/\s+/).filter((t) => t.length > 0)) {
@@ -418,7 +464,7 @@ function checkSource(source, manifest) {
418
464
  );
419
465
  continue;
420
466
  }
421
- const targetName = attr.value.trim();
467
+ const targetName = isDynamicValue(attr) ? "" : attr.value.trim();
422
468
  if (targetName.length > 0 && !controller.targets.includes(targetName)) {
423
469
  const best = nearestName(targetName, controller.targets);
424
470
  report(
@@ -522,13 +568,36 @@ function checkSource(source, manifest) {
522
568
  }
523
569
  }
524
570
  }
571
+ for (const { node, identifier } of scopes) {
572
+ const controller = known[identifier];
573
+ if (!controller) continue;
574
+ const present = presence.get(node)?.get(identifier) ?? /* @__PURE__ */ new Set();
575
+ for (const rule of controller.conditionalTargets) {
576
+ if (!present.has(rule.whenPresent)) continue;
577
+ const missing = rule.require.filter((target) => !present.has(target));
578
+ if (missing.length === 0) continue;
579
+ report(
580
+ node,
581
+ "missing-conditional-target",
582
+ "error",
583
+ `"${identifier}" has a "${rule.whenPresent}" target, which also requires ${list(missing)}.`,
584
+ node,
585
+ rule.suggestion
586
+ );
587
+ }
588
+ }
525
589
  for (const { node, identifier } of scopes) {
526
590
  const controller = known[identifier];
527
591
  if (!controller) continue;
528
592
  for (const req of controller.a11y) {
593
+ if (conditionHolds(node, identifier, req.when) !== true) continue;
529
594
  const elements = req.target === "" ? [node] : targetNodes.get(node)?.get(identifier)?.get(req.target) ?? [];
595
+ if (req.whenDocument && !documentHolds(req.whenDocument)) continue;
596
+ const severity = documentHolds(req.escalateWhen) ? "error" : req.severity ?? "error";
530
597
  for (const el of elements) {
531
- checkA11y(report, el, identifier, req);
598
+ if (!contentHolds(node, identifier, el, req.whenContains)) continue;
599
+ if (!elementHolds(el, req.whenElement)) continue;
600
+ checkA11y(report, el, identifier, req, severity);
532
601
  }
533
602
  }
534
603
  }
@@ -575,6 +644,32 @@ function checkSource(source, manifest) {
575
644
  }
576
645
  }
577
646
  }
647
+ for (const { node, identifier } of scopes) {
648
+ const controller = known[identifier];
649
+ if (!controller) continue;
650
+ for (const rule of controller.forbiddenAria) {
651
+ const elements = rule.target === "" ? [node] : targetNodes.get(node)?.get(identifier)?.get(rule.target) ?? [];
652
+ const where = rule.target === "" ? "scope element" : `"${rule.target}" target`;
653
+ for (const el of elements) {
654
+ if (!contentHolds(node, identifier, el, rule.whenContains)) continue;
655
+ for (const name of rule.attrs) {
656
+ const attr = el.attrs.find((a) => a.name === name);
657
+ if (!attr) continue;
658
+ if (rule.values && (isDynamicValue(attr) || !rule.values.includes(attr.value.trim()))) {
659
+ continue;
660
+ }
661
+ report(
662
+ el,
663
+ "forbidden-aria",
664
+ "warning",
665
+ `${name} on the ${where} of "${identifier}" contradicts the markup around it.`,
666
+ attr,
667
+ rule.suggestion
668
+ );
669
+ }
670
+ }
671
+ }
672
+ }
578
673
  for (const { node, identifier } of scopes) {
579
674
  const controller = known[identifier];
580
675
  if (!controller) continue;
@@ -583,14 +678,7 @@ function checkSource(source, manifest) {
583
678
  const elements = rule.target === "" ? [node] : targeted ?? (rule.fallbackToScope ? [node] : []);
584
679
  const where = rule.target === "" || !targeted ? "scope element" : `"${rule.target}" target`;
585
680
  const when = rule.when;
586
- if (when) {
587
- const hostAttr = node.attrs.find(
588
- (a) => a.name === `data-${identifier}-${dasherize(when.value)}-value`
589
- );
590
- if (hostAttr && isDynamicValue(hostAttr)) continue;
591
- const hostEffective = hostAttr ? hostAttr.value.trim() : when.default;
592
- if (!when.equals.includes(hostEffective)) continue;
593
- }
681
+ if (conditionHolds(node, identifier, when) !== true) continue;
594
682
  const hostContext = when ? ` (${when.value} ${list(when.equals)})` : "";
595
683
  for (const el of elements) {
596
684
  const declared = el.attrs.find((a) => a.name === "data-controller");
@@ -626,6 +714,113 @@ function checkSource(source, manifest) {
626
714
  }
627
715
  }
628
716
  }
717
+ for (const { node, identifier } of scopes) {
718
+ const controller = known[identifier];
719
+ if (!controller) continue;
720
+ for (const rule of controller.companions) {
721
+ const elements = rule.target === "" ? [node] : targetNodes.get(node)?.get(identifier)?.get(rule.target) ?? [];
722
+ const where = rule.target === "" ? "scope element" : `"${rule.target}" target`;
723
+ for (const el of elements) {
724
+ const declared = el.attrs.find((a) => a.name === "data-controller");
725
+ if (declared && isDynamicValue(declared)) continue;
726
+ if (declared && controllerIdentifiers(declared.value).includes(rule.controller)) continue;
727
+ report(
728
+ el,
729
+ "missing-companion",
730
+ "error",
731
+ `The ${where} of "${identifier}" must also declare "${rule.controller}" in data-controller.`,
732
+ declared ?? el,
733
+ rule.suggestion,
734
+ // Appending to an existing list is unambiguous; creating the
735
+ // attribute from nothing is an insertion the anchor cannot express.
736
+ declared ? valueFix(
737
+ declared,
738
+ `${declared.value.trim()} ${rule.controller}`.trim(),
739
+ `Add "${rule.controller}" to data-controller`
740
+ ) : void 0
741
+ );
742
+ }
743
+ }
744
+ }
745
+ for (const { node, identifier } of scopes) {
746
+ const controller = known[identifier];
747
+ if (!controller) continue;
748
+ if (controller.targetDeclarations.length === 0) continue;
749
+ const targetAttrName = `data-${identifier}-target`;
750
+ for (const rule of controller.targetDeclarations) {
751
+ for (const el of declarationCandidates) {
752
+ const marker = el.attrs.find((a) => a.name === rule.attr);
753
+ if (!marker || isDynamicValue(marker)) continue;
754
+ const value = marker.value.trim();
755
+ if (value.length === 0) continue;
756
+ if (rule.values && !rule.values.includes(value)) continue;
757
+ if (findOwner(el, identifier) !== node) continue;
758
+ const declaredAs = el.attrs.find((a) => a.name === targetAttrName);
759
+ if (declaredAs && !isDynamicValue(declaredAs) && declaredAs.value.trim() === rule.target) {
760
+ continue;
761
+ }
762
+ if (declaredAs && isDynamicValue(declaredAs)) continue;
763
+ report(
764
+ el,
765
+ "undeclared-target",
766
+ "error",
767
+ `${rule.attr}="${value}" inside "${identifier}" is not declared as its "${rule.target}" target, so the controller never manages it.`,
768
+ marker,
769
+ rule.suggestion
770
+ );
771
+ }
772
+ }
773
+ }
774
+ for (const { node, identifier } of scopes) {
775
+ const controller = known[identifier];
776
+ if (!controller) continue;
777
+ for (const rule of controller.cardinality) {
778
+ if (conditionHolds(node, identifier, rule.when) !== true) continue;
779
+ const byName = targetNodes.get(node)?.get(identifier);
780
+ const counted = byName?.get(rule.target) ?? [];
781
+ const containers = rule.within === "" ? [node] : byName?.get(rule.within) ?? [];
782
+ const what = describeCounted(rule.target, rule.attr, rule.values);
783
+ const where = rule.within === "" ? "controller scope" : `"${rule.within}" target`;
784
+ for (const container of containers) {
785
+ const matched = [];
786
+ for (const el of counted) {
787
+ if (!isWithin(el, container)) continue;
788
+ if (rule.attr === void 0) {
789
+ matched.push({ el, at: el });
790
+ continue;
791
+ }
792
+ const attr = el.attrs.find((a) => a.name === rule.attr);
793
+ if (!attr) continue;
794
+ if (rule.values && (isDynamicValue(attr) || !rule.values.includes(attr.value.trim()))) {
795
+ continue;
796
+ }
797
+ matched.push({ el, at: attr });
798
+ }
799
+ if (rule.max !== void 0 && matched.length > rule.max) {
800
+ for (const { el, at } of matched.slice(rule.max)) {
801
+ report(
802
+ el,
803
+ "cardinality-violation",
804
+ "error",
805
+ `"${identifier}" allows at most ${rule.max} ${what} per ${where}, but found ${matched.length}.`,
806
+ at,
807
+ rule.suggestion
808
+ );
809
+ }
810
+ }
811
+ if (rule.min !== void 0 && matched.length < rule.min) {
812
+ report(
813
+ container,
814
+ "cardinality-violation",
815
+ "error",
816
+ `"${identifier}" requires at least ${rule.min} ${what} per ${where}, but found ${matched.length}.`,
817
+ container,
818
+ rule.suggestion
819
+ );
820
+ }
821
+ }
822
+ }
823
+ }
629
824
  const ids = /* @__PURE__ */ new Set();
630
825
  walk(tree, (node) => {
631
826
  const id = node.attrs.find((a) => a.name === "id");
@@ -663,7 +858,7 @@ function checkSource(source, manifest) {
663
858
  diagnostics.sort((a, b) => a.line - b.line || a.column - b.column);
664
859
  return diagnostics;
665
860
  }
666
- function checkA11y(report, el, identifier, req) {
861
+ function checkA11y(report, el, identifier, req, severity) {
667
862
  const groups = [
668
863
  { attrs: req.attrs, values: req.values },
669
864
  ...req.or ?? []
@@ -692,11 +887,12 @@ function checkA11y(report, el, identifier, req) {
692
887
  }
693
888
  }
694
889
  if (!anyPresent) {
890
+ const verb = severity === "warning" ? "recommends" : "requires";
695
891
  report(
696
892
  el,
697
893
  "missing-aria",
698
- "error",
699
- `"${identifier}" requires ${describeAttrs([...allowedByAttr.keys()])} on its ${where}.`,
894
+ severity,
895
+ `"${identifier}" ${verb} ${describeAttrs([...allowedByAttr.keys()])} on its ${where}.`,
700
896
  el,
701
897
  req.suggestion
702
898
  );
@@ -715,6 +911,19 @@ function checkA11y(report, el, identifier, req) {
715
911
  );
716
912
  }
717
913
  }
914
+ function isWithin(node, container) {
915
+ let current = node;
916
+ while (current) {
917
+ if (current === container) return true;
918
+ current = current.parent;
919
+ }
920
+ return false;
921
+ }
922
+ function describeCounted(target, attr, values) {
923
+ if (attr === void 0) return `"${target}" targets`;
924
+ const constrained = values && values.length > 0 ? ` set to ${list(values)}` : "";
925
+ return `"${target}" targets with ${attr}${constrained}`;
926
+ }
718
927
  var NATIVE_FOCUSABLE_TAGS = /* @__PURE__ */ new Set(["button", "select", "textarea", "summary"]);
719
928
  function isNativelyFocusable(el) {
720
929
  if (NATIVE_FOCUSABLE_TAGS.has(el.tag)) return true;
@@ -868,10 +1077,15 @@ var CONTROLLER_ARRAY_FIELDS = [
868
1077
  "actions",
869
1078
  "events",
870
1079
  "requiredTargets",
1080
+ "conditionalTargets",
871
1081
  "a11y",
872
1082
  "keyboard",
873
1083
  "managedAria",
874
- "compositions"
1084
+ "compositions",
1085
+ "companions",
1086
+ "targetDeclarations",
1087
+ "cardinality",
1088
+ "forbiddenAria"
875
1089
  ];
876
1090
  function isCompatibleManifest(value) {
877
1091
  if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
@@ -1297,6 +1511,41 @@ var EXAMPLE_RESULT_SCHEMA = {
1297
1511
  },
1298
1512
  required: ["id", "file", "source", "guidance"]
1299
1513
  };
1514
+ var CONTENT_CONDITION_SCHEMA = {
1515
+ type: "object",
1516
+ description: "Condition on the element's own contents arming the rule; absent means it does not depend on them. Evaluated per element, unlike the scope-level `when`.",
1517
+ properties: {
1518
+ target: { type: "string", description: "Target counted inside the element." },
1519
+ min: { type: "integer", description: "Arms the rule when the count is at least this." },
1520
+ max: { type: "integer", description: "Arms the rule when the count is at most this." }
1521
+ },
1522
+ required: ["target"]
1523
+ };
1524
+ var ELEMENT_CONDITION_SCHEMA = {
1525
+ type: "object",
1526
+ description: "Tags the rule is disarmed on because they name the role natively (a <table>'s <caption>, a <fieldset>'s <legend>, an <input>'s <label for>); absent means the rule applies to every spelling.",
1527
+ properties: {
1528
+ exceptTags: {
1529
+ type: "array",
1530
+ items: { type: "string" },
1531
+ description: "Lowercase tag names whose native naming path disarms the rule."
1532
+ }
1533
+ },
1534
+ required: ["exceptTags"]
1535
+ };
1536
+ var DOCUMENT_CONDITION_SCHEMA = {
1537
+ type: "object",
1538
+ description: "Condition on the whole file: how many elements in it carry a role. Backs ARIA's conditional name levels (a toolbar's name becomes required on the second toolbar). Counts per file, which under-approximates a page built from partials.",
1539
+ properties: {
1540
+ role: { type: "string", description: "Role counted across the file." },
1541
+ atLeast: { type: "integer", description: "Holds at this many or more." },
1542
+ focusable: {
1543
+ type: "boolean",
1544
+ description: "Counts only Tab-reachable carriers of the role; absent counts all. ARIA qualifies a separator's condition by focusability (a decorative hr cannot be landed on) but a toolbar's not at all."
1545
+ }
1546
+ },
1547
+ required: ["role", "atLeast"]
1548
+ };
1300
1549
  var CONTROLLER_CONTRACT_SCHEMA = {
1301
1550
  type: "object",
1302
1551
  properties: {
@@ -1309,6 +1558,19 @@ var CONTROLLER_CONTRACT_SCHEMA = {
1309
1558
  ...STRING_ARRAY,
1310
1559
  description: "Targets that must be present at least once inside the controller scope."
1311
1560
  },
1561
+ conditionalTargets: {
1562
+ type: "array",
1563
+ description: "Targets that become required only once another optional target appears \u2014 an opt-in feature that is incomplete without its whole set. Omitting one half passes every other check and silently does nothing.",
1564
+ items: {
1565
+ type: "object",
1566
+ properties: {
1567
+ whenPresent: { type: "string", description: "The optional target that turns it on." },
1568
+ require: { ...STRING_ARRAY, description: "Targets required once it appears." },
1569
+ suggestion: { type: "string" }
1570
+ },
1571
+ required: ["whenPresent", "require", "suggestion"]
1572
+ }
1573
+ },
1312
1574
  a11y: {
1313
1575
  type: "array",
1314
1576
  description: "Author-supplied ARIA the controller relies on but never sets itself.",
@@ -1330,6 +1592,25 @@ var CONTROLLER_CONTRACT_SCHEMA = {
1330
1592
  required: ["attrs"]
1331
1593
  }
1332
1594
  },
1595
+ when: {
1596
+ type: "object",
1597
+ description: "Own-value condition arming the requirement; absent means unconditional. Outside the named configuration the ARIA is not optional but wrong.",
1598
+ properties: {
1599
+ value: { type: "string" },
1600
+ equals: STRING_ARRAY,
1601
+ default: { type: "string" }
1602
+ },
1603
+ required: ["value", "equals", "default"]
1604
+ },
1605
+ whenContains: CONTENT_CONDITION_SCHEMA,
1606
+ whenElement: ELEMENT_CONDITION_SCHEMA,
1607
+ whenDocument: DOCUMENT_CONDITION_SCHEMA,
1608
+ severity: {
1609
+ type: "string",
1610
+ enum: ["error", "warning"],
1611
+ description: 'Level of an unmet requirement; absent means "error". ARIA recommends some names rather than requiring them, and those report as warnings.'
1612
+ },
1613
+ escalateWhen: DOCUMENT_CONDITION_SCHEMA,
1333
1614
  suggestion: { type: "string" }
1334
1615
  },
1335
1616
  required: ["target", "attrs", "suggestion"]
@@ -1404,6 +1685,84 @@ var CONTROLLER_CONTRACT_SCHEMA = {
1404
1685
  },
1405
1686
  required: ["target", "coController", "require", "suggestion"]
1406
1687
  }
1688
+ },
1689
+ companions: {
1690
+ type: "array",
1691
+ description: "Controllers the named target must ALSO declare. Unlike compositions (which only check an already co-located companion), these report the companion being missing.",
1692
+ items: {
1693
+ type: "object",
1694
+ properties: {
1695
+ target: {
1696
+ type: "string",
1697
+ description: 'Target that must declare it; "" means the data-controller element.'
1698
+ },
1699
+ controller: { type: "string", description: "Required companion identifier." },
1700
+ suggestion: { type: "string" }
1701
+ },
1702
+ required: ["target", "controller", "suggestion"]
1703
+ }
1704
+ },
1705
+ targetDeclarations: {
1706
+ type: "array",
1707
+ description: "Reverse direction: elements in scope carrying the attribute must be declared as the named target, or the controller never manages them.",
1708
+ items: {
1709
+ type: "object",
1710
+ properties: {
1711
+ attr: { type: "string", description: 'Marking attribute, e.g. "role".' },
1712
+ values: { ...STRING_ARRAY, description: "Marking values; absent = any value." },
1713
+ target: {
1714
+ type: "string",
1715
+ description: "Target the matched element must be declared as."
1716
+ },
1717
+ suggestion: { type: "string" }
1718
+ },
1719
+ required: ["attr", "target", "suggestion"]
1720
+ }
1721
+ },
1722
+ cardinality: {
1723
+ type: "array",
1724
+ description: "Set-level count bounds: how many of the named target may or must exist per scope or per container target, optionally narrowed to elements carrying an attribute.",
1725
+ items: {
1726
+ type: "object",
1727
+ properties: {
1728
+ within: {
1729
+ type: "string",
1730
+ description: 'Container target bounding the count; "" means the whole scope.'
1731
+ },
1732
+ target: { type: "string", description: "Target name being counted." },
1733
+ attr: { type: "string", description: "Count only elements carrying this attribute." },
1734
+ values: { ...STRING_ARRAY, description: "Restrict the attribute to these values." },
1735
+ min: { type: "integer", description: "Smallest permitted count (inclusive)." },
1736
+ max: { type: "integer", description: "Largest permitted count (inclusive)." },
1737
+ when: {
1738
+ type: "object",
1739
+ description: "Own-value condition arming the bound; absent means unconditional.",
1740
+ properties: {
1741
+ value: { type: "string" },
1742
+ equals: STRING_ARRAY,
1743
+ default: { type: "string" }
1744
+ },
1745
+ required: ["value", "equals", "default"]
1746
+ },
1747
+ suggestion: { type: "string" }
1748
+ },
1749
+ required: ["within", "target", "suggestion"]
1750
+ }
1751
+ },
1752
+ forbiddenAria: {
1753
+ type: "array",
1754
+ description: "ARIA the author owns that the surrounding markup contradicts (e.g. a menu still declaring itself busy once its items are present). Reported as a warning \u2014 unlike managedAria, the attribute is the author's and is required in the other configuration.",
1755
+ items: {
1756
+ type: "object",
1757
+ properties: {
1758
+ target: { type: "string" },
1759
+ attrs: STRING_ARRAY,
1760
+ values: { ...STRING_ARRAY, description: "Forbidden values; absent = any value." },
1761
+ whenContains: CONTENT_CONDITION_SCHEMA,
1762
+ suggestion: { type: "string" }
1763
+ },
1764
+ required: ["target", "attrs", "suggestion"]
1765
+ }
1407
1766
  }
1408
1767
  },
1409
1768
  required: [
@@ -1413,10 +1772,15 @@ var CONTROLLER_CONTRACT_SCHEMA = {
1413
1772
  "actions",
1414
1773
  "events",
1415
1774
  "requiredTargets",
1775
+ "conditionalTargets",
1416
1776
  "a11y",
1417
1777
  "keyboard",
1418
1778
  "managedAria",
1419
- "compositions"
1779
+ "compositions",
1780
+ "companions",
1781
+ "targetDeclarations",
1782
+ "cardinality",
1783
+ "forbiddenAria"
1420
1784
  ]
1421
1785
  };
1422
1786
  var InvalidToolArgsError = class extends Error {
@@ -1429,7 +1793,7 @@ var TOOL_DESCRIPTORS = [
1429
1793
  {
1430
1794
  name: "stimeo_check",
1431
1795
  title: "Check Stimeo UI markup",
1432
- description: "Statically check HTML or ERB source that uses Stimeo UI (stimeo--*) controllers. Returns a CheckReport with diagnostics \u2014 unknown controllers/targets/values/action methods, missing required targets, missing or invalid author-supplied ARIA, keyboard-focusability prerequisites, and misaligned cross-controller composition values \u2014 each with a fix suggestion when available. Call this on generated markup before finalizing it; ok=true means no errors.",
1796
+ description: "Statically check HTML or ERB source that uses Stimeo UI (stimeo--*) controllers \u2014 the headless, accessible Stimulus UI component library for Rails. Returns a CheckReport with diagnostics \u2014 unknown controllers/targets/values/action methods, missing required targets, missing or invalid author-supplied ARIA, keyboard-focusability prerequisites, and misaligned cross-controller composition values \u2014 each with a fix suggestion when available. Call this to validate any Rails view, ERB template, or HTML you write or edit with these components, before finalizing it; ok=true means no errors.",
1433
1797
  annotations: READ_ONLY_ANNOTATIONS,
1434
1798
  inputSchema: {
1435
1799
  type: "object",
@@ -1451,7 +1815,7 @@ var TOOL_DESCRIPTORS = [
1451
1815
  {
1452
1816
  name: "stimeo_catalog",
1453
1817
  title: "List Stimeo UI controllers",
1454
- description: "List every Stimeo UI controller with its targets, values, actions, and events. Use this to discover which stimeo--* controllers exist and the data-* API each accepts before writing markup.",
1818
+ description: "List every Stimeo UI controller with its targets, values, actions, and events. Stimeo UI is a headless, accessible (WAI-ARIA APG) Stimulus UI component library for Rails: dialog/modal, drawer, dropdown menu, tabs, accordion, combobox, listbox, tooltip, popover, toast, carousel, data grid, date picker, tree view, and many more. Start here to find which stimeo--* controller implements a requested component and the data-* API it accepts before writing markup.",
1455
1819
  annotations: READ_ONLY_ANNOTATIONS,
1456
1820
  inputSchema: {
1457
1821
  type: "object",
@@ -1463,7 +1827,7 @@ var TOOL_DESCRIPTORS = [
1463
1827
  {
1464
1828
  name: "stimeo_controller",
1465
1829
  title: "Get one controller's usage contract",
1466
- description: 'Fetch the full usage contract of one Stimeo UI controller by identifier (e.g. "stimeo--tabs"): targets, values, actions, events, required targets, and the accessibility contract \u2014 author-supplied ARIA requirements, keyboard-focusability prerequisites, the ARIA attributes the controller manages at runtime (which the author must NOT hardcode), and cross-controller composition value alignments.',
1830
+ description: 'Fetch the full usage contract of one Stimeo UI controller by identifier (e.g. "stimeo--tabs"): targets, values, actions, events, required targets, and the accessibility contract \u2014 author-supplied ARIA requirements, keyboard-focusability prerequisites, the ARIA attributes the controller manages at runtime (which the author must NOT hardcode), and cross-controller composition value alignments. Use it after stimeo_catalog to write correct, accessible Rails ERB or HTML markup for that Stimulus component.',
1467
1831
  annotations: READ_ONLY_ANNOTATIONS,
1468
1832
  inputSchema: {
1469
1833
  type: "object",
@@ -1481,7 +1845,7 @@ var TOOL_DESCRIPTORS = [
1481
1845
  {
1482
1846
  name: "stimeo_example",
1483
1847
  title: "Get one controller's verified example markup",
1484
- description: 'Fetch verified example markup for one Stimeo UI controller by identifier (e.g. "stimeo--menu"). The example is the official catalog demo \u2014 it passes stimeo_check against the bundled manifest, so use it as the reference for correct structure, required targets, and author-supplied ARIA. class attributes in it are demo styling placeholders to replace; the stimeo--* data attributes and role/aria-* attributes are the contract to keep.',
1848
+ description: 'Fetch verified example markup for one Stimeo UI controller by identifier (e.g. "stimeo--menu"). The example is the official catalog demo \u2014 it passes stimeo_check against the bundled manifest, so use it as the reference for correct structure, required targets, and author-supplied ARIA. class attributes in it are demo styling placeholders to replace; the stimeo--* data attributes and role/aria-* attributes are the contract to keep. Copy it as the starting point when adding that accessible Stimulus component to a Rails ERB view.',
1485
1849
  annotations: READ_ONLY_ANNOTATIONS,
1486
1850
  inputSchema: {
1487
1851
  type: "object",
@@ -1693,7 +2057,13 @@ var McpSession = class {
1693
2057
  title: "Stimeo UI Inspector",
1694
2058
  version: this.#context.manifest.packageVersion
1695
2059
  },
1696
- instructions: "Stimeo UI is a headless Stimulus UI framework for Rails. Use stimeo_catalog to discover controllers, stimeo_controller for one controller's usage and accessibility contract, stimeo_example for verified reference markup, and stimeo_check to validate generated HTML/ERB before presenting it. The full manifest and every example are also exposed as resources (stimeo:// URIs), and the stimeo_build_ui / stimeo_fix_markup prompts bake this workflow into user-invokable templates."
2060
+ // The server's own account of itself. Per the MCP schema this is a hint
2061
+ // a client MAY put in front of the model, which makes it the earliest
2062
+ // point this server can be routed to — clients that defer tool
2063
+ // definitions still tend to load it alongside the tool names. Hence the
2064
+ // trigger condition first, in the words a request would use, and a
2065
+ // length that survives truncation.
2066
+ instructions: "Search for these tools whenever a task involves building, editing, or reviewing an accessible UI component in a Rails view \u2014 dialog/modal, drawer, dropdown menu, tabs, accordion, combobox, toast, and the rest of the catalog \u2014 or whenever HTML/ERB carrying stimeo--* attributes is in play. Stimeo UI is a headless Stimulus UI framework for Rails. Use stimeo_catalog to discover controllers, stimeo_controller for one controller's usage and accessibility contract, stimeo_example for verified reference markup, and stimeo_check to validate generated HTML/ERB before presenting it. The full manifest and every example are also exposed as resources (stimeo:// URIs), and the stimeo_build_ui / stimeo_fix_markup prompts bake this workflow into user-invokable templates."
1697
2067
  });
1698
2068
  }
1699
2069
  #getPrompt(id, params) {
@@ -1807,40 +2177,41 @@ async function runMcpServer(io) {
1807
2177
 
1808
2178
  // src/inspector/cli_bin.ts
1809
2179
  var argv = process2.argv.slice(2);
2180
+ process2.stdout.on("error", (error) => {
2181
+ process2.exit(error.code === "EPIPE" ? 0 : 1);
2182
+ });
1810
2183
  if (argv[0] === "mcp") {
1811
2184
  if (argv.includes("--help") || argv.includes("-h")) {
1812
- process2.exit(runCli(["--help"]));
1813
- }
1814
- process2.stdout.on("error", (error) => {
1815
- process2.exit(error.code === "EPIPE" ? 0 : 1);
1816
- });
1817
- runMcpServer({
1818
- input: process2.stdin,
1819
- write: (line) => {
1820
- process2.stdout.write(`${line}
2185
+ process2.exitCode = runCli(["--help"]);
2186
+ } else {
2187
+ runMcpServer({
2188
+ input: process2.stdin,
2189
+ write: (line) => {
2190
+ process2.stdout.write(`${line}
1821
2191
  `);
1822
- },
1823
- load: () => ({ manifest: loadManifest(), examples: loadExamplesIndex() }),
1824
- logError: (message) => {
1825
- process2.stderr.write(`stimeo mcp: ${message}
2192
+ },
2193
+ load: () => ({ manifest: loadManifest(), examples: loadExamplesIndex() }),
2194
+ logError: (message) => {
2195
+ process2.stderr.write(`stimeo mcp: ${message}
1826
2196
  `);
1827
- }
1828
- }).then(
1829
- // Set the exit code instead of calling process.exit so any buffered
1830
- // stdout responses are flushed before the process ends.
1831
- () => {
1832
- process2.exitCode = 0;
1833
- },
1834
- (error) => {
1835
- process2.stderr.write(
1836
- `stimeo mcp: ${error instanceof Error ? error.message : String(error)}
2197
+ }
2198
+ }).then(
2199
+ // Set the exit code instead of calling process.exit so any buffered
2200
+ // stdout responses are flushed before the process ends.
2201
+ () => {
2202
+ process2.exitCode = 0;
2203
+ },
2204
+ (error) => {
2205
+ process2.stderr.write(
2206
+ `stimeo mcp: ${error instanceof Error ? error.message : String(error)}
1837
2207
  `
1838
- );
1839
- process2.exitCode = 1;
1840
- }
1841
- );
2208
+ );
2209
+ process2.exitCode = 1;
2210
+ }
2211
+ );
2212
+ }
1842
2213
  } else {
1843
- process2.exit(runCli(argv));
2214
+ process2.exitCode = runCli(argv);
1844
2215
  }
1845
2216
  //# sourceMappingURL=cli_bin.js.map
1846
2217
  //# sourceMappingURL=cli_bin.js.map