diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 6e98a39bc5..55ff5d4d9d 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -15,8 +15,7 @@ }); onDestroy(() => { - // Destroy the Wasm editor handle - editor?.handle.free(); + editor?.destroy(); }); diff --git a/frontend/src/README.md b/frontend/src/README.md index 7195b05372..46e25d223f 100644 --- a/frontend/src/README.md +++ b/frontend/src/README.md @@ -4,23 +4,21 @@ Svelte components that build the Graphite editor GUI. These each contain a TypeScript section, a Svelte-templated HTML template section, and an SCSS stylesheet section. The aim is to avoid implementing much editor business logic here, just enough to make things interactive and communicate to the backend where the real business logic should occur. -## I/O managers: `io-managers/` +## Managers: `managers/` -TypeScript files which manage the input/output of browser APIs and link this functionality with the editor backend. These files subscribe to backend events to execute JS APIs, and in response to these APIs or user interactions, they may call functions into the backend (defined in `/frontend/wasm/editor_api.rs`). +TypeScript files which manage the input/output of browser APIs and link this functionality with the editor backend. These files subscribe to backend messages to execute JS APIs, and in response to these APIs or user interactions, they may call functions into the backend (defined in `/frontend/wasm/editor_api.rs`). -Each I/O manager is a self-contained module where one instance is created in `Editor.svelte` when it's mounted to the DOM at app startup. +Each manager module exports a factory function (e.g. `createClipboardManager(editor)`) that sets up message subscriptions and returns a `{ destroy }` object. In `Editor.svelte`, each manager is created at startup and its `destroy()` method is called on unmount to clean up subscriptions and side-effects (e.g. event listeners). Managers use self-accepting HMR to tear down and re-create with updated code during development. -During development when HMR (hot-module replacement) occurs, these are also unmounted to clean up after themselves, so they can be mounted again with the updated code. Therefore, any side-effects that these managers cause (e.g. adding event listeners to the page) need a destructor function that cleans them up. The destructor function, when applicable, is returned by the module and automatically called in `Editor.svelte` on unmount. +## Stores: `stores/` -## State providers: `state-providers/` +TypeScript files which provide reactive state to Svelte components. Each module persists a Svelte writable store at module level (surviving HMR via `import.meta.hot.data`) and exports a factory function (e.g. `createDialogStore(editor)`) that sets up backend message subscriptions and returns an object containing the store's `subscribe` method, any action methods for components to call, and a `destroy` method. -TypeScript files which provide reactive state and importable functions to Svelte components. Each module defines a Svelte writable store `const { subscribe, update } = writable({ .. });` and exports the `subscribe` method from the module in the returned object. Other functions may also be defined in the module and exported after `subscribe`, which provide a way for Svelte components to call functions to manipulate the state. +In `Editor.svelte`, each store is created and passed to Svelte's `setContext()`. Components access stores via `getContext("dialog")` and use the `subscribe` method for reactive state and action methods (like `createCrashDialog()`) to trigger state changes. -In `Editor.svelte`, an instance of each of these are given to Svelte's `setContext()` function. This allows any component to access the state provider instance using `const exampleStateProvider = getContext("exampleStateProvider");`. +## *Managers vs. stores* -## *I/O managers vs. state providers* - -*Some state providers, similarly to I/O managers, may subscribe to backend events, call functions from `editor_api.rs` into the backend, and interact with browser APIs and user input. The difference is that state providers are meant to be made available to components via `getContext()` to use them for reactive state, while I/O managers are meant to be self-contained systems that operate for the lifetime of the application and aren't touched by Svelte components.* +*Both managers and stores subscribe to backend messages and may interact with browser APIs. The difference is that stores expose reactive state to components via `setContext()`/`getContext()`, while managers are self-contained systems that operate for the lifetime of the application and aren't accessed by Svelte components.* ## Utility functions: `utility-functions/` @@ -30,7 +28,7 @@ TypeScript files which define and `export` individual helper functions for use e Instantiates the Wasm and editor backend instances. The function `initWasm()` asynchronously constructs and initializes an instance of the Wasm bindings JS module provided by wasm-bindgen/wasm-pack. The function `createEditor()` constructs an instance of the editor backend. In theory there could be multiple editor instances sharing the same Wasm module instance. The function returns an object where `raw` is the Wasm memory, `handle` provides access to callable backend functions, and `subscriptions` is the subscription router (described below). -`initWasm()` occurs in `main.ts` right before the Svelte application is mounted, then `createEditor()` is run in `Editor.svelte` during the Svelte app's creation. Similarly to the state providers described above, the editor is given via `setContext()` so other components can get it via `getContext` and call functions on `editor.handle` or `editor.subscriptions`. +`initWasm()` occurs in `main.ts` right before the Svelte application is mounted, then `createEditor()` is run in `Editor.svelte` during the Svelte app's creation. Similarly to the stores described above, the editor is given via `setContext()` so other components can get it via `getContext` and call functions on `editor.handle` or `editor.subscriptions`. ## Subscription router: `subscription-router.ts` @@ -42,7 +40,7 @@ The entry point for the Svelte application. ## Editor base instance: `Editor.svelte` -This is where we define global CSS style rules, create/destroy the editor instance, construct/destruct the I/O managers, and construct and `setContext()` the state providers. +This is where we define global CSS style rules, construct all stores and managers with the editor instance, set store contexts for component access, and clean up all `destroy()` methods on unmount. ## Global type augmentations: `global.d.ts` diff --git a/frontend/src/components/Editor.svelte b/frontend/src/components/Editor.svelte index 0f14408298..46e93952ab 100644 --- a/frontend/src/components/Editor.svelte +++ b/frontend/src/components/Editor.svelte @@ -2,20 +2,20 @@ import { onMount, onDestroy, setContext } from "svelte"; import type { Editor } from "@graphite/editor"; - import { createClipboardManager } from "@graphite/io-managers/clipboard"; - import { createHyperlinkManager } from "@graphite/io-managers/hyperlink"; - import { createInputManager } from "@graphite/io-managers/input"; - import { createLocalizationManager } from "@graphite/io-managers/localization"; - import { createPanicManager } from "@graphite/io-managers/panic"; - import { createPersistenceManager } from "@graphite/io-managers/persistence"; - import { createAppWindowState } from "@graphite/state-providers/app-window"; - import { createDialogState } from "@graphite/state-providers/dialog"; - import { createDocumentState } from "@graphite/state-providers/document"; - import { createFontsManager } from "/src/io-managers/fonts"; - import { createFullscreenState } from "@graphite/state-providers/fullscreen"; - import { createNodeGraphState } from "@graphite/state-providers/node-graph"; - import { createPortfolioState } from "@graphite/state-providers/portfolio"; - import { createTooltipState } from "@graphite/state-providers/tooltip"; + import { createClipboardManager } from "@graphite/managers/clipboard"; + import { createFontsManager } from "@graphite/managers/fonts"; + import { createHyperlinkManager } from "@graphite/managers/hyperlink"; + import { createInputManager } from "@graphite/managers/input"; + import { createLocalizationManager } from "@graphite/managers/localization"; + import { createPanicManager } from "@graphite/managers/panic"; + import { createPersistenceManager } from "@graphite/managers/persistence"; + import { createAppWindowStore } from "@graphite/stores/app-window"; + import { createDialogStore } from "@graphite/stores/dialog"; + import { createDocumentStore } from "@graphite/stores/document"; + import { createFullscreenStore } from "@graphite/stores/fullscreen"; + import { createNodeGraphStore } from "@graphite/stores/node-graph"; + import { createPortfolioStore } from "@graphite/stores/portfolio"; + import { createTooltipStore } from "@graphite/stores/tooltip"; import MainWindow from "@graphite/components/window/MainWindow.svelte"; @@ -23,39 +23,35 @@ export let editor: Editor; setContext("editor", editor); - // State provider systems - let dialog = createDialogState(editor); - setContext("dialog", dialog); - let tooltip = createTooltipState(editor); - setContext("tooltip", tooltip); - let document = createDocumentState(editor); - setContext("document", document); - let fullscreen = createFullscreenState(editor); - setContext("fullscreen", fullscreen); - let nodeGraph = createNodeGraphState(editor); - setContext("nodeGraph", nodeGraph); - let portfolio = createPortfolioState(editor); - setContext("portfolio", portfolio); - let appWindow = createAppWindowState(editor); - setContext("appWindow", appWindow); - - // Initialize managers, which are isolated systems that subscribe to backend messages to link them to browser API functionality (like JS events, IndexedDB, etc.) - createClipboardManager(editor); - createHyperlinkManager(editor); - createLocalizationManager(editor); - createPanicManager(editor, dialog); - createPersistenceManager(editor, portfolio); - createFontsManager(editor); - let inputManagerDestructor = createInputManager(editor, dialog, portfolio, document, fullscreen); + const stores = { + dialog: createDialogStore(editor), + tooltip: createTooltipStore(editor), + document: createDocumentStore(editor), + fullscreen: createFullscreenStore(editor), + nodeGraph: createNodeGraphStore(editor), + portfolio: createPortfolioStore(editor), + appWindow: createAppWindowStore(editor), + }; + Object.entries(stores).forEach(([key, store]) => setContext(key, store)); + + const managers = { + clipboard: createClipboardManager(editor), + hyperlink: createHyperlinkManager(editor), + localization: createLocalizationManager(editor), + panic: createPanicManager(editor), + persistence: createPersistenceManager(editor, stores.portfolio), + fonts: createFontsManager(editor), + input: createInputManager(editor, stores.dialog, stores.portfolio, stores.document, stores.fullscreen), + }; onMount(() => { - // Initialize certain setup tasks required by the editor backend to be ready for the user now that the frontend is ready + // Initialize certain setup tasks required by the editor backend to be ready for the user now that the frontend is ready. + // The backend handles idempotency, so this is safe to call again during HMR re-mounts. editor.handle.initAfterFrontendReady(); }); onDestroy(() => { - // Call the destructor for each manager - inputManagerDestructor(); + [...Object.values(stores), ...Object.values(managers)].forEach(({ destroy }) => destroy()); }); diff --git a/frontend/src/components/floating-menus/ColorPicker.svelte b/frontend/src/components/floating-menus/ColorPicker.svelte index 8dee3b7881..ccc7edf0c6 100644 --- a/frontend/src/components/floating-menus/ColorPicker.svelte +++ b/frontend/src/components/floating-menus/ColorPicker.svelte @@ -3,7 +3,7 @@ import { isPlatformNative } from "@graphite/../wasm/pkg/graphite_wasm"; import type { FillChoice, MenuDirection, Color } from "@graphite/../wasm/pkg/graphite_wasm"; - import type { TooltipState } from "@graphite/state-providers/tooltip"; + import type { TooltipStore } from "@graphite/stores/tooltip"; import { contrastingOutlineFactor, fillChoiceColor, @@ -22,7 +22,6 @@ gradientFirstColor, } from "@graphite/utility-functions/colors"; import type { HSV, RGB } from "@graphite/utility-functions/colors"; - import { clamp } from "@graphite/utility-functions/math"; import FloatingMenu, { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte"; import LayoutCol from "@graphite/components/layout/LayoutCol.svelte"; @@ -57,7 +56,7 @@ ]; const dispatch = createEventDispatcher<{ colorOrGradient: FillChoice; startHistoryTransaction: undefined; commitHistoryTransaction: undefined }>(); - const tooltip = getContext("tooltip"); + const tooltip = getContext("tooltip"); export let colorOrGradient: FillChoice; export let allowNone = false; @@ -438,6 +437,10 @@ setOldHSVA(hsv.h, hsv.s, hsv.v, color.alpha, false); } + function clamp(value: number, min = 0, max = 1): number { + return Math.max(min, Math.min(value, max)); + } + export function div(): HTMLDivElement | undefined { return self?.div(); } diff --git a/frontend/src/components/floating-menus/Dialog.svelte b/frontend/src/components/floating-menus/Dialog.svelte index b1150ad06c..3b1b45528a 100644 --- a/frontend/src/components/floating-menus/Dialog.svelte +++ b/frontend/src/components/floating-menus/Dialog.svelte @@ -1,10 +1,9 @@ diff --git a/frontend/src/components/panels/Layers.svelte b/frontend/src/components/panels/Layers.svelte index 286b8a8384..03709fec0f 100644 --- a/frontend/src/components/panels/Layers.svelte +++ b/frontend/src/components/panels/Layers.svelte @@ -4,8 +4,8 @@ import type { LayerPanelEntry, LayerStructureEntry, Layout } from "@graphite/../wasm/pkg/graphite_wasm"; import type { Editor } from "@graphite/editor"; - import type { NodeGraphState } from "@graphite/state-providers/node-graph"; - import type { TooltipState } from "@graphite/state-providers/tooltip"; + import type { NodeGraphStore } from "@graphite/stores/node-graph"; + import type { TooltipStore } from "@graphite/stores/tooltip"; import { pasteFile } from "@graphite/utility-functions/files"; import { operatingSystem } from "@graphite/utility-functions/platform"; import { patchLayout } from "@graphite/utility-functions/widgets"; @@ -42,8 +42,8 @@ }; const editor = getContext("editor"); - const nodeGraph = getContext("nodeGraph"); - const tooltip = getContext("tooltip"); + const nodeGraph = getContext("nodeGraph"); + const tooltip = getContext("tooltip"); let list: LayoutCol | undefined; diff --git a/frontend/src/components/views/Graph.svelte b/frontend/src/components/views/Graph.svelte index 646c621c8e..7668eb0b46 100644 --- a/frontend/src/components/views/Graph.svelte +++ b/frontend/src/components/views/Graph.svelte @@ -5,8 +5,9 @@ import type { FrontendGraphInput, FrontendGraphOutput, FrontendNode } from "@graphite/../wasm/pkg/graphite_wasm"; import type { Editor } from "@graphite/editor"; - import type { DocumentState } from "@graphite/state-providers/document"; - import type { NodeGraphState } from "@graphite/state-providers/node-graph"; + import type { DocumentStore } from "@graphite/stores/document"; + import { closeContextMenu } from "@graphite/stores/node-graph"; + import type { NodeGraphStore } from "@graphite/stores/node-graph"; import NodeCatalog from "@graphite/components/floating-menus/NodeCatalog.svelte"; import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte"; @@ -20,8 +21,8 @@ const FADE_TRANSITION = { duration: 200, easing: cubicInOut }; const editor = getContext("editor"); - const nodeGraph = getContext("nodeGraph"); - const documentState = getContext("document"); + const nodeGraph = getContext("nodeGraph"); + const documentState = getContext("document"); let graph: HTMLDivElement | undefined; @@ -29,7 +30,7 @@ $: gridDotRadius = 1 + Math.floor($nodeGraph.transform.scale - 0.5 + 0.001) / 2; // Close the context menu when the graph view overlay is closed - $: if (!$documentState.graphViewOverlayOpen) nodeGraph.closeContextMenu(); + $: if (!$documentState.graphViewOverlayOpen) closeContextMenu(); let inputElement: HTMLInputElement; let hoveringImportIndex: number | undefined = undefined; @@ -220,7 +221,7 @@ label="Merge Selected Nodes" action={() => { editor.handle.mergeSelectedNodes(); - nodeGraph.closeContextMenu(); + closeContextMenu(); }} flush={true} /> @@ -231,7 +232,7 @@ if ($nodeGraph.contextMenuInformation?.contextMenuData.type === "ModifyNode") { editor.handle.setToNodeOrLayer($nodeGraph.contextMenuInformation.contextMenuData.data.nodeId, currentlyIsNode); } - nodeGraph.closeContextMenu(); + closeContextMenu(); }} disabled={!$nodeGraph.contextMenuInformation.contextMenuData.data.canBeLayer} flush={true} @@ -247,7 +248,7 @@ } else { editor.handle.toggleLayerLock(nodeId); } - nodeGraph.closeContextMenu(); + closeContextMenu(); }} flush={true} /> diff --git a/frontend/src/components/widgets/WidgetSpan.svelte b/frontend/src/components/widgets/WidgetSpan.svelte index 6a402b067e..43aba10b9b 100644 --- a/frontend/src/components/widgets/WidgetSpan.svelte +++ b/frontend/src/components/widgets/WidgetSpan.svelte @@ -4,7 +4,6 @@ import type { LayoutTarget, Widget, WidgetInstance } from "@graphite/../wasm/pkg/graphite_wasm"; import type { Editor } from "@graphite/editor"; import { parseFillChoice } from "@graphite/utility-functions/colors"; - import { debouncer } from "@graphite/utility-functions/debounce"; import NodeCatalog from "@graphite/components/floating-menus/NodeCatalog.svelte"; import BreadcrumbTrailButtons from "@graphite/components/widgets/buttons/BreadcrumbTrailButtons.svelte"; @@ -137,7 +136,7 @@ getProps: (props, index) => ({ ...props, $$events: { - value: (e: CustomEvent) => debouncer((value: unknown) => widgetValueCommitAndUpdate(index, value, false), { debounceTime: 120 }).debounceUpdateValue(e.detail), + value: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, false), }, }), }, @@ -202,7 +201,7 @@ incrementCallbackIncrease: () => widgetValueCommitAndUpdate(index, "Increment", false), incrementCallbackDecrease: () => widgetValueCommitAndUpdate(index, "Decrement", false), $$events: { - value: (e: CustomEvent) => debouncer((value: unknown) => widgetValueUpdate(index, value, true)).debounceUpdateValue(e.detail), + value: (e: CustomEvent) => widgetValueUpdate(index, e.detail, true), startHistoryTransaction: () => widgetValueCommit(index, props.value), }, }), diff --git a/frontend/src/components/widgets/inputs/CurveInput.svelte b/frontend/src/components/widgets/inputs/CurveInput.svelte index 9fc746ca2b..8fbcd27164 100644 --- a/frontend/src/components/widgets/inputs/CurveInput.svelte +++ b/frontend/src/components/widgets/inputs/CurveInput.svelte @@ -2,7 +2,6 @@ import { createEventDispatcher } from "svelte"; import type { Curve, CurveManipulatorGroup, ActionShortcut } from "@graphite/../wasm/pkg/graphite_wasm"; - import { clamp } from "@graphite/utility-functions/math"; import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; @@ -185,6 +184,10 @@ dAttribute = recalculateSvgPath(); updateCurve(); } + + function clamp(value: number, min = 0, max = 1): number { + return Math.max(min, Math.min(value, max)); + } diff --git a/frontend/src/components/widgets/inputs/NumberInput.svelte b/frontend/src/components/widgets/inputs/NumberInput.svelte index 3f5a71d74f..1cabe6823f 100644 --- a/frontend/src/components/widgets/inputs/NumberInput.svelte +++ b/frontend/src/components/widgets/inputs/NumberInput.svelte @@ -4,7 +4,7 @@ import { evaluateMathExpression, isPlatformNative } from "@graphite/../wasm/pkg/graphite_wasm"; import type { NumberInputMode, NumberInputIncrementBehavior, ActionShortcut } from "@graphite/../wasm/pkg/graphite_wasm"; import type { Editor } from "@graphite/editor"; - import { PRESS_REPEAT_DELAY_MS, PRESS_REPEAT_INTERVAL_MS } from "@graphite/io-managers/input"; + import { PRESS_REPEAT_DELAY_MS, PRESS_REPEAT_INTERVAL_MS } from "@graphite/managers/input"; import { browserVersion } from "@graphite/utility-functions/platform"; import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte"; @@ -87,6 +87,12 @@ let shiftKeyDown = false; // Track whether the Ctrl key is currently held down. let ctrlKeyDown = false; + // Cleanup function for active drag interactions, called on destroy to prevent leaked listeners + let activeDragCleanup: (() => void) | undefined; + // Track the slider abort state for cleanup on destroy + let sliderResetAbortHandler: (() => void) | undefined; + let sliderAbortTimeout1: ReturnType | undefined; + let sliderAbortTimeout2: ReturnType | undefined; $: watchValue(value, unit); $: sliderStepValue = isInteger ? (step === undefined ? 1 : step) : "any"; @@ -107,10 +113,31 @@ addEventListener("mousemove", trackShiftAndCtrl); }); onDestroy(() => { + clearTimeout(repeatTimeout); + clearTimeout(sliderAbortTimeout1); + clearTimeout(sliderAbortTimeout2); + + activeDragCleanup?.(); + + // Exit pointer lock if active (non-Safari path) + if (document.pointerLockElement) document.exitPointerLock(); + + // Remove Safari cursor-hidden workaround class if present + const isSafari = browserVersion().toLowerCase().includes("safari"); + if (isSafari) document.body.classList.remove("cursor-hidden"); + + // Clean up any listeners related to tracking the Shift and Ctrl keys removeEventListener("keydown", trackShiftAndCtrl); removeEventListener("keyup", trackShiftAndCtrl); removeEventListener("mousemove", trackShiftAndCtrl); - clearTimeout(repeatTimeout); + + // Clean up any slider-related listeners that may be active + removeEventListener("mousedown", sliderAbortFromMousedown); + removeEventListener("keydown", sliderAbortFromMousedown); + removeEventListener("pointermove", sliderAbortFromDragging); + removeEventListener("keydown", sliderAbortFromDragging); + removeEventListener("keydown", incrementPressAbort); + if (sliderResetAbortHandler) removeEventListener("pointerup", sliderResetAbortHandler); }); // =============================== @@ -297,7 +324,7 @@ pressingArrow = false; clearTimeout(repeatTimeout); updateValue(initialValueBeforeDragging); - removeEventListener("keydown", onIncrementPointerUp); + removeEventListener("keydown", incrementPressAbort); } // ======================================= @@ -333,10 +360,9 @@ alreadyActedGuard = true; isDragging = true; - beginDrag(e); - removeEventListener("pointermove", onMove); - removeEventListener("pointerup", onUp); + activeDragCleanup?.(); + beginDrag(e); }; // If it's a mouseup, we'll begin editing the text field. const onUp = () => { @@ -346,11 +372,15 @@ isDragging = false; self?.focus(); - removeEventListener("pointermove", onMove); - removeEventListener("pointerup", onUp); + activeDragCleanup?.(); }; addEventListener("pointermove", onMove); addEventListener("pointerup", onUp); + activeDragCleanup = () => { + removeEventListener("pointermove", onMove); + removeEventListener("pointerup", onUp); + activeDragCleanup = undefined; + }; } function beginDrag(e: PointerEvent) { @@ -449,16 +479,20 @@ cumulativeDragDelta = 0; // Clean up the event listeners. - removeEventListener("pointerup", pointerUp); - removeEventListener("pointermove", pointerMove); - removeEventListener("pointerlockmove", pointerLockMove); - if (usePointerLock) document.removeEventListener("pointerlockchange", pointerLockChange); + activeDragCleanup?.(); }; addEventListener("pointerup", pointerUp); addEventListener("pointermove", pointerMove); addEventListener("pointerlockmove", pointerLockMove); if (usePointerLock) document.addEventListener("pointerlockchange", pointerLockChange); + activeDragCleanup = () => { + removeEventListener("pointerup", pointerUp); + removeEventListener("pointermove", pointerMove); + removeEventListener("pointerlockmove", pointerLockMove); + if (usePointerLock) document.removeEventListener("pointerlockchange", pointerLockChange); + activeDragCleanup = undefined; + }; } function pointerLockMoveUpdate(delta: number, slow: boolean, snapping: boolean, initialValue: number) { @@ -657,7 +691,7 @@ // End the user's drag by instantaneously disabling and re-enabling the range input element if (inputRangeElement) inputRangeElement.disabled = true; - setTimeout(() => { + sliderAbortTimeout1 = setTimeout(() => { if (inputRangeElement) inputRangeElement.disabled = false; }, 0); @@ -680,11 +714,13 @@ // dragging the slider, hitting Escape, then releasing the mouse button. This results in being transferred by `onSliderInput()` to the // "Deciding" state when we should remain in the "Ready" state as set here. (For debugging, this can be visualized in CSS by // recoloring the fake slider handle, which is shown in the "Deciding" state.) - setTimeout(() => (rangeSliderClickDragState = "Ready"), 0); + sliderAbortTimeout2 = setTimeout(() => (rangeSliderClickDragState = "Ready"), 0); // Clean up the event listener that was used to call this function. removeEventListener("pointerup", sliderResetAbort); + sliderResetAbortHandler = undefined; }; + sliderResetAbortHandler = sliderResetAbort; addEventListener("pointerup", sliderResetAbort); // Clean up the event listeners that were for tracking an abort while dragging the slider, now that we're no longer dragging it. diff --git a/frontend/src/components/widgets/inputs/ScrollbarInput.svelte b/frontend/src/components/widgets/inputs/ScrollbarInput.svelte index 99de85d703..c757ad7883 100644 --- a/frontend/src/components/widgets/inputs/ScrollbarInput.svelte +++ b/frontend/src/components/widgets/inputs/ScrollbarInput.svelte @@ -1,7 +1,7 @@