stimeo-ui 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +136 -0
  3. data/README.md +27 -11
  4. data/dist/cable/index.js +384 -449
  5. data/dist/controllers/accordion_controller.js +174 -55
  6. data/dist/controllers/alert_dialog_controller.js +903 -235
  7. data/dist/controllers/announcer_controller.js +238 -205
  8. data/dist/controllers/aspect_ratio_controller.js +0 -10
  9. data/dist/controllers/auto_submit_controller.js +414 -145
  10. data/dist/controllers/avatar_controller.js +185 -107
  11. data/dist/controllers/breadcrumb_controller.js +0 -130
  12. data/dist/controllers/bulk_select_controller.js +282 -58
  13. data/dist/controllers/calendar_controller.js +228 -260
  14. data/dist/controllers/carousel_controller.js +536 -436
  15. data/dist/controllers/character_counter_controller.js +493 -207
  16. data/dist/controllers/checkbox_controller.js +38 -74
  17. data/dist/controllers/clipboard_controller.js +261 -149
  18. data/dist/controllers/collapsible_controller.js +304 -120
  19. data/dist/controllers/color_picker_controller.js +178 -146
  20. data/dist/controllers/combobox_controller.js +522 -156
  21. data/dist/controllers/command_palette_controller.js +1156 -395
  22. data/dist/controllers/conditional_fields_controller.js +300 -171
  23. data/dist/controllers/confirm_controller.js +980 -262
  24. data/dist/controllers/context_menu_controller.js +346 -153
  25. data/dist/controllers/count_up_controller.js +73 -21
  26. data/dist/controllers/countdown_controller.js +281 -158
  27. data/dist/controllers/currency_input_controller.js +575 -225
  28. data/dist/controllers/data_grid_controller.js +248 -214
  29. data/dist/controllers/date_range_picker_controller.js +338 -235
  30. data/dist/controllers/dialog_controller.js +899 -237
  31. data/dist/controllers/direct_upload_controller.js +97 -167
  32. data/dist/controllers/dirty_form_controller.js +72 -69
  33. data/dist/controllers/dismissible_controller.js +196 -24
  34. data/dist/controllers/drawer_controller.js +974 -355
  35. data/dist/controllers/dropdown_controller.js +339 -96
  36. data/dist/controllers/editable_controller.js +315 -88
  37. data/dist/controllers/empty_state_controller.js +79 -58
  38. data/dist/controllers/file_dropzone_controller.js +322 -211
  39. data/dist/controllers/filter_controller.js +77 -42
  40. data/dist/controllers/flash_controller.js +238 -347
  41. data/dist/controllers/focus_controller.js +679 -240
  42. data/dist/controllers/form_field_controller.js +156 -132
  43. data/dist/controllers/form_validation_controller.js +31 -97
  44. data/dist/controllers/frame_loading_controller.js +275 -217
  45. data/dist/controllers/highlight_controller.js +107 -75
  46. data/dist/controllers/hover_card_controller.js +327 -130
  47. data/dist/controllers/idle_controller.js +271 -101
  48. data/dist/controllers/input_mask_controller.js +483 -97
  49. data/dist/controllers/intersection_controller.js +150 -106
  50. data/dist/controllers/lazy_frame_controller.js +24 -68
  51. data/dist/controllers/listbox_controller.js +523 -240
  52. data/dist/controllers/local_time_controller.js +102 -77
  53. data/dist/controllers/masonry_controller.js +80 -96
  54. data/dist/controllers/menu_controller.js +302 -188
  55. data/dist/controllers/menubar_controller.js +232 -345
  56. data/dist/controllers/meter_controller.js +417 -87
  57. data/dist/controllers/multi_select_controller.js +785 -393
  58. data/dist/controllers/navigation_menu_controller.js +190 -250
  59. data/dist/controllers/nested_form_controller.js +396 -192
  60. data/dist/controllers/network_status_controller.js +268 -62
  61. data/dist/controllers/number_input_controller.js +610 -303
  62. data/dist/controllers/optimistic_controller.js +45 -87
  63. data/dist/controllers/otp_controller.js +658 -255
  64. data/dist/controllers/overflow_indicator_controller.js +122 -113
  65. data/dist/controllers/overflow_menu_controller.js +470 -295
  66. data/dist/controllers/pagination_controller.js +332 -123
  67. data/dist/controllers/password_reveal_controller.js +392 -121
  68. data/dist/controllers/password_strength_controller.js +111 -158
  69. data/dist/controllers/persist_controller.js +165 -111
  70. data/dist/controllers/pointer_drag_controller.js +434 -188
  71. data/dist/controllers/popover_controller.js +269 -113
  72. data/dist/controllers/portal_controller.js +215 -76
  73. data/dist/controllers/preview_guard_controller.js +243 -116
  74. data/dist/controllers/progress_controller.js +391 -78
  75. data/dist/controllers/radio_group_controller.js +278 -211
  76. data/dist/controllers/range_slider_controller.js +220 -113
  77. data/dist/controllers/rating_controller.js +458 -211
  78. data/dist/controllers/read_more_controller.js +138 -63
  79. data/dist/controllers/reading_progress_controller.js +166 -137
  80. data/dist/controllers/relative_time_controller.js +211 -112
  81. data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +103 -38
  82. data/dist/controllers/resizable_controller.js +299 -150
  83. data/dist/controllers/roving_controller.js +49 -88
  84. data/dist/controllers/scroll_area_controller.js +490 -246
  85. data/dist/controllers/scroll_restore_controller.js +14 -55
  86. data/dist/controllers/scroll_visibility_controller.js +432 -149
  87. data/dist/controllers/scrollspy_controller.js +193 -248
  88. data/dist/controllers/separator_controller.js +285 -142
  89. data/dist/controllers/sidebar_controller.js +1069 -404
  90. data/dist/controllers/skeleton_controller.js +259 -101
  91. data/dist/controllers/slider_controller.js +187 -86
  92. data/dist/controllers/smart_sticky_header_controller.js +173 -66
  93. data/dist/controllers/sortable_controller.js +26 -125
  94. data/dist/controllers/spinner_controller.js +288 -173
  95. data/dist/controllers/step_indicator_controller.js +105 -46
  96. data/dist/controllers/stepper_controller.js +120 -73
  97. data/dist/controllers/stick_to_bottom_controller.js +275 -140
  98. data/dist/controllers/sticky_observer_controller.js +220 -49
  99. data/dist/controllers/submit_once_controller.js +376 -227
  100. data/dist/controllers/switch_controller.js +201 -84
  101. data/dist/controllers/tabs_controller.js +41 -30
  102. data/dist/controllers/tags_input_controller.js +293 -173
  103. data/dist/controllers/textarea_autosize_controller.js +108 -74
  104. data/dist/controllers/theme_controller.js +140 -116
  105. data/dist/controllers/time_picker_controller.js +331 -127
  106. data/dist/controllers/toast_controller.js +244 -324
  107. data/dist/controllers/toggle_group_controller.js +252 -191
  108. data/dist/controllers/toolbar_controller.js +219 -120
  109. data/dist/controllers/tooltip_controller.js +158 -124
  110. data/dist/controllers/transition_controller.js +123 -119
  111. data/dist/controllers/tree_view_controller.js +283 -369
  112. data/dist/index.js +8573 -10410
  113. data/dist/positioning/index.js +154 -47
  114. data/lib/generators/stimeo/install/install_generator.rb +7 -2
  115. data/lib/generators/stimeo/install/templates/stimeo.js +19 -6
  116. data/lib/stimeo/ui/version.rb +1 -1
  117. metadata +3 -3
