diff --git a/.storybook/local-preset.js b/.storybook/local-preset.js index d899fe6..13b0411 100644 --- a/.storybook/local-preset.js +++ b/.storybook/local-preset.js @@ -1,3 +1,5 @@ +import distPreset from '../dist/preset.cjs'; + /** * to load the built addon in this test Storybook */ @@ -12,4 +14,5 @@ function managerEntries(entry = []) { module.exports = { managerEntries, previewAnnotations, + ...distPreset, }; diff --git a/README.md b/README.md index c198831..359a3f0 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ # Storybook Addon Test Codegen -[![NPM version](https://badge.fury.io/js/storybook-addon-test-codegen.svg)](https://www.npmjs.com/package/storybook-addon-test-codegen) +[![NPM version](https://img.shields.io/npm/v/storybook-addon-test-codegen)](https://www.npmjs.com/package/storybook-addon-test-codegen) [![NPM downloads](https://img.shields.io/npm/dt/storybook-addon-test-codegen)](https://www.npmjs.com/package/storybook-addon-test-codegen) [![GitHub license](https://img.shields.io/github/license/igrlk/storybook-addon-test-codegen)](https://github.com/igrlk/storybook-addon-test-codegen/blob/main/LICENSE) Interact with your Storybook and get test code generated for you. To see this live, check out the [demo](https://igrlk.github.io/storybook-addon-test-codegen/). -![Alt Text](/assets/addon.gif) + ## Installation @@ -19,7 +21,7 @@ npm install --save-dev storybook-addon-test-codegen ### Peer Dependency -This addon requires `storybook` version `>=8.2.0` to be installed in your project. Ensure you have a compatible version +This addon requires `storybook` version `>=8.3.0` to be installed in your project. Ensure you have a compatible version by running: ```sh @@ -29,6 +31,8 @@ npm install --save-dev storybook@latest If you’re not using Storybook already, you can refer to the [Storybook Getting Started Guide](https://storybook.js.org/docs) for installation instructions. +For `storybook@8.2.*`, use version `1.0.3` of this addon. + ### Register the Addon Once installed, register it as an addon in `.storybook/main.js`. @@ -55,7 +59,9 @@ export default config; Enable recording in the Interaction Recorder tab in the Storybook UI. Interact with your components as you normally would, and the addon will generate test code for you. -Copy both imports and the generated code to your test file. +Click on "Save to story" to save the generated code to the story file. Done πŸŽ‰ + +Alternatively, copy both imports and the generated code to your test file like so: ```jsx // MyComponent.stories.tsx @@ -63,7 +69,7 @@ Copy both imports and the generated code to your test file. // πŸ‘‡ Add the generated imports here import {userEvent, waitFor, within, expect} from "@storybook/test"; -export const MyComponent: Story = { +export const MyComponent = { // ...rest of the story // πŸ‘‡ Add the generated test code here diff --git a/assets/addon.mp4 b/assets/addon.mp4 new file mode 100644 index 0000000..2110a32 Binary files /dev/null and b/assets/addon.mp4 differ diff --git a/package.json b/package.json index c86da3d..b16a3bb 100644 --- a/package.json +++ b/package.json @@ -32,35 +32,37 @@ "files": ["dist/**/*", "README.md", "*.js", "*.d.ts"], "scripts": { "build": "tsup", - "build:watch": "concurrently \"npm run build -- --watch\" \"npm run tailwind:watch\"", + "build:watch": "tsup --watch", + "build-storybook": "storybook build", "check": "biome check --write", "test": "vitest run", "test:watch": "vitest", - "start": "concurrently \"pnpm build:watch\" \"pnpm storybook\"", "prerelease": "zx scripts/prepublish-checks.js", "release": "npm run build && auto shipit", "eject-ts": "zx scripts/eject-typescript.js", - "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build", + "start": "concurrently \"pnpm build:watch\" \"pnpm tailwind:watch\"", + "storybook": "storybook dev -p 6006 --no-open", "prepare": "husky", "tailwind": "tailwindcss -i ./src/tailwind.css -o ./.storybook/tailwind.css", "tailwind:watch": "pnpm tailwind -- --watch", "pr:check": "concurrently \"npm run check\" \"npm run test\" \"npm run build\"" }, "devDependencies": { + "@babel/parser": "^7.26.7", "@biomejs/biome": "^1.9.4", - "@chromatic-com/storybook": "^3.2.4", "@medv/finder": "^4.0.2", "@playwright/test": "^1.49.1", - "@storybook/addon-essentials": "^8.5.3", - "@storybook/addon-interactions": "^8.5.3", - "@storybook/addon-links": "^8.5.3", - "@storybook/addon-themes": "^8.5.3", - "@storybook/blocks": "^8.5.3", + "@storybook/addon-essentials": "^8.6.3", + "@storybook/addon-interactions": "^8.6.3", + "@storybook/addon-links": "^8.6.3", + "@storybook/addon-themes": "^8.6.3", + "@storybook/blocks": "^8.6.3", + "@storybook/csf": "^0.1.13", "@storybook/icons": "^1.3.2", - "@storybook/react": "^8.5.3", - "@storybook/react-vite": "^8.5.3", - "@storybook/test": "^8.5.3", + "@storybook/react": "^8.6.3", + "@storybook/react-vite": "^8.6.3", + "@storybook/test": "^8.6.3", + "@testing-library/dom": "^10.4.0", "@types/jsdom": "^21.1.7", "@types/node": "^18.15.0", "@types/react": "^18.2.65", @@ -68,9 +70,12 @@ "@vitejs/plugin-react": "^4.2.1", "auto": "^11.1.1", "boxen": "^7.1.1", + "concurrently": "^9.1.0", "dedent": "^1.5.1", + "dequal": "^2.0.3", "dom-accessibility-api": "^0.7.0", "globals": "^15.14.0", + "happy-dom": "^15.11.7", "husky": "^9.1.7", "jsdom": "^25.0.1", "lint-staged": "^15.2.11", @@ -79,20 +84,17 @@ "prompts": "^2.4.2", "react": "^18.2.0", "react-dom": "^18.2.0", - "storybook": "^8.5.3", + "storybook": "^8.6.3", + "tailwindcss": "^3.4.17", "tsup": "^8.2.4", "typescript": "^5.5.4", + "use-debounce": "^10.0.4", "vite": "^5.3.5", "vitest": "^2.1.8", - "zx": "^7.2.3", - "@testing-library/dom": "^10.4.0", - "concurrently": "^9.1.0", - "happy-dom": "^15.11.7", - "tailwindcss": "^3.4.17", - "use-debounce": "^10.0.4" + "zx": "^7.2.3" }, "peerDependencies": { - "storybook": ">=8.2.0" + "storybook": ">=8.3.0" }, "publishConfig": { "access": "public" @@ -100,7 +102,8 @@ "bundler": { "exportEntries": ["src/index.ts"], "managerEntries": ["src/manager.tsx"], - "previewEntries": ["src/preview.ts"] + "previewEntries": ["src/preview.ts"], + "nodeEntries": ["src/preset.ts"] }, "storybook": { "displayName": "Test Codegen", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e2db04..885ccbd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,12 +8,12 @@ importers: .: devDependencies: + '@babel/parser': + specifier: ^7.26.7 + version: 7.26.7 '@biomejs/biome': specifier: ^1.9.4 version: 1.9.4 - '@chromatic-com/storybook': - specifier: ^3.2.4 - version: 3.2.4(react@18.3.1)(storybook@8.5.3(prettier@3.4.2)) '@medv/finder': specifier: ^4.0.2 version: 4.0.2 @@ -21,32 +21,35 @@ importers: specifier: ^1.49.1 version: 1.49.1 '@storybook/addon-essentials': - specifier: ^8.5.3 - version: 8.5.3(@types/react@18.3.18)(storybook@8.5.3(prettier@3.4.2)) + specifier: ^8.6.3 + version: 8.6.3(@types/react@18.3.18)(storybook@8.6.3(prettier@3.4.2)) '@storybook/addon-interactions': - specifier: ^8.5.3 - version: 8.5.3(storybook@8.5.3(prettier@3.4.2)) + specifier: ^8.6.3 + version: 8.6.3(storybook@8.6.3(prettier@3.4.2)) '@storybook/addon-links': - specifier: ^8.5.3 - version: 8.5.3(react@18.3.1)(storybook@8.5.3(prettier@3.4.2)) + specifier: ^8.6.3 + version: 8.6.3(react@18.3.1)(storybook@8.6.3(prettier@3.4.2)) '@storybook/addon-themes': - specifier: ^8.5.3 - version: 8.5.3(storybook@8.5.3(prettier@3.4.2)) + specifier: ^8.6.3 + version: 8.6.3(storybook@8.6.3(prettier@3.4.2)) '@storybook/blocks': - specifier: ^8.5.3 - version: 8.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.3(prettier@3.4.2)) + specifier: ^8.6.3 + version: 8.6.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.3(prettier@3.4.2)) + '@storybook/csf': + specifier: ^0.1.13 + version: 0.1.13 '@storybook/icons': specifier: ^1.3.2 version: 1.3.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@storybook/react': - specifier: ^8.5.3 - version: 8.5.3(@storybook/test@8.5.3(storybook@8.5.3(prettier@3.4.2)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.3(prettier@3.4.2))(typescript@5.7.2) + specifier: ^8.6.3 + version: 8.6.3(@storybook/test@8.6.3(storybook@8.6.3(prettier@3.4.2)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.3(prettier@3.4.2))(typescript@5.7.2) '@storybook/react-vite': - specifier: ^8.5.3 - version: 8.5.3(@storybook/test@8.5.3(storybook@8.5.3(prettier@3.4.2)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.29.1)(storybook@8.5.3(prettier@3.4.2))(typescript@5.7.2)(vite@5.4.11(@types/node@18.19.68)) + specifier: ^8.6.3 + version: 8.6.3(@storybook/test@8.6.3(storybook@8.6.3(prettier@3.4.2)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.29.1)(storybook@8.6.3(prettier@3.4.2))(typescript@5.7.2)(vite@5.4.11(@types/node@18.19.68)) '@storybook/test': - specifier: ^8.5.3 - version: 8.5.3(storybook@8.5.3(prettier@3.4.2)) + specifier: ^8.6.3 + version: 8.6.3(storybook@8.6.3(prettier@3.4.2)) '@testing-library/dom': specifier: ^10.4.0 version: 10.4.0 @@ -77,6 +80,9 @@ importers: dedent: specifier: ^1.5.1 version: 1.5.3(babel-plugin-macros@3.1.0) + dequal: + specifier: ^2.0.3 + version: 2.0.3 dom-accessibility-api: specifier: ^0.7.0 version: 0.7.0 @@ -111,8 +117,8 @@ importers: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) storybook: - specifier: ^8.5.3 - version: 8.5.3(prettier@3.4.2) + specifier: ^8.6.3 + version: 8.6.3(prettier@3.4.2) tailwindcss: specifier: ^3.4.17 version: 3.4.17(ts-node@10.9.2(@types/node@18.19.68)(typescript@5.7.2)) @@ -190,6 +196,10 @@ packages: resolution: {integrity: sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==} engines: {node: '>=6.9.0'} + '@babel/generator@7.26.8': + resolution: {integrity: sha512-ef383X5++iZHWAXX0SXQR6ZyQhw/0KtTkrTz61WXRhFM6dhpHulO/RJz79L8S6ugZHJkOOkUrUdxgdF2YiPFnA==} + engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.25.9': resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==} engines: {node: '>=6.9.0'} @@ -224,8 +234,13 @@ packages: resolution: {integrity: sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==} engines: {node: '>=6.9.0'} - '@babel/parser@7.26.3': - resolution: {integrity: sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==} + '@babel/parser@7.26.7': + resolution: {integrity: sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/parser@7.26.8': + resolution: {integrity: sha512-TZIQ25pkSoaKEYYaHbbxkfL36GNsQ6iFiBbeuzAkLnXayKR1yP1zFe+NxuZWWsUyvt8icPU9CCq0sgWGXR1GEw==} engines: {node: '>=6.0.0'} hasBin: true @@ -249,14 +264,30 @@ packages: resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} engines: {node: '>=6.9.0'} + '@babel/template@7.26.8': + resolution: {integrity: sha512-iNKaX3ZebKIsCvJ+0jd6embf+Aulaa3vNBqZ41kM7iTWjx5qzWKXGHiJUW3+nTpQ18SG11hdF8OAzKrpXkb96Q==} + engines: {node: '>=6.9.0'} + '@babel/traverse@7.26.4': resolution: {integrity: sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==} engines: {node: '>=6.9.0'} + '@babel/traverse@7.26.8': + resolution: {integrity: sha512-nic9tRkjYH0oB2dzr/JoGIm+4Q6SuYeLEiIiZDwBscRMYFJ+tMAz98fuel9ZnbXViA2I0HVSSRRK8DW5fjXStA==} + engines: {node: '>=6.9.0'} + '@babel/types@7.26.3': resolution: {integrity: sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==} engines: {node: '>=6.9.0'} + '@babel/types@7.26.7': + resolution: {integrity: sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.26.8': + resolution: {integrity: sha512-eUuWapzEGWFEpHFxgEaBG8e3n6S8L3MSu0oda755rOfabWPnh0Our1AozNFVUxGFIhbKgd1ksprsoDGMinTOTA==} + engines: {node: '>=6.9.0'} + '@biomejs/biome@1.9.4': resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} engines: {node: '>=14.21.3'} @@ -310,12 +341,6 @@ packages: cpu: [x64] os: [win32] - '@chromatic-com/storybook@3.2.4': - resolution: {integrity: sha512-5/bOOYxfwZ2BktXeqcCpOVAoR6UCoeART5t9FVy22hoo8F291zOuX4y3SDgm10B1GVU/ZTtJWPT2X9wZFlxYLg==} - engines: {node: '>=16.0.0', yarn: '>=1.22.18'} - peerDependencies: - storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -618,8 +643,8 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} - '@joshwooding/vite-plugin-react-docgen-typescript@0.4.2': - resolution: {integrity: sha512-feQ+ntr+8hbVudnsTUapiMN9q8T90XA1d5jn9QzY09sNoj4iD9wi0PY1vsBFTda4ZjEaxRK9S81oarR2nj7TFQ==} + '@joshwooding/vite-plugin-react-docgen-typescript@0.5.0': + resolution: {integrity: sha512-qYDdL7fPwLRI+bJNurVcis+tNgJmvWjH4YTBGXTA8xMuxFrnAz6E5o35iyzyKbq5J5Lr8mJGfrR5GXl+WGwhgQ==} peerDependencies: typescript: '>= 4.3.x' vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 @@ -835,113 +860,113 @@ packages: cpu: [x64] os: [win32] - '@storybook/addon-actions@8.5.3': - resolution: {integrity: sha512-7a+SD4EZdZocm+NG1Kx4yV6Aw7+YUlRIyGvKcxsGtYMOLaqrUewApqveXF83+FbYWMoezXcoZCLQFROtS/Z6Fw==} + '@storybook/addon-actions@8.6.3': + resolution: {integrity: sha512-0UrVqRoZFRFCqjtR8ODacpJNqi47qDUnsnB5F7e93U9ihSrH2edOBBX6frl11XKYA23rzq7jtnviFTVOpWpG7Q==} peerDependencies: - storybook: ^8.5.3 + storybook: ^8.6.3 - '@storybook/addon-backgrounds@8.5.3': - resolution: {integrity: sha512-sZcw8/C/HIIgbRBY+0ZYTBc5Py8xvw3bt6lzSVQEXA2aygfJpO/jiQJlmOXTmK3g5F5pjFKaaCodfXT7V/9mzw==} + '@storybook/addon-backgrounds@8.6.3': + resolution: {integrity: sha512-2mmMpMyUsS8rti2guMR4rk4h5YBLNHidxUqTm+U4nITZFfCXNP76To9hfTczpLTvUEpPxSbPG0sCIeHFaw4NRQ==} peerDependencies: - storybook: ^8.5.3 + storybook: ^8.6.3 - '@storybook/addon-controls@8.5.3': - resolution: {integrity: sha512-A4UVQhPyC7FvV+fM50xvEZO26/2uE41Ns0TN0qq7U5EH0Dlj43Salgay6qT8fve6XAI4SgVjkujPVCSbLg/yVQ==} + '@storybook/addon-controls@8.6.3': + resolution: {integrity: sha512-j4Oof3nwjyiO6oNP1bJ98Sz1iZlYhdcgHX284yd0wBO91Q5B2GoCeqyCE+yRCh752ZnnYG1gazJrHmiG6gKxVg==} peerDependencies: - storybook: ^8.5.3 + storybook: ^8.6.3 - '@storybook/addon-docs@8.5.3': - resolution: {integrity: sha512-XVcQlHX963nuoeRkb7qQg89t/9CThdT46UV7jX3FFn08NEMhmDEa+4iVA4l+4xNgJ+Av6uX+u6yRGnM/910mLg==} + '@storybook/addon-docs@8.6.3': + resolution: {integrity: sha512-FRABH+r2huMpAK8iUQiFlYZtYenbqtudX3fNKFK9b38eV1R14kWggVG02lsa6upXbzxWVbMLUdOqaZJHxNbO/A==} peerDependencies: - storybook: ^8.5.3 + storybook: ^8.6.3 - '@storybook/addon-essentials@8.5.3': - resolution: {integrity: sha512-0zbEWQQZCiYRUxMo6FrfwQER/vi+B8mCLLivdjbSVSvZsjmlpcaBA5uBjbsXfIRcedHlou4QiJXn+nR8thDlKA==} + '@storybook/addon-essentials@8.6.3': + resolution: {integrity: sha512-tH+MwkZ6UwRWyhGdq8izVZAZHGWdeiBY1wpIwdceP1Rl2j9s11Gbddb/JlmiXrC+f/Oiylxghaf7EIksVVqLQQ==} peerDependencies: - storybook: ^8.5.3 + storybook: ^8.6.3 - '@storybook/addon-highlight@8.5.3': - resolution: {integrity: sha512-xhsr3W6KTvlOIIe+8JE9/sEOAgkW0yjMZzs47A+bWcxKwcFhAUgVLbAgEzjJ0u248rjGKlCJ2pswWefO+ZKJeg==} + '@storybook/addon-highlight@8.6.3': + resolution: {integrity: sha512-LYZsgZt5q3EZBkZjUEELh/5+TDnUP0njuQ5g6skyKil6vj9+2RI4/Vjodp+ni5+xct5aDhXavRyUnPRfclX/Cg==} peerDependencies: - storybook: ^8.5.3 + storybook: ^8.6.3 - '@storybook/addon-interactions@8.5.3': - resolution: {integrity: sha512-nQuP65iFGgqfVp/O8NxNDUwLTWmQBW4bofUFaT4wzYn7Jk9zobOZYtgQvdqBZtNzBDYmLrfrCutEBj5jVPRyuQ==} + '@storybook/addon-interactions@8.6.3': + resolution: {integrity: sha512-cDvxuMcjoQdtimNrT4BM9AK0qZJhA0Ep/CWPcVK1bAFzqlzBbe//UZa5It/AeC4EMYAr5rFY+LWEli3YPeOnjQ==} peerDependencies: - storybook: ^8.5.3 + storybook: ^8.6.3 - '@storybook/addon-links@8.5.3': - resolution: {integrity: sha512-MRhIif4tCoIucLgGX14dI7yptF9bYH2UaJasyywshzQZKAEjOfX19Aw5fwp2zJt6kukAF6mUxMtWKcQMH2XOmw==} + '@storybook/addon-links@8.6.3': + resolution: {integrity: sha512-3wGiMZxWbgdjEgymUrCVG5PwU0vAYF9EiSHsGxiSxje69l08GLD6s7FTLx0HwvuyiNFcigLcuF45XZnB252RtA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.5.3 + storybook: ^8.6.3 peerDependenciesMeta: react: optional: true - '@storybook/addon-measure@8.5.3': - resolution: {integrity: sha512-unb0bRsnISXWiCBBECxNUUdM12hHpV+1uJUu5OJHtKb26YpiQvewDFLTLjuZJ3NIAfw+F5232Q7K88AWJV6weg==} + '@storybook/addon-measure@8.6.3': + resolution: {integrity: sha512-FC/3pqM2adSnwyPOd9AxEoZD5XWCMKAk16urQFQ0M4+IzRUdf2OV8cc7aM/oZiBX36+q/UCcUWm2SbQ5nzNJpg==} peerDependencies: - storybook: ^8.5.3 + storybook: ^8.6.3 - '@storybook/addon-outline@8.5.3': - resolution: {integrity: sha512-e1MkGN6XVdeRh2oUKGdqEDyAo2TD/47ashAAxw8DEiLRWgBMbQ+KBVH4EOG+dn5395jxh7YgRLJn/miqNnfN5g==} + '@storybook/addon-outline@8.6.3': + resolution: {integrity: sha512-YklKHRkoDLSWawIIBrEI69RAWEdvhkYCOv+fMLu9zBeVPnkwbtIjXN/I+UJwPCm6jlxeEwEUAvbPWZMMf+BkPQ==} peerDependencies: - storybook: ^8.5.3 + storybook: ^8.6.3 - '@storybook/addon-themes@8.5.3': - resolution: {integrity: sha512-wGlrAxU/9WF/CA4EV9VDzWbi4fPKdUweHmAPcnuiU0xG/zEIIL4o2fDr0+9tTctjJfVHgkrsHk74BpP14j+p9g==} + '@storybook/addon-themes@8.6.3': + resolution: {integrity: sha512-UTEsn241v9PpnOR6LEUkPlZ8nzLZeNJGcJZqZKHeAjW+Ag0pXnzvO1QUFxjHuP7i5olzRQh4n2UlWeN/DfLvjw==} peerDependencies: - storybook: ^8.5.3 + storybook: ^8.6.3 - '@storybook/addon-toolbars@8.5.3': - resolution: {integrity: sha512-AWr9Per9WDrbFtNlbVlj6CiEwKOvOyoBt3bCuMHuRfTdqKwkwInEtyUi4//T8U+c1qs7KJBpsWV2vhIuc5sODg==} + '@storybook/addon-toolbars@8.6.3': + resolution: {integrity: sha512-GTC1GPrFNfWvvBaQQnGuL7ZfGK5Q+3ZovwQA9tnPu7QZEwea/4CXvUyQh1u0NwqrFZkrabOad1XvYfpRuCPGSA==} peerDependencies: - storybook: ^8.5.3 + storybook: ^8.6.3 - '@storybook/addon-viewport@8.5.3': - resolution: {integrity: sha512-OkLJ2B8+PiOEAd2HtRG6XewVjtw6AkBMgoSbfKCMr6TWSbuKrOeiwIMqqieAAPVNfsOQ8hTK6JGhr/KPRCKgRA==} + '@storybook/addon-viewport@8.6.3': + resolution: {integrity: sha512-AixZKiQdBVs7ePj5iV0U1IY2jvH0G7wQJwBRTOq4qC1FKiOsZEYmrwc3wLUBUlVqyenXFKN+H40r4VhPzzSfLw==} peerDependencies: - storybook: ^8.5.3 + storybook: ^8.6.3 - '@storybook/blocks@8.5.3': - resolution: {integrity: sha512-a/PpHFmeBtVB9Q/6cNAnqfeCqMowsrI8nGka0Nl7BB3x1eJnS3I1Qo3Skht0LBEsmXOgXk4dwWxpeQL3qHMRkw==} + '@storybook/blocks@8.6.3': + resolution: {integrity: sha512-Ieu6kwqdeAcrLzcX2QIqnCd0XWZi46i4eem8W54JRiOMQMYUpZ7onbciRAP58qxEWrZWqgxPS+tiCTaJe48VVQ==} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.5.3 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^8.6.3 peerDependenciesMeta: react: optional: true react-dom: optional: true - '@storybook/builder-vite@8.5.3': - resolution: {integrity: sha512-MxriwzZSVidaXj3kpH/jCOJZUdF7ofcvxmvrMrNehH9UvXIGM6b73CBC5ucnptbnQ7qxYKdAZiMhQbPHZ9cqOQ==} + '@storybook/builder-vite@8.6.3': + resolution: {integrity: sha512-v/nlBeT7Avn1ld2GHY5dtm1+TKREvtQ+DEcKK5iOWfv2259WqUp0dGnF4fbHcsNCtFurkA/P2uqJ9vc0xOIVUg==} peerDependencies: - storybook: ^8.5.3 + storybook: ^8.6.3 vite: ^4.0.0 || ^5.0.0 || ^6.0.0 - '@storybook/components@8.5.3': - resolution: {integrity: sha512-iC9VbpM8Equ8wXI2syBzov+8wys4sGYW7Xfz67LdSVbCMhsH9FRtvgbDppJQC/ZDCofg4sTAHhWpDV/KAQ385A==} + '@storybook/components@8.6.3': + resolution: {integrity: sha512-q5DQkV+E/j0KfF818RywgqEHjaZTg71q5YY4z0UO8CRSzDQ/VYF6L76oc69corbkJtYAk/GqaYJllzrWykS4sg==} peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - '@storybook/core@8.5.3': - resolution: {integrity: sha512-ZLlr2pltbj/hmC54lggJTnh09FCAJR62lIdiXNwa+V+/eJz0CfD8tfGmZGKPSmaQeZBpMwAOeRM97k2oLPF+0w==} + '@storybook/core@8.6.3': + resolution: {integrity: sha512-0iMTfmo3UFCa1hFJLtThnRIppkIpGPyTL3MElhORP1t5l9lCUq5am0ymbi/TeCbsJPjE86FjeO0NinokL9iQiw==} peerDependencies: prettier: ^2 || ^3 peerDependenciesMeta: prettier: optional: true - '@storybook/csf-plugin@8.5.3': - resolution: {integrity: sha512-u5oyXTFg3KIy4h9qoNyiCG2mJF3OpkLO/AcM4lMAwQVnBvz8pwITvr4jDZByVjGmcIbgKJQnWX+BwdK2NI4yAw==} + '@storybook/csf-plugin@8.6.3': + resolution: {integrity: sha512-0QDLBcMOxSEt1yH28cvIsoiaIokIxDDShMnxVJHWk/7+KZ3xe4lZBfKCWZspZoJmrxgz10gLRifj1b3ysIFlyA==} peerDependencies: - storybook: ^8.5.3 + storybook: ^8.6.3 - '@storybook/csf@0.1.12': - resolution: {integrity: sha512-9/exVhabisyIVL0VxTCxo01Tdm8wefIXKXfltAPTSr8cbLn5JAxGQ6QV3mjdecLGEOucfoVhAKtJfVHxEK1iqw==} + '@storybook/csf@0.1.13': + resolution: {integrity: sha512-7xOOwCLGB3ebM87eemep89MYRFTko+D8qE7EdAAq74lgdqRR5cOUtYWJLjO2dLtP94nqoOdHJo6MdLLKzg412Q==} '@storybook/global@5.0.0': resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} @@ -953,49 +978,49 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - '@storybook/instrumenter@8.5.3': - resolution: {integrity: sha512-pxaTbGeju8MkwouIiaWX5DMWtpRruxqig8W3nZPOvzoSCCbQY+sLMQoyXxFlpGxLBjcvXivkL7AMVBKps5sFEQ==} + '@storybook/instrumenter@8.6.3': + resolution: {integrity: sha512-Y5n6JWCWdOqok08Hgklsc98TBoqROhAhBRSzNWuIaLsRhz8EziXQtuEkWqmVbyYOys25iTZiK3S8+QQkOzGrBw==} peerDependencies: - storybook: ^8.5.3 + storybook: ^8.6.3 - '@storybook/manager-api@8.5.3': - resolution: {integrity: sha512-JtfuMgQpKIPU0ARn1jNPce8FmknpM0Ap0mppWl+KGAWWGadJPDaX/nrY/19dT1kRgIhyOnbX6tgJxII4E9dE5w==} + '@storybook/manager-api@8.6.3': + resolution: {integrity: sha512-7m9MQELc6XpuKIuliqMiQWzl8yVWpUDwTcpr+rTT7l3OfRzw7Y00UFct2tI03YG6EXsxsykw8EmueMQhe0lG5Q==} peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - '@storybook/preview-api@8.5.3': - resolution: {integrity: sha512-dUsuXW+KgDg4tWXOB6dk5j5gwwRUzbPvicHAY9mzbpSVScbWXuE5T/S/9hHlbtfkhFroWQgPx2eB8z3rai+7RQ==} + '@storybook/preview-api@8.6.3': + resolution: {integrity: sha512-y2Ic6eHBQD/AwaCHctKOJ4tOM1r7/mPXfhGh0I+Qf8kZPlDTgQcJ6Z7/Ruma1L+ijXPBWouDaPw51gipcX+t9Q==} peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - '@storybook/react-dom-shim@8.5.3': - resolution: {integrity: sha512-kNIGk6mpXW3Wy+uS9pH9b9w/54EPJnH+QXA6MX4EQgmxhMQlGlS/l/YZp+3jsVQW4YgTmqe740qB+ccJAKZxBQ==} + '@storybook/react-dom-shim@8.6.3': + resolution: {integrity: sha512-vE3LA2TxbzDF1Fso2IgvUtoHc+8a6laKhuJdx8frP5A8M1KGOBfuEPFCCcE49Q90HUlDgwb/zQl1GNq/QjLgWQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.5.3 + storybook: ^8.6.3 - '@storybook/react-vite@8.5.3': - resolution: {integrity: sha512-F30u2Xf+X774wrfQzWgg7vRVJmmJFbBVGdULsAGonkdy1FUeYo7puPiD2Qg6hBYNDyIyxDXVOukkOvTlG7IBRg==} + '@storybook/react-vite@8.6.3': + resolution: {integrity: sha512-A/cA0wM/mMfFcJH7dxhWSbVg9aE2zZKNDioyEbiB042CgrLW3zQ6dvQvA5ohFhsPWZ6GVAyc+r3x0JE55aXxWQ==} engines: {node: '>=18.0.0'} peerDependencies: - '@storybook/test': 8.5.3 + '@storybook/test': 8.6.3 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.5.3 + storybook: ^8.6.3 vite: ^4.0.0 || ^5.0.0 || ^6.0.0 peerDependenciesMeta: '@storybook/test': optional: true - '@storybook/react@8.5.3': - resolution: {integrity: sha512-QIdBSjsnwV/J919i4Fi7DlwxDKHU815t0c4B/w2KTMtKKBkk+Bge+vgVi0/lNqD3eF4w3yjVWGbkzUQZ63yiPg==} + '@storybook/react@8.6.3': + resolution: {integrity: sha512-B4WYRWU2Y71UWl4CG3+mcB7duNln9finJyDB8Y1o2CYWUxgEo+3Bnp3k7NUr++tYVkZI1H+28UWeX0rpCkvReQ==} engines: {node: '>=18.0.0'} peerDependencies: - '@storybook/test': 8.5.3 + '@storybook/test': 8.6.3 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.5.3 + storybook: ^8.6.3 typescript: '>= 4.2.x' peerDependenciesMeta: '@storybook/test': @@ -1003,13 +1028,13 @@ packages: typescript: optional: true - '@storybook/test@8.5.3': - resolution: {integrity: sha512-2smoDbtU6Qh4yk0uD18qGfW6ll7lZBzKlF58Ha1CgWR4o+jpeeTQcfDLH9gG6sNrpojF7AVzMh/aN9BDHD+Chg==} + '@storybook/test@8.6.3': + resolution: {integrity: sha512-UimvhV/PmYoXCwIbGpkyqQfMhjdH2GaHJbV6BWr7M7BHA3kUS6zYJAm2V2CC5SYcmyj7FejLB4tgL7FmLXB6hA==} peerDependencies: - storybook: ^8.5.3 + storybook: ^8.6.3 - '@storybook/theming@8.5.3': - resolution: {integrity: sha512-Jvzw+gT1HNarkJo21WZBq5pU89qDN8u/pD3woSh/1c2h5RS6UylWjQHotPFpcBIQiUSrDFtvCU9xugJm4MD0+w==} + '@storybook/theming@8.6.3': + resolution: {integrity: sha512-sDcWnnko73KOCIc9stQyec9KvTmGOuMswqeKtWh0ha/wsgYB6G2/2j1xOheFmWKPitOsbwgvqtjCP7bRE68uIA==} peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 @@ -1399,18 +1424,6 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} - chromatic@11.20.2: - resolution: {integrity: sha512-c+M3HVl5Y60c7ipGTZTyeWzWubRW70YsJ7PPDpO1D735ib8+Lu3yGF90j61pvgkXGngpkTPHZyBw83lcu2JMxA==} - hasBin: true - peerDependencies: - '@chromatic-com/cypress': ^0.*.* || ^1.0.0 - '@chromatic-com/playwright': ^0.*.* || ^1.0.0 - peerDependenciesMeta: - '@chromatic-com/cypress': - optional: true - '@chromatic-com/playwright': - optional: true - cli-boxes@3.0.0: resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} engines: {node: '>=10'} @@ -1745,10 +1758,6 @@ packages: resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==} engines: {node: '>=4'} - filesize@10.1.6: - resolution: {integrity: sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==} - engines: {node: '>= 10.4.0'} - fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -2609,12 +2618,6 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true - react-confetti@6.1.0: - resolution: {integrity: sha512-7Ypx4vz0+g8ECVxr88W9zhcQpbeujJAVqL14ZnXJ3I23mOI9/oBVTQ3dkJhUmB0D6XOtCZEM6N0Gm9PMngkORw==} - engines: {node: '>=10.18'} - peerDependencies: - react: ^16.3.0 || ^17.0.1 || ^18.0.0 - react-docgen-typescript@2.2.2: resolution: {integrity: sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==} peerDependencies: @@ -2818,8 +2821,8 @@ packages: std-env@3.8.0: resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} - storybook@8.5.3: - resolution: {integrity: sha512-2WtNBZ45u1AhviRU+U+ld588tH8gDa702dNSq5C8UBaE9PlOsazGsyp90dw1s9YRvi+ejrjKAupQAU0GwwUiVg==} + storybook@8.6.3: + resolution: {integrity: sha512-Vbmd8/FXp6X0AOMak6arcg3WdkHj+2AYJTNHbCPVHsCEbnREyRZIG+Eq5/Ffmy6byiz+4OAX5HwsHGSMR6Xmow==} hasBin: true peerDependencies: prettier: ^2 || ^3 @@ -3050,9 +3053,6 @@ packages: typescript: optional: true - tween-functions@1.2.0: - resolution: {integrity: sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==} - type-fest@0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} @@ -3469,7 +3469,7 @@ snapshots: '@babel/helper-compilation-targets': 7.25.9 '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) '@babel/helpers': 7.26.0 - '@babel/parser': 7.26.3 + '@babel/parser': 7.26.7 '@babel/template': 7.25.9 '@babel/traverse': 7.26.4 '@babel/types': 7.26.3 @@ -3483,12 +3483,20 @@ snapshots: '@babel/generator@7.26.3': dependencies: - '@babel/parser': 7.26.3 + '@babel/parser': 7.26.7 '@babel/types': 7.26.3 '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.1.0 + '@babel/generator@7.26.8': + dependencies: + '@babel/parser': 7.26.8 + '@babel/types': 7.26.8 + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.1.0 + '@babel/helper-compilation-targets@7.25.9': dependencies: '@babel/compat-data': 7.26.3 @@ -3526,9 +3534,13 @@ snapshots: '@babel/template': 7.25.9 '@babel/types': 7.26.3 - '@babel/parser@7.26.3': + '@babel/parser@7.26.7': dependencies: - '@babel/types': 7.26.3 + '@babel/types': 7.26.7 + + '@babel/parser@7.26.8': + dependencies: + '@babel/types': 7.26.8 '@babel/plugin-transform-react-jsx-self@7.25.9(@babel/core@7.26.0)': dependencies: @@ -3547,14 +3559,20 @@ snapshots: '@babel/template@7.25.9': dependencies: '@babel/code-frame': 7.26.2 - '@babel/parser': 7.26.3 + '@babel/parser': 7.26.7 '@babel/types': 7.26.3 + '@babel/template@7.26.8': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.26.8 + '@babel/types': 7.26.8 + '@babel/traverse@7.26.4': dependencies: '@babel/code-frame': 7.26.2 '@babel/generator': 7.26.3 - '@babel/parser': 7.26.3 + '@babel/parser': 7.26.7 '@babel/template': 7.25.9 '@babel/types': 7.26.3 debug: 4.4.0 @@ -3562,11 +3580,33 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@7.26.8': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.8 + '@babel/parser': 7.26.8 + '@babel/template': 7.26.8 + '@babel/types': 7.26.8 + debug: 4.4.0 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + '@babel/types@7.26.3': dependencies: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 + '@babel/types@7.26.7': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@babel/types@7.26.8': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@biomejs/biome@1.9.4': optionalDependencies: '@biomejs/cli-darwin-arm64': 1.9.4 @@ -3602,19 +3642,6 @@ snapshots: '@biomejs/cli-win32-x64@1.9.4': optional: true - '@chromatic-com/storybook@3.2.4(react@18.3.1)(storybook@8.5.3(prettier@3.4.2))': - dependencies: - chromatic: 11.20.2 - filesize: 10.1.6 - jsonfile: 6.1.0 - react-confetti: 6.1.0(react@18.3.1) - storybook: 8.5.3(prettier@3.4.2) - strip-ansi: 7.1.0 - transitivePeerDependencies: - - '@chromatic-com/cypress' - - '@chromatic-com/playwright' - - react - '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -3782,8 +3809,9 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 - '@joshwooding/vite-plugin-react-docgen-typescript@0.4.2(typescript@5.7.2)(vite@5.4.11(@types/node@18.19.68))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.5.0(typescript@5.7.2)(vite@5.4.11(@types/node@18.19.68))': dependencies: + glob: 10.4.5 magic-string: 0.27.0 react-docgen-typescript: 2.2.2(typescript@5.7.2) vite: 5.4.11(@types/node@18.19.68) @@ -3997,132 +4025,130 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.29.1': optional: true - '@storybook/addon-actions@8.5.3(storybook@8.5.3(prettier@3.4.2))': + '@storybook/addon-actions@8.6.3(storybook@8.6.3(prettier@3.4.2))': dependencies: '@storybook/global': 5.0.0 '@types/uuid': 9.0.8 dequal: 2.0.3 polished: 4.3.1 - storybook: 8.5.3(prettier@3.4.2) + storybook: 8.6.3(prettier@3.4.2) uuid: 9.0.1 - '@storybook/addon-backgrounds@8.5.3(storybook@8.5.3(prettier@3.4.2))': + '@storybook/addon-backgrounds@8.6.3(storybook@8.6.3(prettier@3.4.2))': dependencies: '@storybook/global': 5.0.0 memoizerific: 1.11.3 - storybook: 8.5.3(prettier@3.4.2) + storybook: 8.6.3(prettier@3.4.2) ts-dedent: 2.2.0 - '@storybook/addon-controls@8.5.3(storybook@8.5.3(prettier@3.4.2))': + '@storybook/addon-controls@8.6.3(storybook@8.6.3(prettier@3.4.2))': dependencies: '@storybook/global': 5.0.0 dequal: 2.0.3 - storybook: 8.5.3(prettier@3.4.2) + storybook: 8.6.3(prettier@3.4.2) ts-dedent: 2.2.0 - '@storybook/addon-docs@8.5.3(@types/react@18.3.18)(storybook@8.5.3(prettier@3.4.2))': + '@storybook/addon-docs@8.6.3(@types/react@18.3.18)(storybook@8.6.3(prettier@3.4.2))': dependencies: '@mdx-js/react': 3.1.0(@types/react@18.3.18)(react@18.3.1) - '@storybook/blocks': 8.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.3(prettier@3.4.2)) - '@storybook/csf-plugin': 8.5.3(storybook@8.5.3(prettier@3.4.2)) - '@storybook/react-dom-shim': 8.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.3(prettier@3.4.2)) + '@storybook/blocks': 8.6.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.3(prettier@3.4.2)) + '@storybook/csf-plugin': 8.6.3(storybook@8.6.3(prettier@3.4.2)) + '@storybook/react-dom-shim': 8.6.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.3(prettier@3.4.2)) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 8.5.3(prettier@3.4.2) + storybook: 8.6.3(prettier@3.4.2) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' - '@storybook/addon-essentials@8.5.3(@types/react@18.3.18)(storybook@8.5.3(prettier@3.4.2))': - dependencies: - '@storybook/addon-actions': 8.5.3(storybook@8.5.3(prettier@3.4.2)) - '@storybook/addon-backgrounds': 8.5.3(storybook@8.5.3(prettier@3.4.2)) - '@storybook/addon-controls': 8.5.3(storybook@8.5.3(prettier@3.4.2)) - '@storybook/addon-docs': 8.5.3(@types/react@18.3.18)(storybook@8.5.3(prettier@3.4.2)) - '@storybook/addon-highlight': 8.5.3(storybook@8.5.3(prettier@3.4.2)) - '@storybook/addon-measure': 8.5.3(storybook@8.5.3(prettier@3.4.2)) - '@storybook/addon-outline': 8.5.3(storybook@8.5.3(prettier@3.4.2)) - '@storybook/addon-toolbars': 8.5.3(storybook@8.5.3(prettier@3.4.2)) - '@storybook/addon-viewport': 8.5.3(storybook@8.5.3(prettier@3.4.2)) - storybook: 8.5.3(prettier@3.4.2) + '@storybook/addon-essentials@8.6.3(@types/react@18.3.18)(storybook@8.6.3(prettier@3.4.2))': + dependencies: + '@storybook/addon-actions': 8.6.3(storybook@8.6.3(prettier@3.4.2)) + '@storybook/addon-backgrounds': 8.6.3(storybook@8.6.3(prettier@3.4.2)) + '@storybook/addon-controls': 8.6.3(storybook@8.6.3(prettier@3.4.2)) + '@storybook/addon-docs': 8.6.3(@types/react@18.3.18)(storybook@8.6.3(prettier@3.4.2)) + '@storybook/addon-highlight': 8.6.3(storybook@8.6.3(prettier@3.4.2)) + '@storybook/addon-measure': 8.6.3(storybook@8.6.3(prettier@3.4.2)) + '@storybook/addon-outline': 8.6.3(storybook@8.6.3(prettier@3.4.2)) + '@storybook/addon-toolbars': 8.6.3(storybook@8.6.3(prettier@3.4.2)) + '@storybook/addon-viewport': 8.6.3(storybook@8.6.3(prettier@3.4.2)) + storybook: 8.6.3(prettier@3.4.2) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' - '@storybook/addon-highlight@8.5.3(storybook@8.5.3(prettier@3.4.2))': + '@storybook/addon-highlight@8.6.3(storybook@8.6.3(prettier@3.4.2))': dependencies: '@storybook/global': 5.0.0 - storybook: 8.5.3(prettier@3.4.2) + storybook: 8.6.3(prettier@3.4.2) - '@storybook/addon-interactions@8.5.3(storybook@8.5.3(prettier@3.4.2))': + '@storybook/addon-interactions@8.6.3(storybook@8.6.3(prettier@3.4.2))': dependencies: '@storybook/global': 5.0.0 - '@storybook/instrumenter': 8.5.3(storybook@8.5.3(prettier@3.4.2)) - '@storybook/test': 8.5.3(storybook@8.5.3(prettier@3.4.2)) + '@storybook/instrumenter': 8.6.3(storybook@8.6.3(prettier@3.4.2)) + '@storybook/test': 8.6.3(storybook@8.6.3(prettier@3.4.2)) polished: 4.3.1 - storybook: 8.5.3(prettier@3.4.2) + storybook: 8.6.3(prettier@3.4.2) ts-dedent: 2.2.0 - '@storybook/addon-links@8.5.3(react@18.3.1)(storybook@8.5.3(prettier@3.4.2))': + '@storybook/addon-links@8.6.3(react@18.3.1)(storybook@8.6.3(prettier@3.4.2))': dependencies: - '@storybook/csf': 0.1.12 '@storybook/global': 5.0.0 - storybook: 8.5.3(prettier@3.4.2) + storybook: 8.6.3(prettier@3.4.2) ts-dedent: 2.2.0 optionalDependencies: react: 18.3.1 - '@storybook/addon-measure@8.5.3(storybook@8.5.3(prettier@3.4.2))': + '@storybook/addon-measure@8.6.3(storybook@8.6.3(prettier@3.4.2))': dependencies: '@storybook/global': 5.0.0 - storybook: 8.5.3(prettier@3.4.2) + storybook: 8.6.3(prettier@3.4.2) tiny-invariant: 1.3.3 - '@storybook/addon-outline@8.5.3(storybook@8.5.3(prettier@3.4.2))': + '@storybook/addon-outline@8.6.3(storybook@8.6.3(prettier@3.4.2))': dependencies: '@storybook/global': 5.0.0 - storybook: 8.5.3(prettier@3.4.2) + storybook: 8.6.3(prettier@3.4.2) ts-dedent: 2.2.0 - '@storybook/addon-themes@8.5.3(storybook@8.5.3(prettier@3.4.2))': + '@storybook/addon-themes@8.6.3(storybook@8.6.3(prettier@3.4.2))': dependencies: - storybook: 8.5.3(prettier@3.4.2) + storybook: 8.6.3(prettier@3.4.2) ts-dedent: 2.2.0 - '@storybook/addon-toolbars@8.5.3(storybook@8.5.3(prettier@3.4.2))': + '@storybook/addon-toolbars@8.6.3(storybook@8.6.3(prettier@3.4.2))': dependencies: - storybook: 8.5.3(prettier@3.4.2) + storybook: 8.6.3(prettier@3.4.2) - '@storybook/addon-viewport@8.5.3(storybook@8.5.3(prettier@3.4.2))': + '@storybook/addon-viewport@8.6.3(storybook@8.6.3(prettier@3.4.2))': dependencies: memoizerific: 1.11.3 - storybook: 8.5.3(prettier@3.4.2) + storybook: 8.6.3(prettier@3.4.2) - '@storybook/blocks@8.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.3(prettier@3.4.2))': + '@storybook/blocks@8.6.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.3(prettier@3.4.2))': dependencies: - '@storybook/csf': 0.1.12 '@storybook/icons': 1.3.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - storybook: 8.5.3(prettier@3.4.2) + storybook: 8.6.3(prettier@3.4.2) ts-dedent: 2.2.0 optionalDependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/builder-vite@8.5.3(storybook@8.5.3(prettier@3.4.2))(vite@5.4.11(@types/node@18.19.68))': + '@storybook/builder-vite@8.6.3(storybook@8.6.3(prettier@3.4.2))(vite@5.4.11(@types/node@18.19.68))': dependencies: - '@storybook/csf-plugin': 8.5.3(storybook@8.5.3(prettier@3.4.2)) + '@storybook/csf-plugin': 8.6.3(storybook@8.6.3(prettier@3.4.2)) browser-assert: 1.2.1 - storybook: 8.5.3(prettier@3.4.2) + storybook: 8.6.3(prettier@3.4.2) ts-dedent: 2.2.0 vite: 5.4.11(@types/node@18.19.68) - '@storybook/components@8.5.3(storybook@8.5.3(prettier@3.4.2))': + '@storybook/components@8.6.3(storybook@8.6.3(prettier@3.4.2))': dependencies: - storybook: 8.5.3(prettier@3.4.2) + storybook: 8.6.3(prettier@3.4.2) - '@storybook/core@8.5.3(prettier@3.4.2)': + '@storybook/core@8.6.3(prettier@3.4.2)(storybook@8.6.3(prettier@3.4.2))': dependencies: - '@storybook/csf': 0.1.12 + '@storybook/theming': 8.6.3(storybook@8.6.3(prettier@3.4.2)) better-opn: 3.0.2 browser-assert: 1.2.1 esbuild: 0.24.2 @@ -4137,15 +4163,16 @@ snapshots: prettier: 3.4.2 transitivePeerDependencies: - bufferutil + - storybook - supports-color - utf-8-validate - '@storybook/csf-plugin@8.5.3(storybook@8.5.3(prettier@3.4.2))': + '@storybook/csf-plugin@8.6.3(storybook@8.6.3(prettier@3.4.2))': dependencies: - storybook: 8.5.3(prettier@3.4.2) + storybook: 8.6.3(prettier@3.4.2) unplugin: 1.16.0 - '@storybook/csf@0.1.12': + '@storybook/csf@0.1.13': dependencies: type-fest: 2.19.0 @@ -4156,78 +4183,77 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/instrumenter@8.5.3(storybook@8.5.3(prettier@3.4.2))': + '@storybook/instrumenter@8.6.3(storybook@8.6.3(prettier@3.4.2))': dependencies: '@storybook/global': 5.0.0 '@vitest/utils': 2.1.8 - storybook: 8.5.3(prettier@3.4.2) + storybook: 8.6.3(prettier@3.4.2) - '@storybook/manager-api@8.5.3(storybook@8.5.3(prettier@3.4.2))': + '@storybook/manager-api@8.6.3(storybook@8.6.3(prettier@3.4.2))': dependencies: - storybook: 8.5.3(prettier@3.4.2) + storybook: 8.6.3(prettier@3.4.2) - '@storybook/preview-api@8.5.3(storybook@8.5.3(prettier@3.4.2))': + '@storybook/preview-api@8.6.3(storybook@8.6.3(prettier@3.4.2))': dependencies: - storybook: 8.5.3(prettier@3.4.2) + storybook: 8.6.3(prettier@3.4.2) - '@storybook/react-dom-shim@8.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.3(prettier@3.4.2))': + '@storybook/react-dom-shim@8.6.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.3(prettier@3.4.2))': dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 8.5.3(prettier@3.4.2) + storybook: 8.6.3(prettier@3.4.2) - '@storybook/react-vite@8.5.3(@storybook/test@8.5.3(storybook@8.5.3(prettier@3.4.2)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.29.1)(storybook@8.5.3(prettier@3.4.2))(typescript@5.7.2)(vite@5.4.11(@types/node@18.19.68))': + '@storybook/react-vite@8.6.3(@storybook/test@8.6.3(storybook@8.6.3(prettier@3.4.2)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.29.1)(storybook@8.6.3(prettier@3.4.2))(typescript@5.7.2)(vite@5.4.11(@types/node@18.19.68))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.4.2(typescript@5.7.2)(vite@5.4.11(@types/node@18.19.68)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.5.0(typescript@5.7.2)(vite@5.4.11(@types/node@18.19.68)) '@rollup/pluginutils': 5.1.4(rollup@4.29.1) - '@storybook/builder-vite': 8.5.3(storybook@8.5.3(prettier@3.4.2))(vite@5.4.11(@types/node@18.19.68)) - '@storybook/react': 8.5.3(@storybook/test@8.5.3(storybook@8.5.3(prettier@3.4.2)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.3(prettier@3.4.2))(typescript@5.7.2) + '@storybook/builder-vite': 8.6.3(storybook@8.6.3(prettier@3.4.2))(vite@5.4.11(@types/node@18.19.68)) + '@storybook/react': 8.6.3(@storybook/test@8.6.3(storybook@8.6.3(prettier@3.4.2)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.3(prettier@3.4.2))(typescript@5.7.2) find-up: 5.0.0 magic-string: 0.30.17 react: 18.3.1 react-docgen: 7.1.0 react-dom: 18.3.1(react@18.3.1) resolve: 1.22.10 - storybook: 8.5.3(prettier@3.4.2) + storybook: 8.6.3(prettier@3.4.2) tsconfig-paths: 4.2.0 vite: 5.4.11(@types/node@18.19.68) optionalDependencies: - '@storybook/test': 8.5.3(storybook@8.5.3(prettier@3.4.2)) + '@storybook/test': 8.6.3(storybook@8.6.3(prettier@3.4.2)) transitivePeerDependencies: - rollup - supports-color - typescript - '@storybook/react@8.5.3(@storybook/test@8.5.3(storybook@8.5.3(prettier@3.4.2)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.3(prettier@3.4.2))(typescript@5.7.2)': + '@storybook/react@8.6.3(@storybook/test@8.6.3(storybook@8.6.3(prettier@3.4.2)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.3(prettier@3.4.2))(typescript@5.7.2)': dependencies: - '@storybook/components': 8.5.3(storybook@8.5.3(prettier@3.4.2)) + '@storybook/components': 8.6.3(storybook@8.6.3(prettier@3.4.2)) '@storybook/global': 5.0.0 - '@storybook/manager-api': 8.5.3(storybook@8.5.3(prettier@3.4.2)) - '@storybook/preview-api': 8.5.3(storybook@8.5.3(prettier@3.4.2)) - '@storybook/react-dom-shim': 8.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.3(prettier@3.4.2)) - '@storybook/theming': 8.5.3(storybook@8.5.3(prettier@3.4.2)) + '@storybook/manager-api': 8.6.3(storybook@8.6.3(prettier@3.4.2)) + '@storybook/preview-api': 8.6.3(storybook@8.6.3(prettier@3.4.2)) + '@storybook/react-dom-shim': 8.6.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.3(prettier@3.4.2)) + '@storybook/theming': 8.6.3(storybook@8.6.3(prettier@3.4.2)) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 8.5.3(prettier@3.4.2) + storybook: 8.6.3(prettier@3.4.2) optionalDependencies: - '@storybook/test': 8.5.3(storybook@8.5.3(prettier@3.4.2)) + '@storybook/test': 8.6.3(storybook@8.6.3(prettier@3.4.2)) typescript: 5.7.2 - '@storybook/test@8.5.3(storybook@8.5.3(prettier@3.4.2))': + '@storybook/test@8.6.3(storybook@8.6.3(prettier@3.4.2))': dependencies: - '@storybook/csf': 0.1.12 '@storybook/global': 5.0.0 - '@storybook/instrumenter': 8.5.3(storybook@8.5.3(prettier@3.4.2)) + '@storybook/instrumenter': 8.6.3(storybook@8.6.3(prettier@3.4.2)) '@testing-library/dom': 10.4.0 '@testing-library/jest-dom': 6.5.0 '@testing-library/user-event': 14.5.2(@testing-library/dom@10.4.0) '@vitest/expect': 2.0.5 '@vitest/spy': 2.0.5 - storybook: 8.5.3(prettier@3.4.2) + storybook: 8.6.3(prettier@3.4.2) - '@storybook/theming@8.5.3(storybook@8.5.3(prettier@3.4.2))': + '@storybook/theming@8.6.3(storybook@8.6.3(prettier@3.4.2))': dependencies: - storybook: 8.5.3(prettier@3.4.2) + storybook: 8.6.3(prettier@3.4.2) '@testing-library/dom@10.4.0': dependencies: @@ -4266,7 +4292,7 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.26.3 + '@babel/parser': 7.26.7 '@babel/types': 7.26.3 '@types/babel__generator': 7.6.8 '@types/babel__template': 7.4.4 @@ -4278,7 +4304,7 @@ snapshots: '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.26.3 + '@babel/parser': 7.26.7 '@babel/types': 7.26.3 '@types/babel__traverse@7.20.6': @@ -4653,8 +4679,6 @@ snapshots: dependencies: readdirp: 4.0.2 - chromatic@11.20.2: {} - cli-boxes@3.0.0: {} cli-cursor@5.0.0: @@ -5019,8 +5043,6 @@ snapshots: dependencies: escape-string-regexp: 1.0.5 - filesize@10.1.6: {} - fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -5796,11 +5818,6 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 - react-confetti@6.1.0(react@18.3.1): - dependencies: - react: 18.3.1 - tween-functions: 1.2.0 - react-docgen-typescript@2.2.2(typescript@5.7.2): dependencies: typescript: 5.7.2 @@ -5808,8 +5825,8 @@ snapshots: react-docgen@7.1.0: dependencies: '@babel/core': 7.26.0 - '@babel/traverse': 7.26.4 - '@babel/types': 7.26.3 + '@babel/traverse': 7.26.8 + '@babel/types': 7.26.8 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.20.6 '@types/doctrine': 0.0.9 @@ -6018,9 +6035,9 @@ snapshots: std-env@3.8.0: {} - storybook@8.5.3(prettier@3.4.2): + storybook@8.6.3(prettier@3.4.2): dependencies: - '@storybook/core': 8.5.3(prettier@3.4.2) + '@storybook/core': 8.6.3(prettier@3.4.2)(storybook@8.6.3(prettier@3.4.2)) optionalDependencies: prettier: 3.4.2 transitivePeerDependencies: @@ -6274,8 +6291,6 @@ snapshots: - tsx - yaml - tween-functions@1.2.0: {} - type-fest@0.21.3: {} type-fest@2.19.0: {} diff --git a/src/CodeBlock.tsx b/src/CodeBlock.tsx index 00e4841..6c130a1 100644 --- a/src/CodeBlock.tsx +++ b/src/CodeBlock.tsx @@ -11,7 +11,6 @@ import { styled } from 'storybook/internal/theming'; const Container = styled.div(({ theme }) => ({ display: 'flex', flexDirection: 'column', - borderRadius: theme.appBorderRadius, border: `1px solid ${theme.appBorderColor}`, })); diff --git a/src/InteractionRecorder.tsx b/src/InteractionRecorder.tsx index a8d9389..d77631d 100644 --- a/src/InteractionRecorder.tsx +++ b/src/InteractionRecorder.tsx @@ -4,6 +4,7 @@ import { Bar, EmptyTabContent } from 'storybook/internal/components'; import { useChannel, useStorybookApi } from 'storybook/internal/manager-api'; import { useDebounce } from 'use-debounce'; import { CodeBlock } from './CodeBlock'; +import { SaveStoryButton } from './SaveStory'; import { combineInteractions } from './codegen/combine-interactions'; import { convertInteractionsToCode } from './codegen/interactions-to-code'; import { EVENTS } from './constants'; @@ -104,6 +105,8 @@ export const InteractionRecorder = () => { Reset + + {code.play.length > 0 && } diff --git a/src/SaveStory.tsx b/src/SaveStory.tsx new file mode 100644 index 0000000..07d3328 --- /dev/null +++ b/src/SaveStory.tsx @@ -0,0 +1,213 @@ +import { CheckIcon } from '@storybook/icons'; +import { dequal as deepEqual } from 'dequal'; +import { type ChangeEvent, useEffect, useState } from 'react'; +import { TooltipNote, WithTooltip } from 'storybook/internal/components'; +import { + experimental_requestResponse, + useStorybookApi, +} from 'storybook/internal/manager-api'; +import type { Args } from 'storybook/internal/types'; +import type { GeneratedCode } from './codegen/interactions-to-code'; +import { EVENTS } from './constants'; +import type { + SaveNewStoryRequestPayload, + SaveNewStoryResponsePayload, +} from './data'; +import { + DisabledButton, + ErrorButton, + ErrorIcon, + RotatingIcon, + SaveContainer, + SaveIconColorful, + SaveInput, + SavedButton, + StyledButton, + StyledCheckIcon, +} from './styles'; + +const stringifyArgs = (args: Record) => + JSON.stringify(args, (_, value) => { + if (typeof value === 'function') { + return '__sb_empty_function_arg__'; + } + return value; + }); + +export const SaveStoryButton = ({ + code, +}: { + code: GeneratedCode; +}) => { + const api = useStorybookApi(); + + const [name, setName] = useState(''); + const [state, setState] = useState< + 'button' | 'input' | 'creating' | 'success' | 'error' + >('button'); + + const storyData = api.getCurrentStoryData(); + useEffect(() => { + setName(storyData?.name); + setState('button'); + }, [storyData?.name]); + + const saveStory = async () => { + setState('creating'); + + const payload: SaveNewStoryRequestPayload = { + code, + csfId: storyData.id, + importPath: storyData.importPath, + args: stringifyArgs( + 'args' in storyData + ? Object.entries(storyData.args || {}).reduce( + (acc, [key, value]) => { + if (!deepEqual(value, storyData.initialArgs?.[key])) { + acc[key] = value; + } + return acc; + }, + {}, + ) + : {}, + ), + name, + }; + + const channel = api.getChannel(); + if (!channel) { + return; + } + + try { + const { newStoryId } = await experimental_requestResponse< + SaveNewStoryRequestPayload, + SaveNewStoryResponsePayload + >( + // biome-ignore lint/suspicious/noExplicitAny: Should be fixed with new package version + channel as any, + EVENTS.SAVE_NEW_STORY_REQUEST, + EVENTS.SAVE_NEW_STORY_RESPONSE, + payload, + ); + + if (newStoryId === storyData.id) { + setState('success'); + + setTimeout(() => { + setState('button'); + }, 2000); + } else { + setState('button'); + + api.addNotification({ + id: 'create-new-story-file-success', + content: { + headline: 'Story file created', + subHeadline: `${name} was created`, + }, + duration: 8_000, + icon: , + }); + } + + await trySelectNewStory(api.selectStory, newStoryId); + } catch (ex) { + console.error(ex); + + api.addNotification({ + id: 'create-new-story-file-error', + content: { + headline: 'Failed to save story', + subHeadline: 'Please try again', + }, + duration: 8_000, + icon: , + }); + + setState('error'); + + setTimeout(() => { + setState('button'); + }, 2000); + } + }; + + const isDevelopment = + // biome-ignore lint/suspicious/noExplicitAny: + (global as any as { CONFIG_TYPE: string }).CONFIG_TYPE === 'DEVELOPMENT'; + + return ( + + {state === 'button' && isDevelopment && ( + setState('input')} variant="outline"> + Save to story + + )} + + {state === 'button' && !isDevelopment && ( + } + > + + Save to story + + + )} + + {state === 'input' && ( + <> + ) => setName(e.target.value)} + /> + + Save + + + )} + + {state === 'creating' && ( + setState('input')} variant="outline"> + Saving + + )} + + {state === 'success' && ( + + Saved + + )} + + {state === 'error' && ( + + Failed to save + + )} + + ); +}; + +export async function trySelectNewStory( + selectStory: (id: string) => Promise | void, + storyId: string, + attempt = 1, +): Promise { + if (attempt > 10) { + throw new Error('We could not select the new story. Please try again.'); + } + + try { + await selectStory(storyId); + } catch (e) { + await new Promise((resolve) => setTimeout(resolve, 500)); + return trySelectNewStory(selectStory, storyId, attempt + 1); + } +} diff --git a/src/codegen/generate-story-code.test.ts b/src/codegen/generate-story-code.test.ts new file mode 100644 index 0000000..247632a --- /dev/null +++ b/src/codegen/generate-story-code.test.ts @@ -0,0 +1,380 @@ +import { loadCsf } from 'storybook/internal/csf-tools'; +import { describe, expect, it } from 'vitest'; +import { generateStoryCode } from './generate-story-code'; +import type { GeneratedCode } from './interactions-to-code'; + +const TEST_CASES = [ + [ + 'New story and new imports', + { + story: `import type { Meta, StoryObj } from '@storybook/react'; +import { Component } from './Component'; + +const meta: Meta = { + component: Component +}; +export default meta; +type Story = StoryObj; + +export const Default: Story = {};`, + code: { + imports: ["import { userEvent, within } from '@storybook/test';"], + play: [ + 'play: async ({ canvasElement }) => {', + '\tconst canvas = within(canvasElement.ownerDocument.body);', + "\tawait userEvent.click(await canvas.findByRole('button'));", + '}', + ], + }, + result: `import { userEvent, within } from '@storybook/test'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Component } from './Component'; + +const meta: Meta = { + component: Component +}; +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const NewStory: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement.ownerDocument.body); + await userEvent.click(await canvas.findByRole('button')); + } +};`, + newStoryId: 'form--new-story', + }, + ], + + [ + 'New story with spaces in name and some existing imports', + { + story: `import { userEvent } from '@storybook/test'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Component } from './Component'; + +const meta: Meta = { + component: Component +}; +export default meta; +type Story = StoryObj; + +export const Default: Story = {};`, + code: { + imports: ["import { userEvent, within } from '@storybook/test';"], + play: [ + 'play: async ({ canvasElement }) => {', + '\tconst canvas = within(canvasElement.ownerDocument.body);', + "\tawait userEvent.click(await canvas.findByRole('button'));", + '}', + ], + }, + result: `import { userEvent, within } from '@storybook/test'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Component } from './Component'; + +const meta: Meta = { + component: Component +}; +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const NewStory: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement.ownerDocument.body); + await userEvent.click(await canvas.findByRole('button')); + } +};`, + newStoryId: 'form--new-story', + }, + ], + + [ + 'New story and all existing imports', + { + story: `import { userEvent, within } from '@storybook/test'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Component } from './Component'; + +const meta: Meta = { + component: Component +}; +export default meta; +type Story = StoryObj; + +export const Default: Story = {};`, + code: { + imports: ["import { userEvent, within } from '@storybook/test';"], + play: [ + 'play: async ({ canvasElement }) => {', + '\tconst canvas = within(canvasElement.ownerDocument.body);', + "\tawait userEvent.click(await canvas.findByRole('button'));", + '}', + ], + }, + result: `import { userEvent, within } from '@storybook/test'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Component } from './Component'; + +const meta: Meta = { + component: Component +}; +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const NewStory: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement.ownerDocument.body); + await userEvent.click(await canvas.findByRole('button')); + } +};`, + newStoryId: 'form--new-story', + }, + ], + + [ + 'Existing story', + { + storyName: 'Default', + story: `import type { Meta, StoryObj } from '@storybook/react'; +import { Component } from './Component'; + +const meta: Meta = { + component: Component +}; +export default meta; +type Story = StoryObj; + +export const Default: Story = {};`, + code: { + imports: ["import { userEvent, within } from '@storybook/test';"], + play: [ + 'play: async ({ canvasElement }) => {', + '\tconst canvas = within(canvasElement.ownerDocument.body);', + "\tawait userEvent.click(await canvas.findByRole('button'));", + '}', + ], + }, + result: `import { userEvent, within } from '@storybook/test'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Component } from './Component'; + +const meta: Meta = { + component: Component +}; +export default meta; +type Story = StoryObj; + +export const Default: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement.ownerDocument.body); + await userEvent.click(await canvas.findByRole('button')); + } +};`, + newStoryId: 'form--default', + }, + ], + + [ + 'Pure javascript', + { + storyName: '!@#$%^&*() New Story1 2!@#$%^&*()', + story: `import { Component } from './Component'; + +export default { + component: Component +}; + +export const Default = {};`, + code: { + imports: ["import { userEvent, within } from '@storybook/test';"], + play: [ + 'play: async ({ canvasElement }) => {', + '\tconst canvas = within(canvasElement.ownerDocument.body);', + "\tawait userEvent.click(await canvas.findByRole('button'));", + '}', + ], + }, + result: `import { userEvent, within } from '@storybook/test'; +import { Component } from './Component'; + +export default { + component: Component +}; + +export const Default = {}; + +export const NewStory12 = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement.ownerDocument.body); + await userEvent.click(await canvas.findByRole('button')); + } +};`, + newStoryId: 'form--new-story-12', + }, + ], + + [ + 'New story with args', + { + storyName: 'NewStory', + args: JSON.stringify({ + hello: { + world: { + foo: 'bar', + baz: [1, 2, { boo: 'bee' }], + }, + }, + }), + story: `import type { Meta, StoryObj } from '@storybook/react'; +import { Component } from './Component'; + +const meta: Meta = { + component: Component +}; +export default meta; +type Story = StoryObj; + +export const Default: Story = {};`, + code: { + imports: ["import { userEvent, within } from '@storybook/test';"], + play: [ + 'play: async ({ canvasElement }) => {', + '\tconst canvas = within(canvasElement.ownerDocument.body);', + "\tawait userEvent.click(await canvas.findByRole('button'));", + '}', + ], + }, + result: `import { userEvent, within } from '@storybook/test'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Component } from './Component'; + +const meta: Meta = { + component: Component +}; +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const NewStory: Story = { + args: { + hello: { + "world": { + "foo": "bar", + + "baz": [1, 2, { + "boo": "bee" + }] + } + } + }, + + play: async ({ canvasElement }) => { + const canvas = within(canvasElement.ownerDocument.body); + await userEvent.click(await canvas.findByRole('button')); + } +};`, + newStoryId: 'form--new-story', + }, + ], + + [ + 'Existing story with args', + { + storyName: 'Default', + args: JSON.stringify({ + hello: { + world: { + foo: 'bar', + baz: [1, 2, { boo: 'bee' }], + }, + }, + }), + story: `import type { Meta, StoryObj } from '@storybook/react'; +import { Component } from './Component'; + +const meta: Meta = { + component: Component +}; +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + existing: "existing value" + } +};`, + code: { + imports: ["import { userEvent, within } from '@storybook/test';"], + play: [ + 'play: async ({ canvasElement }) => {', + '\tconst canvas = within(canvasElement.ownerDocument.body);', + "\tawait userEvent.click(await canvas.findByRole('button'));", + '}', + ], + }, + result: `import { userEvent, within } from '@storybook/test'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Component } from './Component'; + +const meta: Meta = { + component: Component +}; +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + existing: "existing value", + + hello: { + "world": { + "foo": "bar", + + "baz": [1, 2, { + "boo": "bee" + }] + } + } + }, + + play: async ({ canvasElement }) => { + const canvas = within(canvasElement.ownerDocument.body); + await userEvent.click(await canvas.findByRole('button')); + } +};`, + newStoryId: 'form--default', + }, + ], +] satisfies [ + string, + { + storyName?: string; + args?: string; + story: string; + code: GeneratedCode; + result: string; + newStoryId: string; + }, +][]; + +describe('generate-story-code', () => { + it.each(TEST_CASES)('%s', async (_, params) => { + const { storyCode, newStoryId } = await generateStoryCode({ + code: params.code, + name: 'storyName' in params ? params.storyName : 'NewStory', + args: 'args' in params ? params.args : '{}', + csfId: 'form--default', + csf: loadCsf(params.story, { makeTitle: () => 'story' }), + }); + expect(storyCode).toBe(params.result); + expect(newStoryId).toBe(params.newStoryId); + }); +}); diff --git a/src/codegen/generate-story-code.ts b/src/codegen/generate-story-code.ts new file mode 100644 index 0000000..c457ff4 --- /dev/null +++ b/src/codegen/generate-story-code.ts @@ -0,0 +1,68 @@ +import { storyNameFromExport, toId } from '@storybook/csf'; +import { formatFileContent } from 'storybook/internal/common'; +import { type CsfFile, printCsf } from 'storybook/internal/csf-tools'; +import type { GeneratedCode } from './interactions-to-code'; +import { + duplicateStoryWithNewName, + parseArgs, + removeExtraNewlines, + updateArgsInCsfFile, + updateImportsInCsfFile, + updatePlayInCsfFile, +} from './save-story-utils'; + +export const generateStoryCode = async ({ + csf, + csfId, + name, + args, + code, +}: { + csf: CsfFile; + csfId: string; + name: string; + args: string; + code: GeneratedCode; +}) => { + const parsed = csf.parse(); + const stories = Object.entries(parsed._stories); + + const [componentId, storyId] = csfId.split('--'); + + const [originalStoryName] = + stories.find(([key, value]) => value.id.endsWith(`--${storyId}`)) || []; + if (!originalStoryName) { + throw new Error('Source story not found.'); + } + + const newStoryName = name + .replace(/^[^a-z]/i, '') + .replace(/^\d+/, '') + .replace(/[^a-z0-9-_ ]/gi, '') + .replaceAll(/([-_ ]+[a-z0-9])/gi, (match) => + match.toUpperCase().replace(/[-_ ]/g, ''), + ); // from https://github.com/storybookjs/storybook/blob/1fdd2d6c675b81269125af5027e45a357c09f1fa/code/addons/controls/src/SaveStory.tsx#L122 + + const newStoryId = toId(componentId, storyNameFromExport(newStoryName)); + + const node = + csf._storyExports[newStoryName] ?? + duplicateStoryWithNewName(parsed, originalStoryName, newStoryName); + + const parsedArgs = args ? parseArgs(args) : {}; + if (Object.keys(parsedArgs).length) { + await updateArgsInCsfFile(node, args ? parseArgs(args) : {}); + } + + await updatePlayInCsfFile(node, code.play); + + await updateImportsInCsfFile(csf._ast, code.imports); + + return { + storyCode: await formatFileContent( + '.', + removeExtraNewlines(printCsf(csf).code, newStoryName), + ), + newStoryId, + }; +}; diff --git a/src/codegen/save-story-utils.ts b/src/codegen/save-story-utils.ts new file mode 100644 index 0000000..482eb22 --- /dev/null +++ b/src/codegen/save-story-utils.ts @@ -0,0 +1,399 @@ +// Copied from storybook package +// Except for "updatePlayInCsfFile" and "updateImportsInCsfFile" + +import { parser, types as t, traverse } from 'storybook/internal/babel'; +import type { CsfFile } from 'storybook/internal/csf-tools'; + +class SaveStoryError extends Error {} + +// biome-ignore lint/suspicious/noExplicitAny: +export function valueToAST(literal: T): any { + if (literal === null) { + return t.nullLiteral(); + } + switch (typeof literal) { + case 'function': + return parser.parse(literal.toString(), { + allowReturnOutsideFunction: true, + allowSuperOutsideMethod: true, + // @ts-expect-error (it's the contents of the function, it's an expression, trust me) + }).program.body[0]?.expression; + + case 'number': + return t.numericLiteral(literal); + case 'string': + return t.stringLiteral(literal); + case 'boolean': + return t.booleanLiteral(literal); + case 'undefined': + return t.identifier('undefined'); + default: + if (Array.isArray(literal)) { + return t.arrayExpression(literal.map(valueToAST)); + } + return t.objectExpression( + Object.keys(literal) + .filter((k) => { + // @ts-expect-error (it's a completely unknown object) + const value = literal[k]; + return typeof value !== 'undefined'; + }) + .map((k) => { + // @ts-expect-error (it's a completely unknown object) + const value = literal[k]; + return t.objectProperty(t.stringLiteral(k), valueToAST(value)); + }), + ); + } +} + +export const updateArgsInCsfFile = async ( + node: t.Node, + // biome-ignore lint/suspicious/noExplicitAny: + input: Record, +) => { + let found = false; + const args = Object.fromEntries( + Object.entries(input).map(([k, v]) => { + return [k, valueToAST(v)]; + }), + ); + + // detect CSF2 and throw + if (t.isArrowFunctionExpression(node) || t.isCallExpression(node)) { + throw new SaveStoryError('Updating a CSF2 story is not supported'); + } + + if (t.isObjectExpression(node)) { + const properties = node.properties; + const argsProperty = properties.find((property) => { + if (t.isObjectProperty(property)) { + const key = property.key; + return t.isIdentifier(key) && key.name === 'args'; + } + return false; + }); + + if (argsProperty) { + if (t.isObjectProperty(argsProperty)) { + const a = argsProperty.value; + if (t.isObjectExpression(a)) { + for (const p of a.properties) { + if (t.isObjectProperty(p)) { + const key = p.key; + if (t.isIdentifier(key) && key.name in args) { + p.value = args[key.name]; + delete args[key.name]; + } + } + } + + const remainder = Object.entries(args); + if (Object.keys(args).length) { + for (const [key, value] of remainder) { + a.properties.push(t.objectProperty(t.identifier(key), value)); + } + } + } + } + } else { + properties.unshift( + t.objectProperty( + t.identifier('args'), + t.objectExpression( + Object.entries(args).map(([key, value]) => + t.objectProperty(t.identifier(key), value), + ), + ), + ), + ); + } + return; + } + + traverse(node, { + ObjectExpression(path) { + if (found) { + return; + } + + found = true; + const properties = path.get('properties'); + const argsProperty = properties.find((property) => { + if (property.isObjectProperty()) { + const key = property.get('key'); + return key.isIdentifier() && key.node.name === 'args'; + } + return false; + }); + + if (argsProperty) { + if (argsProperty.isObjectProperty()) { + const a = argsProperty.get('value'); + if (a.isObjectExpression()) { + a.traverse({ + ObjectProperty(p) { + const key = p.get('key'); + if (key.isIdentifier() && key.node.name in args) { + p.get('value').replaceWith(args[key.node.name]); + delete args[key.node.name]; + } + }, + noScope: true, + }); + + const remainder = Object.entries(args); + if (Object.keys(args).length) { + for (const [key, value] of remainder) { + a.pushContainer( + 'properties', + t.objectProperty(t.identifier(key), value), + ); + } + } + } + } + } else { + path.unshiftContainer( + 'properties', + t.objectProperty( + t.identifier('args'), + t.objectExpression( + Object.entries(args).map(([key, value]) => + t.objectProperty(t.identifier(key), value), + ), + ), + ), + ); + } + }, + + noScope: true, + }); +}; + +type In = ReturnType; + +export const duplicateStoryWithNewName = ( + csfFile: In, + originalStoryName: string, + newStoryName: string, +) => { + const node = csfFile._storyExports[originalStoryName]; + const cloned = t.cloneNode(node) as t.VariableDeclarator; + + if (!cloned) { + throw new SaveStoryError('cannot clone Node'); + } + + let found = false; + traverse(cloned, { + Identifier(path) { + if (found) { + return; + } + + if (path.node.name === originalStoryName) { + found = true; + path.node.name = newStoryName; + } + }, + ObjectProperty(path) { + const key = path.get('key'); + if (key.isIdentifier() && key.node.name === 'args') { + path.remove(); + } + }, + + noScope: true, + }); + + // detect CSF2 and throw + if ( + t.isArrowFunctionExpression(cloned.init) || + t.isCallExpression(cloned.init) + ) { + throw new SaveStoryError( + 'Creating a new story based on a CSF2 story is not supported', + ); + } + + traverse(csfFile._ast, { + Program(path) { + path.pushContainer( + 'body', + t.exportNamedDeclaration(t.variableDeclaration('const', [cloned])), + ); + }, + }); + + return cloned; +}; + +// biome-ignore lint/suspicious/noExplicitAny: +export const parseArgs = (args: string): Record => + JSON.parse(args, (_, value) => { + if (value === '__sb_empty_function_arg__') { + return () => {}; + } + return value; + }); + +// Removes extra newlines between story properties. See https://github.com/benjamn/recast/issues/242 +// Only updates the part of the code for the story with the given name. +export const removeExtraNewlines = (code: string, name: string) => { + const anything = '([\\s\\S])'; // Multiline match for any character. + const newline = '(\\r\\n|\\r|\\n)'; // Either newlines or carriage returns may be used in the file. + const closing = `${newline}};${newline}`; // Marks the end of the story definition. + const regex = new RegExp( + // Looks for an export by the given name, considers the first closing brace on its own line + // to be the end of the story definition. + `^(?${anything}*)(?export const ${name} =${anything}+?${closing})(?${anything}*)$`, + ); + const { before, story, after } = code.match(regex)?.groups || {}; + return story + ? before + + story.replaceAll( + /(\r\n|\r|\n)(\r\n|\r|\n)([ \t]*[a-z0-9_]+): /gi, + '$2$3:', + ) + + after + : code; +}; + +export const updatePlayInCsfFile = async (node: t.Node, play: string[]) => { + let found = false; + + // detect CSF2 and throw + if (t.isArrowFunctionExpression(node) || t.isCallExpression(node)) { + throw new SaveStoryError('Updating a CSF2 story is not supported'); + } + + traverse(node, { + ObjectExpression(path) { + if (found) { + return; + } + + found = true; + const properties = path.get('properties'); + const playProperty = properties.find((property) => { + if (property.isObjectProperty()) { + const key = property.get('key'); + return key.isIdentifier() && key.node.name === 'play'; + } + return false; + }); + + const playExpression = t.arrowFunctionExpression( + [t.identifier('async ({ canvasElement })')], + t.blockStatement( + preparePlay(play).map((line) => t.expressionStatement(t.identifier(line))), + ), + ); + + if (playProperty) { + if (playProperty.isObjectProperty()) { + playProperty.get('value').replaceWith(playExpression); + } + } else { + path.pushContainer( + 'properties', + t.objectProperty(t.identifier('play'), playExpression), + ); + } + }, + + noScope: true, + }); +}; + +const preparePlay = (play: string[]) => play.slice(1, -1).map(prepareLine); + +const prepareLine = (line: string) => { + let result = line; + + if (result.endsWith(';')) { + result = result.slice(0, -1); + } + + if (result.startsWith('\t')) { + result = result.slice(1); + } + + return result; +}; + +export const updateImportsInCsfFile = async ( + node: t.Node, + imports: string[], +) => { + let found = false; + + // detect CSF2 and throw an error + if (t.isArrowFunctionExpression(node) || t.isCallExpression(node)) { + throw new SaveStoryError('Updating a CSF2 story is not supported'); + } + + traverse(node, { + Program(path) { + if (found) { + return; + } + + found = true; + + const parser = require('@babel/parser'); + + const packagesToImport = imports.map((importString) => { + const importStatement = parser.parse(importString, { + sourceType: 'module', + }); + + return { + importNode: importStatement.program.body[0], + importString, + }; + }); + + const importNodesBySource = new Map(); + for (const { node } of path.get('body')) { + if (t.isImportDeclaration(node)) { + importNodesBySource.set(node.source.value, node); + } + } + + for (const { importNode, importString } of packagesToImport) { + const source = importNode.source.value; + const existingImport = importNodesBySource.get(source); + + if (existingImport) { + const specifiers = importNode.specifiers; + const existingSpecifiers = existingImport.specifiers; + + const existingSpecifierNames = new Set( + existingSpecifiers.map((s) => s.local.name), + ); + + for (const specifier of specifiers) { + if (!existingSpecifierNames.has(specifier.local.name)) { + existingSpecifiers.push(specifier); + } + } + } else { + if (t.isFile(node)) { + node.program.body.unshift( + t.expressionStatement( + t.identifier( + importString.endsWith(';') ? importString.slice(0, -1) : importString, + ), + ), + ); + } + } + } + }, + + noScope: true, + }); +}; diff --git a/src/constants.ts b/src/constants.ts index a544a21..928132a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -6,6 +6,8 @@ export const IS_RECORDING_KEY = `${ADDON_ID}/is-recording`; export const EVENTS = { INTERACTION: `${ADDON_ID}/interaction`, + SAVE_NEW_STORY_REQUEST: `${ADDON_ID}/save-new-story-request`, + SAVE_NEW_STORY_RESPONSE: `${ADDON_ID}/save-new-story-response`, }; export const DOM_EVENTS = [ diff --git a/src/data.ts b/src/data.ts new file mode 100644 index 0000000..83607ac --- /dev/null +++ b/src/data.ts @@ -0,0 +1,13 @@ +import type { GeneratedCode } from './codegen/interactions-to-code'; + +export type SaveNewStoryRequestPayload = { + code: GeneratedCode; + csfId: string; + importPath: string; + args: string; + name: string; +}; + +export type SaveNewStoryResponsePayload = { + newStoryId: string; +}; diff --git a/src/manager.tsx b/src/manager.tsx index 99959d4..7d95b99 100644 --- a/src/manager.tsx +++ b/src/manager.tsx @@ -1,8 +1,10 @@ import { addons, types } from 'storybook/internal/manager-api'; import { AddonPanel, Badge, Spaced } from 'storybook/internal/components'; +import type { ResponseData } from 'storybook/internal/core-events'; import { InteractionRecorder } from './InteractionRecorder'; -import { ADDON_ID, PANEL_ID } from './constants'; +import { ADDON_ID, EVENTS, PANEL_ID } from './constants'; +import type { SaveNewStoryResponsePayload } from './data'; import { useInteractions, useIsRecording } from './state'; function Title() { @@ -23,7 +25,28 @@ function Title() { ); } -addons.register(ADDON_ID, () => { +addons.register(ADDON_ID, (api) => { + const channel = api.getChannel(); + + channel?.on( + EVENTS.SAVE_NEW_STORY_RESPONSE, + (data: ResponseData) => { + if (!data.success) { + return; + } + const story = api.getCurrentStoryData(); + + if (story.type !== 'story') { + return; + } + + api.resetStoryArgs(story); + if (data.payload.newStoryId) { + api.selectStory(data.payload.newStoryId); + } + }, + ); + addons.add(PANEL_ID, { type: types.PANEL, title: Title, diff --git a/src/preset.ts b/src/preset.ts new file mode 100644 index 0000000..77f81f9 --- /dev/null +++ b/src/preset.ts @@ -0,0 +1,75 @@ +import { writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import type { Channel } from 'storybook/internal/channels'; +import { + type RequestData, + type ResponseData, + STORY_RENDERED, +} from 'storybook/internal/core-events'; +import { readCsf } from 'storybook/internal/csf-tools'; +import type { Options } from 'storybook/internal/types'; +import { generateStoryCode } from './codegen/generate-story-code'; +import { EVENTS } from './constants'; +import type { + SaveNewStoryRequestPayload, + SaveNewStoryResponsePayload, +} from './data'; + +export const experimental_serverChannel = async ( + channel: Channel, + options: Options, +) => { + channel.on( + EVENTS.SAVE_NEW_STORY_REQUEST, + async ({ id, payload }: RequestData) => { + const { csfId, importPath, args, name, code } = payload; + + try { + const sourceFilePath = join(process.cwd(), importPath); + + const csf = await readCsf(sourceFilePath, { + makeTitle: (userTitle: string) => userTitle || 'myTitle', + }); + + const { storyCode, newStoryId } = await generateStoryCode({ + csf, + csfId, + name, + args, + code, + }); + + // Writing the CSF file should trigger HMR, which causes the story to rerender. Delay the + // response until that happens, but don't wait too long. + await Promise.all([ + new Promise((resolve) => { + channel.on(STORY_RENDERED, resolve); + setTimeout(() => resolve(channel.off(STORY_RENDERED, resolve)), 3000); + }), + writeFile(sourceFilePath, storyCode), + ]); + + channel.emit(EVENTS.SAVE_NEW_STORY_RESPONSE, { + id, + success: true, + payload: { newStoryId }, + error: null, + } satisfies ResponseData); + } catch (error) { + channel.emit(EVENTS.SAVE_NEW_STORY_RESPONSE, { + id, + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + } satisfies ResponseData); + + if (error instanceof Error) { + console.error( + `Error saving story: ${error.stack || error.message || error.toString()}`, + ); + } + } + }, + ); + + return channel; +}; diff --git a/src/stories/Form.stories.tsx b/src/stories/Form.stories.tsx index 3ca57c7..d23f3d7 100644 --- a/src/stories/Form.stories.tsx +++ b/src/stories/Form.stories.tsx @@ -1,172 +1,11 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { useState } from 'react'; +import { Form } from './Form'; -const Component = () => { - const [email, setEmail] = useState(''); - const [username, setUsername] = useState(''); - const [note, setNote] = useState(''); - const [selectedOption, setSelectedOption] = useState(null); - const [files, setFiles] = useState([]); - const [date, setDate] = useState(''); - const [formError, setFormError] = useState(''); - const [isSubmitSuccessful, setIsSubmitSuccessful] = useState(false); - - const onSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - if (!email) { - return setFormError('Email is required'); - } - - setIsSubmitSuccessful(true); - }; - - return ( -
-
- {isSubmitSuccessful ? ( -
-
Thank you for submitting!
- -
- {[ - `Email: ${email}`, - `Username: ${username}`, - `Note: ${note}`, - `Option: ${selectedOption}`, - `Files: ${files.map((file) => file.name).join(', ')}`, - `Date: ${date}`, - ].map((text) => ( -
{text}
- ))} -
-
- ) : ( -
-
-
- - setEmail(e.target.value)} - placeholder="Enter your email" - className="w-full rounded-md border border-[#e0e0e0] bg-white py-3 px-6 text-base font-medium text-[#6B7280] outline-none focus:border-[#6A64F1] focus:shadow-md" - /> -
-
- setUsername(e.target.value)} - placeholder="Enter your username" - className="w-full rounded-md border border-[#e0e0e0] bg-white py-3 px-6 text-base font-medium text-[#6B7280] outline-none focus:border-[#6A64F1] focus:shadow-md" - /> -
-
- -
-
- -