fix: dedupe shared page components into wrap-page-element

Luke Karrys committed Oct 15, 2023 at 16:33 UTC 3560a454fdd61c69a827b3028eaf707ffe10c575
26 files changed +365 -368
content/index.mdx
+1 -1
@@ -3,7 +3,7 @@ title: npm Documentation
3 edit_on_github: false
4 ---
5
6 -import HeroLayout from 'theme/layout/hero.js'
6 +import HeroLayout from '~/layout/hero.js'
7 export default HeroLayout
8
9 <Index depth='1' />
gatsby-config.js
+2 -2
@@ -3,7 +3,7 @@ const fs = require('fs')
3
4 const {NODE_ENV, GATSBY_PARTIAL_CONTENT, GATSBY_CONTENT_DIR = 'content'} = process.env
5 const DEV = NODE_ENV === 'development'
6 -const CONTENT_DIR = path.resolve(__dirname, '..', GATSBY_CONTENT_DIR)
6 +const CONTENT_DIR = path.resolve(GATSBY_CONTENT_DIR)
7
8 const walkDirs = dir => {
9 const dirs = fs
@@ -77,7 +77,7 @@ module.exports = {
77 {
78 resolve: 'gatsby-plugin-manifest',
79 options: {
80 - icon: path.resolve('./src/images/favicon.png'),
80 + icon: path.resolve('./src/favicon.png'),
81 },
82 },
83 'gatsby-plugin-meta-redirect',
src/components/clipboard-copy.js
+2 -2
@@ -1,5 +1,5 @@
1 import React from 'react'
2 -import {Button, StyledOcticon, themeGet} from '@primer/react'
2 +import {Button, Octicon, themeGet} from '@primer/react'
3 import {CheckIcon, CopyIcon} from '@primer/octicons-react'
4 import styled from 'styled-components'
5 import copy from 'copy-to-clipboard'
@@ -33,7 +33,7 @@ function ClipboardCopy({value}) {
33 announce(`Copied to clipboard`)
34 }}
35 >
36 - {copied ? <StyledOcticon icon={CheckIcon} color="green.5" /> : <StyledOcticon icon={CopyIcon} color="gray.7" />}
36 + {copied ? <Octicon icon={CheckIcon} color="green.5" /> : <Octicon icon={CopyIcon} color="gray.7" />}
37 </CopyToClipboard>
38 )
39 }
src/components/container.js
+1 -1
@@ -3,7 +3,7 @@ import React from 'react'
3
4 function Container({children}) {
5 return (
6 - <Box id="skip-nav" width="100%" maxWidth={960} p={5} mx="auto">
6 + <Box width="100%" maxWidth={960} p={5} mx="auto">
7 {children}
8 </Box>
9 )
src/components/contributors.js
+3 -2
@@ -1,8 +1,6 @@
1 import {Box, Avatar, Link, Text, Tooltip} from '@primer/react'
2 import React from 'react'
3
4 -const pluralize = (word, count) => `${word}${count === 1 ? '' : 's'}`
5 -
4 const months = [
5 'January',
6 'February',
@@ -17,8 +15,11 @@ const months = [
15 'November',
16 'December',
17 ]
18 +
19 const format = d => `${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`
20
21 +const pluralize = (word, count) => `${word}${count === 1 ? '' : 's'}`
22 +
23 function Contributors({logins, latestCommit}) {
24 return (
25 <div>
src/components/drawer.js
+52 -54
@@ -3,59 +3,57 @@ import {Box} from '@primer/react'
3 import {AnimatePresence, motion} from 'framer-motion'
4 import {FocusOn} from 'react-focus-on'
5
6 -function Drawer({isOpen, onDismiss, children}) {
7 - return (
8 - <AnimatePresence>
9 - {isOpen ? (
10 - // These event handlers fix a bug that caused links below the fold
11 - // to be unclickable in macOS Safari.
12 - // Reference: https://github.com/theKashey/react-focus-lock/issues/79
13 - <div
14 - style={{textAlign: 'start', fontSize: '1rem', lineHeight: '1.5rem'}}
15 - onMouseDown={event => event.preventDefault()}
16 - onKeyDown={event => event.target.focus()}
17 - onClick={event => event.target.focus()}
18 - role="button"
19 - tabIndex="0"
20 - >
21 - <FocusOn returnFocus={true} onEscapeKey={() => onDismiss()}>
22 - <Box
23 - position="fixed"
24 - key="overlay"
25 - as={motion.div}
26 - initial={{opacity: 0}}
27 - animate={{opacity: 1}}
28 - exit={{opacity: 0}}
29 - transition={{type: 'tween'}}
30 - top={0}
31 - right={0}
32 - bottom={0}
33 - left={0}
34 - bg="rgba(0, 0, 0, 0.5)"
35 - onClick={() => onDismiss()}
36 - />
37 - <Box
38 - position="fixed"
39 - key="drawer"
40 - as={motion.div}
41 - initial={{x: '100%'}}
42 - animate={{x: 0}}
43 - exit={{x: '100%'}}
44 - transition={{type: 'tween', duration: 0.2}}
45 - width={300}
46 - top={0}
47 - right={0}
48 - bottom={0}
49 - bg="gray.0"
50 - style={{zIndex: 1}}
51 - >
52 - {children}
53 - </Box>
54 - </FocusOn>
55 - </div>
56 - ) : null}
57 - </AnimatePresence>
58 - )
59 -}
6 +const Drawer = ({isOpen, onDismiss, children}) => (
7 + <AnimatePresence>
8 + {isOpen ? (
9 + // These event handlers fix a bug that caused links below the fold
10 + // to be unclickable in macOS Safari.
11 + // Reference: https://github.com/theKashey/react-focus-lock/issues/79
12 + <div
13 + style={{textAlign: 'start', fontSize: '1rem', lineHeight: '1.5rem'}}
14 + onMouseDown={event => event.preventDefault()}
15 + onKeyDown={event => event.target.focus()}
16 + onClick={event => event.target.focus()}
17 + role="button"
18 + tabIndex="0"
19 + >
20 + <FocusOn returnFocus={true} onEscapeKey={() => onDismiss()}>
21 + <Box
22 + position="fixed"
23 + key="overlay"
24 + as={motion.div}
25 + initial={{opacity: 0}}
26 + animate={{opacity: 1}}
27 + exit={{opacity: 0}}
28 + transition={{type: 'tween'}}
29 + top={0}
30 + right={0}
31 + bottom={0}
32 + left={0}
33 + bg="rgba(0, 0, 0, 0.5)"
34 + onClick={() => onDismiss()}
35 + />
36 + <Box
37 + position="fixed"
38 + key="drawer"
39 + as={motion.div}
40 + initial={{x: '100%'}}
41 + animate={{x: 0}}
42 + exit={{x: '100%'}}
43 + transition={{type: 'tween', duration: 0.2}}
44 + width={300}
45 + top={0}
46 + right={0}
47 + bottom={0}
48 + bg="gray.0"
49 + style={{zIndex: 1}}
50 + >
51 + {children}
52 + </Box>
53 + </FocusOn>
54 + </div>
55 + ) : null}
56 + </AnimatePresence>
57 +)
58
59 export default Drawer
src/components/head.js
+7 -4
@@ -1,12 +1,15 @@
1 import React from 'react'
2 import {Helmet} from 'react-helmet'
3 import useSiteMetdata from '../hooks/use-site-metadata'
4 +import {useFrontmatter} from '../layout'
5
5 -function Head(props) {
6 +function Head() {
7 + const fm = useFrontmatter()
8 const siteMetadata = useSiteMetdata()
7 - const title = [props.title, siteMetadata.title].filter(Boolean).join(' | ')
8 - const description = props.description || siteMetadata.description
9 - const lang = props.lang || siteMetadata.lang
9 +
10 + const title = [fm.title, siteMetadata.title].filter(Boolean).join(' | ')
11 + const description = fm.description || siteMetadata.description
12 + const lang = fm.lang || siteMetadata.lang
13
14 return (
15 <Helmet>
src/components/header.js
+22 -27
@@ -9,26 +9,17 @@ import NpmLogo from './npm-logo'
9 import useSearch from '../hooks/use-search'
10 import useSiteMetadata from '../hooks/use-site-metadata'
11 import headerNavItems from '../header-nav.yml'
12 -
13 -export const HEADER_HEIGHT = 66
12 +import {HEADER_HEIGHT} from '../constants'
13
14 const NpmHeaderBar = styled(Box)`
15 height: 10px;
16 background-image: linear-gradient(139deg, #fb8817, #ff4b01, #c12127, #e02aff);
17 `
18
20 -function Header({location, repositoryUrl}) {
19 +function Header() {
20 const siteMetadata = useSiteMetadata()
21 const search = useSearch()
22
24 - const logoStyle = {color: '#cb0000', marginRight: '16px'}
25 - const titleStyle = {
26 - color: '#dddddd',
27 - fontWeight: '600',
28 - display: 'flex',
29 - alignItems: 'center',
30 - }
31 -
23 return (
24 <Box position="sticky" top={0} sx={{zIndex: 1}} role="banner">
25 <NpmHeaderBar />
@@ -41,8 +32,18 @@ function Header({location, repositoryUrl}) {
32 bg="#333333"
33 >
34 <Box display="flex" alignItems="center">
44 - <Link as={GatsbyLink} to="/" style={titleStyle} sx={{mr: 4}}>
45 - <NpmLogo size="32" style={logoStyle} />
35 + <Link
36 + as={GatsbyLink}
37 + to="/"
38 + style={{
39 + color: '#dddddd',
40 + fontWeight: '600',
41 + display: 'flex',
42 + alignItems: 'center',
43 + }}
44 + sx={{mr: 4}}
45 + >
46 + <NpmLogo size="32" style={{color: '#cb0000', marginRight: '16px'}} />
47 {siteMetadata.title}
48 </Link>
49 <Box display={['none', null, null, 'block']} ml={4}>
@@ -51,11 +52,17 @@ function Header({location, repositoryUrl}) {
52 </Box>
53 <Box display="flex">
54 <Box display={['none', null, null, 'block']}>
54 - <HeaderNavItems items={headerNavItems} />
55 + <Box display="flex" alignItems="center" color="gray.2">
56 + {headerNavItems.map((item, index) => (
57 + <Link key={index} href={item.url} sx={{display: 'block', color: 'inherit', ml: 4}}>
58 + {item.title}
59 + </Link>
60 + ))}
61 + </Box>
62 </Box>
63 <Box display={['flex', null, null, 'none']}>
64 <MobileSearch {...search} />
58 - <NavDrawer location={location} repositoryUrl={repositoryUrl} />
65 + <NavDrawer />
66 </Box>
67 </Box>
68 </Box>
@@ -63,16 +70,4 @@ function Header({location, repositoryUrl}) {
70 )
71 }
72
66 -function HeaderNavItems({items}) {
67 - return (
68 - <Box display="flex" alignItems="center" color="gray.2">
69 - {items.map((item, index) => (
70 - <Link key={index} href={item.url} sx={{display: 'block', color: 'inherit', ml: 4}}>
71 - {item.title}
72 - </Link>
73 - ))}
74 - </Box>
75 - )
76 -}
77 -
73 export default Header
src/components/hero.js
+1 -1
@@ -10,7 +10,7 @@ function Hero() {
10 <ThemeProvider colorMode="night" nightScheme="dark_dimmed">
11 <Box sx={{bg: 'canvas.default', py: 6}}>
12 <Container>
13 - <Heading as="h1" sx={{color: 'accent.fg', fontSize: 7, m: 0}}>
13 + <Heading as="h1" sx={{color: 'fg.onEmphasis', fontSize: 7, m: 0}}>
14 {title}
15 </Heading>
16 <Text as="p" sx={{m: 0, color: 'fg.onEmphasis', fontSize: 4}}>
src/components/nav-drawer.js
+20 -28
@@ -25,7 +25,7 @@ const useDrawerIsOpen = () => {
25 return [isOpen, {setOpen, setClose}]
26 }
27
28 -function NavDrawer({location, repositoryUrl}) {
28 +function NavDrawer() {
29 const siteMetadata = useSiteMetadata()
30 const [isOpen, {setOpen, setClose}] = useDrawerIsOpen()
31
@@ -62,39 +62,31 @@ function NavDrawer({location, repositoryUrl}) {
62 </DarkButton>
63 </Box>
64 </Box>
65 - {navItems.length > 0 ? (
66 - <Box display="flex" flexDirection="column">
67 - <NavItems location={location} items={navItems} repositoryUrl={repositoryUrl} />
65 + <Box display="flex" flexDirection="column">
66 + <NavItems items={navItems} />
67 + </Box>
68 + </Box>
69 + <Box display="flex" flexDirection="column" flex="1 0 auto" color="gray.1" bg="gray.9">
70 + {headerNavItems.map((item, index) => (
71 + <Box
72 + borderStyle="solid"
73 + key={item.title}
74 + borderWidth={0}
75 + borderRadius={0}
76 + borderTopWidth={index !== 0 ? 1 : 0}
77 + borderColor="gray.7"
78 + p={4}
79 + >
80 + <Link key={index} href={item.url} sx={{color: 'inherit', display: 'block'}}>
81 + {item.title}
82 + </Link>
83 </Box>
69 - ) : null}
84 + ))}
85 </Box>
71 - {headerNavItems.length > 0 ? (
72 - <Box display="flex" flexDirection="column" flex="1 0 auto" color="gray.1" bg="gray.9">
73 - <HeaderNavItems items={headerNavItems} />
74 - </Box>
75 - ) : null}
86 </Box>
87 </Drawer>
88 </>
89 )
90 }
91
82 -function HeaderNavItems({items}) {
83 - return items.map((item, index) => (
84 - <Box
85 - borderStyle="solid"
86 - key={item.title}
87 - borderWidth={0}
88 - borderRadius={0}
89 - borderTopWidth={index !== 0 ? 1 : 0}
90 - borderColor="gray.7"
91 - p={4}
92 - >
93 - <Link key={index} href={item.url} sx={{color: 'inherit', display: 'block'}}>
94 - {item.title}
95 - </Link>
96 - </Box>
97 - ))
98 -}
99 -
92 export default NavDrawer
src/components/nav-items.js
+5 -10
@@ -4,6 +4,7 @@ import {Box, StyledOcticon, Link, themeGet} from '@primer/react'
4 import {LinkExternalIcon} from '@primer/octicons-react'
5 import styled from 'styled-components'
6 import NavHierarchy from '../util/nav-hierarchy'
7 +import {useLocation, usePageContext} from '../layout'
8
9 const getActiveClass = props => {
10 const location = NavHierarchy.getLocation(props.location.pathname)
@@ -149,22 +150,16 @@ function thirdLevelItems(items) {
150 )
151 }
152
152 -function NavItems({location, repositoryUrl}) {
153 +function NavItems() {
154 + const location = useLocation()
155 + const {repositoryUrl} = usePageContext()
156 const path = NavHierarchy.getLocation(location.pathname)
157 const items = NavHierarchy.getHierarchy(null, {path, hideVariants: true})
158
159 return (
160 <>
161 {topLevelItems(items, path)}
159 - <Box
160 - borderStyle="solid"
161 - borderColor="border.default"
162 - borderWidth={0}
163 - borderTopWidth={1}
164 - borderRadius={0}
165 - py={5}
166 - px={4}
167 - >
162 + <Box borderStyle="solid" borderColor="border.default" borderWidth={0} borderTopWidth={1} py={3} px={4}>
163 <Link href={repositoryUrl} sx={{color: 'inherit'}}>
164 <Box display="flex" justifyContent="space-between" alignItems="center" color="gray.5">
165 GitHub
src/components/page-footer.js
+2 -2
@@ -1,5 +1,5 @@
1 import React from 'react'
2 -import {Box, StyledOcticon, Link} from '@primer/react'
2 +import {Box, Link, Octicon} from '@primer/react'
3 import {PencilIcon} from '@primer/octicons-react'
4 import Contributors from './contributors'
5
@@ -18,7 +18,7 @@ function PageFooter({editUrl, contributors = {}}) {
18 <Box display="grid" gridGap={4}>
19 {editUrl != null ? (
20 <Link href={editUrl}>
21 - <StyledOcticon icon={PencilIcon} mr={2} />
21 + <Octicon icon={PencilIcon} mr={2} />
22 Edit this page on GitHub
23 </Link>
24 ) : null}
src/components/sidebar.js
+22 -24
@@ -1,35 +1,33 @@
1 import {Box} from '@primer/react'
2 import React from 'react'
3 -import {HEADER_HEIGHT} from './header'
3 import NavItems from './nav-items'
4 import navItems from '../nav.yml'
5 +import {HEADER_HEIGHT} from '../constants'
6
7 -function Sidebar({location, repositoryUrl}) {
8 - return (
7 +const Sidebar = () => (
8 + <Box
9 + position="sticky"
10 + top={HEADER_HEIGHT}
11 + height={`calc(100vh - ${HEADER_HEIGHT}px)`}
12 + minWidth={260}
13 + color="gray.8"
14 + bg="gray.0"
15 + role="navigation"
16 + >
17 <Box
10 - position="sticky"
11 - top={HEADER_HEIGHT}
12 - height={`calc(100vh - ${HEADER_HEIGHT}px)`}
13 - minWidth={260}
14 - color="gray.8"
15 - bg="gray.0"
16 - role="navigation"
18 + borderStyle="solid"
19 + borderColor="border.default"
20 + borderWidth={0}
21 + borderRightWidth={1}
22 + borderRadius={0}
23 + height="100%"
24 + style={{overflow: 'auto'}}
25 >
18 - <Box
19 - borderStyle="solid"
20 - borderColor="border.default"
21 - borderWidth={0}
22 - borderRightWidth={1}
23 - borderRadius={0}
24 - height="100%"
25 - style={{overflow: 'auto'}}
26 - >
27 - <Box display="flex" flexDirection="column" role="list">
28 - <NavItems location={location} items={navItems} repositoryUrl={repositoryUrl} />
29 - </Box>
26 + <Box display="flex" flexDirection="column" role="list">
27 + <NavItems items={navItems} />
28 </Box>
29 </Box>
32 - )
33 -}
30 + </Box>
31 +)
32
33 export default Sidebar
src/components/table-of-contents.js
+70 -23
@@ -1,27 +1,74 @@
1 import React from 'react'
2 -import {Box, Link} from '@primer/react'
2 +import {Box, Link, Text, Octicon, Details} from '@primer/react'
3 +import {ChevronDownIcon, ChevronRightIcon} from '@primer/octicons-react'
4 +import {usePageContext} from '../layout'
5 +import {HEADER_HEIGHT} from '../constants'
6
4 -function TableOfContents({items, depth = 0, labelId}) {
5 - return (
6 - <Box
7 - key={items}
8 - as="ul"
9 - role="list"
10 - m={0}
11 - p={0}
12 - css={{listStyle: 'none', lineHeight: '1.4em'}}
13 - aria-labelledby={labelId}
14 - >
15 - {items.map(item => (
16 - <Box as="li" role="listitem" key={item.url} pl={depth > 0 ? 3 : 0}>
17 - <Link key={item.title} href={item.url} sx={{display: 'inline-block', py: 1, color: 'gray.6'}}>
18 - {item.title}
19 - </Link>
20 - {item.items ? <TableOfContents items={item.items} depth={depth + 1} /> : null}
21 - </Box>
22 - ))}
23 - </Box>
24 - )
7 +const TableOfContents = ({items, depth = 0, labelId}) => (
8 + <Box
9 + key={items}
10 + as="ul"
11 + role="list"
12 + m={0}
13 + p={0}
14 + css={{listStyle: 'none', lineHeight: '1.4em'}}
15 + aria-labelledby={labelId}
16 + >
17 + {items.map(item => (
18 + <Box as="li" role="listitem" key={item.url} pl={depth > 0 ? 3 : 0}>
19 + <Link key={item.title} href={item.url} sx={{display: 'inline-block', py: 1, color: 'gray.6'}}>
20 + {item.title}
21 + </Link>
22 + {item.items ? <TableOfContents items={item.items} depth={depth + 1} /> : null}
23 + </Box>
24 + ))}
25 + </Box>
26 +)
27 +
28 +const withTableOfContent = Component => {
29 + const TOC = props => {
30 + const {tableOfContents} = usePageContext()
31 + if (!tableOfContents) {
32 + return null
33 + }
34 + return <Component {...props} items={tableOfContents} />
35 + }
36 + return TOC
37 }
38
27 -export default TableOfContents
39 +export const Mobile = withTableOfContent(({items}) => (
40 + <Box display={['block', null, 'none']} mb={3}>
41 + <Details>
42 + {({open}) => (
43 + <>
44 + <Text as="summary" fontWeight="bold">
45 + <Octicon icon={open ? ChevronDownIcon : ChevronRightIcon} mr={2} />
46 + Table of contents
47 + </Text>
48 + <Box pt={1}>
49 + <TableOfContents items={items} />
50 + </Box>
51 + </>
52 + )}
53 + </Details>
54 + </Box>
55 +))
56 +
57 +export const Desktop = withTableOfContent(({items}) => (
58 + <Box
59 + display={['none', null, 'block']}
60 + css={{gridArea: 'table-of-contents', overflow: 'auto'}}
61 + position="sticky"
62 + top={HEADER_HEIGHT + 24}
63 + mt="6px"
64 + maxHeight={`calc(100vh - ${HEADER_HEIGHT}px - 24px)`}
65 + pr={1}
66 + pl={1}
67 + pb={1}
68 + >
69 + <Text display="inline-block" fontWeight="bold" mb={1} id="table-of-content-label">
70 + Table of contents
71 + </Text>
72 + <TableOfContents items={items} labelId="table-of-content-label" />
73 + </Box>
74 +))
src/components/variant-select.js
+55 -43
@@ -1,45 +1,49 @@
1 import React from 'react'
2 -import {ActionList, ActionMenu} from '@primer/react'
2 +import {ActionList, ActionMenu, Box} from '@primer/react'
3 import NavHierarchy from '../util/nav-hierarchy'
4 +import {useLocation} from '../layout'
5
5 -// VariantSelect: allows a variant to be set up within a document hierarchy
6 -//
7 -// For example, given two paths `/docs/v1.0/foo` and `/docs/v2.0/foo`, the
8 -// second folder acts as a variant. If you use <VariantSelect root="/docs">
9 -// then you'll get a selection for the different variants (v1.0, v2.0).
6 +const VariantItem = ({match, active}) => {
7 + const {variant, page} = match
8
11 -const VariantSelect = ({variantPages, path}) => {
12 - const [open, setOpen] = React.useState(false)
13 - const anchorClickHandler = React.useCallback((event, url) => {
14 - event.preventDefault()
15 - window.location.href = `${url}?v=true`
16 - }, [])
9 + const handleClick = React.useCallback(
10 + event => {
11 + event.preventDefault()
12 + window.location.href = `${page.url}?v=true`
13 + },
14 + [page.url],
15 + )
16 +
17 + const handleKey = React.useCallback(
18 + event => {
19 + if (event.key === 'Enter') {
20 + window.location.href = `${page.url}?v=true`
21 + }
22 + },
23 + [page.url],
24 + )
25 +
26 + return (
27 + <ActionList.Item onKeyDown={handleKey} onClick={handleClick} id={variant.shortName} active={active}>
28 + {variant.title}
29 + </ActionList.Item>
30 + )
31 +}
32
18 - const onItemEnterKey = React.useCallback((event, url) => {
19 - if (event.key === 'Enter') {
20 - window.location.href = `${url}?v=true`
21 - }
22 - }, [])
33 +const VariantMenu = ({variants, path}) => {
34 + const [open, setOpen] = React.useState(false)
35
24 - let selectedItem = variantPages[0]
25 - const items = variantPages.map((match, index) => {
26 - let active = false
27 - if (match.page.url === path) {
28 - selectedItem = match
29 - active = true
30 - }
31 - return (
32 - <ActionList.Item
33 - onKeyDown={e => onItemEnterKey(e, match.page.url)}
34 - onClick={e => anchorClickHandler(e, match.page.url)}
35 - id={match.variant.shortName}
36 - key={index}
37 - active={active}
38 - >
39 - {match.variant.title}
40 - </ActionList.Item>
41 - )
42 - })
36 + const {selected, items} = variants.reduce(
37 + (acc, match, key) => {
38 + const active = match.page.url === path
39 + if (active) {
40 + acc.selected = match
41 + }
42 + acc.items.push({match, key, active})
43 + return acc
44 + },
45 + {selected: variants[0], items: []},
46 + )
47
48 return (
49 <>
@@ -49,11 +53,13 @@ const VariantSelect = ({variantPages, path}) => {
53 in a previous accessibility audit which did not trigger the lint warning. */
54 /* eslint-disable-next-line jsx-a11y/no-autofocus */}
55 <ActionMenu.Button autoFocus aria-describedby="label-versions-list-item">
52 - {selectedItem.variant.title}
56 + {selected.variant.title}
57 </ActionMenu.Button>
58 <ActionMenu.Overlay width="medium" onEscape={() => setOpen(false)}>
59 <ActionList id="versions-list-item" aria-labelledby="label-versions-list-item">
56 - {items}
60 + {items.map(item => (
61 + <VariantItem key={item.key} {...item} />
62 + ))}
63 </ActionList>
64 </ActionMenu.Overlay>
65 </ActionMenu>
@@ -61,16 +67,22 @@ const VariantSelect = ({variantPages, path}) => {
67 )
68 }
69
64 -const VariantSelectLocation = ({root, location}) => {
70 +const VariantSelect = () => {
71 + const location = useLocation()
72 + const root = NavHierarchy.getVariantRoot(location.pathname)
73 const path = NavHierarchy.getPath(location.pathname)
74 const vp = NavHierarchy.getVariantAndPage(root, path)
67 - const variantPages = vp ? NavHierarchy.getVariantsForPage(root, vp.page) : []
75 + const variants = vp ? NavHierarchy.getVariantsForPage(root, vp.page) : []
76
69 - if (!variantPages.length) {
77 + if (!variants.length) {
78 return null
79 }
80
73 - return <VariantSelect variantPages={variantPages} path={path} />
81 + return (
82 + <Box css={{'margin-top': '25px'}}>
83 + <VariantMenu variantPages={variants} path={path} />
84 + </Box>
85 + )
86 }
87
76 -export default VariantSelectLocation
88 +export default VariantSelect
src/constants.js new
+3
@@ -0,0 +1,3 @@
1 +export const HEADER_HEIGHT = 66
2 +
3 +export const SKIP_NAV = 'skip-nav'
src/favicon.png renamed
src/hooks/use-search.js
+2 -3
@@ -4,9 +4,8 @@ import {navigate, graphql, useStaticQuery} from 'gatsby'
4 import {useIsMobile} from './use-breakpoint'
5
6 const useSearchData = () => {
7 - // TODO: this static query runs for all pages changes in dev mode.
8 - // When not testing search explicitly it should be replaced with
9 - // some static data.
7 + // TODO: this static query runs for all pages changes in dev mode. When not
8 + // testing search explicitly it should be replaced with some static data.
9 const rawData = useStaticQuery(graphql`
10 {
11 allMdx {
src/hooks/use-slugger.js deleted
-7
@@ -1,7 +0,0 @@
1 -import React from 'react'
2 -import Slugger from 'github-slugger'
3 -
4 -const SluggerContext = React.createContext(null)
5 -
6 -export const Provider = props => <SluggerContext.Provider value={new Slugger()} {...props} />
7 -export const useSlugger = () => React.useContext(SluggerContext)
src/layout.js new
+45
@@ -0,0 +1,45 @@
1 +import React from 'react'
2 +import {Box} from '@primer/react'
3 +import Slugger from 'github-slugger'
4 +import Head from './components/head'
5 +import Header from './components/header'
6 +import Sidebar from './components/sidebar'
7 +import {SKIP_NAV} from './constants'
8 +
9 +const SluggerContext = React.createContext(null)
10 +const PageContext = React.createContext(null)
11 +const LocationContext = React.createContext(null)
12 +
13 +export const useSlugger = () => React.useContext(SluggerContext)
14 +export const usePageContext = () => React.useContext(PageContext)
15 +export const useLocation = () => React.useContext(LocationContext)
16 +export const useFrontmatter = () => {
17 + const pageContext = usePageContext()
18 + return pageContext.frontmatter || {}
19 +}
20 +
21 +const withLayout = Component => {
22 + const LayoutProvider = props => (
23 + <SluggerContext.Provider value={new Slugger()}>
24 + <PageContext.Provider value={props.pageContext}>
25 + <LocationContext.Provider value={props.location}>
26 + <Box display="flex" flexDirection="column" minHeight="100vh">
27 + <Head />
28 + <Header />
29 + <Box display="flex" flex="1 1 auto" flexDirection="row" css={{zIndex: 0}} role="main">
30 + <Box display={['none', null, null, 'block']}>
31 + <Sidebar />
32 + </Box>
33 + <Box id={SKIP_NAV}>
34 + <Component {...props} />
35 + </Box>
36 + </Box>
37 + </Box>
38 + </LocationContext.Provider>
39 + </PageContext.Provider>
40 + </SluggerContext.Provider>
41 + )
42 + return LayoutProvider
43 +}
44 +
45 +export default withLayout
src/layout/default.js
+36 -107
@@ -1,119 +1,48 @@
1 import React from 'react'
2 -import {Box, Heading, StyledOcticon, Text, Details} from '@primer/react'
3 -import {ChevronDownIcon, ChevronRightIcon} from '@primer/octicons-react'
4 -import Head from '../components/head'
5 -import Header, {HEADER_HEIGHT} from '../components/header'
2 +import {Box, Heading} from '@primer/react'
3 import PageFooter from '../components/page-footer'
7 -import Sidebar from '../components/sidebar'
8 -import TableOfContents from '../components/table-of-contents'
4 +import * as TableOfContents from '../components/table-of-contents'
5 import VariantSelect from '../components/variant-select'
10 -import * as Slugger from '../hooks/use-slugger'
11 -import NavHierarchy from '../util/nav-hierarchy'
6 +import withLayout from '../layout'
7
13 -function Layout({children, pageContext, location}) {
14 - const {repositoryUrl, editUrl, contributors, frontmatter, tableOfContents} = pageContext
8 +const Layout = ({children, pageContext: {frontmatter}}) => {
9 const {title, description} = frontmatter
10
17 - const variantRoot = NavHierarchy.getVariantRoot(location.pathname)
18 -
11 return (
20 - <Slugger.Provider>
21 - <Box display="flex" flexDirection="column" minHeight="100vh">
22 - <Head title={title} description={description} />
23 - <Header repositoryUrl={repositoryUrl} location={location} />
24 - <Box display="flex" flex="1 1 auto" flexDirection="row" css={{zIndex: 0}} role="main">
25 - <Box display={['none', null, null, 'block']}>
26 - <Sidebar repositoryUrl={repositoryUrl} location={location} />
27 - </Box>
28 - <Box
29 - display="grid"
30 - id="skip-nav"
31 - maxWidth="100%"
32 - gridTemplateColumns={['100%', null, 'minmax(0, 65ch) 220px']}
33 - gridTemplateAreas={['"heading" "content"', null, '"heading table-of-contents" "content table-of-contents"']}
34 - gridColumnGap={[null, null, 6, 7]}
35 - gridRowGap={3}
36 - mx="auto"
37 - p={[5, 6, null, 7]}
38 - css={{alignItems: 'start', alignSelf: 'start'}}
39 - role="region"
40 - >
41 - <Box css={{gridArea: 'heading'}}>
42 - <Box
43 - borderStyle="solid"
44 - borderColor="border.default"
45 - borderWidth={0}
46 - borderBottomWidth={1}
47 - borderRadius={0}
48 - pb={2}
49 - >
50 - <Box>
51 - <Box>
52 - <Heading as="h1">{title}</Heading>
53 - {description}
54 - </Box>
55 - </Box>
56 - </Box>
57 - {variantRoot != null ? (
58 - <Box css={{'margin-top': '25px'}}>
59 - <VariantSelect
60 - overlay={true}
61 - direction="se"
62 - menuWidth="min(30ch, 500px)"
63 - root={variantRoot}
64 - location={location}
65 - />
66 - </Box>
67 - ) : null}
68 - </Box>
69 - {tableOfContents ? (
70 - <Box
71 - display={['none', null, 'block']}
72 - css={{gridArea: 'table-of-contents', overflow: 'auto'}}
73 - position="sticky"
74 - top={HEADER_HEIGHT + 24}
75 - mt="6px"
76 - maxHeight={`calc(100vh - ${HEADER_HEIGHT}px - 24px)`}
77 - pr={1}
78 - pl={1}
79 - pb={1}
80 - >
81 - <Text display="inline-block" fontWeight="bold" mb={1} id="table-of-content-label">
82 - Table of contents
83 - </Text>
84 - <TableOfContents items={tableOfContents} labelId="table-of-content-label" />
85 - </Box>
86 - ) : null}
87 - <Box css={{gridArea: 'content'}}>
88 - {tableOfContents ? (
89 - <Box display={['block', null, 'none']} mb={3}>
90 - <Details>
91 - {({open}) => (
92 - <>
93 - <Text as="summary" fontWeight="bold">
94 - {open ? (
95 - <StyledOcticon icon={ChevronDownIcon} mr={2} />
96 - ) : (
97 - <StyledOcticon icon={ChevronRightIcon} mr={2} />
98 - )}
99 - Table of contents
100 - </Text>
101 - <Box pt={1}>
102 - <TableOfContents items={tableOfContents} />
103 - </Box>
104 - </>
105 - )}
106 - </Details>
107 - </Box>
108 - ) : null}
109 - {children}
110 - <PageFooter editUrl={editUrl} contributors={contributors} />
111 - </Box>
112 - </Box>
12 + <Box
13 + display="grid"
14 + maxWidth="100%"
15 + gridTemplateColumns={['100%', null, 'minmax(0, 65ch) 220px']}
16 + gridTemplateAreas={['"heading" "content"', null, '"heading table-of-contents" "content table-of-contents"']}
17 + gridColumnGap={[null, null, 6, 7]}
18 + gridRowGap={3}
19 + mx="auto"
20 + p={[5, 6, null, 7]}
21 + css={{alignItems: 'start', alignSelf: 'start'}}
22 + role="region"
23 + >
24 + <Box css={{gridArea: 'heading'}}>
25 + <Box
26 + borderStyle="solid"
27 + borderColor="border.default"
28 + borderWidth={0}
29 + borderBottomWidth={1}
30 + borderRadius={0}
31 + pb={2}
32 + >
33 + <Heading as="h1">{title}</Heading>
34 + {description}
35 </Box>
36 + <VariantSelect />
37 + </Box>
38 + <TableOfContents.Desktop />
39 + <Box css={{gridArea: 'content'}}>
40 + <TableOfContents.Mobile />
41 + {children}
42 + <PageFooter />
43 </Box>
115 - </Slugger.Provider>
44 + </Box>
45 )
46 }
47
119 -export default Layout
48 +export default withLayout(Layout)
src/layout/hero.js
+7 -21
@@ -1,28 +1,14 @@
1 import React from 'react'
2 import {Box} from '@primer/react'
3 import Container from '../components/container'
4 -import Head from '../components/head'
5 -import Header from '../components/header'
4 import Hero from '../components/hero'
7 -import Sidebar from '../components/sidebar'
8 -import * as Slugger from '../hooks/use-slugger'
5 +import withLayout from '../layout'
6
10 -const HeroLayout = ({children, location, pageContext: {repositoryUrl}}) => (
11 - <Slugger.Provider>
12 - <Box display="flex" flexDirection="column" minHeight="100vh">
13 - <Head />
14 - <Header location={location} repositoryUrl={repositoryUrl} />
15 - <Box display="flex" flex="1 1 auto" flexDirection="row" role="main">
16 - <Box display={['none', null, null, 'block']}>
17 - <Sidebar repositoryUrl={repositoryUrl} location={location} />
18 - </Box>
19 - <Box width="100%">
20 - <Hero />
21 - <Container>{children}</Container>
22 - </Box>
23 - </Box>
24 - </Box>
25 - </Slugger.Provider>
7 +const HeroLayout = ({children}) => (
8 + <Box width="100%">
9 + <Hero />
10 + <Container>{children}</Container>
11 + </Box>
12 )
13
28 -export default HeroLayout
14 +export default withLayout(HeroLayout)
src/mdx/index.js
+3 -3
@@ -6,8 +6,8 @@ import {LinkIcon} from '@primer/octicons-react'
6 import textContent from 'react-addons-text-content'
7 import Code from './code'
8 import NavHierarchy from './nav-hierarchy'
9 -import {HEADER_HEIGHT} from '../components/header'
10 -import {useSlugger} from '../hooks/use-slugger'
9 +import {useSlugger} from '../layout'
10 +import {HEADER_HEIGHT, SKIP_NAV} from '../constants'
11
12 const required = (prop, name) => {
13 if (!prop) {
@@ -25,7 +25,7 @@ export {Code, NavHierarchy as Index}
25 export const Pre = ({children}) => children
26
27 const SkipLinkBase = props => (
28 - <PrimerLink {...props} href="#skip-nav" sx={{backgroundColor: 'blue.6', color: 'white', p: 3, fontSize: 1}}>
28 + <PrimerLink {...props} href={`#${SKIP_NAV}`} sx={{backgroundColor: 'blue.6', color: 'white', p: 3, fontSize: 1}}>
29 Skip to content
30 </PrimerLink>
31 )
src/mdx/nav-hierarchy.js
+1 -1
@@ -1,8 +1,8 @@
1 import React from 'react'
2 -import {useLocation} from '@reach/router' // eslint-disable-line import/no-unresolved
2 import {Box, Link} from '@primer/react'
3 import {Link as GatsbyLink} from 'gatsby'
4 import NavHierarchy from '../util/nav-hierarchy'
5 +import {useLocation} from '../layout'
6
7 function showHierarchy(items, props, depth = 1) {
8 let hierarchy
src/pages/404.js
+2 -1
@@ -1,7 +1,8 @@
1 import React from 'react'
2 +import withLayout from '../layout'
3
4 const Page404 = () => {
5 return <h1>404 page</h1>
6 }
7
7 -export default Page404
8 +export default withLayout(Page404)
src/util/nav-hierarchy.js
+1 -1
@@ -191,7 +191,7 @@ const NavHierarchy = {
191 },
192
193 getVariantAndPage(root, path) {
194 - if (!path.startsWith(`${root}/`)) {
194 + if (!root || !path.startsWith(`${root}/`)) {
195 return null
196 }
197