1
1
mirror of https://github.com/jxnblk/mdx-deck.git synced 2024-11-25 15:50:39 +03:00

Reorganize modules

This commit is contained in:
Brent Jackson 2019-03-02 15:17:14 -05:00
parent e97e7a1df7
commit a838705222
38 changed files with 1018 additions and 855 deletions

2
.gitignore vendored
View File

@ -1,5 +1,5 @@
dist dist
site public
coverage coverage
node_modules node_modules
package-lock.json package-lock.json

View File

@ -0,0 +1,34 @@
# notes
mdx-deck v2 prototype
todo:
- [x] Head
- [x] Image
- [x] Notes
- [x] Appear
- [ ] Code (may handle this with theming)
- [ ] normalize slide styles with v1
- [x] history api fallback
- [x] mdx components
- [x] themes
- [ ] layouts
- [ ] presenter mode
- [ ] timer/clock
- [ ] open new tab button
- [ ] look at google slides behavior
- [ ] overview mode
- [ ] highlighted state bug
- [ ] slide numbers
- [ ] localStorage
- [ ] keyboard shortcuts
- [ ] docs for customizing any component
- [x] swipeable
extras
- [ ] docs for syntax highlighting
- [ ] Print view
- [ ] PDF export?
- [ ] dots??

View File

@ -1,7 +1,7 @@
{ {
"name": "@mdx-deck/components", "name": "@mdx-deck/components",
"version": "2.0.0-0", "version": "2.0.0-0",
"main": "index.js", "main": "src/index.js",
"author": "Brent Jackson <jxnblk@gmail.com>", "author": "Brent Jackson <jxnblk@gmail.com>",
"license": "MIT" "license": "MIT"
} }

View File

@ -0,0 +1,26 @@
import React from 'react'
import Steps from './Steps'
export const Appear = props => {
const arr = React.Children.toArray(props.children)
return (
<Steps
length={arr.length}
render={({ step }) => {
const children = arr.map((child, i) =>
i < step
? child
: React.cloneElement(child, {
style: {
...child.props.style,
visibility: 'hidden',
},
})
)
return <>{children}</>
}}
/>
)
}
export default Appear

View File

@ -0,0 +1,68 @@
import React, { useContext } from 'react'
import { createPortal } from 'react-dom'
export const HeadContext = React.createContext({
tags: [],
push: () => {
console.warn('Missing HeadProvider')
},
})
export const HeadProvider = ({ tags = [], children }) => {
const push = elements => {
tags.push(...elements)
}
const context = { push }
return <HeadContext.Provider value={context}>{children}</HeadContext.Provider>
}
export class Head extends React.Component {
state = {
didMount: false,
}
rehydrate = () => {
const children = React.Children.toArray(this.props.children)
const nodes = [...document.head.querySelectorAll('[data-head]')]
nodes.forEach(node => {
node.remove()
})
children.forEach(child => {
if (child.type === 'title') {
const title = document.head.querySelector('title')
if (title) title.remove()
}
if (child.type === 'meta') {
const { name } = child.props
let meta
if (name) meta = document.head.querySelector(`meta[name="${name}"]`)
if (meta) meta.remove()
}
})
this.setState({ didMount: true })
}
componentDidMount() {
this.rehydrate()
}
render() {
const children = React.Children.toArray(this.props.children).map(child =>
React.cloneElement(child, {
'data-head': true,
})
)
if (!this.state.didMount) {
return (
<HeadContext.Consumer
children={({ push }) => {
push(children)
return false
}}
/>
)
}
return createPortal(children, document.head)
}
}
export default Head

View File

@ -0,0 +1,22 @@
import styled from 'styled-components'
export const Image = styled.div(
{
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
},
props => ({
backgroundSize: props.size,
width: props.width,
height: props.height,
backgroundImage: `url(${props.src})`,
})
)
Image.defaultProps = {
size: 'cover',
width: '100vw',
height: '100vh',
}
export default Image

View File

