1
1
mirror of https://github.com/jxnblk/mdx-deck.git synced 2024-11-29 05:46:00 +03:00

Merge pull request #392 from jxnblk/next-v3

v3
This commit is contained in:
Brent Jackson 2019-07-16 23:04:15 -04:00 committed by GitHub
commit f9bc2456ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
192 changed files with 3911 additions and 5166 deletions

2
.gitignore vendored
View File

@ -3,3 +3,5 @@ public
coverage
node_modules
package-lock.json
public
.cache

View File

@ -1,5 +1,27 @@
# Migration
## Updating to MDX Deck v3
- The `export default` syntax for slide layouts is no longer supported. Replace this syntax with the layout component wrapped around the slide content instead.
- The following packages have been deprecated. Import components directly from the `mdx-deck` package instead.
- `@mdx-deck/components`
- `@mdx-deck/layouts`
- `@mdx-deck/mdx-plugin`
- `@mdx-deck/loader`
- `@mdx-deck/webpack-html-plugin`
- The Gatsby theme package as been renamed: `gatsby-theme-mdx-deck`
- Theming now uses [Theme UI][], and the theme format has changed.
- See the [theming docs](/docs/theming.md) for information on creating custom themes.
- **Or** use the `convertLegacyTheme` utility to shim themes written in the v2 format
- The standalone CLI has been rewritten with Gatsby, and the following CLI flags are no longer supported:
- `--webpack` - use the Gatsby theme directly to customize webpack features
- `--out-dir` - decks are now built in the `public/` directory
- `--no-html` - individual slides are rendered client side, but the first slide is always rendered as static HTML using Gatsby
- Custom `Presenter` components can no longer be added to a theme. Use the component shadowing API with the Gatsby theme instead.
- Multiple MDX files can no longer be combined into a single presentation
[theme ui]: https://theme-ui.com
## Updating to MDX Deck v2
With a few exceptions, decks created with v1 should be compatible with v2. The following is a list of steps to ensure your slide deck will work with v2.

View File

@ -1 +0,0 @@
module.exports = require('./packages/mdx-deck/babel.config')

3
cypress.json Normal file
View File

@ -0,0 +1,3 @@
{
"baseUrl": "http://localhost:8000/"
}

View File

@ -0,0 +1,21 @@
context('MDX Deck', () => {
beforeEach(() => {
cy.visit('http://localhost:8000')
})
it('opens', () => {
cy.visit('http://localhost:8000')
})
it('contains the title', () => {
cy.contains('MDX Deck')
})
/* doesn't work
it('goes to the next slide', () => {
cy.get('body')
.type('{rightarrow}')
.contains('Presentation')
})
*/
})

17
cypress/plugins/index.js Normal file
View File

@ -0,0 +1,17 @@
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}

View File

@ -0,0 +1,25 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })

20
cypress/support/index.js Normal file
View File

