mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
Explore UI changes (#14393)
* Add time ago to explore summary view on desktop * add search settings for columns and default view selection * add descriptions * clarify wording * padding tweak * padding tweaks for mobile * fix size of activity indicator * smaller
This commit is contained in:
parent
9f866be110
commit
e836523bc3
@ -118,7 +118,7 @@ export default function SearchThumbnailFooter({
|
|||||||
<TimeAgo time={searchResult.start_time * 1000} dense />
|
<TimeAgo time={searchResult.start_time * 1000} dense />
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<ActivityIndicator size={24} />
|
<ActivityIndicator size={14} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{formattedDate}
|
{formattedDate}
|
||||||
|
@ -383,7 +383,7 @@ export default function ObjectLifecycle({
|
|||||||
{eventSequence.map((item, index) => (
|
{eventSequence.map((item, index) => (
|
||||||
<CarouselItem key={index}>
|
<CarouselItem key={index}>
|
||||||
<Card className="p-1 text-sm md:p-2" key={index}>
|
<Card className="p-1 text-sm md:p-2" key={index}>
|
||||||
<CardContent className="flex flex-row items-center gap-3 p-1 md:p-6">
|
<CardContent className="flex flex-row items-center gap-3 p-1 md:p-2">
|
||||||
<div className="flex flex-1 flex-row items-center justify-start p-3 pl-1">
|
<div className="flex flex-1 flex-row items-center justify-start p-3 pl-1">
|
||||||
<div
|
<div
|
||||||
className="rounded-lg p-2"
|
className="rounded-lg p-2"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FaArrowRight, FaCog } from "react-icons/fa";
|
import { FaArrowRight, FaFilter } from "react-icons/fa";
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { PlatformAwareSheet } from "./PlatformAwareDialog";
|
import { PlatformAwareSheet } from "./PlatformAwareDialog";
|
||||||
@ -66,7 +66,7 @@ export default function SearchFilterDialog({
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant={moreFiltersSelected ? "select" : "default"}
|
variant={moreFiltersSelected ? "select" : "default"}
|
||||||
>
|
>
|
||||||
<FaCog
|
<FaFilter
|
||||||
className={cn(
|
className={cn(
|
||||||
moreFiltersSelected ? "text-white" : "text-secondary-foreground",
|
moreFiltersSelected ? "text-white" : "text-secondary-foreground",
|
||||||
)}
|
)}
|
||||||
|
109
web/src/components/settings/SearchSettings.tsx
Normal file
109
web/src/components/settings/SearchSettings.tsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { isDesktop } from "react-device-detect";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
|
||||||
|
import { FaCog } from "react-icons/fa";
|
||||||
|
import { Slider } from "../ui/slider";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { DropdownMenuSeparator } from "../ui/dropdown-menu";
|
||||||
|
|
||||||
|
type SearchSettingsProps = {
|
||||||
|
className?: string;
|
||||||
|
columns: number;
|
||||||
|
defaultView: string;
|
||||||
|
setColumns: (columns: number) => void;
|
||||||
|
setDefaultView: (view: string) => void;
|
||||||
|
};
|
||||||
|
export default function SearchSettings({
|
||||||
|
className,
|
||||||
|
columns,
|
||||||
|
setColumns,
|
||||||
|
defaultView,
|
||||||
|
setDefaultView,
|
||||||
|
}: SearchSettingsProps) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const trigger = (
|
||||||
|
<Button className="flex items-center gap-2" size="sm">
|
||||||
|
<FaCog className="text-secondary-foreground" />
|
||||||
|
Settings
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
const content = (
|
||||||
|
<div className={cn(className, "my-3 space-y-5 py-3 md:mt-0 md:py-0")}>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<div className="text-md">Default Search View</div>
|
||||||
|
<div className="space-y-1 text-xs text-muted-foreground">
|
||||||
|
When no filters are selected, display a summary of the most recent
|
||||||
|
tracked objects per label, or display an unfiltered grid.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
value={defaultView}
|
||||||
|
onValueChange={(value) => setDefaultView(value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
{defaultView == "summary" ? "Summary" : "Unfiltered Grid"}
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
{["summary", "grid"].map((value) => (
|
||||||
|
<SelectItem
|
||||||
|
key={value}
|
||||||
|
className="cursor-pointer"
|
||||||
|
value={value}
|
||||||
|
>
|
||||||
|
{value == "summary" ? "Summary" : "Unfiltered Grid"}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<div className="flex w-full flex-col space-y-4">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<div className="text-md">Grid Columns</div>
|
||||||
|
<div className="space-y-1 text-xs text-muted-foreground">
|
||||||
|
Select the number of columns in the results grid.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<Slider
|
||||||
|
value={[columns]}
|
||||||
|
onValueChange={([value]) => setColumns(value)}
|
||||||
|
max={6}
|
||||||
|
min={2}
|
||||||
|
step={1}
|
||||||
|
className="flex-grow"
|
||||||
|
/>
|
||||||
|
<span className="w-9 text-center text-sm font-medium">{columns}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PlatformAwareDialog
|
||||||
|
trigger={trigger}
|
||||||
|
content={content}
|
||||||
|
contentClassName={
|
||||||
|
isDesktop
|
||||||
|
? "scrollbar-container h-auto max-h-[80dvh] overflow-y-auto"
|
||||||
|
: "max-h-[75dvh] overflow-hidden p-4"
|
||||||
|
}
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setOpen(open);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -7,6 +7,7 @@ import ActivityIndicator from "@/components/indicators/activity-indicator";
|
|||||||
import AnimatedCircularProgressBar from "@/components/ui/circular-progress-bar";
|
import AnimatedCircularProgressBar from "@/components/ui/circular-progress-bar";
|
||||||
import { useApiFilterArgs } from "@/hooks/use-api-filter";
|
import { useApiFilterArgs } from "@/hooks/use-api-filter";
|
||||||
import { useTimezone } from "@/hooks/use-date-utils";
|
import { useTimezone } from "@/hooks/use-date-utils";
|
||||||
|
import { usePersistence } from "@/hooks/use-persistence";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { SearchFilter, SearchQuery, SearchResult } from "@/types/search";
|
import { SearchFilter, SearchQuery, SearchResult } from "@/types/search";
|
||||||
import { ModelState } from "@/types/ws";
|
import { ModelState } from "@/types/ws";
|
||||||
@ -28,6 +29,18 @@ export default function Explore() {
|
|||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// grid
|
||||||
|
|
||||||
|
const [columnCount, setColumnCount] = usePersistence("exploreGridColumns", 4);
|
||||||
|
const gridColumns = useMemo(() => columnCount ?? 4, [columnCount]);
|
||||||
|
|
||||||
|
// default layout
|
||||||
|
|
||||||
|
const [defaultView, setDefaultView] = usePersistence(
|
||||||
|
"exploreDefaultView",
|
||||||
|
"summary",
|
||||||
|
);
|
||||||
|
|
||||||
const timezone = useTimezone(config);
|
const timezone = useTimezone(config);
|
||||||
|
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
@ -65,7 +78,11 @@ export default function Explore() {
|
|||||||
const searchQuery: SearchQuery = useMemo(() => {
|
const searchQuery: SearchQuery = useMemo(() => {
|
||||||
// no search parameters
|
// no search parameters
|
||||||
if (searchSearchParams && Object.keys(searchSearchParams).length === 0) {
|
if (searchSearchParams && Object.keys(searchSearchParams).length === 0) {
|
||||||
return null;
|
if (defaultView == "grid") {
|
||||||
|
return ["events", {}];
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parameters, but no search term and not similarity
|
// parameters, but no search term and not similarity
|
||||||
@ -117,7 +134,7 @@ export default function Explore() {
|
|||||||
include_thumbnails: 0,
|
include_thumbnails: 0,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [searchTerm, searchSearchParams, similaritySearch, timezone]);
|
}, [searchTerm, searchSearchParams, similaritySearch, timezone, defaultView]);
|
||||||
|
|
||||||
// paging
|
// paging
|
||||||
|
|
||||||
@ -385,6 +402,8 @@ export default function Explore() {
|
|||||||
searchResults={searchResults}
|
searchResults={searchResults}
|
||||||
isLoading={(isLoadingInitialData || isLoadingMore) ?? true}
|
isLoading={(isLoadingInitialData || isLoadingMore) ?? true}
|
||||||
hasMore={!isReachingEnd}
|
hasMore={!isReachingEnd}
|
||||||
|
columns={gridColumns}
|
||||||
|
defaultView={defaultView}
|
||||||
setSearch={setSearch}
|
setSearch={setSearch}
|
||||||
setSimilaritySearch={(search) => {
|
setSimilaritySearch={(search) => {
|
||||||
setSearchFilter({
|
setSearchFilter({
|
||||||
@ -395,6 +414,8 @@ export default function Explore() {
|
|||||||
}}
|
}}
|
||||||
setSearchFilter={setSearchFilter}
|
setSearchFilter={setSearchFilter}
|
||||||
onUpdateFilter={setSearchFilter}
|
onUpdateFilter={setSearchFilter}
|
||||||
|
setColumns={setColumnCount}
|
||||||
|
setDefaultView={setDefaultView}
|
||||||
loadMore={loadMore}
|
loadMore={loadMore}
|
||||||
refresh={mutate}
|
refresh={mutate}
|
||||||
/>
|
/>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { isIOS, isMobileOnly, isSafari } from "react-device-detect";
|
import { isDesktop, isIOS, isMobileOnly, isSafari } from "react-device-detect";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { useApiHost } from "@/api";
|
import { useApiHost } from "@/api";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@ -17,6 +17,7 @@ import useImageLoaded from "@/hooks/use-image-loaded";
|
|||||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||||
import { useEventUpdate } from "@/api/ws";
|
import { useEventUpdate } from "@/api/ws";
|
||||||
import { isEqual } from "lodash";
|
import { isEqual } from "lodash";
|
||||||
|
import TimeAgo from "@/components/dynamic/TimeAgo";
|
||||||
|
|
||||||
type ExploreViewProps = {
|
type ExploreViewProps = {
|
||||||
searchDetail: SearchResult | undefined;
|
searchDetail: SearchResult | undefined;
|
||||||
@ -197,6 +198,7 @@ function ExploreThumbnailImage({
|
|||||||
className="absolute inset-0"
|
className="absolute inset-0"
|
||||||
imgLoaded={imgLoaded}
|
imgLoaded={imgLoaded}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
ref={imgRef}
|
ref={imgRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -218,6 +220,17 @@ function ExploreThumbnailImage({
|
|||||||
onImgLoad();
|
onImgLoad();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{isDesktop && (
|
||||||
|
<div className="absolute bottom-1 right-1 z-10 rounded-lg bg-black/50 px-2 py-1 text-xs text-white">
|
||||||
|
{event.end_time ? (
|
||||||
|
<TimeAgo time={event.start_time * 1000} dense />
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<ActivityIndicator size={10} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,17 +5,12 @@ import SearchDetailDialog, {
|
|||||||
SearchTab,
|
SearchTab,
|
||||||
} from "@/components/overlay/detail/SearchDetailDialog";
|
} from "@/components/overlay/detail/SearchDetailDialog";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { SearchFilter, SearchResult, SearchSource } from "@/types/search";
|
import { SearchFilter, SearchResult, SearchSource } from "@/types/search";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { isDesktop, isMobileOnly } from "react-device-detect";
|
import { isMobileOnly } from "react-device-detect";
|
||||||
import { LuColumns, LuSearchX } from "react-icons/lu";
|
import { LuSearchX } from "react-icons/lu";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import ExploreView from "../explore/ExploreView";
|
import ExploreView from "../explore/ExploreView";
|
||||||
import useKeyboardListener, {
|
import useKeyboardListener, {
|
||||||
@ -26,14 +21,8 @@ import InputWithTags from "@/components/input/InputWithTags";
|
|||||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||||
import { isEqual } from "lodash";
|
import { isEqual } from "lodash";
|
||||||
import { formatDateToLocaleString } from "@/utils/dateUtil";
|
import { formatDateToLocaleString } from "@/utils/dateUtil";
|
||||||
import { Slider } from "@/components/ui/slider";
|
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from "@/components/ui/popover";
|
|
||||||
import { usePersistence } from "@/hooks/use-persistence";
|
|
||||||
import SearchThumbnailFooter from "@/components/card/SearchThumbnailFooter";
|
import SearchThumbnailFooter from "@/components/card/SearchThumbnailFooter";
|
||||||
|
import SearchSettings from "@/components/settings/SearchSettings";
|
||||||
|
|
||||||
type SearchViewProps = {
|
type SearchViewProps = {
|
||||||
search: string;
|
search: string;
|
||||||
@ -42,12 +31,16 @@ type SearchViewProps = {
|
|||||||
searchResults?: SearchResult[];
|
searchResults?: SearchResult[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
hasMore: boolean;
|
hasMore: boolean;
|
||||||
|
columns: number;
|
||||||
|
defaultView?: string;
|
||||||
setSearch: (search: string) => void;
|
setSearch: (search: string) => void;
|
||||||
setSimilaritySearch: (search: SearchResult) => void;
|
setSimilaritySearch: (search: SearchResult) => void;
|
||||||
setSearchFilter: (filter: SearchFilter) => void;
|
setSearchFilter: (filter: SearchFilter) => void;
|
||||||
onUpdateFilter: (filter: SearchFilter) => void;
|
onUpdateFilter: (filter: SearchFilter) => void;
|
||||||
loadMore: () => void;
|
loadMore: () => void;
|
||||||
refresh: () => void;
|
refresh: () => void;
|
||||||
|
setColumns: (columns: number) => void;
|
||||||
|
setDefaultView: (name: string) => void;
|
||||||
};
|
};
|
||||||
export default function SearchView({
|
export default function SearchView({
|
||||||
search,
|
search,
|
||||||
@ -56,12 +49,16 @@ export default function SearchView({
|
|||||||
searchResults,
|
searchResults,
|
||||||
isLoading,
|
isLoading,
|
||||||
hasMore,
|
hasMore,
|
||||||
|
columns,
|
||||||
|
defaultView = "summary",
|
||||||
setSearch,
|
setSearch,
|
||||||
setSimilaritySearch,
|
setSimilaritySearch,
|
||||||
setSearchFilter,
|
setSearchFilter,
|
||||||
onUpdateFilter,
|
onUpdateFilter,
|
||||||
loadMore,
|
loadMore,
|
||||||
refresh,
|
refresh,
|
||||||
|
setColumns,
|
||||||
|
setDefaultView,
|
||||||
}: SearchViewProps) {
|
}: SearchViewProps) {
|
||||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||||
const { data: config } = useSWR<FrigateConfig>("config", {
|
const { data: config } = useSWR<FrigateConfig>("config", {
|
||||||
@ -70,18 +67,15 @@ export default function SearchView({
|
|||||||
|
|
||||||
// grid
|
// grid
|
||||||
|
|
||||||
const [columnCount, setColumnCount] = usePersistence("exploreGridColumns", 4);
|
|
||||||
const effectiveColumnCount = useMemo(() => columnCount ?? 4, [columnCount]);
|
|
||||||
|
|
||||||
const gridClassName = cn(
|
const gridClassName = cn(
|
||||||
"grid w-full gap-2 px-1 gap-2 lg:gap-4 md:mx-2",
|
"grid w-full gap-2 px-1 gap-2 lg:gap-4 md:mx-2",
|
||||||
isMobileOnly && "grid-cols-2",
|
isMobileOnly && "grid-cols-2",
|
||||||
{
|
{
|
||||||
"sm:grid-cols-2": effectiveColumnCount <= 2,
|
"sm:grid-cols-2": columns <= 2,
|
||||||
"sm:grid-cols-3": effectiveColumnCount === 3,
|
"sm:grid-cols-3": columns === 3,
|
||||||
"sm:grid-cols-4": effectiveColumnCount === 4,
|
"sm:grid-cols-4": columns === 4,
|
||||||
"sm:grid-cols-5": effectiveColumnCount === 5,
|
"sm:grid-cols-5": columns === 5,
|
||||||
"sm:grid-cols-6": effectiveColumnCount === 6,
|
"sm:grid-cols-6": columns === 6,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -342,7 +336,7 @@ export default function SearchView({
|
|||||||
|
|
||||||
{hasExistingSearch && (
|
{hasExistingSearch && (
|
||||||
<ScrollArea className="w-full whitespace-nowrap lg:ml-[35%]">
|
<ScrollArea className="w-full whitespace-nowrap lg:ml-[35%]">
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row gap-2">
|
||||||
<SearchFilterGroup
|
<SearchFilterGroup
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full justify-between md:justify-start lg:justify-end",
|
"w-full justify-between md:justify-start lg:justify-end",
|
||||||
@ -350,6 +344,12 @@ export default function SearchView({
|
|||||||
filter={searchFilter}
|
filter={searchFilter}
|
||||||
onUpdateFilter={onUpdateFilter}
|
onUpdateFilter={onUpdateFilter}
|
||||||
/>
|
/>
|
||||||
|
<SearchSettings
|
||||||
|
columns={columns}
|
||||||
|
setColumns={setColumns}
|
||||||
|
defaultView={defaultView}
|
||||||
|
setDefaultView={setDefaultView}
|
||||||
|
/>
|
||||||
<ScrollBar orientation="horizontal" className="h-0" />
|
<ScrollBar orientation="horizontal" className="h-0" />
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
@ -425,53 +425,13 @@ export default function SearchView({
|
|||||||
<div className="flex h-12 w-full justify-center">
|
<div className="flex h-12 w-full justify-center">
|
||||||
{hasMore && isLoading && <ActivityIndicator />}
|
{hasMore && isLoading && <ActivityIndicator />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isDesktop && columnCount && (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"fixed bottom-12 right-3 z-50 flex flex-row gap-2 lg:bottom-9",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Popover>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<div className="cursor-pointer rounded-lg bg-secondary text-secondary-foreground opacity-75 transition-all duration-300 hover:bg-muted hover:opacity-100">
|
|
||||||
<LuColumns className="size-5 md:m-[6px]" />
|
|
||||||
</div>
|
|
||||||
</PopoverTrigger>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Adjust Grid Columns</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<PopoverContent className="mr-2 w-80">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="font-medium leading-none">
|
|
||||||
Grid Columns
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<Slider
|
|
||||||
value={[effectiveColumnCount]}
|
|
||||||
onValueChange={([value]) => setColumnCount(value)}
|
|
||||||
max={6}
|
|
||||||
min={2}
|
|
||||||
step={1}
|
|
||||||
className="flex-grow"
|
|
||||||
/>
|
|
||||||
<span className="w-9 text-center text-sm font-medium">
|
|
||||||
{effectiveColumnCount}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{searchFilter &&
|
{searchFilter &&
|
||||||
Object.keys(searchFilter).length === 0 &&
|
Object.keys(searchFilter).length === 0 &&
|
||||||
!searchTerm && (
|
!searchTerm &&
|
||||||
|
defaultView == "summary" && (
|
||||||
<div className="scrollbar-container flex size-full flex-col overflow-y-auto">
|
<div className="scrollbar-container flex size-full flex-col overflow-y-auto">
|
||||||
<ExploreView
|
<ExploreView
|
||||||
searchDetail={searchDetail}
|
searchDetail={searchDetail}
|
||||||
|
Loading…
Reference in New Issue
Block a user