@ -0,0 +1,196 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Router, globalHistory, navigate, Link } from '@reach/router'
import { Swipeable } from 'react-swipeable'
import Provider from './Provider'
import Root from './Root'
import Slide from './Slide'
import { default as defaultTheme } from '@mdx-deck/themes'
const NORMAL = 'NORMAL'
const PRESENTER = 'PRESENTER'
const OVERVIEW = 'OVERVIEW'
const PRINT = 'PRINT'
const keys = {
right: 39,
left: 37,
space: 32,
p: 80,
o: 79,
}
const toggleMode = key => state => ({
mode: state.mode === key ? NORMAL : key,
})
const BaseWrapper = props => <>{props.children}</>
export class MDXDeck extends React.Component {
constructor(props) {
super(props)
this.state = {
slides: props.slides,
step: 0,
mode: NORMAL,
}
}
handleKeyDown = e => {
const { key, keyCode, metaKey, ctrlKey, altKey, shiftKey } = e
const { activeElement } = document
if (activeElement.tagName !== 'BODY' && activeElement.tagName !== 'DIV')
return
if (metaKey || ctrlKey) return
const alt = altKey && !shiftKey
const shift = shiftKey && !altKey
if (alt) {
switch (keyCode) {
case keys.p:
this.setState(toggleMode(PRESENTER))
break
case keys.o:
this.setState(toggleMode(OVERVIEW))
break
}
} else {
switch (keyCode) {
case keys.left:
e.preventDefault()
this.previous()
break
case keys.right:
case keys.space:
e.preventDefault()
this.next()
break
}
}
}
getIndex = () => {
const { pathname } = globalHistory.location
return Number(pathname.split('/')[1] || 0)
}
getMeta = i => {
const { slides } = this.state
const { meta = {} } = slides[i] || {}
return meta
}
goto = i => {
const current = this.getIndex()
const reverse = i < current
navigate('/' + i)
const meta = this.getMeta(i)
this.setState({
step: reverse ? meta.steps || 0 : 0,
})
}
previous = () => {
const { slides, step } = this.state
const index = this.getIndex()
const meta = this.getMeta(index)
if (meta.steps && step > 0) {
this.setState(state => ({
step: state.step - 1,
}))
} else {
const previous = index - 1
if (previous < 0) return
this.goto(previous)
}
}
next = () => {
const { slides, step } = this.state
const index = this.getIndex()
const meta = this.getMeta(index)
if (meta.steps && step < meta.steps) {
this.setState(state => ({
step: state.step + 1,
}))
} else {
const next = index + 1
if (next > slides.length - 1) return
this.goto(next)
}
}
register = (index, meta) => {
const { slides } = this.state
const initialMeta = slides[index].meta || {}
slides[index].meta = { ...initialMeta, ...meta }
this.setState({ slides })
}
componentDidMount() {
document.body.addEventListener('keydown', this.handleKeyDown)
}
componentWillUnmount() {
document.body.removeEventListener('keydown', this.handleKeyDown)
}
render() {
const { slides, mode } = this.state
const index = this.getIndex()
const meta = this.getMeta(index)
const context = {
...this.state,
register: this.register,
}
const [FirstSlide] = slides
let Wrapper = BaseWrapper
switch (mode) {
case PRESENTER:
Wrapper = Presenter
break
case OVERVIEW:
Wrapper = Overview
break
}
return (
<Provider {...this.props} {...this.state} index={index}>
<Wrapper {...this.state} index={index}>
<Swipeable onSwipedRight={this.previous} onSwipedLeft={this.next}>
<Root>
{/*<GoogleFonts />*/}
<Router>
<Slide path="/" index={0} {...context}>
<FirstSlide path="/" />
</Slide>
{slides.map((Component, i) => (
<Slide key={i} path={i + '/*'} index={i} {...context}>
<Component path={i + '/*'} />
</Slide>
))}
</Router>
</Root>
</Swipeable>
</Wrapper>
</Provider>
)
}
}
MDXDeck.propTypes = {
slides: PropTypes.array.isRequired,
theme: PropTypes.object.isRequired,
headTags: PropTypes.array.isRequired,
}
MDXDeck.defaultProps = {
slides: [],
theme: defaultTheme,
headTags: [],
}
export default MDXDeck

View File

@ -0,0 +1,19 @@
import React from 'react'
import { withContext } from './context'
export const Notes = withContext(
class extends React.Component {
constructor(props) {
super(props)
const { context, children } = props
if (!context || typeof context.index === 'undefined') return
context.register(context.index, { notes: children })
}
render() {
return false
}
}
)
export default Notes

View File

@ -1,92 +1,65 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import { Link } from '@reach/router'
import get from 'lodash.get'
import Box from './Box'
import Flex from './Flex'
import Zoom from './Zoom' import Zoom from './Zoom'
import Slide from './Slide' import Slide from './Slide'
import Root from './Root'
import Mono from './Mono'
export const Overview = ({ const noop = () => {}
index,
length, export const Overview = props => {
slides = [], const { index, slides } = props
mode,
metadata = {},
update,
step,
...props
}) => {
const notes = get(metadata, index + '.notes')
return ( return (
<Flex <div
color="white" style={{
bg="black" display: 'flex',
css={{
alignItems: 'flex-start', alignItems: 'flex-start',
height: '100vh', height: '100vh',
backgroundColor: 'black',
}} }}
> >
<Box <div
mr="auto" style={{
px={2}
py={3}
css={{
flex: 'none', flex: 'none',
height: '100vh', height: '100vh',
paddingLeft: 4,
paddingRight: 4,
overflowY: 'auto', overflowY: 'auto',
marginRight: 'auto',
}} }}
> >
{slides.map((Component, i) => ( {slides.map((Component, i) => (
<Box <Link
key={i} key={i}
role="link" to={'/' + i}
p={1}
style={{ style={{
outline: i === index ? '1px solid #07c' : null, display: 'block',
}} color: 'inherit',
css={{ textDecoration: 'none',
padding: 0,
marginTop: 4,
marginBottom: 4,
cursor: 'pointer', cursor: 'pointer',
}} outline: i === index ? '4px solid #0cf' : null,
onClick={e => {
update({ index: i })
}} }}
> >
<Zoom zoom={1 / 6}> <Zoom zoom={1 / 6}>
<Root {...props}> <Slide register={noop}>
<Slide> <Component />
<Component /> </Slide>
</Slide>
</Root>
</Zoom> </Zoom>
</Box> </Link>
))} ))}
</Box> </div>
<Box mx="auto" py={4} width={2 / 3}> <div
<Zoom zoom={2 / 3}> style={{
<Root {...props}>{props.children}</Root> width: 200 / 3 + '%',
</Zoom> margin: 'auto',
<Flex> }}
<Box ml="auto" py={2}> >
{index + 1}/{length} <Zoom zoom={2 / 3}>{props.children}</Zoom>
</Box> </div>
</Flex> </div>
<Box mt={3}>{notes}</Box>
</Box>
</Flex>
) )
} }
Overview.propTypes = {
index: PropTypes.number.isRequired,
length: PropTypes.number.isRequired,
update: PropTypes.func.isRequired,
step: PropTypes.number.isRequired,
slides: PropTypes.array,
mode: PropTypes.string,
notes: PropTypes.object,
}
export default Overview export default Overview

View File

