├── .changeset ├── README.md └── config.json ├── .devcontainer ├── Dockerfile ├── devcontainer.json └── seccomp_profile.json ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .husky └── pre-commit ├── .lintstagedrc ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── commitlint.config.js ├── e2e ├── addons │ ├── .ladle │ │ ├── components.tsx │ │ └── config.mjs │ ├── CHANGELOG.md │ ├── package.json │ ├── playwright.config.ts │ ├── src │ │ ├── README.md │ │ ├── a11y.stories.tsx │ │ ├── action.stories.tsx │ │ ├── context.tsx │ │ ├── controls.stories.tsx │ │ ├── docs.stories.mdx │ │ ├── hello.stories.tsx │ │ ├── mdx.stories.mdx │ │ ├── query-parameters.stories.tsx │ │ ├── styles.css │ │ └── width.stories.tsx │ ├── tests │ │ ├── a11y.spec.ts │ │ ├── action.spec.ts │ │ ├── axe.spec.ts │ │ ├── background.spec.ts │ │ ├── controls.spec.ts │ │ ├── docs.spec.ts │ │ ├── hello.spec.ts │ │ ├── mdx.spec.ts │ │ ├── query-parameters.spec.ts │ │ ├── source.spec.ts │ │ └── width.spec.ts │ └── vite.config.js ├── babel │ ├── CHANGELOG.md │ ├── package.json │ ├── playwright.config.ts │ ├── src │ │ ├── README.md │ │ ├── context.tsx │ │ ├── docs.stories.mdx │ │ ├── hello.stories.tsx │ │ └── mdx.stories.mdx │ ├── tests │ │ ├── docs.spec.ts │ │ ├── hello.spec.ts │ │ └── mdx.spec.ts │ └── vite.config.js ├── baseweb │ ├── .ladle │ │ └── components.tsx │ ├── CHANGELOG.md │ ├── package.json │ ├── playwright.config.ts │ ├── src │ │ └── hello.stories.tsx │ ├── tests │ │ └── hello.spec.ts │ └── vite.config.js ├── config-ts │ ├── .ladle │ │ └── config.mjs │ ├── CHANGELOG.md │ ├── package.json │ ├── playwright.config.ts │ ├── src │ │ ├── hello.show.tsx │ │ ├── paths.show.tsx │ │ └── to │ │ │ └── label.ts │ ├── tests │ │ ├── hello.spec.ts │ │ └── paths.spec.ts │ ├── tsconfig.json │ ├── vite-my-plugin.ts │ └── vite.config.ts ├── config │ ├── .ladle │ │ ├── config.mjs │ │ └── head.html │ ├── CHANGELOG.md │ ├── ladle-vite.config.js │ ├── package.json │ ├── playwright.config.ts │ ├── src │ │ ├── hello.show.tsx │ │ └── specific-file.custom.tsx │ └── tests │ │ ├── custom-path.spec.ts │ │ ├── expand-story-tree.spec.ts │ │ ├── head.spec.ts │ │ └── hello.spec.ts ├── css │ ├── .ladle │ │ ├── components.tsx │ │ └── styles.css │ ├── CHANGELOG.md │ ├── package.json │ ├── playwright.config.ts │ ├── postcss.config.js │ ├── src │ │ ├── hello.stories.tsx │ │ └── more.module.css │ ├── tailwind.config.js │ ├── tests │ │ └── hello.spec.ts │ └── vite.config.js ├── decorators │ ├── CHANGELOG.md │ ├── package.json │ ├── playwright.config.ts │ ├── src │ │ ├── args.stories.tsx │ │ ├── hello.stories.tsx │ │ ├── legacy-params.stories.tsx │ │ ├── mock-date.stories.tsx │ │ └── params.stories.tsx │ ├── tests │ │ ├── args.spec.ts │ │ ├── hello.spec.ts │ │ ├── legacy-params.spec.ts │ │ ├── mock-date.spec.ts │ │ └── params.spec.ts │ └── vite.config.js ├── msw │ ├── .gitignore │ ├── .ladle │ │ └── config.mjs │ ├── CHANGELOG.md │ ├── package.json │ ├── playwright.config.ts │ ├── public │ │ ├── posts.json │ │ └── todos.json │ ├── src │ │ ├── posts.stories.tsx │ │ ├── todos.stories.tsx │ │ └── utils.ts │ ├── tests │ │ ├── posts.spec.ts │ │ └── todos.spec.ts │ └── vite.config.js ├── playwright-config │ ├── package.json │ └── src │ │ └── index.ts ├── playwright │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── playwright.config.ts │ ├── src │ │ └── abc.stories.tsx │ ├── tests │ │ ├── snapshot.spec.ts │ │ └── snapshot.spec.ts-snapshots │ │ │ ├── abc--first-darwin.png │ │ │ ├── abc--first-linux.png │ │ │ └── abc--first-win32.png │ └── vite.config.ts ├── programmatic │ ├── CHANGELOG.md │ ├── build.js │ ├── get-meta.js │ ├── package.json │ ├── playwright.config.ts │ ├── preview.js │ ├── serve.js │ ├── src │ │ └── hello.stories.tsx │ ├── tests │ │ └── hello.spec.ts │ └── vite.config.js └── provider │ ├── .gitignore │ ├── .ladle │ └── components.tsx │ ├── CHANGELOG.md │ ├── package.json │ ├── playwright.config.ts │ ├── src │ ├── hello.stories.tsx │ ├── hmr.stories.tsx │ └── meta.stories.tsx │ ├── tests │ ├── hello.spec.ts │ ├── hmr.spec.ts │ └── meta.spec.ts │ └── vite.config.js ├── eslint.config.mjs ├── package.json ├── packages ├── example │ ├── .ladle │ │ └── config.mjs │ ├── CHANGELOG.md │ ├── package.json │ └── src │ │ ├── README.md │ │ ├── a11y.stories.tsx │ │ ├── control.stories.tsx │ │ ├── controls.stories.tsx │ │ ├── docs.stories.mdx │ │ ├── iframe-two.stories.tsx │ │ ├── iframe.stories.tsx │ │ ├── mdx.stories.mdx │ │ ├── my-blah.stories.tsx │ │ ├── my-story--sec.stories.tsx │ │ ├── my-story--sub.stories.tsx │ │ ├── my-story.stories.tsx │ │ ├── not-related.ts │ │ ├── ok-so.stories.tsx │ │ ├── query-parameters.stories.tsx │ │ ├── storyname.stories.tsx │ │ └── syntax.ts ├── ladle │ ├── .npmrc │ ├── CHANGELOG.md │ ├── README.md │ ├── api │ │ ├── build.js │ │ ├── meta.js │ │ ├── msw-node.js │ │ ├── preview.js │ │ └── serve.js │ ├── lib │ │ ├── app │ │ │ ├── exports.ts │ │ │ ├── favicon.svg │ │ │ ├── index.html │ │ │ ├── ladle.css │ │ │ ├── manifest.webmanifest │ │ │ ├── mask-icon.svg │ │ │ ├── robots.txt │ │ │ ├── src │ │ │ │ ├── addon-panel.tsx │ │ │ │ ├── addons │ │ │ │ │ ├── a11y.tsx │ │ │ │ │ ├── action.tsx │ │ │ │ │ ├── control.tsx │ │ │ │ │ ├── ladle.tsx │ │ │ │ │ ├── mode.tsx │ │ │ │ │ ├── rtl.tsx │ │ │ │ │ ├── source.tsx │ │ │ │ │ ├── theme.tsx │ │ │ │ │ └── width.tsx │ │ │ │ ├── app.tsx │ │ │ │ ├── args-provider.tsx │ │ │ │ ├── compose-enhancers.tsx │ │ │ │ ├── context.ts │ │ │ │ ├── debug.ts │ │ │ │ ├── dialog.tsx │ │ │ │ ├── error-boundary.tsx │ │ │ │ ├── get-config.ts │ │ │ │ ├── history.ts │ │ │ │ ├── icons.tsx │ │ │ │ ├── iframe │ │ │ │ │ ├── content.tsx │ │ │ │ │ ├── context.tsx │ │ │ │ │ ├── frame.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── index.tsx │ │ │ │ ├── init-side-effects.ts │ │ │ │ ├── local-storage.tsx │ │ │ │ ├── meta.json │ │ │ │ ├── mock-date.ts │ │ │ │ ├── msw.tsx │ │ │ │ ├── no-stories-error.tsx │ │ │ │ ├── no-stories.tsx │ │ │ │ ├── redirect-events.ts │ │ │ │ ├── reducer.ts │ │ │ │ ├── sidebar │ │ │ │ │ ├── main.tsx │ │ │ │ │ ├── tree-view.tsx │ │ │ │ │ └── utils.ts │ │ │ │ ├── story-hmr.ts │ │ │ │ ├── story-name.ts │ │ │ │ ├── story-not-found.tsx │ │ │ │ ├── story.tsx │ │ │ │ ├── synchronize-head.tsx │ │ │ │ └── ui.tsx │ │ │ ├── touch-icon.png │ │ │ └── window.d.ts │ │ ├── cli │ │ │ ├── apply-cli-config.js │ │ │ ├── build.js │ │ │ ├── cli.js │ │ │ ├── copy-msw-worker.js │ │ │ ├── debug.js │ │ │ ├── deps │ │ │ │ └── lodash.clonedeep.js │ │ │ ├── empty-module.js │ │ │ ├── get-app-id.js │ │ │ ├── get-app-root.js │ │ │ ├── get-folder-size.js │ │ │ ├── get-meta.js │ │ │ ├── get-user-vite-config.js │ │ │ ├── load-config.js │ │ │ ├── merge-vite-configs.js │ │ │ ├── open-browser.js │ │ │ ├── openChrome.applescript │ │ │ ├── preview.js │ │ │ ├── serve.js │ │ │ ├── vite-base.js │ │ │ ├── vite-dev.js │ │ │ ├── vite-plugin │ │ │ │ ├── ast-to-obj.js │ │ │ │ ├── babel.js │ │ │ │ ├── generate │ │ │ │ │ ├── cleanup-windows-path.js │ │ │ │ │ ├── get-components-import.js │ │ │ │ │ ├── get-config-import.js │ │ │ │ │ ├── get-generated-list.js │ │ │ │ │ ├── get-meta-json.js │ │ │ │ │ ├── get-story-imports.js │ │ │ │ │ ├── get-story-list.js │ │ │ │ │ └── get-story-source.js │ │ │ │ ├── get-ast.js │ │ │ │ ├── mdx-plugin.js │ │ │ │ ├── mdx-to-stories.js │ │ │ │ ├── naming-utils.js │ │ │ │ ├── parse │ │ │ │ │ ├── get-default-export.js │ │ │ │ │ ├── get-entry-data.js │ │ │ │ │ ├── get-named-exports.js │ │ │ │ │ └── get-storyname-and-meta.js │ │ │ │ ├── utils.js │ │ │ │ └── vite-plugin.js │ │ │ ├── vite-preview.js │ │ │ └── vite-prod.js │ │ └── shared │ │ │ ├── default-config.js │ │ │ └── types.ts │ ├── package.json │ ├── publish-next.js │ ├── scripts │ │ ├── package-types-helpers.js │ │ ├── revert-package-types.js │ │ ├── update-index-path.js │ │ └── update-package-types.js │ ├── tests │ │ ├── __snapshots__ │ │ │ ├── get-generated-list.test.ts.snap │ │ │ ├── get-meta-json.test.ts.snap │ │ │ └── mdx-to-stories.test.ts.snap │ │ ├── fixtures │ │ │ ├── animals.stories.tsx │ │ │ ├── capitalization.stories.tsx │ │ │ ├── default-meta.stories.tsx │ │ │ ├── default-title.stories.tsx │ │ │ ├── filenameCapitalization.stories.tsx │ │ │ ├── meta.stories.mdx │ │ │ ├── our-animals--mammals.stories.tsx │ │ │ ├── story-meta.stories.tsx │ │ │ ├── story.stories.mdx │ │ │ └── storyname.stories.tsx │ │ ├── get-generated-list.test.ts │ │ ├── get-meta-json.test.ts │ │ ├── mdx-to-stories.test.ts │ │ ├── naming-utils.test.ts │ │ ├── parse │ │ │ ├── .pnpm-debug.log │ │ │ ├── __snapshots__ │ │ │ │ └── get-entry-data.test.ts.snap │ │ │ ├── get-default-export.test.ts │ │ │ ├── get-entry-data.test.ts │ │ │ ├── get-named-exports.test.ts │ │ │ ├── get-storyname-and-meta.test.ts │ │ │ └── utils.ts │ │ └── story-name.test.ts │ ├── tsconfig.typesoutput.json │ └── types │ │ └── generated-list.d.ts └── website │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── babel.config.js │ ├── blog │ ├── 2022-03-15-introducing-ladle.md │ ├── 2022-06-09-ladle-v1.md │ ├── 2022-08-08-visual-snapshots.md │ ├── 2023-09-19-ladle-v3.md │ └── authors.yml │ ├── docs │ ├── a11y.mdx │ ├── actions.mdx │ ├── addons.md │ ├── babel.md │ ├── background.md │ ├── cli.md │ ├── config.md │ ├── controls.mdx │ ├── css.md │ ├── decorators.md │ ├── hotkeys.mdx │ ├── http2.md │ ├── introduction.mdx │ ├── links.md │ ├── mdx.mdx │ ├── meta.md │ ├── mock-date.mdx │ ├── msw.mdx │ ├── nextjs.md │ ├── programmatic.md │ ├── providers.md │ ├── setup.mdx │ ├── source.mdx │ ├── stories.mdx │ ├── troubleshooting.md │ ├── typescript.md │ ├── visual-snapshots.md │ └── width.mdx │ ├── docusaurus.config.js │ ├── package.json │ ├── sidebars.js │ ├── src │ ├── css │ │ └── custom.css │ ├── pages │ │ ├── index.js │ │ └── styles.module.css │ └── theme │ │ └── Footer │ │ └── index.js │ └── static │ ├── .nojekyll │ └── img │ ├── a11y.png │ ├── actions.png │ ├── build-times.png │ ├── compilation-time.svg │ ├── controls.png │ ├── favicon.ico │ ├── favicon.svg │ ├── hotkeys.png │ ├── ladle-baseweb.png │ ├── logo-gray.svg │ ├── logo.svg │ ├── story-navigation.png │ ├── story-source.png │ ├── undraw_docusaurus_mountain.svg │ ├── undraw_docusaurus_react.svg │ ├── undraw_docusaurus_tree.svg │ └── width.png ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── release.sh ├── tsconfig.json ├── turbo.json └── type-tests ├── argTypes.test.tsx ├── args.test.tsx ├── decorators.test.tsx └── meta.test.tsx /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets). 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.0.0/schema.json", 3 | "changelog": ["@changesets/changelog-github", { "repo": "tajo/ladle" }], 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch" 10 | } 11 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ladle", 3 | "build": { 4 | "dockerfile": "Dockerfile", 5 | "args": { 6 | "VARIANT": "v1.23.0-focal" 7 | } 8 | }, 9 | "runArgs": [ 10 | // https://playwright.dev/docs/docker#usage 11 | "--security-opt", 12 | "seccomp=${localWorkspaceFolder}/.devcontainer/seccomp_profile.json" 13 | ], 14 | "remoteUser": "pwuser", 15 | "customizations": { 16 | "vscode": { 17 | "extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve Ladle 4 | title: "" 5 | labels: needs triage 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | 11 | A clear and concise description of what the bug is. 12 | 13 | **Reproduction** 14 | 15 | Please, try to reproduce your issue with [Stackblitz](https://ladle.dev/new). Your issue gets much higher priority since it can be triaged efficiently. 16 | 17 | Alternatively, create a repo. Or describe steps to reproduce and copy&paste the output from: 18 | 19 | ```sh 20 | DEBUG=ladle* pnpm ladle serve 21 | DEBUG=ladle* pnpm ladle build 22 | ``` 23 | 24 | More about [troubleshooting](https://ladle.dev/docs/troubleshooting). 25 | 26 | **Environment** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for Ladle 4 | title: '' 5 | labels: needs triage 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | types: [opened, synchronize] 8 | 9 | jobs: 10 | build: 11 | name: Build and Test 12 | runs-on: ${{ matrix.os }} 13 | timeout-minutes: 15 14 | env: 15 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 16 | TURBO_TEAM: ${{ secrets.TURBO_TEAM }} 17 | CI: true 18 | strategy: 19 | matrix: 20 | os: [ubuntu-latest, windows-latest] 21 | node-version: [22.x] 22 | steps: 23 | - name: Check out code 24 | uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 2 27 | 28 | - uses: pnpm/action-setup@v4 29 | with: 30 | version: 9.15.1 31 | 32 | - name: Setup Node.js environment 33 | uses: actions/setup-node@v4 34 | with: 35 | node-version: ${{ matrix.node-version }} 36 | cache: "pnpm" 37 | 38 | - name: Install dependencies 39 | run: pnpm install --frozen-lockfile 40 | 41 | - name: Install playwright 42 | run: pnpm exec playwright install --with-deps 43 | 44 | - name: Eslint 45 | run: pnpm lint 46 | 47 | - name: Typescript 48 | run: pnpm typecheck 49 | 50 | - name: Build 51 | run: pnpm build 52 | 53 | - name: Unit and e2e tests 54 | run: pnpm test 55 | 56 | - uses: actions/upload-artifact@v4 57 | if: always() 58 | with: 59 | name: playwright-report-${{ matrix.runs-on }} 60 | path: playwright-report-${{ matrix.runs-on }}/ 61 | retention-days: 30 62 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write 16 | pull-requests: write 17 | id-token: write 18 | steps: 19 | - name: Checkout Repo 20 | uses: actions/checkout@v4 21 | with: 22 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 23 | fetch-depth: 0 24 | 25 | - uses: pnpm/action-setup@v4 26 | with: 27 | version: 9.15.1 28 | 29 | - name: Setup Node.js environment 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: 22.11 33 | cache: "pnpm" 34 | 35 | - name: Install dependencies 36 | run: pnpm install --frozen-lockfile 37 | 38 | - name: Creating .npmrc 39 | run: | 40 | cat << EOF > "$HOME/.npmrc" 41 | email=vojtech@miksu.cz 42 | //registry.npmjs.org/:_authToken=$NPM_TOKEN 43 | EOF 44 | env: 45 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 46 | 47 | - name: Create Release Pull Request or Publish to npm 48 | id: changesets 49 | uses: changesets/action@v1 50 | with: 51 | publish: pnpm release 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GIT_DEPLOY_KEY }} 54 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 55 | 56 | - name: Publishing next version 57 | run: cd packages/ladle && pnpm types && ./publish-next.js 58 | env: 59 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .vscode 4 | build 5 | *.tsbuildinfo 6 | .DS_Store 7 | yarn-error.log 8 | cjs 9 | .turbo 10 | .pnpm-store 11 | packages/ladle/typings-for-build 12 | e2e/*/test-results 13 | 14 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm exec lint-staged 2 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,ts,tsx,css,scss,postcss,md,json}": [ 3 | "prettier --write", 4 | "prettier --check" 5 | ], 6 | "*.{js,ts,tsx}": ["eslint --max-warnings=0 --fix"] 7 | } 8 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.9.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | pnpm-workspace.yaml 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": false, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Vojtech Miksu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 2.x.x | :white_check_mark: | 8 | | < 2.0 | :x: | 9 | 10 | ## Reporting a Vulnerability 11 | 12 | You can disclose any vulnerability directly to vojtech@miksu.cz or 13 | through our [discord](https://discord.gg/H6FSHjyW7e) to 14 | any team member. 15 | 16 | 17 | If the vulnerability is verified, you can expect a published fix 18 | in 2 weeks. After that, it should be disclosed publicly. 19 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ["@commitlint/config-conventional"] }; 2 | -------------------------------------------------------------------------------- /e2e/addons/.ladle/components.tsx: -------------------------------------------------------------------------------- 1 | export const args = { 2 | test: true, 3 | }; 4 | 5 | export const argTypes = { 6 | background: { 7 | name: "Main background", 8 | control: { 9 | type: "background", 10 | labels: { 11 | // 'labels' maps option values to string labels 12 | purple: "Purple fun", 13 | }, 14 | }, 15 | options: ["purple", "blue", "white", "pink"], 16 | defaultValue: "white", 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /e2e/addons/.ladle/config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | addons: { 3 | a11y: { 4 | enabled: true, 5 | }, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /e2e/addons/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-addons", 3 | "version": "0.2.79", 4 | "license": "MIT", 5 | "private": true, 6 | "type": "module", 7 | "scripts": { 8 | "serve": "ladle serve -p 61100", 9 | "serve-prod": "ladle preview -p 61100", 10 | "build": "ladle build", 11 | "lint": "echo 'no lint'", 12 | "test-dev": "cross-env TYPE=dev pnpm exec playwright test", 13 | "test-prod": "cross-env TYPE=prod pnpm exec playwright test", 14 | "test": "pnpm test-dev && pnpm test-prod" 15 | }, 16 | "dependencies": { 17 | "@ladle/playwright-config": "workspace:*", 18 | "@ladle/react": "workspace:*", 19 | "@playwright/test": "^1.49.1", 20 | "axe-playwright": "^2.0.3", 21 | "cross-env": "^7.0.3", 22 | "query-string": "^9.1.1", 23 | "react": "^19.0.0", 24 | "react-dom": "^19.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /e2e/addons/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import getPlaywrightConfig from "@ladle/playwright-config"; 2 | 3 | export default getPlaywrightConfig({ 4 | port: 61100, 5 | }); 6 | -------------------------------------------------------------------------------- /e2e/addons/src/README.md: -------------------------------------------------------------------------------- 1 | # test-page 2 | 3 | React component 4 | 5 | Screenshot 6 | 7 | --- 8 | 9 | ## Install 10 | 11 | ``` 12 | $ jz add @test/react-test 13 | ``` 14 | 15 | ## Usage 16 | 17 | ### Test 18 | 19 | Stateful version of the component. It does requests to the uOwn backend and controls state of the tree. 20 | 21 | ``` 22 | yarn add-service 23 | ``` 24 | 25 | - Now you are ready to add react component 26 | 27 | ```js 28 | import { Test } from "test/react"; 29 | ``` 30 | 31 | ```jsx 32 | console.log(`Node selected ${name}`)} 34 | maxWidth="100%" 35 | /> 36 | ``` 37 | 38 | #### Props 39 | 40 | - `uuid?`: string 41 | - `onNodeChange?`: callback function, invokes when changing active node, passes object: `{uuid, name}` 42 | 43 | ## Developing 44 | 45 | Clone this project: 46 | 47 | ``` 48 | $ git clone gitolite@code.test 49 | ``` 50 | 51 | Go to the component folder: `src/test` 52 | 53 | Install the dependencies via `yarn`: 54 | 55 | ``` 56 | $ yarn install 57 | ``` 58 | 59 | ## Ownership 60 | 61 | This template is authored by Santa Claus. 62 | -------------------------------------------------------------------------------- /e2e/addons/src/a11y.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Story } from "@ladle/react"; 2 | 3 | export const Issues: Story = () => ( 4 | <> 5 | 6 | 9 | 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /e2e/addons/src/action.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Story } from "@ladle/react"; 2 | import { action } from "@ladle/react"; 3 | 4 | export const Basic: Story<{ 5 | onClick: () => void; 6 | }> = ({ onClick }) => { 7 | return ( 8 | <> 9 | 12 | 15 | 16 | ); 17 | }; 18 | 19 | Basic.argTypes = { 20 | onClick: { 21 | action: "clicked", 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /e2e/addons/src/context.tsx: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { useLadleContext } from "@ladle/react"; 3 | 4 | function Context() { 5 | const { globalState } = useLadleContext(); 6 | return
{JSON.stringify(globalState)}
; 7 | } 8 | 9 | export default Context; 10 | -------------------------------------------------------------------------------- /e2e/addons/src/docs.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Description, Story } from "@ladle/react"; 2 | import Readme from "./README.md"; 3 | 4 | 5 | 6 | ## Subtitle 7 | 8 | 9 | -------------------------------------------------------------------------------- /e2e/addons/src/hello.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Story } from "@ladle/react"; 2 | import { useLink } from "@ladle/react"; 3 | import Context from "./context"; 4 | 5 | export const World: Story = () => { 6 | const link = useLink(); 7 | return ( 8 | <> 9 | 10 |

Hello World

11 | 14 | 15 | ); 16 | }; 17 | 18 | export const Linked: Story = () => { 19 | return

Linked Story

; 20 | }; 21 | -------------------------------------------------------------------------------- /e2e/addons/src/mdx.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Story, Meta } from "@ladle/react"; 2 | 3 | # MDX Button 4 | 5 | With `MDX`, you can define button w/e.dd 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /e2e/addons/src/query-parameters.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Story } from "@ladle/react"; 2 | import { useEffect, useState } from "react"; 3 | 4 | export const QueryParameters: Story = () => { 5 | const [queryParams, setQueryParams] = useState(""); 6 | 7 | useEffect(() => { 8 | setQueryParams(location.search); 9 | }, [location.search]); 10 | 11 | return

Params: {queryParams}

; 12 | }; 13 | 14 | QueryParameters.decorators = [ 15 | (Component) => { 16 | useEffect(() => { 17 | const url = new URL(window.location.href); 18 | url.searchParams.set("foo", "bar"); 19 | history.pushState({}, "", url); 20 | }, []); 21 | 22 | return ( 23 | <> 24 | 25 | 26 | ); 27 | }, 28 | ]; 29 | -------------------------------------------------------------------------------- /e2e/addons/src/styles.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: rgb(255, 192, 203); 3 | } 4 | -------------------------------------------------------------------------------- /e2e/addons/src/width.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Story } from "@ladle/react"; 2 | import "./styles.css"; 3 | 4 | export const Iframed: Story = () =>

Iframed

; 5 | Iframed.meta = { 6 | iframed: true, 7 | }; 8 | 9 | export const NoIframe: Story = () =>

No Iframe

; 10 | 11 | export const SetCustom: Story = () =>

Width set

