├── .gitignore ├── .prettierignore ├── .storybook ├── addons.js ├── config.js └── webpack.config.js ├── LICENSE ├── README.md ├── knob-temp-1.gif ├── knob-temp-2.gif ├── package.json ├── sb-slow-search.gif ├── src ├── Animate.d.ts ├── Animate.tsx ├── AutoClicker.js ├── AutoTyper.js ├── Togglable.js ├── WithObservableKnob.d.ts ├── WithObservableKnob.tsx ├── defer.js ├── helpers.js ├── index.d.ts └── index.tsx ├── stories ├── Form.jsx ├── Form.stories.js ├── Thermometer.jsx └── Thermometer.stories.js ├── test └── Animate.test.tsx ├── therm.gif ├── thermometer.svg ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .cache 5 | .rts2_cache_cjs 6 | .rts2_cache_esm 7 | .rts2_cache_umd 8 | dist 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | README.md -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-knobs/register'; 3 | import '@storybook/addon-links/register'; 4 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react'; 2 | 3 | // automatically import all files ending in *.stories.js 4 | configure(require.context('../stories', true, /\.stories\.js$/), module); 5 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = async ({ config }) => { 2 | config.module.rules.push({ 3 | test: /\.(ts|tsx)$/, 4 | exclude: /(node_modules\/(?!@g2crowd\/.+)|vendor)/, 5 | use: { loader: "ts-loader" } 6 | }) 7 | config.resolve.extensions.push(".ts", ".tsx") 8 | return config 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Dean Radcliffe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![twitter link](https://img.shields.io/badge/twitter-@DeanDevDad-55acee.svg)[![npm version](https://badge.fury.io/js/storybook-animate.svg)](https://badge.fury.io/js/storybook-animate) 2 | 3 | # Storybook-Animate 4 | 5 | Storybook is great for designing indivdual states of your application's components. 6 | But your users don't see your app as individual states, but a series of states that flow together over time. If storybook lets you design the frames, Storybook-Animate lets you create the flipbook! 7 | 8 | ## Simplicity Breeds Creativity 9 | 10 | Where you used to provide a story of a single component with props: 11 | 12 | ```js 13 | storiesOf('Thermometer', module) 14 | .add('Healthy Temperature', () => ( 15 | 16 | )) 17 | ``` 18 | 19 | You can animate the component with a `propStream` 20 | 21 | ```js 22 | storiesOf('Thermometer', module) 23 | .add('Healthy Temperature', () => ( 24 | 28 | )) 29 | ``` 30 | 31 | And here is the beautiful result: 32 | 33 | ![](./therm.gif) 34 | 35 | Try clicking on the animation to restart it, or using `` to just sit back and admire your animation :) 36 | 37 | ## Dive into Streams 38 | A `propStream` is an [RxJS](https://github.com/ReactiveX/rxjs) Observable, created any way you like. But to ease you in, Storybook-Animate includes some helpers to supplement those provided by [Polyrhythm](https://github.com/deanius/polyrhythm): 39 | 40 | ```js 41 | const climbingTemp = sequenceOf( 42 | after(0.0, { temp: 86 }), 43 | after(1000, { temp: 92 }), 44 | after(1000, { temp: 96 }), 45 | after(1000, { temp: 99 }), 46 | after(1000, { temp: 100.5 }), 47 | after(1000, { temp: 102.2 }) 48 | ).combineWith({scale: "F"}) 49 | ``` 50 | 51 | ## Storybook Your API calls, Loading States, API Errors 52 | 53 | Sometimes your users see loading states for a lot longer than you are able to during development—why not have stories for those loading states too? Feel their pain, and design to improve upon it right there in your UX design tool. 54 | 55 | Like this example from a recent Storybook: 56 | 57 | ![](sb-slow-search.gif) 58 | 59 |
60 | 61 | 62 | Here's how: 63 | 64 | 65 | Imagine an oversimplified auto-complete component such as this one. 66 | 67 | ```js 68 | import { searchApi } from './anotherFile' 69 | 70 | function AutoComplete() { 71 | const [results, setResults] = useState([]) 72 | return ( 73 | { 74 | searchApi(e.target.text) 75 | .then(results => setResults(results)) 76 | }/> 77 | { /* render results */ } 78 | ) 79 | } 80 | ``` 81 | 82 | It calls `searchApi`, a Promise-returning function, to get results, which are objects like `{text: 'Boom', value: 25}`. It then changes internal state with those results to cause a re-render. To simulate a slow state, the first thing we must do is make the component _default_ to using the search function it used before, but make it overridable as a prop. 83 | 84 | ```js 85 | import {searchApi} from './anotherFile' 86 | function AutoComplete({ searchFunction = searchApi }) { 87 | ... 88 | } 89 | ``` 90 | 91 | Now it will call searchApi by default _unless_ another function is provided. Let's provide one. Here's a mock function that, after a delay of 3000 msec, returns the array we'd get from the real service. 92 | 93 | ```js 94 | const slowSearch = term => after(3000, [ 95 | {text: 'Abacus', value: 1}, 96 | {text: 'AbbA', value: 2} 97 | ])).toPromise() 98 | ``` 99 | 100 | And now let's hand this function in to AutoComplete in our stories. 101 | Now AutoComplete can display the results we want, when we want them! 102 | 103 | ```js 104 | storiesOf('Autocomplete', module) 105 | .add('Regular Loading', () => ( 106 | 107 | )) 108 | .add('Slow Loading', () => ( 109 | 110 | )) 111 | ``` 112 | 113 | Why not add mock functions for failed lookups as well? This will make you think, and plan for it. All without leaving Storybook, thanks to Storybook-Animate, and RxJS Observables. 114 | 115 |
116 | 117 | ## Want to use this with the Knobs addon? 118 | 119 | If you have a story that is driven by an Observable, such as one using `` you may wish to use knobs to change that Observable's values, by incorporating the knob's value into the Observable's values. 120 | 121 | Or, you may want to use a knob to **produce** all the values of that Observable. Either way is possible. 122 | 123 |
124 | 125 | Use a knob to change values 126 | 127 | 128 | For the case of modifying an Observable by a knob value, this is done by applying a `map` to every value, in which the knobs value is read. (You may have to read the knob's value once up-front, before the Observable produces a value, to make the knob appear in the Storybook UI). 129 | 130 | Story `ClimbingLoop`: 131 | ```js 132 | 136 | ``` 137 | 138 | Story `KnobControlsScale`: 139 | ```js 140 | ({ 144 | temp, 145 | scale: select("Scale", { C: "C", F: "F" }, "F") 146 | })) 147 | )} 148 | /> 149 | ``` 150 | 151 | ![](./knob-temp-1.gif) 152 | 153 | 154 | Each time the `climbingTemp` Observable has a value, `Animate` will render `` with the scale the knob is set to. 155 | 156 |
157 | 158 |
159 | 160 | Use a knob to produce values 161 | 162 | 163 | What if we want the temperature to be entirely controlled by a knob? We can use `` to create a knob, plus an Observable of its values, and render a component with those values. 164 | 165 | Story `KnobControlsTemperature` 166 | 167 | ```js 168 | ( 171 | ({ temp: v, scale: "F" })) 175 | )} 176 | /> 177 | )} 178 | /> 179 | ``` 180 | 181 |
182 | 183 | ![](./knob-temp-2.gif) 184 | 185 | 186 | ## The Sky's The Limit! 187 | 188 | You can combine Storybook-Animate with other means of animations. CSS animations, React Transition Groups internal to your components - you are only limited by what you can come up with! 189 | 190 | Got ideas that we haven't thought of? Search for an issue or open one if you don't see it. 191 | 192 | ## Examples 193 | 194 | The Thermometer example is in the `stories/` folder of this project. Feel free to post a link to what you've built with it too, we can include it here! 195 | 196 | ![twitter link](https://img.shields.io/badge/twitter-@DeanDevDad-55acee.svg) 197 | -------------------------------------------------------------------------------- /knob-temp-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanrad/storybook-animate/6ede1e607c7977e35b9494dca73f77b3122d8738/knob-temp-1.gif -------------------------------------------------------------------------------- /knob-temp-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanrad/storybook-animate/6ede1e607c7977e35b9494dca73f77b3122d8738/knob-temp-2.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storybook-animate", 3 | "version": "1.0.0", 4 | "main": "dist/index.js", 5 | "repository": "https://github.com/deanius/storybook-animate", 6 | "license": "MIT", 7 | "module": "dist/storybook-animate.esm.js", 8 | "typings": "dist/index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "scripts": { 13 | "start": "tsdx watch", 14 | "build": "tsdx build", 15 | "test": "tsdx test --env=jsdom", 16 | "lint": "tsdx lint", 17 | "storybook": "start-storybook -p 6006", 18 | "build-storybook": "build-storybook" 19 | }, 20 | "peerDependencies": { 21 | "@testing-library/user-event": ">=8.1.0", 22 | "react": ">=16.8" 23 | }, 24 | "husky": { 25 | "hooks": { 26 | "pre-commit": "tsdx lint" 27 | } 28 | }, 29 | "prettier": { 30 | "printWidth": 80, 31 | "semi": false, 32 | "singleQuote": false, 33 | "trailingComma": "none" 34 | }, 35 | "dependencies": { 36 | "@testing-library/dom": "^8.11.3", 37 | "@types/react": "^16.9.2", 38 | "global": "^4.4.0", 39 | "omnibus-rxjs": "^1.0.0", 40 | "polyrhythm": "^1.0.0", 41 | "tsdx": "^0.9.2" 42 | }, 43 | "devDependencies": { 44 | "@babel/core": "^7.6.0", 45 | "@storybook/addon-actions": "^5.2.1", 46 | "@storybook/addon-knobs": "^5.3.14", 47 | "@storybook/addon-links": "^5.2.1", 48 | "@storybook/addons": "^5.2.1", 49 | "@storybook/react": "^5.2.1", 50 | "@testing-library/user-event": "^13.5.0", 51 | "@types/jest": "^25.1.3", 52 | "babel-loader": "^8.0.6", 53 | "react": "^16.9.0", 54 | "rxjs": "^6.5.3", 55 | "ts-loader": "^6.1.0", 56 | "typescript": "^3.6.3" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /sb-slow-search.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanrad/storybook-animate/6ede1e607c7977e35b9494dca73f77b3122d8738/sb-slow-search.gif -------------------------------------------------------------------------------- /src/Animate.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /** 3 | * Rerenders a component each time an Observable of props (propStream) 4 | * yields a new value 5 | */ 6 | export declare const Animate: ({ propStream, component, loop }: { 7 | propStream?: import("polyrhythm").AwaitableObservable | undefined; 8 | component: any; 9 | loop?: boolean | undefined; 10 | }) => JSX.Element; 11 | -------------------------------------------------------------------------------- /src/Animate.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react" 2 | import { after } from "polyrhythm" 3 | import { repeat } from "rxjs/operators" 4 | import { concat } from "rxjs" 5 | /** 6 | * Rerenders a component each time an Observable of props (propStream) 7 | * yields a new value 8 | */ 9 | export const Animate = ({ 10 | propStream = after(0, {}), 11 | component, 12 | loop = false 13 | }) => { 14 | const [props, setProps] = useState({}) 15 | 16 | const propChanges = loop 17 | ? concat(propStream, after(1000, null)).pipe(repeat()) 18 | : propStream 19 | useEffect(() => { 20 | let sub = propChanges.subscribe((newProps) => setProps(newProps || {})) 21 | return () => sub && sub.unsubscribe() 22 | }, []) 23 | return
{React.createElement(component, props)}
24 | } 25 | -------------------------------------------------------------------------------- /src/AutoClicker.js: -------------------------------------------------------------------------------- 1 | import React, { useLayoutEffect, useRef } from 'react'; 2 | import userEvent from '@testing-library/user-event'; 3 | import { defer } from '../util/defer'; 4 | // Tries to click its selector, A noop if none match the selector. 5 | // Intended for use in Storybook stories whose rendering 6 | // or rerendering consists of some user actions. 7 | export function AutoClicker({ 8 | children, 9 | selector = '[role=button]', 10 | delay = 0 11 | }) { 12 | const dom = useRef(); 13 | useLayoutEffect(() => { 14 | defer(() => { 15 | const target = dom.current && dom.current.querySelectorAll(selector)[0]; 16 | target && userEvent.click(target); 17 | }, delay); 18 | }, []); 19 | 20 | return
{children}
; 21 | } 22 | -------------------------------------------------------------------------------- /src/AutoTyper.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useLayoutEffect, useRef } from 'react'; 2 | import userEvent from '@testing-library/user-event'; 3 | import { trigger } from 'polyrhythm'; 4 | // Tries to type text against its selector, (ie for its children). Safe if no children 5 | // exist matching the selector. Intended for use in Storybook. 6 | export function AutoTyper({ 7 | children, 8 | send = '', 9 | selector = 'input', 10 | delay = 0 11 | }) { 12 | const domNode = useRef(); 13 | useLayoutEffect(() => { 14 | const target = 15 | domNode.current && domNode.current.querySelectorAll(selector)[0]; 16 | target && userEvent.type(target, send, { delay }); 17 | }, []); 18 | 19 | useEffect(() => { 20 | trigger('autotyper:mount'); 21 | return () => trigger('autotyper:release'); 22 | }, [domNode.current]); 23 | 24 | return
{children}
; 25 | } 26 | -------------------------------------------------------------------------------- /src/Togglable.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | export function Togglable({ children }) { 4 | const [hidden, setHidden] = useState(false); 5 | const toggleHidden = () => setHidden(old => !old); 6 | 7 | return ( 8 |
9 |
10 | 13 |
14 | {!hidden && children} 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/WithObservableKnob.d.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | /** 3 | A component that exposes a knob's present and future 4 | values as an Observable. 5 | 6 | Example: 7 | ``` 8 | ( 11 | 12 | )} 13 | /> 14 | ``` 15 | */ 16 | export declare function WithObservableKnob({ render, knob: [type, name, value, options] }: { 17 | render: any; 18 | knob: [any, any, any, ({} | undefined)?]; 19 | }): React.ReactElement; 20 | -------------------------------------------------------------------------------- /src/WithObservableKnob.tsx: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject } from "rxjs" 2 | import React, { useMemo } from "react" 3 | 4 | /** 5 | A component that exposes a knob's present and future 6 | values as an Observable. 7 | 8 | Example: 9 | ``` 10 | ( 13 | 14 | )} 15 | /> 16 | ``` 17 | */ 18 | export function WithObservableKnob({ 19 | render, 20 | knob: [type, name, value, options = {}] 21 | }): React.ReactElement { 22 | // Get the current value. Registers our dependency on it. 23 | const knobValue = type.call(type, name, value, options) 24 | 25 | // Set up the Observable and the way to push the next value 26 | const [knobStates, nextKnobValue] = useMemo(() => { 27 | const s = new BehaviorSubject(knobValue) 28 | return [s.asObservable(), (x: any) => s.next(x)] 29 | }, []) 30 | 31 | // pushes the next state 32 | nextKnobValue(knobValue) 33 | 34 | // Compute and return the rendered element once 35 | const elem = useMemo(() => { 36 | return render(knobStates) 37 | }, []) 38 | 39 | return elem 40 | } 41 | -------------------------------------------------------------------------------- /src/defer.js: -------------------------------------------------------------------------------- 1 | // const cancelIt = defer(fn, 100) 2 | // 50msec later... 3 | // cancelIt() 4 | // and fn never was run... 5 | // How can you write code with "threaded" cancelation, but with natural expressive power 6 | export function defer(fn, ms = 0) { 7 | const timeoutId = setTimeout(fn, ms); 8 | return () => clearTimeout(timeoutId); 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | import { concat } from "rxjs" 2 | import { map } from "rxjs/operators" 3 | 4 | export function sequenceOf(...observables) { 5 | const result = concat(...observables) 6 | result.combineWith = (staticProps = {}) => { 7 | return result.pipe( 8 | map(dynProps => Object.assign({}, staticProps, dynProps)) 9 | ) 10 | } 11 | return result 12 | } 13 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | export { Animate } from "./Animate"; 2 | export { WithObservableKnob } from "./WithObservableKnob"; 3 | export { sequenceOf } from "./helpers"; 4 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | export { Animate } from "./Animate" 3 | 4 | // @ts-ignore 5 | export { WithObservableKnob } from "./WithObservableKnob" 6 | 7 | // @ts-ignore 8 | export { sequenceOf } from "./helpers" 9 | -------------------------------------------------------------------------------- /stories/Form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | export const Form = () => { 3 | return
4 |
5 | 6 |
7 |
8 | 9 |
10 |
11 | } -------------------------------------------------------------------------------- /stories/Form.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Form } from "./Form" 3 | import { Animate } from "../src/Animate" 4 | import userEvent from '@testing-library/user-event' 5 | import { concat } from "rxjs" 6 | 7 | import { after } from 'omnibus-rxjs' 8 | import { mapTo } from "rxjs/operators" 9 | 10 | export default { 11 | title: "Form" 12 | } 13 | 14 | const email = () => document.getElementById('email') 15 | const password = () => document.getElementById('password') 16 | 17 | // This is an Observable just for typing side-effects. 18 | // Yield the same (empty) props every time via `mapTo` 19 | const userFillsOutForm = concat( 20 | after(1000), 21 | after(Promise.resolve(), () => { 22 | userEvent.type(email(), 'me@example.com', { delay: 100 }) 23 | }), 24 | after(2000), 25 | after(Promise.resolve(), () => { 26 | userEvent.type(password(), 'password123', { delay: 100 }) 27 | }), 28 | ).pipe( 29 | mapTo({}) 30 | ) 31 | 32 | export const UserFillingOutForm = () => ( 33 | 34 | ) 35 | -------------------------------------------------------------------------------- /stories/Thermometer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | export default function Thermometer(props) { 4 | const { scale = "C", temp = 0 } = props 5 | const width = ((temp - 80) * 360) / 20 6 | const scaledTemp = scale === "C" ? Math.round((temp - 32) / 0.18) / 10 : temp 7 | return ( 8 |
9 |
20 |
30 | {scaledTemp} 31 | {scale === "C" ? "℃" : "℉"} 32 |
33 |
34 | 41 | 42 | 43 | 49 | 56 | 57 | 58 | {" "} 59 |
60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /stories/Thermometer.stories.js: -------------------------------------------------------------------------------- 1 | // import React from "react" 2 | // import Thermometer from "./Thermometer" 3 | // import { Animate } from "../src/Animate" 4 | // import { sequenceOf } from "../src/helpers" 5 | // import { after } from "polyrhythm" 6 | // import { map } from "rxjs/operators" 7 | // import { withKnobs, number, select } from "@storybook/addon-knobs" 8 | 9 | // import { WithObservableKnob } from "../src/WithObservableKnob" 10 | 11 | // export default { 12 | // title: "Thermometer", 13 | // decorators: [withKnobs] 14 | // } 15 | 16 | // const climbingTemp = sequenceOf( 17 | // after(0.0, { temp: 86 }), 18 | // after(1000, { temp: 92 }), 19 | // after(1000, { temp: 96 }), 20 | // after(1000, { temp: 99 }), 21 | // after(1000, { temp: 100.5 }), 22 | // after(1000, { temp: 102.2 }) 23 | // ).combineWith({ scale: "F" }) 24 | 25 | // export const Healthy = () => 26 | // export const Climbing = () => ( 27 | // 28 | // ) 29 | 30 | // export const ClimbingLoop = () => ( 31 | // 32 | // ) 33 | // export const KnobControlsScale = () => ( 34 | // ({ 38 | // temp, 39 | // scale: select("Scale", { C: "C", F: "F" }, "F") 40 | // })) 41 | // )} 42 | // /> 43 | // ) 44 | 45 | // export const KnobControlsTemperature = () => { 46 | // return ( 47 | // ( 50 | // ({ temp: v, scale: "F" })))} 53 | // /> 54 | // )} 55 | // /> 56 | // ) 57 | // } 58 | 59 | // export const KnobControlsScaleAndTemp = () => { 60 | // return ( 61 | // <> 62 | //
63 | // ( 66 | // ({ 70 | // temp: v, 71 | // scale: select("Scale", { C: "C", F: "F" }, "F") 72 | // })) 73 | // )} 74 | // /> 75 | // )} 76 | // /> 77 | //
78 | //
79 | // See the Knobs pane for control of this thermometer. 80 | //

