mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-07 02:18:07 +01:00
Tracked Object Details pane tweaks (#20830)
* add prev/next buttons on desktop * buttons should work with summary and grid view * i18n * small tweaks * don't change dialog size * remove heading and count * remove icons * spacing * two column detail view * add actions to dots menu * move actions menu to its own component * set modal to false on face library dropdown to guard against improper closures https://github.com/shadcn-ui/ui/discussions/6908 * frigate plus layout * remove face training * clean up unused * refactor to remove duplication between mobile and desktop * turn annotation settings into a popover * fix popover * improve annotation offset popver * change icon and popover text in detail stream for annotation settings * clean up * use drawer on mobile * fix setter function * use dialog ref for popover portal * don't portal popover * tweaks * add button type * lower xl max width * fixes * justify
This commit is contained in:
@@ -121,13 +121,13 @@ export default function AnnotationOffsetSlider({ className }: Props) {
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
className="focus:outline-none"
|
||||
aria-label={t("trackingDetails.annotationSettings.offset.desc")}
|
||||
aria-label={t("trackingDetails.annotationSettings.offset.tips")}
|
||||
>
|
||||
<LuInfo className="size-4" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 text-sm">
|
||||
{t("trackingDetails.annotationSettings.offset.desc")}
|
||||
{t("trackingDetails.annotationSettings.offset.tips")}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import Heading from "@/components/ui/heading";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Event } from "@/types/event";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -8,7 +5,6 @@ import axios from "axios";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { LuExternalLink } from "react-icons/lu";
|
||||
import { PiWarningCircle } from "react-icons/pi";
|
||||
import { Link } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import useSWR from "swr";
|
||||
@@ -31,15 +27,11 @@ import { useDocDomain } from "@/hooks/use-doc-domain";
|
||||
|
||||
type AnnotationSettingsPaneProps = {
|
||||
event: Event;
|
||||
showZones: boolean;
|
||||
setShowZones: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
annotationOffset: number;
|
||||
setAnnotationOffset: React.Dispatch<React.SetStateAction<number>>;
|
||||
};
|
||||
export function AnnotationSettingsPane({
|
||||
event,
|
||||
showZones,
|
||||
setShowZones,
|
||||
annotationOffset,
|
||||
setAnnotationOffset,
|
||||
}: AnnotationSettingsPaneProps) {
|
||||
@@ -140,26 +132,12 @@ export function AnnotationSettingsPane({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-3 space-y-3 rounded-lg border border-secondary-foreground bg-background_alt p-2">
|
||||
<Heading as="h4" className="my-2">
|
||||
<div className="p-4">
|
||||
<div className="text-md mb-2">
|
||||
{t("trackingDetails.annotationSettings.title")}
|
||||
</Heading>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row items-center justify-start gap-2 p-3">
|
||||
<Switch
|
||||
id="show-zones"
|
||||
checked={showZones}
|
||||
onCheckedChange={setShowZones}
|
||||
/>
|
||||
<Label className="cursor-pointer" htmlFor="show-zones">
|
||||
{t("trackingDetails.annotationSettings.showAllZones.title")}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t("trackingDetails.annotationSettings.showAllZones.desc")}
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
|
||||
<Separator className="mb-4 flex bg-secondary" />
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
@@ -169,17 +147,18 @@ export function AnnotationSettingsPane({
|
||||
control={form.control}
|
||||
name="annotationOffset"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("trackingDetails.annotationSettings.offset.label")}
|
||||
</FormLabel>
|
||||
<div className="flex flex-col gap-3 md:flex-row-reverse md:gap-8">
|
||||
<div className="flex flex-row items-center gap-3 rounded-lg bg-destructive/50 p-3 text-sm text-primary-variant md:my-5">
|
||||
<PiWarningCircle className="size-24" />
|
||||
<div>
|
||||
<Trans ns="views/explore">
|
||||
trackingDetails.annotationSettings.offset.desc
|
||||
</Trans>
|
||||
<FormItem className="flex flex-row items-start justify-between space-x-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<FormLabel>
|
||||
{t("trackingDetails.annotationSettings.offset.label")}
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
<Trans ns="views/explore">
|
||||
trackingDetails.annotationSettings.offset.millisecondsToOffset
|
||||
</Trans>
|
||||
<FormMessage />
|
||||
<div className="mt-2">
|
||||
{t("trackingDetails.annotationSettings.offset.tips")}
|
||||
<div className="mt-2 flex items-center text-primary">
|
||||
<Link
|
||||
to={getLocaleDocUrl("configuration/reference")}
|
||||
@@ -192,26 +171,19 @@ export function AnnotationSettingsPane({
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
</FormDescription>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="min-w-24">
|
||||
<FormControl>
|
||||
<Input
|
||||
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||
className="text-md w-full border border-input bg-background p-2 text-center hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||
placeholder="0"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
<Trans ns="views/explore">
|
||||
trackingDetails.annotationSettings.offset.millisecondsToOffset
|
||||
</Trans>
|
||||
<div className="mt-2">
|
||||
{t("trackingDetails.annotationSettings.offset.tips")}
|
||||
</div>
|
||||
</FormDescription>
|
||||
</div>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -220,7 +192,9 @@ export function AnnotationSettingsPane({
|
||||
<div className="flex flex-row gap-2 pt-5">
|
||||
<Button
|
||||
className="flex flex-1"
|
||||
variant="default"
|
||||
aria-label={t("button.apply", { ns: "common" })}
|
||||
type="button"
|
||||
onClick={form.handleSubmit(onApply)}
|
||||
>
|
||||
{t("button.apply", { ns: "common" })}
|
||||
|
||||
118
web/src/components/overlay/detail/DetailActionsMenu.tsx
Normal file
118
web/src/components/overlay/detail/DetailActionsMenu.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { Event } from "@/types/event";
|
||||
import { baseUrl } from "@/api/baseUrl";
|
||||
import { ReviewSegment, REVIEW_PADDING } from "@/types/review";
|
||||
import useSWR from "swr";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuPortal,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { HiDotsHorizontal } from "react-icons/hi";
|
||||
import { SearchResult } from "@/types/search";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
|
||||
type Props = {
|
||||
search: SearchResult | Event;
|
||||
config?: FrigateConfig;
|
||||
setSearch?: (s: SearchResult | undefined) => void;
|
||||
setSimilarity?: () => void;
|
||||
faceNames?: string[];
|
||||
onTrainFace?: (name: string) => void;
|
||||
hasFace?: boolean;
|
||||
};
|
||||
|
||||
export default function DetailActionsMenu({
|
||||
search,
|
||||
config,
|
||||
setSearch,
|
||||
setSimilarity,
|
||||
}: Props) {
|
||||
const { t } = useTranslation(["views/explore", "views/faceLibrary"]);
|
||||
const navigate = useNavigate();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const clipTimeRange = useMemo(() => {
|
||||
const startTime = (search.start_time ?? 0) - REVIEW_PADDING;
|
||||
const endTime = (search.end_time ?? Date.now() / 1000) + REVIEW_PADDING;
|
||||
return `start/${startTime}/end/${endTime}`;
|
||||
}, [search]);
|
||||
|
||||
const { data: reviewItem } = useSWR<ReviewSegment>([
|
||||
`review/event/${search.id}`,
|
||||
]);
|
||||
|
||||
return (
|
||||
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DropdownMenuTrigger>
|
||||
<div className="rounded" role="button">
|
||||
<HiDotsHorizontal className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>
|
||||
<a
|
||||
className="w-full"
|
||||
href={`${baseUrl}api/events/${search.id}/snapshot.jpg?bbox=1`}
|
||||
download={`${search.camera}_${search.label}.jpg`}
|
||||
>
|
||||
<div className="flex cursor-pointer items-center gap-2">
|
||||
<span>{t("itemMenu.downloadSnapshot.label")}</span>
|
||||
</div>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem>
|
||||
<a
|
||||
className="w-full"
|
||||
href={`${baseUrl}api/${search.camera}/${clipTimeRange}/clip.mp4`}
|
||||
download
|
||||
>
|
||||
<div className="flex cursor-pointer items-center gap-2">
|
||||
<span>{t("itemMenu.downloadVideo.label")}</span>
|
||||
</div>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{config?.semantic_search.enabled &&
|
||||
setSimilarity != undefined &&
|
||||
search.data?.type == "object" && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
setTimeout(() => {
|
||||
setSearch?.(undefined);
|
||||
setSimilarity?.();
|
||||
}, 0);
|
||||
}}
|
||||
>
|
||||
<div className="flex cursor-pointer items-center gap-2">
|
||||
<span>{t("itemMenu.findSimilar.label")}</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{reviewItem && reviewItem.id && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
setTimeout(() => {
|
||||
navigate(`/review?id=${reviewItem.id}`);
|
||||
}, 0);
|
||||
}}
|
||||
>
|
||||
<div className="flex cursor-pointer items-center gap-2">
|
||||
<span>{t("itemMenu.viewInHistory.label")}</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,21 +2,12 @@ import useSWR from "swr";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Event } from "@/types/event";
|
||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { TrackingDetailsSequence } from "@/types/timeline";
|
||||
import Heading from "@/components/ui/heading";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
||||
import { getIconForLabel } from "@/utils/iconUtil";
|
||||
import { LuCircle, LuFolderX, LuSettings } from "react-icons/lu";
|
||||
import { LuCircle, LuFolderX } from "react-icons/lu";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { AnnotationSettingsPane } from "./AnnotationSettingsPane";
|
||||
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
||||
import HlsVideoPlayer from "@/components/player/HlsVideoPlayer";
|
||||
import { baseUrl } from "@/api/baseUrl";
|
||||
import { REVIEW_PADDING } from "@/types/review";
|
||||
@@ -38,8 +29,6 @@ import axios from "axios";
|
||||
import { toast } from "sonner";
|
||||
import { useDetailStream } from "@/context/detail-stream-context";
|
||||
import { isDesktop, isIOS, isMobileOnly, isSafari } from "react-device-detect";
|
||||
import Chip from "@/components/indicators/Chip";
|
||||
import { FaDownload, FaHistory } from "react-icons/fa";
|
||||
import { useApiHost } from "@/api";
|
||||
import ImageLoadingIndicator from "@/components/indicators/ImageLoadingIndicator";
|
||||
import ObjectTrackOverlay from "../ObjectTrackOverlay";
|
||||
@@ -58,15 +47,13 @@ export function TrackingDetails({
|
||||
}: TrackingDetailsProps) {
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const { t } = useTranslation(["views/explore"]);
|
||||
const navigate = useNavigate();
|
||||
const apiHost = useApiHost();
|
||||
const imgRef = useRef<HTMLImageElement | null>(null);
|
||||
const [imgLoaded, setImgLoaded] = useState(false);
|
||||
const [displaySource, _setDisplaySource] = useState<"video" | "image">(
|
||||
"video",
|
||||
);
|
||||
const { setSelectedObjectIds, annotationOffset, setAnnotationOffset } =
|
||||
useDetailStream();
|
||||
const { setSelectedObjectIds, annotationOffset } = useDetailStream();
|
||||
|
||||
// manualOverride holds a record-stream timestamp explicitly chosen by the
|
||||
// user (eg, clicking a lifecycle row). When null we display `currentTime`.
|
||||
@@ -97,8 +84,6 @@ export function TrackingDetails({
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [_selectedZone, setSelectedZone] = useState("");
|
||||
const [_lifecycleZones, setLifecycleZones] = useState<string[]>([]);
|
||||
const [showControls, setShowControls] = useState(false);
|
||||
const [showZones, setShowZones] = useState(true);
|
||||
const [seekToTimestamp, setSeekToTimestamp] = useState<number | null>(null);
|
||||
|
||||
const aspectRatio = useMemo(() => {
|
||||
@@ -359,7 +344,7 @@ export function TrackingDetails({
|
||||
<div
|
||||
className={cn(
|
||||
isDesktop
|
||||
? "flex size-full gap-4 overflow-hidden"
|
||||
? "flex size-full justify-evenly gap-4 overflow-hidden"
|
||||
: "flex size-full flex-col gap-2",
|
||||
className,
|
||||
)}
|
||||
@@ -452,128 +437,34 @@ export function TrackingDetails({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-2 z-[5] flex items-center gap-2",
|
||||
isIOS ? "right-8" : "right-2",
|
||||
)}
|
||||
>
|
||||
{event && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Chip
|
||||
className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500"
|
||||
onClick={() => {
|
||||
if (event?.id) {
|
||||
const params = new URLSearchParams({
|
||||
id: event.id,
|
||||
}).toString();
|
||||
navigate(`/review?${params}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FaHistory className="size-4 text-white" />
|
||||
</Chip>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>
|
||||
{t("itemMenu.viewInHistory.label")}
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<a
|
||||
download
|
||||
href={`${baseUrl}api/${event.camera}/start/${event.start_time - REVIEW_PADDING}/end/${(event.end_time ?? Date.now() / 1000) + REVIEW_PADDING}/clip.mp4`}
|
||||
>
|
||||
<Chip className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500">
|
||||
<FaDownload className="size-4 text-white" />
|
||||
</Chip>
|
||||
</a>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>
|
||||
{t("button.download", { ns: "common" })}
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cn(isDesktop && "flex-[2] overflow-hidden")}>
|
||||
{isDesktop && tabs && <div className="mb-4">{tabs}</div>}
|
||||
<div
|
||||
className={cn(
|
||||
isDesktop && "justify-between overflow-hidden md:basis-2/5",
|
||||
)}
|
||||
>
|
||||
{isDesktop && tabs && (
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex-1">{tabs}</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
isDesktop && "scrollbar-container h-full overflow-y-auto",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<Heading as="h4">{t("trackingDetails.title")}</Heading>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={showControls ? "select" : "default"}
|
||||
className="size-7 p-1.5"
|
||||
aria-label={t("trackingDetails.adjustAnnotationSettings")}
|
||||
>
|
||||
<LuSettings
|
||||
className="size-5"
|
||||
onClick={() => setShowControls(!showControls)}
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>
|
||||
{t("trackingDetails.adjustAnnotationSettings")}
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="mb-2 text-sm text-muted-foreground">
|
||||
{t("trackingDetails.scrollViewTips")}
|
||||
</div>
|
||||
<div className="min-w-20 text-right text-sm text-muted-foreground">
|
||||
{t("trackingDetails.count", {
|
||||
first: eventSequence?.length ?? 0,
|
||||
second: eventSequence?.length ?? 0,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{config?.cameras[event.camera]?.onvif.autotracking
|
||||
.enabled_in_config && (
|
||||
<div className="-mt-2 mb-2 text-sm text-danger">
|
||||
<div className="mb-2 text-sm text-danger">
|
||||
{t("trackingDetails.autoTrackingTips")}
|
||||
</div>
|
||||
)}
|
||||
{showControls && (
|
||||
<AnnotationSettingsPane
|
||||
event={event}
|
||||
showZones={showZones}
|
||||
setShowZones={setShowZones}
|
||||
annotationOffset={annotationOffset}
|
||||
setAnnotationOffset={(value) => {
|
||||
if (typeof value === "function") {
|
||||
const newValue = value(annotationOffset);
|
||||
setAnnotationOffset(newValue);
|
||||
} else {
|
||||
setAnnotationOffset(value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="mt-4">
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-md bg-secondary p-3 outline outline-[3px] -outline-offset-[2.8px] outline-transparent duration-500",
|
||||
)}
|
||||
className={cn("rounded-md bg-background_alt px-0 py-3 md:px-2")}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div
|
||||
@@ -599,7 +490,7 @@ export function TrackingDetails({
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="capitalize">{label}</span>
|
||||
<span className="text-secondary-foreground">
|
||||
<span className="md:text-md text-xs text-secondary-foreground">
|
||||
{formattedStart ?? ""} - {formattedEnd ?? ""}
|
||||
</span>
|
||||
{event.data?.recognized_license_plate && (
|
||||
|
||||
@@ -16,13 +16,7 @@ import ActivityIndicator from "../indicators/activity-indicator";
|
||||
import { Event } from "@/types/event";
|
||||
import { getIconForLabel } from "@/utils/iconUtil";
|
||||
import { ReviewSegment } from "@/types/review";
|
||||
import {
|
||||
LuChevronDown,
|
||||
LuCircle,
|
||||
LuChevronRight,
|
||||
LuSettings,
|
||||
} from "react-icons/lu";
|
||||
import { MdAutoAwesome } from "react-icons/md";
|
||||
import { LuChevronDown, LuCircle, LuChevronRight } from "react-icons/lu";
|
||||
import { getTranslatedLabel } from "@/utils/i18n";
|
||||
import EventMenu from "@/components/timeline/EventMenu";
|
||||
import { FrigatePlusDialog } from "@/components/overlay/dialog/FrigatePlusDialog";
|
||||
@@ -32,6 +26,8 @@ import { Link } from "react-router-dom";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { usePersistence } from "@/hooks/use-persistence";
|
||||
import { isDesktop } from "react-device-detect";
|
||||
import { PiSlidersHorizontalBold } from "react-icons/pi";
|
||||
import { MdAutoAwesome } from "react-icons/md";
|
||||
|
||||
type DetailStreamProps = {
|
||||
reviewItems?: ReviewSegment[];
|
||||
@@ -237,7 +233,7 @@ export default function DetailStream({
|
||||
className="flex w-full items-center justify-between p-3"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-sm font-medium">
|
||||
<LuSettings className="size-4" />
|
||||
<PiSlidersHorizontalBold className="size-4" />
|
||||
<span>{t("detail.settings")}</span>
|
||||
</div>
|
||||
{controlsExpanded ? (
|
||||
|
||||
@@ -11,13 +11,21 @@ const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & {
|
||||
container?: HTMLElement | null;
|
||||
disablePortal?: boolean;
|
||||
}
|
||||
>(
|
||||
(
|
||||
{ className, container, align = "center", sideOffset = 4, ...props },
|
||||
{
|
||||
className,
|
||||
container,
|
||||
disablePortal = false,
|
||||
align = "center",
|
||||
sideOffset = 4,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => (
|
||||
<PopoverPrimitive.Portal container={container}>
|
||||
) => {
|
||||
const content = (
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
@@ -28,8 +36,18 @@ const PopoverContent = React.forwardRef<
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
),
|
||||
);
|
||||
|
||||
if (disablePortal) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return (
|
||||
<PopoverPrimitive.Portal container={container}>
|
||||
{content}
|
||||
</PopoverPrimitive.Portal>
|
||||
);
|
||||
},
|
||||
);
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user