@radioactive-labs/plutonium 0.49.0 → 0.50.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.
@@ -13326,6 +13326,12 @@
13326
13326
  this.frameTarget.removeEventListener("turbo:fetch-request-error", this.frameFailed);
13327
13327
  }
13328
13328
  frameLoading(event) {
13329
+ if (event) {
13330
+ const trigger = event.target.closest("a, form");
13331
+ const requested = trigger?.dataset?.turboFrame;
13332
+ if (requested && requested !== this.frameTarget.id)
13333
+ return;
13334
+ }
13329
13335
  this.#loadingStarted();
13330
13336
  }
13331
13337
  frameFailed(event) {
@@ -13347,17 +13353,26 @@
13347
13353
  }
13348
13354
  homeButtonClicked(event) {
13349
13355
  this.frameLoading(null);
13356
+ this.srcHistory = [this.originalFrameSrc];
13357
+ this.#updateNavigationButtonsDisplay();
13358
+ this._homeRequested = true;
13350
13359
  this.frameTarget.src = this.originalFrameSrc;
13360
+ this.frameTarget.reload();
13351
13361
  }
13352
13362
  get currentSrc() {
13353
13363
  return this.srcHistory[this.srcHistory.length - 1];
13354
13364
  }
13355
13365
  #notifySrcChanged(src) {
13356
- if (src == this.currentSrc) {
13357
- } else if (src == this.originalFrameSrc)
13366
+ if (this._homeRequested) {
13367
+ this._homeRequested = false;
13358
13368
  this.srcHistory = [src];
13359
- else
13369
+ this.originalFrameSrc = src;
13370
+ } else if (src == this.currentSrc) {
13371
+ } else if (src == this.originalFrameSrc) {
13372
+ this.srcHistory = [src];
13373
+ } else {
13360
13374
  this.srcHistory.push(src);
13375
+ }
13361
13376
  this.#updateNavigationButtonsDisplay();
13362
13377
  if (this.hasMaximizeLinkTarget)
13363
13378
  this.maximizeLinkTarget.href = src;
@@ -16884,6 +16899,21 @@ ${text2}</tr>
16884
16899
  }
16885
16900
  if (this.modal) {
16886
16901
  options2.appendTo = this.modal;
16902
+ options2.position = (instance) => {
16903
+ const input = instance.altInput || instance.input;
16904
+ const inputRect = input.getBoundingClientRect();
16905
+ const modalRect = this.modal.getBoundingClientRect();
16906
+ const cal = instance.calendarContainer;
16907
+ const calHeight = cal.offsetHeight;
16908
+ const spaceBelow = window.innerHeight - inputRect.bottom;
16909
+ const showAbove = spaceBelow < calHeight && inputRect.top > calHeight;
16910
+ const top2 = showAbove ? inputRect.top - modalRect.top - calHeight - 2 : inputRect.bottom - modalRect.top + 2;
16911
+ cal.style.top = `${top2}px`;
16912
+ cal.style.left = `${inputRect.left - modalRect.left}px`;
16913
+ cal.style.right = "auto";
16914
+ cal.classList.toggle("arrowTop", !showAbove);
16915
+ cal.classList.toggle("arrowBottom", showAbove);
16916
+ };
16887
16917
  }
16888
16918
  return options2;
16889
16919
  }
@@ -16959,12 +16989,28 @@ ${text2}</tr>
16959
16989
  connect() {
16960
16990
  this.activeClasses = this.hasActiveClassesValue ? this.activeClassesValue.split(" ") : [];
16961
16991
  this.inActiveClasses = this.hasInActiveClassesValue ? this.inActiveClassesValue.split(" ") : [];
16962
- this.#selectInternal(this.defaultTabValue || this.btnTargets[0].id);
16992
+ const fromHash = this.#buttonIdFromHash();
16993
+ const initialId = fromHash || this.defaultTabValue || this.btnTargets[0]?.id;
16994
+ this.#selectInternal(initialId, { skipFocus: true, skipHashUpdate: true });
16995
+ this._syncFromHash = this._syncFromHash.bind(this);
16996
+ window.addEventListener("hashchange", this._syncFromHash);
16997
+ document.addEventListener("turbo:load", this._syncFromHash);
16998
+ }
16999
+ disconnect() {
17000
+ if (this._syncFromHash) {
17001
+ window.removeEventListener("hashchange", this._syncFromHash);
17002
+ document.removeEventListener("turbo:load", this._syncFromHash);
17003
+ }
17004
+ }
17005
+ _syncFromHash() {
17006
+ const id2 = this.#buttonIdFromHash();
17007
+ if (id2)
17008
+ this.#selectInternal(id2, { skipFocus: true, skipHashUpdate: true });
16963
17009
  }
