├── manager.js ├── preset.js ├── preview.js ├── pnpm-workspace.yaml ├── .prettierignore ├── .vscode └── settings.json ├── src ├── types.ts ├── index.ts ├── constants.ts ├── manager-helpers.tsx ├── stories │ ├── Header.stories.ts │ ├── header.css │ ├── button.css │ ├── Page.stories.ts │ ├── Button.tsx │ ├── assets │ │ ├── direction.svg │ │ ├── flow.svg │ │ ├── code-brackets.svg │ │ ├── comments.svg │ │ ├── repo.svg │ │ ├── plugin.svg │ │ ├── stackalt.svg │ │ └── colors.svg │ ├── page.css │ ├── Button.stories.ts │ ├── Header.tsx │ ├── Page.tsx │ └── Introduction.mdx ├── preset.ts ├── preview.ts ├── components │ ├── Tool.tsx │ ├── Tab.tsx │ ├── List.tsx │ └── Panel.tsx ├── manager.tsx ├── withRoundTrip.ts └── withGlobals.ts ├── .gitignore ├── .prettierrc ├── vite.config.ts ├── .storybook ├── main.ts ├── preview.ts ├── local-preset.ts └── manager.ts ├── tsconfig.json ├── eslint.config.js ├── .github └── workflows │ ├── release.yml │ └── build.yml ├── LICENSE ├── scripts ├── eject-typescript.js ├── prepublish-checks.js └── welcome.js ├── tsup.config.ts ├── package.json └── README.md /manager.js: -------------------------------------------------------------------------------- 1 | import './dist/manager'; 2 | -------------------------------------------------------------------------------- /preset.js: -------------------------------------------------------------------------------- 1 | export * from './dist/preset.js'; 2 | -------------------------------------------------------------------------------- /preview.js: -------------------------------------------------------------------------------- 1 | export * from './dist/preview'; 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | onlyBuiltDependencies: 2 | - esbuild 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | pnpm-lock.yaml 3 | node_modules 4 | storybook-static -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface Result { 2 | divs: DOMRect[]; 3 | styled: DOMRect[]; 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | storybook-static/ 4 | build-storybook.log 5 | .DS_Store 6 | .env 7 | .eslintcache -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "arrowParens": "always", 5 | "printWidth": 120, 6 | "tabWidth": 2, 7 | "useTabs": false 8 | } 9 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { definePreviewAddon } from 'storybook/internal/csf'; 2 | 3 | import addonAnnotations from './preview'; 4 | 5 | export default () => definePreviewAddon(addonAnnotations); 6 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const ADDON_ID = 'my-addon'; 2 | export const TOOL_ID = `${ADDON_ID}/tool`; 3 | export const PANEL_ID = `${ADDON_ID}/panel`; 4 | export const TAB_ID = `${ADDON_ID}/tab`; 5 | export const KEY = `my-addon`; 6 | 7 | export const EVENTS = { 8 | RESULT: `${ADDON_ID}/result`, 9 | REQUEST: `${ADDON_ID}/request`, 10 | }; 11 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import { defineMain } from '@storybook/react-vite/node'; 2 | 3 | const config = defineMain({ 4 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], 5 | addons: ['@storybook/addon-docs', import.meta.resolve('./local-preset.ts')], 6 | framework: '@storybook/react-vite', 7 | }); 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from '@storybook/react-vite'; 2 | 3 | const preview: Preview = { 4 | parameters: { 5 | controls: { 6 | matchers: { 7 | color: /(background|color)$/i, 8 | date: /Date$/, 9 | }, 10 | }, 11 | }, 12 | initialGlobals: { 13 | background: { value: 'light' }, 14 | }, 15 | }; 16 | 17 | export default preview; 18 | -------------------------------------------------------------------------------- /.storybook/local-preset.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url'; 2 | 3 | /** 4 | * to load the built addon in this test Storybook 5 | */ 6 | export function previewAnnotations(entry = []) { 7 | return [...entry, fileURLToPath(import.meta.resolve('../dist/preview.js'))]; 8 | } 9 | 10 | export function managerEntries(entry = []) { 11 | return [...entry, fileURLToPath(import.meta.resolve('../dist/manager.js'))]; 12 | } 13 | 14 | export * from '../dist/preset.js'; 15 | -------------------------------------------------------------------------------- /src/manager-helpers.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { API_HashEntry } from 'storybook/internal/types'; 3 | import { ADDON_ID } from './constants'; 4 | 5 | /** 6 | * This opt-in helper can be manually imported by your users who want to 7 | * use it, and ignored by those who don't. 8 | */ 9 | export function renderLabel(item: API_HashEntry) { 10 | if (item.type !== 'story' && item.type !== 'docs') { 11 | return; 12 | } 13 | 14 | if (item.title.startsWith(ADDON_ID)) { 15 | return 🌟 {item.name}; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.storybook/manager.ts: -------------------------------------------------------------------------------- 1 | import { addons } from 'storybook/manager-api'; 2 | 3 | import { renderLabel } from '../src/manager-helpers'; 4 | 5 | /* 6 | * This is an example of opt-in usage of addon exports. Your users can choose to 7 | * import and use this helper, or not. Opt-in helpers should be exported in their 8 | * own file rather than in `manager.tsx`, because importing `manager.tsx` multiple 9 | * times can cause the addon registration code to run multiple times. 10 | */ 11 | addons.setConfig({ 12 | sidebar: { 13 | renderLabel, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /src/stories/Header.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react-vite'; 2 | import { Header } from './Header'; 3 | 4 | const meta: Meta = { 5 | title: 'Example/Header', 6 | component: Header, 7 | parameters: { 8 | // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout 9 | layout: 'fullscreen', 10 | }, 11 | }; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const LoggedIn: Story = { 17 | args: { 18 | user: { 19 | name: 'Jane Doe', 20 | }, 21 | }, 22 | }; 23 | 24 | export const LoggedOut: Story = {}; 25 | -------------------------------------------------------------------------------- /src/preset.ts: -------------------------------------------------------------------------------- 1 | // You can use presets to augment the Storybook configuration 2 | // You rarely want to do this in addons, 3 | // so often you want to delete this file and remove the reference to it in package.json#exports and package.json#bunder.nodeEntries 4 | // Read more about presets at https://storybook.js.org/docs/addons/writing-presets 5 | 6 | export const viteFinal = async (config: unknown) => { 7 | console.log('This addon is augmenting the Vite config'); 8 | return config; 9 | }; 10 | 11 | export const webpack = async (config: unknown) => { 12 | console.log('This addon is augmenting the Webpack config'); 13 | return config; 14 | }; 15 | -------------------------------------------------------------------------------- /src/stories/header.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 4 | padding: 15px 20px; 5 | display: flex; 6 | align-items: center; 7 | justify-content: space-between; 8 | } 9 | 10 | svg { 11 | display: inline-block; 12 | vertical-align: top; 13 | } 14 | 15 | h1 { 16 | font-weight: 700; 17 | font-size: 20px; 18 | line-height: 1; 19 | margin: 6px 0 6px 10px; 20 | display: inline-block; 21 | vertical-align: top; 22 | } 23 | 24 | button + button { 25 | margin-left: 10px; 26 | } 27 | 28 | .welcome { 29 | color: #333; 30 | font-size: 14px; 31 | margin-right: 10px; 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "skipLibCheck": true, 5 | "target": "esnext", // Node 20 according to https://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping#node-20 6 | "allowJs": true, 7 | "resolveJsonModule": true, 8 | "moduleDetection": "force", 9 | "moduleResolution": "bundler", 10 | "module": "preserve", 11 | "jsx": "react", 12 | "isolatedModules": true, 13 | "verbatimModuleSyntax": true, 14 | "strict": true, 15 | "noUncheckedIndexedAccess": true, 16 | "noImplicitOverride": true, 17 | "noImplicitAny": true, 18 | "lib": ["esnext", "dom", "dom.iterable"], 19 | "baseUrl": ".", 20 | "rootDir": "." 21 | }, 22 | "include": ["src/**/*", "tsup.config.ts"] 23 | } 24 | -------------------------------------------------------------------------------- /src/stories/button.css: -------------------------------------------------------------------------------- 1 | .storybook-button { 2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | font-weight: 700; 4 | border: 0; 5 | border-radius: 3em; 6 | cursor: pointer; 7 | display: inline-block; 8 | line-height: 1; 9 | } 10 | .storybook-button--primary { 11 | color: white; 12 | background-color: #1ea7fd; 13 | } 14 | .storybook-button--secondary { 15 | color: #333; 16 | background-color: transparent; 17 | box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; 18 | } 19 | .storybook-button--small { 20 | font-size: 12px; 21 | padding: 10px 16px; 22 | } 23 | .storybook-button--medium { 24 | font-size: 14px; 25 | padding: 11px 20px; 26 | } 27 | .storybook-button--large { 28 | font-size: 16px; 29 | padding: 12px 24px; 30 | } 31 | -------------------------------------------------------------------------------- /src/stories/Page.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react-vite'; 2 | import { within, userEvent } from 'storybook/test'; 3 | 4 | import { Page } from './Page'; 5 | 6 | const meta: Meta = { 7 | title: 'Example/Page', 8 | component: Page, 9 | parameters: { 10 | // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout 11 | layout: 'fullscreen', 12 | }, 13 | }; 14 | 15 | export default meta; 16 | type Story = StoryObj; 17 | 18 | export const LoggedOut: Story = {}; 19 | 20 | // More on interaction testing: https://storybook.js.org/docs/react/writing-tests/interaction-testing 21 | export const LoggedIn: Story = { 22 | play: async ({ canvasElement }) => { 23 | const canvas = within(canvasElement); 24 | const loginButton = await canvas.getByRole('button', { 25 | name: /Log in/i, 26 | }); 27 | await userEvent.click(loginButton); 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import storybook from 'eslint-plugin-storybook'; 2 | import js from '@eslint/js'; 3 | import prettierRecommended from 'eslint-plugin-prettier/recommended'; 4 | import reactPlugin from 'eslint-plugin-react'; 5 | import tseslint from 'typescript-eslint'; 6 | 7 | export default [ 8 | { 9 | ignores: [ 10 | '.github/dependabot.yml', 11 | '!.*', 12 | '*.tgz', 13 | 'dist/', 14 | 'scripts/', 15 | 'coverage/', 16 | 'node_modules/', 17 | 'storybook-static/', 18 | 'build-storybook.log', 19 | '.DS_Store', 20 | '.env', 21 | '.idea', 22 | '.vscode', 23 | ], 24 | }, 25 | js.configs.recommended, 26 | reactPlugin.configs.flat.recommended, 27 | { 28 | settings: { 29 | react: { 30 | version: 'detect', 31 | }, 32 | }, 33 | }, 34 | ...tseslint.configs.recommended, 35 | ...storybook.configs['flat/recommended'], 36 | prettierRecommended, 37 | ]; 38 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: [push] 4 | 5 | jobs: 6 | release: 7 | runs-on: ubuntu-latest 8 | if: "!contains(github.event.head_commit.message, 'ci skip') && !contains(github.event.head_commit.message, 'skip ci') && github.repository != 'storybookjs/addon-kit'" 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | 13 | - name: Prepare repository 14 | run: git fetch --unshallow --tags 15 | 16 | - name: Install pnpm 17 | uses: pnpm/action-setup@v4 18 | 19 | - name: Use Node.js 20.x 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: 20.x 23 | cache: 'pnpm' 24 | 25 | - name: Install dependencies 26 | run: pnpm install 27 | 28 | - name: Create Release 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 32 | run: | 33 | pnpm run release 34 | -------------------------------------------------------------------------------- /src/preview.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A decorator is a way to wrap a story in extra “rendering” functionality. Many addons define decorators 3 | * in order to augment stories: 4 | * - with extra rendering 5 | * - gather details about how a story is rendered 6 | * 7 | * When writing stories, decorators are typically used to wrap stories with extra markup or context mocking. 8 | * 9 | * https://storybook.js.org/docs/react/writing-stories/decorators 10 | */ 11 | import type { ProjectAnnotations, Renderer } from 'storybook/internal/types'; 12 | 13 | import { KEY } from './constants'; 14 | import { withGlobals } from './withGlobals'; 15 | import { withRoundTrip } from './withRoundTrip'; 16 | 17 | /** 18 | * Note: if you want to use JSX in this file, rename it to `preview.tsx` 19 | * and update the entry prop in tsup.config.ts to use "src/preview.tsx", 20 | */ 21 | 22 | const preview: ProjectAnnotations = { 23 | decorators: [withGlobals, withRoundTrip], 24 | initialGlobals: { 25 | [KEY]: false, 26 | }, 27 | }; 28 | 29 | export default preview; 30 | -------------------------------------------------------------------------------- /src/stories/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './button.css'; 3 | 4 | interface ButtonProps { 5 | /** 6 | * Is this the principal call to action on the page? 7 | */ 8 | primary?: boolean; 9 | /** 10 | * What background color to use 11 | */ 12 | backgroundColor?: string; 13 | /** 14 | * How large should the button be? 15 | */ 16 | size?: 'small' | 'medium' | 'large'; 17 | /** 18 | * Button contents 19 | */ 20 | label: string; 21 | /** 22 | * Optional click handler 23 | */ 24 | onClick?: () => void; 25 | } 26 | 27 | /** 28 | * Primary UI component for user interaction 29 | */ 30 | export const Button = ({ primary = false, size = 'medium', backgroundColor, label, ...props }: ButtonProps) => { 31 | const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary'; 32 | return ( 33 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Storybook contributors 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 | -------------------------------------------------------------------------------- /src/components/Tool.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useCallback, useEffect } from 'react'; 2 | import { useGlobals, type API } from 'storybook/manager-api'; 3 | import { IconButton } from 'storybook/internal/components'; 4 | import { ADDON_ID, KEY, TOOL_ID } from '../constants'; 5 | import { LightningIcon } from '@storybook/icons'; 6 | 7 | export const Tool = memo(function MyAddonSelector({ api }: { api: API }) { 8 | const [globals, updateGlobals, storyGlobals] = useGlobals(); 9 | 10 | const isLocked = KEY in storyGlobals; 11 | const isActive = !!globals[KEY]; 12 | 13 | const toggle = useCallback(() => { 14 | updateGlobals({ 15 | [KEY]: !isActive, 16 | }); 17 | }, [isActive]); 18 | 19 | useEffect(() => { 20 | api.setAddonShortcut(ADDON_ID, { 21 | label: 'Toggle Measure [O]', 22 | defaultShortcut: ['O'], 23 | actionName: 'outline', 24 | showInMenu: false, 25 | action: toggle, 26 | }); 27 | }, [toggle, api]); 28 | 29 | return ( 30 | 31 | 32 | 33 | ); 34 | }); 35 | -------------------------------------------------------------------------------- /src/manager.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { addons, types } from 'storybook/manager-api'; 3 | 4 | import { Panel } from './components/Panel'; 5 | import { Tab } from './components/Tab'; 6 | import { Tool } from './components/Tool'; 7 | import { ADDON_ID, PANEL_ID, TAB_ID, TOOL_ID } from './constants'; 8 | 9 | /** 10 | * Note: if you want to use JSX in this file, rename it to `manager.tsx` 11 | * and update the entry prop in tsup.config.ts to use "src/manager.tsx", 12 | */ 13 | 14 | // Register the addon 15 | addons.register(ADDON_ID, (api) => { 16 | // Register a tool 17 | addons.add(TOOL_ID, { 18 | type: types.TOOL, 19 | title: 'My addon', 20 | match: ({ viewMode, tabId }) => !!((viewMode && viewMode.match(/^(story)$/)) || tabId === TAB_ID), 21 | render: () => , 22 | }); 23 | 24 | // Register a panel 25 | addons.add(PANEL_ID, { 26 | type: types.PANEL, 27 | title: 'My addon', 28 | match: ({ viewMode }) => viewMode === 'story', 29 | render: ({ active }) => , 30 | }); 31 | 32 | // Register a tab 33 | addons.add(TAB_ID, { 34 | type: types.TAB, 35 | title: 'My addon', 36 | render: ({ active }) => , 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/stories/assets/direction.svg: -------------------------------------------------------------------------------- 1 | illustration/direction -------------------------------------------------------------------------------- /src/stories/assets/flow.svg: -------------------------------------------------------------------------------- 1 | illustration/flow -------------------------------------------------------------------------------- /src/stories/page.css: -------------------------------------------------------------------------------- 1 | section { 2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | font-size: 14px; 4 | line-height: 24px; 5 | padding: 48px 20px; 6 | margin: 0 auto; 7 | max-width: 600px; 8 | color: #333; 9 | } 10 | 11 | section h2 { 12 | font-weight: 700; 13 | font-size: 32px; 14 | line-height: 1; 15 | margin: 0 0 4px; 16 | display: inline-block; 17 | vertical-align: top; 18 | } 19 | 20 | section p { 21 | margin: 1em 0; 22 | } 23 | 24 | section a { 25 | text-decoration: none; 26 | color: #1ea7fd; 27 | } 28 | 29 | section ul { 30 | padding-left: 30px; 31 | margin: 1em 0; 32 | } 33 | 34 | section li { 35 | margin-bottom: 8px; 36 | } 37 | 38 | section .tip { 39 | display: inline-block; 40 | border-radius: 1em; 41 | font-size: 11px; 42 | line-height: 12px; 43 | font-weight: 700; 44 | background: #e7fdd8; 45 | color: #66bf3c; 46 | padding: 4px 12px; 47 | margin-right: 10px; 48 | vertical-align: top; 49 | } 50 | 51 | section .tip-wrapper { 52 | font-size: 13px; 53 | line-height: 20px; 54 | margin-top: 40px; 55 | margin-bottom: 40px; 56 | } 57 | 58 | section .tip-wrapper svg { 59 | display: inline-block; 60 | height: 12px; 61 | width: 12px; 62 | margin-right: 4px; 63 | vertical-align: top; 64 | margin-top: 3px; 65 | } 66 | 67 | section .tip-wrapper svg path { 68 | fill: #1ea7fd; 69 | } 70 | -------------------------------------------------------------------------------- /src/withRoundTrip.ts: -------------------------------------------------------------------------------- 1 | import { Result } from 'src/types'; 2 | import { useEffect, useChannel } from 'storybook/preview-api'; 3 | import type { DecoratorFunction } from 'storybook/internal/types'; 4 | 5 | import { EVENTS } from './constants'; 6 | 7 | /** 8 | * This is an example of a function that performs some sort of analysis on the 9 | * canvas. In this example, it returns the bounding rectangles for elements that 10 | * - have a style attribute 11 | * - are divs with fewer than 2 childNodes 12 | */ 13 | const check = (canvas: ParentNode = globalThis.document): Result => { 14 | const divs = canvas.querySelectorAll('div'); 15 | const all = canvas.querySelectorAll('*'); 16 | 17 | return { 18 | divs: Array.from(divs) 19 | .filter((element) => element.childNodes.length < 2) 20 | .map((div) => div.getBoundingClientRect()), 21 | styled: Array.from(all) 22 | .filter((element) => element.hasAttribute('style')) 23 | .map((element) => element.getBoundingClientRect()), 24 | }; 25 | }; 26 | 27 | export const withRoundTrip: DecoratorFunction = (storyFn, context) => { 28 | const canvasElement = context.canvasElement as ParentNode; 29 | const emit = useChannel({ 30 | [EVENTS.REQUEST]: () => { 31 | emit(EVENTS.RESULT, check(canvasElement)); 32 | }, 33 | }); 34 | useEffect(() => { 35 | emit(EVENTS.RESULT, check(canvasElement)); 36 | }); 37 | 38 | return storyFn(); 39 | }; 40 | -------------------------------------------------------------------------------- /src/stories/assets/code-brackets.svg: -------------------------------------------------------------------------------- 1 | illustration/code-brackets -------------------------------------------------------------------------------- /src/stories/assets/comments.svg: -------------------------------------------------------------------------------- 1 | illustration/comments -------------------------------------------------------------------------------- /src/stories/Button.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react-vite'; 2 | 3 | import { Button } from './Button'; 4 | import { fn } from 'storybook/test'; 5 | 6 | // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 7 | const meta: Meta = { 8 | title: 'Example/Button', 9 | component: Button, 10 | // More on argTypes: https://storybook.js.org/docs/react/api/argtypes 11 | argTypes: { 12 | backgroundColor: { control: 'color' }, 13 | }, 14 | args: { 15 | onClick: fn(), 16 | }, 17 | tags: ['autodocs'], 18 | parameters: { 19 | myAddonParameter: ` 20 | 21 | a.id} /> 22 | 23 | `, 24 | }, 25 | }; 26 | 27 | export default meta; 28 | type Story = StoryObj; 29 | 30 | // More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args 31 | export const Primary: Story = { 32 | // More on args: https://storybook.js.org/docs/react/writing-stories/args 33 | args: { 34 | primary: true, 35 | label: 'Button', 36 | }, 37 | }; 38 | 39 | export const Secondary: Story = { 40 | args: { 41 | label: 'Button', 42 | }, 43 | }; 44 | 45 | export const Large: Story = { 46 | args: { 47 | size: 'large', 48 | label: 'Button', 49 | }, 50 | }; 51 | 52 | export const Small: Story = { 53 | args: { 54 | size: 'small', 55 | label: 'Button', 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /src/stories/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Button } from './Button'; 4 | import './header.css'; 5 | 6 | type User = { 7 | name: string; 8 | }; 9 | 10 | interface HeaderProps { 11 | user?: User; 12 | onLogin: () => void; 13 | onLogout: () => void; 14 | onCreateAccount: () => void; 15 | } 16 | 17 | export const Header = ({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) => ( 18 |
19 |
20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |

Acme

29 |
30 |
31 | {user ? ( 32 | <> 33 | 34 | Welcome, {user.name}! 35 | 36 |
45 |
46 |
47 | ); 48 | -------------------------------------------------------------------------------- /scripts/eject-typescript.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | 3 | // Copy TS files and delete src 4 | await $`cp -r ./src ./srcTS`; 5 | await $`rm -rf ./src`; 6 | await $`mkdir ./src`; 7 | 8 | // Install Babel and TS preset 9 | console.log(chalk.green` 10 | 11 | 🔃 Installing dependencies... 12 | 13 | `); 14 | await $`npm install --save-dev @babel/cli @babel/preset-typescript --ignore-scripts`; 15 | 16 | // Convert TS code to JS 17 | await $`babel --no-babelrc --presets @babel/preset-typescript ./srcTS -d ./src --extensions \".js,.jsx,.ts,.tsx\" --ignore "./srcTS/typings.d.ts"`; 18 | 19 | // Format the newly created .js files 20 | console.log(chalk.green` 21 | 22 | 💅 Format the newly created .js files... 23 | 24 | `); 25 | await $`prettier --write ./src`; 26 | 27 | // Add in minimal files required for the TS build setup 28 | console.log(chalk.green` 29 | 30 | ➕ Add minimal files required for the TS build setup 31 | 32 | `); 33 | await $`prettier --write ./src`; 34 | await $`touch ./src/dummy.ts`; 35 | await $`printf "export {};" >> ./src/dummy.ts`; 36 | 37 | await $`touch ./src/typings.d.ts`; 38 | await $`printf 'declare module "global";' >> ./src/typings.d.ts`; 39 | 40 | // Clean up 41 | await $`rm -rf ./srcTS`; 42 | console.log(chalk.green` 43 | 44 | 🧹 Clean up... 45 | 46 | `); 47 | await $`npm uninstall @babel/cli @babel/preset-typescript --ignore-scripts`; 48 | 49 | console.log( 50 | chalk.green.bold` 51 | TypeScript Ejection complete!`, 52 | chalk.green` 53 | Addon code converted with JS. The TypeScript build setup is still available in case you want to adopt TypeScript in the future. 54 | `, 55 | ); 56 | -------------------------------------------------------------------------------- /src/stories/assets/repo.svg: -------------------------------------------------------------------------------- 1 | illustration/repo -------------------------------------------------------------------------------- /src/withGlobals.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useGlobals } from 'storybook/preview-api'; 2 | import type { Renderer, StoryContext, PartialStoryFn as StoryFunction } from 'storybook/internal/types'; 3 | 4 | import { KEY } from './constants'; 5 | 6 | export const withGlobals = (StoryFn: StoryFunction, context: StoryContext) => { 7 | const [globals] = useGlobals(); 8 | const myAddon = globals[KEY]; 9 | const canvas = context.canvasElement as ParentNode; 10 | 11 | // Is the addon being used in the docs panel 12 | const isInDocs = context.viewMode === 'docs'; 13 | 14 | useEffect(() => { 15 | if (!isInDocs) { 16 | addExtraContentToStory(canvas, { 17 | myAddon, 18 | }); 19 | } 20 | }, [myAddon, isInDocs]); 21 | 22 | return StoryFn(); 23 | }; 24 | 25 | /** 26 | * It's not really recommended to inject content into the canvas like this. 27 | * But there are use cases 28 | */ 29 | function addExtraContentToStory(canvas: ParentNode, state: object) { 30 | const preElement = canvas.querySelector(`[data-id="${KEY}"]`) || canvas.appendChild(document.createElement('pre')); 31 | 32 | preElement.setAttribute('data-id', KEY); 33 | preElement.setAttribute( 34 | 'style', 35 | ` 36 | margin-top: 1rem; 37 | padding: 1rem; 38 | background-color: #eee; 39 | border-radius: 3px; 40 | overflow: scroll; 41 | `, 42 | ); 43 | 44 | preElement.innerHTML = `This snippet is injected by the withGlobals decorator. 45 | It updates as the user interacts with the ⚡ or Theme tools in the toolbar above. 46 | 47 | ${JSON.stringify(state, null, 2)} 48 | `; 49 | } 50 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | if: "!contains(github.event.head_commit.message, 'ci skip') && !contains(github.event.head_commit.message, 'skip ci')" 9 | strategy: 10 | matrix: 11 | node-version: [20.19, 22.12] 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Install pnpm 19 | uses: pnpm/action-setup@v4 20 | 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | cache: 'pnpm' 26 | 27 | - name: Install dependencies 28 | run: pnpm install 29 | 30 | - name: Check for linter errors 31 | run: pnpm lint 32 | 33 | build: 34 | runs-on: ubuntu-latest 35 | if: "!contains(github.event.head_commit.message, 'ci skip') && !contains(github.event.head_commit.message, 'skip ci')" 36 | strategy: 37 | matrix: 38 | node-version: [20.19, 22.12] 39 | steps: 40 | - name: Checkout 41 | uses: actions/checkout@v4 42 | with: 43 | fetch-depth: 0 44 | 45 | - name: Install pnpm 46 | uses: pnpm/action-setup@v4 47 | 48 | - name: Use Node.js ${{ matrix.node-version }} 49 | uses: actions/setup-node@v4 50 | with: 51 | node-version: ${{ matrix.node-version }} 52 | cache: 'pnpm' 53 | 54 | - name: Install dependencies 55 | run: pnpm install 56 | 57 | - name: Build addon 58 | run: pnpm build 59 | 60 | - name: Build Storybook 61 | run: pnpm build-storybook 62 | -------------------------------------------------------------------------------- /src/stories/assets/plugin.svg: -------------------------------------------------------------------------------- 1 | illustration/plugin -------------------------------------------------------------------------------- /src/components/Tab.tsx: -------------------------------------------------------------------------------- 1 | import { LightningIcon } from '@storybook/icons'; 2 | import React, { useCallback } from 'react'; 3 | import { Code, H1, IconButton, Link } from 'storybook/internal/components'; 4 | import { useGlobals, useParameter } from 'storybook/manager-api'; 5 | import { styled } from 'storybook/theming'; 6 | 7 | import { KEY } from '../constants'; 8 | 9 | interface TabProps { 10 | active?: boolean; 11 | } 12 | 13 | const TabWrapper = styled.div(({ theme }) => ({ 14 | background: theme.background.content, 15 | padding: '4rem 20px', 16 | minHeight: '100vh', 17 | boxSizing: 'border-box', 18 | })); 19 | 20 | const TabInner = styled.div({ 21 | maxWidth: 768, 22 | marginLeft: 'auto', 23 | marginRight: 'auto', 24 | }); 25 | 26 | export const Tab: React.FC = ({ active }) => { 27 | // https://storybook.js.org/docs/react/addons/addons-api#useparameter 28 | const config = useParameter(KEY, 'fallback value of config from parameter'); 29 | 30 | // https://storybook.js.org/docs/addons/addons-api#useglobals 31 | const [globals, updateGlobals] = useGlobals(); 32 | const value = globals[KEY]; 33 | 34 | const update = useCallback((newValue: typeof value) => { 35 | updateGlobals({ 36 | [KEY]: newValue, 37 | }); 38 | }, []); 39 | 40 | if (!active) { 41 | return null; 42 | } 43 | 44 | return ( 45 | 46 | 47 |

My Addon ({KEY})

48 |

Your addon can create a custom tab in Storybook.

49 |

50 | You have full control over what content is being rendered here. You can use components from{' '} 51 | 52 | storybook/internal/components 53 | {' '} 54 | to match the look and feel of Storybook, for example the <Code /> component below. Or build 55 | a completely custom UI. 56 |

57 | {config} 58 |

59 | You can also have interactive UI here, like a button that updates a global:{' '} 60 | { 63 | update(!value); 64 | }} 65 | > 66 | 67 | 68 |

69 |
70 |
71 | ); 72 | }; 73 | -------------------------------------------------------------------------------- /src/stories/assets/stackalt.svg: -------------------------------------------------------------------------------- 1 | illustration/stackalt -------------------------------------------------------------------------------- /src/components/List.tsx: -------------------------------------------------------------------------------- 1 | import { ArrowDownIcon } from '@storybook/icons'; 2 | import React, { Fragment, useState } from 'react'; 3 | import { styled } from 'storybook/theming'; 4 | 5 | type Item = { 6 | title: string; 7 | description: string; 8 | }; 9 | 10 | interface ListItemProps { 11 | item: Item; 12 | } 13 | 14 | interface ListProps { 15 | items: Item[]; 16 | } 17 | 18 | const ListWrapper = styled.ul({ 19 | listStyle: 'none', 20 | fontSize: 14, 21 | padding: 0, 22 | margin: 0, 23 | }); 24 | 25 | const Wrapper = styled.div(({ theme }) => ({ 26 | display: 'flex', 27 | width: '100%', 28 | borderBottom: `1px solid ${theme.appBorderColor}`, 29 | '&:hover': { 30 | background: theme.background.hoverable, 31 | }, 32 | })); 33 | 34 | const Icon = styled(ArrowDownIcon)(({ theme }) => ({ 35 | height: 10, 36 | width: 10, 37 | minWidth: 10, 38 | color: theme.color.mediumdark, 39 | marginRight: 10, 40 | transition: 'transform 0.1s ease-in-out', 41 | alignSelf: 'center', 42 | display: 'inline-flex', 43 | })); 44 | 45 | const HeaderBar = styled.div(({ theme }) => ({ 46 | padding: theme.layoutMargin, 47 | paddingLeft: theme.layoutMargin - 3, 48 | background: 'none', 49 | color: 'inherit', 50 | textAlign: 'left', 51 | cursor: 'pointer', 52 | borderLeft: '3px solid transparent', 53 | width: '100%', 54 | 55 | '&:focus': { 56 | outline: '0 none', 57 | borderLeft: `3px solid ${theme.color.secondary}`, 58 | }, 59 | })); 60 | 61 | const Description = styled.div(({ theme }) => ({ 62 | padding: theme.layoutMargin, 63 | background: theme.background.content, 64 | fontFamily: theme.typography.fonts.mono, 65 | whiteSpace: 'pre-wrap', 66 | textAlign: 'left', 67 | })); 68 | 69 | export const ListItem: React.FC = ({ item }) => { 70 | const [open, onToggle] = useState(false); 71 | 72 | return ( 73 | 74 | 75 | onToggle(!open)} role="button"> 76 | 81 | {item.title} 82 | 83 | 84 | {open ? {item.description} : null} 85 | 86 | ); 87 | }; 88 | 89 | export const List: React.FC = ({ items }) => ( 90 | 91 | {items.map((item, idx) => ( 92 | 93 | ))} 94 | 95 | ); 96 | -------------------------------------------------------------------------------- /src/stories/Page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Header } from './Header'; 4 | import './page.css'; 5 | 6 | type User = { 7 | name: string; 8 | }; 9 | 10 | export const Page: React.FC = () => { 11 | const [user, setUser] = React.useState(); 12 | 13 | return ( 14 |
15 |
setUser({ name: 'Jane Doe' })} 18 | onLogout={() => setUser(undefined)} 19 | onCreateAccount={() => setUser({ name: 'Jane Doe' })} 20 | /> 21 | 22 |
23 |

Pages in Storybook

24 |

25 | We recommend building UIs with a{' '} 26 | 27 | component-driven 28 | {' '} 29 | process starting with atomic components and ending with pages. 30 |

31 |

32 | Render pages with mock data. This makes it easy to build and review page states without needing to navigate to 33 | them in your app. Here are some handy patterns for managing page data in Storybook: 34 |

35 |
    36 |
  • 37 | Use a higher-level connected component. Storybook helps you compose such data from the "args" of 38 | child component stories 39 |
  • 40 |
  • 41 | Assemble data in the page component from your services. You can mock these services out using Storybook. 42 |
  • 43 |
44 |

45 | Get a guided tutorial on component-driven development at{' '} 46 | 47 | Storybook tutorials 48 | 49 | . Read more in the{' '} 50 | 51 | docs 52 | 53 | . 54 |

55 |
56 | Tip Adjust the width of the canvas with the{' '} 57 | 58 | 59 | 64 | 65 | 66 | Viewports addon in the toolbar 67 |
68 |
69 |
70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /scripts/prepublish-checks.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | 3 | import boxen from 'boxen'; 4 | import { dedent } from 'ts-dedent'; 5 | import { readFile } from 'node:fs/promises'; 6 | import { globalPackages as globalManagerPackages } from 'storybook/internal/manager/globals'; 7 | import { globalPackages as globalPreviewPackages } from 'storybook/internal/preview/globals'; 8 | 9 | const packageJson = await readFile('./package.json', 'utf8').then(JSON.parse); 10 | 11 | const name = packageJson.name; 12 | const displayName = packageJson.storybook.displayName; 13 | 14 | let exitCode = 0; 15 | $.verbose = false; 16 | 17 | /** 18 | * Check that meta data has been updated 19 | */ 20 | if (name.includes('addon-kit') || displayName.includes('Addon Kit')) { 21 | console.error( 22 | boxen( 23 | dedent` 24 | ${chalk.red.bold('Missing metadata')} 25 | 26 | ${chalk.red(dedent`Your package name and/or displayName includes default values from the Addon Kit. 27 | The addon gallery filters out all such addons. 28 | 29 | Please configure appropriate metadata before publishing your addon. For more info, see: 30 | https://storybook.js.org/docs/react/addons/addon-catalog#addon-metadata`)}`, 31 | { padding: 1, borderColor: 'red' }, 32 | ), 33 | ); 34 | 35 | exitCode = 1; 36 | } 37 | 38 | /** 39 | * Check that README has been updated 40 | */ 41 | const readmeTestStrings = 42 | '# 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'; 43 | 44 | if ((await $`cat README.md | grep -E ${readmeTestStrings}`.exitCode) == 0) { 45 | console.error( 46 | boxen( 47 | dedent` 48 | ${chalk.red.bold('README not updated')} 49 | 50 | ${chalk.red(dedent`You are using the default README.md file that comes with the addon kit. 51 | Please update it to provide info on what your addon does and how to use it.`)} 52 | `, 53 | { padding: 1, borderColor: 'red' }, 54 | ), 55 | ); 56 | 57 | exitCode = 1; 58 | } 59 | 60 | /** 61 | * Check that globalized packages are not incorrectly listed as peer dependencies 62 | */ 63 | const peerDependencies = Object.keys(packageJson.peerDependencies || {}); 64 | const globalPackages = [...globalManagerPackages, ...globalPreviewPackages]; 65 | peerDependencies.forEach((dependency) => { 66 | if (globalPackages.includes(dependency)) { 67 | console.error( 68 | boxen( 69 | dedent` 70 | ${chalk.red.bold('Unnecessary peer dependency')} 71 | 72 | ${chalk.red(dedent`You have a peer dependency on ${chalk.bold(dependency)} which is most likely unnecessary 73 | as that is provided by Storybook directly. 74 | Check the "bundling" section in README.md for more information. 75 | If you are absolutely sure you are doing it correct, you should remove this check from scripts/prepublish-checks.js.`)} 76 | `, 77 | { padding: 1, borderColor: 'red' }, 78 | ), 79 | ); 80 | 81 | exitCode = 1; 82 | } 83 | }); 84 | 85 | process.exit(exitCode); 86 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, type Options } from 'tsup'; 2 | 3 | const NODE_TARGET = 'node20.19'; // Minimum Node version supported by Storybook 10 4 | 5 | export default defineConfig(async () => { 6 | // reading the three types of entries from package.json, which has the following structure: 7 | // { 8 | // ... 9 | // "bundler": { 10 | // "managerEntries": ["./src/manager.ts"], 11 | // "previewEntries": ["./src/preview.ts", "./src/index.ts"] 12 | // "nodeEntries": ["./src/preset.ts"] 13 | // } 14 | // } 15 | const packageJson = (await import('./package.json', { with: { type: 'json' } })).default; 16 | 17 | const { 18 | bundler: { managerEntries = [], previewEntries = [], nodeEntries = [] }, 19 | } = packageJson; 20 | 21 | const commonConfig: Options = { 22 | /* 23 | keep this line commented until https://github.com/egoist/tsup/issues/1270 is resolved 24 | clean: options.watch ? false : true, 25 | */ 26 | clean: false, 27 | format: ['esm'], 28 | treeshake: true, 29 | splitting: true, 30 | /* 31 | The following packages are provided by Storybook and should always be externalized 32 | Meaning they shouldn't be bundled with the addon, and they shouldn't be regular dependencies either 33 | */ 34 | external: ['react', 'react-dom', '@storybook/icons'], 35 | }; 36 | 37 | const configs: Options[] = []; 38 | 39 | /* 40 | manager entries are entries meant to be loaded into the manager UI 41 | they'll have manager-specific packages externalized and they won't be usable in node 42 | they won't have types generated for them as they're usually loaded automatically by Storybook 43 | */ 44 | if (managerEntries.length) { 45 | configs.push({ 46 | ...commonConfig, 47 | entry: managerEntries, 48 | platform: 'browser', 49 | target: 'esnext', // we can use esnext for manager entries since Storybook will bundle the addon's manager entries again anyway 50 | }); 51 | } 52 | 53 | /* 54 | preview entries are entries meant to be loaded into the preview iframe 55 | they'll have preview-specific packages externalized and they won't be usable in node 56 | they'll have types generated for them so they can be imported by users when setting up Portable Stories or using CSF factories 57 | */ 58 | if (previewEntries.length) { 59 | configs.push({ 60 | ...commonConfig, 61 | entry: previewEntries, 62 | platform: 'browser', 63 | target: 'esnext', // we can use esnext for preview entries since the builders will bundle the addon's preview entries again anyway 64 | dts: true, 65 | }); 66 | } 67 | 68 | /* 69 | node entries are entries meant to be used in node-only 70 | this is useful for presets, which are loaded by Storybook when setting up configurations 71 | they won't have types generated for them as they're usually loaded automatically by Storybook 72 | */ 73 | if (nodeEntries.length) { 74 | configs.push({ 75 | ...commonConfig, 76 | entry: nodeEntries, 77 | platform: 'node', 78 | target: NODE_TARGET, 79 | }); 80 | } 81 | 82 | return configs; 83 | }); 84 | -------------------------------------------------------------------------------- /src/components/Panel.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, memo, useCallback, useState } from 'react'; 2 | import type { Result } from 'src/types'; 3 | import { AddonPanel } from 'storybook/internal/components'; 4 | import { Button, Placeholder, TabsState } from 'storybook/internal/components'; 5 | import { useChannel } from 'storybook/manager-api'; 6 | import { styled, useTheme } from 'storybook/theming'; 7 | 8 | import { EVENTS } from '../constants'; 9 | import { List } from './List'; 10 | 11 | interface PanelProps { 12 | active?: boolean; 13 | } 14 | 15 | export const RequestDataButton = styled(Button)({ 16 | marginTop: '1rem', 17 | }); 18 | 19 | export const Panel: React.FC = memo(function MyPanel(props: PanelProps) { 20 | const theme = useTheme(); 21 | 22 | // https://storybook.js.org/docs/react/addons/addons-api#useaddonstate 23 | const [{ divs, styled }, setState] = useState({ 24 | divs: [], 25 | styled: [], 26 | }); 27 | 28 | // https://storybook.js.org/docs/react/addons/addons-api#usechannel 29 | const emit = useChannel({ 30 | [EVENTS.RESULT]: (newResults) => { 31 | setState(newResults); 32 | }, 33 | }); 34 | 35 | const fetchData = useCallback(() => { 36 | emit(EVENTS.REQUEST); 37 | }, [emit]); 38 | 39 | return ( 40 | 41 | 42 |
43 | 44 | 45 | Addons can gather details about how a story is rendered. This is panel uses a tab pattern. Click the 46 | button below to fetch data for the other two tabs. 47 | 48 | 49 | Request data 50 | 51 | 52 |
53 |
54 | {divs.length > 0 ? ( 55 | 56 |

The following divs have less than 2 childNodes

57 | ({ 59 | title: `item #${index}`, 60 | description: JSON.stringify(item, null, 2), 61 | }))} 62 | /> 63 |
64 | ) : ( 65 | 66 |

No divs found

67 |
68 | )} 69 |
70 |
71 | {styled.length > 0 ? ( 72 | 73 |

The following elements have a style attribute

74 | ({ 76 | title: `item #${index}`, 77 | description: JSON.stringify(item, null, 2), 78 | }))} 79 | /> 80 |
81 | ) : ( 82 | 83 |

No styled elements found

84 |
85 | )} 86 |
87 |
88 |
89 | ); 90 | }); 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storybook-addon-kit", 3 | "version": "0.0.0", 4 | "description": "everything you need to build a Storybook addon", 5 | "keywords": [ 6 | "storybook-addon" 7 | ], 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/storybookjs/storybook-addon-kit" 11 | }, 12 | "license": "MIT", 13 | "author": "package-author", 14 | "type": "module", 15 | "exports": { 16 | ".": { 17 | "types": "./dist/index.d.ts", 18 | "default": "./dist/index.js" 19 | }, 20 | "./preview": { 21 | "types": "./dist/preview.d.ts", 22 | "default": "./dist/preview.js" 23 | }, 24 | "./preset": "./dist/preset.js", 25 | "./manager": "./dist/manager.js", 26 | "./manager-helpers": "./dist/manager-helpers.js", 27 | "./package.json": "./package.json" 28 | }, 29 | "files": [ 30 | "dist/**/*", 31 | "README.md", 32 | "*.js", 33 | "*.d.ts" 34 | ], 35 | "scripts": { 36 | "prebuild": "node -e \"fs.rmSync('./dist', { recursive: true, force: true })\"", 37 | "build": "tsup", 38 | "build-storybook": "storybook build", 39 | "build:watch": "npm run build -- --watch", 40 | "eject-ts": "zx scripts/eject-typescript.js", 41 | "postinstall": "node scripts/welcome.js", 42 | "format": "prettier --write .", 43 | "lint": "eslint --cache .", 44 | "lint:fix": "pnpm lint --fix", 45 | "prerelease": "zx scripts/prepublish-checks.js", 46 | "release": "npm run build && auto shipit", 47 | "start": "run-p build:watch \"storybook --quiet\"", 48 | "storybook": "storybook dev -p 6006", 49 | "test": "echo \"Error: no test specified\" && exit 1" 50 | }, 51 | "devDependencies": { 52 | "@eslint/js": "^9.36.0", 53 | "@storybook/addon-docs": "next", 54 | "@storybook/icons": "^2.0.0", 55 | "@storybook/react-vite": "next", 56 | "@types/node": "^20", 57 | "@types/react": "^18.2.65", 58 | "@types/react-dom": "^18.2.21", 59 | "@vitejs/plugin-react": "^4.7.0", 60 | "auto": "^11.3.0", 61 | "boxen": "^8.0.1", 62 | "eslint": "^9.36.0", 63 | "eslint-config-prettier": "^10.1.8", 64 | "eslint-plugin-prettier": "^5.5.4", 65 | "eslint-plugin-react": "^7.37.5", 66 | "eslint-plugin-storybook": "next", 67 | "npm-run-all2": "^8.0.4", 68 | "prettier": "^3.6.2", 69 | "prompts": "^2.4.2", 70 | "react": "^18.2.0", 71 | "react-dom": "^18.2.0", 72 | "storybook": "next", 73 | "ts-dedent": "^2.2.0", 74 | "tsup": "^8.5.0", 75 | "typescript": "^5.8.3", 76 | "typescript-eslint": "^8.44.1", 77 | "vite": "^7.0.5", 78 | "zx": "^8.7.1" 79 | }, 80 | "peerDependencies": { 81 | "storybook": "*" 82 | }, 83 | "publishConfig": { 84 | "access": "public" 85 | }, 86 | "bundler": { 87 | "managerEntries": [ 88 | "src/manager.tsx", 89 | "src/manager-helpers.tsx" 90 | ], 91 | "previewEntries": [ 92 | "src/preview.ts", 93 | "src/index.ts" 94 | ], 95 | "nodeEntries": [ 96 | "src/preset.ts" 97 | ] 98 | }, 99 | "storybook": { 100 | "displayName": "Addon Kit", 101 | "supportedFrameworks": [ 102 | "supported-frameworks" 103 | ], 104 | "icon": "https://user-images.githubusercontent.com/321738/63501763-88dbf600-c4cc-11e9-96cd-94adadc2fd72.png" 105 | }, 106 | "packageManager": "pnpm@10.22.0+sha512.bf049efe995b28f527fd2b41ae0474ce29186f7edcb3bf545087bd61fbbebb2bf75362d1307fda09c2d288e1e499787ac12d4fcb617a974718a6051f2eee741c" 107 | } 108 | -------------------------------------------------------------------------------- /scripts/welcome.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable eslint-comments/disable-enable-pair */ 2 | /* eslint-disable no-console */ 3 | import prompts from 'prompts'; 4 | import { dedent } from 'ts-dedent'; 5 | import { dirname, resolve } from 'path'; 6 | import { readFile, writeFile } from 'fs/promises'; 7 | import { execSync } from 'child_process'; 8 | import { fileURLToPath } from 'url'; 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = dirname(__filename); 12 | 13 | // CLI questions 14 | const questions = [ 15 | { 16 | type: 'text', 17 | name: 'authorName', 18 | initial: '', 19 | message: 'What is the package author name?*', 20 | validate: (name) => (name === '' ? "Name can't be empty" : true), 21 | }, 22 | { 23 | type: 'text', 24 | name: 'authorEmail', 25 | initial: '', 26 | message: 'What is the package author email?', 27 | }, 28 | { 29 | type: 'text', 30 | name: 'packageName', 31 | message: 'What is the addon package name (eg: storybook-addon-something)?*', 32 | validate: (name) => (name === '' ? "Package name can't be empty" : true), 33 | }, 34 | { 35 | type: 'text', 36 | name: 'displayName', 37 | message: 'What is the addon display name (this will be used in the addon catalog)?*', 38 | validate: (name) => 39 | name === '' 40 | ? "Display name can't be empty. For more info, see: https://storybook.js.org/docs/react/addons/addon-catalog#addon-metadata" 41 | : true, 42 | }, 43 | { 44 | type: 'text', 45 | name: 'addonDescription', 46 | initial: '', 47 | message: 'Write a short description of the addon*', 48 | validate: (name) => (name === '' ? "Description can't be empty" : true), 49 | }, 50 | { 51 | type: 'text', 52 | name: 'repoUrl', 53 | message: 'Git repo URL for your addon package (https://github.com/...)*', 54 | validate: (url) => (url === '' ? "URL can't be empty" : true), 55 | }, 56 | { 57 | type: 'text', 58 | name: 'addonIcon', 59 | initial: 'https://user-images.githubusercontent.com/321738/63501763-88dbf600-c4cc-11e9-96cd-94adadc2fd72.png', 60 | message: 'URL of your addon icon', 61 | }, 62 | { 63 | type: 'list', 64 | name: 'keywords', 65 | initial: 'storybook-addons', 66 | message: 'Enter addon keywords (comma separated)', 67 | separator: ',', 68 | format: (keywords) => 69 | keywords 70 | .concat(['storybook-addons']) 71 | .map((k) => `"${k}"`) 72 | .join(', '), 73 | }, 74 | { 75 | type: 'list', 76 | name: 'supportedFrameworks', 77 | initial: 'react, vue, angular, web-components, ember, html, svelte, preact, react-native', 78 | message: 'List of frameworks you support (comma separated)?', 79 | separator: ',', 80 | format: (frameworks) => frameworks.map((k) => `"${k}"`).join(', '), 81 | }, 82 | ]; 83 | 84 | const REPLACE_TEMPLATES = { 85 | packageName: 'storybook-addon-kit', 86 | addonDescription: 'everything you need to build a Storybook addon', 87 | packageAuthor: 'package-author', 88 | repoUrl: 'https://github.com/storybookjs/storybook-addon-kit', 89 | keywords: `"storybook-addons"`, 90 | displayName: 'Addon Kit', 91 | supportedFrameworks: `"supported-frameworks"`, 92 | }; 93 | 94 | const bold = (message) => `\u001b[1m${message}\u001b[22m`; 95 | const magenta = (message) => `\u001b[35m${message}\u001b[39m`; 96 | const blue = (message) => `\u001b[34m${message}\u001b[39m`; 97 | 98 | const main = async () => { 99 | console.log( 100 | bold( 101 | magenta( 102 | dedent` 103 | Welcome to Storybook addon-kit! 104 | Please answer the following questions while we prepare this project for you:\n 105 | `, 106 | ), 107 | ), 108 | ); 109 | 110 | const { 111 | authorName, 112 | authorEmail, 113 | packageName, 114 | addonDescription, 115 | repoUrl, 116 | displayName, 117 | keywords, 118 | supportedFrameworks, 119 | } = await prompts(questions); 120 | 121 | if (!authorName || !packageName) { 122 | console.log( 123 | `\nProcess canceled by the user. Feel free to run ${bold( 124 | 'npm run postinstall', 125 | )} to execute the installation steps again!`, 126 | ); 127 | process.exit(0); 128 | } 129 | 130 | const authorField = authorName + (authorEmail ? ` <${authorEmail}>` : ''); 131 | 132 | const packageJson = resolve(__dirname, `../package.json`); 133 | 134 | console.log(`\n👷 Updating package.json...`); 135 | let packageJsonContents = await readFile(packageJson, 'utf-8'); 136 | 137 | packageJsonContents = packageJsonContents 138 | .replace(REPLACE_TEMPLATES.packageName, packageName) 139 | .replace(REPLACE_TEMPLATES.addonDescription, addonDescription) 140 | .replace(REPLACE_TEMPLATES.packageAuthor, authorField) 141 | .replace(REPLACE_TEMPLATES.keywords, keywords) 142 | .replace(REPLACE_TEMPLATES.repoUrl, repoUrl) 143 | .replace(REPLACE_TEMPLATES.displayName, displayName) 144 | .replace(REPLACE_TEMPLATES.supportedFrameworks, supportedFrameworks) 145 | .replace(/\s*"postinstall".*node.*scripts\/welcome.js.*",/, ''); 146 | 147 | await writeFile(packageJson, packageJsonContents); 148 | 149 | console.log('📝 Updating the README...'); 150 | const readme = resolve(__dirname, `../README.md`); 151 | let readmeContents = await readFile(readme, 'utf-8'); 152 | 153 | const regex = /<\!-- README START -->([\s\S]*)<\!-- README END -->/g; 154 | 155 | readmeContents = readmeContents.replace( 156 | regex, 157 | dedent` 158 | # Storybook Addon ${displayName} 159 | ${addonDescription} 160 | `, 161 | ); 162 | 163 | await writeFile(readme, readmeContents); 164 | 165 | console.log(`📦 Creating a commit...`); 166 | execSync('git add . && git commit -m "project setup" --no-verify'); 167 | 168 | console.log( 169 | dedent`\n 170 | 🚀 All done! Run \`npm run start\` to get started. 171 | 172 | Thanks for using this template, ${authorName.split(' ')[0]}! ❤️ 173 | 174 | Feel free to open issues in case there are bugs/feature requests at: 175 | 176 | ${bold(blue('https://github.com/storybookjs/addon-kit'))}\n 177 | `, 178 | ); 179 | }; 180 | 181 | if (!process.env.CI) { 182 | main().catch((e) => console.log(`Something went wrong: ${e}`)); 183 | } 184 | -------------------------------------------------------------------------------- /src/stories/Introduction.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/addon-docs/blocks'; 2 | import Code from './assets/code-brackets.svg'; 3 | import Colors from './assets/colors.svg'; 4 | import Comments from './assets/comments.svg'; 5 | import Direction from './assets/direction.svg'; 6 | import Flow from './assets/flow.svg'; 7 | import Plugin from './assets/plugin.svg'; 8 | import Repo from './assets/repo.svg'; 9 | import StackAlt from './assets/stackalt.svg'; 10 | 11 | 12 | 13 | 116 | 117 | # Welcome to Storybook 118 | 119 | Storybook helps you build UI components in isolation from your app's business logic, data, and context. 120 | That makes it easy to develop hard-to-reach states. Save these UI states as **stories** to revisit during development, testing, or QA. 121 | 122 | Browse example stories now by navigating to them in the sidebar. 123 | View their code in the `stories` directory to learn how they work. 124 | We recommend building UIs with a [**component-driven**](https://componentdriven.org) process starting with atomic components and ending with pages. 125 | 126 |
Configure
127 | 128 | 162 | 163 |
Learn
164 | 165 | 195 | 196 |
197 | TipEdit the Markdown in stories/Introduction.stories.mdx 198 |
199 | -------------------------------------------------------------------------------- /src/stories/assets/colors.svg: -------------------------------------------------------------------------------- 1 | illustration/colors -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Storybook Addon Kit ([demo](https://main--601ada52c3d4040021afdc30.chromatic.com)) 4 | 5 | Simplify the creation of Storybook addons 6 | 7 | - 📝 Live-editing in development 8 | - ⚛️ React/JSX support 9 | - 📦 Transpiling and bundling with [tsup](https://tsup.egoist.dev/) 10 | - 🏷 Plugin metadata 11 | - 🚢 Release management with [Auto](https://github.com/intuit/auto) 12 | - 🧺 Boilerplate and sample code 13 | - 🛄 ESM support 14 | - 🛂 TypeScript by default with option to eject to JS 15 | 16 | ### Migrating to a later Storybook version 17 | 18 | If you have an existing addon that you want to migrate to support the latest version of Storyboook, you can check out the [addon migration guide](https://storybook.js.org/docs/addons/addon-migration-guide). 19 | 20 | ## Getting Started 21 | 22 | Click the **Use this template** button to get started. 23 | 24 | ![](https://user-images.githubusercontent.com/321738/125058439-8d9ef880-e0aa-11eb-9211-e6d7be812959.gif) 25 | 26 | Clone your repository and install dependencies. 27 | 28 | ```sh 29 | npm install 30 | ``` 31 | 32 | 33 | 34 | ### Development scripts 35 | 36 | - `npm run start` runs babel in watch mode and starts Storybook 37 | - `npm run build` build and package your addon code 38 | 39 | ### Switch from TypeScript to JavaScript 40 | 41 | Don't want to use TypeScript? We offer a handy eject command: `npm run eject-ts` 42 | 43 | This will convert all code to JS. It is a destructive process, so we recommended running this before you start writing any code. 44 | 45 | ## What's included? 46 | 47 | ![Demo](https://user-images.githubusercontent.com/42671/107857205-e7044380-6dfa-11eb-8718-ad02e3ba1a3f.gif) 48 | 49 | 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) 50 | 51 | - `src/Tool.tsx` 52 | - `src/Panel.tsx` 53 | - `src/Tab.tsx` 54 | 55 | Which, along with the addon itself, are registered in `src/manager.ts`. 56 | 57 | Managing State and interacting with a story: 58 | 59 | - `src/withGlobals.ts` & `src/Tool.tsx` demonstrates how to use `useGlobals` to manage global state and modify the contents of a Story. 60 | - `src/withRoundTrip.ts` & `src/Panel.tsx` demonstrates two-way communication using channels. 61 | - `src/Tab.tsx` demonstrates how to use `useParameter` to access the current story's parameters. 62 | 63 | Your addon might use one or more of these patterns. Feel free to delete unused code. Update `src/manager.ts` and `src/preview.ts` accordingly. 64 | 65 | Lastly, configure you addon name in `src/constants.ts`. 66 | 67 | ### Bundling 68 | 69 | Addons can interact with a Storybook project in multiple ways. It is recommended to familiarize yourself with [the basics](https://storybook.js.org/docs/react/addons/introduction) before getting started. 70 | 71 | - Manager entries are used to add UI or behavior to the Storybook manager UI. 72 | - Preview entries are used to add UI or behavior to the preview iframe where stories are rendered. 73 | - Presets are used to modify the Storybook configuration, similar to how [users can configure their `main.ts` configurations](https://storybook.js.org/docs/react/api/main-config). 74 | 75 | Since each of these places represents a different environment with different features and modules, it is also recommended to split and build your modules accordingly. This addon-kit comes with a preconfigured [bundling configuration](./tsup.config.ts) that supports this split, and you are free to modify and extend it as needed. 76 | 77 | You can define which modules match which environments in the [`package.json#bundler`](./package.json) property: 78 | 79 | - `exportEntries` is a list of module entries that users can manually import from anywhere they need to. For example, you could have decorators that users need to import into their `preview.ts` file or utility functions that can be used in their `main.ts` files. 80 | - `managerEntries` is a list of module entries meant only for the manager UI. These modules will be bundled to ESM and won't include types since they are mostly loaded by Storybook directly. 81 | - `previewEntries` is a list of module entries meant only for the preview UI. These modules will be bundled to ESM and won't include types since they are mostly loaded by Storybook directly. 82 | 83 | Manager and preview entries are only used in the browser so they only output ESM modules. Export entries could be used both in the browser and in Node depending on their use case, so they both output ESM and CJS modules. 84 | 85 | #### Globalized packages 86 | 87 | Storybook provides a predefined set of packages that are available in the manager UI and the preview UI. In the final bundle of your addon, these packages should not be included. Instead, the imports should stay in place, allowing Storybook to replace those imports with the actual packages during the Storybook build process. 88 | 89 | The list of packages differs between the manager and the preview, which is why there is a slight difference between `managerEntries` and `previewEntries`. Most notably, `react` and `react-dom` are prebundled in the manager but not in the preview. This means that your manager entries can use React to build UI without bundling it or having a direct reference to it. Therefore, it is safe to have React as a `devDependency` even though you are using it in production. _Requiring React as a peer dependency would unnecessarily force your users to install React._ 90 | 91 | An exception to this rule is if you are using React to inject UI into the preview, which does not come prebundled with React. In such cases, you need to move `react` and `react-dom` to a peer dependency. However, we generally advise against this pattern since it would limit the usage of your addon to React-based Storybooks. 92 | 93 | ### Metadata 94 | 95 | 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). 96 | 97 | ## Documentation 98 | 99 | To help the community use your addon and understand its capabilities, please document it thoroughly. 100 | 101 | To get started, replace this README with the content in this sample template. 102 | 103 | ### Sample documentation template 104 | 105 | ````md 106 | # My Addon 107 | 108 | ## Installation 109 | 110 | First, install the package. 111 | 112 | ```sh 113 | npm install --save-dev my-addon 114 | ``` 115 | 116 | Then, register it as an addon in `.storybook/main.js`. 117 | 118 | ```ts 119 | // .storybook/main.ts 120 | 121 | // Replace your-framework with the framework you are using (e.g., react-webpack5, vue3-vite) 122 | import type { StorybookConfig } from '@storybook/your-framework'; 123 | 124 | const config: StorybookConfig = { 125 | // ...rest of config 126 | addons: [ 127 | '@storybook/addon-docs' 128 | 'my-addon', // 👈 register the addon here 129 | ], 130 | }; 131 | 132 | export default config; 133 | ``` 134 | 135 | ## Usage 136 | 137 | The primary way to use this addon is to define the `exampleParameter` parameter. You can do this the 138 | component level, as below, to affect all stories in the file, or you can do it for a single story. 139 | 140 | ```ts 141 | // Button.stories.ts 142 | 143 | // Replace your-framework with the name of your framework 144 | import type { Meta } from '@storybook/your-framework'; 145 | 146 | import { Button } from './Button'; 147 | 148 | const meta: Meta = { 149 | component: Button, 150 | parameters: { 151 | myAddon: { 152 | exampleParameter: true, 153 | // See API section below for available parameters 154 | }, 155 | }, 156 | }; 157 | 158 | export default meta; 159 | ``` 160 | 161 | Another way to use the addon is... 162 | 163 | ## API 164 | 165 | ### Parameters 166 | 167 | This addon contributes the following parameters to Storybook, under the `myAddon` namespace: 168 | 169 | #### `disable` 170 | 171 | Type: `boolean` 172 | 173 | Disable this addon's behavior. This parameter is most useful to allow overriding at more specific 174 | levels. For example, if this parameter is set to true at the project level, it could then be 175 | re-enabled by setting it to false at the meta (component) or story level. 176 | 177 | ### Options 178 | 179 | When registering this addon, you can configure it with the following options, which are passed when 180 | registering the addon, like so: 181 | 182 | ```ts 183 | // .storybook/main.ts 184 | 185 | // Replace your-framework with the framework you are using (e.g., react-webpack5, vue3-vite) 186 | import type { StorybookConfig } from '@storybook/your-framework'; 187 | 188 | const config: StorybookConfig = { 189 | // ...rest of config 190 | addons: [ 191 | '@storybook/addon-docs', 192 | { 193 | name: 'my-addon', 194 | options: { 195 | // 👈 options for my-addon go here 196 | }, 197 | }, 198 | ], 199 | }; 200 | 201 | export default config; 202 | ``` 203 | 204 | #### `useExperimentalBehavior` 205 | 206 | Type: `boolean` 207 | 208 | Enable experimental behavior to... 209 | ```` 210 | 211 | ## Release Management 212 | 213 | ### Setup 214 | 215 | 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: 216 | 217 | - [`NPM_TOKEN`](https://docs.npmjs.com/creating-and-viewing-access-tokens#creating-access-tokens) Create a token with both _Read and Publish_ permissions. 218 | - [`GH_TOKEN`](https://github.com/settings/tokens) Create a token with the `repo` scope. 219 | 220 | Then open your `package.json` and edit the following fields: 221 | 222 | - `name` 223 | - `author` 224 | - `repository` 225 | 226 | #### Local 227 | 228 | To use `auto` locally create a `.env` file at the root of your project and add your tokens to it: 229 | 230 | ```bash 231 | GH_TOKEN= 232 | NPM_TOKEN= 233 | ``` 234 | 235 | Lastly, **create labels on GitHub**. You’ll use these labels in the future when making changes to the package. 236 | 237 | ```bash 238 | npx auto create-labels 239 | ``` 240 | 241 | 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. 242 | 243 | #### GitHub Actions 244 | 245 | This template comes with GitHub actions already set up to publish your addon anytime someone pushes to your repository. 246 | 247 | Go to `Settings > Secrets`, click `New repository secret`, and add your `NPM_TOKEN`. 248 | 249 | ### Creating a release 250 | 251 | To create a release locally you can run the following command, otherwise the GitHub action will make the release for you. 252 | 253 | ```sh 254 | npm run release 255 | ``` 256 | 257 | That will: 258 | 259 | - Build and package the addon code 260 | - Bump the version 261 | - Push a release to GitHub and npm 262 | - Push a changelog to GitHub 263 | --------------------------------------------------------------------------------