├── .browserslistrc
├── .editorconfig
├── .eslintrc.js
├── .gitignore
├── README.md
├── babel.config.js
├── hello.mdx
├── jest.config.js
├── markdown-loader.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── favicon.ico
├── images
│ └── think.gif
└── index.html
├── src
├── App.vue
├── assets
│ └── logo.png
├── cli.js
├── cli
│ ├── colors.js
│ ├── commands
│ │ ├── build.js
│ │ ├── dev.js
│ │ ├── eject.js
│ │ └── index.js
│ ├── constants.js
│ ├── emoji.js
│ ├── index.js
│ └── utils.js
├── components
│ ├── Appear.vue
│ ├── BGImage.js
│ ├── CodeSample.js
│ ├── CodeSurfer.vue
│ └── Modes
│ │ ├── GridMode.vue
│ │ ├── OverviewMode.vue
│ │ └── PresenterMode.vue
├── constants.js
├── index.js
├── layouts
│ └── Split.js
├── main.js
├── mixins
│ ├── InteractsWithSteps.js
│ └── InteractsWithStorage.js
├── router.js
├── store.js
├── styles
│ ├── components
│ │ └── transitions.css
│ ├── main.css
│ ├── prism-atom-dark.css
│ └── utilities
│ │ └── transitions.css
└── views
│ └── Deck.vue
├── tailwind.config.js
├── tests
└── unit
│ ├── .eslintrc.js
│ └── example.spec.js
├── theme.config.js
├── vue.config.js
└── yarn.lock
/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{js,jsx,ts,tsx,vue}]
2 | indent_style = space
3 | indent_size = 2
4 | end_of_line = lf
5 | trim_trailing_whitespace = true
6 | insert_final_newline = true
7 | max_line_length = 100
8 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true
5 | },
6 | extends: [
7 | 'plugin:vue/essential',
8 | 'eslint:recommended'
9 | ],
10 | rules: {
11 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
13 | 'vue/no-unused-components': 1
14 | },
15 | parserOptions: {
16 | parser: 'babel-eslint'
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 | /lib
5 |
6 | # local env files
7 | .env.local
8 | .env.*.local
9 |
10 | # Log files
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 |
15 | # Editor directories and files
16 | .idea
17 | .vscode
18 | *.suo
19 | *.ntvs*
20 | *.njsproj
21 | *.sln
22 | *.sw?
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MDX Deck
2 |
3 | ## Getting Started
4 |
5 | `npm i -D mdx-vue-deck`
6 |
7 | Create an [MDX][mdx] file and separate each slide with `---`.
8 |
9 | ````mdx
10 | # This is the title of my deck
11 |
12 | ---
13 |
14 | # About Me
15 | ```
16 |
17 | ---
18 |
19 | import HelloWold from './components/HelloWold'
20 |
21 | ##
152 | See the Pen 153 | Sprite Sheet by godkin.mo (@godkin-mo) 154 | on CodePen. 155 |
156 | 157 |)`, 'g')
21 | src = _.replace(src, codeRe, replacement => {
22 | const classNameRe = /"className": "(.*)"/g
23 | const className = classNameRe.exec(replacement)
24 |
25 | const codeRe = /({`(.|\r|\n)*?`})<\/code>/g
26 | const code = (codeRe.exec(replacement))[2]
27 |
28 | return `${code}
`
29 | })
30 |
31 | /* eslint-disable-next-line */
32 | const linkRe = new RegExp(`()`, 'g')
33 | src = _.replace(src, linkRe, replacement => {
34 | const urlRe = /"href": "(.*)"/g
35 | const url = urlRe.exec(replacement)
36 |
37 | return `${url[1]}`
38 | })
39 |
40 |
41 | return src
42 | }
43 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mdx-vue-deck",
3 | "version": "0.1.12",
4 | "description": "MDX-based presentation decks",
5 | "author": "Jack Fong",
6 | "scripts": {
7 | "build": "babel-node src/cli.js build hello.mdx",
8 | "test:unit": "vue-cli-service test:unit",
9 | "babelify": "babel src --out-dir lib --copy-files",
10 | "help": "babel-node src/cli.js",
11 | "start": "babel-node src/cli.js dev hello.mdx"
12 | },
13 | "main": "lib/index.js",
14 | "dependencies": {
15 | "@babel/cli": "^7.6.0",
16 | "@babel/node": "^7.6.1",
17 | "@babel/preset-env": "^7.6.0",
18 | "@mdx-js/vue-loader": "^1.4.0",
19 | "@vue/cli-plugin-babel": "^4.0.5",
20 | "@vue/cli-service": "^4.0.5",
21 | "@vue/test-utils": "1.0.0-beta.29",
22 | "babel-core": "7.0.0-bridge.0",
23 | "babel-eslint": "^10.0.3",
24 | "babel-jest": "^25.0.0",
25 | "babel-plugin-prismjs": "^1.1.1",
26 | "chalk": "^2.4.2",
27 | "core-js": "^3.3.2",
28 | "execa": "^2.0.4",
29 | "fs-extra": "^8.1.0",
30 | "markdown-it": "^9.1.0",
31 | "meow": "^5.0.0",
32 | "mousetrap": "^1.6.3",
33 | "node-emoji": "^1.10.0",
34 | "postcss-import": "^12.0.1",
35 | "postcss-nesting": "^7.0.1",
36 | "prettify-xml": "^1.2.0",
37 | "prismjs": "^1.17.1",
38 | "serialize-javascript": "^2.1.2",
39 | "tailwindcss": "^1.1.2",
40 | "tailwindcss-typography": "^2.2.0",
41 | "vue": "^2.6.10",
42 | "vue-router": "^3.0.3",
43 | "vue-template-compiler": "^2.6.10",
44 | "vuex": "^3.0.1"
45 | },
46 | "bin": {
47 | "mdx-vue-deck": "lib/cli.js"
48 | },
49 | "license": "MIT",
50 | "repository": "https://github.com/godkinmo/mdx-vue-deck.git",
51 | "devDependencies": {
52 | "@vue/cli-plugin-eslint": "^4.0.5",
53 | "eslint": "^6.6.0",
54 | "eslint-plugin-vue": "^6.0.1"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('postcss-import'),
4 | // Watching theme file for changes
5 | function (css, opts) {
6 | const resolvedPath = process.env.__TAILWIND_THEME_CONFIG_PATH__
7 |
8 | delete require.cache[require.resolve(resolvedPath)]
9 | opts.messages.push({
10 | type: 'dependency',
11 | file: resolvedPath,
12 | parent: css.source.input.file,
13 | })
14 | },
15 | require('tailwindcss')('./tailwind.config.js'),
16 | require('autoprefixer'),
17 | require('postcss-nesting')()
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/godkinmo/mdx-vue-deck/8c72ee37e4e6919daa67891ff0b13f248a6b4a51/public/favicon.ico
--------------------------------------------------------------------------------
/public/images/think.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/godkinmo/mdx-vue-deck/8c72ee37e4e6919daa67891ff0b13f248a6b4a51/public/images/think.gif
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | mdx-vue-deck
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
146 |
147 |
148 |
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/godkinmo/mdx-vue-deck/8c72ee37e4e6919daa67891ff0b13f248a6b4a51/src/assets/logo.png
--------------------------------------------------------------------------------
/src/cli.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import meow from 'meow'
4 | import pkg from '../package.json'
5 | import commands from './cli/commands'
6 | import * as colors from './cli/colors'
7 | import * as utils from './cli/utils'
8 |
9 | const cli = meow(
10 | `
11 | mdx-vue-deck ${colors.info(pkg.version)}
12 |
13 | Usage:
14 | mdx-vue-deck deck.mdx
15 | mdx-vue-deck deck.mdx -c tailwind.config.js
16 | mdx-vue-deck build deck.mdx
17 | mdx-vue-deck eject
18 |
19 | Options:
20 | -h --host Dev server host
21 | -p --port Dev server port
22 | -c --config Use custom theme config file
23 | --no-open Prevent from opening in default browser
24 | `,
25 | {
26 | description: false,
27 | flags: {
28 | port: {
29 | type: 'string',
30 | alias: 'p',
31 | default: '8000',
32 | },
33 | host: {
34 | type: 'string',
35 | alias: 'h',
36 | default: 'localhost',
37 | },
38 | open: {
39 | type: 'boolean',
40 | alias: 'o',
41 | default: true,
42 | },
43 | config: {
44 | type: 'string',
45 | alias: 'c',
46 | }
47 | },
48 | }
49 | )
50 |
51 | const cmd = cli.input[0]
52 | const filename = cli.input[1]
53 |
54 | if (cli.input.length === 0) {
55 | cli.showHelp(0)
56 | }
57 |
58 | if (['dev', 'build'].includes(cmd)) {
59 | if (typeof filename === 'undefined') {
60 | cli.showHelp(0)
61 | }
62 |
63 | utils.prepareVueCliService(cli, filename)
64 | }
65 |
66 | commands[cmd].run(cli)
67 |
--------------------------------------------------------------------------------
/src/cli/colors.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk'
2 |
3 | /**
4 | * Applies colors to emphasize
5 | *
6 | * @param {...string} msgs
7 | */
8 | export function bold(...msgs) {
9 | return chalk.bold(...msgs)
10 | }
11 |
12 | /**
13 | * Applies colors to inform
14 | *
15 | * @param {...string} msgs
16 | */
17 | export function info(...msgs) {
18 | return chalk.bold.cyan(...msgs)
19 | }
20 |
21 | /**
22 | * Applies colors to signify error
23 | *
24 | * @param {...string} msgs
25 | */
26 | export function error(...msgs) {
27 | return chalk.bold.red(...msgs)
28 | }
29 |
30 | /**
31 | * Applies colors to represent a file
32 | *
33 | * @param {...string} msgs
34 | */
35 | export function file(...msgs) {
36 | return chalk.bold.magenta(...msgs)
37 | }
38 |
--------------------------------------------------------------------------------
/src/cli/commands/build.js:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import * as utils from '../utils'
3 |
4 | export function run() {
5 | utils.vueCliService('build').then(() => {
6 | const source = path.resolve(__dirname, '../../../dist')
7 | const destination = path.join(process.cwd(), 'dist')
8 | if (source === destination) return
9 | utils.copyFile(source, destination)
10 | })
11 | }
12 |
--------------------------------------------------------------------------------
/src/cli/commands/dev.js:
--------------------------------------------------------------------------------
1 | import * as utils from '../utils'
2 |
3 | export function run({ flags }) {
4 | utils.vueCliService(
5 | 'serve',
6 | '--host',
7 | flags.host,
8 | '--port',
9 | flags.port,
10 | flags.open && '--open'
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/src/cli/commands/eject.js:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 |
3 | import * as constants from '../../constants'
4 | import * as colors from '../colors'
5 | import * as emoji from '../emoji'
6 | import * as utils from '../utils'
7 |
8 | export function run() {
9 | utils.header()
10 |
11 | const file = path.join(process.cwd(), 'theme.config.js')
12 | const simplePath = 'theme.config.js'
13 |
14 | utils.exists(file) && utils.die(colors.file(simplePath), 'already exists.')
15 |
16 | utils.copyFile(constants.tailwindThemeConfigFile, file)
17 |
18 | utils.log()
19 | utils.log(emoji.yes, 'Created Tailwind theme config file:', colors.file(simplePath))
20 |
21 | utils.footer()
22 | }
23 |
--------------------------------------------------------------------------------
/src/cli/commands/index.js:
--------------------------------------------------------------------------------
1 | import * as dev from './dev'
2 | import * as build from './build'
3 | import * as eject from './eject'
4 |
5 | export default { dev, build, eject }
6 |
--------------------------------------------------------------------------------
/src/cli/constants.js:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 |
3 | export const tailwindThemeConfigFile = path.resolve(__dirname, './theme.config.js')
4 |
--------------------------------------------------------------------------------
/src/cli/emoji.js:
--------------------------------------------------------------------------------
1 | import { get } from 'node-emoji'
2 |
3 | export const yes = get('white_check_mark')
4 | export const no = get('no_entry_sign')
5 | export const go = get('rocket')
6 | export const pack = get('package')
7 | export const disk = get('floppy_disk')
8 |
--------------------------------------------------------------------------------
/src/cli/index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import meow from 'meow'
4 | import pkg from '../../package.json'
5 | import commands from './commands'
6 | import * as colors from './colors'
7 |
8 | const cli = meow(
9 | `
10 | mdx-vue-deck ${colors.info(pkg.version)}
11 |
12 | Usage:
13 | mdx-vue-deck deck.mdx
14 | mdx-vue-deck deck.mdx -c tailwind.config.js
15 | mdx-vue-deck build deck.mdx
16 | mdx-vue-deck eject
17 |
18 | Options:
19 | -h --host Dev server host
20 | -p --port Dev server port
21 | -c --config Use custom theme config file
22 | --no-open Prevent from opening in default browser
23 | `,
24 | {
25 | description: false,
26 | flags: {
27 | port: {
28 | type: 'string',
29 | alias: 'p',
30 | default: '8000',
31 | },
32 | host: {
33 | type: 'string',
34 | alias: 'h',
35 | default: 'localhost',
36 | },
37 | open: {
38 | type: 'boolean',
39 | alias: 'o',
40 | default: true,
41 | },
42 | config: {
43 | type: 'string',
44 | alias: 'c',
45 | }
46 | },
47 | }
48 | )
49 |
50 | const cmd = cli.input[0]
51 | const filename = cli.input[1]
52 |
53 | if (cli.input.length === 0) {
54 | cli.showHelp(0)
55 | }
56 |
57 | process.env.__TAILWIND_THEME_CONFIG_PATH__ = './theme.config.js'
58 |
59 | commands[cmd].run(cli, filename)
60 |
--------------------------------------------------------------------------------
/src/cli/utils.js:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import execa from 'execa'
3 | import { copySync, existsSync } from 'fs-extra'
4 |
5 | import * as colors from './colors'
6 | import * as emoji from './emoji'
7 | import packageJson from '../../package.json'
8 |
9 | /**
10 | * Prints messages to console.
11 | *
12 | * @param {...string} [msgs]
13 | */
14 | export function log(...msgs) {
15 | console.log(' ', ...msgs)
16 | }
17 |
18 | /**
19 | * Prints application header to console.
20 | */
21 | export function header() {
22 | log()
23 | log(colors.bold(packageJson.name), colors.info(packageJson.version))
24 | }
25 |
26 | /**
27 | * Prints application footer to console.
28 | */
29 | export function footer() {
30 | log()
31 | }
32 |
33 | /**
34 | * Prints error messages to console.
35 | *
36 | * @param {...string} [msgs]
37 | */
38 | export function error(...msgs) {
39 | log()
40 | console.error(' ', emoji.no, colors.error(msgs.join(' ')))
41 | }
42 |
43 | /**
44 | * Kills the process. Optionally prints error messages to console.
45 | *
46 | * @param {...string} [msgs]
47 | */
48 | export function die(...msgs) {
49 | msgs.length && error(...msgs)
50 | footer()
51 | process.exit(1)
52 | }
53 |
54 | /**
55 | * Checks if path exists.
56 | *
57 | * @param {string} path
58 | * @return {boolean}
59 | */
60 | export function exists(path) {
61 | return existsSync(path)
62 | }
63 |
64 | /**
65 | * Copies file source to destination.
66 | *
67 | * @param {string} source
68 | * @param {string} destination
69 | */
70 | export function copyFile(source, destination) {
71 | copySync(source, destination)
72 | }
73 |
74 | /**
75 | * Run vue-cli-service command
76 | */
77 | export function vueCliService(...args) {
78 | return execa('vue-cli-service', args.filter(Boolean), {
79 | cwd: path.resolve(__dirname, '../../'),
80 | stdio: 'inherit',
81 | preferLocal: true,
82 | env: {
83 | SRC_DECK: process.env.__SRC__
84 | }
85 | })
86 | }
87 |
88 | export function prepareVueCliService({ flags }, filename) {
89 | if (flags.config) {
90 | const tailwindThemeConfig = path.resolve(flags.config)
91 | !exists(tailwindThemeConfig) && die(colors.file(flags.config), 'does not exists')
92 | process.env.__TAILWIND_THEME_CONFIG_PATH__ = tailwindThemeConfig
93 | } else {
94 | process.env.__TAILWIND_THEME_CONFIG_PATH__ = path.resolve('./theme.config.js')
95 | }
96 |
97 | const source = path.resolve(filename)
98 |
99 | !exists(source) && die(colors.file(filename), 'does not exists.')
100 |
101 | process.env.__SRC__ = source
102 | }
103 |
--------------------------------------------------------------------------------
/src/components/Appear.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
42 |
--------------------------------------------------------------------------------
/src/components/BGImage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | props: ['src'],
3 |
4 | render(createElement) {
5 | return createElement('div', {
6 | attrs: {
7 | class: 'w-full',
8 | },
9 | }, [
10 | createElement('img', {
11 | attrs: {
12 | class: 'mx-auto h-full',
13 | src: this.src,
14 | }
15 | })
16 | ])
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/CodeSample.js:
--------------------------------------------------------------------------------
1 | const md = require('markdown-it')()
2 | const prettifyXml = require('prettify-xml')
3 |
4 | const getChildrenTextContent = function (children, tag = 'div', classAttr) {
5 | return children.map(function (node) {
6 | return node.children
7 | ? getChildrenTextContent(node.children, node.tag, node.data ? node.data.class : '')
8 | : `<${tag} class="${classAttr}">${node.text}${tag}>`
9 | }).join('')
10 | }
11 |
12 | const getCodeEl = (createElement, slots, { lineNumbers }) => {
13 | if (slots.default[1]) {
14 | const codeLanguageClass = slots.default[1].children[0].data.className
15 |
16 | return createElement('div', {
17 | attrs: { class: `${codeLanguageClass}` }
18 | }, [slots.default[1]])
19 | } else {
20 | const prettyHTML = prettifyXml(getChildrenTextContent(slots.default), {
21 | indent: 4, newline: '\n '
22 | })
23 | const codeHTML = md.render(`
24 | ${prettyHTML}
25 | `)
26 | return createElement('div', {
27 | attrs: { class: `language-html ${lineNumbers ? 'line-numbers' : ''}` },
28 | domProps: {
29 | innerHTML: codeHTML
30 | },
31 | })
32 | }
33 | }
34 |
35 | export default {
36 | render(createElement) {
37 | return createElement('div', {
38 | attrs: {
39 | class: 'relative w-full max-w-3xl overflow-hidden mb-8'
40 | }
41 | }, [
42 | createElement('div', {
43 | attrs: {
44 | class: 'bg-white rounded-t-lg overflow-hidden border-t border-l border-r border-gray-400 p-4'
45 | }
46 | }, [this.$slots.default[0]]),
47 | createElement('div', {
48 | attrs: {
49 | class: 'rounded-b-lg bg-gray-800'
50 | }
51 | }, [
52 | getCodeEl(createElement, this.$slots, {
53 | lineNumbers: this.$attrs.lineNumbers || false
54 | })
55 | ]),
56 | ])
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/components/CodeSurfer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {{ notes }}
10 |
11 |
12 |
13 |
14 |
98 |
--------------------------------------------------------------------------------
/src/components/Modes/GridMode.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
10 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
26 |
--------------------------------------------------------------------------------
/src/components/Modes/OverviewMode.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
10 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | {{ page }}/{{ decks.length }}
24 |
25 |
26 |
27 |
28 |
29 |
54 |
--------------------------------------------------------------------------------
/src/components/Modes/PresenterMode.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
13 |
14 |
15 |
16 |
17 |
18 | {{ page }}/{{ decks.length }}
19 |
20 |
21 |
22 |
23 |
28 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 |
3 | export const tailwindThemeConfigFile = path.resolve(__dirname, '../theme.config.js')
4 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export {
2 | }
3 |
--------------------------------------------------------------------------------
/src/layouts/Split.js:
--------------------------------------------------------------------------------
1 | export default {
2 | render(createElement) {
3 | return createElement('div', {
4 | attrs: {
5 | class: 'w-full flex flex-row -mx-2'
6 | }
7 | }, [
8 | createElement('div', {
9 | attrs: {
10 | class: 'w-1/2 mx-2 flex items-center justify-center'
11 | }
12 | }, [this.$slots.default[0]]),
13 |
14 | createElement('div', {
15 | attrs: {
16 | class: 'w-1/2 mx-2 flex flex-col items-center justify-center'
17 | }
18 | }, [
19 | ...this.$slots.default.slice(1)
20 | ])
21 | ])
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import App from './App.vue'
3 | import router from './router'
4 | import store from './store'
5 |
6 | Vue.config.productionTip = false
7 |
8 | new Vue({
9 | router,
10 | store,
11 | render: h => h(App),
12 | }).$mount('#app')
13 |
--------------------------------------------------------------------------------
/src/mixins/InteractsWithSteps.js:
--------------------------------------------------------------------------------
1 | export default {
2 | data: () => ({
3 | componentPage: null,
4 | }),
5 |
6 | mounted() {
7 | this.$nextTick(() => {
8 | const deckEl = this.$el.closest('.markdown')
9 | if (deckEl) {
10 | this.componentPage = Array.from(deckEl.parentElement.children).indexOf(deckEl) + 1
11 | }
12 | })
13 | },
14 |
15 | beforeDestroy() {
16 | this.$store.commit('setSteps', 0)
17 | },
18 |
19 | watch: {
20 | active(value) {
21 | if (value) {
22 | this.$store.commit('setSteps', this.steps)
23 | }
24 | },
25 | step(step) {
26 | if (this.active) {
27 | history.pushState({}, '', `/#/${this.currentPage}${step ? `.${step}`: ''}`);
28 | }
29 | },
30 | },
31 |
32 | computed: {
33 | currentPage() {
34 | return parseInt(this.$route.params.page)
35 | },
36 | active() {
37 | return this.currentPage === this.componentPage
38 | },
39 | step() {
40 | return this.$store.state.step || 0
41 | }
42 | },
43 | }
44 |
--------------------------------------------------------------------------------
/src/mixins/InteractsWithStorage.js:
--------------------------------------------------------------------------------
1 | const keys = {
2 | page: 'mdx-deck-page',
3 | step: 'mdx-deck-step',
4 | }
5 |
6 | export default {
7 | data: () => ({
8 | focused: true,
9 | }),
10 |
11 | mounted() {
12 | window.addEventListener('focus', this.handleFocus)
13 | window.addEventListener('blur', this.handleBlur)
14 | },
15 |
16 | beforeDestroy() {
17 | window.removeEventListener('focus', this.handleFocus)
18 | window.removeEventListener('blur', this.handleBlur)
19 | },
20 |
21 | watch: {
22 | '$store.state.currentPage'(page) {
23 | if (!this.focused) return
24 | localStorage.setItem(keys.page, page)
25 | },
26 | '$store.state.step' (step) {
27 | localStorage.setItem(keys.step, step)
28 | },
29 | focused: {
30 | immediate: true,
31 | handler(focused) {
32 | if (!focused){
33 | window.addEventListener('storage', this.handleStorageChange)
34 | } else {
35 | window.removeEventListener('storage', this.handleStorageChange)
36 | }
37 | }
38 | },
39 | },
40 |
41 | methods: {
42 | handleFocus() {
43 | this.focused = true
44 | },
45 | handleBlur() {
46 | this.focused = false
47 | },
48 | handleStorageChange(e) {
49 | const n = parseInt(e.newValue, 10)
50 |
51 | if (isNaN(n)) return
52 |
53 | switch (e.key) {
54 | case keys.page:
55 | this.$router.push({ name: 'home', params: { page: n }})
56 | break
57 | case keys.step:
58 | this.$store.commit('setStep', n)
59 | break
60 | default:
61 | break
62 | }
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/router.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 | import Deck from './views/Deck.vue'
4 |
5 | Vue.use(Router)
6 |
7 | export default new Router({
8 | mode: 'hash',
9 | base: process.env.BASE_URL,
10 | routes: [
11 | {
12 | path: '/:page?',
13 | name: 'home',
14 | component: Deck,
15 | },
16 | ],
17 | })
18 |
--------------------------------------------------------------------------------
/src/store.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 |
4 | Vue.use(Vuex)
5 |
6 | export default new Vuex.Store({
7 | state: {
8 | currentPage: 1,
9 | mode: 'normal',
10 | steps: 0,
11 | step: 0,
12 | },
13 | mutations: {
14 | toggleMode(state, next) {
15 | state.mode = state.mode === next
16 | ? 'normal'
17 | : next
18 | },
19 | setCurrentPage(state, page) {
20 | state.currentPage = page
21 | },
22 | setSteps(state, steps) {
23 | state.steps = steps
24 | },
25 | setStep(state, step) {
26 | state.step = step
27 | },
28 | increaseStep(state) {
29 | state.step += 1
30 | },
31 | decreaseStep(state) {
32 | state.step -= 1
33 | }
34 | },
35 | })
36 |
--------------------------------------------------------------------------------
/src/styles/components/transitions.css:
--------------------------------------------------------------------------------
1 | /* Slide Transition
2 | ---------------------------------------------------------------------------- */
3 | .slide-right-enter-active {
4 | transition: all .3s ease-out;
5 | }
6 | .slide-right-leave-active {
7 | transition: all .3s ease-out;
8 | }
9 | .slide-right-enter {
10 | opacity: .5;
11 | transform: scale(.5) translateX(-100%) translateY(50%);
12 | }
13 | .slide-right-leave-to {
14 | opacity: .5;
15 | transform: scale(.5) translateX(100%) translateY(50%);
16 | }
17 |
18 |
19 | .slide-left-enter-active {
20 | transition: all .3s ease-out;
21 | }
22 | .slide-left-leave-active {
23 | transition: all .3s ease-out;
24 | }
25 | .slide-left-enter {
26 | opacity: .5;
27 | transform: scale(.5) translateX(100%) translateY(50%);
28 | }
29 | .slide-left-leave-to {
30 | opacity: .5;
31 | transform: scale(.5) translateX(-100%) translateY(50%);
32 | }
33 |
34 | /* Fade Transition
35 | ---------------------------------------------------------------------------- */
36 | .fade-enter-active,
37 | .fade-leave-active {
38 | transition: opacity .15s;
39 | }
40 |
41 | .fade-enter,
42 | .fade-leave-to {
43 | opacity: 0;
44 | }
45 |
--------------------------------------------------------------------------------
/src/styles/main.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss/base";
2 |
3 | /* Add custom base styles here... */
4 |
5 | @import "tailwindcss/components";
6 |
7 | /* purgecss start ignore */
8 | @import "./components/transitions.css";
9 | @import "./prism-atom-dark.css";
10 | /* purgecss end ignore */
11 |
12 | @import "tailwindcss/utilities";
13 | @import "./utilities/transitions.css";
14 |
--------------------------------------------------------------------------------
/src/styles/prism-atom-dark.css:
--------------------------------------------------------------------------------
1 | /**
2 | * atom-dark theme for `prism.js`
3 | * Based on Atom's `atom-dark` theme: https://github.com/atom/atom-dark-syntax
4 | * @author Joe Gibson (@gibsjose)
5 | */
6 |
7 | code[class*="language-"],
8 | pre[class*="language-"] {
9 | /*color: #c5c8c6;*/
10 | /*text-shadow: 0 1px rgba(0, 0, 0, 0.3);*/
11 | /*font-family: Inconsolata, Monaco, Consolas, 'Courier New', Courier, monospace;*/
12 | direction: ltr;
13 | text-align: left;
14 | white-space: pre;
15 | word-spacing: normal;
16 | word-break: normal;
17 | line-height: 1.5;
18 |
19 | -moz-tab-size: 4;
20 | -o-tab-size: 4;
21 | tab-size: 4;
22 |
23 | -webkit-hyphens: none;
24 | -moz-hyphens: none;
25 | -ms-hyphens: none;
26 | hyphens: none;
27 | }
28 |
29 | /* Code blocks */
30 | pre[class*="language-"] {
31 | @apply p-4;
32 | /*padding: 1em;*/
33 | /*margin: .5em 0;*/
34 | overflow: auto;
35 | border-radius: 0.3em;
36 | }
37 |
38 | /* Inline code */
39 | :not(pre) > code[class*="language-"] {
40 | padding: .1em;
41 | border-radius: .3em;
42 | }
43 |
44 | .token.comment,
45 | .token.prolog,
46 | .token.doctype,
47 | .token.cdata {
48 | @apply text-gray-500;
49 | /*color: #7C7C7C;*/
50 | }
51 |
52 | .token.punctuation {
53 | color: #c5c8c6;
54 | }
55 |
56 | .namespace {
57 | opacity: .7;
58 | }
59 |
60 | .token.property,
61 | .token.keyword,
62 | .token.tag {
63 | color: #96CBFE;
64 | }
65 |
66 | .token.class-name {
67 | color: #FFFFB6;
68 | text-decoration: underline;
69 | }
70 |
71 | .token.boolean,
72 | .token.constant {
73 | color: #99CC99;
74 | }
75 |
76 | .token.symbol,
77 | .token.deleted {
78 | color: #f92672;
79 | }
80 |
81 | .token.number {
82 | color: #FF73FD;
83 | }
84 |
85 | .token.selector,
86 | .token.attr-name,
87 | .token.string,
88 | .token.char,
89 | .token.builtin,
90 | .token.inserted {
91 | color: #A8FF60;
92 | }
93 |
94 | .token.variable {
95 | color: #C6C5FE;
96 | }
97 |
98 | .token.operator {
99 | color: #EDEDED;
100 | }
101 |
102 | .token.entity {
103 | color: #FFFFB6;
104 | /* text-decoration: underline; */
105 | }
106 |
107 | .token.url {
108 | color: #96CBFE;
109 | }
110 |
111 | .language-css .token.string,
112 | .style .token.string {
113 | color: #87C38A;
114 | }
115 |
116 | .token.atrule,
117 | .token.attr-value {
118 | color: #F9EE98;
119 | }
120 |
121 | .token.function {
122 | color: #DAD085;
123 | }
124 |
125 | .token.regex {
126 | color: #E9C062;
127 | }
128 |
129 | .token.important {
130 | color: #fd971f;
131 | }
132 |
133 | .token.important,
134 | .token.bold {
135 | font-weight: bold;
136 | }
137 | .token.italic {
138 | font-style: italic;
139 | }
140 |
141 | .token.entity {
142 | cursor: help;
143 | }
144 |
145 | :not(pre) > code[class*="language-"],
146 | pre[class*="language-"] {
147 | background: transparent;
148 | }
149 |
150 | /*Custom overrides*/
151 | .token.atrule, .token.atrule .token.number {
152 | color: #fff;
153 | }
154 | .token.atrule .token.rule {
155 | color: #f9ee98;
156 | }
157 | .token.function {
158 | color: #f9ee98;
159 | }
160 | .language-css .token.string, .style .token.string {
161 | color: #a8ff60;
162 | }
163 |
164 | /*New overrides*/
165 | code[class*="language-"], pre[class*="language-"] {
166 | @apply subpixel-antialiased !important;
167 | @apply text-code-white !important;
168 | @apply scrolling-touch !important;
169 | }
170 | .token.comment {
171 | @apply text-gray-500 !important;
172 | }
173 | .token.atrule {
174 | @apply text-code-white !important;
175 | }
176 | .token.atrule > .token.property {
177 | @apply text-code-white !important;
178 | }
179 | .token.atrule > .token.property + .token.punctuation {
180 | @apply text-code-white !important;
181 | }
182 | .token.atrule > .token.property + .token.punctuation + .token.number + .token.unit {
183 | @apply text-code-white !important;
184 | }
185 | .token.atrule > .token.number {
186 | @apply text-code-white !important;
187 | }
188 | .token.atrule > .token.unit {
189 | @apply text-code-white !important;
190 | }
191 | .token.function {
192 | @apply text-code-blue !important;
193 | }
194 | .token.number {
195 | @apply text-code-red !important;
196 | }
197 | .token.unit {
198 | @apply text-code-red !important;
199 | }
200 | .token.punctuation {
201 | @apply text-code-blue !important;
202 | }
203 | .token.hexcode {
204 | @apply text-code-blue !important;
205 | }
206 | .token.tag {
207 | @apply text-code-red !important;
208 | }
209 | .token.attr-name {
210 | @apply text-code-yellow !important;
211 | }
212 | .token.attr-value {
213 | @apply text-code-green !important;
214 | }
215 | .token.string {
216 | @apply text-code-green !important;
217 | }
218 | .token.url {
219 | @apply text-code-green !important;
220 | }
221 | .token.selector {
222 | @apply text-code-yellow !important;
223 | }
224 | .token.property {
225 | @apply text-code-yellow !important;
226 | }
227 | .token.rule {
228 | @apply text-code-purple !important;
229 | }
230 | .token.important {
231 | font-weight: inherit !important;
232 | @apply text-code-purple !important;
233 | }
234 |
235 | code.language-js, pre.language-js {
236 | & .token.operator {
237 | @apply text-code-blue !important;
238 | }
239 | & .token.punctuation {
240 | @apply text-code-white !important;
241 | }
242 | & .token.boolean {
243 | @apply text-code-red !important;
244 | }
245 | & .token.keyword {
246 | @apply text-code-white !important;
247 | }
248 | & .token.regex {
249 | @apply text-code-yellow !important;
250 | }
251 | }
252 |
253 | code.language-bash, pre.language-bash {
254 | & .token.function {
255 | @apply text-code-white !important;
256 | }
257 | }
258 |
259 | code.language-diff, pre.language-diff {
260 | @apply text-gray-400 !important;
261 | & .token.deleted {
262 | @apply text-code-red !important;
263 | }
264 | & .token.inserted {
265 | @apply text-code-green !important;
266 | }
267 | }
268 |
--------------------------------------------------------------------------------
/src/styles/utilities/transitions.css:
--------------------------------------------------------------------------------
1 | .origin-left {
2 | transform-origin: left;
3 | }
4 | .origin-right {
5 | transform-origin: right;
6 | }
7 | .transition-all {
8 | transition-property: all;
9 | }
10 | .transition-transform {
11 | transition-property: transform;
12 | }
13 | .transition-fastest {
14 | transition-duration: 50ms;
15 | }
16 | .transition-faster {
17 | transition-duration: 100ms;
18 | }
19 | .transition-fast {
20 | transition-duration: 150ms;
21 | }
22 | .transition-medium {
23 | transition-duration: 200ms;
24 | }
25 | .transition-normal {
26 | transition-duration: 300ms;
27 | }
28 | .transition-slow {
29 | transition-duration: 1000ms;
30 | }
31 | .ease-out-quad {
32 | transition-timing-function: cubic-bezier(.25, .46, .45, .94);
33 | }
34 | .ease-in-quad {
35 | transition-timing-function: cubic-bezier(.55, .085, .68, .53);
36 | }
37 | .scale-0 {
38 | transform: scale(0);
39 | }
40 | .scale-70 {
41 | transform: scale(.7);
42 | }
43 | .scale-100 {
44 | transform: scale(1);
45 | }
46 |
47 | .grayscale-100 {
48 | filter: grayscale(1);
49 | }
50 | .hover\:grayscale-0:hover {
51 | filter: grayscale(0);
52 | }
53 |
--------------------------------------------------------------------------------
/src/views/Deck.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
73 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | important: false,
3 | theme: require(process.env.__TAILWIND_THEME_CONFIG_PATH__),
4 | plugins: [
5 | function({ addBase, config }) {
6 | addBase({
7 | 'h1': { fontSize: config('theme.fontSize.6xl') },
8 | 'h2': { fontSize: config('theme.fontSize.5xl') },
9 | 'h3': { fontSize: config('theme.fontSize.4xl') },
10 | 'h4': { fontSize: config('theme.fontSize.3xl') },
11 | 'h5': { fontSize: config('theme.fontSize.2xl') },
12 | 'h6': { fontSize: config('theme.fontSize.xl') },
13 | })
14 | },
15 | require('tailwindcss-typography')({
16 | componentPrefix: '',
17 | }),
18 | ],
19 | }
20 |
--------------------------------------------------------------------------------
/tests/unit/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true,
5 | jest: true
6 | },
7 | extends: [
8 | 'plugin:vue/essential',
9 | 'eslint:recommended'
10 | ],
11 | rules: {
12 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
13 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
14 | 'vue/no-unused-components': 1
15 | },
16 | parserOptions: {
17 | parser: 'babel-eslint'
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/tests/unit/example.spec.js:
--------------------------------------------------------------------------------
1 | import { shallowMount } from '@vue/test-utils';
2 | import HelloWorld from '@/components/HelloWorld.vue';
3 |
4 | describe('HelloWorld.vue', () => {
5 | it('renders props.msg when passed', () => {
6 | const msg = 'new message';
7 | const wrapper = shallowMount(HelloWorld, {
8 | propsData: { msg },
9 | });
10 | expect(wrapper.text()).toMatch(msg);
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/theme.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extend: {
3 | colors: {
4 | theme: {
5 | text: '#fff',
6 | background: '#011627',
7 | dot: '#828A92',
8 | },
9 | code: {
10 | green: '#b5f4a5',
11 | yellow: '#ffe484',
12 | purple: '#d9a9ff',
13 | red: '#ff8383',
14 | blue: '#93ddfd',
15 | white: '#fff',
16 | },
17 | },
18 | },
19 | // https://github.com/benface/tailwindcss-typography
20 | textStyles: theme => ({
21 | heading: {
22 | output: false,
23 | color: '#6AD798',
24 | fontWeight: theme('fontWeight.bold'),
25 | lineHeight: theme('lineHeight.tight'),
26 | },
27 | h1: { extends: 'heading', fontSize: theme('fontSize.6xl') },
28 | h2: { extends: 'heading', fontSize: theme('fontSize.5xl') },
29 | h3: { extends: 'heading', fontSize: theme('fontSize.4xl') },
30 | link: {
31 | fontWeight: theme('fontWeight.bold'),
32 | color: theme('colors.blue.600'),
33 | '&:hover': {
34 | color: theme('colors.blue.500'),
35 | textDecoration: 'underline',
36 | },
37 | },
38 | markdown: {
39 | backgroundColor: theme('colors.theme.background'),
40 | display: 'flex',
41 | flexDirection: 'column',
42 | alignItems: 'center',
43 | justifyContent: 'center',
44 | overflow: 'hidden',
45 | fontWeight: theme('fontWeight.normal'),
46 | fontSize: '32px',
47 | color: theme('colors.theme.text'),
48 | lineHeight: theme('lineHeight.relaxed'),
49 | '> * + *': {
50 | marginTop: '1.5rem',
51 | },
52 | '> h1': { extends: 'h1' },
53 | '> h2': { extends: 'h2' },
54 | '> h3': { extends: 'h3' },
55 | 'ul': {
56 | paddingLeft: '1.5rem',
57 | listStyleType: 'disc',
58 | },
59 | 'ol': {
60 | paddingLeft: '1.5rem',
61 | listStyleType: 'decimal',
62 | },
63 | 'a': {
64 | extends: 'link',
65 | },
66 | 'b, strong': {
67 | fontWeight: theme('fontWeight.bold'),
68 | },
69 | 'i, em': {
70 | fontStyle: 'italic',
71 | },
72 | 'pre[class*=language-]': {
73 | padding: '1rem',
74 | fontSize: theme('fontSize.base'),
75 | fontFamily: theme('fontFamily.mono'),
76 | fontWeight: theme('fontWeight.normal'),
77 | lineHeight: theme('lineHeight.normal'),
78 | backgroundColor: theme('colors.gray.800'),
79 | color: theme('colors.gray.200'),
80 | whitespace: 'nowrap',
81 | scrollbarWidth: 'none',
82 | },
83 | 'pre[class*=language-]::-webkit-scrollbar': {
84 | display: 'none',
85 | },
86 | 'table': {
87 | width: '50%',
88 | textAlign: 'left',
89 | borderCollapse: 'collapse',
90 | overflow: 'scroll-x',
91 | },
92 | 'table th, table td': {
93 | minWidth: '200px',
94 | },
95 | 'table th': {
96 | fontSize: theme('fontSize.lg'),
97 | fontWeight: theme('fontWeight.semibold'),
98 | padding: '.5rem',
99 | color: theme('colors.gray.900'),
100 | backgroundColor: theme('colors.gray.300'),
101 | },
102 | 'table td': {
103 | fontSize: theme('fontSize.lg'),
104 | padding: '.5rem',
105 | borderWidth: '1px',
106 | borderColor: theme('colors.gray.400'),
107 | }
108 | },
109 | }),
110 | }
111 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | configureWebpack: {
3 | module: {
4 | rules: [
5 | {
6 | test: /.mdx?$/,
7 | use: [
8 | 'babel-loader',
9 | {
10 | loader: require.resolve('./markdown-loader')
11 | },
12 | '@mdx-js/vue-loader'
13 | ]
14 | }
15 | ]
16 | },
17 | },
18 | chainWebpack: config => {
19 | config.resolve.alias
20 | .set('@deck', process.env.SRC_DECK)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------