2025-07-29 02:54:53 +08:00
|
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import * as React from 'react';
|
|
|
|
|
|
|
|
|
|
import type { Emoji } from '@emoji-mart/data';
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
type EmojiCategoryList,
|
|
|
|
|
type EmojiIconList,
|
|
|
|
|
type GridRow,
|
|
|
|
|
EmojiSettings
|
|
|
|
|
} from '@platejs/emoji';
|
|
|
|
|
import {
|
|
|
|
|
type EmojiDropdownMenuOptions,
|
|
|
|
|
type UseEmojiPickerType,
|
|
|
|
|
useEmojiDropdownMenuState
|
|
|
|
|
} from '@platejs/emoji/react';
|
|
|
|
|
import * as Popover from '@radix-ui/react-popover';
|
|
|
|
|
import {
|
|
|
|
|
Apple,
|
|
|
|
|
Flag,
|
|
|
|
|
Magnifier,
|
|
|
|
|
Leaf,
|
|
|
|
|
Lightbulb,
|
|
|
|
|
Music,
|
|
|
|
|
Star,
|
|
|
|
|
Xmark,
|
|
|
|
|
FaceGrin,
|
|
|
|
|
Clock,
|
|
|
|
|
Compass
|
|
|
|
|
} from '../../icons';
|
|
|
|
|
|
|
|
|
|
import { Button } from '@/components/ui/buttons';
|
|
|
|
|
import {
|
|
|
|
|
TooltipBase,
|
|
|
|
|
TooltipContent,
|
|
|
|
|
TooltipProvider,
|
|
|
|
|
TooltipTrigger
|
|
|
|
|
} from '@/components/ui/tooltip';
|
|
|
|
|
import { cn } from '@/lib/utils';
|
|
|
|
|
import { ToolbarButton } from '@/components/ui/toolbar';
|
|
|
|
|
|
|
|
|
|
export function EmojiToolbarButton({
|
|
|
|
|
options,
|
|
|
|
|
...props
|
|
|
|
|
}: {
|
|
|
|
|
options?: EmojiDropdownMenuOptions;
|
|
|
|
|
} & React.ComponentPropsWithoutRef<typeof ToolbarButton>) {
|
|
|
|
|
const { emojiPickerState, isOpen, setIsOpen } = useEmojiDropdownMenuState(options);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<EmojiPopover
|
|
|
|
|
control={
|
|
|
|
|
<ToolbarButton pressed={isOpen} tooltip="Emoji" isDropdown {...props}>
|
|
|
|
|
<FaceGrin />
|
|
|
|
|
</ToolbarButton>
|
|
|
|
|
}
|
|
|
|
|
isOpen={isOpen}
|
|
|
|
|
setIsOpen={setIsOpen}>
|
|
|
|
|
<EmojiPicker
|
|
|
|
|
{...emojiPickerState}
|
|
|
|
|
isOpen={isOpen}
|
|
|
|
|
setIsOpen={setIsOpen}
|
|
|
|
|
settings={options?.settings}
|
|
|
|
|
/>
|
|
|
|
|
</EmojiPopover>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function EmojiPopover({
|
|
|
|
|
children,
|
|
|
|
|
control,
|
|
|
|
|
isOpen,
|
|
|
|
|
setIsOpen
|
|
|
|
|
}: {
|
|
|
|
|
children: React.ReactNode;
|
|
|
|
|
control: React.ReactNode;
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
setIsOpen: (open: boolean) => void;
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<Popover.Root open={isOpen} onOpenChange={setIsOpen}>
|
|
|
|
|
<Popover.Trigger asChild>{control}</Popover.Trigger>
|
|
|
|
|
|
|
|
|
|
<Popover.Portal>
|
|
|
|
|
<Popover.Content className="z-100">{children}</Popover.Content>
|
|
|
|
|
</Popover.Portal>
|
|
|
|
|
</Popover.Root>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function EmojiPicker({
|
|
|
|
|
clearSearch,
|
|
|
|
|
emoji,
|
|
|
|
|
emojiLibrary,
|
|
|
|
|
focusedCategory,
|
|
|
|
|
hasFound,
|
|
|
|
|
i18n,
|
|
|
|
|
icons = {
|
|
|
|
|
categories: emojiCategoryIcons,
|
|
|
|
|
search: emojiSearchIcons
|
|
|
|
|
},
|
|
|
|
|
isSearching,
|
|
|
|
|
refs,
|
|
|
|
|
searchResult,
|
|
|
|
|
searchValue,
|
|
|
|
|
setSearch,
|
|
|
|
|
settings = EmojiSettings,
|
|
|
|
|
visibleCategories,
|
|
|
|
|
handleCategoryClick,
|
|
|
|
|
onMouseOver,
|
|
|
|
|
onSelectEmoji
|
|
|
|
|
}: Omit<UseEmojiPickerType, 'icons'> & {
|
|
|
|
|
icons?: EmojiIconList<React.ReactElement>;
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
'bg-popover text-popover-foreground flex flex-col rounded-xl',
|
|
|
|
|
'h-[23rem] w-80 border shadow-md'
|
|
|
|
|
)}>
|
|
|
|
|
<EmojiPickerNavigation
|
|
|
|
|
onClick={handleCategoryClick}
|
|
|
|
|
emojiLibrary={emojiLibrary}
|
|
|
|
|
focusedCategory={focusedCategory}
|
|
|
|
|
i18n={i18n}
|
|
|
|
|
icons={icons}
|
|
|
|
|
/>
|
|
|
|
|
<EmojiPickerSearchBar i18n={i18n} searchValue={searchValue} setSearch={setSearch}>
|
|
|
|
|
<EmojiPickerSearchAndClear
|
|
|
|
|
clearSearch={clearSearch}
|
|
|
|
|
i18n={i18n}
|
|
|
|
|
searchValue={searchValue}
|
|
|
|
|
/>
|
|
|
|
|
</EmojiPickerSearchBar>
|
|
|
|
|
<EmojiPickerContent
|
|
|
|
|
onMouseOver={onMouseOver}
|
|
|
|
|
onSelectEmoji={onSelectEmoji}
|
|
|
|
|
emojiLibrary={emojiLibrary}
|
|
|
|
|
i18n={i18n}
|
|
|
|
|
isSearching={isSearching}
|
|
|
|
|
refs={refs}
|
|
|
|
|
searchResult={searchResult}
|
|
|
|
|
settings={settings}
|
|
|
|
|
visibleCategories={visibleCategories}
|
|
|
|
|
/>
|
|
|
|
|
<EmojiPickerPreview emoji={emoji} hasFound={hasFound} i18n={i18n} isSearching={isSearching} />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const EmojiButton = React.memo(function EmojiButton({
|
|
|
|
|
emoji,
|
|
|
|
|
index,
|
|
|
|
|
onMouseOver,
|
|
|
|
|
onSelect
|
|
|
|
|
}: {
|
|
|
|
|
emoji: Emoji;
|
|
|
|
|
index: number;
|
|
|
|
|
onMouseOver: (emoji?: Emoji) => void;
|
|
|
|
|
onSelect: (emoji: Emoji) => void;
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
className="group relative flex size-9 cursor-pointer items-center justify-center border-none bg-transparent text-2xl leading-none"
|
|
|
|
|
onClick={() => onSelect(emoji)}
|
|
|
|
|
onMouseEnter={() => onMouseOver(emoji)}
|
|
|
|
|
onMouseLeave={() => onMouseOver()}
|
|
|
|
|
aria-label={emoji.skins[0].native}
|
|
|
|
|
data-index={index}
|
|
|
|
|
tabIndex={-1}
|
|
|
|
|
type="button">
|
|
|
|
|
<div
|
|
|
|
|
className="absolute inset-0 rounded-full opacity-0 group-hover:opacity-100"
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
/>
|
|
|
|
|
<span
|
|
|
|
|
className="relative"
|
|
|
|
|
style={{
|
|
|
|
|
fontFamily:
|
|
|
|
|
'"Apple Color Emoji", "Segoe UI Emoji", NotoColorEmoji, "Noto Color Emoji", "Segoe UI Symbol", "Android Emoji", EmojiSymbols'
|
|
|
|
|
}}
|
|
|
|
|
data-emoji-set="native">
|
|
|
|
|
{emoji.skins[0].native}
|
|
|
|
|
</span>
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const RowOfButtons = React.memo(function RowOfButtons({
|
|
|
|
|
emojiLibrary,
|
|
|
|
|
row,
|
|
|
|
|
onMouseOver,
|
|
|
|
|
onSelectEmoji
|
|
|
|
|
}: {
|
|
|
|
|
row: GridRow;
|
|
|
|
|
} & Pick<UseEmojiPickerType, 'emojiLibrary' | 'onMouseOver' | 'onSelectEmoji'>) {
|
|
|
|
|
return (
|
|
|
|
|
<div key={row.id} className="flex" data-index={row.id}>
|
|
|
|
|
{row.elements.map((emojiId, index) => (
|
|
|
|
|
<EmojiButton
|
|
|
|
|
key={emojiId}
|
|
|
|
|
onMouseOver={onMouseOver}
|
|
|
|
|
onSelect={onSelectEmoji}
|
|
|
|
|
emoji={emojiLibrary.getEmoji(emojiId)}
|
|
|
|
|
index={index}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function EmojiPickerContent({
|
|
|
|
|
emojiLibrary,
|
|
|
|
|
i18n,
|
|
|
|
|
isSearching = false,
|
|
|
|
|
refs,
|
|
|
|
|
searchResult,
|
|
|
|
|
settings = EmojiSettings,
|
|
|
|
|
visibleCategories,
|
|
|
|
|
onMouseOver,
|
|
|
|
|
onSelectEmoji
|
|
|
|
|
}: Pick<
|
|
|
|
|
UseEmojiPickerType,
|
|
|
|
|
| 'emojiLibrary'
|
|
|
|
|
| 'i18n'
|
|
|
|
|
| 'isSearching'
|
|
|
|
|
| 'onMouseOver'
|
|
|
|
|
| 'onSelectEmoji'
|
|
|
|
|
| 'refs'
|
|
|
|
|
| 'searchResult'
|
|
|
|
|
| 'settings'
|
|
|
|
|
| 'visibleCategories'
|
|
|
|
|
>) {
|
|
|
|
|
const getRowWidth = settings.perLine.value * settings.buttonSize.value;
|
|
|
|
|
|
|
|
|
|
const isCategoryVisible = React.useCallback(
|
|
|
|
|
(categoryId: any) => {
|
|
|
|
|
return visibleCategories.has(categoryId) ? visibleCategories.get(categoryId) : false;
|
|
|
|
|
},
|
|
|
|
|
[visibleCategories]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const EmojiList = React.useCallback(() => {
|
|
|
|
|
return emojiLibrary
|
|
|
|
|
.getGrid()
|
|
|
|
|
.sections()
|
|
|
|
|
.map(({ id: categoryId }) => {
|
|
|
|
|
const section = emojiLibrary.getGrid().section(categoryId);
|
|
|
|
|
const { buttonSize } = settings;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={categoryId}
|
2025-07-29 03:15:11 +08:00
|
|
|
|
ref={section.root as React.RefObject<HTMLDivElement>}
|
2025-07-29 02:54:53 +08:00
|
|
|
|
style={{ width: getRowWidth }}
|
|
|
|
|
data-id={categoryId}>
|
|
|
|
|
<div className="bg-popover/90 sticky -top-px z-1 p-1 py-2 text-sm font-semibold backdrop-blur-xs">
|
|
|
|
|
{i18n.categories[categoryId]}
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
className="relative flex flex-wrap"
|
|
|
|
|
style={{ height: section.getRows().length * buttonSize.value }}>
|
|
|
|
|
{isCategoryVisible(categoryId) &&
|
|
|
|
|
section
|
|
|
|
|
.getRows()
|
|
|
|
|
.map((row: GridRow) => (
|
|
|
|
|
<RowOfButtons
|
|
|
|
|
key={row.id}
|
|
|
|
|
onMouseOver={onMouseOver}
|
|
|
|
|
onSelectEmoji={onSelectEmoji}
|
|
|
|
|
emojiLibrary={emojiLibrary}
|
|
|
|
|
row={row}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}, [
|
|
|
|
|
emojiLibrary,
|
|
|
|
|
getRowWidth,
|
|
|
|
|
i18n.categories,
|
|
|
|
|
isCategoryVisible,
|
|
|
|
|
onSelectEmoji,
|
|
|
|
|
onMouseOver,
|
|
|
|
|
settings
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const SearchList = React.useCallback(() => {
|
|
|
|
|
return (
|
|
|
|
|
<div style={{ width: getRowWidth }} data-id="search">
|
|
|
|
|
<div className="bg-popover/90 text-card-foreground sticky -top-px z-1 p-1 py-2 text-sm font-semibold backdrop-blur-xs">
|
|
|
|
|
{i18n.searchResult}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="relative flex flex-wrap">
|
|
|
|
|
{searchResult.map((emoji: Emoji, index: number) => (
|
|
|
|
|
<EmojiButton
|
|
|
|
|
key={emoji.id}
|
|
|
|
|
onMouseOver={onMouseOver}
|
|
|
|
|
onSelect={onSelectEmoji}
|
|
|
|
|
emoji={emojiLibrary.getEmoji(emoji.id)}
|
|
|
|
|
index={index}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}, [emojiLibrary, getRowWidth, i18n.searchResult, searchResult, onSelectEmoji, onMouseOver]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
2025-07-29 03:15:11 +08:00
|
|
|
|
ref={refs.current.contentRoot as React.RefObject<HTMLDivElement>}
|
2025-07-29 02:54:53 +08:00
|
|
|
|
className={cn(
|
|
|
|
|
'h-full min-h-[50%] overflow-x-hidden overflow-y-auto px-2',
|
|
|
|
|
'[&::-webkit-scrollbar]:w-4',
|
|
|
|
|
'[&::-webkit-scrollbar-button]:hidden [&::-webkit-scrollbar-button]:size-0',
|
|
|
|
|
'[&::-webkit-scrollbar-thumb]:bg-muted [&::-webkit-scrollbar-thumb]:hover:bg-muted-foreground/25 [&::-webkit-scrollbar-thumb]:min-h-11 [&::-webkit-scrollbar-thumb]:rounded-full',
|
|
|
|
|
'[&::-webkit-scrollbar-thumb]:border-popover [&::-webkit-scrollbar-thumb]:border-4 [&::-webkit-scrollbar-thumb]:border-solid [&::-webkit-scrollbar-thumb]:bg-clip-padding'
|
|
|
|
|
)}
|
|
|
|
|
data-id="scroll">
|
2025-07-29 03:15:11 +08:00
|
|
|
|
<div ref={refs.current.content as React.RefObject<HTMLDivElement>} className="h-full">
|
2025-07-29 02:54:53 +08:00
|
|
|
|
{isSearching ? SearchList() : EmojiList()}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function EmojiPickerSearchBar({
|
|
|
|
|
children,
|
|
|
|
|
i18n,
|
|
|
|
|
searchValue,
|
|
|
|
|
setSearch
|
|
|
|
|
}: {
|
|
|
|
|
children: React.ReactNode;
|
|
|
|
|
} & Pick<UseEmojiPickerType, 'i18n' | 'searchValue' | 'setSearch'>) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center px-2">
|
|
|
|
|
<div className="relative flex grow items-center">
|
|
|
|
|
<input
|
|
|
|
|
className="bg-muted placeholder:text-muted-foreground block w-full appearance-none rounded-full border-0 px-10 py-2 text-sm outline-none focus-visible:outline-none"
|
|
|
|
|
value={searchValue}
|
|
|
|
|
onChange={(event) => setSearch(event.target.value)}
|
|
|
|
|
placeholder={i18n.search}
|
|
|
|
|
aria-label="Search"
|
|
|
|
|
autoComplete="off"
|
|
|
|
|
type="text"
|
|
|
|
|
autoFocus
|
|
|
|
|
/>
|
|
|
|
|
{children}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function EmojiPickerSearchAndClear({
|
|
|
|
|
clearSearch,
|
|
|
|
|
i18n,
|
|
|
|
|
searchValue
|
|
|
|
|
}: Pick<UseEmojiPickerType, 'clearSearch' | 'i18n' | 'searchValue'>) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="text-foreground flex items-center">
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
'text-foreground absolute top-1/2 left-2.5 z-10 flex size-5 -translate-y-1/2 items-center justify-center'
|
|
|
|
|
)}>
|
|
|
|
|
{emojiSearchIcons.loupe}
|
|
|
|
|
</div>
|
|
|
|
|
{searchValue && (
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
className={cn(
|
|
|
|
|
'text-popover-foreground absolute top-1/2 right-0.5 flex size-8 -translate-y-1/2 cursor-pointer items-center justify-center rounded-full border-none bg-transparent hover:bg-transparent'
|
|
|
|
|
)}
|
|
|
|
|
onClick={clearSearch}
|
|
|
|
|
title={i18n.clear}
|
|
|
|
|
aria-label="Clear"
|
|
|
|
|
type="button"
|
|
|
|
|
prefix={emojiSearchIcons.delete}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function EmojiPreview({ emoji }: Pick<UseEmojiPickerType, 'emoji'>) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="border-muted flex h-14 max-h-14 min-h-14 items-center border-t p-2">
|
|
|
|
|
<div className="flex items-center justify-center text-2xl">{emoji?.skins[0].native}</div>
|
|
|
|
|
<div className="overflow-hidden pl-2">
|
|
|
|
|
<div className="truncate text-sm font-semibold">{emoji?.name}</div>
|
|
|
|
|
<div className="truncate text-sm">{`:${emoji?.id}:`}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function NoEmoji({ i18n }: Pick<UseEmojiPickerType, 'i18n'>) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="border-muted flex h-14 max-h-14 min-h-14 items-center border-t p-2">
|
|
|
|
|
<div className="flex items-center justify-center text-2xl">😢</div>
|
|
|
|
|
<div className="overflow-hidden pl-2">
|
|
|
|
|
<div className="truncate text-sm font-bold">{i18n.searchNoResultsTitle}</div>
|
|
|
|
|
<div className="truncate text-sm">{i18n.searchNoResultsSubtitle}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function PickAnEmoji({ i18n }: Pick<UseEmojiPickerType, 'i18n'>) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="border-muted flex h-14 max-h-14 min-h-14 items-center border-t p-2">
|
|
|
|
|
<div className="flex items-center justify-center text-2xl">☝️</div>
|
|
|
|
|
<div className="overflow-hidden pl-2">
|
|
|
|
|
<div className="truncate text-sm font-semibold">{i18n.pick}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function EmojiPickerPreview({
|
|
|
|
|
emoji,
|
|
|
|
|
hasFound = true,
|
|
|
|
|
i18n,
|
|
|
|
|
isSearching = false,
|
|
|
|
|
...props
|
|
|
|
|
}: Pick<UseEmojiPickerType, 'emoji' | 'hasFound' | 'i18n' | 'isSearching'>) {
|
|
|
|
|
const showPickEmoji = !emoji && (!isSearching || hasFound);
|
|
|
|
|
const showNoEmoji = isSearching && !hasFound;
|
|
|
|
|
const showPreview = emoji && !showNoEmoji && !showNoEmoji;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
{showPreview && <EmojiPreview emoji={emoji} {...props} />}
|
|
|
|
|
{showPickEmoji && <PickAnEmoji i18n={i18n} {...props} />}
|
|
|
|
|
{showNoEmoji && <NoEmoji i18n={i18n} {...props} />}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function EmojiPickerNavigation({
|
|
|
|
|
emojiLibrary,
|
|
|
|
|
focusedCategory,
|
|
|
|
|
i18n,
|
|
|
|
|
icons,
|
|
|
|
|
onClick
|
|
|
|
|
}: {
|
|
|
|
|
onClick: (id: EmojiCategoryList) => void;
|
|
|
|
|
} & Pick<UseEmojiPickerType, 'emojiLibrary' | 'focusedCategory' | 'i18n' | 'icons'>) {
|
|
|
|
|
return (
|
|
|
|
|
<TooltipProvider delayDuration={500}>
|
|
|
|
|
<nav id="emoji-nav" className="border-b-border mb-2.5 border-0 border-b border-solid p-1.5">
|
|
|
|
|
<div className="relative flex items-center justify-evenly">
|
|
|
|
|
{emojiLibrary
|
|
|
|
|
.getGrid()
|
|
|
|
|
.sections()
|
|
|
|
|
.map(({ id }) => (
|
|
|
|
|
<TooltipBase key={id}>
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
<Button
|
|
|
|
|
size="small"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
className={cn(
|
|
|
|
|
'text-muted-foreground hover:bg-muted hover:text-muted-foreground h-fit rounded-full fill-current p-1.5',
|
|
|
|
|
id === focusedCategory &&
|
|
|
|
|
'bg-accent text-accent-foreground pointer-events-none fill-current'
|
|
|
|
|
)}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
onClick(id);
|
|
|
|
|
}}
|
|
|
|
|
aria-label={i18n.categories[id]}
|
|
|
|
|
type="button">
|
|
|
|
|
<span className="inline-flex size-5 items-center justify-center">
|
|
|
|
|
{icons.categories[id].outline}
|
|
|
|
|
</span>
|
|
|
|
|
</Button>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent side="bottom">{i18n.categories[id]}</TooltipContent>
|
|
|
|
|
</TooltipBase>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</nav>
|
|
|
|
|
</TooltipProvider>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const emojiCategoryIcons: Record<
|
|
|
|
|
EmojiCategoryList,
|
|
|
|
|
{
|
|
|
|
|
outline: React.ReactElement;
|
|
|
|
|
solid: React.ReactElement; // Needed to add another solid variant - outline will be used for now
|
|
|
|
|
}
|
|
|
|
|
> = {
|
|
|
|
|
activity: {
|
|
|
|
|
outline: (
|
|
|
|
|
<svg
|
|
|
|
|
className="size-full"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
xmlns="http://www.w3.org/2000/svg">
|
|
|
|
|
<circle cx="12" cy="12" r="10" />
|
|
|
|
|
<path d="M2.1 13.4A10.1 10.1 0 0 0 13.4 2.1" />
|
|
|
|
|
<path d="m5 4.9 14 14.2" />
|
|
|
|
|
<path d="M21.9 10.6a10.1 10.1 0 0 0-11.3 11.3" />
|
|
|
|
|
</svg>
|
|
|
|
|
),
|
|
|
|
|
solid: (
|
|
|
|
|
<svg
|
|
|
|
|
className="size-full"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
xmlns="http://www.w3.org/2000/svg">
|
|
|
|
|
<circle cx="12" cy="12" r="10" />
|
|
|
|
|
<path d="M2.1 13.4A10.1 10.1 0 0 0 13.4 2.1" />
|
|
|
|
|
<path d="m5 4.9 14 14.2" />
|
|
|
|
|
<path d="M21.9 10.6a10.1 10.1 0 0 0-11.3 11.3" />
|
|
|
|
|
</svg>
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
custom: {
|
|
|
|
|
outline: <Star />,
|
|
|
|
|
solid: <Star />
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
flags: {
|
|
|
|
|
outline: <Flag />,
|
|
|
|
|
solid: <Flag />
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
foods: {
|
|
|
|
|
outline: <Apple />,
|
|
|
|
|
solid: <Apple />
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
frequent: {
|
|
|
|
|
outline: <Clock />,
|
|
|
|
|
solid: <Clock />
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
nature: {
|
|
|
|
|
outline: <Leaf />,
|
|
|
|
|
solid: <Leaf />
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
objects: {
|
|
|
|
|
outline: <Lightbulb />,
|
|
|
|
|
solid: <Lightbulb />
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
people: {
|
|
|
|
|
outline: <FaceGrin />,
|
|
|
|
|
solid: <FaceGrin />
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
places: {
|
|
|
|
|
outline: <Compass />,
|
|
|
|
|
solid: <Compass />
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
symbols: {
|
|
|
|
|
outline: <Music />,
|
|
|
|
|
solid: <Music />
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const emojiSearchIcons = {
|
|
|
|
|
delete: <Xmark />,
|
|
|
|
|
loupe: <Magnifier />
|
|
|
|
|
};
|