@ -1,112 +1,68 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types'
import get from 'lodash.get'
import Box from './Box'
import Flex from './Flex'
import Zoom from './Zoom' import Zoom from './Zoom'
import Slide from './Slide' import Slide from './Slide'
import Root from './Root'
import Timer from './Timer'
import Mono from './Mono'
import Button from './Button'
const Anchor = Button.withComponent('a') const noop = () => {}
export const Presenter = ({ export const Presenter = props => {
index, const { slides, index } = props
length, const Current = slides[index]
slides = [],
mode,
metadata = {},
update,
step,
...props
}) => {
const Next = slides[index + 1] const Next = slides[index + 1]
const notes = get(metadata, index + '.notes') const { notes } = Current.meta || {}
return ( return (
<Flex <div
color="white" style={{
bg="black" backgroundColor: 'black',
css={{ display: 'flex',
flexDirection: 'column', flexDirection: 'column',
height: '100vh', height: '100vh',
}} }}
> >
<Flex my="auto"> <div
<Box style={{
mx="auto" marginTop: 'auto',
width={5 / 8} marginBottom: 'auto',
css={{ display: 'flex',
border: '1px solid rgba(128, 128, 128, 0.25)', }}
>
<div
style={{
width: 500 / 8 + '%',
marginLeft: 'auto',
marginRight: 'auto',
}} }}
> >
<Zoom zoom={5 / 8}> <Zoom zoom={5 / 8}>{props.children}</Zoom>
<Root {...props}>{props.children}</Root> </div>
<div
style={{
width: 100 / 4 + '%',
marginLeft: 'auto',
marginRight: 'auto',
}}
>
<Zoom zoom={1 / 4}>
{Next && (
<Slide register={noop}>
<Next />
</Slide>
)}
</Zoom> </Zoom>
</Box> {notes}
<Flex </div>
width={1 / 4} </div>
mx="auto" <div
css={{ style={{
flex: 'none', color: 'white',
flexDirection: 'column', padding: 16,
}} fontSize: 20,
> }}
<Box >
mx="auto" <pre style={{ fontFamily: 'Menlo, monospace' }}>
css={{ {index + 1} of {slides.length}
border: '1px solid rgba(128, 128, 128, 0.25)', </pre>
}} </div>
> </div>
<Zoom zoom={1 / 4}>
<Root {...props}>
{Next && (
<Slide>
<Next />
</Slide>
)}
</Root>
</Zoom>
</Box>
<Box
py={3}
css={{
flex: 'auto',
}}
>
{notes}
</Box>
</Flex>
</Flex>
<Flex mt="auto" px={3} py={3}>
<Mono>
Slide {index + 1} of {length}
</Mono>
<Box mx="auto" />
<Anchor
target="_blank"
rel="noopener noreferrer"
href={`${window.location.origin}/${window.location.hash}`}
>
Open in Normal mode
</Anchor>
<Box mx="auto" />
<Timer />
</Flex>
</Flex>
) )
} }
Presenter.propTypes = {
index: PropTypes.number.isRequired,
length: PropTypes.number.isRequired,
update: PropTypes.func.isRequired,
step: PropTypes.number.isRequired,
slides: PropTypes.array,
mode: PropTypes.string,
metadata: PropTypes.object,
}
export default Presenter

View File

@ -0,0 +1,32 @@
import React from 'react'
import { HeadProvider } from './Head'
import { ThemeProvider } from 'styled-components'
import { MDXProvider } from '@mdx-js/tag'
import mdxComponents from './mdx-components'
const DefaultProvider = props => <>{props.children}</>
export const Provider = props => {
const { headTags, theme, components } = props
const {
Provider: UserProvider = DefaultProvider,
components: themeComponents = {},
} = theme
const allComponents = {
...mdxComponents,
...themeComponents,
}
return (
<HeadProvider tags={headTags}>
<ThemeProvider theme={theme}>
<MDXProvider components={allComponents}>
<UserProvider {...props} />
</MDXProvider>
</ThemeProvider>
</HeadProvider>
)
}
export default Provider

View File

@ -0,0 +1,31 @@
import styled from 'styled-components'
const themed = (...tags) => props =>
tags.map(tag => props.theme[tag] && { ['& ' + tag]: props.theme[tag] })
export const Root = styled.div(
props => ({
fontFamily: props.theme.font,
color: props.theme.colors.text,
backgroundColor: props.theme.colors.background,
}),
props => props.theme.css,
themed(
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'a',
'ul',
'ol',
'li',
'p',
'blockquote',
'img',
'table'
)
)
export default Root

View File

@ -0,0 +1,23 @@
import React from 'react'
import styled from 'styled-components'
import { Context } from './context'
const SlideRoot = styled.div(
{
display: 'flex',
flexDirection: 'column',
width: '100vw',
height: '100vh',
alignItems: 'center',
justifyContent: 'center',
},
props => props.theme.Slide
)
export const Slide = ({ children, ...props }) => (
<Context.Provider value={props}>
<SlideRoot>{children}</SlideRoot>
</Context.Provider>
)
export default Slide

View File

@ -0,0 +1,20 @@
import React from 'react'
import { withContext } from './context'
export const Steps = withContext(
class extends React.Component {
constructor(props) {
super(props)
const { register, index } = props.context
const { length } = props
register(index, { steps: length })
}
render() {
const { context, render } = this.props
const { step } = context
return render({ step })
}
}
)
export default Steps

View File

