summaryrefslogtreecommitdiff
path: root/apps/web/src/selection
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-29 00:37:56 +0200
committerMaze Winther <mazewinther@gmail.com>2026-05-02 03:27:23 +0200
commita9b9cf6bf50f08723896659634f2e6afc17bcd42 (patch)
tree9175b49ff21f4326872ac9e541d2765cc7994ebc /apps/web/src/selection
parentd6622dc6b3523a5658cd512b44f15f7a66fd540c (diff)
downloadopencut-a9b9cf6bf50f08723896659634f2e6afc17bcd42.tar.gz
chore: switch from biome to eslint + prettier; fix ton of lint issues
Diffstat (limited to 'apps/web/src/selection')
-rw-r--r--apps/web/src/selection/hooks/use-box-select.ts85
-rw-r--r--apps/web/src/selection/hooks/use-selection-scope.ts19
-rw-r--r--apps/web/src/selection/selectable-item.tsx10
-rw-r--r--apps/web/src/selection/selectable-surface.tsx26
-rw-r--r--apps/web/src/selection/selection-box.tsx47
-rw-r--r--apps/web/src/selection/types.ts7
6 files changed, 110 insertions, 84 deletions
diff --git a/apps/web/src/selection/hooks/use-box-select.ts b/apps/web/src/selection/hooks/use-box-select.ts
index 90a584b5..3bbff14e 100644
--- a/apps/web/src/selection/hooks/use-box-select.ts
+++ b/apps/web/src/selection/hooks/use-box-select.ts
@@ -4,15 +4,40 @@ import { useCallback, useEffect, useRef, useState } from "react";
import type {
BoxSelectionSnapshot,
ResolveIntersections,
+ SelectionBoxBounds,
} from "@/selection/types";
interface SelectionBoxState<TId> extends BoxSelectionSnapshot<TId> {
startPos: { x: number; y: number };
currentPos: { x: number; y: number };
+ bounds: SelectionBoxBounds | null;
isActive: boolean;
isAdditive: boolean;
}
+function getSelectionBoxBounds({
+ container,
+ startPos,
+ currentPos,
+}: {
+ container: HTMLElement;
+ startPos: { x: number; y: number };
+ currentPos: { x: number; y: number };
+}): SelectionBoxBounds {
+ const containerRect = container.getBoundingClientRect();
+ const startX = startPos.x - containerRect.left;
+ const startY = startPos.y - containerRect.top;
+ const currentX = currentPos.x - containerRect.left;
+ const currentY = currentPos.y - containerRect.top;
+
+ return {
+ left: Math.min(startX, currentX),
+ top: Math.min(startY, currentY),
+ width: Math.abs(currentX - startX),
+ height: Math.abs(currentY - startY),
+ };
+}
+
export function useBoxSelect<TId>({
containerRef,
resolveIntersections,
@@ -40,33 +65,43 @@ export function useBoxSelect<TId>({
const [selectionBox, setSelectionBox] =
useState<SelectionBoxState<TId> | null>(null);
const justFinishedSelectingRef = useRef(false);
- const shouldStartSelectionCheck = shouldStartSelection ?? (() => true);
- const getIsAdditiveSelectionCheck =
- getIsAdditiveSelection ??
- ((event: React.MouseEvent<Element>) => event.ctrlKey || event.metaKey);
const handleMouseDown = useCallback(
(event: React.MouseEvent<Element>) => {
- const canStartSelection = shouldStartSelectionCheck(event);
+ const canStartSelection = shouldStartSelection
+ ? shouldStartSelection(event)
+ : true;
if (!isEnabled || event.button !== 0 || !canStartSelection) {
return;
}
+ const startPos = { x: event.clientX, y: event.clientY };
+ const container = containerRef.current;
setSelectionBox({
- startPos: { x: event.clientX, y: event.clientY },
- currentPos: { x: event.clientX, y: event.clientY },
+ startPos,
+ currentPos: startPos,
+ bounds: container
+ ? getSelectionBoxBounds({
+ container,
+ startPos,
+ currentPos: startPos,
+ })
+ : null,
isActive: false,
- isAdditive: getIsAdditiveSelectionCheck(event),
+ isAdditive: getIsAdditiveSelection
+ ? getIsAdditiveSelection(event)
+ : event.ctrlKey || event.metaKey,
initialSelectedIds: selectedIds,
initialAnchorId: anchorId,
});
},
[
anchorId,
- getIsAdditiveSelectionCheck,
+ containerRef,
+ getIsAdditiveSelection,
isEnabled,
selectedIds,
- shouldStartSelectionCheck,
+ shouldStartSelection,
],
);
@@ -98,11 +133,20 @@ export function useBoxSelect<TId>({
}
const handleMouseMove = ({ clientX, clientY }: MouseEvent) => {
+ const currentPos = { x: clientX, y: clientY };
const deltaX = Math.abs(clientX - selectionBox.startPos.x);
const deltaY = Math.abs(clientY - selectionBox.startPos.y);
+ const container = containerRef.current;
const nextSelectionBox = {
...selectionBox,
- currentPos: { x: clientX, y: clientY },
+ currentPos,
+ bounds: container
+ ? getSelectionBoxBounds({
+ container,
+ startPos: selectionBox.startPos,
+ currentPos,
+ })
+ : null,
isActive: deltaX > 5 || deltaY > 5 || selectionBox.isActive,
};
@@ -133,26 +177,26 @@ export function useBoxSelect<TId>({
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", handleMouseUp);
};
- }, [selectionBox, updateSelection]);
+ }, [containerRef, selectionBox, updateSelection]);
useEffect(() => {
if (!selectionBox) {
return;
}
+ const container = containerRef.current;
const previousBodyUserSelect = document.body.style.userSelect;
- const previousContainerUserSelect =
- containerRef.current?.style.userSelect ?? "";
+ const previousContainerUserSelect = container?.style.userSelect ?? "";
document.body.style.userSelect = "none";
- if (containerRef.current) {
- containerRef.current.style.userSelect = "none";
+ if (container) {
+ container.style.userSelect = "none";
}
return () => {
document.body.style.userSelect = previousBodyUserSelect;
- if (containerRef.current) {
- containerRef.current.style.userSelect = previousContainerUserSelect;
+ if (container) {
+ container.style.userSelect = previousContainerUserSelect;
}
};
}, [containerRef, selectionBox]);
@@ -162,7 +206,10 @@ export function useBoxSelect<TId>({
}, []);
return {
- selectionBox,
+ selectionBox:
+ selectionBox?.isActive && selectionBox.bounds
+ ? { bounds: selectionBox.bounds }
+ : null,
handleMouseDown,
isSelecting: selectionBox?.isActive ?? false,
shouldIgnoreClick,
diff --git a/apps/web/src/selection/hooks/use-selection-scope.ts b/apps/web/src/selection/hooks/use-selection-scope.ts
index ad4d2ce8..7fddab08 100644
--- a/apps/web/src/selection/hooks/use-selection-scope.ts
+++ b/apps/web/src/selection/hooks/use-selection-scope.ts
@@ -1,28 +1,25 @@
-import { useEffect, useRef } from "react";
+import { useEffect, useState } from "react";
+import { useCommittedRef } from "@/hooks/use-committed-ref";
import { useSelectionContext } from "@/selection/context";
import { activateScope, type ScopeEntry } from "@/selection/scope";
export function useSelectionScope() {
const { selectedIds, clearSelection } = useSelectionContext();
- const hasSelectionRef = useRef(selectedIds.length > 0);
- const clearSelectionRef = useRef(clearSelection);
const hasSelection = selectedIds.length > 0;
-
- hasSelectionRef.current = hasSelection;
- clearSelectionRef.current = clearSelection;
-
- const entryRef = useRef<ScopeEntry>({
+ const hasSelectionRef = useCommittedRef(hasSelection);
+ const clearSelectionRef = useCommittedRef(clearSelection);
+ const [entry] = useState<ScopeEntry>(() => ({
hasSelection: () => hasSelectionRef.current,
clear: () => {
clearSelectionRef.current();
},
- });
+ }));
useEffect(() => {
if (!hasSelection) {
return;
}
- return activateScope({ entry: entryRef.current });
- }, [hasSelection]);
+ return activateScope({ entry });
+ }, [entry, hasSelection]);
}
diff --git a/apps/web/src/selection/selectable-item.tsx b/apps/web/src/selection/selectable-item.tsx
index cc9e1dbf..9735c4a8 100644
--- a/apps/web/src/selection/selectable-item.tsx
+++ b/apps/web/src/selection/selectable-item.tsx
@@ -6,7 +6,13 @@ import { SELECTABLE_ITEM_ATTRIBUTE } from "@/selection/attributes";
import type { SelectableItemProps } from "@/selection/types";
import { cn } from "@/utils/ui";
-function setForwardedRef<T>(ref: React.ForwardedRef<T>, value: T | null) {
+function setForwardedRef<T>({
+ ref,
+ value,
+}: {
+ ref: React.ForwardedRef<T>;
+ value: T | null;
+}) {
if (typeof ref === "function") {
ref(value);
return;
@@ -55,7 +61,7 @@ export const SelectableItem = forwardRef<HTMLDivElement, SelectableItemProps>(
const handleRef = useCallback(
(element: HTMLDivElement | null) => {
registerItem(id, element);
- setForwardedRef(forwardedRef, element);
+ setForwardedRef({ ref: forwardedRef, value: element });
},
[forwardedRef, id, registerItem],
);
diff --git a/apps/web/src/selection/selectable-surface.tsx b/apps/web/src/selection/selectable-surface.tsx
index 24166daf..12682ba8 100644
--- a/apps/web/src/selection/selectable-surface.tsx
+++ b/apps/web/src/selection/selectable-surface.tsx
@@ -167,14 +167,15 @@ export function SelectableSurface({
[],
);
- const { selectionBox, handleMouseDown, shouldIgnoreClick } = useBoxSelect({
- containerRef,
- resolveIntersections,
- selectedIds: selectionState.selectedIds,
- anchorId: selectionState.anchorId,
- onSelectionChange: handleBoxSelectionChange,
- shouldStartSelection,
- });
+ const { selectionBox, handleMouseDown, isSelecting, shouldIgnoreClick } =
+ useBoxSelect({
+ containerRef,
+ resolveIntersections,
+ selectedIds: selectionState.selectedIds,
+ anchorId: selectionState.anchorId,
+ onSelectionChange: handleBoxSelectionChange,
+ shouldStartSelection,
+ });
const handleBackgroundClick = useCallback(
(event: React.MouseEvent<HTMLDivElement>) => {
@@ -236,7 +237,7 @@ export function SelectableSurface({
return () => clearTimeout(timer);
}, [getItemElement, onRevealComplete, revealId]);
- const isBoxSelecting = selectionBox?.isActive ?? false;
+ const isBoxSelecting = isSelecting;
const contextValue = useMemo(() => {
return {
@@ -276,12 +277,7 @@ export function SelectableSurface({
onKeyDown={handleBackgroundKeyDown}
>
{children}
- <SelectionBox
- startPos={selectionBox?.startPos ?? null}
- currentPos={selectionBox?.currentPos ?? null}
- containerRef={containerRef}
- isActive={selectionBox?.isActive ?? false}
- />
+ <SelectionBox bounds={selectionBox?.bounds ?? null} />
</div>
</SelectionContext.Provider>
);
diff --git a/apps/web/src/selection/selection-box.tsx b/apps/web/src/selection/selection-box.tsx
index e30f574d..2f2bf933 100644
--- a/apps/web/src/selection/selection-box.tsx
+++ b/apps/web/src/selection/selection-box.tsx
@@ -1,49 +1,22 @@
"use client";
-import { useMemo } from "react";
+import type { SelectionBoxBounds } from "@/selection/types";
interface SelectionBoxProps {
- startPos: { x: number; y: number } | null;
- currentPos: { x: number; y: number } | null;
- containerRef: React.RefObject<HTMLElement | null>;
- isActive: boolean;
+ bounds: SelectionBoxBounds | null;
}
-export function SelectionBox({
- startPos,
- currentPos,
- containerRef,
- isActive,
-}: SelectionBoxProps) {
- const selectionBoxStyle = useMemo(() => {
- if (!isActive || !startPos || !currentPos || !containerRef.current) {
- return null;
- }
-
- const containerRect = containerRef.current.getBoundingClientRect();
- const startX = startPos.x - containerRect.left;
- const startY = startPos.y - containerRect.top;
- const currentX = currentPos.x - containerRect.left;
- const currentY = currentPos.y - containerRect.top;
-
- const left = Math.min(startX, currentX);
- const top = Math.min(startY, currentY);
- const width = Math.abs(currentX - startX);
- const height = Math.abs(currentY - startY);
-
- return {
- left: `${left}px`,
- top: `${top}px`,
- width: `${width}px`,
- height: `${height}px`,
- };
- }, [containerRef, currentPos, isActive, startPos]);
-
- if (!selectionBoxStyle) return null;
+export function SelectionBox({ bounds }: SelectionBoxProps) {
+ if (!bounds) return null;
return (
<div
- style={selectionBoxStyle}
+ style={{
+ left: `${bounds.left}px`,
+ top: `${bounds.top}px`,
+ width: `${bounds.width}px`,
+ height: `${bounds.height}px`,
+ }}
className="border-foreground/50 bg-foreground/5 pointer-events-none absolute z-50 border"
/>
);
diff --git a/apps/web/src/selection/types.ts b/apps/web/src/selection/types.ts
index 31520788..a797d6c0 100644
--- a/apps/web/src/selection/types.ts
+++ b/apps/web/src/selection/types.ts
@@ -8,6 +8,13 @@ export interface BoxSelectionSnapshot<TId = string> {
initialAnchorId: TId | null;
}
+export interface SelectionBoxBounds {
+ left: number;
+ top: number;
+ width: number;
+ height: number;
+}
+
export interface BoxSelectionChange<TId = string>
extends BoxSelectionSnapshot<TId> {
intersectedIds: TId[];