; 12 | SetCustom.meta = { 13 | width: 555, 14 | }; 15 | 16 | export const SetSmall: Story = () =>

Width set

; 17 | SetSmall.meta = { 18 | width: "small", 19 | }; 20 | -------------------------------------------------------------------------------- /e2e/addons/tests/a11y.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test.skip("a11y addon works", async ({ page }) => { 4 | await page.goto("/?story=a11y--issues"); 5 | const button = page.locator('[data-testid="addon-a11y"]'); 6 | await button.click(); 7 | await expect(page.locator('[data-testid="ladle-dialog"]')).toContainText( 8 | "There are 3 axe accessibility violationsElements must have sufficient color contrast (1). Show detailsImages must have alternate text (1). Show detailsForm elements must have labels (1). Show details", 9 | ); 10 | }); 11 | -------------------------------------------------------------------------------- /e2e/addons/tests/action.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test("action passed through argTypes", async ({ page }) => { 4 | await page.goto("/?story=action--basic"); 5 | const userButton = page.locator("#args-button"); 6 | await userButton.click(); 7 | const button = page.locator('[data-testid="addon-action"]'); 8 | await button.click(); 9 | await expect(page.locator(".ladle-addon-modal-body")).toContainText( 10 | "onClick", 11 | ); 12 | }); 13 | 14 | test("dynamic action", async ({ page }) => { 15 | await page.goto("/?story=action--basic"); 16 | const userButton = page.locator("#manual-button"); 17 | await userButton.click(); 18 | const button = page.locator('[data-testid="addon-action"]'); 19 | await button.click(); 20 | await expect(page.locator(".ladle-addon-modal-body")).toContainText("second"); 21 | }); 22 | -------------------------------------------------------------------------------- /e2e/addons/tests/axe.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | import { injectAxe, checkA11y, configureAxe } from "axe-playwright"; 3 | 4 | test("empty label doesn't violate axe run", async ({ page }) => { 5 | await page.goto("/?story=hello--world"); 6 | await page.waitForSelector("[data-storyloaded]"); 7 | await injectAxe(page); 8 | await configureAxe(page); 9 | await checkA11y(page, undefined, { 10 | detailedReport: true, 11 | axeOptions: {}, 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /e2e/addons/tests/background.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test("global argTypes and args exist", async ({ page }) => { 4 | await page.goto("/?story=hello--linked"); 5 | const button = page.locator('[data-testid="addon-control"]'); 6 | await button.click(); 7 | //await page.check("#cities-Prague"); 8 | await expect(page.locator(".ladle-controls-table")).toContainText( 9 | "Main backgroundPurple funbluewhitepinktest", 10 | ); 11 | }); 12 | 13 | test("change background color", async ({ page }) => { 14 | await page.goto("/?story=hello--linked"); 15 | const button = page.locator('[data-testid="addon-control"]'); 16 | await button.click(); 17 | await page.check("#background-pink"); 18 | 19 | const bgDiv = page.locator(".ladle-background"); 20 | const color = await bgDiv.evaluate((e: any) => { 21 | return window.getComputedStyle(e).getPropertyValue("background-color"); 22 | }); 23 | // pink color 24 | expect(color).toBe("rgb(255, 192, 203)"); 25 | }); 26 | -------------------------------------------------------------------------------- /e2e/addons/tests/docs.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test("mdx readme is rendered", async ({ page }) => { 4 | await page.goto("/?story=docs--documentation"); 5 | await expect(page.locator("h1")).toHaveText("test-page"); 6 | await expect(page.locator("img")).toHaveAttribute( 7 | "src", 8 | "https://ladle.dev/img/ladle-baseweb.png", 9 | ); 10 | let i = 0; 11 | const h2s = ["Subtitle", "Install", "Usage", "Developing", "Ownership"]; 12 | for (const h2 of await page.locator("h2").all()) { 13 | await expect(h2).toHaveText(h2s[i]); 14 | i++; 15 | } 16 | }); 17 | 18 | test("mdx story rendering does not throw errors in console", async ({ 19 | page, 20 | }) => { 21 | page.on("console", (msg) => { 22 | expect(msg.type()).not.toBe("error"); 23 | }); 24 | 25 | await page.goto("/?story=docs--documentation"); 26 | await expect(page.locator("h1")).toHaveText("test-page"); 27 | }); 28 | -------------------------------------------------------------------------------- /e2e/addons/tests/hello.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test("navigate to a different story through useLink", async ({ page }) => { 4 | await page.goto("/?story=hello--world"); 5 | const button = page.locator("#btn"); 6 | await button.click(); 7 | await expect(page.locator("h2")).toHaveText("Linked Story"); 8 | }); 9 | 10 | test("useLadleContext works", async ({ page }) => { 11 | await page.goto("/?story=hello--world"); 12 | await expect(page.locator("#context-div")).toHaveText( 13 | `{"theme":"light","mode":"full","story":"hello--world","rtl":false,"source":false,"width":0,"control":{"test":{"type":"boolean","defaultValue":true,"value":true,"description":""},"background":{"name":"Main background","type":"background","labels":{"purple":"Purple fun"},"defaultValue":"white","options":["purple","blue","white","pink"],"value":"white","description":"background"}},"action":[],"controlInitialized":true,"hotkeys":true}`, 14 | ); 15 | }); 16 | -------------------------------------------------------------------------------- /e2e/addons/tests/mdx.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test("mdx story is rendered", async ({ page }) => { 4 | await page.goto("/?story=mdx--first"); 5 | await expect(page.locator("main button")).toHaveText("simple"); 6 | }); 7 | -------------------------------------------------------------------------------- /e2e/addons/tests/source.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test("boolean control works", async ({ page }) => { 4 | await page.goto("/?story=controls--controls"); 5 | const button = page.locator('[data-testid="addon-source"]'); 6 | await button.click(); 7 | await expect(page.locator(".ladle-code")).toContainText( 8 | "src/controls.stories.tsx", 9 | ); 10 | await expect(page.locator("pre")).toContainText( 11 | 'import type { StoryDefault, Story } from "@ladle/react";', 12 | ); 13 | }); 14 | -------------------------------------------------------------------------------- /e2e/addons/vite.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | server: { 3 | open: "none", 4 | host: "127.0.0.1", 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /e2e/babel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-babel", 3 | "version": "0.3.51", 4 | "license": "MIT", 5 | "private": true, 6 | "scripts": { 7 | "serve": "ladle serve -p 61109", 8 | "serve-prod": "ladle preview -p 61109", 9 | "build": "ladle build", 10 | "lint": "echo 'no lint'", 11 | "test-dev": "cross-env TYPE=dev pnpm exec playwright test", 12 | "test-prod": "cross-env TYPE=prod pnpm exec playwright test", 13 | "test": "pnpm test-dev && pnpm test-prod" 14 | }, 15 | "dependencies": { 16 | "@ladle/playwright-config": "workspace:*", 17 | "@ladle/react": "workspace:*", 18 | "@playwright/test": "^1.49.1", 19 | "@vitejs/plugin-react": "^4.3.4", 20 | "cross-env": "^7.0.3", 21 | "react": "^19.0.0", 22 | "react-dom": "^19.0.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /e2e/babel/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import getPlaywrightConfig from "@ladle/playwright-config"; 2 | 3 | export default getPlaywrightConfig({ 4 | port: 61109, 5 | }); 6 | -------------------------------------------------------------------------------- /e2e/babel/src/README.md: -------------------------------------------------------------------------------- 1 | # test-page 2 | 3 | React component 4 | 5 | Screenshot 6 | 7 | --- 8 | 9 | ## Install 10 | 11 | ``` 12 | $ jz add @test/react-test 13 | ``` 14 | 15 | ## Usage 16 | 17 | ### Test 18 | 19 | Stateful version of the component. It does requests to the uOwn backend and controls state of the tree. 20 | 21 | ``` 22 | yarn add-service 23 | ``` 24 | 25 | - Now you are ready to add react component 26 | 27 | ```js 28 | import { Test } from "test/react"; 29 | ``` 30 | 31 | ```jsx 32 | console.log(`Node selected ${name}`)} 34 | maxWidth="100%" 35 | /> 36 | ``` 37 | 38 | #### Props 39 | 40 | - `uuid?`: string 41 | - `onNodeChange?`: callback function, invokes when changing active node, passes object: `{uuid, name}` 42 | 43 | ## Developing 44 | 45 | Clone this project: 46 | 47 | ``` 48 | $ git clone gitolite@code.test 49 | ``` 50 | 51 | Go to the component folder: `src/test` 52 | 53 | Install the dependencies via `yarn`: 54 | 55 | ``` 56 | $ yarn install 57 | ``` 58 | 59 | ## Ownership 60 | 61 | This template is authored by Santa Claus. 62 | -------------------------------------------------------------------------------- /e2e/babel/src/context.tsx: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { useLadleContext } from "@ladle/react"; 3 | 4 | function Context() { 5 | const { globalState } = useLadleContext(); 6 | return
{JSON.stringify(globalState)}
; 7 | } 8 | 9 | export default Context; 10 | -------------------------------------------------------------------------------- /e2e/babel/src/docs.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Description, Story } from "@ladle/react"; 2 | import Readme from "./README.md"; 3 | 4 | 5 | 6 | ## Subtitle 7 | 8 | 9 | -------------------------------------------------------------------------------- /e2e/babel/src/hello.stories.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import type { Story } from "@ladle/react"; 3 | import Context from "./context"; 4 | 5 | export const World: Story = () => { 6 | const [val, setVal] = useState(true); 7 | return ( 8 |
9 | 10 |

Hello World

11 | 14 |
15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /e2e/babel/src/mdx.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Story, Meta } from "@ladle/react"; 2 | 3 | # MDX Button 4 | 5 | With `MDX`, you can define button w/e.dd 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /e2e/babel/tests/docs.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test("mdx readme is rendered", async ({ page }) => { 4 | await page.goto("/?story=docs--documentation"); 5 | await expect(page.locator("h1")).toHaveText("test-page"); 6 | await expect(page.locator("img")).toHaveAttribute( 7 | "src", 8 | "https://ladle.dev/img/ladle-baseweb.png", 9 | ); 10 | let i = 0; 11 | const h2s = ["Subtitle", "Install", "Usage", "Developing", "Ownership"]; 12 | for (const h2 of await page.locator("h2").all()) { 13 | await expect(h2).toHaveText(h2s[i]); 14 | i++; 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /e2e/babel/tests/hello.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test("default story is rendered", async ({ page }) => { 4 | await page.goto("/?story=hello--world"); 5 | await expect(page.locator("h1")).toHaveText("Hello World"); 6 | }); 7 | 8 | test("useLadleContext works", async ({ page }) => { 9 | await page.goto("/?story=hello--world"); 10 | await expect(page.locator("#context-div")).toHaveText( 11 | `{"theme":"light","mode":"full","story":"hello--world","rtl":false,"source":false,"width":0,"control":{},"action":[],"controlInitialized":true,"hotkeys":true}`, 12 | ); 13 | }); 14 | -------------------------------------------------------------------------------- /e2e/babel/tests/mdx.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test("mdx story is rendered", async ({ page }) => { 4 | await page.goto("/?story=mdx--first"); 5 | await expect(page.locator("main button")).toHaveText("simple"); 6 | }); 7 | -------------------------------------------------------------------------------- /e2e/babel/vite.config.js: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react"; 2 | 3 | export default { 4 | plugins: [react()], 5 | server: { 6 | open: "none", 7 | host: "127.0.0.1", 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /e2e/baseweb/.ladle/components.tsx: -------------------------------------------------------------------------------- 1 | import { Provider as StyletronProvider } from "styletron-react"; 2 | import { Client as Styletron } from "styletron-engine-monolithic"; 3 | import { LightTheme, DarkTheme, BaseProvider } from "baseui"; 4 | import type { GlobalProvider } from "@ladle/react"; 5 | 6 | const engine = new Styletron(); 7 | 8 | export const Provider: GlobalProvider = ({ children, globalState }) => ( 9 | 10 | 16 | {children} 17 | 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /e2e/baseweb/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-baseweb", 3 | "version": "0.0.94", 4 | "license": "MIT", 5 | "private": true, 6 | "scripts": { 7 | "serve": "ladle serve -p 61111", 8 | "serve-prod": "ladle preview -p 61111", 9 | "build": "ladle build", 10 | "lint": "echo 'no lint'", 11 | "test-dev": "cross-env TYPE=dev pnpm exec playwright test", 12 | "test-prod": "cross-env TYPE=prod pnpm exec playwright test", 13 | "test": "pnpm test-dev && pnpm test-prod" 14 | }, 15 | "dependencies": { 16 | "@ladle/playwright-config": "workspace:*", 17 | "@ladle/react": "workspace:*", 18 | "@playwright/test": "^1.49.1", 19 | "autoprefixer": "^10.4.20", 20 | "baseui": "^14.0.0", 21 | "cross-env": "^7.0.3", 22 | "postcss": "^8.4.49", 23 | "react": "^19.0.0", 24 | "react-dom": "^19.0.0", 25 | "styletron-engine-monolithic": "^1.0.0", 26 | "styletron-react": "^6.1.1", 27 | "tailwindcss": "^3.4.17" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /e2e/baseweb/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import getPlaywrightConfig from "@ladle/playwright-config"; 2 | 3 | export default getPlaywrightConfig({ 4 | port: 61111, 5 | }); 6 | -------------------------------------------------------------------------------- /e2e/baseweb/src/hello.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Story } from "@ladle/react"; 2 | import { Button } from "baseui/button"; 3 | import { StarRating } from "baseui/rating"; 4 | import { useState } from "react"; 5 | 6 | export const World: Story = () => { 7 | const [open, setOpen] = useState(false); 8 | const [value, setValue] = useState(3); 9 | return ( 10 | <> 11 |

Hello world!

12 | 13 | {open && ( 14 |
15 | setValue(data.value)} 18 | size={22} 19 | value={value} 20 | /> 21 |
22 | )} 23 |

24 | 33 |

34 |

35 | 47 |

48 | 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /e2e/baseweb/tests/hello.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test("Base Web and CSSStyleSheet works correctly without iframe", async ({ 4 | page, 5 | }) => { 6 | await page.goto("/?story=hello--world"); 7 | await expect(page.locator('[data-baseweb="button"]')).toHaveCSS( 8 | "border-bottom-left-radius", 9 | "8px", 10 | ); 11 | await expect(page.locator("h1")).toHaveCSS( 12 | "background-color", 13 | "rgba(0, 0, 0, 0)", 14 | ); 15 | const button = page.locator('[data-testid="add"]'); 16 | await button.click(); 17 | await expect(page.locator("h1")).toHaveCSS( 18 | "background-color", 19 | "rgb(255, 192, 203)", 20 | ); 21 | const buttonRemove = page.locator('[data-testid="remove"]'); 22 | await buttonRemove.click(); 23 | await expect(page.locator("h1")).toHaveCSS( 24 | "background-color", 25 | "rgba(0, 0, 0, 0)", 26 | ); 27 | }); 28 | 29 | test("Base Web and CSSStyleSheet works correctly in iframe", async ({ 30 | page, 31 | }) => { 32 | await page.goto("/?story=hello--world&width=414"); 33 | await expect( 34 | page.frameLocator("iframe").locator('[data-baseweb="button"]'), 35 | ).toHaveCSS("border-bottom-left-radius", "8px"); 36 | await expect(page.frameLocator("iframe").locator("h1")).toHaveCSS( 37 | "background-color", 38 | "rgba(0, 0, 0, 0)", 39 | ); 40 | const button = await page 41 | .frameLocator("iframe") 42 | .locator('[data-testid="add"]'); 43 | await button.click(); 44 | await expect(page.frameLocator("iframe").locator("h1")).toHaveCSS( 45 | "background-color", 46 | "rgb(255, 192, 203)", 47 | ); 48 | const buttonRemove = await page 49 | .frameLocator("iframe") 50 | .locator('[data-testid="remove"]'); 51 | await buttonRemove.click(); 52 | await expect(page.frameLocator("iframe").locator("h1")).toHaveCSS( 53 | "background-color", 54 | "rgba(0, 0, 0, 0)", 55 | ); 56 | }); 57 | -------------------------------------------------------------------------------- /e2e/baseweb/vite.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | server: { 3 | open: "none", 4 | host: "127.0.0.1", 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /e2e/config-ts/.ladle/config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | stories: "src/**/*.show.{js,jsx,ts,tsx}", 3 | }; 4 | -------------------------------------------------------------------------------- /e2e/config-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-config-ts", 3 | "version": "1.0.72", 4 | "license": "MIT", 5 | "private": true, 6 | "scripts": { 7 | "serve": "ladle serve -p 61108", 8 | "serve-prod": "ladle preview -p 61108", 9 | "build": "ladle build", 10 | "lint": "echo 'no lint'", 11 | "test-dev": "cross-env TYPE=dev pnpm exec playwright test", 12 | "test-prod": "cross-env TYPE=prod pnpm exec playwright test", 13 | "test": "pnpm test-dev && pnpm test-prod" 14 | }, 15 | "dependencies": { 16 | "@ladle/playwright-config": "workspace:*", 17 | "@ladle/react": "workspace:*", 18 | "@playwright/test": "^1.49.1", 19 | "cross-env": "^7.0.3", 20 | "react": "^19.0.0", 21 | "react-dom": "^19.0.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /e2e/config-ts/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import getPlaywrightConfig from "@ladle/playwright-config"; 2 | 3 | export default getPlaywrightConfig({ 4 | port: 61108, 5 | }); 6 | -------------------------------------------------------------------------------- /e2e/config-ts/src/hello.show.tsx: -------------------------------------------------------------------------------- 1 | import type { Story } from "@ladle/react"; 2 | 3 | declare const __filename_root: string; 4 | declare const __dirname_root: string; 5 | declare const __filename_myPlugin: string; 6 | declare const __dirname_myPlugin: string; 7 | 8 | export const World: Story = () => { 9 | return ( 10 |
11 |

Hello World

12 | 13 |
14 |
filename root
15 |
{__filename_root}
16 | 17 |
dirname root
18 |
{__dirname_root}
19 | 20 |
filename myPlugin
21 |
{__filename_myPlugin}
22 | 23 |
dirname myPlugin
24 |
{__dirname_myPlugin}
25 |
26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /e2e/config-ts/src/paths.show.tsx: -------------------------------------------------------------------------------- 1 | import { label } from "@/label"; 2 | 3 | export const Path = () =>

{label}

; 4 | -------------------------------------------------------------------------------- /e2e/config-ts/src/to/label.ts: -------------------------------------------------------------------------------- 1 | export const label = "label"; 2 | -------------------------------------------------------------------------------- /e2e/config-ts/tests/hello.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | 3 | test("default story is rendered", async ({ page }) => { 4 | await page.goto("/?story=hello--world"); 5 | await expect(page.locator("h1")).toHaveText("Hello World"); 6 | }); 7 | 8 | test("__filename and __dirname are replaced", async ({ page }) => { 9 | await page.goto("/?story=hello--world"); 10 | 11 | await expect(page.locator("[data-test=filename_root]")).toHaveText( 12 | /e2e[\/\\]config-ts[\/\\]vite\.config\.ts/, 13 | ); 14 | await expect(page.locator("[data-test=dirname_root]")).toHaveText( 15 | /e2e[\/\\]config-ts/, 16 | ); 17 | 18 | await expect(page.locator("[data-test=filename_myPlugin]")).toHaveText( 19 | /e2e[\/\\]config-ts[\/\\]vite-my-plugin\.ts/, 20 | ); 21 | await expect(page.locator("[data-test=dirname_myPlugin]")).toHaveText( 22 | /e2e[\/\\]config-ts/, 23 | ); 24 | }); 25 | -------------------------------------------------------------------------------- /e2e/config-ts/tests/paths.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | 3 | test("vite-tsconfig-path works", async ({ page }) => { 4 | await page.goto("/?story=paths--path"); 5 | await expect(page.locator("h1")).toHaveText("label"); 6 | }); 7 | -------------------------------------------------------------------------------- /e2e/config-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*", "./src/**/*"], 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["./src/to/*"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /e2e/config-ts/vite-my-plugin.ts: -------------------------------------------------------------------------------- 1 | export default function myPlugin() { 2 | return { 3 | name: "vite-my-plugin", 4 | 5 | config() { 6 | return { 7 | define: { 8 | // @ts-ignore 9 | __filename_myPlugin: JSON.stringify(__filename), 10 | // @ts-ignore 11 | __dirname_myPlugin: JSON.stringify(__dirname), 12 | }, 13 | }; 14 | }, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /e2e/config-ts/vite.config.ts: -------------------------------------------------------------------------------- 1 | import myPlugin from "./vite-my-plugin"; 2 | 3 | export default { 4 | server: { 5 | open: "none", 6 | host: "127.0.0.1", 7 | }, 8 | 9 | define: { 10 | // @ts-ignore 11 | __filename_root: JSON.stringify(__filename), 12 | // @ts-ignore 13 | __dirname_root: JSON.stringify(__dirname), 14 | }, 15 | plugins: [myPlugin()], 16 | }; 17 | -------------------------------------------------------------------------------- /e2e/config/.ladle/config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | stories: ["src/**/*.show.{js,jsx,ts,tsx}", "src/specific-file.custom.tsx"], 3 | viteConfig: "ladle-vite.config.js", 4 | appendToHead: ``, 5 | storyOrder: () => ["specific*", "Hello*"], 6 | expandStoryTree: true, 7 | }; 8 | -------------------------------------------------------------------------------- /e2e/config/.ladle/head.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /e2e/config/ladle-vite.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | server: { 3 | open: "none", 4 | host: "127.0.0.1", 5 | }, 6 | resolve: { 7 | // See #184 - this tests the package for the resolve.alias RegExp format. 8 | alias: [{ find: /any-cool-regex/, replacement: "any-string" }], 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /e2e/config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-config", 3 | "version": "0.4.3", 4 | "license": "MIT", 5 | "private": true, 6 | "scripts": { 7 | "serve": "ladle serve -p 61107", 8 | "serve-prod": "ladle preview -p 61107", 9 | "build": "ladle build", 10 | "lint": "echo 'no lint'", 11 | "test-dev": "cross-env TYPE=dev pnpm exec playwright test", 12 | "test-prod": "cross-env TYPE=prod pnpm exec playwright test", 13 | "test": "pnpm test-dev && pnpm test-prod" 14 | }, 15 | "dependencies": { 16 | "@ladle/playwright-config": "workspace:*", 17 | "@ladle/react": "workspace:*", 18 | "@playwright/test": "^1.49.1", 19 | "cross-env": "^7.0.3", 20 | "react": "^19.0.0", 21 | "react-dom": "^19.0.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /e2e/config/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import getPlaywrightConfig from "@ladle/playwright-config"; 2 | 3 | export default getPlaywrightConfig({ 4 | port: 61107, 5 | }); 6 | -------------------------------------------------------------------------------- /e2e/config/src/hello.show.tsx: -------------------------------------------------------------------------------- 1 | import type { Story } from "@ladle/react"; 2 | 3 | export const World: Story = () => { 4 | return

Hello World

; 5 | }; 6 | 7 | export const Styles: Story = () => { 8 | return ( 9 | <> 10 |

text

11 |

append

12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /e2e/config/src/specific-file.custom.tsx: -------------------------------------------------------------------------------- 1 | import type { Story } from "@ladle/react"; 2 | 3 | export const Custom: Story = () => { 4 | return

Custom path

; 5 | }; 6 | -------------------------------------------------------------------------------- /e2e/config/tests/custom-path.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test("custom path story is rendered", async ({ page }) => { 4 | await page.goto("/?story=specific-file--custom"); 5 | 6 | await expect(page.locator("h1")).toHaveText("Custom path"); 7 | }); 8 | -------------------------------------------------------------------------------- /e2e/config/tests/expand-story-tree.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test("story tree is expanded", async ({ page }) => { 4 | await page.goto("/"); 5 | 6 | await expect(page.getByRole("treeitem")).toHaveCount(5); 7 | }); 8 | -------------------------------------------------------------------------------- /e2e/config/tests/head.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test("head.html gets appended", async ({ page }) => { 4 | await page.goto("/?story=hello--styles"); 5 | await expect(page.locator(".file")).toHaveCSS("color", "rgb(255, 192, 203)"); 6 | }); 7 | 8 | test("appendToHead gets appended", async ({ page }) => { 9 | await page.goto("/?story=hello--styles"); 10 | await expect(page.locator(".append")).toHaveCSS("color", "rgb(0, 128, 0)"); 11 | }); 12 | -------------------------------------------------------------------------------- /e2e/config/tests/hello.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test("default story is rendered", async ({ page }) => { 4 | await page.goto("/?story=hello--world"); 5 | await expect(page.locator("h1")).toHaveText("Hello World"); 6 | }); 7 | 8 | test("navigation respects storyOrder from the .ladle/config.mjs", async ({ 9 | page, 10 | }) => { 11 | await page.goto("/"); 12 | await expect(page.locator("nav")).toHaveText( 13 | "Specific fileCustomHelloStylesWorld", 14 | ); 15 | }); 16 | -------------------------------------------------------------------------------- /e2e/css/.ladle/components.tsx: -------------------------------------------------------------------------------- 1 | import "./styles.css"; 2 | -------------------------------------------------------------------------------- /e2e/css/.ladle/styles.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | h1 { 6 | background: yellow; 7 | } 8 | -------------------------------------------------------------------------------- /e2e/css/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-css", 3 | "version": "0.0.94", 4 | "license": "MIT", 5 | "private": true, 6 | "scripts": { 7 | "serve": "ladle serve -p 61102", 8 | "serve-prod": "ladle preview -p 61102", 9 | "build": "ladle build", 10 | "lint": "echo 'no lint'", 11 | "test-dev": "cross-env TYPE=dev pnpm exec playwright test", 12 | "test-prod": "cross-env TYPE=prod pnpm exec playwright test", 13 | "test": "pnpm test-dev && pnpm test-prod" 14 | }, 15 | "dependencies": { 16 | "@ladle/playwright-config": "workspace:*", 17 | "@ladle/react": "workspace:*", 18 | "@playwright/test": "^1.49.1", 19 | "autoprefixer": "^10.4.20", 20 | "cross-env": "^7.0.3", 21 | "postcss": "^8.4.49", 22 | "react": "^19.0.0", 23 | "react-dom": "^19.0.0", 24 | "tailwindcss": "^3.4.17" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /e2e/css/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import getPlaywrightConfig from "@ladle/playwright-config"; 2 | 3 | export default getPlaywrightConfig({ 4 | port: 61102, 5 | }); 6 | -------------------------------------------------------------------------------- /e2e/css/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /e2e/css/src/hello.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Story } from "@ladle/react"; 2 | 3 | // @ts-ignore 4 | import classes from "./more.module.css"; 5 | 6 | export const World: Story = () => { 7 | return ( 8 | <> 9 |

Yellow

10 |

Red

11 |

Tailwind

12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /e2e/css/src/more.module.css: -------------------------------------------------------------------------------- 1 | .red { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /e2e/css/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["./src/**/*.{js,ts,jsx,tsx}"], 3 | theme: {}, 4 | variants: {}, 5 | plugins: [], 6 | }; 7 | -------------------------------------------------------------------------------- /e2e/css/tests/hello.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test("css, css modules and postcss are loaded correctly", async ({ page }) => { 4 | await page.goto("/"); 5 | await expect(page.locator("h1")).toHaveCSS( 6 | "background-color", 7 | "rgb(255, 255, 0)", 8 | ); 9 | await expect(page.locator("h2")).toHaveCSS("color", "rgb(255, 0, 0)"); 10 | await expect(page.locator("h3")).toHaveCSS("font-size", "36px"); 11 | }); 12 | -------------------------------------------------------------------------------- /e2e/css/vite.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | server: { 3 | open: "none", 4 | host: "127.0.0.1", 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /e2e/decorators/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-decorators", 3 | "version": "0.2.103", 4 | "license": "MIT", 5 | "private": true, 6 | "scripts": { 7 | "serve": "ladle serve -p 61103", 8 | "serve-prod": "ladle preview -p 61103", 9 | "build": "ladle build", 10 | "lint": "echo 'no lint'", 11 | "test-dev": "cross-env TYPE=dev pnpm exec playwright test", 12 | "test-prod": "cross-env TYPE=prod pnpm exec playwright test", 13 | "test": "pnpm test-dev && pnpm test-prod" 14 | }, 15 | "dependencies": { 16 | "@ladle/playwright-config": "workspace:*", 17 | "@ladle/react": "workspace:*", 18 | "@playwright/test": "^1.49.1", 19 | "cross-env": "^7.0.3", 20 | "react": "^19.0.0", 21 | "react-dom": "^19.0.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /e2e/decorators/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import getPlaywrightConfig from "@ladle/playwright-config"; 2 | 3 | export default getPlaywrightConfig({ 4 | port: 61103, 5 | }); 6 | -------------------------------------------------------------------------------- /e2e/decorators/src/args.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { StoryDefault, Story } from "@ladle/react"; 2 | 3 | type Props = { label: string }; 4 | 5 | export default { 6 | decorators: [ 7 | (Component, context) => { 8 | console.log("first", context); 9 | return ( 10 |
11 |

first {context.globalState.control.label.value}

12 | 13 |
14 | ); 15 | }, 16 | (Component, context) => { 17 | console.log("second", context); 18 | return ( 19 |
20 |

second {context.globalState.control.label.value}

21 | 22 |
23 | ); 24 | }, 25 | (Component, context) => { 26 | console.log("third", context); 27 | return ( 28 |
29 |

third {context.globalState.control.label.value}

30 | 31 |
32 | ); 33 | }, 34 | ], 35 | } satisfies StoryDefault; 36 | 37 | const Card: Story = ({ label }) => ( 38 | <> 39 |

Label: {label}

40 | 41 | 42 | ); 43 | 44 | export const CardHello = Card.bind({}); 45 | 46 | CardHello.decorators = [ 47 | (Component, context) => { 48 | console.log("private", context); 49 | return ( 50 |
51 |

private {context.globalState.control.label.value}

52 | 53 |
54 | ); 55 | }, 56 | ]; 57 | 58 | CardHello.args = { 59 | label: "Hello", 60 | }; 61 | -------------------------------------------------------------------------------- /e2e/decorators/src/hello.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { StoryDefault, Story } from "@ladle/react"; 2 | 3 | export default { 4 | decorators: [ 5 | (Stories: React.FC) => ( 6 | <> 7 | Decorator 1 8 | 9 | ), 10 | (Stories: React.FC) => ( 11 | <> 12 | Decorator 2 13 | 14 | ), 15 | ], 16 | } satisfies StoryDefault; 17 | 18 | export const World: Story = () => { 19 | return

world

; 20 | }; 21 | 22 | World.decorators = [ 23 | (Stories: React.FC) => ( 24 | <> 25 | Decorator 3 26 | 27 | ), 28 | ]; 29 | -------------------------------------------------------------------------------- /e2e/decorators/src/legacy-params.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { StoryDefault, Story } from "@ladle/react"; 2 | 3 | export default { 4 | args: { 5 | flag: "czech", 6 | }, 7 | argTypes: { 8 | size: { 9 | options: ["small", "medium", "big", "huuuuge"], 10 | control: { type: "select" }, // or type: multi-select 11 | }, 12 | }, 13 | parameters: { 14 | one: 1, 15 | two: 2, 16 | }, 17 | decorators: [ 18 | (Stories: React.FC, context) => { 19 | console.log(context); 20 | return ( 21 | <> 22 | Decorator {context.parameters.one} 23 | {context.args.flag} 24 | 25 | 26 | ); 27 | }, 28 | (Stories: React.FC, context) => ( 29 | <> 30 | Decorator {context.parameters.two} 31 | 32 | 33 | ), 34 | ], 35 | } satisfies StoryDefault; 36 | 37 | export const World: Story = () => { 38 | return

world

; 39 | }; 40 | 41 | World.parameters = { 42 | two: 5, 43 | }; 44 | -------------------------------------------------------------------------------- /e2e/decorators/src/mock-date.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Story } from "@ladle/react"; 2 | 3 | export const Active: Story = () => { 4 | const date = new Date(); 5 | return ( 6 |

7 | {date.toLocaleDateString("en-US", { 8 | weekday: "long", 9 | year: "numeric", 10 | month: "long", 11 | day: "numeric", 12 | })} 13 |

14 | ); 15 | }; 16 | 17 | Active.meta = { 18 | mockDate: "1995-12-17T03:24:00", 19 | }; 20 | 21 | export const Inactive: Story = () => { 22 | const date = new Date(); 23 | return ( 24 |

25 | {date.toLocaleDateString("en-US", { 26 | weekday: "long", 27 | year: "numeric", 28 | month: "long", 29 | day: "numeric", 30 | })} 31 |

32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /e2e/decorators/src/params.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { StoryDefault, Story } from "@ladle/react"; 2 | 3 | export default { 4 | title: "Root / Examples", 5 | meta: { 6 | drink: "coke", 7 | food: "burger", 8 | }, 9 | } satisfies StoryDefault; 10 | 11 | export const First: Story = () => { 12 | return

first

; 13 | }; 14 | 15 | export const Second: Story = () => { 16 | return

second

; 17 | }; 18 | Second.storyName = "Second Renamed"; 19 | Second.meta = { 20 | drink: "water", 21 | }; 22 | -------------------------------------------------------------------------------- /e2e/decorators/tests/args.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test("ladle context is correctly passed to decorators", async ({ page }) => { 4 | await page.goto("/?story=args--card-hello"); 5 | await expect(page.locator("main")).toHaveText( 6 | "third Hellosecond Hellofirst Helloprivate HelloLabel: Hello", 7 | ); 8 | }); 9 | 10 | test("ladle doesn't remount story when control (state) is changed", async ({ 11 | page, 12 | }) => { 13 | await page.goto("/?story=args--card-hello"); 14 | await expect(page.locator("main")).toHaveText( 15 | "third Hellosecond Hellofirst Helloprivate HelloLabel: Hello", 16 | ); 17 | await page.fill("#persist-input", "keep"); 18 | const button = page.locator('[data-testid="addon-control"]'); 19 | await button.click(); 20 | await page.fill("#label", "Bye"); 21 | await expect(page.locator("main")).toHaveText( 22 | "third Byesecond Byefirst Byeprivate ByeLabel: Bye", 23 | ); 24 | expect(await page.locator("#persist-input").inputValue(), "keep"); 25 | }); 26 | -------------------------------------------------------------------------------- /e2e/decorators/tests/hello.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test("decorators are applied in the correct order", async ({ page }) => { 4 | await page.goto("/?story=hello--world"); 5 | await expect(page.locator("main")).toHaveText( 6 | "Decorator 2Decorator 1Decorator 3world", 7 | ); 8 | }); 9 | -------------------------------------------------------------------------------- /e2e/decorators/tests/legacy-params.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test("legacy parameters are passed as a context into decorators", async ({ 4 | page, 5 | }) => { 6 | await page.goto("/?story=legacy-params--world"); 7 | await expect(page.locator("main")).toHaveText( 8 | "Decorator 5Decorator 1czechworld", 9 | ); 10 | }); 11 | -------------------------------------------------------------------------------- /e2e/decorators/tests/mock-date.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test("the date is mocked", async ({ page }) => { 4 | await page.goto("/?story=mock-date--active"); 5 | const dateValue = new Date("1995-12-17T03:24:00"); 6 | await expect(page.locator("h1")).toHaveText( 7 | dateValue.toLocaleDateString("en-US", { 8 | weekday: "long", 9 | year: "numeric", 10 | month: "long", 11 | day: "numeric", 12 | }), 13 | ); 14 | }); 15 | 16 | test("the date is current", async ({ page }) => { 17 | await page.goto("/?story=mock-date--inactive"); 18 | const dateValue = new Date(); 19 | await expect(page.locator("h1")).toHaveText( 20 | dateValue.toLocaleDateString("en-US", { 21 | weekday: "long", 22 | year: "numeric", 23 | month: "long", 24 | day: "numeric", 25 | }), 26 | ); 27 | }); 28 | -------------------------------------------------------------------------------- /e2e/decorators/vite.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | server: { 3 | open: "none", 4 | host: "127.0.0.1", 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /e2e/msw/.gitignore: -------------------------------------------------------------------------------- 1 | public/mockServiceWorker.js 2 | 3 | -------------------------------------------------------------------------------- /e2e/msw/.ladle/config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | addons: { 3 | msw: { 4 | enabled: true, 5 | }, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /e2e/msw/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-msw", 3 | "version": "0.0.94", 4 | "license": "MIT", 5 | "private": true, 6 | "scripts": { 7 | "serve": "ladle serve -p 61112", 8 | "serve-prod": "ladle preview -p 61112", 9 | "build": "ladle build", 10 | "lint": "echo 'no lint'", 11 | "test-dev": "cross-env TYPE=dev pnpm exec playwright test", 12 | "test-prod": "cross-env TYPE=prod pnpm exec playwright test", 13 | "test": "pnpm test-dev && pnpm test-prod" 14 | }, 15 | "dependencies": { 16 | "@ladle/playwright-config": "workspace:*", 17 | "@ladle/react": "workspace:*", 18 | "@playwright/test": "^1.49.1", 19 | "autoprefixer": "^10.4.20", 20 | "baseui": "^14.0.0", 21 | "cross-env": "^7.0.3", 22 | "postcss": "^8.4.49", 23 | "react": "^19.0.0", 24 | "react-dom": "^19.0.0", 25 | "styletron-engine-monolithic": "^1.0.0", 26 | "styletron-react": "^6.1.1", 27 | "tailwindcss": "^3.4.17" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /e2e/msw/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import getPlaywrightConfig from "@ladle/playwright-config"; 2 | 3 | export default getPlaywrightConfig({ 4 | port: 61112, 5 | }); 6 | -------------------------------------------------------------------------------- /e2e/msw/public/posts.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "userId": 1, 4 | "id": 1, 5 | "title": "json post", 6 | "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /e2e/msw/public/todos.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "userId": 1, 4 | "id": 1, 5 | "title": "json todo", 6 | "completed": false 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /e2e/msw/src/posts.stories.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import type { Story } from "@ladle/react"; 3 | import { msw } from "@ladle/react"; 4 | import { fetchData } from "./utils"; 5 | 6 | // @ts-ignore 7 | const FETCH_URL = `${import.meta.env.BASE_URL}posts.json`; 8 | 9 | export const Mocked: Story = () => { 10 | const [posts, setPosts] = useState([]); 11 | useEffect(() => { 12 | fetchData(FETCH_URL, setPosts); 13 | }, []); 14 | return ( 15 | <> 16 |

Posts

17 |
    18 | {posts.map((post: { id: number; title: string }) => ( 19 |
  • {post.title}
  • 20 | ))} 21 |
22 | 23 | ); 24 | }; 25 | 26 | export const Replaced: Story = () => { 27 | const [posts, setPosts] = useState([]); 28 | useEffect(() => { 29 | fetchData(FETCH_URL, setPosts); 30 | }, []); 31 | return ( 32 | <> 33 |

Posts

34 |
    35 | {posts.map((post: { id: number; title: string }) => ( 36 |
  • {post.title}
  • 37 | ))} 38 |
39 | 40 | ); 41 | }; 42 | 43 | export const Live: Story = () => { 44 | const [posts, setPosts] = useState([]); 45 | useEffect(() => { 46 | fetchData(FETCH_URL, setPosts); 47 | }, []); 48 | return ( 49 | <> 50 |

Posts

51 |
    52 | {posts.map((post: { id: number; title: string }) => ( 53 |
  • {post.title}
  • 54 | ))} 55 |
56 | 57 | ); 58 | }; 59 | 60 | // Set default handlers for all stories 61 | export default { 62 | msw: [ 63 | msw.http.get(FETCH_URL, () => { 64 | return msw.HttpResponse.json([{ id: 1, title: "msw post default" }]); 65 | }), 66 | ], 67 | }; 68 | 69 | // Replace handlers 70 | Replaced.msw = [ 71 | msw.http.get(FETCH_URL, () => { 72 | return msw.HttpResponse.json([{ id: 1, title: "msw post replaced" }]); 73 | }), 74 | ]; 75 | 76 | // Reset handlers 77 | Live.msw = []; 78 | -------------------------------------------------------------------------------- /e2e/msw/src/todos.stories.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import type { Story } from "@ladle/react"; 3 | import { msw } from "@ladle/react"; 4 | import { fetchData } from "./utils"; 5 | 6 | // @ts-ignore 7 | const FETCH_URL = `${import.meta.env.BASE_URL}todos.json`; 8 | 9 | export const Mocked: Story = () => { 10 | const [todos, setTodos] = useState([]); 11 | useEffect(() => { 12 | fetchData(FETCH_URL, setTodos); 13 | }, []); 14 | return ( 15 | <> 16 |

Todos

17 |
    18 | {todos.map((todo: { id: number; title: string }) => ( 19 |
  • {todo.title}
  • 20 | ))} 21 |
22 | 23 | ); 24 | }; 25 | 26 | export const Live: Story = () => { 27 | const [todos, setTodos] = useState([]); 28 | useEffect(() => { 29 | fetchData(FETCH_URL, setTodos); 30 | }, []); 31 | return ( 32 | <> 33 |

Todos

34 |
    35 | {todos.map((todo: { id: number; title: string }) => ( 36 |
  • {todo.title}
  • 37 | ))} 38 |
39 | 40 | ); 41 | }; 42 | 43 | Mocked.msw = [ 44 | msw.http.get(FETCH_URL, () => { 45 | return new Response(JSON.stringify([{ id: 1, title: "msw todo" }]), { 46 | headers: { 47 | "Content-Type": "application/json", 48 | "x-msw-bypass": "true", 49 | }, 50 | }); 51 | }), 52 | ]; 53 | -------------------------------------------------------------------------------- /e2e/msw/src/utils.ts: -------------------------------------------------------------------------------- 1 | export const fetchData = async (url: string, setData: (data: any) => void) => { 2 | try { 3 | const data = await fetch(url); 4 | const json = await data.json(); 5 | setData(json); 6 | } catch (e) { 7 | console.error(e); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /e2e/msw/tests/posts.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test("Post Live story fetches origin, removing default msw handler", async ({ 4 | page, 5 | }) => { 6 | await page.goto("/?story=posts--live"); 7 | await expect(page.locator(".ladle-main")).toHaveText("Postsjson post"); 8 | }); 9 | 10 | test("Post Mocked story fetches msw default handler", async ({ page }) => { 11 | await page.goto("/?story=posts--mocked"); 12 | await expect(page.locator(".ladle-main")).toHaveText("Postsmsw post default"); 13 | }); 14 | 15 | test("Post Mocked story fetches msw story level handler", async ({ page }) => { 16 | await page.goto("/?story=posts--replaced"); 17 | await expect(page.locator(".ladle-main")).toHaveText( 18 | "Postsmsw post replaced", 19 | ); 20 | }); 21 | -------------------------------------------------------------------------------- /e2e/msw/tests/todos.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test("Todos Live story fetches origin", async ({ page }) => { 4 | await page.goto("/?story=todos--live"); 5 | await expect(page.locator(".ladle-main")).toHaveText("Todosjson todo"); 6 | }); 7 | 8 | test("Todos mocked story fetches msw", async ({ page }) => { 9 | await page.goto("/?story=todos--mocked"); 10 | await expect(page.locator(".ladle-main")).toHaveText("Todosmsw todo"); 11 | }); 12 | -------------------------------------------------------------------------------- /e2e/msw/vite.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | server: { 3 | open: "none", 4 | host: "127.0.0.1", 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /e2e/playwright-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ladle/playwright-config", 3 | "version": "0.2.71", 4 | "license": "MIT", 5 | "private": true, 6 | "type": "module", 7 | "main": "./src/index.ts", 8 | "scripts": { 9 | "build": "echo 'no build'", 10 | "lint": "echo 'no lint'", 11 | "test": "echo 'no test'" 12 | }, 13 | "dependencies": { 14 | "@playwright/test": "^1.49.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /e2e/playwright-config/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { PlaywrightTestConfig } from "@playwright/test"; 2 | 3 | const getCommand = (type?: string): string => { 4 | switch (type) { 5 | case "dev": 6 | return "pnpm serve"; 7 | case "prod": 8 | return "pnpm serve-prod"; 9 | case "update": 10 | return "pnpm build-preview"; 11 | default: 12 | throw new Error(`Unknown type: ${type}`); 13 | } 14 | }; 15 | 16 | const getPlaywrightConfig = ({ 17 | port, 18 | }: { 19 | port: number; 20 | }): PlaywrightTestConfig => { 21 | return { 22 | // Fail the build on CI if you accidentally left test.only in the source code. 23 | forbidOnly: !!process.env.CI, 24 | // Retry on CI only. 25 | retries: process.env.CI ? 2 : 0, 26 | // Opt out of parallel tests on CI. 27 | workers: process.env.CI ? 1 : undefined, 28 | use: { 29 | baseURL: `http://127.0.0.1:${port}`, 30 | }, 31 | webServer: { 32 | command: getCommand(process.env.TYPE), 33 | url: `http://127.0.0.1:${port}`, 34 | }, 35 | }; 36 | }; 37 | 38 | export default getPlaywrightConfig; 39 | -------------------------------------------------------------------------------- /e2e/playwright/.gitignore: -------------------------------------------------------------------------------- 1 | test-results 2 | 3 | -------------------------------------------------------------------------------- /e2e/playwright/README.md: -------------------------------------------------------------------------------- 1 | # Visual Snapshots with Playwright 2 | 3 | This package demonstrates how you can quickly automate visual snapshots with Ladle and Playwright to cover all your stories. 4 | 5 | Read the [post](https://ladle.dev/blog/visual-snapshots) for more information. (The actual source code here a slightly different since it has a double purpose as an e2e test.) 6 | 7 | ## Run it 8 | 9 | Clone this repo, navigate to this folder and run: 10 | 11 | ```sh 12 | pnpm install 13 | pnpm build #build ladle 14 | pnpm test #run tests 15 | pnpm test:update #update snapshots if there are changes 16 | ``` 17 | -------------------------------------------------------------------------------- /e2e/playwright/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-playwright", 3 | "version": "0.0.65", 4 | "license": "MIT", 5 | "private": true, 6 | "scripts": { 7 | "serve": "ladle serve -p 61110", 8 | "serve-prod": "ladle preview -p 61110", 9 | "build-preview": "ladle build && ladle preview -p 61110", 10 | "build": "ladle build", 11 | "lint": "echo 'no lint'", 12 | "test-dev": "cross-env TYPE=dev pnpm exec playwright test", 13 | "test-prod": "cross-env TYPE=prod pnpm exec playwright test", 14 | "test": "pnpm test-dev && pnpm test-prod", 15 | "test:update": "cross-env TYPE=update pnpm exec playwright test -u" 16 | }, 17 | "dependencies": { 18 | "@ladle/playwright-config": "workspace:*", 19 | "@ladle/react": "workspace:*", 20 | "@playwright/test": "^1.49.1", 21 | "@types/sync-fetch": "^0.4.3", 22 | "cross-env": "^7.0.3", 23 | "react": "^19.0.0", 24 | "react-dom": "^19.0.0", 25 | "start-server-and-test": "^2.0.9", 26 | "sync-fetch": "0.6.0-2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /e2e/playwright/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import getPlaywrightConfig from "@ladle/playwright-config"; 2 | 3 | export default getPlaywrightConfig({ 4 | port: 61110, 5 | }); 6 | -------------------------------------------------------------------------------- /e2e/playwright/src/abc.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Story } from "@ladle/react"; 2 | 3 | export const First: Story = () => { 4 | // not rendering a text since fonts render differently in different operation systems 5 | // and we use this package in our Github Actions CI which runs Ubuntu and Windows 6 | // so doing a blue rectangle instead to keep the setup simple 7 | // ideally, you should use something like Docker to have a consistent setup across 8 | // the local and remote environments 9 | return
; 10 | }; 11 | 12 | export const Second: Story = () => { 13 | return

Second

; 14 | }; 15 | Second.meta = { 16 | skip: true, 17 | }; 18 | -------------------------------------------------------------------------------- /e2e/playwright/tests/snapshot.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | // we can't create tests asynchronously, thus using the sync-fetch lib 3 | import fetch from "sync-fetch"; 4 | 5 | // URL where Ladle is served 6 | const url = "http://127.0.0.1:61110"; 7 | 8 | // set different viewport 9 | // test.use({ 10 | // viewport: { width: 500, height: 400 }, 11 | // }); 12 | 13 | // run tests with browser open 14 | // test.use({ headless: false }); 15 | 16 | // fetch Ladle's meta file 17 | // https://ladle.dev/docs/meta 18 | const stories = fetch(`${url}/meta.json`).json().stories; 19 | 20 | // iterate through stories 21 | Object.keys(stories).forEach((storyKey) => { 22 | // create a test for each story 23 | test(`${storyKey} - compare snapshots`, async ({ page }) => { 24 | // skip stories that are marked as skipped 25 | test.skip(stories[storyKey].meta.skip, "meta.skip is true"); 26 | // navigate to the story 27 | await page.goto(`${url}/?story=${storyKey}&mode=preview`); 28 | // stories are code-splitted, wait for them 29 | await page.waitForSelector("[data-storyloaded]"); 30 | // take and compare a screenshot 31 | await expect(page).toHaveScreenshot(`${storyKey}.png`); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /e2e/playwright/tests/snapshot.spec.ts-snapshots/abc--first-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tajo/ladle/c04af01a01e5c7222a5cff4b5a8bdaae9f112cf6/e2e/playwright/tests/snapshot.spec.ts-snapshots/abc--first-darwin.png -------------------------------------------------------------------------------- /e2e/playwright/tests/snapshot.spec.ts-snapshots/abc--first-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tajo/ladle/c04af01a01e5c7222a5cff4b5a8bdaae9f112cf6/e2e/playwright/tests/snapshot.spec.ts-snapshots/abc--first-linux.png -------------------------------------------------------------------------------- /e2e/playwright/tests/snapshot.spec.ts-snapshots/abc--first-win32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tajo/ladle/c04af01a01e5c7222a5cff4b5a8bdaae9f112cf6/e2e/playwright/tests/snapshot.spec.ts-snapshots/abc--first-win32.png -------------------------------------------------------------------------------- /e2e/playwright/vite.config.ts: -------------------------------------------------------------------------------- 1 | // you might want to disable your browser being automatically opened 2 | // when running "ladle serve" as a part of your test workflow 3 | export default { 4 | server: { 5 | open: "none", 6 | host: "127.0.0.1", 7 | }, 8 | preview: { 9 | open: "none", 10 | host: "127.0.0.1", 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /e2e/programmatic/build.js: -------------------------------------------------------------------------------- 1 | import build from "@ladle/react/build"; 2 | 3 | build({ 4 | port: 61105, 5 | host: "127.0.0.1", 6 | storyOrder: ["hello--world", "hello--ayo"], 7 | }); 8 | -------------------------------------------------------------------------------- /e2e/programmatic/get-meta.js: -------------------------------------------------------------------------------- 1 | import getMeta from "@ladle/react/meta"; 2 | 3 | getMeta(); 4 | -------------------------------------------------------------------------------- /e2e/programmatic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-programmatic", 3 | "version": "0.2.103", 4 | "license": "MIT", 5 | "private": true, 6 | "type": "module", 7 | "scripts": { 8 | "serve": "node serve.js", 9 | "serve-prod": "node preview.js", 10 | "meta": "node get-meta.js", 11 | "build": "node build.js", 12 | "lint": "echo 'no lint'", 13 | "test-dev": "cross-env TYPE=dev pnpm exec playwright test", 14 | "test-prod": "cross-env TYPE=prod pnpm exec playwright test", 15 | "test": "pnpm test-dev && pnpm test-prod" 16 | }, 17 | "dependencies": { 18 | "@ladle/playwright-config": "workspace:*", 19 | "@ladle/react": "workspace:*", 20 | "@playwright/test": "^1.49.1", 21 | "cross-env": "^7.0.3", 22 | "react": "^19.0.0", 23 | "react-dom": "^19.0.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /e2e/programmatic/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import getPlaywrightConfig from "@ladle/playwright-config"; 2 | 3 | export default getPlaywrightConfig({ 4 | port: 61105, 5 | }); 6 | -------------------------------------------------------------------------------- /e2e/programmatic/preview.js: -------------------------------------------------------------------------------- 1 | import preview from "@ladle/react/preview"; 2 | 3 | preview({ 4 | previewPort: 61105, 5 | }); 6 | -------------------------------------------------------------------------------- /e2e/programmatic/serve.js: -------------------------------------------------------------------------------- 1 | import serve from "@ladle/react/serve"; 2 | 3 | serve({ 4 | port: 61105, 5 | host: "127.0.0.1", 6 | storyOrder: ["hello--world", "hello--ayo"], 7 | }); 8 | -------------------------------------------------------------------------------- /e2e/programmatic/src/hello.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Story } from "@ladle/react"; 2 | 3 | export const World: Story = () => { 4 | return

Hello World

; 5 | }; 6 | 7 | export const Ayo: Story = () => { 8 | return

Ayo

; 9 | }; 10 | -------------------------------------------------------------------------------- /e2e/programmatic/vite.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | server: { 3 | open: "none", 4 | }, 5 | preview: { 6 | host: "127.0.0.1", 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /e2e/provider/.gitignore: -------------------------------------------------------------------------------- 1 | src/add.stories.tsx 2 | 3 | -------------------------------------------------------------------------------- /e2e/provider/.ladle/components.tsx: -------------------------------------------------------------------------------- 1 | import type { GlobalProvider, SourceHeader } from "@ladle/react"; 2 | import { createContext } from "react"; 3 | 4 | export const MyContext = createContext("my-context"); 5 | 6 | export const Provider: GlobalProvider = ({ children, storyMeta }) => ( 7 | 8 | {storyMeta?.myMeta &&

{storyMeta.myMeta}

} 9 | {children} 10 |

rendered by provider

11 |
12 | ); 13 | 14 | //@ts-ignore 15 | const config: string = LADLE_PROJECT_PATH; 16 | 17 | export const StorySourceHeader: SourceHeader = ({ path, locStart, locEnd }) => { 18 | return ( 19 | 20 | {config} 21 | {path} 22 | {locStart}-{locEnd} 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /e2e/provider/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-provider", 3 | "version": "0.2.103", 4 | "license": "MIT", 5 | "private": true, 6 | "scripts": { 7 | "serve": "ladle serve -p 61106", 8 | "serve-prod": "ladle preview -p 61106", 9 | "build": "ladle build", 10 | "lint": "echo 'no lint'", 11 | "test-dev": "cross-env TYPE=dev pnpm exec playwright test", 12 | "test-prod": "cross-env TYPE=prod pnpm exec playwright test", 13 | "test": "pnpm test-dev && pnpm test-prod" 14 | }, 15 | "dependencies": { 16 | "@ladle/playwright-config": "workspace:*", 17 | "@ladle/react": "workspace:*", 18 | "@playwright/test": "^1.49.1", 19 | "cross-env": "^7.0.3", 20 | "react": "^19.0.0", 21 | "react-dom": "^19.0.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /e2e/provider/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import getPlaywrightConfig from "@ladle/playwright-config"; 2 | 3 | export default getPlaywrightConfig({ 4 | port: 61106, 5 | }); 6 | -------------------------------------------------------------------------------- /e2e/provider/src/hello.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Story } from "@ladle/react"; 2 | import { useContext } from "react"; 3 | import { MyContext } from "../.ladle/components"; 4 | 5 | export const World: Story = () => { 6 | const value = useContext(MyContext); 7 | return

Hello World - {value}

; 8 | }; 9 | -------------------------------------------------------------------------------- /e2e/provider/src/hmr.stories.tsx: -------------------------------------------------------------------------------- 1 | export const WithState = () => { 2 | return ( 3 | <> 4 | 5 | 6 | 7 | ); 8 | }; 9 | -------------------------------------------------------------------------------- /e2e/provider/src/meta.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Story } from "@ladle/react"; 2 | 3 | export const StoryMeta: Story = () => { 4 | return

Hello World, I have a meta text

; 5 | }; 6 | 7 | StoryMeta.meta = { 8 | myMeta: "This is my meta", 9 | }; 10 | -------------------------------------------------------------------------------- /e2e/provider/tests/hello.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | test("Provider passes context and renders wrapper", async ({ page }) => { 4 | await page.goto("/?story=hello--world"); 5 | await expect(page.locator("h1")).toHaveText("Hello World - some-context"); 6 | await expect(page.locator("p")).toHaveText("rendered by provider"); 7 | }); 8 | 9 | test("StorySourceHeader sets a custom source header", async ({ page }) => { 10 | await page.goto("/?story=hello--world"); 11 | const button = page.locator('[data-testid="addon-source"]'); 12 | await button.click(); 13 | await expect(page.locator("#source-header")).toContainText( 14 | "project/aaa/src/hello.stories.tsx5-8", 15 | ); 16 | }); 17 | 18 | test("meta.json has 3 stories", async ({ request }) => { 19 | const meta = await request.get("http://127.0.0.1:61106/meta.json"); 20 | expect(Object.keys((await meta.json()).stories).length).toEqual(3); 21 | }); 22 | -------------------------------------------------------------------------------- /e2e/provider/tests/hmr.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | import fs from "fs"; 3 | 4 | const before = `export const WithState = () => { 5 | return ( 6 | <> 7 | 8 | 9 | 10 | ); 11 | }; 12 | `; 13 | 14 | const after = `export const WithState = () => { 15 | return ( 16 | <> 17 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | `; 24 | 25 | test.beforeEach(async () => { 26 | fs.writeFileSync("./src/hmr.stories.tsx", before); 27 | }); 28 | 29 | test.afterEach(async () => { 30 | fs.writeFileSync("./src/hmr.stories.tsx", before); 31 | }); 32 | 33 | if (process.env.TYPE === "dev") { 34 | test("hmr with fast refresh works", async ({ page }) => { 35 | await page.goto("/?story=hmr--with-state"); 36 | page.locator("#state-input").fill("some state"); 37 | fs.writeFileSync("./src/hmr.stories.tsx", after); 38 | await page.waitForSelector("#new-button", { timeout: 5000 }); 39 | await expect(page.locator("#state-input")).toHaveValue("some state"); 40 | await expect(page.locator("#new-button")).toHaveText("New"); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /e2e/provider/tests/meta.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | 3 | test("Story meta is available in provider", async ({ page }) => { 4 | await page.goto("/?story=meta--story-meta"); 5 | await expect(page.locator("#myMeta")).toHaveText("This is my meta"); 6 | }); 7 | -------------------------------------------------------------------------------- /e2e/provider/vite.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | server: { 3 | open: "none", 4 | host: "127.0.0.1", 5 | }, 6 | define: { 7 | LADLE_PROJECT_PATH: JSON.stringify("project/aaa/"), 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import react from "eslint-plugin-react/configs/recommended.js"; 3 | import prettier from "eslint-plugin-prettier/recommended"; 4 | import eslint from "@eslint/js"; 5 | import tseslint from "typescript-eslint"; 6 | 7 | export default tseslint.config( 8 | eslint.configs.recommended, 9 | ...tseslint.configs.recommended, 10 | prettier, 11 | react, 12 | { 13 | languageOptions: { 14 | globals: { 15 | ...globals.browser, 16 | ...globals.node, 17 | } 18 | }, 19 | files: ["**/*.ts", "**/*.tsx", "**/*.js"], 20 | rules: { 21 | "@typescript-eslint/no-explicit-any": "off", 22 | "@typescript-eslint/ban-types": "off", 23 | "@typescript-eslint/ban-ts-comment": "off", 24 | "@typescript-eslint/explicit-module-boundary-types": "off", 25 | "@typescript-eslint/no-empty-object-type": "off", 26 | "@typescript-eslint/no-unused-expressions": "off", 27 | "@typescript-eslint/no-unused-vars": "off", 28 | "react/prop-types": "off", 29 | "react/react-in-jsx-scope": "off", 30 | "no-useless-escape": "off", 31 | "no-empty": "off", 32 | "no-global-assign": "off", 33 | }, 34 | settings: { 35 | react: { 36 | version: "detect", 37 | }, 38 | }, 39 | }, 40 | ); 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ladle", 3 | "version": "0.0.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:tajo/ladle.git", 6 | "author": "Vojtech Miksu ", 7 | "license": "MIT", 8 | "private": true, 9 | "packageManager": "pnpm@9.15.1", 10 | "scripts": { 11 | "lint": "eslint \"./{packages,e2e}/*/{lib,src,tests}/**/*.{ts,tsx}\" --max-warnings=0", 12 | "test": "turbo run test --concurrency=1", 13 | "build": "turbo run build", 14 | "typecheck": "tsc", 15 | "prepare": "husky", 16 | "release": "./release.sh" 17 | }, 18 | "workspaces": [ 19 | "packages/example", 20 | "packages/ladle", 21 | "packages/website", 22 | "e2e/addons", 23 | "e2e/babel", 24 | "e2e/commonjs", 25 | "e2e/config", 26 | "e2e/config-ts", 27 | "e2e/css", 28 | "e2e/decorators", 29 | "e2e/playwright", 30 | "e2e/playwright-config", 31 | "e2e/programmatic", 32 | "e2e/provider", 33 | "e2e/baseweb", 34 | "e2e/msw" 35 | ], 36 | "devDependencies": { 37 | "@changesets/changelog-github": "^0.5.0", 38 | "@changesets/cli": "^2.27.11", 39 | "@commitlint/cli": "^19.6.1", 40 | "@commitlint/config-conventional": "^19.6.0", 41 | "@eslint/js": "^9.17.0", 42 | "@playwright/test": "^1.49.1", 43 | "@types/eslint__js": "^8.42.3", 44 | "@types/react": "^19.0.2", 45 | "@types/react-dom": "^19.0.2", 46 | "eslint": "^9.17.0", 47 | "eslint-plugin-prettier": "^5.2.1", 48 | "eslint-plugin-react": "^7.37.2", 49 | "globals": "^15.14.0", 50 | "husky": "^9.1.7", 51 | "lint-staged": "^15.2.11", 52 | "prettier": "^3.4.2", 53 | "turbo": "^2.3.3", 54 | "typescript": "^5.7.2", 55 | "typescript-eslint": "^8.18.1" 56 | }, 57 | "engines": { 58 | "node": ">=22.0.0" 59 | }, 60 | "pnpm": { 61 | "peerDependencyRules": { 62 | "allowedVersions": { 63 | "@types/react": "19", 64 | "react": "19", 65 | "react-dom": "19", 66 | "typescript": "5" 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/example/.ladle/config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is optional. 3 | * 4 | * To setup config for specific stories to be included 5 | * 6 | * stories: [ 7 | * "src/a11y.stories.tsx", 8 | * "src/control.stories.tsx", 9 | * "src/controls.stories.tsx", 10 | * ], 11 | * 12 | * */ 13 | 14 | export default { 15 | appendToHead: ``, 16 | addons: { 17 | a11y: { 18 | enabled: true, 19 | }, 20 | }, 21 | expandStoryTree: true, 22 | }; 23 | -------------------------------------------------------------------------------- /packages/example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.3.79", 4 | "license": "MIT", 5 | "private": true, 6 | "scripts": { 7 | "serve": "ladle serve", 8 | "build": "ladle build", 9 | "lint": "echo 'no lint'", 10 | "test": "echo 'no test'" 11 | }, 12 | "dependencies": { 13 | "@ladle/react": "workspace:*", 14 | "react": "^19.0.0", 15 | "react-dom": "^19.0.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/example/src/README.md: -------------------------------------------------------------------------------- 1 | # test-page 2 | 3 | React component 4 | 5 | Screenshot 6 | 7 | --- 8 | 9 | ## Install 10 | 11 | ``` 12 | $ jz add @test/react-test 13 | ``` 14 | 15 | ## Usage 16 | 17 | ### Test 18 | 19 | Stateful version of the component. It does requests to the uOwn backend and controls state of the tree. 20 | 21 | ``` 22 | yarn add-service 23 | ``` 24 | 25 | - Now you are ready to add react component 26 | 27 | ```js 28 | import { Test } from "test/react"; 29 | ``` 30 | 31 | ```jsx 32 | console.log(`Node selected ${name}`)} 34 | maxWidth="100%" 35 | /> 36 | ``` 37 | 38 | #### Props 39 | 40 | - `uuid?`: string 41 | - `onNodeChange?`: callback function, invokes when changing active node, passes object: `{uuid, name}` 42 | 43 | ## Developing 44 | 45 | Clone this project: 46 | 47 | ``` 48 | $ git clone gitolite@code.test 49 | ``` 50 | 51 | Go to the component folder: `src/test` 52 | 53 | Install the dependencies via `yarn`: 54 | 55 | ``` 56 | $ yarn install 57 | ``` 58 | 59 | ## Ownership 60 | 61 | This template is authored by Santa Claus. 62 | -------------------------------------------------------------------------------- /packages/example/src/a11y.stories.tsx: -------------------------------------------------------------------------------- 1 | import { useLadleContext, ActionType, ThemeState, action } from "@ladle/react"; 2 | import type { Story } from "@ladle/react"; 3 | 4 | const empty = ""; 5 | export const Responsive: Story = () => { 6 | return ( 7 | <> 8 |
19 | Header 20 |
21 | 35 | 36 | ); 37 | }; 38 | Responsive.meta = { 39 | width: "xsmall", 40 | }; 41 | 42 | export const Issues: Story = () => { 43 | const { globalState, dispatch } = useLadleContext(); 44 | return ( 45 | <> 46 | 47 | 53 | 66 |

Theme: {globalState.theme}

67 | 68 | 69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /packages/example/src/control.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Story } from "@ladle/react"; 2 | 3 | const Card: Story<{ 4 | label: string; 5 | }> = ({ label }) =>

Label: {label}

; 6 | 7 | export const CardHello = Card.bind({}); 8 | 9 | CardHello.args = { 10 | label: "Hello", 11 | }; 12 | 13 | export const CardWorld = Card.bind({}); 14 | CardWorld.args = { 15 | label: "World", 16 | }; 17 | -------------------------------------------------------------------------------- /packages/example/src/controls.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Story } from "@ladle/react"; 2 | import { action, linkTo } from "@ladle/react"; 3 | 4 | export const Controls: Story<{ 5 | label: string; 6 | disabled: boolean; 7 | onClick: () => void; 8 | count: number; 9 | colors: string[]; 10 | variant: string; 11 | size: string; 12 | range: number; 13 | }> = ({ count, disabled, label, colors, variant, size, range, onClick }) => { 14 | if (count > 2) { 15 | return

done

; 16 | } 17 | return ( 18 | <> 19 |

Count: {count}

20 |

Disabled: {disabled ? "yes" : "no"}

21 |

Label: {label}

22 |

Colors: {colors.join(",")}

23 |

Variant: {variant}

24 |

Size: {size}

25 |

Range: {range}

26 | 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | Controls.args = { 34 | label: "Hello world", 35 | disabled: false, 36 | count: 2, 37 | colors: ["Red", "Blue"], 38 | }; 39 | Controls.argTypes = { 40 | disabled: { 41 | control: { type: "boolean" }, 42 | defaultValue: true, 43 | }, 44 | variant: { 45 | options: ["primary", "secondary"], 46 | control: { type: "radio" }, 47 | defaultValue: "primary", 48 | }, 49 | size: { 50 | options: ["small", "medium", "big", "huuuuge"], 51 | control: { type: "select" }, 52 | }, 53 | range: { 54 | control: { type: "range", min: 10, step: 0.5 }, 55 | defaultValue: 10, 56 | }, 57 | onClick: { 58 | action: "clicked", 59 | }, 60 | }; 61 | -------------------------------------------------------------------------------- /packages/example/src/docs.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Description, Story } from "@ladle/react"; 2 | import Readme from "./README.md"; 3 | 4 | 5 | 6 | ## This is something? masdadasdsdasd 7 | 8 | > Suspendisse at tempor velit. **Fusce** a fermentum arcu, vitae semper mi. Nunc placerat, mauris ac volutpat tempus, arcu eros accumsan nisi, in congue risus turpis in ligula. Maecenas eu urna ac nulla tempus malesuada non nec nisi. Etiam viverra, urna sit amet iaculis maximus, massa quam efficitur tellus, ac ullamcorper libero leo ac sem. Etiam et ligula nulla. Etiam vestibulum nec elit ut rhoncus. Aliquam velit velit, varius quis ex et, elementum aliquet felis. 9 | > 10 | > > Ok 11 | 12 | --- 13 | 14 | Some [example](https://example.com) link. 15 | 16 | 1. One 17 | 2. Two 18 | 3. Three 19 | 20 | ## okkkkasdasd 21 | 22 | 23 | -------------------------------------------------------------------------------- /packages/example/src/iframe-two.stories.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | 3 | export default { 4 | meta: { 5 | iframed: true, 6 | }, 7 | }; 8 | 9 | export const AutoFocusInputAgain = () => { 10 | const inputRef = useRef(null); 11 | 12 | useEffect(() => { 13 | if (inputRef.current) { 14 | // @ts-ignore 15 | inputRef.current.focus(); 16 | } 17 | }, []); 18 | 19 | return ; 20 | }; 21 | -------------------------------------------------------------------------------- /packages/example/src/iframe.stories.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | 3 | export default { 4 | meta: { 5 | iframed: true, 6 | }, 7 | }; 8 | 9 | export const Page = () =>
Page
; 10 | export const PageB = () =>
Page B
; 11 | export const PageC = () =>
Page C
; 12 | export const AutoFocusInput = () => { 13 | const inputRef = useRef(null); 14 | 15 | useEffect(() => { 16 | if (inputRef.current) { 17 | // @ts-ignore 18 | inputRef.current.focus(); 19 | } 20 | }, []); 21 | 22 | return ; 23 | }; 24 | -------------------------------------------------------------------------------- /packages/example/src/mdx.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Story, Meta } from "@ladle/react"; 2 | 3 | # MDX Button 4 | 5 | With `MDX`, you can define button w/e.dd 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/example/src/my-blah.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { StoryDefault, Story } from "@ladle/react"; 2 | import { useState } from "react"; 3 | 4 | export default { 5 | title: "Demo", 6 | meta: { 7 | baseweb: "test", 8 | browsers: ["chrome"], 9 | }, 10 | decorators: [ 11 | (Story: React.FC) => ( 12 |
13 | 14 |
15 | ), 16 | ], 17 | } satisfies StoryDefault; 18 | 19 | export const Middle: Story = () => { 20 | const [val, setVal] = useState(true); 21 | return ( 22 |
23 |

Newish haha

24 | 27 |
28 | ); 29 | }; 30 | Middle.storyName = "Middle Man"; 31 | 32 | export const Opo2: Story = () => { 33 | const [val, setVal] = useState(true); 34 | return ( 35 |
36 |

coze opo Middle Muhaha tadaokok opsops

37 | 38 | 41 |
42 | ); 43 | }; 44 | 45 | export const Dayum: Story = () => { 46 | const [val, setVal] = useState(true); 47 | return ( 48 |
49 |

opo Middle Muhaha tada

50 | 51 | 54 |
55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /packages/example/src/my-story--sec.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Story } from "@ladle/react"; 2 | import { useState } from "react"; 3 | 4 | export const Lol: Story = () => { 5 | const [val, setVal] = useState(true); 6 | return ( 7 |
8 |

LOL fam

9 | 12 |
13 | ); 14 | }; 15 | 16 | export const Ok: Story = () => { 17 | const [val, setVal] = useState(true); 18 | return ( 19 |
20 |

Yellowasd

21 | 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /packages/example/src/my-story--sub.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Story } from "@ladle/react"; 2 | import { useState } from "react"; 3 | 4 | export const Middle: Story = () => { 5 | const [val, setVal] = useState(true); 6 | return ( 7 |
8 |

Middle fam

9 | 12 |
13 | ); 14 | }; 15 | 16 | export const Yellow: Story = () => { 17 | const [val, setVal] = useState(true); 18 | return ( 19 |
20 |

Yellowasd

21 | 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /packages/example/src/my-story.stories.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import type { Story } from "@ladle/react"; 3 | 4 | export const Big: Story = () => { 5 | const [val, setVal] = useState(true); 6 | return ( 7 |
8 |

Middle fam

9 | 12 |
13 | ); 14 | }; 15 | 16 | export const Ski: Story = () => { 17 | const [val, setVal] = useState(true); 18 | return ( 19 |
20 |

Yellowasd

21 | 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /packages/example/src/not-related.ts: -------------------------------------------------------------------------------- 1 | export default () => { 2 | console.log("not used"); 3 | }; 4 | -------------------------------------------------------------------------------- /packages/example/src/ok-so.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { StoryDefault, Story } from "@ladle/react"; 2 | import identity from "./syntax"; 3 | 4 | export default { 5 | title: "Test", 6 | } satisfies StoryDefault; 7 | 8 | export const Yeah: Story = () => { 9 | return ( 10 |
11 |

This story does not need React import {identity("hey")}

12 |
13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/example/src/query-parameters.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Story } from "@ladle/react"; 2 | import { useEffect, useState } from "react"; 3 | 4 | export const QueryParameters: Story = () => { 5 | const [queryParams, setQueryParams] = useState(""); 6 | 7 | useEffect(() => { 8 | setQueryParams(location.search); 9 | }, [location.search]); 10 | 11 | return

Params: {queryParams}

; 12 | }; 13 | 14 | QueryParameters.decorators = [ 15 | (Component) => { 16 | useEffect(() => { 17 | const url = new URL(window.location.href); 18 | url.searchParams.set("foo", "bar"); 19 | history.pushState({}, "", url); 20 | }, []); 21 | 22 | return ( 23 | <> 24 | 25 | 26 | ); 27 | }, 28 | ]; 29 | -------------------------------------------------------------------------------- /packages/example/src/storyname.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Story } from "@ladle/react"; 2 | 3 | export const Cat: Story = () => { 4 | const Stop = { storyName: "" }; 5 | // should be ignored 6 | Stop.storyName = "What"; 7 | return

Cat

; 8 | }; 9 | 10 | Cat.storyName = "Doggo"; 11 | // @ts-expect-error 12 | Cat.foo = "Ha"; 13 | 14 | export const CapitalCity: Story = () => { 15 | return

DC

; 16 | }; 17 | 18 | export const CapitalReplaced: Story = () => { 19 | return

CapitalReplaced

; 20 | }; 21 | CapitalReplaced.storyName = "Champs Élysées"; 22 | -------------------------------------------------------------------------------- /packages/example/src/syntax.ts: -------------------------------------------------------------------------------- 1 | function identity(arg: Type): Type { 2 | return arg; 3 | } 4 | 5 | export default identity; 6 | -------------------------------------------------------------------------------- /packages/ladle/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true -------------------------------------------------------------------------------- /packages/ladle/README.md: -------------------------------------------------------------------------------- 1 | # Ladle 2 | 3 | Ladle is an environment to develop, test, and share your React components faster. 4 | 5 | - [Documentation](https://www.ladle.dev) 6 | - [Demo](https://react-movable.pages.dev) 7 | - [StackBlitz](https://ladle.dev/new) 8 | - [Discord](https://discord.gg/H6FSHjyW7e) 9 | 10 | ![Ladle BaseWeb](https://raw.githubusercontent.com/tajo/ladle/main/packages/website/static/img/ladle-baseweb.png) 11 | 12 | ## Quick start 13 | 14 | ```bash 15 | mkdir my-ladle 16 | cd my-ladle 17 | pnpm init 18 | pnpm add @ladle/react react react-dom 19 | mkdir src 20 | echo "export const World = () =>

Hey

;" > src/hello.stories.tsx 21 | pnpm ladle serve 22 | ``` 23 | 24 | with yarn 25 | 26 | ```bash 27 | mkdir my-ladle 28 | cd my-ladle 29 | yarn init --yes 30 | yarn add @ladle/react react react-dom 31 | mkdir src 32 | echo "export const World = () =>

Hey

;" > src/hello.stories.tsx 33 | yarn ladle serve 34 | ``` 35 | 36 | with npm 37 | 38 | ```bash 39 | mkdir my-ladle 40 | cd my-ladle 41 | npm init --yes 42 | npm install @ladle/react react react-dom 43 | mkdir src 44 | echo "export const World = () =>

Hey

;" > src/hello.stories.tsx 45 | npx ladle serve 46 | ``` 47 | -------------------------------------------------------------------------------- /packages/ladle/api/build.js: -------------------------------------------------------------------------------- 1 | import build from "../lib/cli/build.js"; 2 | 3 | export default build; 4 | -------------------------------------------------------------------------------- /packages/ladle/api/meta.js: -------------------------------------------------------------------------------- 1 | import getMeta from "../lib/cli/get-meta.js"; 2 | 3 | export default getMeta; 4 | -------------------------------------------------------------------------------- /packages/ladle/api/msw-node.js: -------------------------------------------------------------------------------- 1 | import { setupServer } from "msw/node"; 2 | 3 | export { setupServer }; 4 | -------------------------------------------------------------------------------- /packages/ladle/api/preview.js: -------------------------------------------------------------------------------- 1 | import preview from "../lib/cli/preview.js"; 2 | 3 | export default preview; 4 | -------------------------------------------------------------------------------- /packages/ladle/api/serve.js: -------------------------------------------------------------------------------- 1 | import serve from "../lib/cli/serve.js"; 2 | 3 | export default serve; 4 | -------------------------------------------------------------------------------- /packages/ladle/lib/app/favicon.svg: -------------------------------------------------------------------------------- 1 | 9 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /packages/ladle/lib/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 20 | 21 | Ladle 22 | 23 | 24 | 25 |
26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /packages/ladle/lib/app/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ladle", 3 | "short_name": "Ladle", 4 | "background_color": "#ffffff", 5 | "theme_color": "#ffffff", 6 | "display": "fullscreen" 7 | } 8 | -------------------------------------------------------------------------------- /packages/ladle/lib/app/mask-icon.svg: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/ladle/lib/app/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | -------------------------------------------------------------------------------- /packages/ladle/lib/app/src/addons/mode.tsx: -------------------------------------------------------------------------------- 1 | import queryString from "query-string"; 2 | import { Preview } from "../icons"; 3 | import { ModeState, AddonProps, ActionType } from "../../../shared/types"; 4 | import config from "../get-config"; 5 | 6 | export const getQuery = (locationSearch: string) => { 7 | const mode = queryString.parse(locationSearch).mode as string; 8 | switch (mode) { 9 | case ModeState.Full: 10 | return ModeState.Full; 11 | case ModeState.Preview: 12 | return ModeState.Preview; 13 | default: 14 | return config.addons.mode.defaultState; 15 | } 16 | }; 17 | 18 | export const Button = ({ dispatch }: AddonProps) => { 19 | const text = `Open fullscreen mode. Can be toggled by pressing ${config.hotkeys.fullscreen.join( 20 | " or ", 21 | )}.`; 22 | return ( 23 |
  • 24 | 36 |
  • 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /packages/ladle/lib/app/src/addons/rtl.tsx: -------------------------------------------------------------------------------- 1 | import queryString from "query-string"; 2 | import { useHotkeys } from "react-hotkeys-hook"; 3 | import { Rtl } from "../icons"; 4 | import { AddonProps, ActionType } from "../../../shared/types"; 5 | import config from "../get-config"; 6 | 7 | export const getQuery = (locationSearch: string) => { 8 | const urlVal = queryString.parse(locationSearch).rtl; 9 | if (urlVal === "true") return true; 10 | if (urlVal === "false") return false; 11 | return config.addons.rtl.defaultState; 12 | }; 13 | 14 | export const Button = ({ dispatch, globalState }: AddonProps) => { 15 | const rtlText = "Switch text direction to right to left."; 16 | const ltrText = "Switch text direction to left to right."; 17 | useHotkeys( 18 | config.hotkeys.rtl, 19 | () => dispatch({ type: ActionType.UpdateRtl, value: !globalState.rtl }), 20 | { 21 | enabled: globalState.hotkeys && config.addons.rtl.enabled, 22 | }, 23 | ); 24 | return ( 25 |
  • 26 | 41 |
  • 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /packages/ladle/lib/app/src/context.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | //@ts-ignore 3 | import LadleContext from "@ladle/react-context"; 4 | import type { GlobalAction, GlobalState } from "../../shared/types"; 5 | 6 | export const Context: React.Context<{ 7 | globalState: GlobalState; 8 | dispatch: React.Dispatch; 9 | }> = LadleContext; 10 | 11 | export const useLadleContext = () => 12 | React.useContext<{ 13 | globalState: GlobalState; 14 | dispatch: React.Dispatch; 15 | }>(LadleContext); 16 | -------------------------------------------------------------------------------- /packages/ladle/lib/app/src/debug.ts: -------------------------------------------------------------------------------- 1 | import debug from "debug"; 2 | 3 | // @ts-ignore 4 | export default debug("ladle"); 5 | -------------------------------------------------------------------------------- /packages/ladle/lib/app/src/error-boundary.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export default class ErrorBoundary extends React.Component< 4 | { children: React.ReactElement }, 5 | { hasError: boolean } 6 | > { 7 | constructor(props: any) { 8 | super(props); 9 | this.state = { hasError: false }; 10 | } 11 | 12 | static getDerivedStateFromError() { 13 | return { hasError: true }; 14 | } 15 | 16 | componentDidCatch() {} 17 | 18 | render() { 19 | if (this.state.hasError) { 20 | return null; 21 | } 22 | return this.props.children; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/ladle/lib/app/src/get-config.ts: -------------------------------------------------------------------------------- 1 | import merge from "lodash.merge"; 2 | import { config, stories } from "virtual:generated-list"; 3 | import defaultConfig from "../../shared/default-config"; 4 | import type { Config } from "../../shared/types"; 5 | import debug from "./debug"; 6 | import { sortStories } from "./story-name"; 7 | 8 | if (Object.keys(config).length === 0) { 9 | debug("No custom config found."); 10 | } else { 11 | if (config.storyOrder && typeof config.storyOrder === "string") { 12 | config.storyOrder = new Function("return " + config.storyOrder)(); 13 | } 14 | debug(`Custom config found:`); 15 | debug(config); 16 | } 17 | 18 | // don't merge default width options 19 | if (config?.addons?.width?.options) { 20 | defaultConfig.addons.width.options = {}; 21 | } 22 | const mergedConfig: Config = merge(defaultConfig, config); 23 | if (mergedConfig.defaultStory === "") { 24 | mergedConfig.defaultStory = sortStories( 25 | Object.keys(stories), 26 | mergedConfig.storyOrder, 27 | )[0]; 28 | } 29 | 30 | // don't merge hotkeys 31 | mergedConfig.hotkeys = { 32 | ...mergedConfig.hotkeys, 33 | ...config.hotkeys, 34 | }; 35 | 36 | debug("Final config", mergedConfig); 37 | 38 | export default mergedConfig; 39 | -------------------------------------------------------------------------------- /packages/ladle/lib/app/src/iframe/content.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, ReactElement } from "react"; 2 | 3 | interface ContentProps { 4 | children: ReactElement; 5 | contentDidMount?: () => void; 6 | contentDidUpdate?: () => void; 7 | } 8 | 9 | export default class Content extends Component { 10 | componentDidMount() { 11 | this.props.contentDidMount && this.props.contentDidMount(); 12 | } 13 | 14 | componentDidUpdate() { 15 | this.props.contentDidUpdate && this.props.contentDidUpdate(); 16 | } 17 | 18 | render() { 19 | return React.Children.only(this.props.children); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/ladle/lib/app/src/iframe/context.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, Context } from "react"; 2 | 3 | let doc: Document | undefined; 4 | let win: Window | undefined; 5 | 6 | if (typeof document !== "undefined") { 7 | doc = document; 8 | } 9 | if (typeof window !== "undefined") { 10 | win = window; 11 | } 12 | 13 | interface FrameContextType { 14 | document: Document | undefined; 15 | window: Window | undefined; 16 | } 17 | 18 | export const FrameContext: Context = 19 | React.createContext({ 20 | document: doc, 21 | window: win, 22 | }); 23 | 24 | export const useFrame = (): FrameContextType => useContext(FrameContext); 25 | 26 | export const { 27 | Provider: FrameContextProvider, 28 | Consumer: FrameContextConsumer, 29 | } = FrameContext; 30 | -------------------------------------------------------------------------------- /packages/ladle/lib/app/src/iframe/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Frame } from "./frame"; 2 | export { FrameContext, FrameContextConsumer, useFrame } from "./context"; 3 | -------------------------------------------------------------------------------- /packages/ladle/lib/app/src/index.tsx: -------------------------------------------------------------------------------- 1 | //@ts-ignore 2 | import * as React from "react"; 3 | import * as ReactDOMClient from "react-dom/client"; 4 | import App from "./app"; 5 | 6 | const container = document.getElementById("ladle-root") as HTMLElement; 7 | 8 | const root = ReactDOMClient.createRoot(container); 9 | root.render(); 10 | -------------------------------------------------------------------------------- /packages/ladle/lib/app/src/init-side-effects.ts: -------------------------------------------------------------------------------- 1 | // a separate non-react script, to ensure this is executed asap 2 | import debug from "./debug"; 3 | import { storyIdToTitle, getQueryStory } from "./story-name"; 4 | import config from "./get-config"; 5 | import { getQuery as getQueryTheme } from "./addons/theme"; 6 | import { ThemeState } from "../../shared/types"; 7 | 8 | const title = storyIdToTitle( 9 | getQueryStory(location.search, config.defaultStory), 10 | ); 11 | debug(`Initial document.title: ${title}`); 12 | document.title = `${title} | Ladle`; 13 | 14 | const theme = getQueryTheme(location.search); 15 | debug(`Initial theme state: ${theme}`); 16 | 17 | if (theme === ThemeState.Auto) { 18 | if (window.matchMedia("(prefers-color-scheme: dark)").matches) { 19 | document.documentElement.setAttribute("data-theme", ThemeState.Dark); 20 | } else { 21 | document.documentElement.setAttribute("data-theme", ThemeState.Light); 22 | } 23 | } else { 24 | document.documentElement.setAttribute("data-theme", theme); 25 | } 26 | -------------------------------------------------------------------------------- /packages/ladle/lib/app/src/local-storage.tsx: -------------------------------------------------------------------------------- 1 | export type Settings = { 2 | appId?: string; 3 | sidebarWidth?: number; 4 | }; 5 | 6 | // @ts-ignore 7 | const APP_ID = import.meta.env.VITE_LADLE_APP_ID; 8 | const storageKey = `ladle-settings-${APP_ID}`; 9 | const defaultValue = { appId: APP_ID }; 10 | 11 | export const updateSettings = (settings: Settings) => { 12 | const storageValue = localStorage.getItem(storageKey); 13 | let storageSettings = defaultValue; 14 | try { 15 | if (storageValue) storageSettings = JSON.parse(storageValue); 16 | } catch (e) {} 17 | localStorage.setItem( 18 | storageKey, 19 | JSON.stringify({ ...storageSettings, ...settings }), 20 | ); 21 | }; 22 | 23 | export const getSettings = (): Settings => { 24 | const storageValue = localStorage.getItem(storageKey); 25 | let storageSettings = defaultValue; 26 | try { 27 | if (storageValue) storageSettings = JSON.parse(storageValue); 28 | } catch (e) {} 29 | return storageSettings as Settings; 30 | }; 31 | -------------------------------------------------------------------------------- /packages/ladle/lib/app/src/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "about": { 3 | "homepage": "https://www.ladle.dev", 4 | "github": "https://github.com/tajo/ladle", 5 | "version": 1 6 | }, 7 | "stories": {} 8 | } 9 | -------------------------------------------------------------------------------- /packages/ladle/lib/app/src/mock-date.ts: -------------------------------------------------------------------------------- 1 | // adapted from https://github.com/boblauer/MockDate 2 | 3 | const RealDate = Date; 4 | let now: null | number = null; 5 | 6 | const MockDate = class Date extends RealDate { 7 | constructor(); 8 | constructor(value: any); 9 | constructor( 10 | year: number, 11 | month: number, 12 | date?: number, 13 | hours?: number, 14 | minutes?: number, 15 | seconds?: number, 16 | ms?: number, 17 | ); 18 | 19 | constructor( 20 | y?: number, 21 | m?: number, 22 | d?: number, 23 | h?: number, 24 | M?: number, 25 | s?: number, 26 | ms?: number, 27 | ) { 28 | super(); 29 | 30 | let date; 31 | 32 | switch (arguments.length) { 33 | case 0: 34 | if (now !== null) { 35 | date = new RealDate(now.valueOf()); 36 | } else { 37 | date = new RealDate(); 38 | } 39 | break; 40 | 41 | case 1: 42 | // @ts-ignore 43 | date = new RealDate(y); 44 | break; 45 | 46 | default: 47 | d = typeof d === "undefined" ? 1 : d; 48 | h = h || 0; 49 | M = M || 0; 50 | s = s || 0; 51 | ms = ms || 0; 52 | // @ts-ignore 53 | date = new RealDate(y, m, d, h, M, s, ms); 54 | break; 55 | } 56 | 57 | return date; 58 | } 59 | }; 60 | 61 | MockDate.UTC = RealDate.UTC; 62 | 63 | MockDate.now = function () { 64 | return new MockDate().valueOf(); 65 | }; 66 | 67 | MockDate.parse = function (dateString) { 68 | return RealDate.parse(dateString); 69 | }; 70 | 71 | MockDate.toString = function () { 72 | return RealDate.toString(); 73 | }; 74 | 75 | export function set(date: string): void { 76 | const dateObj = new Date(date.valueOf()); 77 | if (isNaN(dateObj.getTime())) { 78 | throw new TypeError("mockdate: The time set is an invalid date: " + date); 79 | } 80 | 81 | // @ts-ignore 82 | Date = MockDate; 83 | 84 | now = dateObj.valueOf(); 85 | } 86 | 87 | export function reset(): void { 88 | Date = RealDate; 89 | } 90 | 91 | export default { 92 | set, 93 | reset, 94 | }; 95 | -------------------------------------------------------------------------------- /packages/ladle/lib/app/src/msw.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { RequestHandler } from "msw"; 3 | 4 | const Msw = ({ 5 | children, 6 | msw, 7 | }: { 8 | children: React.ReactElement; 9 | msw: RequestHandler[]; 10 | }) => { 11 | const [ready, setReady] = React.useState(false); 12 | React.useEffect(() => { 13 | const initMsw = async () => { 14 | if (msw.length > 0) { 15 | const { setupWorker } = await import("msw/browser"); 16 | if (!window.__ladle_msw) { 17 | window.__ladle_msw = setupWorker(); 18 | window.__ladle_msw.use(...msw); 19 | window.__ladle_msw 20 | .start({ 21 | serviceWorker: { 22 | url: `${(import.meta as any).env.BASE_URL}mockServiceWorker.js`, 23 | }, 24 | }) 25 | .then(() => { 26 | setReady(true); 27 | }); 28 | } else { 29 | window.__ladle_msw.use(...msw); 30 | setReady(true); 31 | } 32 | } 33 | }; 34 | initMsw(); 35 | return () => { 36 | if (window.__ladle_msw) { 37 | window.__ladle_msw.resetHandlers(); 38 | } 39 | }; 40 | }, [msw]); 41 | if (msw.length === 0) return children; 42 | if (!ready) return null; 43 | return children; 44 | }; 45 | 46 | export default Msw; 47 | -------------------------------------------------------------------------------- /packages/ladle/lib/app/src/no-stories-error.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "./ui"; 2 | 3 | const NoStoriesError = ({ error }: { error: string }) => { 4 | console.log(error); 5 | return ( 6 |
    7 |

    SyntaxError when parsing stories ❌

    8 |
    {error}
    9 |

    Check the terminal for more info.

    10 |

    11 | 12 | More information. 13 | 14 |

    15 |

    16 | Please restart Ladle after fixing this issue. 17 |

    18 |

    19 | Github 20 |

    21 |

    22 | Docs 23 |

    24 |
    25 | ); 26 | }; 27 | 28 | export default NoStoriesError; 29 | -------------------------------------------------------------------------------- /packages/ladle/lib/app/src/no-stories.tsx: -------------------------------------------------------------------------------- 1 | import { config } from "virtual:generated-list"; 2 | import { Code, Link } from "./ui"; 3 | 4 | const NoStories = () => ( 5 |
    6 |

    No stories found

    7 |

    8 | The configured glob pattern for stories is: {config.stories}. 9 |

    10 |

    11 | It can be changed through the{" "} 12 | 13 | configuration file 14 | {" "} 15 | or CLI flag --stories=your-glob. 16 |

    17 |

    18 | GitHub 19 |

    20 |

    21 | Docs 22 |

    23 |
    24 | ); 25 | 26 | export default NoStories; 27 | -------------------------------------------------------------------------------- /packages/ladle/lib/app/src/redirect-events.ts: -------------------------------------------------------------------------------- 1 | const isModifierKeyPressed = (event: KeyboardEvent) => 2 | event.altKey || event.ctrlKey || event.shiftKey || event.metaKey; 3 | 4 | const shouldIgnoreEvent = (event: KeyboardEvent) => { 5 | const target = (event.target || {}) as HTMLElement; 6 | if ( 7 | !event.key || 8 | target.isContentEditable || 9 | (["INPUT", "TEXTAREA"].includes(target.nodeName) && 10 | !isModifierKeyPressed(event)) 11 | ) { 12 | return true; 13 | } 14 | return false; 15 | }; 16 | 17 | // redirecting keyboard events from the iframe to the parent document 18 | // so the global hotkeys work no matter where the focus is 19 | export const redirectKeydown = (event: KeyboardEvent) => { 20 | if (shouldIgnoreEvent(event)) return; 21 | const newEvent = new KeyboardEvent("keydown", { 22 | key: event.key, 23 | code: event.code, 24 | keyCode: event.keyCode, 25 | altKey: event.altKey, 26 | ctrlKey: event.ctrlKey, 27 | shiftKey: event.shiftKey, 28 | metaKey: event.metaKey, 29 | }); 30 | document.dispatchEvent(newEvent); 31 | }; 32 | 33 | export const redirectKeyup = (event: KeyboardEvent) => { 34 | if (shouldIgnoreEvent(event)) return; 35 | const newEvent = new KeyboardEvent("keyup", { 36 | key: event.key, 37 | code: event.code, 38 | keyCode: event.keyCode, 39 | altKey: event.altKey, 40 | ctrlKey: event.ctrlKey, 41 | shiftKey: event.shiftKey, 42 | metaKey: event.metaKey, 43 | }); 44 | document.dispatchEvent(newEvent); 45 | }; 46 | -------------------------------------------------------------------------------- /packages/ladle/lib/app/src/reducer.ts: -------------------------------------------------------------------------------- 1 | import { GlobalAction, ActionType, GlobalState } from "../../shared/types"; 2 | import debug from "./debug"; 3 | 4 | const reducer = (state: GlobalState, action: GlobalAction): GlobalState => { 5 | debug("Action dispatched", action); 6 | switch (action.type) { 7 | case ActionType.UpdateAll: 8 | return { ...state, ...action.value }; 9 | case ActionType.UpdateMode: 10 | return { ...state, mode: action.value }; 11 | case ActionType.UpdateAction: { 12 | const result = { ...state }; 13 | if (action.clear) { 14 | result.action = []; 15 | } 16 | if (!action.value) return result; 17 | return { ...state, action: [...result.action, action.value] }; 18 | } 19 | case ActionType.UpdateRtl: 20 | return { ...state, rtl: action.value }; 21 | case ActionType.UpdateSource: 22 | return { ...state, source: action.value }; 23 | case ActionType.UpdateStory: 24 | return { 25 | ...state, 26 | story: action.value, 27 | control: {}, 28 | controlInitialized: false, 29 | width: 0, 30 | action: [], 31 | }; 32 | case ActionType.UpdateTheme: 33 | return { ...state, theme: action.value }; 34 | case ActionType.UpdateWidth: 35 | return { ...state, width: action.value }; 36 | case ActionType.UpdateControl: 37 | return { ...state, control: action.value, controlInitialized: true }; 38 | case ActionType.UpdateControlIntialized: 39 | return { ...state, controlInitialized: action.value }; 40 | case ActionType.UpdateHotkeys: 41 | return { ...state, hotkeys: action.value }; 42 | default: 43 | return state; 44 | } 45 | }; 46 | 47 | export default reducer; 48 | -------------------------------------------------------------------------------- /packages/ladle/lib/app/src/story-hmr.ts: -------------------------------------------------------------------------------- 1 | type WatcherT = () => void; 2 | 3 | export const watchers: WatcherT[] = []; 4 | export const storyUpdated = () => { 5 | watchers.forEach((watcher) => watcher()); 6 | }; 7 | -------------------------------------------------------------------------------- /packages/ladle/lib/app/src/story-not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Code, Link } from "./ui"; 2 | 3 | const StoryNotFound = ({ activeStory }: { activeStory: string }) => ( 4 |
    5 |

    Story not found

    6 |

    7 | The story id {activeStory} you are trying to open does not 8 | exist. Typo? 9 |

    10 |

    11 | Back to home 12 |

    13 |

    14 | GitHub 15 |

    16 |

    17 | Docs 18 |

    19 |
    20 | ); 21 | 22 | export default StoryNotFound; 23 | -------------------------------------------------------------------------------- /packages/ladle/lib/app/touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tajo/ladle/c04af01a01e5c7222a5cff4b5a8bdaae9f112cf6/packages/ladle/lib/app/touch-icon.png -------------------------------------------------------------------------------- /packages/ladle/lib/app/window.d.ts: -------------------------------------------------------------------------------- 1 | import type { SetupWorker } from "msw/browser"; 2 | 3 | declare global { 4 | interface Window { 5 | __ladle_msw: SetupWorker; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/ladle/lib/cli/apply-cli-config.js: -------------------------------------------------------------------------------- 1 | import debug from "./debug.js"; 2 | import path from "path"; 3 | import merge from "lodash.merge"; 4 | import loadConfig from "./load-config.js"; 5 | 6 | /** 7 | * @param params {import("../shared/types").CLIParams} 8 | */ 9 | export default async function applyCLIConfig(params) { 10 | debug(`CLI theme: ${params.theme}`); 11 | debug(`CLI stories: ${params.stories}`); 12 | debug(`CLI host: ${params.host || "undefined"}`); 13 | debug(`CLI port: ${params.port || "undefined"}`); 14 | debug(`CLI out: ${params.outDir || "undefined"}`); 15 | params.config = params.config || ".ladle"; 16 | const configFolder = path.isAbsolute(params.config) 17 | ? params.config 18 | : path.join(process.cwd(), params.config); 19 | const config = await loadConfig(configFolder); 20 | if (params.theme) { 21 | config.addons.theme.defaultState = params.theme; 22 | delete params.theme; 23 | } 24 | merge(config, params); 25 | debug(`Final config:\n${JSON.stringify(config, null, " ")}`); 26 | process.env["VITE_PUBLIC_LADLE_THEME"] = config.addons.theme.defaultState; 27 | return { configFolder, config }; 28 | } 29 | -------------------------------------------------------------------------------- /packages/ladle/lib/cli/build.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import path from "path"; 4 | import { promises as fs } from "fs"; 5 | import { performance } from "perf_hooks"; 6 | import { globby } from "globby"; 7 | import viteProd from "./vite-prod.js"; 8 | import debug from "./debug.js"; 9 | import { getMetaJsonString } from "./vite-plugin/generate/get-meta-json.js"; 10 | import { getEntryData } from "./vite-plugin/parse/get-entry-data.js"; 11 | import getFolderSize from "./get-folder-size.js"; 12 | import applyCLIConfig from "./apply-cli-config.js"; 13 | import getAppId from "./get-app-id.js"; 14 | 15 | /** 16 | * @param params {import("../shared/types").CLIParams} 17 | */ 18 | const build = async (params = {}) => { 19 | const startTime = performance.now(); 20 | debug("Starting build command"); 21 | process.env["VITE_LADLE_APP_ID"] = getAppId(); 22 | const { configFolder, config } = await applyCLIConfig(params); 23 | await viteProd(config, configFolder); 24 | const entryData = await getEntryData( 25 | await globby( 26 | Array.isArray(config.stories) ? config.stories : [config.stories], 27 | ), 28 | ); 29 | const jsonContent = getMetaJsonString(entryData); 30 | await fs.writeFile( 31 | path.join(process.cwd(), config.outDir, "meta.json"), 32 | jsonContent, 33 | ); 34 | console.log("✓ Meta.json successfully created."); 35 | const folderSize = await getFolderSize( 36 | path.join(process.cwd(), config.outDir), 37 | ); 38 | const stopTime = performance.now(); 39 | const inSeconds = (stopTime - startTime) / 1000; 40 | console.log( 41 | `⏱️ Ladle finished the production build in ${Number(inSeconds).toFixed( 42 | 0, 43 | )}s producing ${folderSize} MiB of assets.`, 44 | ); 45 | console.log(config.i18n.buildTooltip); 46 | return true; 47 | }; 48 | 49 | export default build; 50 | -------------------------------------------------------------------------------- /packages/ladle/lib/cli/copy-msw-worker.js: -------------------------------------------------------------------------------- 1 | import { copyFile, access, mkdir } from "fs/promises"; 2 | import { join } from "path"; 3 | // @ts-ignore 4 | import { createRequire } from "node:module"; 5 | 6 | const require = createRequire(import.meta.url); 7 | 8 | /** 9 | * 10 | * @param {string} path 11 | */ 12 | async function ensureDirectoryExists(path) { 13 | try { 14 | await access(path); 15 | } catch { 16 | try { 17 | await mkdir(path); 18 | } catch (err) { 19 | console.error("Error:", err); 20 | } 21 | } 22 | } 23 | 24 | /** 25 | * 26 | * @param {string} publicDir 27 | */ 28 | const copyMswWorker = async (publicDir) => { 29 | await ensureDirectoryExists(publicDir); 30 | const mswWorkerPath = join(publicDir, "mockServiceWorker.js"); 31 | const mswPath = require.resolve("msw"); 32 | await copyFile(join(mswPath, "../../mockServiceWorker.js"), mswWorkerPath); 33 | }; 34 | 35 | export default copyMswWorker; 36 | -------------------------------------------------------------------------------- /packages/ladle/lib/cli/debug.js: -------------------------------------------------------------------------------- 1 | import debug from "debug"; 2 | 3 | export default debug("ladle:cli"); 4 | -------------------------------------------------------------------------------- /packages/ladle/lib/cli/empty-module.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /packages/ladle/lib/cli/get-app-id.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import crypto from "crypto"; 4 | 5 | const getAppId = () => { 6 | let pkgName = "unknown"; 7 | try { 8 | const pkgJson = fs.readFileSync( 9 | path.join(process.cwd() + "/package.json"), 10 | "utf-8", 11 | ); 12 | const parsedPkgJson = JSON.parse(pkgJson); 13 | pkgName = parsedPkgJson.name; 14 | } catch (e) {} 15 | const hash = crypto.createHash("sha256"); 16 | hash.update(process.cwd() + "#" + pkgName); 17 | return hash.digest("hex").slice(0, 6); 18 | }; 19 | 20 | export default getAppId; 21 | -------------------------------------------------------------------------------- /packages/ladle/lib/cli/get-app-root.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import { fileURLToPath } from "url"; 4 | 5 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 6 | 7 | const getAppRoot = () => { 8 | if ( 9 | fs.existsSync( 10 | path.join(__dirname, "../../typings-for-build/app/index.html"), 11 | ) 12 | ) { 13 | // published/compiled folder of our app 14 | return path.join(__dirname, "../../typings-for-build/app"); 15 | } 16 | return path.join(__dirname, "../app"); 17 | }; 18 | 19 | export default getAppRoot; 20 | -------------------------------------------------------------------------------- /packages/ladle/lib/cli/get-folder-size.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | 4 | /** 5 | * 6 | * @param {string} dirPath 7 | * @param {string[]} arrayOfFiles 8 | * @returns 9 | */ 10 | const getAllFiles = function (dirPath, arrayOfFiles = []) { 11 | const files = fs.readdirSync(dirPath); 12 | files.forEach(function (file) { 13 | if (fs.statSync(dirPath + "/" + file).isDirectory()) { 14 | arrayOfFiles = getAllFiles(dirPath + "/" + file, arrayOfFiles); 15 | } else { 16 | arrayOfFiles.push(path.join(dirPath, file)); 17 | } 18 | }); 19 | return arrayOfFiles; 20 | }; 21 | 22 | /** 23 | * 24 | * @param {string} directoryPath 25 | * @returns 26 | */ 27 | const getFolderSize = function (directoryPath) { 28 | const arrayOfFiles = getAllFiles(directoryPath); 29 | 30 | let totalSize = 0; 31 | 32 | arrayOfFiles.forEach(function (filePath) { 33 | totalSize += fs.statSync(filePath).size; 34 | }); 35 | 36 | return Number(totalSize / 1024 / 1024).toFixed(2); 37 | }; 38 | 39 | export default getFolderSize; 40 | -------------------------------------------------------------------------------- /packages/ladle/lib/cli/get-meta.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { getEntryData } from "./vite-plugin/parse/get-entry-data.js"; 4 | import { getMetaJson } from "./vite-plugin/generate/get-meta-json.js"; 5 | import { globby } from "globby"; 6 | import applyCLIConfig from "./apply-cli-config.js"; 7 | 8 | const getMeta = async (params = {}) => { 9 | const { config } = await applyCLIConfig(params); 10 | const entryData = await getEntryData( 11 | await globby( 12 | Array.isArray(config.stories) ? config.stories : [config.stories], 13 | ), 14 | ); 15 | const meta = getMetaJson(entryData); 16 | return meta; 17 | }; 18 | 19 | export default getMeta; 20 | -------------------------------------------------------------------------------- /packages/ladle/lib/cli/load-config.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { pathToFileURL } from "url"; 3 | import merge from "lodash.merge"; 4 | import debug from "./debug.js"; 5 | import defaultConfig from "../shared/default-config.js"; 6 | 7 | /** 8 | * @param {string} configFolder 9 | */ 10 | const loadConfig = async (configFolder) => { 11 | try { 12 | /** 13 | * @type {import('../shared/types').UserConfig} 14 | */ 15 | const config = ( 16 | await import(pathToFileURL(path.join(configFolder, "config.mjs")).href) 17 | ).default; 18 | if (Object.keys(config).length === 0) { 19 | debug("Custom config is empty."); 20 | } else { 21 | debug(`Custom config found: ${JSON.stringify(config, null, " ")}`); 22 | } 23 | // don't merge default width options 24 | if (config?.addons?.width?.options) { 25 | defaultConfig.addons.width.options = {}; 26 | } 27 | const mergedConfig = merge(defaultConfig, config); 28 | 29 | // don't merge hotkeys 30 | // @ts-ignore 31 | mergedConfig.hotkeys = { 32 | ...mergedConfig.hotkeys, 33 | ...config.hotkeys, 34 | }; 35 | 36 | return /** @type {import("../shared/types").Config} */ (mergedConfig); 37 | } catch (e) { 38 | debug(`No custom config found.`); 39 | return /** @type {import("../shared/types").Config} */ (defaultConfig); 40 | } 41 | }; 42 | 43 | export default loadConfig; 44 | -------------------------------------------------------------------------------- /packages/ladle/lib/cli/merge-vite-configs.js: -------------------------------------------------------------------------------- 1 | // copied from https://github.com/vitejs/vite/blob/main/packages/vite/src/node/utils.ts 2 | 3 | //@ts-ignore 4 | function arraify(target) { 5 | return Array.isArray(target) ? target : [target]; 6 | } 7 | 8 | //@ts-ignore 9 | function isObject(value) { 10 | return Object.prototype.toString.call(value) === "[object Object]"; 11 | } 12 | 13 | /** 14 | * 15 | * @param {import('vite').UserConfig} defaults 16 | * @param {import('vite').UserConfig} overrides 17 | * @param {string} rootPath 18 | * @returns 19 | */ 20 | function mergeConfigRecursively(defaults, overrides, rootPath) { 21 | /** @type import('vite').UserConfig */ 22 | const merged = { ...defaults }; 23 | for (const key in overrides) { 24 | //@ts-ignore 25 | const value = overrides[key]; 26 | if (value == null) { 27 | continue; 28 | } 29 | //@ts-ignore 30 | const existing = merged[key]; 31 | if (existing == null) { 32 | //@ts-ignore 33 | merged[key] = value; 34 | continue; 35 | } 36 | if (Array.isArray(existing) || Array.isArray(value)) { 37 | //@ts-ignore 38 | merged[key] = [...arraify(existing ?? []), ...arraify(value ?? [])]; 39 | continue; 40 | } 41 | if (isObject(existing) && isObject(value)) { 42 | //@ts-ignore 43 | merged[key] = mergeConfigRecursively( 44 | existing, 45 | value, 46 | rootPath ? `${rootPath}.${key}` : key, 47 | ); 48 | continue; 49 | } 50 | //@ts-ignore 51 | merged[key] = value; 52 | } 53 | return merged; 54 | } 55 | 56 | /** 57 | * 58 | * @param {import('vite').UserConfig} defaults 59 | * @param {import('vite').UserConfig} overrides 60 | * @param {boolean} isRoot 61 | * @returns 62 | */ 63 | export default function mergeConfig(defaults, overrides, isRoot = true) { 64 | return mergeConfigRecursively(defaults, overrides, isRoot ? "" : "."); 65 | } 66 | -------------------------------------------------------------------------------- /packages/ladle/lib/cli/preview.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import vitePreview from "./vite-preview.js"; 4 | import debug from "./debug.js"; 5 | import applyCLIConfig from "./apply-cli-config.js"; 6 | 7 | /** 8 | * @param params {import("../shared/types").CLIParams} 9 | */ 10 | const preview = async (params = {}) => { 11 | debug("Starting preview command"); 12 | const { configFolder, config } = await applyCLIConfig(params); 13 | await vitePreview(config, configFolder); 14 | }; 15 | 16 | export default preview; 17 | -------------------------------------------------------------------------------- /packages/ladle/lib/cli/serve.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import viteDev from "./vite-dev.js"; 4 | import debug from "./debug.js"; 5 | import applyCLIConfig from "./apply-cli-config.js"; 6 | import getAppId from "./get-app-id.js"; 7 | 8 | /** 9 | * @param params {import("../shared/types").CLIParams} 10 | */ 11 | const serve = async (params = {}) => { 12 | debug("Starting serve command"); 13 | process.env["VITE_LADLE_APP_ID"] = getAppId(); 14 | const { configFolder, config } = await applyCLIConfig(params); 15 | await viteDev(config, configFolder); 16 | }; 17 | 18 | export default serve; 19 | -------------------------------------------------------------------------------- /packages/ladle/lib/cli/vite-plugin/babel.js: -------------------------------------------------------------------------------- 1 | import btraverse from "@babel/traverse"; 2 | import btemplate from "@babel/template"; 3 | import bgenerate from "@babel/generator"; 4 | 5 | export const traverse = 6 | //@ts-ignore 7 | typeof btraverse === "function" ? btraverse : btraverse.default; 8 | export const template = 9 | //@ts-ignore 10 | typeof btemplate === "function" ? btemplate : btemplate.default; 11 | export const generate = 12 | //@ts-ignore 13 | typeof bgenerate === "function" ? bgenerate : bgenerate.default; 14 | -------------------------------------------------------------------------------- /packages/ladle/lib/cli/vite-plugin/generate/cleanup-windows-path.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} path 3 | * @return {string} 4 | */ 5 | function cleanupWindowsPath(path) { 6 | return path.replace(/\\/g, "/"); 7 | } 8 | 9 | export default cleanupWindowsPath; 10 | -------------------------------------------------------------------------------- /packages/ladle/lib/cli/vite-plugin/generate/get-generated-list.js: -------------------------------------------------------------------------------- 1 | import getStoryImports from "./get-story-imports.js"; 2 | import getStoryList from "./get-story-list.js"; 3 | import getStorySource from "./get-story-source.js"; 4 | import getConfigImport from "./get-config-import.js"; 5 | import getComponentsImport from "./get-components-import.js"; 6 | 7 | /** 8 | * @param entryData {import('../../../shared/types').EntryData} 9 | * @param configFolder {string} 10 | * @param config {import("../../../shared/types").Config} 11 | */ 12 | const getGeneratedList = async (entryData, configFolder, config) => { 13 | return ` 14 | ${getStoryImports(entryData)} 15 | ${getStoryList(entryData)} 16 | ${await getConfigImport(configFolder, config)} 17 | ${getComponentsImport(configFolder)} 18 | ${getStorySource(entryData, config.addons.source.enabled)} 19 | export const errorMessage = '';\n 20 | `; 21 | }; 22 | 23 | export default getGeneratedList; 24 | -------------------------------------------------------------------------------- /packages/ladle/lib/cli/vite-plugin/generate/get-meta-json.js: -------------------------------------------------------------------------------- 1 | import { storyIdToMeta } from "../naming-utils.js"; 2 | 3 | /** 4 | * @param entryData {import('../../../shared/types').EntryData} 5 | * @returns {import('../../../shared/types').MetaJson} 6 | */ 7 | export const getMetaJson = (entryData) => { 8 | /** @type {string[]} */ 9 | let storyIds = []; 10 | /** @type {{[key: string]: any}} */ 11 | let storyParams = {}; 12 | /** @type {{[key: string]: any}} */ 13 | let storyMeta = {}; 14 | 15 | Object.keys(entryData).forEach((entry) => { 16 | entryData[entry].stories.forEach( 17 | ({ storyId, locStart, locEnd, namedExport }) => { 18 | storyMeta[storyId] = { locStart, locEnd, filePath: entry, namedExport }; 19 | storyIds.push(storyId); 20 | }, 21 | ); 22 | storyParams = { ...storyParams, ...entryData[entry].storyParams }; 23 | }); 24 | 25 | /** @type {import('../../../shared/types').MetaJson} */ 26 | const result = { 27 | about: { 28 | homepage: "https://www.ladle.dev", 29 | github: "https://github.com/tajo/ladle", 30 | version: 1, 31 | }, 32 | stories: {}, 33 | }; 34 | storyIds.forEach((storyId) => { 35 | result.stories[storyId] = { 36 | ...storyIdToMeta(storyId), 37 | ...storyMeta[storyId], 38 | meta: storyParams[storyId] ? storyParams[storyId].meta : {}, 39 | }; 40 | }); 41 | return result; 42 | }; 43 | 44 | /** 45 | * @param entryData {import('../../../shared/types').EntryData} 46 | */ 47 | export const getMetaJsonString = (entryData) => 48 | JSON.stringify(getMetaJson(entryData), null, " "); 49 | 50 | /** 51 | * @param entryData {import('../../../shared/types').EntryData} 52 | */ 53 | export const getMetaJsonObject = (entryData) => getMetaJson(entryData); 54 | -------------------------------------------------------------------------------- /packages/ladle/lib/cli/vite-plugin/generate/get-story-imports.js: -------------------------------------------------------------------------------- 1 | import t from "@babel/types"; 2 | import path from "path"; 3 | import { template, generate } from "../babel.js"; 4 | import { IMPORT_ROOT } from "../utils.js"; 5 | import cleanupWindowsPath from "./cleanup-windows-path.js"; 6 | 7 | /** 8 | * @param entryData {import('../../../shared/types').EntryData} 9 | */ 10 | const getStoryImports = (entryData) => { 11 | let storyImports = `import { lazy, createElement, Fragment } from "react";\n`; 12 | storyImports += `import composeEnhancers from "/src/compose-enhancers";\n`; 13 | const lazyImport = template(` 14 | const %%component%% = lazy(() => 15 | import(%%source%%).then((module) => { 16 | return { default: composeEnhancers(module, %%story%%) }; 17 | }) 18 | ); 19 | `); 20 | 21 | Object.keys(entryData).forEach((entry) => { 22 | entryData[entry].stories.forEach(({ componentName, namedExport }) => { 23 | const ast = lazyImport({ 24 | source: t.stringLiteral( 25 | cleanupWindowsPath(path.join(IMPORT_ROOT, entry)), 26 | ), 27 | component: t.identifier(componentName), 28 | story: t.stringLiteral(namedExport), 29 | }); 30 | storyImports += `\n${generate(ast).code}`; 31 | }); 32 | }); 33 | 34 | return storyImports; 35 | }; 36 | 37 | export default getStoryImports; 38 | -------------------------------------------------------------------------------- /packages/ladle/lib/cli/vite-plugin/get-ast.js: -------------------------------------------------------------------------------- 1 | import * as parser from "@babel/parser"; 2 | import { codeFrameColumns } from "@babel/code-frame"; 3 | 4 | /** 5 | * @type {parser.ParserPlugin[]} 6 | */ 7 | const plugins = [ 8 | "jsx", 9 | "asyncGenerators", 10 | "classProperties", 11 | "classPrivateProperties", 12 | "classPrivateMethods", 13 | [ 14 | "decorators", 15 | { 16 | decoratorsBeforeExport: true, 17 | }, 18 | ], 19 | "doExpressions", 20 | "dynamicImport", 21 | "exportDefaultFrom", 22 | "exportNamespaceFrom", 23 | "functionBind", 24 | "functionSent", 25 | "importMeta", 26 | "logicalAssignment", 27 | "nullishCoalescingOperator", 28 | "numericSeparator", 29 | "objectRestSpread", 30 | "optionalCatchBinding", 31 | "optionalChaining", 32 | "partialApplication", 33 | "throwExpressions", 34 | "topLevelAwait", 35 | ]; 36 | 37 | /** 38 | * @param {string} code 39 | * @param {string} filename 40 | */ 41 | const getAst = (code, filename) => { 42 | try { 43 | return parser.parse(code, { 44 | sourceType: "module", 45 | plugins: [ 46 | ...plugins, 47 | filename.endsWith(".ts") || filename.endsWith(".tsx") 48 | ? "typescript" 49 | : "flow", 50 | ], 51 | }); 52 | } catch (/** @type {any} */ e) { 53 | console.log(" "); 54 | console.log(" "); 55 | console.log(`${e.toString()} in ${filename}`); 56 | console.log(""); 57 | console.log( 58 | codeFrameColumns( 59 | code, 60 | { start: e.loc }, 61 | { 62 | highlightCode: true, 63 | }, 64 | ), 65 | ); 66 | console.log(""); 67 | throw e; 68 | } 69 | }; 70 | 71 | export default getAst; 72 | -------------------------------------------------------------------------------- /packages/ladle/lib/cli/vite-plugin/naming-utils.js: -------------------------------------------------------------------------------- 1 | export const storyDelimiter = "-"; 2 | export const storyEncodeDelimiter = "$"; 3 | 4 | // BUT preserving delimiters -- 5 | const wordSeparators = 6 | /[\s\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,.\/:;<=>?@\[\]^_`{|}~]+/; 7 | 8 | /** 9 | * @param {string} str 10 | */ 11 | export const capitalize = (str) => { 12 | return str.charAt(0).toUpperCase() + str.slice(1); 13 | }; 14 | 15 | /** 16 | * @param {string} str 17 | * @returns {{name: string; levels: string[]}} 18 | */ 19 | export const storyIdToMeta = (str) => { 20 | const parts = str 21 | .split(`${storyDelimiter}${storyDelimiter}`) 22 | .map((level) => capitalize(level.replace(/-/g, " "))); 23 | return { 24 | name: /** @type {string} */ (parts.pop()), 25 | levels: parts, 26 | }; 27 | }; 28 | 29 | /** 30 | * @param {string} str 31 | */ 32 | export const kebabCase = (str) => { 33 | return str 34 | .replace( 35 | /[A-Z\u00C0-\u00D6\u00D9-\u00DD]+(?![a-z])|[A-Z\u00C0-\u00D6\u00D9-\u00DD]/g, 36 | ($, ofs) => (ofs ? "-" : "") + $.toLowerCase(), 37 | ) 38 | .replace(/\s-/g, "-") 39 | .trim() 40 | .split(wordSeparators) 41 | .join("-"); 42 | }; 43 | 44 | /** 45 | * @param {string} title 46 | */ 47 | export const titleToFileId = (title) => 48 | title 49 | .toLocaleLowerCase() 50 | .replace(/\s*\/\s*/g, `${storyDelimiter}${storyDelimiter}`) 51 | .replace(/\s+/g, storyDelimiter); 52 | 53 | /** 54 | * @param {string} filename 55 | */ 56 | export const getFileId = (filename) => { 57 | const pathParts = filename.split("/"); 58 | return pathParts[pathParts.length - 1].split(".")[0]; 59 | }; 60 | 61 | /** 62 | * @param {string} fileId 63 | * @param {string} namedExport 64 | */ 65 | export const getEncodedStoryName = (fileId, namedExport) => { 66 | return `${fileId}${storyEncodeDelimiter}${storyEncodeDelimiter}${namedExport}` 67 | .toLocaleLowerCase() 68 | .replace(new RegExp(storyDelimiter, "g"), storyEncodeDelimiter); 69 | }; 70 | -------------------------------------------------------------------------------- /packages/ladle/lib/cli/vite-plugin/parse/get-default-export.js: -------------------------------------------------------------------------------- 1 | import { converter } from "../ast-to-obj.js"; 2 | 3 | /** 4 | * @param {import('../../../shared/types').ParsedStoriesResult} result 5 | * @param {any} astPath 6 | */ 7 | const getDefaultExport = (result, astPath) => { 8 | if (!astPath) return; 9 | try { 10 | let objNode = astPath.node.declaration; 11 | if (astPath.node.declaration.type === "Identifier") { 12 | objNode = 13 | astPath.scope.bindings[astPath.node.declaration.name].path.node.init; 14 | } 15 | if ( 16 | ["TSAsExpression", "TSSatisfiesExpression"].includes( 17 | astPath.node.declaration.type, 18 | ) 19 | ) { 20 | objNode = astPath.node.declaration.expression; 21 | } 22 | objNode && 23 | objNode.properties.forEach((/** @type {any} */ prop) => { 24 | if (prop.type === "ObjectProperty" && prop.key.name === "title") { 25 | if (prop.value.type !== "StringLiteral") { 26 | throw new Error("Default title must be a string literal."); 27 | } 28 | result.exportDefaultProps.title = prop.value.value; 29 | } else if ( 30 | prop.type === "ObjectProperty" && 31 | prop.key.type === "Identifier" && 32 | prop.key.name === "meta" 33 | ) { 34 | const obj = converter(prop.value); 35 | const json = JSON.stringify(obj); 36 | result.exportDefaultProps.meta = JSON.parse(json); 37 | } 38 | }); 39 | } catch (e) { 40 | throw new Error( 41 | `Can't parse the default title and meta of ${result.entry}. Meta must be serializable and title a string literal.`, 42 | ); 43 | } 44 | }; 45 | 46 | export default getDefaultExport; 47 | -------------------------------------------------------------------------------- /packages/ladle/lib/cli/vite-plugin/parse/get-storyname-and-meta.js: -------------------------------------------------------------------------------- 1 | import { converter } from "../ast-to-obj.js"; 2 | 3 | /** 4 | * @param {import('../../../shared/types').ParsedStoriesResult} result 5 | * @param {any} astPath 6 | */ 7 | const getStorynameAndMeta = (result, astPath) => { 8 | astPath.node.body.forEach((/** @type {any} */ child) => { 9 | if ( 10 | child.type === "ExpressionStatement" && 11 | child.expression.left && 12 | child.expression.left.property 13 | ) { 14 | if (child.expression.left.property.name === "storyName") { 15 | const storyExport = child.expression.left.object.name; 16 | if (child.expression.right.type !== "StringLiteral") { 17 | throw new Error( 18 | `${storyExport}.storyName in ${result.entry} must be a string literal.`, 19 | ); 20 | } else { 21 | result.namedExportToStoryName[storyExport] = 22 | child.expression.right.value; 23 | } 24 | } else if (child.expression.left.property.name === "meta") { 25 | const storyExport = child.expression.left.object.name; 26 | if (child.expression.right.type !== "ObjectExpression") { 27 | throw new Error( 28 | `${storyExport}.meta in ${result.entry} must be an object expression.`, 29 | ); 30 | } else { 31 | try { 32 | const obj = converter(child.expression.right); 33 | const json = JSON.stringify(obj); 34 | result.namedExportToMeta[storyExport] = JSON.parse(json); 35 | } catch (e) { 36 | throw new Error( 37 | `${storyExport}.meta in ${result.entry} must be serializable.`, 38 | ); 39 | } 40 | } 41 | } 42 | } 43 | }); 44 | }; 45 | 46 | export default getStorynameAndMeta; 47 | -------------------------------------------------------------------------------- /packages/ladle/lib/cli/vite-plugin/utils.js: -------------------------------------------------------------------------------- 1 | // needed for unit tests to remove local specific paths from snapshots 2 | export const IMPORT_ROOT = process.env.IMPORT_ROOT || process.cwd(); 3 | 4 | /** 5 | * @param message {string} 6 | */ 7 | export const printError = (message) => console.error("\x1b[31m%s", message); 8 | 9 | /** 10 | * @param entryData {import('../../shared/types').EntryData} 11 | */ 12 | export const detectDuplicateStoryNames = (entryData) => { 13 | /** @type {{[key: string]: [string, string]}} */ 14 | const stories = {}; 15 | Object.keys(entryData).forEach((entry) => { 16 | entryData[entry].stories.forEach((story) => { 17 | if (stories.hasOwnProperty(story.storyId)) { 18 | throw Error( 19 | ` 20 | There are two stories with the same ID ${story.storyId} as a result 21 | of normalized file name and story name combination. 22 | 23 | - ${entry}: ${story.namedExport} 24 | - ${stories[story.storyId][0]}: ${stories[story.storyId][1]} 25 | 26 | Story IDs need to be unique. 27 | `, 28 | ); 29 | } else { 30 | stories[story.storyId] = [entry, story.namedExport]; 31 | } 32 | }); 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /packages/ladle/lib/cli/vite-prod.js: -------------------------------------------------------------------------------- 1 | import { build } from "vite"; 2 | import path from "path"; 3 | import getBaseViteConfig from "./vite-base.js"; 4 | 5 | /** 6 | * @param config {import("../shared/types").Config} 7 | * @param configFolder {string} 8 | */ 9 | const viteProd = async (config, configFolder) => { 10 | try { 11 | /** 12 | * @type {import('vite').InlineConfig} 13 | */ 14 | const viteConfig = await getBaseViteConfig(config, configFolder, { 15 | mode: config.mode || "production", 16 | build: { 17 | outDir: path.join(process.cwd(), config.outDir), 18 | emptyOutDir: true, 19 | chunkSizeWarningLimit: 2000, 20 | rollupOptions: { 21 | onwarn: (warn, defaultHandler) => { 22 | if ( 23 | warn.message.includes("empty-module.js is dynamically imported") 24 | ) { 25 | return; 26 | } 27 | defaultHandler(warn); 28 | }, 29 | }, 30 | }, 31 | }); 32 | await build(viteConfig); 33 | } catch (e) { 34 | console.log(e); 35 | return false; 36 | } 37 | return true; 38 | }; 39 | 40 | export default viteProd; 41 | -------------------------------------------------------------------------------- /packages/ladle/publish-next.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { execSync } from "child_process"; 4 | import fs from "fs"; 5 | import { 6 | preparePackageJsonForPublish, 7 | revertPackageJson, 8 | } from "./scripts/package-types-helpers.js"; 9 | 10 | const shortHash = execSync("git rev-parse --short HEAD").toString().trim(); 11 | const version = `0.0.0-next-${shortHash}`; 12 | 13 | console.log(`Publishing @ladle/react ${version}`); 14 | 15 | const pkgJson = JSON.parse(fs.readFileSync("./package.json")); 16 | const oldVersion = pkgJson.version; 17 | pkgJson.version = version; 18 | preparePackageJsonForPublish(pkgJson); 19 | fs.writeFileSync("./package.json", JSON.stringify(pkgJson, null, 2)); 20 | 21 | try { 22 | execSync("npm publish --tag next"); 23 | } catch (e) { 24 | console.log(e); 25 | console.log("Publish failed, reverting package.json"); 26 | } 27 | 28 | pkgJson.version = oldVersion; 29 | revertPackageJson(pkgJson); 30 | fs.writeFileSync("./package.json", JSON.stringify(pkgJson, null, 2)); 31 | -------------------------------------------------------------------------------- /packages/ladle/scripts/package-types-helpers.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | let oldTypes = null; 4 | let oldExports = null; 5 | 6 | export function preparePackageJsonForPublish(packageJson) { 7 | oldTypes = packageJson.types; 8 | packageJson.types = "./typings-for-build/app/exports.d.ts"; 9 | 10 | oldExports = JSON.parse(JSON.stringify(packageJson.exports)); 11 | packageJson.exports["."] = { 12 | types: { 13 | import: "./typings-for-build/app/exports.d.ts", 14 | require: "./typings-for-build/app/exports.d.cts", 15 | }, 16 | default: "./lib/app/exports.ts", 17 | }; 18 | 19 | return packageJson; 20 | } 21 | 22 | export function revertPackageJson(packageJson) { 23 | if (!oldTypes) { 24 | console.warn(`'oldTypes' is not defined, so we are unable to revert it`); 25 | } 26 | 27 | if (!oldExports) { 28 | console.warn(`'oldExports' is not defined, so we are unable to revert it`); 29 | } 30 | 31 | packageJson.types = oldTypes ?? packageJson.types; 32 | oldTypes = null; 33 | 34 | packageJson.exports = oldExports ?? packageJson.exports; 35 | oldExports = null; 36 | 37 | return packageJson; 38 | } 39 | -------------------------------------------------------------------------------- /packages/ladle/scripts/revert-package-types.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | const pkgJson = JSON.parse( 4 | fs.readFileSync("./packages/ladle/backup-package.json"), 5 | ); 6 | // write updates to package.json 7 | fs.writeFileSync( 8 | "./packages/ladle/package.json", 9 | JSON.stringify(pkgJson, null, 2), 10 | ); 11 | // remove backup file 12 | fs.rmSync("./packages/ladle/backup-package.json"); 13 | -------------------------------------------------------------------------------- /packages/ladle/scripts/update-index-path.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from "fs"; 4 | import path from "path"; 5 | import { fileURLToPath } from "url"; 6 | 7 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 8 | 9 | const indexPath = path.join(__dirname, "../typings-for-build/app/index.html"); 10 | const index = fs.readFileSync(indexPath, "utf8"); 11 | 12 | fs.writeFileSync( 13 | indexPath, 14 | index 15 | .replace("index.tsx", "index.js") 16 | .replace("init-side-effects.ts", "init-side-effects.js"), 17 | ); 18 | -------------------------------------------------------------------------------- /packages/ladle/scripts/update-package-types.js: -------------------------------------------------------------------------------- 1 | import { preparePackageJsonForPublish } from "./package-types-helpers.js"; 2 | import fs from "fs"; 3 | 4 | const pkgJson = JSON.parse(fs.readFileSync("./packages/ladle/package.json")); 5 | // write out old package.json to a temp file that won't be published 6 | fs.writeFileSync( 7 | "./packages/ladle/backup-package.json", 8 | JSON.stringify(pkgJson, null, 2), 9 | ); 10 | // update existing package.json 11 | preparePackageJsonForPublish(pkgJson); 12 | // write updates to package.json 13 | fs.writeFileSync( 14 | "./packages/ladle/package.json", 15 | JSON.stringify(pkgJson, null, 2), 16 | ); 17 | -------------------------------------------------------------------------------- /packages/ladle/tests/fixtures/animals.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { Story } from "@ladle/react"; 3 | 4 | export const Cat: Story = () => { 5 | return

    Cat

    ; 6 | }; 7 | 8 | export const Dog: Story = () => { 9 | return

    Dog

    ; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/ladle/tests/fixtures/capitalization.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { Story } from "@ladle/react"; 3 | 4 | export const BlueTinyCat: Story = () => { 5 | return

    Blue Tiny Cat

    ; 6 | }; 7 | 8 | export const BigBarkingDog: Story = () => { 9 | return

    Big Barking Dog

    ; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/ladle/tests/fixtures/default-meta.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { StoryDefault, Story } from "../../lib/app/exports"; 3 | 4 | export default { 5 | title: "Title", 6 | meta: { 7 | baseweb: { 8 | foo: "title", 9 | }, 10 | }, 11 | } satisfies StoryDefault; 12 | 13 | export const Cat: Story = () => { 14 | return

    Cat

    ; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/ladle/tests/fixtures/default-title.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { StoryDefault, Story } from "../../lib/app/exports"; 3 | 4 | export default { 5 | title: "Title", 6 | } satisfies StoryDefault; 7 | 8 | export const Cat: Story = () => { 9 | return

    Cat

    ; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/ladle/tests/fixtures/filenameCapitalization.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { Story } from "../../lib/app/exports"; 3 | 4 | export const Test: Story = () => { 5 | return

    Test

    ; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/ladle/tests/fixtures/meta.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from "@ladle/react"; 2 | import Readme from "./README.md"; 3 | 4 | 5 | 6 | # Some Description 7 | -------------------------------------------------------------------------------- /packages/ladle/tests/fixtures/our-animals--mammals.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { Story } from "../../lib/app/exports"; 3 | 4 | export const Cat: Story = () => { 5 | return

    Cat

    ; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/ladle/tests/fixtures/story-meta.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { StoryDefault, Story } from "../../lib/app/exports"; 3 | 4 | export default { 5 | title: "Title", 6 | meta: { 7 | baseweb: { 8 | theme: "dark", 9 | browsers: ["chrome", "webkit"], 10 | }, 11 | }, 12 | } satisfies StoryDefault; 13 | 14 | export const Cat: Story = () => { 15 | return

    Cat

    ; 16 | }; 17 | 18 | Cat.meta = { 19 | baseweb: { 20 | browsers: ["chrome", "firefox"], 21 | width: "500px", 22 | }, 23 | links: true, 24 | }; 25 | 26 | export const Dog: Story = () => { 27 | return

    Dog

    ; 28 | }; 29 | -------------------------------------------------------------------------------- /packages/ladle/tests/fixtures/story.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Story } from "@ladle/react"; 2 | 3 | # MDX Button 4 | 5 | With `MDX`, you can define button. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/ladle/tests/fixtures/storyname.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { Story } from "../../lib/app/exports"; 3 | 4 | export const Cat: Story = () => { 5 | const Stop = { storyName: "" }; 6 | // should be ignored 7 | Stop.storyName = "What"; 8 | return

    Cat

    ; 9 | }; 10 | 11 | Cat.storyName = "Doggo"; 12 | // @ts-expect-error 13 | Cat.foo = "Ha"; 14 | 15 | export const CapitalCity: Story = () => { 16 | return

    DC

    ; 17 | }; 18 | 19 | export const CapitalReplaced: Story = () => { 20 | return

    CapitalReplaced

    ; 21 | }; 22 | CapitalReplaced.storyName = "Champs Élysées"; 23 | -------------------------------------------------------------------------------- /packages/ladle/tests/mdx-to-stories.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "vitest"; 2 | import { VFile } from "vfile"; 3 | import { createFormatAwareProcessors } from "@mdx-js/mdx/internal-create-format-aware-processors"; 4 | import mdxToStories from "../lib/cli/vite-plugin/mdx-to-stories.js"; 5 | import fs from "fs/promises"; 6 | 7 | const { process } = createFormatAwareProcessors({ 8 | jsx: true, 9 | }); 10 | 11 | const getFixture = async (path: string) => { 12 | const value = await fs.readFile(new URL(path, import.meta.url), { 13 | encoding: "utf8", 14 | }); 15 | const file = new VFile({ value, path }); 16 | const compiled = await process(file); 17 | return [String(compiled.value), path]; 18 | }; 19 | 20 | test(" component works", async () => { 21 | const fixture = await getFixture("./fixtures/story.stories.mdx"); 22 | const output = await mdxToStories(...fixture); 23 | expect(output).toMatchSnapshot(); 24 | }); 25 | 26 | test(" component works", async () => { 27 | const fixture = await getFixture("./fixtures/meta.stories.mdx"); 28 | const output = await mdxToStories(...fixture); 29 | expect(output).toMatchSnapshot(); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/ladle/tests/naming-utils.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "vitest"; 2 | import { kebabCase } from "../lib/cli/vite-plugin/naming-utils.js"; 3 | 4 | test("kebabCase", () => { 5 | expect(kebabCase("ChampsÉlysées")).toBe("champs-élysées"); 6 | expect(kebabCase("our-animals--mammals")).toBe("our-animals--mammals"); 7 | expect(kebabCase("the quick brown fox")).toBe("the-quick-brown-fox"); 8 | expect(kebabCase("the-quick-brown-fox")).toBe("the-quick-brown-fox"); 9 | expect(kebabCase("the_quick_brown_fox")).toBe("the-quick-brown-fox"); 10 | expect(kebabCase("theQuickBrownFox")).toBe("the-quick-brown-fox"); 11 | expect(kebabCase("thequickbrownfox")).toBe("thequickbrownfox"); 12 | expect(kebabCase("theQUICKBrownFox")).toBe("the-quick-brown-fox"); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/ladle/tests/parse/.pnpm-debug.log: -------------------------------------------------------------------------------- 1 | { 2 | "0 debug pnpm:scope": { 3 | "selected": 1, 4 | "workspacePrefix": "/Users/vojtech/Projects/ladle" 5 | }, 6 | "1 error pnpm": { 7 | "errno": 1, 8 | "code": "ELIFECYCLE", 9 | "pkgid": "@ladle/react@2.4.2", 10 | "stage": "test", 11 | "script": "vitest", 12 | "pkgname": "@ladle/react", 13 | "err": { 14 | "name": "pnpm", 15 | "message": "@ladle/react@2.4.2 test: `vitest`\nExit status 1", 16 | "code": "ELIFECYCLE", 17 | "stack": "pnpm: @ladle/react@2.4.2 test: `vitest`\nExit status 1\n at EventEmitter. (/Users/vojtech/.node/corepack/pnpm/7.3.0/dist/pnpm.cjs:108415:20)\n at EventEmitter.emit (node:events:527:28)\n at ChildProcess. (/Users/vojtech/.node/corepack/pnpm/7.3.0/dist/pnpm.cjs:94981:18)\n at ChildProcess.emit (node:events:527:28)\n at maybeClose (node:internal/child_process:1092:16)\n at Process.ChildProcess._handle.onexit (node:internal/child_process:302:5)" 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /packages/ladle/tests/parse/get-entry-data.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "vitest"; 2 | import { getSingleEntry } from "../../lib/cli/vite-plugin/parse/get-entry-data.js"; 3 | 4 | test("Single file with two stories", async () => { 5 | const entryData = await getSingleEntry("tests/fixtures/animals.stories.tsx"); 6 | expect(entryData).toMatchSnapshot(); 7 | }); 8 | 9 | test("Capital letters in story names converted into delimiters", async () => { 10 | const entryData = await getSingleEntry( 11 | "tests/fixtures/capitalization.stories.tsx", 12 | ); 13 | expect(entryData).toMatchSnapshot(); 14 | }); 15 | 16 | test("Capital letters in the filename converted into delimiters", async () => { 17 | const entryData = await getSingleEntry( 18 | "tests/fixtures/filenameCapitalization.stories.tsx", 19 | ); 20 | expect(entryData).toMatchSnapshot(); 21 | }); 22 | 23 | test("Turn file name delimiters into spaces and levels correctly", async () => { 24 | const entryData = await getSingleEntry( 25 | "tests/fixtures/our-animals--mammals.stories.tsx", 26 | ); 27 | expect(entryData).toMatchSnapshot(); 28 | }); 29 | 30 | test("Default title is used instead of the file name", async () => { 31 | const entryData = await getSingleEntry( 32 | "tests/fixtures/default-title.stories.tsx", 33 | ); 34 | expect(entryData).toMatchSnapshot(); 35 | }); 36 | 37 | test("Story name replaces named export as a story name", async () => { 38 | const entryData = await getSingleEntry( 39 | "tests/fixtures/storyname.stories.tsx", 40 | ); 41 | expect(entryData).toMatchSnapshot(); 42 | }); 43 | 44 | test("Extract default meta", async () => { 45 | const entryData = await getSingleEntry( 46 | "tests/fixtures/default-meta.stories.tsx", 47 | ); 48 | expect(entryData).toMatchSnapshot(); 49 | }); 50 | 51 | test("Extract and merge story meta", async () => { 52 | const entryData = await getSingleEntry( 53 | "tests/fixtures/story-meta.stories.tsx", 54 | ); 55 | expect(entryData).toMatchSnapshot(); 56 | }); 57 | -------------------------------------------------------------------------------- /packages/ladle/tests/parse/utils.ts: -------------------------------------------------------------------------------- 1 | import traverse from "@babel/traverse"; 2 | import cloneDeep from "../../lib/cli/deps/lodash.clonedeep.js"; 3 | import merge from "lodash.merge"; 4 | import getAst from "../../lib/cli/vite-plugin/get-ast.js"; 5 | import type { ParsedStoriesResult } from "../../lib/shared/types"; 6 | 7 | export const parseWithFn = ( 8 | code: string, 9 | input: Partial, 10 | fn: any, 11 | visitor: string, 12 | filename = "foo.stories.js", 13 | ): ParsedStoriesResult => { 14 | const start: ParsedStoriesResult = merge( 15 | { 16 | entry: "file.js", 17 | stories: [], 18 | exportDefaultProps: { title: undefined, meta: undefined }, 19 | namedExportToMeta: {}, 20 | namedExportToStoryName: {}, 21 | storyParams: {}, 22 | fileId: "file", 23 | }, 24 | input, 25 | ); 26 | const end: ParsedStoriesResult = cloneDeep(start); 27 | (traverse as any)(getAst(code, filename) as any, { 28 | [visitor]: fn.bind(this, end), 29 | }); 30 | return end; 31 | }; 32 | 33 | export const getOutput = ( 34 | input: Partial, 35 | ): ParsedStoriesResult => { 36 | return merge( 37 | { 38 | entry: "file.js", 39 | stories: [], 40 | exportDefaultProps: { title: undefined, meta: undefined }, 41 | namedExportToMeta: {}, 42 | namedExportToStoryName: {}, 43 | storyParams: {}, 44 | fileId: "file", 45 | }, 46 | input, 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /packages/ladle/tests/story-name.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, describe } from "vitest"; 2 | import { sortStories } from "../lib/app/src/story-name"; 3 | 4 | const identity = (s: string[]) => s; 5 | 6 | describe("sortStories", () => { 7 | test("single level with default sort", () => { 8 | expect(sortStories(["a", "c", "b"], identity)).toStrictEqual([ 9 | "a", 10 | "b", 11 | "c", 12 | ]); 13 | }); 14 | test("two levels with default sort", () => { 15 | expect( 16 | sortStories(["a--b", "a--a", "a", "c--b", "b"], identity), 17 | ).toStrictEqual(["a--a", "a--b", "c--b", "a", "b"]); 18 | }); 19 | test("config.sortOrder is array", () => { 20 | expect( 21 | sortStories(["a--b", "a--a", "a", "c", "b"], ["a", "b"]), 22 | ).toStrictEqual(["a", "b"]); 23 | }); 24 | test("config.sortOrder is array with non-existing story", () => { 25 | expect(() => 26 | sortStories(["a--b", "a--a", "a", "c", "b"], ["a", "b", "x"]), 27 | ).toThrow( 28 | `Story "x" does not exist in your storybook. Please check your storyOrder config.`, 29 | ); 30 | }); 31 | test("config.sortOrder is array with wild card", () => { 32 | expect( 33 | sortStories( 34 | ["a--b", "a--a", "a", "c--a", "c--b", "b", "b--x"], 35 | ["c--*", "a*", "b", "a"], 36 | ), 37 | ).toStrictEqual(["c--a", "c--b", "a--a", "a--b", "a", "b"]); 38 | }); 39 | test("config.sortOrder is fn", () => { 40 | expect( 41 | sortStories(["a--b", "a--a", "a", "c--a", "c--b", "b", "b--x"], () => [ 42 | "c--a", 43 | "b", 44 | "b--x", 45 | ]), 46 | ).toStrictEqual(["c--a", "b", "b--x"]); 47 | }); 48 | test("config.sortOrder is fn with wild card", () => { 49 | expect( 50 | sortStories(["a--b", "a--a", "a", "c--a", "c--b", "b", "b--x"], () => [ 51 | "c--*", 52 | "a*", 53 | "b", 54 | "a", 55 | ]), 56 | ).toStrictEqual(["c--a", "c--b", "a--a", "a--b", "a", "b"]); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /packages/ladle/tsconfig.typesoutput.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "declaration": true, 6 | "emitDeclarationOnly": false, 7 | "declarationDir": "typings-for-build", 8 | "outDir": "typings-for-build" 9 | }, 10 | "include": ["lib/**/*", "src/**/*"], 11 | "exclude": ["typings-for-build"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/ladle/types/generated-list.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { GlobalState, GlobalAction, Config } from "../lib/shared/types"; 3 | import { Args, ArgTypes } from "../lib/app/exports"; 4 | 5 | type ReactNodeWithoutObject = 6 | | React.ReactElement 7 | | string 8 | | number 9 | | boolean 10 | | null 11 | | undefined; 12 | 13 | declare module "virtual:generated-list" { 14 | export const list: string[]; 15 | export const config: Config; 16 | export const errorMessage: string; 17 | export const args: Args; 18 | export const argTypes: ArgTypes; 19 | export const stories: { 20 | [key: string]: { 21 | entry: string; 22 | locStart: number; 23 | locEnd: number; 24 | component: React.FC; 25 | meta: any; 26 | }; 27 | }; 28 | export const storySource: { [key: string]: string }; 29 | export const Provider: React.FC<{ 30 | globalState: GlobalState; 31 | dispatch: React.Dispatch; 32 | config: Config; 33 | children: ReactNodeWithoutObject; 34 | storyMeta?: any; 35 | }>; 36 | export const StorySourceHeader: React.FC<{ 37 | path: string; 38 | locStart: number; 39 | locEnd: number; 40 | }>; 41 | } 42 | -------------------------------------------------------------------------------- /packages/website/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /packages/website/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern static website generator. 4 | 5 | ## Installation 6 | 7 | ```console 8 | pnpm install 9 | ``` 10 | 11 | ## Local Development 12 | 13 | ```console 14 | pnpm start 15 | ``` 16 | 17 | This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ## Build 20 | 21 | ```console 22 | pnpm build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ## Deployment 28 | 29 | ```console 30 | GIT_USER= USE_SSH=true pnpm deploy 31 | ``` 32 | 33 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 34 | -------------------------------------------------------------------------------- /packages/website/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve("@docusaurus/core/lib/babel/preset")], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/website/blog/authors.yml: -------------------------------------------------------------------------------- 1 | vojtech: 2 | name: Vojtech Miksu 3 | title: Developer Platform, Uber 4 | url: https://www.miksu.cz 5 | image_url: https://miksu.cz/images/profile.jpg 6 | email: vojtech@miksu.cz 7 | socials: 8 | x: vmiksu 9 | github: tajo 10 | -------------------------------------------------------------------------------- /packages/website/docs/a11y.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: a11y 3 | title: Accessibility 4 | --- 5 | 6 | import Image from "@theme/IdealImage"; 7 | import imgA11y from "../static/img/a11y.png"; 8 | 9 | Ladle is accessible. If you want to build accessible component, the wrapping environment should be accessible and keyboard friendly too. That's why your story comes first and the side navigation / addons second. 10 | 11 | Ladle also includes an a11y testing through [axe-core](https://github.com/dequelabs/axe-core), so you can fix any a11y violations in your components. 12 | 13 | You have to enable it in your `.ladle/config.mjs`: 14 | 15 | ```js 16 | /** @type {import('@ladle/react').UserConfig} */ 17 | export default { 18 | addons: { 19 | a11y: { 20 | enabled: true, 21 | }, 22 | }, 23 | }; 24 | ``` 25 | 26 | A11y addon 27 | -------------------------------------------------------------------------------- /packages/website/docs/actions.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: actions 3 | title: Actions 4 | --- 5 | 6 | import Image from "@theme/IdealImage"; 7 | import imgActions from "../static/img/actions.png"; 8 | 9 | When your stories have interactive elements you can track user actions through this addon. It's a simple alternative to the combination of `console.log` and devtools. It makes the output easier to notice and parse. 10 | 11 | ```tsx 12 | import type { Story } from "@ladle/react"; 13 | 14 | export const MyStory: Story<{ 15 | onClick: () => void; 16 | }> = ({ onClick }) => { 17 | return ; 18 | }; 19 | 20 | MyStory.argTypes = { 21 | onClick: { 22 | action: "clicked", 23 | }, 24 | }; 25 | ``` 26 | 27 | The click on this button creates a notification so you can inspect the event that was emitted: 28 | 29 | Actions 30 | 31 | # Direct usage 32 | 33 | You can also use this feature directly without argTypes: 34 | 35 | ```tsx 36 | import { action } from "@ladle/react"; 37 | 38 | export const MyStory = () => ( 39 | 40 | ); 41 | ``` 42 | 43 | The only argument of `action` is the label describing the event. 44 | -------------------------------------------------------------------------------- /packages/website/docs/addons.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: addons 3 | title: Addons 4 | --- 5 | 6 | Ladle currently does not support third-party addons, but it might in the future. For now, it comes with a set of default addons you can find in the bottom left corner (button icons): 7 | 8 | - [Accessibility](./a11y) (Axe) 9 | - [Background](./background) 10 | - [Controls](./controls) (only if `args` or `argTypes` are defined) 11 | - Dark theme 12 | - [Links](./links) 13 | - [MSW](./msw) (API mocking) 14 | - Preview mode 15 | - Right-to-left 16 | - [Story source code](./source) 17 | - [Width](./width) 18 | 19 | There are also other features you might not even notice at first: 20 | 21 | - Addons and all their state is persisted through the URL. That's useful for sharing or testing a specific story state. The browser navigation works as expected. 22 | - **Ladle is accessible**. If you want to build an accessible component, the wrapping environment should be accessible and keyboard friendly too. That's why your story comes first and the side navigation / addons second. 23 | - **Ladle is responsive**. There is no addon for different viewports. Just use your browser's dev tools. 24 | - Ladle is a single application with no iframes but is careful to not affect your stories in unintended ways. This makes the build faster and smaller and debugging through _Elements_ more straightforward. 25 | - Ladle code-splits stories by default. 26 | - React Fast Refresh is enabled by default. 27 | 28 | ## Storybook interoperability 29 | 30 | Ladle currently supports or partially supports most major features of the [Component Story Format](https://storybook.js.org/docs/react/api/csf). We are long-time users of Storybook, and it is our priority to make the transition between the two tools as seamless as possible (aka no changes in the user code). However, the goal is not to implement every single feature or addon that Storybook provides. 31 | 32 | Some features that are currently missing but are on our roadmap: 33 | 34 | - Generating controls through TS types 35 | -------------------------------------------------------------------------------- /packages/website/docs/babel.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: babel 3 | title: Babel 4 | --- 5 | 6 | Ladle uses [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) by default. This plugin uses [SWC](https://swc.rs/) to transform all React code and is many times faster than Babel. 7 | 8 | If you want to use [Babel](https://babeljs.io/) instead of SWC (in case you need to apply some custom babel plugins), you can do it by installing and adding [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react) into `./vite.config.js`: 9 | 10 | ```js title="vite.config.js" 11 | import react from "@vitejs/plugin-react"; 12 | 13 | export default { 14 | plugins: [react()], 15 | }; 16 | ``` 17 | 18 | Ladle automatically disables the default SWC plugin. 19 | 20 | SWC is up to 20x faster than Babel and can supercharge your development experience. The only downside is that you can't utilize a rich ecosystem of Babel plugins if your project uses a special syntax. However, there are also a few SWC [plugins](https://github.com/swc-project/plugins) and there is a [plugin API](https://swc.rs/docs/plugin/ecmascript/getting-started) so you can write your own. 21 | -------------------------------------------------------------------------------- /packages/website/docs/background.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: background 3 | title: Background 4 | --- 5 | 6 | You can set the background color of the story canvas through a special [control](./controls) with type `background`: 7 | 8 | ```tsx title=".ladle/components.tsx" 9 | export const argTypes = { 10 | background: { 11 | control: { type: "background" }, 12 | options: ["purple", "blue", "white", "pink"], 13 | defaultValue: "purple", 14 | }, 15 | }; 16 | ``` 17 | 18 | This will make it accessible to all stories through the standard control's UI. You can also set it per story level as other controls and even override the global settings: 19 | 20 | ```tsx title="src/hello.stories.tsx" 21 | export const Story = () =>
    Hello
    ; 22 | Story.argTypes = { 23 | background: { 24 | name: "Canvas background", 25 | control: { type: "background" }, 26 | options: ["green", "yellow", "pink"], 27 | defaultValue: "pink", 28 | }, 29 | }; 30 | ``` 31 | 32 | Note that there can be only one active background control. 33 | -------------------------------------------------------------------------------- /packages/website/docs/decorators.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: decorators 3 | title: Decorators 4 | --- 5 | 6 | Ladle supports story decorators, so you can wrap all stories in a file with additional React component(s). This is useful if your stories/components rely on [React Context](https://reactjs.org/docs/context.html) and libraries like `react-router`, `redux` or [next](https://ladle.dev/docs/nextjs/). In the example below, we are adding an extra `margin: 3em` to each story: 7 | 8 | ```tsx 9 | import type { StoryDefault } from "@ladle/react"; 10 | 11 | export default { 12 | decorators: [ 13 | (Component) => ( 14 |
    15 | 16 |
    17 | ), 18 | ], 19 | } satisfies StoryDefault; 20 | ``` 21 | 22 | `decorators` is an array, so you can compose multiple components together. You can also set a [global decorator or provider](./providers). 23 | 24 | Decorators can be also applied to a specific story: 25 | 26 | ```tsx 27 | import type { Story } from "@ladle/react"; 28 | 29 | export const MyStory: Story = () =>
    My Story
    ; 30 | 31 | MyStory.decorators = [ 32 | (Component) => ( 33 |
    34 | 35 |
    36 | ), 37 | ]; 38 | ``` 39 | 40 | ## Context Parameter 41 | 42 | You can also access Ladle's context through the second parameter. This way, your decorators can control every aspect of Ladle, including the state of controls and other addons: 43 | 44 | ```tsx 45 | import type { StoryDefault, Story } from "@ladle/react"; 46 | 47 | type Props = { label: string }; 48 | 49 | export default { 50 | decorators: [ 51 | (Component, context) => ( 52 |
    53 | {context.globalState.control.label.value} 54 | 55 |
    56 | ), 57 | ], 58 | } satisfies StoryDefault; 59 | 60 | const Card: Story = ({ label }) =>

    Label: {label}

    ; 61 | Card.args = { 62 | label: "Hello", 63 | }; 64 | ``` 65 | -------------------------------------------------------------------------------- /packages/website/docs/hotkeys.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: hotkeys 3 | title: Hotkeys 4 | --- 5 | 6 | import Image from "@theme/IdealImage"; 7 | import imgHotkeys from "../static/img/hotkeys.png"; 8 | 9 | Ladle has a few hotkeys to make your life easier: 10 | 11 | - `/` or `⌘ cmd + p` - Focus search input in the sidebar 12 | - `⌥ opt + →` - Go to the next story 13 | - `⌥ opt + ←` - Go to the previous story 14 | - `⌥ opt + ↓` - Go to the next component 15 | - `⌥ opt + ↑` - Go to the previous component 16 | - `c` - Toggle controls addon 17 | - `d` - Toggle dark mode 18 | - `f` - Toggle fullscreen mode 19 | - `w` - Toggle width addon 20 | - `r` - Toggle right-to-left mode 21 | - `s` - Toggle story source addon 22 | - `a` - Toggle accessibility addon 23 | 24 | ## Settings 25 | 26 | These defaults can be customized through the [configuration](config#hotkeys). 27 | 28 | Some stories might have utilize their own set of hotkeys. If you want to prevent conflicts with Ladle, you can disable all Ladle shortcuts for a specific story by using the `meta` parameter: 29 | 30 | ```tsx 31 | export default { 32 | meta: { 33 | hotkeys: false, 34 | }, 35 | }; 36 | Story.meta = { 37 | hotkeys: false, 38 | }; 39 | ``` 40 | 41 | ## Cheat Sheet 42 | 43 | If you need a quick reminder of all the hotkeys, you can open the about addon: 44 | 45 | Hotkeys cheatsheet 46 | -------------------------------------------------------------------------------- /packages/website/docs/introduction.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: introduction 3 | title: Introduction 4 | slug: / 5 | --- 6 | 7 | import Image from "@theme/IdealImage"; 8 | import imgBaseweb from "../static/img/ladle-baseweb.png"; 9 | 10 | **Ladle is a drop-in alternative to Storybook**. It is a tool for developing and testing your React components in an environment that's isolated and faster than most real-world applications. Ladle also creates an index of your components, so you can easily test them through tools like Playwright. 11 | 12 | Ladle Baseweb Demo 13 | 14 | ## Why? Performance! 15 | 16 | Ladle supports only React, embraces the latest standards (ES Modules) and focuses on performance. It's built around [Vite](https://vitejs.dev/) - modules are directly served to the browser and the bundling step is completely skipped. This means **instant server starts** no matter how many components it needs to load. 17 | 18 | Ladle still produces an optimized bundle using [rollup](https://rollupjs.org/guide/en/) when it's time to deploy it. Without adding a single component **Storybook 6.4 outputs 5.1MB of assets. Ladle only 250KB**. Ladle is almost 20x smaller. 19 | 20 | Each Ladle story gets automatically code-split, so it doesn't matter how many components you want it to handle. Ladle always loads fast. 21 | -------------------------------------------------------------------------------- /packages/website/docs/links.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: links 3 | title: Links 4 | --- 5 | 6 | You can link story from another story: 7 | 8 | ```tsx 9 | import * as React from "react"; 10 | import { linkTo } from "@ladle/react"; 11 | import type { Story } from "@ladle/react"; 12 | 13 | export const Link: Story = () => { 14 | return ; 15 | }; 16 | ``` 17 | 18 | The id used `controls--first` is what you find in the URL as the `?story=` parameter. 19 | -------------------------------------------------------------------------------- /packages/website/docs/meta.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: meta 3 | title: Meta 4 | --- 5 | 6 | Ladle exports `meta.json` with the list of all stories and some additional information. In the `serve` mode, it is accessible as `http://localhost:61000/meta.json` and `build` just outputs `meta.json` into the build folder. Example: 7 | 8 | ```json title="meta.json" 9 | { 10 | "about": { 11 | "homepage": "https://www.ladle.dev", 12 | "github": "https://github.com/tajo/ladle", 13 | "version": 1 14 | }, 15 | "stories": { 16 | "control--first": { 17 | "name": "First", 18 | "levels": ["Control"], 19 | "locStart": 12, 20 | "locEnd": 12, 21 | "filePath": "src/control.stories.tsx", 22 | "meta": {} 23 | } 24 | } 25 | } 26 | ``` 27 | 28 | You can also add additional annotations to each story (or stories) through the `meta` object. It needs to be statically analyzable. 29 | 30 | ```tsx title="control.stories.tsx" 31 | export default { 32 | meta: { 33 | baseweb: "test", 34 | browsers: ["chrome"], 35 | }, 36 | }; 37 | 38 | export const First = () =>

    First

    ; 39 | First.meta = { 40 | browsers: ["firefox"], 41 | }; 42 | ``` 43 | 44 | ```json title="meta.json" 45 | { 46 | "stories": { 47 | "control--first": { 48 | "name": "First", 49 | "levels": ["Control"], 50 | "locStart": 8, 51 | "locEnd": 8, 52 | "filePath": "src/control.stories.tsx", 53 | "meta": { "baseweb": "test", "browsers": ["firefox"] } 54 | } 55 | } 56 | } 57 | ``` 58 | 59 | This is useful for further automation. For example, you can load this file in your CI and create visual snapshots for each story. 60 | 61 | ## Testing 62 | 63 | If you use Ladle for end-to-end testing with a framework as [Playwright](https://playwright.dev/), make sure your story is fully loaded before you run the test. Stories are code-split and loaded later in the process. Ladle adds `data-storyloaded` attribute to the `` tag, so you can `await` for it in Playwright: 64 | 65 | ```tsx 66 | await page.waitForSelector("[data-storyloaded]"); 67 | ``` 68 | -------------------------------------------------------------------------------- /packages/website/docs/mock-date.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: mock-date 3 | title: Mock Date 4 | --- 5 | 6 | When you test your stories, it might be useful to mock `new Date()` so the displayed components always render same values. You can use `meta.mockDate` parameter to set a specific date and time: 7 | 8 | ```js title="date-picker.stories.tsx" 9 | import type { Story } from "@ladle/react"; 10 | 11 | export const DatePicker: Story = () => { 12 | const date = new Date(); 13 | return ( 14 |

    {date.toLocaleDateString("en-US")}

    15 | ); 16 | }; 17 | 18 | DatePicker.meta = { 19 | mockDate: "1995-12-17T03:24:00", 20 | }; 21 | ``` 22 | -------------------------------------------------------------------------------- /packages/website/docs/programmatic.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: programmatic 3 | title: Programmatic 4 | --- 5 | 6 | Ladle can be also used through its JavaScript API: 7 | 8 | ```tsx 9 | import serve from "@ladle/react/serve"; 10 | import build from "@ladle/react/build"; 11 | import preview from "@ladle/react/preview"; 12 | 13 | await serve({ 14 | // config: {} 15 | }); 16 | await build({ 17 | // config: {} 18 | }); 19 | await preview({ 20 | // config: {} 21 | }); 22 | ``` 23 | 24 | Explore all config.mjs [options](./config#ladleconfigmjs). 25 | -------------------------------------------------------------------------------- /packages/website/docs/source.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: source 3 | title: Story Source 4 | --- 5 | 6 | import Image from "@theme/IdealImage"; 7 | import imgSource from "../static/img/story-source.png"; 8 | 9 | You can preview the source code of the active story and its origin. 10 | 11 | Story Source Code 12 | 13 | ## Hyperlink 14 | 15 | You can customize the header of the source addon through `.ladle/components.tsx`: 16 | 17 | ```tsx title=".ladle/components.tsx" 18 | import type { SourceHeader } from "@ladle/react"; 19 | export const StorySourceHeader: SourceHeader = ({ path }) => { 20 | return ( 21 | 22 | Github link? {path} 23 | 24 | ); 25 | }; 26 | ``` 27 | 28 | This might be useful if you want provide a hyperlink. 29 | -------------------------------------------------------------------------------- /packages/website/docs/troubleshooting.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: troubleshooting 3 | title: Troubleshooting 4 | --- 5 | 6 | If you run into problems, try to enable verbose output: 7 | 8 | ```bash 9 | DEBUG=ladle* pnpm ladle serve 10 | DEBUG=ladle* pnpm ladle build 11 | ``` 12 | 13 | You can also enable verbose output in the browser console by adding an item into local storage `debug: ladle*` where `debug` is the key and `ladle*` the value. In Chrome, you can do that by opening the dev tools and insert this into the console: 14 | 15 | ``` 16 | localStorage.debug = 'ladle*' 17 | ``` 18 | 19 | ## Create Issue 20 | 21 | You can also search [existing issues](https://github.com/tajo/ladle/issues) or add a new one. 22 | 23 | ## Discord 24 | 25 | Join our [community](https://discord.gg/H6FSHjyW7e). 26 | 27 | ## ES Modules 28 | 29 | Ladle embraces ES Modules and is implemented as an ES module. That requires `Node 20+` and environment that fully supports ESM. 30 | -------------------------------------------------------------------------------- /packages/website/docs/typescript.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: typescript 3 | title: TypeScript 4 | --- 5 | 6 | # TypeScript 7 | 8 | Ladle is written in TypeScript and provides first-class support for TypeScript. 9 | 10 | ## `tsconfig.json` 11 | 12 | Ladle uses [jsx-runtime](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html) so there is not need to import React at the top of each module. However, you need to let TypeScript know: 13 | 14 | ```json title="tsconfig.json" 15 | { 16 | "compilerOptions": { 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src", ".ladle"] 20 | } 21 | ``` 22 | 23 | ## Exported Types 24 | 25 | You can import [many types](https://github.com/tajo/ladle/blob/main/packages/ladle/lib/app/exports.ts#L52-L115) from `@ladle/react` to improve your development experience: 26 | 27 | ```ts 28 | import type { StoryDefault, Story } from "@ladle/react"; 29 | 30 | type Props = { label: string }; 31 | 32 | export default { 33 | title: "New title", 34 | } satisfies StoryDefault; 35 | 36 | const Card: Story = ({ label }) =>

    Label: {label}

    ; 37 | Card.args = { 38 | label: "Hello", 39 | }; 40 | ``` 41 | -------------------------------------------------------------------------------- /packages/website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "0.5.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "serve": "docusaurus serve", 12 | "clear": "docusaurus clear", 13 | "lint": "echo 'no lint'", 14 | "test": "echo 'no test'" 15 | }, 16 | "dependencies": { 17 | "@algolia/client-search": "^5.18.0", 18 | "@docusaurus/core": "^3.6.3", 19 | "@docusaurus/plugin-ideal-image": "^3.6.3", 20 | "@docusaurus/preset-classic": "^3.6.3", 21 | "@docusaurus/theme-common": "^3.6.3", 22 | "@mdx-js/react": "^3.1.0", 23 | "@types/react": "^19.0.2", 24 | "clsx": "^2.1.1", 25 | "prism-react-renderer": "^2.4.1", 26 | "react": "^18.3.0", 27 | "react-dom": "^18.3.0", 28 | "typescript": "^5.7.2" 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.5%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/website/sidebars.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | someSidebar: [ 3 | { 4 | type: "category", 5 | label: "Getting Started", 6 | collapsible: false, 7 | items: ["introduction", "setup", "stories", "decorators"], 8 | }, 9 | { 10 | type: "category", 11 | label: "Features", 12 | items: [ 13 | "css", 14 | "hotkeys", 15 | "mdx", 16 | "meta", 17 | "mock-date", 18 | "providers", 19 | "typescript", 20 | "visual-snapshots", 21 | ], 22 | }, 23 | { 24 | type: "category", 25 | label: "Addons", 26 | items: [ 27 | "addons", 28 | "a11y", 29 | "actions", 30 | "background", 31 | "controls", 32 | "links", 33 | "msw", 34 | "source", 35 | "width", 36 | ], 37 | }, 38 | { 39 | type: "category", 40 | label: "Recipes", 41 | items: ["nextjs"], 42 | }, 43 | { 44 | type: "category", 45 | label: "Configuration", 46 | items: ["cli", "config", "programmatic", "babel", "http2"], 47 | }, 48 | { 49 | type: "category", 50 | label: "Help", 51 | items: [ 52 | "troubleshooting", 53 | { 54 | type: "link", 55 | label: "Contributing", 56 | href: "https://github.com/tajo/ladle/blob/main/CONTRIBUTING.md", 57 | }, 58 | ], 59 | }, 60 | ], 61 | }; 62 | -------------------------------------------------------------------------------- /packages/website/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable docusaurus/copyright-header */ 2 | /** 3 | * Any CSS included here will be global. The classic template 4 | * bundles Infima by default. Infima is a CSS framework designed to 5 | * work well for content-centric websites. 6 | */ 7 | 8 | /* You can override the default Infima variables here. */ 9 | :root { 10 | --ifm-color-primary: #174291; 11 | --ifm-color-primary-dark: rgb(33, 175, 144); 12 | --ifm-color-primary-darker: rgb(31, 165, 136); 13 | --ifm-color-primary-darkest: rgb(26, 136, 112); 14 | --ifm-color-primary-light: rgb(70, 203, 174); 15 | --ifm-color-primary-lighter: rgb(102, 212, 189); 16 | --ifm-color-primary-lightest: rgb(146, 224, 208); 17 | --ifm-code-font-size: 95%; 18 | } 19 | 20 | .docusaurus-highlight-code-line { 21 | background-color: rgb(72, 77, 91); 22 | display: block; 23 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 24 | padding: 0 var(--ifm-pre-padding); 25 | } 26 | 27 | .hero .button { 28 | color: #fff !important; 29 | } 30 | 31 | .hero .button:hover { 32 | color: #000 !important; 33 | } 34 | 35 | [data-theme="dark"] .hero { 36 | color: #fff; 37 | } 38 | 39 | [data-theme="dark"] a { 40 | color: #58a6ff; 41 | } 42 | 43 | [data-theme="dark"] a:hover { 44 | color: #58a6ff; 45 | } 46 | .navbar__logo { 47 | margin-right: 32px; 48 | } 49 | 50 | [data-theme="dark"] .navbar__logo > img { 51 | filter: invert(1); 52 | } 53 | 54 | [data-theme="dark"] .navbar__brand > img { 55 | filter: invert(1); 56 | } 57 | 58 | .main-logo { 59 | width: 75px; 60 | filter: invert(1); 61 | margin-right: 12px; 62 | } 63 | 64 | .features_src-pages-styles-module .container { 65 | padding: 1rem 3rem; 66 | } 67 | 68 | .features_src-pages-styles-module h3 { 69 | margin-top: 2rem; 70 | } 71 | -------------------------------------------------------------------------------- /packages/website/src/pages/styles.module.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable docusaurus/copyright-header */ 2 | 3 | /** 4 | * CSS files with the .module.css suffix will be treated as CSS modules 5 | * and scoped locally. 6 | */ 7 | 8 | .heroBanner { 9 | padding: 4rem 0; 10 | text-align: center; 11 | position: relative; 12 | overflow: hidden; 13 | } 14 | 15 | @media screen and (max-width: 966px) { 16 | .heroBanner { 17 | padding: 2rem; 18 | } 19 | } 20 | 21 | .buttons { 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | flex-wrap: wrap; 26 | } 27 | 28 | .buttons > a { 29 | margin: 0.5rem; 30 | } 31 | 32 | .features { 33 | display: flex; 34 | align-items: center; 35 | padding: 2rem 0; 36 | width: 100%; 37 | } 38 | 39 | .featureImage { 40 | height: 200px; 41 | width: 200px; 42 | } 43 | -------------------------------------------------------------------------------- /packages/website/src/theme/Footer/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | import React from "react"; 8 | import { useThemeConfig } from "@docusaurus/theme-common"; 9 | import FooterLinks from "@theme/Footer/Links"; 10 | import FooterLogo from "@theme/Footer/Logo"; 11 | import FooterCopyright from "@theme/Footer/Copyright"; 12 | import FooterLayout from "@theme/Footer/Layout"; 13 | 14 | function Footer() { 15 | const { footer } = useThemeConfig(); 16 | 17 | if (!footer) { 18 | return null; 19 | } 20 | 21 | const { copyright, links, logo, style } = footer; 22 | return ( 23 | 0 && } 26 | logo={logo && } 27 | copyright={copyright && } 28 | /> 29 | ); 30 | } 31 | 32 | export default React.memo(Footer); 33 | -------------------------------------------------------------------------------- /packages/website/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tajo/ladle/c04af01a01e5c7222a5cff4b5a8bdaae9f112cf6/packages/website/static/.nojekyll -------------------------------------------------------------------------------- /packages/website/static/img/a11y.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tajo/ladle/c04af01a01e5c7222a5cff4b5a8bdaae9f112cf6/packages/website/static/img/a11y.png -------------------------------------------------------------------------------- /packages/website/static/img/actions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tajo/ladle/c04af01a01e5c7222a5cff4b5a8bdaae9f112cf6/packages/website/static/img/actions.png -------------------------------------------------------------------------------- /packages/website/static/img/build-times.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tajo/ladle/c04af01a01e5c7222a5cff4b5a8bdaae9f112cf6/packages/website/static/img/build-times.png -------------------------------------------------------------------------------- /packages/website/static/img/controls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tajo/ladle/c04af01a01e5c7222a5cff4b5a8bdaae9f112cf6/packages/website/static/img/controls.png -------------------------------------------------------------------------------- /packages/website/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tajo/ladle/c04af01a01e5c7222a5cff4b5a8bdaae9f112cf6/packages/website/static/img/favicon.ico -------------------------------------------------------------------------------- /packages/website/static/img/favicon.svg: -------------------------------------------------------------------------------- 1 | 9 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /packages/website/static/img/hotkeys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tajo/ladle/c04af01a01e5c7222a5cff4b5a8bdaae9f112cf6/packages/website/static/img/hotkeys.png -------------------------------------------------------------------------------- /packages/website/static/img/ladle-baseweb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tajo/ladle/c04af01a01e5c7222a5cff4b5a8bdaae9f112cf6/packages/website/static/img/ladle-baseweb.png -------------------------------------------------------------------------------- /packages/website/static/img/logo-gray.svg: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/website/static/img/logo.svg: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/website/static/img/story-navigation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tajo/ladle/c04af01a01e5c7222a5cff4b5a8bdaae9f112cf6/packages/website/static/img/story-navigation.png -------------------------------------------------------------------------------- /packages/website/static/img/story-source.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tajo/ladle/c04af01a01e5c7222a5cff4b5a8bdaae9f112cf6/packages/website/static/img/story-source.png -------------------------------------------------------------------------------- /packages/website/static/img/width.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tajo/ladle/c04af01a01e5c7222a5cff4b5a8bdaae9f112cf6/packages/website/static/img/width.png -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/example' 3 | - 'packages/ladle' 4 | - 'packages/website' 5 | - 'e2e/addons' 6 | - 'e2e/babel' 7 | - 'e2e/commonjs' 8 | - 'e2e/config' 9 | - 'e2e/config-ts' 10 | - 'e2e/css' 11 | - 'e2e/decorators' 12 | - 'e2e/playwright' 13 | - 'e2e/playwright-config' 14 | - 'e2e/programmatic' 15 | - 'e2e/provider' 16 | - 'e2e/baseweb' 17 | - 'e2e/msw' 18 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | echo "Running release" 6 | 7 | echo "Build @ladle/react" 8 | turbo run build --filter=@ladle/react 9 | 10 | echo "Update package.json" 11 | node ./packages/ladle/scripts/update-package-types.js 12 | 13 | echo "Generate Types": 14 | pnpm --filter @ladle/react types 15 | 16 | echo "Changeset publish" 17 | changeset publish 18 | 19 | echo "Revert package.json" 20 | node ./packages/ladle/scripts/revert-package-types.js 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "esnext", 5 | "module": "esnext", 6 | "lib": ["esnext", "dom"], 7 | "allowJs": true, 8 | "checkJs": true, 9 | "jsx": "react-jsx", 10 | "declaration": false, 11 | "declarationMap": false, 12 | "sourceMap": false, 13 | "noEmit": true, 14 | "strict": true, 15 | "noImplicitAny": true, 16 | "noImplicitThis": true, 17 | "alwaysStrict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noImplicitReturns": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "moduleResolution": "node", 23 | "allowSyntheticDefaultImports": true, 24 | "esModuleInterop": true, 25 | "skipLibCheck": true, 26 | "forceConsistentCasingInFileNames": true, 27 | "isolatedModules": true, 28 | "paths": { 29 | "virtual:generated-list": ["./packages/ladle/types/generated-list.d.ts"], 30 | "@/*": ["./e2e/config-ts/src/to/*"] 31 | } 32 | }, 33 | "include": [ 34 | "packages/*/.ladle/*", 35 | "packages/ladle/lib/app/window.d.ts", 36 | "packages/*/lib/**/*", 37 | "packages/*/src/**/*", 38 | "e2e/*/.ladle/*", 39 | "e2e/*/src/*", 40 | "e2e/*/tests/*", 41 | "type-tests/*" 42 | ], 43 | "exclude": ["packages/website/**", "packages/example/src/button.stories.js"] 44 | } 45 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "tasks": { 4 | "build": { 5 | "dependsOn": ["^build"], 6 | "outputs": ["build/**"] 7 | }, 8 | "test": { 9 | "dependsOn": ["^build"], 10 | "outputs": [] 11 | } 12 | }, 13 | "globalDependencies": ["tsconfig.json"] 14 | } 15 | -------------------------------------------------------------------------------- /type-tests/argTypes.test.tsx: -------------------------------------------------------------------------------- 1 | import type { Story } from "../packages/ladle/lib/app/exports"; 2 | 3 | type FooProps = { 4 | foo: string; 5 | bar: number; 6 | }; 7 | 8 | function Foo({ foo, bar }: FooProps) { 9 | return
    {foo + bar}
    ; 10 | } 11 | 12 | export const ArgTypes: Story = Foo; 13 | 14 | ArgTypes.argTypes = { 15 | foo: { 16 | control: { 17 | type: "select", 18 | options: ["foo", 5], 19 | }, 20 | }, 21 | bar: { 22 | control: { 23 | type: "select", 24 | options: [1, 2, 3], 25 | }, 26 | }, 27 | }; 28 | 29 | export const ArgTypes2: Story = Foo; 30 | 31 | ArgTypes2.argTypes = { 32 | foo: { 33 | control: { 34 | type: "select", 35 | options: ["foo", "bar"], 36 | }, 37 | }, 38 | bar: { 39 | control: { 40 | type: "select", 41 | options: [1, 2, 3], 42 | }, 43 | }, 44 | // @ts-expect-error - baz is not a valid arg 45 | baz: { 46 | control: {}, 47 | }, 48 | }; 49 | 50 | export const ArgTypes3: Story = Foo; 51 | 52 | ArgTypes3.argTypes = { 53 | foo: { 54 | control: { 55 | // @ts-expect-error - type is not a valid control type 56 | type: "potato", 57 | }, 58 | }, 59 | }; 60 | 61 | export const ArgTypes4: Story = ({ foo, bar }) => { 62 | return
    {foo + bar}
    ; 63 | }; 64 | 65 | ArgTypes4.argTypes = { 66 | foo: { 67 | control: { 68 | type: "select", 69 | options: ["foo", "bar"], 70 | }, 71 | // @ts-expect-error - defaultValue is not a string 72 | defaultValue: 5, 73 | }, 74 | }; 75 | -------------------------------------------------------------------------------- /type-tests/args.test.tsx: -------------------------------------------------------------------------------- 1 | import type { Story } from "../packages/ladle/lib/app/exports"; 2 | 3 | type FooProps = { 4 | foo: string; 5 | bar: number; 6 | }; 7 | 8 | function Foo({ foo, bar }: FooProps) { 9 | return
    {foo + bar}
    ; 10 | } 11 | 12 | export const Args: Story = Foo; 13 | 14 | Args.args = { 15 | foo: "foo", 16 | // @ts-expect-error - bar is not a string 17 | bar: "1", 18 | }; 19 | 20 | export const Args2: Story = Foo; 21 | 22 | Args2.args = { 23 | foo: "foo", 24 | bar: 1, 25 | // @ts-expect-error - baz is not a valid arg 26 | baz: "baz", 27 | }; 28 | -------------------------------------------------------------------------------- /type-tests/decorators.test.tsx: -------------------------------------------------------------------------------- 1 | import type { Story, StoryDecorator } from "../packages/ladle/lib/app/exports"; 2 | 3 | type FooProps = { 4 | foo: string; 5 | bar: number; 6 | }; 7 | 8 | function Foo({ foo, bar }: FooProps) { 9 | return
    {foo + bar}
    ; 10 | } 11 | 12 | const decorator: StoryDecorator = (Component) => ( 13 |
    14 | 15 |
    16 | ); 17 | 18 | export const Decorators: Story = Foo; 19 | 20 | Decorators.decorators = [decorator]; 21 | 22 | const decoratorWithValidProps: StoryDecorator = (Component) => ( 23 |
    24 | 25 |
    26 | ); 27 | 28 | export const DecoratorsWithOptionalProps: Story = Foo; 29 | 30 | DecoratorsWithOptionalProps.decorators = [decoratorWithValidProps]; 31 | 32 | const decoratorWithInvalidProps: StoryDecorator = (Component) => ( 33 |
    34 | {/* @ts-expect-error - 5 is a number, foo is a string */} 35 | 36 |
    37 | ); 38 | 39 | export const DecoratorsWithInvalidProps: Story = Foo; 40 | 41 | DecoratorsWithInvalidProps.decorators = [decoratorWithInvalidProps]; 42 | 43 | const decoratorWithNoProps: StoryDecorator = (Component) => ( 44 |
    45 | 46 |
    47 | ); 48 | 49 | export const DecoratorsWithNoProps: Story = Foo; 50 | 51 | Decorators.decorators = [decoratorWithNoProps]; 52 | -------------------------------------------------------------------------------- /type-tests/meta.test.tsx: -------------------------------------------------------------------------------- 1 | import type { Story } from "../packages/ladle/lib/app/exports"; 2 | 3 | type FooProps = { 4 | foo: string; 5 | bar: number; 6 | }; 7 | 8 | function Foo({ foo, bar }: FooProps) { 9 | return
    {foo + bar}
    ; 10 | } 11 | 12 | export const Meta: Story = Foo; 13 | 14 | // @ts-expect-error - meta is not a valid arg 15 | Meta.meta = "foo"; 16 | 17 | export const Meta2: Story = Foo; 18 | 19 | Meta2.meta = {}; 20 | --------------------------------------------------------------------------------