use dialog on mobile

This commit is contained in:
Josh Hawkins 2026-03-09 08:35:39 -05:00
parent 6329559d5f
commit 33c9d2345a
2 changed files with 111 additions and 139 deletions

View File

@ -18,6 +18,8 @@ export default function MotionRegionFilterGrid({
active: false,
adding: true,
});
const lastCellRef = useRef<number>(-1);
const gridRef = useRef<HTMLDivElement>(null);
const toggleCell = useCallback(
(index: number, forceAdd?: boolean) => {
@ -40,28 +42,68 @@ export default function MotionRegionFilterGrid({
[selectedCells, onCellsChange],
);
const handlePointerDown = useCallback(
(index: number) => {
const adding = !selectedCells.has(index);
paintingRef.current = { active: true, adding };
toggleCell(index, adding);
const getCellFromPoint = useCallback(
(clientX: number, clientY: number): number | null => {
const grid = gridRef.current;
if (!grid) {
return null;
}
const rect = grid.getBoundingClientRect();
const x = clientX - rect.left;
const y = clientY - rect.top;
if (x < 0 || y < 0 || x >= rect.width || y >= rect.height) {
return null;
}
const col = Math.floor((x / rect.width) * GRID_SIZE);
const row = Math.floor((y / rect.height) * GRID_SIZE);
return row * GRID_SIZE + col;
},
[selectedCells, toggleCell],
[],
);
const handlePointerEnter = useCallback(
(index: number) => {
const handlePointerDown = useCallback(
(e: React.PointerEvent) => {
e.preventDefault();
const index = getCellFromPoint(e.clientX, e.clientY);
if (index === null) {
return;
}
const adding = !selectedCells.has(index);
paintingRef.current = { active: true, adding };
lastCellRef.current = index;
toggleCell(index, adding);
},
[selectedCells, toggleCell, getCellFromPoint],
);
const handlePointerMove = useCallback(
(e: React.PointerEvent) => {
if (!paintingRef.current.active) {
return;
}
const index = getCellFromPoint(e.clientX, e.clientY);
if (index === null || index === lastCellRef.current) {
return;
}
lastCellRef.current = index;
toggleCell(index, paintingRef.current.adding);
},
[toggleCell],
[toggleCell, getCellFromPoint],
);
const handlePointerUp = useCallback(() => {
paintingRef.current.active = false;
lastCellRef.current = -1;
}, []);
return (
@ -79,11 +121,14 @@ export default function MotionRegionFilterGrid({
alt=""
/>
<div
ref={gridRef}
className="absolute inset-0 grid"
style={{
gridTemplateColumns: `repeat(${GRID_SIZE}, 1fr)`,
gridTemplateRows: `repeat(${GRID_SIZE}, 1fr)`,
}}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
>
{Array.from({ length: GRID_SIZE * GRID_SIZE }, (_, index) => {
const isSelected = selectedCells.has(index);
@ -95,11 +140,6 @@ export default function MotionRegionFilterGrid({
? "border border-severity_alert/60 bg-severity_alert/40"
: "border border-transparent hover:bg-white/20"
}
onPointerDown={(e) => {
e.preventDefault();
handlePointerDown(index);
}}
onPointerEnter={() => handlePointerEnter(index)}
/>
);
})}

View File

@ -90,7 +90,6 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
import ReviewActivityCalendar from "@/components/overlay/ReviewActivityCalendar";
import PlatformAwareDialog from "@/components/overlay/dialog/PlatformAwareDialog";
import MotionPreviewsPane from "./MotionPreviewsPane";
@ -1332,7 +1331,6 @@ function MotionReview({
updateSelectedDay={onUpdateSelectedDay}
/>
)}
{isDesktop ? (
<Dialog
open={isRegionFilterOpen}
onOpenChange={(open) => {
@ -1344,11 +1342,11 @@ function MotionReview({
>
<DialogTrigger asChild>
<Button
className="flex items-center gap-2"
className={cn(
isDesktop ? "flex items-center gap-2" : "rounded-lg",
)}
size="sm"
variant={
motionFilterCells.size > 0 ? "select" : "default"
}
variant={motionFilterCells.size > 0 ? "select" : "default"}
aria-label={t("motionPreviews.filter")}
>
<FaFilter
@ -1358,7 +1356,7 @@ function MotionReview({
: "text-secondary-foreground"
}
/>
{t("motionPreviews.filter")}
{isDesktop && t("motionPreviews.filter")}
</Button>
</DialogTrigger>
<DialogContent className="max-h-[90dvh] overflow-y-auto sm:max-w-[85%] md:max-w-[70%] lg:max-w-[60%]">
@ -1395,72 +1393,6 @@ function MotionReview({
</DialogFooter>
</DialogContent>
</Dialog>
) : (
<Drawer
open={isRegionFilterOpen}
onOpenChange={(open) => {
if (open) {
setPendingFilterCells(new Set(motionFilterCells));
}
setIsRegionFilterOpen(open);
}}
>
<DrawerTrigger asChild>
<Button
className="rounded-lg"
size="sm"
variant={
motionFilterCells.size > 0 ? "select" : "default"
}
aria-label={t("motionPreviews.filter")}
>
<FaFilter
className={
motionFilterCells.size > 0
? "text-selected-foreground"
: "text-secondary-foreground"
}
/>
</Button>
</DrawerTrigger>
<DrawerContent className="max-h-[75dvh] overflow-hidden px-4 pb-4">
<div className="space-y-4 py-2">
<div className="space-y-0.5">
<div className="text-md">
{t("motionPreviews.filter")}
</div>
<div className="text-xs text-muted-foreground">
{t("motionPreviews.filterDesc")}
</div>
</div>
<MotionRegionFilterGrid
cameraName={selectedMotionPreviewCamera.name}
selectedCells={pendingFilterCells}
onCellsChange={setPendingFilterCells}
/>
<div className="flex justify-end gap-2">
<Button
variant="outline"
disabled={pendingFilterCells.size === 0}
onClick={() => {
setPendingFilterCells(new Set());
}}
>
{t("motionPreviews.filterClear")}
</Button>
<Button
onClick={() => {
setMotionFilterCells(new Set(pendingFilterCells));
setIsRegionFilterOpen(false);
}}
>
{t("button.apply", { ns: "common" })}
</Button>
</div>
</div>
</DrawerContent>
</Drawer>
)}
<PlatformAwareDialog
trigger={
<Button