16964
17010
  select(event) {
16965
17011
  this.#selectInternal(event.currentTarget.id);
16966
17012
  }
16967
- #selectInternal(id2) {
17013
+ #selectInternal(id2, options2 = {}) {
16968
17014
  const selectedBtn = this.btnTargets.find((element) => element.id === id2);
16969
17015
  if (!selectedBtn) {
16970
17016
  console.error(`Tab Button with id "${id2}" not found`);
@@ -16991,10 +17037,29 @@ ${text2}</tr>
16991
17037
  selectedBtn.classList.add(...this.activeClasses);
16992
17038
  selectedTab.hidden = false;
16993
17039
  selectedTab.setAttribute("aria-hidden", "false");
16994
- if (selectedBtn !== document.activeElement) {
17040
+ if (!options2.skipHashUpdate)
17041
+ this.#updateHash(id2);
17042
+ if (!options2.skipFocus && selectedBtn !== document.activeElement) {
16995
17043
  selectedBtn.focus();
16996
17044
  }
16997
17045
  }
17046
+ // Button ids follow `${identifier}-tab`. The URL hash carries just
17047
+ // the identifier (e.g., #details, #orders).
17048
+ #buttonIdFromHash() {
17049
+ const hash3 = window.location.hash.replace(/^#/, "");
17050
+ if (!hash3)
17051
+ return null;
17052
+ const candidateId = `${hash3}-tab`;
17053
+ const exists = this.btnTargets.some((btn) => btn.id === candidateId);
17054
+ return exists ? candidateId : null;
17055
+ }
17056
+ #updateHash(buttonId) {
17057
+ const identifier = buttonId.replace(/-tab$/, "");
17058
+ const newHash = `#${identifier}`;
17059
+ if (window.location.hash !== newHash) {
17060
+ history.replaceState(null, "", newHash);
17061
+ }
17062
+ }
16998
17063
  };
16999
17064
 
17000
17065
  // node_modules/@uppy/utils/lib/Translator.js
@@ -27674,7 +27739,27 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
27674
27739
  };
27675
27740
 
27676
27741
  // src/js/controllers/sidebar_controller.js
27742
+ var savedScrollTop = 0;
27677
27743
  var sidebar_controller_default = class extends Controller {
27744
+ static targets = ["scroll"];
27745
+ connect() {
27746
+ this.beforeRender = this.beforeRender.bind(this);
27747
+ this.afterRender = this.afterRender.bind(this);
27748
+ document.addEventListener("turbo:before-render", this.beforeRender);
27749
+ document.addEventListener("turbo:render", this.afterRender);
27750
+ }
27751
+ disconnect() {
27752
+ document.removeEventListener("turbo:before-render", this.beforeRender);
27753
+ document.removeEventListener("turbo:render", this.afterRender);
27754
+ }
27755
+ beforeRender() {
27756
+ if (this.hasScrollTarget)
27757
+ savedScrollTop = this.scrollTarget.scrollTop;
27758
+ }
27759
+ afterRender() {
27760
+ if (this.hasScrollTarget)
27761
+ this.scrollTarget.scrollTop = savedScrollTop;
27762
+ }
27678
27763
  };
27679
27764
 
27680
27765
  // src/js/controllers/password_visibility_controller.js
@@ -27819,18 +27904,7 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
27819
27904
 
27820
27905
  // src/js/controllers/bulk_actions_controller.js
