stimeo-ui 0.2.0 → 0.2.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.
Files changed (124) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/controllers/accordion_controller.d.ts +3 -2
  3. package/dist/controllers/accordion_controller.js.map +1 -1
  4. package/dist/controllers/alert_dialog_controller.d.ts +64 -0
  5. package/dist/controllers/alert_dialog_controller.js +318 -0
  6. package/dist/controllers/alert_dialog_controller.js.map +1 -0
  7. package/dist/controllers/carousel_controller.d.ts +100 -0
  8. package/dist/controllers/carousel_controller.js +272 -0
  9. package/dist/controllers/carousel_controller.js.map +1 -0
  10. package/dist/controllers/clipboard_controller.d.ts +72 -0
  11. package/dist/controllers/clipboard_controller.js +144 -0
  12. package/dist/controllers/clipboard_controller.js.map +1 -0
  13. package/dist/controllers/collapsible_controller.d.ts +70 -0
  14. package/dist/controllers/collapsible_controller.js +327 -0
  15. package/dist/controllers/collapsible_controller.js.map +1 -0
  16. package/dist/controllers/color_picker_controller.d.ts +76 -0
  17. package/dist/controllers/color_picker_controller.js +213 -0
  18. package/dist/controllers/color_picker_controller.js.map +1 -0
  19. package/dist/controllers/count_up_controller.js +8 -1
  20. package/dist/controllers/count_up_controller.js.map +1 -1
  21. package/dist/controllers/currency_input_controller.d.ts +72 -0
  22. package/dist/controllers/currency_input_controller.js +147 -0
  23. package/dist/controllers/currency_input_controller.js.map +1 -0
  24. package/dist/controllers/data_grid_controller.d.ts +71 -0
  25. package/dist/controllers/data_grid_controller.js +168 -0
  26. package/dist/controllers/data_grid_controller.js.map +1 -0
  27. package/dist/controllers/date_range_picker_controller.d.ts +87 -0
  28. package/dist/controllers/date_range_picker_controller.js +417 -0
  29. package/dist/controllers/date_range_picker_controller.js.map +1 -0
  30. package/dist/controllers/dismissible_controller.d.ts +57 -0
  31. package/dist/controllers/dismissible_controller.js +117 -0
  32. package/dist/controllers/dismissible_controller.js.map +1 -0
  33. package/dist/controllers/drawer_controller.d.ts +96 -0
  34. package/dist/controllers/drawer_controller.js +630 -0
  35. package/dist/controllers/drawer_controller.js.map +1 -0
  36. package/dist/controllers/editable_controller.d.ts +73 -0
  37. package/dist/controllers/editable_controller.js +168 -0
  38. package/dist/controllers/editable_controller.js.map +1 -0
  39. package/dist/controllers/file_dropzone_controller.d.ts +92 -0
  40. package/dist/controllers/file_dropzone_controller.js +165 -0
  41. package/dist/controllers/file_dropzone_controller.js.map +1 -0
  42. package/dist/controllers/filter_controller.d.ts +60 -0
  43. package/dist/controllers/filter_controller.js +86 -0
  44. package/dist/controllers/filter_controller.js.map +1 -0
  45. package/dist/controllers/flash_controller.js +36 -5
  46. package/dist/controllers/flash_controller.js.map +1 -1
  47. package/dist/controllers/highlight_controller.js +6 -4
  48. package/dist/controllers/highlight_controller.js.map +1 -1
  49. package/dist/controllers/intersection_controller.js +41 -18
  50. package/dist/controllers/intersection_controller.js.map +1 -1
  51. package/dist/controllers/lazy_frame_controller.js +33 -11
  52. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  53. package/dist/controllers/masonry_controller.d.ts +54 -0
  54. package/dist/controllers/masonry_controller.js +142 -0
  55. package/dist/controllers/masonry_controller.js.map +1 -0
  56. package/dist/controllers/menubar_controller.d.ts +62 -0
  57. package/dist/controllers/menubar_controller.js +433 -0
  58. package/dist/controllers/menubar_controller.js.map +1 -0
  59. package/dist/controllers/multi_select_controller.d.ts +115 -0
  60. package/dist/controllers/multi_select_controller.js +472 -0
  61. package/dist/controllers/multi_select_controller.js.map +1 -0
  62. package/dist/controllers/navigation_menu_controller.d.ts +99 -0
  63. package/dist/controllers/navigation_menu_controller.js +384 -0
  64. package/dist/controllers/navigation_menu_controller.js.map +1 -0
  65. package/dist/controllers/overflow_indicator_controller.d.ts +12 -4
  66. package/dist/controllers/overflow_indicator_controller.js +178 -27
  67. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  68. package/dist/controllers/password_reveal_controller.d.ts +49 -0
  69. package/dist/controllers/password_reveal_controller.js +117 -0
  70. package/dist/controllers/password_reveal_controller.js.map +1 -0
  71. package/dist/controllers/range_slider_controller.d.ts +87 -0
  72. package/dist/controllers/range_slider_controller.js +166 -0
  73. package/dist/controllers/range_slider_controller.js.map +1 -0
  74. package/dist/controllers/read_more_controller.d.ts +52 -0
  75. package/dist/controllers/read_more_controller.js +194 -0
  76. package/dist/controllers/read_more_controller.js.map +1 -0
  77. package/dist/controllers/scroll_area_controller.js +15 -2
  78. package/dist/controllers/scroll_area_controller.js.map +1 -1
  79. package/dist/controllers/scroll_restore_controller.d.ts +54 -0
  80. package/dist/controllers/scroll_restore_controller.js +93 -0
  81. package/dist/controllers/scroll_restore_controller.js.map +1 -0
  82. package/dist/controllers/scroll_visibility_controller.js +8 -4
  83. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  84. package/dist/controllers/scrollspy_controller.js +33 -11
  85. package/dist/controllers/scrollspy_controller.js.map +1 -1
  86. package/dist/controllers/separator_controller.d.ts +54 -0
  87. package/dist/controllers/separator_controller.js +87 -0
  88. package/dist/controllers/separator_controller.js.map +1 -0
  89. package/dist/controllers/sidebar_controller.d.ts +107 -0
  90. package/dist/controllers/sidebar_controller.js +761 -0
  91. package/dist/controllers/sidebar_controller.js.map +1 -0
  92. package/dist/controllers/stepper_controller.d.ts +4 -0
  93. package/dist/controllers/stepper_controller.js +28 -12
  94. package/dist/controllers/stepper_controller.js.map +1 -1
  95. package/dist/controllers/stick_to_bottom_controller.js +8 -4
  96. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  97. package/dist/controllers/sticky_observer_controller.d.ts +17 -4
  98. package/dist/controllers/sticky_observer_controller.js +88 -20
  99. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  100. package/dist/controllers/tabs_controller.d.ts +3 -2
  101. package/dist/controllers/tabs_controller.js.map +1 -1
  102. package/dist/controllers/tags_input_controller.d.ts +91 -0
  103. package/dist/controllers/tags_input_controller.js +275 -0
  104. package/dist/controllers/tags_input_controller.js.map +1 -0
  105. package/dist/controllers/theme_controller.js +20 -10
  106. package/dist/controllers/theme_controller.js.map +1 -1
  107. package/dist/controllers/time_picker_controller.d.ts +67 -0
  108. package/dist/controllers/time_picker_controller.js +212 -0
  109. package/dist/controllers/time_picker_controller.js.map +1 -0
  110. package/dist/controllers/toast_controller.d.ts +1 -9
  111. package/dist/controllers/toast_controller.js +36 -9
  112. package/dist/controllers/toast_controller.js.map +1 -1
  113. package/dist/controllers/transition_controller.d.ts +14 -8
  114. package/dist/controllers/transition_controller.js +153 -38
  115. package/dist/controllers/transition_controller.js.map +1 -1
  116. package/dist/controllers/tree_view_controller.d.ts +54 -0
  117. package/dist/controllers/tree_view_controller.js +275 -0
  118. package/dist/controllers/tree_view_controller.js.map +1 -0
  119. package/dist/index.d.ts +27 -1818
  120. package/dist/index.js +811 -295
  121. package/dist/index.js.map +1 -1
  122. package/dist/inspector/examples.json +2 -2
  123. package/dist/inspector/manifest.json +15 -3
  124. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { Controller, Application } from '@hotwired/stimulus';
1
+ import { Application } from '@hotwired/stimulus';
2
2
  import { AccordionController } from './controllers/accordion_controller.js';
3
+ import { AlertDialogController } from './controllers/alert_dialog_controller.js';
3
4
  import { AnnouncerController } from './controllers/announcer_controller.js';
4
5
  import { AspectRatioController } from './controllers/aspect_ratio_controller.js';
5
6
  import { AutoSubmitController } from './controllers/auto_submit_controller.js';
@@ -7,8 +8,12 @@ import { AvatarController } from './controllers/avatar_controller.js';
7
8
  import { BreadcrumbController } from './controllers/breadcrumb_controller.js';
8
9
  import { BulkSelectController } from './controllers/bulk_select_controller.js';
9
10
  import { CalendarController } from './controllers/calendar_controller.js';
11
+ import { CarouselController } from './controllers/carousel_controller.js';
10
12
  import { CharacterCounterController } from './controllers/character_counter_controller.js';
11
13
  import { CheckboxController } from './controllers/checkbox_controller.js';
14
+ import { ClipboardController } from './controllers/clipboard_controller.js';
15
+ import { CollapsibleController } from './controllers/collapsible_controller.js';
16
+ import { ColorPickerController } from './controllers/color_picker_controller.js';
12
17
  import { ComboboxController } from './controllers/combobox_controller.js';
13
18
  import { CommandPaletteController } from './controllers/command_palette_controller.js';
14
19
  import { ConditionalFieldsController } from './controllers/conditional_fields_controller.js';
@@ -16,11 +21,19 @@ import { ConfirmController } from './controllers/confirm_controller.js';
16
21
  import { ContextMenuController } from './controllers/context_menu_controller.js';
17
22
  import { CountUpController } from './controllers/count_up_controller.js';
18
23
  import { CountdownController } from './controllers/countdown_controller.js';
24
+ import { CurrencyInputController } from './controllers/currency_input_controller.js';
25
+ import { DataGridController } from './controllers/data_grid_controller.js';
26
+ import { DateRangePickerController } from './controllers/date_range_picker_controller.js';
19
27
  import { DialogController } from './controllers/dialog_controller.js';
20
28
  import { DirectUploadController } from './controllers/direct_upload_controller.js';
21
29
  import { DirtyFormController } from './controllers/dirty_form_controller.js';
30
+ import { DismissibleController } from './controllers/dismissible_controller.js';
31
+ import { DrawerController } from './controllers/drawer_controller.js';
22
32
  import { DropdownController } from './controllers/dropdown_controller.js';
33
+ import { EditableController } from './controllers/editable_controller.js';
23
34
  import { EmptyStateController } from './controllers/empty_state_controller.js';
35
+ import { FileDropzoneController } from './controllers/file_dropzone_controller.js';
36
+ import { FilterController } from './controllers/filter_controller.js';
24
37
  import { FlashController } from './controllers/flash_controller.js';
25
38
  import { FocusController } from './controllers/focus_controller.js';
26
39
  import { FormFieldController } from './controllers/form_field_controller.js';
@@ -34,8 +47,12 @@ import { IntersectionController } from './controllers/intersection_controller.js
34
47
  import { LazyFrameController } from './controllers/lazy_frame_controller.js';
35
48
  import { ListboxController } from './controllers/listbox_controller.js';
36
49
  import { LocalTimeController } from './controllers/local_time_controller.js';
50
+ import { MasonryController } from './controllers/masonry_controller.js';
37
51
  import { MenuController } from './controllers/menu_controller.js';
52
+ import { MenubarController } from './controllers/menubar_controller.js';
38
53
  import { MeterController } from './controllers/meter_controller.js';
54
+ import { MultiSelectController } from './controllers/multi_select_controller.js';
55
+ import { NavigationMenuController } from './controllers/navigation_menu_controller.js';
39
56
  import { NestedFormController } from './controllers/nested_form_controller.js';
40
57
  import { NetworkStatusController } from './controllers/network_status_controller.js';
41
58
  import { NumberInputController } from './controllers/number_input_controller.js';
@@ -44,6 +61,7 @@ import { OtpController } from './controllers/otp_controller.js';
44
61
  import { OverflowIndicatorController } from './controllers/overflow_indicator_controller.js';
45
62
  import { OverflowMenuController } from './controllers/overflow_menu_controller.js';
46
63
  import { PaginationController } from './controllers/pagination_controller.js';
64
+ import { PasswordRevealController } from './controllers/password_reveal_controller.js';
47
65
  import { PasswordStrengthController } from './controllers/password_strength_controller.js';
48
66
  import { PersistController } from './controllers/persist_controller.js';
49
67
  import { PointerDragController } from './controllers/pointer_drag_controller.js';
@@ -52,15 +70,20 @@ import { PortalController } from './controllers/portal_controller.js';
52
70
  import { PreviewGuardController } from './controllers/preview_guard_controller.js';
53
71
  import { ProgressController } from './controllers/progress_controller.js';
54
72
  import { RadioGroupController } from './controllers/radio_group_controller.js';
