refactor: monorepo setup with lerna (#243)
This commit is contained in:
committed by
GitHub
parent
dac29fbf3c
commit
504d95c34f
25
packages/docs/src/components/DateDisplay.tsx
Normal file
25
packages/docs/src/components/DateDisplay.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import addMinutes from 'date-fns/addMinutes';
|
||||
import format from 'date-fns/format';
|
||||
import parseISO from 'date-fns/parseISO';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
function formatDate(date: Date, dateFormat: string) {
|
||||
return format(addMinutes(date, date.getTimezoneOffset()), dateFormat);
|
||||
}
|
||||
|
||||
interface DateDisplayProps {
|
||||
date: string;
|
||||
format: string;
|
||||
}
|
||||
|
||||
const DateDisplay = ({ date: dateString, format: dateFormat }: DateDisplayProps) => {
|
||||
const [date, setDate] = useState(`${formatDate(parseISO(dateString), dateFormat)} UTC`);
|
||||
|
||||
useEffect(() => {
|
||||
setDate(format(parseISO(dateString), dateFormat));
|
||||
}, [dateFormat, dateString]);
|
||||
|
||||
return <>{date}</>;
|
||||
};
|
||||
|
||||
export default DateDisplay;
|
38
packages/docs/src/components/community/CommunitySection.tsx
Normal file
38
packages/docs/src/components/community/CommunitySection.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import List from '@mui/material/List';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
import type { CommunityLinksSection } from '../../interface';
|
||||
|
||||
const StyledCommunitySection = styled('div')(
|
||||
({ theme }) => `
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
color: ${theme.palette.primary.main}
|
||||
`,
|
||||
);
|
||||
|
||||
interface CommunitySectionProps {
|
||||
section: CommunityLinksSection;
|
||||
}
|
||||
|
||||
const CommunitySection = ({ section }: CommunitySectionProps) => {
|
||||
return (
|
||||
<StyledCommunitySection>
|
||||
<Typography variant="h3">{section.title}</Typography>
|
||||
<List sx={{ width: '100%', bgcolor: 'background.paper' }}>
|
||||
{section.links.map(link => (
|
||||
<ListItemButton key={link.url} href={link.url} target="_blank">
|
||||
<ListItemText primary={link.title} secondary={link.description} />
|
||||
</ListItemButton>
|
||||
))}
|
||||
</List>
|
||||
</StyledCommunitySection>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommunitySection;
|
@ -0,0 +1,6 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const ColorModeContext = createContext({ toggleColorMode: () => {} });
|
||||
|
||||
export default ColorModeContext;
|
293
packages/docs/src/components/docs/DocsContent.tsx
Normal file
293
packages/docs/src/components/docs/DocsContent.tsx
Normal file
@ -0,0 +1,293 @@
|
||||
import { styled } from '@mui/material/styles';
|
||||
|
||||
const DocsContent = styled('div')(
|
||||
({ theme }) => `
|
||||
color: ${theme.palette.text.primary};
|
||||
font-weight: 200;
|
||||
width: 100%;
|
||||
max-width: 1100px;
|
||||
padding: 0 40px 0 56px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
${theme.breakpoints.between('sm', 'lg')} {
|
||||
padding: 0 40px;
|
||||
}
|
||||
|
||||
${theme.breakpoints.down('sm')} {
|
||||
padding: 0 32px;
|
||||
}
|
||||
|
||||
& time {
|
||||
color: #9b9b9b;
|
||||
}
|
||||
|
||||
& p {
|
||||
line-height: 1.5rem;
|
||||
margin: 0 0 16px;
|
||||
font-size: 16px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
& p:not(:first-of-type) {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
& pre + p:not(:first-of-type) {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
& :not(h1,h2,h3,h4,h5,h6) a {
|
||||
color: ${theme.palette.primary.main};
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
& a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
& h2,
|
||||
& h3,
|
||||
& h4,
|
||||
& h5,
|
||||
& h6 {
|
||||
font-weight: 500;
|
||||
margin-top: 28px;
|
||||
margin-bottom: 12px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
& h3 {
|
||||
color: ${theme.palette.text.primary};
|
||||
}
|
||||
|
||||
& h4,
|
||||
& h5,
|
||||
& h6 {
|
||||
color: ${theme.palette.text.secondary};
|
||||
}
|
||||
|
||||
${theme.breakpoints.up('lg')} {
|
||||
& h1 {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
& h1 {
|
||||
font-size: 32px;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
${theme.breakpoints.between('sm', 'lg')} {
|
||||
& h1 {
|
||||
font-size: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
${theme.breakpoints.down('sm')} {
|
||||
& h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
& h2 {
|
||||
color: ${theme.palette.primary.main};
|
||||
font-size: 26px;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
${theme.breakpoints.between('sm', 'lg')} {
|
||||
& h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
${theme.breakpoints.down('sm')} {
|
||||
& h2 {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
& h3 {
|
||||
line-height: 22px;
|
||||
font-size: 24px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
${theme.breakpoints.between('sm', 'lg')} {
|
||||
& h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
${theme.breakpoints.down('sm')} {
|
||||
& h3 {
|
||||
font-size: 17px;
|
||||
}
|
||||
}
|
||||
|
||||
& h4 {
|
||||
font-size: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
${theme.breakpoints.down('lg')} {
|
||||
& h4 {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
& h5 {
|
||||
font-size: 18px;
|
||||
line-height: 19px;
|
||||
}
|
||||
|
||||
${theme.breakpoints.down('lg')} {
|
||||
& h5 {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
& h6 {
|
||||
font-size: 16px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
${theme.breakpoints.down('lg')} {
|
||||
& h6 {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
& h1 + h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.dark & pre,
|
||||
.dark & pre[class*='language-'],
|
||||
.light & pre,
|
||||
.light & pre[class*='language-'] {
|
||||
display: block;
|
||||
line-height: 1.25rem;
|
||||
padding: 1rem;
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
& pre code {
|
||||
background-color: transparent;
|
||||
font-size: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
& code[class*=language-],
|
||||
& pre[class*=language-] {
|
||||
text-shadow: none;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.light & code[class*=language-],
|
||||
.light & pre[class*=language-] {
|
||||
background-color: #EDEEEE;
|
||||
text-shadow: none;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.dark & code[class*=language-],
|
||||
.dark & pre[class*=language-] {
|
||||
background-color: #1E1E1E;
|
||||
text-shadow: none;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
& code {
|
||||
font-size: 85%;
|
||||
padding: 0.2em 0.4em;
|
||||
margin: 0;
|
||||
border-radius: 3px;
|
||||
color: ${theme.palette.text.primary};
|
||||
background-color: ${
|
||||
theme.palette.mode === 'light' ? 'rgba(175,184,193,0.2)' : 'rgba(110,118,129,0.75)'
|
||||
};
|
||||
}
|
||||
|
||||
& blockquote {
|
||||
margin: 8px 1rem;
|
||||
}
|
||||
|
||||
& blockquote > p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
& blockquote::before {
|
||||
border-left: 4px solid ${theme.palette.text.secondary};
|
||||
position: absolute;
|
||||
content: '';
|
||||
font-size: 6em;
|
||||
font-family: roboto, serif;
|
||||
line-height: 1.5rem;
|
||||
margin-left: -0.2em;
|
||||
height: 1.5rem;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
& ol,
|
||||
& ul {
|
||||
padding: 0 0 0 1.5rem;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
& ol li,
|
||||
& ul li {
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
& li ol,
|
||||
& li ul {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
& ul + p {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
& abbr[title] {
|
||||
text-decoration: underline double;
|
||||
}
|
||||
|
||||
& kbd {
|
||||
font-family: 'Oswald', 'Ubuntu Mono', monospace;
|
||||
}
|
||||
|
||||
& img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
${theme.breakpoints.down('md')} {
|
||||
& h2::before {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
& b,
|
||||
& strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
& hr {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.MuiAlert-root {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
export default DocsContent;
|
42
packages/docs/src/components/docs/DocsLeftNav.tsx
Normal file
42
packages/docs/src/components/docs/DocsLeftNav.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import List from '@mui/material/List';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
import DocsLeftNavGroup from './DocsLeftNavGroup';
|
||||
|
||||
import type { DocsGroup } from '../../interface';
|
||||
|
||||
export interface DocsLeftNavProps {
|
||||
docsGroups: DocsGroup[];
|
||||
}
|
||||
|
||||
const DocsLeftNav = ({ docsGroups }: DocsLeftNavProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<List
|
||||
component="nav"
|
||||
aria-labelledby="docs-left-nav"
|
||||
sx={{
|
||||
width: '100%',
|
||||
maxWidth: 280,
|
||||
bgcolor: 'background.paper',
|
||||
position: 'fixed',
|
||||
left: 0,
|
||||
top: '72px',
|
||||
bottom: 0,
|
||||
overflowY: 'auto',
|
||||
paddingBottom: '24px',
|
||||
[theme.breakpoints.down('lg')]: {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
dense
|
||||
>
|
||||
{docsGroups.map(group => (
|
||||
<DocsLeftNavGroup key={group.name} name={group.title} links={group.links} />
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocsLeftNav;
|
68
packages/docs/src/components/docs/DocsLeftNavGroup.tsx
Normal file
68
packages/docs/src/components/docs/DocsLeftNavGroup.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import List from '@mui/material/List';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { DocsGroupLink } from '../../interface';
|
||||
|
||||
export interface DocsLeftNavGroupProps {
|
||||
name: string;
|
||||
links: DocsGroupLink[];
|
||||
}
|
||||
|
||||
const DocsLeftNavGroup = ({ name, links }: DocsLeftNavGroupProps) => {
|
||||
const theme = useTheme();
|
||||
const [open, setOpen] = useState(true);
|
||||
const { asPath } = useRouter();
|
||||
|
||||
const handleClick = () => {
|
||||
setOpen(!open);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<ListItemButton onClick={handleClick}>
|
||||
<ListItemText primary={name} />
|
||||
<ExpandLessIcon
|
||||
sx={{
|
||||
transform: `rotateZ(${open ? 0 : 90}deg)`,
|
||||
transition: theme.transitions.create(['transform']),
|
||||
}}
|
||||
/>
|
||||
</ListItemButton>
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
<List component="div" disablePadding dense>
|
||||
{links.map(link => {
|
||||
const url = `/docs/${link.slug}`;
|
||||
const selected = asPath === url;
|
||||
return (
|
||||
<ListItemButton
|
||||
key={link.slug}
|
||||
component={Link}
|
||||
href={url}
|
||||
sx={{
|
||||
pl: 4,
|
||||
}}
|
||||
selected={selected}
|
||||
>
|
||||
<ListItemText
|
||||
primaryTypographyProps={{
|
||||
color: selected ? theme.palette.primary.main : theme.palette.text.secondary,
|
||||
fontWeight: selected ? 600 : 400,
|
||||
}}
|
||||
primary={link.title}
|
||||
/>
|
||||
</ListItemButton>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</Collapse>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocsLeftNavGroup;
|
7
packages/docs/src/components/docs/DocsRightNav.tsx
Normal file
7
packages/docs/src/components/docs/DocsRightNav.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import DocsTableOfContents from './table_of_contents/DocsTableOfContents';
|
||||
|
||||
const DocsRightNav = () => {
|
||||
return <DocsTableOfContents />;
|
||||
};
|
||||
|
||||
export default DocsRightNav;
|
57
packages/docs/src/components/docs/components/Anchor.tsx
Normal file
57
packages/docs/src/components/docs/components/Anchor.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import Link from 'next/link';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import type { DetailedHTMLProps, AnchorHTMLAttributes, ReactNode } from 'react';
|
||||
|
||||
enum LinkType {
|
||||
SAME_SITE,
|
||||
SAME_PAGE,
|
||||
EXTERNAL,
|
||||
}
|
||||
|
||||
interface AnchorProps
|
||||
extends DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement> {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const Anchor = ({ href = '', children = '' }: AnchorProps) => {
|
||||
const type: LinkType = useMemo(() => {
|
||||
if (href.startsWith('#')) {
|
||||
return LinkType.SAME_PAGE;
|
||||
}
|
||||
|
||||
if (href.startsWith('/') || href.startsWith('.')) {
|
||||
return LinkType.SAME_SITE;
|
||||
}
|
||||
|
||||
return LinkType.EXTERNAL;
|
||||
}, [href]);
|
||||
|
||||
if (type === LinkType.SAME_PAGE) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
document.querySelector(href)?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === LinkType.SAME_SITE) {
|
||||
return <Link href={href}>{children}</Link>;
|
||||
}
|
||||
|
||||
return (
|
||||
<a href={href} target="_blank" rel="noreferrer">
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default Anchor;
|
71
packages/docs/src/components/docs/components/Blockquote.tsx
Normal file
71
packages/docs/src/components/docs/components/Blockquote.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface StyledBlockquoteProps {
|
||||
$color: 'success' | 'error' | 'default';
|
||||
}
|
||||
|
||||
const StyledBlockquote = styled('blockquote')<StyledBlockquoteProps>(
|
||||
({ theme, $color }) => `
|
||||
${
|
||||
$color !== 'default'
|
||||
? `
|
||||
blockquote&::before {
|
||||
border-left: 4px solid ${
|
||||
$color === 'success' ? theme.palette.success.main : theme.palette.error.main
|
||||
};
|
||||
}
|
||||
`
|
||||
: ''
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const getNodeText = (node: ReactNode): string => {
|
||||
if (!node) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (typeof node === 'string' || typeof node === 'boolean' || typeof node === 'number') {
|
||||
return `${node}`;
|
||||
}
|
||||
|
||||
if (node instanceof Array) {
|
||||
return node.map(getNodeText).join('');
|
||||
}
|
||||
|
||||
if ('props' in node && 'children' in node.props) {
|
||||
return getNodeText(node.props.children);
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
interface BlockquoteProps {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const Blockquote = ({ children = '' }: BlockquoteProps) => {
|
||||
const color = useMemo(() => {
|
||||
const text = getNodeText(children).trim();
|
||||
if (text === '') {
|
||||
return 'default';
|
||||
}
|
||||
|
||||
if (text.startsWith('Do: ')) {
|
||||
return 'success';
|
||||
}
|
||||
|
||||
if (text.startsWith("Don't: ")) {
|
||||
return 'error';
|
||||
}
|
||||
|
||||
return 'default';
|
||||
}, [children]);
|
||||
|
||||
return <StyledBlockquote $color={color}>{children}</StyledBlockquote>;
|
||||
};
|
||||
|
||||
export default Blockquote;
|
142
packages/docs/src/components/docs/components/CodeTabs.tsx
Normal file
142
packages/docs/src/components/docs/components/CodeTabs.tsx
Normal file
@ -0,0 +1,142 @@
|
||||
import Box from '@mui/material/Box';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import Prism from 'prismjs';
|
||||
import { isValidElement, useMemo, useState } from 'react';
|
||||
|
||||
import { isNotEmpty } from '../../../util/string.util';
|
||||
|
||||
import type { Grammar } from 'prismjs';
|
||||
import type { ReactElement, ReactNode, SyntheticEvent } from 'react';
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
index: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
function TabPanel(props: TabPanelProps) {
|
||||
const { children, value, index, ...other } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`simple-tabpanel-${index}`}
|
||||
aria-labelledby={`simple-tab-${index}`}
|
||||
{...other}
|
||||
>
|
||||
{value === index && <Box>{children}</Box>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function a11yProps(index: number) {
|
||||
return {
|
||||
id: `simple-tab-${index}`,
|
||||
'aria-controls': `simple-tabpanel-${index}`,
|
||||
};
|
||||
}
|
||||
|
||||
interface CodeLanguage {
|
||||
title: string;
|
||||
grammar: Grammar;
|
||||
language: string;
|
||||
}
|
||||
|
||||
const supportedLanguages: Record<string, CodeLanguage> = {
|
||||
'language-yaml': {
|
||||
title: 'Yaml',
|
||||
grammar: Prism.languages.yaml,
|
||||
language: 'yaml',
|
||||
},
|
||||
'language-js': {
|
||||
title: 'JavaScript',
|
||||
grammar: Prism.languages.javascript,
|
||||
language: 'javascript',
|
||||
},
|
||||
'language-markdown': {
|
||||
title: 'Markdown',
|
||||
grammar: Prism.languages.markdown,
|
||||
language: 'markdown',
|
||||
},
|
||||
};
|
||||
|
||||
interface TabData {
|
||||
title: string;
|
||||
className: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface CodeTabsProps {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const CodeTabs = ({ children }: CodeTabsProps) => {
|
||||
const [value, setValue] = useState(0);
|
||||
|
||||
const handleChange = (_event: SyntheticEvent, newValue: number) => {
|
||||
setValue(newValue);
|
||||
};
|
||||
|
||||
const tabs = useMemo(() => {
|
||||
if (!children || !Array.isArray(children)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return children
|
||||
.filter((child: ReactNode) => isValidElement(child) && child.type === 'pre')
|
||||
.map((child: ReactElement) => child.props.children)
|
||||
.filter((subChild: ReactNode) => isValidElement(subChild) && subChild.type === 'code')
|
||||
.map((code: ReactElement) => {
|
||||
if (!(code.props.className in supportedLanguages)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const language = supportedLanguages[code.props.className];
|
||||
|
||||
return {
|
||||
title: language.title,
|
||||
className: code.props.className,
|
||||
content:
|
||||
typeof code.props.children === 'string' && isNotEmpty(code.props.children)
|
||||
? Prism.highlight(code.props.children, language.grammar, language.language)
|
||||
: '',
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as TabData[];
|
||||
}, [children]);
|
||||
|
||||
if (tabs.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ width: '100%', margin: '8px 0 16px' }}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Tabs
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
aria-label="basic tabs example"
|
||||
sx={{ '.MuiTabs-root': { margin: 0 }, '.MuiTabs-flexContainer': { margin: 0 } }}
|
||||
>
|
||||
{tabs.map((tabData, index) => (
|
||||
<Tab key={tabData.className} label={tabData.title} {...a11yProps(index)} />
|
||||
))}
|
||||
</Tabs>
|
||||
</Box>
|
||||
{tabs.map((tabData, index) => (
|
||||
<TabPanel key={tabData.className} value={value} index={index}>
|
||||
<pre className={tabData.className}>
|
||||
<code
|
||||
className={tabData.className}
|
||||
dangerouslySetInnerHTML={{ __html: tabData.content }}
|
||||
/>
|
||||
</pre>
|
||||
</TabPanel>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeTabs;
|
@ -0,0 +1,23 @@
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
import { useNodeText } from '../../../../util/node.util';
|
||||
import useAnchor from './hooks/useAnchor';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface Header1Props {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const Header1 = ({ children }: Header1Props) => {
|
||||
const textContent = useNodeText(children);
|
||||
const anchor = useAnchor(textContent);
|
||||
|
||||
return (
|
||||
<Typography variant="h1" id={anchor}>
|
||||
{children}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header1;
|
@ -0,0 +1,13 @@
|
||||
import LinkedHeader from './components/LinkedHeader';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface Header2Props {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const Header2 = ({ children }: Header2Props) => {
|
||||
return <LinkedHeader variant="h2">{children}</LinkedHeader>;
|
||||
};
|
||||
|
||||
export default Header2;
|
@ -0,0 +1,13 @@
|
||||
import LinkedHeader from './components/LinkedHeader';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface Header3Props {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const Header3 = ({ children }: Header3Props) => {
|
||||
return <LinkedHeader variant="h3">{children}</LinkedHeader>;
|
||||
};
|
||||
|
||||
export default Header3;
|
@ -0,0 +1,13 @@
|
||||
import LinkedHeader from './components/LinkedHeader';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface Header4Props {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const Header4 = ({ children }: Header4Props) => {
|
||||
return <LinkedHeader variant="h4">{children}</LinkedHeader>;
|
||||
};
|
||||
|
||||
export default Header4;
|
@ -0,0 +1,13 @@
|
||||
import LinkedHeader from './components/LinkedHeader';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface Header5Props {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const Header5 = ({ children }: Header5Props) => {
|
||||
return <LinkedHeader variant="h5">{children}</LinkedHeader>;
|
||||
};
|
||||
|
||||
export default Header5;
|
@ -0,0 +1,13 @@
|
||||
import LinkedHeader from './components/LinkedHeader';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface Header6Props {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const Header6 = ({ children }: Header6Props) => {
|
||||
return <LinkedHeader variant="h6">{children}</LinkedHeader>;
|
||||
};
|
||||
|
||||
export default Header6;
|
@ -0,0 +1,45 @@
|
||||
import LinkIcon from '@mui/icons-material/Link';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
interface AnchorLinkIconProps {
|
||||
variant: 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
||||
}
|
||||
|
||||
const AnchorLinkIcon = ({ variant }: AnchorLinkIconProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const color = useMemo(() => {
|
||||
if (variant === 'h2') {
|
||||
return theme.palette.primary.main;
|
||||
}
|
||||
|
||||
if (variant === 'h3') {
|
||||
return theme.palette.text.primary;
|
||||
}
|
||||
|
||||
return theme.palette.text.secondary;
|
||||
}, [
|
||||
theme.palette.primary.main,
|
||||
theme.palette.text.primary,
|
||||
theme.palette.text.secondary,
|
||||
variant,
|
||||
]);
|
||||
|
||||
return (
|
||||
<LinkIcon
|
||||
fontSize={variant === 'h2' ? 'medium' : 'small'}
|
||||
sx={{
|
||||
color,
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
fontSize: '20px',
|
||||
height: '20px',
|
||||
width: '20px',
|
||||
marginTop: '2px',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnchorLinkIcon;
|
@ -0,0 +1,61 @@
|
||||
import { styled, useTheme } from '@mui/material/styles';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Link from 'next/link';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useNodeText } from '../../../../../util/node.util';
|
||||
import { isNotEmpty } from '../../../../../util/string.util';
|
||||
import useAnchor from '../hooks/useAnchor';
|
||||
import AnchorLinkIcon from './AnchorLinkIcon';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
const StyledLink = styled(Link)(
|
||||
({ theme }) => `
|
||||
position: absolute;
|
||||
margin-left: -28px;
|
||||
top: 0;
|
||||
font-weight: 300;
|
||||
color: ${theme.palette.text.primary};
|
||||
transform: rotateZ(-45deg);
|
||||
|
||||
${theme.breakpoints.down('sm')} {
|
||||
margin-left: -22px;
|
||||
top: -1px;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
interface Header3Props {
|
||||
variant: 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const Header3 = ({ variant, children = '' }: Header3Props) => {
|
||||
const textContent = useNodeText(children);
|
||||
const anchor = useAnchor(textContent);
|
||||
const link = useMemo(() => `#${anchor}`, [anchor]);
|
||||
const theme = useTheme();
|
||||
const hasText = useMemo(() => isNotEmpty(textContent), [textContent]);
|
||||
return (
|
||||
<Typography
|
||||
variant={variant}
|
||||
component={hasText ? variant : 'div'}
|
||||
id={anchor}
|
||||
sx={{
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
marginLeft: '8px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{hasText ? (
|
||||
<StyledLink href={link}>
|
||||
<AnchorLinkIcon variant={variant} />
|
||||
</StyledLink>
|
||||
) : null}
|
||||
{children}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header3;
|
@ -0,0 +1,15 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const getAnchor = (text: string) => {
|
||||
return text
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9 \-_]/g, '')
|
||||
.replace(/[ ]/g, '-');
|
||||
};
|
||||
|
||||
const useAnchor = (text: string) => {
|
||||
return useMemo(() => getAnchor(text), [text]);
|
||||
};
|
||||
|
||||
export default useAnchor;
|
61
packages/docs/src/components/docs/components/table/Table.tsx
Normal file
61
packages/docs/src/components/docs/components/table/Table.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { styled } from '@mui/material/styles';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
const StyledTableContainer = styled(TableContainer)(
|
||||
({ theme }) => `
|
||||
margin-bottom: 16px;
|
||||
|
||||
& td {
|
||||
color: ${theme.palette.text.secondary};
|
||||
}
|
||||
|
||||
& td:nth-of-type(2):not(:last-of-type) {
|
||||
color:
|
||||
${theme.palette.mode === 'light' ? '#751365' : '#ffb6ec'};
|
||||
}
|
||||
|
||||
& thead tr th,
|
||||
& thead tr td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
& tbody tr td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
& tbody tr td:last-of-type {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.non-props-table + & tbody tr td {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
& tbody tr td:last-of-type {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.non-props-table + & tbody tr td:last-of-type {
|
||||
min-width: unset;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
interface DocsTableProps {
|
||||
children?: ReactNode | ReactNode[];
|
||||
}
|
||||
|
||||
const DocsTable = ({ children = [] }: DocsTableProps) => {
|
||||
return (
|
||||
<StyledTableContainer>
|
||||
<Table sx={{ width: '100%' }} aria-label="doc table">
|
||||
{children}
|
||||
</Table>
|
||||
</StyledTableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocsTable;
|
@ -0,0 +1,13 @@
|
||||
import MuiTableBody from '@mui/material/TableBody';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface TableBodyProps {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const TableBody = ({ children }: TableBodyProps) => {
|
||||
return <MuiTableBody>{children}</MuiTableBody>;
|
||||
};
|
||||
|
||||
export default TableBody;
|
@ -0,0 +1,37 @@
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface TableBodyCellProps {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const TableBodyCell = ({ children }: TableBodyCellProps) => {
|
||||
return (
|
||||
<TableCell
|
||||
scope="row"
|
||||
sx={{
|
||||
padding: '16px 12px',
|
||||
'&:first-of-type, &:first-of-type': {
|
||||
paddingLeft: 0,
|
||||
},
|
||||
'&:last-of-type, &:last-of-type': {
|
||||
paddingRight: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
fontSize: '13px',
|
||||
fontFamily: 'Consolas, Menlo, Monaco, Andale Mono, Ubuntu Mono, monospace',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableBodyCell;
|
@ -0,0 +1,13 @@
|
||||
import MuiTableHead from '@mui/material/TableHead';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface TableHeadProps {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const TableHead = ({ children }: TableHeadProps) => {
|
||||
return <MuiTableHead>{children}</MuiTableHead>;
|
||||
};
|
||||
|
||||
export default TableHead;
|
@ -0,0 +1,28 @@
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface TableHeaderCellProps {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const TableHeaderCell = ({ children }: TableHeaderCellProps) => {
|
||||
return (
|
||||
<TableCell
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
padding: '16px 12px',
|
||||
'&:first-of-type, &:first-of-type': {
|
||||
paddingLeft: 0,
|
||||
},
|
||||
'&:last-of-type, &:last-of-type': {
|
||||
paddingRight: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TableCell>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableHeaderCell;
|
@ -0,0 +1,15 @@
|
||||
import MuiTableRow from '@mui/material/TableRow';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface TableRowProps {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const TableRow = ({ children }: TableRowProps) => {
|
||||
return (
|
||||
<MuiTableRow sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>{children}</MuiTableRow>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableRow;
|
@ -0,0 +1,95 @@
|
||||
import { styled } from '@mui/material/styles';
|
||||
|
||||
import type { NestedHeading } from './DocsTableOfContents';
|
||||
|
||||
const StyledList = styled('ul')(
|
||||
({ theme }) => `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
|
||||
${theme.breakpoints.down('lg')} {
|
||||
margin-top: 0;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledListItem = styled('li')(
|
||||
({ theme }) => `
|
||||
& > a {
|
||||
display: flex;
|
||||
padding-left: 32px;
|
||||
text-indent: -16px;
|
||||
color: ${theme.palette.text.secondary};
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 28px;
|
||||
border-left: 2px solid transparent;
|
||||
}
|
||||
|
||||
&.active > a {
|
||||
color: ${theme.palette.primary.main};
|
||||
border-left: 2px solid ${theme.palette.primary.main};
|
||||
}
|
||||
|
||||
& > a:hover {
|
||||
color: ${theme.palette.primary.main};
|
||||
text-decoration: underline;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledChildListItem = styled(StyledListItem)`
|
||||
& > a {
|
||||
padding-left: 48px;
|
||||
text-indent: -16px;
|
||||
}
|
||||
`;
|
||||
|
||||
interface DocsHeadingsProps {
|
||||
headings: NestedHeading[];
|
||||
activeId: string | undefined;
|
||||
}
|
||||
|
||||
const DocsHeadings = ({ headings, activeId }: DocsHeadingsProps) => (
|
||||
<StyledList>
|
||||
{headings.map(heading => (
|
||||
<StyledListItem key={heading.id} className={heading.id === activeId ? 'active' : ''}>
|
||||
<a
|
||||
href={`#${heading.id}`}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
document.querySelector(`#${heading.id}`)?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}}
|
||||
>
|
||||
{heading.title}
|
||||
</a>
|
||||
{heading.items.length > 0 && (
|
||||
<StyledList>
|
||||
{heading.items.map(child => (
|
||||
<StyledChildListItem key={child.id} className={child.id === activeId ? 'active' : ''}>
|
||||
<a
|
||||
href={`#${child.id}`}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
document.querySelector(`#${child.id}`)?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}}
|
||||
>
|
||||
{child.title}
|
||||
</a>
|
||||
</StyledChildListItem>
|
||||
))}
|
||||
</StyledList>
|
||||
)}
|
||||
</StyledListItem>
|
||||
))}
|
||||
</StyledList>
|
||||
);
|
||||
|
||||
export default DocsHeadings;
|
@ -0,0 +1,144 @@
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { isNotEmpty } from '../../../util/string.util';
|
||||
import DocsHeadings from './DocsHeadings';
|
||||
|
||||
export interface Heading {
|
||||
id: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface NestedHeading extends Heading {
|
||||
items: Heading[];
|
||||
}
|
||||
|
||||
const getNestedHeadings = (headingElements: HTMLHeadingElement[]) => {
|
||||
const nestedHeadings: NestedHeading[] = [];
|
||||
|
||||
headingElements.forEach(heading => {
|
||||
const { innerText: title, id } = heading;
|
||||
|
||||
if (heading.nodeName === 'H1' || heading.nodeName === 'H2') {
|
||||
nestedHeadings.push({ id, title, items: [] });
|
||||
} else if (heading.nodeName === 'H3' && nestedHeadings.length > 0) {
|
||||
nestedHeadings[nestedHeadings.length - 1].items.push({
|
||||
id,
|
||||
title,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return nestedHeadings;
|
||||
};
|
||||
|
||||
const useHeadingsData = () => {
|
||||
const [nestedHeadings, setNestedHeadings] = useState<NestedHeading[]>([]);
|
||||
const { asPath } = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const headingElements = Array.from(
|
||||
document.querySelectorAll<HTMLHeadingElement>('main h1, main h2, main h3'),
|
||||
);
|
||||
|
||||
// Created a list of headings, with H3s nested
|
||||
const newNestedHeadings = getNestedHeadings(headingElements);
|
||||
setNestedHeadings(newNestedHeadings);
|
||||
}, [asPath]);
|
||||
|
||||
return { nestedHeadings };
|
||||
};
|
||||
|
||||
const useIntersectionObserver = (setActiveId: (activeId: string) => void) => {
|
||||
const headingElementsRef = useRef<Record<string, IntersectionObserverEntry>>({});
|
||||
const { asPath } = useRouter();
|
||||
useEffect(() => {
|
||||
const headingElements = Array.from(
|
||||
document.querySelectorAll<HTMLHeadingElement>('main h1, main h2, main h3'),
|
||||
);
|
||||
|
||||
if (headingElementsRef.current) {
|
||||
headingElementsRef.current = {};
|
||||
}
|
||||
|
||||
const callback: IntersectionObserverCallback = headings => {
|
||||
headingElementsRef.current = headings.reduce((map, headingElement) => {
|
||||
map[headingElement.target.id] = headingElement;
|
||||
return map;
|
||||
}, headingElementsRef.current as Record<string, IntersectionObserverEntry>);
|
||||
|
||||
// Get all headings that are currently visible on the page
|
||||
const visibleHeadings: IntersectionObserverEntry[] = [];
|
||||
Object.keys(headingElementsRef.current).forEach(key => {
|
||||
const headingElement = (
|
||||
headingElementsRef.current as Record<string, IntersectionObserverEntry>
|
||||
)[key];
|
||||
if (headingElement.isIntersecting && isNotEmpty(headingElement.target.textContent)) {
|
||||
visibleHeadings.push(headingElement);
|
||||
}
|
||||
});
|
||||
|
||||
const getIndexFromId = (id: string) =>
|
||||
headingElements.findIndex(heading => heading.id === id);
|
||||
|
||||
// If there is only one visible heading, this is our "active" heading
|
||||
if (visibleHeadings.length === 1) {
|
||||
setActiveId(visibleHeadings[0].target.id);
|
||||
// If there is more than one visible heading,
|
||||
// choose the one that is closest to the top of the page
|
||||
} else if (visibleHeadings.length > 1) {
|
||||
const sortedVisibleHeadings = visibleHeadings.sort((a, b) =>
|
||||
getIndexFromId(a.target.id) > getIndexFromId(b.target.id) ? 1 : -1,
|
||||
);
|
||||
|
||||
setActiveId(sortedVisibleHeadings[0].target.id);
|
||||
}
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver(callback, {
|
||||
rootMargin: '0px 0px -36px 0px',
|
||||
});
|
||||
|
||||
headingElements.forEach(element => observer.observe(element));
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [setActiveId, asPath]);
|
||||
};
|
||||
|
||||
const StyledNav = styled('nav')(
|
||||
({ theme }) => `
|
||||
width: 100%;
|
||||
padding: 0 20px 16px 0;
|
||||
align-self: flex-start;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
max-height: calc(100vh - 72px);
|
||||
overflow-y: auto;
|
||||
top: 16px;
|
||||
|
||||
${theme.breakpoints.between('md', 'lg')} {
|
||||
top: 24px;
|
||||
}
|
||||
|
||||
${theme.breakpoints.down('md')} {
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const DocsTableOfContents = () => {
|
||||
const [activeId, setActiveId] = useState<string>();
|
||||
const { nestedHeadings } = useHeadingsData();
|
||||
useIntersectionObserver(setActiveId);
|
||||
|
||||
return (
|
||||
<StyledNav aria-label="Table of contents">
|
||||
<DocsHeadings headings={nestedHeadings} activeId={activeId} />
|
||||
</StyledNav>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocsTableOfContents;
|
28
packages/docs/src/components/layout/Container.tsx
Normal file
28
packages/docs/src/components/layout/Container.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { styled } from '@mui/material/styles';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
const StyledContainer = styled('div')(
|
||||
({ theme }) => `
|
||||
max-width: 1280px;
|
||||
width: 100%;
|
||||
padding: 0 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
${theme.breakpoints.down('md')} {
|
||||
padding: 0 32px;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
export interface PageProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const Container = ({ children }: PageProps) => {
|
||||
return <StyledContainer>{children}</StyledContainer>;
|
||||
};
|
||||
|
||||
export default Container;
|
69
packages/docs/src/components/layout/Footer.tsx
Normal file
69
packages/docs/src/components/layout/Footer.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import Button from '@mui/material/Button';
|
||||
import Link from '@mui/material/Link';
|
||||
import { styled } from '@mui/material/styles';
|
||||
|
||||
import config from '../../lib/config';
|
||||
import Container from './Container';
|
||||
|
||||
const StyledFooter = styled('footer')(
|
||||
({ theme }) => `
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: ${theme.palette.mode === 'light' ? '#dddee2' : '#242424'};
|
||||
padding: 24px 0 40px;
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledFooterContent = styled('footer')`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const StyledButtons = styled('div')`
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
const StyledLinks = styled('div')`
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
||||
|
||||
const Footer = () => {
|
||||
return (
|
||||
<StyledFooter>
|
||||
<Container>
|
||||
<StyledFooterContent>
|
||||
<StyledButtons>
|
||||
{config.footer.buttons.map(button => (
|
||||
<Button key={button.url} href={button.url} target="_blank" variant="contained">
|
||||
{button.text}
|
||||
</Button>
|
||||
))}
|
||||
</StyledButtons>
|
||||
<StyledLinks>
|
||||
{config.footer.links.map(link => (
|
||||
<StyledLink key={link.url} href={link.url} target="_blank" color="text.primary">
|
||||
{link.text}
|
||||
</StyledLink>
|
||||
))}
|
||||
</StyledLinks>
|
||||
</StyledFooterContent>
|
||||
</Container>
|
||||
</StyledFooter>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
209
packages/docs/src/components/layout/Header.tsx
Normal file
209
packages/docs/src/components/layout/Header.tsx
Normal file
@ -0,0 +1,209 @@
|
||||
import Brightness4Icon from '@mui/icons-material/Brightness4';
|
||||
import Brightness7Icon from '@mui/icons-material/Brightness7';
|
||||
import GitHubIcon from '@mui/icons-material/GitHub';
|
||||
import MenuIcon from '@mui/icons-material/Menu';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Button from '@mui/material/Button';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { styled, useTheme } from '@mui/material/styles';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import Link from 'next/link';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import Logo from './Logo';
|
||||
import NavigationDrawer from './mobile-drawer/NavigationDrawer';
|
||||
import Search from './search/Search';
|
||||
|
||||
import type { PaletteMode } from '@mui/material';
|
||||
import type { ButtonTypeMap } from '@mui/material/Button';
|
||||
import type { ExtendButtonBase } from '@mui/material/ButtonBase';
|
||||
import type { DocsGroup, MenuItem, SearchablePage } from '../../interface';
|
||||
|
||||
const StyledAppBar = styled(AppBar)(
|
||||
({ theme }) => `
|
||||
background: ${theme.palette.mode === 'light' ? theme.palette.primary.main : '#121212'};
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledToolbar = styled(Toolbar)(
|
||||
({ theme }) => `
|
||||
gap: 16px;
|
||||
height: 72px;
|
||||
|
||||
${theme.breakpoints.down('lg')} {
|
||||
justify-content: space-between;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledIconsWrapper = styled('div')(
|
||||
({ theme }) => `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
${theme.breakpoints.up('lg')} {
|
||||
flex-grow: 1;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledGithubLink = styled('a')(
|
||||
({ theme }) => `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
${theme.breakpoints.down('lg')} {
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledGithubImage = styled('img')`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const StyledMenuButton = styled(IconButton)(
|
||||
({ theme }) => `
|
||||
${theme.breakpoints.up('lg')} {
|
||||
visibility: hidden;
|
||||
height: 0;
|
||||
width: 0;
|
||||
padding: 0;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledDesktopGap = styled('div')(
|
||||
({ theme }) => `
|
||||
flex-grow: 1;
|
||||
|
||||
${theme.breakpoints.down('lg')} {
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledDesktopLink = styled(Button)(
|
||||
({ theme }) => `
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
${theme.breakpoints.down('lg')} {
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
) as ExtendButtonBase<ButtonTypeMap<{}, 'a'>>;
|
||||
|
||||
interface HeaderProps {
|
||||
mode: PaletteMode;
|
||||
docsGroups: DocsGroup[];
|
||||
searchablePages: SearchablePage[];
|
||||
toggleColorMode: () => void;
|
||||
}
|
||||
|
||||
const Header = ({ mode, docsGroups, searchablePages, toggleColorMode }: HeaderProps) => {
|
||||
const theme = useTheme();
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
|
||||
const handleDrawerToggle = useCallback(() => {
|
||||
setMobileOpen(!mobileOpen);
|
||||
}, [mobileOpen]);
|
||||
|
||||
const items: MenuItem[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: 'Docs',
|
||||
path: '/docs',
|
||||
groups: docsGroups.map(group => ({
|
||||
title: group.title,
|
||||
links: group.links.map(link => ({
|
||||
title: link.title,
|
||||
url: `/docs/${link.slug}`,
|
||||
})),
|
||||
})),
|
||||
},
|
||||
{
|
||||
title: 'Contributing',
|
||||
url: '/docs/contributor-guide',
|
||||
},
|
||||
{
|
||||
title: 'Community',
|
||||
url: '/community',
|
||||
},
|
||||
],
|
||||
[docsGroups],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledAppBar position="fixed">
|
||||
<StyledToolbar>
|
||||
<StyledMenuButton
|
||||
size="large"
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="menu"
|
||||
onClick={handleDrawerToggle}
|
||||
>
|
||||
<MenuIcon fontSize="large" />
|
||||
</StyledMenuButton>
|
||||
<Logo />
|
||||
<StyledIconsWrapper>
|
||||
<Search searchablePages={searchablePages} />
|
||||
<IconButton
|
||||
sx={{ [theme.breakpoints.up('lg')]: { ml: 1 } }}
|
||||
onClick={toggleColorMode}
|
||||
color="inherit"
|
||||
title={mode === 'dark' ? 'Turn on the light' : 'Turn off the light'}
|
||||
>
|
||||
{mode === 'dark' ? <Brightness7Icon /> : <Brightness4Icon />}
|
||||
</IconButton>
|
||||
<StyledDesktopGap />
|
||||
<StyledGithubLink
|
||||
href="https://github.com/StaticJsCMS/static-cms"
|
||||
aria-label="Star StaticJsCMS/static-cms on GitHub"
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<StyledGithubImage
|
||||
alt="Star StaticJsCMS/static-cms on GitHub"
|
||||
src="https://img.shields.io/github/stars/StaticJsCMS/static-cms?style=social"
|
||||
/>
|
||||
</StyledGithubLink>
|
||||
<IconButton href="https://github.com/StaticJsCMS/static-cms" color="inherit">
|
||||
<GitHubIcon />
|
||||
</IconButton>
|
||||
</StyledIconsWrapper>
|
||||
{items.map(item => {
|
||||
let url = '#';
|
||||
if ('url' in item) {
|
||||
url = item.url;
|
||||
} else if (item.groups.length > 0 && item.groups[0].links.length > 0) {
|
||||
url = item.groups[0].links[0].url;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledDesktopLink key={`desktop-${item.title}-${url}`} component={Link} href={url}>
|
||||
{item.title}
|
||||
</StyledDesktopLink>
|
||||
);
|
||||
})}
|
||||
{/*
|
||||
<StyledDesktopLink component={Link} href="/blog">Blog</StyledDesktopLink>
|
||||
*/}
|
||||
</StyledToolbar>
|
||||
</StyledAppBar>
|
||||
<NavigationDrawer
|
||||
key="mobile-navigation-drawer"
|
||||
items={items}
|
||||
mobileOpen={mobileOpen}
|
||||
onMobileOpenToggle={handleDrawerToggle}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
22
packages/docs/src/components/layout/Logo.tsx
Normal file
22
packages/docs/src/components/layout/Logo.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { styled } from '@mui/material/styles';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
const StyledImageLink = styled(Link)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const StyledImage = styled(Image)`
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const Logo = () => {
|
||||
return (
|
||||
<StyledImageLink href="/">
|
||||
<StyledImage src="/static-cms-logo.svg" alt="Static CMS" width={182} height={72} />
|
||||
</StyledImageLink>
|
||||
);
|
||||
};
|
||||
|
||||
export default Logo;
|
107
packages/docs/src/components/layout/Page.tsx
Normal file
107
packages/docs/src/components/layout/Page.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import { styled, ThemeProvider, useTheme } from '@mui/material/styles';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useContext, useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
import ColorModeContext from '../context/ColorModeContext';
|
||||
import BasicMeta from '../meta/BasicMeta';
|
||||
import JsonLdMeta from '../meta/JsonLdMeta';
|
||||
import OpenGraphMeta from '../meta/OpenGraphMeta';
|
||||
import TwitterCardMeta from '../meta/TwitterCardMeta';
|
||||
import Container from './Container';
|
||||
import Header from './Header';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import type { DocsGroup, SearchablePage } from '../../interface';
|
||||
|
||||
const StyledPageContentWrapper = styled('div')`
|
||||
display: block;
|
||||
height: calc(100vh - 72px);
|
||||
width: 100%;
|
||||
position: relative;
|
||||
top: 72px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
`;
|
||||
|
||||
export interface PageProps {
|
||||
title?: string;
|
||||
url: string;
|
||||
keywords?: string[];
|
||||
description?: string;
|
||||
children: ReactNode;
|
||||
pageDetails?: {
|
||||
date?: Date;
|
||||
image?: string;
|
||||
};
|
||||
fullWidth?: boolean;
|
||||
docsGroups: DocsGroup[];
|
||||
searchablePages: SearchablePage[];
|
||||
}
|
||||
|
||||
const Page = ({
|
||||
children,
|
||||
title,
|
||||
url,
|
||||
keywords,
|
||||
description,
|
||||
pageDetails,
|
||||
fullWidth = false,
|
||||
docsGroups,
|
||||
searchablePages,
|
||||
}: PageProps) => {
|
||||
const scrollableArea = useRef<HTMLDivElement | null>(null);
|
||||
const theme = useTheme();
|
||||
const colorMode = useContext(ColorModeContext);
|
||||
const { asPath } = useRouter();
|
||||
|
||||
const content = useMemo(() => {
|
||||
if (fullWidth) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return <Container>{children}</Container>;
|
||||
}, [children, fullWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!asPath.includes('#')) {
|
||||
scrollableArea.current?.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'auto',
|
||||
});
|
||||
}
|
||||
}, [asPath]);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<BasicMeta url={url} title={title} keywords={keywords} description={description} />
|
||||
<OpenGraphMeta url={url} title={title} image={pageDetails?.image} description={description} />
|
||||
<TwitterCardMeta
|
||||
url={url}
|
||||
title={title}
|
||||
image={pageDetails?.image}
|
||||
description={description}
|
||||
/>
|
||||
{pageDetails ? (
|
||||
<JsonLdMeta
|
||||
url={url}
|
||||
title={title}
|
||||
keywords={keywords}
|
||||
date={pageDetails.date}
|
||||
image={pageDetails.image}
|
||||
description={description}
|
||||
/>
|
||||
) : null}
|
||||
<Header
|
||||
mode={theme.palette.mode}
|
||||
docsGroups={docsGroups}
|
||||
searchablePages={searchablePages}
|
||||
toggleColorMode={colorMode.toggleColorMode}
|
||||
/>
|
||||
<StyledPageContentWrapper ref={scrollableArea}>{content}</StyledPageContentWrapper>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
@ -0,0 +1,127 @@
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import List from '@mui/material/List';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import Link from 'next/link';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import MobileNavLink from './MobileNavLink';
|
||||
|
||||
import type { MouseEvent } from 'react';
|
||||
import type { MenuItem, MenuLink, MenuLinkGroup } from '../../../interface';
|
||||
|
||||
interface MobileNavItemProps {
|
||||
item: MenuItem;
|
||||
}
|
||||
|
||||
function isMenuLinkGroup(link: MenuItem): link is MenuLinkGroup {
|
||||
return 'groups' in link;
|
||||
}
|
||||
|
||||
const MobileNavItem = ({ item }: MobileNavItemProps) => {
|
||||
const theme = useTheme();
|
||||
const { asPath } = useRouter();
|
||||
|
||||
const selected = useMemo(() => {
|
||||
if ('url' in item) {
|
||||
return asPath === item.url;
|
||||
}
|
||||
|
||||
return asPath.startsWith(item.path);
|
||||
}, [asPath, item]);
|
||||
|
||||
const [open, setOpen] = useState(selected);
|
||||
|
||||
const handleOnClick = useCallback(
|
||||
(link: MenuItem | MenuLink) => (event: MouseEvent) => {
|
||||
if (isMenuLinkGroup(link)) {
|
||||
event.stopPropagation();
|
||||
setOpen(!open);
|
||||
return;
|
||||
}
|
||||
},
|
||||
[open],
|
||||
);
|
||||
|
||||
const url = useMemo(() => {
|
||||
if (isMenuLinkGroup(item)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return item.url;
|
||||
}, [item]);
|
||||
|
||||
const wrappedLink = useMemo(
|
||||
() => (
|
||||
<ListItemButton
|
||||
component={url ? Link : 'button'}
|
||||
href={url}
|
||||
target={url?.startsWith('http') ? '_blank' : undefined}
|
||||
key={`drawer-nav-item-${item.title}`}
|
||||
onClick={handleOnClick(item)}
|
||||
selected={selected}
|
||||
>
|
||||
<ListItemText primary={item.title} />
|
||||
{isMenuLinkGroup(item) ? (
|
||||
<ExpandLessIcon
|
||||
sx={{
|
||||
transform: `rotateZ(${open ? 0 : 90}deg)`,
|
||||
transition: theme.transitions.create(['transform']),
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</ListItemButton>
|
||||
),
|
||||
[handleOnClick, item, open, selected, theme.transitions, url],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{wrappedLink}
|
||||
{isMenuLinkGroup(item) ? (
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
{item.groups.map(group => (
|
||||
<List
|
||||
key={group.title}
|
||||
component="div"
|
||||
subheader={
|
||||
<ListSubheader
|
||||
component="div"
|
||||
id="nested-list-subheader"
|
||||
sx={{
|
||||
lineHeight: '32px',
|
||||
textTransform: 'uppercase',
|
||||
top: '-1px',
|
||||
}}
|
||||
>
|
||||
{group.title}
|
||||
</ListSubheader>
|
||||
}
|
||||
disablePadding
|
||||
sx={{
|
||||
marginTop: '8px',
|
||||
'&:not(:first-of-type)': {
|
||||
marginTop: '20px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{group.links.map(link => (
|
||||
<MobileNavLink
|
||||
key={`drawer-nav-item-${item.title}-sub-item-${link.title}`}
|
||||
link={link}
|
||||
onClick={handleOnClick(link)}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
))}
|
||||
</Collapse>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileNavItem;
|
@ -0,0 +1,37 @@
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import type { MouseEvent } from 'react';
|
||||
import type { MenuLink } from '../../../interface';
|
||||
|
||||
interface MobileNavLinkProps {
|
||||
link: MenuLink;
|
||||
onClick: (event: MouseEvent) => void;
|
||||
}
|
||||
|
||||
const MobileNavLink = ({ link, onClick }: MobileNavLinkProps) => {
|
||||
const { title, url } = link;
|
||||
const { asPath } = useRouter();
|
||||
|
||||
const selected = useMemo(() => {
|
||||
return asPath === url;
|
||||
}, [asPath, url]);
|
||||
|
||||
return (
|
||||
<ListItemButton
|
||||
component={Link}
|
||||
href={url}
|
||||
target={url.startsWith('http') ? '_blank' : undefined}
|
||||
sx={{ paddingLeft: '24px', paddingTop: '4px', paddingBottom: '4px' }}
|
||||
onClick={onClick}
|
||||
selected={selected}
|
||||
>
|
||||
<ListItemText primary={title} />
|
||||
</ListItemButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileNavLink;
|
@ -0,0 +1,104 @@
|
||||
import Divider from '@mui/material/Divider';
|
||||
import List from '@mui/material/List';
|
||||
import { styled, useTheme } from '@mui/material/styles';
|
||||
import SwipeableDrawer from '@mui/material/SwipeableDrawer';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import MobileNavItem from './MobileNavItem';
|
||||
import Logo from '../Logo';
|
||||
|
||||
import type { MenuItem } from '../../../interface';
|
||||
|
||||
const DRAWER_WIDTH = 300;
|
||||
|
||||
const StyledDrawerContents = styled('div')`
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const StyledLogoWrapper = styled('div')(
|
||||
({ theme }) => `
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 16px 0;
|
||||
background: ${
|
||||
theme.palette.mode === 'light' ? theme.palette.primary.main : theme.palette.background.paper
|
||||
};
|
||||
`,
|
||||
);
|
||||
|
||||
interface NavigationDrawerProps {
|
||||
items: MenuItem[];
|
||||
mobileOpen: boolean;
|
||||
onMobileOpenToggle: () => void;
|
||||
}
|
||||
|
||||
const NavigationDrawer = ({ items, mobileOpen, onMobileOpenToggle }: NavigationDrawerProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const iOS = useMemo(
|
||||
() => typeof navigator !== 'undefined' && /iPad|iPhone|iPod/.test(navigator.userAgent),
|
||||
[],
|
||||
);
|
||||
|
||||
const drawer = useMemo(
|
||||
() => (
|
||||
<StyledDrawerContents key="drawer-nav-contents" onClick={onMobileOpenToggle}>
|
||||
<StyledLogoWrapper key="drawer-nav-logo-wrapper">
|
||||
<Logo key="drawer-nav-logo" />
|
||||
</StyledLogoWrapper>
|
||||
<Divider key="drawer-nav-divider" sx={{ borderColor: 'rgba(255, 255, 255, 0.8)' }} />
|
||||
<List key="drawer-nav-list">
|
||||
{items.map(item => (
|
||||
<MobileNavItem key={`drawer-nav-item-${item.title}`} item={item} />
|
||||
))}
|
||||
</List>
|
||||
</StyledDrawerContents>
|
||||
),
|
||||
[items, onMobileOpenToggle],
|
||||
);
|
||||
|
||||
const container = useMemo(
|
||||
() => (typeof window !== 'undefined' ? window.document.body : undefined),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<SwipeableDrawer
|
||||
key="swipable-drawer"
|
||||
disableBackdropTransition={!iOS}
|
||||
disableDiscovery={iOS}
|
||||
container={container}
|
||||
variant="temporary"
|
||||
open={mobileOpen}
|
||||
onOpen={onMobileOpenToggle}
|
||||
onClose={onMobileOpenToggle}
|
||||
ModalProps={{
|
||||
keepMounted: true, // Better open performance on mobile.
|
||||
}}
|
||||
sx={{
|
||||
display: 'none',
|
||||
[theme.breakpoints.down('lg')]: {
|
||||
display: 'block',
|
||||
},
|
||||
width: '80%',
|
||||
maxWidth: DRAWER_WIDTH,
|
||||
'& .MuiBackdrop-root': {
|
||||
width: '100%',
|
||||
},
|
||||
'& .MuiDrawer-paper': {
|
||||
boxSizing: 'border-box',
|
||||
width: '80%',
|
||||
maxWidth: DRAWER_WIDTH,
|
||||
background: theme.palette.background.paper,
|
||||
},
|
||||
'& .MuiListSubheader-root': {
|
||||
textAlign: 'left',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{drawer}
|
||||
</SwipeableDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavigationDrawer;
|
85
packages/docs/src/components/layout/search/Search.tsx
Normal file
85
packages/docs/src/components/layout/search/Search.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import Button from '@mui/material/Button';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useCallback, useState } from 'react';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
|
||||
import SearchModal from './SearchModal';
|
||||
|
||||
import type { SearchablePage } from '../../../interface';
|
||||
|
||||
const StyledSearchPlaceholderBox = styled(Button)(
|
||||
({ theme }) => `
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
width: 244px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 4px;
|
||||
padding: 0 12px;
|
||||
color: inherit;
|
||||
text-transform: none;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
${theme.breakpoints.down('lg')} {
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledIconButton = styled(IconButton)(
|
||||
({ theme }) => `
|
||||
${theme.breakpoints.up('lg')} {
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
export interface SearchProps {
|
||||
searchablePages: SearchablePage[];
|
||||
}
|
||||
|
||||
const Search = ({ searchablePages }: SearchProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleOpen = useCallback(() => {
|
||||
setOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledSearchPlaceholderBox onClick={handleOpen}>
|
||||
<SearchIcon />
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="inherit"
|
||||
sx={{
|
||||
fontFamily: "'Roboto',-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif",
|
||||
fontWeight: 400,
|
||||
fontSize: '16px',
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
Search the docs
|
||||
</Typography>
|
||||
</StyledSearchPlaceholderBox>
|
||||
<StyledIconButton onClick={handleOpen} color="inherit">
|
||||
<SearchIcon />
|
||||
</StyledIconButton>
|
||||
<SearchModal open={open} onClose={handleClose} searchablePages={searchablePages} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Search;
|
257
packages/docs/src/components/layout/search/SearchModal.tsx
Normal file
257
packages/docs/src/components/layout/search/SearchModal.tsx
Normal file
@ -0,0 +1,257 @@
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import Button from '@mui/material/Button';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import { styled, useTheme } from '@mui/material/styles';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { useSearchScores } from '../../../util/search.util';
|
||||
import { isNotEmpty } from '../../../util/string.util';
|
||||
import SearchResult from './SearchResult';
|
||||
import SuggestionLink from './SuggestionLink';
|
||||
|
||||
import type { ChangeEvent, FC } from 'react';
|
||||
import type { SearchablePage } from '../../../interface';
|
||||
|
||||
const SEARCH_RESULTS_TO_SHOW = 5;
|
||||
|
||||
const StyledDialog = styled(Dialog)(
|
||||
({ theme }) => `
|
||||
${theme.breakpoints.between('md', 'lg')} {
|
||||
& .MuiDialog-paper {
|
||||
width: 60vw;
|
||||
height: 60vh;
|
||||
maxHeight: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
${theme.breakpoints.up('lg')} {
|
||||
& .MuiDialog-paper {
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
}
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledDialogContent = styled(DialogContent)`
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
const StyledTextField = styled(TextField)`
|
||||
height: 68px;
|
||||
|
||||
& .MuiInputBase-root {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
& .MuiInputBase-root.MuiInput-root::after {
|
||||
border-bottom: unset;
|
||||
}
|
||||
|
||||
& .MuiInput-root {
|
||||
font-size: 18px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledSuggestions = styled('div')`
|
||||
display: grid;
|
||||
gap: 48px;
|
||||
padding: 24px;
|
||||
grid-template-columns: repeat(2, minmax(0, 2fr));
|
||||
grid-template-rows: repeat(2, minmax(0, 2fr));
|
||||
`;
|
||||
|
||||
const StyledSuggestionSection = styled('div')`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
`;
|
||||
|
||||
interface SearchModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
searchablePages: SearchablePage[];
|
||||
}
|
||||
|
||||
const SearchModal: FC<SearchModalProps> = ({ open, onClose, searchablePages }) => {
|
||||
const inputRef = useRef<HTMLInputElement>();
|
||||
|
||||
const theme = useTheme();
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
const [canFocus, setCanFocus] = useState(true);
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const handleFocus = () => {
|
||||
if (canFocus && open && inputRef.current) {
|
||||
inputRef.current?.focus();
|
||||
setSearch('');
|
||||
setCanFocus(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setCanFocus(true);
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
});
|
||||
}, [onClose]);
|
||||
|
||||
const handleSearchChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
setSearch(event.target.value);
|
||||
}, []);
|
||||
|
||||
const searchResults = useSearchScores(search, searchablePages);
|
||||
|
||||
const renderedResults = useMemo(
|
||||
() =>
|
||||
searchResults?.length > 0 ? (
|
||||
[...Array<unknown>(SEARCH_RESULTS_TO_SHOW)].map((_, index) => {
|
||||
if (searchResults.length <= index) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = searchResults[index];
|
||||
const { entry } = result;
|
||||
let summary = entry.textContent;
|
||||
|
||||
if (!result.isExactTitleMatch) {
|
||||
const match = new RegExp(
|
||||
`(?:[\\s]+[^\\s]+){0,10}[\\s]*${search}(?![^<>]*(([/"']|]]|\\b)>))[\\s]*(?:[^\\s]+\\s){0,25}`,
|
||||
'ig',
|
||||
).exec(entry.textContent);
|
||||
if (match && match.length >= 1) {
|
||||
summary = `...${match[0].trim()}...`;
|
||||
} else {
|
||||
const match = new RegExp(
|
||||
`(?:[\\s]+[^\\s]+){0,10}[\\s]*(${search
|
||||
.split(' ')
|
||||
.join('|')})(?![^<>]*(([/"']|]]|\\b)>))[\\s]*(?:[^\\s]+\\s){0,25}`,
|
||||
'ig',
|
||||
).exec(entry.textContent);
|
||||
if (match && match.length >= 1) {
|
||||
summary = `...${match[0].trim()}...`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
summary = summary?.replace(
|
||||
new RegExp(`(${search.split(' ').join('|')})(?![^<>]*(([/"']|]]|\\b)>))`, 'ig'),
|
||||
`<strong style="color: ${theme.palette.primary.main}">$1</strong>`,
|
||||
);
|
||||
|
||||
return (
|
||||
<SearchResult
|
||||
key={`result-${entry.url}`}
|
||||
entry={entry}
|
||||
summary={summary}
|
||||
onClick={handleClose}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : isNotEmpty(search) ? (
|
||||
<Typography
|
||||
variant="h3"
|
||||
component="div"
|
||||
key="no-results"
|
||||
sx={{ width: '100%', textAlign: 'center', marginTop: '16px' }}
|
||||
>
|
||||
No results found
|
||||
</Typography>
|
||||
) : (
|
||||
<StyledSuggestions>
|
||||
<StyledSuggestionSection>
|
||||
<Typography variant="h3" sx={{ marginBottom: '4px' }}>
|
||||
Getting Started
|
||||
</Typography>
|
||||
<SuggestionLink href="/docs/start-with-a-template">
|
||||
Start With a Template
|
||||
</SuggestionLink>
|
||||
<SuggestionLink href="/docs/add-to-your-site">Add to Your Site</SuggestionLink>
|
||||
<SuggestionLink href="/docs/configuration-options">
|
||||
Configuration Options
|
||||
</SuggestionLink>
|
||||
<SuggestionLink href="/docs/collection-overview">Collections</SuggestionLink>
|
||||
</StyledSuggestionSection>
|
||||
<StyledSuggestionSection>
|
||||
<Typography variant="h3" sx={{ marginBottom: '4px' }}>
|
||||
Backends
|
||||
</Typography>
|
||||
<SuggestionLink href="/docs/github-backend">GitHub</SuggestionLink>
|
||||
<SuggestionLink href="/docs/bitbucket-backend">Bitbucket</SuggestionLink>
|
||||
<SuggestionLink href="/docs/gitlab-backend">GitLab</SuggestionLink>
|
||||
</StyledSuggestionSection>
|
||||
<StyledSuggestionSection>
|
||||
<Typography variant="h3" sx={{ marginBottom: '4px' }}>
|
||||
Platform Guides
|
||||
</Typography>
|
||||
<SuggestionLink href="/docs/nextjs">Next</SuggestionLink>
|
||||
<SuggestionLink href="/docs/gatsby">Gatsby</SuggestionLink>
|
||||
<SuggestionLink href="/docs/jekyll">Jekyll</SuggestionLink>
|
||||
<SuggestionLink href="/docs/hugo">Hugo</SuggestionLink>
|
||||
</StyledSuggestionSection>
|
||||
<StyledSuggestionSection>
|
||||
<Typography variant="h3" sx={{ marginBottom: '4px' }}>
|
||||
Widgets
|
||||
</Typography>
|
||||
<SuggestionLink href="/docs/widget-string">String</SuggestionLink>
|
||||
<SuggestionLink href="/docs/widget-image">Image</SuggestionLink>
|
||||
<SuggestionLink href="/docs/widget-datetime">Datetime</SuggestionLink>
|
||||
<SuggestionLink href="/docs/widget-markdown">Markdown</SuggestionLink>
|
||||
</StyledSuggestionSection>
|
||||
</StyledSuggestions>
|
||||
),
|
||||
[handleClose, search, searchResults, theme.palette.primary.main],
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledDialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
fullScreen={fullScreen}
|
||||
fullWidth
|
||||
onFocus={handleFocus}
|
||||
>
|
||||
<StyledDialogContent>
|
||||
<StyledTextField
|
||||
autoFocus
|
||||
id="search"
|
||||
placeholder="Search"
|
||||
fullWidth
|
||||
variant="standard"
|
||||
value={search}
|
||||
onChange={handleSearchChange}
|
||||
InputProps={{
|
||||
inputRef,
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
),
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="inherit"
|
||||
sx={{ p: '0 6px', m: 0, width: 'auto', minWidth: 'unset' }}
|
||||
onClick={onClose}
|
||||
>
|
||||
ESC
|
||||
</Button>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<div>{renderedResults}</div>
|
||||
</StyledDialogContent>
|
||||
</StyledDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchModal;
|
117
packages/docs/src/components/layout/search/SearchResult.tsx
Normal file
117
packages/docs/src/components/layout/search/SearchResult.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import Button from '@mui/material/Button';
|
||||
import { styled, useTheme } from '@mui/material/styles';
|
||||
import Link from 'next/link';
|
||||
|
||||
import type { SearchablePage } from '../../../interface';
|
||||
|
||||
const StyledSearchResultBody = styled('div')`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
gap: 8px;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledSearchResultTitleWrapper = styled('div')`
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
const StyledSearchResultTitle = styled('h3')(
|
||||
({ theme }) => `
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
line-height: 22px;
|
||||
color: ${theme.palette.primary.main};
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledSearchResultContent = styled('div')(
|
||||
({ theme }) => `
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
line-height: 1.25;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
color: ${theme.palette.text.secondary};
|
||||
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin-top: 4px;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
interface SearchResultProps {
|
||||
entry: SearchablePage;
|
||||
summary: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const SearchResult = ({ entry: { url, title }, summary, onClick }: SearchResultProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
// const Icon = useMemo(() => {
|
||||
// switch (type) {
|
||||
// case NEWS:
|
||||
// return ArticleIcon;
|
||||
// case BULLETIN:
|
||||
// return NewspaperIcon;
|
||||
// default:
|
||||
// return WebIcon;
|
||||
// }
|
||||
// }, [type]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
component={Link}
|
||||
href={url}
|
||||
key={`result-${url}`}
|
||||
onClick={onClick}
|
||||
// startIcon={<Icon fontSize="large" />}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
textAlign: 'left',
|
||||
width: '100%',
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: 'inherit',
|
||||
letterSpacing: 'inherit',
|
||||
textTransform: 'unset',
|
||||
gap: '8px',
|
||||
padding: '16px',
|
||||
boxSize: 'border-box',
|
||||
borderBottom: '1px solid rgba(255, 255, 255, 0.25)',
|
||||
'&:hover': {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
'.MuiButton-startIcon': {
|
||||
marginLeft: 0,
|
||||
'*:nth-of-type(1)': {
|
||||
fontSize: '24px',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<StyledSearchResultBody>
|
||||
<StyledSearchResultTitleWrapper>
|
||||
<StyledSearchResultTitle>{title}</StyledSearchResultTitle>
|
||||
</StyledSearchResultTitleWrapper>
|
||||
<StyledSearchResultContent
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: summary,
|
||||
}}
|
||||
/>
|
||||
</StyledSearchResultBody>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchResult;
|
@ -0,0 +1,36 @@
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||
import MuiLink from '@mui/material/Link';
|
||||
import Link from 'next/link';
|
||||
|
||||
import type { FC } from 'react';
|
||||
|
||||
interface SuggestionLinkProps {
|
||||
href: string;
|
||||
children: string;
|
||||
}
|
||||
|
||||
const SuggestionLink: FC<SuggestionLinkProps> = ({ href, children }) => {
|
||||
return (
|
||||
<MuiLink
|
||||
component={Link}
|
||||
href={href}
|
||||
color="primary"
|
||||
sx={{
|
||||
textDecoration: 'none',
|
||||
display: 'flex',
|
||||
paddingLeft: '8px',
|
||||
transition: 'gap 0.1s ease-in-out',
|
||||
gap: 0,
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
gap: '4px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon fontSize="small" sx={{ marginTop: '1px', marginLeft: '2px' }} />
|
||||
</MuiLink>
|
||||
);
|
||||
};
|
||||
|
||||
export default SuggestionLink;
|
26
packages/docs/src/components/meta/BasicMeta.tsx
Normal file
26
packages/docs/src/components/meta/BasicMeta.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import Head from 'next/head';
|
||||
|
||||
import config from '../../lib/config';
|
||||
|
||||
interface BasicMetaProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
keywords?: string[];
|
||||
url: string;
|
||||
}
|
||||
|
||||
const BasicMeta = ({ title, description, keywords, url }: BasicMetaProps) => {
|
||||
return (
|
||||
<Head>
|
||||
<title>{title ? [title, config.site_title].join(' | ') : config.site_title}</title>
|
||||
<meta name="description" content={description ? description : config.site_description} />
|
||||
<meta
|
||||
name="keywords"
|
||||
content={keywords ? keywords.join(',') : config.site_keywords.join(',')}
|
||||
/>
|
||||
<link rel="canonical" href={config.base_url.replace(/\/$/g, '') + url} />
|
||||
</Head>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicMeta;
|
37
packages/docs/src/components/meta/JsonLdMeta.tsx
Normal file
37
packages/docs/src/components/meta/JsonLdMeta.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import formatISO from 'date-fns/formatISO';
|
||||
import Head from 'next/head';
|
||||
import { jsonLdScriptProps } from 'react-schemaorg';
|
||||
|
||||
import config from '../../lib/config';
|
||||
|
||||
import type { BlogPosting } from 'schema-dts';
|
||||
|
||||
interface JsonLdMetaProps {
|
||||
url: string;
|
||||
title?: string;
|
||||
keywords?: string[];
|
||||
date?: Date;
|
||||
image?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const JsonLdMeta = ({ url, title, keywords, date, image, description }: JsonLdMetaProps) => {
|
||||
return (
|
||||
<Head>
|
||||
<script
|
||||
{...jsonLdScriptProps<BlogPosting>({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BlogPosting',
|
||||
mainEntityOfPage: config.base_url.replace(/\/$/g, '') + url,
|
||||
headline: title ?? config.site_title,
|
||||
keywords: keywords ? keywords.join(',') : undefined,
|
||||
datePublished: date ? formatISO(date) : undefined,
|
||||
image,
|
||||
description,
|
||||
})}
|
||||
/>
|
||||
</Head>
|
||||
);
|
||||
};
|
||||
|
||||
export default JsonLdMeta;
|
35
packages/docs/src/components/meta/OpenGraphMeta.tsx
Normal file
35
packages/docs/src/components/meta/OpenGraphMeta.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import Head from 'next/head';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import config from '../../lib/config';
|
||||
|
||||
interface OpenGraphMetaProps {
|
||||
url: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
const OpenGraphMeta = ({ url, title, description, image }: OpenGraphMetaProps) => {
|
||||
const imageUrl = useMemo(() => {
|
||||
return image
|
||||
? `${config.base_url.replace(/\/$/g, '')}/${image.replace(/^\//g, '')}`
|
||||
: `${config.base_url.replace(/\/$/g, '')}/${config.site_image.replace(/^\//g, '')}`;
|
||||
}, [image]);
|
||||
|
||||
return (
|
||||
<Head>
|
||||
<meta property="og:site_name" content={config.site_title} />
|
||||
<meta property="og:url" content={config.base_url.replace(/\/$/g, '') + url} />
|
||||
<meta property="og:title" content={title ? [title, config.site_title].join(' | ') : ''} />
|
||||
<meta
|
||||
property="og:description"
|
||||
content={description ? description : config.site_description}
|
||||
/>
|
||||
<meta property="og:image" content={imageUrl} />
|
||||
<meta property="og:type" content="article" />
|
||||
</Head>
|
||||
);
|
||||
};
|
||||
|
||||
export default OpenGraphMeta;
|
37
packages/docs/src/components/meta/TwitterCardMeta.tsx
Normal file
37
packages/docs/src/components/meta/TwitterCardMeta.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import Head from 'next/head';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import config from '../../lib/config';
|
||||
|
||||
interface TwitterCardMetaProps {
|
||||
url: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
const TwitterCardMeta = ({ url, title, description, image }: TwitterCardMetaProps) => {
|
||||
const imageUrl = useMemo(() => {
|
||||
return image
|
||||
? `${config.base_url.replace(/\/$/g, '')}/${image.replace(/^\//g, '')}`
|
||||
: `${config.base_url.replace(/\/$/g, '')}/${config.site_image.replace(/^\//g, '')}`;
|
||||
}, [image]);
|
||||
|
||||
return (
|
||||
<Head>
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:url" content={config.base_url.replace(/\/$/g, '') + url} />
|
||||
<meta
|
||||
property="twitter:title"
|
||||
content={title ? [title, config.site_title].join(' | ') : config.site_title}
|
||||
/>
|
||||
<meta
|
||||
property="twitter:description"
|
||||
content={description ? description : config.site_description}
|
||||
/>
|
||||
<meta property="twitter:image" content={imageUrl} />
|
||||
</Head>
|
||||
);
|
||||
};
|
||||
|
||||
export default TwitterCardMeta;
|
3
packages/docs/src/constants.ts
Normal file
3
packages/docs/src/constants.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const SEARCH_RESULTS_TO_SHOW = 10;
|
||||
|
||||
export const SUMMARY_MIN_PARAGRAPH_LENGTH = 150;
|
152
packages/docs/src/interface.ts
Normal file
152
packages/docs/src/interface.ts
Normal file
@ -0,0 +1,152 @@
|
||||
import type { GrayMatterFile } from 'gray-matter';
|
||||
|
||||
export interface FileMatter {
|
||||
readonly fileName: string;
|
||||
readonly fullPath: string;
|
||||
readonly matterResult: GrayMatterFile<string>;
|
||||
}
|
||||
|
||||
export interface FooterLink {
|
||||
readonly text: string;
|
||||
readonly url: string;
|
||||
}
|
||||
|
||||
export interface SiteConfig {
|
||||
readonly base_url: string;
|
||||
readonly repo_url: string;
|
||||
readonly site_title: string;
|
||||
readonly site_description: string;
|
||||
readonly site_image: string;
|
||||
readonly site_keywords: string[];
|
||||
readonly footer: {
|
||||
buttons: FooterLink[];
|
||||
links: FooterLink[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface Overview {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface GetStarted {
|
||||
readonly title: string;
|
||||
readonly url: string;
|
||||
}
|
||||
|
||||
export interface CallToAction {
|
||||
readonly title: string;
|
||||
readonly subtitle: string;
|
||||
readonly button_text: string;
|
||||
readonly url: string;
|
||||
}
|
||||
|
||||
export interface FeatureIntro {
|
||||
title: string;
|
||||
subtitle1: string;
|
||||
subtitle2: string;
|
||||
}
|
||||
|
||||
export interface Feature {
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
export interface HomepageData {
|
||||
readonly title: string;
|
||||
readonly subtitle: string;
|
||||
readonly get_started: GetStarted;
|
||||
readonly overviews: Overview[];
|
||||
readonly features_intro: FeatureIntro;
|
||||
readonly features: Feature[];
|
||||
readonly call_to_action: CallToAction;
|
||||
}
|
||||
|
||||
export interface Release {
|
||||
readonly date: string;
|
||||
readonly version: string;
|
||||
readonly description: string;
|
||||
}
|
||||
|
||||
export interface DocsData {
|
||||
readonly group: string;
|
||||
readonly title: string;
|
||||
readonly weight: number;
|
||||
readonly slug: string;
|
||||
}
|
||||
|
||||
export interface DocsPageHeading {
|
||||
readonly title: string;
|
||||
readonly anchor: string;
|
||||
}
|
||||
|
||||
export interface DocsPage {
|
||||
readonly fullPath: string;
|
||||
readonly headings: DocsPageHeading[];
|
||||
readonly textContent: string;
|
||||
readonly content: string;
|
||||
readonly data: DocsData;
|
||||
}
|
||||
|
||||
export interface DocsGroupLink {
|
||||
readonly title: string;
|
||||
readonly slug: string;
|
||||
}
|
||||
|
||||
export interface DocsGroup {
|
||||
readonly name: string;
|
||||
readonly title: string;
|
||||
readonly links: DocsGroupLink[];
|
||||
}
|
||||
|
||||
export interface MenuGroup {
|
||||
readonly name: string;
|
||||
readonly title: string;
|
||||
}
|
||||
|
||||
export interface Menu {
|
||||
readonly docs: MenuGroup[];
|
||||
}
|
||||
|
||||
export interface CommunityLink {
|
||||
readonly title: string;
|
||||
readonly description: string;
|
||||
readonly url: string;
|
||||
}
|
||||
|
||||
export interface CommunityLinksSection {
|
||||
readonly title: string;
|
||||
readonly links: CommunityLink[];
|
||||
}
|
||||
|
||||
export interface CommunityData {
|
||||
readonly title: string;
|
||||
readonly subtitle: string;
|
||||
readonly sections: CommunityLinksSection[];
|
||||
}
|
||||
|
||||
export interface MenuLink {
|
||||
readonly title: string;
|
||||
readonly url: string;
|
||||
}
|
||||
|
||||
export interface MenuLinkSubGroup {
|
||||
readonly title: string;
|
||||
readonly links: MenuLink[];
|
||||
}
|
||||
|
||||
export interface MenuLinkGroup {
|
||||
readonly title: string;
|
||||
readonly path: string;
|
||||
readonly groups: MenuLinkSubGroup[];
|
||||
}
|
||||
|
||||
export type MenuItem = MenuLinkGroup | MenuLink;
|
||||
|
||||
export interface SearchablePage {
|
||||
readonly title: string;
|
||||
readonly url: string;
|
||||
readonly headings: DocsPageHeading[];
|
||||
readonly textContent: string;
|
||||
}
|
5
packages/docs/src/lib/community.ts
Normal file
5
packages/docs/src/lib/community.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import data from '../../content/community.json';
|
||||
|
||||
import type { CommunityData } from '../interface';
|
||||
|
||||
export default data as CommunityData;
|
5
packages/docs/src/lib/config.ts
Normal file
5
packages/docs/src/lib/config.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import config from '../../content/config.json';
|
||||
|
||||
import type { SiteConfig } from '../interface';
|
||||
|
||||
export default config as SiteConfig;
|
179
packages/docs/src/lib/docs.ts
Normal file
179
packages/docs/src/lib/docs.ts
Normal file
@ -0,0 +1,179 @@
|
||||
import fs from 'fs';
|
||||
import matter from 'gray-matter';
|
||||
import yaml from 'js-yaml';
|
||||
import path from 'path';
|
||||
|
||||
import { getAnchor } from '../components/docs/components/headers/hooks/useAnchor';
|
||||
import { SUMMARY_MIN_PARAGRAPH_LENGTH } from '../constants';
|
||||
import menu from './menu';
|
||||
|
||||
import type { GetStaticProps } from 'next';
|
||||
import type {
|
||||
DocsData,
|
||||
DocsGroup,
|
||||
DocsGroupLink,
|
||||
DocsPage,
|
||||
FileMatter,
|
||||
SearchablePage,
|
||||
} from '../interface';
|
||||
|
||||
export interface SearchProps {
|
||||
searchablePages: SearchablePage[];
|
||||
}
|
||||
|
||||
export interface DocsMenuProps extends SearchProps {
|
||||
docsGroups: DocsGroup[];
|
||||
}
|
||||
|
||||
const docsDirectory = path.join(process.cwd(), 'content/docs');
|
||||
|
||||
let docsMatterCache: FileMatter[];
|
||||
let docsCache: [DocsPage[], DocsGroup[]];
|
||||
|
||||
export function fetchDocsMatter(): FileMatter[] {
|
||||
if (docsMatterCache && process.env.NODE_ENV !== 'development') {
|
||||
return docsMatterCache;
|
||||
}
|
||||
// Get file names under /docs
|
||||
const fileNames = fs.readdirSync(docsDirectory);
|
||||
const allDocsMatter = fileNames
|
||||
.filter(it => it.endsWith('.mdx'))
|
||||
.map(fileName => {
|
||||
// Read file as string
|
||||
const fullPath = path.join(docsDirectory, fileName);
|
||||
const fileContents = fs.readFileSync(fullPath, 'utf8');
|
||||
|
||||
// Use gray-matter to parse the doc metadata section
|
||||
const matterResult = matter(fileContents, {
|
||||
engines: {
|
||||
yaml: s => yaml.load(s, { schema: yaml.JSON_SCHEMA }) as object,
|
||||
},
|
||||
});
|
||||
return { fileName, fullPath, matterResult };
|
||||
});
|
||||
|
||||
// Sort docs by date
|
||||
docsMatterCache = allDocsMatter.sort(
|
||||
(a, b) => a.matterResult.data.weight - b.matterResult.data.weight,
|
||||
);
|
||||
|
||||
return docsMatterCache;
|
||||
}
|
||||
|
||||
function getHeadings(content: string): string[] {
|
||||
const headingRegex = /^## ([^\n]+)/gm;
|
||||
let matches = headingRegex.exec(content);
|
||||
|
||||
const headings: string[] = [];
|
||||
while (matches && matches.length === 2) {
|
||||
headings.push(matches[1]);
|
||||
matches = headingRegex.exec(content);
|
||||
}
|
||||
|
||||
return headings;
|
||||
}
|
||||
|
||||
function getTextContent(content: string): string {
|
||||
const textContentRegex =
|
||||
/^(?:-|\*|\n)((?!```|<| |const|interface|export|import|let|var|CMS\.)(?:[`\-# {*]*)[a-zA-Z]+[^|\n]+)$/gm;
|
||||
let matches = textContentRegex.exec(content);
|
||||
|
||||
const paragraphs: string[] = [];
|
||||
while (matches && matches.length === 2) {
|
||||
paragraphs.push(
|
||||
matches[1]
|
||||
.replace(/(^- )|(`)|(^[#]+ )|(\*\*)|((?<= )_)|(^_)|(_(?=[ .]{1}))|(_$)/gm, '')
|
||||
.replace(/\[([^\]]+)\]\((?:[^)]+)\)/gm, '$1'),
|
||||
);
|
||||
matches = textContentRegex.exec(content);
|
||||
}
|
||||
|
||||
return paragraphs.join('\n');
|
||||
}
|
||||
|
||||
export function fetchDocsContent(): [DocsPage[], DocsGroup[]] {
|
||||
if (docsCache && process.env.NODE_ENV !== 'development') {
|
||||
return docsCache;
|
||||
}
|
||||
|
||||
const allDocsData: DocsPage[] = fetchDocsMatter().map(
|
||||
({ fileName, fullPath, matterResult: { data, content } }) => {
|
||||
const slug = fileName.replace(/\.mdx$/, '');
|
||||
|
||||
const summaryRegex = /^<p>([\w\W]+?)<\/p>/i;
|
||||
let summaryMatch = summaryRegex.exec(content);
|
||||
|
||||
const htmlSummaryRegex =
|
||||
/^([\s\n]*(?:<(?:p|ul|ol|h1|h2|h3|h4|h5|h6|div)>(?:[\s\S])*?<\/(?:p|ul|ol|h1|h2|h3|h4|h5|h6|div)>[\s\n]*){1,2})/i;
|
||||
if (
|
||||
!summaryMatch ||
|
||||
summaryMatch.length < 2 ||
|
||||
summaryMatch[1].length < SUMMARY_MIN_PARAGRAPH_LENGTH
|
||||
) {
|
||||
summaryMatch = htmlSummaryRegex.exec(content);
|
||||
}
|
||||
|
||||
return {
|
||||
fullPath,
|
||||
data: {
|
||||
...data,
|
||||
slug,
|
||||
} as DocsData,
|
||||
textContent: getTextContent(content),
|
||||
headings: getHeadings(content).map(heading => ({
|
||||
title: heading,
|
||||
anchor: getAnchor(heading),
|
||||
})),
|
||||
content,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const pagesByGroup: Record<string, DocsGroupLink[]> = allDocsData.reduce((acc, doc) => {
|
||||
if (!(doc.data.group in acc)) {
|
||||
acc[doc.data.group] = [];
|
||||
}
|
||||
acc[doc.data.group].push({
|
||||
title: doc.data.title,
|
||||
slug: doc.data.slug,
|
||||
});
|
||||
return acc;
|
||||
}, {} as Record<string, DocsGroupLink[]>);
|
||||
|
||||
const docsGroups: DocsGroup[] = menu.docs.map(group => ({
|
||||
...group,
|
||||
links: pagesByGroup[group.name] ?? [],
|
||||
}));
|
||||
|
||||
docsCache = [allDocsData, docsGroups];
|
||||
|
||||
return docsCache;
|
||||
}
|
||||
|
||||
export function getSearchablePages(): SearchablePage[] {
|
||||
const pages = fetchDocsContent()[0];
|
||||
|
||||
return pages.map(page => ({
|
||||
title: page.data.title,
|
||||
textContent: page.textContent,
|
||||
url: `/docs/${page.data.slug}`,
|
||||
headings: page.headings,
|
||||
}));
|
||||
}
|
||||
|
||||
export const getSearchStaticProps: GetStaticProps = (): { props: SearchProps } => {
|
||||
return {
|
||||
props: {
|
||||
searchablePages: getSearchablePages(),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getDocsMenuStaticProps: GetStaticProps = (): { props: DocsMenuProps } => {
|
||||
return {
|
||||
props: {
|
||||
docsGroups: fetchDocsContent()[1],
|
||||
searchablePages: getSearchablePages(),
|
||||
},
|
||||
};
|
||||
};
|
5
packages/docs/src/lib/homepage.ts
Normal file
5
packages/docs/src/lib/homepage.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import data from '../../content/homepage.json';
|
||||
|
||||
import type { HomepageData } from '../interface';
|
||||
|
||||
export default data as HomepageData;
|
5
packages/docs/src/lib/menu.ts
Normal file
5
packages/docs/src/lib/menu.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import data from '../../content/menu.json';
|
||||
|
||||
import type { Menu } from '../interface';
|
||||
|
||||
export default data.menu as Menu;
|
5
packages/docs/src/lib/releases.ts
Normal file
5
packages/docs/src/lib/releases.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import data from '../../content/releases.json';
|
||||
|
||||
import type { Release } from '../interface';
|
||||
|
||||
export default data.releases as Release[];
|
82
packages/docs/src/pages/_app.tsx
Normal file
82
packages/docs/src/pages/_app.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import '../styles/globals.css';
|
||||
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import Prism from 'prismjs';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import ColorModeContext from '../components/context/ColorModeContext';
|
||||
import homepageData from '../lib/homepage';
|
||||
import useCreateTheme from '../styles/theme';
|
||||
|
||||
import type { PaletteMode } from '@mui/material';
|
||||
import type { AppProps } from 'next/app';
|
||||
|
||||
require('prismjs/components/prism-javascript');
|
||||
require('prismjs/components/prism-typescript');
|
||||
require('prismjs/components/prism-css');
|
||||
require('prismjs/components/prism-jsx');
|
||||
require('prismjs/components/prism-tsx');
|
||||
require('prismjs/components/prism-yaml');
|
||||
require('prismjs/components/prism-json');
|
||||
require('prismjs/components/prism-toml');
|
||||
require('prismjs/components/prism-markup-templating');
|
||||
require('prismjs/components/prism-handlebars');
|
||||
require('prismjs/components/prism-markdown');
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
const [mode, setMode] = useState<PaletteMode>('dark');
|
||||
const colorMode = useMemo(
|
||||
() => ({
|
||||
// The dark mode switch would invoke this method
|
||||
toggleColorMode: () => {
|
||||
const newMode = mode === 'light' ? 'dark' : 'light';
|
||||
setMode(newMode);
|
||||
localStorage.setItem('palette-mode', newMode);
|
||||
},
|
||||
}),
|
||||
[mode],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setMode(localStorage?.getItem('palette-mode') === 'light' ? 'light' : 'dark');
|
||||
}, []);
|
||||
|
||||
const { asPath } = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
Prism.highlightAll();
|
||||
}, [asPath]);
|
||||
|
||||
// Update the theme only if the mode changes
|
||||
const theme = useCreateTheme(mode);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<meta charSet="utf-8" />
|
||||
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/icons/icon-32x32.png" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no, viewport-fit=cover"
|
||||
/>
|
||||
<meta name="theme-color" content="#2e3034" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="description" content={homepageData.title} />
|
||||
</Head>
|
||||
<ColorModeContext.Provider value={colorMode}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Component {...pageProps} />
|
||||
</ThemeProvider>
|
||||
</ColorModeContext.Provider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default MyApp;
|
13
packages/docs/src/pages/_document.tsx
Normal file
13
packages/docs/src/pages/_document.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { Head, Html, Main, NextScript } from 'next/document';
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head />
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
}
|
105
packages/docs/src/pages/community.tsx
Normal file
105
packages/docs/src/pages/community.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import { styled } from '@mui/material/styles';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
import CommunitySection from '../components/community/CommunitySection';
|
||||
import Container from '../components/layout/Container';
|
||||
import Page from '../components/layout/Page';
|
||||
import communityData from '../lib/community';
|
||||
import { getDocsMenuStaticProps } from '../lib/docs';
|
||||
|
||||
import type { DocsMenuProps } from '../lib/docs';
|
||||
|
||||
const StyledCommunityContent = styled('div')(
|
||||
({ theme }) => `
|
||||
width: 100%;
|
||||
padding-top: 72px;
|
||||
min-height: calc(100vh - 72px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 80px;
|
||||
|
||||
${theme.breakpoints.between('md', 'lg')} {
|
||||
padding-top: 48px;
|
||||
gap: 56px
|
||||
}
|
||||
|
||||
${theme.breakpoints.down('md')} {
|
||||
padding-top: 32px;
|
||||
gap: 40px
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledTitle = styled('div')(
|
||||
({ theme }) => `
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
|
||||
${theme.breakpoints.down('lg')} {
|
||||
gap: 8px
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledCommunityLinks = styled('section')(
|
||||
({ theme }) => `
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: ${theme.palette.mode === 'light' ? '#dddee2' : '#242424'};
|
||||
padding: 64px 0 32px;
|
||||
flex-grow: 1;
|
||||
|
||||
${theme.breakpoints.between('md', 'lg')} {
|
||||
padding: 48px 0 32px;
|
||||
}
|
||||
|
||||
${theme.breakpoints.down('md')} {
|
||||
padding: 40px 0 32px;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledCommunityLinksContent = styled('div')`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 40px;
|
||||
`;
|
||||
|
||||
const Community = ({ docsGroups, searchablePages }: DocsMenuProps) => {
|
||||
return (
|
||||
<Page url="/community" docsGroups={docsGroups} searchablePages={searchablePages} fullWidth>
|
||||
<StyledCommunityContent>
|
||||
<Container>
|
||||
<StyledTitle>
|
||||
<Typography variant="h1" color="primary">
|
||||
{communityData.title}
|
||||
</Typography>
|
||||
<Typography variant="h2" color="text.primary">
|
||||
{communityData.subtitle}
|
||||
</Typography>
|
||||
</StyledTitle>
|
||||
</Container>
|
||||
<StyledCommunityLinks>
|
||||
<Container>
|
||||
<StyledCommunityLinksContent>
|
||||
{communityData.sections.map(section => (
|
||||
<CommunitySection key={section.title} section={section} />
|
||||
))}
|
||||
</StyledCommunityLinksContent>
|
||||
</Container>
|
||||
</StyledCommunityLinks>
|
||||
</StyledCommunityContent>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default Community;
|
||||
|
||||
export const getStaticProps = getDocsMenuStaticProps;
|
179
packages/docs/src/pages/docs/[doc].tsx
Normal file
179
packages/docs/src/pages/docs/[doc].tsx
Normal file
@ -0,0 +1,179 @@
|
||||
import Alert from '@mui/material/Alert';
|
||||
import { styled, useTheme } from '@mui/material/styles';
|
||||
import { MDXRemote } from 'next-mdx-remote';
|
||||
import { serialize } from 'next-mdx-remote/serialize';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
|
||||
import Anchor from '../../components/docs/components/Anchor';
|
||||
import Blockquote from '../../components/docs/components/Blockquote';
|
||||
import CodeTabs from '../../components/docs/components/CodeTabs';
|
||||
import Header1 from '../../components/docs/components/headers/Header1';
|
||||
import Header2 from '../../components/docs/components/headers/Header2';
|
||||
import Header3 from '../../components/docs/components/headers/Header3';
|
||||
import Header4 from '../../components/docs/components/headers/Header4';
|
||||
import Header5 from '../../components/docs/components/headers/Header5';
|
||||
import Header6 from '../../components/docs/components/headers/Header6';
|
||||
import DocsTable from '../../components/docs/components/table/Table';
|
||||
import TableBody from '../../components/docs/components/table/TableBody';
|
||||
import TableBodyCell from '../../components/docs/components/table/TableBodyCell';
|
||||
import TableHead from '../../components/docs/components/table/TableHead';
|
||||
import TableHeaderCell from '../../components/docs/components/table/TableHeaderCell';
|
||||
import DocsContent from '../../components/docs/DocsContent';
|
||||
import DocsLeftNav from '../../components/docs/DocsLeftNav';
|
||||
import DocsRightNav from '../../components/docs/DocsRightNav';
|
||||
import Page from '../../components/layout/Page';
|
||||
import { fetchDocsContent, getSearchablePages } from '../../lib/docs';
|
||||
|
||||
import type { MDXRemoteSerializeResult } from 'next-mdx-remote';
|
||||
import type { GetStaticPaths, GetStaticProps } from 'next/types';
|
||||
import type { DocsGroup, DocsPage, SearchablePage } from '../../interface';
|
||||
|
||||
const StyledDocsView = styled('div')(
|
||||
({ theme }) => `
|
||||
display: grid;
|
||||
grid-template-columns: calc(100% - 240px) 240px;
|
||||
margin-left: 280px;
|
||||
width: calc(100% - 280px);
|
||||
padding-top: 16px;
|
||||
|
||||
${theme.breakpoints.down('lg')} {
|
||||
margin-left: 0;
|
||||
padding-top: 24px;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
${theme.breakpoints.down('md')} {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledDocsContentWrapper = styled('main')(
|
||||
({ theme }) => `
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
margin-bottom: 40px;
|
||||
|
||||
${theme.breakpoints.between('md', 'lg')} {
|
||||
width: calc(100vw - 250px);
|
||||
}
|
||||
|
||||
${theme.breakpoints.down('lg')} {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
${theme.breakpoints.down('md')} {
|
||||
width: 100vw;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
interface DocsProps {
|
||||
docsGroups: DocsGroup[];
|
||||
searchablePages: SearchablePage[];
|
||||
title: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
source: MDXRemoteSerializeResult;
|
||||
}
|
||||
|
||||
const Docs = ({
|
||||
docsGroups,
|
||||
searchablePages,
|
||||
title,
|
||||
slug,
|
||||
description = '',
|
||||
source,
|
||||
}: DocsProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Page
|
||||
title={title}
|
||||
url={`/docs/${slug}`}
|
||||
description={description}
|
||||
docsGroups={docsGroups}
|
||||
searchablePages={searchablePages}
|
||||
fullWidth
|
||||
>
|
||||
<DocsLeftNav docsGroups={docsGroups} />
|
||||
<StyledDocsView className={theme.palette.mode}>
|
||||
<StyledDocsContentWrapper>
|
||||
<DocsContent>
|
||||
<Header1>{title}</Header1>
|
||||
<MDXRemote
|
||||
{...source}
|
||||
components={{
|
||||
h1: Header1,
|
||||
h2: Header2,
|
||||
h3: Header3,
|
||||
h4: Header4,
|
||||
h5: Header5,
|
||||
h6: Header6,
|
||||
blockquote: Blockquote,
|
||||
table: DocsTable,
|
||||
thead: TableHead,
|
||||
tbody: TableBody,
|
||||
th: TableHeaderCell,
|
||||
td: TableBodyCell,
|
||||
a: Anchor,
|
||||
CodeTabs,
|
||||
Alert,
|
||||
}}
|
||||
/>
|
||||
</DocsContent>
|
||||
</StyledDocsContentWrapper>
|
||||
<DocsRightNav />
|
||||
</StyledDocsView>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default Docs;
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
const paths = fetchDocsContent()[0].map(docs => `/docs/${docs.data.slug}`);
|
||||
return {
|
||||
paths,
|
||||
fallback: false,
|
||||
};
|
||||
};
|
||||
|
||||
const buildSlugToDocsContent = (docsContents: DocsPage[]) => {
|
||||
const hash: Record<string, DocsPage> = {};
|
||||
docsContents.forEach(docs => (hash[docs.data.slug] = docs));
|
||||
return hash;
|
||||
};
|
||||
|
||||
let slugToDocsContent = buildSlugToDocsContent(fetchDocsContent()[0]);
|
||||
let docsGroups = fetchDocsContent()[1];
|
||||
|
||||
export const getStaticProps: GetStaticProps = async ({ params }): Promise<{ props: DocsProps }> => {
|
||||
const slug = params?.doc as string;
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
slugToDocsContent = buildSlugToDocsContent(fetchDocsContent()[0]);
|
||||
docsGroups = fetchDocsContent()[1];
|
||||
}
|
||||
|
||||
const { content, data } = slugToDocsContent[slug];
|
||||
const source = await serialize(content, {
|
||||
mdxOptions: {
|
||||
remarkPlugins: [remarkGfm],
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
docsGroups,
|
||||
searchablePages: getSearchablePages(),
|
||||
title: data.title,
|
||||
slug: data.slug,
|
||||
description: '',
|
||||
source,
|
||||
},
|
||||
};
|
||||
};
|
451
packages/docs/src/pages/index.tsx
Normal file
451
packages/docs/src/pages/index.tsx
Normal file
@ -0,0 +1,451 @@
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardActionArea from '@mui/material/CardActionArea';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import { styled, useTheme } from '@mui/material/styles';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
import DateDisplay from '../components/DateDisplay';
|
||||
import Container from '../components/layout/Container';
|
||||
import Page from '../components/layout/Page';
|
||||
import config from '../lib/config';
|
||||
import { getDocsMenuStaticProps } from '../lib/docs';
|
||||
import homepageData from '../lib/homepage';
|
||||
import releases from '../lib/releases';
|
||||
|
||||
import type { DocsMenuProps } from '../lib/docs';
|
||||
|
||||
const StyledHomagePageContent = styled('div')(
|
||||
({ theme }) => `
|
||||
width: 100%;
|
||||
padding-top: 72px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 88px;
|
||||
align-items: center;
|
||||
|
||||
${theme.breakpoints.down('md')} {
|
||||
padding-top: 32px;
|
||||
gap: 0;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledIntroSection = styled('section')`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const StyledIntroSectionContent = styled('div')`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
`;
|
||||
|
||||
const StyledOverviewSection = styled('section')(
|
||||
({ theme }) => `
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
${theme.breakpoints.down('md')} {
|
||||
margin-top: 64px;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledOverviewSectionContent = styled('div')(
|
||||
({ theme }) => `
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 64px;
|
||||
|
||||
${theme.breakpoints.down('md')} {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledOverviewList = styled('div')`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
`;
|
||||
|
||||
const StyledOverview = styled('div')`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
const StyledImageWrapper = styled('div')`
|
||||
width: 100%;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const StyledCallToActionSection = styled('section')(
|
||||
({ theme }) => `
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 0;
|
||||
overflow: visible;
|
||||
z-index: 1;
|
||||
|
||||
${theme.breakpoints.down('md')} {
|
||||
height: auto;
|
||||
margin-top: 64px;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledCallToActionContainer = styled('div')(
|
||||
({ theme }) => `
|
||||
max-width: 1280px;
|
||||
width: 100%;
|
||||
padding: 0 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
${theme.breakpoints.down('md')} {
|
||||
padding: 0;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledCallToActionCard = styled(Card)(
|
||||
({ theme }) => `
|
||||
width: 80%;
|
||||
|
||||
${theme.breakpoints.down('md')} {
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledCallToActionCardContent = styled(CardContent)(
|
||||
({ theme }) => `
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 24px 40px;
|
||||
line-height: 30px;
|
||||
gap: 24px;
|
||||
|
||||
${theme.breakpoints.down('md')} {
|
||||
flex-direction: column;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledCallToActionText = styled('div')`
|
||||
flex-grow: 1;
|
||||
`;
|
||||
|
||||
const StyledReleasesSection = styled('section')(
|
||||
({ theme }) => `
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: ${theme.palette.mode === 'light' ? '#dddee2' : '#242424'};
|
||||
padding: 64px 0;
|
||||
|
||||
${theme.breakpoints.down('md')} {
|
||||
padding: 48px 0;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledReleasesSectionContent = styled('div')(
|
||||
({ theme }) => `
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 48px;
|
||||
|
||||
${theme.breakpoints.down('md')} {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledReleaseCardContent = styled(CardContent)`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
const StyledFeaturesSection = styled('section')(
|
||||
({ theme }) => `
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-bottom: 80px;
|
||||
|
||||
${theme.breakpoints.down('md')} {
|
||||
height: auto;
|
||||
margin-top: 48px;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledFeaturesSectionIntro = styled('div')(
|
||||
({ theme }) => `
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 32px 0 104px;
|
||||
|
||||
${theme.breakpoints.down('md')} {
|
||||
padding: 32px 0 48px;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledFeaturesSectionContent = styled('div')(
|
||||
({ theme }) => `
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 48px;
|
||||
|
||||
${theme.breakpoints.down('md')} {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const StyledFeature = styled('div')`
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
const StyledFeatureText = styled('div')`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 0 16px;
|
||||
`;
|
||||
|
||||
const Home = ({ docsGroups, searchablePages }: DocsMenuProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Page url="/" docsGroups={docsGroups} searchablePages={searchablePages} fullWidth>
|
||||
<StyledHomagePageContent>
|
||||
<StyledIntroSection>
|
||||
<Container>
|
||||
<StyledIntroSectionContent>
|
||||
<Typography variant="h1" color="primary">
|
||||
{homepageData.title}
|
||||
</Typography>
|
||||
<Typography variant="h2" color="text.primary">
|
||||
{homepageData.subtitle}
|
||||
</Typography>
|
||||
<Button
|
||||
component={Link}
|
||||
href={homepageData.get_started.url}
|
||||
variant="contained"
|
||||
size="large"
|
||||
>
|
||||
{homepageData.get_started.title}
|
||||
</Button>
|
||||
</StyledIntroSectionContent>
|
||||
</Container>
|
||||
</StyledIntroSection>
|
||||
<StyledOverviewSection>
|
||||
<Container>
|
||||
<StyledOverviewSectionContent>
|
||||
<StyledOverviewList>
|
||||
{homepageData.overviews.map(overview => (
|
||||
<StyledOverview key={overview.title}>
|
||||
<Typography variant="h3" color="text.primary">
|
||||
{overview.title}
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
{overview.description}
|
||||
</Typography>
|
||||
</StyledOverview>
|
||||
))}
|
||||
</StyledOverviewList>
|
||||
<StyledImageWrapper>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src="/img/screenshot-editor.webp" />
|
||||
</StyledImageWrapper>
|
||||
</StyledOverviewSectionContent>
|
||||
</Container>
|
||||
</StyledOverviewSection>
|
||||
<StyledCallToActionSection>
|
||||
<StyledCallToActionContainer>
|
||||
<StyledCallToActionCard raised>
|
||||
<StyledCallToActionCardContent>
|
||||
<StyledCallToActionText>
|
||||
<Typography variant="subtitle1" color="text.primary" component="strong">
|
||||
{homepageData.call_to_action.title}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" color="text.secondary" component="span">
|
||||
{homepageData.call_to_action.subtitle}
|
||||
</Typography>
|
||||
</StyledCallToActionText>
|
||||
<Button
|
||||
component={Link}
|
||||
href={homepageData.call_to_action.url}
|
||||
variant="contained"
|
||||
size="large"
|
||||
sx={{ width: '188px', whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{homepageData.call_to_action.button_text}
|
||||
</Button>
|
||||
</StyledCallToActionCardContent>
|
||||
</StyledCallToActionCard>
|
||||
</StyledCallToActionContainer>
|
||||
</StyledCallToActionSection>
|
||||
<StyledReleasesSection>
|
||||
<Container>
|
||||
<StyledReleasesSectionContent>
|
||||
{[...Array(3)].map((_, index) => {
|
||||
if (index >= releases.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const release = releases[index];
|
||||
return (
|
||||
<CardActionArea
|
||||
key={release.version}
|
||||
href={`${config.repo_url}/releases/tag/${release.version}`}
|
||||
>
|
||||
<StyledReleaseCardContent>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
color="text.primary"
|
||||
sx={{ display: 'flex', alignItems: 'center', gap: '8px' }}
|
||||
>
|
||||
<>
|
||||
<Chip label={release.version} color="primary" />
|
||||
<DateDisplay date={release.date} format="MMMM dd, yyyy" />
|
||||
</>
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{release.description}
|
||||
</Typography>
|
||||
</StyledReleaseCardContent>
|
||||
</CardActionArea>
|
||||
);
|
||||
})}
|
||||
</StyledReleasesSectionContent>
|
||||
</Container>
|
||||
</StyledReleasesSection>
|
||||
<StyledFeaturesSection>
|
||||
<Container>
|
||||
<StyledFeaturesSectionIntro>
|
||||
<Typography
|
||||
variant="h2"
|
||||
color="text.primary"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{homepageData.features_intro.title}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
component="div"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
textAlign: 'center',
|
||||
marginTop: '24px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{homepageData.features_intro.subtitle1} {homepageData.features_intro.subtitle2}
|
||||
</Typography>
|
||||
</StyledFeaturesSectionIntro>
|
||||
<StyledFeaturesSectionContent>
|
||||
{homepageData.features.map(feature => (
|
||||
<StyledFeature key={feature.title}>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={feature.image} width="100%" height="auto" />
|
||||
<StyledFeatureText>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
color="text.primary"
|
||||
sx={{ display: 'flex', alignItems: 'center', gap: '8px' }}
|
||||
>
|
||||
{feature.title}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{feature.description}
|
||||
</Typography>
|
||||
</StyledFeatureText>
|
||||
</StyledFeature>
|
||||
))}
|
||||
</StyledFeaturesSectionContent>
|
||||
</Container>
|
||||
</StyledFeaturesSection>
|
||||
<footer>
|
||||
{theme.palette.mode === 'light' ? (
|
||||
<a
|
||||
key="netlify-logo-light"
|
||||
href="https://www.netlify.com"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Image
|
||||
width={114}
|
||||
height={51}
|
||||
src="/img/netlify-color-bg.svg"
|
||||
alt="Deploys by Netlify"
|
||||
/>
|
||||
</a>
|
||||
) : (
|
||||
<a
|
||||
key="netlify-logo-dark"
|
||||
href="https://www.netlify.com"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Image
|
||||
width={114}
|
||||
height={51}
|
||||
src="/img/netlify-color-accent.svg"
|
||||
alt="Deploys by Netlify"
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
</footer>
|
||||
</StyledHomagePageContent>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
|
||||
export const getStaticProps = getDocsMenuStaticProps;
|
229
packages/docs/src/styles/globals.css
Normal file
229
packages/docs/src/styles/globals.css
Normal file
@ -0,0 +1,229 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#__next {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* PrismJS 1.29.0 (Dark)
|
||||
https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+json+toml+typescript+yaml */
|
||||
.dark code[class*='language-'],
|
||||
.dark pre[class*='language-'] {
|
||||
color: #ccc;
|
||||
background: 0 0;
|
||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
font-size: 1em;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.5;
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
.dark pre[class*='language-'] {
|
||||
padding: 1em;
|
||||
margin: 0.5em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
.dark :not(pre) > code[class*='language-'],
|
||||
.dark pre[class*='language-'] {
|
||||
background: #2d2d2d;
|
||||
}
|
||||
.dark :not(pre) > code[class*='language-'] {
|
||||
padding: 0.1em;
|
||||
border-radius: 0.3em;
|
||||
white-space: normal;
|
||||
}
|
||||
.dark .token.block-comment,
|
||||
.dark .token.cdata,
|
||||
.dark .token.comment,
|
||||
.dark .token.doctype,
|
||||
.dark .token.prolog {
|
||||
color: #999;
|
||||
}
|
||||
.dark .token.punctuation {
|
||||
color: #ccc;
|
||||
}
|
||||
.dark .token.attr-name,
|
||||
.dark .token.deleted,
|
||||
.dark .token.namespace,
|
||||
.dark .token.tag {
|
||||
color: #e2777a;
|
||||
}
|
||||
.dark .token.function-name {
|
||||
color: #6196cc;
|
||||
}
|
||||
.dark .token.boolean,
|
||||
.dark .token.function,
|
||||
.dark .token.number {
|
||||
color: #f08d49;
|
||||
}
|
||||
.dark .token.class-name,
|
||||
.dark .token.constant,
|
||||
.dark .token.property,
|
||||
.dark .token.symbol {
|
||||
color: #f8c555;
|
||||
}
|
||||
.dark .token.atrule,
|
||||
.dark .token.builtin,
|
||||
.dark .token.important,
|
||||
.dark .token.keyword,
|
||||
.dark .token.selector {
|
||||
color: #cc99cd;
|
||||
}
|
||||
.dark .token.attr-value,
|
||||
.dark .token.char,
|
||||
.dark .token.regex,
|
||||
.dark .token.string,
|
||||
.dark .token.variable {
|
||||
color: #7ec699;
|
||||
}
|
||||
.dark .token.entity,
|
||||
.dark .token.operator,
|
||||
.dark .token.url {
|
||||
color: #67cdcc;
|
||||
}
|
||||
.dark .token.bold,
|
||||
.dark .token.important {
|
||||
font-weight: 700;
|
||||
}
|
||||
.dark .token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
.dark .token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
.dark .token.inserted {
|
||||
color: green;
|
||||
}
|
||||
|
||||
/* PrismJS 1.29.0 (Light)
|
||||
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+json+toml+typescript+yaml */
|
||||
.light code[class*='language-'],
|
||||
.light pre[class*='language-'] {
|
||||
color: #000;
|
||||
background: 0 0;
|
||||
text-shadow: 0 1px #fff;
|
||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
font-size: 1em;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.5;
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
.light code[class*='language-'] ::-moz-selection,
|
||||
.light code[class*='language-']::-moz-selection,
|
||||
.light pre[class*='language-'] ::-moz-selection,
|
||||
.light pre[class*='language-']::-moz-selection {
|
||||
text-shadow: none;
|
||||
background: #b3d4fc;
|
||||
}
|
||||
.light code[class*='language-'] ::selection,
|
||||
.light code[class*='language-']::selection,
|
||||
.light pre[class*='language-'] ::selection,
|
||||
.light pre[class*='language-']::selection {
|
||||
text-shadow: none;
|
||||
background: #b3d4fc;
|
||||
}
|
||||
@media print {
|
||||
.light code[class*='language-'],
|
||||
.light pre[class*='language-'] {
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
.light pre[class*='language-'] {
|
||||
padding: 1em;
|
||||
margin: 0.5em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
.light :not(pre) > code[class*='language-'],
|
||||
.light pre[class*='language-'] {
|
||||
background: #f5f2f0;
|
||||
}
|
||||
.light :not(pre) > code[class*='language-'] {
|
||||
padding: 0.1em;
|
||||
border-radius: 0.3em;
|
||||
white-space: normal;
|
||||
}
|
||||
.light .token.cdata,
|
||||
.light .token.comment,
|
||||
.light .token.doctype,
|
||||
.light .token.prolog {
|
||||
color: #708090;
|
||||
}
|
||||
.light .token.punctuation {
|
||||
color: #999;
|
||||
}
|
||||
.light .token.namespace {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.light .token.boolean,
|
||||
.light .token.constant,
|
||||
.light .token.deleted,
|
||||
.light .token.number,
|
||||
.light .token.property,
|
||||
.light .token.symbol,
|
||||
.light .token.tag {
|
||||
color: #905;
|
||||
}
|
||||
.light .token.attr-name,
|
||||
.light .token.builtin,
|
||||
.light .token.char,
|
||||
.light .token.inserted,
|
||||
.light .token.selector,
|
||||
.light .token.string {
|
||||
color: #690;
|
||||
}
|
||||
.light .language-css .token.string,
|
||||
.light .style .token.string,
|
||||
.light .token.entity,
|
||||
.light .token.operator,
|
||||
.light .token.url {
|
||||
color: #9a6e3a;
|
||||
background: hsla(0, 0%, 100%, 0.5);
|
||||
}
|
||||
.light .token.atrule,
|
||||
.light .token.attr-value,
|
||||
.light .token.keyword {
|
||||
color: #07a;
|
||||
}
|
||||
.light .token.class-name,
|
||||
.light .token.function {
|
||||
color: #dd4a68;
|
||||
}
|
||||
.light .token.important,
|
||||
.light .token.regex,
|
||||
.light .token.variable {
|
||||
color: #e90;
|
||||
}
|
||||
.light .token.bold,
|
||||
.light .token.important {
|
||||
font-weight: 700;
|
||||
}
|
||||
.light .token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
.light .token.entity {
|
||||
cursor: help;
|
||||
}
|
94
packages/docs/src/styles/theme.ts
Normal file
94
packages/docs/src/styles/theme.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import darkScrollbar from '@mui/material/darkScrollbar';
|
||||
import { createTheme } from '@mui/material/styles';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import type { Components, PaletteMode, PaletteOptions, Theme } from '@mui/material';
|
||||
|
||||
const useCreateTheme = (mode: PaletteMode) => {
|
||||
const theme = useMemo(() => createTheme(), []);
|
||||
|
||||
const palette: PaletteOptions = useMemo(
|
||||
() =>
|
||||
mode === 'light'
|
||||
? {
|
||||
mode,
|
||||
primary: {
|
||||
main: '#1b72de',
|
||||
},
|
||||
secondary: {
|
||||
main: '#1b72de',
|
||||
},
|
||||
background: {
|
||||
default: '#f9f9f9',
|
||||
paper: '#f9f9f9',
|
||||
},
|
||||
}
|
||||
: {
|
||||
mode,
|
||||
primary: {
|
||||
main: '#5ecffb',
|
||||
},
|
||||
secondary: {
|
||||
main: '#5ecffb',
|
||||
},
|
||||
background: {
|
||||
default: '#2e3034',
|
||||
paper: '#2e3034',
|
||||
},
|
||||
},
|
||||
[mode],
|
||||
);
|
||||
|
||||
const components: Components<Omit<Theme, 'components'>> | undefined = useMemo(
|
||||
() =>
|
||||
mode === 'light'
|
||||
? {}
|
||||
: {
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: {
|
||||
body: darkScrollbar(),
|
||||
},
|
||||
},
|
||||
},
|
||||
[mode],
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
createTheme({
|
||||
palette,
|
||||
typography: {
|
||||
fontFamily: "'Roboto', -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif",
|
||||
h1: {
|
||||
fontSize: '42px',
|
||||
fontWeight: 'bold',
|
||||
lineHeight: 1.3,
|
||||
marginBottom: '16px',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
fontSize: '30px',
|
||||
},
|
||||
},
|
||||
h2: {
|
||||
fontSize: '24px',
|
||||
lineHeight: 1.3,
|
||||
position: 'relative',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
fontSize: '20px',
|
||||
},
|
||||
},
|
||||
h3: {
|
||||
fontSize: '20px',
|
||||
lineHeight: 1.3,
|
||||
position: 'relative',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
fontSize: '18px',
|
||||
},
|
||||
},
|
||||
},
|
||||
components,
|
||||
}),
|
||||
[components, palette, theme.breakpoints],
|
||||
);
|
||||
};
|
||||
|
||||
export default useCreateTheme;
|
23
packages/docs/src/util/node.util.ts
Normal file
23
packages/docs/src/util/node.util.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { isNotEmpty } from './string.util';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export const getNodeText = (node: ReactNode): string => {
|
||||
if (['string', 'number'].includes(typeof node)) {
|
||||
return `${node}`;
|
||||
}
|
||||
|
||||
if (node instanceof Array) {
|
||||
return node.map(getNodeText).filter(isNotEmpty).join('');
|
||||
}
|
||||
|
||||
if (typeof node === 'object' && node && 'props' in node) {
|
||||
return getNodeText(node.props.children);
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
export const useNodeText = (node: ReactNode) => useMemo(() => getNodeText(node), [node]);
|
11
packages/docs/src/util/null.util.ts
Normal file
11
packages/docs/src/util/null.util.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export function isNotNullish<T>(value: T | null | undefined): value is T {
|
||||
return value !== undefined && value !== null;
|
||||
}
|
||||
|
||||
export function isNullish<T>(value: T | null | undefined): value is null | undefined {
|
||||
return value === undefined || value === null;
|
||||
}
|
||||
|
||||
export function filterNullish<T>(value: (T | null | undefined)[] | null | undefined): T[] {
|
||||
return value?.filter<T>(isNotNullish) ?? [];
|
||||
}
|
89
packages/docs/src/util/search.util.ts
Normal file
89
packages/docs/src/util/search.util.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { isEmpty } from './string.util';
|
||||
|
||||
import type { DocsPageHeading, SearchablePage } from '../interface';
|
||||
|
||||
const PARTIAL_MATCH_WORD_LENGTH_THRESHOLD = 5;
|
||||
const WHOLE_WORD_MATCH_FAVOR_WEIGHT = 2;
|
||||
const TITLE_FAVOR_WEIGHT = 15;
|
||||
|
||||
export interface SearchScore {
|
||||
entry: SearchablePage;
|
||||
metaScore: number;
|
||||
score: number;
|
||||
isExactTitleMatch: boolean;
|
||||
matchedHeader: DocsPageHeading | undefined;
|
||||
}
|
||||
|
||||
function getSearchScore(words: string[], entry: SearchablePage): SearchScore {
|
||||
let score = 0;
|
||||
let metaScore = 0;
|
||||
|
||||
for (const word of words) {
|
||||
score +=
|
||||
(entry.title.match(new RegExp(`\\b${word}\\b`, 'gi')) ?? []).length *
|
||||
TITLE_FAVOR_WEIGHT *
|
||||
WHOLE_WORD_MATCH_FAVOR_WEIGHT;
|
||||
score +=
|
||||
(entry.textContent.match(new RegExp(`\\b${word}\\b`, 'gi')) ?? []).length *
|
||||
WHOLE_WORD_MATCH_FAVOR_WEIGHT;
|
||||
|
||||
if (word.length >= PARTIAL_MATCH_WORD_LENGTH_THRESHOLD) {
|
||||
score += (entry.title.match(new RegExp(`${word}`, 'gi')) ?? []).length * TITLE_FAVOR_WEIGHT;
|
||||
score += (entry.textContent.match(new RegExp(`${word}`, 'gi')) ?? []).length;
|
||||
}
|
||||
}
|
||||
|
||||
const exactMatchFavorWeight = words.length;
|
||||
const exactSearch = words.join(' ').toLowerCase();
|
||||
const isExactTitleMatch = entry.title.toLowerCase().includes(exactSearch);
|
||||
|
||||
const exactTitleMatchScore =
|
||||
(isExactTitleMatch ? 1 : 0) *
|
||||
TITLE_FAVOR_WEIGHT *
|
||||
exactMatchFavorWeight *
|
||||
WHOLE_WORD_MATCH_FAVOR_WEIGHT;
|
||||
|
||||
if (isExactTitleMatch) {
|
||||
metaScore += 1;
|
||||
}
|
||||
|
||||
score += exactTitleMatchScore;
|
||||
score +=
|
||||
(entry.textContent.match(new RegExp(`\\b${exactSearch}\\b`, 'gi')) ?? []).length *
|
||||
exactMatchFavorWeight *
|
||||
WHOLE_WORD_MATCH_FAVOR_WEIGHT;
|
||||
|
||||
return {
|
||||
score,
|
||||
metaScore,
|
||||
entry,
|
||||
isExactTitleMatch: exactTitleMatchScore > 0,
|
||||
matchedHeader: entry.headings.find(header => header.title.toLowerCase().includes(exactSearch)),
|
||||
};
|
||||
}
|
||||
|
||||
export function useSearchScores(query: string | null, entries: SearchablePage[]): SearchScore[] {
|
||||
return useMemo(() => {
|
||||
if (!query || isEmpty(query.trim())) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const queryWords = query.split(' ').filter(word => word.trim().length > 0);
|
||||
|
||||
const scores = entries
|
||||
.map(entry => getSearchScore(queryWords, entry))
|
||||
.filter(result => result.score > 0);
|
||||
|
||||
scores.sort((a, b) => {
|
||||
if (a.metaScore !== b.metaScore) {
|
||||
return b.metaScore - a.metaScore;
|
||||
}
|
||||
|
||||
return b.score - a.score;
|
||||
});
|
||||
|
||||
return scores;
|
||||
}, [entries, query]);
|
||||
}
|
24
packages/docs/src/util/string.util.ts
Normal file
24
packages/docs/src/util/string.util.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { isNotNullish, isNullish } from './null.util';
|
||||
|
||||
export function isEmpty(value: string | null | undefined): value is null | undefined {
|
||||
return isNullish(value) || value === '';
|
||||
}
|
||||
|
||||
export function isNotEmpty(value: string | null | undefined): value is string {
|
||||
return isNotNullish(value) && value !== '';
|
||||
}
|
||||
|
||||
export function toTitleCase(str: string): string {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
||||
}
|
||||
|
||||
export function toTitleCaseFromKey(str: string) {
|
||||
return str.replace(/_/g, ' ').replace(/\w\S*/g, toTitleCase);
|
||||
}
|
||||
|
||||
export function toTitleCaseFromVariableName(str: string) {
|
||||
return str
|
||||
.split(/(?=[A-Z])/)
|
||||
.join(' ')
|
||||
.replace(/\w\S*/g, toTitleCase);
|
||||
}
|
7
packages/docs/src/util/transientOptions.ts
Normal file
7
packages/docs/src/util/transientOptions.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { CreateMUIStyled } from '@mui/material/styles';
|
||||
|
||||
const transientOptions: Parameters<CreateMUIStyled>[1] = {
|
||||
shouldForwardProp: (propName: string) => !propName.startsWith('$'),
|
||||
};
|
||||
|
||||
export default transientOptions;
|
Reference in New Issue
Block a user