Add Sidebar components
This commit is contained in:
parent
9bd913e174
commit
837d03a93b
10 changed files with 629 additions and 203 deletions
575
package-lock.json
generated
575
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -24,13 +24,18 @@
|
||||||
"@khanacademy/simple-markdown": "0.8.6",
|
"@khanacademy/simple-markdown": "0.8.6",
|
||||||
"@matrix-org/olm": "3.2.14",
|
"@matrix-org/olm": "3.2.14",
|
||||||
"@tippyjs/react": "4.2.6",
|
"@tippyjs/react": "4.2.6",
|
||||||
|
"@vanilla-extract/css": "1.9.3",
|
||||||
|
"@vanilla-extract/recipes": "0.3.0",
|
||||||
|
"@vanilla-extract/vite-plugin": "3.7.1",
|
||||||
"blurhash": "2.0.4",
|
"blurhash": "2.0.4",
|
||||||
"browser-encrypt-attachment": "0.3.0",
|
"browser-encrypt-attachment": "0.3.0",
|
||||||
|
"classnames": "2.3.2",
|
||||||
"dateformat": "5.0.3",
|
"dateformat": "5.0.3",
|
||||||
"emojibase-data": "7.0.1",
|
"emojibase-data": "7.0.1",
|
||||||
"file-saver": "2.0.5",
|
"file-saver": "2.0.5",
|
||||||
"flux": "4.0.3",
|
"flux": "4.0.3",
|
||||||
"folds": "1.0.2",
|
"focus-trap-react": "10.0.2",
|
||||||
|
"folds": "1.0.4",
|
||||||
"formik": "2.2.9",
|
"formik": "2.2.9",
|
||||||
"html-react-parser": "3.0.4",
|
"html-react-parser": "3.0.4",
|
||||||
"immer": "9.0.16",
|
"immer": "9.0.16",
|
||||||
|
@ -69,7 +74,7 @@
|
||||||
"prettier": "2.8.1",
|
"prettier": "2.8.1",
|
||||||
"sass": "1.56.2",
|
"sass": "1.56.2",
|
||||||
"typescript": "4.9.4",
|
"typescript": "4.9.4",
|
||||||
"vite": "4.0.1",
|
"vite": "4.0.4",
|
||||||
"vite-plugin-static-copy": "0.13.0"
|
"vite-plugin-static-copy": "0.13.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
111
src/app/components/sidebar/Sidebar.css.ts
Normal file
111
src/app/components/sidebar/Sidebar.css.ts
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
|
||||||
|
import { color, config, DefaultReset, toRem } from 'folds';
|
||||||
|
|
||||||
|
export const Sidebar = style([
|
||||||
|
DefaultReset,
|
||||||
|
{
|
||||||
|
width: toRem(66),
|
||||||
|
backgroundColor: color.Background.Container,
|
||||||
|
borderRight: `${config.borderWidth.B300} solid ${color.Background.ContainerLine}`,
|
||||||
|
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
color: color.Background.OnContainer,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const SidebarStack = style([
|
||||||
|
DefaultReset,
|
||||||
|
{
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: config.space.S300,
|
||||||
|
padding: `${config.space.S300} 0`,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const PUSH_X = 2;
|
||||||
|
export const SidebarAvatarBox = recipe({
|
||||||
|
base: [
|
||||||
|
DefaultReset,
|
||||||
|
{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
position: 'relative',
|
||||||
|
transition: 'transform 200ms cubic-bezier(0, 0.8, 0.67, 0.97)',
|
||||||
|
|
||||||
|
selectors: {
|
||||||
|
'&:hover': {
|
||||||
|
transform: `translateX(${toRem(PUSH_X)})`,
|
||||||
|
},
|
||||||
|
'&::before': {
|
||||||
|
content: '',
|
||||||
|
display: 'none',
|
||||||
|
position: 'absolute',
|
||||||
|
left: toRem(-11.5 - PUSH_X),
|
||||||
|
width: toRem(3 + PUSH_X),
|
||||||
|
height: toRem(16),
|
||||||
|
borderRadius: `0 ${toRem(4)} ${toRem(4)} 0`,
|
||||||
|
background: 'CurrentColor',
|
||||||
|
transition: 'height 200ms linear',
|
||||||
|
},
|
||||||
|
'&:hover::before': {
|
||||||
|
display: 'block',
|
||||||
|
width: toRem(3),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
variants: {
|
||||||
|
active: {
|
||||||
|
true: {
|
||||||
|
selectors: {
|
||||||
|
'&::before': {
|
||||||
|
display: 'block',
|
||||||
|
height: toRem(24),
|
||||||
|
},
|
||||||
|
'&:hover::before': {
|
||||||
|
width: toRem(3 + PUSH_X),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SidebarAvatarBoxVariants = RecipeVariants<typeof SidebarAvatarBox>;
|
||||||
|
|
||||||
|
export const SidebarBadgeBox = recipe({
|
||||||
|
base: [
|
||||||
|
DefaultReset,
|
||||||
|
{
|
||||||
|
position: 'absolute',
|
||||||
|
zIndex: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
variants: {
|
||||||
|
hasCount: {
|
||||||
|
true: {
|
||||||
|
top: toRem(-6),
|
||||||
|
right: toRem(-6),
|
||||||
|
},
|
||||||
|
false: {
|
||||||
|
top: toRem(-2),
|
||||||
|
right: toRem(-2),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
hasCount: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SidebarBadgeBoxVariants = RecipeVariants<typeof SidebarBadgeBox>;
|
||||||
|
|
||||||
|
export const SidebarBadgeOutline = style({
|
||||||
|
boxShadow: `0 0 0 ${config.borderWidth.B500} ${color.Background.Container}`,
|
||||||
|
});
|
8
src/app/components/sidebar/Sidebar.tsx
Normal file
8
src/app/components/sidebar/Sidebar.tsx
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { as } from 'folds';
|
||||||
|
import React from 'react';
|
||||||
|
import * as css from './Sidebar.css';
|
||||||
|
|
||||||
|
export const Sidebar = as<'div'>(({ as: AsSidebar = 'div', className, ...props }, ref) => (
|
||||||
|
<AsSidebar className={classNames(css.Sidebar, className)} {...props} ref={ref} />
|
||||||
|
));
|
75
src/app/components/sidebar/SidebarAvatar.tsx
Normal file
75
src/app/components/sidebar/SidebarAvatar.tsx
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { as, Avatar, Box, color, config, Text, Tooltip, TooltipProvider } from 'folds';
|
||||||
|
import React, { forwardRef, MouseEventHandler, ReactNode } from 'react';
|
||||||
|
import * as css from './Sidebar.css';
|
||||||
|
|
||||||
|
const SidebarAvatarBox = as<'div', css.SidebarAvatarBoxVariants>(
|
||||||
|
({ as: AsSidebarAvatarBox = 'div', className, active, ...props }, ref) => (
|
||||||
|
<AsSidebarAvatarBox
|
||||||
|
className={classNames(css.SidebarAvatarBox({ active }), className)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const SidebarAvatar = forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
css.SidebarAvatarBoxVariants &
|
||||||
|
css.SidebarBadgeBoxVariants & {
|
||||||
|
outlined?: boolean;
|
||||||
|
avatarChildren: ReactNode;
|
||||||
|
tooltip: ReactNode | string;
|
||||||
|
notificationBadge?: (badgeClassName: string) => ReactNode;
|
||||||
|
onClick?: MouseEventHandler<HTMLButtonElement>;
|
||||||
|
onContextMenu?: MouseEventHandler<HTMLButtonElement>;
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
active,
|
||||||
|
hasCount,
|
||||||
|
outlined,
|
||||||
|
avatarChildren,
|
||||||
|
tooltip,
|
||||||
|
notificationBadge,
|
||||||
|
onClick,
|
||||||
|
onContextMenu,
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => (
|
||||||
|
<SidebarAvatarBox active={active} ref={ref}>
|
||||||
|
<TooltipProvider
|
||||||
|
delay={0}
|
||||||
|
position="right"
|
||||||
|
tooltip={
|
||||||
|
<Tooltip>
|
||||||
|
<Text size="T300">{tooltip}</Text>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(avRef) => (
|
||||||
|
<Avatar
|
||||||
|
ref={avRef}
|
||||||
|
as="button"
|
||||||
|
onClick={onClick}
|
||||||
|
onContextMenu={onContextMenu}
|
||||||
|
style={{
|
||||||
|
border: outlined
|
||||||
|
? `${config.borderWidth.B300} solid ${color.Background.ContainerLine}`
|
||||||
|
: undefined,
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{avatarChildren}
|
||||||
|
</Avatar>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
|
{notificationBadge && (
|
||||||
|
<Box className={css.SidebarBadgeBox({ hasCount })}>
|
||||||
|
{notificationBadge(css.SidebarBadgeOutline)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</SidebarAvatarBox>
|
||||||
|
)
|
||||||
|
);
|
21
src/app/components/sidebar/SidebarContent.tsx
Normal file
21
src/app/components/sidebar/SidebarContent.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { Box, Scroll } from 'folds';
|
||||||
|
|
||||||
|
type SidebarContentProps = {
|
||||||
|
scrollable: ReactNode;
|
||||||
|
sticky: ReactNode;
|
||||||
|
};
|
||||||
|
export function SidebarContent({ scrollable, sticky }: SidebarContentProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box direction="Column" grow="Yes">
|
||||||
|
<Scroll variant="Background" size="0">
|
||||||
|
{scrollable}
|
||||||
|
</Scroll>
|
||||||
|
</Box>
|
||||||
|
<Box direction="Column" shrink="No">
|
||||||
|
{sticky}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
10
src/app/components/sidebar/SidebarStack.tsx
Normal file
10
src/app/components/sidebar/SidebarStack.tsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { as } from 'folds';
|
||||||
|
import * as css from './Sidebar.css';
|
||||||
|
|
||||||
|
export const SidebarStack = as<'div'>(
|
||||||
|
({ as: AsSidebarStack = 'div', className, ...props }, ref) => (
|
||||||
|
<AsSidebarStack className={classNames(css.SidebarStack, className)} {...props} ref={ref} />
|
||||||
|
)
|
||||||
|
);
|
13
src/app/components/sidebar/SidebarStackSeparator.tsx
Normal file
13
src/app/components/sidebar/SidebarStackSeparator.tsx
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Line, toRem } from 'folds';
|
||||||
|
|
||||||
|
export function SidebarStackSeparator() {
|
||||||
|
return (
|
||||||
|
<Line
|
||||||
|
role="separator"
|
||||||
|
style={{ width: toRem(24), margin: '0 auto' }}
|
||||||
|
variant="Background"
|
||||||
|
size="300"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
5
src/app/components/sidebar/index.ts
Normal file
5
src/app/components/sidebar/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export * from './Sidebar';
|
||||||
|
export * from './SidebarAvatar';
|
||||||
|
export * from './SidebarContent';
|
||||||
|
export * from './SidebarStack';
|
||||||
|
export * from './SidebarStackSeparator';
|
|
@ -2,6 +2,7 @@ import { defineConfig } from 'vite';
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
import { wasm } from '@rollup/plugin-wasm';
|
import { wasm } from '@rollup/plugin-wasm';
|
||||||
import { viteStaticCopy } from 'vite-plugin-static-copy';
|
import { viteStaticCopy } from 'vite-plugin-static-copy';
|
||||||
|
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
|
||||||
|
|
||||||
const copyFiles = {
|
const copyFiles = {
|
||||||
targets: [
|
targets: [
|
||||||
|
@ -33,6 +34,7 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
viteStaticCopy(copyFiles),
|
viteStaticCopy(copyFiles),
|
||||||
|
vanillaExtractPlugin(),
|
||||||
wasm(),
|
wasm(),
|
||||||
react(),
|
react(),
|
||||||
],
|
],
|
||||||
|
@ -41,7 +43,4 @@ export default defineConfig({
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
copyPublicDir: false,
|
copyPublicDir: false,
|
||||||
},
|
},
|
||||||
optimizeDeps: {
|
|
||||||
include: ["folds"],
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue