diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..8932ea1 --- /dev/null +++ b/.babelrc @@ -0,0 +1,10 @@ +{ + "presets": [ + "env", + "stage-0", + "react" + ], + "plugins": [ + "transform-runtime" + ] +} diff --git a/.npmignore b/.npmignore index 90ffae2..17f04c7 100644 --- a/.npmignore +++ b/.npmignore @@ -8,4 +8,5 @@ examples demo dist .travis.yml +.babelrc create-x0 diff --git a/README.md b/README.md index 49a235e..8e964e3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # x0 -Zero-config React development environment and static site generator +Document & develop React components without breaking a sweat [![Build Status][build-badge]][build] @@ -17,19 +17,16 @@ npm install -g @compositor/x0 ## Features - Zero-config -- Hot-loading development environment -- Works with virtually any React component\* -- No confusing APIs +- No plugins +- Components over configuration +- Use markdown, MDX, or React components - Automatic file system based routing -- Exports static HTML -- Exports JS bundles -- Works with CSS-in-JS libraries like [styled-components][sc] and [emotion][emotion] -- Support for async data fetching +- Completely customizable +- Export static sites +- Works as an isolated development environment Read more about x0 in our [blog post](https://compositor.io/blog/x0-making-react-component-development-stupid-simple/). -\* Custom [webpack configuration](#webpack) is required for components that rely on webpack-based features - ## Getting Started x0 renders a directory of React components, automatically handling routing based on filename. @@ -315,16 +312,17 @@ export default () => ( ### JSX Format -x0 includes support for the [Compositor JSX][jsx-loader] file format. +x0 includes support for the Compositor JSX file format. ```jsx --- title: Hello -scope: import * as scope from 'rebass' --- +import { Box, Heading } from 'rebass' + - {props.title} + {frontMatter.title} ``` @@ -395,3 +393,4 @@ See the [example](https://github.com/c8r/x0/tree/master/examples/webpack-config) [react-loadable]: https://github.com/thejameskyle/react-loadable [webpack-merge]: https://github.com/survivejs/webpack-merge [webpack]: https://webpack.js.org + diff --git a/cli.js b/cli.js index ed53b71..1e7ef10 100755 --- a/cli.js +++ b/cli.js @@ -28,11 +28,13 @@ const cli = meow(` ${chalk.gray('Options')} --webpack Path to webpack config file + --match String to match routes against using minimatch ${chalk.gray('Dev Server')} -o --open Open dev server in default browser -p --port Port for dev server + --analyze Runs with webpack-bundle-analyzer plugin ${chalk.gray('Build')} @@ -51,6 +53,7 @@ const cli = meow(` type: 'string', alias: 'p' }, + analyze: {}, // build outDir: { type: 'string', @@ -68,6 +71,9 @@ const cli = meow(` type: 'string', alias: 'c' }, + match: { + type: 'string' + }, scope: { type: 'string', }, @@ -81,6 +87,11 @@ const cli = meow(` }) const [ cmd, file ] = cli.input + +if (!cmd) { + cli.showHelp(0) +} + const input = path.resolve(file || cmd) const stats = fs.statSync(input) const dirname = stats.isDirectory() ? input : path.dirname(input) @@ -106,13 +117,6 @@ if (opts.webpack) { if (webpackConfig) opts.webpack = require(webpackConfig) } -if (opts.app) { - opts.app = path.resolve(opts.app) -} else { - const app = findup.sync('_app.js', { cwd: dirname }) - if (app) opts.app = app -} - if (opts.template) { opts.template = require(path.resolve(opts.template)) } @@ -127,7 +131,7 @@ log(chalk.cyan('@compositor/x0')) switch (cmd) { case 'build': log.start('building static site') - const { build } = require('.') + const build = require('./lib/build') build(opts) .then(res => { log.stop('site saved to ' + opts.outDir) @@ -137,7 +141,7 @@ switch (cmd) { case 'dev': default: log.start('starting dev server') - const { dev } = require('.') + const dev = require('./lib/dev') dev(opts) .then(({ server }) => { const { port } = server.options diff --git a/components.js b/components.js new file mode 100644 index 0000000..a2958b7 --- /dev/null +++ b/components.js @@ -0,0 +1 @@ +module.exports = require('./src') diff --git a/docs/404.md b/docs/404.md new file mode 100644 index 0000000..598dbb8 --- /dev/null +++ b/docs/404.md @@ -0,0 +1,2 @@ + +# Page not found diff --git a/docs/_app.js b/docs/_app.js index e6aca89..478845d 100644 --- a/docs/_app.js +++ b/docs/_app.js @@ -1,58 +1,40 @@ import React from 'react' +import * as scope from 'rebass' +import { Link } from 'react-router-dom' +import { ScopeProvider, SidebarLayout } from '../components' import { - Provider, + Provider as RebassProvider, Flex, Box, Container, - Text, - Caps, - BlockLink, } from 'rebass' -import { Link } from 'react-router-dom' -import { Logo } from '@compositor/logo' -import theme from './theme' -export default ({ render }) => - - - - - - - Compositor - - - - - - - x0 - - - - - GitHub - - - - {render()} - - - - - GitHub - - - - - Compositor - - - - - © 2018 Compositor, Inc. All rights reserved - - - - +import LandingLayout from './_layout' +import theme from './_theme' + +export default class App extends React.Component { + static defaultProps = { + title: 'x0' + } + + render () { + const { + routes, + route, + children, + } = this.props + const { layout } = (route && route.props) || {} + + const Layout = layout === 'landing' + ? LandingLayout + : SidebarLayout + + return ( + + + + + + ) + } +} diff --git a/docs/_layout.js b/docs/_layout.js new file mode 100644 index 0000000..9301e5e --- /dev/null +++ b/docs/_layout.js @@ -0,0 +1,56 @@ +import React from 'react' +import { + Flex, + Box, + Container, + Text, + Caps, + BlockLink, +} from 'rebass' +import { Link } from 'react-router-dom' +import { Logo } from '@compositor/logo' + +export default ({ children }) => + + + + + + + Compositor + + + + + + + x0 + + + + + GitHub + + + + {children} + + + + + GitHub + + + + + Compositor + + + + + © 2018 Compositor, Inc. All rights reserved + + + + diff --git a/docs/_scope.js b/docs/_scope.js deleted file mode 100644 index dbf7edf..0000000 --- a/docs/_scope.js +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react' -import styled from 'styled-components' -import * as Rebass from 'rebass' -import { heading, link } from '@compositor/md' - -const Pre = styled(Rebass.Pre)({ - borderRadius: '8px' -}) - -export default { - ...Rebass, - h1: heading(props => - ), - h2: heading(props => - ), - h3: heading(props => - ), - a: link(props => - - ), - pre: props => -
-}
diff --git a/docs/theme.js b/docs/_theme.js
similarity index 100%
rename from docs/theme.js
rename to docs/_theme.js
diff --git a/docs/docs.js b/docs/docs.js
deleted file mode 100644
index 4be4317..0000000
--- a/docs/docs.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import React from 'react'
-import {
-  Container
-} from 'rebass'
-import styled from 'styled-components'
-import Readme from '../README.md'
-import scope from './_scope'
-
-const Prose = styled.div([], {
-  '& img': {
-    display: 'block',
-    maxWidth: '100%',
-    height: 'auto',
-  },
-  '& .demo-image, & a[href="LICENSE/"]': {
-    display: 'none'
-  }
-})
-
-export default props =>
-  
-    
-      
-    
-  
diff --git a/docs/dynamic.js b/docs/dynamic.js
deleted file mode 100644
index 3e76460..0000000
--- a/docs/dynamic.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import React from 'react'
-import { Link } from 'react-router-dom'
-
-const routes = [
-  { path: '/dynamic' },
-  { path: '/dynamic/hello' },
-  { path: '/dynamic/hi' },
-]
-
-export default class extends React.Component {
-  static getInitialProps = async ({ path }) => {
-    let title = 'dynamic'
-    switch (path) {
-      case '/dynamic/hello':
-        title = 'hello'
-        break
-      case '/dynamic/hi':
-        title = 'hi'
-        break
-    }
-    return {
-      routes,
-      path: '/dynamic/:id*',
-      title,
-    }
-  }
-
-  render () {
-    return 
-
dynamic routing
- Home - Dynamic Routes - Hello - Hi -
- } -} diff --git a/docs/index.js b/docs/index.js index 52d8dc5..0290f41 100644 --- a/docs/index.js +++ b/docs/index.js @@ -21,6 +21,10 @@ const Video = styled.video([], { }) export default class extends React.Component { + static defaultProps = { + layout: 'landing' + } + render () { return ( @@ -54,6 +58,7 @@ export default class extends React.Component { GitHub + {/* + */} diff --git a/docs/webpack.config.js b/docs/webpack.config.js deleted file mode 100644 index 9718488..0000000 --- a/docs/webpack.config.js +++ /dev/null @@ -1,23 +0,0 @@ -module.exports = { - module: { - rules: [ - { - test: /\.md$/, - use: [ - // 'raw-loader' - { - loader: 'babel-loader', - options: { - presets: [ - 'env', - 'stage-0', - 'react' - ] - } - }, - '@compositor/md-loader' - ] - } - ] - } -} diff --git a/examples/jsx/pages/index.jsx b/examples/jsx/pages/index.jsx index 562ff3b..529f90e 100644 --- a/examples/jsx/pages/index.jsx +++ b/examples/jsx/pages/index.jsx @@ -1,7 +1,8 @@ --- title: Hello JSX -scope: import scope from './_scope' --- +import { Box, Heading, Text, Link } from 'rebass' + {props.title} diff --git a/lib/build.js b/lib/build.js index e0e8665..fc1854f 100644 --- a/lib/build.js +++ b/lib/build.js @@ -1,4 +1,4 @@ -const fs = require('fs-extra') +const fs = require('fs') const path = require('path') const webpack = require('webpack') const MiniHTMLWebpackPlugin = require('mini-html-webpack-plugin') @@ -8,7 +8,7 @@ const React = require('react') const { renderToString, renderToStaticMarkup } = require('react-dom/server') const { StaticRouter } = require('react-router-dom') const semver = require('semver') - +const rimraf = require('rimraf') const util = require('util') const baseConfig = require('./config') @@ -18,7 +18,7 @@ const getApp = opts => { const config = merge(baseConfig, opts.webpack) config.mode = 'development' - config.entry = opts.entry || path.join(__dirname, './entry.js') + config.entry = opts.entry || path.join(__dirname, '../src/entry.js') config.output= { path: opts.tempdir, filename: 'App.js', @@ -44,7 +44,6 @@ const getApp = opts => { const STYLED_COMPONENTS_VERSION = '>=3.0' const EMOTION_VERSION = '>=9.0' -const GLAMOR_VERSION = '>=2.0' const getCSSLibrary = opts => { if (opts.cssLibrary) return opts.cssLibrary @@ -63,11 +62,6 @@ const getCSSLibrary = opts => { if (!semver.satisfies(emotionVersion, EMOTION_VERSION)) return null return 'emotion' } - if (deps.glamor) { // || deps.glamorous) { - const glamorVersion = semver.coerce(deps.glamor) - if (!semver.satisfies(glamorVersion, GLAMOR_VERSION)) return null - return 'glamor' - } return null } @@ -88,35 +82,25 @@ const renderHTML = ({ const { ServerStyleSheet } = require('styled-components') const sheet = new ServerStyleSheet() html = render( - sheet.collectStyles( - React.createElement(App.default, { routes, path }) - ) + sheet.collectStyles(app) ) css = sheet.getStyleTags() - return { path, html, css, props } + break case 'emotion': const { renderStylesToString } = require('emotion-server') html = renderStylesToString( render(app) ) - return { path, html, props } - case 'glamor': - // doesn't seem to be working... - const glamor = require('glamor/server') - const res = glamor.renderStatic(() => ( - render(app) - )) - html = res.html - css = `` - return { path, html, css, props } + break default: html = render(app) - return { path, html, props } + break } + return { html, css, path, props } } const remove = filename => { - fs.remove(filename, err => { + rimraf(filename, err => { if (err) console.log(err) }) } @@ -165,7 +149,7 @@ module.exports = async (opts) => { DEV: JSON.stringify(false), OPTIONS: JSON.stringify(opts), DIRNAME: JSON.stringify(opts.dirname), - APP: JSON.stringify(opts.app) + MATCH: JSON.stringify(opts.match) }) ) @@ -191,7 +175,7 @@ module.exports = async (opts) => { if (opts.debug) { config.stats = 'verbose' } - config.entry = path.join(__dirname, './entry') + config.entry = path.join(__dirname, '../src/entry') config.output = { path: opts.outDir, filename: 'bundle.js', @@ -208,15 +192,32 @@ module.exports = async (opts) => { }) ) }) + // 404 + config.plugins.push( + new MiniHTMLWebpackPlugin({ + filename: '404.html', + template, + context: Object.assign({}, opts, + renderHTML({ + opts, + routes, + App, + props: {}, + path: '/404' + }) + ) + }) + ) const compiler = webpack(config) + remove(opts.tempdir) + return new Promise((resolve, reject) => { compiler.run((err, stats) => { if (err) { reject(err) return } - remove(opts.tempdir) if (opts.static) { const bundle = path.join(opts.outDir, 'bundle.js') remove(bundle) diff --git a/lib/config.js b/lib/config.js index 04dafb0..df88d86 100644 --- a/lib/config.js +++ b/lib/config.js @@ -1,5 +1,12 @@ const path = require('path') +const remark = { + images: require('remark-images'), + emoji: require('remark-emoji'), + slug: require('remark-slug'), + autolinkHeadings: require('remark-autolink-headings'), +} + const babel = { presets: [ 'babel-preset-env', @@ -22,25 +29,32 @@ const rules = [ { test: /\.js$/, exclude: path.resolve(__dirname, '../node_modules'), - include: path.resolve(__dirname), + include: [ + path.resolve(__dirname, '..'), + path.resolve(__dirname, '../src') + ], loader: require.resolve('babel-loader'), options: babel }, { - test: /\.jsx$/, - loader: require.resolve('@compositor/jsx-loader'), - options: {} - }, - { - test: /\.mdx$/, + test: /\.(md|mdx|jsx)$/, use: [ { loader: require.resolve('babel-loader'), options: babel }, { - loader: require.resolve('@mdx-js/loader') - } + loader: require.resolve('@mdx-js/loader'), + options: { + mdPlugins: [ + remark.slug, + remark.autolinkHeadings, + remark.images, + remark.emoji, + ] + } + }, + path.join(__dirname, './mdx-fm-loader'), ] } ] diff --git a/lib/dev.js b/lib/dev.js index c81c5ff..7e2b964 100644 --- a/lib/dev.js +++ b/lib/dev.js @@ -24,7 +24,7 @@ module.exports = async (opts) => { config.mode = 'development' config.context = opts.dirname - config.entry = opts.entry || path.join(__dirname, './entry') + config.entry = opts.entry || path.join(__dirname, '../src/entry') config.output = { path: path.join(process.cwd(), 'dev'), filename: 'dev.js', @@ -49,7 +49,7 @@ module.exports = async (opts) => { DEV: JSON.stringify(true), OPTIONS: JSON.stringify(opts), DIRNAME: JSON.stringify(opts.dirname), - APP: JSON.stringify(opts.app), + MATCH: JSON.stringify(opts.match) }) ) @@ -60,6 +60,18 @@ module.exports = async (opts) => { }) ) + if (opts.analyze) { + const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') + const analyzerPort = typeof opts.analyze === 'string' + ? opts.analyze + : 8888 + config.plugins.push( + new BundleAnalyzerPlugin({ + analyzerPort + }) + ) + } + if (opts.debug) { config.stats = 'verbose' // todo: enable other logging diff --git a/lib/entry.js b/lib/entry.js deleted file mode 100644 index 41eb723..0000000 --- a/lib/entry.js +++ /dev/null @@ -1,194 +0,0 @@ -import path from 'path' -import React from 'react' -import { render, hydrate } from 'react-dom' -import { - StaticRouter, - BrowserRouter, - Switch, - Route, - Link, - withRouter -} from 'react-router-dom' - -const IS_CLIENT = typeof document !== 'undefined' -const req = require.context(DIRNAME, false, /\.(js|mdx|jsx)$/) - -const { filename, basename = '', disableScroll } = OPTIONS -const index = filename ? path.basename(filename, path.extname(filename)) : 'index' - -const getComponents = req => req.keys().map(key => ({ - key, - name: path.basename(key, path.extname(key)), - module: req(key), - Component: req(key).default || req(key) -})) - .filter(component => !/^(\.|_)/.test(component.name)) - .filter(component => typeof component.Component === 'function') - -const initialComponents = getComponents(req) - -const Index = ({ routes = [] }) => ( - -
{DIRNAME}
-
    - {routes.map(route => ( -
  • - - {route.name} - -
  • - ))} -
-
-) - -const DefaultApp = ({ render, routes }) => ( - - {render()} - ( - - )} /> - -) - -class Catch extends React.Component { - static getDerivedStateFromProps (props, state) { - if (!state.err) return null - return { err: null } - } - - state = { - err: null - } - - componentDidCatch (err) { - this.setState({ err }) - } - - render () { - const { err } = this.state - - if (err) { - return ( -
-      )
-    }
-
-    return this.props.children
-  }
-}
-
-const ScrollTop = withRouter(class extends React.Component {
-  componentDidUpdate(prevProps) {
-    if (this.props.location.pathname !== prevProps.location.pathname) {
-      window.scrollTo(0, 0)
-    }
-  }
-  render () {
-    return false
-  }
-})
-
-const Router = IS_CLIENT ? BrowserRouter : StaticRouter
-const App = withRouter(APP ? (require(APP).default || require(APP)) : DefaultApp)
-
-export const getRoutes = async (components = initialComponents) => {
-  const routes = await components.map(async ({ key, name, module, Component }) => {
-    const exact = name === index
-    let pathname = exact ? '/' : '/' + name
-    const props = Component.getInitialProps
-      ? await Component.getInitialProps({ path: pathname })
-      : {}
-    pathname = props.path || pathname
-    return {
-      key: name,
-      name,
-      path: pathname,
-      exact,
-      module,
-      Component,
-      props
-    }
-  })
-  return Promise.all(routes)
-}
-
-export default class Root extends React.Component {
-  static defaultProps = {
-    path: '/',
-    basename
-  }
-  state = this.props
-
-  render () {
-    const {
-      routes,
-      basename,
-      path = '/'
-    } = this.state
-
-    return (
-      
-        
-          
-             (
-                routes.map(({ Component, ...route }) => (
-                   (
-                      
-                        
-                      
-                    )}
-                  />
-                ))
-              )}
-            />
-          
-          {!disableScroll && }
-        
-      
-    )
-  }
-}
-
-let app
-if (IS_CLIENT) {
-  const mount = DEV ? render : hydrate
-  const div = window.root || document.body.appendChild(
-    document.createElement('div')
-  )
-  getRoutes()
-    .then(routes => {
-      app = mount(, div)
-    })
-}
-
-if (IS_CLIENT && module.hot) {
-  module.hot.accept()
-}
-
diff --git a/lib/mdx-fm-loader.js b/lib/mdx-fm-loader.js
new file mode 100644
index 0000000..28a04c6
--- /dev/null
+++ b/lib/mdx-fm-loader.js
@@ -0,0 +1,13 @@
+// front-matter loader for mdx
+const matter = require('gray-matter')
+const stringifyObject = require('stringify-object')
+
+module.exports = async function (src) {
+  const callback = this.async()
+  const { content, data } = matter(src)
+
+  const code = `export const frontMatter = ${stringifyObject(data)}
+${content}
+  `
+  return callback(null, code)
+}
diff --git a/package.json b/package.json
index e37f89e..df4460e 100644
--- a/package.json
+++ b/package.json
@@ -1,15 +1,16 @@
 {
   "name": "@compositor/x0",
-  "version": "5.0.8",
-  "description": "Zero-config React development environment and static site generator",
+  "version": "6.0.0-4",
+  "description": "Document & develop React components without breaking a sweat",
   "main": "index.js",
   "bin": {
     "x0": "cli.js"
   },
   "scripts": {
-    "start": "./cli.js docs -op 8888",
+    "start": "./cli.js docs -p 8989",
     "build": "./cli.js build docs",
-    "test": "ava -T 20s",
+    "test": "nyc ava --timeout=60s",
+    "test:components": "nyc ava test/components.js",
     "cover": "nyc report --reporter=html --reporter=lcov"
   },
   "keywords": [
@@ -27,64 +28,77 @@
   "author": "Brent Jackson",
   "license": "MIT",
   "dependencies": {
-    "@compositor/jsx-loader": "^1.0.0-4",
     "@compositor/log": "^1.0.0-0",
-    "@mdx-js/loader": "^0.9.0",
-    "@mdx-js/mdx": "^0.9.0",
+    "@mdx-js/loader": "^0.11.0",
+    "@mdx-js/mdx": "^0.10.1",
+    "@mdx-js/tag": "^0.11.0",
+    "@rebass/markdown": "^1.0.0-1",
     "babel-core": "^6.26.3",
     "babel-loader": "^7.1.4",
-    "babel-plugin-macros": "^2.2.1",
+    "babel-plugin-macros": "^2.2.2",
     "babel-plugin-transform-runtime": "^6.23.0",
     "babel-preset-env": "^1.7.0",
     "babel-preset-react": "^6.24.1",
     "babel-preset-stage-0": "^6.24.1",
     "babel-register": "^6.26.0",
+    "browser-env": "^3.2.5",
     "chalk": "^2.4.1",
     "clipboardy": "^1.2.3",
     "connect-history-api-fallback": "^1.5.0",
-    "emotion": "^9.1.3",
-    "emotion-server": "^9.1.3",
+    "emotion": "^9.2.3",
+    "emotion-server": "^9.2.3",
     "find-up": "^2.1.0",
     "fs-extra": "^6.0.1",
     "glamor": "^2.20.40",
+    "gray-matter": "^4.0.1",
     "html-minifier": "^3.5.16",
     "koa-connect": "^2.0.1",
+    "lodash.sortby": "^4.7.0",
     "meow": "^5.0.0",
     "mini-html-webpack-plugin": "^0.2.3",
+    "minimatch": "^3.0.4",
     "pkg-conf": "^2.1.0",
-    "react": "^16.4.0",
+    "prop-types": "^15.6.2",
+    "react": "^16.4.1",
     "react-dev-utils": "^5.0.1",
-    "react-dom": "^16.4.0",
-    "react-router": "^4.2.0",
-    "react-router-dom": "^4.2.2",
+    "react-dom": "^16.4.1",
+    "react-live": "^1.10.1",
+    "react-router": "^4.3.1",
+    "react-router-dom": "^4.3.1",
+    "react-scope-provider": "^1.0.0-1",
     "read-pkg-up": "^3.0.0",
+    "rebass": "^2.0.0-6",
+    "remark-autolink-headings": "^5.0.0",
+    "remark-emoji": "^2.0.1",
+    "remark-images": "^0.8.1",
+    "remark-slug": "^5.0.0",
     "semver": "^5.5.0",
+    "stringify-object": "^3.2.2",
+    "styled-components": ">=3.0.0",
+    "styled-system": "^2.3.1",
     "update-notifier": "^2.5.0",
     "webpack": "^4.10.2",
-    "webpack-merge": "^4.1.2",
-    "webpack-serve": "^1.0.2"
+    "webpack-bundle-analyzer": "^2.13.1",
+    "webpack-merge": "^4.1.3",
+    "webpack-serve": "^1.0.4"
   },
   "devDependencies": {
-    "@compositor/logo": "^1.3.5",
-    "@compositor/md-loader": "^1.0.34",
+    "@compositor/logo": "^1.4.0",
     "ava": "^0.25.0",
     "isomorphic-fetch": "^2.2.1",
-    "nano-style": "^1.0.0",
     "nyc": "^12.0.1",
-    "raw-loader": "^0.5.1",
-    "rebass": "^2.0.0-2",
+    "react-test-renderer": "^16.4.1",
     "refunk": "^3.0.1",
     "rimraf": "^2.6.2",
-    "styled-components": "^3.3.0",
-    "styled-system": "^2.2.5"
+    "sinon": "^6.0.0"
   },
   "x0": {
-    "title": "Compositor x0",
+    "title": "x0",
     "basename": "/x0",
     "meta": [
       {
         "name": "description",
-        "content": "Zero-config React development environment and static site generator"
+        "content": "Document & develop React components without breaking a sweat"
       },
       {
         "name": "twitter:card",
@@ -111,22 +125,13 @@
   "ava": {
     "files": [
       "test/*",
-      "!test/components",
-      "!test/output"
+      "!**/test/components",
+      "!**/test/output"
     ],
     "require": [
       "babel-register"
     ],
-    "babel": {
-      "presets": [
-        "env",
-        "stage-0",
-        "react"
-      ],
-      "plugins": [
-        "transform-runtime"
-      ]
-    }
+    "babel": "inherit"
   },
   "engines": {
     "node": ">=8.0"
diff --git a/src/Catch.js b/src/Catch.js
new file mode 100644
index 0000000..ae57c52
--- /dev/null
+++ b/src/Catch.js
@@ -0,0 +1,38 @@
+import React from 'react'
+
+export default class Catch extends React.Component {
+  state = {
+    err: null
+  }
+
+  componentDidCatch (err) {
+    this.setState({ err })
+  }
+
+  componentWillReceiveProps (next) {
+    if (!this.state.err) return
+    this.setState({ err: null })
+  }
+
+  render () {
+    const { err } = this.state
+
+    if (!err) return this.props.children
+
+    return (
+      
+    )
+  }
+}
diff --git a/src/CenteredLayout.js b/src/CenteredLayout.js
new file mode 100644
index 0000000..353c4bf
--- /dev/null
+++ b/src/CenteredLayout.js
@@ -0,0 +1,12 @@
+import React from 'react'
+import {
+  Container
+} from 'rebass'
+
+export default props => props.active
+  ? 
+  : props.children
diff --git a/src/FileList.js b/src/FileList.js
new file mode 100644
index 0000000..5a58375
--- /dev/null
+++ b/src/FileList.js
@@ -0,0 +1,17 @@
+import React from 'react'
+import { Link } from 'react-router-dom'
+
+export default ({ routes = [] }) => (
+  
+    
{DIRNAME}
+
    + {routes.map(route => ( +
  • + + {route.name} + +
  • + ))} +
+
+) diff --git a/src/Library.js b/src/Library.js new file mode 100644 index 0000000..4d9ad57 --- /dev/null +++ b/src/Library.js @@ -0,0 +1,80 @@ +import React from 'react' +import { Link } from 'react-router-dom' +import styled from 'styled-components' +import { + style, + gridGap, + gridAutoRows, + borderColor +} from 'styled-system' + +const gridWidth = style({ + prop: 'width', + cssProperty: 'gridTemplateColumns', + getter: n => `repeat(auto-fit, minmax(${n}px, 1fr))` +}) + +const Grid = styled.div([], { + display: 'grid' +}, + gridWidth, + gridGap, + gridAutoRows +) + +Grid.defaultProps = { + width: 256, + gridAutoRows: 192 +} + +const Card = styled(Link)([], { + display: 'block', + textDecoration: 'none', + color: 'inherit', + overflow: 'hidden', + border: '1px solid' +}, borderColor) + +Card.defaultProps = { + borderColor: 'gray' +} + +export default class extends React.Component { + static defaultProps = { + fullWidth: true, + hidePagination: true + } + + render () { + const { + routes = [], + route, + location + } = this.props + + const examples = routes + .filter(r => r.dirname === route.dirname) + .filter(r => r !== route) + + return ( + + + {examples.map(({ + key, + path, + name, + Component + }) => ( + + +
{name}
+
+ ))} +
+
+ ) + } +} diff --git a/src/LiveEditor.js b/src/LiveEditor.js new file mode 100644 index 0000000..6ac3ac6 --- /dev/null +++ b/src/LiveEditor.js @@ -0,0 +1,73 @@ +import React from 'react' +import { + LiveProvider, + LivePreview, + LiveEditor, + LiveError +} from 'react-live' +import { ScopeConsumer } from 'react-scope-provider' +import { Box } from 'rebass' +import { color, borderColor } from 'styled-system' +import styled from 'styled-components' + +const transformCode = src => `${src}` + +const Preview = styled(LivePreview)([], { + padding: '16px', + border: '1px solid', + borderRadius: '2px 2px 0 0', +}, borderColor) +Preview.defaultProps = { + borderColor: 'gray' +} + +const Editor = styled(LiveEditor)([], { + fontFamily: 'Menlo, monospace', + fontSize: '13px', + margin: 0, + padding: '16px', + borderRadius: '0 0 2px 2px', + '&:focus': { + outline: 'none', + boxShadow: 'inset 0 0 0 1px #6cf', + } +}, color) +Editor.defaultProps = { + bg: 'gray' +} + +const Err = styled(LiveError)([], { + fontFamily: 'Menlo, monospace', + fontSize: '13px', + padding: '8px', + color: 'white', + backgroundColor: 'red' +}) + +export default ({ + code, + scope, + render +}) => ( + + + {scope => ( + + {typeof render === 'function' ? ( + render({ code, scope }) + ) : ( + + + + + + )} + + )} + + +) diff --git a/src/LivePreview.js b/src/LivePreview.js new file mode 100644 index 0000000..989b18f --- /dev/null +++ b/src/LivePreview.js @@ -0,0 +1,30 @@ +import React from 'react' +import { + LiveProvider, + LivePreview, + LiveError +} from 'react-live' +import { ScopeConsumer } from 'react-scope-provider' +import { Box } from 'rebass' + +const transformCode = str => `${str}` + +export default ({ + code, + scope +}) => ( + + + {scope => ( + + + + + )} + + +) diff --git a/src/ScopeProvider.js b/src/ScopeProvider.js new file mode 100644 index 0000000..d302900 --- /dev/null +++ b/src/ScopeProvider.js @@ -0,0 +1,20 @@ +import React from 'react' +import { MDXProvider } from '@mdx-js/tag' +import { ScopeProvider } from 'react-scope-provider' +import defaultScope from './scope' + +export default props => { + const scope = { + ...defaultScope, + ...props.scope + } + return ( + + + + {props.children} + + + + ) +} diff --git a/src/ScrollTop.js b/src/ScrollTop.js new file mode 100644 index 0000000..72da1ff --- /dev/null +++ b/src/ScrollTop.js @@ -0,0 +1,22 @@ +import React from 'react' +import { withRouter } from 'react-router-dom' + +export default withRouter(class extends React.Component { + componentDidUpdate (prev) { + const { pathname, hash } = this.props.location + if (prev.location.pathname !== pathname) { + window.scrollTo(0, 0) + } + + // check performance of this + if (hash) { + const el = document.getElementById(hash.slice(1)) + if (!el) return + el.scrollIntoView() + } + } + + render () { + return false + } +}) diff --git a/src/SidebarLayout.js b/src/SidebarLayout.js new file mode 100644 index 0000000..3559aea --- /dev/null +++ b/src/SidebarLayout.js @@ -0,0 +1,308 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { + Link as RouterLink, + NavLink as RouterNavLink +} from 'react-router-dom' +import styled from 'styled-components' +import { + Provider as RebassProvider, + Flex, + Box, + Fixed, + Container, + Text, + Close, + Toolbar, + Divider, + Heading, + NavLink, + BlockLink, + Button, + ButtonTransparent, +} from 'rebass' +import { borderColor, themeGet } from 'styled-system' + +const breakpoint = `@media screen and (min-width: 48em)` + +export const Root = styled(Flex)([], { + minHeight: '100vh' +}) + +export const Sidebar = styled('div')([], { + width: '256px', + height: '100vh', + flex: 'none', + overflowY: 'auto', + WebkitOverflowScrolling: 'touch', + transition: 'transform .2s ease-out', + backgroundColor: '#fff', + borderRight: '1px solid', + position: 'fixed', + top: 0, + left: 0, + bottom: 0, +}, props => ({ + transform: props.open ? 'translateX(0)' : 'translateX(-100%)', + [breakpoint]: { + transform: 'none' + } +}), borderColor) +Sidebar.defaultProps = { + borderColor: 'gray' +} + +export const Overlay = styled('div')([], { + position: 'fixed', + top: 0, + right: 0, + bottom: 0, + left: 0, +}) + +export const MobileOnly = styled.div([], { + [breakpoint]: { + display: 'none' + }, +}) + +export const MenuIcon = ({ size = 24, ...props }) => + + + + +export const Main = props => + + +export const MaxWidth = props => + + +export const Content = styled(Box)([], { + minHeight: 'calc(100vh - 208px)' +}) + +export const UL = styled('ul')([], { + listStyle: 'none', + margin: 0, + paddingLeft: 0, + paddingBottom: '48px', +}) + +export const LI = styled('li')([], { +}) + +const depthPad = ({ to = '' }) => + (1 + to.split('/') + .filter(s => s.length) + .slice(1).length) * 16 + +const Link = styled(props => ( + +))([], props => ({ + borderLeft: '4px solid', + borderColor: 'transparent', + '&.active, &:focus': { + color: themeGet('colors.blue', '#07c')(props), + outline: 'none', + }, + '&:focus': { + borderColor: 'inherit', + } +})) + +Link.defaultProps = { + to: '' +} + +const unhyphenate = str => str.replace(/(\w)(-)(\w)/g, '$1 $3') +const upperFirst = str => str.charAt(0).toUpperCase() + str.slice(1) +const format = str => upperFirst(unhyphenate(str)) + +const NavBar = ({ + title, + logo, + focus, + update, +}) => + + {logo} + + {title} + + + + +export const Nav = ({ + routes = [], + ...props +}) => + + + +
    + {routes.map(route => ( +
  • + {/^https?:\/\//.test(route.path) ? ( + + {route.name} + + ) : ( + + {format(route.name)} + + )} +
  • + ))} +
+
+ +export const Pagination = ({ previous, next }) => + + {previous && ( + + Previous: + + {format(previous.name)} + + + )} + + {next && ( + + Next: + + {format(next.name)} + + + )} + + +const MobileNav = ({ + title, + logo, + update +}) => + + + update(toggle('menu'))}> + {logo || } + + + {title} + + + + + + +const toggle = key => state => ({ [key]: !state[key] }) +const close = state => ({ menu: false }) + +export default class Layout extends React.Component { + static propTypes = { + routes: PropTypes.array.isRequired + } + + state = { + menu: false, + update: fn => this.setState(fn) + } + + render () { + const { + routes = [], + children, + route, + title = 'x0', + logo, + } = this.props + const { menu, update } = this.state + + const opts = route ? route.props : {} + if (opts.layout === false) return children + const Wrapper = opts.fullWidth + ? React.Fragment + : MaxWidth + + const index = routes.findIndex(r => r === route) + const pagination = { + previous: routes[index - 1], + next: routes[index + 1] + } + + return ( + + + + + {menu && update(close)} />} + update(close)}> +