├── .babelrc
├── .eslintrc
├── .gitignore
├── .npmignore
├── .prettierrc
├── README.md
├── blocks.js
├── demo
├── github-button.js
├── images
│ ├── camera.jpg
│ ├── conference.jpg
│ ├── presa-ui.jpg
│ └── stairs.jpg
├── index.html
├── index.js
└── sidebar-layout.js
├── package.json
├── src
├── assets
│ ├── icons.js
│ └── raw
│ │ ├── arrow-left.svg
│ │ ├── arrow-right.svg
│ │ ├── fullscreen.svg
│ │ ├── presa-logo.png
│ │ ├── presa-ui.png
│ │ ├── presa.svg
│ │ └── toc.svg
├── blocks
│ ├── code
│ │ ├── code.js
│ │ ├── color-scheme.js
│ │ └── index.js
│ ├── index.js
│ ├── typography.js
│ └── video-background.js
├── components
│ ├── birds-eye-mode
│ │ └── index.js
│ ├── built-with.js
│ ├── controls
│ │ ├── control-button.js
│ │ ├── control-group.js
│ │ ├── fullscreen-toggle.js
│ │ ├── index.js
│ │ ├── mini-progress.js
│ │ ├── slide-switcher.js
│ │ └── toc-toggle.js
│ ├── fragment
│ │ ├── constants.js
│ │ ├── fragment.js
│ │ ├── index.js
│ │ ├── manager.js
│ │ └── next-index.js
│ ├── fullscreen-mode
│ │ └── index.js
│ ├── global-background.js
│ ├── presentation-container.js
│ ├── presentation.js
│ ├── remote-control.js
│ ├── slide
│ │ ├── background.js
│ │ ├── context.js
│ │ ├── index.js
│ │ ├── layouts.js
│ │ ├── placeholder.js
│ │ ├── slide-decl.js
│ │ ├── slide-theme.js
│ │ └── slide.js
│ └── slideshow-mode
│ │ ├── index.js
│ │ └── toc.js
├── index.js
└── theme.js
├── tests
├── fragments.test.js
├── next-index.test.js
├── support
│ └── test-container.js
└── video-background.test.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "react",
4 | [
5 | "env",
6 | {
7 | "targets": {
8 | "browsers": ["last 2 versions", "not samsung 6.2"]
9 | }
10 | }
11 | ]
12 | ],
13 | "plugins": [
14 | "transform-object-rest-spread",
15 | "babel-plugin-transform-class-properties"
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "jest/globals": true
6 | },
7 | "parser": "babel-eslint",
8 | "parserOptions": {
9 | "ecmaVersion": 2017,
10 | "sourceType": "module",
11 | "ecmaFeatures": {
12 | "experimentalObjectRestSpread": true,
13 | "jsx": true
14 | }
15 | },
16 | "plugins": ["react", "jest"],
17 | "extends": ["plugin:react/recommended","eslint:recommended", "prettier"]
18 | }
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/osx,node,sublimetext
3 | lib
4 | dist
5 |
6 | ### Node ###
7 | # Logs
8 | logs
9 | *.log
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 |
14 | # Runtime data
15 | pids
16 | *.pid
17 | *.seed
18 | *.pid.lock
19 |
20 | # Directory for instrumented libs generated by jscoverage/JSCover
21 | lib-cov
22 |
23 | # Coverage directory used by tools like istanbul
24 | coverage
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (http://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Typescript v1 declaration files
46 | typings/
47 |
48 | # Optional npm cache directory
49 | .npm
50 |
51 | # Optional eslint cache
52 | .eslintcache
53 |
54 | # Optional REPL history
55 | .node_repl_history
56 |
57 | # Output of 'npm pack'
58 | *.tgz
59 |
60 | # Yarn Integrity file
61 | .yarn-integrity
62 |
63 | # dotenv environment variables file
64 | .env
65 |
66 | .cache
67 |
68 | ### OSX ###
69 | *.DS_Store
70 | .AppleDouble
71 | .LSOverride
72 |
73 | # Icon must end with two \r
74 | Icon
75 |
76 | # Thumbnails
77 | ._*
78 |
79 | # Files that might appear in the root of a volume
80 | .DocumentRevisions-V100
81 | .fseventsd
82 | .Spotlight-V100
83 | .TemporaryItems
84 | .Trashes
85 | .VolumeIcon.icns
86 | .com.apple.timemachine.donotpresent
87 |
88 | # Directories potentially created on remote AFP share
89 | .AppleDB
90 | .AppleDesktop
91 | Network Trash Folder
92 | Temporary Items
93 | .apdisk
94 |
95 | ### SublimeText ###
96 | # cache files for sublime text
97 | *.tmlanguage.cache
98 | *.tmPreferences.cache
99 | *.stTheme.cache
100 |
101 | # workspace files are user-specific
102 | *.sublime-workspace
103 |
104 | # project files should be checked into the repository, unless a significant
105 | # proportion of contributors will probably not be using SublimeText
106 | # *.sublime-project
107 |
108 | # sftp configuration file
109 | sftp-config.json
110 |
111 | # Package control specific files
112 | Package Control.last-run
113 | Package Control.ca-list
114 | Package Control.ca-bundle
115 | Package Control.system-ca-bundle
116 | Package Control.cache/
117 | Package Control.ca-certs/
118 | Package Control.merged-ca-bundle
119 | Package Control.user-ca-bundle
120 | oscrypto-ca-bundle.crt
121 | bh_unicode_properties.cache
122 |
123 | # Sublime-github package stores a github token in this file
124 | # https://packagecontrol.io/packages/sublime-github
125 | GitHub.sublime-settings
126 |
127 | # End of https://www.gitignore.io/api/osx,node,sublimetext
128 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/molefrog/presa/31a68fd268c4af880d8f27c0298272f23f9fc304/.npmignore
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true
4 | }
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | Present with joy in React. Minimal and self-contained framework for presentations built with `styled-components`.
4 | Presa aims to be:
5 |
6 | * **Lightweight.** No external css needed and as minimal dependencies as possible.
7 | * **Extendable.** _Presa_ uses `styled-components` so almost all of its internal components can be extended and themized.
8 | * **Modular.** Core barebone and building blocks are separated and may be optionally excluded from the presentation.
9 | * **Aestetically pleasing.** Simple but functional UI, typography included.
10 |
11 | Here is how Presa UI looks like:
12 |
13 | [](http://molefrog.com/stateful-animations/)
14 |
15 | List of currently supported features:
16 |
17 | * Slideshow mode with optinonal table of the contents in a sidebar.
18 | * Fullscreen API.
19 | * Supports clicker and keyboard navigation.
20 | * _Bird's eye_ view mode.
21 | * Slides are _components_. They are not rendered until the slide is active.
22 |
23 | ### Getting started
24 |
25 | Let's add a simple slide with a background.
26 |
27 | ```JavaScript
28 | import { Presentation, Slide } from 'presa'
29 |
30 | // No need to pass indexes here
31 | const Deck = () => (
32 |
33 |
34 | Let talk about JavaScript!
35 |
36 |
37 | )
38 |
39 | // Make sure you render into a full-page container
40 | ReactDOM.render( , container)
41 | ```
42 |
43 | If you do that in your app, Presa will run automatically.
44 |
45 | ### Contributing
46 |
47 | Feel free to open issues and PRs! If you want to develop Presa locally you can test your features
48 | by adding them to the demo deck inside the `demo/` folder. To open the development server run `yarn dev`.
49 |
50 | The project uses [Prettier](https://prettier.io/) which runs automatically before every commit making
51 | the code base consistent. See also [text editor integrations](https://prettier.io/docs/en/editors.html).
52 |
--------------------------------------------------------------------------------
/blocks.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./lib/blocks')
2 |
--------------------------------------------------------------------------------
/demo/github-button.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 |
3 | class GithubButton extends Component {
4 | render() {
5 | const { user, repo, className } = this.props
6 | const type = this.props.type || 'star'
7 |
8 | const frameSource =
9 | '//ghbtns.com/github-btn.html' +
10 | `?user=${user}&repo=${repo}&type=${type}&count=true&size=large`
11 |
12 | return (
13 |
20 | )
21 | }
22 | }
23 |
24 | export default GithubButton
25 |
--------------------------------------------------------------------------------
/demo/images/camera.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/molefrog/presa/31a68fd268c4af880d8f27c0298272f23f9fc304/demo/images/camera.jpg
--------------------------------------------------------------------------------
/demo/images/conference.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/molefrog/presa/31a68fd268c4af880d8f27c0298272f23f9fc304/demo/images/conference.jpg
--------------------------------------------------------------------------------
/demo/images/presa-ui.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/molefrog/presa/31a68fd268c4af880d8f27c0298272f23f9fc304/demo/images/presa-ui.jpg
--------------------------------------------------------------------------------
/demo/images/stairs.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/molefrog/presa/31a68fd268c4af880d8f27c0298272f23f9fc304/demo/images/stairs.jpg
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/demo/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import styled, { injectGlobal } from 'styled-components'
4 |
5 | import { Presentation, Slide } from '../src'
6 | import { H1, H2, H3, H4, Code } from '../src/blocks'
7 | import VideoBackground from '../src/blocks/video-background'
8 |
9 | import { Presa } from '../src/assets/icons'
10 | import SidebarLayout from './sidebar-layout'
11 | import GithubButton from './github-button'
12 |
13 | const baseTextColor = '#444'
14 | const primaryColor = '#3c59ff'
15 |
16 | const PitchDeck = () => (
17 |
18 | (
21 |
26 | )}
27 | >
28 | Presa
29 |
30 | Create slides in React , present with joy! Built with
31 | styled-components 💅
32 |
33 |
34 |
35 | Presa is lightweight, declarative and modular. Each slide is a React
36 | component: only rendered when visible.
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | —
45 | Press the ➡️ button on your keyboard to go to the next slide or use
46 | controls below.
47 |
48 |
49 |
50 |
51 |
52 | Quick Start
53 | creating your first presentation in 10 seconds
54 |
55 |
56 | Install Presa in your project by running{' '}
57 | yarn add presa command. You'll need to
58 | install react and{' '}
59 | styled-components as well.
60 |
61 |
62 |
63 |
64 |
65 | {`import { Presentation, Slide } from 'presa'
66 |
67 | const App = () =>
68 |
69 |
70 | Hello, everyone!
71 |
72 |
73 | {/* Add your own slides here */}
74 |
75 |
76 | // Make sure you render into a full-page container
77 | ReactDOM.render( , container)`}
78 |
79 |
80 |
86 |
87 | Slide Backgrounds
88 |
89 | Presa supports images, colors and custom elements
90 | as slide backgrounds
91 |
92 |
93 |
94 |
95 |
96 | {`// Use an image
97 |
98 |
99 | // Add an overlay
100 |
101 |
102 | // Or custom CSS prop
103 | `}
105 |
106 |
107 |
114 | }
115 | fade={0.2}
116 | centered
117 | >
118 |
119 | Video Backgrounds
120 | made with custom background elements
121 |
122 |
123 |
124 |
125 | {`// blocks are optional elements
126 | import { VideoBackground } from 'presa/blocks'
127 |
128 | // \`background\` accepts any valid React element —
129 | // allows to use custom backgrounds.
130 |
132 | } />
133 |
134 | // (you can pass YouTube link or link to a local file)`}
135 |
136 |
137 | (
141 |
146 | )}
147 | >
148 |
149 | Including Blocks
150 | Reusable components for your slides
151 |
152 |
153 | Presa ships with additional components that can be used in slides.
154 | These components are not added by default.
155 |
156 |
157 |
158 | Currently available blocks: {'H1'} ,{' '}
159 | {'H2'} , {'H3'} ,{' '}
160 | {'Code'}
161 |
162 |
163 |
164 |
165 |
166 | {`import { H1, H2, Code } from 'presa/blocks'
167 |
168 |
169 | JavaScript
170 | A beginner's guide
171 |
172 |
173 | {\'Object.new.tap(&:inspect);\'}
174 |
175 |
176 | `}
177 |
178 |
179 |
180 |
181 | Check out more
182 | Let us know if you want to use Presa for your talk!
183 |
184 | https://github.com/molefrog/presa
185 |
186 |
187 |
188 | )
189 |
190 | const NumberedNumber = styled.div`
191 | width: 50px;
192 | height: 50px;
193 | border-radius: 6px;
194 | font-size: 26px;
195 | font-weight: bold;
196 | display: flex;
197 | align-items: center;
198 | justify-content: center;
199 | margin: 10px 0;
200 |
201 | color: ${props => props.color};
202 | border: 3px solid ${props => props.color};
203 | text-shadow: none;
204 | `
205 |
206 | const NumberedCont = styled.div`
207 | display: flex;
208 | flex-flow: column;
209 | align-items: ${props => (props.centered ? 'center' : 'flex-start')};
210 | text-align: ${props => (props.centered ? 'center' : 'left')};
211 |
212 | padding-bottom: 100px;
213 |
214 | ${props =>
215 | props.inverse &&
216 | `
217 | color: white;
218 | text-shadow: 1px 2px rgba(0,0,0,0.6);`};
219 | `
220 |
221 | const Numbered = props => (
222 |
223 |
224 | {props.number}
225 |
226 | {props.children}
227 |
228 | )
229 |
230 | const Footnote = styled.div`
231 | color: #999;
232 | font-size: 18px;
233 | `
234 |
235 | const PresaTitle = styled(H1)`
236 | color: ${primaryColor};
237 | `
238 |
239 | const PresaSlogan = styled(H3)`
240 | color: #444;
241 | margin-bottom: 40px;
242 | `
243 |
244 | const StarOnGithub = styled.div`
245 | margin-top: 20px;
246 | margin-bottom: 90px;
247 | `
248 |
249 | const Description = styled.p`
250 | margin: 40px 0;
251 | text-align: left;
252 | line-height: 1.5;
253 | `
254 |
255 | const InlineCode = styled.code`
256 | letter-spacing: -0.5px;
257 | background: rgba(60, 89, 255, 0.07);
258 | color: ${primaryColor};
259 | padding: 3px 8px;
260 | border-radius: 3px;
261 | `
262 |
263 | const PresaIcon = styled(Presa)`
264 | display: inline-block;
265 | margin-right: 14px;
266 | width: 40px;
267 | height: 40px;
268 | `
269 |
270 | // to prevent additional scrollbars
271 | injectGlobal`
272 | body {
273 | margin: 0;
274 | padding: 0;
275 | }
276 | `
277 |
278 | const rerenderApp = () => {
279 | const container = document.getElementById('container')
280 |
281 | // clean up and render
282 | ReactDOM.unmountComponentAtNode(container)
283 | ReactDOM.render( , container)
284 | }
285 |
286 | if (module.hot) {
287 | module.hot.accept(rerenderApp)
288 | }
289 |
290 | document.addEventListener('DOMContentLoaded', () => {
291 | rerenderApp()
292 | })
293 |
--------------------------------------------------------------------------------
/demo/sidebar-layout.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 |
4 | import styled from 'styled-components'
5 |
6 | const SidebarLayout = props => {
7 | const proportion = props.proportion || '4/5'
8 | let [left, right] = proportion.split('/')
9 |
10 | const isRight = props.position === 'right'
11 |
12 | const leftRatio = 100.0 * left / (left + right)
13 | const rightRatio = 100.0 * left / (left + right)
14 |
15 | return (
16 |
17 |
23 |
24 | {props.children}
25 |
26 |
27 | )
28 | }
29 |
30 | const Side = styled.div`
31 | background: url(${props => props.background});
32 | background-size: cover;
33 | background-position: center;
34 |
35 | flex-grow: ${props => props.weight};
36 | flex-basis: ${props => props.percentage}%;
37 |
38 | order: ${props => (props.isRight ? 2 : 0)};
39 | `
40 |
41 | const Content = styled.div`
42 | padding: 2.5em 3em;
43 |
44 | flex-grow: ${props => props.weight};
45 | flex-basis: ${props => props.percentage}%;
46 | `
47 |
48 | const Container = styled.div`
49 | height: 100%;
50 | width: 100%;
51 |
52 | display: flex;
53 | `
54 |
55 | export default SidebarLayout
56 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "presa",
3 | "version": "1.0.3",
4 | "description": "Stylish React presentation framework.",
5 | "main": "lib/index.js",
6 | "repository": "https://github.com/molefrog/presa.git",
7 | "author": "Alexey Taktarov ",
8 | "license": "MIT",
9 | "scripts": {
10 | "build": "babel src --out-dir lib",
11 | "dev": "parcel demo/index.html --open",
12 | "precommit": "lint-staged"
13 | },
14 | "peerDependencies": {
15 | "react": "^15.0.0 || ^16.0.0",
16 | "styled-components": "^2.0.0"
17 | },
18 | "devDependencies": {
19 | "babel-cli": "^6.26.0",
20 | "babel-core": "^6.26.0",
21 | "babel-eslint": "^8.2.2",
22 | "babel-jest": "^22.4.3",
23 | "babel-plugin-transform-class-properties": "^6.24.1",
24 | "babel-plugin-transform-object-rest-spread": "^6.26.0",
25 | "babel-preset-env": "^1.6.1",
26 | "babel-preset-react": "^6.24.1",
27 | "eslint": "^4.19.1",
28 | "eslint-config-prettier": "^2.9.0",
29 | "eslint-plugin-jest": "^21.15.0",
30 | "eslint-plugin-react": "^7.7.0",
31 | "husky": "^0.14.3",
32 | "jest": "^22.4.3",
33 | "lint-staged": "^7.0.0",
34 | "parcel-bundler": "^1.7.0",
35 | "prettier": "^1.11.1",
36 | "prop-types": "^15.6.1",
37 | "react": "^16.3.0",
38 | "react-dom": "^16.3.0",
39 | "react-syntax-highlighter": "^7.0.2",
40 | "react-test-renderer": "^16.3.0",
41 | "styled-components": "^3.2.3"
42 | },
43 | "dependencies": {
44 | "create-react-context": "^0.2.1",
45 | "get-youtube-id": "^1.0.0"
46 | },
47 | "lint-staged": {
48 | "*.{js,json,css,md}": ["prettier --write", "git add"]
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/assets/icons.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len */
2 | import React from 'react'
3 |
4 | export const Presa = props => (
5 |
12 |
13 |
17 |
23 |
29 |
35 |
36 |
37 | )
38 |
39 | export const Navigation = props => (
40 |
41 |
46 |
47 | )
48 |
49 | export const LeftArrow = props => (
50 |
51 |
56 |
57 | )
58 |
59 | export const RightArrow = props => (
60 |
61 |
66 |
67 | )
68 |
69 | export const Fullscreen = props => (
70 |
71 |
72 |
73 |
74 |
75 | )
76 |
--------------------------------------------------------------------------------
/src/assets/raw/arrow-left.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/raw/arrow-right.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/raw/fullscreen.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/raw/presa-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/molefrog/presa/31a68fd268c4af880d8f27c0298272f23f9fc304/src/assets/raw/presa-logo.png
--------------------------------------------------------------------------------
/src/assets/raw/presa-ui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/molefrog/presa/31a68fd268c4af880d8f27c0298272f23f9fc304/src/assets/raw/presa-ui.png
--------------------------------------------------------------------------------
/src/assets/raw/presa.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/raw/toc.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/blocks/code/code.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import styled, { withTheme } from 'styled-components'
3 | import PropTypes from 'prop-types'
4 |
5 | import defaultColorScheme from './color-scheme'
6 |
7 | const codeFactory = Highlight => {
8 | const defaultLineHeight = 1.4
9 |
10 | // Override additional styles without
11 | // touching the theme object.
12 | const StyledHighlight = styled(Highlight)`
13 | font-size: ${props => props.fontSize}px;
14 | line-height: ${defaultLineHeight};
15 | text-align: left;
16 |
17 | &,
18 | code,
19 | pre {
20 | font-family: ${props => props.theme.monoFont};
21 | }
22 | `
23 |
24 | class Code extends Component {
25 | static propTypes = {
26 | language: PropTypes.string,
27 | fontSize: PropTypes.number,
28 | children: PropTypes.string
29 | }
30 |
31 | static defaultProps = {
32 | // why not?
33 | language: 'javascript'
34 | }
35 |
36 | getSyntaxTheme() {
37 | const overridenTheme = this.props.theme.syntaxHighlight
38 |
39 | // Allow to override syntax highlighting theme
40 | // using global styled-components theme settings.
41 | if (overridenTheme) {
42 | return overridenTheme
43 | }
44 |
45 | return defaultColorScheme
46 | }
47 |
48 | render() {
49 | const { className, language, theme } = this.props
50 | const code = this.props.children
51 |
52 | // Use given font size or fall back to theme defaults
53 | const fontSize = this.props.fontSize || theme.slide.baseFontSize
54 |
55 | return (
56 |
62 | {code}
63 |
64 | )
65 | }
66 | }
67 |
68 | // Make sure the component has access to `theme` prop
69 | return withTheme(Code)
70 | }
71 |
72 | export default codeFactory
73 |
--------------------------------------------------------------------------------
/src/blocks/code/color-scheme.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This default syntax highligher color
3 | * scheme is based on Solarized theme from
4 | * `react-syntax-highlighter` package (for Prism).
5 | */
6 |
7 | // Base color
8 | const textColor = '#4d4d4c'
9 |
10 | // Gray for comments
11 | const commentColor = '#818181'
12 |
13 | // Indigo blue
14 | const entityColor = '#6582C9'
15 |
16 | // Green
17 | const keywordColor = '#718c00'
18 |
19 | // Trinidad red
20 | const operatorColor = '#CA4D26'
21 |
22 | const syntaxTheme = {
23 | hljs: {
24 | color: textColor
25 | },
26 | comment: {
27 | color: commentColor
28 | },
29 | prolog: {
30 | color: commentColor
31 | },
32 | doctype: {
33 | color: commentColor
34 | },
35 | cdata: {
36 | color: commentColor
37 | },
38 | property: {
39 | color: entityColor
40 | },
41 | tag: {
42 | color: entityColor
43 | },
44 | boolean: {
45 | color: entityColor
46 | },
47 | number: {
48 | color: entityColor
49 | },
50 | constant: {
51 | color: entityColor
52 | },
53 | symbol: {
54 | color: entityColor
55 | },
56 | deleted: {
57 | color: entityColor
58 | },
59 | function: {
60 | color: entityColor
61 | },
62 | selector: {
63 | color: keywordColor
64 | },
65 | 'attr-name': {
66 | color: keywordColor
67 | },
68 | string: {
69 | color: keywordColor
70 | },
71 | char: {
72 | color: keywordColor
73 | },
74 | builtin: {
75 | color: keywordColor
76 | },
77 | url: {
78 | color: keywordColor
79 | },
80 | inserted: {
81 | color: keywordColor
82 | },
83 | atrule: {
84 | color: keywordColor
85 | },
86 | 'attr-value': {
87 | color: keywordColor
88 | },
89 | keyword: {
90 | color: keywordColor
91 | },
92 | regex: {
93 | color: operatorColor
94 | },
95 | variable: {
96 | color: operatorColor
97 | },
98 | operator: {
99 | color: operatorColor
100 | }
101 | }
102 |
103 | export default syntaxTheme
104 |
--------------------------------------------------------------------------------
/src/blocks/code/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import codeFactory from './code'
3 |
4 | const warning =
5 | 'The `Code` component relies on `react-syntax-highlighter` ' +
6 | 'package. Please install it via the package manager.'
7 |
8 | // Use warning as a fallback
9 | let Code = () => React.createElement('code', null, warning)
10 |
11 | try {
12 | const highlight = require('react-syntax-highlighter/prism')
13 | Code = codeFactory(highlight.default)
14 | } catch (_) {
15 | // show additional warning in the console
16 | console.warn(warning)
17 | }
18 |
19 | export default Code
20 |
--------------------------------------------------------------------------------
/src/blocks/index.js:
--------------------------------------------------------------------------------
1 | // Export individual building blocks
2 | import Code from './code'
3 |
4 | export * from './typography'
5 | export { Code }
6 |
--------------------------------------------------------------------------------
/src/blocks/typography.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | // Don't style anything after h3
4 | const maxHeading = 3
5 |
6 | const calcFontSize = props => {
7 | const scale = props.theme.slide.fontScale || 1.0
8 | const level = props.level || 0
9 |
10 | // [0..maxHeading]
11 | const pow = Math.min(Math.max(0, maxHeading - level + 1), maxHeading)
12 |
13 | const size = Math.pow(scale, pow)
14 | return `${size}em`
15 | }
16 |
17 | export const Header = styled.h1`
18 | font-size: ${calcFontSize};
19 | font-weight: ${props => props.weight};
20 | color: ${props => props.color};
21 | line-height: 1.2;
22 | margin: 0.2em 0;
23 | `
24 |
25 | export const makeHeader = (tag, level, weight) =>
26 | Header.withComponent(tag).extend.attrs({ level, weight })``
27 |
28 | // Header components
29 | export const H1 = makeHeader('h1', 1, 'bold')
30 | export const H2 = makeHeader('h2', 2, 'normal')
31 | export const H3 = makeHeader('h3', 3, 'normal')
32 | export const H4 = makeHeader('h4', 4, 'normal')
33 |
34 | // Friendly aliases
35 | export const Title = H1
36 | export const Subtitle = H2
37 | export const Caption = H3
38 |
--------------------------------------------------------------------------------
/src/blocks/video-background.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import styled from 'styled-components'
4 | import getYouTubeId from 'get-youtube-id'
5 |
6 | import { BaseBackground as BaseBg } from '../components/slide/background'
7 |
8 | // Serializes a hash of settings into YouTube query
9 | // compatible format.
10 | // { foo: true } => foo=1
11 | export const makeQuery = (options = {}) => {
12 | return Object.entries(options)
13 | .map(pair => {
14 | let [k, v] = pair
15 | if (typeof v === 'boolean') v = Number(v)
16 |
17 | return `${k}=${v}`
18 | })
19 | .join('&')
20 | }
21 |
22 | class VideoBackground extends React.Component {
23 | static propTypes = {
24 | className: PropTypes.string,
25 | src: PropTypes.string.isRequired,
26 | autoPlay: PropTypes.bool,
27 | controls: PropTypes.bool,
28 | loop: PropTypes.bool,
29 | mute: PropTypes.bool
30 | }
31 |
32 | static defaultProps = {
33 | autoPlay: true,
34 | controls: false,
35 | loop: true,
36 | mute: false
37 | }
38 |
39 | renderYouTube(videoId) {
40 | const baseUrl = '//www.youtube.com/embed/'
41 | const { controls, loop, autoPlay, mute, className } = this.props
42 |
43 | const query = makeQuery({
44 | autoplay: autoPlay,
45 | showinfo: false,
46 | controls,
47 | loop,
48 | mute
49 | })
50 |
51 | const videoSrc = `${baseUrl}${videoId}?${query}`
52 |
53 | return
54 | }
55 |
56 | render() {
57 | // try to extract youtube id first
58 | const ytVideo = getYouTubeId(this.props.src, { fuzzy: false })
59 |
60 | if (ytVideo) {
61 | return this.renderYouTube(ytVideo)
62 | }
63 |
64 | return
65 | }
66 | }
67 |
68 | const IFrame = BaseBg.withComponent('iframe').extend`
69 | border: none;
70 |
71 | // when video is being loaded
72 | background: black;
73 | `
74 |
75 | const Video = BaseBg.withComponent('video')
76 |
77 | export default VideoBackground
78 |
--------------------------------------------------------------------------------
/src/components/birds-eye-mode/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled, { css } from 'styled-components'
3 |
4 | import Controls from '../controls'
5 | import { Slide } from '../slide'
6 |
7 | const gridSlideWidth = 320
8 |
9 | class NavigationMode extends React.Component {
10 | constructor(props) {
11 | super(props)
12 | this.state = {
13 | isSlideLoaded: {}
14 | }
15 | }
16 |
17 | loadSlide = (index, loaded = true) => {
18 | this.setState(state => ({
19 | isSlideLoaded: {
20 | ...state.isSlideLoaded,
21 | [String(index)]: loaded
22 | }
23 | }))
24 | }
25 |
26 | handleSlideClick = index => {
27 | this.props.switchSlide(index)
28 | this.props.toggleBirdsEye()
29 | }
30 |
31 | handleIntersection = (entries, observer) => {
32 | // Get the list of slides ids that are
33 | // currently visible within the viewport.
34 | const visible = entries
35 | .filter(entry => entry.isIntersecting)
36 | .map(entry => entry.target.dataset.slideId)
37 |
38 | visible.forEach(id => this.loadSlide(id))
39 | }
40 |
41 | componentDidMount() {
42 | const { IntersectionObserver } = window
43 |
44 | if (!IntersectionObserver || !this._root) {
45 | // Fallback for older browsers:
46 | // load everything up!
47 | this.props.slides.forEach((_, idx) => this.loadSlide(idx))
48 | return
49 | }
50 |
51 | const options = {
52 | threshold: 0.5
53 | }
54 |
55 | this._observer = new IntersectionObserver(this.handleIntersection, options)
56 |
57 | // Fire observer up for slide items
58 | ;[].forEach.call(this._root.querySelectorAll('[data-slide-id]'), el =>
59 | this._observer.observe(el)
60 | )
61 | }
62 |
63 | componentWillUnmount() {
64 | const { _observer } = this
65 |
66 | if (_observer && _observer.disconnect) {
67 | // Stop observing elements when unmounted
68 | _observer.disconnect()
69 | }
70 | }
71 |
72 | render() {
73 | const { slides, currentSlide } = this.props
74 |
75 | return (
76 | (this._root = el)}>
77 |
78 | {slides.map((slide, index) => {
79 | const isLoaded = !!this.state.isSlideLoaded[index.toString()]
80 |
81 | return (
82 | this.handleSlideClick(index)}
84 | key={index}
85 | data-slide-id={index}
86 | >
87 |
95 | {slide.name}
96 |
97 | )
98 | })}
99 |
100 |
101 | )
102 | }
103 | }
104 |
105 | const SlideName = styled.div`
106 | color: ${props => props.theme.darkGrayColor};
107 | text-align: center;
108 | padding: 0px 10px;
109 | margin-top: 10px;
110 | `
111 |
112 | const SlideItem = styled.div`
113 | &:hover {
114 | ${SlideName} {
115 | color: black;
116 | }
117 | }
118 | `
119 |
120 | const SlideCard = styled(Slide)`
121 | border-radius: 4px;
122 | overflow: hidden;
123 | cursor: pointer;
124 | box-shadow: 0px 0px 1px 0px rgba(0, 0, 0, 0.15),
125 | 0px 1px 6px 0px rgba(0, 0, 0, 0.03)
126 | ${props =>
127 | props.isCurrent &&
128 | css`
129 | , 0px 0px 0px 4px ${props => props.theme.primaryColor};
130 | `};
131 | `
132 |
133 | const Container = styled.div`
134 | max-width: 1100px;
135 | margin: 0px auto;
136 | padding: 32px 12px 64px 12px;
137 | `
138 |
139 | const Grid = styled.div`
140 | display: grid;
141 |
142 | grid-column-gap: 20px;
143 | grid-row-gap: 24px;
144 | justify-content: center;
145 |
146 | grid-template-columns: repeat(auto-fill, ${gridSlideWidth}px);
147 | grid-auto-flow: dense;
148 | `
149 |
150 | export default NavigationMode
151 |
--------------------------------------------------------------------------------
/src/components/built-with.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 |
4 | import { Presa } from '../assets/icons'
5 |
6 | const Copyright = ({ size = 14 }) => (
7 |
12 |
13 |
14 | Built with Presa.
15 |
16 |
17 | )
18 |
19 | const PresaIcon = styled(Presa)`
20 | margin-right: 5px;
21 | `
22 |
23 | const BuiltWith = styled.a`
24 | text-decoration: none;
25 | color: black;
26 |
27 | display: flex;
28 | align-items: center;
29 | font-size: ${props => props.size}px;
30 | font-family: ${props => props.theme.monoFont};
31 |
32 | &:hover {
33 | color: ${props => props.theme.primaryColor};
34 | ${PresaIcon} {
35 | path {
36 | stroke: ${props => props.theme.primaryColor};
37 | }
38 | }
39 | }
40 | `
41 |
42 | export default Copyright
43 |
--------------------------------------------------------------------------------
/src/components/controls/control-button.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 | import styled, { css } from 'styled-components'
3 |
4 | const ControlButton = styled.button`
5 | /* Base geometry */
6 | width: ${props => props.size}px;
7 | height: ${props => props.size}px;
8 | border-radius: ${props => props.size}px;
9 |
10 | /* Center the content */
11 | display: inline-flex;
12 | justify-content: center;
13 | align-items: center;
14 |
15 | cursor: pointer;
16 | border: none;
17 | outline: none;
18 | transition: all 0.2s ease;
19 |
20 | /* Default state */
21 | svg {
22 | transition: transform 0.15s ease;
23 | }
24 |
25 | path {
26 | transition: fill 0.2s ease;
27 | fill: ${props => props.theme.darkGrayColor};
28 | }
29 |
30 | /* Hovered state */
31 | &:hover {
32 | background: rgba(0, 0, 0, 0.015);
33 | path {
34 | fill: ${props => props.theme.textColor};
35 | }
36 | }
37 |
38 | /* Active state */
39 | &:active {
40 | background: rgba(0, 0, 0, 0.03);
41 | svg {
42 | transform: ${props => props.activeIconTransform || 'none'};
43 | }
44 | }
45 |
46 | ${props =>
47 | props.toggled &&
48 | css`
49 | &,
50 | &:hover {
51 | path {
52 | fill: ${props => props.theme.primaryColor};
53 | }
54 | }
55 | `};
56 |
57 | ${props =>
58 | props.disabled &&
59 | `
60 | opacity: 0.2;
61 | pointer-events: none;
62 | `};
63 | `
64 |
65 | ControlButton.propTypes = {
66 | size: PropTypes.number,
67 | disabled: PropTypes.bool
68 | }
69 |
70 | ControlButton.defaultProps = {
71 | size: 40
72 | }
73 |
74 | export default ControlButton
75 |
--------------------------------------------------------------------------------
/src/components/controls/control-group.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const ControlGroup = styled.div`
4 | display: inline-flex;
5 | background: white;
6 | box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.1), 0 2px 8px 0 rgba(0, 0, 0, 0.04);
7 | border-radius: 50px;
8 | padding: 4px;
9 | `
10 |
11 | export default ControlGroup
12 |
--------------------------------------------------------------------------------
/src/components/controls/fullscreen-toggle.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import ControlGroup from './control-group'
4 | import ControlButton from './control-button'
5 | import { Fullscreen as FullscreenIcon } from '../../assets/icons'
6 |
7 | const FullscreenToggle = ({ className, onClick }) => (
8 |
9 |
10 |
11 |
12 |
13 | )
14 |
15 | export default FullscreenToggle
16 |
--------------------------------------------------------------------------------
/src/components/controls/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 |
4 | import SlideSwitcher from './slide-switcher'
5 | import FullscreenToggle from './fullscreen-toggle'
6 | import TocToggle from './toc-toggle'
7 |
8 | const ControlsRoot = props => (
9 |
10 |
11 |
12 |
13 |
14 | props.toggleToc()} />
15 |
16 |
17 | )
18 |
19 | const Controls = styled.div`
20 | display: flex;
21 | align-items: center;
22 | `
23 |
24 | const AdditionalControls = styled.div`
25 | margin-left: 8px;
26 |
27 | > div {
28 | margin-right: 6px;
29 | }
30 | `
31 |
32 | export default ControlsRoot
33 |
--------------------------------------------------------------------------------
/src/components/controls/mini-progress.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import styled from 'styled-components'
5 |
6 | class MiniProgress extends React.Component {
7 | static propTypes = {
8 | current: PropTypes.number.isRequired,
9 | total: PropTypes.number.isRequired,
10 | height: PropTypes.number,
11 | width: PropTypes.number
12 | }
13 |
14 | static defaultProps = {
15 | height: 4,
16 | width: 40
17 | }
18 |
19 | render() {
20 | const { total, current, width, height } = this.props
21 | const percentage = total ? (current + 1) / total : 0.0
22 |
23 | return (
24 |
25 |
26 |
27 | )
28 | }
29 | }
30 |
31 | const progressBackground = '#dddddd'
32 |
33 | const ProgressBox = styled.div`
34 | height: ${props => props.height}px;
35 | border-radius: ${props => props.height}px;
36 | width: ${props => props.width}px;
37 |
38 | background: ${progressBackground};
39 | position: relative;
40 | overflow: hidden;
41 | `
42 |
43 | const Progress = styled.div`
44 | position: absolute;
45 | height: 100%;
46 | width: 0;
47 | left: 0;
48 |
49 | background: ${props => props.theme.primaryColor};
50 | border-radius: ${props => props.radius}px;
51 | `
52 |
53 | export default MiniProgress
54 |
--------------------------------------------------------------------------------
/src/components/controls/slide-switcher.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 |
4 | import ControlGroup from './control-group'
5 | import ControlButton from './control-button'
6 | import { LeftArrow, RightArrow } from '../../assets/icons'
7 |
8 | import MiniProgress from './mini-progress'
9 |
10 | class SlideSwitcher extends React.Component {
11 | render() {
12 | const {
13 | className,
14 | slide,
15 | slides,
16 | showNextSlide,
17 | showPrevSlide,
18 | toggleBirdsEye
19 | } = this.props
20 |
21 | return (
22 |
23 | showPrevSlide()}
25 | disabled={slide.isFirst}
26 | activeIconTransform="translateX(-1px)"
27 | >
28 |
29 |
30 |
31 | toggleBirdsEye()}
33 | current={slide.index}
34 | total={slides.length}
35 | />
36 |
37 | showNextSlide()}
39 | disabled={slide.isLast}
40 | activeIconTransform="translateX(1px)"
41 | >
42 |
43 |
44 |
45 | )
46 | }
47 | }
48 |
49 | const CurrentSlide = ({ current, total, onClick }) => (
50 |
51 |
52 | {current + 1}
53 | /{total}
54 |
55 |
56 |
57 | )
58 |
59 | const TotalLabel = styled.span`
60 | color: ${props => props.theme.mutedTextColor};
61 | `
62 |
63 | const Navigation = styled.div`
64 | font-family: ${props => props.theme.monoFont};
65 | color: ${props => props.theme.textColor};
66 | user-select: none;
67 | font-size: 12px;
68 | margin-bottom: 4px;
69 | `
70 |
71 | const NavigationOuter = styled.div`
72 | align-self: center;
73 |
74 | padding: 8px 6px;
75 | margin: 0 2px;
76 | border-radius: 6px;
77 | cursor: pointer;
78 |
79 | display: flex;
80 | flex-flow: column nowrap;
81 | align-items: center;
82 |
83 | &:hover {
84 | background: rgba(0, 0, 0, 0.02);
85 | }
86 | `
87 |
88 | export default SlideSwitcher
89 |
--------------------------------------------------------------------------------
/src/components/controls/toc-toggle.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import ControlGroup from './control-group'
4 | import ControlButton from './control-button'
5 | import { Navigation as TocIcon } from '../../assets/icons'
6 |
7 | const FullscreenToggle = ({ toggled, className, onClick }) => (
8 |
9 |
10 |
11 |
12 |
13 | )
14 |
15 | export default FullscreenToggle
16 |
--------------------------------------------------------------------------------
/src/components/fragment/constants.js:
--------------------------------------------------------------------------------
1 | export const ALL_FRAGMENTS = Infinity
2 | export const NO_FRAGMENTS = -Infinity
3 |
--------------------------------------------------------------------------------
/src/components/fragment/fragment.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import Manager from './manager'
5 | import styled from 'styled-components'
6 |
7 | const OpacityBehaviour = styled.div`
8 | transition: opacity 0.2s ease-in;
9 | display: ${props => (props.inline ? 'inline-block' : 'block')};
10 | opacity: ${props => (props.active ? 1 : 0)};
11 | `
12 |
13 | class Fragment extends Component {
14 | static propTypes = {
15 | behaviour: PropTypes.any,
16 | index: PropTypes.number,
17 | manager: PropTypes.object.isRequired
18 | }
19 |
20 | static defaultProps = {
21 | behaviour: OpacityBehaviour
22 | }
23 |
24 | constructor(props) {
25 | super(props)
26 | this._instance = props.manager.registerFragment(props.index)
27 | }
28 |
29 | componentWillUnmount() {
30 | this._instance.unregister()
31 | }
32 |
33 | render() {
34 | const { manager, behaviour, ...restProps } = this.props
35 | const Behaviour = behaviour
36 |
37 | const isActive = manager.isIndexActive(this._instance.index)
38 | return
39 | }
40 | }
41 |
42 | const FragmentConnected = props => (
43 |
44 | {args => }
45 |
46 | )
47 |
48 | export default FragmentConnected
49 |
--------------------------------------------------------------------------------
/src/components/fragment/index.js:
--------------------------------------------------------------------------------
1 | import Fragment from './fragment'
2 |
3 | export default Fragment
4 |
--------------------------------------------------------------------------------
/src/components/fragment/manager.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import createReactContext from 'create-react-context'
4 |
5 | import { ALL_FRAGMENTS, NO_FRAGMENTS } from './constants'
6 | import nextIndex from './next-index'
7 |
8 | const Context = createReactContext()
9 |
10 | class FragmentManager extends Component {
11 | static Consumer = Context.Consumer
12 |
13 | static propTypes = {
14 | children: PropTypes.any,
15 | initialIndex: PropTypes.number
16 | }
17 |
18 | // by default all fragments are activated
19 | static defaultProps = {
20 | initialIndex: ALL_FRAGMENTS
21 | }
22 |
23 | constructor(props) {
24 | super(props)
25 |
26 | this._lastAutoIndex = -1
27 | this._fragments = []
28 |
29 | this.state = { index: props.initialIndex }
30 | }
31 |
32 | navigate(shift = 1) {
33 | const indexes = this._fragments.map(f => f.index)
34 |
35 | // no fragments found, skip
36 | if (!indexes.length) {
37 | return false
38 | }
39 |
40 | const nextValue = nextIndex(
41 | [NO_FRAGMENTS, ...indexes],
42 | this.state.index,
43 | shift
44 | )
45 |
46 | // switch to the next fragment in the list
47 | if (nextValue !== null) {
48 | this.setState({ index: nextValue })
49 | return true
50 | }
51 |
52 | // return false otherwise. a signal that
53 | // the next slide should be shown instead.
54 | return false
55 | }
56 |
57 | isIndexActive(index) {
58 | return index <= this.state.index
59 | }
60 |
61 | registerFragment(options = {}) {
62 | let index = options.index
63 |
64 | if (typeof index === 'undefined') {
65 | index = ++this._lastAutoIndex
66 | }
67 |
68 | const fragmentId = Symbol()
69 |
70 | const registered = {
71 | id: fragmentId,
72 | index: index,
73 | unregister: () => this.unregisterFragment(fragmentId)
74 | }
75 |
76 | this._fragments.push(registered)
77 | return registered
78 | }
79 |
80 | unregisterFragment(id) {
81 | // remove the fragment reference from the list
82 | const idx = this._fragments.findIndex(f => f.id === id)
83 | idx && this._fragments.splice(idx, 1)
84 | }
85 |
86 | render() {
87 | return (
88 |
89 | {this.props.children}
90 |
91 | )
92 | }
93 | }
94 |
95 | export default FragmentManager
96 |
--------------------------------------------------------------------------------
/src/components/fragment/next-index.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This function is used in order to calculate the index
3 | * of the next fragment.
4 | * `variants` — a list of indexes, e.g. [1, 0, 10]
5 | * `current` — current index, may not be present in the list.
6 | * `shift` — how many indexes to skip, use negative to move
7 | * backwards.
8 | *
9 | * Returns `null` if the index is out of bounds.
10 | */
11 | const nextIndex = (variants, current, shift = 1) => {
12 | const sorted = unique(variants).sort()
13 | const closest = closestIndex(sorted, current, shift > 0)
14 |
15 | if (closest === null) {
16 | return null
17 | }
18 |
19 | return elementAt(sorted, closest + shift)
20 | }
21 |
22 | const closestIndex = (sortedList, elem, right = true) => {
23 | const predicate = x => (right ? x >= elem : elem >= x)
24 | const sliced = sortedList.filter(predicate)
25 |
26 | if (!sliced.length) {
27 | return null
28 | }
29 |
30 | const closestElement = right ? sliced[0] : sliced[sliced.length - 1]
31 | return sortedList.indexOf(closestElement)
32 | }
33 |
34 | const elementAt = (array, index) => {
35 | if (index < 0 || index >= array.length) return null
36 | return array[index]
37 | }
38 |
39 | // returns a list of unique elements of the array
40 | const unique = list => {
41 | let result = []
42 | list.forEach(x => result.indexOf(x) === -1 && result.push(x))
43 | return result
44 | }
45 |
46 | export default nextIndex
47 |
--------------------------------------------------------------------------------
/src/components/fullscreen-mode/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 |
4 | import { Slide } from '../slide'
5 |
6 | const KEY_ESC = 27
7 |
8 | class FullscreenMode extends React.Component {
9 | constructor(props) {
10 | super(props)
11 | this.state = {
12 | screenWidth: document.documentElement.clientWidth,
13 | screenHeight: document.documentElement.clientHeight
14 | }
15 | }
16 |
17 | componentWillMount() {
18 | this.onResize()
19 | }
20 |
21 | // TODO: implement an event throttling
22 | onResize = () => {
23 | this.setState({
24 | screenWidth: document.documentElement.clientWidth,
25 | screenHeight: document.documentElement.clientHeight
26 | })
27 | }
28 |
29 | onKeyDown = event => {
30 | if (event.keyCode === KEY_ESC) {
31 | this.props.toggleFullscreen()
32 | }
33 | }
34 |
35 | componentDidMount() {
36 | window.addEventListener('resize', this.onResize)
37 | document.body.addEventListener('keydown', this.onKeyDown)
38 | }
39 |
40 | componentWillUnmount() {
41 | window.removeEventListener('resize', this.onResize)
42 | document.body.removeEventListener('keydown', this.onKeyDown)
43 | }
44 |
45 | render() {
46 | const { slide, slideWidth, slideHeight } = this.props
47 |
48 | const screenGeometry = {
49 | width: this.state.screenWidth,
50 | height: this.state.screenHeight
51 | }
52 |
53 | return (
54 |
55 |
56 |
63 |
64 |
65 | )
66 | }
67 | }
68 |
69 | const Layer = styled.div`
70 | position: absolute;
71 | top: 0;
72 | left: 0;
73 | width: 100%;
74 | height: 100%;
75 |
76 | display: flex;
77 | flex-flow: row nowrap;
78 | align-items: center;
79 | justify-content: center;
80 | `
81 |
82 | const Fullscreen = styled.div`
83 | height: 100%;
84 | width: 100%;
85 | overflow: hidden;
86 | `
87 |
88 | export default FullscreenMode
89 |
--------------------------------------------------------------------------------
/src/components/global-background.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { withTheme } from 'styled-components'
4 |
5 | // Helper component. Update the body background
6 | // according to the `background` prop.
7 | class BodyBackground extends React.Component {
8 | static propTypes = {
9 | background: PropTypes.string
10 | }
11 |
12 | constructor(props) {
13 | super(props)
14 | this.useBackground(props.background)
15 | }
16 |
17 | useBackground(prop) {
18 | const body = document.querySelector('body')
19 | body && (body.style.background = prop)
20 | }
21 |
22 | componentWillReceiveProps(nextProps) {
23 | if (nextProps.background !== this.props.background) {
24 | this.useBackground(nextProps.background)
25 | }
26 | }
27 |
28 | shouldComponentUpdate() {
29 | return false
30 | }
31 |
32 | // Don't render anything
33 | render() {
34 | return null
35 | }
36 | }
37 |
38 | const GlobalBackground = props => {
39 | let color = props.theme.backgroundColor
40 |
41 | if (props.isFullscreen) {
42 | color = props.theme.fullscreenBackgroundColor
43 | }
44 |
45 | return
46 | }
47 |
48 | export default withTheme(GlobalBackground)
49 |
--------------------------------------------------------------------------------
/src/components/presentation-container.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import styled, { ThemeProvider } from 'styled-components'
4 |
5 | import GlobalBackground from './global-background'
6 | import RemoteControl from './remote-control'
7 | import FullscreenMode from './fullscreen-mode'
8 | import SlideshowMode from './slideshow-mode'
9 | import BirdsEyeMode from './birds-eye-mode'
10 |
11 | // default theme for styled components
12 | import defaultTheme from '../theme'
13 |
14 | import { NO_FRAGMENTS, ALL_FRAGMENTS } from './fragment/constants'
15 |
16 | const modes = {
17 | FULLSCREEN: 'FULLSCREEN',
18 | SLIDESHOW: 'SLIDESHOW',
19 | BIRDSEYE: 'BIRDSEYE'
20 | }
21 |
22 | // Gets the current slide index from
23 | // the url hash e.g. /#10
24 | const extractIndexFromLocation = () => {
25 | const numbers = window.location.hash.replace(/\D/g, '')
26 | return parseInt(numbers) || 0
27 | }
28 |
29 | class Presentation extends Component {
30 | static propTypes = {
31 | name: PropTypes.string,
32 | aspectRatio: PropTypes.number,
33 | baseWidth: PropTypes.number,
34 | slides: PropTypes.array,
35 | theme: PropTypes.object,
36 | tableOfContents: PropTypes.bool,
37 | useFullscreenAPI: PropTypes.bool
38 | }
39 |
40 | static defaultProps = {
41 | name: 'An awesome presentation',
42 | aspectRatio: 16.0 / 9.0,
43 | baseWidth: 1066.0,
44 | tableOfContents: false,
45 | useFullscreenAPI: false,
46 | theme: {}
47 | }
48 |
49 | constructor(props) {
50 | super(props)
51 |
52 | const currentIndex = Math.min(
53 | props.slides.length,
54 | extractIndexFromLocation()
55 | )
56 |
57 | this.state = {
58 | slides: props.slides,
59 | presentationName: props.name,
60 | presentMode: modes.SLIDESHOW,
61 | currentSlide: currentIndex,
62 | initialFragment: NO_FRAGMENTS,
63 | showToc: props.tableOfContents,
64 |
65 | slideWidth: props.baseWidth,
66 | slideHeight: props.baseWidth / props.aspectRatio
67 | }
68 | }
69 |
70 | // Jump through `shift` number of slides/fragments
71 | navigate = shift => {
72 | const { slides, currentSlide } = this.state
73 |
74 | // first, talk to the current fragment manager
75 | // it returns `false` if no fragments left
76 | const manager = this._fragmentManager
77 | if (manager && manager.navigate(shift)) {
78 | return
79 | }
80 |
81 | // go to prev/next slide
82 | const id = currentSlide + shift
83 | const limited = Math.max(0, Math.min(id, slides.length - 1))
84 |
85 | const forwards = shift >= 0
86 | this.switchSlide(limited, forwards)
87 | }
88 |
89 | switchSlide = (id, forwards = true) => {
90 | window.location.hash = id.toString()
91 |
92 | // When moving forwards show no fragments first,
93 | // when going backwards all fragments activated first.
94 | const fragment = forwards ? NO_FRAGMENTS : ALL_FRAGMENTS
95 | this.setState({ currentSlide: id, initialFragment: fragment })
96 | }
97 |
98 | toggleFullscreen = goFullscreen => {
99 | const { useFullscreenAPI } = this.props
100 |
101 | if (typeof goFullscreen === 'undefined') {
102 | goFullscreen = !this.state.presentMode === modes.FULLSCREEN
103 | }
104 |
105 | if (goFullscreen && useFullscreenAPI) {
106 | const docEl = document.documentElement
107 |
108 | // Use browser's Fullscreen API
109 | if (docEl && docEl.webkitRequestFullscreen) {
110 | docEl.webkitRequestFullscreen()
111 | }
112 | }
113 |
114 | this.setState(() => ({
115 | presentMode: goFullscreen ? modes.FULLSCREEN : modes.SLIDESHOW
116 | }))
117 | }
118 |
119 | toggleBirdsEye = () => {
120 | this.setState(state => ({
121 | presentMode:
122 | state.presentMode === modes.BIRDSEYE ? modes.SLIDESHOW : modes.BIRDSEYE
123 | }))
124 | }
125 |
126 | toggleToc = shown => {
127 | const visible = typeof shown === 'undefined' ? !this.state.showToc : shown
128 |
129 | this.setState({
130 | showToc: visible
131 | })
132 | }
133 |
134 | setFragmentManager = manager => {
135 | this._fragmentManager = manager
136 | }
137 |
138 | getConnectedState() {
139 | const { slides, currentSlide, presentMode, initialFragment } = this.state
140 |
141 | return {
142 | ...this.state,
143 |
144 | slide: {
145 | ...slides[currentSlide],
146 | id: currentSlide,
147 | index: currentSlide,
148 | initialFragmentIndex: initialFragment,
149 | isFirst: currentSlide <= 0,
150 | isLast: currentSlide >= slides.length - 1,
151 | setFragmentManager: this.setFragmentManager
152 | },
153 |
154 | isSlideshow: presentMode === modes.SLIDESHOW,
155 | isFullscreen: presentMode === modes.FULLSCREEN,
156 | isBirdsEye: presentMode === modes.BIRDSEYE,
157 | toggleFullscreen: this.toggleFullscreen,
158 |
159 | switchSlide: this.switchSlide,
160 | toggleToc: this.toggleToc,
161 | toggleBirdsEye: this.toggleBirdsEye,
162 | showNextSlide: () => this.navigate(+1),
163 | showPrevSlide: () => this.navigate(-1)
164 | }
165 | }
166 |
167 | render() {
168 | const state = this.getConnectedState()
169 |
170 | const theme = {
171 | ...defaultTheme,
172 | ...this.props.theme
173 | }
174 |
175 | return (
176 |
177 |
178 |
179 |
180 | null}
184 | />
185 |
186 | {state.isFullscreen && }
187 | {state.isSlideshow && }
188 | {state.isBirdsEye && }
189 |
190 |
191 | )
192 | }
193 | }
194 |
195 | const GlobalContainer = styled.div`
196 | /* Fit the whole browser window */
197 | width: 100vw;
198 | height: 100vh;
199 |
200 | /* Setup typography */
201 | font-family: ${props => props.theme.baseFont};
202 | -webkit-font-smoothing: antialiased;
203 | -moz-osx-font-smoothing: grayscale;
204 |
205 | * {
206 | box-sizing: border-box;
207 | }
208 |
209 | color: ${props => props.theme.textColor};
210 | `
211 |
212 | export default Presentation
213 |
--------------------------------------------------------------------------------
/src/components/presentation.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import PresentationContainer from './presentation-container'
5 |
6 | class Presentation extends Component {
7 | static propTypes = {
8 | children: PropTypes.node.isRequired
9 | }
10 |
11 | getSlideStructure() {
12 | const { children } = this.props
13 |
14 | return React.Children.map(children, slideElement => {
15 | return {
16 | name: slideElement.props.name,
17 | description: slideElement.props.description,
18 | element: slideElement
19 | }
20 | })
21 | }
22 |
23 | render() {
24 | // We assume that slides don't change over
25 | // application lifetime. Normally this render
26 | // method will only be called once.
27 | const slides = this.getSlideStructure()
28 |
29 | // All futher logic runs inside `PresentationContainer`
30 | return
31 | }
32 | }
33 |
34 | export default Presentation
35 |
--------------------------------------------------------------------------------
/src/components/remote-control.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | // Provides an adapter for presentation remotes.
5 | //
6 | // Technically, remotes are very small keyboards with a limited
7 | // number of keys. The forward and backward buttons simply send the same
8 | // key codes as the cursor keys on a regular keyboard.
9 |
10 | // The button to blank the screen simply sends the letter 'B' (and if
11 | // you didn't know this, try hitting the 'B' key on your keyboard in your
12 | // presentation software).
13 | // (https://www.themobilepresenter.com/article.php/how-does-a-remote-work)
14 |
15 | const k = {
16 | KEY_LEFT: 37,
17 | KEY_RIGHT: 39,
18 | KEY_DOWN: 40,
19 | KEY_UP: 38,
20 | KEY_SPACE: 32,
21 |
22 | // some clickers supports it
23 | // for example, Logitech
24 | KEY_PAGE_UP: 33,
25 | KEY_PAGE_DOWN: 34,
26 |
27 | // Blank screen
28 | KEY_B: 66
29 | }
30 |
31 | class RemoteControl extends React.Component {
32 | static propTypes = {
33 | onNext: PropTypes.func.isRequired,
34 | onPrev: PropTypes.func.isRequired,
35 | onMute: PropTypes.func.isRequired
36 | }
37 |
38 | handleKeyDown = e => {
39 | // Go to the next slide: right,
40 | // down arrow or space
41 | if (
42 | e.keyCode === k.KEY_RIGHT ||
43 | e.keyCode === k.KEY_DOWN ||
44 | e.keyCode === k.KEY_SPACE ||
45 | e.keyCode === k.KEY_PAGE_DOWN
46 | ) {
47 | return this.props.onNext()
48 | }
49 |
50 | // Go to previous slide
51 | if (
52 | e.keyCode === k.KEY_LEFT ||
53 | e.keyCode === k.KEY_UP ||
54 | e.keyCode === k.KEY_PAGE_UP
55 | ) {
56 | return this.props.onPrev()
57 | }
58 |
59 | if (e.keyCode === k.KEY_B) {
60 | return this.props.onMute()
61 | }
62 | }
63 |
64 | componentDidMount() {
65 | document.body.addEventListener('keydown', this.handleKeyDown)
66 | }
67 |
68 | componentWillUnmount() {
69 | document.body.removeEventListener('keydown', this.handleKeyDown)
70 | }
71 |
72 | render() {
73 | return null
74 | }
75 | }
76 |
77 | export default RemoteControl
78 |
--------------------------------------------------------------------------------
/src/components/slide/background.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 |
4 | export const backgroundFor = props => {
5 | const { background, fade } = props
6 | const ownProps = { background, fade }
7 |
8 | if (!background) {
9 | return null
10 | }
11 |
12 | // React element passed, simply return it
13 | // This allows to specify custom background components.
14 | if (React.isValidElement(background)) {
15 | return background
16 | }
17 |
18 | if (typeof background === 'string') {
19 | // It's an image url
20 | const imgRx = /\.(jpe?g|gif|png|bmp|svg)$/i
21 |
22 | if (background.match(imgRx)) {
23 | return
24 | } else {
25 | // Interpret background as a value for
26 | // `background` css property.
27 | return
28 | }
29 | }
30 |
31 | const invalidTypeWarn =
32 | 'Invalid `background` prop passed. It can be either a ' +
33 | 'string (e.g. `blue` or `http://foo.io/image.jpg`) or ' +
34 | 'a valid React element.'
35 |
36 | console.warn(invalidTypeWarn)
37 | return null
38 | }
39 |
40 | export const BaseBackground = styled.div`
41 | width: 100%;
42 | height: 100%;
43 | `
44 |
45 | export const PlainBackground = BaseBackground.extend`
46 | ${props => props.raw && `background: ${props.raw}`};
47 |
48 | /*
49 | This linear gradient hack makes it possible to
50 | use an overlay together with the background image.
51 | */
52 | ${props =>
53 | props.image &&
54 | `background: linear-gradient(
55 | rgba(0, 0, 0, ${props.fade || 0.0}),
56 | rgba(0, 0, 0, ${props.fade || 0.0})
57 | ), url(${props.image}) center center / cover no-repeat`};
58 | `
59 |
--------------------------------------------------------------------------------
/src/components/slide/context.js:
--------------------------------------------------------------------------------
1 | import createReactContext from 'create-react-context'
2 |
3 | const Context = createReactContext()
4 | export default Context
5 |
--------------------------------------------------------------------------------
/src/components/slide/index.js:
--------------------------------------------------------------------------------
1 | import SlideDecl from './slide-decl'
2 | import Slide from './slide'
3 |
4 | export * from './layouts'
5 |
6 | export { Slide, SlideDecl }
7 |
--------------------------------------------------------------------------------
/src/components/slide/layouts.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 |
4 | // Base layout component —
5 | // fits an entire viewport
6 | const PlainLayout = styled.div`
7 | width: 100%;
8 | height: 100%;
9 | `
10 |
11 | const DefaultLayout = PlainLayout.extend`
12 | padding: ${props => props.theme.slide.layoutPadding};
13 | `
14 |
15 | // A slide layout centered both
16 | // vertically and horizontally
17 | const CenteredLayout = DefaultLayout.extend`
18 | display: flex;
19 | flex-direction: column;
20 | justify-content: center;
21 | align-items: center;
22 | text-align: center;
23 | `
24 |
25 | export { PlainLayout, DefaultLayout, CenteredLayout }
26 |
--------------------------------------------------------------------------------
/src/components/slide/placeholder.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import styled, { keyframes } from 'styled-components'
4 |
5 | import { Presa } from '../../assets/icons'
6 |
7 | const bouncing = keyframes`
8 | 0% { transform: none; }
9 | 25% { transform: translateY(-6px); }
10 | 95% { transform: translateY(2px); }
11 | 100% { transform: none; }
12 | `
13 |
14 | /*
15 | * This component will be used inside slides that are
16 | * not loaded yet.
17 | */
18 | const Placeholder = props => (
19 |
20 |
21 |
22 | )
23 |
24 | const Icon = styled(Presa)`
25 | will-change: transform;
26 | animation: ${bouncing} 1.4s infinite;
27 | `
28 |
29 | const Container = styled.div`
30 | width: 100%;
31 | height: 100%;
32 |
33 | display: flex;
34 | align-items: center;
35 | justify-content: center;
36 |
37 | path {
38 | stroke: ${props => props.theme.placeholderColor};
39 | }
40 | `
41 |
42 | export default Placeholder
43 |
--------------------------------------------------------------------------------
/src/components/slide/slide-decl.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import styled from 'styled-components'
5 |
6 | import { backgroundFor } from './background'
7 | import { DefaultLayout, CenteredLayout } from './layouts'
8 | import FragmentManager from '../fragment/manager'
9 |
10 | class Slide extends Component {
11 | static propTypes = {
12 | onBackgroundChange: PropTypes.func,
13 | centered: PropTypes.bool,
14 | className: PropTypes.string,
15 |
16 | initialFragmentIndex: PropTypes.number,
17 | setFragmentManager: PropTypes.func,
18 |
19 | children: PropTypes.any,
20 |
21 | // Layout determines how content is positioned on a slide
22 | layout: PropTypes.oneOfType([
23 | PropTypes.string,
24 | PropTypes.bool,
25 | PropTypes.func
26 | ])
27 | }
28 |
29 | static defaultProps = {
30 | centered: false,
31 | layout: 'default'
32 | }
33 |
34 | componentWillMount() {
35 | const bgChanged = this.props.onBackgroundChange
36 |
37 | const backgroundEl = backgroundFor(this.props)
38 | bgChanged && bgChanged(backgroundEl)
39 | }
40 |
41 | getLayoutComponent(handle) {
42 | switch (handle) {
43 | case 'default':
44 | return DefaultLayout
45 | case 'centered':
46 | return CenteredLayout
47 | }
48 |
49 | // use no layout
50 | return null
51 | }
52 |
53 | renderWithinLayout(children) {
54 | let layout = this.props.layout
55 | const { centered } = this.props
56 |
57 | // Custom layout rendering function
58 | if (typeof layout === 'function') {
59 | return layout(children)
60 | }
61 |
62 | if (centered) layout = 'centered'
63 | const Layout = this.getLayoutComponent(layout)
64 |
65 | if (Layout) {
66 | return {children}
67 | } else {
68 | return children
69 | }
70 | }
71 |
72 | render() {
73 | const { className, centered } = this.props
74 |
75 | return (
76 |
77 |
81 | {this.renderWithinLayout(this.props.children)}
82 |
83 |
84 | )
85 | }
86 | }
87 |
88 | const SlideContent = styled.div`
89 | width: 100%;
90 | height: 100%;
91 |
92 | font-family: ${props => props.theme.slide.baseFont};
93 | font-size: ${props => props.theme.slide.baseFontSize}px;
94 | color: ${props => props.theme.slide.textColor};
95 |
96 | ${props =>
97 | props.centered &&
98 | `
99 | display: flex;
100 | flex-flow: column nowrap;
101 | align-items: center;
102 | justify-content: center;
103 | `};
104 | `
105 |
106 | export default Slide
107 |
--------------------------------------------------------------------------------
/src/components/slide/slide-theme.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { ThemeProvider } from 'styled-components'
3 | import { defaultSlideTheme } from '../../theme'
4 |
5 | // SlideTheme provides computed theme defaults
6 | // to child components on a slide.
7 | const themeTransformer = theme => {
8 | let slideTheme = {}
9 | const inheritedKeys = ['baseFont', 'monoFont', 'textColor']
10 |
11 | inheritedKeys.forEach(key => (slideTheme[key] = theme[key]))
12 | Object.assign(slideTheme, defaultSlideTheme, theme.slide)
13 |
14 | return {
15 | ...theme,
16 | slide: slideTheme
17 | }
18 | }
19 |
20 | const SlideTheme = props => (
21 |
22 | )
23 |
24 | export default SlideTheme
25 |
--------------------------------------------------------------------------------
/src/components/slide/slide.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import styled, { withTheme } from 'styled-components'
5 | import SlideTheme from './slide-theme'
6 | import Placeholder from './placeholder'
7 |
8 | /*
9 | * The `Slide` component represents a real slide
10 | * instance rendered inside the slideshow.
11 | * Supports scaling (needed to fit the slide into a
12 | * specific viewport).
13 | */
14 |
15 | class Slide extends React.Component {
16 | static propTypes = {
17 | // Slide settings
18 | slide: PropTypes.object.isRequired,
19 | width: PropTypes.number.isRequired,
20 | height: PropTypes.number.isRequired,
21 |
22 | background: PropTypes.string,
23 | loaded: PropTypes.bool,
24 | fitInto: PropTypes.shape({
25 | width: PropTypes.number,
26 | height: PropTypes.number
27 | })
28 | }
29 |
30 | static defaultProps = {
31 | loaded: true
32 | }
33 |
34 | constructor(props) {
35 | super(props)
36 | this.state = {
37 | slideBackground: null
38 | }
39 | }
40 |
41 | handleBackgroundChange = background => {
42 | this.setState({
43 | slideBackground: background
44 | })
45 | }
46 |
47 | calculateZoom() {
48 | const { width, height } = this.props
49 | const ft = this.props.fitInto
50 |
51 | if (!ft) return 1.0
52 |
53 | const fitAspect = width / height
54 |
55 | // These magic values help to handle edge cases:
56 | // when either width or height is not present.
57 | let aspect = (ft.width || Infinity) / (ft.height || -0)
58 |
59 | if (fitAspect >= aspect) {
60 | return ft.width / width
61 | } else {
62 | return ft.height / height
63 | }
64 | }
65 |
66 | render() {
67 | const { width, height, slide, loaded } = this.props
68 | const { slideBackground } = this.state
69 |
70 | const zoom = this.calculateZoom()
71 | const background =
72 | this.props.background || this.props.theme.slide.background
73 |
74 | const layerProps = {
75 | style: {
76 | width: width,
77 | height: height,
78 | transform: zoom === 1.0 ? 'none' : `scale(${zoom}, ${zoom})`,
79 | transformOrigin: 'top left'
80 | }
81 | }
82 |
83 | return (
84 |
92 | {!loaded && (
93 |
94 |
95 |
96 | )}
97 |
98 | {loaded &&
99 | slideBackground && {slideBackground} }
100 |
101 | {loaded && (
102 |
103 | {React.cloneElement(slide.element, {
104 | onBackgroundChange: this.handleBackgroundChange,
105 | setFragmentManager: slide.setFragmentManager,
106 | initialFragmentIndex: slide.initialFragmentIndex
107 | })}
108 |
109 | )}
110 |
111 | )
112 | }
113 | }
114 |
115 | const Frame = styled.div`
116 | overflow: hidden;
117 | position: relative;
118 | `
119 |
120 | const Layer = styled.div`
121 | position: absolute;
122 | top: 0;
123 | left: 0;
124 |
125 | width: 100%;
126 | height: 100%;
127 | `
128 |
129 | const ConnectedSlide = withTheme(Slide)
130 |
131 | const SlideOuter = props => (
132 |
133 |
134 |
135 | )
136 |
137 | export default SlideOuter
138 |
--------------------------------------------------------------------------------
/src/components/slideshow-mode/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 |
4 | import Toc from './toc'
5 | import Controls from '../controls'
6 | import { Slide } from '../slide'
7 |
8 | const limitBy = (val, min, max) => Math.max(min, Math.min(val, max))
9 |
10 | class SlideshowMode extends React.Component {
11 | constructor(props) {
12 | super(props)
13 | this.state = {
14 | viewportWidth: props.slideWidth
15 | }
16 | }
17 |
18 | recalcViewport = () => {
19 | if (this._slideCont) {
20 | const original = this.props.slideWidth
21 | const width = this._slideCont.clientWidth
22 | const value = limitBy(width, original * 0.5, original)
23 |
24 | this.setState({
25 | viewportWidth: value
26 | })
27 | }
28 | }
29 |
30 | componentDidUpdate(prevProps) {
31 | if (prevProps.showToc !== this.props.showToc) {
32 | this.recalcViewport()
33 | }
34 | }
35 |
36 | componentDidMount() {
37 | window.addEventListener('resize', this.recalcViewport)
38 | this.recalcViewport()
39 | }
40 |
41 | componentWillUnmount() {
42 | window.removeEventListener('resize', this.recalcViewport)
43 | }
44 |
45 | render() {
46 | const { slide, showToc } = this.props
47 |
48 | return (
49 |
50 | {showToc && }
51 |
52 |
53 | (this._slideCont = el)}>
54 |
61 |
62 |
63 |
66 |
67 |
68 | )
69 | }
70 | }
71 |
72 | const TocColumn = styled(Toc)`
73 | flex-shrink: 0;
74 | `
75 |
76 | const Slideshow = styled.div`
77 | display: flex;
78 | overflow: hidden;
79 | flex-flow: row nowrap;
80 | height: 100%;
81 | `
82 |
83 | const Main = styled.div`
84 | flex-grow: 1;
85 | flex-shrink: 1;
86 | box-sizing: border-box;
87 | padding: 20px 24px;
88 | overflow: auto;
89 | `
90 |
91 | const SlideCard = styled(Slide)`
92 | overflow: hidden;
93 | box-shadow: 0px 5px 16px -2px rgba(0, 0, 0, 0.1);
94 | border-radius: 4px;
95 | flex-shrink: 0;
96 | `
97 |
98 | const CurrentSlide = styled.div`
99 | display: flex;
100 | justify-content: center;
101 | `
102 |
103 | const Footer = styled.div`
104 | padding: 24px;
105 | display: flex;
106 | justify-content: center;
107 | `
108 |
109 | export default SlideshowMode
110 |
--------------------------------------------------------------------------------
/src/components/slideshow-mode/toc.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 |
4 | import { RightArrow } from '../../assets/icons'
5 | import Copyright from '../built-with'
6 |
7 | const autoScroll = true
8 |
9 | class Toc extends React.Component {
10 | componentDidUpdate(prevProps) {
11 | const { currentSlide } = this.props
12 | if (prevProps.currentSlide !== currentSlide) {
13 | this.scrollToItem(currentSlide)
14 | }
15 | }
16 |
17 | componentDidMount() {
18 | this.scrollToItem(this.props.currentSlide)
19 | }
20 |
21 | scrollToItem(index) {
22 | const _toc = this._tocEl
23 |
24 | if (!autoScroll || !_toc) {
25 | return
26 | }
27 |
28 | const item = _toc.querySelector(`[data-index="${index}"]`)
29 | if (item && item.scrollIntoView) {
30 | item.scrollIntoView({ block: 'center' })
31 | }
32 | }
33 |
34 | render() {
35 | const { className, slides, currentSlide, switchSlide } = this.props
36 |
37 | return (
38 |
39 |
40 | {this.props.presentationName}
41 |
42 |
43 |
44 | (this._tocEl = el)}>
45 | {slides.map((slide, index) => (
46 | currentSlide}
50 | onClick={() => switchSlide(index)}
51 | data-index={index}
52 | >
53 | {' '}
54 | {slide.name || `Slide #${index + 1}`}
55 |
56 | ))}
57 |
58 |
59 | )
60 | }
61 | }
62 |
63 | const Header = styled.div`
64 | flex-shrink: 0;
65 | padding-top: 15px;
66 | margin-bottom: 16px;
67 | `
68 |
69 | // filter out `isVisible` prop
70 | const SlideArrow = styled(({ isVisible, ...rest }) => )`
71 | position: absolute;
72 | opacity: 0;
73 | left: -6px;
74 |
75 | ${props =>
76 | props.isVisible &&
77 | `
78 | opacity: 1;
79 | `};
80 | `
81 |
82 | const NavigationBody = styled.div`
83 | height: 100%;
84 | overflow: hidden;
85 | box-sizing: border-box;
86 |
87 | width: 280px;
88 | display: flex;
89 | flex-flow: column nowrap;
90 | padding: 0 22px;
91 |
92 | @media (max-width: 960px) {
93 | width: 250px;
94 | font-size: 15px;
95 | }
96 | `
97 |
98 | const PresentationName = styled.div`
99 | font-size: 26px;
100 | font-weight: bold;
101 | line-height: 1;
102 | margin-bottom: 5px;
103 | `
104 |
105 | const TableOfContents = styled.div`
106 | flex-grow: 1;
107 | flex-shrink: 1;
108 | overflow-y: auto;
109 | padding-bottom: 20px;
110 |
111 | &::-webkit-scrollbar {
112 | display: none;
113 | }
114 | `
115 |
116 | const SlideItem = styled.div`
117 | margin-bottom: 5px;
118 | cursor: pointer;
119 | color: #666;
120 | padding: 5px 0;
121 | position: relative;
122 |
123 | &:hover {
124 | color: #222;
125 | }
126 |
127 | ${props =>
128 | props.current &&
129 | `
130 | color: black;
131 | padding-left: 18px;
132 | `} ${props =>
133 | props.future &&
134 | `
135 | opacity: 0.4;
136 | `};
137 | `
138 |
139 | export default Toc
140 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import Presentation from './components/presentation'
2 | import { SlideDecl as Slide } from './components/slide'
3 | import BuiltWithPresa from './components/built-with'
4 | import Fragment from './components/fragment'
5 |
6 | // Export base presentation components
7 | export { Slide, Presentation, BuiltWithPresa, Fragment }
8 |
--------------------------------------------------------------------------------
/src/theme.js:
--------------------------------------------------------------------------------
1 | export const defaultSlideTheme = {
2 | baseFontSize: 22,
3 | fontScale: 1.333,
4 | background: '#ffffff',
5 | layoutPadding: '2.5em 5em'
6 | }
7 |
8 | export default {
9 | // Settings related to slide content appearance
10 | // Can be overwritten
11 | slide: {},
12 |
13 | // Using web-safe font defaults: base serif font and monospace
14 | baseFont:
15 | '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, ' +
16 | 'Ubuntu, Cantarell, "Helvetica Neue", sans-serif',
17 |
18 | monoFont:
19 | '"SF Mono", "Monaco", "Inconsolata", "Fira Mono", "Droid Sans Mono", ' +
20 | '"Source Code Pro", monospace',
21 |
22 | /* Color palette */
23 | // Slideshow background
24 | backgroundColor: '#fafafa',
25 |
26 | fullscreenBackgroundColor: '#000000',
27 |
28 | // Used as an accent color in
29 | // active elements
30 | primaryColor: '#3c59ff',
31 |
32 | // Default text color
33 | textColor: '#222222',
34 |
35 | // Icon background
36 | darkGrayColor: '#5a5a5a',
37 |
38 | // Stardust gray
39 | mutedTextColor: '#9E9E9E',
40 |
41 | placeholderColor: '#f1f1f1'
42 | }
43 |
--------------------------------------------------------------------------------
/tests/fragments.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import TestRenderer from 'react-test-renderer'
3 | import TestContainer from './support/test-container'
4 |
5 | import RegularFragment from '../src/components/fragment'
6 | import Slide from '../src/components/slide/slide-decl'
7 |
8 | // Create TestFragment: it does not render content when not active
9 | // Why? easier to test.
10 | /* eslint-disable react/prop-types */
11 | const Hideable = props => (props.active ?
: null)
12 | const Fragment = props =>
13 | /* eslint-enable react/prop-types */
14 |
15 | describe('fragments feature', () => {
16 | it('displays a currently active fragment and all previous', () => {
17 | const root = TestRenderer.create(
18 |
19 |
20 | visible
21 | visible
22 | not yet visible
23 |
24 |
25 | ).root
26 |
27 | expect(root.findAllByProps({ tag: 'visible' }).length).toEqual(2)
28 | expect(root.findAllByProps({ tag: 'not yet visible' }).length).toEqual(0)
29 | })
30 |
31 | it('supports fragments defined on any level', () => {
32 | const root = TestRenderer.create(
33 |
34 |
35 |
36 | visible
37 |
38 |
39 |
40 | not visible
41 |
42 |
43 |
44 |
45 | ).root
46 |
47 | expect(root.findAllByProps({ tag: 'visible' }).length).toEqual(1)
48 | expect(root.findAllByProps({ tag: 'not visible' }).length).toEqual(0)
49 | })
50 |
51 | it('auto assigns indexes to fragments by their order', () => {
52 | const root = TestRenderer.create(
53 |
54 |
55 |
56 | visible
57 |
58 |
59 |
60 | not visible
61 |
62 |
63 |
64 |
65 | ).root
66 |
67 | expect(root.findAllByProps({ tag: 'visible' }).length).toEqual(1)
68 | expect(root.findAllByProps({ tag: 'not visible' }).length).toEqual(0)
69 | })
70 | })
71 |
--------------------------------------------------------------------------------
/tests/next-index.test.js:
--------------------------------------------------------------------------------
1 | import nextIndex from '../src/components/fragment/next-index'
2 |
3 | describe('nextIndex function', () => {
4 | it('gives the next index', () => {
5 | expect(nextIndex([0, 1, 2], 1)).toEqual(2)
6 | expect(nextIndex([0, 3, 5, 7], 5)).toEqual(7)
7 | })
8 |
9 | it('returns null when out of bounds', () => {
10 | expect(nextIndex([0, 2, 3], 5)).toEqual(null)
11 | expect(nextIndex([1], 5)).toEqual(null)
12 | expect(nextIndex([1], 1)).toEqual(null)
13 | expect(nextIndex([1, 2, 3], 2, -3)).toEqual(null)
14 | })
15 |
16 | it('ignores order and repeated objects', () => {
17 | expect(nextIndex([0, 4, 1, 1, 1], 1)).toEqual(4)
18 | expect(nextIndex([7, 2, 5], 2)).toEqual(5)
19 | })
20 |
21 | it('falls back to the closest element if current is missing', () => {
22 | expect(nextIndex([1, 7, 9], 0)).toEqual(7)
23 | expect(nextIndex([0, 1, 7, 9], 500, -2)).toEqual(1)
24 | expect(nextIndex([0, 7, 9], 2)).toEqual(9)
25 | expect(nextIndex([0, 7, 9], 2, -10)).toEqual(null)
26 | expect(nextIndex([0, 7, 9], 0, 10)).toEqual(null)
27 | })
28 |
29 | it('supports ∞/-∞ as elements', () => {
30 | expect(nextIndex([-Infinity, 0, 1, 2], 0, -1)).toEqual(-Infinity)
31 | expect(nextIndex([Infinity, 0, 1, 2], 2, 1)).toEqual(Infinity)
32 | expect(nextIndex([Infinity, 0, 1, 2], 2, 2)).toEqual(null)
33 | })
34 | })
35 |
--------------------------------------------------------------------------------
/tests/support/test-container.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { ThemeProvider } from 'styled-components'
3 |
4 | import theme from '../../src/theme'
5 |
6 | const TestContainer = ({ children }) => (
7 |
8 | )
9 |
10 | export default TestContainer
11 |
--------------------------------------------------------------------------------
/tests/video-background.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import TestRenderer from 'react-test-renderer'
3 |
4 | import VideoBackground, { makeQuery } from '../src/blocks/video-background'
5 |
6 | describe('makeQuery', () => {
7 | it('serializes key-value pair using 1/0 instead of bools', () => {
8 | expect(makeQuery({ foo: true })).toEqual('foo=1')
9 | expect(makeQuery({ foo: false })).toEqual('foo=0')
10 | })
11 |
12 | it('supports multiple keys', () => {
13 | expect(makeQuery({ foo: true, bar: 'hello' })).toEqual('foo=1&bar=hello')
14 | })
15 | })
16 |
17 | describe('VideoBackground component', () => {
18 | it('renders a `video` element', () => {
19 | const root = TestRenderer.create( ).root
20 |
21 | expect(root.findAllByType('video').length).toEqual(1)
22 | })
23 |
24 | it('supports video attributes', () => {
25 | const video =
26 | const root = TestRenderer.create(video).root
27 |
28 | const videoEl = root.findByType('video')
29 |
30 | expect(videoEl.props).toMatchObject({
31 | loop: true,
32 | autoPlay: true
33 | })
34 | })
35 |
36 | it('renders an iframe when src is youtube link', () => {
37 | const root = TestRenderer.create(
38 |
39 | ).root
40 |
41 | expect(root.findAllByType('iframe').length).toEqual(1)
42 | })
43 |
44 | it('supports shortened youtube links', () => {
45 | const root = TestRenderer.create(
46 |
47 | ).root
48 |
49 | expect(root.findAllByType('iframe').length).toEqual(1)
50 | })
51 |
52 | it('supports `mute` prop', () => {
53 | const root = TestRenderer.create(
54 |
55 | ).root
56 |
57 | expect(root.findByType('iframe').props.src).toMatch('mute=1')
58 | })
59 | })
60 |
--------------------------------------------------------------------------------