├── .github └── workflows │ └── release.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .storybook ├── local-preset.js ├── main.ts ├── preview-head.html └── preview.ts ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets └── icon.png ├── manager.js ├── package-lock.json ├── package.json ├── preview.js ├── read-remote.mjs ├── scripts ├── eject-typescript.js ├── prepublish-checks.js └── welcome.js ├── src ├── Tool.tsx ├── components │ └── CodeflowLogo.tsx ├── constants.ts ├── index.ts ├── manager.ts ├── preview.ts └── stories │ ├── Button.stories.ts │ ├── Button.tsx │ ├── Header.stories.ts │ ├── Header.tsx │ ├── Introduction.mdx │ ├── Page.stories.ts │ ├── Page.tsx │ ├── assets │ ├── code-brackets.svg │ ├── colors.svg │ ├── comments.svg │ ├── direction.svg │ ├── flow.svg │ ├── plugin.svg │ ├── repo.svg │ └── stackalt.svg │ ├── button.css │ ├── header.css │ └── page.css ├── tsconfig.json ├── tsup.config.ts └── vite.config.ts /.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')" 9 | steps: 10 | - uses: actions/checkout@v2 11 | 12 | - name: Prepare repository 13 | run: git fetch --unshallow --tags 14 | 15 | - name: Use Node.js 16.x 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: 16.x 19 | 20 | - name: Install dependencies 21 | run: npm install --ignore-scripts 22 | 23 | - name: Create Release 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 27 | run: | 28 | npm run release 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | storybook-static/ 4 | build-storybook.log 5 | .DS_Store 6 | .env -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.storybook/local-preset.js: -------------------------------------------------------------------------------- 1 | /** 2 | * to load the built addon in this test Storybook 3 | */ 4 | function previewAnnotations(entry = []) { 5 | return [...entry, require.resolve("../dist/preview.js")]; 6 | } 7 | 8 | function managerEntries(entry = []) { 9 | return [...entry, require.resolve("../dist/manager.js")]; 10 | } 11 | 12 | module.exports = { 13 | managerEntries, 14 | previewAnnotations, 15 | }; 16 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from "@storybook/react-vite"; 2 | const config: StorybookConfig = { 3 | stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], 4 | addons: [ 5 | "@storybook/addon-links", 6 | "@storybook/addon-essentials", 7 | "@storybook/addon-interactions", 8 | "./local-preset.js", 9 | ], 10 | framework: { 11 | name: "@storybook/react-vite", 12 | options: {}, 13 | }, 14 | docs: { 15 | autodocs: "tag", 16 | }, 17 | }; 18 | export default config; 19 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from "@storybook/react"; 2 | 3 | const preview: Preview = { 4 | parameters: { 5 | backgrounds: { 6 | default: "light", 7 | }, 8 | actions: { argTypesRegex: "^on[A-Z].*" }, 9 | controls: { 10 | matchers: { 11 | color: /(background|color)$/i, 12 | date: /Date$/, 13 | }, 14 | }, 15 | }, 16 | }; 17 | 18 | export default preview; 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.0.2 (Tue Oct 03 2023) 2 | 3 | #### ⚠️ Pushed to `main` 4 | 5 | - Update README.md ([@sulco](https://github.com/sulco)) 6 | - Add integration icon for the catalog ([@sulco](https://github.com/sulco)) 7 | 8 | #### Authors: 1 9 | 10 | - Tomek Sułkowski ([@sulco](https://github.com/sulco)) 11 | 12 | --- 13 | 14 | # v0.0.1 (Tue Oct 03 2023) 15 | 16 | #### 🐛 Bug Fix 17 | 18 | - Initial version, preparing for release [#1](https://github.com/stackblitz/storybook-addon-stackblitz/pull/1) ([@sulco](https://github.com/sulco)) 19 | 20 | #### ⚠️ Pushed to `main` 21 | 22 | - update package name, add installation instructions ([@sulco](https://github.com/sulco)) 23 | - cleanup + initial readme ([@sulco](https://github.com/sulco)) 24 | - update storybook ([@sulco](https://github.com/sulco)) 25 | - initial commit ([@sulco](https://github.com/sulco)) 26 | 27 | #### Authors: 1 28 | 29 | - Tomek Sułkowski ([@sulco](https://github.com/sulco)) 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Storybook Addon StackBlitz 2 | Create a one-click Pull Request environment right from your component 3 | 4 | ## Installation 5 | 6 | yarn: 7 | ```bash 8 | yarn add --dev @stackblitz/storybook-addon-stackblitz 9 | ``` 10 | 11 | npm: 12 | ```bash 13 | npm install @stackblitz/storybook-addon-stackblitz --save-dev 14 | ``` 15 | 16 | pnpm: 17 | ```bash 18 | pnpm add --save-dev @stackblitz/storybook-addon-stackblitz 19 | ``` 20 | 21 | ## Usage 22 | 23 | Add the following to your `.storybook/main.ts` (or `.storybook/main.js`) exports: 24 | 25 | ```typescript 26 | export default { 27 | addons: ['@stackblitz/storybook-addon-stackblitz'], 28 | }; 29 | ``` 30 | 31 | Configure the repository URL in the `.storybook/preview.ts` (or `.storybook/preview.js`): 32 | ```typescript 33 | export default { 34 | parameters: { 35 | repositoryUrl: 'https://github.com/[username]/[reponame]' 36 | }, 37 | }; 38 | ``` 39 | 40 | In your story files set the file path for the specific story, for instance: 41 | ```typescript 42 | export const Primary: Story = { 43 | args: {/* ... */}, 44 | parameters: { 45 | filePath: 'src/stories/Button.tsx' 46 | } 47 | }; 48 | ``` 49 | 50 | If your components are placed in multiple repositories, you can also define the repository URL per the specific story: 51 | ```typescript 52 | export const Primary: Story = { 53 | args: {/* ... */}, 54 | parameters: { 55 | filePath: 'src/stories/Button.tsx', 56 | repositoryUrl: 'https://github.com/[username]/[reponame]' 57 | } 58 | }; 59 | ``` 60 | 61 | 62 | ## Development scripts 63 | 64 | - `yarn start` runs babel in watch mode and starts Storybook 65 | - `yarn build` build and package your addon code 66 | 67 | ## Addon icon 68 | 69 | (for Storybook [integration catalog](https://storybook.js.org/integrations)) 70 | 71 | ![icon](https://github.com/stackblitz/storybook-addon-stackblitz/assets/1511906/d8b5c029-8633-4d16-8a9e-5d5fe8456aeb) 72 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/storybook-addon-stackblitz/main/assets/icon.png -------------------------------------------------------------------------------- /manager.js: -------------------------------------------------------------------------------- 1 | export * from "./dist/manager"; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@stackblitz/storybook-addon-stackblitz", 3 | "version": "0.0.5", 4 | "description": "Create a one-click Pull Request environment right from your component", 5 | "keywords": [ 6 | "storybook-addons", 7 | "stackblitz", 8 | "pull-request", 9 | "ide", 10 | "code" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/stackblitz/storybook-addon-stackblitz" 15 | }, 16 | "type": "module", 17 | "author": "Tomek Sułkowski ", 18 | "license": "MIT", 19 | "exports": { 20 | ".": { 21 | "types": "./dist/index.d.ts", 22 | "require": "./dist/index.cjs", 23 | "import": "./dist/index.js" 24 | }, 25 | "./preview": { 26 | "types": "./dist/index.d.ts", 27 | "import": "./dist/preview.js", 28 | "require": "./dist/preview.js" 29 | }, 30 | "./preset": "./dist/preset.cjs", 31 | "./manager": "./dist/manager.js", 32 | "./package.json": "./package.json" 33 | }, 34 | "main": "dist/index.js", 35 | "module": "dist/index.mjs", 36 | "types": "dist/index.d.ts", 37 | "files": [ 38 | "dist/**/*", 39 | "README.md", 40 | "*.js", 41 | "*.d.ts" 42 | ], 43 | "scripts": { 44 | "clean": "rimraf ./dist", 45 | "prebuild": "npm run clean", 46 | "build": "tsup", 47 | "build:watch": "npm run build -- --watch", 48 | "test": "echo \"Error: no test specified\" && exit 1", 49 | "start": "run-p build:watch 'storybook --quiet'", 50 | "prerelease": "zx scripts/prepublish-checks.js", 51 | "release": "npm run build && auto shipit", 52 | "eject-ts": "zx scripts/eject-typescript.js", 53 | "storybook": "storybook dev -p 6006", 54 | "build-storybook": "storybook build" 55 | }, 56 | "devDependencies": { 57 | "@storybook/addon-essentials": "^8.0.0", 58 | "@storybook/addon-interactions": "^8.0.0", 59 | "@storybook/addon-links": "^8.0.0", 60 | "@storybook/react": "^8.0.0", 61 | "@storybook/react-vite": "^8.0.0", 62 | "@storybook/testing-library": "^0.2.2", 63 | "@types/node": "^20.11.28", 64 | "@types/react": "^18.2.66", 65 | "@vitejs/plugin-react": "^4.2.1", 66 | "auto": "^11.1.1", 67 | "boxen": "^7.1.1", 68 | "dedent": "^1.5.1", 69 | "ini": "^4.1.2", 70 | "npm-run-all": "^4.1.5", 71 | "prettier": "^3.2.5", 72 | "prompts": "^2.4.2", 73 | "prop-types": "^15.8.1", 74 | "react": "^18.2.0", 75 | "react-dom": "^18.2.0", 76 | "rimraf": "^5.0.5", 77 | "storybook": "^8.0.0", 78 | "tsup": "^8.0.2", 79 | "typescript": "^5.4.2", 80 | "vite": "^5.1.6", 81 | "zx": "^7.2.3" 82 | }, 83 | "publishConfig": { 84 | "access": "public" 85 | }, 86 | "bundler": { 87 | "exportEntries": [ 88 | "src/index.ts" 89 | ], 90 | "managerEntries": [ 91 | "src/manager.ts" 92 | ], 93 | "previewEntries": [ 94 | "src/preview.ts" 95 | ] 96 | }, 97 | "storybook": { 98 | "displayName": "StackBlitz", 99 | "supportedFrameworks": [ 100 | "react", 101 | "vue", 102 | "angular", 103 | "web-components", 104 | "ember", 105 | "html", 106 | "svelte", 107 | "preact", 108 | "react-native" 109 | ], 110 | "icon": "https://user-images.githubusercontent.com/1511906/272314785-d8b5c029-8633-4d16-8a9e-5d5fe8456aeb.png" 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /preview.js: -------------------------------------------------------------------------------- 1 | export * from "./dist/preview"; 2 | -------------------------------------------------------------------------------- /read-remote.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import ini from 'ini'; 3 | 4 | const config = ini.parse(fs.readFileSync('./.git/config', 'utf-8')) 5 | 6 | const remoteOrigin = config['remote "origin"'] 7 | 8 | if (remoteOrigin) { 9 | const remoteUrl = config['remote "origin"'].url.replace('.git', ''); 10 | const stackBlitzUrl = remoteUrl.replace('https://', 'https://pr.new/'); 11 | } else { 12 | console.warn('no remote origin set'); 13 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scripts/prepublish-checks.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | 3 | import boxen from "boxen"; 4 | import dedent from "dedent"; 5 | import { readFile } from 'fs/promises'; 6 | import { globalPackages as globalManagerPackages } from "@storybook/manager/globals"; 7 | import { globalPackages as globalPreviewPackages } from "@storybook/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 | 86 | process.exit(exitCode); 87 | -------------------------------------------------------------------------------- /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: 38 | "What is the addon display name (this will be used in the addon catalog)?*", 39 | validate: (name) => 40 | name === "" 41 | ? "Display name can't be empty. For more info, see: https://storybook.js.org/docs/react/addons/addon-catalog#addon-metadata" 42 | : true, 43 | }, 44 | { 45 | type: "text", 46 | name: "addonDescription", 47 | initial: "", 48 | message: "Write a short description of the addon*", 49 | validate: (name) => (name === "" ? "Description can't be empty" : true), 50 | }, 51 | { 52 | type: "text", 53 | name: "repoUrl", 54 | message: "Git repo URL for your addon package (https://github.com/...)*", 55 | validate: (url) => (url === "" ? "URL can't be empty" : true), 56 | }, 57 | { 58 | type: "text", 59 | name: "addonIcon", 60 | initial: 61 | "https://user-images.githubusercontent.com/321738/63501763-88dbf600-c4cc-11e9-96cd-94adadc2fd72.png", 62 | message: "URL of your addon icon", 63 | }, 64 | { 65 | type: "list", 66 | name: "keywords", 67 | initial: "storybook-addons", 68 | message: "Enter addon keywords (comma separated)", 69 | separator: ",", 70 | format: (keywords) => 71 | keywords 72 | .concat(["storybook-addons"]) 73 | .map((k) => `"${k}"`) 74 | .join(", "), 75 | }, 76 | { 77 | type: "list", 78 | name: "supportedFrameworks", 79 | initial: 80 | "react, vue, angular, web-components, ember, html, svelte, preact, react-native", 81 | message: "List of frameworks you support (comma separated)?", 82 | separator: ",", 83 | format: (frameworks) => frameworks.map((k) => `"${k}"`).join(", "), 84 | }, 85 | ]; 86 | 87 | const REPLACE_TEMPLATES = { 88 | packageName: "storybook-addon-kit", 89 | addonDescription: "everything you need to build a Storybook addon", 90 | packageAuthor: "package-author", 91 | repoUrl: "https://github.com/storybookjs/storybook-addon-kit", 92 | keywords: `"storybook-addons"`, 93 | displayName: "Addon Kit", 94 | supportedFrameworks: `"supported-frameworks"`, 95 | }; 96 | 97 | const bold = (message) => `\u001b[1m${message}\u001b[22m`; 98 | const magenta = (message) => `\u001b[35m${message}\u001b[39m`; 99 | const blue = (message) => `\u001b[34m${message}\u001b[39m`; 100 | 101 | const main = async () => { 102 | console.log( 103 | bold( 104 | magenta( 105 | dedent` 106 | Welcome to Storybook addon-kit! 107 | Please answer the following questions while we prepare this project for you:\n 108 | ` 109 | ) 110 | ) 111 | ); 112 | 113 | const { 114 | authorName, 115 | authorEmail, 116 | packageName, 117 | addonDescription, 118 | repoUrl, 119 | displayName, 120 | keywords, 121 | supportedFrameworks, 122 | } = await prompts(questions); 123 | 124 | if (!authorName || !packageName) { 125 | console.log( 126 | `\nProcess canceled by the user. Feel free to run ${bold( 127 | "npm run postinstall" 128 | )} to execute the installation steps again!` 129 | ); 130 | process.exit(0); 131 | } 132 | 133 | const authorField = authorName + (authorEmail ? ` <${authorEmail}>` : ""); 134 | 135 | const packageJson = resolve(__dirname, `../package.json`); 136 | 137 | console.log(`\n👷 Updating package.json...`); 138 | let packageJsonContents = await readFile(packageJson, "utf-8"); 139 | 140 | packageJsonContents = packageJsonContents 141 | .replace(REPLACE_TEMPLATES.packageName, packageName) 142 | .replace(REPLACE_TEMPLATES.addonDescription, addonDescription) 143 | .replace(REPLACE_TEMPLATES.packageAuthor, authorField) 144 | .replace(REPLACE_TEMPLATES.keywords, keywords) 145 | .replace(REPLACE_TEMPLATES.repoUrl, repoUrl) 146 | .replace(REPLACE_TEMPLATES.displayName, displayName) 147 | .replace(REPLACE_TEMPLATES.supportedFrameworks, supportedFrameworks) 148 | .replace(/\s*"postinstall".*node.*scripts\/welcome.js.*",/, ''); 149 | 150 | await writeFile(packageJson, packageJsonContents); 151 | 152 | console.log("📝 Updating the README..."); 153 | const readme = resolve(__dirname, `../README.md`); 154 | let readmeContents = await readFile(readme, "utf-8"); 155 | 156 | const regex = /<\!-- README START -->([\s\S]*)<\!-- README END -->/g; 157 | 158 | readmeContents = readmeContents.replace( 159 | regex, 160 | dedent` 161 | # Storybook Addon ${displayName} 162 | ${addonDescription} 163 | ` 164 | ); 165 | 166 | await writeFile(readme, readmeContents); 167 | 168 | console.log(`📦 Creating a commit...`); 169 | execSync('git add . && git commit -m "project setup" --no-verify'); 170 | 171 | console.log( 172 | dedent`\n 173 | 🚀 All done! Run \`npm run start\` to get started. 174 | 175 | Thanks for using this template, ${authorName.split(" ")[0]}! ❤️ 176 | 177 | Feel free to open issues in case there are bugs/feature requests at: 178 | 179 | ${bold(blue("https://github.com/storybookjs/addon-kit"))}\n 180 | ` 181 | ); 182 | }; 183 | 184 | main().catch((e) => console.log(`Something went wrong: ${e}`)); 185 | -------------------------------------------------------------------------------- /src/Tool.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, MouseEvent } from "react"; 2 | import { useParameter, useStorybookApi } from "@storybook/manager-api"; 3 | import { IconButton, Link } from "@storybook/components"; 4 | import { PARAM_KEY, TOOL_ID } from "./constants"; 5 | import CodeflowLogo from "./components/CodeflowLogo"; 6 | 7 | export const Tool = function MyAddonSelector() { 8 | const repositoryUrl = useParameter(PARAM_KEY.REPO); 9 | const branch = useParameter(PARAM_KEY.BRANCH, 'main'); 10 | const filePath = useParameter(PARAM_KEY.FILE_PATH); 11 | 12 | const api = useStorybookApi(); 13 | const [disabled, setDisabled] = useState(false) 14 | 15 | const currentStory = api.getCurrentStoryData(); 16 | 17 | if (!currentStory) { 18 | return null; 19 | } 20 | 21 | console.log({repositoryUrl}) 22 | if (!repositoryUrl && !disabled) { 23 | console.warn(`"${PARAM_KEY}" parameter not defined. Make sure to configure it in your story.`); 24 | setDisabled(true); 25 | } else if (repositoryUrl && disabled) { 26 | setDisabled(false); 27 | } 28 | 29 | let stackblitzUrl = `https://pr.new/${repositoryUrl}`; 30 | if (filePath) { 31 | stackblitzUrl = `${stackblitzUrl}/blob/${branch}/${filePath}`; 32 | /* 33 | * We've just addded `/` between all segments not caring if user already appended or prepanded them, 34 | * so let's remove any possible double `//` (not preceded by `:` so we don't mess up the `https://`) 35 | */ 36 | stackblitzUrl = stackblitzUrl.replaceAll(/(?; 41 | } 42 | 43 | return ( 44 | 50 | 51 | 52 | ); 53 | }; -------------------------------------------------------------------------------- /src/components/CodeflowLogo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { SVGProps } from "react" 3 | 4 | const CodeflowLogo = (props: SVGProps) => ( 5 | 6 | 10 | 11 | ) 12 | export default CodeflowLogo 13 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const ADDON_ID = "storybook/stackblitz"; 2 | export const TOOL_ID = `${ADDON_ID}/tool`; 3 | export const PARAM_KEY = { 4 | FILE_PATH: `filePath`, 5 | REPO: `repositoryUrl`, 6 | BRANCH: `branch`, 7 | }; 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // make it work with --isolatedModules 2 | export default {}; 3 | -------------------------------------------------------------------------------- /src/manager.ts: -------------------------------------------------------------------------------- 1 | import { addons, types } from "@storybook/manager-api"; 2 | import { ADDON_ID, TOOL_ID } from "./constants"; 3 | import { Tool } from "./Tool"; 4 | 5 | addons.register(ADDON_ID, () => { 6 | addons.add(TOOL_ID, { 7 | type: types.TOOL, 8 | title: "StackBlitz", 9 | match: ({ viewMode }) => !!(viewMode && viewMode.match(/^(story|docs)$/)), 10 | render: Tool 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Renderer, ProjectAnnotations } from "@storybook/types"; 2 | import { PARAM_KEY } from "./constants"; 3 | 4 | const preview: ProjectAnnotations = { 5 | parameters: { 6 | // [PARAM_KEY.REPO]: 'https://github.com/[username]/[reponame]', 7 | } 8 | }; 9 | 10 | export default preview; 11 | -------------------------------------------------------------------------------- /src/stories/Button.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { Button } from "./Button"; 4 | 5 | const meta: Meta = { 6 | title: "Example/Button", 7 | component: Button, 8 | argTypes: { 9 | backgroundColor: { control: "color" }, 10 | }, 11 | tags: ["autodocs"], 12 | parameters: { 13 | repositoryUrl: `https://github.com/sulco/viteconf-storybook-demo`, 14 | }, 15 | }; 16 | 17 | export default meta; 18 | type Story = StoryObj; 19 | 20 | export const Primary: Story = { 21 | args: { 22 | primary: true, 23 | label: "Get my free ticket!", 24 | }, 25 | parameters: { 26 | filePath: 'src/stories/Button.tsx' 27 | } 28 | }; 29 | 30 | export const Secondary: Story = { 31 | args: { 32 | label: "Button", 33 | }, 34 | }; 35 | 36 | export const Large: Story = { 37 | args: { 38 | size: "large", 39 | label: "Button", 40 | }, 41 | }; 42 | 43 | export const Small: Story = { 44 | args: { 45 | size: "small", 46 | label: "Button", 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /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 = ({ 31 | primary = false, 32 | size = 'medium', 33 | backgroundColor, 34 | label, 35 | ...props 36 | }: ButtonProps) => { 37 | const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary'; 38 | return ( 39 | 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /src/stories/Header.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 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/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 | 27 | 31 | 35 | 36 | 37 |