27821
27906
  var bulk_actions_controller_default = class extends Controller {
27822
- static targets = ["checkbox", "checkboxAll", "toolbar", "selectedCount", "actionButton", "selectionCell"];
27823
- static values = {
27824
- hasActions: { type: Boolean, default: false }
27825
- };
27826
- connect() {
27827
- if (this.hasActionsValue) {
27828
- this.enableSelection();
27829
- }
27830
- }
27831
- enableSelection() {
27832
- this.selectionCellTargets.forEach((el) => el.classList.remove("hidden"));
27833
- }
27907
+ static targets = ["checkbox", "checkboxAll", "toolbar", "selectedCount", "actionButton", "filterPills"];
27834
27908
  toggle() {
27835
27909
  this.updateUI();
27836
27910
  }
@@ -27849,6 +27923,9 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
27849
27923
  if (this.hasToolbarTarget) {
27850
27924
  this.toolbarTarget.classList.toggle("hidden", checked.length === 0);
27851
27925
  }
27926
+ if (this.hasFilterPillsTarget) {
27927
+ this.filterPillsTarget.classList.toggle("hidden", checked.length > 0);
27928
+ }
27852
27929
  if (this.hasSelectedCountTarget) {
27853
27930
  this.selectedCountTarget.textContent = checked.length;
27854
27931
  }
@@ -27884,6 +27961,14 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
27884
27961
  const allowedActions = checkbox.dataset.allowedActions;
27885
27962
  return allowedActions ? allowedActions.split(",").filter((a4) => a4) : [];
27886
27963
  }
27964
+ clearSelection() {
27965
+ this.checkboxTargets.forEach((cb) => cb.checked = false);
27966
+ if (this.hasCheckboxAllTarget) {
27967
+ this.checkboxAllTarget.checked = false;
27968
+ this.checkboxAllTarget.indeterminate = false;
27969
+ }
27970
+ this.updateUI();
27971
+ }
27887
27972
  get checked() {
27888
27973
  return this.checkboxTargets.filter((cb) => cb.checked);
27889
27974
  }
@@ -27894,6 +27979,56 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
27894
27979
 
27895
27980
  // src/js/controllers/filter_panel_controller.js
27896
27981
  var filter_panel_controller_default = class extends Controller {
27982
+ static targets = ["panel", "backdrop"];
27983
+ connect() {
27984
+ this._onKeydown = this._onKeydown.bind(this);
27985
+ }
27986
+ disconnect() {
27987
+ if (this.isOpen) {
27988
+ document.removeEventListener("keydown", this._onKeydown);
27989
+ this._unlockBodyScroll();
27990
+ }
27991
+ }
27992
+ toggle() {
27993
+ this.isOpen ? this.close() : this.open();
27994
+ }
27995
+ open() {
27996
+ if (this.hasPanelTarget) {
27997
+ this.panelTarget.setAttribute("data-open", "");
27998
+ this.panelTarget.setAttribute("aria-hidden", "false");
27999
+ }
28000
+ if (this.hasBackdropTarget)
28001
+ this.backdropTarget.setAttribute("data-open", "");
28002
+ this._lockBodyScroll();
28003
+ document.addEventListener("keydown", this._onKeydown);
28004
+ }
28005
+ close() {
28006
+ if (this.hasPanelTarget) {
28007
+ this.panelTarget.removeAttribute("data-open");
28008
+ this.panelTarget.setAttribute("aria-hidden", "true");
28009
+ }
28010
+ if (this.hasBackdropTarget)
28011
+ this.backdropTarget.removeAttribute("data-open");
28012
+ this._unlockBodyScroll();
28013
+ document.removeEventListener("keydown", this._onKeydown);
28014
+ }
28015
+ // Mirrors remote-modal's approach: stash the body's current overflow
28016
+ // and restore it on close. Avoids stomping a value another component
28017
+ // (e.g. an open dialog) may have set.
28018
+ _lockBodyScroll() {
28019
+ if (this._previousBodyOverflow != null)
28020
+ return;
28021
+ this._previousBodyOverflow = document.body.style.overflow;
28022
+ document.body.style.overflow = "hidden";
28023
+ }
28024
+ _unlockBodyScroll() {
28025
+ if (this._previousBodyOverflow == null)
28026
+ return;
28027
+ document.body.style.overflow = this._previousBodyOverflow;
28028
+ this._previousBodyOverflow = null;
28029
+ }
28030
+ // Reset every input under this controller's scope, then submit so the
28031
+ // table reflects the cleared filters immediately.
27897
28032
  clear() {
27898
28033
  this.element.querySelectorAll("input, select, textarea").forEach((input) => {
27899
28034
  if (input.type === "checkbox" || input.type === "radio") {
@@ -27901,23 +28036,27 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
27901
28036
  } else if (input.tagName === "SELECT") {
27902
28037
  input.selectedIndex = 0;
27903
28038
  } else if (input.type === "hidden") {
27904
- if (input.dataset.controller === "flatpickr") {
28039
+ if (input.dataset.controller === "flatpickr")
27905
28040
  input.value = "";
27906
- }
27907
28041
  } else {
27908
28042
  input.value = "";
27909
28043
  }
27910
28044
  });
27911
28045
  this.element.querySelectorAll('[data-controller="flatpickr"]').forEach((input) => {
27912
28046
  const controller = this.application.getControllerForElementAndIdentifier(input, "flatpickr");
27913
- if (controller?.picker) {
28047
+ if (controller?.picker)
27914
28048
  controller.picker.clear();
27915
- }
27916
28049
  });
27917
- const form = this.element.closest("form");
27918
- if (form) {
28050
+ const form = this.element.querySelector("form");
28051
+ if (form)
27919
28052
  form.requestSubmit();
27920
- }
28053
+ }
28054
+ get isOpen() {
28055
+ return this.hasPanelTarget && this.panelTarget.hasAttribute("data-open");
28056
+ }
28057
+ _onKeydown(event) {
28058
+ if (event.key === "Escape")
28059
+ this.close();
27921
28060
  }
27922
28061
  };
