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:
parent
e97e7a1df7
commit
a838705222
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,5 @@
|
||||
dist
|
||||
site
|
||||
public
|
||||
coverage
|
||||
node_modules
|
||||
package-lock.json
|
||||
|
34
packages/components/notes.md
Normal file
34
packages/components/notes.md
Normal 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??
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mdx-deck/components",
|
||||
"version": "2.0.0-0",
|
||||
"main": "index.js",
|
||||
"main": "src/index.js",
|
||||
"author": "Brent Jackson <jxnblk@gmail.com>",
|
||||
"license": "MIT"
|
||||
}
|
||||
|
26
packages/components/src/Appear.js
Normal file
26
packages/components/src/Appear.js
Normal 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
|
68
packages/components/src/Head.js
Normal file
68
packages/components/src/Head.js
Normal 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
|
22
packages/components/src/Image.js
Normal file
22
packages/components/src/Image.js
Normal 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
|
196
packages/components/src/MDXDeck.js
Normal file
196
packages/components/src/MDXDeck.js
Normal 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
|
19
packages/components/src/Notes.js
Normal file
19
packages/components/src/Notes.js
Normal 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
|
@ -1,92 +1,65 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import get from 'lodash.get'
|
||||
import Box from './Box'
|
||||
import Flex from './Flex'
|
||||
import { Link } from '@reach/router'
|
||||
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')
|
||||
const noop = () => {}
|
||||
|
||||
export const Overview = props => {
|
||||
const { index, slides } = props
|
||||
|
||||
return (
|
||||
<Flex
|
||||
color="white"
|
||||
bg="black"
|
||||
css={{
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
height: '100vh',
|
||||
backgroundColor: 'black',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
mr="auto"
|
||||
px={2}
|
||||
py={3}
|
||||
css={{
|
||||
<div
|
||||
style={{
|
||||
flex: 'none',
|
||||
height: '100vh',
|
||||
paddingLeft: 4,
|
||||
paddingRight: 4,
|
||||
overflowY: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}
|
||||
>
|
||||
{slides.map((Component, i) => (
|
||||
<Box
|
||||
<Link
|
||||
key={i}
|
||||
role="link"
|
||||
p={1}
|
||||
to={'/' + i}
|
||||
style={{
|
||||
outline: i === index ? '1px solid #07c' : null,
|
||||
}}
|
||||
css={{
|
||||
display: 'block',
|
||||
color: 'inherit',
|
||||
textDecoration: 'none',
|
||||
padding: 0,
|
||||
marginTop: 4,
|
||||
marginBottom: 4,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={e => {
|
||||
update({ index: i })
|
||||
outline: i === index ? '4px solid #0cf' : null,
|
||||
}}
|
||||
>
|
||||
<Zoom zoom={1 / 6}>
|
||||
<Root {...props}>
|
||||
<Slide>
|
||||
<Component />
|
||||
</Slide>
|
||||
</Root>
|
||||
<Slide register={noop}>
|
||||
<Component />
|
||||
</Slide>
|
||||
</Zoom>
|
||||
</Box>
|
||||
</Link>
|
||||
))}
|
||||
</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>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: 200 / 3 + '%',
|
||||
margin: 'auto',
|
||||
}}
|
||||
>
|
||||
<Zoom zoom={2 / 3}>{props.children}</Zoom>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -1,112 +1,68 @@
|
||||
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')
|
||||
const noop = () => {}
|
||||
|
||||
export const Presenter = ({
|
||||
index,
|
||||
length,
|
||||
slides = [],
|
||||
mode,
|
||||
metadata = {},
|
||||
update,
|
||||
step,
|
||||
...props
|
||||
}) => {
|
||||
export const Presenter = props => {
|
||||
const { slides, index } = props
|
||||
const Current = slides[index]
|
||||
const Next = slides[index + 1]
|
||||
const notes = get(metadata, index + '.notes')
|
||||
const { notes } = Current.meta || {}
|
||||
|
||||
return (
|
||||
<Flex
|
||||
color="white"
|
||||
bg="black"
|
||||
css={{
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'black',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<Flex my="auto">
|
||||
<Box
|
||||
mx="auto"
|
||||
width={5 / 8}
|
||||
css={{
|
||||
border: '1px solid rgba(128, 128, 128, 0.25)',
|
||||
<div
|
||||
style={{
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 500 / 8 + '%',
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}
|
||||
>
|
||||
<Zoom zoom={5 / 8}>
|
||||
<Root {...props}>{props.children}</Root>
|
||||
<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>
|
||||
</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>
|
||||
{notes}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
color: 'white',
|
||||
padding: 16,
|
||||
fontSize: 20,
|
||||
}}
|
||||
>
|
||||
<pre style={{ fontFamily: 'Menlo, monospace' }}>
|
||||
{index + 1} of {slides.length}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
|
32
packages/components/src/Provider.js
Normal file
32
packages/components/src/Provider.js
Normal 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
|
31
packages/components/src/Root.js
Normal file
31
packages/components/src/Root.js
Normal 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
|
23
packages/components/src/Slide.js
Normal file
23
packages/components/src/Slide.js
Normal 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
|
20
packages/components/src/Steps.js
Normal file
20
packages/components/src/Steps.js
Normal 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
|
@ -1,41 +1,23 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const ZoomRoot = styled.div(
|
||||
[],
|
||||
{
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
props => ({
|
||||
width: 100 * props.zoom + 'vw',
|
||||
height: 100 * props.zoom + 'vh',
|
||||
})
|
||||
)
|
||||
|
||||
ZoomRoot.propTypes = {
|
||||
zoom: PropTypes.number.isRequired,
|
||||
}
|
||||
const ZoomRoot = 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})`,
|
||||
}))
|
||||
|
||||
ZoomInner.propTypes = {
|
||||
zoom: PropTypes.number.isRequired,
|
||||
}
|
||||
|
||||
const Zoom = ({ zoom, ...props }) => (
|
||||
export const Zoom = ({ zoom, ...props }) => (
|
||||
<ZoomRoot zoom={zoom}>
|
||||
<ZoomInner zoom={zoom} {...props} />
|
||||
</ZoomRoot>
|
||||
)
|
||||
|
||||
Zoom.propTypes = {
|
||||
zoom: PropTypes.number,
|
||||
}
|
||||
|
||||
Zoom.defaultProps = {
|
||||
zoom: 1,
|
||||
}
|
||||
|
92
packages/components/src/_ref/Overview.js
Normal file
92
packages/components/src/_ref/Overview.js
Normal 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
|
112
packages/components/src/_ref/Presenter.js
Normal file
112
packages/components/src/_ref/Presenter.js
Normal 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
|
8
packages/components/src/_temp.js
Normal file
8
packages/components/src/_temp.js
Normal file
@ -0,0 +1,8 @@
|
||||
// may cut some of this
|
||||
|
||||
const GoogleFonts = withTheme(
|
||||
props =>
|
||||
!!props.theme.googleFont && (
|
||||
<link href={props.theme.googleFont} rel="stylesheet" />
|
||||
)
|
||||
)
|
11
packages/components/src/context.js
Normal file
11
packages/components/src/context.js
Normal 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)
|
@ -1,641 +1,7 @@
|
||||
/*
|
||||
* mdx-deck v2 prototype
|
||||
*
|
||||
* todo:
|
||||
* - [x] Head
|
||||
* - [x] Image
|
||||
* - [x] Notes
|
||||
* - [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}</>
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export { MDXDeck } from './MDXDeck'
|
||||
export { Head } from './Head'
|
||||
export { Image } from './Image'
|
||||
export { Notes } from './Notes'
|
||||
export { Steps } from './Steps'
|
||||
export { Appear } from './Appear'
|
||||
export { withContext, useDeck } from './context'
|
||||
|
58
packages/components/src/mdx-components.js
Normal file
58
packages/components/src/mdx-components.js
Normal 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
|
16
packages/html-plugin/README.md
Normal file
16
packages/html-plugin/README.md
Normal 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()],
|
||||
}
|
||||
```
|
@ -3,5 +3,8 @@
|
||||
"version": "2.0.0-0",
|
||||
"author": "Brent Jackson <jxnblk@gmail.com>",
|
||||
"license": "MIT",
|
||||
"repository": "github:jxnblk/mdx-deck"
|
||||
"repository": "github:jxnblk/mdx-deck",
|
||||
"dependencies": {
|
||||
"webpack-sources": "^1.3.0"
|
||||
}
|
||||
}
|
||||
|
7
packages/layouts/package.json
Normal file
7
packages/layouts/package.json
Normal 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"
|
||||
}
|
@ -1,14 +1,12 @@
|
||||
const { getOptions } = require('loader-utils')
|
||||
const mdx = require('@mdx-js/mdx')
|
||||
// const normalizeNewline = require('normalize-newline')
|
||||
const mdxPluginSplit = require('mdx-plugin-split')
|
||||
const mdxPlugin = require('@mdx-deck/mdx-plugin')
|
||||
|
||||
module.exports = async function(src) {
|
||||
const callback = this.async()
|
||||
const options = getOptions(this) || {}
|
||||
// options.skipExport = true
|
||||
options.mdPlugins = options.mdPlugins || []
|
||||
options.mdPlugins.push(mdxPluginSplit)
|
||||
options.mdPlugins.push(mdxPlugin)
|
||||
|
||||
const result = mdx.sync(src, options)
|
||||
|
||||
@ -16,7 +14,5 @@ module.exports = async function(src) {
|
||||
import { MDXTag } from '@mdx-js/tag'
|
||||
${result}`
|
||||
|
||||
console.log(code)
|
||||
|
||||
return callback(null, code)
|
||||
}
|
12
packages/loader/package.json
Normal file
12
packages/loader/package.json
Normal 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"
|
||||
}
|
||||
}
|
94
packages/mdx-deck/demo.mdx
Normal file
94
packages/mdx-deck/demo.mdx
Normal 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
|
||||
|
||||
|
@ -9,7 +9,7 @@ const remark = {
|
||||
emoji: require('remark-emoji'),
|
||||
unwrapImages: require('remark-unwrap-images'),
|
||||
}
|
||||
const HTMLPlugin = require('./html-plugin')
|
||||
const HTMLPlugin = require('@mdx-deck/webpack-html-plugin')
|
||||
const babel = require('../babel.config')
|
||||
|
||||
const rules = [
|
||||
@ -35,7 +35,7 @@ const rules = [
|
||||
options: babel,
|
||||
},
|
||||
{
|
||||
loader: require.resolve('./loader.js'),
|
||||
loader: require.resolve('@mdx-deck/loader'),
|
||||
options: {
|
||||
mdPlugins: [remark.emoji, remark.unwrapImages],
|
||||
},
|
||||
@ -80,7 +80,7 @@ const createConfig = (opts = {}) => {
|
||||
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, {
|
||||
OPTIONS: JSON.stringify(opts),
|
||||
|
@ -1,23 +1,13 @@
|
||||
import React from 'react'
|
||||
import { render } from 'react-dom'
|
||||
import { MDXDeck } from './index'
|
||||
import { MDXDeck } from '@mdx-deck/components'
|
||||
|
||||
const mod = require(FILENAME)
|
||||
const { slides } = mod
|
||||
const { theme, components, Provider } = mod
|
||||
|
||||
console.log(slides)
|
||||
const { slides, theme } = mod
|
||||
|
||||
export default class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<MDXDeck
|
||||
slides={slides}
|
||||
theme={theme}
|
||||
components={components}
|
||||
Provider={Provider}
|
||||
/>
|
||||
)
|
||||
return <MDXDeck slides={slides} theme={theme} />
|
||||
}
|
||||
}
|
||||
|
@ -2,13 +2,12 @@
|
||||
"name": "mdx-deck",
|
||||
"version": "1.10.0",
|
||||
"description": "MDX-based presentation decks",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
"mdx-deck": "./cli.js"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "./cli.js docs/index.mdx -p 8080",
|
||||
"build": "./cli.js build docs/index.mdx -d site",
|
||||
"start": "./cli.js demo.mdx -p 8080",
|
||||
"build": "./cli.js build demo.mdx -d public",
|
||||
"help": "./cli.js",
|
||||
"test": "jest"
|
||||
},
|
||||
@ -24,6 +23,9 @@
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
|
||||
"@babel/preset-env": "^7.3.4",
|
||||
"@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/tag": "^0.18.0",
|
||||
"@reach/router": "^1.2.1",
|
||||
@ -60,7 +62,6 @@
|
||||
"webpack-hot-middleware": "^2.24.3",
|
||||
"webpack-merge": "^4.2.1",
|
||||
"webpack-node-externals": "^1.7.2",
|
||||
"webpack-sources": "^1.3.0",
|
||||
"webpackbar": "^3.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mdx-plugin-split",
|
||||
"version": "0.0.2",
|
||||
"name": "@mdx-deck/mdx-plugin",
|
||||
"version": "2.0.0-0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
7
packages/themes/package.json
Normal file
7
packages/themes/package.json
Normal 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"
|
||||
}
|
@ -5362,6 +5362,14 @@ mdx-deck-code-surfer@^0.5.5:
|
||||
code-surfer "^0.5.5"
|
||||
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:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/mem/-/mem-4.1.0.tgz#aeb9be2d21f47e78af29e4ac5978e8afa2ca5b8a"
|
||||
|
Loading…
Reference in New Issue
Block a user