├── .codesandbox
└── tasks.json
├── .devcontainer
└── devcontainer.json
├── .github
├── dependabot.yml
└── workflows
│ └── release.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .storybook
├── local-preset.js
├── main.ts
├── preview-head.html
└── preview.tsx
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── manager.js
├── package.json
├── pnpm-lock.yaml
├── preset.js
├── preview.js
├── scripts
├── eject-typescript.js
├── prepublish-checks.js
└── welcome.js
├── src
├── Tool.tsx
├── constants.ts
├── index.ts
├── manager.tsx
├── preset.ts
├── stories
│ ├── Button.stories.ts
│ ├── Text.mdx
│ └── Text.stories.tsx
└── utils.ts
├── tsconfig.json
├── tsup.config.ts
└── vite.config.ts
/.codesandbox/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // These tasks will run in order when initializing your CodeSandbox project.
3 | "setupTasks": [
4 | {
5 | "name": "Install Dependencies",
6 | "command": "pnpm install"
7 | }
8 | ],
9 |
10 | // These tasks can be run from CodeSandbox. Running one will open a log in the app.
11 | "tasks": {
12 | "start": {
13 | "name": "start",
14 | "command": "pnpm run start"
15 | },
16 | "postinstall": {
17 | "name": "postinstall",
18 | "command": "pnpm run postinstall"
19 | },
20 | "clean": {
21 | "name": "clean",
22 | "command": "pnpm run clean"
23 | },
24 | "prebuild": {
25 | "name": "prebuild",
26 | "command": "pnpm run prebuild"
27 | },
28 | "build": {
29 | "name": "build",
30 | "command": "pnpm run build",
31 | "runAtStart": true
32 | },
33 | "build:watch": {
34 | "name": "build:watch",
35 | "command": "pnpm run build:watch"
36 | },
37 | "test": {
38 | "name": "test",
39 | "command": "pnpm run test"
40 | },
41 | "prerelease": {
42 | "name": "prerelease",
43 | "command": "pnpm run prerelease"
44 | },
45 | "release": {
46 | "name": "release",
47 | "command": "pnpm run release"
48 | },
49 | "eject-ts": {
50 | "name": "eject-ts",
51 | "command": "pnpm run eject-ts"
52 | },
53 | "storybook": {
54 | "name": "storybook",
55 | "command": "pnpm run storybook",
56 | "runAtStart": true,
57 | "preview": {
58 | "port": 6006
59 | }
60 | },
61 | "build-storybook": {
62 | "name": "build-storybook",
63 | "command": "pnpm run build-storybook"
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the
2 | // README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
3 | {
4 | "name": "Node.js & TypeScript",
5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
6 | "image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye"
7 |
8 | // Features to add to the dev container. More info: https://containers.dev/features.
9 | // "features": {},
10 |
11 | // Use 'forwardPorts' to make a list of ports inside the container available locally.
12 | // "forwardPorts": [],
13 |
14 | // Use 'postCreateCommand' to run commands after the container is created.
15 | // "postCreateCommand": "yarn install",
16 |
17 | // Configure tool-specific properties.
18 | // "customizations": {},
19 |
20 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
21 | // "remoteUser": "root"
22 | }
23 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for more information:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 | # https://containers.dev/guide/dependabot
6 |
7 | version: 2
8 | updates:
9 | - package-ecosystem: "devcontainers"
10 | directory: "/"
11 | schedule:
12 | interval: weekly
13 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | release:
10 | runs-on: ubuntu-latest
11 | if: "!contains(github.event.head_commit.message, 'ci skip') && !contains(github.event.head_commit.message, 'skip ci')"
12 | steps:
13 | - uses: actions/checkout@v2
14 |
15 | - name: Prepare repository
16 | run: git fetch --unshallow --tags
17 |
18 | - name: Use Node.js 20.x
19 | uses: actions/setup-node@v3
20 | with:
21 | node-version: 20.x
22 |
23 | - name: Install dependencies
24 | run: npm install --ignore-scripts
25 |
26 | - name: Authenticate with Registry
27 | run: echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} > .npmrc
28 | env:
29 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
30 |
31 | - name: Pre-release
32 | run: npm run prepublish
33 |
34 | - name: Publish
35 | run: |
36 | npm publish
37 |
--------------------------------------------------------------------------------
/.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 | function managerEntries(entry = []) {
2 | return [...entry, require.resolve("../dist/manager.js")];
3 | }
4 |
5 | module.exports = {
6 | managerEntries,
7 | };
8 |
--------------------------------------------------------------------------------
/.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.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import type { Preview } from "@storybook/react";
4 | import { Theme } from "@radix-ui/themes";
5 |
6 | const preview: Preview = {
7 | parameters: {
8 | codesandbox: {
9 | apiToken: import.meta.env.VITE_CSB_API_KEY,
10 |
11 | fallbackImport: "@radix-ui/themes",
12 |
13 | privacy: "public",
14 |
15 | dependencies: {
16 | "@radix-ui/themes": "latest",
17 | },
18 |
19 | provider: `import { Theme } from "@radix-ui/themes";
20 | import '@radix-ui/themes/styles.css';
21 |
22 | export default ThemeProvider = ({ children }) => {
23 | return (
24 |
25 | {children}
26 |
27 | )
28 | }`,
29 | },
30 | },
31 | decorators: [
32 | (Story) => (
33 |
34 |
35 |
36 | ),
37 | ],
38 | };
39 |
40 | export default preview;
41 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guidelines
2 |
3 | ## Addon structure
4 |
5 | The file outlines the structure of the addon's codebase:
6 |
7 | - `src/Tool.tsx`: "Open in CodeSandbox" button implementation and Storybook parameters parsers.
8 | - `src/manager.tsx`: addon configuration.
9 | - `.storybook`: this directory holds development configuration for testing the addon.
10 | - `./preview.tsx`: example of an ideal global parameteres setup.
11 | - `src/stories/Button.stories.ts`: example of an ideal story parameter setup.
12 |
13 | ## Commands
14 |
15 | In order to test the addon, you should run two commands:
16 |
17 | 1. `pnpm run build`: to build the CodeSandbox addon (re-run on every change);
18 | 2. `pnpm run storybook`: to open the Storybook instance with the new addon (reload every change);
19 |
20 | Or, just use `pnpm run start`
21 |
22 | ## Deployment
23 |
24 | The GitHub Action (`.github/workflows/release.yml`) should automatically publish new versions to NPM. However, it's **necessary** to manually update the `package.json#version` number to bump the library version.
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction, and
10 | distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by the
13 | copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all other
16 | entities that control, are controlled by, or are under common control with
17 | that entity. For the purposes of this definition, "control" means (i) the
18 | power, direct or indirect, to cause the direction or management of such
19 | entity, whether by contract or otherwise, or (ii) ownership of fifty percent
20 | (50%) or more of the outstanding shares, or (iii) beneficial ownership of
21 | such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity exercising
24 | permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation source, and
28 | configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical transformation
31 | or translation of a Source form, including but not limited to compiled
32 | object code, generated documentation, and conversions to other media types.
33 |
34 | "Work" shall mean the work of authorship, whether in Source or Object form,
35 | made available under the License, as indicated by a copyright notice that is
36 | included in or attached to the work (an example is provided in the Appendix
37 | below).
38 |
39 | "Derivative Works" shall mean any work, whether in Source or Object form,
40 | that is based on (or derived from) the Work and for which the editorial
41 | revisions, annotations, elaborations, or other modifications represent, as a
42 | whole, an original work of authorship. For the purposes of this License,
43 | Derivative Works shall not include works that remain separable from, or
44 | merely link (or bind by name) to the interfaces of, the Work and Derivative
45 | Works thereof.
46 |
47 | "Contribution" shall mean any work of authorship, including the original
48 | version of the Work and any modifications or additions to that Work or
49 | Derivative Works thereof, that is intentionally submitted to Licensor for
50 | inclusion in the Work by the copyright owner or by an individual or Legal
51 | Entity authorized to submit on behalf of the copyright owner. For the
52 | purposes of this definition, "submitted" means any form of electronic,
53 | verbal, or written communication sent to the Licensor or its
54 | representatives, including but not limited to communication on electronic
55 | mailing lists, source code control systems, and issue tracking systems that
56 | are managed by, or on behalf of, the Licensor for the purpose of discussing
57 | and improving the Work, but excluding communication that is conspicuously
58 | marked or otherwise designated in writing by the copyright owner as "Not a
59 | Contribution."
60 |
61 | "Contributor" shall mean Licensor and any individual or Legal Entity on
62 | behalf of whom a Contribution has been received by Licensor and subsequently
63 | incorporated within the Work.
64 |
65 | 2. Grant of Copyright License. Subject to the terms and conditions of this
66 | License, each Contributor hereby grants to You a perpetual, worldwide,
67 | non-exclusive, no-charge, royalty-free, irrevocable copyright license to
68 | reproduce, prepare Derivative Works of, publicly display, publicly perform,
69 | sublicense, and distribute the Work and such Derivative Works in Source or
70 | Object form.
71 |
72 | 3. Grant of Patent License. Subject to the terms and conditions of this
73 | License, each Contributor hereby grants to You a perpetual, worldwide,
74 | non-exclusive, no-charge, royalty-free, irrevocable (except as stated in
75 | this section) patent license to make, have made, use, offer to sell, sell,
76 | import, and otherwise transfer the Work, where such license applies only to
77 | those patent claims licensable by such Contributor that are necessarily
78 | infringed by their Contribution(s) alone or by combination of their
79 | Contribution(s) with the Work to which such Contribution(s) was submitted.
80 | If You institute patent litigation against any entity (including a
81 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a
82 | Contribution incorporated within the Work constitutes direct or contributory
83 | patent infringement, then any patent licenses granted to You under this
84 | License for that Work shall terminate as of the date such litigation is
85 | filed.
86 |
87 | 4. Redistribution. You may reproduce and distribute copies of the Work or
88 | Derivative Works thereof in any medium, with or without modifications, and
89 | in Source or Object form, provided that You meet the following conditions:
90 |
91 | (a) You must give any other recipients of the Work or Derivative Works a
92 | copy of this License; and
93 |
94 | (b) You must cause any modified files to carry prominent notices stating
95 | that You changed the files; and
96 |
97 | (c) You must retain, in the Source form of any Derivative Works that You
98 | distribute, all copyright, patent, trademark, and attribution notices from
99 | the Source form of the Work, excluding those notices that do not pertain to
100 | any part of the Derivative Works; and
101 |
102 | (d) If the Work includes a "NOTICE" text file as part of its distribution,
103 | then any Derivative Works that You distribute must include a readable copy
104 | of the attribution notices contained within such NOTICE file, excluding
105 | those notices that do not pertain to any part of the Derivative Works, in at
106 | least one of the following places: within a NOTICE text file distributed as
107 | part of the Derivative Works; within the Source form or documentation, if
108 | provided along with the Derivative Works; or, within a display generated by
109 | the Derivative Works, if and wherever such third-party notices normally
110 | appear. The contents of the NOTICE file are for informational purposes only
111 | and do not modify the License. You may add Your own attribution notices
112 | within Derivative Works that You distribute, alongside or as an addendum to
113 | the NOTICE text from the Work, provided that such additional attribution
114 | notices cannot be construed as modifying the License.
115 |
116 | You may add Your own copyright statement to Your modifications and may
117 | provide additional or different license terms and conditions for use,
118 | reproduction, or distribution of Your modifications, or for any such
119 | Derivative Works as a whole, provided Your use, reproduction, and
120 | distribution of the Work otherwise complies with the conditions stated in
121 | this License.
122 |
123 | 5. Submission of Contributions. Unless You explicitly state otherwise, any
124 | Contribution intentionally submitted for inclusion in the Work by You to the
125 | Licensor shall be under the terms and conditions of this License, without
126 | any additional terms or conditions. Notwithstanding the above, nothing
127 | herein shall supersede or modify the terms of any separate license agreement
128 | you may have executed with Licensor regarding such Contributions.
129 |
130 | 6. Trademarks. This License does not grant permission to use the trade names,
131 | trademarks, service marks, or product names of the Licensor, except as
132 | required for reasonable and customary use in describing the origin of the
133 | Work and reproducing the content of the NOTICE file.
134 |
135 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in
136 | writing, Licensor provides the Work (and each Contributor provides its
137 | Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
138 | KIND, either express or implied, including, without limitation, any
139 | warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or
140 | FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining
141 | the appropriateness of using or redistributing the Work and assume any risks
142 | associated with Your exercise of permissions under this License.
143 |
144 | 8. Limitation of Liability. In no event and under no legal theory, whether in
145 | tort (including negligence), contract, or otherwise, unless required by
146 | applicable law (such as deliberate and grossly negligent acts) or agreed to
147 | in writing, shall any Contributor be liable to You for damages, including
148 | any direct, indirect, special, incidental, or consequential damages of any
149 | character arising as a result of this License or out of the use or inability
150 | to use the Work (including but not limited to damages for loss of goodwill,
151 | work stoppage, computer failure or malfunction, or any and all other
152 | commercial damages or losses), even if such Contributor has been advised of
153 | the possibility of such damages.
154 |
155 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or
156 | Derivative Works thereof, You may choose to offer, and charge a fee for,
157 | acceptance of support, warranty, indemnity, or other liability obligations
158 | and/or rights consistent with this License. However, in accepting such
159 | obligations, You may act only on Your own behalf and on Your sole
160 | responsibility, not on behalf of any other Contributor, and only if You
161 | agree to indemnify, defend, and hold each Contributor harmless for any
162 | liability incurred by, or claims asserted against, such Contributor by
163 | reason of your accepting any such warranty or additional liability.
164 |
165 | END OF TERMS AND CONDITIONS
166 |
167 | APPENDIX: How to apply the Apache License to your work.
168 |
169 | To apply the Apache License to your work, attach the following
170 | boilerplate notice, with the fields enclosed by brackets "[]"
171 | replaced with your own identifying information. (Don't include
172 | the brackets!) The text should be enclosed in the appropriate
173 | comment syntax for the file format. We also recommend that a
174 | file or class name and description of purpose be included on the
175 | same "printed page" as the copyright notice for easier
176 | identification within third-party archives.
177 |
178 | Copyright 2022 CodeSandbox BV
179 |
180 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use
181 | this file except in compliance with the License. You may obtain a copy of the
182 | License at
183 |
184 | http://www.apache.org/licenses/LICENSE-2.0
185 |
186 | Unless required by applicable law or agreed to in writing, software distributed
187 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
188 | CONDITIONS OF ANY KIND, either express or implied. See the License for the
189 | specific language governing permissions and limitations under the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Storybook CodeSandbox Addon
6 |
7 | [](https://codesandbox.io/p/github/codesandbox/storybook-addon)
8 |
9 | The `@codesandbox/storybook-addon` is a Storybook addon that allows users accessing a story-book page to export the code from the story as a CodeSandbox Sandbox.
10 | Sandboxes will always be created within the same workspace providing a closed-circuit feedback system. The add-on also provides support for private dependencies.
11 |
12 | ## Usage
13 |
14 | Once configured, you can use the "Open in CodeSandbox" button within your Storybook environment to export stories to CodeSandbox effortlessly.
15 |
16 | ## Configuration
17 |
18 | ```js
19 | // .storybook/main.js
20 |
21 | module.exports = {
22 | // ...
23 | addons: ["@codesandbox/storybook-addon"],
24 | };
25 | ```
26 |
27 |
28 | Storybook configuration (required)
29 |
30 |
31 |
32 | To run the addon, you'll need to configure it in your Storybook's `.storybook/preview.js` file.
33 |
34 | ```js
35 | // .storybook/preview.js
36 |
37 | const preview: Preview = {
38 | parameters: {
39 | codesandbox: {
40 | /**
41 | * @required
42 | * Workspace API key from codesandbox.io/t/permissions.
43 | * This sandbox is created inside the given workspace
44 | * and can be shared with team members.
45 | */
46 | apiToken: "",
47 |
48 | /**
49 | * @optional
50 | * If a given sandbox id is provided, all other options
51 | * will be ignored and the addon will open the sandbox.
52 | */
53 | sandboxId: "SANDBOX-ID",
54 |
55 | /**
56 | * @optional
57 | * Pass custom files/modules into the sandbox. These files
58 | * will be added to the file system of the sandbox and can
59 | * be imported by other files
60 | */
61 | files: {
62 | // Example:
63 | "index.js": `
64 | export const foo = () => console.log("Hello World");`
65 | "App.js": `
66 | import { foo } from "./index.js";
67 |
68 | foo();`,
69 | },
70 |
71 | /**
72 | * @optional
73 | * Template preset to be used in the sandbox. This will
74 | * determine the initial setup of the sandbox, such as
75 | * bundler, dependencies, and files.
76 | */
77 | template: "react" | "angular", // Defaults to "react"
78 |
79 | /**
80 | * @optional
81 | * Dependencies list to be installed in the sandbox.
82 | *
83 | * @note You cannot use local modules or packages since
84 | * this story runs in an isolated environment (sandbox)
85 | * inside CodeSandbox. As such, the sandbox doesn't have
86 | * access to your file system.
87 | *
88 | * Example:
89 | */
90 | dependencies: {
91 | "@radix-ui/themes": "latest",
92 | "@myscope/mypackage": "1.0.0",
93 | },
94 |
95 | /**
96 | * @required
97 | * CodeSandbox will try to import all components by default from
98 | * the given package, in case `mapComponent` property is not provided.
99 | *
100 | * This property is useful when your components imports are predictable
101 | * and come from a single package and entry point.
102 | */
103 | fallbackImport: "@radix-ui/themes",
104 |
105 | /**
106 | * @optional
107 | * The default visibility of the new sandboxes inside the workspace.
108 | *
109 | * @note Use `private` if there is a private registry or private NPM
110 | * configured in your workspace.
111 | */
112 | privacy: "private" | "public",
113 |
114 | /**
115 | * @optional
116 | * All required providers to run the sandbox properly,
117 | * such as themes, i18n, store, and so on.
118 | *
119 | * @note Remember to use only the dependencies listed above.
120 | *
121 | * Example:
122 | */
123 | provider: `import { Theme } from "@radix-ui/themes";
124 | import '@radix-ui/themes/styles.css';
125 |
126 | export default ThemeProvider = ({ children }) => {
127 | return (
128 |
129 | {children}
130 |
131 | )
132 | }`,
133 |
134 | /**
135 | * @optional
136 | * Query parameters to be passed to the sandbox url that is opened.
137 | */
138 | queryParams: {
139 | "file": "/src/App.js",
140 | },
141 | },
142 | },
143 | };
144 |
145 | export default preview;
146 | ```
147 |
148 |
149 |
150 |
151 | Story configuration (recommended)
152 |
153 | ````ts
154 | import type { Meta, StoryObj } from "@storybook/react";
155 |
156 | const meta: Meta = {
157 | title: "Example/Button",
158 | component: Button,
159 | parameters: {
160 | codesandbox: {
161 | /**
162 | * To import all components used within each story in
163 | * CodeSandbox, provide all necessary packages and modules.
164 | *
165 | * Given the following story:
166 | * ```js
167 | * import Provider from "@myscope/mypackage";
168 | * import { Button } from "@radix-ui/themes";
169 | * import "@radix-ui/themes/styles.css";
170 | * ```
171 | *
172 | * You need to map all imports to the following:
173 | */
174 | mapComponent: {
175 | // Example of default imports
176 | "@myscope/mypackage": "Provider",
177 |
178 | // Example of named functions
179 | "@radix-ui/themes": ["Button"],
180 |
181 | // Example of static imports
182 | "@radix-ui/themes/styles.css": true,
183 | },
184 |
185 | /**
186 | * @note You cannot use local modules or packages since
187 | * this story runs in an isolated environment (sandbox)
188 | * inside CodeSandbox. As such, the sandbox doesn't have
189 | * access to your file system.
190 | */
191 | },
192 | },
193 | };
194 | ````
195 |
196 |
197 |
198 |
199 |
200 | Make sure to provide the necessary values for [`apiToken`](https://codesandbox.io/t/permissions) and any additional dependencies or providers required for your specific setup.
201 |
202 | For now, this addon only support [Component Story Format (CSF)](Component Story Format (CSF)) stories format.
203 |
204 | ## Additional Notes
205 |
206 | - Ensure that you have proper permissions and access rights to the CodeSandbox workspace specified in the configuration.
207 | - Verify the correctness of the dependencies and providers listed in the configuration to ensure the sandbox runs smoothly.
208 |
--------------------------------------------------------------------------------
/manager.js:
--------------------------------------------------------------------------------
1 | import './dist/manager';
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@codesandbox/storybook-addon",
3 | "version": "0.2.2",
4 | "description": "CSB",
5 | "keywords": [
6 | "storybook-addons"
7 | ],
8 | "repository": {
9 | "type": "git",
10 | "url": "github.com"
11 | },
12 | "packageManager": "pnpm@9.15.0",
13 | "type": "module",
14 | "license": "Apache-2.0",
15 | "author": "CodeSandbox ",
16 | "exports": {
17 | ".": {
18 | "types": "./dist/index.d.ts",
19 | "require": "./dist/index.cjs",
20 | "import": "./dist/index.js"
21 | },
22 | "./preset": "./dist/preset.cjs",
23 | "./manager": "./dist/manager.js",
24 | "./package.json": "./package.json"
25 | },
26 | "files": [
27 | "dist/**/*",
28 | "README.md",
29 | "*.js",
30 | "*.d.ts"
31 | ],
32 | "scripts": {
33 | "start": "pnpm run build && pnpm run storybook",
34 | "clean": "rimraf ./dist",
35 | "prebuild": "npm run clean",
36 | "build": "tsup",
37 | "build:watch": "npm run build -- --watch",
38 | "test": "echo \"Error: no test specified\" && exit 1",
39 | "prepublish": "zx scripts/prepublish-checks.js && npm run build",
40 | "eject-ts": "zx scripts/eject-typescript.js",
41 | "storybook": "storybook dev -p 6006",
42 | "build-storybook": "storybook build"
43 | },
44 | "devDependencies": {
45 | "@radix-ui/themes": "^2.0.3",
46 | "@storybook/addon-essentials": "^7.6.6",
47 | "@storybook/addon-interactions": "^7.6.6",
48 | "@storybook/addon-links": "^7.6.6",
49 | "@storybook/blocks": "^7.6.6",
50 | "@storybook/core-events": "^7.6.6",
51 | "@storybook/manager": "^7.6.6",
52 | "@storybook/manager-api": "^7.6.6",
53 | "@storybook/preview": "^7.6.6",
54 | "@storybook/preview-api": "^7.6.6",
55 | "@storybook/react": "^7.6.6",
56 | "@storybook/react-vite": "^7.6.6",
57 | "@storybook/testing-library": "^0.2.2",
58 | "@storybook/theming": "^7.6.6",
59 | "@storybook/types": "^7.6.6",
60 | "@types/node": "^18.15.0",
61 | "@types/react": "^18.2.55",
62 | "@types/react-dom": "^18.2.19",
63 | "@vitejs/plugin-react": "^4.2.1",
64 | "boxen": "^7.1.1",
65 | "dedent": "^1.5.1",
66 | "npm-run-all": "^4.1.5",
67 | "prompts": "^2.4.2",
68 | "react": "^18.2.0",
69 | "react-dom": "^18.2.0",
70 | "rimraf": "^5.0.5",
71 | "storybook": "^7.6.6",
72 | "tsup": "^8.0.2",
73 | "typescript": "^5.3.3",
74 | "vite": "^5.1.2",
75 | "zx": "^7.2.3"
76 | },
77 | "publishConfig": {
78 | "access": "public"
79 | },
80 | "bundler": {
81 | "exportEntries": [
82 | "src/index.ts"
83 | ],
84 | "managerEntries": [
85 | "src/manager.tsx"
86 | ],
87 | "nodeEntries": [
88 | "src/preset.ts"
89 | ]
90 | },
91 | "storybook": {
92 | "displayName": "CodeSandbox",
93 | "supportedFrameworks": [
94 | "react"
95 | ],
96 | "icon": "https://user-images.githubusercontent.com/321738/63501763-88dbf600-c4cc-11e9-96cd-94adadc2fd72.png"
97 | },
98 | "dependencies": {
99 | "@storybook/addon-docs": "^7.6.17",
100 | "@storybook/addon-storysource": "^7.6.17",
101 | "@storybook/docs-tools": "^7.6.17",
102 | "@storybook/icons": "^1.2.9",
103 | "prettier": "^3.2.5"
104 | }
105 | }
--------------------------------------------------------------------------------
/preset.js:
--------------------------------------------------------------------------------
1 | // this file is slightly misleading. It needs to be CJS, and thus in this "type": "module" package it should be named preset.cjs
2 | // but Storybook won't pick that filename up so we have to name it preset.js instead
3 |
4 | module.exports = require('./dist/preset.cjs');
5 |
--------------------------------------------------------------------------------
/preview.js:
--------------------------------------------------------------------------------
1 | export * from './dist/preview';
2 |
--------------------------------------------------------------------------------
/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, { memo, useEffect, useState } from "react";
2 | import { API, useParameter, useStorybookApi } from "@storybook/manager-api";
3 | import { BoxIcon } from "@storybook/icons";
4 | import { IconButton } from "@storybook/components";
5 |
6 | import { TOOL_ID } from "./constants";
7 | import { convertSandboxToTemplate, parseFileTree, parseImports } from "./utils";
8 | const SNIPPET_RENDERED = `storybook/docs/snippet-rendered`;
9 |
10 | export type CSBParameters =
11 | | {
12 | apiToken: string;
13 | privacy?: "private" | "public";
14 | fallbackImport?: string;
15 | mapComponent?: Record;
16 | dependencies?: Record;
17 | provider?: string;
18 | sandboxId: string;
19 | files: Record;
20 | template?: "react" | "angular";
21 | queryParams?: Record;
22 | }
23 | | undefined;
24 |
25 | function createUrl(
26 | sandboxId: string,
27 | queryParams: Record,
28 | ): URL {
29 | const url = new URL(`https://codesandbox.io/p/sandbox/${sandboxId}`);
30 | url.searchParams.set("file", "/src/App.js");
31 | for (const [key, value] of Object.entries(queryParams)) {
32 | url.searchParams.set(key, value);
33 | }
34 | url.searchParams.set("utm-source", "storybook-addon");
35 | return url;
36 | }
37 |
38 | export const CodeSandboxTool = memo(function MyAddonSelector({
39 | api,
40 | }: {
41 | api: API;
42 | }) {
43 | const { getCurrentStoryData, addNotification } = useStorybookApi();
44 | const storyData = getCurrentStoryData();
45 | const codesandboxParameters: CSBParameters = useParameter("codesandbox");
46 |
47 | const [storySource, setStorySource] = useState();
48 | const [loading, setLoading] = useState(false);
49 |
50 | useEffect(function getStorySourceCode() {
51 | api
52 | .getChannel()
53 | .on(SNIPPET_RENDERED, ({ source }) => setStorySource(source));
54 | }, []);
55 |
56 | /**
57 | * Options
58 | */
59 | const options = {
60 | template: codesandboxParameters?.template ?? "react",
61 | mapComponent: codesandboxParameters?.mapComponent ?? {},
62 | dependencies: codesandboxParameters?.dependencies ?? {},
63 | provider:
64 | codesandboxParameters?.provider ??
65 | `export default GenericProvider = ({ children }) => {
66 | return children
67 | }`,
68 | files: codesandboxParameters?.files ?? {},
69 | apiToken: codesandboxParameters?.apiToken,
70 | sandboxId: codesandboxParameters?.sandboxId,
71 | queryParams: codesandboxParameters?.queryParams ?? {},
72 | };
73 |
74 | async function createSandbox() {
75 | try {
76 | if (options.sandboxId) {
77 | window.open(
78 | createUrl(options.sandboxId, options.queryParams).toString(),
79 | "_blank",
80 | );
81 |
82 | return;
83 | }
84 |
85 | setLoading(true);
86 |
87 | const { fallbackImport } = codesandboxParameters;
88 | const importsMap = options.mapComponent;
89 |
90 | // If fallbackImport is provided, add it to importsMap
91 | if (fallbackImport) {
92 | const componentNames = parseFileTree(storySource);
93 |
94 | // Check if fallbackImport is already in importsMap
95 | if (importsMap[fallbackImport]) {
96 | const currentFallbackImport = importsMap[fallbackImport];
97 |
98 | // Merge them
99 | if (Array.isArray(currentFallbackImport)) {
100 | importsMap[fallbackImport] = [
101 | ...new Set([...componentNames, ...currentFallbackImport]),
102 | ];
103 | } else {
104 | // Invalid use case
105 | throw new Error(
106 | "Invalid fallback import usage. The `import` used inside `mapComponent` and also used as `fallbackImport` must be an array.",
107 | );
108 | }
109 | } else {
110 | // Just added (0-config case)
111 | importsMap[fallbackImport] = componentNames;
112 | }
113 | }
114 |
115 | const imports = parseImports(importsMap);
116 |
117 | const files = await convertSandboxToTemplate({
118 | ...options,
119 | imports,
120 | storySource,
121 | });
122 |
123 | if (!codesandboxParameters.apiToken) {
124 | throw new Error("Missing `apiToken` property");
125 | }
126 |
127 | const response = await fetch("https://api.codesandbox.io/sandbox", {
128 | method: "POST",
129 | body: JSON.stringify({
130 | title: `${storyData.title} - Storybook`,
131 | files,
132 | privacy: codesandboxParameters.privacy === "public" ? 0 : 2,
133 | }),
134 | headers: {
135 | Authorization: `Bearer ${codesandboxParameters.apiToken}`,
136 | "Content-Type": "application/json",
137 | "X-CSB-API-Version": "2023-07-01",
138 | },
139 | });
140 |
141 | const data: { data: { alias: string } } = await response.json();
142 |
143 | window.open(
144 | createUrl(data.data.alias, options.queryParams).toString(),
145 | "_blank",
146 | );
147 |
148 | setLoading(false);
149 | } catch (error) {
150 | setLoading(false);
151 |
152 | addNotification({
153 | content: {
154 | headline: "CodeSandbox: something went wrong",
155 | subHeadline:
156 | "Make sure you have a valid API token, or check the console for more details.",
157 | },
158 | id: "csb-error",
159 | link: "",
160 | });
161 |
162 | console.error(error.message);
163 |
164 | throw error;
165 | }
166 | }
167 |
168 | if (!codesandboxParameters || !storySource) {
169 | return;
170 | }
171 |
172 | return (
173 |
185 |
186 |
187 | {loading ? "Loading..." : "Open in CodeSandbox"}
188 |
189 | );
190 | });
191 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const ADDON_ID = "storybook/codesandbox";
2 | export const TOOL_ID = `${ADDON_ID}/tool`;
3 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export default {};
2 |
--------------------------------------------------------------------------------
/src/manager.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { addons, types } from "@storybook/manager-api";
3 | import { ADDON_ID, TOOL_ID } from "./constants";
4 | import { CodeSandboxTool } from "./Tool";
5 |
6 | addons.register(ADDON_ID, (api) => {
7 | addons.add(TOOL_ID, {
8 | type: types.TOOL,
9 | title: "CodeSandbox",
10 | match: ({ viewMode }) => !!(viewMode && viewMode.match(/^(story|docs)$/)),
11 | render: () => ,
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/src/preset.ts:
--------------------------------------------------------------------------------
1 | export const viteFinal = async (config: any) => {
2 | return config;
3 | };
4 |
5 | export const webpack = async (config: any) => {
6 | return config;
7 | };
8 |
--------------------------------------------------------------------------------
/src/stories/Button.stories.ts:
--------------------------------------------------------------------------------
1 | import type { Meta, StoryObj } from "@storybook/react";
2 | import "@radix-ui/themes/styles.css";
3 |
4 | import { Button } from "@radix-ui/themes";
5 |
6 | const meta: Meta = {
7 | title: "Example/Button",
8 | component: Button,
9 | argTypes: {
10 | variant: {
11 | options: ["classic", "soft", "outline"],
12 | control: { type: "radio" },
13 | },
14 | size: {
15 | options: ["1", "2", "3", "4"],
16 | control: { type: "radio" },
17 | },
18 | },
19 | parameters: {
20 | codesandbox: {
21 | template: "angular",
22 | files: {
23 | "/src/app/app.component.css": `button {
24 | }`,
25 | },
26 | privacy: "private",
27 | mapComponent: {
28 | "@myscope/mypackage": "Provider",
29 | "@radix-ui/themes": ["Button"],
30 | "@radix-ui/themes/styles.css": true,
31 | },
32 | },
33 | },
34 | };
35 |
36 | export default meta;
37 | type Story = StoryObj;
38 |
39 | export const Primary: Story = {
40 | args: {
41 | variant: "classic",
42 | children: "Button",
43 | },
44 | };
45 |
46 | export const Secondary: Story = {
47 | args: {
48 | variant: "soft",
49 | children: "Button",
50 | },
51 | };
52 |
53 | export const Large: Story = {
54 | args: {
55 | variant: "outline",
56 | size: "4",
57 | children: "Button",
58 | },
59 | };
60 |
--------------------------------------------------------------------------------
/src/stories/Text.mdx:
--------------------------------------------------------------------------------
1 | import { Meta, Source, } from '@storybook/blocks';
2 | import * as TextStories from './Text.stories';
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/stories/Text.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import type { Meta, StoryObj } from "@storybook/react";
3 | import "@radix-ui/themes/styles.css";
4 |
5 | import { Text } from "@radix-ui/themes";
6 |
7 | const meta: Meta = {
8 | component: Text,
9 | argTypes: {
10 | size: {
11 | options: ["1", "2", "3", "4"],
12 | control: { type: "radio" },
13 | },
14 | },
15 | args: {
16 | children: "The quick brown fox jumps over the lazy dog.",
17 | },
18 | };
19 |
20 | export const Base: StoryObj = {};
21 |
22 | export const Small: StoryObj = {
23 | args: {
24 | size: "1",
25 | },
26 | parameters: {
27 | codesandbox: {
28 | mapComponent: {
29 | "@radix-ui/themes": ["Button"],
30 | "@radix-ui/themes/styles.css": true,
31 | },
32 | },
33 | },
34 | };
35 |
36 | export const JSX = (props) => (
37 |
38 | The quick brown fox jumps over the lazy dog. The quick brown fox jumps over
39 | the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown
40 | fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog.
41 | The quick brown fox jumps over the lazy dog. The quick brown fox jumps over
42 | the lazy dog. The quick brown fox jumps over the lazy dog.
43 |
44 | );
45 |
46 | export default meta;
47 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import prettier from "prettier/standalone";
2 | import prettierPluginBabel from "prettier/plugins/babel";
3 | import prettierPluginEstree from "prettier/plugins/estree";
4 | import ts from "typescript";
5 |
6 | import { CSBParameters } from "./Tool";
7 |
8 | interface SandpackBundlerFile {
9 | code: string;
10 | hidden?: boolean;
11 | active?: boolean;
12 | readOnly?: boolean;
13 | }
14 |
15 | type SandpackBundlerFiles = Record;
16 |
17 | function standardizeFilesFormat(
18 | files: Record,
19 | ): SandpackBundlerFiles {
20 | return Object.entries(files).reduce((acc, [key, value]) => {
21 | if (typeof value === "string") {
22 | return {
23 | ...acc,
24 | [key]: {
25 | code: value,
26 | },
27 | };
28 | }
29 |
30 | return { ...acc, [key]: value };
31 | }, {});
32 | }
33 |
34 | export const parseImports = (
35 | mapComponent: Required,
36 | ) => {
37 | let imports = ``;
38 |
39 | for (const [key, value] of Object.entries(mapComponent)) {
40 | if (Array.isArray(value)) {
41 | imports += `import { ${value.join(", ")} } from '${key}';\n`;
42 | } else if (value === true) {
43 | imports += `import '${key}';\n`;
44 | } else if (typeof value === "string") {
45 | imports += `import ${value} from '${key}';\n`;
46 | }
47 | }
48 |
49 | return imports;
50 | };
51 |
52 | /**
53 | * Parse the source code with TreeSitter and return the component names
54 | */
55 | export function parseFileTree(sourceCode: string): Array {
56 | const parsed = ts.createSourceFile(
57 | "story.tsx",
58 | sourceCode,
59 | ts.ScriptTarget.ESNext,
60 | );
61 |
62 | const program = ts.createProgram({
63 | rootNames: ["/story.tsx"],
64 | host: {
65 | readFile(_fileName) {
66 | return sourceCode;
67 | },
68 | getSourceFile(_fileName) {
69 | return parsed;
70 | },
71 | writeFile() {},
72 | getDefaultLibFileName() {
73 | return "lib.d.ts";
74 | },
75 | useCaseSensitiveFileNames() {
76 | return false;
77 | },
78 | getCurrentDirectory() {
79 | return "/";
80 | },
81 | getCanonicalFileName() {
82 | return "/story.tsx";
83 | },
84 | getNewLine() {
85 | return "\n";
86 | },
87 | fileExists() {
88 | return true;
89 | },
90 | getDirectories() {
91 | return [];
92 | },
93 | },
94 | options: {
95 | target: ts.ScriptTarget.ESNext,
96 | module: ts.ModuleKind.ESNext,
97 | },
98 | });
99 |
100 | const checker = program.getTypeChecker();
101 |
102 | const components: Array = [];
103 |
104 | function addTagName(tagName: ts.JsxTagNameExpression) {
105 | if (!ts.isIdentifier(tagName)) {
106 | return;
107 | }
108 |
109 | // Check if the variable is declared already in the source file,
110 | // we only want variables that are not already declared.
111 | const symbol = checker.getSymbolAtLocation(tagName);
112 | const declarations = symbol?.getDeclarations();
113 | if (declarations) {
114 | return;
115 | }
116 |
117 | const componentName = tagName.getText(parsed);
118 | if (componentName[0].toUpperCase() === componentName[0]) {
119 | components.push(componentName);
120 | }
121 | }
122 |
123 | function walk(node: ts.Node) {
124 | if (ts.isJsxOpeningElement(node)) {
125 | addTagName(node.tagName);
126 | } else if (ts.isJsxSelfClosingElement(node)) {
127 | addTagName(node.tagName);
128 | }
129 |
130 | node.forEachChild((w) => walk(w));
131 | }
132 | walk(parsed);
133 |
134 | return components;
135 | }
136 |
137 | type Props = {
138 | storySource: string;
139 | imports: string;
140 | } & CSBParameters;
141 |
142 | export function convertSandboxToTemplate(
143 | props: Props,
144 | ): Promise {
145 | switch (props.template) {
146 | case "react":
147 | return createReactTemplate(props);
148 | case "angular":
149 | return createAngularTemplate(props);
150 | default:
151 | return;
152 | }
153 | }
154 |
155 | async function createAngularTemplate(
156 | props: Props,
157 | ): Promise {
158 | const files = {
159 | "/src/app/app.component.css": {
160 | code: `body {
161 | font-family: sans-serif;
162 | -webkit-font-smoothing: auto;
163 | -moz-font-smoothing: auto;
164 | -moz-osx-font-smoothing: grayscale;
165 | font-smoothing: auto;
166 | text-rendering: optimizeLegibility;
167 | font-smooth: always;
168 | -webkit-tap-highlight-color: transparent;
169 | -webkit-touch-callout: none;
170 | }
171 | h1 {
172 | font-size: 1.5rem;
173 | }`,
174 | },
175 | "/src/app/app.component.html": {
176 | code: `