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,135 +1331,68 @@ function MotionReview({
updateSelectedDay={onUpdateSelectedDay} updateSelectedDay={onUpdateSelectedDay}
/> />
)} )}
{isDesktop ? ( <Dialog
<Dialog open={isRegionFilterOpen}
open={isRegionFilterOpen} onOpenChange={(open) => {
onOpenChange={(open) => { if (open) {
if (open) { setPendingFilterCells(new Set(motionFilterCells));
setPendingFilterCells(new Set(motionFilterCells)); }
} setIsRegionFilterOpen(open);
setIsRegionFilterOpen(open); }}
}} >
> <DialogTrigger asChild>
<DialogTrigger asChild> <Button
<Button className={cn(
className="flex items-center gap-2" isDesktop ? "flex items-center gap-2" : "rounded-lg",
size="sm" )}
variant={ size="sm"
motionFilterCells.size > 0 ? "select" : "default" variant={motionFilterCells.size > 0 ? "select" : "default"}
aria-label={t("motionPreviews.filter")}
>
<FaFilter
className={
motionFilterCells.size > 0
? "text-selected-foreground"
: "text-secondary-foreground"
} }
aria-label={t("motionPreviews.filter")}
>
<FaFilter
className={
motionFilterCells.size > 0
? "text-selected-foreground"
: "text-secondary-foreground"
}
/>
{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%]">
<DialogHeader>
<DialogTitle>{t("motionPreviews.filter")}</DialogTitle>
<DialogDescription>
{t("motionPreviews.filterDesc")}
</DialogDescription>
</DialogHeader>
<MotionRegionFilterGrid
cameraName={selectedMotionPreviewCamera.name}
selectedCells={pendingFilterCells}
onCellsChange={setPendingFilterCells}
/> />
<DialogFooter className="justify-end gap-1"> {isDesktop && t("motionPreviews.filter")}
<Button </Button>
variant="outline" </DialogTrigger>
disabled={pendingFilterCells.size === 0} <DialogContent className="max-h-[90dvh] overflow-y-auto sm:max-w-[85%] md:max-w-[70%] lg:max-w-[60%]">
onClick={() => { <DialogHeader>
setPendingFilterCells(new Set()); <DialogTitle>{t("motionPreviews.filter")}</DialogTitle>
}} <DialogDescription>
> {t("motionPreviews.filterDesc")}
{t("motionPreviews.filterClear")} </DialogDescription>
</Button> </DialogHeader>
<Button <MotionRegionFilterGrid
variant="select" cameraName={selectedMotionPreviewCamera.name}
onClick={() => { selectedCells={pendingFilterCells}
setMotionFilterCells(new Set(pendingFilterCells)); onCellsChange={setPendingFilterCells}
setIsRegionFilterOpen(false); />
}} <DialogFooter className="justify-end gap-1">
>
{t("button.apply", { ns: "common" })}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
) : (
<Drawer
open={isRegionFilterOpen}
onOpenChange={(open) => {
if (open) {
setPendingFilterCells(new Set(motionFilterCells));
}
setIsRegionFilterOpen(open);
}}
>
<DrawerTrigger asChild>
<Button <Button
className="rounded-lg" variant="outline"
size="sm" disabled={pendingFilterCells.size === 0}
variant={ onClick={() => {
motionFilterCells.size > 0 ? "select" : "default" setPendingFilterCells(new Set());
} }}
aria-label={t("motionPreviews.filter")}
> >
<FaFilter {t("motionPreviews.filterClear")}
className={
motionFilterCells.size > 0
? "text-selected-foreground"
: "text-secondary-foreground"
}
/>
</Button> </Button>
</DrawerTrigger> <Button
<DrawerContent className="max-h-[75dvh] overflow-hidden px-4 pb-4"> variant="select"
<div className="space-y-4 py-2"> onClick={() => {
<div className="space-y-0.5"> setMotionFilterCells(new Set(pendingFilterCells));
<div className="text-md"> setIsRegionFilterOpen(false);
{t("motionPreviews.filter")} }}
</div> >
<div className="text-xs text-muted-foreground"> {t("button.apply", { ns: "common" })}
{t("motionPreviews.filterDesc")} </Button>
</div> </DialogFooter>
</div> </DialogContent>
<MotionRegionFilterGrid </Dialog>
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