diff --git a/.circleci/config.yml b/.circleci/config.yml
index b4e68b5a7786..46e95d6f1484 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -35,6 +35,7 @@ jobs:
- node_modules
- examples/angular-cli/node_modules
- examples/cra-kitchen-sink/node_modules
+ - examples/mithril-kitchen-sink/node_modules
- examples/official-storybook/node_modules
- examples/polymer-cli/node_modules
- examples/vue-kitchen-sink/node_modules
@@ -100,6 +101,11 @@ jobs:
command: |
cd examples/official-storybook
yarn build-storybook
+ - run:
+ name: "Build mithril kitchen-sink"
+ command: |
+ cd examples/mithril-kitchen-sink
+ yarn build-storybook
- run:
name: "Visually test storybook"
command: |
@@ -148,6 +154,11 @@ jobs:
command: |
cd examples/official-storybook
yarn storybook --smoke-test
+ - run:
+ name: "Run mithril kitchen-sink (smoke test)"
+ command: |
+ cd examples/mithril-kitchen-sink
+ yarn storybook --smoke-test
react-native:
<<: *defaults
steps:
diff --git a/.github/autolabeler.yml b/.github/autolabeler.yml
index 956c243397a9..d89887889513 100644
--- a/.github/autolabeler.yml
+++ b/.github/autolabeler.yml
@@ -17,6 +17,7 @@
'app: react-native': ["app/react-native/**"]
'app: react': ["app/react/**"]
'app: vue': ["app/vue/**"]
+'app: mithril': ["app/mithril/**"]
'babel / webpack': ["webpack", "babel"]
'cli': ["lib/cli/**"]
'compatibility with other tools': []
diff --git a/ADDONS_SUPPORT.md b/ADDONS_SUPPORT.md
index 19303b2ae306..148b756ac9f0 100644
--- a/ADDONS_SUPPORT.md
+++ b/ADDONS_SUPPORT.md
@@ -1,19 +1,19 @@
## Addon / Framework Support Table
-| |[React](app/react)|[React Native](app/react-native)|[Vue](app/vue)|[Angular](app/angular)| [Polymer](app/polymer)|
-| ----------- |:-------:|:-------:|:-------:|:-------:|:-------:|
-|[a11y](addons/a11y) |+| | | | |
-|[actions](addons/actions) |+|+|+|+|+|
-|[background](addons/background) |+| | | | |
-|[centered](addons/centered) |+| |+| | |
-|[events](addons/events) |+| | | | |
-|[graphql](addons/graphql) |+| | | | |
-|[info](addons/info) |+| | | | |
-|[jest](addons/jest) |+| | | | |
-|[knobs](addons/knobs) |+|+|+|+|+|
-|[links](addons/links) |+|+|+|+|+|
-|[notes](addons/notes) |+| |+|+|+|
-|[options](addons/options) |+|+|+|+|+|
-|[storyshots](addons/storyshots) |+|+|+|+| |
-|[storysource](addons/storysource)|+| |+|+|+|
-|[viewport](addons/viewport) |+| |+|+|+|
+| |[React](app/react)|[React Native](app/react-native)|[Vue](app/vue)|[Angular](app/angular)| [Polymer](app/polymer)| [Mithril](app/mithril)|
+| ----------- |:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|
+|[a11y](addons/a11y) |+| | | | | |
+|[actions](addons/actions) |+|+|+|+|+|+|
+|[background](addons/background) |+| | | | |+|
+|[centered](addons/centered) |+| |+| | |+|
+|[events](addons/events) |+| | | | | |
+|[graphql](addons/graphql) |+| | | | | |
+|[info](addons/info) |+| | | | | |
+|[jest](addons/jest) |+| | | | | |
+|[knobs](addons/knobs) |+|+|+|+|+|+|
+|[links](addons/links) |+|+|+|+|+|+|
+|[notes](addons/notes) |+| |+|+|+|+|
+|[options](addons/options) |+|+|+|+|+|+|
+|[storyshots](addons/storyshots) |+|+|+|+| | |
+|[storysource](addons/storysource)|+| |+|+|+|+|
+|[viewport](addons/viewport) |+| |+|+|+|+|
diff --git a/README.md b/README.md
index ac0ca350409e..07d611abcb3c 100644
--- a/README.md
+++ b/README.md
@@ -72,6 +72,7 @@ For additional help, join us [in our Slack](https://now-examples-slackin-rrirkqo
- [Vue](app/vue)
- [Angular](app/angular)
- [Polymer](app/polymer) alpha
+- [Mithril](app/mithril) alpha
### Sub Projects
@@ -107,6 +108,7 @@ See [Addon / Framework Support Table](ADDONS_SUPPORT.md)
- [Vue](https://storybooks-vue.netlify.com/)
- [Angular](https://storybooks-angular.netlify.com/)
- [Polymer](https://storybooks-polymer.netlify.com/)
+- [Mithril](https://storybooks-mithril.netlify.com/)
### 3.4
- [React Official](https://release-3-4--storybooks-official.netlify.com)
diff --git a/addons/background/README.md b/addons/background/README.md
index 0dae593a512e..55f0146e95b4 100644
--- a/addons/background/README.md
+++ b/addons/background/README.md
@@ -69,3 +69,10 @@ storiesOf("Button", module)
.addDecorator(backgrounds)
.add("with text", () => Click me );
```
+
+> In the case of Mithril, use these imports:
+>
+> ```js
+> import { storiesOf } from '@storybook/mithril';
+> import backgrounds from "@storybook/addon-backgrounds/mithril";
+> ```
diff --git a/addons/background/mithril.js b/addons/background/mithril.js
new file mode 100644
index 000000000000..884a541476ec
--- /dev/null
+++ b/addons/background/mithril.js
@@ -0,0 +1 @@
+module.exports = require('./dist/mithril');
diff --git a/addons/background/src/mithril.js b/addons/background/src/mithril.js
new file mode 100644
index 000000000000..a9fd92f71e79
--- /dev/null
+++ b/addons/background/src/mithril.js
@@ -0,0 +1,39 @@
+/** @jsx m */
+
+// eslint-disable-next-line import/no-extraneous-dependencies
+import m from 'mithril';
+
+import addons from '@storybook/addons';
+
+export class BackgroundDecorator {
+ constructor(vnode) {
+ this.props = vnode.attrs;
+
+ const { channel, story } = vnode.attrs;
+
+ // A channel is explicitly passed in for testing
+ if (channel) {
+ this.channel = channel;
+ } else {
+ this.channel = addons.getChannel();
+ }
+
+ this.story = story();
+ }
+
+ oncreate() {
+ this.channel.emit('background-set', this.props.backgrounds);
+ }
+
+ onremove() {
+ this.channel.emit('background-unset');
+ }
+
+ view() {
+ return m(this.story);
+ }
+}
+
+export default backgrounds => story => ({
+ view: () => ,
+});
diff --git a/addons/centered/README.md b/addons/centered/README.md
index f7298db32882..b24f3819ee28 100644
--- a/addons/centered/README.md
+++ b/addons/centered/README.md
@@ -49,11 +49,29 @@ storiesOf('MyComponent', module)
.add('without props', () => ({
components: { MyComponent },
template: ' '
- })
+ }))
.add('with some props', () => ({
components: { MyComponent },
template: ' '
- });
+ }));
+```
+
+example for Mithril:
+
+```js
+import { storiesOf } from '@storybook/mithril';
+import centered from '@storybook/addon-centered/mithril';
+
+import MyComponent from '../Component';
+
+storiesOf('MyComponent', module)
+ .addDecorator(centered)
+ .add('without props', () => ({
+ view: () =>
+ }))
+ .add('with some props', () => ({
+ view: () =>
+ }));
```
Also, you can also add this decorator globally
@@ -84,6 +102,19 @@ configure(function () {
}, module);
```
+example for Mithril:
+
+```js
+import { configure, addDecorator } from '@storybook/mithril';
+import centered from '@storybook/addon-centered/mithril';
+
+addDecorator(centered);
+
+configure(function () {
+ //...
+}, module);
+```
+
#### As an extension
##### 1 - Configure the extension
diff --git a/addons/centered/mithril.js b/addons/centered/mithril.js
new file mode 100644
index 000000000000..884a541476ec
--- /dev/null
+++ b/addons/centered/mithril.js
@@ -0,0 +1 @@
+module.exports = require('./dist/mithril');
diff --git a/addons/centered/src/mithril.js b/addons/centered/src/mithril.js
new file mode 100644
index 000000000000..682d947e8307
--- /dev/null
+++ b/addons/centered/src/mithril.js
@@ -0,0 +1,30 @@
+/** @jsx m */
+
+// eslint-disable-next-line import/no-extraneous-dependencies
+import m from 'mithril';
+
+const style = {
+ position: 'fixed',
+ top: 0,
+ left: 0,
+ bottom: 0,
+ right: 0,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ overflow: 'auto',
+};
+
+const innerStyle = {
+ margin: 'auto',
+};
+
+export default function(storyFn) {
+ return {
+ view: () => (
+
+ ),
+ };
+}
diff --git a/addons/knobs/README.md b/addons/knobs/README.md
index 9a4b8e6912c9..7a98a6331c8a 100644
--- a/addons/knobs/README.md
+++ b/addons/knobs/README.md
@@ -118,6 +118,13 @@ stories.add('as dynamic variables', () => {
> import { storiesOf } from '@storybook/angular';
> import { withKnobs, text, boolean, number } from '@storybook/addon-knobs/angular';
> ```
+>
+> In the case of Mithril, use these imports:
+>
+> ```js
+> import { storiesOf } from '@storybook/mithril';
+> import { withKnobs, text, boolean, number } from '@storybook/addon-knobs/mithril';
+> ```
You can see your Knobs in a Storybook panel as shown below.
diff --git a/addons/knobs/mithril.js b/addons/knobs/mithril.js
new file mode 100644
index 000000000000..884a541476ec
--- /dev/null
+++ b/addons/knobs/mithril.js
@@ -0,0 +1 @@
+module.exports = require('./dist/mithril');
diff --git a/addons/knobs/src/mithril/WrapStory.js b/addons/knobs/src/mithril/WrapStory.js
new file mode 100644
index 000000000000..dd0ff093e754
--- /dev/null
+++ b/addons/knobs/src/mithril/WrapStory.js
@@ -0,0 +1,68 @@
+// eslint-disable-next-line import/no-extraneous-dependencies
+import m from 'mithril';
+
+export default class WrapStory {
+ constructor(vnode) {
+ this.knobChanged = this.knobChanged.bind(this);
+ this.knobClicked = this.knobClicked.bind(this);
+ this.resetKnobs = this.resetKnobs.bind(this);
+ this.setPaneKnobs = this.setPaneKnobs.bind(this);
+ this.props = vnode.attrs;
+ this.storyContent = vnode.attrs.initialContent;
+ }
+
+ oncreate() {
+ // Watch for changes in knob editor.
+ this.props.channel.on('addon:knobs:knobChange', this.knobChanged);
+ // Watch for clicks in knob editor.
+ this.props.channel.on('addon:knobs:knobClick', this.knobClicked);
+ // Watch for the reset event and reset knobs.
+ this.props.channel.on('addon:knobs:reset', this.resetKnobs);
+ // Watch for any change in the knobStore and set the panel again for those
+ // changes.
+ this.props.knobStore.subscribe(this.setPaneKnobs);
+ // Set knobs in the panel for the first time.
+ this.setPaneKnobs();
+ }
+
+ onremove() {
+ this.props.channel.removeListener('addon:knobs:knobChange', this.knobChanged);
+ this.props.channel.removeListener('addon:knobs:knobClick', this.knobClicked);
+ this.props.channel.removeListener('addon:knobs:reset', this.resetKnobs);
+ this.props.knobStore.unsubscribe(this.setPaneKnobs);
+ }
+
+ setPaneKnobs(timestamp = +new Date()) {
+ const { channel, knobStore } = this.props;
+ channel.emit('addon:knobs:setKnobs', { knobs: knobStore.getAll(), timestamp });
+ }
+
+ knobChanged(change) {
+ const { name, value } = change;
+ const { knobStore, storyFn, context } = this.props;
+ // Update the related knob and it's value.
+ const knobOptions = knobStore.get(name);
+
+ knobOptions.value = value;
+ knobStore.markAllUnused();
+ this.storyContent = storyFn(context);
+ m.redraw();
+ }
+
+ knobClicked(clicked) {
+ const knobOptions = this.props.knobStore.get(clicked.name);
+ knobOptions.callback();
+ }
+
+ resetKnobs() {
+ const { knobStore, storyFn, context } = this.props;
+ knobStore.reset();
+ this.storyContent = storyFn(context);
+ m.redraw();
+ this.setPaneKnobs(false);
+ }
+
+ view() {
+ return m(this.storyContent);
+ }
+}
diff --git a/addons/knobs/src/mithril/index.js b/addons/knobs/src/mithril/index.js
new file mode 100644
index 000000000000..f2dbdd29c2ae
--- /dev/null
+++ b/addons/knobs/src/mithril/index.js
@@ -0,0 +1,49 @@
+/** @jsx m */
+
+// eslint-disable-next-line import/no-extraneous-dependencies
+import m from 'mithril';
+import addons from '@storybook/addons';
+
+import WrapStory from './WrapStory';
+
+import {
+ knob,
+ text,
+ boolean,
+ number,
+ color,
+ object,
+ array,
+ date,
+ select,
+ selectV2,
+ button,
+ manager,
+} from '../base';
+
+export { knob, text, boolean, number, color, object, array, date, select, selectV2, button };
+
+export const mithrilHandler = (channel, knobStore) => getStory => context => {
+ const initialContent = getStory(context);
+ const props = { context, storyFn: getStory, channel, knobStore, initialContent };
+ return {
+ view: () => ,
+ };
+};
+
+function wrapperKnobs(options) {
+ const channel = addons.getChannel();
+ manager.setChannel(channel);
+
+ if (options) channel.emit('addon:knobs:setOptions', options);
+
+ return mithrilHandler(channel, manager.knobStore);
+}
+
+export function withKnobs(storyFn, context) {
+ return wrapperKnobs()(storyFn)(context);
+}
+
+export function withKnobsOptions(options = {}) {
+ return (storyFn, context) => wrapperKnobs(options)(storyFn)(context);
+}
diff --git a/app/mithril/.npmignore b/app/mithril/.npmignore
new file mode 100644
index 000000000000..6d236e2ff68d
--- /dev/null
+++ b/app/mithril/.npmignore
@@ -0,0 +1,2 @@
+docs
+.babelrc
diff --git a/app/mithril/README.md b/app/mithril/README.md
new file mode 100644
index 000000000000..a373ca63ffb9
--- /dev/null
+++ b/app/mithril/README.md
@@ -0,0 +1,33 @@
+# Storybook for Mithril
+
+[![Build Status on CircleCI](https://circleci.com/gh/storybooks/storybook.svg?style=shield)](https://circleci.com/gh/storybooks/storybook)
+[![CodeFactor](https://www.codefactor.io/repository/github/storybooks/storybook/badge)](https://www.codefactor.io/repository/github/storybooks/storybook)
+[![Known Vulnerabilities](https://snyk.io/test/github/storybooks/storybook/8f36abfd6697e58cd76df3526b52e4b9dc894847/badge.svg)](https://snyk.io/test/github/storybooks/storybook/8f36abfd6697e58cd76df3526b52e4b9dc894847)
+[![BCH compliance](https://bettercodehub.com/edge/badge/storybooks/storybook)](https://bettercodehub.com/results/storybooks/storybook) [![codecov](https://codecov.io/gh/storybooks/storybook/branch/master/graph/badge.svg)](https://codecov.io/gh/storybooks/storybook)
+[![Storybook Slack](https://now-examples-slackin-rrirkqohko.now.sh/badge.svg)](https://now-examples-slackin-rrirkqohko.now.sh/)
+[![Backers on Open Collective](https://opencollective.com/storybook/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/storybook/sponsors/badge.svg)](#sponsors)
+
+* * *
+
+Storybook for Mithril is a UI development environment for your Mithril components.
+With it, you can visualize different states of your UI components and develop them interactively.
+
+![Storybook Screenshot](docs/demo.gif)
+
+Storybook runs outside of your app.
+So you can develop UI components in isolation without worrying about app specific dependencies and requirements.
+
+## Getting Started
+
+```sh
+npm i -g @storybook/cli
+cd my-mithril-app
+getstorybook
+```
+
+For more information visit: [storybook.js.org](https://storybook.js.org)
+
+* * *
+
+Storybook also comes with a lot of [addons](https://storybook.js.org/addons/introduction) and a great API to customize as you wish.
+You can also build a [static version](https://storybook.js.org/basics/exporting-storybook) of your storybook and deploy it anywhere you want.
diff --git a/app/mithril/bin/build.js b/app/mithril/bin/build.js
new file mode 100755
index 000000000000..780773c6cd31
--- /dev/null
+++ b/app/mithril/bin/build.js
@@ -0,0 +1,3 @@
+#!/usr/bin/env node
+
+require('../dist/server/build');
diff --git a/app/mithril/bin/index.js b/app/mithril/bin/index.js
new file mode 100755
index 000000000000..2e96258ce63d
--- /dev/null
+++ b/app/mithril/bin/index.js
@@ -0,0 +1,3 @@
+#!/usr/bin/env node
+
+require('../dist/server');
diff --git a/app/mithril/docs/demo.gif b/app/mithril/docs/demo.gif
new file mode 100644
index 000000000000..c8366f8534a6
Binary files /dev/null and b/app/mithril/docs/demo.gif differ
diff --git a/app/mithril/docs/react_storybook_screenshot.png b/app/mithril/docs/react_storybook_screenshot.png
new file mode 100644
index 000000000000..9763382042b2
Binary files /dev/null and b/app/mithril/docs/react_storybook_screenshot.png differ
diff --git a/app/mithril/docs/storybooks_io_logo.png b/app/mithril/docs/storybooks_io_logo.png
new file mode 100644
index 000000000000..3dd9b09f3a95
Binary files /dev/null and b/app/mithril/docs/storybooks_io_logo.png differ
diff --git a/app/mithril/package.json b/app/mithril/package.json
new file mode 100644
index 000000000000..839d89004fd3
--- /dev/null
+++ b/app/mithril/package.json
@@ -0,0 +1,67 @@
+{
+ "name": "@storybook/mithril",
+ "version": "3.4.0-rc.3",
+ "description": "Storybook for Mithril: Develop Mithril Component in isolation.",
+ "homepage": "https://github.com/storybooks/storybook/tree/master/app/mithril",
+ "bugs": {
+ "url": "https://github.com/storybooks/storybook/issues"
+ },
+ "license": "MIT",
+ "main": "dist/client/index.js",
+ "jsnext:main": "src/client/index.js",
+ "bin": {
+ "build-storybook": "./bin/build.js",
+ "start-storybook": "./bin/index.js",
+ "storybook-server": "./bin/index.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/storybooks/storybook.git"
+ },
+ "scripts": {
+ "dev": "cross-env DEV_BUILD=1 nodemon --watch ./src --exec \"yarn prepare\"",
+ "prepare": "node ../../scripts/prepare.js"
+ },
+ "dependencies": {
+ "@storybook/addons": "3.4.0-rc.3",
+ "@storybook/channel-postmessage": "3.4.0-rc.3",
+ "@storybook/client-logger": "3.4.0-rc.3",
+ "@storybook/core": "3.4.0-rc.3",
+ "@storybook/node-logger": "3.4.0-rc.3",
+ "@storybook/ui": "3.4.0-rc.3",
+ "airbnb-js-shims": "^1.4.1",
+ "babel-loader": "^7.1.4",
+ "babel-plugin-macros": "^2.2.0",
+ "babel-plugin-transform-regenerator": "^6.26.0",
+ "babel-plugin-transform-runtime": "^6.23.0",
+ "babel-preset-env": "^1.6.1",
+ "babel-preset-minify": "^0.3.0",
+ "babel-preset-stage-0": "^6.24.1",
+ "babel-runtime": "^6.26.0",
+ "case-sensitive-paths-webpack-plugin": "^2.1.2",
+ "common-tags": "^1.7.2",
+ "core-js": "^2.5.4",
+ "dotenv-webpack": "^1.5.5",
+ "find-cache-dir": "^1.0.0",
+ "global": "^4.3.2",
+ "html-loader": "^0.5.5",
+ "html-webpack-plugin": "^3.1.0",
+ "json5": "^0.5.1",
+ "markdown-loader": "^2.0.2",
+ "react": "^16.2.0",
+ "react-dev-utils": "^5.0.0",
+ "react-dom": "^16.2.0",
+ "util-deprecate": "^1.0.2",
+ "webpack": "^4.3.0",
+ "webpack-hot-middleware": "^2.21.2"
+ },
+ "devDependencies": {
+ "mithril": "^1.1.6",
+ "nodemon": "^1.17.2"
+ },
+ "peerDependencies": {
+ "babel-core": "^6.26.0 || ^7.0.0-0",
+ "babel-runtime": ">=6.0.0",
+ "mithril": "^1.1.6"
+ }
+}
diff --git a/app/mithril/src/client/index.js b/app/mithril/src/client/index.js
new file mode 100644
index 000000000000..308ceea8889f
--- /dev/null
+++ b/app/mithril/src/client/index.js
@@ -0,0 +1,8 @@
+export {
+ storiesOf,
+ setAddon,
+ addDecorator,
+ configure,
+ getStorybook,
+ forceReRender,
+} from './preview';
diff --git a/app/mithril/src/client/preview/error_display.js b/app/mithril/src/client/preview/error_display.js
new file mode 100644
index 000000000000..aa50e814985d
--- /dev/null
+++ b/app/mithril/src/client/preview/error_display.js
@@ -0,0 +1,45 @@
+/** @jsx m */
+
+import m from 'mithril';
+
+const mainStyle = {
+ position: 'fixed',
+ top: 0,
+ bottom: 0,
+ left: 0,
+ right: 0,
+ padding: 20,
+ backgroundColor: 'rgb(187, 49, 49)',
+ color: '#FFF',
+ WebkitFontSmoothing: 'antialiased',
+};
+
+const headingStyle = {
+ fontSize: 20,
+ fontWeight: 600,
+ letterSpacing: 0.2,
+ margin: '10px 0',
+ fontFamily: `
+ -apple-system, ".SFNSText-Regular", "San Francisco", Roboto, "Segoe UI",
+ "Helvetica Neue", "Lucida Grande", sans-serif
+ `,
+};
+
+const codeStyle = {
+ fontSize: 14,
+ width: '100vw',
+ overflow: 'auto',
+};
+
+const ErrorDisplay = {
+ view: vnode => (
+
+
{vnode.attrs.error.message}
+
+ {vnode.attrs.error.stack}
+
+
+ ),
+};
+
+export default ErrorDisplay;
diff --git a/app/mithril/src/client/preview/index.js b/app/mithril/src/client/preview/index.js
new file mode 100644
index 000000000000..b04f8439b607
--- /dev/null
+++ b/app/mithril/src/client/preview/index.js
@@ -0,0 +1,17 @@
+import { start } from '@storybook/core/client';
+
+import render from './render';
+
+const { clientApi, configApi, forceReRender } = start(render);
+
+export const {
+ storiesOf,
+ setAddon,
+ addDecorator,
+ addParameters,
+ clearDecorators,
+ getStorybook,
+} = clientApi;
+
+export const { configure } = configApi;
+export { forceReRender };
diff --git a/app/mithril/src/client/preview/render.js b/app/mithril/src/client/preview/render.js
new file mode 100644
index 000000000000..1c6272a64110
--- /dev/null
+++ b/app/mithril/src/client/preview/render.js
@@ -0,0 +1,93 @@
+/* global document */
+/** @jsx m */
+
+import m from 'mithril';
+import { stripIndents } from 'common-tags';
+import { logger } from '@storybook/client-logger';
+import ErrorDisplay from './error_display';
+
+// check whether we're running on node/browser
+const isBrowser = typeof window !== 'undefined';
+
+let rootEl = null;
+let previousKind = '';
+let previousStory = '';
+
+if (isBrowser) {
+ rootEl = document.getElementById('root');
+}
+
+export function renderError(error) {
+ const properError = new Error(error.title);
+ properError.stack = error.description;
+
+ const redBox = ;
+ m.mount(rootEl, { view: () => redBox });
+}
+
+export function renderException(error) {
+ // We always need to render redbox in the mainPage if we get an error.
+ // Since this is an error, this affects to the main page as well.
+ const realError = new Error(error.message);
+ realError.stack = error.stack;
+ const redBox = ;
+ m.mount(rootEl, { view: () => redBox });
+
+ // Log the stack to the console. So, user could check the source code.
+ logger.error(error.stack);
+}
+
+export function renderMain(data, storyStore, forceRender) {
+ if (storyStore.size() === 0) return;
+
+ const NoPreview = { view: () => No Preview Available!
};
+ const noPreview = ;
+ const { selectedKind, selectedStory } = data;
+
+ const story = storyStore.getStory(selectedKind, selectedStory);
+ if (!story) {
+ m.mount(rootEl, { view: () => noPreview });
+ return;
+ }
+
+ if (!forceRender && selectedKind === previousKind && previousStory === selectedStory) {
+ return;
+ }
+
+ previousKind = selectedKind;
+ previousStory = selectedStory;
+
+ const context = {
+ kind: selectedKind,
+ story: selectedStory,
+ };
+
+ const element = story(context);
+
+ if (!element) {
+ const error = {
+ title: `Expecting a Mithril element from the story: "${selectedStory}" of "${selectedKind}".`,
+ description: stripIndents`
+ Did you forget to return the Mithril element from the story?
+ Use "() => MyComp" or "() => { return MyComp; }" when defining the story.
+ `,
+ };
+ renderError(error);
+ return;
+ }
+
+ m.mount(rootEl, { view: () => m(element) });
+}
+
+export default function renderPreview({ reduxStore, storyStore }, forceRender = false) {
+ const state = reduxStore.getState();
+ if (state.error) {
+ return renderException(state.error);
+ }
+
+ try {
+ return renderMain(state, storyStore, forceRender);
+ } catch (ex) {
+ return renderException(ex);
+ }
+}
diff --git a/app/mithril/src/server/babel_config.js b/app/mithril/src/server/babel_config.js
new file mode 100644
index 000000000000..7758d5279985
--- /dev/null
+++ b/app/mithril/src/server/babel_config.js
@@ -0,0 +1,68 @@
+import fs from 'fs';
+import path from 'path';
+import JSON5 from 'json5';
+import { logger } from '@storybook/node-logger';
+import defaultConfig from './config/babel';
+
+function removeReactHmre(presets) {
+ const index = presets.indexOf('react-hmre');
+ if (index > -1) {
+ presets.splice(index, 1);
+ }
+}
+
+// Tries to load a .babelrc and returns the parsed object if successful
+function loadFromPath(babelConfigPath) {
+ let config;
+ if (fs.existsSync(babelConfigPath)) {
+ const content = fs.readFileSync(babelConfigPath, 'utf-8');
+ try {
+ config = JSON5.parse(content);
+ config.babelrc = false;
+ logger.info('=> Loading custom .babelrc');
+ } catch (e) {
+ logger.error(`=> Error parsing .babelrc file: ${e.message}`);
+ throw e;
+ }
+ }
+
+ if (!config) return null;
+
+ // Remove react-hmre preset.
+ // It causes issues with react-storybook.
+ // We don't really need it.
+ // Earlier, we fix this by running storybook in the production mode.
+ // But, that hide some useful debug messages.
+ if (config.presets) {
+ removeReactHmre(config.presets);
+ }
+
+ if (config.env && config.env.development && config.env.development.presets) {
+ removeReactHmre(config.env.development.presets);
+ }
+
+ return config;
+}
+
+export default function(configDir) {
+ let babelConfig = loadFromPath(path.resolve(configDir, '.babelrc'));
+ let inConfigDir = true;
+
+ if (!babelConfig) {
+ babelConfig = loadFromPath('.babelrc');
+ inConfigDir = false;
+ }
+
+ if (babelConfig) {
+ // If the custom config uses babel's `extends` clause, then replace it with
+ // an absolute path. `extends` will not work unless we do this.
+ if (babelConfig.extends) {
+ babelConfig.extends = inConfigDir
+ ? path.resolve(configDir, babelConfig.extends)
+ : path.resolve(babelConfig.extends);
+ }
+ }
+
+ const finalConfig = babelConfig || defaultConfig;
+ return finalConfig;
+}
diff --git a/app/mithril/src/server/babel_config.test.js b/app/mithril/src/server/babel_config.test.js
new file mode 100644
index 000000000000..8c7c0acd2f4c
--- /dev/null
+++ b/app/mithril/src/server/babel_config.test.js
@@ -0,0 +1,84 @@
+import loadBabelConfig from './babel_config';
+
+// eslint-disable-next-line global-require
+jest.mock('fs', () => require('../../../../__mocks__/fs'));
+jest.mock('path', () => ({
+ resolve: () => '.babelrc',
+}));
+
+const setup = ({ files }) => {
+ // eslint-disable-next-line no-underscore-dangle, global-require
+ require('fs').__setMockFiles(files);
+};
+
+describe('babel_config', () => {
+ // As the 'fs' is going to be mocked, let's call require.resolve
+ // so the require.cache has the correct route to the file.
+ // In fact let's use it in the tests :)
+ it('should return the config with the extra plugins when `plugins` is an array.', () => {
+ setup({
+ files: {
+ '.babelrc': `{
+ "presets": [
+ "env",
+ "foo-preset"
+ ],
+ "plugins": [
+ "foo-plugin"
+ ]
+ }`,
+ },
+ });
+
+ const config = loadBabelConfig('.foo');
+
+ expect(config).toEqual({
+ babelrc: false,
+ plugins: ['foo-plugin'],
+ presets: ['env', 'foo-preset'],
+ });
+ });
+
+ it('should return the config with the extra plugins when `plugins` is not an array.', () => {
+ setup({
+ files: {
+ '.babelrc': `{
+ "presets": [
+ "env",
+ "foo-preset"
+ ],
+ "plugins": "bar-plugin"
+ }`,
+ },
+ });
+
+ const config = loadBabelConfig('.bar');
+
+ expect(config).toEqual({
+ babelrc: false,
+ plugins: 'bar-plugin',
+ presets: ['env', 'foo-preset'],
+ });
+ });
+
+ it('should return the config only with the extra plugins when `plugins` is not present.', () => {
+ // Mock a `.babelrc` config file with no plugins key.
+ setup({
+ files: {
+ '.babelrc': `{
+ "presets": [
+ "env",
+ "foo-preset"
+ ]
+ }`,
+ },
+ });
+
+ const config = loadBabelConfig('.biz');
+
+ expect(config).toEqual({
+ babelrc: false,
+ presets: ['env', 'foo-preset'],
+ });
+ });
+});
diff --git a/app/mithril/src/server/build.js b/app/mithril/src/server/build.js
new file mode 100755
index 000000000000..804f258a0ba4
--- /dev/null
+++ b/app/mithril/src/server/build.js
@@ -0,0 +1,12 @@
+import { buildStatic } from '@storybook/core/server';
+import path from 'path';
+import packageJson from '../../package.json';
+import getBaseConfig from './config/webpack.config.prod';
+import loadConfig from './config';
+
+buildStatic({
+ packageJson,
+ getBaseConfig,
+ loadConfig,
+ defaultFavIcon: path.resolve(__dirname, 'public/favicon.ico'),
+});
diff --git a/app/mithril/src/server/config.js b/app/mithril/src/server/config.js
new file mode 100644
index 000000000000..25f113cd195b
--- /dev/null
+++ b/app/mithril/src/server/config.js
@@ -0,0 +1,86 @@
+/* eslint-disable global-require, import/no-dynamic-require */
+import fs from 'fs';
+import path from 'path';
+import findCacheDir from 'find-cache-dir';
+import { logger } from '@storybook/node-logger';
+import { createDefaultWebpackConfig } from '@storybook/core/server';
+import loadBabelConfig from './babel_config';
+
+// `baseConfig` is a webpack configuration bundled with storybook.
+// Storybook will look in the `configDir` directory
+// (inside working directory) if a config path is not provided.
+export default function(configType, baseConfig, configDir) {
+ const config = baseConfig;
+
+ const babelConfig = loadBabelConfig(configDir);
+ config.module.rules[0].query = {
+ // This is a feature of `babel-loader` for webpack (not Babel itself).
+ // It enables a cache directory for faster-rebuilds
+ // `find-cache-dir` will create the cache directory under the node_modules directory.
+ cacheDirectory: findCacheDir({ name: 'react-storybook' }),
+ ...babelConfig,
+ };
+
+ // Check whether a config.js file exists inside the storybook
+ // config directory and throw an error if it's not.
+ const storybookConfigPath = path.resolve(configDir, 'config.js');
+ if (!fs.existsSync(storybookConfigPath)) {
+ const err = new Error(`=> Create a storybook config file in "${configDir}/config.js".`);
+ throw err;
+ }
+ config.entry.preview.push(require.resolve(storybookConfigPath));
+
+ // Check whether addons.js file exists inside the storybook.
+ // Load the default addons.js file if it's missing.
+ // Insert it after polyfills.js, but before client/manager.
+ const storybookCustomAddonsPath = path.resolve(configDir, 'addons.js');
+ if (fs.existsSync(storybookCustomAddonsPath)) {
+ logger.info('=> Loading custom addons config.');
+ config.entry.manager.splice(1, 0, storybookCustomAddonsPath);
+ }
+
+ const defaultConfig = createDefaultWebpackConfig(config);
+
+ // Check whether user has a custom webpack config file and
+ // return the (extended) base configuration if it's not available.
+ const customConfigPath = path.resolve(configDir, 'webpack.config.js');
+
+ if (!fs.existsSync(customConfigPath)) {
+ logger.info('=> Using default webpack setup.');
+ return defaultConfig;
+ }
+ const customConfig = require(customConfigPath);
+
+ if (typeof customConfig === 'function') {
+ logger.info('=> Loading custom webpack config (full-control mode).');
+ return customConfig(config, configType, defaultConfig);
+ }
+ logger.info('=> Loading custom webpack config (extending mode).');
+ return {
+ ...customConfig,
+ // We'll always load our configurations after the custom config.
+ // So, we'll always load the stuff we need.
+ ...config,
+ // Override with custom devtool if provided
+ devtool: customConfig.devtool || config.devtool,
+ // We need to use our and custom plugins.
+ plugins: [...config.plugins, ...(customConfig.plugins || [])],
+ module: {
+ ...config.module,
+ // We need to use our and custom rules.
+ ...customConfig.module,
+ rules: [
+ ...config.module.rules,
+ ...((customConfig.module && customConfig.module.rules) || []),
+ ],
+ },
+ resolve: {
+ ...config.resolve,
+ ...customConfig.resolve,
+ alias: {
+ ...config.alias,
+ ...(customConfig.resolve && customConfig.resolve.alias),
+ },
+ },
+ };
+}
diff --git a/app/mithril/src/server/config/babel.js b/app/mithril/src/server/config/babel.js
new file mode 100644
index 000000000000..77a413a7cb48
--- /dev/null
+++ b/app/mithril/src/server/config/babel.js
@@ -0,0 +1,28 @@
+module.exports = {
+ // Don't try to find .babelrc because we want to force this configuration.
+ babelrc: false,
+ presets: [
+ [
+ require.resolve('babel-preset-env'),
+ {
+ targets: {
+ browsers: ['last 2 versions', 'safari >= 7'],
+ },
+ modules: process.env.NODE_ENV === 'test' ? 'commonjs' : false,
+ },
+ ],
+ require.resolve('babel-preset-stage-0'),
+ ],
+ plugins: [
+ require.resolve('babel-plugin-macros'),
+ require.resolve('babel-plugin-transform-regenerator'),
+ [
+ require.resolve('babel-plugin-transform-runtime'),
+ {
+ helpers: true,
+ polyfill: true,
+ regenerator: true,
+ },
+ ],
+ ],
+};
diff --git a/app/mithril/src/server/config/babel.prod.js b/app/mithril/src/server/config/babel.prod.js
new file mode 100644
index 000000000000..0418f1a1dc3b
--- /dev/null
+++ b/app/mithril/src/server/config/babel.prod.js
@@ -0,0 +1,28 @@
+module.exports = {
+ // Don't try to find .babelrc because we want to force this configuration.
+ babelrc: false,
+ presets: [
+ [
+ require.resolve('babel-preset-env'),
+ {
+ targets: {
+ browsers: ['last 2 versions', 'safari >= 7'],
+ },
+ modules: false,
+ },
+ ],
+ require.resolve('babel-preset-stage-0'),
+ require.resolve('babel-preset-minify'),
+ ],
+ plugins: [
+ require.resolve('babel-plugin-transform-regenerator'),
+ [
+ require.resolve('babel-plugin-transform-runtime'),
+ {
+ helpers: true,
+ polyfill: true,
+ regenerator: true,
+ },
+ ],
+ ],
+};
diff --git a/app/mithril/src/server/config/globals.js b/app/mithril/src/server/config/globals.js
new file mode 100644
index 000000000000..defcc85dc655
--- /dev/null
+++ b/app/mithril/src/server/config/globals.js
@@ -0,0 +1,4 @@
+/* globals window */
+
+window.STORYBOOK_REACT_CLASSES = {};
+window.STORYBOOK_ENV = 'mithril';
diff --git a/app/mithril/src/server/config/polyfills.js b/app/mithril/src/server/config/polyfills.js
new file mode 100644
index 000000000000..869b6824b5ff
--- /dev/null
+++ b/app/mithril/src/server/config/polyfills.js
@@ -0,0 +1,3 @@
+import 'core-js/es6/symbol';
+import 'core-js/fn/array/iterator';
+import 'airbnb-js-shims';
diff --git a/app/mithril/src/server/config/utils.js b/app/mithril/src/server/config/utils.js
new file mode 100644
index 000000000000..fc73c38a37f0
--- /dev/null
+++ b/app/mithril/src/server/config/utils.js
@@ -0,0 +1,35 @@
+import path from 'path';
+
+export const includePaths = [path.resolve('./')];
+
+export const excludePaths = [path.resolve('node_modules')];
+
+export const nodeModulesPaths = path.resolve('./node_modules');
+
+export const nodePaths = (process.env.NODE_PATH || '')
+ .split(process.platform === 'win32' ? ';' : ':')
+ .filter(Boolean)
+ .map(p => path.resolve('./', p));
+
+// Load environment variables starts with STORYBOOK_ to the client side.
+export function loadEnv(options = {}) {
+ const defaultNodeEnv = options.production ? 'production' : 'development';
+ const env = {
+ NODE_ENV: JSON.stringify(process.env.NODE_ENV || defaultNodeEnv),
+ // This is to support CRA's public folder feature.
+ // In production we set this to dot(.) to allow the browser to access these assests
+ // even when deployed inside a subpath. (like in GitHub pages)
+ // In development this is just empty as we always serves from the root.
+ PUBLIC_URL: JSON.stringify(options.production ? '.' : ''),
+ };
+
+ Object.keys(process.env)
+ .filter(name => /^STORYBOOK_/.test(name))
+ .forEach(name => {
+ env[name] = JSON.stringify(process.env[name]);
+ });
+
+ return {
+ 'process.env': env,
+ };
+}
diff --git a/app/mithril/src/server/config/webpack.config.js b/app/mithril/src/server/config/webpack.config.js
new file mode 100644
index 000000000000..056565dc92bc
--- /dev/null
+++ b/app/mithril/src/server/config/webpack.config.js
@@ -0,0 +1,94 @@
+import path from 'path';
+import webpack from 'webpack';
+import Dotenv from 'dotenv-webpack';
+import InterpolateHtmlPlugin from 'react-dev-utils/InterpolateHtmlPlugin';
+import WatchMissingNodeModulesPlugin from 'react-dev-utils/WatchMissingNodeModulesPlugin';
+import CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin';
+import HtmlWebpackPlugin from 'html-webpack-plugin';
+import { managerPath } from '@storybook/core/server';
+
+import { includePaths, excludePaths, nodeModulesPaths, loadEnv, nodePaths } from './utils';
+import babelLoaderConfig from './babel';
+import { getPreviewHeadHtml, getManagerHeadHtml } from '../utils';
+import { version } from '../../../package.json';
+
+export default function(configDir) {
+ const config = {
+ mode: 'development',
+ devtool: 'cheap-module-source-map',
+ entry: {
+ manager: [require.resolve('./polyfills'), managerPath],
+ preview: [
+ require.resolve('./polyfills'),
+ require.resolve('./globals'),
+ `${require.resolve('webpack-hot-middleware/client')}?reload=true`,
+ ],
+ },
+ output: {
+ path: path.join(__dirname, 'dist'),
+ filename: 'static/[name].bundle.js',
+ publicPath: '/',
+ },
+ plugins: [
+ new HtmlWebpackPlugin({
+ filename: 'index.html',
+ chunks: ['manager'],
+ data: {
+ managerHead: getManagerHeadHtml(configDir),
+ version,
+ },
+ template: require.resolve('../index.html.ejs'),
+ }),
+ new HtmlWebpackPlugin({
+ filename: 'iframe.html',
+ excludeChunks: ['manager'],
+ data: {
+ previewHead: getPreviewHeadHtml(configDir),
+ },
+ template: require.resolve('../iframe.html.ejs'),
+ }),
+ new InterpolateHtmlPlugin(process.env),
+ new webpack.DefinePlugin(loadEnv()),
+ new webpack.HotModuleReplacementPlugin(),
+ new CaseSensitivePathsPlugin(),
+ new WatchMissingNodeModulesPlugin(nodeModulesPaths),
+ new webpack.ProgressPlugin(),
+ new Dotenv({ silent: true }),
+ ],
+ module: {
+ rules: [
+ {
+ test: /\.jsx?$/,
+ loader: require.resolve('babel-loader'),
+ query: babelLoaderConfig,
+ include: includePaths,
+ exclude: excludePaths,
+ },
+ {
+ test: /\.md$/,
+ use: [
+ {
+ loader: require.resolve('html-loader'),
+ },
+ {
+ loader: require.resolve('markdown-loader'),
+ },
+ ],
+ },
+ ],
+ },
+ resolve: {
+ // Since we ship with json-loader always, it's better to move extensions to here
+ // from the default config.
+ extensions: ['.js', '.json', '.jsx'],
+ // Add support to NODE_PATH. With this we could avoid relative path imports.
+ // Based on this CRA feature: https://github.com/facebookincubator/create-react-app/issues/253
+ modules: ['node_modules'].concat(nodePaths),
+ },
+ performance: {
+ hints: false,
+ },
+ };
+
+ return config;
+}
diff --git a/app/mithril/src/server/config/webpack.config.prod.js b/app/mithril/src/server/config/webpack.config.prod.js
new file mode 100644
index 000000000000..cf4a52274623
--- /dev/null
+++ b/app/mithril/src/server/config/webpack.config.prod.js
@@ -0,0 +1,86 @@
+import webpack from 'webpack';
+import Dotenv from 'dotenv-webpack';
+import InterpolateHtmlPlugin from 'react-dev-utils/InterpolateHtmlPlugin';
+import HtmlWebpackPlugin from 'html-webpack-plugin';
+import { managerPath } from '@storybook/core/server';
+import babelLoaderConfig from './babel.prod';
+import { includePaths, excludePaths, loadEnv, nodePaths } from './utils';
+import { getPreviewHeadHtml, getManagerHeadHtml } from '../utils';
+import { version } from '../../../package.json';
+
+export default function(configDir) {
+ const entries = {
+ preview: [require.resolve('./polyfills'), require.resolve('./globals')],
+ manager: [require.resolve('./polyfills'), managerPath],
+ };
+
+ const config = {
+ mode: 'production',
+ bail: true,
+ devtool: '#cheap-module-source-map',
+ entry: entries,
+ output: {
+ filename: 'static/[name].[chunkhash].bundle.js',
+ // Here we set the publicPath to ''.
+ // This allows us to deploy storybook into subpaths like GitHub pages.
+ // This works with css and image loaders too.
+ // This is working for storybook since, we don't use pushState urls and
+ // relative URLs works always.
+ publicPath: '',
+ },
+ plugins: [
+ new HtmlWebpackPlugin({
+ filename: 'index.html',
+ chunks: ['manager'],
+ data: {
+ managerHead: getManagerHeadHtml(configDir),
+ version,
+ },
+ template: require.resolve('../index.html.ejs'),
+ }),
+ new HtmlWebpackPlugin({
+ filename: 'iframe.html',
+ excludeChunks: ['manager'],
+ data: {
+ previewHead: getPreviewHeadHtml(configDir),
+ },
+ template: require.resolve('../iframe.html.ejs'),
+ }),
+ new InterpolateHtmlPlugin(process.env),
+ new webpack.DefinePlugin(loadEnv({ production: true })),
+ new Dotenv({ silent: true }),
+ ],
+ module: {
+ rules: [
+ {
+ test: /\.jsx?$/,
+ loader: require.resolve('babel-loader'),
+ query: babelLoaderConfig,
+ include: includePaths,
+ exclude: excludePaths,
+ },
+ {
+ test: /\.md$/,
+ use: [
+ {
+ loader: require.resolve('html-loader'),
+ },
+ {
+ loader: require.resolve('markdown-loader'),
+ },
+ ],
+ },
+ ],
+ },
+ resolve: {
+ // Since we ship with json-loader always, it's better to move extensions to here
+ // from the default config.
+ extensions: ['.js', '.json', '.jsx'],
+ // Add support to NODE_PATH. With this we could avoid relative path imports.
+ // Based on this CRA feature: https://github.com/facebookincubator/create-react-app/issues/253
+ modules: ['node_modules'].concat(nodePaths),
+ },
+ };
+
+ return config;
+}
diff --git a/app/mithril/src/server/iframe.html.ejs b/app/mithril/src/server/iframe.html.ejs
new file mode 100644
index 000000000000..8d0c1baa34fd
--- /dev/null
+++ b/app/mithril/src/server/iframe.html.ejs
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+ Storybook
+ <%= htmlWebpackPlugin.options.data.previewHead %>
+
+
+
+
+
+
diff --git a/app/mithril/src/server/index.html.ejs b/app/mithril/src/server/index.html.ejs
new file mode 100644
index 000000000000..dfcb56218d36
--- /dev/null
+++ b/app/mithril/src/server/index.html.ejs
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+ Storybook
+
+ <%= htmlWebpackPlugin.options.data.managerHead %>
+
+
+
+
+
+
diff --git a/app/mithril/src/server/index.js b/app/mithril/src/server/index.js
new file mode 100755
index 000000000000..69df1fe59e54
--- /dev/null
+++ b/app/mithril/src/server/index.js
@@ -0,0 +1,12 @@
+import { buildDev } from '@storybook/core/server';
+import path from 'path';
+import packageJson from '../../package.json';
+import getBaseConfig from './config/webpack.config';
+import loadConfig from './config';
+
+buildDev({
+ packageJson,
+ getBaseConfig,
+ loadConfig,
+ defaultFavIcon: path.resolve(__dirname, 'public/favicon.ico'),
+});
diff --git a/app/mithril/src/server/public/favicon.ico b/app/mithril/src/server/public/favicon.ico
new file mode 100755
index 000000000000..e1cf7f1c59fd
Binary files /dev/null and b/app/mithril/src/server/public/favicon.ico differ
diff --git a/app/mithril/src/server/utils.js b/app/mithril/src/server/utils.js
new file mode 100644
index 000000000000..913f4c3219ea
--- /dev/null
+++ b/app/mithril/src/server/utils.js
@@ -0,0 +1,30 @@
+import path from 'path';
+import fs from 'fs';
+import deprecate from 'util-deprecate';
+
+const fallbackHeadUsage = deprecate(() => {},
+'Usage of head.html has been deprecated. Please rename head.html to preview-head.html');
+
+export function getPreviewHeadHtml(configDirPath) {
+ const headHtmlPath = path.resolve(configDirPath, 'preview-head.html');
+ const fallbackHtmlPath = path.resolve(configDirPath, 'head.html');
+ let headHtml = '';
+ if (fs.existsSync(headHtmlPath)) {
+ headHtml = fs.readFileSync(headHtmlPath, 'utf8');
+ } else if (fs.existsSync(fallbackHtmlPath)) {
+ headHtml = fs.readFileSync(fallbackHtmlPath, 'utf8');
+ fallbackHeadUsage();
+ }
+
+ return headHtml;
+}
+
+export function getManagerHeadHtml(configDirPath) {
+ const scriptPath = path.resolve(configDirPath, 'manager-head.html');
+ let scriptHtml = '';
+ if (fs.existsSync(scriptPath)) {
+ scriptHtml = fs.readFileSync(scriptPath, 'utf8');
+ }
+
+ return scriptHtml;
+}
diff --git a/app/mithril/src/server/utils.test.js b/app/mithril/src/server/utils.test.js
new file mode 100644
index 000000000000..a8db3b65a210
--- /dev/null
+++ b/app/mithril/src/server/utils.test.js
@@ -0,0 +1,69 @@
+import { getPreviewHeadHtml, getManagerHeadHtml } from './utils';
+
+// eslint-disable-next-line global-require
+jest.mock('fs', () => require('../../../../__mocks__/fs'));
+jest.mock('path', () => ({
+ resolve: (a, p) => p,
+}));
+
+const setup = ({ files }) => {
+ // eslint-disable-next-line no-underscore-dangle, global-require
+ require('fs').__setMockFiles(files);
+};
+
+const HEAD_HTML_CONTENTS = 'UNITTEST_HEAD_HTML_CONTENTS';
+
+describe('getPreviewHeadHtml', () => {
+ it('returns an empty string without head.html present', () => {
+ setup({
+ files: {},
+ });
+
+ const result = getPreviewHeadHtml('first');
+ expect(result).toEqual('');
+ });
+
+ it('return contents of head.html when present', () => {
+ setup({
+ files: {
+ 'head.html': HEAD_HTML_CONTENTS,
+ },
+ });
+
+ const result = getPreviewHeadHtml('second');
+ expect(result).toEqual(HEAD_HTML_CONTENTS);
+ });
+
+ it('returns contents of preview-head.html when present', () => {
+ setup({
+ files: {
+ 'preview-head.html': HEAD_HTML_CONTENTS,
+ },
+ });
+
+ const result = getPreviewHeadHtml('second');
+ expect(result).toEqual(HEAD_HTML_CONTENTS);
+ });
+});
+
+describe('getManagerHeadHtml', () => {
+ it('returns an empty string without manager-head.html present', () => {
+ setup({
+ files: {},
+ });
+
+ const result = getManagerHeadHtml('first');
+ expect(result).toEqual('');
+ });
+
+ it('returns contents of manager-head.html when present', () => {
+ setup({
+ files: {
+ 'manager-head.html': HEAD_HTML_CONTENTS,
+ },
+ });
+
+ const result = getManagerHeadHtml('second');
+ expect(result).toEqual(HEAD_HTML_CONTENTS);
+ });
+});
diff --git a/docs/gatsby-config.js b/docs/gatsby-config.js
index a4eaeba5257f..ba05f36ff20f 100644
--- a/docs/gatsby-config.js
+++ b/docs/gatsby-config.js
@@ -12,6 +12,7 @@ module.exports = {
'/basics/guide-react/',
'/basics/guide-vue/',
'/basics/guide-angular/',
+ '/basics/guide-mithril/',
'/basics/writing-stories/',
'/basics/exporting-storybook/',
'/basics/faq/',
diff --git a/docs/src/pages/basics/guide-mithril/index.md b/docs/src/pages/basics/guide-mithril/index.md
new file mode 100644
index 000000000000..9f75c147564b
--- /dev/null
+++ b/docs/src/pages/basics/guide-mithril/index.md
@@ -0,0 +1,108 @@
+---
+id: 'guide-mithril'
+title: 'Storybook for Mithril'
+---
+
+You may have tried to use our quick start guide to setup your project for Storybook. If you want to set up Storybook manually, this is the guide for you.
+
+> This will also help you to understand how Storybook works.
+
+## Starter Guide Mithril
+
+Storybook has its own Webpack setup and a dev server.
+
+In this guide, we will set up Storybook for your Mithril project.
+
+## Table of contents
+
+- [Add @storybook/mithril](#add-storybookmithril)
+- [Add mithril and babel-core](#add-mithril-and-babel-core)
+- [Create the config file](#create-the-config-file)
+- [Write your stories](#write-your-stories)
+- [Run your Storybook](#run-your-storybook)
+
+## Add @storybook/mithril
+
+First of all, you need to add `@storybook/mithril` to your project. To do that, simply run:
+
+```sh
+npm i --save-dev @storybook/mithril
+```
+
+## Add mithril and babel-core
+
+Make sure that you have `mithril` and `babel-core` in your dependencies as well because we list these as a peerDependency:
+
+```sh
+npm i --save mithril
+npm i --save-dev babel-core
+```
+
+Then add the following NPM script to your package json in order to start the storybook later in this guide:
+
+```json
+{
+ "scripts": {
+ "storybook": "start-storybook -p 9001 -c .storybook"
+ }
+}
+```
+
+## Create the config file
+
+Storybook can be configured in several different ways.
+Thatโs why we need a config directory. We've added a `-c` option to the above NPM script mentioning `.storybook` as the config directory.
+
+For the basic Storybook configuration file, you don't need to do much, but simply tell Storybook where to find stories.
+
+To do that, simply create a file at `.storybook/config.js` with the following content:
+
+```js
+import { configure } from '@storybook/mithril';
+
+function loadStories() {
+ require('../stories/index.js');
+ // You can require as many stories as you need.
+}
+
+configure(loadStories, module);
+```
+
+That'll load stories in `../stories/index.js`.
+
+## Write your stories
+
+Now you can write some stories inside the `../stories/index.js` file, like this:
+
+```js
+/** @jsx m */
+
+import m from 'mithril';
+
+import { storiesOf } from '@storybook/mithril';
+import { action } from '@storybook/addon-actions';
+import Button from '../components/Button';
+
+storiesOf('Button', module)
+ .add('with text', () => ({
+ view: () => Hello Button
+ }))
+ .add('with some emoji', () => ({
+ view: () => ๐ ๐ ๐ ๐ฏ
+ }));
+```
+
+Story is a single state of your component. In the above case, there are two stories for the native button component:
+
+1. with text
+2. with some emoji
+
+## Run your Storybook
+
+Now everything is ready. Simply run your storybook with:
+
+```sh
+npm run storybook
+```
+
+Now you can change components and write stories whenever you need to.
diff --git a/docs/src/pages/basics/live-examples/index.md b/docs/src/pages/basics/live-examples/index.md
index 18ec48ece6a5..425630295b10 100644
--- a/docs/src/pages/basics/live-examples/index.md
+++ b/docs/src/pages/basics/live-examples/index.md
@@ -10,6 +10,7 @@ title: 'Live Examples'
- [Vue](https://storybooks-vue.netlify.com/)
- [Angular](https://storybooks-angular.netlify.com/)
- [Polymer](https://storybooks-polymer.netlify.com/)
+- [Mithril](https://storybooks-mithril.netlify.com/)
### 3.4
- [React Official](https://release-3-4--storybooks-official.netlify.com)
diff --git a/docs/src/pages/basics/quick-start-guide/index.md b/docs/src/pages/basics/quick-start-guide/index.md
index 71ee3d4157ad..c46b998e5220 100644
--- a/docs/src/pages/basics/quick-start-guide/index.md
+++ b/docs/src/pages/basics/quick-start-guide/index.md
@@ -3,7 +3,7 @@ id: 'quick-start-guide'
title: 'Quick Start Guide'
---
-Storybook is very easy to use. You can use it with any kind of React or Vue or Angular project.
+Storybook is very easy to use. You can use it with any kind of React or Vue or Angular or Mithril project.
Follow these steps to get started with Storybook.
```sh
@@ -23,4 +23,4 @@ Then you can access your storybook from the browser.
* * *
-To learn more about what `getstorybook` command does, have a look at our [Start Guide for React](/basics/guide-react/) or [Start Guide for Vue](/basics/guide-vue/) or [Start Guide for Angular](/basics/guide-angular/).
+To learn more about what `getstorybook` command does, have a look at our [Start Guide for React](/basics/guide-react/) or [Start Guide for Vue](/basics/guide-vue/) or [Start Guide for Angular](/basics/guide-angular/) or [Start Guide for Mithril](/basics/guide-mithril/).
diff --git a/docs/src/pages/basics/slow-start-guide/index.md b/docs/src/pages/basics/slow-start-guide/index.md
index a49d890f60c0..ce7882948d81 100644
--- a/docs/src/pages/basics/slow-start-guide/index.md
+++ b/docs/src/pages/basics/slow-start-guide/index.md
@@ -8,3 +8,4 @@ Storybook supports multiple UI libraries, the manual setup for each is different
- [Storybook for React](/basics/guide-react/)
- [Storybook for Vue](/basics/guide-vue/)
- [Storybook for Angular](/basics/guide-angular/)
+- [Storybook for Mithril](/basics/guide-mithril/)
diff --git a/examples/mithril-kitchen-sink/.babelrc b/examples/mithril-kitchen-sink/.babelrc
new file mode 100644
index 000000000000..5e1d82404dd3
--- /dev/null
+++ b/examples/mithril-kitchen-sink/.babelrc
@@ -0,0 +1,5 @@
+{
+ "plugins": [
+ "transform-react-jsx"
+ ]
+}
diff --git a/examples/mithril-kitchen-sink/.gitignore b/examples/mithril-kitchen-sink/.gitignore
new file mode 100644
index 000000000000..6c96c5cff124
--- /dev/null
+++ b/examples/mithril-kitchen-sink/.gitignore
@@ -0,0 +1,15 @@
+# See http://help.github.com/ignore-files/ for more about ignoring files.
+
+# dependencies
+node_modules
+
+# testing
+coverage
+
+# production
+build
+
+# misc
+.DS_Store
+.env
+npm-debug.log
diff --git a/examples/mithril-kitchen-sink/.storybook/addons.js b/examples/mithril-kitchen-sink/.storybook/addons.js
new file mode 100644
index 000000000000..ff96ea46a3bd
--- /dev/null
+++ b/examples/mithril-kitchen-sink/.storybook/addons.js
@@ -0,0 +1,8 @@
+import '@storybook/addon-storysource/register';
+import '@storybook/addon-actions/register';
+import '@storybook/addon-links/register';
+import '@storybook/addon-notes/register';
+import '@storybook/addon-knobs/register';
+import '@storybook/addon-viewport/register';
+import '@storybook/addon-options/register';
+import '@storybook/addon-backgrounds/register';
diff --git a/examples/mithril-kitchen-sink/.storybook/config.js b/examples/mithril-kitchen-sink/.storybook/config.js
new file mode 100644
index 000000000000..aff56383c053
--- /dev/null
+++ b/examples/mithril-kitchen-sink/.storybook/config.js
@@ -0,0 +1,13 @@
+import { configure } from '@storybook/mithril';
+import { setOptions } from '@storybook/addon-options';
+
+setOptions({
+ hierarchyRootSeparator: /\|/,
+});
+
+function loadStories() {
+ const req = require.context('../src/stories', true, /\.stories\.js$/);
+ req.keys().forEach(filename => req(filename));
+}
+
+configure(loadStories, module);
diff --git a/examples/mithril-kitchen-sink/.storybook/webpack.config.js b/examples/mithril-kitchen-sink/.storybook/webpack.config.js
new file mode 100644
index 000000000000..692ad69caccc
--- /dev/null
+++ b/examples/mithril-kitchen-sink/.storybook/webpack.config.js
@@ -0,0 +1,12 @@
+const path = require('path');
+
+module.exports = (storybookBaseConfig, configType, defaultConfig) => {
+ defaultConfig.module.rules.push({
+ test: [/\.stories\.js$/],
+ loaders: [require.resolve('@storybook/addon-storysource/loader')],
+ include: [path.resolve(__dirname, '../src')],
+ enforce: 'pre',
+ });
+
+ return defaultConfig;
+};
diff --git a/examples/mithril-kitchen-sink/README.md b/examples/mithril-kitchen-sink/README.md
new file mode 100644
index 000000000000..765012ecfa46
--- /dev/null
+++ b/examples/mithril-kitchen-sink/README.md
@@ -0,0 +1,3 @@
+# Storybook Demo
+
+This is a demo app to test Mithril integration with Storybook. Run `npm install` or `yarn install` to sync Storybook module with the source code and run `npm run storybook` or `yarn storybook` to start the Storybook.
diff --git a/examples/mithril-kitchen-sink/package.json b/examples/mithril-kitchen-sink/package.json
new file mode 100644
index 000000000000..21ff6e0650be
--- /dev/null
+++ b/examples/mithril-kitchen-sink/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "mithril-example",
+ "version": "3.4.0-rc.3",
+ "private": true,
+ "scripts": {
+ "build-storybook": "build-storybook",
+ "storybook": "start-storybook -p 9007"
+ },
+ "dependencies": {
+ "mithril": "^1.1.6"
+ },
+ "devDependencies": {
+ "@storybook/addon-actions": "3.4.0-rc.3",
+ "@storybook/addon-backgrounds": "3.4.0-rc.3",
+ "@storybook/addon-centered": "3.4.0-rc.3",
+ "@storybook/addon-knobs": "3.4.0-rc.3",
+ "@storybook/addon-links": "3.4.0-rc.3",
+ "@storybook/addon-notes": "3.4.0-rc.3",
+ "@storybook/addon-options": "3.4.0-rc.3",
+ "@storybook/addon-storyshots": "3.4.0-rc.3",
+ "@storybook/addon-storysource": "3.4.0-rc.3",
+ "@storybook/addon-viewport": "3.4.0-rc.3",
+ "@storybook/addons": "3.4.0-rc.3",
+ "@storybook/mithril": "3.4.0-rc.3",
+ "babel-core": "^6.26.0",
+ "babel-plugin-transform-react-jsx": "^6.24.1",
+ "webpack": "^4.3.0"
+ }
+}
diff --git a/examples/mithril-kitchen-sink/src/BaseButton.js b/examples/mithril-kitchen-sink/src/BaseButton.js
new file mode 100644
index 000000000000..ea451b9569d0
--- /dev/null
+++ b/examples/mithril-kitchen-sink/src/BaseButton.js
@@ -0,0 +1,12 @@
+import m from 'mithril';
+
+const BaseButton = {
+ view: ({ attrs }) =>
+ m(
+ 'button',
+ { disabled: attrs.disabled, onclick: attrs.onclick, style: attrs.style },
+ attrs.label
+ ),
+};
+
+export default BaseButton;
diff --git a/examples/mithril-kitchen-sink/src/Button.js b/examples/mithril-kitchen-sink/src/Button.js
new file mode 100644
index 000000000000..8fcb462dd537
--- /dev/null
+++ b/examples/mithril-kitchen-sink/src/Button.js
@@ -0,0 +1,23 @@
+/** @jsx m */
+
+import m from 'mithril';
+
+const style = {
+ border: '1px solid #eee',
+ borderRadius: '3px',
+ backgroundColor: '#FFFFFF',
+ cursor: 'pointer',
+ fontSize: '15px',
+ padding: '3px 10px',
+ margin: '10px',
+};
+
+const Button = {
+ view: vnode => (
+
+ {vnode.children}
+
+ ),
+};
+
+export default Button;
diff --git a/examples/mithril-kitchen-sink/src/Welcome.js b/examples/mithril-kitchen-sink/src/Welcome.js
new file mode 100644
index 000000000000..c24a34d9c152
--- /dev/null
+++ b/examples/mithril-kitchen-sink/src/Welcome.js
@@ -0,0 +1,137 @@
+/** @jsx m */
+
+import m from 'mithril';
+
+const Main = {
+ view: vnode => (
+
+ {vnode.children}
+
+ ),
+};
+
+const Title = {
+ view: vnode => {vnode.children} ,
+};
+
+const Note = {
+ view: vnode => (
+
+ {vnode.children}
+
+ ),
+};
+
+const InlineCode = {
+ view: vnode => (
+
+ {vnode.children}
+
+ ),
+};
+
+const Link = {
+ view: vnode => (
+
+ {vnode.children}
+
+ ),
+};
+
+const NavButton = {
+ view: vnode => (
+
+ {vnode.children}
+
+ ),
+};
+
+const Welcome = {
+ view: vnode => (
+
+ Welcome to storybook
+ This is a UI component dev environment for your app.
+
+ We've added some basic stories inside the src/stories directory.
+
+ A story is a single state of one or more UI components. You can have as many stories as you
+ want.
+
+ (Basically a story is like a visual test case.)
+
+
+ See these sample stories for a
+ component called Button
+ .
+
+
+ Just like that, you can add your own components as stories.
+
+ You can also edit those components and see changes right away.
+
+ (Try editing the Button stories located at{' '}
+ src/stories/index.js .)
+
+
+ Usually we create stories with smaller UI components in the app.
+ Have a look at the{' '}
+
+ Writing Stories
+ {' '}
+ section in our documentation.
+
+
+ NOTE:
+
+ Have a look at the .storybook/webpack.config.js to add webpack
+ loaders and plugins you are using in this project.
+
+
+ ),
+};
+
+export default Welcome;
diff --git a/examples/mithril-kitchen-sink/src/stories/addon-actions.stories.js b/examples/mithril-kitchen-sink/src/stories/addon-actions.stories.js
new file mode 100644
index 000000000000..6ec373132e4d
--- /dev/null
+++ b/examples/mithril-kitchen-sink/src/stories/addon-actions.stories.js
@@ -0,0 +1,24 @@
+/** @jsx m */
+
+import m from 'mithril';
+
+import { storiesOf } from '@storybook/mithril';
+import { action } from '@storybook/addon-actions';
+import Button from '../Button';
+
+storiesOf('Addons|Actions', module)
+ .add('Action only', () => ({
+ view: () => Click me to log the action ,
+ }))
+ .add('Action and method', () => ({
+ view: () => (
+ {
+ e.preventDefault();
+ action('log2')(e.target);
+ }}
+ >
+ Click me to log the action
+
+ ),
+ }));
diff --git a/examples/mithril-kitchen-sink/src/stories/addon-backgrounds.stories.js b/examples/mithril-kitchen-sink/src/stories/addon-backgrounds.stories.js
new file mode 100644
index 000000000000..373a25de5bb6
--- /dev/null
+++ b/examples/mithril-kitchen-sink/src/stories/addon-backgrounds.stories.js
@@ -0,0 +1,22 @@
+/** @jsx m */
+
+import m from 'mithril';
+
+import { storiesOf } from '@storybook/mithril';
+
+import backgrounds from '@storybook/addon-backgrounds/mithril';
+import BaseButton from '../BaseButton';
+
+storiesOf('Addons|Backgrounds', module)
+ .addDecorator(
+ backgrounds([
+ { name: 'twitter', value: '#00aced' },
+ { name: 'facebook', value: '#3b5998', default: true },
+ ])
+ )
+ .add('story 1', () => ({
+ view: () => ,
+ }))
+ .add('story 2', () => ({
+ view: () => ,
+ }));
diff --git a/examples/mithril-kitchen-sink/src/stories/addon-centered.stories.js b/examples/mithril-kitchen-sink/src/stories/addon-centered.stories.js
new file mode 100644
index 000000000000..e97deabdef51
--- /dev/null
+++ b/examples/mithril-kitchen-sink/src/stories/addon-centered.stories.js
@@ -0,0 +1,13 @@
+/** @jsx m */
+
+import m from 'mithril';
+
+import { storiesOf } from '@storybook/mithril';
+import Centered from '@storybook/addon-centered/mithril';
+import Button from '../Button';
+
+storiesOf('Addons|Centered', module)
+ .addDecorator(Centered)
+ .add('button', () => ({
+ view: () => A button ,
+ }));
diff --git a/examples/mithril-kitchen-sink/src/stories/addon-knobs.stories.js b/examples/mithril-kitchen-sink/src/stories/addon-knobs.stories.js
new file mode 100644
index 000000000000..c4246eee5005
--- /dev/null
+++ b/examples/mithril-kitchen-sink/src/stories/addon-knobs.stories.js
@@ -0,0 +1,71 @@
+/** @jsx m */
+
+import m from 'mithril';
+
+import { storiesOf } from '@storybook/mithril';
+import { action } from '@storybook/addon-actions';
+import {
+ withKnobs,
+ text,
+ number,
+ boolean,
+ array,
+ select,
+ color,
+ date,
+ button,
+} from '@storybook/addon-knobs/mithril';
+
+storiesOf('Addons|Knobs', module)
+ .addDecorator(withKnobs)
+ .add('Simple', () => {
+ const name = text('Name', 'John Doe');
+ const age = number('Age', 44);
+ const content = `I am ${name} and I'm ${age} years old.`;
+
+ return {
+ view: () => {content}
,
+ };
+ })
+ .add('All knobs', () => {
+ const name = text('Name', 'Jane');
+ const stock = number('Stock', 20, {
+ range: true,
+ min: 0,
+ max: 30,
+ step: 5,
+ });
+ const fruits = {
+ apples: 'Apple',
+ bananas: 'Banana',
+ cherries: 'Cherry',
+ };
+ const fruit = select('Fruit', fruits, 'apple');
+ const price = number('Price', 2.25);
+
+ const colour = color('Border', 'deeppink');
+ const today = date('Today', new Date('Jan 20 2017 GMT+0'));
+ const items = array('Items', ['Laptop', 'Book', 'Whiskey']);
+ const nice = boolean('Nice', true);
+
+ const stockMessage = stock
+ ? `I have a stock of ${stock} ${fruit}, costing $${price} each.`
+ : `I'm out of ${fruit}${nice ? ', Sorry!' : '.'}`;
+ const salutation = nice ? 'Nice to meet you!' : 'Leave me alone!';
+ const dateOptions = { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' };
+
+ button('Arbitrary action', action('You clicked it!'));
+
+ return {
+ view: () => (
+
+
My name is {name},
+
today is {new Date(today).toLocaleDateString('en-US', dateOptions)}
+
{stockMessage}
+
Also, I have:
+
{items.map(item => `${item} `).join('')}
+
{salutation}
+
+ ),
+ };
+ });
diff --git a/examples/mithril-kitchen-sink/src/stories/addon-links.stories.js b/examples/mithril-kitchen-sink/src/stories/addon-links.stories.js
new file mode 100644
index 000000000000..51e786293367
--- /dev/null
+++ b/examples/mithril-kitchen-sink/src/stories/addon-links.stories.js
@@ -0,0 +1,11 @@
+/** @jsx m */
+
+import m from 'mithril';
+
+import { storiesOf } from '@storybook/mithril';
+import { linkTo } from '@storybook/addon-links';
+import Button from '../Button';
+
+storiesOf('Addons|Links', module).add('Go to welcome', () => ({
+ view: () => This buttons links to Welcome ,
+}));
diff --git a/examples/mithril-kitchen-sink/src/stories/addon-notes.stories.js b/examples/mithril-kitchen-sink/src/stories/addon-notes.stories.js
new file mode 100644
index 000000000000..ba1edfa822f1
--- /dev/null
+++ b/examples/mithril-kitchen-sink/src/stories/addon-notes.stories.js
@@ -0,0 +1,48 @@
+/** @jsx m */
+/* eslint-disable jsx-a11y/accessible-emoji */
+
+import m from 'mithril';
+
+import { storiesOf } from '@storybook/mithril';
+import { withNotes } from '@storybook/addon-notes';
+
+storiesOf('Addons|Notes', module)
+ .add(
+ 'Simple note',
+ withNotes({ text: 'My notes on some bold text' })(() => ({
+ view: () => (
+
+
+ Etiam vulputate elit eu venenatis eleifend. Duis nec lectus augue. Morbi egestas diam
+ sed vulputate mollis. Fusce egestas pretium vehicula. Integer sed neque diam. Donec
+ consectetur velit vitae enim varius, ut placerat arcu imperdiet. Praesent sed faucibus
+ arcu. Nullam sit amet nibh a enim eleifend rhoncus. Donec pretium elementum leo at
+ fermentum. Nulla sollicitudin, mauris quis semper tempus, sem metus tristique diam,
+ efficitur pulvinar mi urna id urna.
+
+
+ ),
+ }))
+ )
+ .add(
+ 'Note with HTML',
+ withNotes({
+ text: `
+ My notes on emojies
+
+ It's not all that important to be honest, but..
+
+ Emojis are great, I love emojis, in fact I like using them in my Component notes too! ๐
+ `,
+ })(() => ({
+ view: () => (
+
+ ๐ค๐ณ๐ฏ๐ฎ
+
+ ๐๐ฉ๐๐ฑ
+
+ ๐ค๐๐ถ๐
+
+ ),
+ }))
+ );
diff --git a/examples/mithril-kitchen-sink/src/stories/index.stories.js b/examples/mithril-kitchen-sink/src/stories/index.stories.js
new file mode 100644
index 000000000000..a0952e562bfd
--- /dev/null
+++ b/examples/mithril-kitchen-sink/src/stories/index.stories.js
@@ -0,0 +1,25 @@
+import m from 'mithril';
+
+import { storiesOf } from '@storybook/mithril';
+import { action } from '@storybook/addon-actions';
+import { linkTo } from '@storybook/addon-links';
+
+import Button from '../Button';
+import Welcome from '../Welcome';
+
+storiesOf('Welcome', module).add('to Storybook', () => ({
+ view: () => m(Welcome, { showApp: linkTo('Button') }),
+}));
+
+storiesOf('Button', module)
+ .add('with text', () => ({
+ view: () => m(Button, { onclick: action('clicked') }, 'Hello Button'),
+ }))
+ .add('with some emoji', () => ({
+ view: () =>
+ m(
+ Button,
+ { onclick: action('clicked') },
+ m('span', { role: 'img', ariaLabel: 'so cool' }, '๐ ๐ ๐ ๐ฏ')
+ ),
+ }));
diff --git a/examples/official-storybook/built-storybooks/mithril-kitchen-sink b/examples/official-storybook/built-storybooks/mithril-kitchen-sink
new file mode 120000
index 000000000000..c027b899fc0d
--- /dev/null
+++ b/examples/official-storybook/built-storybooks/mithril-kitchen-sink
@@ -0,0 +1 @@
+../../mithril-kitchen-sink/storybook-static
\ No newline at end of file
diff --git a/examples/official-storybook/stories/__snapshots__/app-acceptance.stories.storyshot b/examples/official-storybook/stories/__snapshots__/app-acceptance.stories.storyshot
index 6ebeacf6f19b..c3321d85cc11 100644
--- a/examples/official-storybook/stories/__snapshots__/app-acceptance.stories.storyshot
+++ b/examples/official-storybook/stories/__snapshots__/app-acceptance.stories.storyshot
@@ -16,6 +16,14 @@ exports[`Storyshots App|acceptance cra-kitchen-sink 1`] = `
/>
`;
+exports[`Storyshots App|acceptance mithril-kitchen-sink 1`] = `
+
+`;
+
exports[`Storyshots App|acceptance polymer-cli 1`] = `