73
+ import { RangeSliderController } from './controllers/range_slider_controller.js';
55
74
  import { RatingController } from './controllers/rating_controller.js';
75
+ import { ReadMoreController } from './controllers/read_more_controller.js';
56
76
  import { ReadingProgressController } from './controllers/reading_progress_controller.js';
57
77
  import { RelativeTimeController } from './controllers/relative_time_controller.js';
58
78
  import { ResetBeforeCacheController } from './controllers/reset_before_cache_controller.js';
59
79
  import { ResizableController } from './controllers/resizable_controller.js';
60
80
  import { RovingController } from './controllers/roving_controller.js';
61
81
  import { ScrollAreaController } from './controllers/scroll_area_controller.js';
82
+ import { ScrollRestoreController } from './controllers/scroll_restore_controller.js';
62
83
  import { ScrollVisibilityController } from './controllers/scroll_visibility_controller.js';
63
84
  import { ScrollspyController } from './controllers/scrollspy_controller.js';
85
+ import { SeparatorController } from './controllers/separator_controller.js';
86
+ import { SidebarController } from './controllers/sidebar_controller.js';
64
87
  import { SkeletonController } from './controllers/skeleton_controller.js';
65
88
  import { SliderController } from './controllers/slider_controller.js';
66
89
  import { SmartStickyHeaderController } from './controllers/smart_sticky_header_controller.js';
@@ -73,1830 +96,16 @@ import { StickyObserverController } from './controllers/sticky_observer_controll
73
96
  import { SubmitOnceController } from './controllers/submit_once_controller.js';
74
97
  import { SwitchController } from './controllers/switch_controller.js';
75
98
  import { TabsController } from './controllers/tabs_controller.js';
99
+ import { TagsInputController } from './controllers/tags_input_controller.js';
76
100
  import { TextareaAutosizeController } from './controllers/textarea_autosize_controller.js';
77
101
  import { ThemeController } from './controllers/theme_controller.js';
102
+ import { TimePickerController } from './controllers/time_picker_controller.js';
78
103
  import { ToastController } from './controllers/toast_controller.js';
79
104
  import { ToggleGroupController } from './controllers/toggle_group_controller.js';
80
105
  import { ToolbarController } from './controllers/toolbar_controller.js';
81
106
  import { TooltipController } from './controllers/tooltip_controller.js';
82
107
  import { TransitionController } from './controllers/transition_controller.js';