@ -0,0 +1,20 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@ -1,154 +1,36 @@
# Advanced Usage
## Custom MDX components
MDX Deck includes default components for MDX, but to provide custom components to the [MDXProvider][], add a `components` object to the `theme`.
```js
// example theme
import Heading from './Heading'
export default {
components: {
h1: Heading,
},
}
```
See the [MDX][] docs for more or take a look
at the [default set of components](../packages/components/src/mdx-components.js) as a reference.
## Custom Provider component
A custom Provider component is useful for adding custom context providers in React or adding persistent UI elements to the entire deck.
To define a custom Provider component, you'll need to import it into your custom theme and set it using the key `Provider` like shown below:
To define a custom Provider component, you'll need to import it and add it as the `Provider` key in your theme.
```js
// example theme.js
import Provider from './Provider'
export default {
font: 'Georgia, serif',
fonts: {
body: 'Georgia, serif',
},
Provider,
}
```
A custom Provider component will receive the application's state as props,
which can be used to show custom page numbers or add other elements to the UI.
#### Props
- `slides`: (array) the components for each slide
- `index`: (number) the current slide index
- `mode`: (string) the current mode (one of `'normal'`, `'presenter'`, or `'overview'`)
- `step`: (number) the current visible step in an Appear or Step component
- Each slide includes a `meta` object with a `notes` field when the Notes component is used within a slide
#### Example
The example below will display the current slide out of the total amount of slides.
Use the `useDeck` hook to get the presentation state in your custom `Provider` component.
```js
// Example Provider.js
// example Provider.js
import React from 'react'
import { useDeck } from 'mdx-deck'
function AtTheBottomCenter ({ children }) {
const css = {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
color: #ffffff;
textAlign: 'center',
}
export default props => {
const state = useDeck()
return <div css={css}>
{children}
return (
<div>
{props.children}
</div>
}
export function Provider ({ children, ...props }) {
return <>
{children}
<AtTheBottomCenter>{props.index}/{props.slides.length}</AtTheBottomCenter>
</>
)
}
```
## Combining multiple mdx files
Unlike the official `@mdx-js/loader`,
the `@mdx-deck/loader` exports an additional `slides` array of components instead of just the entire document.
Multiple MDX files can be combined into a single presentation if the filesize is getting difficult to manage.
First create a couple `.mdx` files like any other MDX Deck file, with `---` to separate the different slides.
```mdx
# one.mdx
---
This is the first file
```
```mdx
# two.mdx
---
This is the second file
```
Next, create a `.js` file to import and combine the two `.mdx` files.
```js
// deck.js
// if you want to include a theme, you would export here:
// export { dark as theme } from 'mdx-deck/themes';
import { slides as one } from './one.mdx'
import { slides as two } from './two.mdx'
export const slides = [...one, ...two]
```
Then, point the MDX Deck CLI comment in your `package.json` to the `deck.js` file.
```json
"scripts": {
"start": "mdx-deck deck.js"
}
```
## Custom webpack config
Webpack configuration files named `webpack.config.js` will automatically be merged with the built-in configuration,
using [webpack-merge](https://github.com/survivejs/webpack-merge).
To use a custom filename, pass the file path to the `--webpack` flag.
```js
// webpack.config.js example
module.exports = {
module: {
rules: [
{
test: /\.svg$/,
use: [{ loader: 'babel-loader' }, { loader: 'react-svg-loader' }],
},
],
},
}
```
**Careful**: When overwriting the loader for `mdx` files, make sure to include the default loader from `@mdx-deck/loader`.
## Custom Components
To build custom components that hook into internal MDX Deck state, you might want to use the following APIs:
- [useSteps](api.md#usesteps-hook)
- [useDeck](api.md#usedeck-hook)
[mdx]: https://mdxjs.com
[mdxprovider]: https://github.com/mdx-js/mdx/blob/master/docs/getting-started/index.md#mdxprovider

View File

@ -1,62 +1,39 @@
# API
MDX Deck consists of several different packages. The core `mdx-deck` package includes the CLI, `@mdx-deck/components`,
`@mdx-deck/themes`, and `@mdx-deck/layouts`.
The core `mdx-deck` package is a wrapper around the Gatsby CLI with the `gatsby-theme-mdx-deck` package.
## Components
See the [components docs](components.md) for details.
- `Head`: Adds elements to the document `<head>`
- `Notes`: Adds speaker notes to a slide
- `Appear`: Steps through child elements one-by-one
- `Embed`: Embed MDX Deck slides in other React applications
- `MDXDeck`
- `MDXDeckState`
- `Head`
- `Image`
- `Notes`
- `Steps`
- `Appear`
- `Slide`
- `Zoom`
- `Embed`
See [Components](components.md) for more info.
## Layouts
See the [layouts docs](layouts.md) for details.
- `Image`: A fullscreen background image layout component
- `Invert`: Inverts the foreground and background colors of the slide
- `Split`: Renders the first element on the left and other elements to the right
- `SplitRight`: Renders the first element on the right and other elements to the left
- `Horizontal`: Renders all child elements side-by-side
- `FullScreenCode`: Renders a single child code block fullscreen
- `Invert`
- `Split`
- `SplitRight`
- `FullScreenCode`
- `Horizontal`
See [Layouts](layouts.md) for more info.
## Themes
## Hooks
See the [themes](themes.md) & [theming](theming.md) docs for details.
### `useSteps`
## Context
MDX Deck uses a stateful React context for each slide.
Use the context APIs with caution as they are less stable than the end-user MDX Deck API.
- `metadata` (object) object for storing slide metadata such as speaker notes and step count
- `step` (number) the current step index
- `mode` (string) the current application mode
- `modes` (object) object of application modes
- `update` (function) updates application state
- `register` (function) registers slide metadata
- `index` (number) the current slide index
- `goto` (function) function to navigate to a specific slide
- `previous` (function) navigate to the previous slide or step
- `next` (function) navigate to the next slide or step
## `useSteps` Hook
The `useSteps` hook can be used to register custom components that rely on steps, similar to the Appear component.
It takes one argument for the total length of steps and returns the current step state.
The `useSteps` hook can be used to register custom components that rely on multiple "steps" within a single slide,
similar to the Appear component.
The hook takes one argument for the total `length` of steps and returns the current `step` state.
```jsx
// example
import React from 'react'
import { useSteps } from '@mdx-deck/components'
import { useSteps } from 'mdx-deck'
export default props => {
const length = 4
@ -70,13 +47,22 @@ export default props => {
}
```
## `useDeck` Hook
## `useDeck`
The `useDeck` component can be used to hook into MDX Deck state.
It returns the [app context](#context) and can be used in a custom [Provider component][] or other custom components.
The `useDeck` hook returns the MDX Deck state, including:
- `setState`
- `mode`
- `index`
- `length`
- `step`
- `metadata`
- `steps`
- `notes`
- `slug`
```jsx
// example custom Provider
// example
import React from 'react'
import { useDeck } from '@mdx-deck/components'
@ -86,14 +72,7 @@ export default props => {
return (
<div>
{props.children}
<div
css={{
position: 'fixed',
right: 0,
bottom: 0,
margin: 16,
}}
>
<div>
Slide {state.index + 1}/{state.length}
</div>
</div>
@ -101,20 +80,10 @@ export default props => {
}
```
## `useTheme` Hook
## CLI Options
The `useTheme` hook returns the current [theme](theming.md).
```jsx
// example
import React from 'react'
import { useTheme } from '@mdx-deck/components'
export default props => {
const theme = useTheme()
return <h2 style={{ border: `1px solid ${theme.colors.text}` }}>Hello</h2>
}
```
[provider component]: advanced.md#custom-provider-component
-p --port Dev server port
-h --host Host the dev server listens to
--no-open Prevent from opening in default browser
```

View File

@ -1,8 +0,0 @@
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
.BundleAnalyzerPlugin
console.log('bundle analyzer')
module.exports = {
plugins: [new BundleAnalyzerPlugin()],
}

View File

@ -1,10 +1,10 @@
# Components
MDX Deck includes a few built-in components to help with creating presentations.
MDX Deck includes components to help with creating presentations.
## Head
Use the `<Head />` component to set content in the document head.
Use the `Head` component to set content in the document head.
```mdx
// example for twitter cards
@ -22,7 +22,7 @@ import { Head } from 'mdx-deck'
## Image
Use the `<Image />` component to render a fullscreen image (using the CSS `background-image` property).
Use the `Image` component to render a fullscreen image with the CSS `background-image` property.
```mdx
import { Image } from 'mdx-deck'
@ -30,117 +30,58 @@ import { Image } from 'mdx-deck'
<Image src="kitten.png" />
```
### Props
```mdx
import { Image } from 'mdx-deck'
<Image src='kittens.png'>
# Kittens
</Image>
```
**Props**
- `src` (string) image URL
- `size` (string) CSS background-size
## Appear
Use the `<Appear />` component to make its children appear one at a time within a single slide.
Use the `Appear` component to make child elements appear one at a time within a single slide.
Use the left and right arrow keys to step through each element.
```mdx
import { Appear } from 'mdx-deck'
<ul>
<Appear>
<li>One</li>
<Appear>
<li>Two</li>
<li>Three</li>
<li>Four</li>
</Appear>
</ul>
```
Internally, the `<Appear />` component uses the `<Step />` component, which can be used to build custom components with similar behavior.
Internally, the `Appear` component uses the `useSteps` hook, which can be used to build custom components with similar behavior.
## Speaker Notes
## Notes
Speaker notes that only show in presenter mode can be added to any slide with the Notes component.
Speaker notes that only show in presenter mode can be added to any slide with the `Notes` component.
```mdx
import { Notes } from 'mdx-deck'
# Slide Content
<Notes>Only visible in presenter mode</Notes>
<Notes>
- Only visible in presenter mode
- Markdown syntax can be used with empty lines around the content
</Notes>
```
## Layouts
MDX Deck includes a few built-in layouts for common slide variations.
Export a layout as the `default` within a slide to wrap the contents.
### Invert
Inverts the foreground and background colors from the theme.
```mdx
import { Invert } from 'mdx-deck/layouts'
# Normal
---
<Invert>
# Inverted
</Invert>
```
### Split
Creates a horizontal layout with the first child on the left and all other children on the right.
```mdx
import { Split } from 'mdx-deck/layouts'
<Split>
![](kitten.png)
## Meow
</Split>
```
### SplitRight
Same as the Split component, but renders the first child on the right.
```mdx
import { SplitRight } from 'mdx-deck/layouts'
<SplitRight>
![](kitten.png)
## Meow
</SplitRight>
```
### Horizontal
Similar to the Split components, but renders all children side-by-side
### FullScreenCode
Render fenced code blocks fullscreen.
````mdx
import { FullScreenCode } from 'mdx-deck/layouts'
<FullScreenCode>
```jsx
<Button>Beep</Button>
```
</FullScreenCode>
````
## Embed
**Experimental**
@ -150,7 +91,7 @@ This can be used to embed slide previews in other places, like a blog post write
```jsx
import React from 'react'
import { Embed } from '@mdx-deck/components'
import { Embed } from 'mdx-deck'
import deck from './my-deck.mdx'
export default props => (

View File

@ -1,14 +1,11 @@
import { Head, Image, Notes, Appear } from '@mdx-deck/components'
import { Invert, Split, SplitRight, FullScreenCode, Horizontal} from '@mdx-deck/layouts'
import {
Head, Image, Notes, Appear,
Invert, Split, SplitRight, FullScreenCode, Horizontal
} from 'mdx-deck'
import Counter from './Counter'
import { future } from '@mdx-deck/themes'
import future from '@mdx-deck/themes/future'
import aspect from '@mdx-deck/themes/aspect'
export const themes = [
future,
aspect,
]
export const themes = [ future ]
<Head>
<title>mdx-deck</title>
@ -33,15 +30,10 @@ MDX-based presentation decks
[MDX]: https://github.com/mdx-js/mdx
---
import { Box } from '@rebass/emotion'
<Box
fontSize={[ 6, 7 ]}
p={4}
color='navy'
bg='magenta'>
Import React components
</Box>
### Import and Use React Components
<Counter />
---
@ -112,7 +104,7 @@ class extends React.Component {
---
<Image
src='https://source.unsplash.com/random/768x2048'
src='https://source.unsplash.com/random/768x2048?new_york'
size='contain'
/>
@ -122,19 +114,13 @@ Testing object fit
---
### Real React Components
<Counter />
---
<Image src='https://images.unsplash.com/photo-1462331940025-496dfbfc7564?w=2048&q=20' />
<Image src='https://images.unsplash.com/photo-1462331940025-496dfbfc7564?new_york&w=2048&q=20' />
---
<Split>
![](https://images.unsplash.com/photo-1462331940025-496dfbfc7564?w=2048&q=20)
![](https://images.unsplash.com/photo-1462331940025-496dfbfc7564?new_york&w=2048&q=20)
## Split Layout
@ -144,7 +130,7 @@ Testing object fit
<SplitRight>
![](https://images.unsplash.com/photo-1462331940025-496dfbfc7564?w=2048&q=20)
![](https://images.unsplash.com/photo-1462331940025-496dfbfc7564?new_york&w=2048&q=20)
## Split Layout
@ -186,7 +172,7 @@ Testing object fit
---
![](https://images.unsplash.com/photo-1462331940025-496dfbfc7564?w=2048&h=1024&q=20&fit=crop)
![](https://images.unsplash.com/photo-1462331940025-496dfbfc7564?new_york&w=2048&h=1024&q=20&fit=crop)
Inline image

View File

@ -1,6 +1,6 @@
# Exporting
## Static Bundle
## Static Build
To export your deck as a static HTML page with JS bundle,
add a `build` script to your `package.json` file.
@ -11,29 +11,28 @@ add a `build` script to your `package.json` file.
}
```
### PDF & Screenshots
## PDF
To export a deck as PDF or create a PNG screenshot, install the export CLI package:
To export a deck as PDF, use the [`website-pdf`](https://www.npmjs.com/package/website-pdf) CLI.
Start the MDX Deck dev server,
then run the following command to create a PDF:
```sh
npm i @mdx-deck/export
npx website-pdf http://localhost:8000/print -o deck.pdf
```
Then run the following command to create a PDF:
## PNG
To export a PNG image, use the [`capture-website-cli`](https://github.com/sindresorhus/capture-website-cli) CLI.
Start the dev server, then run the following:
```sh
mdx-deck-export pdf deck.mdx
npx capture-website-cli http://localhost:8000 deck.png
```
Or export the first slide as a PNG:
## Open Graph Image
```sh
mdx-deck-export png deck.mdx
```
### OG Image
To use the image as an open graph image, use the [Head](components.md#Head) component to add a meta tag.
To add an open graph image, use the [Head](components.md#Head) component to add a meta tag.
Note that the meta tag should point to a full URL, including schema and domain name.
```mdx

51
docs/gatsby.md Normal file
View File

@ -0,0 +1,51 @@
# Usage with Gatsby
The core MDX Deck application is built as a Gatsby theme.
This means you can install MDX Deck as a theme within an existing Gatsby site to include presentations along with other content, such as a landing page or blog.
The theme also supports adding multiple presentations to a single site.
Install the theme in your Gatsby site.
```sh
npm i gatsby-theme-mdx-deck
```
Add the theme to the `plugins` array in your configuration.
```js
// gatsby-config.js
module.exports = {
plugins: [
'gatsby-theme-mdx-deck',
]
}
```
Create a directory to store your presentations.
```sh
mkdir decks
```
Add MDX Deck presentations to this directory.
Each deck will create a new page using the filename as its route.
```mdx
<!-- decks/hello.mdx -->
# Hello
---
This is my presentation
```
After running `gatsby develop`, this presentation should be viewable at `http://localhost:8000/hello` .
## Component Shadowing
Because MDX Deck is built as a Gatsby theme, you can leverage the component shadowing API to override any part of the interface
and create child themes based on MDX Deck that provide custom behavior.
See the [gatsby-theme-mdx-deck](../packages/gatsby-theme) docs for more documentation and options.

View File

@ -1,7 +1,7 @@
# Layouts
Each slide can include a custom layout around its content.
This can be used as a substitute for slide templates found in other presentation apps and libraries.
This is a way to provide *templates* for certain slides.
```js
// example Layout.js
@ -41,6 +41,75 @@ which means you can use a nested ThemeProvider or target elements with CSS-in-JS
## Built-in Layouts
mdx-deck includes a few built-in layouts for common slide variations.
MDX Deck includes a few built-in layouts for common slide variations.
### Invert
Inverts the foreground and background colors from the theme.
```mdx
import { Invert } from 'mdx-deck/layouts'
# Normal
---
<Invert>
# Inverted
</Invert>
```
### Split
Creates a horizontal layout with the first child on the left and all other children on the right.
```mdx
import { Split } from 'mdx-deck/layouts'
<Split>
![](kitten.png)
## Meow
</Split>
```
### SplitRight
Same as the Split component, but renders the first child on the right.
```mdx
import { SplitRight } from 'mdx-deck/layouts'
<SplitRight>
![](kitten.png)
## Meow
</SplitRight>
```
### Horizontal
Similar to the Split components, but renders all children side-by-side
### FullScreenCode
Renders code blocks fullscreen.
````mdx
import { FullScreenCode } from 'mdx-deck/layouts'
<FullScreenCode>
```jsx
<Button>Beep</Button>
```
</FullScreenCode>
````
See the [Components docs](components.md#layouts) for more.

View File

@ -7,20 +7,13 @@
"license": "MIT",
"scripts": {
"start": "mdx-deck demo.mdx",
"build": "mdx-deck build demo.mdx",
"now-build": "yarn build"
"build": "mdx-deck build demo.mdx"
},
"dependencies": {
"@emotion/core": "^10.0.7",
"@emotion/styled": "^10.0.7",
"@mdx-deck/components": "^2.5.0",
"@mdx-deck/layouts": "^2.5.0",
"@mdx-deck/themes": "^2.5.0",
"@rebass/emotion": "^3.0.0",
"mdx-deck": "^2.5.1",
"rebass": "^3.0.1"
},
"devDependencies": {
"webpack-bundle-analyzer": "^3.3.2"
"mdx-deck": "^2.5.0",
"styled-system": "^5.0.15"
}
}

23
docs/presenting.md Normal file
View File

@ -0,0 +1,23 @@
# Presenting
1. Enter presenter mode by pressing `Opt + P`
2. Click the link at the bottom to open the presentation in another tab
3. Move the new tab to its own window in the screen or projector that the audience sees
4. Control the presentation from the original window
5. Be sure to hide your mouse cursor so that it's not visible to the audience
## Speaker Notes
Notes that only show in presenter mode can be added to any slide
by using the `<Notes />` component.
```mdx
import { Notes } from 'mdx-deck'
# Slide Content
<Notes>
Only visible in presenter mode
</Notes>
```

View File

@ -1,128 +1,105 @@
# Themes
![](images/default.png)
Default
```sh
npm i @mdx-deck/themes
```
![Default theme](images/default.png)
---
![Big theme](images/big.png)
```js
export { default as theme } from '@mdx-deck/themes'
import { big } from '@mdx-deck/themes'
```
---
![](images/big.png)
Big
![Book theme](images/book.png)
```js
export { big as theme } from '@mdx-deck/themes'
import { book } from '@mdx-deck/themes'
```
---
![](images/book.png)
Book
![Code theme](images/code.png)
```js
export { book as theme } from '@mdx-deck/themes'
import { code } from '@mdx-deck/themes'
```
---
![](images/code.png)
Code
![Comic theme](images/comic.png)
```js
export { code as theme } from '@mdx-deck/themes'
import { comic } from '@mdx-deck/themes'
```
---
![](images/comic.png)
Comic
![Condensed theme](images/condensed.png)
```js
export { comic as theme } from '@mdx-deck/themes'
import { condensed } from '@mdx-deck/themes'
```
---
![](images/condensed.png)
Condensed
![Dark theme](images/dark.png)
```js
export { condensed as theme } from '@mdx-deck/themes'
import { dark } from '@mdx-deck/themes'
```
---
![](images/dark.png)
Dark
![Future theme](images/future.png)
```js
export { dark as theme } from '@mdx-deck/themes'
import { future } from '@mdx-deck/themes'
```
---
![](images/future.png)
Future
![Hack theme](images/hack.png)
```js
export { future as theme } from '@mdx-deck/themes'
import { hack } from '@mdx-deck/themes'
```
---
![](images/hack.png)
Hack
![Notes theme](images/notes.png)
```js
export { hack as theme } from '@mdx-deck/themes'
import { notes } from '@mdx-deck/themes'
```
---
<!--
![](images/lobster.png)
Lobster
-->
![](images/notes.png)
Notes
![Script theme](images/script.png)
```js
export { notes as theme } from '@mdx-deck/themes'
import { script } from '@mdx-deck/themes'
```
---
<!--
![](images/rye.png)
Rye
-->
![](images/script.png)
Script
![Swiss theme](images/swiss.png)
```js
export { script as theme } from '@mdx-deck/themes'
import { swiss } from '@mdx-deck/themes'
```
---
![](images/swiss.png)
Swiss
![Yellow theme](images/yellow.png)
```js
export { swiss as theme } from '@mdx-deck/themes'
```
---
![](images/yellow.png)
Yellow
```js
export { yellow as theme } from '@mdx-deck/themes'
import { yellow } from '@mdx-deck/themes'
```
---
@ -130,37 +107,15 @@ export { yellow as theme } from '@mdx-deck/themes'
Poppins
```js
export { poppins as theme } from '@mdx-deck/themes'
import { poppins } from '@mdx-deck/themes'
```
---
Syntax Highlighter
Syntax Highlighting
```js
export { syntaxHighlighter as theme } from '@mdx-deck/themes'
import { highlight } from '@mdx-deck/themes'
import { prism } from '@mdx-deck/themes'
```
---
Syntax Highlighter Prism
```js
export { syntaxHighlighterPrism as theme } from '@mdx-deck/themes'
```
---
Aspect 16:9
```js
export { aspect as theme } from '@mdx-deck/themes'
```
---
Aspect 4:3
```js
export { aspect43 as theme } from '@mdx-deck/themes'
```

View File

@ -1,52 +1,57 @@
# Theming
mdx-deck uses [emotion][] for styling, making practically any part of the presentation themeable.
MDX Deck uses [Theme UI][] and [Emotion][] for styling, making practically any part of the presentation themeable.
## Built-in Themes
mdx-deck includes several built-in themes to change the look and feel of the presentation.
MDX Deck includes several built-in themes to change the look and feel of the presentation.
Export `theme` from your MDX file to enable a theme.
```mdx
export { dark as theme } from 'mdx-deck/themes'
import { themes } from 'mdx-deck'
export const theme = themes.dark
# Dark Theme
```
View the [List of Themes](themes.md).
View the [Themes](themes.md) docs to see all available themes.
## Custom Themes
A custom theme can be provided by exporting `theme` from the MDX file.
```mdx
export { default as theme } from './theme'
import myTheme from './theme'
export const theme = myTheme
# Hello
```
The theme should be an object with fields for fonts, colors, and CSS for individual components.
Themes are based on [Theme UI][] and support customizing typography, color, layout, and other element styles.
```js
// Example theme.js
export default {
// add a custom font
font: 'Roboto, sans-serif',
// custom colors
fonts: {
body: 'Roboto, sans-serif',
monospace: '"Roboto Mono", monospace',
},
colors: {
text: '#f0f',
text: 'white',
background: 'black',
primary: 'blue',
},
}
```
## Composing Themes
Multiple themes can be used together.
For example, this allows the use of a syntax highlighting theme,
along with a color theme, and a separate typography theme.
Multiple themes can be composed together,
allowing you to create separate themes for typography, color, and components, and mix and match them as needed.
To compose themes together export a `themes` array instead of a single theme.
To compose multiple themes together, export a `themes` array instead of a single theme.
```mdx
import { syntaxHighlighter } from 'mdx-deck/themes'
@ -57,30 +62,44 @@ export const themes = [syntaxHighlighter, customTheme]
# Cool. :sunglasses:
```
Please note that themes are deep merged together and the last theme specified will override fields from themes before it.
Note that themes are deeply merged together and the last theme specified will override fields from themes before it.
### Google Fonts
## Google Fonts
Themes can specify a `googleFont` field to automatically add a `<link>` tag to the document head.
Alternatively, use the `<Head />` component to add a custom `<link>` tag.
### Syntax Highlighting
## Syntax Highlighting
By default fenced code blocks do not include any syntax highlighting.
Themes can provide a set of custom MDX components, including a replacement for the default `code` component that can add syntax highlighting with libraries like [react-syntax-highlighter][].
MDX Deck includes two themes for adding syntax highlighting with [react-syntax-highlighter][]: `syntaxHighlighter` and `syntaxHighlighterPrism`.
MDX Deck includes two themes for adding syntax highlighting with [react-syntax-highlighter][]: `highlight` and `prism`.
```mdx
import { prism } from 'mdx-deck/themes'
export const themes = [ prism ]
```
Since MDX supports using React components inline, you can also import a syntax highlighting component directly, if you prefer.
### Styling Elements
```mdx
import Highlighter from 'react-syntax-highlighter'
Each element can be styled with a theme.
Add a style object (or string) to the theme to target specific elements.
<Highlighter language='javascript'>
{`export const hello = 'hi'`}
</Highlighter>
```
## Styling Elements
Add a `theme.styles` object to style specific markdown elements.
```js
// example theme
export default {
styles: {
h1: {
textTransform: 'uppercase',
letterSpacing: '0.1em',
@ -89,49 +108,25 @@ export default {
fontStyle: 'italic',
},
}
}
```
See the [reference](#reference) below for a full list of element keys.
## Reference
The following keys are available for theming:
- `font`: base font family
- `monospace`: font family for `<pre>` and `<code>`
- `colors`: object of colors used for MDX components
- `text`: root foreground color
- `background`: root background color
- `code`: text color for `<pre>` and `<code>`
- `codeBackground`: background color for `<pre>` and `<code>`
- `css`: root CSS object
- `heading`: CSS for all headings
- `h1`: CSS for `<h1>`
- `h2`: CSS for `<h2>`
- `h3`: CSS for `<h3>`
- `h4`: CSS for `<h4>`
- `h5`: CSS for `<h5>`
- `h6`: CSS for `<h6>`
- `p`: CSS for `<p>`
- `a`: CSS for `<a>`
- `ul`: CSS for `<ul>`
- `ol`: CSS for `<ol>`
- `li`: CSS for `<li>`
- `img`: CSS for `<img>`
- `blockquote`: CSS for `<blockquote>`
- `table`: CSS for `<table>`
- `pre`: CSS for `<pre>`
- `code`: CSS for `<code>`
- `Slide`: CSS to apply to the wrapping Slide component
- `components`: object of MDX components to render markdown
- `Provider`: component for wrapping the entire app
- `Presenter`: component for wrapping the presenter mode
- `googleFont`: CSS HREF for adding a Google Font `<link>` tag
## Advanced Usage
For more advanced customizations see the [Advanced Usage](advanced.md) docs.
- `primary`: primary color
- `fonts.body`: base font family
- `fonts.heading`: heading font family
- `fonts.monospace`: font family for `<pre>` and `<code>`
- `text.heading`: styles for all headings
- `styles.Slide`: styles for the wrapping Slide component
- `components`: object of MDX components
- `Provider`: component for wrapping the entire presentation
- `googleFont`: Stylesheet URL for adding a Google Font
[emotion]: https://emotion.sh
[theme ui]: https://theme-ui.com
[mdx]: https://github.com/mdx-js/mdx
[react-syntax-highlighter]: https://github.com/conorhastings/react-syntax-highlighter

View File

@ -1,3 +1,3 @@
[build]
command = "npm i yarn@latest && yarn && yarn build"
publish = "docs/dist"
publish = "docs/public"

View File

@ -8,22 +8,21 @@
],
"scripts": {
"start": "yarn workspace @mdx-deck/docs start",
"analyze-bundle": "yarn workspace @mdx-deck/docs start --webpack bundle-analyzer.config.js",
"build": "yarn workspace @mdx-deck/docs build",
"start-theme": "yarn workspace @mdx-deck/gatsby-theme start",
"build-theme": "yarn workspace @mdx-deck/gatsby-theme build",
"export": "yarn workspace @mdx-deck/export pdf",
"test": "jest"
"export": "./packages/export/cli.js http://localhost:8000/print -o docs/public/deck.pdf",
"cypress:open": "cypress open",
"cypress:run": "cypress run",
"test:dev": "start-server-and-test start http://localhost:8000 cypress:open",
"test": "start-server-and-test start http://localhost:8000 cypress:run"
},
"devDependencies": {
"@babel/core": "^7.3.4",
"@babel/preset-env": "^7.3.4",
"@babel/preset-react": "^7.0.0",
"cypress": "^3.4.0",
"husky": "^3.0.0",
"jest": "^24.3.1",
"lerna": "^3.13.1",
"lint-staged": "^9.1.0",
"prettier": "^1.16.4"
"prettier": "^1.16.4",
"start-server-and-test": "^1.9.1"
},
"jest": {
"coverageReporters": [
@ -46,8 +45,5 @@
"prettier --write",
"git add"
]
},
"dependencies": {
"@mdx-js/mdx": "^1.0.1"
}
}

View File

@ -1,5 +0,0 @@
# @mdx-deck/components
React components use in MDX Deck
https://github.com/jxnblk/mdx-deck

View File

@ -1,27 +0,0 @@
{
"name": "@mdx-deck/components",
"version": "2.5.0",
"main": "src/index.js",
"author": "Brent Jackson <jxnblk@gmail.com>",
"license": "MIT",
"dependencies": {
"@emotion/core": "^10.0.7",
"@emotion/styled": "^10.0.7",
"@mdx-js/react": "^1.0.1",
"@reach/router": "^1.2.1",
"emotion-theming": "^10.0.7",
"hhmmss": "^1.0.0",
"lodash.merge": "^4.6.1",
"react-swipeable": "^5.0.1",
"resize-observer-polyfill": "^1.5.1"
},
"peerDependencies": {
"@mdx-deck/themes": "^2.0.0-0"
},
"devDependencies": {
"react": "^16.8.3",
"react-dom": "^16.8.3",
"react-test-renderer": "^16.8.4",
"react-testing-library": "^6.1.2"
}
}

View File

@ -1,21 +0,0 @@
import React from 'react'
import useSteps from './useSteps'
export const Appear = props => {
const arr = React.Children.toArray(props.children)
const step = useSteps(arr.length)
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

@ -1,51 +0,0 @@
import React from 'react'
import styled from '@emotion/styled'
import FluidFontSize from './FluidFontSize'
import useTheme from './useTheme'
const getPadding = ratio =>
ratio > 1 ? (1 / ratio) * 100 + '%' : ratio * 100 + '%'
const paddingBottom = props => ({
paddingBottom: getPadding(props.theme.aspectRatio),
})
const Outer = styled('div')(
{
width: '100%',
height: 0,
margin: 'auto',
position: 'relative',
},
paddingBottom
)
const Inner = styled.div(
{
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
},
props => props.theme.Slide
)
export default props => {
const theme = useTheme()
if (!theme.aspectRatio) {
return <>{props.children}</>
}
return (
<Outer>
<FluidFontSize base={10}>
<Inner>{props.children}</Inner>
</FluidFontSize>
</Outer>
)
}

View File

@ -1,25 +0,0 @@
import React from 'react'
export class Catch extends React.Component {
state = {
err: null,
}
componentDidCatch(err) {
this.setState({ err })
}
componentDidUpdate() {
if (!this.state.err) return
this.setState({ err: null })
}
render() {
if (this.state.err) {
return <pre children={this.state.err.toString()} />
}
return <>{this.props.children}</>
}
}
export default Catch

View File

@ -1,57 +0,0 @@
import React, { useState, useEffect } from 'react'
import hhmmss from 'hhmmss'
import Pre from './Pre'
const spacer = <div style={{ margin: 4 }} />
let timer
const Clock = props => {
const [time, setTime] = useState(new Date().toLocaleTimeString())
const [seconds, setSeconds] = useState(0)
const [on, setTimer] = useState(false)
useEffect(() => {
const tick = () => {
const now = new Date()
setTime(now.toLocaleTimeString())
if (on) setSeconds(seconds + 1)
}
timer = setInterval(tick, 1000)
return () => {
clearInterval(timer)
}
})
return (
<div
style={{
display: 'flex',
alignItems: 'center',
}}
>
<button
disabled={!seconds || on}
onClick={e => {
setSeconds(0)
}}
>
Reset
</button>
{spacer}
<button
onClick={e => {
setTimer(!on)
}}
>
{on ? 'Pause' : 'Start'}
</button>
{spacer}
<Pre>
{hhmmss(seconds)} | {time}
</Pre>
</div>
)
}
export default Clock

View File

@ -1,85 +0,0 @@
/** @jsx jsx */
// experimental component for embedding MDX Decks
// in other React applications
/*
*
* import React from 'react'
* import { DeckEmbed } from '@mdx-deck/components'
* import deck from './my-deck.mdx'
*
* export default props =>
* <>
* <h1>The first slide</h1>
* <DeckEmbed src={deck} />
* <h1>The second slide</h1>
* <DeckEmbed src={deck} slide={2} />
* </>
*
*/
import { jsx } from '@emotion/core'
import Provider from './Provider'
import Slide from './Slide'
import GoogleFonts from './GoogleFonts'
import splitSlides from './splitSlides'
// fix for regression in gatsby-theme
import merge from 'lodash.merge'
import defaultTheme from '@mdx-deck/themes/base'
const Placeholder = ({ index }) => (
<pre style={{ fontSize: 16 }}>not found: slide {index}</pre>
)
// fix for regression in gatsby-theme
const mergeThemes = themes =>
themes.reduce(
(acc, theme) =>
typeof theme === 'function' ? theme(acc) : merge(acc, theme),
{}
)
const wrapper = props => {
const { slides, theme: baseTheme, themes, ratio, zoom } = splitSlides(props)
// fix for regression in gatsby-theme
const theme = mergeThemes([
defaultTheme,
baseTheme,
...themes,
{
aspectRatio: ratio,
Slide: {
maxWidth: '100%',
height: 'auto',
},
},
])
const Content = slides[props.slide - 1] || Placeholder
return (
<Provider theme={theme}>
<GoogleFonts />
<Slide zoom={zoom}>
<Content index={props.slide - 1} />
</Slide>
</Provider>
)
}
export const Embed = ({
src: Deck,
slide = 1,
ratio = 16 / 9,
zoom = 1,
...props
}) => (
<Deck
{...props}
components={{ wrapper }}
slide={slide}
ratio={ratio}
zoom={zoom}
/>
)
export default Embed

View File

@ -1,31 +0,0 @@
// prototype for fluid resizable font size
import React, { useLayoutEffect, useRef, useState } from 'react'
import ResizeObserver from 'resize-observer-polyfill'
export const FluidFontSize = ({ base = 16, children, className }) => {
const div = useRef(null)
const [fontSize, setFontSize] = useState(base)
useLayoutEffect(() => {
const observer = new ResizeObserver(entries => {
entries.forEach(entry => {
if (entry.target !== div.current) return
const { width } = entry.contentRect
const ratio = width / 320
const next = Math.floor(ratio * base)
setFontSize(next)
})
})
observer.observe(div.current)
return () => observer.unobserve(div.current)
}, [base])
return (
<div ref={div} className={className} style={{ fontSize }}>
{children}
</div>
)
}
export default FluidFontSize

View File

@ -1,14 +0,0 @@
import React from 'react'
import { withTheme } from 'emotion-theming'
import { Head } from './Head'
const GoogleFonts = withTheme(({ theme }) => {
if (!theme.googleFont) return false
return (
<Head portal>
<link rel="stylesheet" href={theme.googleFont} />
</Head>
)
})
export default GoogleFonts

View File

@ -1,66 +0,0 @@
import React, { useEffect } from 'react'
import Zoom from './Zoom'
import Slide from './Slide'
export const Grid = props => {
const { index, slides, modes, update, goto } = props
const activeThumb = React.createRef()
useEffect(() => {
const el = activeThumb.current
if (!el) return
if (typeof el.scrollIntoViewIfNeeded === 'function') {
el.scrollIntoViewIfNeeded()
}
})
return (
<div
style={{
height: '100vh',
overflowY: 'auto',
color: 'white',
backgroundColor: 'black',
}}
>
<div
style={{
display: 'flex',
justifyContent: 'flex-start',
flexWrap: 'wrap',
}}
>
{slides.map((Component, i) => (
<div
ref={i === index ? activeThumb : null}
key={i}
role="link"
onClick={e => {
goto(i)
update({ mode: modes.NORMAL })
}}
style={{
display: 'block',
width: 'calc(25vw - 4px)',
height: 'calc(25vh - 4px)',
margin: '2px',
overflow: 'hidden',
color: 'inherit',
textDecoration: 'none',
cursor: 'pointer',
outline: i === index ? '4px solid #0cf' : null,
}}
>
<Zoom zoom={1 / 4}>
<Slide>
<Component />
</Slide>
</Zoom>
</div>
))}
</div>
</div>
)
}
export default Grid

View File

@ -1,60 +0,0 @@
import React, { useEffect, useContext } from 'react'
import { createPortal } from 'react-dom'
let didMount = false
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>
}
// get head for all slides
export const UserHead = ({ mdx }) =>
!!mdx &&
React.createElement(mdx, {
components: {
wrapper: props => {
if (!didMount) return false
const heads = React.Children.toArray(props.children).filter(
child => child.props.originalType === Head
)
const head = React.Children.toArray(
heads.reduce(
(acc, head) => [
...acc,
...React.Children.toArray(head.props.children),
],
[]
)
)
return createPortal(head, document.head)
},
},
})
export const Head = props => {
const { push } = useContext(HeadContext)
const children = React.Children.toArray(props.children)
useEffect(() => {
didMount = true
}, [])
if (!didMount) {
push(children)
return false
}
if (!props.portal) return false
return createPortal(children, document.head)
}
export default Head

View File

@ -1,22 +0,0 @@
import styled from '@emotion/styled'
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

@ -1,81 +0,0 @@
import { useEffect } from 'react'
import { globalHistory, navigate } from '@reach/router'
const keys = {
right: 39,
left: 37,
space: 32,
p: 80,
o: 79,
g: 71,
pgUp: 33,
pgDown: 34,
}
const inputElements = ['INPUT', 'TEXTAREA', 'A', 'BUTTON']
const toggleMode = key => state => ({
mode: state.mode === key ? 'normal' : key,
})
const handleKeyDown = props => e => {
const { basepath, update, modes } = props
const { keyCode, metaKey, ctrlKey, altKey, shiftKey } = e
const { activeElement } = document
if (inputElements.includes(activeElement.tagName)) {
return
}
if (metaKey || ctrlKey) return
const alt = altKey && !shiftKey
const { pathname } = globalHistory.location
if (keyCode === keys.p && shiftKey && altKey) {
navigate(basepath + '/print')
update({ mode: modes.PRINT })
}
if (pathname === '/print') return
if (alt) {
switch (keyCode) {
case keys.p:
update(toggleMode(modes.PRESENTER))
break
case keys.o:
update(toggleMode(modes.OVERVIEW))
break
case keys.g:
update(toggleMode(modes.GRID))
break
default:
break
}
} else {
switch (keyCode) {
case keys.pgUp:
case keys.left:
e.preventDefault()
props.previous()
break
case keys.pgDown:
case keys.right:
case keys.space:
e.preventDefault()
props.next()
break
default:
break
}
}
}
export default props => {
useEffect(() => {
const handler = handleKeyDown(props)
window.addEventListener('keydown', handler)
return () => {
window.removeEventListener('keydown', handler)
}
}, [props.metadata])
return false
}

View File

@ -1,203 +0,0 @@
import React, { useContext, useReducer } from 'react'
import PropTypes from 'prop-types'
import { Router, globalHistory, navigate } from '@reach/router'
import { Swipeable } from 'react-swipeable'
import merge from 'lodash.merge'
import defaultTheme from '@mdx-deck/themes/base'
import Provider from './Provider'
import Slide from './Slide'
import Presenter from './Presenter'
import Overview from './Overview'
import Grid from './Grid'
import Print from './Print'
import GoogleFonts from './GoogleFonts'
import Catch from './Catch'
import Keyboard from './Keyboard'
import Storage from './Storage'
import QueryString from './QueryString'
import Style from './Style'
const NORMAL = 'normal'
const PRESENTER = 'presenter'
const OVERVIEW = 'overview'
const GRID = 'grid'
const PRINT = 'print'
const modes = {
NORMAL,
PRESENTER,
OVERVIEW,
GRID,
PRINT,
}
const BaseWrapper = props => <>{props.children}</>
const getIndex = ({ basepath }) => {
const { pathname } = globalHistory.location
const root = pathname.replace(basepath, '')
const n = Number(root.split('/')[1])
const index = isNaN(n) ? 0 : n
return index
}
const mergeThemes = themes =>
themes.reduce(
(acc, theme) =>
typeof theme === 'function' ? theme(acc) : merge(acc, theme),
{}
)
const mergeState = (state, next) =>
merge({}, state, typeof next === 'function' ? next(state) : next)
const useState = init => useReducer(mergeState, init)
export const MDXDeckContext = React.createContext()
const useDeckState = () => {
const context = useContext(MDXDeckContext)
if (context) return context
const [state, setState] = useState({
metadata: {},
step: 0,
mode: NORMAL,
})
return {
state,
setState,
}
}
export const MDXDeckState = ({ children }) => {
const context = useDeckState()
return <MDXDeckContext.Provider value={context} children={children} />
}
export const MDXDeck = props => {
const { slides, basepath, theme: baseTheme, themes = [] } = props
const { state, setState } = useDeckState(MDXDeckContext)
const theme = mergeThemes([defaultTheme, baseTheme, ...themes])
const index = getIndex(props)
const getMeta = i => {
return state.metadata[i] || {}
}
const getWrapper = mode => {
switch (mode) {
case PRESENTER:
return theme.Presenter || Presenter
case OVERVIEW:
return Overview
case GRID:
return Grid
default:
return BaseWrapper
}
}
const register = (index, meta) => {
setState({
metadata: {
...state.metadata,
[index]: {
...state.metadata[index],
...meta,
},
},
})
}
const goto = nextIndex => {
const current = getIndex(props)
const reverse = nextIndex < current
const { search } = globalHistory.location
navigate(basepath + '/' + nextIndex + search)
const meta = getMeta(nextIndex)
setState({
step: reverse ? meta.steps || 0 : 0,
})
}
const previous = () => {
const current = getIndex(props)
const meta = getMeta(current)
if (meta.steps && state.step > 0) {
setState({ step: state.step - 1 })
} else {
const p = current - 1
if (p < 0) return
goto(p)
}
}
const next = () => {
const current = getIndex(props)
const meta = getMeta(current)
if (meta.steps && state.step < meta.steps) {
setState({ step: state.step + 1 })
} else {
const n = current + 1
if (n > slides.length - 1) return
goto(n)
}
}
const context = {
...state,
update: setState,
register,
length: slides.length,
modes,
index,
goto,
previous,
next,
}
const [First] = slides
const Wrapper = getWrapper(state.mode)
return (
<Provider {...props} {...context} theme={theme}>
<Catch>
<Style {...context} />
<Keyboard {...props} {...context} />
<Storage {...context} />
<QueryString {...context} />
<GoogleFonts />
<Wrapper {...props} {...context}>
<Swipeable onSwipedRight={previous} onSwipedLeft={next}>
<Router basepath={basepath}>
<Slide path="/" index={0} context={context}>
<First path="/" />
</Slide>
{slides.map((Component, i) => (
<Slide key={i} path={i + '/*'} index={i} context={context}>
<Component path={i + '/*'} />
</Slide>
))}
<Print path="print" {...props} />
</Router>
</Swipeable>
</Wrapper>
</Catch>
</Provider>
)
}
MDXDeck.propTypes = {
slides: PropTypes.array.isRequired,
headTags: PropTypes.array.isRequired,
}
MDXDeck.defaultProps = {
basepath: '',
slides: [],
headTags: [],
}
export default MDXDeck

View File

@ -1,17 +0,0 @@
import { useEffect } from 'react'
import { useDeck } from './context'
export const Notes = props => {
const context = useDeck()
useEffect(() => {
if (!context || !context.register) return
if (typeof context.index === 'undefined') return
context.register(context.index, {
notes: props.children,
})
}, [])
return false
}
export default Notes

View File

@ -1,80 +0,0 @@
import React, { useEffect } from 'react'
import Zoom from './Zoom'
import Slide from './Slide'
import Pre from './Pre'
export const Overview = props => {
const { goto, index, slides } = props
const activeThumb = React.createRef()
useEffect(() => {
const el = activeThumb.current
if (!el) return
if (typeof el.scrollIntoViewIfNeeded === 'function') {
el.scrollIntoViewIfNeeded()
}
})
return (
<div
style={{
display: 'flex',
alignItems: 'flex-start',
height: '100vh',
color: 'white',
backgroundColor: 'black',
}}
>
<div
style={{
flex: 'none',
height: '100vh',
paddingLeft: 4,
paddingRight: 4,
overflowY: 'auto',
marginRight: 'auto',
}}
>
{slides.map((Component, i) => (
<div
ref={i === index ? activeThumb : null}
key={i}
role="link"
onClick={e => {
goto(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>
<Component />
</Slide>
</Zoom>
</div>
))}
</div>
<div
style={{
width: 200 / 3 + '%',
margin: 'auto',
}}
>
<Zoom zoom={2 / 3}>{props.children}</Zoom>
<Pre>
{index} of {slides.length - 1}
</Pre>
</div>
</div>
)
}
export default Overview

View File

@ -1,13 +0,0 @@
import React from 'react'
export default props => (
<pre
{...props}
style={{
fontFamily: 'Menlo, monospace',
fontSize: 18,
whiteSpace: 'pre-wrap',
...props.style,
}}
/>
)

View File

@ -1,86 +0,0 @@
import React from 'react'
import { globalHistory } from '@reach/router'
import Zoom from './Zoom'
import Slide from './Slide'
import Pre from './Pre'
import Clock from './Clock'
export const Presenter = props => {
const { slides, metadata, index } = props
const Next = slides[index + 1]
const { notes } = metadata[index] || {}
return (
<div
style={{
color: 'white',
backgroundColor: 'black',
display: 'flex',
flexDirection: 'column',
height: '100vh',
}}
>
<div
style={{
marginTop: 'auto',
marginBottom: 'auto',
display: 'flex',
}}
>
<div
style={{
width: 500 / 8 + '%',
minWidth: 0,
marginLeft: 'auto',
marginRight: 'auto',
}}
>
<Zoom zoom={5 / 8}>{props.children}</Zoom>
</div>
<div
style={{
width: 100 / 4 + '%',
minWidth: 0,
marginLeft: 'auto',
marginRight: 'auto',
}}
>
<Zoom zoom={1 / 4}>
{Next && (
<Slide>
<Next />
</Slide>
)}
</Zoom>
<Pre>{notes}</Pre>
</div>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
padding: 16,
}}
>
<Pre>
{index} of {slides.length - 1}
</Pre>
<div style={{ margin: 'auto' }} />
<a
target="_blank"
rel="noopener noreferrer"
href={globalHistory.location.origin + globalHistory.location.pathname}
style={{
color: 'inherit',
}}
>
Open New Window
</a>
<div style={{ margin: 'auto' }} />
<Clock />
</div>
</div>
)
}
export default Presenter

View File

@ -1,14 +0,0 @@
import React from 'react'
import Slide from './Slide'
export const Print = props => (
<>
{props.slides.map((Component, i) => (
<Slide key={i} index={i}>
<Component />
</Slide>
))}
</>
)
export default Print

View File

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

View File

@ -1,31 +0,0 @@
import { useEffect } from 'react'
import { globalHistory, navigate } from '@reach/router'
import querystring from 'querystring'
const getQuery = () => {
const query = querystring.parse(
globalHistory.location.search.replace(/^\?/, '')
)
return query
}
export default ({ mode, modes, update, index }) => {
useEffect(() => {
const state = getQuery()
update(state)
}, [])
useEffect(() => {
const { pathname, search } = globalHistory.location
if (mode !== modes.NORMAL && mode !== modes.PRINT) {
const query = '?' + querystring.stringify({ mode })
if (query === search) return
navigate(query)
} else {
if (!search) return
navigate(pathname)
}
}, [mode])
return false
}

View File

@ -1,17 +0,0 @@
import React from 'react'
export const Ratio = ({ ratio, children }) => (
<div
css={{
width: '100%',
height: 0,
paddingBottom: ratio * 100 + '%',
overflow: 'hidden',
position: 'relative',
}}
>
{children}
</div>
)
export default Ratio

View File

@ -1,90 +0,0 @@
import React from 'react'
import styled from '@emotion/styled'
import { Context } from './context'
import AspectRatioSlide from './AspectRatioSlide'
const themed = (...tags) => props =>
tags.map(tag => props.theme[tag] && { ['& ' + tag]: props.theme[tag] })
const themedHeadings = props => ({
'& h1, & h2, & h3, & h4, & h5, & h6': props.theme.heading,
})
const themedLinks = props => ({
'& a': {
color: props.theme.colors.link,
},
})
// backwards compatibility
const themedQuote = props => ({
'& blockquote': props.theme.quote,
})
const themedCode = props => ({
'& code, & pre': {
fontFamily: props.theme.monospace,
color: props.theme.colors.code,
background: props.theme.colors.codeBackground,
},
})
const Root = styled.div(
props => ({
fontFamily: props.theme.font,
color: props.theme.colors.text,
backgroundColor: props.theme.colors.background,
width: '100vw',
height: '100vh',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
}),
props => props.theme.css,
props => props.theme.Slide,
themedLinks,
themedHeadings,
themedCode,
themedQuote,
themed(
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'a',
'ul',
'ol',
'li',
'p',
'blockquote',
'img',
'table',
'pre',
'code'
)
)
export const Slide = ({ index, context, ...props }) => (
<Context.Provider
value={{
index,
...context,
}}
>
<Root>
<AspectRatioSlide {...props} />
</Root>
</Context.Provider>
)
Slide.defaultProps = {
context: {
step: Infinity,
},
}
export default Slide

View File

@ -1,8 +0,0 @@
import useSteps from './useSteps'
export const Steps = props => {
const step = useSteps(props.length)
return props.render({ step })
}
export default Steps

View File

@ -1,56 +0,0 @@
import { useEffect, useState } from 'react'
const STORAGE_INDEX = 'mdx-slide'
const STORAGE_STEP = 'mdx-step'
export const useLocalStorage = (handler, args = []) => {
const [focused, setFocused] = useState(false)
const handleFocus = () => {
setFocused(true)
}
const handleBlur = () => {
setFocused(false)
}
useEffect(() => {
setFocused(document.hasFocus())
if (!focused) window.addEventListener('storage', handler)
window.addEventListener('focus', handleFocus)
window.addEventListener('blur', handleBlur)
return () => {
if (!focused) window.removeEventListener('storage', handler)
window.removeEventListener('focus', handleFocus)
window.removeEventListener('blur', handleBlur)
}
}, [focused, ...args])
}
export const useSetStorage = (key, value) => {
useEffect(() => {
localStorage.setItem(key, value)
}, [key, value])
}
const handleStorageChange = ({ goto, update }) => e => {
const { key } = e
switch (key) {
case STORAGE_INDEX:
const index = parseInt(e.newValue, 10)
goto(index)
break
case STORAGE_STEP:
const step = parseInt(e.newValue, 10)
update({ step })
break
default:
break
}
}
export default ({ goto, update, index, step }) => {
const handler = handleStorageChange({ goto, update })
useLocalStorage(handler)
useSetStorage(STORAGE_INDEX, index)
useSetStorage(STORAGE_STEP, step)
return false
}

View File

@ -1,24 +0,0 @@
import React from 'react'
import { globalHistory } from '@reach/router'
import { Global } from '@emotion/core'
const isPrintPath = () => {
const { pathname } = globalHistory.location
const parts = pathname.split('/')
const path = parts[parts.length - 1]
return path === 'print'
}
export default ({ mode, modes }) => {
if (mode === modes.PRINT) return false
if (isPrintPath()) return false
return (
<Global
styles={{
body: {
overflow: 'hidden',
},
}}
/>
)
}

View File

@ -1,25 +0,0 @@
import React from 'react'
import styled from '@emotion/styled'
const ZoomRoot = styled.div(props => ({
backgroundColor: props.theme.colors.background,
width: `calc(${100 * props.zoom}vw)`,
height: `calc(${100 * props.zoom}vh)`,
}))
const ZoomInner = styled.div([], props => ({
transformOrigin: '0 0',
transform: `scale(${props.zoom})`,
}))
export const Zoom = ({ zoom, ...props }) => (
<ZoomRoot zoom={zoom}>
<ZoomInner zoom={zoom} {...props} />
</ZoomRoot>
)
Zoom.defaultProps = {
zoom: 1,
}
export default Zoom

View File

@ -1,12 +0,0 @@
import React from 'react'
import TestRenderer from 'react-test-renderer'
import Appear from '../Appear'
test('Appear renders', () => {
const json = TestRenderer.create(
<Appear>
<h1>Hello</h1>
</Appear>
).toJSON()
expect(json).toMatchSnapshot()
})

View File

@ -1,5 +0,0 @@
import React from 'react'
import TestRenderer from 'react-test-renderer'
import Clock from '../Clock'
test.todo('Clock renders')

View File

@ -1,5 +0,0 @@
import React from 'react'
import TestRenderer from 'react-test-renderer'
import GoogleFonts from '../GoogleFonts'
test.todo('GoogleFonts renders')

View File

@ -1,15 +0,0 @@
import React from 'react'
import TestRenderer from 'react-test-renderer'
import { Head, HeadProvider } from '../Head'
test.skip('Head populates HeadProviders tag prop', () => {
const tags = []
TestRenderer.create(
<HeadProvider tags={tags}>
<Head>
<title>Hello</title>
</Head>
</HeadProvider>
)
expect(tags.length).toBe(1)
})

View File

@ -1,35 +0,0 @@
import React, { useContext } from 'react'
import { render, cleanup } from 'react-testing-library'
import renderer from 'react-test-renderer'
import { MDXDeckState, MDXDeckContext, MDXDeck } from '../MDXDeck'
afterEach(cleanup)
const slides = [() => <pre>one</pre>, () => <pre>two</pre>]
describe('MDXDeckState', () => {
test('provides state', () => {
let context
const Consumer = props => {
context = useContext(MDXDeckContext)
return false
}
const deck = renderer.create(
<MDXDeckState>
<Consumer />
</MDXDeckState>
)
expect(typeof context.state).toBe('object')
expect(typeof context.state.metadata).toBe('object')
expect(context.state.step).toBe(0)
expect(context.state.mode).toBe('normal')
expect(typeof context.setState).toBe('function')
})
test.todo('setState updates state')
})
test('renders', () => {
const json = renderer.create(<MDXDeck slides={slides} />).toJSON()
expect(json).toMatchSnapshot()
})

View File

@ -1,8 +0,0 @@
import React from 'react'
import TestRenderer from 'react-test-renderer'
import Pre from '../Pre'
test('Pre renders', () => {
const json = TestRenderer.create(<Pre children="hi" />).toJSON()
expect(json).toMatchSnapshot()
})

View File

@ -1,8 +0,0 @@
import React from 'react'
import TestRenderer from 'react-test-renderer'
import Steps from '../Steps'
test('Steps renders', () => {
const json = TestRenderer.create(<Steps render={() => 'hi'} />).toJSON()
expect(json).toMatchSnapshot()
})

View File

@ -1,13 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Appear renders 1`] = `
<h1
style={
Object {
"visibility": "hidden",
}
}
>
Hello
</h1>
`;

View File

@ -1,33 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders 1`] = `
<div>
<div
role="group"
style={
Object {
"outline": "none",
}
}
tabIndex="-1"
>
<div
className="css-suf238-Root ekm8v3h0"
>
<div
role="group"
style={
Object {
"outline": "none",
}
}
tabIndex="-1"
>
<pre>
one
</pre>
</div>
</div>
</div>
</div>
`;

View File

@ -1,15 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Pre renders 1`] = `
<pre
style={
Object {
"fontFamily": "Menlo, monospace",
"fontSize": 18,
"whiteSpace": "pre-wrap",
}
}
>
hi
</pre>
`;

View File

@ -1,3 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Steps renders 1`] = `"hi"`;

View File

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

View File

@ -1,20 +0,0 @@
export { MDXDeck, MDXDeckState } from './MDXDeck'
export { default as Clock } from './Clock'
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'
export { default as useSteps } from './useSteps'
export { default as useTheme } from './useTheme'
export { Slide } from './Slide'
export { Zoom } from './Zoom'
export { Embed } from './Embed'
// private
export { splitSlides } from './splitSlides'
export { Ratio } from './Ratio'
export { Provider } from './Provider'

View File

@ -1,57 +0,0 @@
import React from 'react'
import styled from '@emotion/styled'
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 = {
wrapper: props => props.children,
pre: props => props.children,
code,
inlineCode,
img,
table,
}
export default components

View File

@ -1,29 +0,0 @@
import React from 'react'
export const splitSlides = props => {
const { theme, themes } = props
const arr = React.Children.toArray(props.children)
const splits = []
const slides = []
arr.forEach((child, i) => {
if (child.props.mdxType === 'hr') splits.push(i)
})
let previousSplit = 0
splits.forEach(i => {
const children = [...arr.slice(previousSplit, i)]
slides.push(() => children)
previousSplit = i + 1
})
slides.push(() => [...arr.slice(previousSplit)])
return {
...props,
theme,
themes,
slides,
}
}
export default splitSlides

View File

@ -1,13 +0,0 @@
import { useContext, useEffect } from 'react'
import { Context } from './context'
export default length => {
const context = useContext(Context)
const { register, index, step } = context
useEffect(() => {
if (typeof register !== 'function') return
register(index, { steps: length })
}, [length])
return step
}

View File

@ -1,4 +0,0 @@
import { useContext } from 'react'
import { ThemeContext } from '@emotion/core'
export default () => useContext(ThemeContext)

View File

@ -1,28 +0,0 @@
# MDX Deck Export CLI
```sh
npm i -D @mdx-deck/export
```
Export as PDF
```sh
mdx-deck-export pdf deck.mdx
```
Export as PNG
```sh
mdx-deck-export png deck.mdx
```
## Options
```
-d --out-dir Output directory
-f --out-file Output filename
-p --port Server port
-w --width Width in pixels
-h --height Height in pixels
--no-sandbox Disable puppeteer sandbox
```

View File

@ -1,68 +0,0 @@
const path = require('path')
const puppeteer = require('puppeteer')
const mkdirp = require('mkdirp')
const dev = require('mdx-deck/lib/dev')
const findup = require('find-up')
module.exports = async opts => {
const { type, outDir, outFile, port, width, height, sandbox } = opts
const args = []
if (!sandbox) {
args.push('--no-sandbox', '--disable-setuid-sandbox')
}
if (opts.webpack) {
opts.webpack = require(path.resolve(opts.webpack))
} else {
const webpackConfig = findup.sync('webpack.config.js', { cwd: opts.dirname })
if (webpackConfig) opts.webpack = require(webpackConfig)
}
const server = await dev(opts)
const browser = await puppeteer.launch({ args })
const page = await browser.newPage()
const filename = path.join(
outDir,
path.basename(outFile, path.extname(outFile)) + '.' + type
)
mkdirp.sync(outDir)
switch (type) {
case 'pdf':
const url = `http://localhost:${port}/print`
await page.goto(url, {
waitUntil: 'networkidle2',
})
await page.pdf({
width,
height,
path: filename,
scale: 1,
printBackground: true,
})
break
case 'png':
await page.setViewport({ width, height })
await page.goto('http://localhost:' + port, {
waitUntil: 'networkidle2',
})
await page.screenshot({
path: filename,
type: 'png',
clip: {
x: 0,
y: 0,
width,
height,
},
})
break
}
await browser.close()
await server.close()
return filename
}

View File

@ -1,20 +0,0 @@
{
"name": "@mdx-deck/export",
"version": "2.5.1",
"main": "index.js",
"author": "Brent Jackson <jxnblk@gmail.com>",
"license": "MIT",
"bin": {
"mdx-deck-export": "./cli.js"
},
"scripts": {
"pdf": "./cli.js pdf ../../docs/demo.mdx -d ../../docs/dist",
"png": "./cli.js png ../../docs/demo.mdx -d ../../docs/dist"
},
"dependencies": {
"mdx-deck": "^2.5.1",
"meow": "^5.0.0",
"mkdirp": "^0.5.1",
"puppeteer": "^1.13.0"
}
}

View File

@ -1,4 +0,0 @@
.cache
public
src/pages
src/decks

View File

@ -1,68 +1,75 @@
# @mdx-deck/gatsby-theme
**WIP** A Gatsby theme for adding MDX Decks to your Gatsby site
# gatsby-theme-mdx-deck
Add MDX Deck presentations to any Gatsby site
```sh
npm i @mdx-deck/gatsby-theme
npm i gatsby-theme-mdx-deck
```
_Note:_ This theme **requires MDX v1** and will not work with previous versions of MDX
```js
// gatsby-config.js
module.exports = {
plugins: ['@mdx-deck/gatsby-theme'],
plugins: [
'gatsby-theme-mdx-deck',
]
}
```
Add MDX Decks to the `src/decks/` directory. The filename will be used for the route of that deck.
Add one or more MDX presentation files to the `decks/` directory.
The filenames will be used for creating routes to each deck.
**/src/decks/hello.mdx**
Example `decks/hello.mdx`
```mdx
# Hello!
---
## Beep
## Beep boop
```
## Using Layouts
## Layouts
Slide layout components must be rendered inline, _not_ using the default export syntax.
Individual slides can be wrapped with layout components,
which work similarly to slide templates found in other presentation software.
**/src/decks/hello.mdx**
Example `decks/hello.mdx`
```mdx
import Layout from './my-layout'
<Layout>
# Hello Layout
# Hello
</Layout>
---
## Beep boop
```
## Theme Config
## Configuration Options
The following options can be passed to the gatsby theme.
The Gatsby theme accepts the following options.
```js
// gatsby-config.js
module.exports = {
plugins: [
{
resolve: '@mdx-deck/gatsby-theme',
resolve: 'gatsby-theme-mdx-deck',
options: {
// disable gatsby-mdx plugin use this when your site already uses gatsby-mdx
// enable or disable gatsby-plugin-mdx
mdx: false,
// source directory for decks
path: 'src/presentations',
// name routes' basepath
name: 'presentations',
},
},
],
// source directory
contentPath: 'decks',
// base path for routes generate by this theme
basePath: ''
}
}
]
}
```

View File

@ -0,0 +1,6 @@
# Beep
---
## Boop bop

View File

@ -0,0 +1,69 @@
import { Head, Appear, Notes } from '../src'
<Head>
<link
rel='stylesheet'
href='https://fonts.googleapis.com/css?family=Roboto+Mono&display=swap'
/>
</Head>
# Hello
---
`@mdx-deck/gatsby-theme-next`
<Notes>
Hi
Hello, my secret notes...
</Notes>
---
## Beep
---
## Appear:
<ul>
<Appear>
<li>One</li>
<li>Two</li>
<li>Three</li>
</Appear>
</ul>
---
## Hi
---
```jsx
import React from 'react'
import { Embed } from 'mdx-deck'
import Hello from './hello.mdx'
export default props =>
<Embed
src={Hello}
slide={2}
/>
```
---
More steps
<Appear>
<div>beep</div>
<div>boop</div>
</Appear>
---
## The End

View File

@ -0,0 +1,6 @@
This deck does not have any headings
---
Which means the resolver won't get a title

View File

@ -1,43 +1,31 @@
const path = require('path')
const pkg = require('./package.json')
const remarkPlugins = [require('remark-emoji'), require('remark-unwrap-images')]
const IS_LOCAL = process.cwd() === __dirname
const themeConfig = (opts = {}) => {
const { path: source = 'src/decks', name = 'decks', mdx = true } = opts
const remarkPlugins = [require('remark-unwrap-images'), require('remark-emoji')]
const config = (opts = {}) => {
const { mdx = true, contentPath: name = 'decks' } = opts
return {
plugins: [
'gatsby-plugin-emotion',
{
resolve: 'gatsby-source-filesystem',
options: {
name,
path: path.resolve(source),
path: name,
},
},
mdx && {
resolve: 'gatsby-plugin-mdx',
options: {
extensions: ['.mdx', '.md'],
remarkPlugins,
},
},
{
resolve: 'gatsby-plugin-compile-es6-packages',
options: {
modules: [
pkg.name,
'@mdx-deck/components',
'@mdx-deck/themes',
'@mdx-deck/layouts',
],
},
},
'gatsby-plugin-react-helmet',
'gatsby-plugin-emotion',
'gatsby-plugin-catch-links',
'gatsby-plugin-theme-ui',
].filter(Boolean),
}
}
module.exports = IS_LOCAL ? themeConfig() : themeConfig
module.exports = IS_LOCAL ? config() : config

View File

@ -1,105 +1,186 @@
const fs = require('fs')
const path = require('path')
const { createFilePath } = require('gatsby-source-filesystem')
const Debug = require('debug')
const mkdirp = require('mkdirp')
// based on gatsby-theme-blog
const fs = require(`fs`)
const path = require(`path`)
const mkdirp = require(`mkdirp`)
const crypto = require(`crypto`)
const Debug = require(`debug`)
const pkg = require('./package.json')
const debug = Debug('@mdx-deck/gatsby-theme')
const debug = Debug(pkg.name)
let basePath
let contentPath
const DeckTemplate = require.resolve(`./src/templates/deck`)
const DecksTemplate = require.resolve(`./src/templates/decks`)
exports.onPreBootstrap = ({ store }, opts = {}) => {
const { path: source = 'src/decks' } = opts
const isDir = fs.statSync(source).isDirectory()
const dirname = isDir ? source : path.dirname(source)
const { program } = store.getState()
const dir = path.join(program.directory, dirname)
debug(`Initializing ${dir} directory`)
mkdirp.sync(dir)
basePath = opts.basePath || `/`
contentPath = opts.contentPath || `decks`
if (opts.cli) return
const dirname = path.join(program.directory, contentPath)
mkdirp.sync(dirname)
debug(`Initializing ${dirname} directory`)
}
exports.onCreateNode = ({ node, actions, getNode }, opts = {}) => {
const { name = 'decks' } = opts
if (node.internal.type !== 'Mdx') return
const value = path.join('/', name, createFilePath({ node, getNode }))
actions.createNodeField({
name: 'deck',
node,
value,
const mdxResolverPassthrough = fieldName => async (
source,
args,
context,
info
) => {
const type = info.schema.getType(`Mdx`)
const mdxNode = context.nodeModel.getNodeById({
id: source.parent,
})
const resolver = type.getFields()[fieldName].resolve
const result = await resolver(mdxNode, args, context, {
fieldName,
})
return result
}
const stripSlash = str => str.replace(/\/$/, '')
const resolveTitle = async (...args) => {
const headings = await mdxResolverPassthrough('headings')(...args)
const [first = {}] = headings
return first.value || ''
}
exports.createPages = async ({ graphql, actions }, opts = {}) => {
const { name = 'decks' } = opts
exports.sourceNodes = ({ actions, schema }) => {
const { createTypes } = actions
createTypes(
schema.buildObjectType({
name: `Deck`,
fields: {
id: { type: `ID!` },
slug: {
type: `String!`,
},
title: {
type: `String!`,
resolve: resolveTitle,
},
body: {
type: `String!`,
resolve: mdxResolverPassthrough(`body`),
},
},
interfaces: [`Node`],
})
)
}
exports.createPages = async ({ graphql, actions, reporter }) => {
const { createPage } = actions
const result = await graphql(`
{
allMdx {
allDeck {
edges {
node {
id
fields {
deck
}
parent {
... on File {
name
sourceInstanceName
}
}
slug
title
}
}
}
}
`)
if (result.errors) {
debug(result.errors)
return
reporter.panic(result.errors)
}
const decks = result.data.allMdx.edges
.filter(edge => {
return edge.node.parent.sourceInstanceName === name
})
.map(edge => edge.node)
const { allDeck } = result.data
const decks = allDeck.edges
// single deck mode
if (decks.length === 1) {
const [deck] = decks
const pathname = path.join('/', name)
const matchPath = path.join(pathname, '*')
actions.createPage({
path: pathname,
const matchPath = path.join(basePath, '*')
createPage({
path: basePath,
matchPath,
component: require.resolve('./src/templates/deck.js'),
component: DeckTemplate,
context: {
id: deck.id,
basepath: stripSlash(pathname),
...deck.node,
slug: '',
},
})
return
}
// index page
actions.createPage({
path: path.join('/', name),
component: require.resolve('./src/templates/index.js'),
decks.forEach(({ node }, index) => {
const { slug } = node
const matchPath = path.join(slug, '*')
createPage({
path: slug,
matchPath,
component: DeckTemplate,
context: node,
})
createPage({
path: slug + '/print',
component: DeckTemplate,
context: node,
})
})
decks.forEach(deck => {
const matchPath = path.join(deck.fields.deck, '*')
actions.createPage({
path: deck.fields.deck,
matchPath: path.join(deck.fields.deck, '*'),
component: require.resolve('./src/templates/deck.js'),
// index page
createPage({
path: basePath,
component: DecksTemplate,
context: {
id: deck.id,
basepath: stripSlash(deck.fields.deck),
decks,
},
})
}
exports.onCreateNode = ({ node, actions, getNode, createNodeId }) => {
const { createNode, createParentChildLink } = actions
const toPath = node => {
const { dir } = path.parse(node.relativePath)
return path.join(basePath, dir, node.name)
}
if (node.internal.type !== `Mdx`) return
const fileNode = getNode(node.parent)
const source = fileNode.sourceInstanceName
if (node.internal.type === `Mdx` && source === contentPath) {
const slug = toPath(fileNode)
createNode({
slug,
// Required fields.
id: createNodeId(`${node.id} >>> Deck`),
parent: node.id,
children: [],
internal: {
type: `Deck`,
contentDigest: crypto
.createHash(`md5`)
.update(JSON.stringify({ slug }))
.digest(`hex`),
content: JSON.stringify({ slug }),
description: `Slide Decks`,
},
})
createParentChildLink({ parent: fileNode, child: node })
}
}
exports.onCreateDevServer = ({ app }) => {
console.log('onCreateDevServer')
if (typeof process.send !== 'function') return
process.send({
mdxDeck: true,
})
}

View File

@ -0,0 +1 @@
export { wrapPageElement } from './src'

View File

@ -1 +1 @@
// noop
export * from './src'

View File

@ -1,36 +1,45 @@
{
"name": "@mdx-deck/gatsby-theme",
"version": "2.5.0",
"name": "gatsby-theme-mdx-deck",
"version": "0.0.0",
"main": "index.js",
"author": "Brent Jackson <jxnblk@gmail.com>",
"license": "MIT",
"scripts": {
"start": "gatsby develop",
"build": "gatsby build"
"build": "gatsby build",
"clean": "gatsby clean"
},
"peerDependencies": {
"gatsby": "^2.3.17",
"devDependencies": {
"gatsby": "^2.13.6",
"react": "^16.8.6",
"react-dom": "^16.8.6"
},
"devDependencies": {
"gatsby": "^2.3.17",
"peerDependencies": {
"gatsby": "^2.13.6",
"react": "^16.8.6",
"react-dom": "^16.8.6"
},
"dependencies": {
"@mdx-deck/components": "^2.5.0",
"@mdx-deck/themes": "^2.5.0",
"@mdx-js/mdx": "^1.0.1",
"@mdx-js/react": "^1.0.1",
"@emotion/core": "^10.0.14",
"@mdx-deck/themes": "*",
"@mdx-js/mdx": "^1.0.21",
"@mdx-js/react": "^1.0.21",
"@reach/router": "^1.2.1",
"debug": "^4.1.1",
"gatsby-plugin-compile-es6-packages": "^1.1.0",
"gatsby-plugin-emotion": "^4.0.6",
"gatsby-plugin-mdx": "^1.0.6",
"gatsby-source-filesystem": "^2.0.29",
"gatsby": "^2.13.24",
"gatsby-plugin-catch-links": "^2.1.0",
"gatsby-plugin-emotion": "^4.1.0",
"gatsby-plugin-mdx": "^1.0.13",
"gatsby-plugin-react-helmet": "^3.1.0",
"gatsby-plugin-theme-ui": "^0.2.6",
"gatsby-source-filesystem": "^2.1.3",
"hhmmss": "^1.0.0",
"lodash.get": "^4.4.2",
"lodash.merge": "^4.6.1",
"mkdirp": "^0.5.1",
"react-helmet": "^6.0.0-beta",
"react-swipeable": "^5.3.0",
"remark-emoji": "^2.0.2",
"remark-unwrap-images": "^1.0.0"
"remark-unwrap-images": "^1.0.0",
"theme-ui": "^0.2.14"
}
}

View File

@ -0,0 +1,35 @@
import React, { useReducer } from 'react'
import merge from 'lodash.merge'
import Context from '../context'
const reducer = (state, next) =>
typeof next === 'function'
? merge({}, state, next(state))
: merge({}, state, next)
export default props => {
const [state, setState] = useReducer(reducer, {
mode: 'normal',
step: 0,
metadata: {},
})
const register = (index, key, value) => {
if (state.metadata[index] && state.metadata[index][key]) return
setState({
metadata: {
[index]: {
[key]: value,
},
},
})
}
const context = {
...state,
setState,
register,
}
return <Context.Provider value={context}>{props.children}</Context.Provider>
}

View File

@ -0,0 +1,18 @@
import React from 'react'
import useSteps from '../hooks/use-steps'
export const Appear = props => {
const children = React.Children.toArray(props.children)
const step = useSteps(children.length)
const styled = children.map((child, i) =>
React.cloneElement(child, {
style: {
visibility: i < step ? 'visible' : 'hidden',
},
})
)
return <>{styled}</>
}
export default Appear

View File

@ -0,0 +1,19 @@
import { useEffect, useState } from 'react'
export const Clock = props => {
const [time, setTime] = useState(new Date().toLocaleTimeString())
useEffect(() => {
const tick = () => {
const now = new Date()
setTime(now.toLocaleTimeString())
}
const timer = setInterval(tick, 1000)
return () => {
clearInterval(timer)
}
}, [])
return time
}
export default Clock

View File

@ -0,0 +1,148 @@
import React from 'react'
import { Router, globalHistory } from '@reach/router'
import { Global } from '@emotion/core'
import { ThemeProvider } from 'theme-ui'
import { Helmet } from 'react-helmet'
import get from 'lodash.get'
import merge from 'lodash.merge'
import useKeyboard from '../hooks/use-keyboard'
import useStorage from '../hooks/use-storage'
import useDeck from '../hooks/use-deck'
import Context from '../context'
import Wrapper from './wrapper'
import Slide from './slide'
import { modes } from '../constants'
import Presenter from './presenter'
import Overview from './overview'
import Grid from './grid'
const Keyboard = () => {
useKeyboard()
return false
}
const Storage = () => {
useStorage()
return false
}
const Print = ({ slides }) => {
const outer = useDeck()
const context = {
...outer,
mode: modes.print,
}
return (
<Context.Provider value={context}>
{slides.map((slide, i) => (
<Slide key={i} slide={slide} preview />
))}
</Context.Provider>
)
}
const getIndex = () => {
const { pathname } = globalHistory.location
const paths = pathname.split('/')
const n = Number(paths[paths.length - 1])
const index = isNaN(n) ? 0 : n
return index
}
const GoogleFont = ({ theme }) => {
if (!theme.googleFont) return false
return (
<Helmet>
<link rel="stylesheet" href={theme.googleFont} />
</Helmet>
)
}
const mergeThemes = (...themes) =>
themes.reduce(
(acc, theme) =>
typeof theme === 'function' ? theme(acc) : merge(acc, theme),
{}
)
export default ({
slides = [],
pageContext: { title, slug },
theme = {},
themes = [],
...props
}) => {
const outer = useDeck()
const index = getIndex()
const head = slides.head.children
const { components, ...mergedTheme } = mergeThemes(theme, ...themes)
const context = {
...outer,
slug,
length: slides.length,
index,
steps: get(outer, `metadata.${index}.steps`),
notes: get(outer, `metadata.${index}.notes`),
theme: mergedTheme,
}
let Mode = ({ children }) => <React.Fragment children={children} />
switch (context.mode) {
case modes.presenter:
Mode = Presenter
break
case modes.overview:
Mode = Overview
break
case modes.grid:
Mode = Grid
break
default:
break
}
return (
<>
<Helmet>
<title>{title}</title>
{head}
</Helmet>
<GoogleFont theme={mergedTheme} />
<Context.Provider value={context}>
<ThemeProvider components={components} theme={mergedTheme}>
<Global
styles={{
body: {
margin: 0,
overflow: context.mode === modes.normal ? 'hidden' : null,
},
}}
/>
<Keyboard />
<Storage />
<Wrapper>
<Mode slides={slides}>
<Router
basepath={slug}
style={{
height: '100%',
}}>
<Slide index={0} path="/" slide={slides[0]} />
{slides.map((slide, i) => (
<Slide key={i} index={i} path={i + '/*'} slide={slide} />
))}
<Print path="/print" slides={slides} />
</Router>
</Mode>
</Wrapper>
</ThemeProvider>
</Context.Provider>
</>
)
}

View File

@ -0,0 +1,41 @@
/** @jsx jsx */
import { jsx } from 'theme-ui'
import { Link } from 'gatsby'
export default ({ decks }) => {
return (
<div
sx={{
fontFamily: 'ui',
fontWeight: 'bold',
px: 4,
py: 3,
}}>
<h1>MDX Deck</h1>
<ul
sx={{
p: 0,
}}>
{decks.map(d => (
<li
key={d.id}
sx={{
my: 2,
}}>
<Link
to={d.slug}
sx={{
color: 'inherit',
textDecoration: 'none',
':hover': {
textDecoration: 'underline',
},
}}>
{d.title || d.slug}
</Link>
</li>
))}
</ul>
</div>
)
}

View File

@ -0,0 +1,42 @@
/** @jsx jsx */
import { jsx } from 'theme-ui'
import splitSlides from '../split-slides'
import Slide from './slide'
import Zoom from './zoom'
const wrapper = ({ slide: i, ratio, zoom, ...props }) => {
const slides = splitSlides(props)
const slide = slides[i - 1]
if (!slide) {
return <pre>No slide found (slide {i})</pre>
}
return (
<Zoom zoom={zoom} ratio={ratio}>
<Slide slide={slide} preview />
</Zoom>
)
}
const components = {
wrapper,
}
export const Embed = ({
src: Deck,
slide = 1,
ratio = 16 / 9,
zoom = 1,
...props
}) => (
<Deck
{...props}
components={components}
slide={slide}
ratio={ratio}
zoom={zoom}
/>
)
export default Embed

View File

@ -0,0 +1,21 @@
/** @jsx jsx */
import { jsx } from 'theme-ui'
export const FullScreenCode = ({ ...props }) => (
<div
{...props}
sx={{
width: '100%',
height: '100%',
pre: {
// hack for prism styles
margin: '0 !important',
width: '100%',
height: '100%',
overflow: 'auto',
},
}}
/>
)
export default FullScreenCode

View File

@ -0,0 +1,36 @@
/** @jsx jsx */
import { jsx } from 'theme-ui'
import { navigate } from '@reach/router'
import useDeck from '../hooks/use-deck'
import { modes } from '../constants'
import SlideList from './slide-list'
export default ({ slides }) => {
const { slug, setState } = useDeck()
return (
<div
sx={{
minHeight: '100vh',
color: 'white',
bg: 'black',
}}>
<div
sx={{
display: 'flex',
flexWrap: 'wrap',
}}>
<SlideList
slides={slides}
onClick={i => {
navigate([slug, i].join('/'))
setState({ mode: modes.normal })
}}
sx={{
width: '25%',
m: 0,
}}
/>
</div>
</div>
)
}

View File

@ -0,0 +1,5 @@
export const Head = props => false
Head.mdxDeckHead = true
export default Head

View File

@ -0,0 +1,26 @@
/** @jsx jsx */
import { jsx } from 'theme-ui'
import React from 'react'
export const Horizontal = ({ ...props }) => {
const children = React.Children.toArray(props.children)
return (
<div
{...props}
sx={{
display: 'flex',
alignItems: 'center',
height: '100%',
textAlign: 'center',
}}>
{children.map((child, i) => (
<div key={child.key} sx={{ width: 100 / children.length + '%' }}>
{child}
</div>
))}
</div>
)
}
export default Horizontal

View File

@ -0,0 +1,26 @@
/** @jsx jsx */
import { jsx } from 'theme-ui'
export const Image = ({
width = '100%',
height = '100%',
size = 'cover',
src,
css,
...props
}) => (
<div
{...props}
sx={{
width,
height,
backgroundImage: `url(${src})`,
backgroundSize: size,
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
}}
css={css}
/>
)
export default Image

View File

@ -0,0 +1,23 @@
/** @jsx jsx */
import { jsx } from 'theme-ui'
export const Invert = ({ ...props }) => (
<div
{...props}
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
height: '100%',
color: 'background',
bg: 'text',
a: {
color: 'inherit',
},
}}
/>
)
export default Invert

View File

@ -0,0 +1,13 @@
import { useEffect } from 'react'
import useDeck from '../hooks/use-deck'
export const Notes = props => {
const context = useDeck()
useEffect(() => {
context.register(context.index, 'notes', props.children)
}, [props.children])
return false
}
export default Notes

View File

@ -0,0 +1,62 @@
/** @jsx jsx */
import { jsx } from 'theme-ui'
import { navigate } from '@reach/router'
import useDeck from '../hooks/use-deck'
import Zoom from './zoom'
import SlideList from './slide-list'
export default ({ slides, children }) => {
const { slug, index, length } = useDeck()
return (
<div
sx={{
display: 'flex',
height: '100vh',
fontFamily: 'ui',
color: 'white',
bg: 'black',
}}>
<div
sx={{
width: 100 / 6 + '%',
minWidth: 0,
flex: 'none',
height: '100vh',
overflowY: 'auto',
WebkitOverflowScrolling: 'touch',
p: 2,
}}>
<SlideList
slides={slides}
zoom={1 / 6}
onClick={i => {
navigate([slug, i].join('/'))
}}
/>
</div>
<div
sx={{
width: 500 / 6 + '%',
py: 3,
pr: 3,
display: 'flex',
flexDirection: 'column',
height: '100vh',
}}>
<div
sx={{
flex: '1 1 auto',
}}>
<Zoom zoom={5 / 6}>{children}</Zoom>
</div>
<div
sx={{
py: 3,
}}>
{index} / {length - 1}
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,47 @@
/** @jsx jsx */
import { jsx } from 'theme-ui'
import React from 'react'
import { globalHistory } from '@reach/router'
import useDeck from '../hooks/use-deck'
import Clock from './clock'
import Timer from './timer'
export default props => {
const context = useDeck()
const { index, length } = context
return (
<React.Fragment>
<div>
{index} / {length - 1}
</div>
<div
sx={{
mx: 4,
}}>
<a
href={globalHistory.location.href}
rel="noopener noreferrer"
target="_blank"
sx={{
color: 'inherit',
textDecoration: 'none',
}}>
Open in New Window
</a>
</div>
<div sx={{ mx: 'auto' }} />
<div
sx={{
display: 'flex',
alignItems: 'center',
mx: 4,
}}>
<Timer />
</div>
<div>
<Clock />
</div>
</React.Fragment>
)
}

View File

@ -0,0 +1,72 @@
/** @jsx jsx */
import { jsx } from 'theme-ui'
import React from 'react'
import Zoom from './zoom'
import Slide from './slide'
import useDeck from '../hooks/use-deck'
import Footer from './presenter-footer'
export const Presenter = ({ slides, children }) => {
const context = useDeck()
const next = slides[context.index + 1]
const notes = context.notes ? React.Children.toArray(context.notes) : false
return (
<div
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
height: '100vh',
fontFamily: 'ui',
color: 'white',
bg: 'black',
}}>
<div
sx={{
display: 'flex',
flex: '1 1 auto',
height: '60vh',
}}>
<div
sx={{
width: '75%',
p: 3,
}}>
<Zoom zoom={3 / 4}>{children}</Zoom>
</div>
<div
sx={{
width: '25%',
p: 3,
}}>
<Zoom ratio={4 / 3} zoom={1 / 4}>
<Slide slide={next} preview />
</Zoom>
{notes && (
<div
sx={{
my: 3,
}}>
{notes}
</div>
)}
</div>
</div>
<div
sx={{
height: 96,
p: 3,
display: 'flex',
alignItems: 'center',
fontSize: 1,
fontWeight: 'bold',
fontVariantNumeric: 'tabular-nums',
}}>
<Footer />
</div>
</div>
)
}
export default Presenter

View File

@ -0,0 +1,61 @@
/** @jsx jsx */
import { jsx } from 'theme-ui'
import React, { useEffect, useRef } from 'react'
import Zoom from './zoom'
import Slide from './slide'
import useDeck from '../hooks/use-deck'
const noop = () => {}
export const SlideList = ({
slides = [],
ratio = 16 / 9,
zoom = 1 / 4,
onClick = noop,
...props
}) => {
const { index } = useDeck()
const thumb = useRef(null)
useEffect(() => {
const el = thumb.current
if (!el) return
if (typeof el.scrollIntoViewIfNeeded === 'function') {
el.scrollIntoViewIfNeeded()
}
})
return (
<React.Fragment>
{slides.map((slide, i) => (
<div
{...props}
key={i}
role="link"
ref={i === index ? thumb : null}
onClick={e => {
onClick(i)
}}
style={
index === i
? {
position: 'relative',
zIndex: 1,
}
: null
}
sx={{
m: 2,
cursor: 'pointer',
outline: index === i ? `4px solid cyan` : null,
}}>
<Zoom zoom={zoom} ratio={ratio}>
<Slide slide={slide} preview />
</Zoom>
</div>
))}
</React.Fragment>
)
}
export default SlideList

View File

@ -0,0 +1,42 @@
/** @jsx jsx */
import { jsx } from 'theme-ui'
import React, { Fragment } from 'react'
import Context from '../context'
import useDeck from '../hooks/use-deck'
import useSwipe from '../hooks/use-swipe'
import { modes } from '../constants'
export const Slide = ({ slide, index, preview, ...props }) => {
const outer = useDeck()
const swipeProps = useSwipe()
const context = {
...outer,
index,
preview,
}
return (
<Context.Provider value={context}>
<div
{...(!preview ? swipeProps : {})}
sx={{
boxSizing: 'border-box',
width: '100%',
height: context.mode === modes.print ? '100vh' : '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
position: 'relative',
color: 'text',
bg: 'background',
variant: 'styles.Slide',
}}>
{slide}
</div>
</Context.Provider>
)
}
export default Slide

View File

@ -0,0 +1,22 @@
/** @jsx jsx */
import { jsx } from 'theme-ui'
import React from 'react'
export const SplitRight = ({ children, ...props }) => {
const [first, ...rest] = React.Children.toArray(children)
return (
<div
{...props}
sx={{
display: 'flex',
alignItems: 'center',
height: '100%',
textAlign: 'center',
}}>
<div sx={{ width: '50%' }}>{rest}</div>
<div sx={{ width: '50%' }}>{first}</div>
</div>
)
}
export default SplitRight

View File

@ -0,0 +1,22 @@
/** @jsx jsx */
import { jsx } from 'theme-ui'
import React from 'react'
export const Split = ({ children, ...props }) => {
const [first, ...rest] = React.Children.toArray(children)
return (
<div
{...props}
sx={{
display: 'flex',
alignItems: 'center',
height: '100%',
textAlign: 'center',
}}>
<div sx={{ width: '50%' }}>{first}</div>
<div sx={{ width: '50%' }}>{rest}</div>
</div>
)
}
export default Split

View File

@ -0,0 +1,59 @@
/** @jsx jsx */
import { jsx } from 'theme-ui'
import React, { useEffect } from 'react'
import hhmmss from 'hhmmss'
import useDeck from '../hooks/use-deck'
let ticker
export const Timer = props => {
const { setState, timer = false, seconds = 0 } = useDeck()
useEffect(() => {
const tick = () => {
if (!timer) return
setState({
seconds: seconds + 1,
})
}
ticker = setInterval(tick, 1000)
return () => {
clearInterval(ticker)
}
}, [timer, seconds])
const toggle = () => {
setState({
timer: !timer,
})
}
const reset = () => {
setState({ seconds: 0 })
}
return (
<React.Fragment>
<button
onClick={reset}
disabled={!seconds}
title="Reset timer"
sx={{
mx: 1,
}}>
Reset
</button>{' '}
<button
title={timer ? 'Stop timer' : 'Start timer'}
onClick={toggle}
sx={{
mx: 1,
}}>
{timer ? 'Stop' : 'Start'}
</button>{' '}
{hhmmss(seconds)}
</React.Fragment>
)
}
export default Timer

Some files were not shown because too many files have changed in this diff Show More