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

View File

@ -90,7 +90,6 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
import ReviewActivityCalendar from "@/components/overlay/ReviewActivityCalendar"; import ReviewActivityCalendar from "@/components/overlay/ReviewActivityCalendar";
import PlatformAwareDialog from "@/components/overlay/dialog/PlatformAwareDialog"; import PlatformAwareDialog from "@/components/overlay/dialog/PlatformAwareDialog";
import MotionPreviewsPane from "./MotionPreviewsPane"; import MotionPreviewsPane from "./MotionPreviewsPane";
@ -1332,7 +1331,6 @@ function MotionReview({
updateSelectedDay={onUpdateSelectedDay} updateSelectedDay={onUpdateSelectedDay}
/> />
)} )}
{isDesktop ? (
<Dialog <Dialog
open={isRegionFilterOpen} open={isRegionFilterOpen}
onOpenChange={(open) => { onOpenChange={(open) => {
@ -1344,11 +1342,11 @@ function MotionReview({
> >
<DialogTrigger asChild> <DialogTrigger asChild>
<Button <Button
className="flex items-center gap-2" className={cn(
isDesktop ? "flex items-center gap-2" : "rounded-lg",
)}
size="sm" size="sm"
variant={ variant={motionFilterCells.size > 0 ? "select" : "default"}
motionFilterCells.size > 0 ? "select" : "default"
}
aria-label={t("motionPreviews.filter")} aria-label={t("motionPreviews.filter")}
> >
<FaFilter <FaFilter
@ -1358,7 +1356,7 @@ function MotionReview({
: "text-secondary-foreground" : "text-secondary-foreground"
} }
/> />
{t("motionPreviews.filter")} {isDesktop && t("motionPreviews.filter")}
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-h-[90dvh] overflow-y-auto sm:max-w-[85%] md:max-w-[70%] lg:max-w-[60%]"> <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> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </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 <PlatformAwareDialog
trigger={ trigger={
<Button <Button