81 | // Use Case: Components that take Observables in their props (such as 82 | // Animate) can give Storybook consumers control of those Observables 83 | // using WithObservableKnob. 84 | //

85 | //
86 | // 87 | // ) 88 | // } 89 | -------------------------------------------------------------------------------- /test/Animate.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as ReactDOM from "react-dom" 3 | import { Animate } from "../src" 4 | 5 | describe("it", () => { 6 | it("renders without crashing", () => { 7 | const div = document.createElement("div") 8 | ReactDOM.render( ""} />, div) 9 | ReactDOM.unmountComponentAtNode(div) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /therm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanrad/storybook-animate/6ede1e607c7977e35b9494dca73f77b3122d8738/therm.gif -------------------------------------------------------------------------------- /thermometer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Slice 1 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "types"], 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "rootDir": "./", 11 | "strict": true, 12 | "noImplicitAny": false, 13 | "strictNullChecks": true, 14 | "strictFunctionTypes": true, 15 | "strictPropertyInitialization": true, 16 | "noImplicitThis": true, 17 | "alwaysStrict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noImplicitReturns": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "moduleResolution": "node", 23 | "baseUrl": "./", 24 | "paths": { 25 | "*": ["src/*", "node_modules/*"] 26 | }, 27 | "jsx": "react", 28 | "esModuleInterop": true 29 | } 30 | } 31 | --------------------------------------------------------------------------------