215 lines
5.5 KiB
TypeScript
215 lines
5.5 KiB
TypeScript
import React, { ComponentProps, MutableRefObject, ReactNode } from 'react';
|
|
import { Box, Header, Line, Scroll, Text, as } from 'folds';
|
|
import classNames from 'classnames';
|
|
import { ContainerColor } from '../../styles/ContainerColor.css';
|
|
import * as css from './style.css';
|
|
import * as swipeCss from '../mobile/mobile-gestures.css';
|
|
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
|
import { SidebarDockedCallPanel } from '../../features/call/SidebarDockedCallPanel';
|
|
import { useShowCompactMasterView } from '../../hooks/useCompactNav';
|
|
import { isStationeryTheme, useTheme } from '../../hooks/useTheme';
|
|
|
|
type PageRootProps = {
|
|
nav: ReactNode;
|
|
children: ReactNode;
|
|
};
|
|
|
|
export function PageRoot({ nav, children }: PageRootProps) {
|
|
const screenSize = useScreenSizeContext();
|
|
const showCompactMaster = useShowCompactMasterView();
|
|
const compact = screenSize === ScreenSize.Mobile;
|
|
|
|
// Master list routes: only the channel/space list is interactive.
|
|
// Never mount the detail/underlay stack here — an empty detail layer would
|
|
// sit above the list with pointer-events and freeze taps (DM → space bug).
|
|
if (compact && showCompactMaster) {
|
|
return (
|
|
<Box grow="Yes" className={ContainerColor({ variant: 'Background' })}>
|
|
{nav}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
if (compact && !showCompactMaster) {
|
|
return (
|
|
<Box
|
|
grow="Yes"
|
|
className={classNames(ContainerColor({ variant: 'Background' }), swipeCss.CompactDetailRoot)}
|
|
>
|
|
{nav && (
|
|
<Box className={swipeCss.CompactNavUnderlay} aria-hidden>
|
|
{nav}
|
|
</Box>
|
|
)}
|
|
<Box grow="Yes" className={swipeCss.CompactDetailLayer}>
|
|
{children}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Box grow="Yes" className={ContainerColor({ variant: 'Background' })}>
|
|
{nav}
|
|
{screenSize !== ScreenSize.Mobile && (
|
|
<Line variant="Background" size="300" direction="Vertical" />
|
|
)}
|
|
{children}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
type ClientDrawerLayoutProps = {
|
|
children: ReactNode;
|
|
};
|
|
export function PageNav({
|
|
size,
|
|
children,
|
|
className,
|
|
}: ClientDrawerLayoutProps & css.PageNavVariants & { className?: string }) {
|
|
const screenSize = useScreenSizeContext();
|
|
const isMobile = screenSize === ScreenSize.Mobile;
|
|
|
|
return (
|
|
<Box
|
|
grow={isMobile ? 'Yes' : undefined}
|
|
className={classNames(css.PageNav({ size }), className)}
|
|
shrink={isMobile ? 'Yes' : 'No'}
|
|
data-page-nav=""
|
|
>
|
|
<Box grow="Yes" direction="Column">
|
|
{children}
|
|
<SidebarDockedCallPanel />
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
export const PageNavHeader = as<'header', css.PageNavHeaderVariants>(
|
|
({ className, outlined, ...props }, ref) => (
|
|
<Header
|
|
className={classNames(css.PageNavHeader({ outlined }), className)}
|
|
variant="Background"
|
|
size="600"
|
|
data-folder-tab="nav"
|
|
{...props}
|
|
ref={ref}
|
|
/>
|
|
)
|
|
);
|
|
|
|
export function PageNavContent({
|
|
scrollRef,
|
|
scrollProps,
|
|
children,
|
|
}: {
|
|
children: ReactNode;
|
|
scrollRef?: MutableRefObject<HTMLDivElement | null>;
|
|
scrollProps?: React.ComponentProps<typeof Scroll>;
|
|
}) {
|
|
const theme = useTheme();
|
|
const hideScrollbar = isStationeryTheme(theme);
|
|
|
|
return (
|
|
<Box grow="Yes" direction="Column">
|
|
<Scroll
|
|
ref={scrollRef}
|
|
variant="Background"
|
|
direction="Vertical"
|
|
size={hideScrollbar ? '0' : '300'}
|
|
hideTrack
|
|
visibility="Hover"
|
|
{...scrollProps}
|
|
>
|
|
<div className={css.PageNavContent}>{children}</div>
|
|
</Scroll>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
export const Page = as<'div'>(({ className, ...props }, ref) => (
|
|
<Box
|
|
grow="Yes"
|
|
direction="Column"
|
|
className={classNames(ContainerColor({ variant: 'Surface' }), className)}
|
|
data-page=""
|
|
{...props}
|
|
ref={ref}
|
|
/>
|
|
));
|
|
|
|
export const PageHeader = as<'div', css.PageHeaderVariants>(
|
|
({ className, outlined, balance, ...props }, ref) => (
|
|
<Header
|
|
as="header"
|
|
size="600"
|
|
className={classNames(css.PageHeader({ balance, outlined }), className)}
|
|
data-folder-tab-bar=""
|
|
{...props}
|
|
ref={ref}
|
|
/>
|
|
)
|
|
);
|
|
|
|
export const PageContent = as<'div'>(({ className, ...props }, ref) => (
|
|
<div className={classNames(css.PageContent, className)} {...props} ref={ref} />
|
|
));
|
|
|
|
export function PageHeroEmpty({ children }: { children: ReactNode }) {
|
|
return (
|
|
<Box
|
|
className={classNames(ContainerColor({ variant: 'SurfaceVariant' }), css.PageHeroEmpty)}
|
|
direction="Column"
|
|
alignItems="Center"
|
|
justifyContent="Center"
|
|
gap="200"
|
|
>
|
|
{children}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
export const PageHeroSection = as<'div', ComponentProps<typeof Box>>(
|
|
({ className, ...props }, ref) => (
|
|
<Box
|
|
direction="Column"
|
|
className={classNames(css.PageHeroSection, className)}
|
|
{...props}
|
|
ref={ref}
|
|
/>
|
|
)
|
|
);
|
|
|
|
export function PageHero({
|
|
icon,
|
|
title,
|
|
subTitle,
|
|
children,
|
|
}: {
|
|
icon: ReactNode;
|
|
title: ReactNode;
|
|
subTitle: ReactNode;
|
|
children?: ReactNode;
|
|
}) {
|
|
return (
|
|
<Box direction="Column" gap="400">
|
|
<Box direction="Column" alignItems="Center" gap="200">
|
|
{icon}
|
|
</Box>
|
|
<Box as="h2" direction="Column" gap="200" alignItems="Center">
|
|
<Text align="Center" size="H2">
|
|
{title}
|
|
</Text>
|
|
<Text align="Center" priority="400">
|
|
{subTitle}
|
|
</Text>
|
|
</Box>
|
|
{children}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
export const PageContentCenter = as<'div'>(({ className, ...props }, ref) => (
|
|
<div className={classNames(css.PageContentCenter, className)} {...props} ref={ref} />
|
|
));
|