diff --git a/.babelrc.js b/.babelrc.js new file mode 100644 index 0000000..8559a13 --- /dev/null +++ b/.babelrc.js @@ -0,0 +1,19 @@ +module.exports = { + presets: [ + "@babel/preset-env", + "@babel/preset-typescript", + "@babel/preset-react", + ], + env: { + esm: { + presets: [ + [ + "@babel/preset-env", + { + modules: false, + }, + ], + ], + }, + }, +}; diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..2b0f6e2 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,28 @@ +name: Release + +on: [push] + +jobs: + release: + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, 'ci skip') && !contains(github.event.head_commit.message, 'skip ci')" + steps: + - uses: actions/checkout@v2 + + - name: Prepare repository + run: git fetch --unshallow --tags + + - name: Use Node.js 14.x + uses: actions/setup-node@v1 + with: + node-version: 14.x + + - name: Install dependencies + uses: bahmutov/npm-install@v1 + + - name: Create Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + yarn release \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a951d51 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +dist/ +node_modules/ +storybook-static/ +build-storybook.log +.DS_Store +.env \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..1521c8b --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +dist diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.prettierrc @@ -0,0 +1 @@ +{} diff --git a/.storybook/main.js b/.storybook/main.js new file mode 100644 index 0000000..4f864dc --- /dev/null +++ b/.storybook/main.js @@ -0,0 +1,7 @@ +module.exports = { + stories: [ + "../stories/**/*.stories.mdx", + "../stories/**/*.stories.@(js|jsx|ts|tsx)", + ], + addons: ["../preset.js", "@storybook/addon-essentials"], +}; diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..62eb6da --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Storybook contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..04d1737 --- /dev/null +++ b/README.md @@ -0,0 +1,114 @@ +# Storybook Addon Kit + +Simplify the creation of Storybook addons + +- ๐Ÿ“ Live-editing in development +- โš›๏ธ React/JSX support +- ๐Ÿ“ฆ Transpiling and bundling with Babel +- ๐Ÿท Plugin metadata +- ๐Ÿšข Release management with [Auto](https://github.com/intuit/auto) +- ๐Ÿงบ Boilerplate and sample code +- ๐Ÿ›„ ESM support +- ๐Ÿ›‚ TypeScript by default with option to eject to JS + +## Getting Started + +Click the **Use this template** button to get started. + +![](https://user-images.githubusercontent.com/321738/125058439-8d9ef880-e0aa-11eb-9211-e6d7be812959.gif) + +Clone your repository and install dependencies. + +```sh +yarn +``` + +### Development scripts + +- `yarn start` runs babel in watch mode and starts Storybook +- `yarn build` build and package your addon code + +### Switch from TypeScript to JavaScript + +Don't want to use TypeScript? We offer a handy eject command: `yarn eject-ts` + +This will convert all code to JS. It is a destructive process, so we recommended running this before you start writing any code. + +## What's included? + +![Demo](https://user-images.githubusercontent.com/42671/107857205-e7044380-6dfa-11eb-8718-ad02e3ba1a3f.gif) + +The addon code lives in `src`. It demonstrates all core addon related concepts. The three [UI paradigms](https://storybook.js.org/docs/react/addons/addon-types#ui-based-addons) + +- `src/Tool.js` +- `src/Panel.js` +- `src/Tab.js` + +Which, along with the addon itself, are registered in `src/preset/manager.js`. + +Managing State and interacting with a story: + +- `src/withGlobals.js` & `src/Tool.js` demonstrates how to use `useGlobals` to manage global state and modify the contents of a Story. +- `src/withRoundTrip.js` & `src/Panel.js` demonstrates two-way communication using channels. +- `src/Tab.js` demonstrates how to use `useParameter` to access the current story's parameters. + +Your addon might use one or more of these patterns. Feel free to delete unused code. Update `src/preset/manager.js` and `src/preset/preview.js` accordingly. + +Lastly, configure you addon name in `src/constants.js`. + +### Metadata + +Storybook addons are listed in the [catalog](https://storybook.js.org/addons) and distributed via npm. The catalog is populated by querying npm's registry for Storybook-specific metadata in `package.json`. This project has been configured with sample data. Learn more about available options in the [Addon metadata docs](https://storybook.js.org/docs/react/addons/addon-catalog#addon-metadata). + +## Release Management + +### Setup + +This project is configured to use [auto](https://github.com/intuit/auto) for release management. It generates a changelog and pushes it to both GitHub and npm. Therefore, you need to configure access to both: + +- [`NPM_TOKEN`](https://docs.npmjs.com/creating-and-viewing-access-tokens#creating-access-tokens) Create a token with both _Read and Publish_ permissions. +- [`GH_TOKEN`](https://github.com/settings/tokens) Create a token with the `repo` scope. + +Then open your `package.json` and edit the following fields: + +- `name` +- `author` +- `repository` + +#### Local + +To use `auto` locally create a `.env` file at the root of your project and add your tokens to it: + +```bash +GH_TOKEN= +NPM_TOKEN= +``` + +Lastly, **create labels on GitHub**. Youโ€™ll use these labels in the future when making changes to the package. + +```bash +npx auto create-labels +``` + +If you check on GitHub, youโ€™ll now see a set of labels that `auto` would like you to use. Use these to tag future pull requests. + +#### GitHub Actions + +This template comes with GitHub actions already set up to publish your addon anytime someone pushes to your repository. + +Go to `Settings > Secrets`, click `New repository secret`, and add your `NPM_TOKEN`. + +### Creating a releasing + +To create a release locally you can run the following command, otherwise the GitHub action will make the release for you. + +```sh +yarn release +``` + +That will: + +- Build and package the addon code +- Bump the version +- Push a release to GitHub and npm +- Push a changelog to GitHub diff --git a/package.json b/package.json new file mode 100644 index 0000000..61dd189 --- /dev/null +++ b/package.json @@ -0,0 +1,92 @@ +{ + "name": "storybook-addon-kit", + "version": "0.0.0", + "description": "everything you need to build a Storybook addon", + "keywords": [ + "storybook-addons", + "style", + "test" + ], + "repository": { + "type": "git", + "url": "https://github.com/storybookjs/storybook-addon-kit" + }, + "author": "winkerVSbecks", + "license": "MIT", + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "types": "dist/ts/index.d.ts", + "files": [ + "dist/**/*", + "README.md", + "*.js", + "*.d.ts" + ], + "scripts": { + "clean": "rimraf ./dist", + "buildBabel": "concurrently \"yarn buildBabel:cjs\" \"yarn buildBabel:esm\"", + "buildBabel:cjs": "babel ./src -d ./dist/cjs --extensions \".js,.jsx,.ts,.tsx\"", + "buildBabel:esm": "babel ./src -d ./dist/esm --env-name esm --extensions \".js,.jsx,.ts,.tsx\"", + "buildTsc": "tsc --declaration --emitDeclarationOnly --outDir ./dist/ts", + "prebuild": "yarn clean", + "build": "concurrently \"yarn buildBabel\" \"yarn buildTsc\"", + "build:watch": "concurrently \"yarn buildBabel:esm -- --watch\" \"yarn buildTsc -- --watch\"", + "test": "echo \"Error: no test specified\" && exit 1", + "storybook": "start-storybook -p 6006", + "start": "concurrently \"yarn build:watch\" \"yarn storybook -- --no-manager-cache --quiet\"", + "build-storybook": "build-storybook", + "prerelease": "zx scripts/prepublish-checks.mjs", + "release": "yarn build && auto shipit", + "eject-ts": "zx scripts/eject-typescript.mjs" + }, + "devDependencies": { + "@babel/cli": "^7.12.1", + "@babel/core": "^7.12.3", + "@babel/preset-env": "^7.12.1", + "@babel/preset-react": "^7.12.5", + "@babel/preset-typescript": "^7.13.0", + "@storybook/addon-essentials": "^6.2.9", + "@storybook/react": "^6.2.9", + "auto": "^10.3.0", + "babel-loader": "^8.1.0", + "boxen": "^5.0.1", + "concurrently": "^6.2.0", + "dedent": "^0.7.0", + "prettier": "^2.3.1", + "prop-types": "^15.7.2", + "react": "^17.0.1", + "react-dom": "^17.0.1", + "rimraf": "^3.0.2", + "typescript": "^4.2.4", + "zx": "^1.14.1" + }, + "peerDependencies": { + "@storybook/addons": "^6.2.9", + "@storybook/api": "^6.2.9", + "@storybook/components": "^6.2.9", + "@storybook/core-events": "^6.2.9", + "@storybook/theming": "^6.2.9", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + }, + "publishConfig": { + "access": "public" + }, + "storybook": { + "displayName": "Addon Kit", + "supportedFrameworks": [ + "react", + "vue", + "angular" + ], + "icon": "https://user-images.githubusercontent.com/321738/63501763-88dbf600-c4cc-11e9-96cd-94adadc2fd72.png" + } +} diff --git a/preset.js b/preset.js new file mode 100644 index 0000000..37cc891 --- /dev/null +++ b/preset.js @@ -0,0 +1,12 @@ +function config(entry = []) { + return [...entry, require.resolve("./dist/esm/preset/preview")]; +} + +function managerEntries(entry = []) { + return [...entry, require.resolve("./dist/esm/preset/manager")]; +} + +module.exports = { + managerEntries, + config, +}; diff --git a/scripts/eject-typescript.mjs b/scripts/eject-typescript.mjs new file mode 100644 index 0000000..c9edb47 --- /dev/null +++ b/scripts/eject-typescript.mjs @@ -0,0 +1,30 @@ +#!/usr/bin/env zx + +// Copy TS files and delete src +await $`cp -r ./src ./srcTS`; +await $`rm -rf ./src`; +await $`mkdir ./src`; + +// Convert TS code to JS +await $`babel --no-babelrc --presets @babel/preset-typescript ./srcTS -d ./src --extensions \".js,.jsx,.ts,.tsx\" --ignore "./srcTS/typings.d.ts"`; + +// Format the newly created .js files +await $`prettier --write ./src`; + +// Add in minimal files required for the TS build setup +await $`touch ./src/dummy.ts`; +await $`printf "export {};" >> ./src/dummy.ts`; + +await $`touch ./src/typings.d.ts`; +await $`printf 'declare module "global";' >> ./src/typings.d.ts`; + +// Clean up +await $`rm -rf ./srcTS`; + +console.log( + chalk.green.bold` +TypeScript Ejection complete!`, + chalk.green` +Addon code converted with JS. The TypeScript build setup is still available in case you want to adopt TypeScript in the future. +` +); diff --git a/scripts/prepublish-checks.mjs b/scripts/prepublish-checks.mjs new file mode 100644 index 0000000..c53f3c0 --- /dev/null +++ b/scripts/prepublish-checks.mjs @@ -0,0 +1,56 @@ +#!/usr/bin/env zx + +const packageJson = require("../package.json"); +const boxen = require("boxen"); +const dedent = require("dedent"); + +const name = packageJson.name; +const displayName = packageJson.storybook.displayName; + +let exitCode = 0; +$.verbose = false; + +/** + * Check that meta data has been updated + */ +if (name.includes("addon-kit") || displayName.includes("Addon Kit")) { + console.error( + boxen( + dedent` + ${chalk.red.bold("Missing metadata")} + + ${chalk.red(dedent`Your package name and/or displayName includes default values from the Addon Kit. + The addon gallery filters out all such addons. + + Please configure appropriate metadata before publishing your addon. For more info, see: + https://storybook.js.org/docs/react/addons/addon-catalog#addon-metadata`)}`, + { padding: 1, borderColor: "red" } + ) + ); + + exitCode = 1; +} + +/** + * Check that README has been updated + */ +const readmeTestStrings = + "# Storybook Addon Kit|Click the \\*\\*Use this template\\*\\* button to get started.|https://user-images.githubusercontent.com/42671/106809879-35b32000-663a-11eb-9cdc-89f178b5273f.gif"; + +if ((await $`cat README.md | grep -E ${readmeTestStrings}`.exitCode) == 0) { + console.error( + boxen( + dedent` + ${chalk.red.bold("README not updated")} + + ${chalk.red(dedent`You are using the default README.md file that comes with the addon kit. + Please update it to provide info on what your addon does and how to use it.`)} + `, + { padding: 1, borderColor: "red" } + ) + ); + + exitCode = 1; +} + +process.exit(exitCode); diff --git a/src/Panel.tsx b/src/Panel.tsx new file mode 100644 index 0000000..3f7236f --- /dev/null +++ b/src/Panel.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import { useAddonState, useChannel } from "@storybook/api"; +import { AddonPanel } from "@storybook/components"; +import { ADDON_ID, EVENTS } from "./constants"; +import { PanelContent } from "./components/PanelContent"; + +interface PanelProps { + active: boolean; +} + +export const Panel: React.FC = (props) => { + // https://storybook.js.org/docs/react/addons/addons-api#useaddonstate + const [results, setState] = useAddonState(ADDON_ID, { + danger: [], + warning: [], + }); + + // https://storybook.js.org/docs/react/addons/addons-api#usechannel + const emit = useChannel({ + [EVENTS.RESULT]: (newResults) => setState(newResults), + }); + + return ( + + { + emit(EVENTS.REQUEST); + }} + clearData={() => { + emit(EVENTS.CLEAR); + }} + /> + + ); +}; diff --git a/src/Tab.tsx b/src/Tab.tsx new file mode 100644 index 0000000..1cfc887 --- /dev/null +++ b/src/Tab.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import { useParameter } from "@storybook/api"; +import { PARAM_KEY } from "./constants"; +import { TabContent } from "./components/TabContent"; + +interface TabProps { + active: boolean; +} + +export const Tab: React.FC = ({ active }) => { + // https://storybook.js.org/docs/react/addons/addons-api#useparameter + const paramData = useParameter(PARAM_KEY, ""); + + return active ? : null; +}; diff --git a/src/Tool.tsx b/src/Tool.tsx new file mode 100644 index 0000000..c5d39b6 --- /dev/null +++ b/src/Tool.tsx @@ -0,0 +1,31 @@ +import React, { useCallback } from "react"; +import { useGlobals } from "@storybook/api"; +import { Icons, IconButton } from "@storybook/components"; +import { TOOL_ID } from "./constants"; + +export const Tool = () => { + const [{ myAddon }, updateGlobals] = useGlobals(); + + const toggleMyTool = useCallback( + () => + updateGlobals({ + myAddon: myAddon ? undefined : true, + }), + [myAddon] + ); + + return ( + + {/* + Checkout https://next--storybookjs.netlify.app/official-storybook/?path=/story/basics-icon--labels + for the full list of icons + */} + + + ); +}; diff --git a/src/components/List.tsx b/src/components/List.tsx new file mode 100644 index 0000000..aa89621 --- /dev/null +++ b/src/components/List.tsx @@ -0,0 +1,95 @@ +import React, { Fragment, useState } from "react"; +import { styled, themes, convert } from "@storybook/theming"; +import { Icons, IconsProps } from "@storybook/components"; + +const ListWrapper = styled.ul({ + listStyle: "none", + fontSize: 14, + padding: 0, + margin: 0, +}); + +const Wrapper = styled.div({ + display: "flex", + width: "100%", + borderBottom: `1px solid ${convert(themes.normal).appBorderColor}`, + "&:hover": { + background: convert(themes.normal).background.hoverable, + }, +}); + +const Icon = styled(Icons)({ + height: 10, + width: 10, + minWidth: 10, + color: convert(themes.normal).color.mediumdark, + marginRight: 10, + transition: "transform 0.1s ease-in-out", + alignSelf: "center", + display: "inline-flex", +}); + +const HeaderBar = styled.div({ + padding: convert(themes.normal).layoutMargin, + paddingLeft: convert(themes.normal).layoutMargin - 3, + background: "none", + color: "inherit", + textAlign: "left", + cursor: "pointer", + borderLeft: "3px solid transparent", + width: "100%", + + "&:focus": { + outline: "0 none", + borderLeft: `3px solid ${convert(themes.normal).color.secondary}`, + }, +}); + +const Description = styled.div({ + padding: convert(themes.normal).layoutMargin, + marginBottom: convert(themes.normal).layoutMargin, + fontStyle: "italic", +}); + +type Item = { + title: string; + description: string; +}; + +interface ListItemProps { + item: Item; +} + +export const ListItem: React.FC = ({ item }) => { + const [open, onToggle] = useState(false); + + return ( + + + onToggle(!open)} role="button"> + + {item.title} + + + {open ? {item.description} : null} + + ); +}; + +interface ListProps { + items: Item[]; +} + +export const List: React.FC = ({ items }) => ( + + {items.map((item, idx) => ( + + ))} + +); diff --git a/src/components/PanelContent.tsx b/src/components/PanelContent.tsx new file mode 100644 index 0000000..9fcfb5f --- /dev/null +++ b/src/components/PanelContent.tsx @@ -0,0 +1,76 @@ +import React, { Fragment } from "react"; +import { styled, themes, convert } from "@storybook/theming"; +import { TabsState, Placeholder, Button } from "@storybook/components"; +import { List } from "./List"; + +export const RequestDataButton = styled(Button)({ + marginTop: "1rem", +}); + +type Results = { + danger: any[]; + warning: any[]; +}; + +interface PanelContentProps { + results: Results; + fetchData: () => void; + clearData: () => void; +} + +/** + * Checkout https://github.com/storybookjs/storybook/blob/next/addons/jest/src/components/Panel.tsx + * for a real world example + */ +export const PanelContent: React.FC = ({ + results, + fetchData, + clearData, +}) => ( + +
+ + + Addons can gather details about how a story is rendered. This is panel + uses a tab pattern. Click the button below to fetch data for the other + two tabs. + + + + Request data + + + + Clear data + + + +
+
+ +
+
+ +
+
+); diff --git a/src/components/TabContent.tsx b/src/components/TabContent.tsx new file mode 100644 index 0000000..78ee961 --- /dev/null +++ b/src/components/TabContent.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import { styled } from "@storybook/theming"; +import { Title, Source, Link } from "@storybook/components"; + +const TabWrapper = styled.div(({ theme }) => ({ + background: theme.background.content, + padding: "4rem 20px", + minHeight: "100vh", + boxSizing: "border-box", +})); + +const TabInner = styled.div({ + maxWidth: 768, + marginLeft: "auto", + marginRight: "auto", +}); + +interface TabContentProps { + code: string; +} + +export const TabContent: React.FC = ({ code }) => ( + + + My Addon +

+ Your addon can create a custom tab in Storybook. For example, the + official{" "} + + @storybook/addon-docs + {" "} + uses this pattern. +

+

+ You have full control over what content is being rendered here. You can + use components from{" "} + + @storybook/components + {" "} + to match the look and feel of Storybook, for example the{" "} + <Source /> component below. Or build a completely + custom UI. +

+ +
+
+); diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..d7ef8e4 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,11 @@ +export const ADDON_ID = "storybook/my-addon"; +export const TOOL_ID = `${ADDON_ID}/tool`; +export const PANEL_ID = `${ADDON_ID}/panel`; +export const TAB_ID = `${ADDON_ID}/tab`; +export const PARAM_KEY = `myAddonParameter`; + +export const EVENTS = { + RESULT: `${ADDON_ID}/result`, + REQUEST: `${ADDON_ID}/request`, + CLEAR: `${ADDON_ID}/clear`, +}; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..644402a --- /dev/null +++ b/src/index.ts @@ -0,0 +1,6 @@ +if (module && module.hot && module.hot.decline) { + module.hot.decline(); +} + +// make it work with --isolatedModules +export default {}; diff --git a/src/preset/manager.ts b/src/preset/manager.ts new file mode 100644 index 0000000..5c1845f --- /dev/null +++ b/src/preset/manager.ts @@ -0,0 +1,36 @@ +import { addons, types } from "@storybook/addons"; + +import { ADDON_ID, TOOL_ID, PANEL_ID } from "../constants"; +import { Tool } from "../Tool"; +import { Panel } from "../Panel"; +import { Tab } from "../Tab"; + +// Register the addon +addons.register(ADDON_ID, () => { + // Register the tool + addons.add(TOOL_ID, { + type: types.TOOL, + title: "My addon", + match: ({ viewMode }) => !!(viewMode && viewMode.match(/^(story|docs)$/)), + render: Tool, + }); + + // Register the panel + addons.add(PANEL_ID, { + type: types.PANEL, + title: "My addon", + match: ({ viewMode }) => viewMode === "story", + render: Panel, + }); + + // Register the tab + addons.add(PANEL_ID, { + type: types.TAB, + title: "My addon", + //๐Ÿ‘‡ Checks the current route for the story + route: ({ storyId }) => `/myaddon/${storyId}`, + //๐Ÿ‘‡ Shows the Tab UI element in myaddon view mode + match: ({ viewMode }) => viewMode === "myaddon", + render: Tab, + }); +}); diff --git a/src/preset/preview.ts b/src/preset/preview.ts new file mode 100644 index 0000000..b2ba734 --- /dev/null +++ b/src/preset/preview.ts @@ -0,0 +1,14 @@ +/** + * A decorator is a way to wrap a story in extra โ€œrenderingโ€ functionality. Many addons define decorators + * in order to augment stories: + * - with extra rendering + * - gather details about how a story is rendered + * + * When writing stories, decorators are typically used to wrap stories with extra markup or context mocking. + * + * https://storybook.js.org/docs/react/writing-stories/decorators#gatsby-focus-wrapper + */ +import { withGlobals } from "../withGlobals"; +import { withRoundTrip } from "../withRoundTrip"; + +export const decorators = [withGlobals, withRoundTrip]; diff --git a/src/typings.d.ts b/src/typings.d.ts new file mode 100644 index 0000000..3563aeb --- /dev/null +++ b/src/typings.d.ts @@ -0,0 +1 @@ +declare module "global"; diff --git a/src/withGlobals.ts b/src/withGlobals.ts new file mode 100644 index 0000000..df134f1 --- /dev/null +++ b/src/withGlobals.ts @@ -0,0 +1,44 @@ +import { StoryFn as StoryFunction, StoryContext } from "@storybook/addons"; +import { useEffect, useGlobals } from "@storybook/addons"; + +export const withGlobals = (StoryFn: StoryFunction, context: StoryContext) => { + const [{ myAddon }] = useGlobals(); + // Is the addon being used in the docs panel + const isInDocs = context.viewMode === "docs"; + + useEffect(() => { + // Execute your side effect here + // For example, to manipulate the contents of the preview + const selectorId = isInDocs + ? `#anchor--${context.id} .docs-story` + : `#root`; + + displayToolState(selectorId, { + myAddon, + isInDocs, + }); + }, [myAddon]); + + return StoryFn(); +}; + +function displayToolState(selector: string, state: any) { + const rootElement = document.querySelector(selector); + let preElement = rootElement.querySelector("pre"); + + if (!preElement) { + preElement = document.createElement("pre"); + preElement.style.setProperty("margin-top", "2rem"); + preElement.style.setProperty("padding", "1rem"); + preElement.style.setProperty("background-color", "#eee"); + preElement.style.setProperty("border-radius", "3px"); + preElement.style.setProperty("max-width", "600px"); + rootElement.appendChild(preElement); + } + + preElement.innerText = `This snippet is injected by the withGlobals decorator. +It updates as the user interacts with the โšก tool in the toolbar above. + +${JSON.stringify(state, null, 2)} +`; +} diff --git a/src/withRoundTrip.ts b/src/withRoundTrip.ts new file mode 100644 index 0000000..b90b36f --- /dev/null +++ b/src/withRoundTrip.ts @@ -0,0 +1,46 @@ +import { StoryFn as StoryFunction, useChannel } from "@storybook/addons"; +import { STORY_CHANGED } from "@storybook/core-events"; +import { EVENTS } from "./constants"; + +export const withRoundTrip = (storyFn: StoryFunction) => { + const emit = useChannel({ + [EVENTS.REQUEST]: () => { + emit(EVENTS.RESULT, { + danger: [ + { + title: "Panels are the most common type of addon in the ecosystem", + description: + "For example the official @storybook/actions and @storybook/a11y use this pattern", + }, + { + title: + "You can specify a custom title for your addon panel and have full control over what content it renders", + description: + "@storybook/components offers components to help you addons with the look and feel of Storybook itself", + }, + ], + warning: [ + { + title: + 'This tabbed UI pattern is a popular option to display "test" reports.', + description: + "It's used by @storybook/addon-jest and @storybook/addon-a11y. @storybook/components offers this and other components to help you quickly build an addon", + }, + ], + }); + }, + [STORY_CHANGED]: () => { + emit(EVENTS.RESULT, { + danger: [], + warning: [], + }); + }, + [EVENTS.CLEAR]: () => { + emit(EVENTS.RESULT, { + danger: [], + warning: [], + }); + }, + }); + return storyFn(); +}; diff --git a/stories/Button.js b/stories/Button.js new file mode 100644 index 0000000..19c9656 --- /dev/null +++ b/stories/Button.js @@ -0,0 +1,54 @@ +import React from "react"; +import PropTypes from "prop-types"; +import "./button.css"; + +/** + * Primary UI component for user interaction + */ +export const Button = ({ primary, backgroundColor, size, label, ...props }) => { + const mode = primary + ? "storybook-button--primary" + : "storybook-button--secondary"; + return ( + + ); +}; + +Button.propTypes = { + /** + * Is this the principal call to action on the page? + */ + primary: PropTypes.bool, + /** + * What background color to use + */ + backgroundColor: PropTypes.string, + /** + * How large should the button be? + */ + size: PropTypes.oneOf(["small", "medium", "large"]), + /** + * Button contents + */ + label: PropTypes.string.isRequired, + /** + * Optional click handler + */ + onClick: PropTypes.func, +}; + +Button.defaultProps = { + backgroundColor: null, + primary: false, + size: "medium", + onClick: undefined, +}; diff --git a/stories/Button.stories.js b/stories/Button.stories.js new file mode 100644 index 0000000..60bc6cd --- /dev/null +++ b/stories/Button.stories.js @@ -0,0 +1,39 @@ +import React from "react"; +import { Button } from "./Button"; + +export default { + title: "Example/Button", + component: Button, + parameters: { + myAddonParameter: ` + + a.id} /> + +`, + }, +}; + +const Template = (args) =>