83
-
84
- /**
85
- * Headless, accessible **alert dialog** behavior.
86
- *
87
- * Markup contract (identifier: `stimeo--alert-dialog`):
88
- * <div data-controller="stimeo--alert-dialog">
89
- * <button data-stimeo--alert-dialog-target="trigger"
90
- * data-action="click->stimeo--alert-dialog#open">Delete…</button>
91
- * <div data-stimeo--alert-dialog-target="dialog" role="alertdialog"
92
- * aria-modal="true" aria-labelledby="t" aria-describedby="d" hidden>
93
- * <h2 id="t">…</h2><p id="d">…</p>
94
- * <button data-stimeo--alert-dialog-target="initialFocus"
95
- * data-action="click->stimeo--alert-dialog#cancel">Cancel</button>
96
- * <button data-action="click->stimeo--alert-dialog#confirm">Delete</button>
97
- * </div>
98
- * </div>
99
- *
100
- * Implements the WAI-ARIA APG **Alert Dialog** pattern. It is the same modal as
101
- * `stimeo--dialog` with two deliberate differences that suit an *interrupting
102
- * confirmation*: it never closes on a backdrop click (an accidental dismissal
103
- * would be dangerous), and it exposes explicit {@link confirm}/{@link cancel}
104
- * actions that emit events so the consumer only writes the message and "what to
105
- * do on confirm".
106
- *
107
- * @remarks
108
- * Behavior only. The modal lifecycle (focus trap, scroll lock, background
109
- * `inert`, focus restore, teardown reversal) is delegated to the shared
110
- * {@link FocusTrap}. The only closing affordances are the confirm/cancel actions
111
- * and `Escape` (which cancels) — there is intentionally no backdrop close.
112
- *
113
- * Behavior provided:
114
- * - {@link open} shows the dialog and moves focus to the `initialFocus` target
115
- * (the least destructive action, by convention), else the first focusable
116
- * element.
117
- * - `Tab`/`Shift+Tab` cycle focus within the dialog (focus trap).
118
- * - {@link confirm} closes and dispatches `stimeo--alert-dialog:confirm`.
119
- * - {@link cancel} and `Escape` close and dispatch `stimeo--alert-dialog:cancel`
120
- * with a `reason` of `"user"` / `"escape"`. Focus returns to the opener.
121
- */
122
- declare class AlertDialogController extends Controller<HTMLElement> {
123
- #private;
124
- static targets: string[];
125
- static actions: readonly ["cancel", "confirm", "open"];
126
- static events: readonly ["cancel", "confirm"];
127
- readonly triggerTarget: HTMLElement;
128
- readonly dialogTarget: HTMLElement;
129
- readonly initialFocusTarget: HTMLElement;
130
- readonly hasTriggerTarget: boolean;
131
- readonly hasDialogTarget: boolean;
132
- readonly hasInitialFocusTarget: boolean;
133
- /** Starts closed (idempotently reflects the closed state on the markup). */
134
- connect(): void;
135
- /** Reverts the modal side effects if torn down while open (Turbo navigation). */
136
- disconnect(): void;
137
- /** Opens the dialog, traps focus, and locks background scroll. */
138
- open(): void;
139
- /** Confirms: closes and dispatches `confirm`. Bound via `data-action`. */
140
- confirm(): void;
141
- /** Cancels (user action): closes and dispatches `cancel` with `reason: "user"`. */
142
- cancel(): void;
143
- }
144
-
145
- /**
146
- * Headless, accessible **Carousel** (slideshow) behavior.
147
- *
148
- * Markup contract (identifier: `stimeo--carousel`):
149
- * <section data-controller="stimeo--carousel" aria-roledescription="carousel"
150
- * aria-label="Featured"
151
- * data-stimeo--carousel-autoplay-value="false"
152
- * data-stimeo--carousel-interval-value="5000"
153
- * data-stimeo--carousel-loop-value="true"
154
- * data-action="mouseenter->stimeo--carousel#pause
155
- * mouseleave->stimeo--carousel#resume
156
- * focusin->stimeo--carousel#pause
157
- * focusout->stimeo--carousel#resume">
158
- * <button data-stimeo--carousel-target="playToggle"
159
- * data-action="stimeo--carousel#togglePlay">…</button>
160
- * <div data-stimeo--carousel-target="viewport">
161
- * <div role="tabpanel" data-stimeo--carousel-target="slide">…</div>
162
- * <div role="tabpanel" data-stimeo--carousel-target="slide" hidden>…</div>
163
- * </div>
164
- * <button data-stimeo--carousel-target="prev" data-action="stimeo--carousel#prev">‹</button>
165
- * <button data-stimeo--carousel-target="next" data-action="stimeo--carousel#next">›</button>
166
- * <div role="tablist">
167
- * <button role="tab" data-stimeo--carousel-target="picker"
168
- * data-action="stimeo--carousel#goto
169
- * keydown->stimeo--carousel#onPickerKeydown"></button>
170
- * </div>
171
- * </section>
172
- *
173
- * Implements the WAI-ARIA APG **Carousel** (tabbed) pattern. The current slide is
174
- * exposed through `data-state` (`active`/`inactive`) and the `hidden` attribute on
175
- * inactive slides (removing them from focus order); the matching picker carries
176
- * `aria-selected` and the single roving `tabindex`. The play/pause toggle's
177
- * `aria-pressed` mirrors the autoplay state.
178
- *
179
- * @remarks
180
- * Behavior only — transitions, layout, and visuals are the consumer's CSS.
181
- * Autoplay honors WCAG 2.2.2: it suspends while the pointer is over the carousel
182
- * and **hard-stops** when keyboard focus enters (it does not silently resume on
183
- * focus out — the user must press play), so motion never surprises a keyboard
184
- * user. The interval is cleared on `disconnect()` (Turbo navigation included).
185
- * Picker arrow keys move focus only (manual activation); slide changes never steal
186
- * focus from the control the user operated.
187
- */
188
- declare class CarouselController extends Controller<HTMLElement> {
189
- #private;
190
- static targets: string[];
191
- static values: {
192
- autoplay: {
193
- type: BooleanConstructor;
194
- default: boolean;
195
- };
196
- interval: {
197
- type: NumberConstructor;
198
- default: number;
199
- };
200
- loop: {
201
- type: BooleanConstructor;
202
- default: boolean;
203
- };
204
- };
205
- static actions: readonly ["goto", "next", "onPickerKeydown", "pause", "prev", "resume", "togglePlay"];
206
- static events: readonly ["change", "pause", "play"];
207
- readonly slideTargets: HTMLElement[];
208
- readonly pickerTargets: HTMLElement[];
209
- readonly playToggleTarget: HTMLElement;
210
- readonly hasPlayToggleTarget: boolean;
211
- autoplayValue: boolean;
212
- intervalValue: number;
213
- loopValue: boolean;
214
- /** Renders the initial slide and starts autoplay when requested. */
215
- connect(): void;
216
- /** Clears the autoplay interval so it never fires after teardown. */
217
- disconnect(): void;
218
- /** Advances to the next slide. Bound via `data-action`. */
219
- next(): void;
220
- /** Returns to the previous slide. Bound via `data-action`. */
221
- prev(): void;
222
- /** Jumps to the slide whose picker was activated (click / Enter / Space). */
223
- goto(event: Event): void;
224
- /** Toggles autoplay on the user's explicit request and syncs the timer. */
225
- togglePlay(): void;
226
- /**
227
- * Suspends autoplay. Hover (`mouseenter`) is a temporary suspension that resumes
228
- * on leave; keyboard focus (`focusin`) is a hard stop that turns autoplay off so
229
- * it cannot resume without an explicit play (WCAG 2.2.2).
230
- */
231
- pause(event?: Event): void;
232
- /**
233
- * Lifts a hover suspension (`mouseleave`) and resumes autoplay if it is still
234
- * on. A `focusout` does nothing here: the focus pause was a hard stop, so the
235
- * user must press play to restart.
236
- */
237
- resume(event?: Event): void;
238
- /** Picker roving: arrows move focus only; Home/End activate first/last slide. */
239
- onPickerKeydown(event: KeyboardEvent): void;
240
- }
241
-
242
- /**
243
- * Headless copy-to-clipboard behavior with a live-region completion notice.
244
- *
245
- * Markup contract (identifier: `stimeo--clipboard`):
246
- * <div data-controller="stimeo--clipboard"
247
- * data-stimeo--clipboard-feedback-duration-value="2000">
248
- * <input type="text" value="https://example.com" readonly
249
- * data-stimeo--clipboard-target="source">
250
- * <button type="button" data-stimeo--clipboard-target="button"
251
- * data-action="stimeo--clipboard#copy">Copy</button>
252
- * <span role="status" aria-live="polite"
253
- * data-stimeo--clipboard-target="feedback"></span>
254
- * </div>
255
- *
256
- * No dedicated APG pattern; this follows the Button + live-region practice. The
257
- * copy uses the standard `navigator.clipboard` API (no extra dependency); when
258
- * it is unavailable or rejects, the failure is surfaced rather than silently
259
- * swallowed, and never communicated by icon alone — the `role="status"` region
260
- * carries text so screen readers announce the outcome.
261
- *
262
- * @remarks
263
- * Behavior only — icon swaps and styling are the consumer's, keyed off
264
- * `data-state` (`idle` / `copied` / `error`). The completion notice clears
265
- * itself after `feedbackDuration`; that timer is torn down on disconnect (Turbo)
266
- * via {@link SafeTimeout}.
267
- */
268
- declare class ClipboardController extends Controller<HTMLElement> {
269
- #private;
270
- static targets: string[];
271
- static values: {
272
- text: {
273
- type: StringConstructor;
274
- default: string;
275
- };
276
- feedbackDuration: {
277
- type: NumberConstructor;
278
- default: number;
279
- };
280
- copiedLabel: {
281
- type: StringConstructor;
282
- default: string;
283
- };
284
- errorLabel: {
285
- type: StringConstructor;
286
- default: string;
287
- };
288
- };
289
- static actions: readonly ["copy"];
290
- static events: readonly ["copy"];
291
- readonly sourceTarget: HTMLElement;
292
- readonly buttonTarget: HTMLElement;
293
- readonly feedbackTarget: HTMLElement;
294
- readonly hasSourceTarget: boolean;
295
- readonly hasButtonTarget: boolean;
296
- readonly hasFeedbackTarget: boolean;
297
- textValue: string;
298
- feedbackDurationValue: number;
299
- copiedLabelValue: string;
300
- errorLabelValue: string;
301
- connect(): void;
302
- disconnect(): void;
303
- /**
304
- * Copies the resolved text and reports the outcome. Bound via `data-action`
305
- * (click). Always dispatches `stimeo--clipboard:copy` with `{ success, text }`
306
- * — including on failure — so consumers can react either way.
307
- */
308
- copy(): Promise<void>;
309
- }
310
-
311
- /**
312
- * Headless, accessible single-disclosure (collapsible) behavior.
313
- *
314
- * Markup contract (identifier: `stimeo--collapsible`):
315
- * <div data-controller="stimeo--collapsible">
316
- * <button data-stimeo--collapsible-target="trigger"
317
- * data-action="stimeo--collapsible#toggle"
318
- * aria-expanded="false" aria-controls="more">Show details</button>
319
- * <div id="more" data-stimeo--collapsible-target="content"
320
- * data-state="closed" hidden>…</div>
321
- * </div>
322
- *
323
- * Implements the WAI-ARIA APG **Disclosure** pattern for a single inline region.
324
- * Unlike {@link AccordionController} it manages exactly one trigger/content pair
325
- * with no sibling coordination, and unlike a dropdown the content expands in
326
- * flow and never closes on an outside click.
327
- *
328
- * @remarks
329
- * Behavior only — the consumer owns the height transition and all decoration.
330
- * The controller keeps the open lifecycle ordered so `hidden` (effectively
331
- * `display:none`) never blocks measurement or the transition:
332
- * - **Open**: drop `hidden` → measure the natural height into
333
- * `--stimeo-collapsible-content-height` → set `data-state="open"`.
334
- * - **Close**: set `data-state="closed"` (CSS shrinks the height) → re-apply
335
- * `hidden` after `transitionend`. With no transition (or reduced motion, which
336
- * the consumer's CSS expresses as a zero duration) it is applied immediately.
337
- */
338
- declare class CollapsibleController extends Controller<HTMLElement> {
339
- #private;
340
- static targets: string[];
341
- static values: {
342
- open: {
343
- type: BooleanConstructor;
344
- default: boolean;
345
- };
346
- };
347
- static actions: readonly ["toggle"];
348
- readonly triggerTarget: HTMLElement;
349
- readonly contentTarget: HTMLElement;
350
- readonly hasTriggerTarget: boolean;
351
- readonly hasContentTarget: boolean;
352
- openValue: boolean;
353
- /**
354
- * Establishes the initial open/closed state without animating.
355
- *
356
- * The DOM is the source of truth on reconnect (Turbo cache restore / morph): an
357
- * **explicit** state attribute — `aria-expanded="true"`/`"false"` (or, with no
358
- * trigger, `data-state="open"`/`"closed"`) — is honored verbatim so a region the
359
- * user opened *or* closed survives a back-navigation, even when the declarative
360
- * `open` Value disagrees. The Value only seeds a genuinely fresh render where no
361
- * state attribute is present yet. Mirrors `sidebar`'s `#restoreCollapsed`.
362
- */
363
- connect(): void;
364
- disconnect(): void;
365
- /** Toggles the region open/closed. Bound via `data-action` (click). */
366
- toggle(): void;
367
- }
368
-
369
- /**
370
- * Headless, accessible **Color Picker** behavior.
371
- *
372
- * Markup contract (identifier: `stimeo--color-picker`):
373
- * <div data-controller="stimeo--color-picker"
374
- * data-stimeo--color-picker-value-value="#3366cc">
375
- * <div role="slider" aria-label="Hue" data-channel="hue" tabindex="0"
376
- * aria-valuemin="0" aria-valuemax="360" aria-valuenow="210"
377
- * data-stimeo--color-picker-target="slider"
378
- * data-action="keydown->stimeo--color-picker#onKeydown
379
- * pointerdown->stimeo--color-picker#onPointerDown"></div>
380
- * <!-- saturation / lightness / alpha sliders share the same contract -->
381
- * <input type="text" aria-label="Hex color"
382
- * data-stimeo--color-picker-target="hex"
383
- * data-action="change->stimeo--color-picker#onHexInput" />
384
- * <div data-stimeo--color-picker-target="preview" aria-hidden="true"></div>
385
- * <input type="hidden" data-stimeo--color-picker-target="field" />
386
- * </div>
387
- *
388
- * Decomposes color selection into independent APG **Slider** channels (hue,
389
- * saturation, lightness, optional alpha) instead of a 2-D palette, so every
390
- * adjustment is keyboard- and screen-reader-operable. Each slider exposes
391
- * `aria-valuenow` and a human-readable `aria-valuetext` (e.g. "Hue 210 degrees");
392
- * the hex input stays two-way synced; the current color is published on the
393
- * preview and root as the `--stimeo-color` custom property and mirrored into a
394
- * hidden form field.
395
- *
396
- * @remarks
397
- * Behavior only — the swatch/gradient visuals are the consumer's CSS/canvas, fed
398
- * by `--stimeo-color`. Pointer-drag listeners on `document` are bound to an
399
- * {@link AbortController} and released on drag end and on `disconnect()` (Turbo
400
- * navigation included). Color is fully reconstructable from the `value` (hex), so
401
- * there is no transient state to restore after a Turbo cache/morph.
402
- *
403
- * The internal model is integer HSL(A), so a hex → HSL → hex round-trip is not
404
- * exactly bijective: a typed hex can normalize to a near (not identical) value
405
- * once the HSL sliders are touched. This keeps the model small and zero-dep; use a
406
- * dedicated color library on the consumer side if exact hex preservation matters.
407
- */
408
- declare class ColorPickerController extends Controller<HTMLElement> {
409
- #private;
410
- static targets: string[];
411
- static values: {
412
- value: {
413
- type: StringConstructor;
414
- default: string;
415
- };
416
- alpha: {
417
- type: BooleanConstructor;
418
- default: boolean;
419
- };
420
- };
421
- static actions: readonly ["onHexInput", "onKeydown", "onPointerDown"];
422
- static events: readonly ["change"];
423
- readonly sliderTargets: HTMLElement[];
424
- readonly hexTarget: HTMLInputElement;
425
- readonly hasHexTarget: boolean;
426
- readonly previewTargets: HTMLElement[];
427
- readonly fieldTargets: HTMLInputElement[];
428
- valueValue: string;
429
- alphaValue: boolean;
430
- /** Seeds the model from the initial hex value and renders every surface. */
431
- connect(): void;
432
- /** Cancels any active pointer drag so document listeners never leak. */
433
- disconnect(): void;
434
- /** Keyboard stepping on the focused channel slider (APG Slider model). */
435
- onKeydown(event: KeyboardEvent): void;
436
- /** Begins a pointer drag on a channel slider and tracks movement. */
437
- onPointerDown(event: PointerEvent): void;
438
- /** Parses the hex input on confirm and syncs every channel + surface. */
439
- onHexInput(): void;
440
- }
441
-
442
- /**
443
- * Headless currency / amount input behavior: groups digits for display while
444
- * keeping a machine-readable number in a hidden field, and mirrors the
445
- * normalized value to a visually-hidden span so assistive tech reads the real
446
- * amount rather than the grouped display string.
447
- *
448
- * Markup contract (identifier: `stimeo--currency-input`):
449
- * <div data-controller="stimeo--currency-input"
450
- * data-stimeo--currency-input-locale-value="en-US"
451
- * data-stimeo--currency-input-currency-value="USD"
452
- * data-stimeo--currency-input-precision-value="2">
453
- * <input type="text" inputmode="decimal"
454
- * aria-describedby="amt-sr"
455
- * data-stimeo--currency-input-target="display"
456
- * data-action="input->stimeo--currency-input#onInput
457
- * blur->stimeo--currency-input#format" />
458
- * <span id="amt-sr" class="visually-hidden"
459
- * data-stimeo--currency-input-target="srValue"></span>
460
- * <input type="hidden" data-stimeo--currency-input-target="field" />
461
- * </div>
462
- *
463
- * @remarks
464
- * Behavior only — no styling, no validation (range/required belong to the
465
- * consumer or Form Field). The display field is the sole Tab stop and keeps its
466
- * native text-editing behavior; this controller never steals focus. Grouping is
467
- * applied on every input while preserving the caret (counted by digits to its
468
- * left), and the fixed-precision rounding is applied on `blur`.
469
- *
470
- * Honest a11y note: a hidden `<input>` is not exposed to assistive tech, so the
471
- * normalized value is *also* published as text in the `srValue` span referenced
472
- * by the display's `aria-describedby` — that, not the hidden field, is what a
473
- * screen reader announces.
474
- */
475
- declare class CurrencyInputController extends Controller<HTMLElement> {
476
- #private;
477
- static targets: string[];
478
- static values: {
479
- locale: {
480
- type: StringConstructor;
481
- default: string;
482
- };
483
- currency: {
484
- type: StringConstructor;
485
- default: string;
486
- };
487
- precision: {
488
- type: NumberConstructor;
489
- default: number;
490
- };
491
- };
492
- static actions: readonly ["format", "onInput"];
493
- static events: readonly ["change"];
494
- readonly displayTarget: HTMLInputElement;
495
- readonly fieldTarget: HTMLInputElement;
496
- readonly srValueTarget: HTMLElement;
497
- readonly hasDisplayTarget: boolean;
498
- readonly hasFieldTarget: boolean;
499
- readonly hasSrValueTarget: boolean;
500
- localeValue: string;
501
- currencyValue: string;
502
- precisionValue: number;
503
- /** Normalizes any pre-filled display value to its fixed-precision form. */
504
- connect(): void;
505
- /** Re-groups digits as the user types, preserving the caret position. */
506
- onInput(): void;
507
- /** Applies the fixed-precision rounding on blur. */
508
- format(): void;
509
- }
510
-
511
- /**
512
- * Headless, accessible **Data Grid** behavior: column sorting, row selection, and
513
- * roving keyboard navigation over an interactive table.
514
- *
515
- * Markup contract (identifier: `stimeo--data-grid`):
516
- * <table data-controller="stimeo--data-grid" role="grid" aria-label="Users"
517
- * data-stimeo--data-grid-selection-value="single">
518
- * <thead><tr role="row">
519
- * <th role="columnheader" aria-sort="none" tabindex="-1"
520
- * data-stimeo--data-grid-target="columnHeader"
521
- * data-action="click->stimeo--data-grid#sort
522
- * keydown->stimeo--data-grid#onKeydown">Name</th>
523
- * </tr></thead>
524
- * <tbody><tr role="row" aria-selected="false"
525
- * data-stimeo--data-grid-target="row">
526
- * <td role="gridcell" tabindex="0" data-stimeo--data-grid-target="cell"
527
- * data-action="keydown->stimeo--data-grid#onKeydown">Jane</td>
528
- * </tr></tbody>
529
- * </table>
530
- *
531
- * Implements the WAI-ARIA APG **Grid** pattern plus `aria-sort`. The whole grid is
532
- * a single Tab stop (roving `tabindex`: exactly one cell/header is `0`, the rest
533
- * `-1`); arrow keys move both DOM focus and that tabbable position. Sort state is
534
- * exposed via `aria-sort` on headers, selection via `aria-selected` on rows.
535
- *
536
- * @remarks
537
- * Behavior only — the consumer performs the actual data sort/render in response to
538
- * the `sort` event and owns all styling. No timers or observers are held, so there
539
- * is nothing to leak across Turbo navigations; `connect()` rebuilds the single tab
540
- * stop idempotently from the DOM.
541
- *
542
- * Behavior provided:
543
- * - `Arrow*` move between cells (clamped at edges); `Home`/`End` to the row's
544
- * first/last cell; `Ctrl+Home`/`Ctrl+End` to the grid's first/last cell.
545
- * - `Enter`/`Space` cycles a header's sort (`none→ascending→descending`) or toggles
546
- * the focused row's selection when selection is enabled.
547
- */
548
- declare class DataGridController extends Controller<HTMLElement> {
549
- #private;
550
- static targets: string[];
551
- static values: {
552
- selection: {
553
- type: StringConstructor;
554
- default: string;
555
- };
556
- };
557
- static actions: readonly ["onKeydown", "sort", "toggleSelect"];
558
- static events: readonly ["selectionchange", "sort"];
559
- readonly columnHeaderTargets: HTMLElement[];
560
- readonly rowTargets: HTMLElement[];
561
- readonly cellTargets: HTMLElement[];
562
- selectionValue: string;
563
- /** Establishes a single tab stop across all navigable cells/headers. */
564
- connect(): void;
565
- /**
566
- * Keeps `aria-multiselectable` in step with the `selection` Value. Fires on connect
567
- * (so it self-heals after a Turbo morph) and on any runtime change, so the ARIA
568
- * never drifts from the selection logic, which reads `selectionValue` live.
569
- */
570
- selectionValueChanged(): void;
571
- /** Cycles the activated column header's sort and emits `sort`. */
572
- sort(event: Event): void;
573
- /** Toggles selection of the row owning the event target. Bound optionally. */
574
- toggleSelect(event: Event): void;
575
- /** Grid navigation + sort/select activation. Bound to cells and headers. */
576
- onKeydown(event: KeyboardEvent): void;
577
- }
578
-
579
- /**
580
- * Headless, accessible **date range picker** behavior. A derivative of
581
- * {@link CalendarController}: it reuses the month-grid navigation model and adds
582
- * two-point range selection, in-progress range preview, and presets.
583
- *
584
- * Markup contract (identifier: `stimeo--date-range-picker`):
585
- * <div data-controller="stimeo--date-range-picker"
586
- * data-stimeo--date-range-picker-min-value="2026-01-01"
587
- * data-stimeo--date-range-picker-max-value="2026-12-31">
588
- * <button data-action="stimeo--date-range-picker#prev">Prev</button>
589
- * <span data-stimeo--date-range-picker-target="monthLabel" aria-live="polite"></span>
590
- * <button data-action="stimeo--date-range-picker#next">Next</button>
591
- * <div role="grid" data-stimeo--date-range-picker-target="grid">
592
- * <!-- exactly 42 cell targets (7 days × 6 rows) -->
593
- * <button role="gridcell" tabindex="-1"
594
- * data-stimeo--date-range-picker-target="cell"
595
- * data-action="click->stimeo--date-range-picker#selectDate
596
- * mouseenter->stimeo--date-range-picker#previewTo
597
- * focus->stimeo--date-range-picker#previewTo
598
- * keydown->stimeo--date-range-picker#onKeydown"></button>
599
- * </div>
600
- * <button data-range="last7" data-action="stimeo--date-range-picker#applyPreset">…</button>
601
- * <span role="status" aria-live="polite" data-stimeo--date-range-picker-target="status"></span>
602
- * <input type="hidden" data-stimeo--date-range-picker-target="startField" />
603
- * <input type="hidden" data-stimeo--date-range-picker-target="endField" />
604
- * </div>
605
- *
606
- * @remarks
607
- * Behavior only — the consumer styles the grid and renders the range using the
608
- * `data-range-start` / `data-in-range` / `data-range-end` hooks. Assistive tech
609
- * is told the two confirmed endpoints via `aria-selected`; inner cells use
610
- * `data-in-range` (visual only) so the announcement stays to the two ends. The
611
- * confirmed range is also mirrored to the live `status` region.
612
- *
613
- * Selection model: the first click/Enter sets a *pending* start and enters
614
- * "selecting" mode (preview follows the pointer/focus); the second confirms the
615
- * end (auto-swapped if earlier than the start) and dispatches `change`. Escape
616
- * abandons an in-progress selection, restoring the last confirmed range.
617
- */
618
- declare class DateRangePickerController extends Controller<HTMLElement> {
619
- #private;
620
- static targets: string[];
621
- static values: {
622
- min: {
623
- type: StringConstructor;
624
- default: string;
625
- };
626
- max: {
627
- type: StringConstructor;
628
- default: string;
629
- };
630
- };
631
- static actions: readonly ["applyPreset", "next", "onKeydown", "prev", "previewTo", "selectDate"];
632
- static events: readonly ["change"];
633
- readonly gridTarget: HTMLElement;
634
- readonly monthLabelTarget: HTMLElement;
635
- readonly cellTargets: HTMLElement[];
636
- readonly statusTarget: HTMLElement;
637
- readonly startFieldTarget: HTMLInputElement;
638
- readonly endFieldTarget: HTMLInputElement;
639
- readonly hasMonthLabelTarget: boolean;
640
- readonly hasStatusTarget: boolean;
641
- readonly hasStartFieldTarget: boolean;
642
- readonly hasEndFieldTarget: boolean;
643
- minValue: string;
644
- maxValue: string;
645
- /** Seeds the range from any pre-filled hidden fields and renders the grid. */
646
- connect(): void;
647
- /** Cancels any pending deferred focus so it never fires on a detached element. */
648
- disconnect(): void;
649
- /** Navigates to the previous month. */
650
- prev(event?: Event): void;
651
- /** Navigates to the next month. */
652
- next(event?: Event): void;
653
- /** Confirms a range endpoint from a clicked cell. */
654
- selectDate(event: Event): void;
655
- /** Previews the range up to a hovered/focused cell while selecting. */
656
- previewTo(event: Event): void;
657
- /** Applies a named preset (`today` / `last7` / `last30` / `thisMonth`). */
658
- applyPreset(event: Event): void;
659
- /** Grid keyboard navigation, selection (Enter/Space), and Escape-to-cancel. */
660
- onKeydown(event: KeyboardEvent): void;
661
- }
662
-
663
- /**
664
- * Headless "dismissible" behavior for banners, notices, and inline alerts.
665
- *
666
- * Markup contract (identifier: `stimeo--dismissible`):
667
- * <div data-controller="stimeo--dismissible"
668
- * data-stimeo--dismissible-mode-value="remove">
669
- * <div data-stimeo--dismissible-target="root" role="status">
670
- * <p>Saved.</p>
671
- * <button type="button" aria-label="Close"
672
- * data-action="stimeo--dismissible#dismiss">×</button>
673
- * </div>
674
- * </div>
675
- *
676
- * A general utility with no dedicated APG pattern. Its accessibility job is to
677
- * keep focus from being orphaned: if focus is inside the element being removed,
678
- * it is moved to a safe place first so the close button vanishing never strands
679
- * the user (WCAG 2.4.3).
680
- *
681
- * @remarks
682
- * Behavior only — the consumer owns any exit transition (use `hide` mode, which
683
- * adds `hidden`, and animate off `data-state`) and any semantics like
684
- * `role="alert"`. Focus retreats to, in order: the `fallback` target → the next
685
- * focusable element after the root → the previous one → `document.body` as a
686
- * last resort (weak focus; prefer providing a `fallback`).
687
- */
688
- declare class DismissibleController extends Controller<HTMLElement> {
689
- #private;
690
- static targets: string[];
691
- static values: {
692
- mode: {
693
- type: StringConstructor;
694
- default: string;
695
- };
696
- closeOnEscape: {
697
- type: BooleanConstructor;
698
- default: boolean;
699
- };
700
- };
701
- static actions: readonly ["dismiss"];
702
- static events: readonly ["dismiss"];
703
- readonly rootTarget: HTMLElement;
704
- readonly fallbackTarget: HTMLElement;
705
- readonly hasRootTarget: boolean;
706
- readonly hasFallbackTarget: boolean;
707
- modeValue: string;
708
- closeOnEscapeValue: boolean;
709
- connect(): void;
710
- disconnect(): void;
711
- /** Keeps the Escape listener aligned with a live `closeOnEscape` Value. */
712
- closeOnEscapeValueChanged(): void;
713
- /** Dismisses the element. Bound via `data-action` (click on the close button). */
714
- dismiss(): void;
715
- }
716
-
717
- /**
718
- * Headless, accessible **drawer / slide-over** behavior.
719
- *
720
- * Markup contract (identifier: `stimeo--drawer`):
721
- * <div data-controller="stimeo--drawer" data-stimeo--drawer-placement-value="right">
722
- * <button data-stimeo--drawer-target="trigger"
723
- * data-action="click->stimeo--drawer#open">Open panel</button>
724
- * <div data-stimeo--drawer-target="overlay"
725
- * data-action="click->stimeo--drawer#closeOnBackdrop" hidden>
726
- * <div data-stimeo--drawer-target="panel" role="dialog" aria-modal="true"
727
- * aria-labelledby="t" data-state="closed" hidden>
728
- * <h2 id="t">…</h2>
729
- * <button data-action="click->stimeo--drawer#close">Close</button>
730
- * </div>
731
- * </div>
732
- * </div>
733
- *
734
- * Implements the WAI-ARIA APG **Dialog (Modal)** pattern. It is the same modal as
735
- * `stimeo--dialog`; what it adds is the state plumbing an enter/exit *slide*
736
- * needs: `data-state` (`open`/`closed`) is synced on the panel and overlay so CSS
737
- * can animate, and `hidden` is applied only *after* the close transition finishes
738
- * (so the exit animation can play). `placement` is reflected as `data-placement`
739
- * for the CSS to read — the controller never computes coordinates.
740
- *
741
- * @remarks
742
- * Behavior only. The modal lifecycle (focus trap, scroll lock, background
743
- * `inert`, focus restore, teardown reversal) is delegated to the shared
744
- * {@link FocusTrap}. Placement, slide direction, distance, and easing are all the
745
- * consumer's CSS — `data-placement` is merely a flag.
746
- *
747
- * Behavior provided:
748
- * - {@link open}/{@link close} toggle `data-state` and (deferred) `hidden`.
749
- * - On open, focus moves to the first focusable element in the panel.
750
- * - `Tab`/`Shift+Tab` cycle focus within the panel; `Escape` closes.
751
- * - {@link closeOnBackdrop} closes only when the overlay *itself* is clicked.
752
- */
753
- declare class DrawerController extends Controller<HTMLElement> {
754
- #private;
755
- static targets: string[];
756
- static values: {
757
- placement: {
758
- type: StringConstructor;
759
- default: string;
760
- };
761
- open: {
762
- type: BooleanConstructor;
763
- default: boolean;
764
- };
765
- };
766
- static actions: readonly ["close", "closeOnBackdrop", "open"];
767
- readonly triggerTarget: HTMLElement;
768
- readonly overlayTarget: HTMLElement;
769
- readonly panelTarget: HTMLElement;
770
- readonly hasTriggerTarget: boolean;
771
- readonly hasOverlayTarget: boolean;
772
- readonly hasPanelTarget: boolean;
773
- placementValue: string;
774
- openValue: boolean;
775
- /**
776
- * Reflects placement and establishes the initial open/closed state.
777
- *
778
- * The DOM is the source of truth on reconnect (Turbo cache restore / morph): a
779
- * restored snapshot whose panel is already `data-state="open"` stays open
780
- * rather than being re-derived from the declarative `open` Value (which would
781
- * close a user-opened drawer). The `open` Value only seeds a genuinely fresh
782
- * render. We normalize to a clean closed baseline first so {@link open} runs its
783
- * full reveal + trap activation — the {@link FocusTrap} is a fresh instance
784
- * after a reconnect and must be re-activated.
785
- */
786
- connect(): void;
787
- /** Reverts the modal side effects and pending hide if torn down while open. */
788
- disconnect(): void;
789
- /** Keeps `data-placement` in sync if the value changes at runtime. */
790
- placementValueChanged(): void;
791
- /** Opens the drawer: reveals it, syncs `data-state`, traps focus. */
792
- open(): void;
793
- /**
794
- * Closes the drawer: syncs `data-state` to start the exit transition, then
795
- * defers both `hidden` *and* the modal teardown (scroll lock / background
796
- * `inert` / focus restore) until the transition finishes — see
797
- * `#applyHidden`. This keeps the background inert and focus trapped while
798
- * the drawer is still visually on screen, preserving the modal contract during
799
- * the exit animation.
800
- */
801
- close(): void;
802
- /** Closes only when the overlay itself (not its contents) is clicked. */
803
- closeOnBackdrop(event: MouseEvent): void;
804
- }
805
-
806
- /**
807
- * Headless, accessible inline-editing behavior.
808
- *
809
- * Markup contract (identifier: `stimeo--editable`):
810
- * <div data-controller="stimeo--editable"
811
- * data-stimeo--editable-submit-on-blur-value="true">
812
- * <button type="button" aria-label="Edit title"
813
- * data-stimeo--editable-target="display"
814
- * data-action="click->stimeo--editable#edit
815
- * keydown->stimeo--editable#onDisplayKeydown">Current title</button>
816
- * <input type="text" aria-label="Title" hidden
817
- * data-stimeo--editable-target="input"
818
- * data-action="keydown->stimeo--editable#onKeydown
819
- * blur->stimeo--editable#onBlur" />
820
- * </div>
821
- *
822
- * There is no dedicated APG pattern; this implements a display ⇄ edit toggle with
823
- * focus management and keyboard commit/cancel, leaning on native form-control
824
- * labeling (WCAG 1.3.1 / 4.1.2). The look is the consumer's CSS, keyed off
825
- * `data-mode`.
826
- *
827
- * Behavior provided:
828
- * - Activating the display element (`Enter`/`Space` via the `<button>`, or `F2`)
829
- * enters edit mode, focuses the input, and selects its text.
830
- * - `Enter` (single-line) or `Ctrl+Enter` / `Cmd+Enter` (multiline `<textarea>`)
831
- * saves; `Escape` cancels. Both return focus to the display element. Keys
832
- * pressed during an IME composition act on the composition, never the edit:
833
- * a conversion-cancelling `Escape` keeps editing and a conversion-confirming
834
- * `Enter` never saves (lifecycle-tracked, covering events that omit
835
- * `isComposing`).
836
- * - Losing focus while editing saves when `submitOnBlur` is true (the default),
837
- * honoring wherever focus moved; when false, editing is kept.
838
- * - Saving dispatches `stimeo--editable:change` with `{ value, previous }` only
839
- * when the value actually changed; cancelling dispatches
840
- * `stimeo--editable:cancel`.
841
- */
842
- declare class EditableController extends Controller<HTMLElement> {
843
- #private;
844
- static targets: string[];
845
- static values: {
846
- submitOnBlur: {
847
- type: BooleanConstructor;
848
- default: boolean;
849
- };
850
- };
851
- static actions: readonly ["edit", "onBlur", "onDisplayKeydown", "onKeydown"];
852
- static events: readonly ["cancel", "change"];
853
- readonly displayTarget: HTMLElement;
854
- readonly inputTarget: HTMLInputElement | HTMLTextAreaElement;
855
- readonly hasDisplayTarget: boolean;
856
- readonly hasInputTarget: boolean;
857
- submitOnBlurValue: boolean;
858
- /** Establishes the initial display mode (display shown, input hidden). */
859
- connect(): void;
860
- /** Releases the composition listeners so nothing outlives the element. */
861
- disconnect(): void;
862
- /** Tracks an input added initially or after connect (e.g. a Turbo swap). */
863
- inputTargetConnected(input: HTMLElement): void;
864
- /** Removes composition listeners when the active input is replaced or removed. */
865
- inputTargetDisconnected(input: HTMLElement): void;
866
- /** Enters edit mode: seeds the input from the display text, focuses, selects. */
867
- edit(): void;
868
- /** Adds `F2` as an editing entry point alongside the button's native activation. */
869
- onDisplayKeydown(event: KeyboardEvent): void;
870
- /** Commits on `Enter` (or `Ctrl+Enter` when multiline) and cancels on `Escape`. */
871
- onKeydown(event: KeyboardEvent): void;
872
- /** Saves on blur when `submitOnBlur` is set; otherwise keeps editing. */
873
- onBlur(): void;
874
- }
875
-
876
- /**
877
- * Headless, accessible file drag-and-drop / upload field.
878
- *
879
- * Markup contract (identifier: `stimeo--file-dropzone`):
880
- * <div data-controller="stimeo--file-dropzone"
881
- * data-stimeo--file-dropzone-max-size-value="5242880">
882
- * <div data-stimeo--file-dropzone-target="zone"
883
- * data-action="dragover->stimeo--file-dropzone#onDragOver
884
- * dragleave->stimeo--file-dropzone#onDragLeave
885
- * drop->stimeo--file-dropzone#onDrop">
886
- * <button type="button" data-stimeo--file-dropzone-target="trigger"
887
- * data-action="click->stimeo--file-dropzone#openDialog">Choose…</button>
888
- * <input type="file" accept="image/*" multiple class="visually-hidden"
889
- * data-stimeo--file-dropzone-target="input"
890
- * data-action="change->stimeo--file-dropzone#onChange" />
891
- * </div>
892
- * <ul data-stimeo--file-dropzone-target="list" aria-label="Selected files"></ul>
893
- * <span role="status" aria-live="polite" class="visually-hidden"
894
- * data-stimeo--file-dropzone-target="status"></span>
895
- * <template data-stimeo--file-dropzone-target="itemTemplate">…</template>
896
- * </div>
897
- *
898
- * There is no single APG pattern; the native `<input type="file">` stays the
899
- * primary, keyboard-operable path and the drop zone is an enhancement, mapping to
900
- * WCAG 2.1.1, 2.4.7, 4.1.2, 4.1.3, and 1.4.1 (drag state is conveyed in words,
901
- * not color alone).
902
- *
903
- * Behavior provided:
904
- * - Click / keyboard via the `trigger` opens the native file dialog; drag-and-drop
905
- * over the `zone` adds files (with a `data-dragover` flag and a spoken hint).
906
- * - Each file is validated against `accept`, `maxSize`, and the file count
907
- * (`maxFiles`, or 1 when the input is not `multiple`); rejects fire
908
- * `stimeo--file-dropzone:reject` and set `data-…-invalid`.
909
- * - Accepted files render from `itemTemplate` with a `Remove {name}` button and an
910
- * image thumbnail (`objectURL`); every change dispatches
911
- * `stimeo--file-dropzone:change` with the current `File[]`.
912
- * - Removing a file revokes its `objectURL` and moves focus to the next (else
913
- * previous) remove button, falling back to the trigger; `disconnect()` revokes
914
- * every outstanding `objectURL`.
915
- */
916
- declare class FileDropzoneController extends Controller<HTMLElement> {
917
- #private;
918
- static targets: string[];
919
- static values: {
920
- maxSize: {
921
- type: NumberConstructor;
922
- default: number;
923
- };
924
- maxFiles: {
925
- type: NumberConstructor;
926
- default: number;
927
- };
928
- dragLabel: {
929
- type: StringConstructor;
930
- default: string;
931
- };
932
- };
933
- static actions: readonly ["onChange", "onDragLeave", "onDragOver", "onDrop", "openDialog"];
934
- static events: readonly ["change", "reject"];
935
- readonly inputTarget: HTMLInputElement;
936
- readonly listTarget: HTMLElement;
937
- readonly triggerTarget: HTMLElement;
938
- readonly itemTemplateTarget: HTMLTemplateElement;
939
- readonly statusTarget: HTMLElement;
940
- readonly hasListTarget: boolean;
941
- readonly hasTriggerTarget: boolean;
942
- readonly hasItemTemplateTarget: boolean;
943
- readonly hasStatusTarget: boolean;
944
- readonly hasZoneTarget: boolean;
945
- readonly zoneTarget: HTMLElement;
946
- maxSizeValue: number;
947
- maxFilesValue: number;
948
- dragLabelValue: string;
949
- /** Wires file removal as a delegated listener on the list container. */
950
- connect(): void;
951
- /** Revokes any outstanding preview URLs so none leaks across navigations. */
952
- disconnect(): void;
953
- /** Opens the native file dialog. Bound via `data-action` (trigger click). */
954
- openDialog(): void;
955
- /** Adds the files chosen through the native dialog. */
956
- onChange(): void;
957
- /** Marks the zone as a drop target and announces the affordance in words. */
958
- onDragOver(event: DragEvent): void;
959
- /** Clears the drag-over flag when the pointer leaves the zone. */
960
- onDragLeave(): void;
961
- /** Accepts dropped files, clearing the drag-over state. */
962
- onDrop(event: DragEvent): void;
963
- }
964
-
965
- /** A control whose "on" state contributes its token to the active filter set. */
966
- type FilterControl = HTMLElement;
967
- /**
968
- * Headless faceted-filter behavior: shows or hides a collection of items based on the
969
- * set of currently-active facet tokens, decoupled from how those tokens are toggled
970
- * (native checkboxes/radios, or button toggles such as `stimeo--toggle-group`).
971
- *
972
- * Markup contract (identifier: `stimeo--filter`):
973
- * <div data-controller="stimeo--filter" data-stimeo--filter-match-value="all">
974
- * <!-- controls: each contributes its token while "on" (aria-pressed / checked) -->
975
- * <button type="button" aria-pressed="false" data-value="apg:dialog"
976
- * data-stimeo--filter-target="control">Dialog</button>
977
- * <!-- items: filtered in/out by their space-separated tokens -->
978
- * <li data-stimeo--filter-target="item"
979
- * data-stimeo--filter-tokens="apg:dialog overlay">…</li>
980
- * <!-- optional: a container hidden once it holds no visible item -->
981
- * <section data-stimeo--filter-target="group">…</section>
982
- * <!-- optional: an element revealed when nothing matches -->
983
- * <p data-stimeo--filter-target="empty" hidden>No matches</p>
984
- * </div>
985
- *
986
- * `match` decides how multiple active tokens combine: `all` (default — an item must
987
- * carry every active token) or `any` (at least one). With no active token every item
988
- * is shown. Items are toggled via `hidden`; groups with zero visible items and the
989
- * `empty` element are kept in sync, and `stimeo--filter:change` is dispatched.
990
- *
991
- * @remarks
992
- * Behavior only — the consumer owns all styling and the controls' own accessible
993
- * state (e.g. `aria-pressed`). Visibility is re-derived from the live DOM on
994
- * `connect()` (Morph/Turbo-safe) and on every native `change` that bubbles to the
995
- * root; button toggles that emit no native `change` wire their event to the `apply`
996
- * action (e.g. `stimeo--toggle-group:change->stimeo--filter#apply`).
997
- */
998
- declare class FilterController extends Controller<HTMLElement> {
999
- #private;
1000
- static targets: string[];
1001
- static values: {
1002
- match: {
1003
- type: StringConstructor;
1004
- default: string;
1005
- };
1006
- };
1007
- static actions: readonly ["apply", "clear"];
1008
- static events: readonly ["change"];
1009
- readonly itemTargets: HTMLElement[];
1010
- readonly controlTargets: FilterControl[];
1011
- readonly groupTargets: HTMLElement[];
1012
- readonly emptyTargets: HTMLElement[];
1013
- matchValue: string;
1014
- connect(): void;
1015
- disconnect(): void;
1016
- /** Re-derives every item's visibility from the active tokens and syncs groups/empty. */
1017
- apply(): void;
1018
- /** Turns every control off (uncheck / aria-pressed="false") and re-applies. */
1019
- clear(): void;
1020
- }
1021
-
1022
- /**
1023
- * Headless **Masonry** layout helper: assigns each item to the shortest column so
1024
- * variable-height cards pack without vertical gaps. There is no APG widget — this
1025
- * is a layout-only utility that emits state hooks, never visual structure.
1026
- *
1027
- * Markup contract (identifier: `stimeo--masonry`):
1028
- * <div data-controller="stimeo--masonry"
1029
- * data-stimeo--masonry-min-column-width-value="240"
1030
- * data-stimeo--masonry-gap-value="16">
1031
- * <div data-stimeo--masonry-target="item">…</div>
1032
- * <div data-stimeo--masonry-target="item">…</div>
1033
- * </div>
1034
- *
1035
- * The column count is derived responsively from the container width and
1036
- * `minColumnWidth`; each item is then placed into whichever column is currently
1037
- * shortest (measured from item heights). The count is published on the controller
1038
- * element as the `--stimeo-masonry-columns` custom property and each item gets a
1039
- * `data-column` index, so the consumer's CSS owns the actual placement.
1040
- *
1041
- * @remarks
1042
- * Behavior only. **DOM order is never changed** — reading order and focus order
1043
- * stay the source markup order (WCAG 1.3.2). The visual packing is purely the
1044
- * column assignment a consumer reads from `data-column`; this controller writes no
1045
- * positioning styles. Re-layout runs on connect, on resize ({@link LayoutObserver}),
1046
- * and on item add/remove ({@link MutationObserver}); both observers are released on
1047
- * `disconnect()` (Turbo navigation included). Use only for independent cards whose
1048
- * visual order carries no meaning.
1049
- */
1050
- declare class MasonryController extends Controller<HTMLElement> {
1051
- #private;
1052
- static targets: string[];
1053
- static values: {
1054
- minColumnWidth: {
1055
- type: NumberConstructor;
1056
- default: number;
1057
- };
1058
- gap: {
1059
- type: NumberConstructor;
1060
- default: number;
1061
- };
1062
- };
1063
- static events: readonly ["layout"];
1064
- readonly itemTargets: HTMLElement[];
1065
- minColumnWidthValue: number;
1066
- gapValue: number;
1067
- /** Observes size/content changes and performs the first layout pass. */
1068
- connect(): void;
1069
- /** Releases both observers and the load listener so nothing fires after detach. */
1070
- disconnect(): void;
1071
- }
1072
-
1073
- /**
1074
- * Headless, accessible **menubar** behavior.
1075
- *
1076
- * Markup contract (identifier: `stimeo--menubar`):
1077
- * <div data-controller="stimeo--menubar" role="menubar" aria-label="Main">
1078
- * <button role="menuitem" aria-haspopup="menu" aria-expanded="false"
1079
- * aria-controls="m-file" data-stimeo--menubar-target="top"
1080
- * data-action="click->stimeo--menubar#toggle
1081
- * keydown->stimeo--menubar#onTopKeydown">File</button>
1082
- * <ul id="m-file" role="menu" data-stimeo--menubar-target="menu" hidden>
1083
- * <li role="none">
1084
- * <button role="menuitem" tabindex="-1" data-stimeo--menubar-target="item"
1085
- * data-action="click->stimeo--menubar#activate
1086
- * keydown->stimeo--menubar#onItemKeydown">New</button>
1087
- * </li>
1088
- * </ul>
1089
- * <!-- more top items + menus -->
1090
- * </div>
1091
- *
1092
- * Implements the WAI-ARIA APG **Menubar** pattern (single level): the top items
1093
- * form one Tab stop via roving tabindex and the arrow keys move between them;
1094
- * `ArrowDown`/`Enter`/`Space` open a menu (`ArrowUp` opens it at the last item),
1095
- * the arrow keys then move within the menu, and pressing `ArrowLeft`/`ArrowRight`
1096
- * while a menu is open jumps to the adjacent top menu. `Escape` closes and
1097
- * returns focus to the owning top item — while a menu is open the menubar is a
1098
- * layer on the shared {@link EscapeLayer} stack, claiming a press only while
1099
- * focus is inside the controller or fell to the body, so one keypress closes
1100
- * exactly one layer (the shared layered-Escape contract). `Tab` and an outside
1101
- * click close.
1102
- *
1103
- * @remarks
1104
- * Behavior only. Each top item↔menu pair is linked by `aria-controls`/`id` (not by
1105
- * position), so the markup order is free. Menu placement and viewport-edge
1106
- * collision are out of scope — static placement is the consumer's CSS, and dynamic
1107
- * placement is delegated to the opt-in `stimeo-ui/positioning` module (never
1108
- * imported here, keeping the core zero-dependency). Roving mechanics across the top
1109
- * items are delegated to {@link RovingTabindex}.
1110
- */
1111
- declare class MenubarController extends Controller<HTMLElement> {
1112
- #private;
1113
- static targets: string[];
1114
- static actions: readonly ["activate", "onItemKeydown", "onTopKeydown", "toggle"];
1115
- readonly topTargets: HTMLElement[];
1116
- readonly menuTargets: HTMLElement[];
1117
- readonly itemTargets: HTMLElement[];
1118
- /** Establishes the single tab stop and the closed baseline. */
1119
- connect(): void;
1120
- /** Removes the document listener, stack membership, and any pending typeahead timer. */
1121
- disconnect(): void;
1122
- /** Toggles a top item's menu. Bound via `data-action` (click on the top item). */
1123
- toggle(event: Event): void;
1124
- /** Keyboard handling while focus is on a top item. */
1125
- onTopKeydown(event: KeyboardEvent): void;
1126
- /** Keyboard handling while focus is on a menu item. */
1127
- onItemKeydown(event: KeyboardEvent): void;
1128
- /** Closes the owning menu after an item is activated and refocuses its top. */
1129
- activate(event: Event): void;
1130
- }
1131
-
1132
- /**
1133
- * Headless, accessible multi-select combobox with chips.
1134
- *
1135
- * Markup contract (identifier: `stimeo--multi-select`):
1136
- * <div data-controller="stimeo--multi-select">
1137
- * <ul data-stimeo--multi-select-target="tags" aria-label="Selected"></ul>
1138
- * <input type="text" role="combobox" aria-expanded="false"
1139
- * aria-autocomplete="list" aria-controls="ms-list"
1140
- * data-stimeo--multi-select-target="input"
1141
- * data-action="input->stimeo--multi-select#filter
1142
- * keydown->stimeo--multi-select#onKeydown
1143
- * focus->stimeo--multi-select#open" />
1144
- * <ul id="ms-list" role="listbox" aria-multiselectable="true" hidden
1145
- * data-stimeo--multi-select-target="list">
1146
- * <li id="ms-opt-1" role="option" aria-selected="false" data-value="apple"
1147
- * data-stimeo--multi-select-target="option"
1148
- * data-action="click->stimeo--multi-select#toggleOption">Apple</li>
1149
- * </ul>
1150
- * <span role="status" aria-live="polite" class="visually-hidden"
1151
- * data-stimeo--multi-select-target="status"></span>
1152
- * <!-- Optional: submit the selection as name="fruits[]" hidden inputs. -->
1153
- * <div data-stimeo--multi-select-target="fields"></div>
1154
- * <template data-stimeo--multi-select-target="tagTemplate">…</template>
1155
- * </div>
1156
- *
1157
- * Implements the WAI-ARIA APG **Combobox** pattern in its list-autocomplete,
1158
- * multi-select form. Focus stays on the input; the active option is tracked with
1159
- * `aria-activedescendant` and selection with `aria-selected`. Selected options are
1160
- * mirrored as removable chips. For single selection use {@link ListboxController}
1161
- * or Combobox; for free-text tags use {@link TagsInputController}.
1162
- *
1163
- * Behavior provided:
1164
- * - Typing filters options by substring and opens the list; `ArrowDown`/`ArrowUp`
1165
- * (wrapping), `Home`/`End` move the active option; `Enter` toggles it (the list
1166
- * stays open); `Escape`/`Tab`/outside click close.
1167
- * - Toggling syncs `aria-selected`, adds/removes a `Remove {label}` chip, mirrors
1168
- * the live region, and dispatches `stimeo--multi-select:change` with `values`;
1169
- * filtering dispatches `stimeo--multi-select:filter` for async candidates.
1170
- * - The chips are one roving Tab stop: `ArrowLeft`/`ArrowRight` move between them,
1171
- * `Delete`/`Backspace` remove the focused chip, and `Backspace` on an empty
1172
- * input removes the last; removal re-homes focus to a neighbor or the input.
1173
- * - `max` caps the selection (`0` = unlimited).
1174
- * - With a `fields` target the selected values are mirrored into named hidden
1175
- * inputs (default name `options[]`), so the selection submits with a normal
1176
- * form and no consumer JS — parity with {@link TagsInputController}. An optional
1177
- * `form` value sets the hidden inputs' `form` attribute, associating them with a
1178
- * `<form>` by id even when the picker lives outside it. Purely additive: with no
1179
- * `fields` target the behavior is unchanged.
1180
- */
1181
- declare class MultiSelectController extends Controller<HTMLElement> {
1182
- #private;
1183
- static targets: string[];
1184
- static values: {
1185
- max: {
1186
- type: NumberConstructor;
1187
- default: number;
1188
- };
1189
- name: {
1190
- type: StringConstructor;
1191
- default: string;
1192
- };
1193
- form: {
1194
- type: StringConstructor;
1195
- default: string;
1196
- };
1197
- };
1198
- static actions: readonly ["close", "filter", "onKeydown", "open", "toggleOption"];
1199
- static events: readonly ["change", "filter"];
1200
- readonly inputTarget: HTMLInputElement;
1201
- readonly listTarget: HTMLElement;
1202
- readonly optionTargets: HTMLElement[];
1203
- readonly tagsTarget: HTMLElement;
1204
- readonly tagTargets: HTMLElement[];
1205
- readonly tagTemplateTarget: HTMLTemplateElement;
1206
- readonly statusTarget: HTMLElement;
1207
- readonly fieldsTarget: HTMLElement;
1208
- readonly hasListTarget: boolean;
1209
- readonly hasInputTarget: boolean;
1210
- readonly hasTagsTarget: boolean;
1211
- readonly hasTagTemplateTarget: boolean;
1212
- readonly hasStatusTarget: boolean;
1213
- readonly hasFieldsTarget: boolean;
1214
- maxValue: number;
1215
- nameValue: string;
1216
- formValue: string;
1217
- readonly hasFormValue: boolean;
1218
- /** Starts closed, syncs chips for any pre-selected options, and listens out. */
1219
- connect(): void;
1220
- /** Tears down document and chip listeners on disconnect (Turbo included). */
1221
- disconnect(): void;
1222
- /** Tracks an input added initially or after connect. */
1223
- inputTargetConnected(input: HTMLInputElement): void;
1224
- /** Removes composition listeners when the active input is replaced or removed. */
1225
- inputTargetDisconnected(input: HTMLInputElement): void;
1226
- /** Filters confirmed input text, opens, and re-seeds the active option. */
1227
- filter(event?: InputEvent): void;
1228
- /** Opens the list and activates the first visible option when none is active. */
1229
- open(): void;
1230
- /** Closes the list and clears the active option. */
1231
- close(): void;
1232
- /** Routes input keyboard interaction per the multi-select combobox model. */
1233
- onKeydown(event: KeyboardEvent): void;
1234
- /**
1235
- * Toggles the clicked option's selection. Bound via `data-action`. Focus is
1236
- * re-homed to the input afterwards: options are non-focusable, so the click blurs
1237
- * the input to `body` — and with the list deliberately staying open, every
1238
- * keyboard affordance (Escape, arrows, typing) is bound to the input and would
1239
- * otherwise go dead until the user clicks back in ("focus stays on the input").
1240
- */
1241
- toggleOption(event: Event): void;
1242
- }
1243
-
1244
- /**
1245
- * Headless, accessible **navigation menu** behavior (disclosure navigation).
1246
- *
1247
- * Markup contract (identifier: `stimeo--navigation-menu`):
1248
- * <nav data-controller="stimeo--navigation-menu" aria-label="Main">
1249
- * <ul>
1250
- * <li>
1251
- * <button data-stimeo--navigation-menu-target="trigger"
1252
- * aria-expanded="false" aria-controls="nav-products"
1253
- * data-action="click->stimeo--navigation-menu#toggle
1254
- * keydown->stimeo--navigation-menu#onTriggerKeydown">
1255
- * Products
1256
- * </button>
1257
- * <div id="nav-products" data-stimeo--navigation-menu-target="panel" hidden>
1258
- * <a href="/a">Product A</a><a href="/b">Product B</a>
1259
- * </div>
1260
- * </li>
1261
- * </ul>
1262
- * </nav>
1263
- *
1264
- * Implements the WAI-ARIA APG **Disclosure** navigation pattern: each top-level
1265
- * button toggles its sub-panel (`aria-expanded` + `hidden` synced), only one panel
1266
- * is open at a time, and the panel content is a plain set of links (not a
1267
- * `role="menu"`). Focus is **not** trapped — `Tab` moves through the links
1268
- * naturally. `Escape` closes the open panel and returns focus to its trigger.
1269
- * While a panel is open the nav is a layer on the shared {@link EscapeLayer}
1270
- * stack; it claims a press only while focus is inside the nav or fell to the
1271
- * body (a click on non-focusable panel content), so one keypress closes exactly
1272
- * one layer (the shared layered-Escape contract). An outside click, or focus
1273
- * leaving the nav for a known external destination, closes it. `ArrowLeft`/
1274
- * `ArrowRight` move focus between triggers without rewriting `tabindex` (they
1275
- * keep their natural Tab order). Hover open/close is opt-in via `openOnHover`.
1276
- *
1277
- * By default the hover region is each trigger and its panel. An optional
1278
- * `hoverArea` target widens it: mark a wrapper that contains a trigger (e.g. the
1279
- * `<li>` holding a top-level *link*, its disclosure button, and its panel — the
1280
- * APG "Disclosure Navigation with Top-Level Links" arrangement) and hovering
1281
- * anywhere over that wrapper opens the contained trigger's panel. A trigger or
1282
- * panel inside a `hoverArea` defers to the wrapper (its own edges stop scheduling
1283
- * open/close), so pointer movement within the area never flickers the panel.
1284
- * Keep a trigger's panel inside its `hoverArea`: a panel left outside simply
1285
- * falls back to the default two-region behavior — moving straight across a
1286
- * shared edge never schedules a close (the leave handler checks
1287
- * `relatedTarget`), and `hoverDelay` bridges the pointer's travel across an
1288
- * actual gap. Targets added or removed while connected (e.g. a Turbo Stream
1289
- * append) re-wire the hover listeners via Stimulus target callbacks.
1290
- * `hoverArea` has no effect unless `openOnHover` is enabled.
1291
- *
1292
- * @remarks
1293
- * Behavior only. Panel layout, mega-menu styling, and animation are the
1294
- * consumer's CSS. Static placement is CSS; viewport-edge collision avoidance is
1295
- * delegated to the opt-in `stimeo-ui/positioning` module (never imported here, so
1296
- * the core stays zero-dependency). For an app command menu with arrow roving and
1297
- * `role="menu"`, use `stimeo--menubar` instead.
1298
- */
1299
- declare class NavigationMenuController extends Controller<HTMLElement> {
1300
- #private;
1301
- static targets: string[];
1302
- static values: {
1303
- openOnHover: {
1304
- type: BooleanConstructor;
1305
- default: boolean;
1306
- };
1307
- hoverDelay: {
1308
- type: NumberConstructor;
1309
- default: number;
1310
- };
1311
- };
1312
- static actions: readonly ["onTriggerKeydown", "toggle"];
1313
- readonly triggerTargets: HTMLElement[];
1314
- readonly panelTargets: HTMLElement[];
1315
- readonly hoverAreaTargets: HTMLElement[];
1316
- openOnHoverValue: boolean;
1317
- hoverDelayValue: number;
1318
- /** Establishes the closed baseline and the dismissal listeners. */
1319
- connect(): void;
1320
- /** Removes every listener, pending hover timer, and stack membership registered while connected. */
1321
- disconnect(): void;
1322
- /** Re-wires hover listeners when a target is added after connect (Turbo Streams etc.). */
1323
- triggerTargetConnected(): void;
1324
- /** Re-wires hover listeners when a target is removed while connected. */
1325
- triggerTargetDisconnected(): void;
1326
- /** See {@link NavigationMenuController.triggerTargetConnected}. */
1327
- panelTargetConnected(): void;
1328
- /** See {@link NavigationMenuController.triggerTargetDisconnected}. */
1329
- panelTargetDisconnected(): void;
1330
- /** See {@link NavigationMenuController.triggerTargetConnected}. */
1331
- hoverAreaTargetConnected(): void;
1332
- /** See {@link NavigationMenuController.triggerTargetDisconnected}. */
1333
- hoverAreaTargetDisconnected(): void;
1334
- /** Toggles a trigger's panel (single-open). Bound via `data-action` (click). */
1335
- toggle(event: Event): void;
1336
- /** `ArrowLeft`/`ArrowRight` move focus between triggers (keeping Tab order). */
1337
- onTriggerKeydown(event: KeyboardEvent): void;
1338
- }
1339
-
1340
- /**
1341
- * Headless password show/hide (unmask) toggle behavior.
1342
- *
1343
- * Markup contract (identifier: `stimeo--password-reveal`):
1344
- * <div data-controller="stimeo--password-reveal">
1345
- * <input type="password" aria-label="Password"
1346
- * data-stimeo--password-reveal-target="input">
1347
- * <button type="button" aria-pressed="false" aria-label="Show password"
1348
- * data-stimeo--password-reveal-target="toggle"
1349
- * data-action="stimeo--password-reveal#toggle"></button>
1350
- * </div>
1351
- *
1352
- * No dedicated APG pattern; this follows the toggle **Button** practice. The
1353
- * accessible name stays state-independent ("Show password") while the pressed
1354
- * state is conveyed by `aria-pressed`.
1355
- *
1356
- * @remarks
1357
- * Behavior only — icon rendering is the consumer's, keyed off `aria-pressed` /
1358
- * `data-state` (`hidden` / `visible`). Flipping `input.type` can drop focus and
1359
- * the caret, so when (and only when) the input was the focused element its focus
1360
- * and selection are restored afterward; when the toggle button holds focus
1361
- * (keyboard use) focus is left on the button. An optional `autoHide` re-masks
1362
- * after a delay, and that timer is torn down on disconnect (Turbo).
1363
- */
1364
- declare class PasswordRevealController extends Controller<HTMLElement> {
1365
- #private;
1366
- static targets: string[];
1367
- static values: {
1368
- autoHide: {
1369
- type: NumberConstructor;
1370
- default: number;
1371
- };
1372
- };
1373
- static actions: readonly ["toggle"];
1374
- static events: readonly ["toggle"];
1375
- readonly inputTarget: HTMLInputElement;
1376
- readonly toggleTarget: HTMLElement;
1377
- readonly hasInputTarget: boolean;
1378
- readonly hasToggleTarget: boolean;
1379
- autoHideValue: number;
1380
- connect(): void;
1381
- disconnect(): void;
1382
- /** Toggles the input between masked and revealed. Bound via `data-action`. */
1383
- toggle(): void;
1384
- }
1385
-
1386
- /**
1387
- * Headless, accessible **two-thumb range slider** behavior (APG Slider —
1388
- * Multi-Thumb). A derivative of {@link SliderController}: it manages two thumbs
1389
- * (`start` ≤ `end`) that constrain each other.
1390
- *
1391
- * Markup contract (identifier: `stimeo--range-slider`):
1392
- * <div data-controller="stimeo--range-slider"
1393
- * data-stimeo--range-slider-min-value="0"
1394
- * data-stimeo--range-slider-max-value="100"
1395
- * data-stimeo--range-slider-step-value="1"
1396
- * data-stimeo--range-slider-start-value="20"
1397
- * data-stimeo--range-slider-end-value="80">
1398
- * <div data-stimeo--range-slider-target="track"
1399
- * data-action="pointerdown->stimeo--range-slider#onPointerDown">
1400
- * <div role="slider" tabindex="0" aria-label="Minimum"
1401
- * data-stimeo--range-slider-target="startThumb"
1402
- * data-action="keydown->stimeo--range-slider#onKeydown"></div>
1403
- * <div role="slider" tabindex="0" aria-label="Maximum"
1404
- * data-stimeo--range-slider-target="endThumb"
1405
- * data-action="keydown->stimeo--range-slider#onKeydown"></div>
1406
- * </div>
1407
- * </div>
1408
- *
1409
- * @remarks
1410
- * Behavior only — the consumer owns all layout (positioning the thumbs and the
1411
- * selected range from the fractions). Only the horizontal orientation is handled
1412
- * in this MVP. Each thumb's movable range is bounded by the *other* thumb's
1413
- * current value, reflected on its `aria-valuemin`/`aria-valuemax` so assistive
1414
- * tech announces the live constraint.
1415
- *
1416
- * Behavior provided (per focused thumb):
1417
- * - `ArrowRight`/`ArrowUp` increase and `ArrowLeft`/`ArrowDown` decrease by one
1418
- * step; `Home`/`End` jump to that thumb's movable min/max; `PageUp`/`PageDown`
1419
- * move by ten steps. A thumb never crosses the other.
1420
- * - Pointer press/drag on the track moves the nearest thumb.
1421
- */
1422
- declare class RangeSliderController extends Controller<HTMLElement> {
1423
- #private;
1424
- static targets: string[];
1425
- static values: {
1426
- min: {
1427
- type: NumberConstructor;
1428
- default: number;
1429
- };
1430
- max: {
1431
- type: NumberConstructor;
1432
- default: number;
1433
- };
1434
- step: {
1435
- type: NumberConstructor;
1436
- default: number;
1437
- };
1438
- start: {
1439
- type: NumberConstructor;
1440
- default: number;
1441
- };
1442
- end: {
1443
- type: NumberConstructor;
1444
- default: number;
1445
- };
1446
- };
1447
- static actions: readonly ["onKeydown", "onPointerDown"];
1448
- static events: readonly ["change"];
1449
- readonly trackTarget: HTMLElement;
1450
- readonly startThumbTarget: HTMLElement;
1451
- readonly endThumbTarget: HTMLElement;
1452
- readonly hasTrackTarget: boolean;
1453
- readonly hasStartThumbTarget: boolean;
1454
- readonly hasEndThumbTarget: boolean;
1455
- minValue: number;
1456
- maxValue: number;
1457
- stepValue: number;
1458
- startValue: number;
1459
- endValue: number;
1460
- /** Normalizes the initial pair (clamped, snapped, ordered) and renders. */
1461
- connect(): void;
1462
- /** Cancels any active pointer drag so document listeners never leak. */
1463
- disconnect(): void;
1464
- /** Keyboard stepping for whichever thumb is focused (the action's element). */
1465
- onKeydown(event: KeyboardEvent): void;
1466
- /** Begins a pointer drag on the track, moving the thumb nearest the press. */
1467
- onPointerDown(event: PointerEvent): void;
1468
- }
1469
-
1470
- /**
1471
- * Headless "read more / truncate" behavior for visually clamped text.
1472
- *
1473
- * Markup contract (identifier: `stimeo--read-more`):
1474
- * <div data-controller="stimeo--read-more">
1475
- * <p id="bio" data-stimeo--read-more-target="content" data-state="collapsed">…</p>
1476
- * <button data-stimeo--read-more-target="trigger"
1477
- * data-action="stimeo--read-more#toggle"
1478
- * aria-expanded="false" aria-controls="bio" hidden>Read more</button>
1479
- * </div>
1480
- *
1481
- * There is no dedicated APG widget; the toggle borrows the **Disclosure**
1482
- * convention (`aria-expanded`). The visual clamp itself (`-webkit-line-clamp`
1483
- * etc.) is the consumer's CSS, keyed off `data-state`.
1484
- *
1485
- * @remarks
1486
- * Behavior only. The full text always stays in the DOM — the clamp is purely
1487
- * visual, so assistive technology reads everything regardless of state; here
1488
- * `aria-expanded` therefore signals the *visual* expansion, not content hidden
1489
- * from AT. The controller's extra job is **overflow detection**: when the
1490
- * content is not actually clamped (it fits), the toggle is `hidden` so no
1491
- * pointless "read more" is offered. This is re-evaluated on resize.
1492
- */
1493
- declare class ReadMoreController extends Controller<HTMLElement> {
1494
- #private;
1495
- static targets: string[];
1496
- static values: {
1497
- collapsed: {
1498
- type: BooleanConstructor;
1499
- default: boolean;
1500
- };
1501
- };
1502
- static actions: readonly ["toggle"];
1503
- readonly contentTarget: HTMLElement;
1504
- readonly triggerTarget: HTMLElement;
1505
- readonly hasContentTarget: boolean;
1506
- readonly hasTriggerTarget: boolean;
1507
- collapsedValue: boolean;
1508
- connect(): void;
1509
- disconnect(): void;
1510
- /** Toggles between the collapsed (clamped) and expanded states. */
1511
- toggle(): void;
1512
- }
1513
-
1514
- /**
1515
- * Headless **Scroll Restore** behavior: persists and restores the scroll
1516
- * position of an inner scrolling region across Turbo Drive navigations (and full
1517
- * reloads within the same tab session). No APG widget — a pure state-preservation
1518
- * utility, like {@link import("./scroll_visibility_controller").ScrollVisibilityController}
1519
- * and the Sticky State Observer.
1520
- *
1521
- * Markup contract (identifier: `stimeo--scroll-restore`):
1522
- * <div data-controller="stimeo--scroll-restore"
1523
- * data-stimeo--scroll-restore-key-value="sidebar"
1524
- * style="overflow: auto">
1525
- * …long content…
1526
- * </div>
1527
- *
1528
- * Turbo swaps the whole `<body>` on navigation, so an inner scroll container is
1529
- * rebuilt with `scrollTop` reset to 0. Rather than have every app hand-write a
1530
- * controller for this (which contradicts a "ship the behavior" library), this
1531
- * persists the offset under a stable key in `sessionStorage` and restores it on
1532
- * `connect()`.
1533
- *
1534
- * @remarks
1535
- * Behavior only — it sets no ARIA/`data-*`/CSS and never moves focus (restore is
1536
- * a plain `scrollTop`/`scrollLeft` assignment). The `scroll` listener is internal
1537
- * and `passive` (no consumer `data-action` needed): each event records the live
1538
- * offset synchronously, and a `requestAnimationFrame` coalesces the writes to
1539
- * `sessionStorage`. On `disconnect()` (Turbo navigation included) it flushes the
1540
- * **last captured** offset rather than re-reading the element — by the time Turbo
1541
- * fires `disconnect` it has already detached the node, whose `scrollTop` then
1542
- * reads `0`, so a fresh read would clobber the saved position with `0`. Keying by
1543
- * `key` (falling back to the element `id`) in `sessionStorage` makes it
1544
- * multi-instance safe and survives full reloads — unlike a module-scope variable,
1545
- * which only survives Turbo Drive and assumes a single instance.
1546
- */
1547
- declare class ScrollRestoreController extends Controller<HTMLElement> {
1548
- #private;
1549
- static values: {
1550
- key: {
1551
- type: StringConstructor;
1552
- default: string;
1553
- };
1554
- axis: {
1555
- type: StringConstructor;
1556
- default: string;
1557
- };
1558
- };
1559
- keyValue: string;
1560
- axisValue: string;
1561
- connect(): void;
1562
- disconnect(): void;
1563
- }
1564
-
1565
- /**
1566
- * Headless **Separator** behavior (APG Separator pattern).
1567
- *
1568
- * Markup contract (identifier: `stimeo--separator`):
1569
- * <!-- decorative -->
1570
- * <div data-controller="stimeo--separator" role="separator"
1571
- * data-stimeo--separator-orientation-value="horizontal"></div>
1572
- *
1573
- * <!-- focusable, value-bearing -->
1574
- * <div data-controller="stimeo--separator" role="separator" tabindex="0"
1575
- * aria-label="Resize sidebar" aria-orientation="vertical"
1576
- * aria-valuemin="0" aria-valuemax="100" aria-valuenow="50"
1577
- * data-stimeo--separator-focusable-value="true"
1578
- * data-action="keydown->stimeo--separator#onKeydown"></div>
1579
- *
1580
- * Most separators are static (`role="separator"` + `aria-orientation`); this
1581
- * controller adds those semantics and, for the optional **focusable** variant,
1582
- * keeps `aria-valuenow` in sync and emits arrow-key value changes. The actual
1583
- * pane resize/drag is out of scope — that belongs to {@link ResizableController}.
1584
- *
1585
- * @remarks
1586
- * Behavior only — line drawing is the consumer's CSS. Increment direction
1587
- * follows the slider convention: ArrowUp/ArrowRight increase, ArrowDown/ArrowLeft
1588
- * decrease, scoped to the relevant axis for the orientation.
1589
- */
1590
- declare class SeparatorController extends Controller<HTMLElement> {
1591
- #private;
1592
- static values: {
1593
- orientation: {
1594
- type: StringConstructor;
1595
- default: string;
1596
- };
1597
- focusable: {
1598
- type: BooleanConstructor;
1599
- default: boolean;
1600
- };
1601
- step: {
1602
- type: NumberConstructor;
1603
- default: number;
1604
- };
1605
- };
1606
- static actions: readonly ["onKeydown"];
1607
- static events: readonly ["change"];
1608
- orientationValue: string;
1609
- focusableValue: boolean;
1610
- stepValue: number;
1611
- connect(): void;
1612
- /** Adjusts the value on arrow / Home / End keys (focusable variant only). */
1613
- onKeydown(event: KeyboardEvent): void;
1614
- }
1615
-
1616
- /**
1617
- * Headless, accessible **responsive collapsible sidebar** behavior.
1618
- *
1619
- * Markup contract (identifier: `stimeo--sidebar`):
1620
- * <div data-controller="stimeo--sidebar"
1621
- * data-stimeo--sidebar-breakpoint-value="768"
1622
- * data-stimeo--sidebar-key-value="main-nav">
1623
- * <header>
1624
- * <button data-stimeo--sidebar-target="trigger"
1625
- * data-action="click->stimeo--sidebar#toggle"
1626
- * aria-expanded="true" aria-controls="app-sidebar">Menu</button>
1627
- * </header>
1628
- * <div data-stimeo--sidebar-target="backdrop"
1629
- * data-action="click->stimeo--sidebar#close" hidden></div>
1630
- * <aside id="app-sidebar" data-stimeo--sidebar-target="panel"
1631
- * aria-label="Main" data-mode="inline" data-state="expanded">
1632
- * <nav aria-label="…">…</nav>
1633
- * </aside>
1634
- * </div>
1635
- * <main>…</main> <!-- a body-level sibling so it can be made inert in overlay -->
1636
- *
1637
- * No dedicated APG pattern: the base is **Disclosure** (the trigger's
1638
- * `aria-expanded` controls the panel's expanded state) and, *below* the
1639
- * `breakpoint`, it borrows the **Dialog (Modal)** focus behavior via the shared
1640
- * {@link FocusTrap} (the same trap used by dialog / alert-dialog / drawer).
1641
- *
1642
- * Above the breakpoint it is an **inline**, non-modal element that toggles
1643
- * `expanded`↔`collapsed` (a rail), persisting that preference in `localStorage`.
1644
- * Below it, it becomes an **overlay** off-canvas panel: opening activates the
1645
- * trap (focus move, `Tab` cycle, `Escape`, body scroll lock, background `inert`,
1646
- * focus restore); closing defers `hidden` and the trap teardown until the exit
1647
- * transition ends (synchronously when there is none).
1648
- *
1649
- * @remarks
1650
- * Behavior only — rail width, slide, and backdrop are the consumer's CSS, keyed
1651
- * off `data-mode` (`inline`/`overlay`) and `data-state`. `aria-expanded` is an
1652
- * abstract "is the panel expanded" flag, independent of the visual difference
1653
- * between an inline collapsed rail (still in the DOM) and an overlay closed panel
1654
- * (`hidden`/off-canvas). The `role="dialog"` semantics are intentionally **not**
1655
- * applied — the sidebar stays an `<aside>`/`<nav>` landmark and only borrows the
1656
- * modal *behavior* (cf. drawer's note on `<aside role="dialog">` conflicts).
1657
- * The collapsed preference persists across Turbo navigations and full reloads;
1658
- * the transient overlay-open state never persists, so "back/forward" never
1659
- * restores a stuck-open menu.
1660
- */
1661
- declare class SidebarController extends Controller<HTMLElement> {
1662
- #private;
1663
- static targets: string[];
1664
- static values: {
1665
- breakpoint: {
1666
- type: NumberConstructor;
1667
- default: number;
1668
- };
1669
- key: {
1670
- type: StringConstructor;
1671
- default: string;
1672
- };
1673
- collapsed: {
1674
- type: BooleanConstructor;
1675
- default: boolean;
1676
- };
1677
- };
1678
- static actions: readonly ["close", "open", "toggle"];
1679
- readonly triggerTarget: HTMLElement;
1680
- readonly panelTarget: HTMLElement;
1681
- readonly backdropTarget: HTMLElement;
1682
- readonly hasTriggerTarget: boolean;
1683
- readonly hasPanelTarget: boolean;
1684
- readonly hasBackdropTarget: boolean;
1685
- breakpointValue: number;
1686
- keyValue: string;
1687
- collapsedValue: boolean;
1688
- connect(): void;
1689
- disconnect(): void;
1690
- /** Toggles the panel: inline flips collapsed/expanded, overlay flips open/closed. */
1691
- toggle(): void;
1692
- /** Shows the panel (inline: expand; overlay: open). */
1693
- open(): void;
1694
- /** Hides the panel (inline: collapse; overlay: close). */
1695
- close(): void;
1696
- }
1697
-
1698
- /**
1699
- * Headless, accessible free-input tags / chips field.
1700
- *
1701
- * Markup contract (identifier: `stimeo--tags-input`):
1702
- * <div data-controller="stimeo--tags-input"
1703
- * data-stimeo--tags-input-delimiter-value=",">
1704
- * <ul role="list" aria-label="Tags" data-stimeo--tags-input-target="tags"></ul>
1705
- * <input type="text" aria-label="Add tag" aria-describedby="tags-help"
1706
- * data-stimeo--tags-input-target="input"
1707
- * data-action="keydown->stimeo--tags-input#onKeydown" />
1708
- * <span role="status" aria-live="polite" class="visually-hidden"
1709
- * data-stimeo--tags-input-target="status"></span>
1710
- * <div data-stimeo--tags-input-target="fields"></div>
1711
- * <template data-stimeo--tags-input-target="tagTemplate">
1712
- * <li role="listitem" data-stimeo--tags-input-target="tag">
1713
- * <span data-tags-input-slot="label"></span>
1714
- * <!-- Removal is delegated on the tags container; no per-chip action needed. -->
1715
- * <button type="button" tabindex="-1">×</button>
1716
- * </li>
1717
- * </template>
1718
- * </div>
1719
- *
1720
- * There is no single established APG pattern; this composes a labeled text input
1721
- * with a roving-tabindex list of removable chips, mapping to WCAG 2.1.1, 2.4.7,
1722
- * 4.1.2, 4.1.3, and 1.3.1. Unlike {@link MultiSelectController} (pick from a
1723
- * candidate list) the user types arbitrary strings.
1724
- *
1725
- * Behavior provided:
1726
- * - `Enter` or the configured delimiter commits the trimmed input as a tag.
1727
- * - Empty / duplicate / over-limit additions are rejected with
1728
- * `stimeo--tags-input:reject`; duplicates are allowed when `allowDuplicates`.
1729
- * - Tags render from the `tagTemplate`, each with a `Remove {label}` button; the
1730
- * `fields` container mirrors the tag set as `name`d hidden inputs for form
1731
- * submission, and every change dispatches `stimeo--tags-input:change`.
1732
- * - The remove buttons form one roving Tab stop: `ArrowLeft`/`ArrowRight` move
1733
- * between them (right past the end returns to the input), `Delete`/`Backspace`
1734
- * delete the focused tag, and `Backspace` on an empty input deletes the last.
1735
- * - Removing a tag moves focus to the neighboring tag, else back to the input.
1736
- */
1737
- declare class TagsInputController extends Controller<HTMLElement> {
1738
- #private;
1739
- static targets: string[];
1740
- static values: {
1741
- delimiter: {
1742
- type: StringConstructor;
1743
- default: string;
1744
- };
1745
- max: {
1746
- type: NumberConstructor;
1747
- default: number;
1748
- };
1749
- allowDuplicates: {
1750
- type: BooleanConstructor;
1751
- default: boolean;
1752
- };
1753
- name: {
1754
- type: StringConstructor;
1755
- default: string;
1756
- };
1757
- };
1758
- static actions: readonly ["onKeydown"];
1759
- static events: readonly ["change", "reject"];
1760
- readonly inputTarget: HTMLInputElement;
1761
- readonly tagsTarget: HTMLElement;
1762
- readonly tagTargets: HTMLElement[];
1763
- readonly tagTemplateTarget: HTMLTemplateElement;
1764
- readonly statusTarget: HTMLElement;
1765
- readonly fieldsTarget: HTMLElement;
1766
- readonly hasStatusTarget: boolean;
1767
- readonly hasFieldsTarget: boolean;
1768
- readonly hasTagTemplateTarget: boolean;
1769
- readonly hasInputTarget: boolean;
1770
- delimiterValue: string;
1771
- maxValue: number;
1772
- allowDuplicatesValue: boolean;
1773
- nameValue: string;
1774
- /** Wires tag-list keyboard navigation and removal, and seeds the single Tab stop. */
1775
- connect(): void;
1776
- /** Releases the delegated listeners so no handler outlives the element. */
1777
- disconnect(): void;
1778
- /** Tracks an input added initially or after connect. */
1779
- inputTargetConnected(input: HTMLInputElement): void;
1780
- /** Removes composition listeners when the active input is replaced or removed. */
1781
- inputTargetDisconnected(input: HTMLInputElement): void;
1782
- /** Commits on `Enter`/delimiter and deletes the last tag on empty `Backspace`. */
1783
- onKeydown(event: KeyboardEvent): void;
1784
- }
1785
-
1786
- /**
1787
- * Headless, accessible **time picker** behavior. Each segment (hour, minute,
1788
- * optional second, optional AM/PM) is an APG **Spinbutton**; the controller
1789
- * composes them into an `HH:MM[:SS]` value mirrored to a hidden field.
1790
- *
1791
- * Markup contract (identifier: `stimeo--time-picker`):
1792
- * <div data-controller="stimeo--time-picker"
1793
- * data-stimeo--time-picker-hour-cycle-value="24"
1794
- * role="group" aria-label="Time">
1795
- * <span role="spinbutton" aria-label="Hours" tabindex="0"
1796
- * aria-valuenow="9" aria-valuemin="0" aria-valuemax="23" aria-valuetext="09"
1797
- * data-segment="hour" data-stimeo--time-picker-target="segment"
1798
- * data-action="keydown->stimeo--time-picker#onKeydown">09</span>
1799
- * <span aria-hidden="true">:</span>
1800
- * <span role="spinbutton" aria-label="Minutes" tabindex="0"
1801
- * data-segment="minute" data-stimeo--time-picker-target="segment"
1802
- * data-action="keydown->stimeo--time-picker#onKeydown">30</span>
1803
- * <input type="hidden" data-stimeo--time-picker-target="field" />
1804
- * </div>
1805
- *
1806
- * @remarks
1807
- * Behavior only — no styling, no locale formatting. Every segment is its own Tab
1808
- * stop (multi-tabstop, *not* roving); `ArrowLeft`/`ArrowRight` are an auxiliary
1809
- * move between segments. `ArrowUp`/`ArrowDown` step the focused segment, wrapping
1810
- * (and, when `wrap` is set, carrying minutes→hours and seconds→minutes). Typing
1811
- * digits enters a value directly and advances after two digits. AM/PM is modeled
1812
- * as a `meridiem` spinbutton that toggles; the hidden field is always 24-hour.
1813
- */
1814
- declare class TimePickerController extends Controller<HTMLElement> {
1815
- #private;
1816
- static targets: string[];
1817
- static values: {
1818
- hourCycle: {
1819
- type: NumberConstructor;
1820
- default: number;
1821
- };
1822
- step: {
1823
- type: NumberConstructor;
1824
- default: number;
1825
- };
1826
- seconds: {
1827
- type: BooleanConstructor;
1828
- default: boolean;
1829
- };
1830
- wrap: {
1831
- type: BooleanConstructor;
1832
- default: boolean;
1833
- };
1834
- };
1835
- static actions: readonly ["onKeydown"];
1836
- static events: readonly ["change"];
1837
- readonly segmentTargets: HTMLElement[];
1838
- readonly fieldTarget: HTMLInputElement;
1839
- readonly hasFieldTarget: boolean;
1840
- hourCycleValue: number;
1841
- stepValue: number;
1842
- secondsValue: boolean;
1843
- wrapValue: boolean;
1844
- /** Seeds each segment from its initial `aria-valuenow` and syncs the field. */
1845
- connect(): void;
1846
- /** Handles stepping, inter-segment focus moves, jumps, and direct entry. */
1847
- onKeydown(event: KeyboardEvent): void;
1848
- }
1849
-
1850
- /**
1851
- * Headless, accessible single-select tree view.
1852
- *
1853
- * Markup contract (identifier: `stimeo--tree-view`):
1854
- * <ul data-controller="stimeo--tree-view" role="tree" aria-label="Files">
1855
- * <li role="treeitem" aria-expanded="false" aria-selected="false" tabindex="0"
1856
- * data-stimeo--tree-view-target="item"
1857
- * data-action="keydown->stimeo--tree-view#onKeydown
1858
- * click->stimeo--tree-view#onClick">
1859
- * <span>src</span>
1860
- * <ul role="group" data-stimeo--tree-view-target="group" hidden>
1861
- * <li role="treeitem" aria-selected="false" tabindex="-1"
1862
- * data-stimeo--tree-view-target="item" data-action="…">…</li>
1863
- * </ul>
1864
- * </li>
1865
- * </ul>
1866
- *
1867
- * Implements the WAI-ARIA APG **Tree View** (single-select) pattern. Parent/child
1868
- * structure is read from the DOM nesting (`treeitem` → child `group`). The whole
1869
- * tree is one Tab stop (roving tabindex); arrows navigate visible items, expand /
1870
- * collapse, and move between parent and child.
1871
- *
1872
- * Behavior provided:
1873
- * - `ArrowDown`/`ArrowUp` move between visible items; `Home`/`End` jump to the
1874
- * first / last visible item; printable characters typeahead by label prefix.
1875
- * - `ArrowRight` expands a collapsed parent or steps into its first child;
1876
- * `ArrowLeft` collapses an expanded parent or steps to the parent item.
1877
- * - `Enter`/`Space`/click select the item (single selection via `aria-selected`).
1878
- * - `aria-expanded` and each child `group`'s `hidden` stay in sync, dispatching
1879
- * `stimeo--tree-view:toggle`; selection dispatches `stimeo--tree-view:select`.
1880
- */
1881
- declare class TreeViewController extends Controller<HTMLElement> {
1882
- #private;
1883
- static targets: string[];
1884
- static actions: readonly ["onClick", "onKeydown"];
1885
- static events: readonly ["select", "toggle"];
1886
- readonly itemTargets: HTMLElement[];
1887
- /** Establishes the single tab stop (keeps an existing one, else the first). */
1888
- connect(): void;
1889
- /** Clears the pending typeahead-reset timer. */
1890
- disconnect(): void;
1891
- /**
1892
- * Routes tree keyboard interaction. Because `treeitem`s nest, only the handler
1893
- * on the nearest item to the event target acts; the same keydown bubbling to an
1894
- * ancestor item's handler is ignored to avoid double moves / selections.
1895
- */
1896
- onKeydown(event: KeyboardEvent): void;
1897
- /** Selects the clicked item (nearest to the target only). */
1898
- onClick(event: Event): void;
1899
- }
108
+ import { TreeViewController } from './controllers/tree_view_controller.js';
1900
109
 
1901
110
  /**
1902
111
  * Maps Stimeo controller identifiers to their controller classes.