stimeo-ui 0.6.0 → 0.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.
Files changed (212) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/dist/controllers/alert_dialog_controller.d.ts +2 -0
  3. package/dist/controllers/alert_dialog_controller.js +75 -4
  4. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  5. package/dist/controllers/auto_submit_controller.d.ts +16 -3
  6. package/dist/controllers/auto_submit_controller.js +94 -16
  7. package/dist/controllers/auto_submit_controller.js.map +1 -1
  8. package/dist/controllers/avatar_controller.js +47 -5
  9. package/dist/controllers/avatar_controller.js.map +1 -1
  10. package/dist/controllers/breadcrumb_controller.d.ts +2 -0
  11. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  12. package/dist/controllers/bulk_select_controller.d.ts +2 -0
  13. package/dist/controllers/bulk_select_controller.js.map +1 -1
  14. package/dist/controllers/calendar_controller.d.ts +2 -0
  15. package/dist/controllers/calendar_controller.js.map +1 -1
  16. package/dist/controllers/carousel_controller.d.ts +81 -47
  17. package/dist/controllers/carousel_controller.js +451 -100
  18. package/dist/controllers/carousel_controller.js.map +1 -1
  19. package/dist/controllers/character_counter_controller.d.ts +52 -20
  20. package/dist/controllers/character_counter_controller.js +338 -63
  21. package/dist/controllers/character_counter_controller.js.map +1 -1
  22. package/dist/controllers/checkbox_controller.d.ts +3 -0
  23. package/dist/controllers/checkbox_controller.js.map +1 -1
  24. package/dist/controllers/color_picker_controller.d.ts +3 -0
  25. package/dist/controllers/color_picker_controller.js.map +1 -1
  26. package/dist/controllers/combobox_controller.d.ts +2 -0
  27. package/dist/controllers/combobox_controller.js.map +1 -1
  28. package/dist/controllers/command_palette_controller.d.ts +2 -0
  29. package/dist/controllers/command_palette_controller.js +75 -4
  30. package/dist/controllers/command_palette_controller.js.map +1 -1
  31. package/dist/controllers/conditional_fields_controller.d.ts +42 -14
  32. package/dist/controllers/conditional_fields_controller.js +345 -51
  33. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  34. package/dist/controllers/confirm_controller.d.ts +2 -0
  35. package/dist/controllers/confirm_controller.js +75 -4
  36. package/dist/controllers/confirm_controller.js.map +1 -1
  37. package/dist/controllers/count_up_controller.d.ts +2 -0
  38. package/dist/controllers/count_up_controller.js.map +1 -1
  39. package/dist/controllers/countdown_controller.d.ts +4 -0
  40. package/dist/controllers/countdown_controller.js.map +1 -1
  41. package/dist/controllers/currency_input_controller.d.ts +40 -4
  42. package/dist/controllers/currency_input_controller.js +305 -74
  43. package/dist/controllers/currency_input_controller.js.map +1 -1
  44. package/dist/controllers/data_grid_controller.d.ts +3 -0
  45. package/dist/controllers/data_grid_controller.js.map +1 -1
  46. package/dist/controllers/dialog_controller.js +75 -4
  47. package/dist/controllers/dialog_controller.js.map +1 -1
  48. package/dist/controllers/direct_upload_controller.d.ts +59 -25
  49. package/dist/controllers/direct_upload_controller.js +212 -46
  50. package/dist/controllers/direct_upload_controller.js.map +1 -1
  51. package/dist/controllers/dirty_form_controller.d.ts +14 -6
  52. package/dist/controllers/dirty_form_controller.js +192 -29
  53. package/dist/controllers/dirty_form_controller.js.map +1 -1
  54. package/dist/controllers/dismissible_controller.d.ts +2 -0
  55. package/dist/controllers/dismissible_controller.js +83 -18
  56. package/dist/controllers/dismissible_controller.js.map +1 -1
  57. package/dist/controllers/drawer_controller.js +75 -4
  58. package/dist/controllers/drawer_controller.js.map +1 -1
  59. package/dist/controllers/empty_state_controller.d.ts +2 -0
  60. package/dist/controllers/empty_state_controller.js.map +1 -1
  61. package/dist/controllers/file_dropzone_controller.d.ts +123 -27
  62. package/dist/controllers/file_dropzone_controller.js +386 -63
  63. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  64. package/dist/controllers/filter_controller.d.ts +2 -0
  65. package/dist/controllers/filter_controller.js.map +1 -1
  66. package/dist/controllers/flash_controller.d.ts +7 -1
  67. package/dist/controllers/flash_controller.js +3 -1
  68. package/dist/controllers/flash_controller.js.map +1 -1
  69. package/dist/controllers/focus_controller.d.ts +2 -0
  70. package/dist/controllers/focus_controller.js +75 -4
  71. package/dist/controllers/focus_controller.js.map +1 -1
  72. package/dist/controllers/form_field_controller.d.ts +50 -10
  73. package/dist/controllers/form_field_controller.js +280 -62
  74. package/dist/controllers/form_field_controller.js.map +1 -1
  75. package/dist/controllers/form_validation_controller.d.ts +10 -8
  76. package/dist/controllers/form_validation_controller.js +208 -83
  77. package/dist/controllers/form_validation_controller.js.map +1 -1
  78. package/dist/controllers/frame_loading_controller.d.ts +5 -1
  79. package/dist/controllers/frame_loading_controller.js +2 -1
  80. package/dist/controllers/frame_loading_controller.js.map +1 -1
  81. package/dist/controllers/highlight_controller.d.ts +2 -0
  82. package/dist/controllers/highlight_controller.js.map +1 -1
  83. package/dist/controllers/idle_controller.d.ts +2 -0
  84. package/dist/controllers/idle_controller.js.map +1 -1
  85. package/dist/controllers/input_mask_controller.d.ts +43 -15
  86. package/dist/controllers/input_mask_controller.js +251 -76
  87. package/dist/controllers/input_mask_controller.js.map +1 -1
  88. package/dist/controllers/lazy_frame_controller.d.ts +2 -0
  89. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  90. package/dist/controllers/listbox_controller.d.ts +2 -0
  91. package/dist/controllers/listbox_controller.js.map +1 -1
  92. package/dist/controllers/local_time_controller.d.ts +2 -0
  93. package/dist/controllers/local_time_controller.js.map +1 -1
  94. package/dist/controllers/masonry_controller.d.ts +2 -0
  95. package/dist/controllers/masonry_controller.js.map +1 -1
  96. package/dist/controllers/meter_controller.d.ts +2 -0
  97. package/dist/controllers/meter_controller.js.map +1 -1
  98. package/dist/controllers/multi_select_controller.d.ts +2 -0
  99. package/dist/controllers/multi_select_controller.js.map +1 -1
  100. package/dist/controllers/nested_form_controller.d.ts +52 -13
  101. package/dist/controllers/nested_form_controller.js +450 -42
  102. package/dist/controllers/nested_form_controller.js.map +1 -1
  103. package/dist/controllers/network_status_controller.d.ts +2 -0
  104. package/dist/controllers/network_status_controller.js.map +1 -1
  105. package/dist/controllers/number_input_controller.d.ts +2 -0
  106. package/dist/controllers/number_input_controller.js +47 -5
  107. package/dist/controllers/number_input_controller.js.map +1 -1
  108. package/dist/controllers/otp_controller.d.ts +62 -23
  109. package/dist/controllers/otp_controller.js +472 -114
  110. package/dist/controllers/otp_controller.js.map +1 -1
  111. package/dist/controllers/overflow_indicator_controller.d.ts +2 -0
  112. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  113. package/dist/controllers/overflow_menu_controller.d.ts +2 -0
  114. package/dist/controllers/overflow_menu_controller.js +2 -1
  115. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  116. package/dist/controllers/pagination_controller.d.ts +2 -0
  117. package/dist/controllers/pagination_controller.js +2 -1
  118. package/dist/controllers/pagination_controller.js.map +1 -1
  119. package/dist/controllers/password_reveal_controller.d.ts +2 -0
  120. package/dist/controllers/password_reveal_controller.js.map +1 -1
  121. package/dist/controllers/password_strength_controller.d.ts +2 -0
  122. package/dist/controllers/password_strength_controller.js.map +1 -1
  123. package/dist/controllers/persist_controller.d.ts +30 -16
  124. package/dist/controllers/persist_controller.js +432 -122
  125. package/dist/controllers/persist_controller.js.map +1 -1
  126. package/dist/controllers/pointer_drag_controller.d.ts +3 -0
  127. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  128. package/dist/controllers/popover_controller.js +77 -4
  129. package/dist/controllers/popover_controller.js.map +1 -1
  130. package/dist/controllers/portal_controller.d.ts +2 -0
  131. package/dist/controllers/portal_controller.js.map +1 -1
  132. package/dist/controllers/preview_guard_controller.d.ts +2 -0
  133. package/dist/controllers/preview_guard_controller.js.map +1 -1
  134. package/dist/controllers/progress_controller.d.ts +3 -0
  135. package/dist/controllers/progress_controller.js.map +1 -1
  136. package/dist/controllers/radio_group_controller.d.ts +3 -0
  137. package/dist/controllers/radio_group_controller.js.map +1 -1
  138. package/dist/controllers/rating_controller.d.ts +2 -0
  139. package/dist/controllers/rating_controller.js +9 -5
  140. package/dist/controllers/rating_controller.js.map +1 -1
  141. package/dist/controllers/reading_progress_controller.d.ts +2 -0
  142. package/dist/controllers/reading_progress_controller.js.map +1 -1
  143. package/dist/controllers/resizable_controller.d.ts +2 -0
  144. package/dist/controllers/resizable_controller.js.map +1 -1
  145. package/dist/controllers/roving_controller.d.ts +2 -0
  146. package/dist/controllers/roving_controller.js.map +1 -1
  147. package/dist/controllers/scroll_area_controller.d.ts +23 -15
  148. package/dist/controllers/scroll_area_controller.js +462 -162
  149. package/dist/controllers/scroll_area_controller.js.map +1 -1
  150. package/dist/controllers/scroll_visibility_controller.d.ts +2 -0
  151. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  152. package/dist/controllers/scrollspy_controller.d.ts +2 -0
  153. package/dist/controllers/scrollspy_controller.js.map +1 -1
  154. package/dist/controllers/separator_controller.d.ts +54 -11
  155. package/dist/controllers/separator_controller.js +354 -38
  156. package/dist/controllers/separator_controller.js.map +1 -1
  157. package/dist/controllers/sidebar_controller.js +83 -10
  158. package/dist/controllers/sidebar_controller.js.map +1 -1
  159. package/dist/controllers/skeleton_controller.d.ts +2 -0
  160. package/dist/controllers/skeleton_controller.js.map +1 -1
  161. package/dist/controllers/slider_controller.d.ts +2 -0
  162. package/dist/controllers/slider_controller.js.map +1 -1
  163. package/dist/controllers/smart_sticky_header_controller.d.ts +2 -0
  164. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  165. package/dist/controllers/spinner_controller.d.ts +4 -1
  166. package/dist/controllers/spinner_controller.js +7 -5
  167. package/dist/controllers/spinner_controller.js.map +1 -1
  168. package/dist/controllers/step_indicator_controller.d.ts +2 -0
  169. package/dist/controllers/step_indicator_controller.js.map +1 -1
  170. package/dist/controllers/stepper_controller.d.ts +2 -0
  171. package/dist/controllers/stepper_controller.js.map +1 -1
  172. package/dist/controllers/stick_to_bottom_controller.d.ts +2 -0
  173. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  174. package/dist/controllers/sticky_observer_controller.d.ts +2 -0
  175. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  176. package/dist/controllers/submit_once_controller.d.ts +98 -39
  177. package/dist/controllers/submit_once_controller.js +402 -122
  178. package/dist/controllers/submit_once_controller.js.map +1 -1
  179. package/dist/controllers/switch_controller.d.ts +2 -0
  180. package/dist/controllers/switch_controller.js.map +1 -1
  181. package/dist/controllers/tags_input_controller.d.ts +2 -0
  182. package/dist/controllers/tags_input_controller.js.map +1 -1
  183. package/dist/controllers/textarea_autosize_controller.d.ts +31 -9
  184. package/dist/controllers/textarea_autosize_controller.js +131 -3
  185. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  186. package/dist/controllers/theme_controller.d.ts +2 -0
  187. package/dist/controllers/theme_controller.js +8 -6
  188. package/dist/controllers/theme_controller.js.map +1 -1
  189. package/dist/controllers/time_picker_controller.d.ts +2 -0
  190. package/dist/controllers/time_picker_controller.js.map +1 -1
  191. package/dist/controllers/toast_controller.d.ts +4 -0
  192. package/dist/controllers/toast_controller.js.map +1 -1
  193. package/dist/controllers/toggle_group_controller.d.ts +2 -0
  194. package/dist/controllers/toggle_group_controller.js.map +1 -1
  195. package/dist/controllers/toolbar_controller.js.map +1 -1
  196. package/dist/controllers/transition_controller.d.ts +2 -0
  197. package/dist/controllers/transition_controller.js.map +1 -1
  198. package/dist/controllers/tree_view_controller.d.ts +2 -0
  199. package/dist/controllers/tree_view_controller.js +2 -1
  200. package/dist/controllers/tree_view_controller.js.map +1 -1
  201. package/dist/index.js +4293 -1447
  202. package/dist/index.js.map +1 -1
  203. package/dist/inspector/cli.d.ts +115 -4
  204. package/dist/inspector/cli.js +195 -15
  205. package/dist/inspector/cli.js.map +1 -1
  206. package/dist/inspector/cli_bin.js +257 -47
  207. package/dist/inspector/cli_bin.js.map +1 -1
  208. package/dist/inspector/examples.json +11 -11
  209. package/dist/inspector/manifest.json +615 -66
  210. package/dist/positioning/index.d.ts +2 -0
  211. package/dist/positioning/index.js.map +1 -1
  212. package/package.json +2 -2
