mirror of
https://github.com/c8r/x0.git
synced 2024-10-26 15:15:04 +03:00
Add basic layout component
This commit is contained in:
parent
a6e3e8f10e
commit
4556918cbd
10
README.md
10
README.md
@ -368,9 +368,13 @@ See the [example](https://github.com/c8r/x0/tree/master/examples/webpack-config)
|
||||
|
||||
**REMOVE BEFORE MERGING**
|
||||
|
||||
- [ ] pass RouterState props to view
|
||||
- [ ] props.Component/children in custom apps
|
||||
- [ ] changelog/docs
|
||||
|
||||
- [x] SidebarLayout
|
||||
- [x] test getInitialProps
|
||||
- [x] double check dynamic routing
|
||||
- [x] pass RouterState props to view
|
||||
- [x] props.children in custom app
|
||||
- [x] require.context `_app`
|
||||
- [x] peer deps
|
||||
- [x] props.ignore
|
||||
@ -383,7 +387,9 @@ See the [example](https://github.com/c8r/x0/tree/master/examples/webpack-config)
|
||||
- [x] minimatch
|
||||
- [x] move client modules to src
|
||||
- [x] adjust resolve
|
||||
- [x] props.Component in custom app?
|
||||
- [ ] ~~Head component with react helmet~~ doesn't work
|
||||
- [ ] ~~scope loader shim for jsx front matter~~
|
||||
|
||||
### Breaking
|
||||
|
||||
|
13
docs/_app.js
13
docs/_app.js
@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import * as scope from 'rebass'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ScopeProvider } from '../components'
|
||||
import { ScopeProvider, SidebarLayout } from '../components'
|
||||
import {
|
||||
Flex,
|
||||
Box,
|
||||
@ -20,9 +20,20 @@ export default class App extends React.Component {
|
||||
route,
|
||||
children,
|
||||
// alternative to props.children
|
||||
Component,
|
||||
render,
|
||||
} = this.props
|
||||
|
||||
// built-in layout test
|
||||
if (false) {
|
||||
return (
|
||||
<ScopeProvider scope={scope}>
|
||||
<SidebarLayout {...this.props} />
|
||||
</ScopeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<ScopeProvider scope={scope}>
|
||||
{false ? (
|
||||
|
@ -7,7 +7,7 @@
|
||||
"x0": "cli.js"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "./cli.js docs -op 8888",
|
||||
"start": "./cli.js docs -p 8888",
|
||||
"build": "./cli.js build docs",
|
||||
"test": "nyc ava -T 20s",
|
||||
"cover": "nyc report --reporter=html --reporter=lcov"
|
||||
@ -56,6 +56,7 @@
|
||||
"mini-html-webpack-plugin": "^0.2.3",
|
||||
"minimatch": "^3.0.4",
|
||||
"pkg-conf": "^2.1.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.4.1",
|
||||
"react-dev-utils": "^5.0.1",
|
||||
"react-dom": "^16.4.1",
|
||||
|
319
src/SidebarLayout.js
Normal file
319
src/SidebarLayout.js
Normal file
@ -0,0 +1,319 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
Link as RouterLink,
|
||||
NavLink as RouterNavLink
|
||||
} from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
import {
|
||||
Provider as RebassProvider,
|
||||
Flex,
|
||||
Box,
|
||||
Fixed,
|
||||
Container,
|
||||
Text,
|
||||
Close,
|
||||
Toolbar,
|
||||
Divider,
|
||||
Heading,
|
||||
NavLink,
|
||||
BlockLink,
|
||||
Button,
|
||||
ButtonTransparent,
|
||||
} from 'rebass'
|
||||
import { borderColor, themeGet } from 'styled-system'
|
||||
|
||||
const breakpoint = `@media screen and (min-width: 48em)`
|
||||
|
||||
export const Root = styled(Flex)([], {
|
||||
minHeight: '100vh'
|
||||
})
|
||||
|
||||
export const Sidebar = styled('div')([], {
|
||||
width: '256px',
|
||||
height: '100vh',
|
||||
flex: 'none',
|
||||
overflowY: 'auto',
|
||||
transition: 'transform .2s ease-out',
|
||||
backgroundColor: '#fff',
|
||||
borderRight: '1px solid',
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
[breakpoint]: {
|
||||
// transition: 'none'
|
||||
}
|
||||
}, props => ({
|
||||
transform: props.open ? 'translateX(0)' : 'translateX(-100%)',
|
||||
[breakpoint]: {
|
||||
transform: 'none'
|
||||
}
|
||||
}), borderColor)
|
||||
Sidebar.defaultProps = {
|
||||
borderColor: 'gray'
|
||||
}
|
||||
|
||||
export const Overlay = styled('div')([], {
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
})
|
||||
|
||||
export const MobileOnly = styled.div([], {
|
||||
[breakpoint]: {
|
||||
display: 'none'
|
||||
},
|
||||
})
|
||||
|
||||
export const MenuIcon = ({ size = 24, ...props }) =>
|
||||
<svg
|
||||
{...props}
|
||||
viewBox='0 0 24 24'
|
||||
width={size}
|
||||
height={size}
|
||||
fill='currentcolor'
|
||||
>
|
||||
<path d='M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z' />
|
||||
</svg>
|
||||
|
||||
export const FocusButton = styled(Button)([], {
|
||||
width: '2em',
|
||||
height: '2em',
|
||||
})
|
||||
FocusButton.defaultProps = {
|
||||
fontSize: 0,
|
||||
px: '0.25em',
|
||||
py: '0.25em',
|
||||
m: 2,
|
||||
color: 'black',
|
||||
bg: 'gray'
|
||||
}
|
||||
|
||||
export const Main = props =>
|
||||
<Box
|
||||
{...props}
|
||||
is='main'
|
||||
flex='1 1 auto'
|
||||
w={1}
|
||||
pl={[ null, null, 256 ]}
|
||||
/>
|
||||
|
||||
export const MaxWidth = props =>
|
||||
<Container
|
||||
{...props}
|
||||
maxWidth={768}
|
||||
px={4}
|
||||
pt={4}
|
||||
pb={6}
|
||||
/>
|
||||
|
||||
export const UL = styled('ul')([], {
|
||||
listStyle: 'none',
|
||||
margin: 0,
|
||||
paddingLeft: 0,
|
||||
paddingBottom: '48px',
|
||||
})
|
||||
|
||||
export const LI = styled('li')([], {
|
||||
})
|
||||
|
||||
const depthPad = ({ to = '' }) =>
|
||||
(1 + to.split('/')
|
||||
.filter(s => s.length)
|
||||
.slice(1).length) * 16
|
||||
|
||||
const Link = styled(props => (
|
||||
<NavLink
|
||||
{...props}
|
||||
is={RouterNavLink}
|
||||
w={1}
|
||||
pl={(depthPad(props) - 4) + 'px'}
|
||||
/>
|
||||
))([], props => ({
|
||||
borderLeft: '4px solid',
|
||||
borderColor: 'transparent',
|
||||
'&.active, &:focus': {
|
||||
color: themeGet('colors.blue', '#07c')(props),
|
||||
outline: 'none',
|
||||
},
|
||||
'&:focus': {
|
||||
borderColor: 'inherit',
|
||||
}
|
||||
}))
|
||||
|
||||
Link.defaultProps = {
|
||||
to: ''
|
||||
}
|
||||
|
||||
const unhyphenate = str => str.replace(/(\w)(-)(\w)/g, '$1 $3')
|
||||
const upperFirst = str => str.charAt(0).toUpperCase() + str.slice(1)
|
||||
const format = str => upperFirst(unhyphenate(str))
|
||||
|
||||
const NavBar = ({
|
||||
title,
|
||||
focus,
|
||||
update,
|
||||
}) =>
|
||||
<Toolbar
|
||||
color='inherit'
|
||||
bg='transparent'>
|
||||
<Heading
|
||||
px={2}
|
||||
fontSize={1}>
|
||||
{title}
|
||||
</Heading>
|
||||
<Box mx='auto' />
|
||||
{DEV && (
|
||||
<FocusButton
|
||||
title='focus'
|
||||
onClick={e => update(toggle('focus'))}>
|
||||
F
|
||||
</FocusButton>
|
||||
)}
|
||||
</Toolbar>
|
||||
|
||||
export const Nav = ({
|
||||
routes = [],
|
||||
...props
|
||||
}) =>
|
||||
<React.Fragment>
|
||||
<NavBar {...props} />
|
||||
<Divider my={0} />
|
||||
<UL>
|
||||
{/* rename to route.type='section' */}
|
||||
{routes.map(route => route.exact ? (
|
||||
<LI key={route.section + '-exact'}>
|
||||
<Link to={route.path} exact>
|
||||
{format(route.name)} (index)
|
||||
</Link>
|
||||
</LI>
|
||||
) : (
|
||||
<LI key={route.key}>
|
||||
{/^https?:\/\//.test(route.path) ? (
|
||||
<NavLink pl={3} href={route.path}>
|
||||
{route.name}
|
||||
</NavLink>
|
||||
) : (
|
||||
<Link to={route.path} exact>
|
||||
{format(route.name)}
|
||||
</Link>
|
||||
)}
|
||||
</LI>
|
||||
))}
|
||||
</UL>
|
||||
</React.Fragment>
|
||||
|
||||
export const Pagination = ({ previous, next }) =>
|
||||
<Flex py={4} flexWrap='wrap'>
|
||||
{previous && (
|
||||
<BlockLink
|
||||
py={2}
|
||||
is={RouterLink}
|
||||
to={previous.path}>
|
||||
<Text mb={1}>Previous:</Text>
|
||||
<Text
|
||||
fontSize={3}
|
||||
fontWeight='bold'>
|
||||
{format(previous.name)}
|
||||
</Text>
|
||||
</BlockLink>
|
||||
)}
|
||||
<Box mx='auto' />
|
||||
{next && (
|
||||
<BlockLink
|
||||
py={2}
|
||||
is={RouterLink}
|
||||
to={next.path}>
|
||||
<Text mb={1}>Next:</Text>
|
||||
<Text
|
||||
fontSize={3}
|
||||
fontWeight='bold'>
|
||||
{format(next.name)}
|
||||
</Text>
|
||||
</BlockLink>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
// move to app
|
||||
const toggle = key => state => ({ [key]: !state[key] })
|
||||
const close = state => ({ menu: false })
|
||||
|
||||
export default class Layout extends React.Component {
|
||||
static propTypes = {
|
||||
// content: PropTypes.node.isRequired,
|
||||
routes: PropTypes.array.isRequired
|
||||
}
|
||||
|
||||
state = {
|
||||
menu: false,
|
||||
update: fn => this.setState(fn)
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
routes = [],
|
||||
children,
|
||||
route,
|
||||
location,
|
||||
title = 'x0',
|
||||
// theme,
|
||||
// color,
|
||||
} = this.props
|
||||
const { menu, update } = this.state
|
||||
|
||||
const Wrapper = route && route.props && route.props.fullWidth
|
||||
? React.Fragment
|
||||
: MaxWidth
|
||||
|
||||
const index = routes.findIndex(r => r === route)
|
||||
const pagination = {
|
||||
previous: routes[index - 1],
|
||||
next: routes[index + 1]
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MobileOnly>
|
||||
<Toolbar px={0} color='inherit' bg='transparent'>
|
||||
<ButtonTransparent
|
||||
px={2}
|
||||
borderRadius={0}
|
||||
m={0}
|
||||
mr='auto'
|
||||
title='Toggle Menu'
|
||||
onClick={e => update(toggle('menu'))}>
|
||||
<MenuIcon />
|
||||
</ButtonTransparent>
|
||||
<Heading fontSize={1}>
|
||||
{title}
|
||||
</Heading>
|
||||
<Box width={48} ml='auto' />
|
||||
</Toolbar>
|
||||
<Divider my={0} />
|
||||
</MobileOnly>
|
||||
|
||||
<Root>
|
||||
{menu && <Overlay onClick={e => update(close)} />}
|
||||
<Sidebar
|
||||
open={menu}
|
||||
onClick={e => update(close)}>
|
||||
<Nav
|
||||
title={title}
|
||||
routes={routes}
|
||||
update={update}
|
||||
/>
|
||||
</Sidebar>
|
||||
<Main tabIndex={menu ? -1 : undefined}>
|
||||
<Wrapper>
|
||||
{children}
|
||||
<Pagination {...pagination} />
|
||||
</Wrapper>
|
||||
</Main>
|
||||
</Root>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
10
src/entry.js
10
src/entry.js
@ -40,9 +40,9 @@ const getComponents = req => req.keys()
|
||||
|
||||
const initialComponents = getComponents(req)
|
||||
|
||||
const DefaultApp = ({ render, routes }) => (
|
||||
const DefaultApp = ({ children, routes }) => (
|
||||
<Switch>
|
||||
{render()}
|
||||
{children}
|
||||
<Route render={props => (
|
||||
<FileList
|
||||
{...props}
|
||||
@ -98,7 +98,8 @@ export const getRoutes = async (components = initialComponents) => {
|
||||
}
|
||||
|
||||
const RouterState = withRouter(({ render, ...props }) => {
|
||||
const route = props.routes.find(r => r.path === props.location.pathname)
|
||||
const { pathname } = props.location
|
||||
const route = props.routes.find(r => r.path === pathname || r.href === pathname)
|
||||
return render({ ...props, route })
|
||||
})
|
||||
|
||||
@ -155,7 +156,8 @@ export default class Root extends React.Component {
|
||||
{...router}
|
||||
routes={routes}
|
||||
render={render}
|
||||
children={render()}
|
||||
Component={render}
|
||||
children={render(router)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -6,3 +6,6 @@ export { default as FileList } from './FileList'
|
||||
export { default as Catch } from './Catch'
|
||||
export { default as ScrollTop } from './ScrollTop'
|
||||
export { default as scope } from './scope'
|
||||
|
||||
// layouts
|
||||
export { default as SidebarLayout } from './SidebarLayout'
|
||||
|
Loading…
Reference in New Issue
Block a user