Acme

38 |
39 |
40 | {user ? ( 41 | <> 42 | 43 | Welcome, {user.name}! 44 | 45 |
54 |
55 |
56 | ); 57 | -------------------------------------------------------------------------------- /src/stories/Introduction.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/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 | 174 | 175 |
Learn
176 | 177 | 207 | 208 |
209 | TipEdit the Markdown in{' '} 210 | stories/Introduction.stories.mdx 211 |
212 | -------------------------------------------------------------------------------- /src/stories/Page.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { within, userEvent } from '@storybook/testing-library'; 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 | -------------------------------------------------------------------------------- /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 33 | needing to navigate to them in your app. Here are some handy patterns for managing page 34 | data in Storybook: 35 |

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

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

57 |
58 | Tip Adjust the width of the canvas with the{' '} 59 | 60 | 61 | 66 | 67 | 68 | Viewports addon in the toolbar 69 |
70 |
71 |
72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /src/stories/assets/code-brackets.svg: -------------------------------------------------------------------------------- 1 | illustration/code-brackets -------------------------------------------------------------------------------- /src/stories/assets/colors.svg: -------------------------------------------------------------------------------- 1 | illustration/colors -------------------------------------------------------------------------------- /src/stories/assets/comments.svg: -------------------------------------------------------------------------------- 1 | illustration/comments -------------------------------------------------------------------------------- /src/stories/assets/direction.svg: -------------------------------------------------------------------------------- 1 | illustration/direction -------------------------------------------------------------------------------- /src/stories/assets/flow.svg: -------------------------------------------------------------------------------- 1 | illustration/flow -------------------------------------------------------------------------------- /src/stories/assets/plugin.svg: -------------------------------------------------------------------------------- 1 | illustration/plugin -------------------------------------------------------------------------------- /src/stories/assets/repo.svg: -------------------------------------------------------------------------------- 1 | illustration/repo -------------------------------------------------------------------------------- /src/stories/assets/stackalt.svg: -------------------------------------------------------------------------------- 1 | illustration/stackalt -------------------------------------------------------------------------------- /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 | padding: 1rem 6rem; 13 | font-size: 90% !important; 14 | line-height: 1.25rem; 15 | height: 56px; 16 | width: 200px; 17 | border-radius: 30px; 18 | background: linear-gradient(#d946ef, #a21caf); 19 | border: solid 1px #d359e6b3; 20 | box-shadow: 1px 1px 4px #000a; 21 | } 22 | .storybook-button--secondary { 23 | color: #333; 24 | background-color: transparent; 25 | box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; 26 | } 27 | .storybook-button--small { 28 | font-size: 12px; 29 | padding: 10px 16px; 30 | } 31 | .storybook-button--medium { 32 | font-size: 14px; 33 | padding: 11px 20px; 34 | } 35 | .storybook-button--large { 36 | font-size: 16px; 37 | padding: 12px 24px; 38 | } 39 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "baseUrl": ".", 5 | "esModuleInterop": true, 6 | "experimentalDecorators": true, 7 | "incremental": false, 8 | "isolatedModules": true, 9 | "jsx": "react", 10 | "lib": ["es2020", "dom"], 11 | "module": "commonjs", 12 | "noImplicitAny": true, 13 | "rootDir": "./src", 14 | "skipLibCheck": true, 15 | "target": "ES2020" 16 | }, 17 | "include": ["src/**/*"] 18 | } 19 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, type Options } from "tsup"; 2 | import { readFile } from "fs/promises"; 3 | import { globalPackages as globalManagerPackages } from "@storybook/manager/globals"; 4 | import { globalPackages as globalPreviewPackages } from "@storybook/preview/globals"; 5 | 6 | // The current browsers supported by Storybook v7 7 | const BROWSER_TARGET: Options['target'] = ["chrome100", "safari15", "firefox91"]; 8 | const NODE_TARGET: Options['target'] = ["node18"]; 9 | 10 | type BundlerConfig = { 11 | bundler?: { 12 | exportEntries?: string[]; 13 | nodeEntries?: string[]; 14 | managerEntries?: string[]; 15 | previewEntries?: string[]; 16 | }; 17 | }; 18 | 19 | export default defineConfig(async (options) => { 20 | // reading the three types of entries from package.json, which has the following structure: 21 | // { 22 | // ... 23 | // "bundler": { 24 | // "exportEntries": ["./src/index.ts"], 25 | // "managerEntries": ["./src/manager.ts"], 26 | // "previewEntries": ["./src/preview.ts"] 27 | // "nodeEntries": ["./src/preset.ts"] 28 | // } 29 | // } 30 | const packageJson = await readFile('./package.json', 'utf8').then(JSON.parse) as BundlerConfig; 31 | const { 32 | bundler: { 33 | exportEntries = [], 34 | managerEntries = [], 35 | previewEntries = [], 36 | nodeEntries = [], 37 | } = {}, 38 | } = packageJson; 39 | 40 | const commonConfig: Options = { 41 | splitting: false, 42 | minify: !options.watch, 43 | treeshake: true, 44 | sourcemap: true, 45 | clean: true, 46 | }; 47 | 48 | const configs: Options[] = []; 49 | 50 | // export entries are entries meant to be manually imported by the user 51 | // they are not meant to be loaded by the manager or preview 52 | // they'll be usable in both node and browser environments, depending on which features and modules they depend on 53 | if (exportEntries.length) { 54 | configs.push({ 55 | ...commonConfig, 56 | entry: exportEntries, 57 | dts: { 58 | resolve: true, 59 | }, 60 | format: ["esm", "cjs"], 61 | target: [...BROWSER_TARGET, ...NODE_TARGET], 62 | platform: "neutral", 63 | external: [...globalManagerPackages, ...globalPreviewPackages], 64 | }); 65 | } 66 | 67 | // manager entries are entries meant to be loaded into the manager UI 68 | // they'll have manager-specific packages externalized and they won't be usable in node 69 | // they won't have types generated for them as they're usually loaded automatically by Storybook 70 | if (managerEntries.length) { 71 | configs.push({ 72 | ...commonConfig, 73 | entry: managerEntries, 74 | format: ["esm"], 75 | target: BROWSER_TARGET, 76 | platform: "browser", 77 | external: globalManagerPackages, 78 | }); 79 | } 80 | 81 | // preview entries are entries meant to be loaded into the preview iframe 82 | // they'll have preview-specific packages externalized and they won't be usable in node 83 | // they'll have types generated for them so they can be imported when setting up Portable Stories 84 | if (previewEntries.length) { 85 | configs.push({ 86 | ...commonConfig, 87 | entry: previewEntries, 88 | dts: { 89 | resolve: true, 90 | }, 91 | format: ["esm"], 92 | target: BROWSER_TARGET, 93 | platform: "browser", 94 | external: globalPreviewPackages, 95 | }); 96 | } 97 | 98 | // node entries are entries meant to be used in node-only 99 | // this is useful for presets, which are loaded by Storybook when setting up configurations 100 | // they won't have types generated for them as they're usually loaded automatically by Storybook 101 | if (nodeEntries.length) { 102 | configs.push({ 103 | ...commonConfig, 104 | entry: nodeEntries, 105 | format: ["cjs"], 106 | target: NODE_TARGET, 107 | platform: "node", 108 | }); 109 | } 110 | 111 | return configs; 112 | }); 113 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------