Browse Source

feat: improved layout, mobile menu

fix/enable-imgix
Thomas Osmonson 4 years ago
parent
commit
977d8ee732
  1. 12
      lib/babel-plugin-nextjs-mdx-patch.js
  2. 28
      lib/remark-plugins.js
  3. 3
      next.config.js
  4. 9
      package.json
  5. 5
      src/common/constants.ts
  6. 5
      src/common/data/clarity-ref.ts
  7. 30
      src/common/hooks/use-active-heading.tsx
  8. 43
      src/common/hooks/use-headroom.ts
  9. 65
      src/common/hooks/use-scroll.tsx
  10. 2
      src/components/content-wrapper.tsx
  11. 81
      src/components/header.tsx
  12. 127
      src/components/layouts/base-layout.tsx
  13. 5
      src/components/mdx/components/link.tsx
  14. 5
      src/components/mdx/md-contents.tsx
  15. 84
      src/components/mdx/styles.tsx
  16. 30
      src/components/search.tsx
  17. 170
      src/components/side-nav.tsx
  18. 41
      src/components/toc.tsx
  19. 1
      src/pages/_app.tsx
  20. 2
      src/pages/_document.tsx
  21. 812
      yarn.lock

12
lib/babel-plugin-nextjs-mdx-patch.js

@ -10,7 +10,7 @@
*/ */
// https://nextjs.org/docs/basic-features/data-fetching // https://nextjs.org/docs/basic-features/data-fetching
const DATA_FETCH_FNS = ['getStaticPaths', 'getStaticProps', 'getServerProps'] const DATA_FETCH_FNS = ['getStaticPaths', 'getStaticProps', 'getServerProps'];
module.exports = () => { module.exports = () => {
return { return {
@ -19,14 +19,12 @@ module.exports = () => {
if ( if (
DATA_FETCH_FNS.includes(path.node.value.name) && DATA_FETCH_FNS.includes(path.node.value.name) &&
path.findParent( path.findParent(
(path) => path => path.isVariableDeclarator() && path.node.id.name === 'layoutProps'
path.isVariableDeclarator() &&
path.node.id.name === 'layoutProps',
) )
) { ) {
path.remove() path.remove();
} }
}, },
}, },
} };
} };

28
lib/remark-plugins.js

@ -1,16 +1,26 @@
const memoize = require('micro-memoize'); const memoize = require('micro-memoize');
const path = require('path'); const path = require('path');
const include = require('./remark-include');
const vscode = require('remark-vscode');
const emoji = require('remark-emoji');
const paragraphAlerts = require('./remark-paragraph-alerts');
const images = require('remark-images');
const unwrapImages = require('remark-unwrap-images');
const normalizeHeadings = require('remark-normalize-headings');
const slug = require('remark-slug');
const headingID = require('remark-heading-id');
const remarkPlugins = [ const remarkPlugins = [
[require('./remark-include'), { resolveFrom: path.join(__dirname, '../src/includes') }], [memoize(include), { resolveFrom: path.join(__dirname, '../src/includes') }],
require('remark-vscode'), memoize(vscode),
memoize(require('./remark-paragraph-alerts')), memoize(paragraphAlerts),
memoize(require('remark-external-links')), memoize(emoji),
memoize(require('remark-emoji')), memoize(images),
memoize(require('remark-images')), memoize(unwrapImages),
memoize(require('remark-unwrap-images')), memoize(normalizeHeadings),
memoize(require('remark-normalize-headings')), memoize(slug),
memoize(require('remark-slug')), memoize(headingID),
]; ];
module.exports = { remarkPlugins }; module.exports = { remarkPlugins };

3
next.config.js