@ -1,41 +1,23 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components' import styled from 'styled-components'
const ZoomRoot = styled.div( const ZoomRoot = styled.div(props => ({
[], backgroundColor: props.theme.colors.background,
{ width: 100 * props.zoom + 'vw',
backgroundColor: 'white', height: 100 * props.zoom + 'vh',
}, }))
props => ({
width: 100 * props.zoom + 'vw',
height: 100 * props.zoom + 'vh',
})
)
ZoomRoot.propTypes = {
zoom: PropTypes.number.isRequired,
}
const ZoomInner = styled.div([], props => ({ const ZoomInner = styled.div([], props => ({
transformOrigin: '0 0', transformOrigin: '0 0',
transform: `scale(${props.zoom})`, transform: `scale(${props.zoom})`,
})) }))
ZoomInner.propTypes = { export const Zoom = ({ zoom, ...props }) => (
zoom: PropTypes.number.isRequired,
}
const Zoom = ({ zoom, ...props }) => (
<ZoomRoot zoom={zoom}> <ZoomRoot zoom={zoom}>
<ZoomInner zoom={zoom} {...props} /> <ZoomInner zoom={zoom} {...props} />
</ZoomRoot> </ZoomRoot>
) )
Zoom.propTypes = {
zoom: PropTypes.number,
}
Zoom.defaultProps = { Zoom.defaultProps = {
zoom: 1, zoom: 1,
} }

View File

@ -0,0 +1,92 @@
import React from 'react'
import PropTypes from 'prop-types'
import get from 'lodash.get'
import Box from './Box'
import Flex from './Flex'
import Zoom from './Zoom'
import Slide from './Slide'
import Root from './Root'
import Mono from './Mono'
export const Overview = ({
index,
length,
slides = [],
mode,
metadata = {},
update,
step,
...props
}) => {
const notes = get(metadata, index + '.notes')
return (
<Flex
color="white"
bg="black"
css={{
alignItems: 'flex-start',
height: '100vh',
}}
>
<Box
mr="auto"
px={2}
py={3}
css={{
flex: 'none',
height: '100vh',
overflowY: 'auto',
}}
>
{slides.map((Component, i) => (
<Box
key={i}
role="link"
p={1}
style={{
outline: i === index ? '1px solid #07c' : null,
}}
css={{
cursor: 'pointer',
}}
onClick={e => {
update({ index: i })
}}
>
<Zoom zoom={1 / 6}>
<Root {...props}>
<Slide>
<Component />
</Slide>
</Root>
</Zoom>
</Box>
))}
</Box>
<Box mx="auto" py={4} width={2 / 3}>
<Zoom zoom={2 / 3}>
<Root {...props}>{props.children}</Root>
</Zoom>
<Flex>
<Box ml="auto" py={2}>
{index + 1}/{length}
</Box>
</Flex>
<Box mt={3}>{notes}</Box>
</Box>
</Flex>
)
}
Overview.propTypes = {
index: PropTypes.number.isRequired,
length: PropTypes.number.isRequired,
update: PropTypes.func.isRequired,
step: PropTypes.number.isRequired,
slides: PropTypes.array,
mode: PropTypes.string,
notes: PropTypes.object,
}
export default Overview

View File

@ -0,0 +1,112 @@
import React from 'react'
import PropTypes from 'prop-types'
import get from 'lodash.get'
import Box from './Box'
import Flex from './Flex'
import Zoom from './Zoom'
import Slide from './Slide'
import Root from './Root'
import Timer from './Timer'
import Mono from './Mono'
import Button from './Button'
const Anchor = Button.withComponent('a')
export const Presenter = ({
index,
length,
slides = [],
mode,
metadata = {},
update,
step,
...props
}) => {
const Next = slides[index + 1]
const notes = get(metadata, index + '.notes')
return (
<Flex
color="white"
bg="black"
css={{
flexDirection: 'column',
height: '100vh',
}}
>
<Flex my="auto">
<Box
mx="auto"
width={5 / 8}
css={{
border: '1px solid rgba(128, 128, 128, 0.25)',
}}
>
<Zoom zoom={5 / 8}>
<Root {...props}>{props.children}</Root>
</Zoom>
</Box>
<Flex
width={1 / 4}
mx="auto"
css={{
flex: 'none',
flexDirection: 'column',
}}
>
<Box
mx="auto"
css={{
border: '1px solid rgba(128, 128, 128, 0.25)',
}}
>
<Zoom zoom={1 / 4}>
<Root {...props}>
{Next && (
<Slide>
<Next />
</Slide>
)}
</Root>
</Zoom>
</Box>
<Box
py={3}
css={{
flex: 'auto',
}}
>
{notes}
</Box>
</Flex>
</Flex>
<Flex mt="auto" px={3} py={3}>
<Mono>
Slide {index + 1} of {length}
</Mono>
<Box mx="auto" />
<Anchor
target="_blank"
rel="noopener noreferrer"
href={`${window.location.origin}/${window.location.hash}`}
>
Open in Normal mode
</Anchor>
<Box mx="auto" />
<Timer />
</Flex>
</Flex>
)
}
Presenter.propTypes = {
index: PropTypes.number.isRequired,
length: PropTypes.number.isRequired,
update: PropTypes.func.isRequired,
step: PropTypes.number.isRequired,
slides: PropTypes.array,
mode: PropTypes.string,
metadata: PropTypes.object,
}
export default Presenter

View File

@ -0,0 +1,8 @@
// may cut some of this
const GoogleFonts = withTheme(
props =>
!!props.theme.googleFont && (
<link href={props.theme.googleFont} rel="stylesheet" />
)
)

View File

@ -0,0 +1,11 @@
import React, { useContext } from 'react'
export const Context = React.createContext(null)
export const withContext = Component => props => (
<Context.Consumer
children={context => <Component {...props} context={context} />}
/>
)
export const useDeck = () => useContext(Context)

View File

