Files
frigate/web/src/components/overlay/MobileReviewSettingsDrawer.tsx
T

696 lines
21 KiB
TypeScript
Raw Normal View History

2024-05-10 11:42:56 -06:00
import { useCallback, useState } from "react";
2026-05-17 15:40:33 -05:00
import { baseUrl } from "@/api/baseUrl";
2024-03-27 17:03:05 -06:00
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
import { Button } from "../ui/button";
import { FaArrowDown, FaCalendarAlt, FaCog, FaFilter } from "react-icons/fa";
2026-05-30 22:35:03 -05:00
import { LuBug, LuSearch, LuShare2 } from "react-icons/lu";
2024-03-27 17:03:05 -06:00
import { TimeRange } from "@/types/timeline";
2026-04-14 09:19:50 -05:00
import { ExportContent, ExportPreviewDialog, ExportTab } from "./ExportDialog";
2026-03-04 10:07:34 -06:00
import {
DebugReplayContent,
SaveDebugReplayOverlay,
} from "./DebugReplayDialog";
import { ExportMode, GeneralFilter } from "@/types/filter";
2024-03-27 17:03:05 -06:00
import ReviewActivityCalendar from "./ReviewActivityCalendar";
import { SelectSeparator } from "../ui/select";
2025-02-09 17:02:36 -06:00
import {
RecordingsSummary,
ReviewFilter,
ReviewSeverity,
ReviewSummary,
} from "@/types/review";
2024-03-27 17:03:05 -06:00
import { getEndOfDayTimestamp } from "@/utils/dateUtil";
import { GeneralFilterContent } from "../filter/ReviewFilterGroup";
import { toast } from "sonner";
2026-03-04 10:07:34 -06:00
import axios, { AxiosError } from "axios";
2024-03-27 17:03:05 -06:00
import SaveExportOverlay from "./SaveExportOverlay";
2026-04-07 08:16:19 -05:00
import { isMobile } from "react-device-detect";
import { useTranslation } from "react-i18next";
2026-03-04 10:07:34 -06:00
import { useNavigate } from "react-router-dom";
2026-04-14 09:19:50 -05:00
import { StartExportResponse } from "@/types/export";
import { ShareTimestampContent } from "./ShareTimestampDialog";
2026-05-24 07:48:52 -05:00
import { useIsAdmin } from "@/hooks/use-is-admin";
2026-03-04 10:07:34 -06:00
type DrawerMode =
| "none"
| "select"
| "export"
| "calendar"
| "filter"
| "debug-replay"
| "share-timestamp";
2024-03-27 17:03:05 -06:00
2026-03-04 10:07:34 -06:00
const DRAWER_FEATURES = [
"export",
"calendar",
"filter",
"debug-replay",
"share-timestamp",
2026-05-30 22:35:03 -05:00
"motion-search",
2026-03-04 10:07:34 -06:00
] as const;
2024-03-28 08:43:05 -06:00
export type DrawerFeatures = (typeof DRAWER_FEATURES)[number];
const DEFAULT_DRAWER_FEATURES: DrawerFeatures[] = [
"export",
"calendar",
"filter",
2026-03-04 10:07:34 -06:00
"debug-replay",
"share-timestamp",
2026-05-30 22:35:03 -05:00
"motion-search",
2024-03-28 08:43:05 -06:00
];
2024-03-27 17:03:05 -06:00
type MobileReviewSettingsDrawerProps = {
2024-03-28 08:43:05 -06:00
features?: DrawerFeatures[];
2024-03-27 17:03:05 -06:00
camera: string;
filter?: ReviewFilter;
2024-05-01 08:11:16 -06:00
currentSeverity?: ReviewSeverity;
2024-03-27 17:03:05 -06:00
latestTime: number;
currentTime: number;
range?: TimeRange;
mode: ExportMode;
showExportPreview: boolean;
2024-03-28 11:53:36 -06:00
reviewSummary?: ReviewSummary;
2025-02-09 17:02:36 -06:00
recordingsSummary?: RecordingsSummary;
2024-05-10 11:42:56 -06:00
allLabels: string[];
2024-06-11 09:19:17 -05:00
allZones: string[];
2026-03-04 10:07:34 -06:00
debugReplayMode?: ExportMode;
debugReplayRange?: TimeRange;
setDebugReplayMode?: (mode: ExportMode) => void;
setDebugReplayRange?: (range: TimeRange | undefined) => void;
onShareTimestamp?: (timestamp: number) => void;
2026-05-30 22:35:03 -05:00
onMotionSearch?: () => void;
2024-03-27 17:03:05 -06:00
onUpdateFilter: (filter: ReviewFilter) => void;
setRange: (range: TimeRange | undefined) => void;
setMode: (mode: ExportMode) => void;
setShowExportPreview: (showPreview: boolean) => void;
2024-03-27 17:03:05 -06:00
};
export default function MobileReviewSettingsDrawer({
2024-03-28 08:43:05 -06:00
features = DEFAULT_DRAWER_FEATURES,
2024-03-27 17:03:05 -06:00
camera,
filter,
2024-05-01 08:11:16 -06:00
currentSeverity,
2024-03-27 17:03:05 -06:00
latestTime,
currentTime,
range,
mode,
showExportPreview,
2024-03-28 11:53:36 -06:00
reviewSummary,
2025-02-09 17:02:36 -06:00
recordingsSummary,
2024-05-10 11:42:56 -06:00
allLabels,
2024-06-11 09:19:17 -05:00
allZones,
2026-03-04 10:07:34 -06:00
debugReplayMode = "none",
debugReplayRange,
setDebugReplayMode = () => {},
setDebugReplayRange = () => {},
onShareTimestamp = () => {},
2026-05-30 22:35:03 -05:00
onMotionSearch,
2024-03-27 17:03:05 -06:00
onUpdateFilter,
setRange,
setMode,
setShowExportPreview,
2024-03-27 17:03:05 -06:00
}: MobileReviewSettingsDrawerProps) {
2026-03-04 10:07:34 -06:00
const { t } = useTranslation([
"views/recording",
"components/dialog",
"views/replay",
2026-05-30 22:35:03 -05:00
"views/events",
"common",
2026-03-04 10:07:34 -06:00
]);
2026-05-24 07:48:52 -05:00
const isAdmin = useIsAdmin();
2026-03-04 10:07:34 -06:00
const navigate = useNavigate();
2024-03-27 17:03:05 -06:00
const [drawerMode, setDrawerMode] = useState<DrawerMode>("none");
2026-04-14 09:19:50 -05:00
const [exportTab, setExportTab] = useState<ExportTab>("export");
2026-03-04 10:07:34 -06:00
const [selectedReplayOption, setSelectedReplayOption] = useState<
"1" | "5" | "custom" | "timeline"
>("1");
const [isDebugReplayStarting, setIsDebugReplayStarting] = useState(false);
const [selectedShareOption, setSelectedShareOption] = useState<
"current" | "custom"
>("current");
const [shareTimestampAtOpen, setShareTimestampAtOpen] = useState(
Math.floor(currentTime),
);
const [customShareTimestamp, setCustomShareTimestamp] = useState(
Math.floor(currentTime),
);
2024-03-27 17:03:05 -06:00
// exports
const [name, setName] = useState("");
2026-01-03 08:03:33 -07:00
const [selectedCaseId, setSelectedCaseId] = useState<string | undefined>(
undefined,
);
2026-04-14 09:19:50 -05:00
const [singleNewCaseName, setSingleNewCaseName] = useState("");
const [singleNewCaseDescription, setSingleNewCaseDescription] = useState("");
const [isStartingExport, setIsStartingExport] = useState(false);
const onStartExport = useCallback(async () => {
if (isStartingExport) {
return false;
}
2024-03-27 17:03:05 -06:00
if (!range) {
2026-04-14 09:19:50 -05:00
toast.error(
t("export.toast.error.noVaildTimeSelected", {
ns: "components/dialog",
}),
{
position: "top-center",
},
);
return false;
2024-03-27 17:03:05 -06:00
}
2024-07-16 09:04:33 -06:00
if (range.before < range.after) {
2026-04-14 09:19:50 -05:00
toast.error(
t("export.toast.error.endTimeMustAfterStartTime", {
ns: "components/dialog",
}),
{
position: "top-center",
},
);
return false;
2024-07-16 09:04:33 -06:00
}
2026-04-14 09:19:50 -05:00
setIsStartingExport(true);
try {
let exportCaseId: string | undefined = selectedCaseId;
if (selectedCaseId === "new" && singleNewCaseName.trim().length > 0) {
const caseResp = await axios.post("cases", {
name: singleNewCaseName.trim(),
description: singleNewCaseDescription.trim() || undefined,
});
exportCaseId = caseResp.data?.id;
} else if (selectedCaseId === "new" || selectedCaseId === "none") {
exportCaseId = undefined;
}
await axios.post<StartExportResponse>(
2024-04-07 14:35:45 -06:00
`export/${camera}/start/${Math.round(range.after)}/end/${Math.round(range.before)}`,
{
2026-04-14 09:19:50 -05:00
source: "recordings",
2024-04-07 14:35:45 -06:00
name,
2026-04-14 09:19:50 -05:00
export_case_id: exportCaseId,
2024-04-07 14:35:45 -06:00
},
2026-04-14 09:19:50 -05:00
);
toast.success(t("export.toast.queued", { ns: "components/dialog" }), {
position: "top-center",
action: (
2026-05-17 15:40:33 -05:00
<a
href={`${baseUrl}export`}
target="_blank"
rel="noopener noreferrer"
>
2026-04-14 09:19:50 -05:00
<Button>
{t("export.toast.view", { ns: "components/dialog" })}
</Button>
</a>
),
2024-03-27 17:03:05 -06:00
});
2026-04-14 09:19:50 -05:00
setName("");
setSelectedCaseId(undefined);
setSingleNewCaseName("");
setSingleNewCaseDescription("");
setRange(undefined);
setMode("none");
return true;
} catch (error) {
const apiError = error as {
response?: { data?: { message?: string; detail?: string } };
};
const errorMessage =
apiError.response?.data?.message ||
apiError.response?.data?.detail ||
"Unknown error";
toast.error(
t("export.toast.error.failed", {
ns: "components/dialog",
error: errorMessage,
}),
{
position: "top-center",
},
);
return false;
} finally {
setIsStartingExport(false);
}
}, [
camera,
isStartingExport,
name,
range,
selectedCaseId,
singleNewCaseDescription,
singleNewCaseName,
setRange,
setMode,
t,
]);
2024-03-27 17:03:05 -06:00
2026-03-04 10:07:34 -06:00
const onStartDebugReplay = useCallback(async () => {
if (
!debugReplayRange ||
debugReplayRange.before <= debugReplayRange.after
) {
toast.error(
t("dialog.toast.error", {
error: "End time must be after start time",
ns: "views/replay",
}),
{ position: "top-center" },
);
return;
}
setIsDebugReplayStarting(true);
try {
const response = await axios.post("debug_replay/start", {
camera: camera,
start_time: debugReplayRange.after,
end_time: debugReplayRange.before,
});
if (response.status === 202 || response.status === 200) {
2026-03-04 10:07:34 -06:00
setDebugReplayMode("none");
setDebugReplayRange(undefined);
setDrawerMode("none");
navigate("/replay");
}
} catch (error) {
const axiosError = error as AxiosError<{
message?: string;
detail?: string;
}>;
const errorMessage =
axiosError.response?.data?.message ||
axiosError.response?.data?.detail ||
"Unknown error";
if (axiosError.response?.status === 409) {
toast.error(t("dialog.toast.alreadyActive", { ns: "views/replay" }), {
position: "top-center",
});
} else {
toast.error(
t("dialog.toast.error", {
error: errorMessage,
ns: "views/replay",
}),
{
position: "top-center",
},
);
}
} finally {
setIsDebugReplayStarting(false);
}
}, [
camera,
debugReplayRange,
navigate,
setDebugReplayMode,
setDebugReplayRange,
t,
]);
2024-03-27 17:03:05 -06:00
// filters
const [currentFilter, setCurrentFilter] = useState<GeneralFilter>({
labels: filter?.labels,
zones: filter?.zones,
showAll: filter?.showAll,
...filter,
});
2024-03-27 17:03:05 -06:00
if (!isMobile) {
return;
}
let content;
if (drawerMode == "select") {
content = (
2024-05-14 10:06:44 -05:00
<div className="flex w-full flex-col gap-2 p-4">
2024-03-28 08:43:05 -06:00
{features.includes("export") && (
<Button
2024-05-14 10:06:44 -05:00
className="flex w-full items-center justify-center gap-2"
aria-label={t("export")}
2024-04-29 15:23:24 -06:00
onClick={() => {
2026-04-14 09:19:50 -05:00
setExportTab("export");
2024-04-29 15:23:24 -06:00
setDrawerMode("export");
setMode("select");
}}
2024-03-28 08:43:05 -06:00
>
2024-05-14 10:06:44 -05:00
<FaArrowDown className="rounded-md bg-secondary-foreground fill-secondary p-1" />
{t("export")}
2024-03-28 08:43:05 -06:00
</Button>
)}
{features.includes("calendar") && (
<Button
2024-05-14 10:06:44 -05:00
className="flex w-full items-center justify-center gap-2"
aria-label={t("calendar")}
2024-04-09 17:19:31 -06:00
variant={filter?.after ? "select" : "default"}
2024-03-28 08:43:05 -06:00
onClick={() => setDrawerMode("calendar")}
>
<FaCalendarAlt
2024-04-09 17:19:31 -06:00
className={`${filter?.after ? "text-selected-foreground" : "text-secondary-foreground"}`}
/>
{t("calendar")}
2024-03-28 08:43:05 -06:00
</Button>
)}
{features.includes("filter") && (
<Button
2024-05-14 10:06:44 -05:00
className="flex w-full items-center justify-center gap-2"
aria-label={t("filter")}
2024-06-11 09:46:05 -05:00
variant={filter?.labels || filter?.zones ? "select" : "default"}
2024-03-28 08:43:05 -06:00
onClick={() => setDrawerMode("filter")}
>
<FaFilter
2024-06-11 09:46:05 -05:00
className={`${filter?.labels || filter?.zones ? "text-selected-foreground" : "text-secondary-foreground"}`}
/>
{t("filter")}
2024-03-28 08:43:05 -06:00
</Button>
)}
2026-05-30 22:35:03 -05:00
{features.includes("share-timestamp") && (
<Button
className="flex w-full items-center justify-center gap-2"
aria-label={t("recording.shareTimestamp.label", {
ns: "components/dialog",
})}
onClick={() => {
const initialTimestamp = Math.floor(currentTime);
setShareTimestampAtOpen(initialTimestamp);
setCustomShareTimestamp(initialTimestamp);
setSelectedShareOption("current");
setDrawerMode("share-timestamp");
}}
>
<LuShare2 className="size-5 rounded-md bg-secondary-foreground stroke-secondary p-1" />
{t("recording.shareTimestamp.label", {
ns: "components/dialog",
})}
</Button>
)}
{features.includes("motion-search") && onMotionSearch && (
<Button
className="flex w-full items-center justify-center gap-2"
aria-label={t("motionSearch.menuItem", { ns: "views/events" })}
onClick={() => {
onMotionSearch();
setDrawerMode("none");
}}
>
<LuSearch className="size-5 rounded-md bg-secondary-foreground stroke-secondary p-1" />
{t("motionSearch.menuItem", { ns: "views/events" })}
</Button>
)}
2026-05-24 07:48:52 -05:00
{isAdmin && features.includes("debug-replay") && (
2026-03-04 10:07:34 -06:00
<Button
className="flex w-full items-center justify-center gap-2"
aria-label={t("title", { ns: "views/replay" })}
onClick={() => {
setDebugReplayRange({
2026-04-30 11:53:34 -06:00
after: latestTime - 60,
2026-03-04 10:07:34 -06:00
before: latestTime,
});
setSelectedReplayOption("1");
setDrawerMode("debug-replay");
setDebugReplayMode("select");
}}
>
<LuBug className="size-5 rounded-md bg-secondary-foreground fill-secondary stroke-secondary p-1" />
{t("title", { ns: "views/replay" })}
</Button>
)}
2024-03-27 17:03:05 -06:00
</div>
);
} else if (drawerMode == "export") {
content = (
<ExportContent
latestTime={latestTime}
currentTime={currentTime}
range={range}
name={name}
2026-01-03 08:03:33 -07:00
selectedCaseId={selectedCaseId}
2026-04-14 09:19:50 -05:00
singleNewCaseName={singleNewCaseName}
singleNewCaseDescription={singleNewCaseDescription}
activeTab={exportTab}
isStartingExport={isStartingExport}
2024-03-27 17:03:05 -06:00
onStartExport={onStartExport}
2026-04-14 09:19:50 -05:00
setActiveTab={setExportTab}
2024-03-27 17:03:05 -06:00
setName={setName}
2026-01-03 08:03:33 -07:00
setSelectedCaseId={setSelectedCaseId}
2026-04-14 09:19:50 -05:00
setSingleNewCaseName={setSingleNewCaseName}
setSingleNewCaseDescription={setSingleNewCaseDescription}
2024-03-27 17:03:05 -06:00
setRange={setRange}
setMode={(mode) => {
setMode(mode);
2026-04-14 09:19:50 -05:00
if (mode == "timeline" || mode == "timeline_multi") {
2024-03-27 17:03:05 -06:00
setDrawerMode("none");
}
}}
onCancel={() => {
setMode("none");
setRange(undefined);
2026-01-03 08:03:33 -07:00
setSelectedCaseId(undefined);
2026-04-14 09:19:50 -05:00
setSingleNewCaseName("");
setSingleNewCaseDescription("");
setExportTab("export");
2024-03-27 17:03:05 -06:00
setDrawerMode("select");
}}
/>
);
} else if (drawerMode == "calendar") {
content = (
2024-05-14 10:06:44 -05:00
<div className="flex w-full flex-col">
<div className="relative h-8 w-full">
2024-03-27 17:03:05 -06:00
<div
className="absolute left-0 text-selected"
onClick={() => setDrawerMode("select")}
>
{t("button.back", { ns: "common" })}
2024-03-27 17:03:05 -06:00
</div>
<div className="absolute left-1/2 -translate-x-1/2 text-muted-foreground">
{t("calendar")}
2024-03-27 17:03:05 -06:00
</div>
</div>
2024-09-26 15:30:56 -05:00
<div className="flex w-full flex-row justify-center">
<ReviewActivityCalendar
reviewSummary={reviewSummary}
2025-02-09 17:02:36 -06:00
recordingsSummary={recordingsSummary}
2024-09-26 15:30:56 -05:00
selectedDay={
filter?.after == undefined
? undefined
: new Date(filter.after * 1000)
}
onSelect={(day) => {
onUpdateFilter({
...filter,
after: day == undefined ? undefined : day.getTime() / 1000,
before:
day == undefined ? undefined : getEndOfDayTimestamp(day),
});
}}
/>
</div>
2024-03-27 17:03:05 -06:00
<SelectSeparator />
2024-05-14 10:06:44 -05:00
<div className="flex items-center justify-center p-2">
2024-03-27 17:03:05 -06:00
<Button
aria-label={t("button.reset", { ns: "common" })}
2024-03-27 17:03:05 -06:00
onClick={() => {
onUpdateFilter({
...filter,
after: undefined,
before: undefined,
});
}}
>
{t("button.reset", { ns: "common" })}
2024-03-27 17:03:05 -06:00
</Button>
</div>
</div>
);
} else if (drawerMode == "filter") {
content = (
2024-06-11 09:19:17 -05:00
<div className="scrollbar-container flex h-auto w-full flex-col overflow-y-auto overflow-x-hidden">
2024-05-14 10:06:44 -05:00
<div className="relative mb-2 h-8 w-full">
2024-03-27 17:03:05 -06:00
<div
className="absolute left-0 text-selected"
onClick={() => setDrawerMode("select")}
>
{t("button.back", { ns: "common" })}
2024-03-27 17:03:05 -06:00
</div>
<div className="absolute left-1/2 -translate-x-1/2 text-muted-foreground">
{t("filter")}
2024-03-27 17:03:05 -06:00
</div>
</div>
<GeneralFilterContent
2024-03-28 08:43:05 -06:00
allLabels={allLabels}
2024-03-27 17:03:05 -06:00
selectedLabels={filter?.labels}
2024-05-01 08:11:16 -06:00
currentSeverity={currentSeverity}
2024-06-11 09:19:17 -05:00
allZones={allZones}
filter={currentFilter}
2024-06-11 09:19:17 -05:00
selectedZones={filter?.zones}
onUpdateFilter={setCurrentFilter}
onApply={() => {
if (currentFilter !== filter) {
onUpdateFilter(currentFilter);
}
}}
onReset={() => {
const resetFilter: GeneralFilter = {};
setCurrentFilter(resetFilter);
onUpdateFilter(resetFilter);
2024-05-01 08:11:16 -06:00
}}
2024-03-27 17:03:05 -06:00
onClose={() => setDrawerMode("select")}
/>
</div>
);
2026-03-04 10:07:34 -06:00
} else if (drawerMode == "debug-replay") {
const handleTimeOptionChange = (
option: "1" | "5" | "custom" | "timeline",
) => {
setSelectedReplayOption(option);
if (option === "custom" || option === "timeline") {
return;
}
2026-04-30 11:53:34 -06:00
const minutes = parseInt(option, 10);
2026-03-04 10:07:34 -06:00
const end = latestTime;
2026-04-30 11:53:34 -06:00
setDebugReplayRange({ after: end - minutes * 60, before: end });
2026-03-04 10:07:34 -06:00
};
content = (
<DebugReplayContent
currentTime={currentTime}
latestTime={latestTime}
range={debugReplayRange}
selectedOption={selectedReplayOption}
isStarting={isDebugReplayStarting}
onSelectedOptionChange={handleTimeOptionChange}
onStart={onStartDebugReplay}
onCancel={() => {
setDebugReplayMode("none");
setDebugReplayRange(undefined);
setDrawerMode("select");
}}
setRange={setDebugReplayRange}
setMode={(mode) => {
setDebugReplayMode(mode);
if (mode == "timeline") {
setDrawerMode("none");
}
}}
/>
);
} else if (drawerMode == "share-timestamp") {
content = (
<div className="w-full">
<div className="relative h-8 w-full">
<div className="absolute left-1/2 -translate-x-1/2 text-muted-foreground">
{t("recording.shareTimestamp.title", { ns: "components/dialog" })}
</div>
</div>
<ShareTimestampContent
currentTime={shareTimestampAtOpen}
selectedOption={selectedShareOption}
setSelectedOption={setSelectedShareOption}
customTimestamp={customShareTimestamp}
setCustomTimestamp={setCustomShareTimestamp}
onShareTimestamp={(timestamp) => {
onShareTimestamp(timestamp);
setDrawerMode("none");
}}
onCancel={() => setDrawerMode("select")}
/>
</div>
);
2024-03-27 17:03:05 -06:00
}
return (
<>
<SaveExportOverlay
2024-05-14 10:06:44 -05:00
className="pointer-events-none absolute left-1/2 top-8 z-50 -translate-x-1/2"
2026-04-14 09:19:50 -05:00
show={mode == "timeline" || mode == "timeline_multi"}
hidePreview={mode == "timeline_multi"}
isSaving={isStartingExport}
saveLabel={
mode == "timeline_multi"
? t("export.fromTimeline.useThisRange", { ns: "components/dialog" })
: undefined
}
onSave={() => {
if (mode == "timeline_multi") {
setExportTab("multi");
setDrawerMode("export");
setMode("select");
return;
}
void onStartExport();
}}
onCancel={() => {
setExportTab("export");
setRange(undefined);
setMode("none");
}}
onPreview={() => setShowExportPreview(true)}
/>
2026-03-04 10:07:34 -06:00
<SaveDebugReplayOverlay
className="pointer-events-none absolute left-1/2 top-8 z-50 -translate-x-1/2"
show={debugReplayRange != undefined && debugReplayMode == "timeline"}
isStarting={isDebugReplayStarting}
onSave={onStartDebugReplay}
onCancel={() => {
setDebugReplayMode("none");
setDebugReplayRange(undefined);
}}
/>
<ExportPreviewDialog
camera={camera}
range={range}
showPreview={showExportPreview}
setShowPreview={setShowExportPreview}
2024-03-27 17:03:05 -06:00
/>
<Drawer
open={drawerMode != "none"}
onOpenChange={(open) => {
if (!open) {
setDrawerMode("none");
}
}}
>
<DrawerTrigger asChild>
<Button
className="rounded-lg smart-capitalize"
aria-label={t("filters")}
2024-06-11 09:46:05 -05:00
variant={
filter?.labels || filter?.after || filter?.zones
? "select"
: "default"
}
2024-03-27 17:03:05 -06:00
size="sm"
onClick={() => setDrawerMode("select")}
>
<FaCog
2024-06-11 09:46:05 -05:00
className={`${filter?.labels || filter?.after || filter?.zones ? "text-selected-foreground" : "text-secondary-foreground"}`}
/>
2024-03-27 17:03:05 -06:00
</Button>
</DrawerTrigger>
2026-03-04 10:07:34 -06:00
<DrawerContent
className={`mx-1 flex max-h-[80dvh] flex-col items-center gap-2 rounded-t-2xl px-4 pb-4 ${drawerMode == "export" || drawerMode == "debug-replay" ? "overflow-visible" : "overflow-hidden"}`}
>
2024-03-27 17:03:05 -06:00
{content}
</DrawerContent>
</Drawer>
</>
);
}