extract layout and emoji group components

This commit is contained in:
Ajay Bura 2025-09-14 08:28:54 +05:30
parent cd963d91d3
commit 90d9d4243e
7 changed files with 159 additions and 129 deletions

View file

@ -1,48 +0,0 @@
import { style } from '@vanilla-extract/css';
import { DefaultReset, color, config, toRem } from 'folds';
export const Base = style({
maxWidth: toRem(432),
width: `calc(100vw - 2 * ${config.space.S400})`,
height: toRem(450),
backgroundColor: color.Surface.Container,
color: color.Surface.OnContainer,
border: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`,
borderRadius: config.radii.R400,
boxShadow: config.shadow.E200,
overflow: 'hidden',
});
export const NativeEmojiSidebarStack = style({
position: 'sticky',
bottom: '-67%',
zIndex: 1,
});
export const Header = style({
padding: config.space.S300,
paddingBottom: 0,
});
export const EmojiGroup = style({
padding: `${config.space.S300} 0`,
});
export const EmojiGroupLabel = style({
position: 'sticky',
top: config.space.S200,
zIndex: 1,
margin: 'auto',
padding: `${config.space.S100} ${config.space.S200}`,
borderRadius: config.radii.Pill,
backgroundColor: color.SurfaceVariant.Container,
color: color.SurfaceVariant.OnContainer,
});
export const EmojiGroupContent = style([
DefaultReset,
{
padding: `0 ${config.space.S200}`,
},
]);

View file

@ -3,20 +3,17 @@ import React, {
FocusEventHandler, FocusEventHandler,
MouseEventHandler, MouseEventHandler,
UIEventHandler, UIEventHandler,
ReactNode,
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
useRef, useRef,
} from 'react'; } from 'react';
import { Box, Icons, Line, Scroll, Text, as } from 'folds'; import { Box, Icons, Scroll } from 'folds';
import FocusTrap from 'focus-trap-react'; import FocusTrap from 'focus-trap-react';
import { isKeyHotkey } from 'is-hotkey'; import { isKeyHotkey } from 'is-hotkey';
import classNames from 'classnames';
import { MatrixClient, Room } from 'matrix-js-sdk'; import { MatrixClient, Room } from 'matrix-js-sdk';
import { Atom, atom, useAtomValue, useSetAtom } from 'jotai'; import { Atom, atom, useAtomValue, useSetAtom } from 'jotai';
import * as css from './EmojiBoard.css';
import { IEmoji, IEmojiGroup, emojiGroups, emojis } from '../../plugins/emoji'; import { IEmoji, IEmojiGroup, emojiGroups, emojis } from '../../plugins/emoji';
import { IEmojiGroupLabels, useEmojiGroupLabels } from './useEmojiGroupLabels'; import { IEmojiGroupLabels, useEmojiGroupLabels } from './useEmojiGroupLabels';
import { IEmojiGroupIcons, useEmojiGroupIcons } from './useEmojiGroupIcons'; import { IEmojiGroupIcons, useEmojiGroupIcons } from './useEmojiGroupIcons';
@ -48,86 +45,16 @@ import {
CustomEmojiItem, CustomEmojiItem,
ImageGroupIcon, ImageGroupIcon,
GroupIcon, GroupIcon,
getEmojiItemInfo,
getDOMGroupId,
EmojiGroup,
EmojiBoardLayout,
} from './components'; } from './components';
import { EmojiBoardTab, EmojiItemInfo, EmojiType } from './types'; import { EmojiBoardTab, EmojiType } from './types';
const RECENT_GROUP_ID = 'recent_group'; const RECENT_GROUP_ID = 'recent_group';
const SEARCH_GROUP_ID = 'search_group'; const SEARCH_GROUP_ID = 'search_group';
const getDOMGroupId = (id: string): string => `EmojiBoardGroup-${id}`;
const getEmojiItemInfo = (element: Element): EmojiItemInfo | undefined => {
const type = element.getAttribute('data-emoji-type') as EmojiType | undefined;
const data = element.getAttribute('data-emoji-data');
const label = element.getAttribute('title');
const shortcode = element.getAttribute('data-emoji-shortcode');
if (type && data && shortcode && label)
return {
type,
data,
shortcode,
label,
};
return undefined;
};
const activeGroupIdAtom = atom<string | undefined>(undefined);
const EmojiBoardLayout = as<
'div',
{
header: ReactNode;
sidebar?: ReactNode;
children: ReactNode;
}
>(({ className, header, sidebar, children, ...props }, ref) => (
<Box
display="InlineFlex"
className={classNames(css.Base, className)}
direction="Row"
{...props}
ref={ref}
>
<Box direction="Column" grow="Yes">
<Box className={css.Header} direction="Column" shrink="No">
{header}
</Box>
{children}
</Box>
<Line size="300" direction="Vertical" />
{sidebar}
</Box>
));
export const EmojiGroup = as<
'div',
{
id: string;
label: string;
children: ReactNode;
}
>(({ className, id, label, children, ...props }, ref) => (
<Box
id={getDOMGroupId(id)}
data-group-id={id}
className={classNames(css.EmojiGroup, className)}
direction="Column"
gap="200"
{...props}
ref={ref}
>
<Text id={`EmojiGroup-${id}-label`} as="label" className={css.EmojiGroupLabel} size="O400">
{label}
</Text>
<div aria-labelledby={`EmojiGroup-${id}-label`} className={css.EmojiGroupContent}>
<Box wrap="Wrap" justifyContent="Center">
{children}
</Box>
</div>
</Box>
));
type EmojiSidebarProps = { type EmojiSidebarProps = {
activeGroupAtom: Atom<string | undefined>; activeGroupAtom: Atom<string | undefined>;
handleOpenGroup: (groupId: string) => void; handleOpenGroup: (groupId: string) => void;
@ -185,7 +112,13 @@ function EmojiSidebar({
})} })}
</SidebarStack> </SidebarStack>
)} )}
<SidebarStack className={css.NativeEmojiSidebarStack}> <SidebarStack
style={{
position: 'sticky',
bottom: '-67%',
zIndex: 1,
}}
>
<SidebarDivider /> <SidebarDivider />
{groups.map((group) => ( {groups.map((group) => (
<GroupIcon <GroupIcon
@ -397,7 +330,9 @@ export function EmojiBoard({
[emojiTab] [emojiTab]
); );
const setPreviewData = useSetAtom(previewAtom); const setPreviewData = useSetAtom(previewAtom);
const activeGroupIdAtom = useMemo(() => atom<string | undefined>(undefined), []);
const setActiveGroupId = useSetAtom(activeGroupIdAtom); const setActiveGroupId = useSetAtom(activeGroupIdAtom);
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication(); const useAuthentication = useMediaAuthentication();
const emojiGroupLabels = useEmojiGroupLabels(); const emojiGroupLabels = useEmojiGroupLabels();

View file

@ -0,0 +1,34 @@
import { as, Box, Text } from 'folds';
import React, { ReactNode } from 'react';
import classNames from 'classnames';
import * as css from './styles.css';
export const getDOMGroupId = (id: string): string => `EmojiBoardGroup-${id}`;
export const EmojiGroup = as<
'div',
{
id: string;
label: string;
children: ReactNode;
}
>(({ className, id, label, children, ...props }, ref) => (
<Box
id={getDOMGroupId(id)}
data-group-id={id}
className={classNames(css.EmojiGroup, className)}
direction="Column"
gap="200"
{...props}
ref={ref}
>
<Text id={`EmojiGroup-${id}-label`} as="label" className={css.EmojiGroupLabel} size="O400">
{label}
</Text>
<div aria-labelledby={`EmojiGroup-${id}-label`} className={css.EmojiGroupContent}>
<Box wrap="Wrap" justifyContent="Center">
{children}
</Box>
</div>
</Box>
));

View file

@ -1,12 +1,28 @@
import React from 'react'; import React from 'react';
import { Box } from 'folds'; import { Box } from 'folds';
import { MatrixClient } from 'matrix-js-sdk'; import { MatrixClient } from 'matrix-js-sdk';
import { EmojiType } from '../types'; import { EmojiItemInfo, EmojiType } from '../types';
import * as css from './styles.css'; import * as css from './styles.css';
import { PackImageReader } from '../../../plugins/custom-emoji'; import { PackImageReader } from '../../../plugins/custom-emoji';
import { IEmoji } from '../../../plugins/emoji'; import { IEmoji } from '../../../plugins/emoji';
import { mxcUrlToHttp } from '../../../utils/matrix'; import { mxcUrlToHttp } from '../../../utils/matrix';
export const getEmojiItemInfo = (element: Element): EmojiItemInfo | undefined => {
const label = element.getAttribute('title');
const type = element.getAttribute('data-emoji-type') as EmojiType | undefined;
const data = element.getAttribute('data-emoji-data');
const shortcode = element.getAttribute('data-emoji-shortcode');
if (type && data && shortcode && label)
return {
type,
data,
shortcode,
label,
};
return undefined;
};
type EmojiItemProps = { type EmojiItemProps = {
emoji: IEmoji; emoji: IEmoji;
}; };

View file

@ -0,0 +1,30 @@
import { as, Box, Line } from 'folds';
import React, { ReactNode } from 'react';
import classNames from 'classnames';
import * as css from './styles.css';
export const EmojiBoardLayout = as<
'div',
{
header: ReactNode;
sidebar?: ReactNode;
children: ReactNode;
}
>(({ className, header, sidebar, children, ...props }, ref) => (
<Box
display="InlineFlex"
className={classNames(css.Base, className)}
direction="Row"
{...props}
ref={ref}
>
<Box direction="Column" grow="Yes">
<Box className={css.Header} direction="Column" shrink="No">
{header}
</Box>
{children}
</Box>
<Line size="300" direction="Vertical" />
{sidebar}
</Box>
));

View file

@ -4,3 +4,5 @@ export * from './Sidebar';
export * from './NoStickerPacks'; export * from './NoStickerPacks';
export * from './Preview'; export * from './Preview';
export * from './Item'; export * from './Item';
export * from './Group';
export * from './Layout';

View file

@ -1,6 +1,31 @@
import { style } from '@vanilla-extract/css'; import { style } from '@vanilla-extract/css';
import { toRem, color, config, DefaultReset, FocusOutline } from 'folds'; import { toRem, color, config, DefaultReset, FocusOutline } from 'folds';
/**
* Layout
*/
export const Base = style({
maxWidth: toRem(432),
width: `calc(100vw - 2 * ${config.space.S400})`,
height: toRem(450),
backgroundColor: color.Surface.Container,
color: color.Surface.OnContainer,
border: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`,
borderRadius: config.radii.R400,
boxShadow: config.shadow.E200,
overflow: 'hidden',
});
export const Header = style({
padding: config.space.S300,
paddingBottom: 0,
});
/**
* Sidebar
*/
export const Sidebar = style({ export const Sidebar = style({
width: toRem(54), width: toRem(54),
backgroundColor: color.Surface.Container, backgroundColor: color.Surface.Container,
@ -27,6 +52,10 @@ export const SidebarBtnImg = style({
objectFit: 'contain', objectFit: 'contain',
}); });
/**
* Preview
*/
export const Preview = style({ export const Preview = style({
padding: config.space.S200, padding: config.space.S200,
margin: config.space.S300, margin: config.space.S300,
@ -56,6 +85,38 @@ export const PreviewImg = style([
}, },
]); ]);
/**
* Group
*/
export const EmojiGroup = style({
position: 'relative',
padding: `${config.space.S300} 0`,
});
export const EmojiGroupLabel = style({
position: 'sticky',
top: config.space.S200,
zIndex: 1,
margin: 'auto',
padding: `${config.space.S100} ${config.space.S200}`,
borderRadius: config.radii.Pill,
backgroundColor: color.SurfaceVariant.Container,
color: color.SurfaceVariant.OnContainer,
});
export const EmojiGroupContent = style([
DefaultReset,
{
padding: `0 ${config.space.S200}`,
},
]);
/**
* Item
*/
export const EmojiItem = style([ export const EmojiItem = style([
DefaultReset, DefaultReset,
FocusOutline, FocusOutline,