@ -1,641 +1,7 @@
/* export { MDXDeck } from './MDXDeck'
* mdx-deck v2 prototype export { Head } from './Head'
* export { Image } from './Image'
* todo: export { Notes } from './Notes'
* - [x] Head export { Steps } from './Steps'
* - [x] Image export { Appear } from './Appear'
* - [x] Notes export { withContext, useDeck } from './context'
* - [x] Appear
* - [ ] Code (may handle this with theming)
* - [x] history api fallback
* - [x] mdx components
* - [x] themes
* - [ ] layouts
* - [ ] presenter mode
* - [ ] timer/clock
* - [ ] open new tab button
* - [ ] look at google slides behavior
* - [ ] overview mode
* - [ ] highlighted state bug
* - [ ] slide numbers
* - [ ] localStorage
* - [ ] keyboard shortcuts
* - [ ] docs for customizing any component
* - [x] swipeable
*
* extras
* - [ ] docs for syntax highlighting
* - [ ] Print view
* - [ ] PDF export?
* - [ ] dots??
*/
import React from 'react'
import { createPortal } from 'react-dom'
import PropTypes from 'prop-types'
import { Router, globalHistory, navigate, Link } from '@reach/router'
import styled, { ThemeProvider, withTheme } from 'styled-components'
import { MDXProvider } from '@mdx-js/tag'
import { Swipeable } from 'react-swipeable'
import { default as defaultTheme } from './themes'
const NORMAL = 'NORMAL'
const PRESENTER = 'PRESENTER'
const OVERVIEW = 'OVERVIEW'
const PRINT = 'PRINT'
export const Context = React.createContext(null)
export const withContext = Component => props => (
<Context.Consumer
children={context => <Component {...props} context={context} />}
/>
)
// TODO check against v1 styles
const SlideRoot = styled.div(
{
display: 'flex',
flexDirection: 'column',
width: '100vw',
height: '100vh',
alignItems: 'center',
justifyContent: 'center',
},
props => props.theme.Slide
)
const Slide = ({ children, ...props }) => (
<Context.Provider value={props}>
<SlideRoot>{children}</SlideRoot>
</Context.Provider>
)
const DefaultProvider = props => <>{props.children}</>
// MDX components
const Heading = styled.h1({ margin: 0 })
const inlineCode = styled.code(
props => ({
fontFamily: props.theme.monospace,
}),
props => props.theme.code
)
const code = styled.pre(
props => ({
fontFamily: props.theme.monospace,
fontSize: '.75em',
}),
props => props.theme.pre
)
const img = styled.img({
maxWidth: '100%',
height: 'auto',
objectFit: 'cover',
})
const TableWrap = styled.div({
overflowX: 'auto',
})
const Table = styled.table({
width: '100%',
borderCollapse: 'separate',
borderSpacing: 0,
'& td, & th': {
textAlign: 'left',
paddingRight: '.5em',
paddingTop: '.25em',
paddingBottom: '.25em',
borderBottom: '1px solid',
verticalAlign: 'top',
},
})
const table = props => (
<TableWrap>
<Table {...props} />
</TableWrap>
)
const components = {
pre: props => props.children,
code,
inlineCode,
img,
table,
}
const keys = {
right: 39,
left: 37,
space: 32,
p: 80,
o: 79,
}
const toggleMode = key => state => ({
mode: state.mode === key ? NORMAL : key,
})
const ZoomOuter = styled.div(props => ({
backgroundColor: props.theme.colors.background,
width: 100 * props.zoom + 'vw',
height: 100 * props.zoom + 'vh',
}))
const ZoomInner = styled.div(props => ({
transformOrigin: '0 0',
transform: `scale(${props.zoom})`,
}))
const Zoom = props => (
<ZoomOuter zoom={props.zoom}>
<ZoomInner {...props} />
</ZoomOuter>
)
Zoom.defaultProps = {
zoom: 1,
}
const noop = () => {}
const Presenter = props => {
const { slides, index } = props
const Current = slides[index]
const Next = slides[index + 1]
const { notes } = Current.meta || {}
return (
<div
style={{
backgroundColor: 'black',
display: 'flex',
flexDirection: 'column',
height: '100vh',
}}
>
<div
style={{
marginTop: 'auto',
marginBottom: 'auto',
display: 'flex',
}}
>
<div
style={{
width: 500 / 8 + '%',
marginLeft: 'auto',
marginRight: 'auto',
}}
>
<Zoom zoom={5 / 8}>{props.children}</Zoom>
</div>
<div
style={{
width: 100 / 4 + '%',
marginLeft: 'auto',
marginRight: 'auto',
}}
>
<Zoom zoom={1 / 4}>
{Next && (
<Slide register={noop}>
<Next />
</Slide>
)}
</Zoom>
{notes}
</div>
</div>
<div
style={{
color: 'white',
padding: 16,
fontSize: 20,
}}
>
<pre style={{ fontFamily: 'Menlo, monospace' }}>
{index + 1} of {slides.length}
</pre>
</div>
</div>
)
}
const Overview = props => {
const { index, slides } = props
return (
<div
style={{
display: 'flex',
alignItems: 'flex-start',
height: '100vh',
backgroundColor: 'black',
}}
>
<div
style={{
flex: 'none',
height: '100vh',
paddingLeft: 4,
paddingRight: 4,
overflowY: 'auto',
marginRight: 'auto',
}}
>
{slides.map((Component, i) => (
<Link
key={i}
to={'/' + i}
style={{
display: 'block',
color: 'inherit',
textDecoration: 'none',
padding: 0,
marginTop: 4,
marginBottom: 4,
cursor: 'pointer',
outline: i === index ? '4px solid #0cf' : null,
}}
>
<Zoom zoom={1 / 6}>
<Slide register={noop}>
<Component />
</Slide>
</Zoom>
</Link>
))}
</div>
<div
style={{
width: 200 / 3 + '%',
margin: 'auto',
}}
>
<Zoom zoom={2 / 3}>{props.children}</Zoom>
</div>
</div>
)
}
const Root = props => <>{props.children}</>
const themed = (...tags) => props =>
tags.map(tag => props.theme[tag] && { ['& ' + tag]: props.theme[tag] })
const RootStyles = styled.div(
props => ({
fontFamily: props.theme.font,
color: props.theme.colors.text,
backgroundColor: props.theme.colors.background,
}),
props => props.theme.css,
themed(
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'a',
'ul',
'ol',
'li',
'p',
'blockquote',
'img',
'table'
)
)
const GoogleFonts = withTheme(
props =>
!!props.theme.googleFont && (
<link href={props.theme.googleFont} rel="stylesheet" />
)
)
export class MDXDeck extends React.Component {
constructor(props) {
super(props)
this.state = {
slides: props.slides,
step: 0,
mode: NORMAL,
}
}
handleKeyDown = e => {
const { key, keyCode, metaKey, ctrlKey, altKey, shiftKey } = e
const { activeElement } = document
if (activeElement.tagName !== 'BODY' && activeElement.tagName !== 'DIV')
return
if (metaKey || ctrlKey) return
const alt = altKey && !shiftKey
const shift = shiftKey && !altKey
if (alt) {
switch (keyCode) {
case keys.p:
this.setState(toggleMode(PRESENTER))
break
case keys.o:
this.setState(toggleMode(OVERVIEW))
break
}
} else {
switch (keyCode) {
case keys.left:
e.preventDefault()
this.previous()
break
case keys.right:
case keys.space:
e.preventDefault()
this.next()
break
}
}
}
getIndex = () => {
const { pathname } = globalHistory.location
return Number(pathname.split('/')[1] || 0)
}
getMeta = i => {
const { slides } = this.state
const { meta = {} } = slides[i] || {}
return meta
}
goto = i => {
const current = this.getIndex()
const reverse = i < current
navigate('/' + i)
const meta = this.getMeta(i)
this.setState({
step: reverse ? meta.steps || 0 : 0,
})
}
previous = () => {
const { slides, step } = this.state
const index = this.getIndex()
const meta = this.getMeta(index)
if (meta.steps && step > 0) {
this.setState(state => ({
step: state.step - 1,
}))
} else {
const previous = index - 1
if (previous < 0) return
this.goto(previous)
}
}
next = () => {
const { slides, step } = this.state
const index = this.getIndex()
const meta = this.getMeta(index)
if (meta.steps && step < meta.steps) {
this.setState(state => ({
step: state.step + 1,
}))
} else {
const next = index + 1
if (next > slides.length - 1) return
this.goto(next)
}
}
register = (index, meta) => {
const { slides } = this.state
const initialMeta = slides[index].meta || {}
slides[index].meta = { ...initialMeta, ...meta }
this.setState({ slides })
}
componentDidMount() {
document.body.addEventListener('keydown', this.handleKeyDown)
}
componentWillUnmount() {
document.body.removeEventListener('keydown', this.handleKeyDown)
}
render() {
const { headTags, theme, components } = this.props
const { slides, mode } = this.state
const index = this.getIndex()
const meta = this.getMeta(index)
const context = {
...this.state,
register: this.register,
}
const {
Provider = DefaultProvider,
components: themeComponents = {},
} = theme
const [FirstSlide] = slides
const mdxComponents = {
...components,
...themeComponents,
}
let Wrapper = Root
switch (mode) {
case PRESENTER:
Wrapper = Presenter
break
case OVERVIEW:
Wrapper = Overview
break
}
return (
<HeadProvider tags={headTags}>
<ThemeProvider theme={theme}>
<MDXProvider components={mdxComponents}>
<Provider {...this.state} index={index}>
<Wrapper {...this.state} index={index}>
<Swipeable
onSwipedRight={this.previous}
onSwipedLeft={this.next}
>
<RootStyles>
<GoogleFonts />
<Router>
<Slide path="/" index={0} {...context}>
<FirstSlide path="/" />
</Slide>
{slides.map((Component, i) => (
<Slide key={i} path={i + '/*'} index={i} {...context}>
<Component path={i + '/*'} />
</Slide>
))}
</Router>
</RootStyles>
</Swipeable>
</Wrapper>
</Provider>
</MDXProvider>
</ThemeProvider>
</HeadProvider>
)
}
}
MDXDeck.propTypes = {
slides: PropTypes.array.isRequired,
theme: PropTypes.object.isRequired,
components: PropTypes.object,
// Provider: PropTypes.func,
headTags: PropTypes.array.isRequired,
}
MDXDeck.defaultProps = {
slides: [],
theme: defaultTheme,
headTags: [],
components,
}
const HeadContext = React.createContext({
tags: [],
push: () => {
console.warn('Missing HeadProvider')
},
})
const HeadProvider = ({ tags = [], children }) => {
const push = elements => {
tags.push(...elements)
}
const context = { push }
return <HeadContext.Provider value={context}>{children}</HeadContext.Provider>
}
export class Head extends React.Component {
state = {
didMount: false,
}
rehydrate = () => {
const children = React.Children.toArray(this.props.children)
const nodes = [...document.head.querySelectorAll('[data-head]')]
nodes.forEach(node => {
node.remove()
})
children.forEach(child => {
if (child.type === 'title') {
const title = document.head.querySelector('title')
if (title) title.remove()
}
if (child.type === 'meta') {
const { name } = child.props
let meta
if (name) meta = document.head.querySelector(`meta[name="${name}"]`)
if (meta) meta.remove()
}
})
this.setState({ didMount: true })
}
componentDidMount() {
this.rehydrate()
}
render() {
const children = React.Children.toArray(this.props.children).map(child =>
React.cloneElement(child, {
'data-head': true,
})
)
if (!this.state.didMount) {
return (
<HeadContext.Consumer
children={({ push }) => {
push(children)
return false
}}
/>
)
}
return createPortal(children, document.head)
}
}
export const Image = styled.div(
{
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
},
props => ({
backgroundSize: props.size,
width: props.width,
height: props.height,
backgroundImage: `url(${props.src})`,
})
)
Image.defaultProps = {
size: 'cover',
width: '100vw',
height: '100vh',
}
export const Notes = withContext(
class extends React.Component {
constructor(props) {
super(props)
const { context, children } = props
if (!context || typeof context.index === 'undefined') return
context.register(context.index, { notes: children })
}
render() {
return false
}
}
)
export const Steps = withContext(
class extends React.Component {
constructor(props) {
super(props)
const { register, index } = props.context
const { length } = props
register(index, { steps: length })
}
render() {
const { context, render } = this.props
const { step } = context
return render({ step })
}
}
)
export const Appear = props => {
const arr = React.Children.toArray(props.children)
return (
<Steps
length={arr.length}
render={({ step }) => {
const children = arr.map((child, i) =>
i < step
? child
: React.cloneElement(child, {
style: {
...child.props.style,
visibility: 'hidden',
},
})
)
return <>{children}</>
}}
/>
)
}

