base
This commit is contained in:
@@ -0,0 +1,442 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { EmojiPicker } from "./emoji-picker";
|
||||
import type { TChangeHandlerProps } from "./helper";
|
||||
import { EmojiIconPickerTypes } from "./helper";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Emoji/EmojiPicker",
|
||||
component: EmojiPicker,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
} satisfies Meta<typeof EmojiPicker>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
isOpen: false,
|
||||
handleToggle: () => {},
|
||||
onChange: () => {},
|
||||
label: "Default",
|
||||
},
|
||||
render() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedValue, setSelectedValue] = useState<TChangeHandlerProps | null>(null);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<EmojiPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={setIsOpen}
|
||||
onChange={setSelectedValue}
|
||||
label="😊 Pick an emoji or icon"
|
||||
defaultOpen={EmojiIconPickerTypes.EMOJI}
|
||||
closeOnSelect
|
||||
/>
|
||||
{selectedValue && (
|
||||
<div className="rounded-sm border border-subtle bg-layer-1 p-4 text-13">
|
||||
<div className="mb-2 font-medium">Selected:</div>
|
||||
<pre className="text-11">{JSON.stringify(selectedValue, null, 2)}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const OpenToEmojiTab: Story = {
|
||||
args: {
|
||||
isOpen: false,
|
||||
handleToggle: () => {},
|
||||
onChange: () => {},
|
||||
label: "Emoji Tab",
|
||||
},
|
||||
render() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedValue, setSelectedValue] = useState<TChangeHandlerProps | null>(null);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<EmojiPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={setIsOpen}
|
||||
onChange={setSelectedValue}
|
||||
label="😊 Choose Emoji"
|
||||
defaultOpen={EmojiIconPickerTypes.EMOJI}
|
||||
closeOnSelect
|
||||
/>
|
||||
{selectedValue && (
|
||||
<div className="text-13">Selected: {selectedValue.type === "emoji" ? selectedValue.value : "Icon"}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const OpenToIconTab: Story = {
|
||||
args: {
|
||||
isOpen: false,
|
||||
handleToggle: () => {},
|
||||
onChange: () => {},
|
||||
label: "Icon Tab",
|
||||
},
|
||||
render() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedValue, setSelectedValue] = useState<TChangeHandlerProps | null>(null);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<EmojiPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={setIsOpen}
|
||||
onChange={setSelectedValue}
|
||||
label="🎨 Choose Icon"
|
||||
defaultOpen={EmojiIconPickerTypes.ICON}
|
||||
closeOnSelect
|
||||
/>
|
||||
{selectedValue && (
|
||||
<div className="text-13">
|
||||
Selected:{" "}
|
||||
{selectedValue.type === "icon" && typeof selectedValue.value === "object"
|
||||
? selectedValue.value.name
|
||||
: "Emoji"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const LucideIcons: Story = {
|
||||
args: {
|
||||
isOpen: false,
|
||||
handleToggle: () => {},
|
||||
onChange: () => {},
|
||||
label: "Lucide Icons",
|
||||
},
|
||||
render() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedValue, setSelectedValue] = useState<TChangeHandlerProps | null>(null);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<EmojiPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={setIsOpen}
|
||||
onChange={setSelectedValue}
|
||||
label="Lucide Icons"
|
||||
defaultOpen={EmojiIconPickerTypes.ICON}
|
||||
closeOnSelect
|
||||
iconType="lucide"
|
||||
/>
|
||||
{selectedValue && (
|
||||
<div className="rounded-sm border border-subtle bg-layer-1 p-4 text-13">
|
||||
<div className="mb-2 font-medium">Selected Icon:</div>
|
||||
<pre className="text-11">{JSON.stringify(selectedValue, null, 2)}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const MaterialIcons: Story = {
|
||||
args: {
|
||||
isOpen: false,
|
||||
handleToggle: () => {},
|
||||
onChange: () => {},
|
||||
label: "Material Icons",
|
||||
},
|
||||
render() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedValue, setSelectedValue] = useState<TChangeHandlerProps | null>(null);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<EmojiPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={setIsOpen}
|
||||
onChange={setSelectedValue}
|
||||
label="Material Icons"
|
||||
defaultOpen={EmojiIconPickerTypes.ICON}
|
||||
closeOnSelect
|
||||
iconType="material"
|
||||
/>
|
||||
{selectedValue && (
|
||||
<div className="rounded-sm border border-subtle bg-layer-1 p-4 text-13">
|
||||
<div className="mb-2 font-medium">Selected Icon:</div>
|
||||
<pre className="text-11">{JSON.stringify(selectedValue, null, 2)}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const CloseOnSelectDisabled: Story = {
|
||||
args: {
|
||||
isOpen: false,
|
||||
handleToggle: () => {},
|
||||
onChange: () => {},
|
||||
label: "Close On Select Disabled",
|
||||
},
|
||||
render() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedValues, setSelectedValues] = useState<TChangeHandlerProps[]>([]);
|
||||
|
||||
const handleChange = (value: TChangeHandlerProps) => {
|
||||
setSelectedValues((prev) => [...prev, value]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<EmojiPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={setIsOpen}
|
||||
onChange={handleChange}
|
||||
label="Select Multiple (Stays Open)"
|
||||
defaultOpen={EmojiIconPickerTypes.EMOJI}
|
||||
closeOnSelect={false}
|
||||
/>
|
||||
<button
|
||||
className="rounded-sm bg-layer-1 px-3 py-1.5 text-13 hover:bg-surface-2"
|
||||
onClick={() => setSelectedValues([])}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
{selectedValues.length > 0 && (
|
||||
<div className="rounded-sm border border-subtle bg-layer-1 p-4 text-13">
|
||||
<div className="mb-2 font-medium">Selected ({selectedValues.length}):</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedValues.map((val, idx) => (
|
||||
<span key={idx} className="text-16">
|
||||
{val.type === "emoji" ? val.value : "🎨"}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomSearchPlaceholder: Story = {
|
||||
args: {
|
||||
isOpen: false,
|
||||
handleToggle: () => {},
|
||||
onChange: () => {},
|
||||
label: "Custom Search",
|
||||
},
|
||||
render() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedValue, setSelectedValue] = useState<TChangeHandlerProps | null>(null);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<EmojiPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={setIsOpen}
|
||||
onChange={setSelectedValue}
|
||||
label="Custom Search"
|
||||
defaultOpen={EmojiIconPickerTypes.EMOJI}
|
||||
closeOnSelect
|
||||
searchPlaceholder="Type to find emojis..."
|
||||
/>
|
||||
{selectedValue && <div className="text-13">Selected: {JSON.stringify(selectedValue)}</div>}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const SearchDisabled: Story = {
|
||||
args: {
|
||||
isOpen: false,
|
||||
handleToggle: () => {},
|
||||
onChange: () => {},
|
||||
label: "Search Disabled",
|
||||
},
|
||||
render() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedValue, setSelectedValue] = useState<TChangeHandlerProps | null>(null);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<EmojiPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={setIsOpen}
|
||||
onChange={setSelectedValue}
|
||||
label="No Search"
|
||||
defaultOpen={EmojiIconPickerTypes.EMOJI}
|
||||
closeOnSelect
|
||||
searchDisabled
|
||||
/>
|
||||
{selectedValue && <div className="text-13">Selected: {JSON.stringify(selectedValue)}</div>}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomIconColor: Story = {
|
||||
args: {
|
||||
isOpen: false,
|
||||
handleToggle: () => {},
|
||||
onChange: () => {},
|
||||
label: "Custom Icon Color",
|
||||
},
|
||||
render() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedValue, setSelectedValue] = useState<TChangeHandlerProps | null>(null);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<EmojiPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={setIsOpen}
|
||||
onChange={setSelectedValue}
|
||||
label="Custom Icon Color"
|
||||
defaultOpen={EmojiIconPickerTypes.ICON}
|
||||
closeOnSelect
|
||||
defaultIconColor="#FF5733"
|
||||
/>
|
||||
{selectedValue && (
|
||||
<div className="rounded-sm border border-subtle bg-layer-1 p-4 text-13">
|
||||
<pre className="text-11">{JSON.stringify(selectedValue, null, 2)}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const DifferentPlacements: Story = {
|
||||
args: {
|
||||
isOpen: false,
|
||||
handleToggle: () => {},
|
||||
onChange: () => {},
|
||||
label: "Different Placements",
|
||||
},
|
||||
render() {
|
||||
const [isOpen1, setIsOpen1] = useState(false);
|
||||
const [isOpen2, setIsOpen2] = useState(false);
|
||||
const [isOpen3, setIsOpen3] = useState(false);
|
||||
const [isOpen4, setIsOpen4] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="space-y-8 p-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="w-32 text-13">Bottom Start:</span>
|
||||
<EmojiPicker
|
||||
isOpen={isOpen1}
|
||||
handleToggle={setIsOpen1}
|
||||
onChange={() => {}}
|
||||
label="Bottom Start"
|
||||
placement="bottom-start"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="w-32 text-13">Bottom End:</span>
|
||||
<EmojiPicker
|
||||
isOpen={isOpen2}
|
||||
handleToggle={setIsOpen2}
|
||||
onChange={() => {}}
|
||||
label="Bottom End"
|
||||
placement="bottom-end"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="w-32 text-13">Top Start:</span>
|
||||
<EmojiPicker
|
||||
isOpen={isOpen3}
|
||||
handleToggle={setIsOpen3}
|
||||
onChange={() => {}}
|
||||
label="Top Start"
|
||||
placement="top-start"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="w-32 text-13">Top End:</span>
|
||||
<EmojiPicker
|
||||
isOpen={isOpen4}
|
||||
handleToggle={setIsOpen4}
|
||||
onChange={() => {}}
|
||||
label="Top End"
|
||||
placement="top-end"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const InFormContext: Story = {
|
||||
args: {
|
||||
isOpen: false,
|
||||
handleToggle: () => {},
|
||||
onChange: () => {},
|
||||
label: "In Form Context",
|
||||
},
|
||||
render() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
title: "",
|
||||
emoji: null as TChangeHandlerProps | null,
|
||||
});
|
||||
|
||||
const handleEmojiChange = (value: TChangeHandlerProps) => {
|
||||
setFormData((prev) => ({ ...prev, emoji: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
alert(`Form submitted:\n${JSON.stringify(formData, null, 2)}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-md p-4">
|
||||
<form onSubmit={handleSubmit} className="space-y-4 rounded-lg border border-subtle p-6">
|
||||
<div>
|
||||
<label className="mb-2 block text-13 font-medium">Project Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.title}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, title: e.target.value }))}
|
||||
className="w-full rounded-sm border border-subtle bg-layer-1 px-3 py-2"
|
||||
placeholder="Enter project title"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-2 block text-13 font-medium">Project Icon</label>
|
||||
<EmojiPicker
|
||||
isOpen={isOpen}
|
||||
handleToggle={setIsOpen}
|
||||
onChange={handleEmojiChange}
|
||||
label={formData.emoji && formData.emoji.type === "emoji" ? formData.emoji.value : "Click to select icon"}
|
||||
defaultOpen={EmojiIconPickerTypes.EMOJI}
|
||||
closeOnSelect
|
||||
buttonClassName="px-4 py-2 bg-layer-1 border border-subtle rounded-sm hover:bg-surface-2 w-full text-left"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full rounded-sm bg-accent-primary px-4 py-2 text-on-color hover:bg-accent-primary/80"
|
||||
>
|
||||
Create Project
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { useMemo, useCallback } from "react";
|
||||
import { Tabs } from "@base-ui-components/react";
|
||||
import { Popover } from "../popover";
|
||||
import { cn } from "../utils/classname";
|
||||
import { convertPlacementToSideAndAlign } from "../utils/placement";
|
||||
import { EmojiRoot } from "./emoji/emoji";
|
||||
import type { TCustomEmojiPicker } from "./helper";
|
||||
import { emojiToString, EmojiIconPickerTypes } from "./helper";
|
||||
import { IconRoot } from "./icon/icon-root";
|
||||
|
||||
export function EmojiPicker(props: TCustomEmojiPicker) {
|
||||
const {
|
||||
isOpen,
|
||||
handleToggle,
|
||||
buttonClassName,
|
||||
closeOnSelect = true,
|
||||
defaultIconColor = "#6d7b8a",
|
||||
defaultOpen = EmojiIconPickerTypes.EMOJI,
|
||||
disabled = false,
|
||||
dropdownClassName,
|
||||
label,
|
||||
onChange,
|
||||
placement = "bottom-start",
|
||||
searchDisabled = false,
|
||||
searchPlaceholder = "Search",
|
||||
iconType = "lucide",
|
||||
side = "bottom",
|
||||
align = "start",
|
||||
} = props;
|
||||
|
||||
// side and align calculations
|
||||
const { finalSide, finalAlign } = useMemo(() => {
|
||||
if (placement) {
|
||||
const converted = convertPlacementToSideAndAlign(placement);
|
||||
return { finalSide: converted.side, finalAlign: converted.align };
|
||||
}
|
||||
return { finalSide: side, finalAlign: align };
|
||||
}, [placement, side, align]);
|
||||
|
||||
const handleEmojiChange = useCallback(
|
||||
(value: string) => {
|
||||
onChange({
|
||||
type: EmojiIconPickerTypes.EMOJI,
|
||||
value: emojiToString(value),
|
||||
});
|
||||
if (closeOnSelect) handleToggle(false);
|
||||
},
|
||||
[onChange, closeOnSelect, handleToggle]
|
||||
);
|
||||
|
||||
const handleIconChange = useCallback(
|
||||
(value: { name: string; color: string }) => {
|
||||
onChange({
|
||||
type: EmojiIconPickerTypes.ICON,
|
||||
value: value,
|
||||
});
|
||||
if (closeOnSelect) handleToggle(false);
|
||||
},
|
||||
[onChange, closeOnSelect, handleToggle]
|
||||
);
|
||||
|
||||
const tabs = useMemo(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
key: "emoji",
|
||||
label: "Emoji",
|
||||
content: (
|
||||
<EmojiRoot
|
||||
onChange={handleEmojiChange}
|
||||
searchPlaceholder={searchPlaceholder}
|
||||
searchDisabled={searchDisabled}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "icon",
|
||||
label: "Icon",
|
||||
content: (
|
||||
<IconRoot
|
||||
defaultColor={defaultIconColor}
|
||||
onChange={handleIconChange}
|
||||
searchDisabled={searchDisabled}
|
||||
iconType={iconType}
|
||||
/>
|
||||
),
|
||||
},
|
||||
].map((tab) => ({
|
||||
key: tab.key,
|
||||
label: tab.label,
|
||||
content: tab.content,
|
||||
})),
|
||||
[defaultIconColor, searchDisabled, searchPlaceholder, iconType, handleEmojiChange, handleIconChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={handleToggle}>
|
||||
<Popover.Button className={cn("outline-none", buttonClassName)} disabled={disabled}>
|
||||
{label}
|
||||
</Popover.Button>
|
||||
<Popover.Panel
|
||||
positionerClassName="z-50"
|
||||
className={cn("w-80 overflow-hidden rounded-md border-[0.5px] border-strong bg-surface-1", dropdownClassName)}
|
||||
side={finalSide}
|
||||
align={finalAlign}
|
||||
sideOffset={8}
|
||||
data-prevent-outside-click="true"
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onFocus={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Tab") {
|
||||
return;
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
handleToggle(false);
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Tabs.Root defaultValue={defaultOpen}>
|
||||
<Tabs.List className="grid grid-cols-2 gap-1 px-3.5 pt-3">
|
||||
{tabs.map((tab) => (
|
||||
<Tabs.Tab
|
||||
key={tab.key}
|
||||
value={tab.key}
|
||||
className={({ selected }) =>
|
||||
cn("rounded-sm border border-subtle bg-layer-1 py-1 text-13", {
|
||||
"bg-surface-1 text-primary": selected,
|
||||
"text-placeholder hover:bg-layer-1/60 hover:text-tertiary": !selected,
|
||||
})
|
||||
}
|
||||
>
|
||||
{tab.label}
|
||||
</Tabs.Tab>
|
||||
))}
|
||||
</Tabs.List>
|
||||
{tabs.map((tab) => (
|
||||
<Tabs.Panel key={tab.key} value={tab.key} className="h-80 overflow-hidden overflow-y-auto">
|
||||
{tab.content}
|
||||
</Tabs.Panel>
|
||||
))}
|
||||
</Tabs.Root>
|
||||
</Popover.Panel>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { EmojiPicker } from "frimousse";
|
||||
import { cn } from "../../utils";
|
||||
|
||||
type EmojiRootProps = {
|
||||
onChange: (value: string) => void;
|
||||
searchPlaceholder?: string;
|
||||
searchDisabled?: boolean;
|
||||
};
|
||||
|
||||
export function EmojiRoot(props: EmojiRootProps) {
|
||||
const { onChange, searchPlaceholder = "Search", searchDisabled = false } = props;
|
||||
const searchWrapperRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
const focusInput = () => {
|
||||
const searchWrapper = searchWrapperRef.current;
|
||||
if (searchWrapper) {
|
||||
const inputElement = searchWrapper.querySelector("input");
|
||||
if (inputElement) {
|
||||
inputElement.removeAttribute("disabled");
|
||||
inputElement.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
focusInput();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EmojiPicker.Root
|
||||
data-slot="emoji-picker"
|
||||
className="isolate flex h-full w-full flex-col rounded-md border-none p-2"
|
||||
onEmojiSelect={(val) => onChange(val.emoji)}
|
||||
>
|
||||
<div className="sticky top-0 z-10 flex items-center justify-between gap-2 bg-surface-1 px-1.5 py-2 [&>[data-slot='emoji-picker-search-wrapper']]:flex-grow [&>[data-slot='emoji-picker-search-wrapper']]:p-0">
|
||||
<div ref={searchWrapperRef} data-slot="emoji-picker-search-wrapper" className="p-2">
|
||||
<EmojiPicker.Search
|
||||
placeholder={searchPlaceholder}
|
||||
disabled={searchDisabled}
|
||||
className="block h-full w-full flex-grow-0 rounded-md border-[0.5px] border-subtle bg-transparent p-0 px-3 py-2 text-16 placeholder-(--text-color-placeholder) focus:border-accent-strong focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<EmojiPicker.SkinToneSelector
|
||||
data-slot="emoji-picker-skin-tone-selector"
|
||||
className="hover:bg-accent mx-2 mb-1.5 size-8 flex-shrink-0 rounded-md bg-surface-1 text-16"
|
||||
/>
|
||||
</div>
|
||||
<EmojiPicker.Viewport data-slot="emoji-picker-content" className={cn("relative flex-1 outline-none")}>
|
||||
<EmojiPicker.List
|
||||
data-slot="emoji-picker-list"
|
||||
className={cn("pb-2 select-none")}
|
||||
components={{
|
||||
CategoryHeader: ({ category, ...props }) => (
|
||||
<div
|
||||
data-slot="emoji-picker-list-category-header"
|
||||
className="bg-surface-1 px-3 pb-1.5 text-11 font-medium text-tertiary"
|
||||
{...props}
|
||||
>
|
||||
{category.label}
|
||||
</div>
|
||||
),
|
||||
Row: ({ children, ...props }) => (
|
||||
<div data-slot="emoji-picker-list-row" className="scroll-my-1.5 px-1.5" {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Emoji: ({ emoji, ...props }) => (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={emoji?.label ?? emoji?.emoji}
|
||||
data-slot="emoji-picker-list-emoji"
|
||||
className="data-active:bg-accent flex size-8 items-center justify-center rounded-md text-16"
|
||||
{...props}
|
||||
>
|
||||
{emoji.emoji}
|
||||
</button>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EmojiPicker.Viewport>
|
||||
</EmojiPicker.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
export * from "./emoji";
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { TPlacement, TSide, TAlign } from "../utils/placement";
|
||||
|
||||
export const EmojiIconPickerTypes = {
|
||||
EMOJI: "emoji",
|
||||
ICON: "icon",
|
||||
} as const;
|
||||
|
||||
export type TChangeHandlerProps =
|
||||
| {
|
||||
type: typeof EmojiIconPickerTypes.EMOJI;
|
||||
value: string;
|
||||
}
|
||||
| {
|
||||
type: typeof EmojiIconPickerTypes.ICON;
|
||||
value: {
|
||||
name: string;
|
||||
color: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TEmojiIconPickerTypes = typeof EmojiIconPickerTypes.EMOJI | typeof EmojiIconPickerTypes.ICON;
|
||||
|
||||
export type TCustomEmojiPicker = {
|
||||
isOpen: boolean;
|
||||
handleToggle: (value: boolean) => void;
|
||||
buttonClassName?: string;
|
||||
className?: string;
|
||||
closeOnSelect?: boolean;
|
||||
defaultIconColor?: string;
|
||||
defaultOpen?: TEmojiIconPickerTypes;
|
||||
disabled?: boolean;
|
||||
dropdownClassName?: string;
|
||||
label: React.ReactNode;
|
||||
onChange: (value: TChangeHandlerProps) => void;
|
||||
placement?: TPlacement;
|
||||
searchDisabled?: boolean;
|
||||
searchPlaceholder?: string;
|
||||
iconType?: "material" | "lucide";
|
||||
theme?: "light" | "dark";
|
||||
side?: TSide;
|
||||
align?: TAlign;
|
||||
};
|
||||
|
||||
export type TIconsListProps = {
|
||||
defaultColor: string;
|
||||
onChange: (val: { name: string; color: string }) => void;
|
||||
searchDisabled?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adjusts the given hex color to ensure it has enough contrast.
|
||||
* @param {string} hex - The hex color code input by the user.
|
||||
* @returns {string} - The adjusted hex color code.
|
||||
*/
|
||||
export const adjustColorForContrast = (hex: string): string => {
|
||||
// Ensure hex color is valid
|
||||
if (!/^#([0-9A-F]{3}){1,2}$/i.test(hex)) {
|
||||
throw new Error("Invalid hex color code");
|
||||
}
|
||||
|
||||
// Convert hex to RGB
|
||||
let r = 0,
|
||||
g = 0,
|
||||
b = 0;
|
||||
if (hex.length === 4) {
|
||||
r = parseInt(hex[1] + hex[1], 16);
|
||||
g = parseInt(hex[2] + hex[2], 16);
|
||||
b = parseInt(hex[3] + hex[3], 16);
|
||||
} else if (hex.length === 7) {
|
||||
r = parseInt(hex[1] + hex[2], 16);
|
||||
g = parseInt(hex[3] + hex[4], 16);
|
||||
b = parseInt(hex[5] + hex[6], 16);
|
||||
}
|
||||
|
||||
// Calculate luminance
|
||||
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||
|
||||
// If the color is too light, darken it
|
||||
if (luminance > 0.5) {
|
||||
r = Math.max(0, r - 50);
|
||||
g = Math.max(0, g - 50);
|
||||
b = Math.max(0, b - 50);
|
||||
}
|
||||
|
||||
// Convert RGB back to hex
|
||||
const toHex = (value: number): string => {
|
||||
const hex = value.toString(16);
|
||||
return hex.length === 1 ? "0" + hex : hex;
|
||||
};
|
||||
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
||||
};
|
||||
|
||||
export const DEFAULT_COLORS = ["#95999f", "#6d7b8a", "#5e6ad2", "#02b5ed", "#02b55c", "#f2be02", "#e57a00", "#f38e82"];
|
||||
|
||||
/**
|
||||
* Enhanced emoji to decimal conversion that preserves emoji sequences
|
||||
* This function handles complex emoji sequences including skin tone modifiers
|
||||
* @param emoji - The emoji string to convert
|
||||
* @returns Array of decimal Unicode code points
|
||||
*/
|
||||
export function emojiToDecimalEnhanced(emoji: string): number[] {
|
||||
const codePoints: number[] = [];
|
||||
|
||||
// Use Array.from to properly handle multi-byte Unicode characters
|
||||
const characters = Array.from(emoji);
|
||||
|
||||
for (const char of characters) {
|
||||
const codePoint = char.codePointAt(0);
|
||||
if (codePoint !== undefined) {
|
||||
codePoints.push(codePoint);
|
||||
}
|
||||
}
|
||||
|
||||
return codePoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced decimal to emoji conversion that handles emoji sequences
|
||||
* @param decimals - Array of decimal Unicode code points
|
||||
* @returns The reconstructed emoji string
|
||||
*/
|
||||
export function decimalToEmojiEnhanced(decimals: number[]): string {
|
||||
return decimals.map((decimal) => String.fromCodePoint(decimal)).join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts emoji to a string representation for storage
|
||||
* This creates a comma-separated string of decimal values
|
||||
* @param emoji - The emoji string to convert
|
||||
* @returns String representation of decimal values
|
||||
*/
|
||||
export function emojiToString(emoji: string): string {
|
||||
const decimals = emojiToDecimalEnhanced(emoji);
|
||||
return decimals.join("-");
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts string representation back to emoji
|
||||
* @param emojiString - Comma-separated string of decimal values
|
||||
* @returns The reconstructed emoji string
|
||||
*/
|
||||
export function stringToEmoji(emojiString: string): string {
|
||||
if (!emojiString) return "";
|
||||
const decimals = emojiString
|
||||
.split("-")
|
||||
.map((s) => Number(s.trim()))
|
||||
.filter((n) => Number.isFinite(n) && n >= 0 && n <= 0x10ffff);
|
||||
try {
|
||||
return decimalToEmojiEnhanced(decimals);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export const getEmojiSize = (size: number) => size * 0.9 * 0.0625;
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { SearchIcon } from "../../icons";
|
||||
import { cn } from "../../utils/classname";
|
||||
import { adjustColorForContrast, DEFAULT_COLORS } from "../helper";
|
||||
import { LucideIconsList } from "./lucide-root";
|
||||
import { MaterialIconList } from "./material-root";
|
||||
|
||||
type IconRootProps = {
|
||||
onChange: (value: { name: string; color: string }) => void;
|
||||
defaultColor: string;
|
||||
searchDisabled?: boolean;
|
||||
iconType: "material" | "lucide";
|
||||
};
|
||||
|
||||
export function IconRoot(props: IconRootProps) {
|
||||
const { defaultColor, onChange, searchDisabled = false, iconType } = props;
|
||||
// states
|
||||
const [activeColor, setActiveColor] = useState(defaultColor);
|
||||
const [showHexInput, setShowHexInput] = useState(false);
|
||||
const [hexValue, setHexValue] = useState("");
|
||||
const [isInputFocused, setIsInputFocused] = useState(false);
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (DEFAULT_COLORS.includes(defaultColor.toLowerCase() ?? "")) setShowHexInput(false);
|
||||
else {
|
||||
setHexValue(defaultColor?.slice(1, 7) ?? "");
|
||||
setShowHexInput(true);
|
||||
}
|
||||
}, [defaultColor]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="sticky top-0 flex flex-col bg-surface-1">
|
||||
{!searchDisabled && (
|
||||
<div className="flex w-full items-center px-2 py-[15px]">
|
||||
<div
|
||||
className={cn("relative flex h-10 w-full items-center gap-2 rounded-lg border bg-surface-2 px-[30px]", {
|
||||
"border-accent-strong": isInputFocused,
|
||||
"border-transparent": !isInputFocused,
|
||||
})}
|
||||
onFocus={() => setIsInputFocused(true)}
|
||||
onBlur={() => setIsInputFocused(false)}
|
||||
>
|
||||
<SearchIcon className="absolute bottom-3 left-2.5 h-3.5 w-3.5 text-placeholder" />
|
||||
|
||||
<input
|
||||
placeholder="Search"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className="block h-full w-full rounded-md border-[0.5px] border-none border-subtle bg-transparent p-0 px-3 py-2 text-16 placeholder-(--text-color-placeholder) focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid h-9 grid-cols-9 items-center justify-items-center gap-2 px-2.5 py-1">
|
||||
{showHexInput ? (
|
||||
<div className="col-span-8 ml-2 flex items-center gap-1 justify-self-stretch">
|
||||
<span
|
||||
className="mr-1 h-4 w-4 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: `#${hexValue}`,
|
||||
}}
|
||||
/>
|
||||
<span className="flex-shrink-0 text-11 text-tertiary">HEX</span>
|
||||
<span className="-mr-1 flex-shrink-0 text-11 text-secondary">#</span>
|
||||
<input
|
||||
type="text"
|
||||
value={hexValue}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setHexValue(value);
|
||||
if (/^[0-9A-Fa-f]{6}$/.test(value)) setActiveColor(adjustColorForContrast(`#${value}`));
|
||||
}}
|
||||
className="block flex-grow rounded-sm border-[0.5px] border-none border-subtle bg-transparent px-3 py-2 pl-0 text-11 text-secondary placeholder-(--text-color-placeholder) ring-0 focus:outline-none"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
DEFAULT_COLORS.map((curCol) => (
|
||||
<button
|
||||
key={curCol}
|
||||
type="button"
|
||||
className="grid size-5 place-items-center"
|
||||
onClick={() => {
|
||||
setActiveColor(curCol);
|
||||
setHexValue(curCol.slice(1, 7));
|
||||
}}
|
||||
>
|
||||
<span className="h-4 w-4 cursor-pointer rounded-full" style={{ backgroundColor: curCol }} />
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={cn("grid h-4 w-4 place-items-center rounded-full border border-transparent", {
|
||||
"border-strong-1": !showHexInput,
|
||||
})}
|
||||
onClick={() => {
|
||||
setShowHexInput((prevData) => !prevData);
|
||||
setHexValue(activeColor.slice(1, 7));
|
||||
}}
|
||||
>
|
||||
{showHexInput ? (
|
||||
<span className="h-4 w-4 rounded-full conical-gradient" />
|
||||
) : (
|
||||
<span className="grid place-items-center text-10 text-tertiary">#</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex h-6 w-full items-center gap-2 py-1 pr-3 pl-4">
|
||||
<InfoIcon className="h-3 w-3" />
|
||||
<p className="text-11"> Colors will be adjusted to ensure sufficient contrast.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 grid grid-cols-8 justify-items-center gap-1 px-2.5">
|
||||
{iconType === "material" ? (
|
||||
<MaterialIconList query={query} onChange={onChange} activeColor={activeColor} />
|
||||
) : (
|
||||
<LucideIconsList query={query} onChange={onChange} activeColor={activeColor} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
export * from "./icon-root";
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { LUCIDE_ICONS_LIST } from "../lucide-icons";
|
||||
|
||||
type LucideIconsListProps = {
|
||||
onChange: (value: { name: string; color: string }) => void;
|
||||
activeColor: string;
|
||||
query: string;
|
||||
};
|
||||
|
||||
export function LucideIconsList(props: LucideIconsListProps) {
|
||||
const { query, onChange, activeColor } = props;
|
||||
|
||||
const filteredArray = LUCIDE_ICONS_LIST.filter((icon) => icon.name.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
return (
|
||||
<>
|
||||
{filteredArray.map((icon) => (
|
||||
<button
|
||||
key={icon.name}
|
||||
type="button"
|
||||
className="grid h-9 w-9 place-items-center rounded-sm text-16 select-none hover:bg-layer-1"
|
||||
onClick={() => {
|
||||
onChange({
|
||||
name: icon.name,
|
||||
color: activeColor,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<icon.element style={{ color: activeColor }} className="size-4" />
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import useFontFaceObserver from "use-font-face-observer";
|
||||
import { MATERIAL_ICONS_LIST } from "../material-icons";
|
||||
|
||||
type MaterialIconListProps = {
|
||||
onChange: (value: { name: string; color: string }) => void;
|
||||
activeColor: string;
|
||||
query: string;
|
||||
};
|
||||
|
||||
export function MaterialIconList(props: MaterialIconListProps) {
|
||||
const { query, onChange, activeColor } = props;
|
||||
|
||||
const filteredArray = MATERIAL_ICONS_LIST.filter((icon) => icon.name.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
const isMaterialSymbolsFontLoaded = useFontFaceObserver([
|
||||
{
|
||||
family: `Material Symbols Rounded`,
|
||||
style: `normal`,
|
||||
weight: `normal`,
|
||||
stretch: `condensed`,
|
||||
},
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{filteredArray.map((icon) => (
|
||||
<button
|
||||
key={icon.name}
|
||||
type="button"
|
||||
className="grid h-9 w-9 place-items-center rounded-sm text-16 select-none hover:bg-layer-1"
|
||||
onClick={() => {
|
||||
onChange({
|
||||
name: icon.name,
|
||||
color: activeColor,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{isMaterialSymbolsFontLoaded ? (
|
||||
<span style={{ color: activeColor }} className="material-symbols-rounded text-20! leading-5!">
|
||||
{icon.name}
|
||||
</span>
|
||||
) : (
|
||||
<span className="size-5 animate-pulse rounded-sm bg-layer-1" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
export * from "./emoji-picker";
|
||||
export * from "./helper";
|
||||
export * from "./logo";
|
||||
export * from "./lucide-icons";
|
||||
export * from "./material-icons";
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
// Due to some weird issue with the import order, the import of useFontFaceObserver
|
||||
// should be after the imported here rather than some below helper functions as it is in the original file
|
||||
|
||||
import useFontFaceObserver from "use-font-face-observer";
|
||||
// plane imports
|
||||
import type { TLogoProps } from "@plane/types";
|
||||
// local imports
|
||||
import { getEmojiSize, stringToEmoji } from "./helper";
|
||||
import { LUCIDE_ICONS_LIST } from "./lucide-icons";
|
||||
|
||||
type Props = {
|
||||
logo?: TLogoProps;
|
||||
size?: number;
|
||||
type?: "lucide" | "material";
|
||||
};
|
||||
|
||||
export function Logo({ logo, size = 16, type = "material" }: Props) {
|
||||
const isMaterialSymbolsFontLoaded = useFontFaceObserver([
|
||||
{
|
||||
family: "Material Symbols Rounded",
|
||||
style: "normal",
|
||||
weight: "normal",
|
||||
stretch: "condensed",
|
||||
},
|
||||
]);
|
||||
|
||||
// Reusable loading skeleton
|
||||
const loadingSkeleton = <span style={{ height: size, width: size }} className="rounded-sm bg-layer-1" />;
|
||||
|
||||
// Early returns for loading/empty states
|
||||
if (!logo || !logo.in_use) return loadingSkeleton;
|
||||
|
||||
const { in_use, emoji, icon } = logo;
|
||||
const value = in_use === "emoji" ? emoji?.value : icon?.name;
|
||||
|
||||
if (!value) return loadingSkeleton;
|
||||
|
||||
// Emoji rendering
|
||||
if (in_use === "emoji") {
|
||||
return (
|
||||
<span
|
||||
className="flex items-center justify-center"
|
||||
style={{
|
||||
fontSize: `${getEmojiSize(size)}rem`,
|
||||
lineHeight: `${getEmojiSize(size)}rem`,
|
||||
height: size,
|
||||
width: size,
|
||||
}}
|
||||
>
|
||||
{stringToEmoji(emoji?.value || "")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// Icon rendering
|
||||
if (in_use === "icon") {
|
||||
const color = icon?.color;
|
||||
|
||||
// Lucide icon
|
||||
if (type === "lucide") {
|
||||
const lucideIcon = LUCIDE_ICONS_LIST.find((item) => item.name === value);
|
||||
if (!lucideIcon) return null;
|
||||
|
||||
const LucideIconElement = lucideIcon.element;
|
||||
return <LucideIconElement style={{ color, height: size, width: size }} />;
|
||||
}
|
||||
|
||||
if (!isMaterialSymbolsFontLoaded) return loadingSkeleton;
|
||||
|
||||
// Material icon
|
||||
return (
|
||||
<span
|
||||
className="material-symbols-rounded"
|
||||
style={{
|
||||
fontSize: size,
|
||||
color,
|
||||
scale: "115%",
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import {
|
||||
Activity,
|
||||
Airplay,
|
||||
AlertCircle,
|
||||
AlertOctagon,
|
||||
AlertTriangle,
|
||||
AlignCenter,
|
||||
AlignJustify,
|
||||
AlignLeft,
|
||||
AlignRight,
|
||||
Anchor,
|
||||
Aperture,
|
||||
Archive,
|
||||
ArrowDown,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
ArrowUp,
|
||||
AtSign,
|
||||
Award,
|
||||
BarChart,
|
||||
BarChart2,
|
||||
Battery,
|
||||
BatteryCharging,
|
||||
Bell,
|
||||
BellOff,
|
||||
Book,
|
||||
Bookmark,
|
||||
BookOpen,
|
||||
Box,
|
||||
Briefcase,
|
||||
Calendar,
|
||||
Camera,
|
||||
CameraOff,
|
||||
Cast,
|
||||
Check,
|
||||
CheckCircle,
|
||||
CheckSquare,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ChevronUp,
|
||||
Clipboard,
|
||||
Clock,
|
||||
Cloud,
|
||||
CloudDrizzle,
|
||||
CloudLightning,
|
||||
CloudOff,
|
||||
CloudRain,
|
||||
CloudSnow,
|
||||
Code,
|
||||
Codepen,
|
||||
Codesandbox,
|
||||
Coffee,
|
||||
Columns,
|
||||
Command,
|
||||
Compass,
|
||||
Copy,
|
||||
CornerDownLeft,
|
||||
CornerDownRight,
|
||||
CornerLeftDown,
|
||||
CornerLeftUp,
|
||||
CornerRightDown,
|
||||
CornerRightUp,
|
||||
CornerUpLeft,
|
||||
CornerUpRight,
|
||||
Cpu,
|
||||
CreditCard,
|
||||
Crop,
|
||||
Crosshair,
|
||||
Database,
|
||||
Delete,
|
||||
Disc,
|
||||
Divide,
|
||||
DivideCircle,
|
||||
DivideSquare,
|
||||
DollarSign,
|
||||
Download,
|
||||
DownloadCloud,
|
||||
Dribbble,
|
||||
Droplet,
|
||||
Edit,
|
||||
Edit2,
|
||||
Edit3,
|
||||
ExternalLink,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Facebook,
|
||||
FastForward,
|
||||
Feather,
|
||||
Figma,
|
||||
File,
|
||||
FileMinus,
|
||||
FilePlus,
|
||||
FileText,
|
||||
Film,
|
||||
Filter,
|
||||
Flag,
|
||||
Folder,
|
||||
FolderMinus,
|
||||
FolderPlus,
|
||||
Framer,
|
||||
Frown,
|
||||
Gift,
|
||||
GitBranch,
|
||||
GitCommit,
|
||||
GitMerge,
|
||||
GitPullRequest,
|
||||
Github,
|
||||
Gitlab,
|
||||
Globe,
|
||||
Grid,
|
||||
HardDrive,
|
||||
Hash,
|
||||
Headphones,
|
||||
Heart,
|
||||
HelpCircle,
|
||||
Hexagon,
|
||||
Home,
|
||||
Image,
|
||||
Inbox,
|
||||
Info,
|
||||
Instagram,
|
||||
Italic,
|
||||
Key,
|
||||
Layers,
|
||||
Layout,
|
||||
LifeBuoy,
|
||||
Link,
|
||||
Link2,
|
||||
Linkedin,
|
||||
List,
|
||||
Loader,
|
||||
Lock,
|
||||
LogIn,
|
||||
LogOut,
|
||||
Mail,
|
||||
Map as MapIcon,
|
||||
MapPin,
|
||||
Maximize,
|
||||
Maximize2,
|
||||
Meh,
|
||||
Menu,
|
||||
MessageCircle,
|
||||
MessageSquare,
|
||||
Mic,
|
||||
MicOff,
|
||||
Minimize,
|
||||
Minimize2,
|
||||
Minus,
|
||||
MinusCircle,
|
||||
MinusSquare,
|
||||
CircleChevronDown,
|
||||
UsersRound,
|
||||
ToggleLeft,
|
||||
Search,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
|
||||
export const LUCIDE_ICONS_LIST = [
|
||||
{ name: "Activity", element: Activity },
|
||||
{ name: "Airplay", element: Airplay },
|
||||
{ name: "AlertCircle", element: AlertCircle },
|
||||
{ name: "AlertOctagon", element: AlertOctagon },
|
||||
{ name: "AlertTriangle", element: AlertTriangle },
|
||||
{ name: "AlignCenter", element: AlignCenter },
|
||||
{ name: "AlignJustify", element: AlignJustify },
|
||||
{ name: "AlignLeft", element: AlignLeft },
|
||||
{ name: "AlignRight", element: AlignRight },
|
||||
{ name: "Anchor", element: Anchor },
|
||||
{ name: "Aperture", element: Aperture },
|
||||
{ name: "Archive", element: Archive },
|
||||
{ name: "ArrowDown", element: ArrowDown },
|
||||
{ name: "ArrowLeft", element: ArrowLeft },
|
||||
{ name: "ArrowRight", element: ArrowRight },
|
||||
{ name: "ArrowUp", element: ArrowUp },
|
||||
{ name: "AtSign", element: AtSign },
|
||||
{ name: "Award", element: Award },
|
||||
{ name: "BarChart", element: BarChart },
|
||||
{ name: "BarChart2", element: BarChart2 },
|
||||
{ name: "Battery", element: Battery },
|
||||
{ name: "BatteryCharging", element: BatteryCharging },
|
||||
{ name: "Bell", element: Bell },
|
||||
{ name: "BellOff", element: BellOff },
|
||||
{ name: "Book", element: Book },
|
||||
{ name: "Bookmark", element: Bookmark },
|
||||
{ name: "BookOpen", element: BookOpen },
|
||||
{ name: "Box", element: Box },
|
||||
{ name: "Briefcase", element: Briefcase },
|
||||
{ name: "Calendar", element: Calendar },
|
||||
{ name: "Camera", element: Camera },
|
||||
{ name: "CameraOff", element: CameraOff },
|
||||
{ name: "Cast", element: Cast },
|
||||
{ name: "CircleChevronDown", element: CircleChevronDown },
|
||||
{ name: "Check", element: Check },
|
||||
{ name: "CheckCircle", element: CheckCircle },
|
||||
{ name: "CheckSquare", element: CheckSquare },
|
||||
{ name: "ChevronDown", element: ChevronDown },
|
||||
{ name: "ChevronLeft", element: ChevronLeft },
|
||||
{ name: "ChevronRight", element: ChevronRight },
|
||||
{ name: "ChevronUp", element: ChevronUp },
|
||||
{ name: "Clipboard", element: Clipboard },
|
||||
{ name: "Clock", element: Clock },
|
||||
{ name: "Cloud", element: Cloud },
|
||||
{ name: "CloudDrizzle", element: CloudDrizzle },
|
||||
{ name: "CloudLightning", element: CloudLightning },
|
||||
{ name: "CloudOff", element: CloudOff },
|
||||
{ name: "CloudRain", element: CloudRain },
|
||||
{ name: "CloudSnow", element: CloudSnow },
|
||||
{ name: "Code", element: Code },
|
||||
{ name: "Codepen", element: Codepen },
|
||||
{ name: "Codesandbox", element: Codesandbox },
|
||||
{ name: "Coffee", element: Coffee },
|
||||
{ name: "Columns", element: Columns },
|
||||
{ name: "Command", element: Command },
|
||||
{ name: "Compass", element: Compass },
|
||||
{ name: "Copy", element: Copy },
|
||||
{ name: "CornerDownLeft", element: CornerDownLeft },
|
||||
{ name: "CornerDownRight", element: CornerDownRight },
|
||||
{ name: "CornerLeftDown", element: CornerLeftDown },
|
||||
{ name: "CornerLeftUp", element: CornerLeftUp },
|
||||
{ name: "CornerRightDown", element: CornerRightDown },
|
||||
{ name: "CornerRightUp", element: CornerRightUp },
|
||||
{ name: "CornerUpLeft", element: CornerUpLeft },
|
||||
{ name: "CornerUpRight", element: CornerUpRight },
|
||||
{ name: "Cpu", element: Cpu },
|
||||
{ name: "CreditCard", element: CreditCard },
|
||||
{ name: "Crop", element: Crop },
|
||||
{ name: "Crosshair", element: Crosshair },
|
||||
{ name: "Database", element: Database },
|
||||
{ name: "Delete", element: Delete },
|
||||
{ name: "Disc", element: Disc },
|
||||
{ name: "Divide", element: Divide },
|
||||
{ name: "DivideCircle", element: DivideCircle },
|
||||
{ name: "DivideSquare", element: DivideSquare },
|
||||
{ name: "DollarSign", element: DollarSign },
|
||||
{ name: "Download", element: Download },
|
||||
{ name: "DownloadCloud", element: DownloadCloud },
|
||||
{ name: "Dribbble", element: Dribbble },
|
||||
{ name: "Droplet", element: Droplet },
|
||||
{ name: "Edit", element: Edit },
|
||||
{ name: "Edit2", element: Edit2 },
|
||||
{ name: "Edit3", element: Edit3 },
|
||||
{ name: "ExternalLink", element: ExternalLink },
|
||||
{ name: "Eye", element: Eye },
|
||||
{ name: "EyeOff", element: EyeOff },
|
||||
{ name: "Facebook", element: Facebook },
|
||||
{ name: "FastForward", element: FastForward },
|
||||
{ name: "Feather", element: Feather },
|
||||
{ name: "Figma", element: Figma },
|
||||
{ name: "File", element: File },
|
||||
{ name: "FileMinus", element: FileMinus },
|
||||
{ name: "FilePlus", element: FilePlus },
|
||||
{ name: "FileText", element: FileText },
|
||||
{ name: "Film", element: Film },
|
||||
{ name: "Filter", element: Filter },
|
||||
{ name: "Flag", element: Flag },
|
||||
{ name: "Folder", element: Folder },
|
||||
{ name: "FolderMinus", element: FolderMinus },
|
||||
{ name: "FolderPlus", element: FolderPlus },
|
||||
{ name: "Framer", element: Framer },
|
||||
{ name: "Frown", element: Frown },
|
||||
{ name: "Gift", element: Gift },
|
||||
{ name: "GitBranch", element: GitBranch },
|
||||
{ name: "GitCommit", element: GitCommit },
|
||||
{ name: "GitMerge", element: GitMerge },
|
||||
{ name: "GitPullRequest", element: GitPullRequest },
|
||||
{ name: "Github", element: Github },
|
||||
{ name: "Gitlab", element: Gitlab },
|
||||
{ name: "Globe", element: Globe },
|
||||
{ name: "Grid", element: Grid },
|
||||
{ name: "HardDrive", element: HardDrive },
|
||||
{ name: "Hash", element: Hash },
|
||||
{ name: "Headphones", element: Headphones },
|
||||
{ name: "Heart", element: Heart },
|
||||
{ name: "HelpCircle", element: HelpCircle },
|
||||
{ name: "Hexagon", element: Hexagon },
|
||||
{ name: "Home", element: Home },
|
||||
{ name: "Image", element: Image },
|
||||
{ name: "Inbox", element: Inbox },
|
||||
{ name: "Info", element: Info },
|
||||
{ name: "Instagram", element: Instagram },
|
||||
{ name: "Italic", element: Italic },
|
||||
{ name: "Key", element: Key },
|
||||
{ name: "Layers", element: Layers },
|
||||
{ name: "Layout", element: Layout },
|
||||
{ name: "LifeBuoy", element: LifeBuoy },
|
||||
{ name: "Link", element: Link },
|
||||
{ name: "Link2", element: Link2 },
|
||||
{ name: "Linkedin", element: Linkedin },
|
||||
{ name: "List", element: List },
|
||||
{ name: "Loader", element: Loader },
|
||||
{ name: "Lock", element: Lock },
|
||||
{ name: "LogIn", element: LogIn },
|
||||
{ name: "LogOut", element: LogOut },
|
||||
{ name: "Mail", element: Mail },
|
||||
{ name: "Map", element: MapIcon },
|
||||
{ name: "MapPin", element: MapPin },
|
||||
{ name: "Maximize", element: Maximize },
|
||||
{ name: "Maximize2", element: Maximize2 },
|
||||
{ name: "Meh", element: Meh },
|
||||
{ name: "Menu", element: Menu },
|
||||
{ name: "MessageCircle", element: MessageCircle },
|
||||
{ name: "MessageSquare", element: MessageSquare },
|
||||
{ name: "Mic", element: Mic },
|
||||
{ name: "MicOff", element: MicOff },
|
||||
{ name: "Minimize", element: Minimize },
|
||||
{ name: "Minimize2", element: Minimize2 },
|
||||
{ name: "Minus", element: Minus },
|
||||
{ name: "MinusCircle", element: MinusCircle },
|
||||
{ name: "MinusSquare", element: MinusSquare },
|
||||
{ name: "Search", element: Search },
|
||||
{ name: "ToggleLeft", element: ToggleLeft },
|
||||
{ name: "User", element: User },
|
||||
{ name: "UsersRound", element: UsersRound },
|
||||
];
|
||||
@@ -0,0 +1,608 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
export const MATERIAL_ICONS_LIST = [
|
||||
{
|
||||
name: "search",
|
||||
},
|
||||
{
|
||||
name: "home",
|
||||
},
|
||||
{
|
||||
name: "menu",
|
||||
},
|
||||
{
|
||||
name: "close",
|
||||
},
|
||||
{
|
||||
name: "settings",
|
||||
},
|
||||
{
|
||||
name: "done",
|
||||
},
|
||||
{
|
||||
name: "check_circle",
|
||||
},
|
||||
{
|
||||
name: "favorite",
|
||||
},
|
||||
{
|
||||
name: "add",
|
||||
},
|
||||
{
|
||||
name: "delete",
|
||||
},
|
||||
{
|
||||
name: "arrow_back",
|
||||
},
|
||||
{
|
||||
name: "star",
|
||||
},
|
||||
{
|
||||
name: "logout",
|
||||
},
|
||||
{
|
||||
name: "add_circle",
|
||||
},
|
||||
{
|
||||
name: "cancel",
|
||||
},
|
||||
{
|
||||
name: "arrow_drop_down",
|
||||
},
|
||||
{
|
||||
name: "more_vert",
|
||||
},
|
||||
{
|
||||
name: "check",
|
||||
},
|
||||
{
|
||||
name: "check_box",
|
||||
},
|
||||
{
|
||||
name: "toggle_on",
|
||||
},
|
||||
{
|
||||
name: "open_in_new",
|
||||
},
|
||||
{
|
||||
name: "refresh",
|
||||
},
|
||||
{
|
||||
name: "login",
|
||||
},
|
||||
{
|
||||
name: "radio_button_unchecked",
|
||||
},
|
||||
{
|
||||
name: "more_horiz",
|
||||
},
|
||||
{
|
||||
name: "apps",
|
||||
},
|
||||
{
|
||||
name: "radio_button_checked",
|
||||
},
|
||||
{
|
||||
name: "download",
|
||||
},
|
||||
{
|
||||
name: "remove",
|
||||
},
|
||||
{
|
||||
name: "toggle_off",
|
||||
},
|
||||
{
|
||||
name: "bolt",
|
||||
},
|
||||
{
|
||||
name: "arrow_upward",
|
||||
},
|
||||
{
|
||||
name: "filter_list",
|
||||
},
|
||||
{
|
||||
name: "delete_forever",
|
||||
},
|
||||
{
|
||||
name: "autorenew",
|
||||
},
|
||||
{
|
||||
name: "key",
|
||||
},
|
||||
{
|
||||
name: "sort",
|
||||
},
|
||||
{
|
||||
name: "sync",
|
||||
},
|
||||
{
|
||||
name: "add_box",
|
||||
},
|
||||
{
|
||||
name: "block",
|
||||
},
|
||||
{
|
||||
name: "restart_alt",
|
||||
},
|
||||
{
|
||||
name: "menu_open",
|
||||
},
|
||||
{
|
||||
name: "shopping_cart_checkout",
|
||||
},
|
||||
{
|
||||
name: "expand_circle_down",
|
||||
},
|
||||
{
|
||||
name: "backspace",
|
||||
},
|
||||
{
|
||||
name: "undo",
|
||||
},
|
||||
{
|
||||
name: "done_all",
|
||||
},
|
||||
{
|
||||
name: "do_not_disturb_on",
|
||||
},
|
||||
{
|
||||
name: "open_in_full",
|
||||
},
|
||||
{
|
||||
name: "double_arrow",
|
||||
},
|
||||
{
|
||||
name: "sync_alt",
|
||||
},
|
||||
{
|
||||
name: "zoom_in",
|
||||
},
|
||||
{
|
||||
name: "done_outline",
|
||||
},
|
||||
{
|
||||
name: "drag_indicator",
|
||||
},
|
||||
{
|
||||
name: "fullscreen",
|
||||
},
|
||||
{
|
||||
name: "star_half",
|
||||
},
|
||||
{
|
||||
name: "settings_accessibility",
|
||||
},
|
||||
{
|
||||
name: "reply",
|
||||
},
|
||||
{
|
||||
name: "exit_to_app",
|
||||
},
|
||||
{
|
||||
name: "unfold_more",
|
||||
},
|
||||
{
|
||||
name: "library_add",
|
||||
},
|
||||
{
|
||||
name: "cached",
|
||||
},
|
||||
{
|
||||
name: "select_check_box",
|
||||
},
|
||||
{
|
||||
name: "terminal",
|
||||
},
|
||||
{
|
||||
name: "change_circle",
|
||||
},
|
||||
{
|
||||
name: "disabled_by_default",
|
||||
},
|
||||
{
|
||||
name: "swap_horiz",
|
||||
},
|
||||
{
|
||||
name: "swap_vert",
|
||||
},
|
||||
{
|
||||
name: "app_registration",
|
||||
},
|
||||
{
|
||||
name: "download_for_offline",
|
||||
},
|
||||
{
|
||||
name: "close_fullscreen",
|
||||
},
|
||||
{
|
||||
name: "file_open",
|
||||
},
|
||||
{
|
||||
name: "minimize",
|
||||
},
|
||||
{
|
||||
name: "open_with",
|
||||
},
|
||||
{
|
||||
name: "dataset",
|
||||
},
|
||||
{
|
||||
name: "add_task",
|
||||
},
|
||||
{
|
||||
name: "start",
|
||||
},
|
||||
{
|
||||
name: "keyboard_voice",
|
||||
},
|
||||
{
|
||||
name: "create_new_folder",
|
||||
},
|
||||
{
|
||||
name: "forward",
|
||||
},
|
||||
{
|
||||
name: "settings_applications",
|
||||
},
|
||||
{
|
||||
name: "compare_arrows",
|
||||
},
|
||||
{
|
||||
name: "redo",
|
||||
},
|
||||
{
|
||||
name: "zoom_out",
|
||||
},
|
||||
{
|
||||
name: "publish",
|
||||
},
|
||||
{
|
||||
name: "html",
|
||||
},
|
||||
{
|
||||
name: "token",
|
||||
},
|
||||
{
|
||||
name: "switch_access_shortcut",
|
||||
},
|
||||
{
|
||||
name: "fullscreen_exit",
|
||||
},
|
||||
{
|
||||
name: "sort_by_alpha",
|
||||
},
|
||||
{
|
||||
name: "delete_sweep",
|
||||
},
|
||||
{
|
||||
name: "indeterminate_check_box",
|
||||
},
|
||||
{
|
||||
name: "view_timeline",
|
||||
},
|
||||
{
|
||||
name: "settings_backup_restore",
|
||||
},
|
||||
{
|
||||
name: "arrow_drop_down_circle",
|
||||
},
|
||||
{
|
||||
name: "assistant_navigation",
|
||||
},
|
||||
{
|
||||
name: "sync_problem",
|
||||
},
|
||||
{
|
||||
name: "clear_all",
|
||||
},
|
||||
{
|
||||
name: "density_medium",
|
||||
},
|
||||
{
|
||||
name: "heart_plus",
|
||||
},
|
||||
{
|
||||
name: "filter_alt_off",
|
||||
},
|
||||
{
|
||||
name: "expand",
|
||||
},
|
||||
{
|
||||
name: "subdirectory_arrow_right",
|
||||
},
|
||||
{
|
||||
name: "download_done",
|
||||
},
|
||||
{
|
||||
name: "arrow_outward",
|
||||
},
|
||||
{
|
||||
name: "123",
|
||||
},
|
||||
{
|
||||
name: "swipe_left",
|
||||
},
|
||||
{
|
||||
name: "auto_mode",
|
||||
},
|
||||
{
|
||||
name: "saved_search",
|
||||
},
|
||||
{
|
||||
name: "place_item",
|
||||
},
|
||||
{
|
||||
name: "system_update_alt",
|
||||
},
|
||||
{
|
||||
name: "javascript",
|
||||
},
|
||||
{
|
||||
name: "search_off",
|
||||
},
|
||||
{
|
||||
name: "output",
|
||||
},
|
||||
{
|
||||
name: "select_all",
|
||||
},
|
||||
{
|
||||
name: "fit_screen",
|
||||
},
|
||||
{
|
||||
name: "swipe_up",
|
||||
},
|
||||
{
|
||||
name: "dynamic_form",
|
||||
},
|
||||
{
|
||||
name: "hide_source",
|
||||
},
|
||||
{
|
||||
name: "swipe_right",
|
||||
},
|
||||
{
|
||||
name: "switch_access_shortcut_add",
|
||||
},
|
||||
{
|
||||
name: "browse_gallery",
|
||||
},
|
||||
{
|
||||
name: "css",
|
||||
},
|
||||
{
|
||||
name: "density_small",
|
||||
},
|
||||
{
|
||||
name: "assistant_direction",
|
||||
},
|
||||
{
|
||||
name: "check_small",
|
||||
},
|
||||
{
|
||||
name: "youtube_searched_for",
|
||||
},
|
||||
{
|
||||
name: "move_up",
|
||||
},
|
||||
{
|
||||
name: "swap_horizontal_circle",
|
||||
},
|
||||
{
|
||||
name: "data_thresholding",
|
||||
},
|
||||
{
|
||||
name: "install_mobile",
|
||||
},
|
||||
{
|
||||
name: "move_down",
|
||||
},
|
||||
{
|
||||
name: "dataset_linked",
|
||||
},
|
||||
{
|
||||
name: "keyboard_command_key",
|
||||
},
|
||||
{
|
||||
name: "view_kanban",
|
||||
},
|
||||
{
|
||||
name: "swipe_down",
|
||||
},
|
||||
{
|
||||
name: "key_off",
|
||||
},
|
||||
{
|
||||
name: "transcribe",
|
||||
},
|
||||
{
|
||||
name: "send_time_extension",
|
||||
},
|
||||
{
|
||||
name: "swipe_down_alt",
|
||||
},
|
||||
{
|
||||
name: "swipe_left_alt",
|
||||
},
|
||||
{
|
||||
name: "swipe_right_alt",
|
||||
},
|
||||
{
|
||||
name: "swipe_up_alt",
|
||||
},
|
||||
{
|
||||
name: "keyboard_option_key",
|
||||
},
|
||||
{
|
||||
name: "cycle",
|
||||
},
|
||||
{
|
||||
name: "rebase",
|
||||
},
|
||||
{
|
||||
name: "rebase_edit",
|
||||
},
|
||||
{
|
||||
name: "empty_dashboard",
|
||||
},
|
||||
{
|
||||
name: "magic_exchange",
|
||||
},
|
||||
{
|
||||
name: "acute",
|
||||
},
|
||||
{
|
||||
name: "point_scan",
|
||||
},
|
||||
{
|
||||
name: "step_into",
|
||||
},
|
||||
{
|
||||
name: "cheer",
|
||||
},
|
||||
{
|
||||
name: "emoticon",
|
||||
},
|
||||
{
|
||||
name: "explosion",
|
||||
},
|
||||
{
|
||||
name: "water_bottle",
|
||||
},
|
||||
{
|
||||
name: "weather_hail",
|
||||
},
|
||||
{
|
||||
name: "syringe",
|
||||
},
|
||||
{
|
||||
name: "pill",
|
||||
},
|
||||
{
|
||||
name: "genetics",
|
||||
},
|
||||
{
|
||||
name: "allergy",
|
||||
},
|
||||
{
|
||||
name: "medical_mask",
|
||||
},
|
||||
{
|
||||
name: "body_fat",
|
||||
},
|
||||
{
|
||||
name: "barefoot",
|
||||
},
|
||||
{
|
||||
name: "infrared",
|
||||
},
|
||||
{
|
||||
name: "wrist",
|
||||
},
|
||||
{
|
||||
name: "metabolism",
|
||||
},
|
||||
{
|
||||
name: "conditions",
|
||||
},
|
||||
{
|
||||
name: "taunt",
|
||||
},
|
||||
{
|
||||
name: "altitude",
|
||||
},
|
||||
{
|
||||
name: "tibia",
|
||||
},
|
||||
{
|
||||
name: "footprint",
|
||||
},
|
||||
{
|
||||
name: "eyeglasses",
|
||||
},
|
||||
{
|
||||
name: "man_3",
|
||||
},
|
||||
{
|
||||
name: "woman_2",
|
||||
},
|
||||
{
|
||||
name: "rheumatology",
|
||||
},
|
||||
{
|
||||
name: "tornado",
|
||||
},
|
||||
{
|
||||
name: "landslide",
|
||||
},
|
||||
{
|
||||
name: "foggy",
|
||||
},
|
||||
{
|
||||
name: "severe_cold",
|
||||
},
|
||||
{
|
||||
name: "tsunami",
|
||||
},
|
||||
{
|
||||
name: "vape_free",
|
||||
},
|
||||
{
|
||||
name: "sign_language",
|
||||
},
|
||||
{
|
||||
name: "emoji_symbols",
|
||||
},
|
||||
{
|
||||
name: "clear_night",
|
||||
},
|
||||
{
|
||||
name: "emoji_food_beverage",
|
||||
},
|
||||
{
|
||||
name: "hive",
|
||||
},
|
||||
{
|
||||
name: "thunderstorm",
|
||||
},
|
||||
{
|
||||
name: "communication",
|
||||
},
|
||||
{
|
||||
name: "rocket",
|
||||
},
|
||||
{
|
||||
name: "pets",
|
||||
},
|
||||
{
|
||||
name: "public",
|
||||
},
|
||||
{
|
||||
name: "quiz",
|
||||
},
|
||||
{
|
||||
name: "mood",
|
||||
},
|
||||
{
|
||||
name: "gavel",
|
||||
},
|
||||
{
|
||||
name: "eco",
|
||||
},
|
||||
{
|
||||
name: "diamond",
|
||||
},
|
||||
{
|
||||
name: "forest",
|
||||
},
|
||||
{
|
||||
name: "rainy",
|
||||
},
|
||||
{
|
||||
name: "skull",
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user