@@ -1,13 +1,31 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/theme_controller.ts
3
+ var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
4
+ var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
5
+ function stateReasonFor(event) {
6
+ if (!event) return "api";
7
+ if (FOCUS_EVENTS.has(event.type)) return "focus";
8
+ if (POINTER_EVENTS.has(event.type)) return "pointer";
9
+ return "user";
10
+ }
11
+
12
+ function actionSource(source) {
13
+ if (source instanceof HTMLElement) {
14
+ return { event: null, host: source, origin: source, reason: "api" };
15
+ }
16
+ const event = source ?? null;
17
+ return {
18
+ event,
19
+ host: event?.currentTarget instanceof Element ? event.currentTarget : null,
20
+ origin: event?.target instanceof Element ? event.target : null,
21
+ reason: stateReasonFor(event)
22
+ };
23
+ }
4
24
 
5
- // src/utils/logical_scroll.ts
6
25
  function isRtl(element) {
7
26
  return window.getComputedStyle(element).direction === "rtl";
8
27
  }
9
28
 
10
- // src/utils/arrow_step.ts
11
29
  function logicalArrowStep(key, element) {
12
30
  if (key === "ArrowDown") return 1;
13
31
  if (key === "ArrowUp") return -1;
@@ -23,7 +41,6 @@ function hasModifierChord(event) {
23
41
  return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
24
42
  }
25
43
 
26
- // src/utils/declared_value.ts
27
44
  function parseDeclared(raw, parse, fallback) {
28
45
  try {
29
46
  return parse(raw);
@@ -43,31 +60,84 @@ function validSelector(element, raw, fallback) {
43
60
  );
44
61
  }
45
62
 
46
- // src/utils/roving_tabindex.ts
63
+ var ListenerSet = class {
64
+ #abort = new AbortController();
65
+ add(target, type, handler, options) {
66
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
67
+ }
68
+ dispose() {
69
+ this.#abort.abort();
70
+ this.#abort = new AbortController();
71
+ }
72
+ };
73
+
74
+ var MicrotaskCoalescer = class {
75
+ #run;
76
+ #queued = false;
77
+ #active = false;
78
+ #generation = 0;
79
+ constructor(run) {
80
+ this.#run = run;
81
+ }
82
+ activate() {
83
+ this.#active = true;
84
+ }
85
+ cancel() {
86
+ this.#active = false;
87
+ this.#queued = false;
88
+ this.#generation += 1;
89
+ }
90
+ get pending() {
91
+ return this.#queued;
92
+ }
93
+ schedule() {
94
+ if (!this.#active || this.#queued) return;
95
+ this.#queued = true;
96
+ const generation = this.#generation;
97
+ queueMicrotask(() => {
98
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
99
+ this.#queued = false;
100
+ this.#run();
101
+ });
102
+ }
103
+ };
104
+
105
+ var MorphRenderWatcher = class {
106
+ #listeners = new ListenerSet();
107
+ #pass;
108
+ #element = null;
109
+ #onMorph = () => this.#pass.schedule();
110
+ constructor(run) {
111
+ this.#pass = new MicrotaskCoalescer(run);
112
+ }
113
+ observe(element) {
114
+ if (this.#element === element) return;
115
+ this.disconnect();
116
+ this.#element = element;
117
+ this.#pass.activate();
118
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
119
+ }
120
+ schedule() {
121
+ this.#pass.schedule();
122
+ }
123
+ get pending() {
124
+ return this.#pass.pending;
125
+ }
126
+ disconnect() {
127
+ this.#listeners.dispose();
128
+ this.#pass.cancel();
129
+ this.#element = null;
130
+ }
131
+ };
132
+
47
133
  var RovingTabindex = class {
48
- /** Returns the current ordered item elements; called on every operation. */
49
134
  #getItems;
50
- /**
51
- * @param getItems - Returns the current ordered item elements. Called on every
52
- * operation so the live target list is always used.
53
- */
54
135
  constructor(getItems) {
55
136
  this.#getItems = getItems;
56
137
  }
57
- /** Index of the currently tabbable item (`tabindex="0"`), or `-1` if none. */
58
138
  get activeIndex() {
59
139
  return this.#getItems().findIndex((item) => item.tabIndex === 0);
60
140
  }
61
- /**
62
- * Makes exactly the item at `index` tabbable (`tabindex="0"`) and removes every
63
- * other item from the Tab sequence (`tabindex="-1"`). An out-of-range `index`
64
- * (e.g. `-1`) leaves all items at `-1`, which a controller can use to express
65
- * "nothing is currently tabbable".
66
- *
67
- * @param index - Position of the item to make tabbable.
68
- * @param options - Pass `{ focus: true }` to also move DOM focus to that item,
69
- * and `items` to reuse an event-scoped collection snapshot.
70
- */
71
141
  setActive(index, options = {}) {
72
142
  const { focus = false } = options;
73
143
  const items = options.items ?? this.#getItems();
@@ -83,7 +153,6 @@ function rovingMove(current, length, delta, wrap) {
83
153
  return (next + length) % length;
84
154
  }
85
155
 
86
- // src/utils/safe_storage.ts
87
156
  function readLocalStorage(key) {
88
157
  try {
89
158
  return { ok: true, value: window.localStorage.getItem(key) };
@@ -100,7 +169,10 @@ function writeLocalStorage(key, value) {
100
169
  }
101
170
  }
102
171
 
103
- // src/controllers/theme_controller.ts
172
+ function targetSelector(identifier, name) {
173
+ return `[data-${identifier}-target~="${name}"]`;
174
+ }
175
+
104
176
  var MODES = ["light", "dark", "system"];
105
177
  var isMode = (value) => typeof value === "string" && MODES.includes(value);
106
178
  var DEFAULT_MODE = "system";
@@ -114,30 +186,20 @@ var ThemeController = class extends Controller {
114
186
  };
115
187
  static actions = ["set", "toggle"];
116
188
  static events = ["change", "reconcile"];
117
- /** The OS dark-mode query, watched so `system` tracks live changes. */
118
189
  #media = null;
119
- /** Gate for the target and `mode` callbacks, which Stimulus runs before `connect()`. */
190
+ #listeners = new ListenerSet();
120
191
  #connected = false;
121
- /** The `target` declaration after validation; the default when unparsable. */
122
192
  #targetSelector = DEFAULT_TARGET;
123
- /** Owns the single Tab stop across the option set (APG radiogroup). */
193
+ #storageKeyRead = null;
124
194
  #roving = new RovingTabindex(() => this.optionTargets);
125
- /**
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
- */
131
195
  #published = {
132
196
  mode: DEFAULT_MODE,
133
197
  resolved: "light"
134
198
  };
135
- /** Re-resolves while in `system` mode when the OS preference flips. */
136
199
  #onMediaChange = () => {
137
200
  if (this.#mode !== "system") return;
138
- this.#commit();
201
+ this.#commit("media");
139
202
  };
140
- /** Arrow/Home/End navigation for the radiogroup (APG radio pattern). */
141
203
  #onKeydown = (event) => {
142
204
  if (event.defaultPrevented) return;
143
205
  if (isReservedArrowChord(event)) return;
@@ -169,37 +231,32 @@ var ThemeController = class extends Controller {
169
231
  const mode = this.#optionMode(option);
170
232
  if (mode) this.#setMode(mode);
171
233
  };
234
+ #morph = new MorphRenderWatcher(() => {
235
+ this.#applyTheme();
236
+ this.#reconcileControls();
237
+ });
172
238
  connect() {
239
+ this.#morph.observe(this.element);
240
+ this.#storageKeyRead = this.storageKeyValue;
173
241
  const stored = this.#readStored();
174
242
  if (stored) this.modeValue = stored;
175
243
  this.#media = window.matchMedia?.("(prefers-color-scheme: dark)") ?? null;
176
- this.#media?.addEventListener("change", this.#onMediaChange);
177
- this.element.addEventListener("keydown", this.#onKeydown);
244
+ if (this.#media) this.#listeners.add(this.#media, "change", this.#onMediaChange);
245
+ this.#listeners.add(this.element, "keydown", this.#onKeydown);
178
246
  this.#connected = true;
179
247
  this.#applyTheme();
180
248
  this.#syncControls();
181
249
  this.#published = this.#current;
182
250
  }
183
251
  disconnect() {
252
+ this.#morph.disconnect();
184
253
  this.#connected = false;
185
- this.#media?.removeEventListener("change", this.#onMediaChange);
186
- this.element.removeEventListener("keydown", this.#onKeydown);
254
+ this.#listeners.dispose();
187
255
  }
188
- /** Validates the `target` declaration once, so the render path never parses. */
189
256
  targetValueChanged() {
190
257
  this.#targetSelector = validSelector(this.element, this.targetValue, DEFAULT_TARGET);
258
+ this.#morph.schedule();
191
259
  }
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
260
  modeValueChanged() {
204
261
  if (!this.#connected) return;
205
262
  if (this.modeValue === this.#published.mode) return;
@@ -208,67 +265,53 @@ var ThemeController = class extends Controller {
208
265
  this.#applyTheme();
209
266
  this.#reconcileControls();
210
267
  }
211
- /** Re-derives the single Tab stop and ARIA for an option set that changed. */
268
+ storageKeyValueChanged() {
269
+ if (!this.#connected) return;
270
+ const key = this.storageKeyValue;
271
+ if (key === this.#storageKeyRead) return;
272
+ this.#storageKeyRead = key;
273
+ const stored = this.#readStored();
274
+ if (stored === null || stored === this.modeValue) return;
275
+ this.modeValue = stored;
276
+ this.#applyTheme();
277
+ this.#reconcileControls();
278
+ }
212
279
  optionTargetConnected() {
213
- if (this.#connected) this.#reconcileControls();
280
+ if (this.#connected) this.#morph.schedule();
214
281
  }
215
- /** Re-derives them again when an option leaves, so a Tab stop always remains. */
216
282
  optionTargetDisconnected() {
217
- if (this.#connected) this.#reconcileControls();
218
- }
219
- /**
220
- * Selects the mode the activated option declares.
221
- *
222
- * Read through the same lane that decides which option is checked, so the two
223
- * can never disagree about what an option declares.
224
- */
225
- set(event) {
226
- const option = event.currentTarget;
227
- if (!(option instanceof HTMLElement)) return;
283
+ if (this.#connected) this.#morph.schedule();
284
+ }
285
+ set(source) {
286
+ const { host, origin, reason } = actionSource(source);
287
+ const option = host?.closest(targetSelector(this.identifier, "option"));
288
+ if (!option || !this.optionTargets.includes(option)) return;
289
+ if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
290
+ return;
228
291
  const mode = this.#optionMode(option);
229
- if (mode) this.#setMode(mode);
292
+ if (mode) this.#setMode(mode, reason);
230
293
  }
231
- /** Toggles light↔dark for the 2-value single-button contract. */
232
- toggle() {
233
- this.#setMode(this.#resolved() === "dark" ? "light" : "dark");
294
+ toggle(event) {
295
+ const mode = this.#readStored() ?? this.#mode;
296
+ const dark = mode === "dark" || mode === "system" && this.#media?.matches === true;
297
+ this.#setMode(dark ? "light" : "dark", stateReasonFor(event));
234
298
  }
235
- /** Central mode change: persist, apply to the root, resync controls, announce. */
236
- #setMode(mode) {
299
+ #setMode(mode, reason = "user") {
237
300
  this.modeValue = mode;
238
301
  this.#writeStored(mode);
239
- this.#commit();
240
- }
241
- /**
242
- * Applies the current mode and reports it, but reports only a real move: the
243
- * event means "the selection or the effective theme moved", so re-choosing the
244
- * option already chosen is not one.
245
- */
246
- #commit() {
302
+ this.#commit(reason);
303
+ }
304
+ #commit(reason) {
247
305
  this.#applyTheme();
248
306
  this.#syncControls();
249
307
  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
- */
308
+ if (moved) this.dispatch("change", { detail: { ...moved, reason } });
309
+ }
258
310
  #reconcileControls() {
259
311
  this.#syncControls();
260
312
  const moved = this.#settle();
261
313
  if (moved) this.dispatch("reconcile", { detail: moved });
262
314
  }
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
315
  #settle() {
273
316
  const next = this.#current;
274
317
  const last = this.#published;
@@ -276,15 +319,9 @@ var ThemeController = class extends Controller {
276
319
  if (last.mode === next.mode && last.resolved === next.resolved) return null;
277
320
  return { ...next };
278
321
  }
279
- /** The pair the `change` and `reconcile` details carry, read from current state. */
280
322
  get #current() {
281
323
  return { mode: this.#mode, resolved: this.#resolved() };
282
324
  }
283
- /**
284
- * Writes `data-theme` + `color-scheme` (the resolved theme) onto the target.
285
- *
286
- * @stimeoRenderRoot
287
- */
288
325
  #applyTheme() {
289
326
  const root = this.#targetElement();
290
327
  if (!root) return;
@@ -292,11 +329,6 @@ var ThemeController = class extends Controller {
292
329
  root.setAttribute("data-theme", resolved);
293
330
  root.style.setProperty("color-scheme", resolved);
294
331
  }
295
- /**
296
- * Keeps the radiogroup (aria-checked + roving tabindex) or toggle (aria-pressed) in sync.
297
- *
298
- * @stimeoRenderRoot
299
- */
300
332
  #syncControls() {
301
333
  const options = this.optionTargets;
302
334
  if (options.length > 0) {
@@ -314,43 +346,35 @@ var ThemeController = class extends Controller {
314
346
  this.element.setAttribute("aria-pressed", String(this.#resolved() === "dark"));
315
347
  }
316
348
  }
317
- /** Whether the controller element is the button of the 2-value contract. */
318
349
  get #isToggleButton() {
319
350
  return this.element.tagName === "BUTTON" || this.element.getAttribute("role") === "button";
320
351
  }
321
- /** The selected mode after validation; an unreadable declaration is the default. */
322
352
  get #mode() {
323
353
  return isMode(this.modeValue) ? this.modeValue : DEFAULT_MODE;
324
354
  }
325
- /** The effective theme: the OS preference when `system`, else the mode itself. */
326
355
  #resolved() {
327
356
  const mode = this.#mode;
328
357
  if (mode === "dark") return "dark";
329
358
  if (mode === "light") return "light";
330
359
  return this.#media?.matches ? "dark" : "light";
331
360
  }
332
- /** An option's mode from its `data-value`, or `null` when that is not one of the three. */
333
361
  #optionMode(option) {
334
362
  const mode = option.getAttribute("data-value");
335
363
  return isMode(mode) ? mode : null;
336
364
  }
337
- /** Resolves the state-hook target (`<html>` by default). */
338
365
  #targetElement() {
339
366
  const selector = this.#targetSelector;
340
367
  if (selector === DEFAULT_TARGET || selector === ":root") return document.documentElement;
341
368
  return document.querySelector(selector);
342
369
  }
343
- /** Reads a persisted, validated mode from `localStorage` (null when absent/blocked). */
344
370
  #readStored() {
345
371
  const result = readLocalStorage(this.storageKeyValue);
346
372
  return result.ok && isMode(result.value) ? result.value : null;
347
373
  }
348
- /** Persists the mode, swallowing storage errors (private mode / quota). */
349
374
  #writeStored(mode) {
350
375
  writeLocalStorage(this.storageKeyValue, mode);
351
376
  }
352
377
  };
353
378
 
354
379
  export { ThemeController };
355
- //# sourceMappingURL=theme_controller.js.map
356
380
  //# sourceMappingURL=theme_controller.js.map