View File

@ -0,0 +1,58 @@
import React from 'react'
import styled from 'styled-components'
export const Heading = styled.h1({ margin: 0 })
export const inlineCode = styled.code(
props => ({
fontFamily: props.theme.monospace,
}),
props => props.theme.code
)
export const code = styled.pre(
props => ({
fontFamily: props.theme.monospace,
fontSize: '.75em',
}),
props => props.theme.pre
)
export const img = styled.img({
maxWidth: '100%',
height: 'auto',
objectFit: 'cover',
})
export const TableWrap = styled.div({
overflowX: 'auto',
})
export const Table = styled.table({
width: '100%',
borderCollapse: 'separate',
borderSpacing: 0,
'& td, & th': {
textAlign: 'left',
paddingRight: '.5em',
paddingTop: '.25em',
paddingBottom: '.25em',
borderBottom: '1px solid',
verticalAlign: 'top',
},
})
export const table = props => (
<TableWrap>
<Table {...props} />
</TableWrap>
)
export const components = {
pre: props => props.children,
code,
inlineCode,
img,
table,
}
export default components

View File

@ -0,0 +1,16 @@
# @mdx-deck/webpack-html-plugin
Webpack plugin for generating HTML
```sh
npm i @mdx-deck/webpack-html-plugin
```
```js
// webpack.config.js
const HTMLPlugin = require('@mdx-deck/webpack-html-plugin')
module.exports = {
plugins: [new HTMLPlugin()],
}
```

