diff --git a/ADDONS_SUPPORT.md b/ADDONS_SUPPORT.md index a9de7742d5ec..e367da5b116c 100644 --- a/ADDONS_SUPPORT.md +++ b/ADDONS_SUPPORT.md @@ -1,19 +1,21 @@ -## Addon / Framework Support Table - -| | [React](app/react)|[React Native](app/react-native)|[Vue](app/vue)|[Angular](app/angular)| [Polymer](app/polymer)| [Mithril](app/mithril)| [HTML](app/html)| [Marko](app/marko)| [Svelte](app/svelte)| [Riot](app/riot)| [Ember](app/ember)| -| ----------- |:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:| -|[a11y](addons/a11y) |+| |+|+|+|+|+|+| | |+| -|[actions](addons/actions) |+|+|+|+|+|+|+|+|+|+|+| -|[backgrounds](addons/backgrounds)|+| |+|+|+|+|+|+|+|+|+| -|[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) |+| |+|+|+|+|+|+|+|+|+| +## Addon / Framework Support Table + +| | [React](app/react)|[React Native](app/react-native)|[Vue](app/vue)|[Angular](app/angular)| [Polymer](app/polymer)| [Mithril](app/mithril)| [HTML](app/html)| [Marko](app/marko)| [Svelte](app/svelte)| [Riot](app/riot)| [Ember](app/ember)| +| ----------- |:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:| +|[a11y](addons/a11y) |+| |+|+|+|+|+|+| | |+| +|[actions](addons/actions) |+|+|+|+|+|+|+|+|+|+|+| +|[backgrounds](addons/backgrounds)|+|*|+|+|+|+|+|+|+|+|+| +|[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 Native on device addon (addons/onDevice-\) diff --git a/app/react-native/docs/addons.md b/app/react-native/docs/addons.md new file mode 100644 index 000000000000..1ede5633ad73 --- /dev/null +++ b/app/react-native/docs/addons.md @@ -0,0 +1,50 @@ +# Addons + +Storybook supports addons. You can read more about them [here](https://storybook.js.org/addons/introduction/) + +There is one big difference in React Native is that it has two types of addons: Addons that work in the browser +and addons that work on the app itself (on device addons). + +## Browser addons +Browser addons are default addons to storybook. You create a file called addons.js inside storybook and it is +automatically added inside your browser. + +## On device addons +On device addons are addons that are displayed in your app in addons panel. +To use them you have to create a file called `rn-addons.js` next to your storybook entry. +Because React Native does not dynamically resolve imports, you also have to manually import them. +Example: +**storybook/index.js** +``` +import { getStorybookUI, configure } from '@storybook/react-native'; +import './rn-addons'; +// import stories +configure(() => { + require($PATH_TO_STORIES); +}, module); + +const StorybookUI = getStorybookUI(); +export default StorybookUI; + +**storybook/rn-addons.js** +``` +import '@storybook/addon-ondevice-knobs/register'; +import '@storybook/addon-ondevice-notes/register'; +... +``` + +This step is done automatically when you install Storybook for the first time and also described in [Manual Setup](https://github.com/storybooks/storybook/blob/master/app/react-native/docs/manual-setup.md) + +## Compatibility +Addon compatibilty can be found [here](https://github.com/storybooks/storybook/blob/master/ADDONS_SUPPORT.md) + +## Performance of on device addons +Because on device addons are inside the app, they are also rerendered on every change. This can reduce performance a lot. + +## Writing the on device addons +On device addons use same addon store and api as web addons. The only difference in api is that you don't have `api` prop +and have to rely on channel for everything. + +The main difference between browser and app addons is that the render has to be supported by React Native (View, Text). +For more info about writing addons read [writing addons](https://storybook.js.org/addons/writing-addons/) section in +storybook documentation. diff --git a/app/react-native/docs/manual-setup.md b/app/react-native/docs/manual-setup.md index 4ad7912a929b..321f23248c4d 100644 --- a/app/react-native/docs/manual-setup.md +++ b/app/react-native/docs/manual-setup.md @@ -6,32 +6,38 @@ First, install the `@storybook/react-native` module npm install @storybook/react-native ``` -Create a new directory called `storybook` in your project root and create an entry file (index.ios.js or index.android.js) as given below. (Don't forget to replace "MyApplicationName" with your app name). +Create a new directory called `storybook` in your project root and create an entry file (index.js) as given below. +(Don't forget to replace "MyApplicationName" with your app name). +**storybook/index.js** ```js import { AppRegistry } from 'react-native'; import { getStorybookUI, configure } from '@storybook/react-native'; -import './addons'; +import './rn-addons'; -// import your stories -configure(function() { +// import stories +configure(() => { + // eslint-disable-next-line global-require require('./stories'); }, module); -const StorybookUI = getStorybookUI({ - port: 7007, - host: 'localhost', -}); -AppRegistry.registerComponent('MyApplicationName', () => StorybookUI); +const StorybookUIRoot = getStorybookUI(); + +AppRegistry.registerComponent('MyApplicationName', () => StorybookUIRoot); +export default StorybookUIRoot; ``` -Create a file named `addons.js` file in `storybook` directory to use addons. Here is a list of default addons: +Create a file called `rn-addons.js` +In this file you can import on device addons. -```js -import '@storybook/addon-actions'; -import '@storybook/addon-links'; +**storybook/rn-addons.js** +``` +import '@storybook/addon-ondevice-knobs/register'; +import '@storybook/addon-ondevice-notes/register'; +... ``` + Then write your first story in the `stories` directory like this: ```js @@ -58,12 +64,31 @@ storiesOf('CenteredView') )); ``` -Then add following NPM script into your `package.json` file: +Finally replace your app entry with +```js +import './storybook'; +``` +If you cannot replace your entry point just make sure that the component exported from `./storybook` is displayed +somewhere in your app. `StorybookUI` is simply a RN `View` component that can be embedded anywhere in your +RN application, e.g. on a tab or within an admin screen. + +## Server support + +If you want to support having a storybook server running add following NPM script into your `package.json` file: ```json { "scripts": { - "storybook": "storybook start -p 7007" + "storybook": "storybook start" } } ``` + +If you want to have addons inside browser, create a file named `addons.js` file in `storybook`. Here is a list of default addons: + +**storybook/addons.js** +```js +import '@storybook/addon-actions'; +import '@storybook/addon-links'; +``` + diff --git a/app/react-native/docs/server.md b/app/react-native/docs/server.md new file mode 100644 index 000000000000..fcfe2abe7630 --- /dev/null +++ b/app/react-native/docs/server.md @@ -0,0 +1,24 @@ +# Storybook server +The default usage of React Native Storybook till version 4 involved starting Storybook server. +Starting from v4 we do not expect user to start the server since in most cases it is not really necessary. + +In case you still want to run Storybook server simply call `npm run storybook` or `npx storybook start`. + +## Benefits of storybook server + +* ### Websockets connection +The main benefit you get from running storybook server is that your app will be listening for websockets connection. +That means that you can create your own tools that integrate with your storybook app. + +* ### IDE Plugins +Having server running allows you to control your storybook view from inside web page or your ide. + +There is a plugin for [JetBrains IDEs](https://plugins.jetbrains.com/plugin/9910-storybook) and there is one +for [VS Code](https://github.com/orta/vscode-react-native-storybooks). + + +* ### Web addons +There are Storybook addons that work with React Native but do not have on device implementations. + + + diff --git a/app/react-native/package.json b/app/react-native/package.json index c281601c4462..d3326ff164c4 100644 --- a/app/react-native/package.json +++ b/app/react-native/package.json @@ -29,6 +29,7 @@ }, "dependencies": { "@storybook/addons": "4.0.0-alpha.24", + "@storybook/channels": "4.0.0-alpha.24", "@storybook/channel-websocket": "4.0.0-alpha.24", "@storybook/core": "4.0.0-alpha.24", "@storybook/core-events": "4.0.0-alpha.24", @@ -57,8 +58,7 @@ "prop-types": "^15.6.2", "raw-loader": "^0.5.1", "react-dev-utils": "6.0.4", - "react-native-compat": "^1.0.0", - "react-native-iphone-x-helper": "^1.2.0", + "react-native-swipe-gestures": "^1.0.2", "shelljs": "^0.8.2", "universal-dotenv": "^1.9.1", "url-parse": "^1.4.3", diff --git a/app/react-native/readme.md b/app/react-native/readme.md index 5c329aeb9686..b30bcb7547fb 100644 --- a/app/react-native/readme.md +++ b/app/react-native/readme.md @@ -104,8 +104,8 @@ You can pass these parameters to getStorybookUI call in your storybook entry poi ``` { - onDeviceUI: Boolean (false) - -- display stories list on the device + onDeviceUI: Boolean (true) + -- display navigator and addons on the device disableWebsockets: Boolean (false) -- allows to display stories without running storybook server. Should be used with onDeviceUI secured: Boolean (false) @@ -116,6 +116,10 @@ You can pass these parameters to getStorybookUI call in your storybook entry poi -- port to use query: String ("") -- additional query string to pass to websockets + isUIHidden: Boolean (false) + -- should the ui be closed initialy. + tabOpen: Number (0) + -- which tab should be open. -1 Navigator, 0 Preview, 1 Addons } ``` diff --git a/app/react-native/src/bin/storybook-start.js b/app/react-native/src/bin/storybook-start.js index fb660525e743..ebdae9425db4 100644 --- a/app/react-native/src/bin/storybook-start.js +++ b/app/react-native/src/bin/storybook-start.js @@ -5,8 +5,8 @@ import program from 'commander'; import Server from '../server'; program - .option('-h, --host ', 'host to listen on') - .option('-p, --port ', 'port to listen on') + .option('-h, --host ', 'host to listen on', 'localhost') + .option('-p, --port ', 'port to listen on', 7007) .option('-s, --secured', 'whether server is running on https') .option('-c, --config-dir [dir-name]', 'storybook config directory') .option('-e, --environment [environment]', 'DEVELOPMENT/PRODUCTION environment for webpack') @@ -33,7 +33,7 @@ server.listen(...listenAddr, err => { if (err) { throw err; } - const address = `http://${program.host || 'localhost'}:${program.port}/`; + const address = `http://${program.host}:${program.port}/`; console.info(`\nReact Native Storybook started on => ${address}\n`); if (program.smokeTest) { process.exit(0); diff --git a/app/react-native/src/preview/components/OnDeviceUI/absolute-positioned-keyboard-aware-view.js b/app/react-native/src/preview/components/OnDeviceUI/absolute-positioned-keyboard-aware-view.js new file mode 100644 index 000000000000..454926269351 --- /dev/null +++ b/app/react-native/src/preview/components/OnDeviceUI/absolute-positioned-keyboard-aware-view.js @@ -0,0 +1,89 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { Platform, Keyboard, Dimensions, View } from 'react-native'; + +import style from './style'; + +// Android changes screen size when keyboard opens. +// To avoid issues we use absolute positioned element with predefined screen size +export default class AbsolutePositionedKeyboardAwareView extends PureComponent { + componentWillMount() { + this.keyboardDidShowListener = Keyboard.addListener( + 'keyboardDidShow', + this.keyboardDidShowHandler + ); + this.keyboardDidHideListener = Keyboard.addListener( + 'keyboardDidHide', + this.keyboardDidHideHandler + ); + Dimensions.addEventListener('change', this.removeKeyboardOnOrientationChange); + } + + componentWillUnmount() { + this.keyboardDidShowListener.remove(); + this.keyboardDidHideListener.remove(); + Dimensions.removeEventListener('change', this.removeKeyboardOnOrientationChange); + } + + keyboardDidShowHandler = e => { + if (Platform.OS === 'android') { + const { previewWidth } = this.props; + // There is bug in RN android that keyboardDidShow event is called simply when you go from portrait to landscape. + // To make sure that this is keyboard event we check screen width + if (previewWidth === e.endCoordinates.width) { + this.keyboardOpen = true; + } + } + }; + + // When rotating screen from portrait to landscape with keyboard open on android it calls keyboardDidShow, but doesn't call + // keyboardDidHide. To avoid issues we set keyboardOpen to false immediately on keyboardChange. + removeKeyboardOnOrientationChange = () => { + if (Platform.OS === 'android') { + this.keyboardOpen = false; + } + }; + + keyboardDidHideHandler = () => { + if (this.keyboardOpen) { + this.keyboardOpen = false; + } + }; + + onLayoutHandler = ({ nativeEvent }) => { + if (!this.keyboardOpen) { + const { width, height } = nativeEvent.layout; + const { onLayout } = this.props; + + onLayout({ + previewHeight: height, + previewWidth: width, + }); + } + }; + + render() { + const { children, previewWidth, previewHeight } = this.props; + + return ( + + + {children} + + + ); + } +} + +AbsolutePositionedKeyboardAwareView.propTypes = { + children: PropTypes.node.isRequired, + previewWidth: PropTypes.number.isRequired, + previewHeight: PropTypes.number.isRequired, + onLayout: PropTypes.func.isRequired, +}; diff --git a/app/react-native/src/preview/components/OnDeviceUI/addons/index.js b/app/react-native/src/preview/components/OnDeviceUI/addons/index.js new file mode 100644 index 000000000000..57b46d500a6d --- /dev/null +++ b/app/react-native/src/preview/components/OnDeviceUI/addons/index.js @@ -0,0 +1,47 @@ +import React, { PureComponent } from 'react'; +import { View, Text } from 'react-native'; +import addons from '@storybook/addons'; + +import AddonsList from './list'; +import AddonWrapper from './wrapper'; +import style from '../style'; + +export default class Addons extends PureComponent { + constructor() { + super(); + + addons.loadAddons({}); + this.panels = addons.getPanels(); + + this.state = { + addonSelected: Object.keys(this.panels)[0] || null, + }; + } + + onPressAddon = addonSelected => { + this.setState({ addonSelected }); + }; + + render() { + const { addonSelected } = this.state; + + if (Object.keys(this.panels).length === 0) { + return ( + + No onDevice addons loaded. + + ); + } + + return ( + + + + + ); + } +} diff --git a/app/react-native/src/preview/components/OnDeviceUI/addons/list.js b/app/react-native/src/preview/components/OnDeviceUI/addons/list.js new file mode 100644 index 000000000000..3e55ed82de46 --- /dev/null +++ b/app/react-native/src/preview/components/OnDeviceUI/addons/list.js @@ -0,0 +1,50 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { View, ScrollView, StyleSheet } from 'react-native'; + +import Button from '../navigation/button'; + +const style = StyleSheet.create({ + list: { + flexDirection: 'row', + backgroundColor: 'white', + borderBottomWidth: 1, + borderBottomColor: '#e6e6e6', + }, +}); + +export default class AddonList extends PureComponent { + renderTab = (id, title) => { + const { addonSelected, onPressAddon } = this.props; + + return ( + + ); + }; + + render() { + const { panels } = this.props; + const addonKeys = Object.keys(panels); + + return ( + + + {addonKeys.map(id => this.renderTab(id, panels[id].title))} + + + ); + } +} + +AddonList.propTypes = { + panels: PropTypes.objectOf( + PropTypes.shape({ + title: PropTypes.string.isRequired, + render: PropTypes.func.isRequired, + }).isRequired + ).isRequired, + onPressAddon: PropTypes.func.isRequired, + addonSelected: PropTypes.string.isRequired, +}; diff --git a/app/react-native/src/preview/components/OnDeviceUI/addons/tab.js b/app/react-native/src/preview/components/OnDeviceUI/addons/tab.js new file mode 100644 index 000000000000..3d3a9d94d68e --- /dev/null +++ b/app/react-native/src/preview/components/OnDeviceUI/addons/tab.js @@ -0,0 +1,27 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { TouchableOpacity, Text } from 'react-native'; + +import style from '../style'; + +export default class Tab extends PureComponent { + onPressHandler = () => { + const { onPress, id } = this.props; + onPress(id); + }; + + render() { + const { title } = this.props; + return ( + + {title} + + ); + } +} + +Tab.propTypes = { + onPress: PropTypes.func.isRequired, + id: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, +}; diff --git a/app/react-native/src/preview/components/OnDeviceUI/addons/wrapper.js b/app/react-native/src/preview/components/OnDeviceUI/addons/wrapper.js new file mode 100644 index 000000000000..9874e510bdca --- /dev/null +++ b/app/react-native/src/preview/components/OnDeviceUI/addons/wrapper.js @@ -0,0 +1,45 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { View, ScrollView, KeyboardAvoidingView, Platform } from 'react-native'; + +import style from '../style'; + +export default class Wrapper extends PureComponent { + render() { + const { panels, addonSelected } = this.props; + + const keyboardVerticalOffset = Platform.OS === 'ios' ? 60 : 0; + + const addonKeys = Object.keys(panels); + + return addonKeys.map(id => { + const selected = addonSelected === id; + + return ( + + + {panels[id].render({ active: selected })} + + + ); + }); + } +} + +Wrapper.propTypes = { + panels: PropTypes.objectOf( + PropTypes.shape({ + title: PropTypes.string.isRequired, + render: PropTypes.func.isRequired, + }).isRequired + ).isRequired, + addonSelected: PropTypes.string, +}; + +Wrapper.defaultProps = { + addonSelected: '', +}; diff --git a/app/react-native/src/preview/components/OnDeviceUI/animation.js b/app/react-native/src/preview/components/OnDeviceUI/animation.js new file mode 100644 index 000000000000..625fd78a6674 --- /dev/null +++ b/app/react-native/src/preview/components/OnDeviceUI/animation.js @@ -0,0 +1,77 @@ +import { NAVIGATOR, PREVIEW, ADDONS } from './navigation/consts'; + +const PREVIEW_SCALE = 0.3; + +const panelWidth = width => width * (1 - PREVIEW_SCALE - 0.05); + +export function getNavigatorPanelPosition(animatedValue, previewWidth) { + return [ + { + transform: [ + { + translateX: animatedValue.interpolate({ + inputRange: [NAVIGATOR, PREVIEW], + outputRange: [0, -panelWidth(previewWidth)], + }), + }, + ], + width: panelWidth(previewWidth), + }, + ]; +} + +export function getAddonPanelPosition(animatedValue, previewWidth) { + return [ + { + transform: [ + { + translateX: animatedValue.interpolate({ + inputRange: [PREVIEW, ADDONS], + outputRange: [previewWidth, previewWidth - panelWidth(previewWidth)], + }), + }, + ], + width: panelWidth(previewWidth), + }, + ]; +} + +export function getPreviewPosition( + animatedValue, + previewWidth, + previewHeight, + slideBetweenAnimation +) { + const translateX = previewWidth / 2 - (previewWidth * PREVIEW_SCALE) / 2 - 6; + const translateY = -(previewHeight / 2 - (previewHeight * PREVIEW_SCALE) / 2 - 12); + + return { + transform: [ + { + translateX: animatedValue.interpolate({ + inputRange: [NAVIGATOR, PREVIEW, ADDONS], + outputRange: [translateX, 0, -translateX], + }), + }, + { + translateY: animatedValue.interpolate({ + inputRange: [NAVIGATOR, PREVIEW, ADDONS], + outputRange: [translateY, slideBetweenAnimation ? translateY : 0, translateY], + }), + }, + ], + }; +} + +export function getPreviewScale(animatedValue, slideBetweenAnimation) { + return { + transform: [ + { + scale: animatedValue.interpolate({ + inputRange: [NAVIGATOR, PREVIEW, ADDONS], + outputRange: [PREVIEW_SCALE, slideBetweenAnimation ? PREVIEW_SCALE : 1, PREVIEW_SCALE], + }), + }, + ], + }; +} diff --git a/app/react-native/src/preview/components/OnDeviceUI/index.js b/app/react-native/src/preview/components/OnDeviceUI/index.js index aab08cb88d2c..b249643adfd5 100644 --- a/app/react-native/src/preview/components/OnDeviceUI/index.js +++ b/app/react-native/src/preview/components/OnDeviceUI/index.js @@ -1,167 +1,155 @@ -import React, { Component } from 'react'; +import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; -import { ifIphoneX } from 'react-native-iphone-x-helper'; - -import { Dimensions, View, TouchableWithoutFeedback, Image, Text } from 'react-native'; +import { SafeAreaView, Animated, TouchableOpacity } from 'react-native'; import Events from '@storybook/core-events'; -import style from './style'; + import StoryListView from '../StoryListView'; import StoryView from '../StoryView'; +import Addons from './addons'; +import Panel from './panel'; +import Navigation from './navigation'; +import AbsolutePositionedKeyboardAwareView from './absolute-positioned-keyboard-aware-view'; -/** - * Returns true if the screen is in portrait mode - */ -const isDeviceInPortrait = () => { - const dim = Dimensions.get('screen'); - return dim.height >= dim.width; -}; +import { PREVIEW } from './navigation/consts'; + +import { + getPreviewPosition, + getPreviewScale, + getAddonPanelPosition, + getNavigatorPanelPosition, +} from './animation'; + +import style from './style'; -const openMenuImage = require('./menu_open.png'); -const closeMenuImage = require('./menu_close.png'); +const ANIMATION_DURATION = 300; -const DRAWER_WIDTH = 250; +export default class OnDeviceUI extends PureComponent { + constructor(props) { + super(props); -export default class OnDeviceUI extends Component { - constructor(...args) { - super(...args); + const tabOpen = props.tabOpen || PREVIEW; this.state = { - isMenuOpen: false, - selectedKind: null, - selectedStory: null, - isPortrait: isDeviceInPortrait(), + tabOpen, + slideBetweenAnimation: false, + selection: props.initialStory || {}, + storyFn: props.initialStory ? props.initialStory.storyFn : null, + previewWidth: 0, + previewHeight: 0, }; + + this.animatedValue = new Animated.Value(tabOpen); + this.forceRender = this.forceUpdate.bind(this); } - componentDidMount() { + componentWillMount() { const { events } = this.props; - - Dimensions.addEventListener('change', this.handleDeviceRotation); events.on(Events.SELECT_STORY, this.handleStoryChange); + events.on(Events.FORCE_RE_RENDER, this.forceRender); } componentWillUnmount() { const { events } = this.props; - - Dimensions.removeEventListener('change', this.handleDeviceRotation); events.removeListener(Events.SELECT_STORY, this.handleStoryChange); + events.removeListener(Events.FORCE_RE_RENDER, this.forceRender); } - handleDeviceRotation = () => { - this.setState({ - isPortrait: isDeviceInPortrait(), - }); + onLayout = ({ previewWidth, previewHeight }) => { + this.setState({ previewWidth, previewHeight }); + }; + + handleOpenPreview = () => { + this.handleToggleTab(PREVIEW); }; handleStoryChange = selection => { - const { kind, story } = selection; this.setState({ - selectedKind: kind, - selectedStory: story, + selection: { + kind: selection.kind, + story: selection.story, + }, + storyFn: selection.storyFn, }); }; - handleToggleMenu = () => { - const { isMenuOpen } = this.state; + handleToggleTab = newTabOpen => { + const { tabOpen } = this.state; + + if (newTabOpen === tabOpen) { + return; + } + + Animated.timing(this.animatedValue, { + toValue: newTabOpen, + duration: ANIMATION_DURATION, + useNativeDriver: true, + }).start(); this.setState({ - isMenuOpen: !isMenuOpen, + tabOpen: newTabOpen, + // True if swiping between navigator and addons + slideBetweenAnimation: tabOpen + newTabOpen === PREVIEW, }); }; render() { - const { stories, events, url } = this.props; - const { isPortrait, isMenuOpen, selectedKind, selectedStory } = this.state; - - const iPhoneXStyles = ifIphoneX( - isPortrait - ? { - marginVertical: 30, - } - : { - marginHorizontal: 30, - }, - {} - ); - - const menuStyles = [ - style.menuContainer, - { - transform: [ - { - translateX: isMenuOpen ? 0 : -DRAWER_WIDTH - 30, - }, - ], - }, - iPhoneXStyles, + const { stories, events, url, isUIHidden } = this.props; + const { + tabOpen, + slideBetweenAnimation, + selection, + storyFn, + previewWidth, + previewHeight, + } = this.state; + + const previewWrapperStyles = [ + style.flex, + getPreviewPosition(this.animatedValue, previewWidth, previewHeight, slideBetweenAnimation), ]; - const headerStyles = [ - style.headerContainer, - { - opacity: +!isMenuOpen, - }, + const previewStyles = [ + style.flex, + tabOpen !== 0 && style.previewMinimized, + getPreviewScale(this.animatedValue, slideBetweenAnimation), ]; - const previewContainerStyles = [style.previewContainer, iPhoneXStyles]; - - const previewWrapperStyles = [style.previewWrapper, iPhoneXStyles]; - - /* - Checks if import is a base64 encoded string uri. - If using haul as bundler, some projects are set up to include small files as base64 strings. - */ - let openIcon = openMenuImage; - if (typeof openIcon === 'string') { - openIcon = { uri: openMenuImage }; - } - let closeIcon = closeMenuImage; - if (typeof closeIcon === 'string') { - closeIcon = { uri: closeMenuImage }; - } - return ( - - - - - - - - - - {selectedKind} {selectedStory} - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + ); } } @@ -179,8 +167,18 @@ OnDeviceUI.propTypes = { removeListener: PropTypes.func.isRequired, }).isRequired, url: PropTypes.string, + tabOpen: PropTypes.number, + isUIHidden: PropTypes.bool, + initialStory: PropTypes.shape({ + story: PropTypes.string.isRequired, + kind: PropTypes.string.isRequired, + storyFn: PropTypes.func.isRequired, + }), }; OnDeviceUI.defaultProps = { url: '', + tabOpen: 0, + isUIHidden: false, + initialStory: null, }; diff --git a/app/react-native/src/preview/components/OnDeviceUI/menu_close.png b/app/react-native/src/preview/components/OnDeviceUI/menu_close.png deleted file mode 100644 index 557b3e21aebd..000000000000 Binary files a/app/react-native/src/preview/components/OnDeviceUI/menu_close.png and /dev/null differ diff --git a/app/react-native/src/preview/components/OnDeviceUI/menu_open.png b/app/react-native/src/preview/components/OnDeviceUI/menu_open.png deleted file mode 100644 index 64a204a093d3..000000000000 Binary files a/app/react-native/src/preview/components/OnDeviceUI/menu_open.png and /dev/null differ diff --git a/app/react-native/src/preview/components/OnDeviceUI/navigation/bar.js b/app/react-native/src/preview/components/OnDeviceUI/navigation/bar.js new file mode 100644 index 000000000000..bba508dac85d --- /dev/null +++ b/app/react-native/src/preview/components/OnDeviceUI/navigation/bar.js @@ -0,0 +1,42 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { StyleSheet, View } from 'react-native'; + +import Button from './button'; +import { NAVIGATOR, PREVIEW, ADDONS } from './consts'; + +const style = StyleSheet.create({ + bar: { + flexDirection: 'row', + paddingHorizontal: 8, + backgroundColor: 'white', + borderBottomWidth: 1, + borderTopWidth: 1, + borderBottomColor: '#e6e6e6', + borderTopColor: '#e6e6e6', + }, +}); + +export default class Bar extends PureComponent { + render() { + const { index, onPress } = this.props; + return ( + + + + + + ); + } +} + +Bar.propTypes = { + onPress: PropTypes.func.isRequired, + index: PropTypes.number.isRequired, +}; diff --git a/app/react-native/src/preview/components/OnDeviceUI/navigation/button.js b/app/react-native/src/preview/components/OnDeviceUI/navigation/button.js new file mode 100644 index 000000000000..94e1c343f88c --- /dev/null +++ b/app/react-native/src/preview/components/OnDeviceUI/navigation/button.js @@ -0,0 +1,57 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { View, StyleSheet, Text, TouchableOpacity } from 'react-native'; + +const style = StyleSheet.create({ + text: { + color: '#999999', + paddingHorizontal: 8, + paddingVertical: 10, + fontSize: 11, + }, + underline: { + height: 3, + backgroundColor: 'transparent', + }, +}); + +export default class Button extends PureComponent { + onPress = () => { + const { onPress, id } = this.props; + onPress(id); + }; + + render() { + const { active, children } = this.props; + + return ( + + + {children.toUpperCase()} + + + + ); + } +} + +Button.propTypes = { + onPress: PropTypes.func.isRequired, + active: PropTypes.bool.isRequired, + id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, + children: PropTypes.node.isRequired, +}; diff --git a/app/react-native/src/preview/components/OnDeviceUI/navigation/consts.js b/app/react-native/src/preview/components/OnDeviceUI/navigation/consts.js new file mode 100644 index 000000000000..7c8f4bcbafeb --- /dev/null +++ b/app/react-native/src/preview/components/OnDeviceUI/navigation/consts.js @@ -0,0 +1,3 @@ +export const NAVIGATOR = -1; +export const PREVIEW = 0; +export const ADDONS = 1; diff --git a/app/react-native/src/preview/components/OnDeviceUI/navigation/index.js b/app/react-native/src/preview/components/OnDeviceUI/navigation/index.js new file mode 100644 index 000000000000..a82d4147c6f3 --- /dev/null +++ b/app/react-native/src/preview/components/OnDeviceUI/navigation/index.js @@ -0,0 +1,71 @@ +import React, { PureComponent } from 'react'; +import { View } from 'react-native'; +import GestureRecognizer from 'react-native-swipe-gestures'; +import PropTypes from 'prop-types'; + +import Bar from './bar'; +import VisibilityButton from './visibility-button'; + +const SWIPE_CONFIG = { + velocityThreshold: 0.2, + directionalOffsetThreshold: 80, +}; + +export default class Navigation extends PureComponent { + constructor(props) { + super(props); + this.state = { + isUIVisible: props.initialUiVisible, + }; + } + + handleToggleUI = () => { + const { isUIVisible } = this.state; + + this.setState({ isUIVisible: !isUIVisible }); + }; + + handleSwipeLeft = () => { + const { tabOpen, onChangeTab } = this.props; + if (tabOpen < 1) { + onChangeTab(tabOpen + 1); + } + }; + + handleSwipeRight = () => { + const { tabOpen, onChangeTab } = this.props; + if (tabOpen > -1) { + onChangeTab(tabOpen - 1); + } + }; + + render() { + const { tabOpen, onChangeTab } = this.props; + const { isUIVisible } = this.state; + + return ( + + {isUIVisible && ( + + + + )} + + + ); + } +} + +Navigation.propTypes = { + initialUiVisible: PropTypes.bool, + tabOpen: PropTypes.number.isRequired, + onChangeTab: PropTypes.func.isRequired, +}; + +Navigation.defaultProps = { + initialUiVisible: true, +}; diff --git a/app/react-native/src/preview/components/OnDeviceUI/navigation/visibility-button.js b/app/react-native/src/preview/components/OnDeviceUI/navigation/visibility-button.js new file mode 100644 index 000000000000..1719b18e71df --- /dev/null +++ b/app/react-native/src/preview/components/OnDeviceUI/navigation/visibility-button.js @@ -0,0 +1,25 @@ +import React, { PureComponent } from 'react'; +import { Text, TouchableOpacity } from 'react-native'; +import PropTypes from 'prop-types'; +import style from '../style'; + +export default class VisibilityButton extends PureComponent { + render() { + const { onPress } = this.props; + return ( + + + + ); + } +} + +VisibilityButton.propTypes = { + onPress: PropTypes.func.isRequired, +}; diff --git a/app/react-native/src/preview/components/OnDeviceUI/panel.js b/app/react-native/src/preview/components/OnDeviceUI/panel.js new file mode 100644 index 000000000000..3723076c8ef6 --- /dev/null +++ b/app/react-native/src/preview/components/OnDeviceUI/panel.js @@ -0,0 +1,25 @@ +import React, { PureComponent } from 'react'; +import { StyleSheet, Animated } from 'react-native'; +import PropTypes from 'prop-types'; + +const style = StyleSheet.create({ + panel: { + ...StyleSheet.absoluteFillObject, + borderWidth: 1, + borderTopWidth: 1, + borderBottomWidth: 0, + borderColor: '#e6e6e6', + }, +}); + +export default class Panel extends PureComponent { + render() { + const { children, style: propsStyle } = this.props; + return {children}; + } +} + +Panel.propTypes = { + style: PropTypes.arrayOf(PropTypes.object).isRequired, + children: PropTypes.node.isRequired, +}; diff --git a/app/react-native/src/preview/components/OnDeviceUI/style.js b/app/react-native/src/preview/components/OnDeviceUI/style.js index ad4db0ba484e..f130d7f9a935 100644 --- a/app/react-native/src/preview/components/OnDeviceUI/style.js +++ b/app/react-native/src/preview/components/OnDeviceUI/style.js @@ -1,42 +1,46 @@ -import { StyleSheet } from 'react-native-compat'; - export default { main: { flex: 1, - flexDirection: 'row', - backgroundColor: 'white', - }, - icon: { - width: 30, - height: 30, - }, - headerContainer: { - flexDirection: 'row', - alignItems: 'center', - margin: 10, }, headerText: { marginLeft: 10, fontSize: 20, color: 'black', }, - menuContainer: { - ...StyleSheet.absoluteFillObject, - right: null, - paddingHorizontal: 10, - paddingBottom: 10, - backgroundColor: 'white', + text: { + fontSize: 18, }, - previewContainer: { - flex: 1, + center: { + alignItems: 'center', + justifyContent: 'center', }, - previewWrapper: { - flex: 1, + hideButtonText: { + fontSize: 14, + color: '#999999', + }, + hideButton: { + backgroundColor: 'transparent', + position: 'absolute', + right: 8, + bottom: 12, }, - closeButton: { - marginVertical: 10, + previewMinimized: { + borderWidth: 1, + borderColor: '#b3b3b3', }, - preview: { - ...StyleSheet.absoluteFillObject, + tab: { + marginRight: 15, + }, + addonList: { + flexDirection: 'row', + }, + invisible: { + height: 0, + width: 0, + opacity: 0, + position: 'absolute', + }, + flex: { + flex: 1, }, }; diff --git a/app/react-native/src/preview/components/StoryListView/index.js b/app/react-native/src/preview/components/StoryListView/index.js index bbf4acb1ebe9..a53522b648ec 100644 --- a/app/react-native/src/preview/components/StoryListView/index.js +++ b/app/react-native/src/preview/components/StoryListView/index.js @@ -43,25 +43,16 @@ export default class StoryListView extends Component { }; this.storyAddedHandler = this.handleStoryAdded.bind(this); - this.changeStoryHandler = this.changeStory.bind(this); props.stories.on(Events.STORY_ADDED, this.storyAddedHandler); } componentDidMount() { - const { stories } = this.props; - this.handleStoryAdded(); - const dump = stories.dumpStoryBook(); - const nonEmptyKind = dump.find(kind => kind.stories.length > 0); - if (nonEmptyKind) { - this.changeStory(nonEmptyKind.kind, nonEmptyKind.stories[0]); - } } componentWillUnmount() { const { stories } = this.props; - stories.removeListener(Events.STORY_ADDED, this.storyAddedHandler); } @@ -93,13 +84,13 @@ export default class StoryListView extends Component { } render() { - const { width, selectedKind, selectedStory } = this.props; + const { selectedKind, selectedStory } = this.props; const { data } = this.state; return ( ( { - this.setState({ storyFn, selection }); + selectStory = selection => { + this.setState({ storyFn: selection.storyFn, selection }); }; renderHelp = () => { @@ -44,8 +47,29 @@ export default class StoryView extends Component { ); }; + renderOnDeviceUIHelp = () => ( + + Please open navigator and select a story to preview. + + ); + render() { - const { storyFn, selection } = this.state; + const { listenToEvents } = this.props; + + if (listenToEvents) { + const { storyFn, selection } = this.state; + const { kind, story } = selection; + + return storyFn ? ( + + {storyFn()} + + ) : ( + this.renderHelp() + ); + } + + const { storyFn, selection } = this.props; const { kind, story } = selection; return storyFn ? ( @@ -53,12 +77,18 @@ export default class StoryView extends Component { {storyFn()} ) : ( - this.renderHelp() + this.renderOnDeviceUIHelp() ); } } StoryView.propTypes = { + listenToEvents: PropTypes.bool, + storyFn: PropTypes.func, + selection: PropTypes.shape({ + kind: PropTypes.string, + story: PropTypes.string, + }), events: PropTypes.shape({ on: PropTypes.func.isRequired, removeListener: PropTypes.func.isRequired, @@ -68,4 +98,7 @@ StoryView.propTypes = { StoryView.defaultProps = { url: '', + listenToEvents: false, + selection: {}, + storyFn: null, }; diff --git a/app/react-native/src/preview/index.js b/app/react-native/src/preview/index.js index 5ca36c4e7f01..47d060502a12 100644 --- a/app/react-native/src/preview/index.js +++ b/app/react-native/src/preview/index.js @@ -6,8 +6,8 @@ import parse from 'url-parse'; import addons from '@storybook/addons'; import Events from '@storybook/core-events'; +import Channel from '@storybook/channels'; import createChannel from '@storybook/channel-websocket'; -import { EventEmitter } from 'events'; import { StoryStore, ClientApi } from '@storybook/core/client'; import OnDeviceUI from './components/OnDeviceUI'; import StoryView from './components/StoryView'; @@ -40,50 +40,85 @@ export default class Preview { } getStorybookUI(params = {}) { - return () => { - let webUrl = null; - let channel = null; - - try { - channel = addons.getChannel(); - } catch (e) { - // getChannel throws if the channel is not defined, - // which is fine in this case (we will define it below) - } + let webUrl = null; + let channel = null; - if (!channel || params.resetStorybook) { - if (params.onDeviceUI && params.disableWebsockets) { - channel = new EventEmitter(); - } else { - const host = params.host || parse(NativeModules.SourceCode.scriptURL).hostname; - const port = params.port !== false ? `:${params.port || 7007}` : ''; - - const query = params.query || ''; - const { secured } = params; - const websocketType = secured ? 'wss' : 'ws'; - const httpType = secured ? 'https' : 'http'; - - const url = `${websocketType}://${host}${port}/${query}`; - webUrl = `${httpType}://${host}${port}`; - channel = createChannel({ url }); - } + const onDeviceUI = params.onDeviceUI !== false; + + // should the initial story be sent to storybookUI + // set to true if using disableWebsockets or if connection to WebsocketServer fails. + let setInitialStory = false; - addons.setChannel(channel); + try { + channel = addons.getChannel(); + } catch (e) { + // getChannel throws if the channel is not defined, + // which is fine in this case (we will define it below) + } - channel.emit(Events.CHANNEL_CREATED); + if (!channel || params.resetStorybook) { + if (onDeviceUI && params.disableWebsockets) { + channel = new Channel({ async: true }); + } else { + const host = + params.host || parse(NativeModules.SourceCode.scriptURL).hostname || 'localhost'; + const port = params.port !== false ? `:${params.port || 7007}` : ''; + + const query = params.query || ''; + const { secured } = params; + const websocketType = secured ? 'wss' : 'ws'; + const httpType = secured ? 'https' : 'http'; + + const url = `${websocketType}://${host}${port}/${query}`; + webUrl = `${httpType}://${host}${port}`; + channel = createChannel({ + url, + async: onDeviceUI, + onError: () => { + this._setInitialStory(); + + setInitialStory = true; + }, + }); } - channel.on(Events.GET_STORIES, () => this._sendSetStories()); - channel.on(Events.SET_CURRENT_STORY, d => this._selectStory(d)); - this._sendSetStories(); + addons.setChannel(channel); + + channel.emit(Events.CHANNEL_CREATED); + } + + channel.on(Events.GET_STORIES, () => this._sendSetStories()); + channel.on(Events.SET_CURRENT_STORY, d => this._selectStory(d)); + this._sendSetStories(); + + // If the app is started with server running, set the story as the one selected in the browser + if (webUrl) { this._sendGetCurrentStory(); + } else { + setInitialStory = true; + } + + const preview = this; + + // react-native hot module loader must take in a Class - https://github.com/facebook/react-native/issues/10991 + // eslint-disable-next-line react/prefer-stateless-function + return class StorybookRoot extends React.PureComponent { + render() { + if (onDeviceUI) { + return ( + + ); + } - // finally return the preview component - return params.onDeviceUI ? ( - - ) : ( - - ); + return ; + } }; } @@ -98,10 +133,31 @@ export default class Preview { channel.emit(Events.GET_CURRENT_STORY); } - _selectStory(selection) { + _setInitialStory = () => { + const story = this._getInitialStory(); + if (story) { + this._selectStory(story); + } + }; + + _getInitialStory = () => { + const dump = this._stories.dumpStoryBook(); + const nonEmptyKind = dump.find(kind => kind.stories.length > 0); + if (nonEmptyKind) { + return this._getStory({ kind: nonEmptyKind.kind, story: nonEmptyKind.stories[0] }); + } + + return null; + }; + + _getStory(selection) { const { kind, story } = selection; const storyFn = this._stories.getStoryWithContext(kind, story); + return { ...selection, storyFn }; + } + + _selectStory(selection) { const channel = addons.getChannel(); - channel.emit(Events.SELECT_STORY, selection, storyFn); + channel.emit(Events.SELECT_STORY, this._getStory(selection)); } } diff --git a/examples/crna-kitchen-sink/app.json b/examples/crna-kitchen-sink/app.json index 7acefb4d1c1e..b99f83a818b5 100644 --- a/examples/crna-kitchen-sink/app.json +++ b/examples/crna-kitchen-sink/app.json @@ -1,5 +1,10 @@ { "expo": { - "sdkVersion": "24.0.0" + "sdkVersion": "24.0.0", + "androidStatusBarColor": "#C2185B", + "androidStatusBar": { + "barStyle": "light-content", + "backgroundColor": "#C2185B" + } } } diff --git a/examples/crna-kitchen-sink/package.json b/examples/crna-kitchen-sink/package.json index c527b840b4a1..d125353611d1 100644 --- a/examples/crna-kitchen-sink/package.json +++ b/examples/crna-kitchen-sink/package.json @@ -8,7 +8,7 @@ "eject": "react-native-scripts eject", "ios": "react-native-scripts ios", "start": "react-native-scripts start", - "storybook": "storybook start -p 7007", + "storybook": "storybook start", "test": "node node_modules/jest/bin/jest.js --watch" }, "jest": { @@ -35,6 +35,10 @@ "@storybook/core-events": "file:../../packs/storybook-core-events.tgz", "@storybook/node-logger": "file:../../packs/storybook-node-logger.tgz", "@storybook/react-native": "file:../../packs/storybook-react-native.tgz", + "@storybook/addon-notes": "file:../../packs/storybook-addon-notes.tgz", + "@storybook/addon-ondevice-backgrounds": "file:../../packs/storybook-addon-ondevice-backgrounds.tgz", + "@storybook/addon-ondevice-knobs": "file:../../packs/storybook-addon-ondevice-knobs.tgz", + "@storybook/addon-ondevice-notes": "file:../../packs/storybook-addon-ondevice-notes.tgz", "@storybook/ui": "file:../../packs/storybook-ui.tgz", "jest-expo": "^24.0.0", "react-dom": "^16.2.0", diff --git a/examples/crna-kitchen-sink/storybook/addons.js b/examples/crna-kitchen-sink/storybook/addons.js index 02d363f13be9..8741fd55b89f 100644 --- a/examples/crna-kitchen-sink/storybook/addons.js +++ b/examples/crna-kitchen-sink/storybook/addons.js @@ -2,3 +2,4 @@ import '@storybook/addon-actions/register'; import '@storybook/addon-links/register'; import '@storybook/addon-options/register'; import '@storybook/addon-knobs/register'; +import '@storybook/addon-notes/register'; diff --git a/examples/crna-kitchen-sink/storybook/index.js b/examples/crna-kitchen-sink/storybook/index.js index 812f1aad29c7..4bf516784a56 100644 --- a/examples/crna-kitchen-sink/storybook/index.js +++ b/examples/crna-kitchen-sink/storybook/index.js @@ -1,32 +1,10 @@ -import { AppRegistry } from 'react-native'; -import React, { Component } from 'react'; import { getStorybookUI, configure } from '@storybook/react-native'; -import { setOptions } from '@storybook/addon-options'; +import './rn-addons'; -// import stories configure(() => { // eslint-disable-next-line global-require require('./stories'); }, module); -const StorybookUIRoot = getStorybookUI({ port: 7007, onDeviceUI: true }); - -setTimeout( - () => - setOptions({ - name: 'CRNA React Native App', - onDeviceUI: true, - }), - 100 -); - -// react-native hot module loader must take in a Class - https://github.com/facebook/react-native/issues/10991 -// eslint-disable-next-line react/prefer-stateless-function -class StorybookUIHMRRoot extends Component { - render() { - return ; - } -} - -AppRegistry.registerComponent('crna-kitchen-sink', () => StorybookUIHMRRoot); -export default StorybookUIHMRRoot; +const StorybookUIRoot = getStorybookUI(); +export default StorybookUIRoot; diff --git a/examples/crna-kitchen-sink/storybook/rn-addons.js b/examples/crna-kitchen-sink/storybook/rn-addons.js new file mode 100644 index 000000000000..9617c17a5d89 --- /dev/null +++ b/examples/crna-kitchen-sink/storybook/rn-addons.js @@ -0,0 +1,3 @@ +require('@storybook/addon-ondevice-knobs/register'); +require('@storybook/addon-ondevice-notes/register'); +require('@storybook/addon-ondevice-backgrounds/register'); diff --git a/examples/crna-kitchen-sink/storybook/stories/CenterView/style.js b/examples/crna-kitchen-sink/storybook/stories/CenterView/style.js index ff347fd9841f..bc7b2bbafc86 100644 --- a/examples/crna-kitchen-sink/storybook/stories/CenterView/style.js +++ b/examples/crna-kitchen-sink/storybook/stories/CenterView/style.js @@ -3,6 +3,5 @@ export default { flex: 1, justifyContent: 'center', alignItems: 'center', - backgroundColor: '#F5FCFF', }, }; diff --git a/examples/crna-kitchen-sink/storybook/stories/Knobs/index.js b/examples/crna-kitchen-sink/storybook/stories/Knobs/index.js index 6c65a02cc46d..e3d008f33a20 100644 --- a/examples/crna-kitchen-sink/storybook/stories/Knobs/index.js +++ b/examples/crna-kitchen-sink/storybook/stories/Knobs/index.js @@ -11,7 +11,7 @@ import { array, date, object, -} from '@storybook/addon-knobs/react'; +} from '@storybook/addon-knobs'; export default () => { const name = text('Name', 'Storyteller'); diff --git a/examples/crna-kitchen-sink/storybook/stories/index.js b/examples/crna-kitchen-sink/storybook/stories/index.js index a9720029b43c..f23789318965 100644 --- a/examples/crna-kitchen-sink/storybook/stories/index.js +++ b/examples/crna-kitchen-sink/storybook/stories/index.js @@ -1,19 +1,44 @@ import React from 'react'; import { Text } from 'react-native'; -import { storiesOf, addParameters } from '@storybook/react-native'; +import { storiesOf, addDecorator, addParameters } from '@storybook/react-native'; import { action } from '@storybook/addon-actions'; import { linkTo } from '@storybook/addon-links'; import { withKnobs } from '@storybook/addon-knobs'; - +import { withNotes } from '@storybook/addon-ondevice-notes'; +import { withBackgrounds } from '@storybook/addon-ondevice-backgrounds'; import knobsWrapper from './Knobs'; import Button from './Button'; import CenterView from './CenterView'; import Welcome from './Welcome'; -storiesOf('Welcome', module).add('to Storybook', () => ); +addDecorator(withNotes); +addDecorator( + withBackgrounds([ + { name: 'twitter', value: '#6cff5d', default: true }, + { name: 'facebook', value: '#3b5998' }, + ]) +); + +storiesOf('Welcome', module).add('to Storybook', () => , { + notes: ` +# Markdown!\n +* List Item +* [List Item with Link](https://twitter.com/Charles_Mangwa) +`, +}); storiesOf('Button', module) + .addParameters({ + backgrounds: [ + { name: 'red', value: '#F44336' }, + { name: 'blue', value: '#2196F3', default: true }, + ], + notes: ` +# Custom note\n +_This component doesn't look right_ +`, + }) .addDecorator(getStory => {getStory()}) .add('with text', () => (