@ -26,9 +26,6 @@ module.exports = withBundleAnalyzer({
], ],
}); });
if (!options.isServer) {
config.node['fs'] = 'empty';
}
if (!options.dev) { if (!options.dev) {
const splitChunks = config.optimization && config.optimization.splitChunks; const splitChunks = config.optimization && config.optimization.splitChunks;
if (splitChunks) { if (splitChunks) {

9
package.json

@ -37,7 +37,7 @@
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"mdi-react": "7.3.0", "mdi-react": "7.3.0",
"micro-memoize": "^4.0.9", "micro-memoize": "^4.0.9",
"next": "^9.5.0", "next": "^9.5.1-canary.0",
"next-google-fonts": "^1.1.0", "next-google-fonts": "^1.1.0",
"next-mdx-enhanced": "^3.0.0", "next-mdx-enhanced": "^3.0.0",
"next-mdx-remote": "^0.6.0", "next-mdx-remote": "^0.6.0",
@ -54,6 +54,7 @@
"prismjs": "^1.20.0", "prismjs": "^1.20.0",
"react-children-utilities": "^2.1.3", "react-children-utilities": "^2.1.3",
"react-gesture-responder": "^2.1.0", "react-gesture-responder": "^2.1.0",
"react-headroom": "^3.0.0",
"react-icons": "^3.9.0", "react-icons": "^3.9.0",
"react-is": "^16.13.1", "react-is": "^16.13.1",
"react-live": "^2.2.2", "react-live": "^2.2.2",
@ -65,13 +66,14 @@
"remark-external-links": "^6.1.0", "remark-external-links": "^6.1.0",
"remark-footnotes": "^1.0.0", "remark-footnotes": "^1.0.0",
"remark-frontmatter": "^2.0.0", "remark-frontmatter": "^2.0.0",
"remark-heading-id": "^1.0.0",
"remark-images": "2.0.0", "remark-images": "2.0.0",
"remark-normalize-headings": "^2.0.0", "remark-normalize-headings": "^2.0.0",
"remark-parse": "^8.0.3", "remark-parse": "^8.0.3",
"remark-slug": "6.0.0", "remark-slug": "6.0.0",
"remark-squeeze-paragraphs": "^4.0.0", "remark-squeeze-paragraphs": "^4.0.0",
"remark-unwrap-images": "2.0.0", "remark-unwrap-images": "2.0.0",
"remark-vscode": "^1.0.0-beta.1", "remark-vscode": "^1.0.0-beta.2",
"store": "^2.0.12", "store": "^2.0.12",
"strip-markdown": "^3.1.2", "strip-markdown": "^3.1.2",
"swr": "^0.2.3", "swr": "^0.2.3",
@ -82,8 +84,7 @@
"unist-util-is": "^4.0.2", "unist-util-is": "^4.0.2",
"unist-util-select": "^3.0.1", "unist-util-select": "^3.0.1",
"unist-util-visit": "^2.0.3", "unist-util-visit": "^2.0.3",
"use-events": "^1.4.2", "use-events": "^1.4.2"
"webpack": "^4.43.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/preset-react": "^7.10.4", "@babel/preset-react": "^7.10.4",

5
src/common/constants.ts

@ -1,7 +1,6 @@
export const SIDEBAR_WIDTH = 280; export const SIDEBAR_WIDTH = 240;
export const TOC_WIDTH = 280; export const TOC_WIDTH = 240;
export const CONTENT_MAX_WIDTH = 1104; export const CONTENT_MAX_WIDTH = 1104;
export const SHIKI_THEME = 'Material-Theme-Default'; export const SHIKI_THEME = 'Material-Theme-Default';
export const THEME_STORAGE_KEY = 'theme'; export const THEME_STORAGE_KEY = 'theme';

5
src/common/data/clarity-ref.ts

@ -1,5 +1,6 @@
import { renderMdx } from '@common/data/mdx'; import { renderMdx } from '@common/data/mdx';
import CLARITY_REFERENCE from '../../_data/clarityRef.json'; import CLARITY_REFERENCE from '../../_data/clarityRef.json';
import { slugify } from '@common/utils';
const wrapInClarityTicks = (string: string) => { const wrapInClarityTicks = (string: string) => {
let newString = ''; let newString = '';
@ -34,7 +35,7 @@ const generateMarkdown = () => {
${entry.description} ${entry.description}
#### Example #### Example {#${slugify(entry.name)}-example}
${wrapInClarityTicks(entry.example)} ${wrapInClarityTicks(entry.example)}
`; `;
@ -47,7 +48,7 @@ ${wrapInClarityTicks(entry.example)}
${entry.description} ${entry.description}
#### Example #### Example {#${slugify(entry.name)}-example}
${wrapInClarityTicks(entry.example)} ${wrapInClarityTicks(entry.example)}
`; `;

30
src/common/hooks/use-active-heading.tsx

@ -10,19 +10,37 @@ interface ActiveHeadingReturn {
doChangeSlugInView: (value: string) => void; doChangeSlugInView: (value: string) => void;
} }
export const useActiveHeading = (_slug: string): ActiveHeadingReturn => { const getHash = (url: string) => url?.includes('#') && url.split('#')[1];
export const useWatchActiveHeadingChange = () => {
const router = useRouter(); const router = useRouter();
const asPath = router && router.asPath; const asPath = router && router.asPath;
const { activeSlug, slugInView, doChangeActiveSlug, doChangeSlugInView } = useAppState(); const { activeSlug, doChangeActiveSlug } = useAppState();
const urlHash = asPath?.includes('#') && asPath.split('#')[1]; const urlHash = getHash(asPath);
const location = typeof window !== 'undefined' && window.location.href; const handleRouteChange = url => {
if (url) {
const hash = getHash(url);
if (hash) doChangeActiveSlug(hash);
}
};
useEffect(() => { useEffect(() => {
if (urlHash && !activeSlug) { if ((urlHash && !activeSlug) || (urlHash && urlHash !== activeSlug)) {
doChangeActiveSlug(urlHash); doChangeActiveSlug(urlHash);
} }
}, [asPath, urlHash, location]); router.events.on('hashChangeStart', handleRouteChange);
router.events.on('routeChangeStart', handleRouteChange);
return () => {
router.events.off('hashChangeStart', handleRouteChange);
router.events.off('routeChangeStart', handleRouteChange);
};
}, []);
};
export const useActiveHeading = (_slug: string): ActiveHeadingReturn => {
const { activeSlug, slugInView, doChangeActiveSlug, doChangeSlugInView } = useAppState();
const location = typeof window !== 'undefined' && window.location.href;
const isActive = _slug === activeSlug; const isActive = _slug === activeSlug;

43
src/common/hooks/use-headroom.ts

@ -0,0 +1,43 @@
import { Ref, useEffect, useState } from 'react';
import debounce from 'lodash.debounce';
import { useRect } from '@reach/rect';
import { useScroll } from '@common/hooks/use-scroll';
export const useHeadroom = (target: Ref<HTMLDivElement>, { useStyle = true, wait = 0 } = {}) => {
let styleInserted = false;
const rect = useRect(target as any);
const { scrollY, scrollDirection } = useScroll();
if (typeof document !== 'undefined') {
const header = document.querySelector('.headroom');
const listener = debounce(() => {
header?.classList?.toggle('unpinned', window.pageYOffset >= rect?.height);
}, 50);
useEffect(() => {
if (
scrollDirection === 'down' &&
header.classList.contains('unpinned') &&
header.classList.contains('hidden')
) {
header.classList.remove('hidden');
}
if (
scrollDirection === 'up' &&
header.classList.contains('unpinned') &&
!header.classList.contains('hidden')
) {
header.classList.add('hidden');
}
}, [scrollDirection]);
useEffect(() => {
if (rect) {
document.addEventListener('scroll', listener, { passive: true });
return () => document.removeEventListener('scroll', listener);
}
}, [rect]);
}
};

65
src/common/hooks/use-scroll.tsx

@ -0,0 +1,65 @@
/**
* useScroll React custom hook
* Usage:
* const { scrollX, scrollY, scrollDirection } = useScroll();7
* Original Source: https://gist.github.com/joshuacerbito/ea318a6a7ca4336e9fadb9ae5bbb8f4
*/
import { useState, useEffect } from 'react';
type SSRRect = {
bottom: number;
height: number;
left: number;
right: number;
top: number;
width: number;
x: number;
y: number;
};
const EmptySSRRect: SSRRect = {
bottom: 0,
height: 0,
left: 0,
right: 0,
top: 0,
width: 0,
x: 0,
y: 0,
};
export const useScroll = () => {
const [lastScrollTop, setLastScrollTop] = useState<number>(0);
const [bodyOffset, setBodyOffset] = useState<DOMRect | SSRRect>(
typeof window === 'undefined' || !window.document
? EmptySSRRect
: document.body.getBoundingClientRect()
);
const [scrollY, setScrollY] = useState<number>(bodyOffset.top);
const [scrollX, setScrollX] = useState<number>(bodyOffset.left);
const [scrollDirection, setScrollDirection] = useState<'down' | 'up' | undefined>();
const listener = () => {
setBodyOffset(
typeof window === 'undefined' || !window.document
? EmptySSRRect
: document.body.getBoundingClientRect()
);
setScrollY(-bodyOffset.top);
setScrollX(bodyOffset.left);
setScrollDirection(lastScrollTop > -bodyOffset.top ? 'down' : 'up');
setLastScrollTop(-bodyOffset.top);
};
useEffect(() => {
window.addEventListener('scroll', listener);
return () => {
window.removeEventListener('scroll', listener);
};
});
return {
scrollY,
scrollX,
scrollDirection,
};
};

2
src/components/content-wrapper.tsx

@ -4,7 +4,7 @@ import { Flex, FlexProps, space } from '@blockstack/ui';
const ContentWrapper: React.FC<FlexProps> = props => ( const ContentWrapper: React.FC<FlexProps> = props => (
<Flex <Flex
flexShrink={0} flexShrink={0}
px={space(['none', 'none', 'base', 'base'])} px={space(['none', 'none', 'extra-loose', 'extra-loose'])}
pt={space(['base', 'base', 'extra-loose'])} pt={space(['base', 'base', 'extra-loose'])}
mt={space('extra-loose')} mt={space('extra-loose')}
pb={[4, 4, 6]} pb={[4, 4, 6]}

81
src/components/header.tsx

@ -1,31 +1,20 @@
import React from 'react'; import React from 'react';
import { import { Flex, Box, BlockstackIcon, Stack, color, space, ChevronIcon } from '@blockstack/ui';
Flex,
Box,
BlockstackIcon,
Stack,
color,
space,
transition,
ChevronIcon,
} from '@blockstack/ui';
import { Link, Text, LinkProps } from '@components/typography'; import { Link, Text, LinkProps } from '@components/typography';
import MenuIcon from 'mdi-react/MenuIcon'; import MenuIcon from 'mdi-react/MenuIcon';
import CloseIcon from 'mdi-react/CloseIcon'; import CloseIcon from 'mdi-react/CloseIcon';
import { useLockBodyScroll } from '@common/hooks/use-lock-body-scroll';
import { useMobileMenuState } from '@common/hooks/use-mobile-menu'; import { useMobileMenuState } from '@common/hooks/use-mobile-menu';
import { SideNav } from './side-nav';
import GithubIcon from 'mdi-react/GithubIcon'; import GithubIcon from 'mdi-react/GithubIcon';
import { IconButton } from '@components/icon-button'; import { IconButton } from '@components/icon-button';
import { border } from '@common/utils';
import routes from '@common/routes'; import routes from '@common/routes';
import { css } from '@styled-system/css'; import { css } from '@styled-system/css';
import NextLink from 'next/link'; import NextLink from 'next/link';
import MagnifyIcon from 'mdi-react/MagnifyIcon';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { ColorModeButton } from '@components/color-mode-button'; import { ColorModeButton } from '@components/color-mode-button';
import Search from '@components/search'; import dynamic from 'next/dynamic';
const Search = dynamic(() => import('./search'));
import Headroom from 'react-headroom';
import { border } from '@common/utils';
const MenuButton = ({ ...rest }: any) => { const MenuButton = ({ ...rest }: any) => {
const { isOpen, handleOpen, handleClose } = useMobileMenuState(); const { isOpen, handleOpen, handleClose } = useMobileMenuState();
const Icon = isOpen ? CloseIcon : MenuIcon; const Icon = isOpen ? CloseIcon : MenuIcon;
@ -61,7 +50,7 @@ const BreadCrumbs: React.FC<any> = props => {
return route && section ? ( return route && section ? (
<Flex align="center"> <Flex align="center">
<Box> <Box>
<Text fontSize="14px" fontWeight="600"> <Text fontSize={['12px', '12px', '14px']} fontWeight="500">
Docs Docs
</Text> </Text>
</Box> </Box>
@ -69,7 +58,7 @@ const BreadCrumbs: React.FC<any> = props => {
<ChevronIcon size="20px" /> <ChevronIcon size="20px" />
</Box> </Box>
<Box> <Box>
<Text fontSize="14px" fontWeight="600"> <Text fontSize={['12px', '12px', '14px']} fontWeight="500">
{section?.title} {section?.title}
</Text> </Text>
</Box> </Box>
@ -77,7 +66,7 @@ const BreadCrumbs: React.FC<any> = props => {
<ChevronIcon size="20px" /> <ChevronIcon size="20px" />
</Box> </Box>
<Box> <Box>
<Text fontSize="14px" fontWeight="600"> <Text fontSize={['12px', '12px', '14px']} fontWeight="500">
{route?.title || (route?.headings.length && route.headings[0])} {route?.title || (route?.headings.length && route.headings[0])}
</Text> </Text>
</Box> </Box>
@ -109,26 +98,9 @@ const GithubButton = (props: LinkProps) => (
</IconButton> </IconButton>
); );
const MobileSideNav = () => { const HeaderWrapper: React.FC<any> = React.forwardRef((props, ref) => (
const { isOpen } = useMobileMenuState(); <Box top={2} ref={ref} width="100%" {...props} />
useLockBodyScroll(isOpen); ));
return (
<SideNav
position="fixed"
top={`${HEADER_HEIGHT}px`}
maxHeight={`calc(100vh - ${HEADER_HEIGHT}px)`}
width="100%"
zIndex={99}
bg={color('bg')}
display={isOpen ? ['block', 'block', 'none'] : 'none'}
border="unset"
/>
);
};
const HeaderWrapper: React.FC<any> = props => (
<Box position="fixed" zIndex={9999} width="100%" {...props} />
);
const nav = [ const nav = [
{ {
@ -139,19 +111,26 @@ const nav = [
]; ];
const SubBar: React.FC<any> = props => ( const SubBar: React.FC<any> = props => (
<Flex <Flex
justifyContent="space-between" position="sticky"
zIndex={99}
top={0}
align="center" align="center"
height="60px" height="60px"
width="100%"
px={['extra-loose', 'extra-loose', 'base', 'base']}
bg={color('bg')} bg={color('bg')}
borderBottom={border()} borderBottom={border()}
style={{ {...props}
backdropFilter: 'blur(5px)',
}}
> >
<BreadCrumbs /> <Flex
<Search /> px={space(['extra-loose', 'extra-loose', 'base', 'base'])}
flexGrow={1}
justifyContent="space-between"
align="center"
maxWidth="1280px"
mx="auto"
>
<BreadCrumbs />
<Search />
</Flex>
</Flex> </Flex>
); );
@ -164,13 +143,14 @@ const Header = ({ hideSubBar, ...rest }: any) => {
<Flex <Flex
justifyContent="space-between" justifyContent="space-between"
align="center" align="center"
px={['extra-loose', 'extra-loose', 'base', 'base']}
bg={color('bg')} bg={color('bg')}
borderBottom={border()}
style={{ style={{
backdropFilter: 'blur(5px)', backdropFilter: 'blur(5px)',
}} }}
height="72px" height="72px"
maxWidth="1280px"
mx="auto"
px={space(['extra-loose', 'extra-loose', 'base', 'base'])}
{...rest} {...rest}
> >
<NextLink href="/" passHref> <NextLink href="/" passHref>
@ -224,9 +204,8 @@ const Header = ({ hideSubBar, ...rest }: any) => {
<MenuButton /> <MenuButton />
</Flex> </Flex>
</Flex> </Flex>
{!hideSubBar && <SubBar />}
</HeaderWrapper> </HeaderWrapper>
<MobileSideNav /> {!hideSubBar && <SubBar />}
</> </>
); );
}; };

127
src/components/layouts/base-layout.tsx

@ -1,11 +1,130 @@
import React from 'react'; import React from 'react';
import { Flex } from '@blockstack/ui'; import { Flex, Box, FlexProps, color, space, CloseIcon, Fade, Transition } from '@blockstack/ui';
import { SideNav } from '../side-nav'; import { SideNav } from '../side-nav';
import { Header, HEADER_HEIGHT } from '../header'; import { Header, HEADER_HEIGHT } from '../header';
import { Main } from '../main'; import { Main } from '../main';
import { Footer } from '../footer'; import { Footer } from '../footer';
import NotFoundPage from '@pages/404'; import NotFoundPage from '@pages/404';
import { SIDEBAR_WIDTH } from '@common/constants'; import { SIDEBAR_WIDTH } from '@common/constants';
import { useWatchActiveHeadingChange } from '@common/hooks/use-active-heading';
import { useLockBodyScroll } from '@common/hooks/use-lock-body-scroll';
import { useMobileMenuState } from '@common/hooks/use-mobile-menu';
import { border } from '@common/utils';
const MobileMenu: React.FC<FlexProps> = props => {
const { isOpen, handleClose } = useMobileMenuState();
const [slideIn, setSlideIn] = React.useState(false);
React.useEffect(() => {
if (isOpen && !slideIn) {
setTimeout(() => {
setSlideIn(true);
}, 0);
} else if (slideIn && !isOpen) {
setSlideIn(false);
}
}, [isOpen]);
useLockBodyScroll(isOpen);
return (
<Box position="fixed" zIndex={999999} left={0} top={0}>
<Fade in={isOpen} timeout={250}>
{styles => (
<Box style={{ willChange: 'opacity', ...styles }}>
<Box
position="fixed"
onClick={handleClose}
zIndex={999999}
left={0}
top={0}
size="100%"
bg="ink"
opacity={0.5}
/>
<Transition
timeout={350}
styles={{
init: {
opacity: 0,
transform: 'translateX(50%)',
},
entered: {
opacity: 1,
transform: 'translateX(0)',
},
exited: {
opacity: 0,
transform: 'translateX(50%)',
},
}}
in={isOpen}
>
{slideStyles => (
<Box
position="fixed"
zIndex={999999}
right={0}
top={0}
width="80%"
height="100%"
bg={color('bg')}
style={{
willChange: 'opacity, transform',
...slideStyles,
}}
borderLeft={border()}
>
<Flex
align="center"
justifyContent="flex-end"
height="72px"
px={space(['extra-loose', 'extra-loose', 'base', 'base'])}
position="fixed"
top={0}
right={0}
zIndex={999999}
>
<Box
_hover={{
cursor: 'pointer',
}}
onClick={handleClose}
size="14px"
mr={space('tight')}
color={color('invert')}
>
<CloseIcon />
</Box>
</Flex>
<Box
maxHeight="100vh"
overflow="auto"
px={space(['extra-loose', 'extra-loose', 'base', 'base'])}
py={space('extra-loose')}
>
<SideNav
height="unset"
overflow="inherit"
width="100%"
containerProps={{
position: 'static',
overflow: 'inherit',
height: 'unset',
pt: 0,
pb: 0,
px: 0,
width: '100%',
}}
/>
</Box>
</Box>
)}
</Transition>
</Box>
)}
</Fade>
</Box>
);
};
const BaseLayout: React.FC<{ isHome?: boolean }> = ({ children, isHome }) => { const BaseLayout: React.FC<{ isHome?: boolean }> = ({ children, isHome }) => {
let isErrorPage = false; let isErrorPage = false;
@ -16,10 +135,13 @@ const BaseLayout: React.FC<{ isHome?: boolean }> = ({ children, isHome }) => {
isErrorPage = true; isErrorPage = true;
} }
}); });
useWatchActiveHeadingChange();
return ( return (
<Flex minHeight="100vh" flexDirection="column"> <Flex minHeight="100vh" flexDirection="column">
<MobileMenu />
<Header hideSubBar={isHome || isErrorPage} /> <Header hideSubBar={isHome || isErrorPage} />
<Flex width="100%" flexGrow={1}> <Flex width="100%" flexGrow={1} maxWidth="1280px" mx="auto">
{!isHome && <SideNav display={['none', 'none', 'block']} />} {!isHome && <SideNav display={['none', 'none', 'block']} />}
<Flex <Flex
flexGrow={1} flexGrow={1}
@ -29,7 +151,6 @@ const BaseLayout: React.FC<{ isHome?: boolean }> = ({ children, isHome }) => {
`calc(100% - ${isHome ? 0 : SIDEBAR_WIDTH}px)`, `calc(100% - ${isHome ? 0 : SIDEBAR_WIDTH}px)`,
`calc(100% - ${isHome ? 0 : SIDEBAR_WIDTH}px)`, `calc(100% - ${isHome ? 0 : SIDEBAR_WIDTH}px)`,
]} ]}
mt={`${HEADER_HEIGHT}px`}
flexDirection="column" flexDirection="column"
> >
<Main mx="unset" width={'100%'}> <Main mx="unset" width={'100%'}>

5
src/components/mdx/components/link.tsx

@ -16,7 +16,10 @@ export const SmartLink = ({ href, ...rest }: { href: string }) => {
}; };
export const Link = forwardRef( export const Link = forwardRef(
(props: { href: string; target?: string; rel?: string } & BoxProps, ref: Ref<HTMLDivElement>) => ( (
props: { href?: string; target?: string; rel?: string } & BoxProps,
ref: Ref<HTMLDivElement>
) => (
<Box <Box
as="a" as="a"
ref={ref} ref={ref}

5
src/components/mdx/md-contents.tsx

@ -16,7 +16,6 @@ export const MDContents: React.FC<any> = React.memo(({ headings, children }) =>
} }
mx="unset" mx="unset"
pt="unset" pt="unset"
px="unset"
css={css(styleOverwrites as any)} css={css(styleOverwrites as any)}
> >
{children} {children}
@ -25,9 +24,11 @@ export const MDContents: React.FC<any> = React.memo(({ headings, children }) =>
<TableOfContents <TableOfContents
display={['none', 'none', 'none', 'block']} display={['none', 'none', 'none', 'block']}
position="sticky" position="sticky"
top="195px" top={space('base')}
pt="64px"
pl={space('extra-loose')} pl={space('extra-loose')}
headings={headings} headings={headings}
limit={2}
/> />
) : null} ) : null}
</> </>

84
src/components/mdx/styles.tsx

@ -7,33 +7,49 @@ import { border } from '@common/utils';
export const MdxOverrides = createGlobalStyle` export const MdxOverrides = createGlobalStyle`
@counter-style list { @counter-style list {
pad: "0"; pad: "0";
} }
.DocSearch-Container{ .headroom {
z-index: 99999; top: 0;
left: 0;
right: 0;
zIndex: 1;
}
.headroom--unfixed {
position: relative;
transform: translateY(0);
}
.headroom--scrolled {
transition: transform 200ms ease-in-out;
}
.headroom--unpinned {
position: fixed;
transform: translateY(-100%);
}
.headroom--pinned {
position: fixed;
transform: translateY(0%);
} }
:root{ :root{
--docsearch-modal-background: ${color('bg')}; --docsearch-modal-background: ${color('bg')};
--docsearch-primary-color-R: 84; --docsearch-text-color: ${color('text-title')};
--docsearch-primary-color-G: 104; --docsearch-icon-color: ${color('text-caption')};
--docsearch-primary-color-B: 255;
--docsearch-primary-color: ${color('accent')}; --docsearch-primary-color: ${color('accent')};
--docsearch-input-color: ${color('text-title')}; --docsearch-input-color: ${color('text-title')};
--docsearch-highlight-color: var(--docsearch-primary-color); --docsearch-highlight-color: ${color('bg-alt')};
--docsearch-placeholder-color: ${color('text-caption')}; --docsearch-placeholder-color: ${color('text-caption')};
--docsearch-container-background: rgba(22,22,22,0.75); --docsearch-container-background: rgba(22,22,22,0.75);
--docsearch-modal-shadow: inset 1px 1px 0px 0px hsla(0,0%,100%,0.5),0px 3px 8px 0px #555a64; --docsearch-modal-shadow: inset 0px 0px 1px 1px ${color('border')};
--docsearch-searchbox-background: var(--ifm-color-emphasis-300); --docsearch-searchbox-background: var(--ifm-color-emphasis-300);
--docsearch-searchbox-focus-background: #fff; --docsearch-searchbox-focus-background: ${color('bg')};;
--docsearch-searchbox-shadow: inset 0px 0px 0px 2px rgba(var(--docsearch-primary-color-R),var(--docsearch-primary-color-G),var(--docsearch-primary-color-B),0.5); --docsearch-searchbox-shadow: inset 0px 0px 1px 1px ${color('border')};
--docsearch-hit-color: var(--ifm-color-emphasis-800); --docsearch-hit-color: var(--ifm-color-emphasis-800);
--docsearch-hit-active-color: #fff; --docsearch-hit-active-color: ${color('text-title')};
--docsearch-hit-background: #fff; --docsearch-hit-background: ${color('bg')};
--docsearch-hit-shadow: 0px 1px 3px 0px #d4d9e1; --docsearch-hit-shadow: inset 0px 0px 1px 1px ${color('border')};
--docsearch-key-gradient: linear-gradient(-225deg,#d5dbe4,#f8f8f8); --docsearch-key-gradient: transparent;
--docsearch-key-shadow: inset 0px -2px 0px 0px #cdcde6,inset 0px 0px 1px 1px #fff,0px 1px 2px 1px rgba(30,35,90,0.4); --docsearch-key-shadow: inset 0px -2px 0px 0px transparent,inset 0px 0px 1px 1px transparent,0px 1px 2px 1px transparent;
--docsearch-footer-background: #fff; --docsearch-footer-background: ${color('bg')};
--docsearch-footer-shadow: 0px -1px 0px 0px #e0e3e8; --docsearch-footer-shadow: inset 0px 0px 1px 1px ${color('border')};
--docsearch-logo-color: #5468ff; --docsearch-logo-color: #5468ff;
--docsearch-muted-color: #969faf; --docsearch-muted-color: #969faf;
--docsearch-modal-width: 560px; --docsearch-modal-width: 560px;
@ -44,6 +60,40 @@ z-index: 99999;
--docsearch-spacing: 12px; --docsearch-spacing: 12px;
--docsearch-icon-stroke-width: 1.4; --docsearch-icon-stroke-width: 1.4;
} }
.DocSearch-Container{
z-index: 99999;
}
.DocSearch-SearchBar{
padding: var(--docsearch-spacing);
}
.DocSearch-Reset:hover{
color: ${color('accent')};
}
.DocSearch-Form{
input{
color: ${color('text-title')};
}
&:focus-within{
box-shadow: 0 0 0 3px rgba(170, 179, 255, 0.75);
}
}
.DocSearch-Help{
text-align: center;
}
.DocSearch-Prefill{
color: ${color('accent')} !important;
}
.DocSearch-Hit{
mark{
color: ${color('accent')} !important;
}
}
.DocSearch-Hit-source{
color: ${color('text-caption')};
}
.DocSearch-MagnifierLabel{
color: ${color('accent')};
}
pre{ pre{
display: inline-block; display: inline-block;

30
src/components/search.tsx

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Box, Flex, Portal, space, Fade, themeColor, color } from '@blockstack/ui'; import { Box, Flex, Portal, space, Fade, themeColor, color } from '@blockstack/ui';
import { useDocSearchKeyboardEvents, DocSearchModal } from '@docsearch/react'; import { useDocSearchKeyboardEvents } from '@docsearch/react';
import { border } from '@common/utils'; import { border } from '@common/utils';
import { Text } from '@components/typography'; import { Text } from '@components/typography';
@ -17,10 +17,11 @@ const getLocalUrl = href => {
.replace('storage/clidocs', 'core/cmdLineRef'); .replace('storage/clidocs', 'core/cmdLineRef');
return url; return url;
}; };
function Hit({ hit, children }: any) { function Hit({ hit, children }: any) {
const url = getLocalUrl(hit.url); const url = getLocalUrl(hit.url);
return ( return (
<Link href={url} passHref> <Link href={url} as={url} passHref scroll={!url.includes('#')}>
<a>{children}</a> <a>{children}</a>
</Link> </Link>
); );
@ -29,7 +30,7 @@ function Hit({ hit, children }: any) {
const navigator = { const navigator = {
navigate: async ({ suggestionUrl }: any) => { navigate: async ({ suggestionUrl }: any) => {
const url = getLocalUrl(suggestionUrl); const url = getLocalUrl(suggestionUrl);
return Router.push(url); return Router.push(url, url);
}, },
}; };
@ -38,14 +39,29 @@ const searchOptions = {
indexName: 'blockstack', indexName: 'blockstack',
navigator, navigator,
}; };
export const SearchBox: React.FC<any> = () => {
let DocSearchModal: any = null;
export const SearchBox: React.FC<any> = React.memo(() => {
const [isOpen, setIsOpen] = React.useState(false); const [isOpen, setIsOpen] = React.useState(false);
const importDocSearchModalIfNeeded = React.useCallback(function importDocSearchModalIfNeeded() {
if (DocSearchModal) {
return Promise.resolve();
}
return Promise.all([import('@docsearch/react/modal')]).then(([{ DocSearchModal: Modal }]) => {
DocSearchModal = Modal;
});
}, []);
const onOpen = React.useCallback( const onOpen = React.useCallback(
function onOpen() { function onOpen() {
setIsOpen(true); void importDocSearchModalIfNeeded().then(() => {
setIsOpen(true);
});
}, },
[setIsOpen] [importDocSearchModalIfNeeded, setIsOpen]
); );
const onClose = React.useCallback( const onClose = React.useCallback(
@ -90,6 +106,6 @@ export const SearchBox: React.FC<any> = () => {
</Box> </Box>
</> </>
); );
}; });
export default SearchBox; export default SearchBox;

170
src/components/side-nav.tsx

@ -1,65 +1,62 @@
import React from 'react'; import React from 'react';
import { Flex, Box, color, space, ChevronIcon } from '@blockstack/ui'; import { Flex, Box, color, space, ChevronIcon, BoxProps } from '@blockstack/ui';
import { border } from '@common/utils'; import { Text, Caption, LinkProps } from '@components/typography';
import { Text, Caption } from '@components/typography';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import routes from '@common/routes'; import routes from '@common/routes';
import { useMobileMenuState } from '@common/hooks/use-mobile-menu'; import { useMobileMenuState } from '@common/hooks/use-mobile-menu';
import dynamic from 'next/dynamic';
const SearchBox = dynamic(() => import('./search'));
import { SIDEBAR_WIDTH } from '@common/constants'; import { SIDEBAR_WIDTH } from '@common/constants';
import { HEADER_HEIGHT } from '@components/header';
const Wrapper = ({ width = `${SIDEBAR_WIDTH}px`, children, ...rest }: any) => ( const Wrapper: React.FC<BoxProps & { containerProps?: BoxProps }> = ({
<Box width = `${SIDEBAR_WIDTH}px`,
position="relative" containerProps,
width={width} children,
maxWidth={width} ...rest
height={`calc(100vh - ${HEADER_HEIGHT}px)`} }) => {
flexGrow={0} return (
flexShrink={0} <Box width={width} maxWidth={width} flexGrow={0} flexShrink={0} {...rest}>
overflow="auto" <Box
{...rest} position="sticky"
> width={width}
<Box maxHeight={`calc(100vh - 60px)`}
position="fixed" overflow="auto"
top={HEADER_HEIGHT} pb="62px"
width={width} px={space('base')}
height={`calc(100vh - ${HEADER_HEIGHT}px)`} top="60px"
overflow="auto" pt={space('base')}
borderRight={['none', border(), border()]} {...containerProps}
pb={space('base-loose')} >
> {children}
{children} </Box>
</Box> </Box>
</Box> );
); };
const LinkItem = React.forwardRef(({ isActive, ...rest }: any, ref) => ( const LinkItem: React.FC<LinkProps & { isActive?: boolean }> = React.forwardRef(
<Text ({ isActive, ...rest }, ref) => (
ref={ref} <Text
_hover={ ref={ref}
!isActive _hover={
? { !isActive
color: 'var(--colors-accent)', ? {
cursor: 'pointer', color: 'var(--colors-accent)',
textDecoration: 'underline', cursor: 'pointer',
} textDecoration: 'underline',
: null }
} : null
color={isActive ? color('accent') : color('text-body')} }
fontWeight={isActive ? 'semibold' : 'normal'} color={isActive ? color('accent') : color('text-caption')}
fontSize={['16px', '16px', '14px']} fontSize="14px"
lineHeight="20px" lineHeight="20px"
as="a" as="a"
display="block" display="block"
py={space(['extra-tight', 'extra-tight', 'extra-tight'])} py={space(['extra-tight', 'extra-tight', 'extra-tight'])}
{...rest} {...rest}
/> />
)); )
);
const Links = ({ routes, prefix = '', ...rest }: any) => { const Links: React.FC<BoxProps & { routes?: any }> = ({ routes, prefix = '', ...rest }) => {
const router = useRouter(); const router = useRouter();
const { handleClose } = useMobileMenuState(); const { handleClose } = useMobileMenuState();
const { pathname } = router; const { pathname } = router;
@ -67,7 +64,7 @@ const Links = ({ routes, prefix = '', ...rest }: any) => {
return routes.map((route, linkKey) => { return routes.map((route, linkKey) => {
const isActive = pathname === `/${route.path}`; const isActive = pathname === `/${route.path}`;
return ( return (
<Box width="100%" px="base" py="1px" key={linkKey} onClick={handleClose} {...rest}> <Box width="100%" py="1px" key={linkKey} onClick={handleClose} {...rest}>
<Link href={`/${route.path}`} passHref> <Link href={`/${route.path}`} passHref>
<LinkItem isActive={isActive} width="100%" href={`/${route.path}`}> <LinkItem isActive={isActive} width="100%" href={`/${route.path}`}>
{route.title || {route.title ||
@ -80,57 +77,72 @@ const Links = ({ routes, prefix = '', ...rest }: any) => {
}); });
}; };
const SectionTitle = ({ children, textStyles, ...rest }: any) => ( const SectionTitle: React.FC<BoxProps & { textStyles?: BoxProps }> = ({
<Box px={space('base')} pb={space('extra-tight')} {...rest}> children,
<Caption fontSize="14px" fontWeight="600" color={color('text-title')} {...textStyles}> textStyles,
...rest
}) => (
<Box pb={space('extra-tight')} {...rest}>
<Caption fontSize="14px" fontWeight="500" color={color('text-title')} {...textStyles}>
{children} {children}
</Caption> </Caption>
</Box> </Box>
); );
const Section = ({ section, isLast, ...rest }: any) => { const Section = ({ section, visible, isLast, isFirst, setVisible, ...rest }: any) => {
const router = useRouter(); const isVisible = section.title === visible?.title;
const { pathname } = router;
const isActive = section.routes.find(route => pathname === `/${route.path}`);
const [visible, setVisible] = React.useState(isActive);
React.useEffect(() => {
if (isActive && !visible) {
setVisible(true);
}
}, [router, isActive]);
return ( return (
<Box width="100%" pt={space('base')} {...rest}> <Box width="100%" pt={isFirst ? 'unset' : space('base')} {...rest}>
{section.title ? ( {section.title ? (
<Flex <Flex
width="100%"
align="center" align="center"
pr={space('base')} onClick={() => setVisible(section)}
justify="space-between"
onClick={() => setVisible(!visible)}
_hover={{ _hover={{
cursor: 'pointer', cursor: 'pointer',
}} }}
> >
<SectionTitle>{section.title}</SectionTitle> <SectionTitle>{section.title}</SectionTitle>
<Box color={color('text-caption')}> <Box ml="extra-tight" color={color('text-caption')}>
<ChevronIcon size="24px" direction={visible ? 'up' : 'down'} /> <ChevronIcon size="20px" direction={visible ? 'up' : 'down'} />
</Box> </Box>
</Flex> </Flex>
) : null} ) : null}
{visible && <Links routes={section.routes} />} {isVisible && <Links routes={section.routes} />}
</Box> </Box>
); );
}; };
const SideNav = ({ ...rest }: any) => { export const SideNav: React.FC<BoxProps & { containerProps?: BoxProps }> = ({
containerProps,
...rest
}) => {
const router = useRouter();
const { pathname } = router;
const active = routes.find(section =>
section.routes.find(route => pathname === `/${route.path}`)
);
const [visible, setVisible] = React.useState(active);
const handleSectionClick = (section: any) => {
if (section?.title === active?.title) {
setVisible(false);
} else {
setVisible(section);
}
};
return ( return (
<Wrapper {...rest}> <Wrapper containerProps={containerProps} {...rest}>
{routes.map((section, sectionKey, arr) => ( {routes.map((section, sectionKey, arr) => (
<Section key={sectionKey} section={section} isLast={sectionKey === arr.length - 1} /> <Section
visible={visible}
key={sectionKey}
section={section}
isLast={sectionKey === arr.length - 1}
isFirst={sectionKey === 0}
setVisible={handleSectionClick}
/>
))} ))}
</Wrapper> </Wrapper>
); );
}; };
export { SideNav };

41
src/components/toc.tsx

@ -5,7 +5,7 @@ import { slugify } from '@common/utils';
import { Text } from '@components/typography'; import { Text } from '@components/typography';
import { Link } from '@components/mdx'; import { Link } from '@components/mdx';
import { useActiveHeading } from '@common/hooks/use-active-heading'; import { useActiveHeading } from '@common/hooks/use-active-heading';
import NextLink from 'next/link';
const getLevelPadding = (level: number) => { const getLevelPadding = (level: number) => {
switch (level) { switch (level) {
case 2: case 2:
@ -17,30 +17,30 @@ const getLevelPadding = (level: number) => {
} }
}; };
const Item = ({ slug, label, level }) => { const Item = ({ slug, label, level, limit }) => {
const { isActive: _isActive, doChangeActiveSlug, slugInView } = useActiveHeading(slug); const { isActive: _isActive, doChangeActiveSlug, slugInView } = useActiveHeading(slug);
const isOnScreen = slugInView === slug; const isOnScreen = slugInView === slug;
const isActive = isOnScreen || _isActive; const isActive = isOnScreen || _isActive;
return ( return !limit || level <= limit + 1 ? (
<Box pl={getLevelPadding(level - 2)} py={space('extra-tight')}> <Box pl={getLevelPadding(level - 2)} py={space('extra-tight')}>
<Link <NextLink href={`#${slug}`} passHref>
href={`#${slug}`} <Link
fontSize="14px" fontSize="14px"
color={isActive ? color('text-title') : color('text-caption')} color={isActive ? color('text-title') : color('text-caption')}
fontWeight={isActive ? '600' : '400'} fontWeight={isActive ? '600' : '400'}
onClick={() => doChangeActiveSlug(slug)} textDecoration="none"
textDecoration="none" _hover={{
_hover={{ textDecoration: 'underline',
textDecoration: 'underline', color: color('accent'),
color: color('accent'), }}
}} pointerEvents={isActive ? 'none' : 'unset'}
pointerEvents={isActive ? 'none' : 'unset'} >
> <Box as="span" dangerouslySetInnerHTML={{ __html: label }} />
<Box as="span" dangerouslySetInnerHTML={{ __html: label }} /> </Link>
</Link> </NextLink>
</Box> </Box>
); ) : null;
}; };
export const TableOfContents = ({ export const TableOfContents = ({
@ -49,6 +49,7 @@ export const TableOfContents = ({
label = 'On this page', label = 'On this page',
columns = 1, columns = 1,
display, display,
limit,
...rest ...rest
}: { }: {
headings?: { headings?: {
@ -57,6 +58,7 @@ export const TableOfContents = ({
}[]; }[];
noLabel?: boolean; noLabel?: boolean;
label?: string; label?: string;
limit?: number;
columns?: number | number[]; columns?: number | number[];
} & BoxProps) => { } & BoxProps) => {
return ( return (
@ -85,6 +87,7 @@ export const TableOfContents = ({
{headings.map((heading, index) => { {headings.map((heading, index) => {
return index > 0 ? ( return index > 0 ? (
<Item <Item
limit={limit}
level={heading.level} level={heading.level}
slug={slugify(heading.content)} slug={slugify(heading.content)}
label={heading.content} label={heading.content}

1
src/pages/_app.tsx

@ -93,7 +93,6 @@ const AppWrapper = ({ children, isHome }: any) => {
const MyApp = ({ Component, pageProps, colorMode, ...rest }: any) => { const MyApp = ({ Component, pageProps, colorMode, ...rest }: any) => {
const { isHome } = pageProps; const { isHome } = pageProps;
return ( return (
<> <>
<GoogleFonts href="https://fonts.googleapis.com/css2?family=Fira+Code&family=Inter:wght@400;500;600;700&display=swap" /> <GoogleFonts href="https://fonts.googleapis.com/css2?family=Fira+Code&family=Inter:wght@400;500;600;700&display=swap" />

2
src/pages/_document.tsx

@ -49,6 +49,8 @@ export default class MyDocument extends Document<any> {
/> />
<link rel="preconnect" href="https://bh4d9od16a-dsn.algolia.net" crossOrigin="true" /> <link rel="preconnect" href="https://bh4d9od16a-dsn.algolia.net" crossOrigin="true" />
<link rel="preconnect" href="https://cdn.usefathom.com" crossOrigin="true" /> <link rel="preconnect" href="https://cdn.usefathom.com" crossOrigin="true" />
<link rel="preconnect" href="https://fonts.googleapis.com" crossOrigin="true" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="true" />
<Main /> <Main />
<NextScript /> <NextScript />
</body> </body>

812
yarn.lock

File diff suppressed because it is too large
Loading…
Cancel
Save