View File

@ -3,5 +3,8 @@
"version": "2.0.0-0", "version": "2.0.0-0",
"author": "Brent Jackson <jxnblk@gmail.com>", "author": "Brent Jackson <jxnblk@gmail.com>",
"license": "MIT", "license": "MIT",
"repository": "github:jxnblk/mdx-deck" "repository": "github:jxnblk/mdx-deck",
"dependencies": {
"webpack-sources": "^1.3.0"
}
} }

View File

@ -0,0 +1,7 @@
{
"name": "@mdx-deck/layouts",
"version": "2.0.0-0",
"main": "index.js",
"author": "Brent Jackson <jxnblk@gmail.com>",
"license": "MIT"
}

View File

@ -1,14 +1,12 @@
const { getOptions } = require('loader-utils') const { getOptions } = require('loader-utils')
const mdx = require('@mdx-js/mdx') const mdx = require('@mdx-js/mdx')
// const normalizeNewline = require('normalize-newline') const mdxPlugin = require('@mdx-deck/mdx-plugin')
const mdxPluginSplit = require('mdx-plugin-split')
module.exports = async function(src) { module.exports = async function(src) {
const callback = this.async() const callback = this.async()
const options = getOptions(this) || {} const options = getOptions(this) || {}
// options.skipExport = true
options.mdPlugins = options.mdPlugins || [] options.mdPlugins = options.mdPlugins || []
options.mdPlugins.push(mdxPluginSplit) options.mdPlugins.push(mdxPlugin)
const result = mdx.sync(src, options) const result = mdx.sync(src, options)
@ -16,7 +14,5 @@ module.exports = async function(src) {
import { MDXTag } from '@mdx-js/tag' import { MDXTag } from '@mdx-js/tag'
${result}` ${result}`
console.log(code)
return callback(null, code) return callback(null, code)
} }

View File

@ -0,0 +1,12 @@
{
"name": "@mdx-deck/loader",
"version": "2.0.0-0",
"main": "index.js",
"author": "Brent Jackson <jxnblk@gmail.com>",
"license": "MIT",
"dependencies": {
"@mdx-deck/mdx-plugin": "^2.0.0-0",
"@mdx-js/mdx": "^0.20.0",
"loader-utils": "^1.2.3"
}
}

View File

@ -0,0 +1,94 @@
import { Head, Image, Notes, Appear, Steps } from '@mdx-deck/components'
<Head>
<title>MDX Deck Demo</title>
</Head>
# Hello MDX Deck
---
## This is v2
---
## What's New
<ul>
<Appear>
<li>Reach Router</li>
<li>Less opinionated styles</li>
<li>more stuff</li>
</Appear>
</ul>
---
<Image
src='https://source.unsplash.com/random/1024x768'
size='contain'
/>
---
## This slide has notes
<Notes>
Hello, secret speaker notes
</Notes>
---
```js
const codeExample = require('./code-example')
```
---
## More Appear
<Appear>
<div>One</div>
<div>Two</div>
<div>Three</div>
<div>Four</div>
<div>Five</div>
<div>Six</div>
</Appear>
---
## Steps Components
<Steps
length={3}
render={({ step }) => (
<pre>
Step: {step}
</pre>
)}
/>
---
export default props =>
<div
style={{
width: '100vw',
height: '100vh',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'tomato'
}}>
{props.children}
</div>
## With a (tomato) layout
---
## Last Slide

View File

@ -9,7 +9,7 @@ const remark = {
emoji: require('remark-emoji'), emoji: require('remark-emoji'),
unwrapImages: require('remark-unwrap-images'), unwrapImages: require('remark-unwrap-images'),
} }
const HTMLPlugin = require('./html-plugin') const HTMLPlugin = require('@mdx-deck/webpack-html-plugin')
const babel = require('../babel.config') const babel = require('../babel.config')
const rules = [ const rules = [
@ -35,7 +35,7 @@ const rules = [
options: babel, options: babel,
}, },
{ {
loader: require.resolve('./loader.js'), loader: require.resolve('@mdx-deck/loader'),
options: { options: {
mdPlugins: [remark.emoji, remark.unwrapImages], mdPlugins: [remark.emoji, remark.unwrapImages],
}, },
@ -80,7 +80,7 @@ const createConfig = (opts = {}) => {
path.join(opts.dirname, 'node_modules') path.join(opts.dirname, 'node_modules')
) )
config.entry = [path.join(__dirname, '../src/entry.js')] config.entry = [path.join(__dirname, './entry.js')]
const defs = Object.assign({}, opts.globals, { const defs = Object.assign({}, opts.globals, {
OPTIONS: JSON.stringify(opts), OPTIONS: JSON.stringify(opts),

View File

@ -1,23 +1,13 @@
import React from 'react' import React from 'react'
import { render } from 'react-dom' import { render } from 'react-dom'
import { MDXDeck } from './index' import { MDXDeck } from '@mdx-deck/components'
const mod = require(FILENAME) const mod = require(FILENAME)
const { slides } = mod const { slides, theme } = mod
const { theme, components, Provider } = mod
console.log(slides)
export default class App extends React.Component { export default class App extends React.Component {
render() { render() {
return ( return <MDXDeck slides={slides} theme={theme} />
<MDXDeck
slides={slides}
theme={theme}
components={components}
Provider={Provider}
/>
)
} }
} }

View File

@ -2,13 +2,12 @@
"name": "mdx-deck", "name": "mdx-deck",
"version": "1.10.0", "version": "1.10.0",
"description": "MDX-based presentation decks", "description": "MDX-based presentation decks",
"main": "src/index.js",
"bin": { "bin": {
"mdx-deck": "./cli.js" "mdx-deck": "./cli.js"
}, },
"scripts": { "scripts": {
"start": "./cli.js docs/index.mdx -p 8080", "start": "./cli.js demo.mdx -p 8080",
"build": "./cli.js build docs/index.mdx -d site", "build": "./cli.js build demo.mdx -d public",
"help": "./cli.js", "help": "./cli.js",
"test": "jest" "test": "jest"
}, },
@ -24,6 +23,9 @@
"@babel/plugin-syntax-dynamic-import": "^7.0.0", "@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/preset-env": "^7.3.4", "@babel/preset-env": "^7.3.4",
"@babel/preset-react": "^7.0.0", "@babel/preset-react": "^7.0.0",
"@mdx-deck/components": "^2.0.0-0",
"@mdx-deck/loader": "^2.0.0-0",
"@mdx-deck/webpack-html-plugin": "^2.0.0-0",
"@mdx-js/mdx": "^0.20.0", "@mdx-js/mdx": "^0.20.0",
"@mdx-js/tag": "^0.18.0", "@mdx-js/tag": "^0.18.0",
"@reach/router": "^1.2.1", "@reach/router": "^1.2.1",
@ -60,7 +62,6 @@
"webpack-hot-middleware": "^2.24.3", "webpack-hot-middleware": "^2.24.3",
"webpack-merge": "^4.2.1", "webpack-merge": "^4.2.1",
"webpack-node-externals": "^1.7.2", "webpack-node-externals": "^1.7.2",
"webpack-sources": "^1.3.0",
"webpackbar": "^3.1.5" "webpackbar": "^3.1.5"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "mdx-plugin-split", "name": "@mdx-deck/mdx-plugin",
"version": "0.0.2", "version": "2.0.0-0",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -0,0 +1,7 @@
{
"name": "@mdx-deck/themes",
"version": "2.0.0-0",
"main": "src/index.js",
"author": "Brent Jackson <jxnblk@gmail.com>",
"license": "MIT"
}

View File

@ -5362,6 +5362,14 @@ mdx-deck-code-surfer@^0.5.5:
code-surfer "^0.5.5" code-surfer "^0.5.5"
memoize-one "^4.0.2" memoize-one "^4.0.2"
mdx-plugin-split@0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/mdx-plugin-split/-/mdx-plugin-split-0.0.2.tgz#608d90f12108593badfc54ce24533ef82fd41d47"
integrity sha512-Kl8H0CVO4QPJ1AZ+Weaf7uHMB+px4uffZ2h3tl7ItcvU4VJkASRjeFYJN5GtQbmM8RpXTgxyp+xfaQqHOClNXQ==
dependencies:
unist-util-is "^2.1.2"
unist-util-visit "^1.4.0"
mem@^4.0.0: mem@^4.0.0:
version "4.1.0" version "4.1.0"
resolved "https://registry.yarnpkg.com/mem/-/mem-4.1.0.tgz#aeb9be2d21f47e78af29e4ac5978e8afa2ca5b8a" resolved "https://registry.yarnpkg.com/mem/-/mem-4.1.0.tgz#aeb9be2d21f47e78af29e4ac5978e8afa2ca5b8a"