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
const DATA_FETCH_FNS = ['getStaticPaths', 'getStaticProps', 'getServerProps']
const DATA_FETCH_FNS = ['getStaticPaths', 'getStaticProps', 'getServerProps'];
module.exports = () => {
return {
@ -19,14 +19,12 @@ module.exports = () => {
if (
DATA_FETCH_FNS.includes(path.node.value.name) &&
path.findParent(
(path) =>
path.isVariableDeclarator() &&
path.node.id.name === 'layoutProps',
path => 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 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 = [
[require('./remark-include'), { resolveFrom: path.join(__dirname, '../src/includes') }],
require('remark-vscode'),
memoize(require('./remark-paragraph-alerts')),
memoize(require('remark-external-links')),
memoize(require('remark-emoji')),
memoize(require('remark-images')),
memoize(require('remark-unwrap-images')),
memoize(require('remark-normalize-headings')),
memoize(require('remark-slug')),
[memoize(include), { resolveFrom: path.join(__dirname, '../src/includes') }],
memoize(vscode),
memoize(paragraphAlerts),
memoize(emoji),
memoize(images),
memoize(unwrapImages),
memoize(normalizeHeadings),
memoize(slug),
memoize(headingID),
];
module.exports = { remarkPlugins };

3
next.config.js

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

9
package.json

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

5
src/common/constants.ts

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

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

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

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

@ -10,19 +10,37 @@ interface ActiveHeadingReturn {
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 asPath = router && router.asPath;
const { activeSlug, slugInView, doChangeActiveSlug, doChangeSlugInView } = useAppState();
const urlHash = asPath?.includes('#') && asPath.split('#')[1];
const { activeSlug, doChangeActiveSlug } = useAppState();
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(() => {
if (urlHash && !activeSlug) {
if ((urlHash && !activeSlug) || (urlHash && urlHash !== activeSlug)) {
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;

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 => (
<Flex
flexShrink={0}
px={space(['none', 'none', 'base', 'base'])}
px={space(['none', 'none', 'extra-loose', 'extra-loose'])}
pt={space(['base', 'base', 'extra-loose'])}
mt={space('extra-loose')}
pb={[4, 4, 6]}

81
src/components/header.tsx

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

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

@ -1,11 +1,130 @@
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 { Header, HEADER_HEIGHT } from '../header';
import { Main } from '../main';
import { Footer } from '../footer';
import NotFoundPage from '@pages/404';
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 }) => {
let isErrorPage = false;
@ -16,10 +135,13 @@ const BaseLayout: React.FC<{ isHome?: boolean }> = ({ children, isHome }) => {
isErrorPage = true;
}
});
useWatchActiveHeadingChange();
return (
<Flex minHeight="100vh" flexDirection="column">
<MobileMenu />
<Header hideSubBar={isHome || isErrorPage} />
<Flex width="100%" flexGrow={1}>
<Flex width="100%" flexGrow={1} maxWidth="1280px" mx="auto">
{!isHome && <SideNav display={['none', 'none', 'block']} />}
<Flex
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)`,
]}
mt={`${HEADER_HEIGHT}px`}
flexDirection="column"
>
<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(
(props: { href: string; target?: string; rel?: string } & BoxProps, ref: Ref<HTMLDivElement>) => (
(
props: { href?: string; target?: string; rel?: string } & BoxProps,
ref: Ref<HTMLDivElement>
) => (
<Box
as="a"
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"
pt="unset"
px="unset"
css={css(styleOverwrites as any)}
>
{children}
@ -25,9 +24,11 @@ export const MDContents: React.FC<any> = React.memo(({ headings, children }) =>
<TableOfContents
display={['none', 'none', 'none', 'block']}
position="sticky"
top="195px"
top={space('base')}
pt="64px"
pl={space('extra-loose')}
headings={headings}
limit={2}
/>
) : null}
</>

84
src/components/mdx/styles.tsx

@ -7,33 +7,49 @@ import { border } from '@common/utils';
export const MdxOverrides = createGlobalStyle`
@counter-style list {
pad: "0";
}
.DocSearch-Container{
z-index: 99999;
.headroom {
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{
--docsearch-modal-background: ${color('bg')};
--docsearch-primary-color-R: 84;
--docsearch-primary-color-G: 104;
--docsearch-primary-color-B: 255;
--docsearch-text-color: ${color('text-title')};
--docsearch-icon-color: ${color('text-caption')};
--docsearch-primary-color: ${color('accent')};
--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-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-focus-background: #fff;
--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-focus-background: ${color('bg')};;
--docsearch-searchbox-shadow: inset 0px 0px 1px 1px ${color('border')};
--docsearch-hit-color: var(--ifm-color-emphasis-800);
--docsearch-hit-active-color: #fff;
--docsearch-hit-background: #fff;
--docsearch-hit-shadow: 0px 1px 3px 0px #d4d9e1;
--docsearch-key-gradient: linear-gradient(-225deg,#d5dbe4,#f8f8f8);
--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-footer-background: #fff;
--docsearch-footer-shadow: 0px -1px 0px 0px #e0e3e8;
--docsearch-hit-active-color: ${color('text-title')};
--docsearch-hit-background: ${color('bg')};
--docsearch-hit-shadow: inset 0px 0px 1px 1px ${color('border')};
--docsearch-key-gradient: transparent;
--docsearch-key-shadow: inset 0px -2px 0px 0px transparent,inset 0px 0px 1px 1px transparent,0px 1px 2px 1px transparent;
--docsearch-footer-background: ${color('bg')};
--docsearch-footer-shadow: inset 0px 0px 1px 1px ${color('border')};
--docsearch-logo-color: #5468ff;
--docsearch-muted-color: #969faf;
--docsearch-modal-width: 560px;
@ -44,6 +60,40 @@ z-index: 99999;
--docsearch-spacing: 12px;
--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{
display: inline-block;

30
src/components/search.tsx

@ -1,6 +1,6 @@
import React from 'react';
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 { Text } from '@components/typography';
@ -17,10 +17,11 @@ const getLocalUrl = href => {
.replace('storage/clidocs', 'core/cmdLineRef');
return url;
};
function Hit({ hit, children }: any) {
const url = getLocalUrl(hit.url);
return (
<Link href={url} passHref>
<Link href={url} as={url} passHref scroll={!url.includes('#')}>
<a>{children}</a>
</Link>
);
@ -29,7 +30,7 @@ function Hit({ hit, children }: any) {
const navigator = {
navigate: async ({ suggestionUrl }: any) => {
const url = getLocalUrl(suggestionUrl);
return Router.push(url);
return Router.push(url, url);
},
};
@ -38,14 +39,29 @@ const searchOptions = {
indexName: 'blockstack',
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 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(
function onOpen() {
setIsOpen(true);
void importDocSearchModalIfNeeded().then(() => {
setIsOpen(true);
});
},
[setIsOpen]
[importDocSearchModalIfNeeded, setIsOpen]
);
const onClose = React.useCallback(
@ -90,6 +106,6 @@ export const SearchBox: React.FC<any> = () => {
</Box>
</>
);
};
});
export default SearchBox;

170
src/components/side-nav.tsx

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

41
src/components/toc.tsx

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

812
yarn.lock

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