@@ -48,6 +48,16 @@ interface ControllerManifest {
48
48
  * {@link ConditionalTargetRule}.
49
49
  */
50
50
  readonly conditionalTargets: readonly ConditionalTargetRule[];
51
+ /**
52
+ * Actions required on a particular element in one Value configuration. See
53
+ * {@link RequiredActionRule}.
54
+ */
55
+ readonly requiredActions: readonly RequiredActionRule[];
56
+ /**
57
+ * States an action opens that the consumer's wiring has to close. See
58
+ * {@link ActionCompletionRule}.
59
+ */
60
+ readonly actionCompletion: readonly ActionCompletionRule[];
51
61
  /**
52
62
  * Accessibility requirements the *consumer's markup* must satisfy (stage 3):
53
63
  * ARIA attributes the controller does **not** set at runtime and therefore
@@ -140,6 +150,8 @@ interface ControllerManifest {
140
150
  interface ValueCondition {
141
151
  /** Value name (camelCase, as declared in `static values`). */
142
152
  readonly value: string;
153
+ /** Stimulus decoder used before comparing the effective value. */
154
+ readonly type: "boolean" | "string";
143
155
  /** Effective values that arm the rule. */
144
156
  readonly equals: readonly string[];
145
157
  /** The value's declared default, used when the attribute is absent. */
@@ -157,12 +169,19 @@ interface ValueCondition {
157
169
  * emptiness alone, since the controller cannot tell "still loading" from
158
170
  * "nothing to show" and therefore never infers it.
159
171
  *
172
+ * `within: "scope"` widens the count to the whole controller scope, for the
173
+ * contracts where a *sibling* decides the requirement: a carousel's slides are
174
+ * `tabpanel`s when the carousel has pickers to control them and `group`s when it
175
+ * has none, and no slide ever holds a picker of its own.
176
+ *
160
177
  * Bounds are inclusive and independently optional; a condition with neither is
161
178
  * meaningless and rejected by the manifest tests.
162
179
  */
163
180
  interface ContentCondition {
164
- /** Target name counted **inside** the element the rule applies to. */
181
+ /** Target name counted inside the counting container. */
165
182
  readonly target: string;
183
+ /** Where to count: the element the rule applies to, or the controller scope. */
184
+ readonly within?: "element" | "scope";
166
185
  /** Arms the rule when the count is at least this. */
167
186
  readonly min?: number;
168
187
  /** Arms the rule when the count is at most this. */
@@ -608,6 +627,85 @@ interface ConditionalTargetRule {
608
627
  * controller then refuses to build from.
609
628
  */
610
629
  readonly requireInside?: boolean;
630
+ /**
631
+ * Require each {@link require} entry to share its nearest enclosing host —
632
+ * the closest ancestor matching one of these selectors — with the
633
+ * `whenPresent` element.
634
+ *
635
+ * Neither presence nor {@link requireInside} can express a pair that must sit
636
+ * in the *same* container without being nested in one another. A controller
637
+ * that swaps one host's two halves reads them per host, so halves split
638
+ * across two hosts leave each host with an incomplete pair, and the swap
639
+ * silently does nothing on markup where both names are present.
640
+ *
641
+ * Only a pair whose halves both resolve to a host is judged. A template
642
+ * helper that emits the host leaves nothing in the parsed markup, so an
643
+ * element with no host above it is indistinguishable from one whose host is
644
+ * generated — reporting either would be a guess, and the check stays quiet.
645
+ */
646
+ readonly requireSameHost?: readonly HostSelector[];
647
+ /**
648
+ * What to call the shared host in the diagnostic (e.g. `submit control`).
649
+ * The selectors' tag names read as a disjunction of element names, which
650
+ * names the mechanism rather than the thing the author is looking at.
651
+ */
652
+ readonly hostLabel?: string;
653
+ /** Human-readable fix suggestion shown by the CLI (stage 4). */
654
+ readonly suggestion: string;
655
+ }
656
+ /**
657
+ * One element shape that can act as a host for {@link
658
+ * ConditionalTargetRule.requireSameHost}.
659
+ *
660
+ * A tag alone is too coarse where the element's `type` decides its role, so an
661
+ * optional attribute filter narrows it. Omitting {@link attr} accepts the tag
662
+ * however it is spelled.
663
+ */
664
+ interface HostSelector {
665
+ /** Lowercased tag name. */
666
+ readonly tag: string;
667
+ /** Attribute that must carry one of {@link values} for the tag to qualify. */
668
+ readonly attr?: string;
669
+ /** Accepted lowercased values of {@link attr}. */
670
+ readonly values?: readonly string[];
671
+ }
672
+ /**
673
+ * An action that puts a controller into a state it cannot leave on its own.
674
+ *
675
+ * A controller whose busy state is opened declaratively has no way to know the
676
+ * consumer never wired the matching exit: nothing throws, the markup is
677
+ * well-formed, and the only symptom is a control that stays disabled for the
678
+ * rest of the page's life. The check reads the wiring the author did declare
679
+ * and says so before it ships.
680
+ */
681
+ interface ActionCompletionRule {
682
+ /** Action that opens the state. */
683
+ readonly opens: string;
684
+ /** Any one of these actions closes it again. */
685
+ readonly closedBy: readonly string[];
686
+ /** Event types for {@link opens} that the controller cannot complete itself. */
687
+ readonly whenTriggeredBy: readonly string[];
688
+ /** Value whose non-zero number supplies an automatic exit instead. */
689
+ readonly escapeValue?: string;
690
+ /** Human-readable fix suggestion shown by the CLI (stage 4). */
691
+ readonly suggestion: string;
692
+ }
693
+ /**
694
+ * An action binding that must exist for one configured behavior to be usable.
695
+ *
696
+ * Declaring an action method on the controller only proves the method exists;
697
+ * Stimulus still needs the consumer to wire it. Optional interactive variants
698
+ * can otherwise enter the Tab order while every key silently does nothing.
699
+ */
700
+ interface RequiredActionRule {
701
+ /** Target whose element must carry the action; `""` means the scope element. */
702
+ readonly target: string;
703
+ /** Public controller action method that must be wired. */
704
+ readonly action: string;
705
+ /** Event types that make the binding usable for this contract. */
706
+ readonly eventTypes: readonly string[];
707
+ /** Configuration in which the binding becomes mandatory. */
708
+ readonly when?: ValueCondition;
611
709
  /** Human-readable fix suggestion shown by the CLI (stage 4). */
612
710
  readonly suggestion: string;
613
711
  }
@@ -618,10 +716,10 @@ interface ConditionalTargetRule {
618
716
  * ""))`) before applying these bounds. ERB-generated values are undecidable and
619
717
  * skipped rather than guessed.
620
718
  */
621
- interface ValueConstraint {
719
+ interface NumericValueConstraint {
622
720
  /** Value name (camelCase, as declared in `static values`). */
623
721
  readonly value: string;
624
- /** Decoder family. Numeric bounds are the first supported semantic family. */
722
+ /** Decoder family. */
625
723
  readonly type: "number";
626
724
  /** Reject `NaN` and infinities when true. */
627
725
  readonly finite?: boolean;
@@ -632,6 +730,19 @@ interface ValueConstraint {
632
730
  /** Human-readable fix suggestion shown by the CLI (stage 4). */
633
731
  readonly suggestion: string;
634
732
  }
733
+ /** A statically checkable finite set of accepted String Value literals. */
734
+ interface StringValueConstraint {
735
+ /** Value name (camelCase, as declared in `static values`). */
736
+ readonly value: string;
737
+ /** Decoder family. */
738
+ readonly type: "string";
739
+ /** Exact authored literals accepted by the public contract. */
740
+ readonly allowedValues: readonly string[];
741
+ /** Human-readable fix suggestion shown by the CLI (stage 4). */
742
+ readonly suggestion: string;
743
+ }
744
+ /** A statically checkable semantic contract on one declared Stimulus Value. */
745
+ type ValueConstraint = NumericValueConstraint | StringValueConstraint;
635
746
  /**
636
747
  * A statically checkable relationship between two numeric Stimulus Values.
637
748
  *
@@ -659,7 +770,7 @@ type DiagnosticSeverity = "error" | "warning";
659
770
  * for testing, machine-readable output, and `data-stimeo-ignore` suppression
660
771
  * lists (which is why the set also exists as a runtime array).
661
772
  */
662
- declare const DIAGNOSTIC_CODES: readonly ["unknown-controller", "unknown-target", "unknown-value", "invalid-value", "unknown-action-controller", "unknown-action-method", "orphan-target", "missing-required-target", "missing-conditional-target", "missing-aria", "invalid-aria-value", "keyboard-inaccessible", "invalid-host", "unresolved-idref", "managed-aria", "composition-mismatch", "missing-companion", "undeclared-target", "cardinality-violation", "forbidden-aria", "unknown-ignore-code"];
773
+ declare const DIAGNOSTIC_CODES: readonly ["unknown-controller", "unknown-target", "unknown-value", "invalid-value", "unknown-action-controller", "unknown-action-method", "orphan-target", "missing-required-target", "missing-conditional-target", "missing-required-action", "missing-action-completion", "missing-aria", "invalid-aria-value", "keyboard-inaccessible", "invalid-host", "unresolved-idref", "managed-aria", "composition-mismatch", "missing-companion", "undeclared-target", "cardinality-violation", "forbidden-aria", "missing-announcer", "unknown-ignore-code"];
663
774
  /** See {@link DIAGNOSTIC_CODES}. */
664
775
  type DiagnosticCode = (typeof DIAGNOSTIC_CODES)[number];
665
776
  /**
@@ -5,6 +5,66 @@ import { fileURLToPath } from 'url';
5
5
 
6
6
  // src/inspector/cli.ts
7
7
 
8
+ // src/inspector/announcer_rules.ts
9
+ var ANNOUNCER_IDENTIFIER = "stimeo--announcer";
10
+ var ANNOUNCING_CONTROLLERS = [
11
+ "stimeo--auto-submit",
12
+ "stimeo--character-counter",
13
+ "stimeo--countdown",
14
+ "stimeo--direct-upload",
15
+ "stimeo--empty-state",
16
+ "stimeo--file-dropzone",
17
+ "stimeo--flash",
18
+ "stimeo--form-field",
19
+ "stimeo--frame-loading",
20
+ "stimeo--meter",
21
+ "stimeo--multi-select",
22
+ "stimeo--nested-form",
23
+ "stimeo--network-status",
24
+ "stimeo--progress",
25
+ "stimeo--skeleton",
26
+ "stimeo--spinner",
27
+ "stimeo--submit-once",
28
+ "stimeo--tags-input"
29
+ ];
30
+ function positionAt(source, index) {
31
+ const before = source.slice(0, index);
32
+ const line = before.split("\n").length;
33
+ const lastBreak = before.lastIndexOf("\n");
34
+ return { line, column: index - lastBreak };
35
+ }
36
+ function firstAnnouncingHit(scanned) {
37
+ let best = null;
38
+ let bestIndex = Number.POSITIVE_INFINITY;
39
+ for (const identifier of ANNOUNCING_CONTROLLERS) {
40
+ const index = scanned.source.indexOf(identifier);
41
+ if (index === -1 || index >= bestIndex) continue;
42
+ bestIndex = index;
43
+ best = { file: scanned.file, identifier, ...positionAt(scanned.source, index) };
44
+ }
45
+ return best;
46
+ }
47
+ function announcerSeatingDiagnostic(sources) {
48
+ let speaker = null;
49
+ for (const scanned of sources) {
50
+ if (scanned.source.includes(ANNOUNCER_IDENTIFIER)) return null;
51
+ if (!speaker) speaker = firstAnnouncingHit(scanned);
52
+ }
53
+ if (!speaker) return null;
54
+ return {
55
+ file: speaker.file,
56
+ diagnostic: {
57
+ code: "missing-announcer",
58
+ severity: "warning",
59
+ message: `"${speaker.identifier}" speaks through the shared announcer, and none of the scanned files seats one.`,
60
+ line: speaker.line,
61
+ column: speaker.column,
62
+ length: speaker.identifier.length,
63
+ suggestion: `Add an element with data-controller="${ANNOUNCER_IDENTIFIER}" (and an assertive/polite target) to the layout, or include the layout in the checked paths.`
64
+ }
65
+ };
66
+ }
67
+
8
68
  // src/inspector/html_parser.ts
9
69
  var ERB_ELEMENT_TAG = "#erb";
10
70
  var VOID_ELEMENTS = /* @__PURE__ */ new Set([
@@ -930,10 +990,16 @@ function actionDescriptors(dataActionValue) {
930
990
  if (hash === -1) continue;
931
991
  let lhs = descriptor.slice(0, hash);
932
992
  const arrow = lhs.indexOf("->");
933
- if (arrow !== -1) lhs = lhs.slice(arrow + 2);
993
+ let eventType = "";
994
+ if (arrow !== -1) {
995
+ eventType = lhs.slice(0, arrow);
996
+ lhs = lhs.slice(arrow + 2);
997
+ const at = eventType.indexOf("@");
998
+ if (at !== -1) eventType = eventType.slice(0, at);
999
+ }
934
1000
  if (!lhs.startsWith(NAMESPACE_PREFIX)) continue;
935
1001
  const method = /^[a-zA-Z_$][\w$]*/.exec(descriptor.slice(hash + 1))?.[0] ?? "";
936
- descriptors.push({ identifier: lhs, method });
1002
+ descriptors.push({ identifier: lhs, method, eventType });
937
1003
  }
938
1004
  return descriptors;
939
1005
  }
@@ -952,6 +1018,8 @@ var DIAGNOSTIC_CODES = [
952
1018
  "orphan-target",
953
1019
  "missing-required-target",
954
1020
  "missing-conditional-target",
1021
+ "missing-required-action",
1022
+ "missing-action-completion",
955
1023
  "missing-aria",
956
1024
  "invalid-aria-value",
957
1025
  "keyboard-inaccessible",
@@ -963,6 +1031,7 @@ var DIAGNOSTIC_CODES = [
963
1031
  "undeclared-target",
964
1032
  "cardinality-violation",
965
1033
  "forbidden-aria",
1034
+ "missing-announcer",
966
1035
  "unknown-ignore-code"
967
1036
  ];
968
1037
 
@@ -973,6 +1042,19 @@ function isInside(node, ancestor) {
973
1042
  }
974
1043
  return false;
975
1044
  }
1045
+ function matchesHost(node, selector) {
1046
+ if (node.tag !== selector.tag) return false;
1047
+ if (!selector.attr) return true;
1048
+ const attr = node.attrs.find((candidate) => candidate.name === selector.attr);
1049
+ return attr !== void 0 && (selector.values ?? []).includes(attr.value.trim().toLowerCase());
1050
+ }
1051
+ function nearestHost(node, selectors) {
1052
+ for (let current = node.parent; current; current = current.parent) {
1053
+ if (current.origin !== "markup" || current.opaque) return void 0;
1054
+ if (selectors.some((selector) => matchesHost(current, selector))) return current;
1055
+ }
1056
+ return null;
1057
+ }
976
1058
  function checkSource(source, manifest) {
977
1059
  const diagnostics = [];
978
1060
  const tree = parseHtml(neutralizeErb(source), erbElements(source));
@@ -991,7 +1073,9 @@ function checkSource(source, manifest) {
991
1073
  (a) => a.name === `data-${identifier}-${dasherize(when.value)}-value`
992
1074
  );
993
1075
  if (hostAttr && isDynamicValue(hostAttr)) return null;
994
- return when.equals.includes(hostAttr ? hostAttr.value.trim() : when.default);
1076
+ const raw = hostAttr ? hostAttr.value : when.default;
1077
+ const effective = when.type === "boolean" ? String(!(raw === "0" || raw.toLowerCase() === "false")) : raw;
1078
+ return when.equals.includes(effective);
995
1079
  };
996
1080
  const isContentEditable2 = (element) => {
997
1081
  let current = element;
@@ -1064,6 +1148,17 @@ function checkSource(source, manifest) {
1064
1148
  const scopes = [];
1065
1149
  const presence = /* @__PURE__ */ new Map();
1066
1150
  const targetNodes = /* @__PURE__ */ new Map();
1151
+ const wiredActions = /* @__PURE__ */ new Map();
1152
+ const recordAction = (owner, identifier, element, method, eventType) => {
1153
+ let byId = wiredActions.get(owner);
1154
+ if (!byId) {
1155
+ byId = /* @__PURE__ */ new Map();
1156
+ wiredActions.set(owner, byId);
1157
+ }
1158
+ const found = byId.get(identifier);
1159
+ if (found) found.push({ element, method, eventType });
1160
+ else byId.set(identifier, [{ element, method, eventType }]);
1161
+ };
1067
1162
  const recordPresence = (owner, identifier, target) => {
1068
1163
  let byId = presence.get(owner);
1069
1164
  if (!byId) {
@@ -1095,8 +1190,9 @@ function checkSource(source, manifest) {
1095
1190
  const contentHolds = (scope, identifier, element, condition) => {
1096
1191
  if (!condition) return true;
1097
1192
  const nodes = targetNodes.get(scope)?.get(identifier)?.get(condition.target) ?? [];
1193
+ const container = condition.within === "scope" ? scope : element;
1098
1194
  let held = 0;
1099
- for (const node of nodes) if (isWithin(node, element)) held += 1;
1195
+ for (const node of nodes) if (isWithin(node, container)) held += 1;
1100
1196
  if (condition.min !== void 0 && held < condition.min) return false;
1101
1197
  if (condition.max !== void 0 && held > condition.max) return false;
1102
1198
  return true;
@@ -1272,11 +1368,12 @@ function checkSource(source, manifest) {
1272
1368
  (candidate) => dasherize(candidate.value) === parsed.valueToken
1273
1369
  );
1274
1370
  if (constraint && !valueSatisfiesConstraint(attr.value, constraint)) {
1371
+ const literal = constraint.type === "string" ? attr.value : attr.value.trim();
1275
1372
  report(
1276
1373
  node,
1277
1374
  "invalid-value",
1278
1375
  "error",
1279
- `Invalid value "${attr.value.trim()}" for "${parsed.identifier}.${parsed.valueToken}". Expected ${describeValueConstraint(constraint)}.`,
1376
+ `Invalid value "${literal}" for "${parsed.identifier}.${parsed.valueToken}". Expected ${describeValueConstraint(constraint)}.`,
1280
1377
  attr,
1281
1378
  constraint.suggestion
1282
1379
  );
@@ -1287,8 +1384,12 @@ function checkSource(source, manifest) {
1287
1384
  }
1288
1385
  }
1289
1386
  if (attr.name === "data-action") {
1290
- for (const { identifier, method } of actionDescriptors(attr.value)) {
1387
+ for (const { identifier, method, eventType } of actionDescriptors(attr.value)) {
1291
1388
  const controller = known[identifier];
1389
+ if (controller && method.length > 0) {
1390
+ const owner = findOwner(node, identifier);
1391
+ if (owner) recordAction(owner, identifier, node, method, eventType);
1392
+ }
1292
1393
  if (!controller) {
1293
1394
  const best = nearestName(identifier, knownIdentifiers);
1294
1395
  report(
@@ -1372,19 +1473,87 @@ function checkSource(source, manifest) {
1372
1473
  if (!present.has(rule.whenPresent)) continue;
1373
1474
  const nodesOf = (target) => targetNodes.get(node)?.get(identifier)?.get(target) ?? [];
1374
1475
  const owners = nodesOf(rule.whenPresent);
1375
- const satisfied = (target) => rule.requireInside ? nodesOf(target).some((el) => owners.some((owner) => isInside(el, owner))) : present.has(target);
1476
+ const hosts = rule.requireSameHost;
1477
+ const sharesHost = (target, selectors) => {
1478
+ const ownerHosts = owners.map((owner) => nearestHost(owner, selectors));
1479
+ if (ownerHosts.some((host) => !host)) return true;
1480
+ const candidates = nodesOf(target);
1481
+ if (candidates.length === 0) return false;
1482
+ return candidates.some((el) => {
1483
+ const host = nearestHost(el, selectors);
1484
+ return host ? ownerHosts.includes(host) : true;
1485
+ });
1486
+ };
1487
+ const satisfied = (target) => {
1488
+ if (hosts) return sharesHost(target, hosts);
1489
+ if (rule.requireInside)
1490
+ return nodesOf(target).some((el) => owners.some((owner) => isInside(el, owner)));
1491
+ return present.has(target);
1492
+ };
1376
1493
  const missing = rule.require.filter((target) => !satisfied(target));
1377
1494
  if (missing.length === 0) continue;
1378
1495
  report(
1379
1496
  node,
1380
1497
  "missing-conditional-target",
1381
1498
  complete ? "error" : "warning",
1382
- rule.requireInside ? `"${identifier}" requires ${list(missing)} inside its "${rule.whenPresent}".` : `"${identifier}" has a "${rule.whenPresent}" target, which also requires ${list(missing)}.`,
1499
+ hosts ? `"${identifier}" requires ${list(missing)} in the same ${rule.hostLabel ?? "element"} as its "${rule.whenPresent}".` : rule.requireInside ? `"${identifier}" requires ${list(missing)} inside its "${rule.whenPresent}".` : `"${identifier}" has a "${rule.whenPresent}" target, which also requires ${list(missing)}.`,
1383
1500
  node,
1384
1501
  complete ? rule.suggestion : UNRESOLVED_TARGET_HINT
1385
1502
  );
1386
1503
  }
1387
1504
  }
1505
+ for (const { node, identifier } of scopes) {
1506
+ const controller = known[identifier];
1507
+ if (!controller) continue;
1508
+ const wired = wiredActions.get(node)?.get(identifier) ?? [];
1509
+ for (const rule of controller.requiredActions) {
1510
+ if (conditionHolds(node, identifier, rule.when) !== true) continue;
1511
+ const elements = rule.target === "" ? [node] : targetNodes.get(node)?.get(identifier)?.get(rule.target) ?? [];
1512
+ for (const element of elements) {
1513
+ const actionAttr = element.attrs.find((attr) => attr.name === "data-action");
1514
+ if (element.opaque || actionAttr && isDynamicValue(actionAttr)) continue;
1515
+ const satisfied = wired.some(
1516
+ (action) => action.element === element && action.method === rule.action && rule.eventTypes.includes(action.eventType)
1517
+ );
1518
+ if (satisfied) continue;
1519
+ const where = rule.target === "" ? "scope element" : `"${rule.target}" target`;
1520
+ report(
1521
+ element,
1522
+ "missing-required-action",
1523
+ "error",
1524
+ `The ${where} of "${identifier}" requires action "${rule.action}" on ${list(rule.eventTypes)} in this configuration.`,
1525
+ element,
1526
+ rule.suggestion
1527
+ );
1528
+ }
1529
+ }
1530
+ }
1531
+ for (const { node, identifier } of scopes) {
1532
+ const controller = known[identifier];
1533
+ if (!controller) continue;
1534
+ const wired = wiredActions.get(node)?.get(identifier) ?? [];
1535
+ if (wired.length === 0) continue;
1536
+ for (const rule of controller.actionCompletion) {
1537
+ const opens = wired.some(
1538
+ (action) => action.method === rule.opens && rule.whenTriggeredBy.includes(action.eventType)
1539
+ );
1540
+ if (!opens) continue;
1541
+ if (wired.some((action) => rule.closedBy.includes(action.method))) continue;
1542
+ if (rule.escapeValue) {
1543
+ const attrName = `data-${identifier}-${dasherize(rule.escapeValue)}-value`;
1544
+ const authored = node.attrs.find((attr) => attr.name === attrName);
1545
+ if (authored && (isDynamicValue(authored) || Number(authored.value.trim()) !== 0)) continue;
1546
+ }
1547
+ report(
1548
+ node,
1549
+ "missing-action-completion",
1550
+ "warning",
1551
+ `"${identifier}" opens a state with "${rule.opens}" here, and nothing in this scope closes it.`,
1552
+ node,
1553
+ rule.suggestion
1554
+ );
1555
+ }
1556
+ }
1388
1557
  for (const { node, identifier } of scopes) {
1389
1558
  const controller = known[identifier];
1390
1559
  if (!controller) continue;
@@ -1687,6 +1856,7 @@ function checkSource(source, manifest) {
1687
1856
  return diagnostics;
1688
1857
  }
1689
1858
  function valueSatisfiesConstraint(raw, constraint) {
1859
+ if (constraint.type === "string") return constraint.allowedValues.includes(raw);
1690
1860
  const value = decodeNumericValue(raw);
1691
1861
  if (Number.isNaN(value)) return false;
1692
1862
  if (constraint.finite && !Number.isFinite(value)) return false;
@@ -1698,6 +1868,7 @@ function decodeNumericValue(raw) {
1698
1868
  return Number(raw.replace(/_/g, ""));
1699
1869
  }
1700
1870
  function describeValueConstraint(constraint) {
1871
+ if (constraint.type === "string") return `one of ${list(constraint.allowedValues)}`;
1701
1872
  const parts = [constraint.finite ? "a finite number" : "a number"];
1702
1873
  if (constraint.greaterThan !== void 0) {
1703
1874
  parts.push(`greater than ${constraint.greaterThan}`);
@@ -1952,6 +2123,8 @@ var CONTROLLER_ARRAY_FIELDS = [
1952
2123
  "events",
1953
2124
  "requiredTargets",
1954
2125
  "conditionalTargets",
2126
+ "requiredActions",
2127
+ "actionCompletion",
1955
2128
  "a11y",
1956
2129
  "keyboard",
1957
2130
  "hosts",
@@ -2014,10 +2187,16 @@ function collectFiles(target, out) {
2014
2187
  }
2015
2188
  if (isCheckableFile(target)) out.push(target);
2016
2189
  }
2017
- function checkFile(file, manifest) {
2018
- const source = readFileSync(file, "utf8");
2190
+ function checkFile(file, source, manifest) {
2019
2191
  return { file, diagnostics: checkSource(source, manifest) };
2020
2192
  }
2193
+ function withAnnouncerSeating(reports, sources) {
2194
+ const seating = announcerSeatingDiagnostic(sources);
2195
+ if (!seating) return [...reports];
2196
+ return reports.map(
2197
+ (report) => report.file === seating.file ? { file: report.file, diagnostics: [...report.diagnostics, seating.diagnostic] } : report
2198
+ );
2199
+ }
2021
2200
  function formatDiagnostic(d) {
2022
2201
  const where = `${d.line}:${d.column}`.padEnd(7);
2023
2202
  const head = ` ${where} ${d.severity.padEnd(7)} ${d.message} [${d.code}]`;
@@ -2150,13 +2329,14 @@ ${USAGE}`);
2150
2329
  const manifest = load();
2151
2330
  const cwd = process.cwd();
2152
2331
  const reports = [];
2332
+ const sources = [];
2153
2333
  for (const file of files.sort()) {
2154
- reports.push({
2155
- file: relative(cwd, file) || file,
2156
- diagnostics: checkFile(file, manifest).diagnostics
2157
- });
2334
+ const path = relative(cwd, file) || file;
2335
+ const source = readFileSync(file, "utf8");
2336
+ sources.push({ file: path, source });
2337
+ reports.push(checkFile(path, source, manifest));
2158
2338
  }
2159
- const summary = buildCheckReport(reports, files.length);
2339
+ const summary = buildCheckReport(withAnnouncerSeating(reports, sources), files.length);
2160
2340
  if (json) {
2161
2341
  write(JSON.stringify(summary, null, 2));
2162
2342
  return summary.errorCount > 0 ? 1 : 0;