stimeo-ui 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +212 -0
- package/README.md +120 -0
- package/dist/cable/index.js +123 -29
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/accordion_controller.d.ts +30 -3
- package/dist/controllers/accordion_controller.js +98 -9
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +8 -7
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.d.ts +3 -2
- package/dist/controllers/announcer_controller.js +96 -62
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.js +83 -7
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.js +1 -1
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.d.ts +10 -7
- package/dist/controllers/breadcrumb_controller.js +38 -11
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.d.ts +4 -4
- package/dist/controllers/bulk_select_controller.js +7 -6
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +101 -23
- package/dist/controllers/calendar_controller.js +340 -123
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +67 -22
- package/dist/controllers/carousel_controller.js +263 -38
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.d.ts +1 -1
- package/dist/controllers/character_counter_controller.js +40 -2
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.js +81 -12
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.d.ts +28 -2
- package/dist/controllers/clipboard_controller.js +63 -5
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +25 -1
- package/dist/controllers/collapsible_controller.js +99 -14
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +29 -8
- package/dist/controllers/color_picker_controller.js +80 -34
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.d.ts +10 -1
- package/dist/controllers/combobox_controller.js +106 -16
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +3 -3
- package/dist/controllers/command_palette_controller.js +35 -3
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.js +85 -17
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.js +3 -0
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +6 -0
- package/dist/controllers/context_menu_controller.js +32 -12
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +30 -1
- package/dist/controllers/countdown_controller.js +129 -26
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +77 -16
- package/dist/controllers/currency_input_controller.js +221 -67
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +63 -18
- package/dist/controllers/data_grid_controller.js +195 -29
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +41 -6
- package/dist/controllers/date_range_picker_controller.js +151 -30
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.d.ts +10 -3
- package/dist/controllers/dialog_controller.js +35 -8
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.js +22 -4
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.d.ts +2 -2
- package/dist/controllers/dirty_form_controller.js +46 -13
- package/dist/controllers/dirty_form_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.js +1 -0
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.d.ts +18 -10
- package/dist/controllers/drawer_controller.js +54 -19
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/dropdown_controller.d.ts +9 -3
- package/dist/controllers/dropdown_controller.js +36 -9
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/editable_controller.js +34 -0
- package/dist/controllers/editable_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.js +144 -51
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/filter_controller.d.ts +10 -4
- package/dist/controllers/filter_controller.js +20 -6
- package/dist/controllers/filter_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +26 -6
- package/dist/controllers/flash_controller.js +432 -71
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.js +1 -0
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js +7 -5
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js +19 -13
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.js +45 -8
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.js +82 -25
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +10 -2
- package/dist/controllers/hover_card_controller.js +40 -14
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +16 -3
- package/dist/controllers/idle_controller.js +90 -5
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.d.ts +5 -2
- package/dist/controllers/input_mask_controller.js +65 -9
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.js +3 -0
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.js +11 -2
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +50 -8
- package/dist/controllers/listbox_controller.js +203 -45
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.js +10 -5
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +3 -3
- package/dist/controllers/masonry_controller.js +31 -15
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +9 -3
- package/dist/controllers/menu_controller.js +45 -16
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +11 -0
- package/dist/controllers/menubar_controller.js +58 -24
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.js +9 -5
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +18 -3
- package/dist/controllers/multi_select_controller.js +278 -104
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.d.ts +11 -0
- package/dist/controllers/navigation_menu_controller.js +48 -15
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.js +37 -8
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.js +9 -1
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +36 -8
- package/dist/controllers/number_input_controller.js +124 -21
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/optimistic_controller.js +42 -5
- package/dist/controllers/optimistic_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +22 -7
- package/dist/controllers/otp_controller.js +198 -55
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +8 -12
- package/dist/controllers/overflow_indicator_controller.js +115 -21
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +26 -6
- package/dist/controllers/overflow_menu_controller.js +141 -44
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +19 -11
- package/dist/controllers/pagination_controller.js +74 -28
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +15 -1
- package/dist/controllers/password_reveal_controller.js +59 -2
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/persist_controller.js +30 -8
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.js +131 -52
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +9 -3
- package/dist/controllers/popover_controller.js +45 -11
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +1 -1
- package/dist/controllers/portal_controller.js +6 -2
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/preview_guard_controller.js +16 -1
- package/dist/controllers/preview_guard_controller.js.map +1 -1
- package/dist/controllers/progress_controller.js +8 -4
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +6 -4
- package/dist/controllers/radio_group_controller.js +42 -17
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +49 -1
- package/dist/controllers/range_slider_controller.js +88 -42
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +14 -3
- package/dist/controllers/rating_controller.js +39 -15
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.d.ts +24 -2
- package/dist/controllers/read_more_controller.js +100 -7
- package/dist/controllers/read_more_controller.js.map +1 -1
- package/dist/controllers/reading_progress_controller.js +65 -19
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.js +10 -5
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.d.ts +16 -2
- package/dist/controllers/resizable_controller.js +82 -22
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.js +75 -27
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.js +37 -16
- package/dist/controllers/scroll_restore_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.js +49 -30
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +3 -2
- package/dist/controllers/scrollspy_controller.js +71 -26
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +41 -14
- package/dist/controllers/separator_controller.js +66 -37
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +20 -3
- package/dist/controllers/sidebar_controller.js +77 -18
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.js +6 -1
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +45 -7
- package/dist/controllers/slider_controller.js +82 -47
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/smart_sticky_header_controller.js +60 -26
- package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
- package/dist/controllers/sortable_controller.js +17 -2
- package/dist/controllers/sortable_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.js +10 -2
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +19 -17
- package/dist/controllers/step_indicator_controller.js +18 -17
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +34 -9
- package/dist/controllers/stepper_controller.js +101 -19
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +19 -0
- package/dist/controllers/stick_to_bottom_controller.js +104 -8
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +3 -2
- package/dist/controllers/submit_once_controller.js +45 -9
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +29 -7
- package/dist/controllers/switch_controller.js +101 -10
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +12 -0
- package/dist/controllers/tabs_controller.js +21 -2
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +15 -3
- package/dist/controllers/tags_input_controller.js +209 -59
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.js +29 -3
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +20 -3
- package/dist/controllers/theme_controller.js +64 -14
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.js +23 -8
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +55 -15
- package/dist/controllers/toast_controller.js +451 -105
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +49 -7
- package/dist/controllers/toggle_group_controller.js +159 -23
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js +32 -0
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +8 -0
- package/dist/controllers/tooltip_controller.js +39 -13
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.js +4 -0
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +39 -8
- package/dist/controllers/tree_view_controller.js +169 -16
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.d.ts +28 -1
- package/dist/index.js +5002 -1911
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +72 -6
- package/dist/inspector/cli.js +262 -51
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +309 -51
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +40 -40
- package/dist/inspector/manifest.json +529 -61
- package/dist/positioning/index.js +2 -0
- package/dist/positioning/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -113,20 +113,20 @@ var ThemeController = class extends Controller {
|
|
|
113
113
|
target: { type: String, default: DEFAULT_TARGET }
|
|
114
114
|
};
|
|
115
115
|
static actions = ["set", "toggle"];
|
|
116
|
-
static events = ["change"];
|
|
116
|
+
static events = ["change", "reconcile"];
|
|
117
117
|
/** The OS dark-mode query, watched so `system` tracks live changes. */
|
|
118
118
|
#media = null;
|
|
119
|
-
/** Gate for the target callbacks, which Stimulus runs before `connect()`. */
|
|
119
|
+
/** Gate for the target and `mode` callbacks, which Stimulus runs before `connect()`. */
|
|
120
120
|
#connected = false;
|
|
121
121
|
/** The `target` declaration after validation; the default when unparsable. */
|
|
122
122
|
#targetSelector = DEFAULT_TARGET;
|
|
123
123
|
/** Owns the single Tab stop across the option set (APG radiogroup). */
|
|
124
124
|
#roving = new RovingTabindex(() => this.optionTargets);
|
|
125
125
|
/**
|
|
126
|
-
* The pair last reported, so a move can be told from a
|
|
127
|
-
* readable after the fact — assigning the Value updates the
|
|
128
|
-
* comparison, and the OS query has already flipped by the time it
|
|
129
|
-
* so what was reported has to be kept rather than recomputed.
|
|
126
|
+
* The pair on screen as last reported or re-seeded, so a move can be told from a
|
|
127
|
+
* repeat. Neither side is readable after the fact — assigning the Value updates the
|
|
128
|
+
* mode before any comparison, and the OS query has already flipped by the time it
|
|
129
|
+
* notifies — so what was reported has to be kept rather than recomputed.
|
|
130
130
|
*/
|
|
131
131
|
#published = {
|
|
132
132
|
mode: DEFAULT_MODE,
|
|
@@ -189,13 +189,32 @@ var ThemeController = class extends Controller {
|
|
|
189
189
|
targetValueChanged() {
|
|
190
190
|
this.#targetSelector = validSelector(this.element, this.targetValue, DEFAULT_TARGET);
|
|
191
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Re-renders for a `mode` declaration changed after connect — a Turbo morph
|
|
194
|
+
* re-rendering the server's markup over the live element, or a script.
|
|
195
|
+
*
|
|
196
|
+
* A stored choice outranks the declaration: a Value that disagrees with it is written
|
|
197
|
+
* back to it, so a morph that brings the server's default never overrides what the
|
|
198
|
+
* user saved. Without one, the declaration is applied. Either way storage is left
|
|
199
|
+
* alone, and a pair on screen that moved is reported as `reconcile`, not `change` —
|
|
200
|
+
* the page moved the declaration, not a selection. The reported pair is re-seeded
|
|
201
|
+
* first, so the next selection is compared with what is on screen.
|
|
202
|
+
*/
|
|
203
|
+
modeValueChanged() {
|
|
204
|
+
if (!this.#connected) return;
|
|
205
|
+
if (this.modeValue === this.#published.mode) return;
|
|
206
|
+
const stored = this.#readStored();
|
|
207
|
+
if (stored !== null && this.modeValue !== stored) this.modeValue = stored;
|
|
208
|
+
this.#applyTheme();
|
|
209
|
+
this.#reconcileControls();
|
|
210
|
+
}
|
|
192
211
|
/** Re-derives the single Tab stop and ARIA for an option set that changed. */
|
|
193
212
|
optionTargetConnected() {
|
|
194
|
-
if (this.#connected) this.#
|
|
213
|
+
if (this.#connected) this.#reconcileControls();
|
|
195
214
|
}
|
|
196
215
|
/** Re-derives them again when an option leaves, so a Tab stop always remains. */
|
|
197
216
|
optionTargetDisconnected() {
|
|
198
|
-
if (this.#connected) this.#
|
|
217
|
+
if (this.#connected) this.#reconcileControls();
|
|
199
218
|
}
|
|
200
219
|
/**
|
|
201
220
|
* Selects the mode the activated option declares.
|
|
@@ -227,18 +246,45 @@ var ThemeController = class extends Controller {
|
|
|
227
246
|
#commit() {
|
|
228
247
|
this.#applyTheme();
|
|
229
248
|
this.#syncControls();
|
|
249
|
+
const moved = this.#settle();
|
|
250
|
+
if (moved) this.dispatch("change", { detail: moved });
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Re-derives the controls from the mode on screen for a change the page made — a
|
|
254
|
+
* `mode` declaration or an option set — and reports a pair that moved since the
|
|
255
|
+
* last report as `reconcile`. An option coming or going moves neither half of the
|
|
256
|
+
* pair, so of these changes only a declaration reports.
|
|
257
|
+
*/
|
|
258
|
+
#reconcileControls() {
|
|
259
|
+
this.#syncControls();
|
|
260
|
+
const moved = this.#settle();
|
|
261
|
+
if (moved) this.dispatch("reconcile", { detail: moved });
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Takes the pair on screen as the reported one and returns it when it differs
|
|
265
|
+
* from the one reported before, or `null` when neither half moved.
|
|
266
|
+
*
|
|
267
|
+
* The baseline moves before anything is dispatched, so a listener that selects
|
|
268
|
+
* another mode is measured from the pair it was just told about. What is
|
|
269
|
+
* returned is a copy: the baseline has to survive a listener that writes to what
|
|
270
|
+
* it was handed, or the next unchanged operation reads as a move.
|
|
271
|
+
*/
|
|
272
|
+
#settle() {
|
|
230
273
|
const next = this.#current;
|
|
231
274
|
const last = this.#published;
|
|
232
275
|
this.#published = next;
|
|
233
|
-
if (last.mode
|
|
234
|
-
|
|
235
|
-
}
|
|
276
|
+
if (last.mode === next.mode && last.resolved === next.resolved) return null;
|
|
277
|
+
return { ...next };
|
|
236
278
|
}
|
|
237
|
-
/** The pair the `change`
|
|
279
|
+
/** The pair the `change` and `reconcile` details carry, read from current state. */
|
|
238
280
|
get #current() {
|
|
239
281
|
return { mode: this.#mode, resolved: this.#resolved() };
|
|
240
282
|
}
|
|
241
|
-
/**
|
|
283
|
+
/**
|
|
284
|
+
* Writes `data-theme` + `color-scheme` (the resolved theme) onto the target.
|
|
285
|
+
*
|
|
286
|
+
* @stimeoRenderRoot
|
|
287
|
+
*/
|
|
242
288
|
#applyTheme() {
|
|
243
289
|
const root = this.#targetElement();
|
|
244
290
|
if (!root) return;
|
|
@@ -246,7 +292,11 @@ var ThemeController = class extends Controller {
|
|
|
246
292
|
root.setAttribute("data-theme", resolved);
|
|
247
293
|
root.style.setProperty("color-scheme", resolved);
|
|
248
294
|
}
|
|
249
|
-
/**
|
|
295
|
+
/**
|
|
296
|
+
* Keeps the radiogroup (aria-checked + roving tabindex) or toggle (aria-pressed) in sync.
|
|
297
|
+
*
|
|
298
|
+
* @stimeoRenderRoot
|
|
299
|
+
*/
|
|
250
300
|
#syncControls() {
|
|
251
301
|
const options = this.optionTargets;
|
|
252
302
|
if (options.length > 0) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.ts","../../src/utils/declared_value.ts","../../src/utils/roving_tabindex.ts","../../src/utils/safe_storage.ts","../../src/controllers/theme_controller.ts"],"names":[],"mappings":"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","file":"theme_controller.js","sourcesContent":["/** Normalized scroll position and maximum distance on one logical axis. */\nexport interface LogicalScrollMetrics {\n position: number;\n max: number;\n}\n\n/**\n * Whether horizontal scrolling on `element` follows right-to-left inline flow.\n *\n * Resolved from the **computed** `direction`, so the authoring contract is the\n * usual `dir=\"rtl\"` (or a stylesheet) on the element or any ancestor.\n *\n * Scope: horizontal writing modes. A vertical writing mode (`writing-mode:\n * vertical-rl`) also inverts the horizontal axis, which this check does not\n * model — vertical writing modes are out of scope for the scroll utilities\n * (their consumers describe axes as horizontal/vertical, not inline/block).\n */\nexport function isRtl(element: Element): boolean {\n return window.getComputedStyle(element).direction === \"rtl\";\n}\n\n/**\n * Returns scroll distance from the logical start edge.\n *\n * CSSOM View exposes standards-mode RTL horizontal offsets as `0` at the inline\n * start (right) and increasingly negative values toward the inline end (left).\n * The normalized position is always clamped to `[0, max]`, which also absorbs\n * Safari's elastic overscroll values.\n */\nexport function logicalScrollMetrics(\n element: HTMLElement,\n horizontal: boolean,\n): LogicalScrollMetrics {\n const max = Math.max(\n 0,\n horizontal\n ? element.scrollWidth - element.clientWidth\n : element.scrollHeight - element.clientHeight,\n );\n const raw = horizontal ? element.scrollLeft : element.scrollTop;\n const position = horizontal && isRtl(element) ? -raw : raw;\n return { position: Math.min(max, Math.max(0, position)), max };\n}\n\n/**\n * Converts a logical start/end delta to the physical value accepted by\n * `Element.scrollBy`.\n */\nexport function physicalScrollDelta(\n element: HTMLElement,\n horizontal: boolean,\n logicalDelta: number,\n): number {\n return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;\n}\n","import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n\n/**\n * True when a press arrived carrying any modifier, for the keys the browser and\n * the OS own outright: return without calling `preventDefault()` and without\n * moving any state.\n *\n * `Control+Home` and `Control+End` jump the document to its ends, and a widget\n * that swallows them makes the shortcut work or not depending on where focus\n * happens to sit. {@link isReservedArrowChord} answers the same question for the\n * arrows, but returns `false` for every other key so that this one can decide.\n *\n * There is no `allow` list here on purpose. APG assigns no modifier chord to\n * `Home`/`End`, so a widget that wanted one would be claiming a combination the\n * pattern never gave it.\n *\n * @example\n * ```ts\n * case \"Home\":\n * case \"End\":\n * if (hasModifierChord(event)) return;\n * ```\n */\nexport function hasModifierChord(event: KeyboardEvent): boolean {\n return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;\n}\n","/**\n * Readers for the String Values whose text can only be checked by handing it to\n * a parser: a CSS selector, a regular-expression source, a JSON object.\n *\n * Stimulus reads each of them as an ordinary string, so the controller connects\n * and the flaw surfaces later, inside the first handler that consumes the value\n * (a `SyntaxError` from `new RegExp`, a `DOMException` from the selector engine),\n * where it takes the whole handler down on every event. Parsing here, once, in\n * the `<name>ValueChanged` callback keeps the failure local to the declaration:\n * an unreadable value falls back to its default, the element stays alive, and\n * the hot path only ever receives a value that parsed.\n *\n * Which default a broken declaration falls back to is the caller's contract, so\n * every reader takes (or returns) the fallback rather than choosing one.\n */\n\n/**\n * The result of `parse(raw)`, or `fallback` when `parse` throws.\n *\n * Any exception counts as \"does not parse\": the parsers these Values feed\n * (`RegExp`, `JSON.parse`, the selector engine) all report a malformed input by\n * throwing, and none of them throws for another reason.\n */\nexport function parseDeclared<T>(raw: string, parse: (raw: string) => T, fallback: T): T {\n try {\n return parse(raw);\n } catch {\n return fallback;\n }\n}\n\n/** What {@link validSelector} needs of the element it probes with. */\nexport interface SelectorProbe {\n matches(selector: string): boolean;\n}\n\n/**\n * `raw` when it is a non-empty selector the DOM accepts, otherwise `fallback`.\n *\n * `element.matches` is the probe: the engine parses the selector and throws for\n * one it cannot read. Whether the selector actually matches `element` plays no\n * part, so a selector aimed at another element still passes.\n *\n * The parameter names the one method the probe uses rather than `Element`, so\n * this module carries no DOM type and the readers below stay importable from\n * the Node-side Inspector, which checks the same declarations statically.\n */\nexport function validSelector(element: SelectorProbe, raw: string, fallback: string): string {\n if (raw.length === 0) return fallback;\n return parseDeclared(\n raw,\n (selector) => {\n element.matches(selector);\n return selector;\n },\n fallback,\n );\n}\n\n/** How `compileRegExp` wraps a source before compiling it. */\nexport type RegExpAnchor = \"none\" | \"exact\";\n\n/**\n * `source` compiled as a `RegExp`, or `null` when it does not compile.\n *\n * `\"exact\"` wraps the source as `^(?:source)$`, so the whole input has to match\n * and an alternation inside the source cannot escape the anchors. The source is\n * compiled on its own first, because an unbalanced source can be made to parse\n * by the wrapper's own parentheses — `0)|(1` becomes `^(?:0)|(1)$` — which would\n * accept a declaration that is not a regular expression and leave half of it\n * outside the anchors. The default that replaces a broken source is the\n * caller's, so `null` is returned rather than a fallback pattern.\n */\nexport function compileRegExp(source: string, anchor: RegExpAnchor = \"none\"): RegExp | null {\n return parseDeclared(\n source,\n (text) => {\n const bare = new RegExp(text);\n return anchor === \"exact\" ? new RegExp(`^(?:${text})$`) : bare;\n },\n null,\n );\n}\n\n/**\n * The JSON object `raw` declares, or `null` when the text does not parse or\n * parses to something other than a plain object (`null`, an array, a scalar).\n * Values are returned as parsed; narrowing them is the caller's contract.\n */\nexport function parseJsonObject(raw: string): Record<string, unknown> | null {\n const parsed = parseDeclared<unknown>(raw, (text) => JSON.parse(text), null);\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) return null;\n return parsed as Record<string, unknown>;\n}\n","/**\n * Low-level roving-tabindex primitive shared by composite-widget controllers.\n *\n * The APG roving-tabindex pattern keeps a composite widget a single Tab stop:\n * exactly one item is in the Tab sequence (`tabindex=\"0\"`) while the rest are\n * removed from it (`tabindex=\"-1\"`), and the arrow keys move both DOM focus and\n * that single tabbable position together. {@link RovingTabindex} owns *only* that\n * mechanical bookkeeping — \"which one item is tabbable, and move focus there\".\n *\n * It is intentionally **policy-free**. Orientation, wrapping vs. clamping,\n * selection-follows-focus, typeahead, and `Home`/`End` semantics differ per APG\n * pattern (Radio Group, Toolbar, Rating, …); folding them into one helper would\n * flatten those widgets to a lowest common denominator and lose each pattern's\n * correctness. Those decisions therefore stay in each controller, which calls\n * {@link RovingTabindex.setActive} with an index it computed itself (optionally\n * via the pure {@link rovingMove} helper).\n *\n * @remarks\n * Items are read lazily through a getter so a controller can add or remove\n * targets (Stimulus re-scans the DOM) without re-wiring this helper. A caller\n * that already captured one event-scoped snapshot may pass it to\n * {@link RovingTabindex.setActive}, avoiding a second live-target scan while\n * preserving one coherent collection for the whole operation.\n */\nexport class RovingTabindex {\n /** Returns the current ordered item elements; called on every operation. */\n readonly #getItems: () => HTMLElement[];\n\n /**\n * @param getItems - Returns the current ordered item elements. Called on every\n * operation so the live target list is always used.\n */\n constructor(getItems: () => HTMLElement[]) {\n this.#getItems = getItems;\n }\n\n /** Index of the currently tabbable item (`tabindex=\"0\"`), or `-1` if none. */\n get activeIndex(): number {\n return this.#getItems().findIndex((item) => item.tabIndex === 0);\n }\n\n /**\n * Makes exactly the item at `index` tabbable (`tabindex=\"0\"`) and removes every\n * other item from the Tab sequence (`tabindex=\"-1\"`). An out-of-range `index`\n * (e.g. `-1`) leaves all items at `-1`, which a controller can use to express\n * \"nothing is currently tabbable\".\n *\n * @param index - Position of the item to make tabbable.\n * @param options - Pass `{ focus: true }` to also move DOM focus to that item,\n * and `items` to reuse an event-scoped collection snapshot.\n */\n setActive(\n index: number,\n options: { focus?: boolean; items?: readonly HTMLElement[] } = {},\n ): void {\n const { focus = false } = options;\n const items = options.items ?? this.#getItems();\n items.forEach((item, i) => {\n item.tabIndex = i === index ? 0 : -1;\n });\n if (focus) items[index]?.focus();\n }\n}\n\n/** Edge behavior for {@link rovingMove}: cycle past the ends, or stop at them. */\nexport type RovingWrap = \"wrap\" | \"clamp\";\n\n/**\n * Pure helper that resolves the target index for a one-step directional move.\n *\n * Keyboard/orientation mapping stays in the caller: it decides that a key means\n * `delta` `+1` (next) or `-1` (previous) and whether the widget should `\"wrap\"`\n * (Radio Group, Toolbar) or `\"clamp\"` at the ends.\n *\n * @param current - The index focus is moving from.\n * @param length - Number of items in the set.\n * @param delta - `+1` to move to the next item, `-1` for the previous.\n * @param wrap - `\"wrap\"` cycles around the ends; `\"clamp\"` stops at them.\n * @returns The resolved index, or `-1` when there are no items.\n */\nexport function rovingMove(\n current: number,\n length: number,\n delta: number,\n wrap: RovingWrap,\n): number {\n if (length === 0) return -1;\n const next = current + delta;\n if (wrap === \"wrap\") return (next + length) % length;\n return Math.min(length - 1, Math.max(0, next));\n}\n","/** A successful guarded storage operation. */\nexport interface StorageSuccess<T> {\n readonly ok: true;\n readonly value: T;\n}\n\n/** A guarded storage operation rejected by the browser. */\nexport interface StorageFailure {\n readonly ok: false;\n readonly error: unknown;\n}\n\n/** Result of accessing browser storage without allowing an exception to escape. */\nexport type StorageResult<T> = StorageSuccess<T> | StorageFailure;\n\n/**\n * Guarded `localStorage` access shared by controllers that persist client state.\n *\n * `window.localStorage` can throw while resolving the storage object or while\n * reading, writing, or removing a key. The result keeps an unavailable store\n * distinct from an available store whose key is unset, so callers can expose an\n * accurate success or failure state without duplicating exception guards.\n */\n\n/**\n * Reads `key` from `localStorage`.\n *\n * @param key - The storage key to read.\n * @returns A successful result containing the stored string or `null`, or a\n * failure result when storage is unavailable.\n */\nexport function readLocalStorage(key: string): StorageResult<string | null> {\n try {\n return { ok: true, value: window.localStorage.getItem(key) };\n } catch (error: unknown) {\n return { ok: false, error };\n }\n}\n\n/**\n * Writes `value` under `key` in `localStorage`.\n *\n * @param key - The storage key to write.\n * @param value - The string to store.\n * @returns Success only after `setItem` completes; otherwise the browser error.\n */\nexport function writeLocalStorage(key: string, value: string): StorageResult<void> {\n try {\n window.localStorage.setItem(key, value);\n return { ok: true, value: undefined };\n } catch (error: unknown) {\n return { ok: false, error };\n }\n}\n\n/**\n * Removes `key` from `localStorage`.\n *\n * @param key - The storage key to remove.\n * @returns Success only after `removeItem` completes; otherwise the browser error.\n */\nexport function removeLocalStorage(key: string): StorageResult<void> {\n try {\n window.localStorage.removeItem(key);\n return { ok: true, value: undefined };\n } catch (error: unknown) {\n return { ok: false, error };\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { hasModifierChord, isReservedArrowChord, logicalArrowStep } from \"../utils/arrow_step\";\nimport { validSelector } from \"../utils/declared_value\";\nimport { RovingTabindex, rovingMove } from \"../utils/roving_tabindex\";\nimport { readLocalStorage, writeLocalStorage } from \"../utils/safe_storage\";\n\n/** The three selectable modes; `system` follows the OS `prefers-color-scheme`. */\ntype ThemeMode = \"light\" | \"dark\" | \"system\";\n/** The two effective (resolved) themes applied to the root. */\ntype ResolvedTheme = \"light\" | \"dark\";\n\nconst MODES: readonly ThemeMode[] = [\"light\", \"dark\", \"system\"];\nconst isMode = (value: unknown): value is ThemeMode =>\n typeof value === \"string\" && (MODES as readonly string[]).includes(value);\n\n/** The mode a declaration falls back to when it cannot be read as one. */\nconst DEFAULT_MODE: ThemeMode = \"system\";\n/** The state-hook target a declaration falls back to when it cannot be parsed. */\nconst DEFAULT_TARGET = \"html\";\n\n/**\n * Headless **theme / color-scheme toggle** — persists a light/dark/system choice to\n * `localStorage`, follows the OS setting while in `system`, and reflects the\n * effective theme onto the root for the consumer's CSS. Ships no colors; only state\n * hooks.\n *\n * Two markup contracts (identifier: `stimeo--theme`):\n *\n * Canonical 3-value radiogroup (use this when `system` is offered):\n * <div data-controller=\"stimeo--theme\" data-stimeo--theme-mode-value=\"system\"\n * role=\"radiogroup\" aria-label=\"Theme\">\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-value=\"light\">Light</button>\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-value=\"dark\">Dark</button>\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-value=\"system\">System</button>\n * </div>\n *\n * Auxiliary 2-value toggle (light↔dark only — `system` is not representable):\n * <button data-controller=\"stimeo--theme\" data-action=\"click->stimeo--theme#toggle\"\n * aria-pressed=\"false\">Dark mode</button>\n *\n * `change` dispatches `{ mode, resolved }`, and only when one of the two moved.\n *\n * @remarks\n * Behavior only — the actual palette is the consumer's CSS keyed off `data-theme`\n * on the root. It applies `data-theme` (the *resolved* light/dark) and a matching\n * `color-scheme` to the `target` element (`<html>` by default), keeps the radiogroup\n * `aria-checked` + roving tabindex (APG radio) or the single button's `aria-pressed`\n * in sync. Arrow/Home/End move focus across the radiogroup as the APG radio pattern\n * requires; applying a theme never moves focus. The `prefers-color-scheme` listener is attached on\n * `connect()` and removed on `disconnect()` (Turbo included). FOUC avoidance for the\n * very first paint is an inline `<head>` snippet, not this controller.\n *\n * Every declaration is validated where it enters, and an unreadable one falls back\n * to that Value's default rather than taking the widget with it: a `mode` outside\n * the three modes reads as `system`, and a `target` that is not a parsable selector\n * reads as `html`. An option whose `data-value` is outside the three can be focused\n * but never becomes the selection, so no more than one option is ever checked —\n * and none at all when the resolved mode matches no option, where the first one\n * keeps the Tab stop.\n *\n * The radiogroup stays one Tab stop through `RovingTabindex`, re-derived\n * whenever an option enters or leaves, so a set rendered after connect or swapped\n * by a Turbo morph carries the same single stop as any other.\n */\nexport class ThemeController extends Controller<HTMLElement> {\n static override targets = [\"option\"];\n static override values = {\n mode: { type: String, default: DEFAULT_MODE },\n storageKey: { type: String, default: \"stimeo-theme\" },\n target: { type: String, default: DEFAULT_TARGET },\n };\n static actions = [\"set\", \"toggle\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly optionTargets: HTMLElement[];\n declare readonly hasOptionTarget: boolean;\n\n declare modeValue: string;\n declare storageKeyValue: string;\n declare targetValue: string;\n\n /** The OS dark-mode query, watched so `system` tracks live changes. */\n #media: MediaQueryList | null = null;\n\n /** Gate for the target callbacks, which Stimulus runs before `connect()`. */\n #connected = false;\n\n /** The `target` declaration after validation; the default when unparsable. */\n #targetSelector = DEFAULT_TARGET;\n\n /** Owns the single Tab stop across the option set (APG radiogroup). */\n readonly #roving = new RovingTabindex(() => this.optionTargets);\n\n /**\n * The pair last reported, so a move can be told from a repeat. Neither side is\n * readable after the fact — assigning the Value updates the mode before any\n * comparison, and the OS query has already flipped by the time it notifies —\n * so what was reported has to be kept rather than recomputed.\n */\n #published: { mode: ThemeMode; resolved: ResolvedTheme } = {\n mode: DEFAULT_MODE,\n resolved: \"light\",\n };\n\n /** Re-resolves while in `system` mode when the OS preference flips. */\n readonly #onMediaChange = (): void => {\n // Only `system` follows the OS; an explicit mode already decided the answer.\n if (this.#mode !== \"system\") return;\n this.#commit();\n };\n\n /** Arrow/Home/End navigation for the radiogroup (APG radio pattern). */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n // A descendant widget that already claimed the key (a grabbed drag handle, a\n // nested menu) must not ALSO act on it — composition depends on this yield.\n if (event.defaultPrevented) return;\n if (isReservedArrowChord(event)) return;\n // Resolved once per keydown: every `optionTargets` access re-queries the scope.\n const options = this.optionTargets;\n const target = event.target as HTMLElement | null;\n const current = options.indexOf(target as HTMLElement);\n if (current === -1) return;\n\n let next = current;\n // Logical, not physical. The helper reverses only the horizontal pair, so\n // folding Down/Up into the same branch stays correct.\n const step = logicalArrowStep(event.key, this.element);\n switch (event.key) {\n case \"ArrowDown\":\n case \"ArrowRight\":\n case \"ArrowUp\":\n case \"ArrowLeft\":\n next = rovingMove(current, options.length, step, \"wrap\");\n break;\n case \"Home\":\n case \"End\":\n // Control+Home jumps the document, not the widget.\n if (hasModifierChord(event)) return;\n next = event.key === \"Home\" ? 0 : options.length - 1;\n break;\n default:\n return;\n }\n event.preventDefault();\n const option = options[next];\n if (!option) return;\n option.focus();\n const mode = this.#optionMode(option);\n // An option outside the three modes can hold focus, but the selection does\n // not follow it there.\n if (mode) this.#setMode(mode);\n };\n\n override connect(): void {\n const stored = this.#readStored();\n if (stored) this.modeValue = stored;\n\n this.#media = window.matchMedia?.(\"(prefers-color-scheme: dark)\") ?? null;\n this.#media?.addEventListener(\"change\", this.#onMediaChange);\n this.element.addEventListener(\"keydown\", this.#onKeydown);\n\n this.#connected = true;\n this.#applyTheme();\n this.#syncControls();\n // Connecting is not a change: seed the baseline instead of announcing one.\n this.#published = this.#current;\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#media?.removeEventListener(\"change\", this.#onMediaChange);\n this.element.removeEventListener(\"keydown\", this.#onKeydown);\n }\n\n /** Validates the `target` declaration once, so the render path never parses. */\n targetValueChanged(): void {\n this.#targetSelector = validSelector(this.element, this.targetValue, DEFAULT_TARGET);\n }\n\n /** Re-derives the single Tab stop and ARIA for an option set that changed. */\n optionTargetConnected(): void {\n if (this.#connected) this.#syncControls();\n }\n\n /** Re-derives them again when an option leaves, so a Tab stop always remains. */\n optionTargetDisconnected(): void {\n if (this.#connected) this.#syncControls();\n }\n\n /**\n * Selects the mode the activated option declares.\n *\n * Read through the same lane that decides which option is checked, so the two\n * can never disagree about what an option declares.\n */\n set(event: Event): void {\n const option = event.currentTarget;\n if (!(option instanceof HTMLElement)) return;\n const mode = this.#optionMode(option);\n if (mode) this.#setMode(mode);\n }\n\n /** Toggles light↔dark for the 2-value single-button contract. */\n toggle(): void {\n this.#setMode(this.#resolved() === \"dark\" ? \"light\" : \"dark\");\n }\n\n /** Central mode change: persist, apply to the root, resync controls, announce. */\n #setMode(mode: ThemeMode): void {\n this.modeValue = mode;\n this.#writeStored(mode);\n this.#commit();\n }\n\n /**\n * Applies the current mode and reports it, but reports only a real move: the\n * event means \"the selection or the effective theme moved\", so re-choosing the\n * option already chosen is not one.\n */\n #commit(): void {\n this.#applyTheme();\n this.#syncControls();\n const next = this.#current;\n const last = this.#published;\n this.#published = next;\n if (last.mode !== next.mode || last.resolved !== next.resolved) {\n // A copy: the baseline has to survive a listener that writes to what it\n // was handed, or the next unchanged operation reads as a change.\n this.dispatch(\"change\", { detail: { ...next } });\n }\n }\n\n /** The pair the `change` detail carries, read from current state. */\n get #current(): { mode: ThemeMode; resolved: ResolvedTheme } {\n return { mode: this.#mode, resolved: this.#resolved() };\n }\n\n /** Writes `data-theme` + `color-scheme` (the resolved theme) onto the target. */\n #applyTheme(): void {\n const root = this.#targetElement();\n if (!root) return;\n const resolved = this.#resolved();\n root.setAttribute(\"data-theme\", resolved);\n root.style.setProperty(\"color-scheme\", resolved);\n }\n\n /** Keeps the radiogroup (aria-checked + roving tabindex) or toggle (aria-pressed) in sync. */\n #syncControls(): void {\n const options = this.optionTargets;\n if (options.length > 0) {\n const mode = this.#mode;\n let selected = -1;\n options.forEach((option, index) => {\n const isSelected = this.#optionMode(option) === mode;\n option.setAttribute(\"aria-checked\", String(isSelected));\n if (isSelected && selected === -1) selected = index;\n });\n // APG: a radiogroup with no selection keeps its first radio tabbable.\n this.#roving.setActive(selected === -1 ? 0 : selected, { items: options });\n return;\n }\n // `aria-pressed` belongs to the 2-value contract, where the controller sits on\n // the button itself. A radiogroup whose options have not rendered yet is not\n // that, and must not be told it is a toggle.\n if (this.#isToggleButton) {\n this.element.setAttribute(\"aria-pressed\", String(this.#resolved() === \"dark\"));\n }\n }\n\n /** Whether the controller element is the button of the 2-value contract. */\n get #isToggleButton(): boolean {\n return this.element.tagName === \"BUTTON\" || this.element.getAttribute(\"role\") === \"button\";\n }\n\n /** The selected mode after validation; an unreadable declaration is the default. */\n get #mode(): ThemeMode {\n return isMode(this.modeValue) ? this.modeValue : DEFAULT_MODE;\n }\n\n /** The effective theme: the OS preference when `system`, else the mode itself. */\n #resolved(): ResolvedTheme {\n const mode = this.#mode;\n if (mode === \"dark\") return \"dark\";\n if (mode === \"light\") return \"light\";\n return this.#media?.matches ? \"dark\" : \"light\";\n }\n\n /** An option's mode from its `data-value`, or `null` when that is not one of the three. */\n #optionMode(option: HTMLElement): ThemeMode | null {\n const mode = option.getAttribute(\"data-value\");\n return isMode(mode) ? mode : null;\n }\n\n /** Resolves the state-hook target (`<html>` by default). */\n #targetElement(): HTMLElement | null {\n const selector = this.#targetSelector;\n if (selector === DEFAULT_TARGET || selector === \":root\") return document.documentElement;\n return document.querySelector<HTMLElement>(selector);\n }\n\n /** Reads a persisted, validated mode from `localStorage` (null when absent/blocked). */\n #readStored(): ThemeMode | null {\n const result = readLocalStorage(this.storageKeyValue);\n return result.ok && isMode(result.value) ? result.value : null;\n }\n\n /** Persists the mode, swallowing storage errors (private mode / quota). */\n #writeStored(mode: ThemeMode): void {\n writeLocalStorage(this.storageKeyValue, mode);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.ts","../../src/utils/declared_value.ts","../../src/utils/roving_tabindex.ts","../../src/utils/safe_storage.ts","../../src/controllers/theme_controller.ts"],"names":[],"mappings":"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","file":"theme_controller.js","sourcesContent":["/** Normalized scroll position and maximum distance on one logical axis. */\nexport interface LogicalScrollMetrics {\n position: number;\n max: number;\n}\n\n/**\n * Whether horizontal scrolling on `element` follows right-to-left inline flow.\n *\n * Resolved from the **computed** `direction`, so the authoring contract is the\n * usual `dir=\"rtl\"` (or a stylesheet) on the element or any ancestor.\n *\n * Scope: horizontal writing modes. A vertical writing mode (`writing-mode:\n * vertical-rl`) also inverts the horizontal axis, which this check does not\n * model — vertical writing modes are out of scope for the scroll utilities\n * (their consumers describe axes as horizontal/vertical, not inline/block).\n */\nexport function isRtl(element: Element): boolean {\n return window.getComputedStyle(element).direction === \"rtl\";\n}\n\n/**\n * Returns scroll distance from the logical start edge.\n *\n * CSSOM View exposes standards-mode RTL horizontal offsets as `0` at the inline\n * start (right) and increasingly negative values toward the inline end (left).\n * The normalized position is always clamped to `[0, max]`, which also absorbs\n * Safari's elastic overscroll values.\n */\nexport function logicalScrollMetrics(\n element: HTMLElement,\n horizontal: boolean,\n): LogicalScrollMetrics {\n const max = Math.max(\n 0,\n horizontal\n ? element.scrollWidth - element.clientWidth\n : element.scrollHeight - element.clientHeight,\n );\n const raw = horizontal ? element.scrollLeft : element.scrollTop;\n const position = horizontal && isRtl(element) ? -raw : raw;\n return { position: Math.min(max, Math.max(0, position)), max };\n}\n\n/**\n * Converts a logical start/end delta to the physical value accepted by\n * `Element.scrollBy`.\n */\nexport function physicalScrollDelta(\n element: HTMLElement,\n horizontal: boolean,\n logicalDelta: number,\n): number {\n return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;\n}\n","import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n\n/**\n * True when a press arrived carrying any modifier, for the keys the browser and\n * the OS own outright: return without calling `preventDefault()` and without\n * moving any state.\n *\n * `Control+Home` and `Control+End` jump the document to its ends, and a widget\n * that swallows them makes the shortcut work or not depending on where focus\n * happens to sit. {@link isReservedArrowChord} answers the same question for the\n * arrows, but returns `false` for every other key so that this one can decide.\n *\n * There is no `allow` list here on purpose. APG assigns no modifier chord to\n * `Home`/`End`, so a widget that wanted one would be claiming a combination the\n * pattern never gave it.\n *\n * @example\n * ```ts\n * case \"Home\":\n * case \"End\":\n * if (hasModifierChord(event)) return;\n * ```\n */\nexport function hasModifierChord(event: KeyboardEvent): boolean {\n return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;\n}\n","/**\n * Readers for the String Values whose text can only be checked by handing it to\n * a parser: a CSS selector, a regular-expression source, a JSON object.\n *\n * Stimulus reads each of them as an ordinary string, so the controller connects\n * and the flaw surfaces later, inside the first handler that consumes the value\n * (a `SyntaxError` from `new RegExp`, a `DOMException` from the selector engine),\n * where it takes the whole handler down on every event. Parsing here, once, in\n * the `<name>ValueChanged` callback keeps the failure local to the declaration:\n * an unreadable value falls back to its default, the element stays alive, and\n * the hot path only ever receives a value that parsed.\n *\n * Which default a broken declaration falls back to is the caller's contract, so\n * every reader takes (or returns) the fallback rather than choosing one.\n */\n\n/**\n * The result of `parse(raw)`, or `fallback` when `parse` throws.\n *\n * Any exception counts as \"does not parse\": the parsers these Values feed\n * (`RegExp`, `JSON.parse`, the selector engine) all report a malformed input by\n * throwing, and none of them throws for another reason.\n */\nexport function parseDeclared<T>(raw: string, parse: (raw: string) => T, fallback: T): T {\n try {\n return parse(raw);\n } catch {\n return fallback;\n }\n}\n\n/** What {@link validSelector} needs of the element it probes with. */\nexport interface SelectorProbe {\n matches(selector: string): boolean;\n}\n\n/**\n * `raw` when it is a non-empty selector the DOM accepts, otherwise `fallback`.\n *\n * `element.matches` is the probe: the engine parses the selector and throws for\n * one it cannot read. Whether the selector actually matches `element` plays no\n * part, so a selector aimed at another element still passes.\n *\n * The parameter names the one method the probe uses rather than `Element`, so\n * this module carries no DOM type and the readers below stay importable from\n * the Node-side Inspector, which checks the same declarations statically.\n */\nexport function validSelector(element: SelectorProbe, raw: string, fallback: string): string {\n if (raw.length === 0) return fallback;\n return parseDeclared(\n raw,\n (selector) => {\n element.matches(selector);\n return selector;\n },\n fallback,\n );\n}\n\n/** How `compileRegExp` wraps a source before compiling it. */\nexport type RegExpAnchor = \"none\" | \"exact\";\n\n/**\n * `source` compiled as a `RegExp`, or `null` when it does not compile.\n *\n * `\"exact\"` wraps the source as `^(?:source)$`, so the whole input has to match\n * and an alternation inside the source cannot escape the anchors. The source is\n * compiled on its own first, because an unbalanced source can be made to parse\n * by the wrapper's own parentheses — `0)|(1` becomes `^(?:0)|(1)$` — which would\n * accept a declaration that is not a regular expression and leave half of it\n * outside the anchors. The default that replaces a broken source is the\n * caller's, so `null` is returned rather than a fallback pattern.\n */\nexport function compileRegExp(source: string, anchor: RegExpAnchor = \"none\"): RegExp | null {\n return parseDeclared(\n source,\n (text) => {\n const bare = new RegExp(text);\n return anchor === \"exact\" ? new RegExp(`^(?:${text})$`) : bare;\n },\n null,\n );\n}\n\n/**\n * The JSON object `raw` declares, or `null` when the text does not parse or\n * parses to something other than a plain object (`null`, an array, a scalar).\n * Values are returned as parsed; narrowing them is the caller's contract.\n */\nexport function parseJsonObject(raw: string): Record<string, unknown> | null {\n const parsed = parseDeclared<unknown>(raw, (text) => JSON.parse(text), null);\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) return null;\n return parsed as Record<string, unknown>;\n}\n","/**\n * Low-level roving-tabindex primitive shared by composite-widget controllers.\n *\n * The APG roving-tabindex pattern keeps a composite widget a single Tab stop:\n * exactly one item is in the Tab sequence (`tabindex=\"0\"`) while the rest are\n * removed from it (`tabindex=\"-1\"`), and the arrow keys move both DOM focus and\n * that single tabbable position together. {@link RovingTabindex} owns *only* that\n * mechanical bookkeeping — \"which one item is tabbable, and move focus there\".\n *\n * It is intentionally **policy-free**. Orientation, wrapping vs. clamping,\n * selection-follows-focus, typeahead, and `Home`/`End` semantics differ per APG\n * pattern (Radio Group, Toolbar, Rating, …); folding them into one helper would\n * flatten those widgets to a lowest common denominator and lose each pattern's\n * correctness. Those decisions therefore stay in each controller, which calls\n * {@link RovingTabindex.setActive} with an index it computed itself (optionally\n * via the pure {@link rovingMove} helper).\n *\n * @remarks\n * Items are read lazily through a getter so a controller can add or remove\n * targets (Stimulus re-scans the DOM) without re-wiring this helper. A caller\n * that already captured one event-scoped snapshot may pass it to\n * {@link RovingTabindex.setActive}, avoiding a second live-target scan while\n * preserving one coherent collection for the whole operation.\n */\nexport class RovingTabindex {\n /** Returns the current ordered item elements; called on every operation. */\n readonly #getItems: () => HTMLElement[];\n\n /**\n * @param getItems - Returns the current ordered item elements. Called on every\n * operation so the live target list is always used.\n */\n constructor(getItems: () => HTMLElement[]) {\n this.#getItems = getItems;\n }\n\n /** Index of the currently tabbable item (`tabindex=\"0\"`), or `-1` if none. */\n get activeIndex(): number {\n return this.#getItems().findIndex((item) => item.tabIndex === 0);\n }\n\n /**\n * Makes exactly the item at `index` tabbable (`tabindex=\"0\"`) and removes every\n * other item from the Tab sequence (`tabindex=\"-1\"`). An out-of-range `index`\n * (e.g. `-1`) leaves all items at `-1`, which a controller can use to express\n * \"nothing is currently tabbable\".\n *\n * @param index - Position of the item to make tabbable.\n * @param options - Pass `{ focus: true }` to also move DOM focus to that item,\n * and `items` to reuse an event-scoped collection snapshot.\n */\n setActive(\n index: number,\n options: { focus?: boolean; items?: readonly HTMLElement[] } = {},\n ): void {\n const { focus = false } = options;\n const items = options.items ?? this.#getItems();\n items.forEach((item, i) => {\n item.tabIndex = i === index ? 0 : -1;\n });\n if (focus) items[index]?.focus();\n }\n}\n\n/** Edge behavior for {@link rovingMove}: cycle past the ends, or stop at them. */\nexport type RovingWrap = \"wrap\" | \"clamp\";\n\n/**\n * Pure helper that resolves the target index for a one-step directional move.\n *\n * Keyboard/orientation mapping stays in the caller: it decides that a key means\n * `delta` `+1` (next) or `-1` (previous) and whether the widget should `\"wrap\"`\n * (Radio Group, Toolbar) or `\"clamp\"` at the ends.\n *\n * @param current - The index focus is moving from.\n * @param length - Number of items in the set.\n * @param delta - `+1` to move to the next item, `-1` for the previous.\n * @param wrap - `\"wrap\"` cycles around the ends; `\"clamp\"` stops at them.\n * @returns The resolved index, or `-1` when there are no items.\n */\nexport function rovingMove(\n current: number,\n length: number,\n delta: number,\n wrap: RovingWrap,\n): number {\n if (length === 0) return -1;\n const next = current + delta;\n if (wrap === \"wrap\") return (next + length) % length;\n return Math.min(length - 1, Math.max(0, next));\n}\n","/** A successful guarded storage operation. */\nexport interface StorageSuccess<T> {\n readonly ok: true;\n readonly value: T;\n}\n\n/** A guarded storage operation rejected by the browser. */\nexport interface StorageFailure {\n readonly ok: false;\n readonly error: unknown;\n}\n\n/** Result of accessing browser storage without allowing an exception to escape. */\nexport type StorageResult<T> = StorageSuccess<T> | StorageFailure;\n\n/**\n * Guarded `localStorage` access shared by controllers that persist client state.\n *\n * `window.localStorage` can throw while resolving the storage object or while\n * reading, writing, or removing a key. The result keeps an unavailable store\n * distinct from an available store whose key is unset, so callers can expose an\n * accurate success or failure state without duplicating exception guards.\n */\n\n/**\n * Reads `key` from `localStorage`.\n *\n * @param key - The storage key to read.\n * @returns A successful result containing the stored string or `null`, or a\n * failure result when storage is unavailable.\n */\nexport function readLocalStorage(key: string): StorageResult<string | null> {\n try {\n return { ok: true, value: window.localStorage.getItem(key) };\n } catch (error: unknown) {\n return { ok: false, error };\n }\n}\n\n/**\n * Writes `value` under `key` in `localStorage`.\n *\n * @param key - The storage key to write.\n * @param value - The string to store.\n * @returns Success only after `setItem` completes; otherwise the browser error.\n */\nexport function writeLocalStorage(key: string, value: string): StorageResult<void> {\n try {\n window.localStorage.setItem(key, value);\n return { ok: true, value: undefined };\n } catch (error: unknown) {\n return { ok: false, error };\n }\n}\n\n/**\n * Removes `key` from `localStorage`.\n *\n * @param key - The storage key to remove.\n * @returns Success only after `removeItem` completes; otherwise the browser error.\n */\nexport function removeLocalStorage(key: string): StorageResult<void> {\n try {\n window.localStorage.removeItem(key);\n return { ok: true, value: undefined };\n } catch (error: unknown) {\n return { ok: false, error };\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { hasModifierChord, isReservedArrowChord, logicalArrowStep } from \"../utils/arrow_step\";\nimport { validSelector } from \"../utils/declared_value\";\nimport { RovingTabindex, rovingMove } from \"../utils/roving_tabindex\";\nimport { readLocalStorage, writeLocalStorage } from \"../utils/safe_storage\";\n\n/** The three selectable modes; `system` follows the OS `prefers-color-scheme`. */\ntype ThemeMode = \"light\" | \"dark\" | \"system\";\n/** The two effective (resolved) themes applied to the root. */\ntype ResolvedTheme = \"light\" | \"dark\";\n\nconst MODES: readonly ThemeMode[] = [\"light\", \"dark\", \"system\"];\nconst isMode = (value: unknown): value is ThemeMode =>\n typeof value === \"string\" && (MODES as readonly string[]).includes(value);\n\n/** The mode a declaration falls back to when it cannot be read as one. */\nconst DEFAULT_MODE: ThemeMode = \"system\";\n/** The state-hook target a declaration falls back to when it cannot be parsed. */\nconst DEFAULT_TARGET = \"html\";\n\n/**\n * Headless **theme / color-scheme toggle** — persists a light/dark/system choice to\n * `localStorage`, follows the OS setting while in `system`, and reflects the\n * effective theme onto the root for the consumer's CSS. Ships no colors; only state\n * hooks.\n *\n * Two markup contracts (identifier: `stimeo--theme`):\n *\n * Canonical 3-value radiogroup (use this when `system` is offered):\n * <div data-controller=\"stimeo--theme\" data-stimeo--theme-mode-value=\"system\"\n * role=\"radiogroup\" aria-label=\"Theme\">\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-value=\"light\">Light</button>\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-value=\"dark\">Dark</button>\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-value=\"system\">System</button>\n * </div>\n *\n * Auxiliary 2-value toggle (light↔dark only — `system` is not representable):\n * <button data-controller=\"stimeo--theme\" data-action=\"click->stimeo--theme#toggle\"\n * aria-pressed=\"false\">Dark mode</button>\n *\n * `change` dispatches `{ mode, resolved }` when a selection or the OS preference moves\n * one of the two. `reconcile` dispatches the same `{ mode, resolved }` when a `mode`\n * declaration that changes after connect moves one of them — the declaration\n * itself, or a stored choice that outranks it. Connecting is silent.\n *\n * @remarks\n * Behavior only — the actual palette is the consumer's CSS keyed off `data-theme`\n * on the root. It applies `data-theme` (the *resolved* light/dark) and a matching\n * `color-scheme` to the `target` element (`<html>` by default), keeps the radiogroup\n * `aria-checked` + roving tabindex (APG radio) or the single button's `aria-pressed`\n * in sync. Arrow/Home/End move focus across the radiogroup as the APG radio pattern\n * requires; applying a theme never moves focus. The `prefers-color-scheme` listener is attached on\n * `connect()` and removed on `disconnect()` (Turbo included). FOUC avoidance for the\n * very first paint is an inline `<head>` snippet, not this controller. A `mode`\n * declaration rewritten after connect is followed, but never over a stored choice\n * ({@link ThemeController.modeValueChanged}).\n *\n * Every declaration is validated where it enters, and an unreadable one falls back\n * to that Value's default rather than taking the widget with it: a `mode` outside\n * the three modes reads as `system`, and a `target` that is not a parsable selector\n * reads as `html`. An option whose `data-value` is outside the three can be focused\n * but never becomes the selection, so no more than one option is ever checked —\n * and none at all when the resolved mode matches no option, where the first one\n * keeps the Tab stop.\n *\n * The radiogroup stays one Tab stop through `RovingTabindex`, re-derived\n * whenever an option enters or leaves, so a set rendered after connect or swapped\n * by a Turbo morph carries the same single stop as any other.\n */\nexport class ThemeController extends Controller<HTMLElement> {\n static override targets = [\"option\"];\n static override values = {\n mode: { type: String, default: DEFAULT_MODE },\n storageKey: { type: String, default: \"stimeo-theme\" },\n target: { type: String, default: DEFAULT_TARGET },\n };\n static actions = [\"set\", \"toggle\"] as const;\n static events = [\"change\", \"reconcile\"] as const;\n\n declare readonly optionTargets: HTMLElement[];\n declare readonly hasOptionTarget: boolean;\n\n declare modeValue: string;\n declare storageKeyValue: string;\n declare targetValue: string;\n\n /** The OS dark-mode query, watched so `system` tracks live changes. */\n #media: MediaQueryList | null = null;\n\n /** Gate for the target and `mode` callbacks, which Stimulus runs before `connect()`. */\n #connected = false;\n\n /** The `target` declaration after validation; the default when unparsable. */\n #targetSelector = DEFAULT_TARGET;\n\n /** Owns the single Tab stop across the option set (APG radiogroup). */\n readonly #roving = new RovingTabindex(() => this.optionTargets);\n\n /**\n * The pair on screen as last reported or re-seeded, so a move can be told from a\n * repeat. Neither side is readable after the fact — assigning the Value updates the\n * mode before any comparison, and the OS query has already flipped by the time it\n * notifies — so what was reported has to be kept rather than recomputed.\n */\n #published: { mode: ThemeMode; resolved: ResolvedTheme } = {\n mode: DEFAULT_MODE,\n resolved: \"light\",\n };\n\n /** Re-resolves while in `system` mode when the OS preference flips. */\n readonly #onMediaChange = (): void => {\n // Only `system` follows the OS; an explicit mode already decided the answer.\n if (this.#mode !== \"system\") return;\n this.#commit();\n };\n\n /** Arrow/Home/End navigation for the radiogroup (APG radio pattern). */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n // A descendant widget that already claimed the key (a grabbed drag handle, a\n // nested menu) must not ALSO act on it — composition depends on this yield.\n if (event.defaultPrevented) return;\n if (isReservedArrowChord(event)) return;\n // Resolved once per keydown: every `optionTargets` access re-queries the scope.\n const options = this.optionTargets;\n const target = event.target as HTMLElement | null;\n const current = options.indexOf(target as HTMLElement);\n if (current === -1) return;\n\n let next = current;\n // Logical, not physical. The helper reverses only the horizontal pair, so\n // folding Down/Up into the same branch stays correct.\n const step = logicalArrowStep(event.key, this.element);\n switch (event.key) {\n case \"ArrowDown\":\n case \"ArrowRight\":\n case \"ArrowUp\":\n case \"ArrowLeft\":\n next = rovingMove(current, options.length, step, \"wrap\");\n break;\n case \"Home\":\n case \"End\":\n // Control+Home jumps the document, not the widget.\n if (hasModifierChord(event)) return;\n next = event.key === \"Home\" ? 0 : options.length - 1;\n break;\n default:\n return;\n }\n event.preventDefault();\n const option = options[next];\n if (!option) return;\n option.focus();\n const mode = this.#optionMode(option);\n // An option outside the three modes can hold focus, but the selection does\n // not follow it there.\n if (mode) this.#setMode(mode);\n };\n\n override connect(): void {\n const stored = this.#readStored();\n if (stored) this.modeValue = stored;\n\n this.#media = window.matchMedia?.(\"(prefers-color-scheme: dark)\") ?? null;\n this.#media?.addEventListener(\"change\", this.#onMediaChange);\n this.element.addEventListener(\"keydown\", this.#onKeydown);\n\n this.#connected = true;\n this.#applyTheme();\n this.#syncControls();\n // Connecting is not a change: seed the baseline instead of announcing one.\n this.#published = this.#current;\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#media?.removeEventListener(\"change\", this.#onMediaChange);\n this.element.removeEventListener(\"keydown\", this.#onKeydown);\n }\n\n /** Validates the `target` declaration once, so the render path never parses. */\n targetValueChanged(): void {\n this.#targetSelector = validSelector(this.element, this.targetValue, DEFAULT_TARGET);\n }\n\n /**\n * Re-renders for a `mode` declaration changed after connect — a Turbo morph\n * re-rendering the server's markup over the live element, or a script.\n *\n * A stored choice outranks the declaration: a Value that disagrees with it is written\n * back to it, so a morph that brings the server's default never overrides what the\n * user saved. Without one, the declaration is applied. Either way storage is left\n * alone, and a pair on screen that moved is reported as `reconcile`, not `change` —\n * the page moved the declaration, not a selection. The reported pair is re-seeded\n * first, so the next selection is compared with what is on screen.\n */\n modeValueChanged(): void {\n // Stimulus runs this for the initial declaration before `connect()`, which renders\n // that declaration itself.\n if (!this.#connected) return;\n // A Value that already names the mode on screen is how the controller's own writes\n // come back; resolving it against storage again would undo a selection whose save\n // failed.\n if (this.modeValue === this.#published.mode) return;\n const stored = this.#readStored();\n if (stored !== null && this.modeValue !== stored) this.modeValue = stored;\n this.#applyTheme();\n this.#reconcileControls();\n }\n\n /** Re-derives the single Tab stop and ARIA for an option set that changed. */\n optionTargetConnected(): void {\n if (this.#connected) this.#reconcileControls();\n }\n\n /** Re-derives them again when an option leaves, so a Tab stop always remains. */\n optionTargetDisconnected(): void {\n if (this.#connected) this.#reconcileControls();\n }\n\n /**\n * Selects the mode the activated option declares.\n *\n * Read through the same lane that decides which option is checked, so the two\n * can never disagree about what an option declares.\n */\n set(event: Event): void {\n const option = event.currentTarget;\n if (!(option instanceof HTMLElement)) return;\n const mode = this.#optionMode(option);\n if (mode) this.#setMode(mode);\n }\n\n /** Toggles light↔dark for the 2-value single-button contract. */\n toggle(): void {\n this.#setMode(this.#resolved() === \"dark\" ? \"light\" : \"dark\");\n }\n\n /** Central mode change: persist, apply to the root, resync controls, announce. */\n #setMode(mode: ThemeMode): void {\n this.modeValue = mode;\n this.#writeStored(mode);\n this.#commit();\n }\n\n /**\n * Applies the current mode and reports it, but reports only a real move: the\n * event means \"the selection or the effective theme moved\", so re-choosing the\n * option already chosen is not one.\n */\n #commit(): void {\n this.#applyTheme();\n this.#syncControls();\n const moved = this.#settle();\n if (moved) this.dispatch(\"change\", { detail: moved });\n }\n\n /**\n * Re-derives the controls from the mode on screen for a change the page made — a\n * `mode` declaration or an option set — and reports a pair that moved since the\n * last report as `reconcile`. An option coming or going moves neither half of the\n * pair, so of these changes only a declaration reports.\n */\n #reconcileControls(): void {\n this.#syncControls();\n const moved = this.#settle();\n if (moved) this.dispatch(\"reconcile\", { detail: moved });\n }\n\n /**\n * Takes the pair on screen as the reported one and returns it when it differs\n * from the one reported before, or `null` when neither half moved.\n *\n * The baseline moves before anything is dispatched, so a listener that selects\n * another mode is measured from the pair it was just told about. What is\n * returned is a copy: the baseline has to survive a listener that writes to what\n * it was handed, or the next unchanged operation reads as a move.\n */\n #settle(): { mode: ThemeMode; resolved: ResolvedTheme } | null {\n const next = this.#current;\n const last = this.#published;\n this.#published = next;\n if (last.mode === next.mode && last.resolved === next.resolved) return null;\n return { ...next };\n }\n\n /** The pair the `change` and `reconcile` details carry, read from current state. */\n get #current(): { mode: ThemeMode; resolved: ResolvedTheme } {\n return { mode: this.#mode, resolved: this.#resolved() };\n }\n\n /**\n * Writes `data-theme` + `color-scheme` (the resolved theme) onto the target.\n *\n * @stimeoRenderRoot\n */\n #applyTheme(): void {\n const root = this.#targetElement();\n if (!root) return;\n const resolved = this.#resolved();\n root.setAttribute(\"data-theme\", resolved);\n root.style.setProperty(\"color-scheme\", resolved);\n }\n\n /**\n * Keeps the radiogroup (aria-checked + roving tabindex) or toggle (aria-pressed) in sync.\n *\n * @stimeoRenderRoot\n */\n #syncControls(): void {\n const options = this.optionTargets;\n if (options.length > 0) {\n const mode = this.#mode;\n let selected = -1;\n options.forEach((option, index) => {\n const isSelected = this.#optionMode(option) === mode;\n option.setAttribute(\"aria-checked\", String(isSelected));\n if (isSelected && selected === -1) selected = index;\n });\n // APG: a radiogroup with no selection keeps its first radio tabbable.\n this.#roving.setActive(selected === -1 ? 0 : selected, { items: options });\n return;\n }\n // `aria-pressed` belongs to the 2-value contract, where the controller sits on\n // the button itself. A radiogroup whose options have not rendered yet is not\n // that, and must not be told it is a toggle.\n if (this.#isToggleButton) {\n this.element.setAttribute(\"aria-pressed\", String(this.#resolved() === \"dark\"));\n }\n }\n\n /** Whether the controller element is the button of the 2-value contract. */\n get #isToggleButton(): boolean {\n return this.element.tagName === \"BUTTON\" || this.element.getAttribute(\"role\") === \"button\";\n }\n\n /** The selected mode after validation; an unreadable declaration is the default. */\n get #mode(): ThemeMode {\n return isMode(this.modeValue) ? this.modeValue : DEFAULT_MODE;\n }\n\n /** The effective theme: the OS preference when `system`, else the mode itself. */\n #resolved(): ResolvedTheme {\n const mode = this.#mode;\n if (mode === \"dark\") return \"dark\";\n if (mode === \"light\") return \"light\";\n return this.#media?.matches ? \"dark\" : \"light\";\n }\n\n /** An option's mode from its `data-value`, or `null` when that is not one of the three. */\n #optionMode(option: HTMLElement): ThemeMode | null {\n const mode = option.getAttribute(\"data-value\");\n return isMode(mode) ? mode : null;\n }\n\n /** Resolves the state-hook target (`<html>` by default). */\n #targetElement(): HTMLElement | null {\n const selector = this.#targetSelector;\n if (selector === DEFAULT_TARGET || selector === \":root\") return document.documentElement;\n return document.querySelector<HTMLElement>(selector);\n }\n\n /** Reads a persisted, validated mode from `localStorage` (null when absent/blocked). */\n #readStored(): ThemeMode | null {\n const result = readLocalStorage(this.storageKeyValue);\n return result.ok && isMode(result.value) ? result.value : null;\n }\n\n /** Persists the mode, swallowing storage errors (private mode / quota). */\n #writeStored(mode: ThemeMode): void {\n writeLocalStorage(this.storageKeyValue, mode);\n }\n}\n"]}
|
|
@@ -18,6 +18,16 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
18
18
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// src/utils/field_mirror.ts
|
|
22
|
+
function writeField(field, value) {
|
|
23
|
+
if (field.value === value) return false;
|
|
24
|
+
field.value = value;
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
function commitField(target) {
|
|
28
|
+
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
// src/utils/microtask_coalescer.ts
|
|
22
32
|
var MicrotaskCoalescer = class {
|
|
23
33
|
#run;
|
|
@@ -51,6 +61,11 @@ var MicrotaskCoalescer = class {
|
|
|
51
61
|
}
|
|
52
62
|
};
|
|
53
63
|
|
|
64
|
+
// src/utils/target_selector.ts
|
|
65
|
+
function targetSelector(identifier, name) {
|
|
66
|
+
return `[data-${identifier}-target~="${name}"]`;
|
|
67
|
+
}
|
|
68
|
+
|
|
54
69
|
// src/controllers/time_picker_controller.ts
|
|
55
70
|
var AM = 0;
|
|
56
71
|
var PM = 1;
|
|
@@ -68,7 +83,10 @@ var TimePickerController = class extends Controller {
|
|
|
68
83
|
};
|
|
69
84
|
static actions = ["onKeydown"];
|
|
70
85
|
static events = ["change", "reconcile"];
|
|
71
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* Collapses target, Value, and retained-attribute morphs into one render that
|
|
88
|
+
* dispatches no `change`.
|
|
89
|
+
*/
|
|
72
90
|
#reconcile = new MicrotaskCoalescer(() => this.#reconcileDom());
|
|
73
91
|
/** Canonical state; the displayed hour and meridiem are derived from this value. */
|
|
74
92
|
#state = { hour: 0, minute: 0, second: 0 };
|
|
@@ -133,7 +151,7 @@ var TimePickerController = class extends Controller {
|
|
|
133
151
|
onKeydown(event) {
|
|
134
152
|
if (isReservedArrowChord(event)) return;
|
|
135
153
|
const segment = event.target?.closest(
|
|
136
|
-
"
|
|
154
|
+
targetSelector(this.identifier, "segment")
|
|
137
155
|
);
|
|
138
156
|
if (!segment || !this.segmentTargets.includes(segment)) return;
|
|
139
157
|
const kind = this.#kindOf(segment);
|
|
@@ -180,7 +198,7 @@ var TimePickerController = class extends Controller {
|
|
|
180
198
|
/** Clears direct-entry state when Tab, Shift+Tab, or pointer focus leaves a segment. */
|
|
181
199
|
#onFocusOut = (event) => {
|
|
182
200
|
const segment = event.target?.closest(
|
|
183
|
-
"
|
|
201
|
+
targetSelector(this.identifier, "segment")
|
|
184
202
|
);
|
|
185
203
|
if (!segment || !this.segmentTargets.includes(segment)) return;
|
|
186
204
|
this.#clearTypeBuffer();
|
|
@@ -316,8 +334,7 @@ var TimePickerController = class extends Controller {
|
|
|
316
334
|
#commitRender() {
|
|
317
335
|
this.#renderSegments();
|
|
318
336
|
const value = this.#composedValue;
|
|
319
|
-
|
|
320
|
-
if (fieldChanged) this.fieldTarget.dispatchEvent(new Event("change", { bubbles: true }));
|
|
337
|
+
if (this.#writeField(value)) commitField(this.fieldTarget);
|
|
321
338
|
if (value !== this.#lastValue) this.dispatch("change", { detail: { value } });
|
|
322
339
|
this.#lastValue = value;
|
|
323
340
|
}
|
|
@@ -336,9 +353,7 @@ var TimePickerController = class extends Controller {
|
|
|
336
353
|
}
|
|
337
354
|
/** Writes a composed value to the optional form field and reports whether it changed. */
|
|
338
355
|
#writeField(value) {
|
|
339
|
-
|
|
340
|
-
this.fieldTarget.value = value;
|
|
341
|
-
return true;
|
|
356
|
+
return this.hasFieldTarget && writeField(this.fieldTarget, value);
|
|
342
357
|
}
|
|
343
358
|
/** The canonical form value composed as `HH:MM[:SS]`. */
|
|
344
359
|
get #composedValue() {
|