diff --git a/package-lock.json b/package-lock.json
index c9dfa6e1..60bc16d6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,7 +12,6 @@
"markdown-it": "^14.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
- "react-resizable": "^4.0.2",
"react-sidebar": "^3.0.2",
"react-toastify": "^11.0.3"
},
@@ -6244,40 +6243,12 @@
"react": "^19.2.8"
}
},
- "node_modules/react-draggable": {
- "version": "4.7.0",
- "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.7.0.tgz",
- "integrity": "sha512-kTpANmKWVnFXiZ76Ag2ZowiFStuBYnJ606PI1TbUsOg29/400/JNIxI9+CuenhiAqFuXWJffz6F4UI3R51kUug==",
- "license": "MIT",
- "dependencies": {
- "clsx": "^2.1.1",
- "prop-types": "^15.8.1"
- },
- "peerDependencies": {
- "react": ">= 16.3.0",
- "react-dom": ">= 16.3.0"
- }
- },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
- "node_modules/react-resizable": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-4.0.2.tgz",
- "integrity": "sha512-jZD9ghYRmyJCw0+awYctSZ+9pmX1WXQvzDrTovELYc8obC/BShTI2r4c14LIVzeQ+vJZNb0yKM7bG2eqv7Vfyg==",
- "license": "MIT",
- "dependencies": {
- "prop-types": "15.x",
- "react-draggable": "^4.5.0"
- },
- "peerDependencies": {
- "react": ">= 16.3",
- "react-dom": ">= 16.3"
- }
- },
"node_modules/react-sidebar": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/react-sidebar/-/react-sidebar-3.0.2.tgz",
diff --git a/package.json b/package.json
index 7a4fe453..0904cbff 100644
--- a/package.json
+++ b/package.json
@@ -23,7 +23,6 @@
"markdown-it": "^14.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
- "react-resizable": "^4.0.2",
"react-sidebar": "^3.0.2",
"react-toastify": "^11.0.3"
},
diff --git a/src/css/Layout.module.css b/src/css/Layout.module.css
index e2a3544d..45e01f22 100644
--- a/src/css/Layout.module.css
+++ b/src/css/Layout.module.css
@@ -3,11 +3,13 @@
top: 0;
width: inherit;
height: inherit;
+}
- :global(.react-resizable) {
- width: 0 !important;
- height: 0 !important;
- }
+.handleEast,
+.handleSouth,
+.handleSouthEast {
+ touch-action: none;
+ user-select: none;
}
.handleEast {
diff --git a/src/ui/components/Layout.tsx b/src/ui/components/Layout.tsx
index 1bdd73c4..f2111b3e 100644
--- a/src/ui/components/Layout.tsx
+++ b/src/ui/components/Layout.tsx
@@ -1,12 +1,10 @@
import * as React from "react";
-import { ResizableBox } from "react-resizable";
import { type LayoutStateRef } from "../../livesplit-core";
import { type WebRenderer } from "../../livesplit-core/livesplit_core";
import AutoRefresh from "../../util/AutoRefresh";
import { type UrlCache } from "../../util/UrlCache";
import { type GeneralSettings } from "../views/MainSettings";
-
-import classes from "../../css/Layout.module.css";
+import { LayoutResizeHandles } from "./LayoutResizeHandles";
export function Layout({
getState,
@@ -70,53 +68,11 @@ export function Layout({
}}
/>
{allowResize && (
-
- e.stopPropagation()}
- className={classes.handleEast}
- />
- }
- onResize={(_event, data) =>
- onResize(data.size.width, data.size.height)
- }
- />
- e.stopPropagation()}
- className={classes.handleSouth}
- />
- }
- onResize={(_event, data) =>
- onResize(data.size.width, data.size.height)
- }
- />
- e.stopPropagation()}
- className={classes.handleSouthEast}
- />
- }
- onResize={(_event, data) =>
- onResize(data.size.width, data.size.height)
- }
- />
-
+
)}
diff --git a/src/ui/components/LayoutResizeHandles.tsx b/src/ui/components/LayoutResizeHandles.tsx
new file mode 100644
index 00000000..8ce101b9
--- /dev/null
+++ b/src/ui/components/LayoutResizeHandles.tsx
@@ -0,0 +1,126 @@
+import * as React from "react";
+
+import classes from "../../css/Layout.module.css";
+
+const MINIMUM_WIDTH = 100;
+const MINIMUM_HEIGHT = 40;
+
+type ResizeAxis = "horizontal" | "vertical" | "both";
+
+export function LayoutResizeHandles({
+ width,
+ height,
+ onResize,
+}: {
+ width: number;
+ height: number;
+ onResize: (width: number, height: number) => void;
+}) {
+ const stopListening = React.useRef<(() => void) | null>(null);
+
+ React.useEffect(
+ () => () => {
+ stopListening.current?.();
+ },
+ [],
+ );
+
+ const startResize =
+ (axis: ResizeAxis) => (event: React.PointerEvent) => {
+ // Ignore secondary pointers and non-primary mouse buttons. Pointer
+ // events cover mouse, touch, and pen input without needing separate
+ // compatibility paths for each input type.
+ if (!event.isPrimary || event.button !== 0) {
+ return;
+ }
+
+ const pointerId = event.pointerId;
+ const startX = event.clientX;
+ const startY = event.clientY;
+ let lastWidth = width;
+ let lastHeight = height;
+ const ownerDocument = event.currentTarget.ownerDocument;
+ const ownerWindow = ownerDocument.defaultView;
+
+ // Listen on the document while dragging so the hit area can remain
+ // deliberately narrow without losing movement as soon as the
+ // pointer leaves it. The cleanup ref also handles the component
+ // being unmounted or the window losing focus mid-drag.
+ function resize(pointerEvent: PointerEvent) {
+ if (pointerEvent.pointerId !== pointerId) {
+ return;
+ }
+
+ const newWidth =
+ axis === "vertical"
+ ? width
+ : Math.max(
+ MINIMUM_WIDTH,
+ width + pointerEvent.clientX - startX,
+ );
+ const newHeight =
+ axis === "horizontal"
+ ? height
+ : Math.max(
+ MINIMUM_HEIGHT,
+ height + pointerEvent.clientY - startY,
+ );
+
+ if (newWidth !== lastWidth || newHeight !== lastHeight) {
+ lastWidth = newWidth;
+ lastHeight = newHeight;
+ onResize(newWidth, newHeight);
+ }
+ pointerEvent.preventDefault();
+ }
+
+ function stopResize(pointerEvent: PointerEvent) {
+ if (pointerEvent.pointerId === pointerId) {
+ cleanup();
+ }
+ }
+
+ function cleanup() {
+ ownerDocument.removeEventListener("pointermove", resize);
+ ownerDocument.removeEventListener("pointerup", stopResize);
+ ownerDocument.removeEventListener("pointercancel", stopResize);
+ ownerWindow?.removeEventListener("blur", cleanup);
+ stopListening.current = null;
+ }
+
+ stopListening.current?.();
+ stopListening.current = cleanup;
+ ownerDocument.addEventListener("pointermove", resize, {
+ passive: false,
+ });
+ ownerDocument.addEventListener("pointerup", stopResize);
+ ownerDocument.addEventListener("pointercancel", stopResize);
+ ownerWindow?.addEventListener("blur", cleanup);
+ event.preventDefault();
+ };
+
+ const handleProps = {
+ "aria-hidden": true,
+ onClick: (event: React.MouseEvent) => event.stopPropagation(),
+ } as const;
+
+ return (
+
+ );
+}