27923
28062
 
@@ -27998,6 +28137,238 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
27998
28137
  }
27999
28138
  };
28000
28139
 
28140
+ // src/js/controllers/icon_rail_controller.js
28141
+ var icon_rail_controller_default = class extends Controller {
28142
+ static values = {
28143
+ storageKey: { type: String, default: "pu_rail_pinned" }
28144
+ };
28145
+ connect() {
28146
+ const pinned = localStorage.getItem(this.storageKeyValue) === "true";
28147
+ if (pinned) {
28148
+ document.body.classList.add("pu-rail-pinned");
28149
+ }
28150
+ }
28151
+ togglePin() {
28152
+ const pinned = document.body.classList.toggle("pu-rail-pinned");
28153
+ localStorage.setItem(this.storageKeyValue, pinned);
28154
+ }
28155
+ };
28156
+
28157
+ // src/js/controllers/icon_rail_flyout_controller.js
28158
+ var icon_rail_flyout_controller_default = class extends Controller {
28159
+ static targets = ["trigger", "panel"];
28160
+ static values = {
28161
+ closeDelay: { type: Number, default: 150 }
28162
+ };
28163
+ connect() {
28164
+ this._closeTimer = null;
28165
+ this._open = false;
28166
+ this._panel = null;
28167
+ this._panelHome = null;
28168
+ this._onPanelEnter = () => {
28169
+ if (this._closeTimer) {
28170
+ clearTimeout(this._closeTimer);
28171
+ this._closeTimer = null;
28172
+ }
28173
+ };
28174
+ this._onPanelLeave = () => this.scheduleClose();
28175
+ }
28176
+ disconnect() {
28177
+ this._returnPanel();
28178
+ }
28179
+ open() {
28180
+ if (this._closeTimer) {
28181
+ clearTimeout(this._closeTimer);
28182
+ this._closeTimer = null;
28183
+ }
28184
+ if (this._open)
28185
+ return;
28186
+ if (!this._panel && !this.hasPanelTarget)
28187
+ return;
28188
+ this._open = true;
28189
+ this.element.dataset.flyoutOpen = "true";
28190
+ this._portalPanel();
28191
+ this._position();
28192
+ }
28193
+ scheduleClose() {
28194
+ if (this._closeTimer)
28195
+ clearTimeout(this._closeTimer);
28196
+ this._closeTimer = setTimeout(() => this.close(), this.closeDelayValue);
28197
+ }
28198
+ close() {
28199
+ if (!this._open)
28200
+ return;
28201
+ this._open = false;
28202
+ delete this.element.dataset.flyoutOpen;
28203
+ this._returnPanel();
28204
+ }
28205
+ toggle(event) {
28206
+ event.preventDefault();
28207
+ this._open ? this.close() : this.open();
28208
+ }
28209
+ closeOnEsc(event) {
28210
+ if (event.key === "Escape")
28211
+ this.close();
28212
+ }
28213
+ _portalPanel() {
28214
+ if (this._panel)
28215
+ return;
28216
+ const panel = this.panelTarget;
28217
+ if (!panel)
28218
+ return;
28219
+ this._panel = panel;
28220
+ this._panelHome = panel.parentElement;
28221
+ panel.addEventListener("mouseenter", this._onPanelEnter);
28222
+ panel.addEventListener("mouseleave", this._onPanelLeave);
28223
+ document.body.appendChild(panel);
28224
+ panel.style.display = "block";
28225
+ }
28226
+ _returnPanel() {
28227
+ if (!this._panel)
28228
+ return;
28229
+ const panel = this._panel;
28230
+ panel.removeEventListener("mouseenter", this._onPanelEnter);
28231
+ panel.removeEventListener("mouseleave", this._onPanelLeave);
28232
+ panel.style.position = "";
28233
+ panel.style.left = "";
28234
+ panel.style.top = "";
28235
+ panel.style.display = "";
28236
+ if (this._panelHome && document.contains(this._panelHome)) {
28237
+ this._panelHome.appendChild(panel);
28238
+ } else {
28239
+ panel.remove();
28240
+ }
28241
+ this._panel = null;
28242
+ this._panelHome = null;
28243
+ }
28244
+ _position() {
28245
+ if (!this._panel || !this.hasTriggerTarget)
28246
+ return;
28247
+ const panel = this._panel;
28248
+ const triggerRect = this.triggerTarget.getBoundingClientRect();
28249
+ panel.style.position = "fixed";
28250
+ panel.style.left = `${triggerRect.right + 4}px`;
28251
+ panel.style.top = `${triggerRect.top}px`;
28252
+ requestAnimationFrame(() => {
28253
+ const panelRect = panel.getBoundingClientRect();
28254
+ const viewportH = window.innerHeight;
28255
+ if (panelRect.bottom > viewportH - 8) {
28256
+ const overflow = panelRect.bottom - (viewportH - 8);
28257
+ panel.style.top = `${parseFloat(panel.style.top) - overflow}px`;
28258
+ }
28259
+ });
28260
+ }
28261
+ };
28262
+
28263
+ // src/js/controllers/table_header_controller.js
28264
+ var table_header_controller_default = class extends Controller {
28265
+ headerClick(event) {
28266
+ if (!event.shiftKey)
28267
+ return;
28268
+ const link2 = event.currentTarget;
28269
+ const multiHref = link2.dataset.tableHeaderMultiHref;
28270
+ if (!multiHref)
28271
+ return;
28272
+ event.preventDefault();
28273
+ Turbo.visit(multiHref);
28274
+ }
28275
+ };
28276
+
28277
+ // src/js/controllers/table_column_menu_controller.js
28278
+ var table_column_menu_controller_default = class extends Controller {
28279
+ static targets = ["panel"];
28280
+ connect() {
28281
+ this._onDocClick = this._onDocClick.bind(this);
28282
+ }
28283
+ toggle(event) {
28284
+ event.preventDefault();
28285
+ event.stopPropagation();
28286
+ if (this.hasPanelTarget) {
28287
+ const isNowVisible = !this.panelTarget.classList.toggle("hidden");
28288
+ if (isNowVisible) {
28289
+ document.addEventListener("click", this._onDocClick);
28290
+ this._onKey = (e4) => {
28291
+ if (e4.key === "Escape")
28292
+ this._close();
28293
+ };
28294
+ document.addEventListener("keydown", this._onKey);
28295
+ } else {
28296
+ this._unbind();
28297
+ }
28298
+ }
28299
+ }
28300
+ _close() {
28301
+ if (this.hasPanelTarget)
28302
+ this.panelTarget.classList.add("hidden");
28303
+ this._unbind();
28304
+ }
28305
+ _unbind() {
28306
+ document.removeEventListener("click", this._onDocClick);
28307
+ if (this._onKey) {
28308
+ document.removeEventListener("keydown", this._onKey);
28309
+ this._onKey = null;
28310
+ }
28311
+ }
28312
+ _onDocClick(event) {
28313
+ if (!this.element.contains(event.target))
28314
+ this._close();
28315
+ }
28316
+ };
28317
+
28318
+ // src/js/controllers/capture_url_controller.js
28319
+ var capture_url_controller_default = class extends Controller {
28320
+ connect() {
28321
+ if ("value" in this.element) {
28322
+ this.element.value = window.location.href;
28323
+ }
28324
+ }
28325
+ };
28326
+
28327
+ // src/js/controllers/row_click_controller.js
28328
+ var row_click_controller_default = class extends Controller {
28329
+ click(event) {
28330
+ if (event.target.closest("a, button, input, label, select, textarea, [data-row-click-ignore]")) {
28331
+ return;
28332
+ }
28333
+ this.element.querySelector('[data-row-click-target="show"]')?.click();
28334
+ }
28335
+ };
28336
+
28337
+ // src/js/controllers/view_switcher_controller.js
28338
+ var view_switcher_controller_default = class extends Controller {
28339
+ static values = { cookieName: String, cookiePath: { type: String, default: "/" } };
28340
+ select(event) {
28341
+ const view = event.params.view;
28342
+ if (!view || !this.cookieNameValue)
28343
+ return;
28344
+ const maxAge = 60 * 60 * 24 * 365;
28345
+ const path = this.cookiePathValue || "/";
28346
+ document.cookie = `${this.cookieNameValue}=${encodeURIComponent(view)}; Path=${path}; Max-Age=${maxAge}; SameSite=Lax`;
28347
+ const url = new URL(window.location.href);
28348
+ url.searchParams.delete("view");
28349
+ window.location.href = url.toString();
28350
+ }
28351
+ };
28352
+
28353
+ // src/js/controllers/autosubmit_controller.js
28354
+ var autosubmit_controller_default = class extends Controller {
28355
+ static values = { delay: { type: Number, default: 300 } };
28356
+ connect() {
28357
+ this._timer = null;
28358
+ }
28359
+ disconnect() {
28360
+ if (this._timer)
28361
+ clearTimeout(this._timer);
28362
+ }
28363
+ submit() {
28364
+ if (this._timer)
28365
+ clearTimeout(this._timer);
28366
+ this._timer = setTimeout(() => {
28367
+ this.element.closest("form")?.requestSubmit();
28368
+ }, this.delayValue);
28369
+ }
28370
+ };
28371
+
28001
28372
  // src/js/controllers/register_controllers.js
28002
28373
  function register_controllers_default(application2) {
28003
28374
  application2.register("password-visibility", password_visibility_controller_default);
@@ -28025,6 +28396,14 @@ this.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e4.byteLength}`), e4.tif
28025
28396
  application2.register("filter-panel", filter_panel_controller_default);
28026
28397
  application2.register("textarea-autogrow", textarea_autogrow_controller_default);
28027
28398
  application2.register("clipboard", clipboard_controller_default);
28399
+ application2.register("icon-rail", icon_rail_controller_default);
28400
+ application2.register("icon-rail-flyout", icon_rail_flyout_controller_default);
28401
+ application2.register("table-header", table_header_controller_default);
28402
+ application2.register("table-column-menu", table_column_menu_controller_default);
28403
+ application2.register("capture-url", capture_url_controller_default);
28404
+ application2.register("row-click", row_click_controller_default);
28405
+ application2.register("view-switcher", view_switcher_controller_default);
28406
+ application2.register("autosubmit", autosubmit_controller_default);
28028
28407
  }
28029
28408
 
28030
28409
  // src/js/turbo/turbo_actions.js