mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
Icon picker component (#11310)
* icon picker component * keep box the same size when filtering icons
This commit is contained in:
parent
50ee447e52
commit
f8523d9ddf
@ -1,8 +1,4 @@
|
|||||||
import {
|
import { CameraGroupConfig, FrigateConfig } from "@/types/frigateConfig";
|
||||||
CameraGroupConfig,
|
|
||||||
FrigateConfig,
|
|
||||||
GROUP_ICONS,
|
|
||||||
} from "@/types/frigateConfig";
|
|
||||||
import { isDesktop, isMobile } from "react-device-detect";
|
import { isDesktop, isMobile } from "react-device-detect";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { MdHome } from "react-icons/md";
|
import { MdHome } from "react-icons/md";
|
||||||
@ -10,7 +6,6 @@ import { usePersistedOverlayState } from "@/hooks/use-overlay-state";
|
|||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||||
import { getIconForGroup } from "@/utils/iconUtil";
|
|
||||||
import { LuPencil, LuPlus } from "react-icons/lu";
|
import { LuPencil, LuPlus } from "react-icons/lu";
|
||||||
import { Dialog, DialogContent, DialogTitle } from "../ui/dialog";
|
import { Dialog, DialogContent, DialogTitle } from "../ui/dialog";
|
||||||
import { Drawer, DrawerContent } from "../ui/drawer";
|
import { Drawer, DrawerContent } from "../ui/drawer";
|
||||||
@ -31,13 +26,6 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "../ui/dropdown-menu";
|
} from "../ui/dropdown-menu";
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@ -62,6 +50,9 @@ import { ScrollArea, ScrollBar } from "../ui/scroll-area";
|
|||||||
import { usePersistence } from "@/hooks/use-persistence";
|
import { usePersistence } from "@/hooks/use-persistence";
|
||||||
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import * as LuIcons from "react-icons/lu";
|
||||||
|
import IconPicker, { IconName, IconRenderer } from "../icons/IconPicker";
|
||||||
|
import { isValidIconName } from "@/utils/iconUtil";
|
||||||
|
|
||||||
type CameraGroupSelectorProps = {
|
type CameraGroupSelectorProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -168,7 +159,12 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
|
|||||||
isDesktop ? showTooltip(undefined) : null
|
isDesktop ? showTooltip(undefined) : null
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{getIconForGroup(config.icon)}
|
{config && config.icon && isValidIconName(config.icon) && (
|
||||||
|
<IconRenderer
|
||||||
|
icon={LuIcons[config.icon]}
|
||||||
|
className="size-4"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPortal>
|
<TooltipPortal>
|
||||||
@ -503,7 +499,12 @@ export function CameraGroupEdit({
|
|||||||
cameras: z.array(z.string()).min(2, {
|
cameras: z.array(z.string()).min(2, {
|
||||||
message: "You must select at least two cameras.",
|
message: "You must select at least two cameras.",
|
||||||
}),
|
}),
|
||||||
icon: z.string(),
|
icon: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "You must select an icon." })
|
||||||
|
.refine((value) => Object.keys(LuIcons).includes(value), {
|
||||||
|
message: "Invalid icon",
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
@ -559,10 +560,10 @@ export function CameraGroupEdit({
|
|||||||
|
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
mode: "onChange",
|
mode: "onSubmit",
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: (editingGroup && editingGroup[0]) ?? "",
|
name: (editingGroup && editingGroup[0]) ?? "",
|
||||||
icon: editingGroup && editingGroup[1].icon,
|
icon: editingGroup && (editingGroup[1].icon as IconName),
|
||||||
cameras: editingGroup && editingGroup[1].cameras,
|
cameras: editingGroup && editingGroup[1].cameras,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -571,7 +572,7 @@ export function CameraGroupEdit({
|
|||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="mt-2 space-y-6 overflow-y-hidden"
|
className="mt-2 space-y-6 overflow-y-auto"
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@ -631,29 +632,20 @@ export function CameraGroupEdit({
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="icon"
|
name="icon"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="space-y-3">
|
<FormItem className="flex flex-col space-y-2">
|
||||||
<FormLabel>Icon</FormLabel>
|
<FormLabel>Icon</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select
|
<IconPicker
|
||||||
onValueChange={field.onChange}
|
selectedIcon={{
|
||||||
defaultValue={field.value}
|
name: field.value,
|
||||||
>
|
Icon: field.value
|
||||||
<FormControl>
|
? LuIcons[field.value as IconName]
|
||||||
<SelectTrigger>
|
: undefined,
|
||||||
<SelectValue placeholder="Select an icon" />
|
}}
|
||||||
</SelectTrigger>
|
setSelectedIcon={(newIcon) => {
|
||||||
</FormControl>
|
field.onChange(newIcon?.name ?? undefined);
|
||||||
<SelectContent>
|
}}
|
||||||
{GROUP_ICONS.map((gIcon) => (
|
/>
|
||||||
<SelectItem key={gIcon} value={gIcon}>
|
|
||||||
<div className="flex flex-row justify-start items-center gap-2">
|
|
||||||
<div className="size-4">{getIconForGroup(gIcon)}</div>
|
|
||||||
{gIcon}
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@ -662,7 +654,7 @@ export function CameraGroupEdit({
|
|||||||
|
|
||||||
<Separator className="flex my-2 bg-secondary" />
|
<Separator className="flex my-2 bg-secondary" />
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 pt-5">
|
<div className="flex flex-row gap-2 py-5 md:pb-0">
|
||||||
<Button className="flex flex-1" onClick={onCancel}>
|
<Button className="flex flex-1" onClick={onCancel}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
154
web/src/components/icons/IconPicker.tsx
Normal file
154
web/src/components/icons/IconPicker.tsx
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import React, { useCallback, useMemo, useRef, useState } from "react";
|
||||||
|
import { IconType } from "react-icons";
|
||||||
|
import * as LuIcons from "react-icons/lu";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { IoClose } from "react-icons/io5";
|
||||||
|
import Heading from "../ui/heading";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
|
||||||
|
export type IconName = keyof typeof LuIcons;
|
||||||
|
|
||||||
|
export type IconElement = {
|
||||||
|
name?: string;
|
||||||
|
Icon?: IconType;
|
||||||
|
};
|
||||||
|
|
||||||
|
type IconPickerProps = {
|
||||||
|
selectedIcon?: IconElement;
|
||||||
|
setSelectedIcon?: React.Dispatch<
|
||||||
|
React.SetStateAction<IconElement | undefined>
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function IconPicker({
|
||||||
|
selectedIcon,
|
||||||
|
setSelectedIcon,
|
||||||
|
}: IconPickerProps) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
|
||||||
|
const iconSets = useMemo(() => [...Object.entries(LuIcons)], []);
|
||||||
|
|
||||||
|
const icons = useMemo(
|
||||||
|
() =>
|
||||||
|
iconSets.filter(
|
||||||
|
([name]) =>
|
||||||
|
name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
searchTerm === "",
|
||||||
|
),
|
||||||
|
[iconSets, searchTerm],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleIconSelect = useCallback(
|
||||||
|
({ name, Icon }: IconElement) => {
|
||||||
|
if (setSelectedIcon) {
|
||||||
|
setSelectedIcon({ name, Icon });
|
||||||
|
}
|
||||||
|
setSearchTerm("");
|
||||||
|
},
|
||||||
|
[setSelectedIcon],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={containerRef}>
|
||||||
|
<Popover
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setOpen(open);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
{!selectedIcon?.name || !selectedIcon?.Icon ? (
|
||||||
|
<Button className="text-muted-foreground w-full mt-2">
|
||||||
|
Select an icon
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<div className="hover:cursor-pointer">
|
||||||
|
<div className="flex flex-row w-full justify-between items-center gap-2 my-3">
|
||||||
|
<div className="flex flex-row items-center gap-2">
|
||||||
|
<selectedIcon.Icon size={15} />
|
||||||
|
<div className="text-sm">
|
||||||
|
{selectedIcon.name
|
||||||
|
.replace(/^Lu/, "")
|
||||||
|
.replace(/([A-Z])/g, " $1")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<IoClose
|
||||||
|
className="mx-2 hover:cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
handleIconSelect({ name: undefined, Icon: undefined });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
align="start"
|
||||||
|
side="top"
|
||||||
|
container={containerRef.current}
|
||||||
|
className="max-h-[50dvh]"
|
||||||
|
>
|
||||||
|
<div className="flex flex-row justify-between items-center mb-3">
|
||||||
|
<Heading as="h4">Select an icon</Heading>
|
||||||
|
<IoClose
|
||||||
|
size={15}
|
||||||
|
className="hover:cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search for an icon..."
|
||||||
|
className="mb-3"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col flex-1 h-[20dvh]">
|
||||||
|
<div className="grid grid-cols-6 my-2 gap-2 max-h-[20dvh] overflow-y-auto pr-1">
|
||||||
|
{icons.map(([name, Icon]) => (
|
||||||
|
<div
|
||||||
|
key={name}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-row justify-center items-start hover:cursor-pointer p-1 rounded-lg",
|
||||||
|
selectedIcon?.name === name
|
||||||
|
? "bg-selected text-white"
|
||||||
|
: "hover:bg-secondary-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
size={20}
|
||||||
|
onClick={() => {
|
||||||
|
handleIconSelect({ name, Icon });
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type IconRendererProps = {
|
||||||
|
icon: IconType;
|
||||||
|
size?: number;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function IconRenderer({ icon, size, className }: IconRendererProps) {
|
||||||
|
return <>{React.createElement(icon, { size, className })}</>;
|
||||||
|
}
|
@ -1,29 +1,36 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Popover = PopoverPrimitive.Root
|
const Popover = PopoverPrimitive.Root;
|
||||||
|
|
||||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||||
|
|
||||||
const PopoverContent = React.forwardRef<
|
const PopoverContent = React.forwardRef<
|
||||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & {
|
||||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
container?: HTMLElement | null;
|
||||||
<PopoverPrimitive.Portal>
|
}
|
||||||
<PopoverPrimitive.Content
|
>(
|
||||||
ref={ref}
|
(
|
||||||
align={align}
|
{ className, container, align = "center", sideOffset = 4, ...props },
|
||||||
sideOffset={sideOffset}
|
ref,
|
||||||
className={cn(
|
) => (
|
||||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
<PopoverPrimitive.Portal container={container}>
|
||||||
className
|
<PopoverPrimitive.Content
|
||||||
)}
|
ref={ref}
|
||||||
{...props}
|
align={align}
|
||||||
/>
|
sideOffset={sideOffset}
|
||||||
</PopoverPrimitive.Portal>
|
className={cn(
|
||||||
))
|
"z-50 w-72 rounded-lg border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||||
|
|
||||||
export { Popover, PopoverTrigger, PopoverContent }
|
export { Popover, PopoverTrigger, PopoverContent };
|
||||||
|
@ -28,6 +28,7 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import SummaryTimeline from "@/components/timeline/SummaryTimeline";
|
import SummaryTimeline from "@/components/timeline/SummaryTimeline";
|
||||||
import { isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
|
import IconPicker, { IconElement } from "@/components/icons/IconPicker";
|
||||||
|
|
||||||
// Color data
|
// Color data
|
||||||
const colors = [
|
const colors = [
|
||||||
@ -207,6 +208,8 @@ function UIPlayground() {
|
|||||||
const [isEventsReviewTimeline, setIsEventsReviewTimeline] = useState(true);
|
const [isEventsReviewTimeline, setIsEventsReviewTimeline] = useState(true);
|
||||||
const birdseyeConfig = config?.birdseye;
|
const birdseyeConfig = config?.birdseye;
|
||||||
|
|
||||||
|
const [selectedIcon, setSelectedIcon] = useState<IconElement>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-full h-full">
|
<div className="w-full h-full">
|
||||||
@ -214,6 +217,15 @@ function UIPlayground() {
|
|||||||
<div className="flex-1 content-start gap-2 overflow-y-auto no-scrollbar mt-4 mr-5">
|
<div className="flex-1 content-start gap-2 overflow-y-auto no-scrollbar mt-4 mr-5">
|
||||||
<Heading as="h2">UI Playground</Heading>
|
<Heading as="h2">UI Playground</Heading>
|
||||||
|
|
||||||
|
<IconPicker
|
||||||
|
selectedIcon={selectedIcon}
|
||||||
|
setSelectedIcon={setSelectedIcon}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{selectedIcon?.name && (
|
||||||
|
<p>Selected icon name: {selectedIcon.name}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<Heading as="h4" className="my-5">
|
<Heading as="h4" className="my-5">
|
||||||
Scrubber
|
Scrubber
|
||||||
</Heading>
|
</Heading>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { IconName } from "@/components/icons/IconPicker";
|
||||||
import { LivePlayerMode } from "./live";
|
import { LivePlayerMode } from "./live";
|
||||||
|
|
||||||
export interface UiConfig {
|
export interface UiConfig {
|
||||||
@ -222,11 +223,9 @@ export interface CameraConfig {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GROUP_ICONS = ["car", "cat", "dog", "leaf"] as const;
|
|
||||||
|
|
||||||
export type CameraGroupConfig = {
|
export type CameraGroupConfig = {
|
||||||
cameras: string[];
|
cameras: string[];
|
||||||
icon: (typeof GROUP_ICONS)[number];
|
icon: IconName;
|
||||||
order: number;
|
order: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { IconName } from "@/components/icons/IconPicker";
|
||||||
import { BsPersonWalking } from "react-icons/bs";
|
import { BsPersonWalking } from "react-icons/bs";
|
||||||
import {
|
import {
|
||||||
FaAmazon,
|
FaAmazon,
|
||||||
@ -6,35 +7,18 @@ import {
|
|||||||
FaCarSide,
|
FaCarSide,
|
||||||
FaCat,
|
FaCat,
|
||||||
FaCheckCircle,
|
FaCheckCircle,
|
||||||
FaCircle,
|
|
||||||
FaDog,
|
FaDog,
|
||||||
FaFedex,
|
FaFedex,
|
||||||
FaFire,
|
FaFire,
|
||||||
FaLeaf,
|
|
||||||
FaUps,
|
FaUps,
|
||||||
} from "react-icons/fa";
|
} from "react-icons/fa";
|
||||||
import { GiHummingbird } from "react-icons/gi";
|
import { GiHummingbird } from "react-icons/gi";
|
||||||
import { LuBox, LuLassoSelect } from "react-icons/lu";
|
import { LuBox, LuLassoSelect } from "react-icons/lu";
|
||||||
|
import * as LuIcons from "react-icons/lu";
|
||||||
import { MdRecordVoiceOver } from "react-icons/md";
|
import { MdRecordVoiceOver } from "react-icons/md";
|
||||||
|
|
||||||
export function getIconTypeForGroup(icon: string) {
|
export function isValidIconName(value: string): value is IconName {
|
||||||
switch (icon) {
|
return Object.keys(LuIcons).includes(value as IconName);
|
||||||
case "car":
|
|
||||||
return FaCarSide;
|
|
||||||
case "cat":
|
|
||||||
return FaCat;
|
|
||||||
case "dog":
|
|
||||||
return FaDog;
|
|
||||||
case "leaf":
|
|
||||||
return FaLeaf;
|
|
||||||
default:
|
|
||||||
return FaCircle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getIconForGroup(icon: string, className: string = "size-4") {
|
|
||||||
const GroupIcon = getIconTypeForGroup(icon);
|
|
||||||
return <GroupIcon className={className} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getIconForLabel(label: string, className?: string) {
|
export function getIconForLabel(label: string, className?: string) {
|
||||||
|
Loading…
Reference in New Issue
Block a user