diff --git a/src/app/components/emoji-board/EmojiBoard.css.tsx b/src/app/components/emoji-board/EmojiBoard.css.tsx index ba4ca4e0..271e3fb8 100644 --- a/src/app/components/emoji-board/EmojiBoard.css.tsx +++ b/src/app/components/emoji-board/EmojiBoard.css.tsx @@ -1,5 +1,5 @@ import { style } from '@vanilla-extract/css'; -import { DefaultReset, FocusOutline, color, config, toRem } from 'folds'; +import { DefaultReset, color, config, toRem } from 'folds'; export const Base = style({ maxWidth: toRem(432), @@ -13,52 +13,17 @@ export const Base = style({ overflow: 'hidden', }); -export const Sidebar = style({ - width: toRem(54), - backgroundColor: color.Surface.Container, - color: color.Surface.OnContainer, - position: 'relative', -}); - -export const SidebarContent = style({ - padding: `${config.space.S200} 0`, -}); - -export const SidebarStack = style({ - width: '100%', - backgroundColor: color.Surface.Container, -}); - export const NativeEmojiSidebarStack = style({ position: 'sticky', bottom: '-67%', zIndex: 1, }); -export const SidebarDivider = style({ - width: toRem(18), -}); - export const Header = style({ padding: config.space.S300, paddingBottom: 0, }); -export const EmojiBoardTab = style({ - cursor: 'pointer', -}); - -export const Footer = style({ - padding: config.space.S200, - margin: config.space.S300, - marginTop: 0, - minHeight: toRem(40), - - borderRadius: config.radii.R400, - backgroundColor: color.SurfaceVariant.Container, - color: color.SurfaceVariant.OnContainer, -}); - export const EmojiGroup = style({ padding: `${config.space.S300} 0`, }); @@ -81,56 +46,3 @@ export const EmojiGroupContent = style([ padding: `0 ${config.space.S200}`, }, ]); - -export const EmojiPreview = style([ - DefaultReset, - { - width: toRem(32), - height: toRem(32), - fontSize: toRem(32), - lineHeight: toRem(32), - }, -]); - -export const EmojiItem = style([ - DefaultReset, - FocusOutline, - { - width: toRem(48), - height: toRem(48), - fontSize: toRem(32), - lineHeight: toRem(32), - borderRadius: config.radii.R400, - cursor: 'pointer', - - ':hover': { - backgroundColor: color.Surface.ContainerHover, - }, - }, -]); - -export const StickerItem = style([ - EmojiItem, - { - width: toRem(112), - height: toRem(112), - }, -]); - -export const CustomEmojiImg = style([ - DefaultReset, - { - width: toRem(32), - height: toRem(32), - objectFit: 'contain', - }, -]); - -export const StickerImg = style([ - DefaultReset, - { - width: toRem(96), - height: toRem(96), - objectFit: 'contain', - }, -]); diff --git a/src/app/components/emoji-board/EmojiBoard.tsx b/src/app/components/emoji-board/EmojiBoard.tsx index 72a60f2b..d7beb2e9 100644 --- a/src/app/components/emoji-board/EmojiBoard.tsx +++ b/src/app/components/emoji-board/EmojiBoard.tsx @@ -4,37 +4,20 @@ import React, { MouseEventHandler, UIEventHandler, ReactNode, - memo, useCallback, useEffect, useMemo, useRef, } from 'react'; -import { - Badge, - Box, - Chip, - Icon, - IconButton, - Icons, - Input, - Line, - Scroll, - Text, - Tooltip, - TooltipProvider, - as, - config, - toRem, -} from 'folds'; +import { Box, Icons, Line, Scroll, Text, as } from 'folds'; import FocusTrap from 'focus-trap-react'; import { isKeyHotkey } from 'is-hotkey'; import classNames from 'classnames'; import { MatrixClient, Room } from 'matrix-js-sdk'; -import { atom, useAtomValue, useSetAtom } from 'jotai'; +import { Atom, atom, useAtomValue, useSetAtom } from 'jotai'; import * as css from './EmojiBoard.css'; -import { EmojiGroupId, IEmoji, IEmojiGroup, emojiGroups, emojis } from '../../plugins/emoji'; +import { IEmoji, IEmojiGroup, emojiGroups, emojis } from '../../plugins/emoji'; import { IEmojiGroupLabels, useEmojiGroupLabels } from './useEmojiGroupLabels'; import { IEmojiGroupIcons, useEmojiGroupIcons } from './useEmojiGroupIcons'; import { preventScrollWithArrowKey, stopPropagation } from '../../utils/keyboard'; @@ -47,32 +30,30 @@ import { useAsyncSearch, UseAsyncSearchOptions } from '../../hooks/useAsyncSearc import { useDebounce } from '../../hooks/useDebounce'; import { useThrottle } from '../../hooks/useThrottle'; import { addRecentEmoji } from '../../plugins/recent-emoji'; -import { mobileOrTablet } from '../../utils/user-agent'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { ImagePack, ImageUsage, PackImageReader } from '../../plugins/custom-emoji'; import { getEmoticonSearchStr } from '../../plugins/utils'; +import { + SearchInput, + EmojiBoardTabs, + SidebarStack, + SidebarDivider, + Sidebar, + NoStickerPacks, + createPreviewDataAtom, + Preview, + PreviewData, + EmojiItem, + StickerItem, + CustomEmojiItem, + ImageGroupIcon, + GroupIcon, +} from './components'; +import { EmojiBoardTab, EmojiItemInfo, EmojiType } from './types'; const RECENT_GROUP_ID = 'recent_group'; const SEARCH_GROUP_ID = 'search_group'; -export enum EmojiBoardTab { - Emoji = 'Emoji', - Sticker = 'Sticker', -} - -enum EmojiType { - Emoji = 'emoji', - CustomEmoji = 'customEmoji', - Sticker = 'sticker', -} - -export type EmojiItemInfo = { - type: EmojiType; - data: string; - shortcode: string; - label: string; -}; - const getDOMGroupId = (id: string): string => `EmojiBoardGroup-${id}`; const getEmojiItemInfo = (element: Element): EmojiItemInfo | undefined => { @@ -93,63 +74,14 @@ const getEmojiItemInfo = (element: Element): EmojiItemInfo | undefined => { const activeGroupIdAtom = atom(undefined); -function Sidebar({ children }: { children: ReactNode }) { - return ( - - - - {children} - - - - ); -} - -const SidebarStack = as<'div'>(({ className, children, ...props }, ref) => ( - - {children} - -)); -function SidebarDivider() { - return ; -} - -function Header({ children }: { children: ReactNode }) { - return ( - - {children} - - ); -} - -function Content({ children }: { children: ReactNode }) { - return {children}; -} - -function Footer({ children }: { children: ReactNode }) { - return ( - - {children} - - ); -} - const EmojiBoardLayout = as< 'div', { header: ReactNode; sidebar?: ReactNode; - footer?: ReactNode; children: ReactNode; } ->(({ className, header, sidebar, footer, children, ...props }, ref) => ( +>(({ className, header, sidebar, children, ...props }, ref) => ( - {header} + + {header} + {children} - {footer} {sidebar} )); -function EmojiBoardTabs({ - tab, - onTabChange, -}: { - tab: EmojiBoardTab; - onTabChange: (tab: EmojiBoardTab) => void; -}) { - return ( - - onTabChange(EmojiBoardTab.Sticker)} - > - - Sticker - - - onTabChange(EmojiBoardTab.Emoji)} - > - - Emoji - - - - ); -} - -export function SidebarBtn({ - active, - label, - id, - onItemClick, - children, -}: { - active?: boolean; - label: string; - id: T; - onItemClick: (id: T) => void; - children: ReactNode; -}) { - return ( - - {label} - - } - > - {(ref) => ( - onItemClick(id)} - size="400" - radii="300" - variant="Surface" - > - {children} - - )} - - ); -} - export const EmojiGroup = as< 'div', { @@ -272,189 +128,120 @@ export const EmojiGroup = as< )); -export function EmojiItem({ - label, - type, - data, - shortcode, - children, -}: { - label: string; - type: EmojiType; - data: string; - shortcode: string; - children: ReactNode; -}) { - return ( - - {children} - - ); -} - -export function StickerItem({ - label, - type, - data, - shortcode, - children, -}: { - label: string; - type: EmojiType; - data: string; - shortcode: string; - children: ReactNode; -}) { - return ( - - {children} - - ); -} - -function RecentEmojiSidebarStack({ onItemClick }: { onItemClick: (id: string) => void }) { - const activeGroupId = useAtomValue(activeGroupIdAtom); - - return ( - - onItemClick(RECENT_GROUP_ID)} - > - - - - ); -} - -function ImagePackSidebarStack({ - mx, - packs, - usage, - onItemClick, - useAuthentication, -}: { - mx: MatrixClient; +type EmojiSidebarProps = { + activeGroupAtom: Atom; + handleOpenGroup: (groupId: string) => void; packs: ImagePack[]; - usage: ImageUsage; - onItemClick: (id: string) => void; - useAuthentication?: boolean; -}) { - const activeGroupId = useAtomValue(activeGroupIdAtom); - return ( - - {usage === ImageUsage.Emoticon && } - {packs.map((pack) => { - let label = pack.meta.name; - if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name; - return ( - - {label - - ); - })} - - ); -} - -function NativeEmojiSidebarStack({ - groups, - icons, - labels, - onItemClick, -}: { groups: IEmojiGroup[]; icons: IEmojiGroupIcons; labels: IEmojiGroupLabels; - onItemClick: (id: EmojiGroupId) => void; -}) { - const activeGroupId = useAtomValue(activeGroupIdAtom); +}; +function EmojiSidebar({ + activeGroupAtom, + handleOpenGroup, + packs, + groups, + icons, + labels, +}: EmojiSidebarProps) { + const mx = useMatrixClient(); + const useAuthentication = useMediaAuthentication(); + + const activeGroupId = useAtomValue(activeGroupAtom); + const usage = ImageUsage.Emoticon; + return ( - - - {groups.map((group) => ( - - - - ))} - + + + + + {packs.length > 0 && ( + + + {packs.map((pack) => { + let label = pack.meta.name; + if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name; + + const url = + mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) || + pack.meta.avatar; + + return ( + + ); + })} + + )} + + + {groups.map((group) => ( + + ))} + + ); } -export function RecentEmojiGroup({ - label, - id, - emojis: recentEmojis, -}: { - label: string; - id: string; - emojis: IEmoji[]; -}) { +type StickerSidebarProps = { + activeGroupAtom: Atom; + handleOpenGroup: (groupId: string) => void; + packs: ImagePack[]; +}; +function StickerSidebar({ activeGroupAtom, handleOpenGroup, packs }: StickerSidebarProps) { + const mx = useMatrixClient(); + const useAuthentication = useMediaAuthentication(); + + const activeGroupId = useAtomValue(activeGroupAtom); + const usage = ImageUsage.Sticker; + return ( - - {recentEmojis.map((emoji) => ( - - {emoji.unicode} - - ))} - + + + + {packs.map((pack) => { + let label = pack.meta.name; + if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name; + + const url = + mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) || pack.meta.avatar; + + return ( + + ); + })} + + ); } -export function SearchEmojiGroup({ +export function SearchGroup({ mx, tab, label, @@ -471,171 +258,104 @@ export function SearchEmojiGroup({ }) { return ( - {tab === EmojiBoardTab.Emoji - ? searchResult.map((emoji) => - 'unicode' in emoji ? ( - - {emoji.unicode} - - ) : ( - - {emoji.body - - ) - ) - : searchResult.map((emoji) => - 'unicode' in emoji ? null : ( - - {emoji.body - - ) - )} + {searchResult.map((emoji) => { + if ('unicode' in emoji) { + return ; + } + if (tab === EmojiBoardTab.Sticker) { + return ( + + ); + } + return ( + + ); + })} ); } -export const CustomEmojiGroups = memo( - ({ - mx, - groups, - useAuthentication, - }: { - mx: MatrixClient; - groups: ImagePack[]; - useAuthentication?: boolean; - }) => ( +type StickerGroupsProps = { + packs: ImagePack[]; +}; +function StickerGroups({ packs }: StickerGroupsProps) { + const mx = useMatrixClient(); + const useAuthentication = useMediaAuthentication(); + + if (packs.length === 0) { + return ; + } + return packs.map((pack) => ( + + {pack + .getImages(ImageUsage.Sticker) + .sort((a, b) => a.shortcode.localeCompare(b.shortcode)) + .map((image) => ( + + ))} + + )); +} + +type EmojiGroupsProps = { + recentEmojis: IEmoji[]; + packs: ImagePack[]; + groups: IEmojiGroup[]; + labels: IEmojiGroupLabels; +}; +export function EmojiGroups({ recentEmojis, packs, groups, labels }: EmojiGroupsProps) { + const mx = useMatrixClient(); + const useAuthentication = useMediaAuthentication(); + + return ( <> - {groups.map((pack) => ( + + {recentEmojis.map((emoji) => ( + + ))} + + {packs.map((pack) => ( {pack .getImages(ImageUsage.Emoticon) .sort((a, b) => a.shortcode.localeCompare(b.shortcode)) .map((image) => ( - - {image.body - + mx={mx} + useAuthentication={useAuthentication} + image={image} + /> ))} ))} - - ) -); - -export const StickerGroups = memo( - ({ - mx, - groups, - useAuthentication, - }: { - mx: MatrixClient; - groups: ImagePack[]; - useAuthentication?: boolean; - }) => ( - <> - {groups.length === 0 && ( - - - - No Sticker Packs! - - Add stickers from user, room or space settings. - - - - )} - {groups.map((pack) => ( - - {pack - .getImages(ImageUsage.Sticker) - .sort((a, b) => a.shortcode.localeCompare(b.shortcode)) - .map((image) => ( - - {image.body - - ))} - - ))} - - ) -); - -export const NativeEmojiGroups = memo( - ({ groups, labels }: { groups: IEmojiGroup[]; labels: IEmojiGroupLabels }) => ( - <> {groups.map((emojiGroup) => ( {emojiGroup.emojis.map((emoji) => ( - - {emoji.unicode} - + ))} ))} - ) -); + ); +} + +const DefaultEmojiPreview: PreviewData = { key: '🙂', shortcode: 'slight_smile' }; const SEARCH_OPTIONS: UseAsyncSearchOptions = { limit: 1000, @@ -644,6 +364,19 @@ const SEARCH_OPTIONS: UseAsyncSearchOptions = { }, }; +type EmojiBoardProps = { + tab?: EmojiBoardTab; + onTabChange?: (tab: EmojiBoardTab) => void; + imagePackRooms: Room[]; + requestClose: () => void; + returnFocusOnDeactivate?: boolean; + onEmojiSelect?: (unicode: string, shortcode: string) => void; + onCustomEmojiSelect?: (mxc: string, shortcode: string) => void; + onStickerSelect?: (mxc: string, shortcode: string, label: string) => void; + allowTextCustomEmoji?: boolean; + addToRecentEmoji?: boolean; +}; + export function EmojiBoard({ tab = EmojiBoardTab.Emoji, onTabChange, @@ -655,22 +388,15 @@ export function EmojiBoard({ onStickerSelect, allowTextCustomEmoji, addToRecentEmoji = true, -}: { - tab?: EmojiBoardTab; - onTabChange?: (tab: EmojiBoardTab) => void; - imagePackRooms: Room[]; - requestClose: () => void; - returnFocusOnDeactivate?: boolean; - onEmojiSelect?: (unicode: string, shortcode: string) => void; - onCustomEmojiSelect?: (mxc: string, shortcode: string) => void; - onStickerSelect?: (mxc: string, shortcode: string, label: string) => void; - allowTextCustomEmoji?: boolean; - addToRecentEmoji?: boolean; -}) { +}: EmojiBoardProps) { const emojiTab = tab === EmojiBoardTab.Emoji; - const stickerTab = tab === EmojiBoardTab.Sticker; const usage = emojiTab ? ImageUsage.Emoticon : ImageUsage.Sticker; + const previewAtom = useMemo( + () => createPreviewDataAtom(emojiTab ? DefaultEmojiPreview : undefined), + [emojiTab] + ); + const setPreviewData = useSetAtom(previewAtom); const setActiveGroupId = useSetAtom(activeGroupIdAtom); const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); @@ -680,8 +406,6 @@ export function EmojiBoard({ const recentEmojis = useRecentEmoji(mx, 21); const contentScrollRef = useRef(null); - const emojiPreviewRef = useRef(null); - const emojiPreviewTextRef = useRef(null); const searchList = useMemo(() => { let list: Array = []; @@ -753,26 +477,22 @@ export function EmojiBoard({ } }; + const handleTextCustomEmojiSelect = (textEmoji: string) => { + onCustomEmojiSelect?.(textEmoji, textEmoji); + requestClose(); + }; + const handleEmojiPreview = useCallback( (element: HTMLButtonElement) => { const emojiInfo = getEmojiItemInfo(element); - if (!emojiInfo || !emojiPreviewTextRef.current) return; - if (emojiInfo.type === EmojiType.Emoji && emojiPreviewRef.current) { - emojiPreviewRef.current.textContent = emojiInfo.data; - } else if (emojiInfo.type === EmojiType.CustomEmoji && emojiPreviewRef.current) { - const img = document.createElement('img'); - img.className = css.CustomEmojiImg; - img.setAttribute( - 'src', - mxcUrlToHttp(mx, emojiInfo.data, useAuthentication) || emojiInfo.data - ); - img.setAttribute('alt', emojiInfo.shortcode); - emojiPreviewRef.current.textContent = ''; - emojiPreviewRef.current.appendChild(img); - } - emojiPreviewTextRef.current.textContent = `:${emojiInfo.shortcode}:`; + if (!emojiInfo) return; + + setPreviewData({ + key: emojiInfo.data, + shortcode: emojiInfo.shortcode, + }); }, - [mx, useAuthentication] + [setPreviewData] ); const throttleEmojiHover = useThrottle(handleEmojiPreview, { @@ -816,96 +536,36 @@ export function EmojiBoard({ > - - {onTabChange && } - } - outlined - onClick={() => { - const searchInput = document.querySelector( - '[data-emoji-board-search="true"]' - ); - const textReaction = searchInput?.value.trim(); - if (!textReaction) return; - onCustomEmojiSelect?.(textReaction, textReaction); - requestClose(); - }} - > - React - - ) : ( - - ) - } - onChange={handleOnChange} - autoFocus={!mobileOrTablet()} - /> - - + + {onTabChange && } + + } sidebar={ - - {emojiTab && recentEmojis.length > 0 && ( - - )} - {imagePacks.length > 0 && ( - - )} - {emojiTab && ( - - )} - - } - footer={ emojiTab ? ( -
- - 😃 - - - :smiley: - -
+ ) : ( - imagePacks.length > 0 && ( -
- - :smiley: - -
- ) + ) } > - + {searchedItems && ( - )} - {emojiTab && recentEmojis.length > 0 && ( - - )} - {emojiTab && ( - + ) : ( + )} - {stickerTab && ( - - )} - {emojiTab && } - + +
); diff --git a/src/app/components/emoji-board/components/Item.tsx b/src/app/components/emoji-board/components/Item.tsx new file mode 100644 index 00000000..d033132d --- /dev/null +++ b/src/app/components/emoji-board/components/Item.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { Box } from 'folds'; +import { MatrixClient } from 'matrix-js-sdk'; +import { EmojiType } from '../types'; +import * as css from './styles.css'; +import { PackImageReader } from '../../../plugins/custom-emoji'; +import { IEmoji } from '../../../plugins/emoji'; +import { mxcUrlToHttp } from '../../../utils/matrix'; + +type EmojiItemProps = { + emoji: IEmoji; +}; +export function EmojiItem({ emoji }: EmojiItemProps) { + return ( + + {emoji.unicode} + + ); +} + +type CustomEmojiItemProps = { + mx: MatrixClient; + useAuthentication?: boolean; + image: PackImageReader; +}; +export function CustomEmojiItem({ mx, useAuthentication, image }: CustomEmojiItemProps) { + return ( + + {image.body + + ); +} + +type StickerItemProps = { + mx: MatrixClient; + useAuthentication?: boolean; + image: PackImageReader; +}; + +export function StickerItem({ mx, useAuthentication, image }: StickerItemProps) { + return ( + + {image.body + + ); +} diff --git a/src/app/components/emoji-board/components/NoStickerPacks.tsx b/src/app/components/emoji-board/components/NoStickerPacks.tsx new file mode 100644 index 00000000..6703362c --- /dev/null +++ b/src/app/components/emoji-board/components/NoStickerPacks.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Box, toRem, config, Icons, Icon, Text } from 'folds'; + +export function NoStickerPacks() { + return ( + + + + No Sticker Packs! + + Add stickers from user, room or space settings. + + + + ); +} diff --git a/src/app/components/emoji-board/components/Preview.tsx b/src/app/components/emoji-board/components/Preview.tsx new file mode 100644 index 00000000..3f5f8d3a --- /dev/null +++ b/src/app/components/emoji-board/components/Preview.tsx @@ -0,0 +1,53 @@ +import { Box, Text } from 'folds'; +import React from 'react'; +import { Atom, atom, useAtomValue } from 'jotai'; +import * as css from './styles.css'; +import { useMatrixClient } from '../../../hooks/useMatrixClient'; +import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; +import { mxcUrlToHttp } from '../../../utils/matrix'; + +export type PreviewData = { + key: string; + shortcode: string; +}; + +export const createPreviewDataAtom = (initial?: PreviewData) => + atom(initial); + +type PreviewProps = { + previewAtom: Atom; +}; +export function Preview({ previewAtom }: PreviewProps) { + const mx = useMatrixClient(); + const useAuthentication = useMediaAuthentication(); + + const { key, shortcode } = useAtomValue(previewAtom) ?? {}; + + if (!shortcode) return null; + + return ( + + {key && ( + + {key.startsWith('mxc://') ? ( + {shortcode} + ) : ( + key + )} + + )} + + :{shortcode}: + + + ); +} diff --git a/src/app/components/emoji-board/components/SearchInput.tsx b/src/app/components/emoji-board/components/SearchInput.tsx new file mode 100644 index 00000000..6de4d977 --- /dev/null +++ b/src/app/components/emoji-board/components/SearchInput.tsx @@ -0,0 +1,51 @@ +import React, { ChangeEventHandler, useRef } from 'react'; +import { Input, Chip, Icon, Icons, Text } from 'folds'; +import { mobileOrTablet } from '../../../utils/user-agent'; + +type SearchInputProps = { + query?: string; + onChange: ChangeEventHandler; + allowTextCustomEmoji?: boolean; + onTextCustomEmojiSelect?: (text: string) => void; +}; +export function SearchInput({ + query, + onChange, + allowTextCustomEmoji, + onTextCustomEmojiSelect, +}: SearchInputProps) { + const inputRef = useRef(null); + + const handleReact = () => { + const textEmoji = inputRef.current?.value.trim(); + if (!textEmoji) return; + onTextCustomEmojiSelect?.(textEmoji); + }; + + return ( + } + outlined + onClick={handleReact} + > + React + + ) : ( + + ) + } + onChange={onChange} + autoFocus={!mobileOrTablet()} + /> + ); +} diff --git a/src/app/components/emoji-board/components/Sidebar.tsx b/src/app/components/emoji-board/components/Sidebar.tsx new file mode 100644 index 00000000..de22b483 --- /dev/null +++ b/src/app/components/emoji-board/components/Sidebar.tsx @@ -0,0 +1,130 @@ +import React, { ReactNode } from 'react'; +import { + Box, + Scroll, + Line, + as, + TooltipProvider, + Tooltip, + Text, + IconButton, + Icon, + IconSrc, + Icons, +} from 'folds'; +import classNames from 'classnames'; +import * as css from './styles.css'; + +export function Sidebar({ children }: { children: ReactNode }) { + return ( + + + + {children} + + + + ); +} + +export const SidebarStack = as<'div'>(({ className, children, ...props }, ref) => ( + + {children} + +)); +export function SidebarDivider() { + return ; +} + +function SidebarBtn({ + active, + label, + id, + onClick, + children, +}: { + active?: boolean; + label: string; + id: T; + onClick: (id: T) => void; + children: ReactNode; +}) { + return ( + + {label} + + } + > + {(ref) => ( + onClick(id)} + size="400" + radii="300" + variant="Surface" + > + {children} + + )} + + ); +} + +type GroupIconProps = { + active: boolean; + id: T; + label: string; + icon: IconSrc; + onClick: (id: T) => void; +}; +export function GroupIcon({ + active, + id, + label, + icon, + onClick, +}: GroupIconProps) { + return ( + + + + ); +} + +type ImageGroupIconProps = { + active: boolean; + id: T; + label: string; + url?: string; + onClick: (id: T) => void; +}; +export function ImageGroupIcon({ + active, + id, + label, + url, + onClick, +}: ImageGroupIconProps) { + return ( + + {url ? ( + {label} + ) : ( + + )} + + ); +} diff --git a/src/app/components/emoji-board/components/Tabs.tsx b/src/app/components/emoji-board/components/Tabs.tsx new file mode 100644 index 00000000..d433354f --- /dev/null +++ b/src/app/components/emoji-board/components/Tabs.tsx @@ -0,0 +1,44 @@ +import React, { CSSProperties } from 'react'; +import { Badge, Box, Text } from 'folds'; +import { EmojiBoardTab } from '../types'; + +const styles: CSSProperties = { + cursor: 'pointer', +}; + +export function EmojiBoardTabs({ + tab, + onTabChange, +}: { + tab: EmojiBoardTab; + onTabChange: (tab: EmojiBoardTab) => void; +}) { + return ( + + onTabChange(EmojiBoardTab.Sticker)} + > + + Sticker + + + onTabChange(EmojiBoardTab.Emoji)} + > + + Emoji + + + + ); +} diff --git a/src/app/components/emoji-board/components/index.tsx b/src/app/components/emoji-board/components/index.tsx new file mode 100644 index 00000000..4aeae217 --- /dev/null +++ b/src/app/components/emoji-board/components/index.tsx @@ -0,0 +1,6 @@ +export * from './SearchInput'; +export * from './Tabs'; +export * from './Sidebar'; +export * from './NoStickerPacks'; +export * from './Preview'; +export * from './Item'; diff --git a/src/app/components/emoji-board/components/styles.css.ts b/src/app/components/emoji-board/components/styles.css.ts new file mode 100644 index 00000000..7c9de007 --- /dev/null +++ b/src/app/components/emoji-board/components/styles.css.ts @@ -0,0 +1,100 @@ +import { style } from '@vanilla-extract/css'; +import { toRem, color, config, DefaultReset, FocusOutline } from 'folds'; + +export const Sidebar = style({ + width: toRem(54), + backgroundColor: color.Surface.Container, + color: color.Surface.OnContainer, + position: 'relative', +}); + +export const SidebarContent = style({ + padding: `${config.space.S200} 0`, +}); + +export const SidebarStack = style({ + width: '100%', + backgroundColor: color.Surface.Container, +}); + +export const SidebarDivider = style({ + width: toRem(18), +}); + +export const SidebarBtnImg = style({ + width: toRem(24), + height: toRem(24), + objectFit: 'contain', +}); + +export const Preview = style({ + padding: config.space.S200, + margin: config.space.S300, + marginTop: 0, + minHeight: toRem(40), + + borderRadius: config.radii.R400, + backgroundColor: color.SurfaceVariant.Container, + color: color.SurfaceVariant.OnContainer, +}); + +export const PreviewEmoji = style([ + DefaultReset, + { + width: toRem(32), + height: toRem(32), + fontSize: toRem(32), + lineHeight: toRem(32), + }, +]); +export const PreviewImg = style([ + DefaultReset, + { + width: toRem(32), + height: toRem(32), + objectFit: 'contain', + }, +]); + +export const EmojiItem = style([ + DefaultReset, + FocusOutline, + { + width: toRem(48), + height: toRem(48), + fontSize: toRem(32), + lineHeight: toRem(32), + borderRadius: config.radii.R400, + cursor: 'pointer', + + ':hover': { + backgroundColor: color.Surface.ContainerHover, + }, + }, +]); + +export const StickerItem = style([ + EmojiItem, + { + width: toRem(112), + height: toRem(112), + }, +]); + +export const CustomEmojiImg = style([ + DefaultReset, + { + width: toRem(32), + height: toRem(32), + objectFit: 'contain', + }, +]); + +export const StickerImg = style([ + DefaultReset, + { + width: toRem(96), + height: toRem(96), + objectFit: 'contain', + }, +]); diff --git a/src/app/components/emoji-board/index.ts b/src/app/components/emoji-board/index.ts index 430cec07..7b1cce3b 100644 --- a/src/app/components/emoji-board/index.ts +++ b/src/app/components/emoji-board/index.ts @@ -1 +1,2 @@ export * from './EmojiBoard'; +export * from './types'; diff --git a/src/app/components/emoji-board/types.ts b/src/app/components/emoji-board/types.ts new file mode 100644 index 00000000..de94cc56 --- /dev/null +++ b/src/app/components/emoji-board/types.ts @@ -0,0 +1,17 @@ +export enum EmojiBoardTab { + Emoji = 'Emoji', + Sticker = 'Sticker', +} + +export enum EmojiType { + Emoji = 'emoji', + CustomEmoji = 'customEmoji', + Sticker = 'sticker', +} + +export type EmojiItemInfo = { + type: EmojiType; + data: string; + shortcode: string; + label: string; +};