├── .gitattributes ├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── actions │ ├── check-quality │ │ └── action.yml │ └── setup-project │ │ └── action.yml └── workflows │ ├── deploy-pages.yml │ ├── quality.yml │ └── update-deps.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── biome.json ├── examples └── react │ ├── .storylite │ ├── canvas.tsx │ ├── config.tsx │ └── index.tsx │ ├── README.md │ ├── canvas.html │ ├── index.html │ ├── package.json │ ├── public │ └── logo.svg │ ├── src │ ├── components │ │ ├── ForwardRefButton.tsx │ │ └── LinkableBtn.tsx │ └── styles │ │ ├── components.css │ │ ├── storylite-iframe.css │ │ └── storylite-ui.css │ ├── stories │ ├── buttons.stories.tsx │ ├── forwardref.stories.tsx │ └── index.stories.tsx │ ├── tsconfig.json │ └── vite.config.ts ├── jest.config.js ├── jest.env-browser.js ├── lerna.json ├── nx.json ├── package.json ├── packages ├── docs │ ├── .storylite │ │ ├── canvas.tsx │ │ ├── config.tsx │ │ └── index.tsx │ ├── README.md │ ├── canvas.html │ ├── index.html │ ├── package.json │ ├── public │ │ └── logo.svg │ ├── src │ │ ├── components │ │ │ └── LinkableBtn.tsx │ │ └── styles │ │ │ ├── components.css │ │ │ ├── storylite-iframe.css │ │ │ └── storylite-ui.css │ ├── stories │ │ ├── buttons.stories.tsx │ │ └── index.stories.mdx │ ├── tsconfig.json │ └── vite.config.ts ├── storylite │ ├── .gitignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── assets │ │ ├── logo.svg │ │ └── lucide │ │ │ ├── LICENSE │ │ │ └── svg │ │ │ ├── bookmark.svg │ │ │ ├── box-select.svg │ │ │ ├── box.svg │ │ │ ├── contrast.svg │ │ │ ├── expand.svg │ │ │ ├── external-link.svg │ │ │ ├── github.svg │ │ │ ├── grid-3x3.svg │ │ │ ├── minus-square.svg │ │ │ ├── monitor-smartphone.svg │ │ │ ├── moon.svg │ │ │ ├── plus-square.svg │ │ │ ├── sun.svg │ │ │ ├── x-circle.svg │ │ │ └── zap.svg │ ├── declaration.d.ts │ ├── package.json │ ├── screenshot.png │ ├── src │ │ ├── app │ │ │ ├── index.test.tsx │ │ │ ├── index.tsx │ │ │ ├── renderApp.tsx │ │ │ ├── routes.ts │ │ │ └── stores │ │ │ │ ├── DefaultTitle.tsx │ │ │ │ ├── global.ts │ │ │ │ └── global.types.ts │ │ ├── components │ │ │ ├── Btn.tsx │ │ │ ├── InlineHtml.tsx │ │ │ ├── Story.tsx │ │ │ ├── addons │ │ │ │ └── getToolbarAddons.tsx │ │ │ ├── canvas │ │ │ │ ├── CanvasIframe.tsx │ │ │ │ ├── CanvasIframeBody.tsx │ │ │ │ └── CanvasRoot.tsx │ │ │ ├── layouts │ │ │ │ ├── ErrorLayout.tsx │ │ │ │ ├── IframeLayout.tsx │ │ │ │ └── TopFrameLayout.tsx │ │ │ ├── sidebar │ │ │ │ ├── Sidebar.tsx │ │ │ │ └── SidebarTitle.tsx │ │ │ └── toolbar │ │ │ │ ├── Toolbar.tsx │ │ │ │ └── ToolbarAddon.tsx │ │ ├── hooks │ │ │ └── useDetectTheme.ts │ │ ├── index.ts │ │ ├── pages │ │ │ ├── 404.tsx │ │ │ ├── index.tsx │ │ │ ├── preview │ │ │ │ ├── index.tsx │ │ │ │ └── story.tsx │ │ │ └── stories │ │ │ │ └── story.tsx │ │ ├── services │ │ │ ├── csf-api │ │ │ │ └── navigation.tsx │ │ │ ├── messenger │ │ │ │ ├── index.ts │ │ │ │ ├── useWindowMessenger.ts │ │ │ │ ├── windowMessaging.ts │ │ │ │ └── windowMessaging.types.ts │ │ │ ├── renderer │ │ │ │ └── react.tsx │ │ │ ├── router │ │ │ │ ├── createStoryLiteRouter.tsx │ │ │ │ ├── getStoryLiteBasePath.ts │ │ │ │ ├── index.ts │ │ │ │ ├── router.class.ts │ │ │ │ ├── router.component.tsx │ │ │ │ ├── router.parser.test.tsx │ │ │ │ ├── router.parser.ts │ │ │ │ ├── router.store.tsx │ │ │ │ └── router.types.ts │ │ │ └── storage │ │ │ │ └── localStorage.ts │ │ ├── styles │ │ │ ├── addons │ │ │ │ ├── grid.css │ │ │ │ ├── maximize.css │ │ │ │ ├── outline.css │ │ │ │ └── responsive.css │ │ │ ├── base.css │ │ │ ├── components │ │ │ │ ├── btn.css │ │ │ │ ├── sidebar.css │ │ │ │ ├── stories.css │ │ │ │ └── toolbar.css │ │ │ ├── dark.css │ │ │ ├── iframe.css │ │ │ ├── index.css │ │ │ ├── layouts.css │ │ │ ├── mobile.css │ │ │ └── variables.css │ │ ├── types │ │ │ ├── components.ts │ │ │ ├── config.ts │ │ │ ├── core.ts │ │ │ ├── index.ts │ │ │ ├── parameters.ts │ │ │ ├── story.ts │ │ │ └── ui.ts │ │ └── utility │ │ │ ├── index.ts │ │ │ └── parametersToDataProps.ts │ ├── tsconfig.json │ └── tsup.config.ts └── vite-plugin │ ├── .gitignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ ├── index.ts │ ├── plugin.ts │ ├── story-collector.ts │ ├── types.ts │ └── virtual-modules.d.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── tsconfig.json /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @itsjavi 2 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a 6 | harassment-free experience for everyone, regardless of age, body size, visible or invisible 7 | disability, ethnicity, sex characteristics, gender identity and expression, level of experience, 8 | education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or 9 | sexual identity and orientation. 10 | 11 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and 12 | healthy community. 13 | 14 | ## Our Standards 15 | 16 | Examples of behavior that contributes to a positive environment for our community include: 17 | 18 | - Demonstrating empathy and kindness toward other people 19 | - Being respectful of differing opinions, viewpoints, and experiences 20 | - Giving and gracefully accepting constructive feedback 21 | - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the 22 | experience 23 | - Focusing on what is best not just for us as individuals, but for the overall community 24 | 25 | Examples of unacceptable behavior include: 26 | 27 | - The use of sexualized language or imagery, and sexual attention or advances of any kind 28 | - Trolling, insulting or derogatory comments, and personal or political attacks 29 | - Public or private harassment 30 | - Publishing others' private information, such as a physical or email address, without their 31 | explicit permission 32 | - Other conduct which could reasonably be considered inappropriate in a professional setting 33 | 34 | ## Enforcement Responsibilities 35 | 36 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior 37 | and will take appropriate and fair corrective action in response to any behavior that they deem 38 | inappropriate, threatening, offensive, or harmful. 39 | 40 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, 41 | code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and 42 | will communicate reasons for moderation decisions when appropriate. 43 | 44 | ## Scope 45 | 46 | This Code of Conduct applies within all community spaces, and also applies when an individual is 47 | officially representing the community in public spaces. Examples of representing our community 48 | include using an official e-mail address, posting via an official social media account, or acting as 49 | an appointed representative at an online or offline event. 50 | 51 | ## Enforcement 52 | 53 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community 54 | leaders responsible for enforcement via private message on Twitter/X to @itsjavidotcom. 55 | 56 | All complaints will be reviewed and investigated promptly and fairly. 57 | 58 | All community leaders are obligated to respect the privacy and security of the reporter of any 59 | incident. 60 | 61 | ## Enforcement Guidelines 62 | 63 | Community leaders will follow these Community Impact Guidelines in determining the consequences for 64 | any action they deem in violation of this Code of Conduct: 65 | 66 | ### 1. Correction 67 | 68 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or 69 | unwelcome in the community. 70 | 71 | **Consequence**: A private, written warning from community leaders, providing clarity around the 72 | nature of the violation and an explanation of why the behavior was inappropriate. A public apology 73 | may be requested. 74 | 75 | ### 2. Warning 76 | 77 | **Community Impact**: A violation through a single incident or series of actions. 78 | 79 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people 80 | involved, including unsolicited interaction with those enforcing the Code of Conduct, for a 81 | specified period of time. This includes avoiding interactions in community spaces as well as 82 | external channels like social media. Violating these terms may lead to a temporary or permanent ban. 83 | 84 | ### 3. Temporary Ban 85 | 86 | **Community Impact**: A serious violation of community standards, including sustained inappropriate 87 | behavior. 88 | 89 | **Consequence**: A temporary ban from any sort of interaction or public communication with the 90 | community for a specified period of time. No public or private interaction with the people involved, 91 | including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this 92 | period. Violating these terms may lead to a permanent ban. 93 | 94 | ### 4. Permanent Ban 95 | 96 | **Community Impact**: Demonstrating a pattern of violation of community standards, including 97 | sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement 98 | of classes of individuals. 99 | 100 | **Consequence**: A permanent ban from any sort of public interaction within the community. 101 | 102 | ## Attribution 103 | 104 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at 105 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 106 | 107 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement 108 | ladder][Mozilla CoC]. 109 | 110 | For answers to common questions about this code of conduct, see the FAQ at 111 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 112 | [https://www.contributor-covenant.org/translations][translations]. 113 | 114 | [homepage]: https://www.contributor-covenant.org 115 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 116 | [Mozilla CoC]: https://github.com/mozilla/diversity 117 | [FAQ]: https://www.contributor-covenant.org/faq 118 | [translations]: https://www.contributor-covenant.org/translations 119 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [itsjavi] 4 | ko_fi: itsjavi 5 | custom: ['https://paypal.me/itsjavidotcom/5', 'https://paypal.me/itsjavidotcom/10'] 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve this project 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://stackblitz.com/) or similar. Your 16 | issue will get much higher priority since it can be triaged efficiently. 17 | 18 | Alternatively, create a repository reproducing the issue. 19 | 20 | **Environment** 21 | 22 | - OS with version: [e.g. iOS 15.2] 23 | - Browser with version [e.g. Chrome 101] 24 | - JS Runtime with version: [e.g. NodeJS 18.5] 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: needs triage 6 | assignees: '' 7 | --- 8 | 9 | **Description** 10 | 11 | **Is your feature request related to a problem with an existing feature? Please describe.** A clear 12 | and concise description of what the problem is, e.g.: I'd like that feature X allows me to [...]" 13 | 14 | **Is your feature request related to an improvement? Please describe.** A clear and concise 15 | description of the new feature, e.g. "I'd like to be able to [...]" 16 | 17 | **Desired Behaviour** 18 | 19 | Describe the solution you'd like. A clear and concise description of what you want to happen, and 20 | how you would like the feature to work. 21 | 22 | **Considered Alternatives** 23 | 24 | Describe alternatives you've considered. A clear and concise description of any alternative 25 | solutions or features you've considered. 26 | 27 | **Additional context** 28 | 29 | Add any other context or screenshots about the feature request here. 30 | -------------------------------------------------------------------------------- /.github/actions/check-quality/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Check code quality' 2 | description: 'Checks code quality: formatter, linters, build and test' 3 | runs: 4 | using: 'composite' 5 | steps: 6 | - shell: bash 7 | run: pnpm type-check 8 | 9 | - shell: bash 10 | run: pnpm lint 11 | 12 | - shell: bash 13 | run: pnpm build 14 | 15 | - shell: bash 16 | run: pnpm publint 17 | 18 | - shell: bash 19 | run: pnpm test:ci 20 | -------------------------------------------------------------------------------- /.github/actions/setup-project/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Setup project' 2 | description: 'Sets up project and its dependencies' 3 | runs: 4 | using: 'composite' 5 | steps: 6 | - uses: pnpm/action-setup@v3 7 | with: 8 | version: ${{ env.PNPM_VERSION }} 9 | run_install: false 10 | 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: ${{ env.NODE_VERSION }} 14 | # registry-url: https://npm.pkg.github.com 15 | cache: 'pnpm' 16 | cache-dependency-path: './pnpm-lock.yaml' 17 | 18 | - shell: bash 19 | run: pnpm install --frozen-lockfile 20 | -------------------------------------------------------------------------------- /.github/workflows/deploy-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Github Pages 2 | 3 | env: 4 | NODE_VERSION: '>=20.5.0' 5 | PNPM_VERSION: 8.8 6 | 7 | on: 8 | workflow_dispatch: 9 | push: 10 | branches: ['main'] 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: 'pages' 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Build job 26 | build: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v4 30 | 31 | - name: 'Setup project' 32 | uses: ./.github/actions/setup-project 33 | 34 | - name: 'Check code quality' 35 | uses: ./.github/actions/check-quality 36 | 37 | - name: Setup Pages 38 | uses: actions/configure-pages@v4 39 | 40 | - name: Build with Vite 41 | run: | 42 | pnpm -r run build 43 | ls -la ./packages/docs/dist 44 | echo "Moving dist to _site" 45 | rm -rf _site && mv ./packages/docs/dist ./_site 46 | ls -la ./_site 47 | 48 | - name: Upload artifact 49 | uses: actions/upload-pages-artifact@v2 50 | 51 | # Deployment job 52 | deploy: 53 | environment: 54 | name: github-pages 55 | url: ${{ steps.deployment.outputs.page_url }} 56 | runs-on: ubuntu-latest 57 | needs: build 58 | steps: 59 | - name: Deploy to GitHub Pages 60 | id: deployment 61 | uses: actions/deploy-pages@v2 62 | -------------------------------------------------------------------------------- /.github/workflows/quality.yml: -------------------------------------------------------------------------------- 1 | name: Quality Checks 2 | 3 | env: 4 | NODE_VERSION: '>=20.5.0' 5 | PNPM_VERSION: 8.8 6 | 7 | on: 8 | push: 9 | branches: 10 | - 'main' 11 | pull_request: 12 | branches: 13 | - main 14 | 15 | concurrency: 16 | group: checkCodeQuality-${{ github.ref }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | checkCodeQuality: 21 | name: 'Lint, Build & Test' 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | 26 | - name: 'Setup project' 27 | uses: ./.github/actions/setup-project 28 | 29 | - name: 'Check code quality' 30 | uses: ./.github/actions/check-quality 31 | 32 | - name: Upload coverage reports to Codecov 33 | uses: codecov/codecov-action@v4 34 | with: 35 | directory: ./ 36 | token: ${{ secrets.CODECOV_TOKEN }} 37 | -------------------------------------------------------------------------------- /.github/workflows/update-deps.yml: -------------------------------------------------------------------------------- 1 | name: Update dependencies 2 | 3 | env: 4 | NODE_VERSION: '>=20.5.0' 5 | PNPM_VERSION: 8.8 6 | 7 | on: 8 | schedule: 9 | - cron: 0 16 * * * # every day at 16pm 10 | workflow_dispatch: 11 | 12 | concurrency: 13 | group: updateDependencies-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | updateDependencies: 18 | name: 'Update dependencies' 19 | runs-on: ubuntu-latest 20 | outputs: 21 | has_changes: ${{ steps.git_diff.outputs.has_changes }} 22 | steps: 23 | - uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 # fetch all history so that git diff can compare against HEAD~1 26 | 27 | - name: 'Setup project' 28 | uses: ./.github/actions/setup-project 29 | 30 | - name: 'Update all dependencies' 31 | run: pnpm -r update --latest 32 | 33 | - name: 'Check code quality' 34 | uses: ./.github/actions/check-quality 35 | 36 | - name: Compare Changes 37 | id: git_diff 38 | run: | 39 | git status 40 | changes=$(git diff --name-only HEAD~0) 41 | echo "Changed Files: $changes" 42 | if [[ -n "$changes" ]]; then 43 | echo "has_changes=yes" >> "$GITHUB_OUTPUT" 44 | else 45 | echo "has_changes=" >> "$GITHUB_OUTPUT" 46 | fi 47 | 48 | - name: Install jq 49 | if: ${{ steps.git_diff.outputs.has_changes == 'yes' }} 50 | run: | 51 | sudo apt-get update 52 | sudo apt-get install -y jq 53 | 54 | - name: Check for existing PR 55 | id: check_open_pr 56 | if: ${{ steps.git_diff.outputs.has_changes == 'yes' }} 57 | env: 58 | GH_TOKEN: ${{ github.token }} 59 | run: | 60 | pr_number=$(gh pr list --state open --label "update-deps" --json number --jq '.[0].number') 61 | echo "pr_number=${pr_number:-0}" >> "$GITHUB_OUTPUT" 62 | 63 | - name: Create PR if not exists 64 | id: create_pr 65 | if: 66 | ${{ steps.git_diff.outputs.has_changes == 'yes' && steps.check_open_pr.outputs.pr_number 67 | == 0 }} 68 | env: 69 | GH_TOKEN: ${{ github.token }} # make sure your GITHUB_TOKEN has PR creation permissions under "Workflow Permissions" of your repo Actions settings 70 | run: | 71 | # Prepare git 72 | git config --global user.email "noreply@github.com" 73 | git config --global user.name "Github Actions" 74 | git branch -u origin/main main 75 | # Create branch, commit and PR 76 | branch_name="update-deps/$(date +'%Y%m%d-%H%M%S')" 77 | git checkout -b $branch_name 78 | git add -A 79 | git commit -m "chore(deps): update to latest versions" 80 | git push origin $branch_name 81 | gh pr create --title "chore: update dependencies 🪄" --base main --head $branch_name --label "update-deps" \ 82 | --body "This PR updates all package dependencies to their latest version. Please review the changes and merge if everything looks good." 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | .pnpm-store 8 | 9 | # testing 10 | coverage 11 | .coverage 12 | 13 | # misc 14 | .DS_Store 15 | *.pem 16 | 17 | # debug 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | .pnpm-debug.log* 22 | 23 | # local env files 24 | .env.local 25 | .env.development.local 26 | .env.test.local 27 | .env.production.local 28 | 29 | # caches 30 | .turbo/ 31 | .next/ 32 | .cache/ 33 | .eslintcache 34 | v8-compile-cache-* 35 | .pnpm-store/ 36 | 37 | # builds 38 | dist 39 | build 40 | *.tsbuildinfo 41 | 42 | # other 43 | .vercel/ 44 | .local/ 45 | .history/ 46 | 47 | .vscode/pinned-files.json 48 | .nx/ 49 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["biomejs.biome"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.wordWrapColumn": 120, 3 | "editor.rulers": [120], 4 | "editor.fontSize": 14, 5 | "files.autoSave": "onFocusChange", 6 | "testing.automaticallyOpenPeekView": "failureInVisibleDocument", 7 | "files.exclude": { 8 | "*/*/node_modules": false, 9 | "**/.git": true, 10 | "**/.svn": true, 11 | "**/.hg": true, 12 | "**/CVS": true, 13 | "**/.DS_Store": true, 14 | "**/Thumbs.db": true, 15 | "**/.turbo": true, 16 | "**/.vercel": true, 17 | "**/dist": false 18 | }, 19 | "editor.codeActionsOnSave": { 20 | "source.addMissingImports": "explicit", 21 | "source.organizeImports.biome": "explicit", 22 | "quickfix.biome": "explicit" 23 | }, 24 | "[markdown]": { 25 | "editor.defaultFormatter": "esbenp.prettier-vscode", 26 | "editor.fontSize": 14, 27 | "editor.lineHeight": 18, 28 | "editor.wordWrap": "off", 29 | "editor.wordWrapColumn": 100, 30 | "editor.lineNumbers": "off", 31 | "editor.quickSuggestions": { 32 | "comments": "off", 33 | "strings": "on", 34 | "other": "on" 35 | }, 36 | "editor.minimap.enabled": true 37 | }, 38 | "[json]": { 39 | "editor.defaultFormatter": "vscode.json-language-features" 40 | }, 41 | "[javascript]": { 42 | "editor.defaultFormatter": "biomejs.biome" 43 | }, 44 | "[javascriptreact]": { 45 | "editor.defaultFormatter": "biomejs.biome" 46 | }, 47 | "[typescript]": { 48 | "editor.defaultFormatter": "biomejs.biome" 49 | }, 50 | "[typescriptreact]": { 51 | "editor.defaultFormatter": "biomejs.biome" 52 | }, 53 | "[css]": { 54 | "editor.defaultFormatter": "vscode.css-language-features" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | Thanks for your interest to contribute to this project. Please take a moment and read through this 4 | guide: 5 | 6 | ## Repository 7 | 8 | - We use Node v18 or v20 and the `pnpm` package manager. 9 | - This project is a monorepo using `pnpm` workspaces. 10 | - We use [Convention Commits](https://www.conventionalcommits.org/en/v1.0.0/) for our commit 11 | messages. 12 | 13 | ## Developing 14 | 15 | The different packages can be found in `packages/*`, and that's where you'll be mainly working. 16 | 17 | ### Quick Start 18 | 19 | Here are the basic commands you'll need to get started: 20 | 21 | ```sh 22 | 23 | # Install dependencies 24 | pnpm install 25 | 26 | # Start the dev server 27 | pnpm dev 28 | 29 | # Build dist files 30 | pnpm build 31 | 32 | # Run tests 33 | pnpm test 34 | 35 | # Run tests with coverage 36 | pnpm test:coverage 37 | 38 | # Lint 39 | pnpm lint 40 | 41 | # Format (format + lint --fix) 42 | pnpm format 43 | 44 | # Type check 45 | pnpm type-check 46 | 47 | # Run all quality checks (formatter-check, type-check, lint, build, publint, test) 48 | pnpm quality-checks 49 | 50 | ``` 51 | 52 | ## Testing 53 | 54 | We use `jest` to run tests. You can run all tests with: 55 | 56 | ```sh 57 | pnpm test 58 | ``` 59 | 60 | Tests ending with `*.test.tsx` or `*.dom.test.ts` are considered browser tests and will be run in a 61 | browser-like environment using `jsdom`. 62 | 63 | Tests ending with `*.test.ts` (except `*.dom.test.ts`) are considered universal tests and will be 64 | run in both `node` and `jsdom` environments. 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Javi Aguilar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StoryLite 2 | 3 | > **Project Status** (2025-02-01): I currently have no buffer to work on this project. If you'd like to contribute, feel free to open a pull request. Thank you for your understanding! 4 | 5 | 6 | 7 |

8 | npm package 9 | bundlephobia 10 | bundlephobia
11 | build status 12 | code coverage 13 | homepage 14 |

15 | 16 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/edit/storylite-demo?file=stories/index.stories.tsx) 17 | 18 | StoryLite is a modern and lightweight toolkit for crafting and managing design systems and 19 | React components with ease. Inspired by the popular StoryBook UI and powered by Vite⚡️, StoryLite offers 20 | a streamlined and familiar developer experience, thanks to the story format compatible with 21 | [Component Story Format (CSF) 3.0](https://storybook.js.org/docs/react/api/csf). 22 | 23 | With StoryLite, you can quickly create, test, and refine React-based UI components in isolation, ensuring that 24 | your application maintains a consistent look and feel. 25 | 26 | Tailored for smaller React projects that crave simplicity without the overhead of a full StoryBook setup, 27 | StoryLite provides an intuitive UI that's customizable to your unique needs. 28 | 29 | ![StoryLite](https://raw.githubusercontent.com/itsjavi/storylite/main/packages/storylite/screenshot.png) 30 | 31 | ## Features 32 | 33 | - **Lightweight** (36 KB minified, 10KB minified + gzipped), with few dependencies, specially 34 | tailored for Single-Page Apps. 35 | - **Mobile-friendly** UI 36 | - **Full control**: You decide where to mount the StoryLite React app and how to run it with plain 37 | Vite. No custom servers or other uncontrolled moving parts. 38 | - Fully **customizable**: UI styles, iframe styles, addons, etc. 39 | - Fully **isolated**: The canvas iframe lands with no styles by default. You control what styles to 40 | load. 41 | - **Interoperable with StoryBook**'s CSF 3.0 format: With some minimal changes, you can almost 42 | instantly make your StoryBook stories work with StoryLite when they follow the Component Story 43 | Format. 44 | - **Render modes** via `renderFrame`: You can decide wether to render your story inside the iframe 45 | (`iframe`), or outside in the same DOM tree as the UI (`root`). This is useful, e.g. for search 46 | engines to be able to index the content. 47 | - **Built-in Addons** with the basics: dark mode, mobile view, grid, outline, maximize, open in new 48 | tab, etc. 49 | - **HMR (Hot Module Reload)** support when story files (or any module they use) change. 50 | - **SSG (Static Site Generation)** support thanks to Vite. 51 | - **Markdown and MDX** support via Vite plugins. 52 | 53 | ## Installation 54 | 55 | To install StoryLite, simply run one of the following commands in the project where you want to 56 | define the stories: 57 | 58 | ```bash 59 | # Using npm 60 | npm install -D @storylite/storylite vite @storylite/vite-plugin 61 | 62 | # Using yarn 63 | yarn add -D @storylite/storylite vite @storylite/vite-plugin 64 | 65 | # Using pnpm 66 | pnpm add -D @storylite/storylite vite @storylite/vite-plugin 67 | ``` 68 | 69 | For the next steps, please check the 70 | [example React](https://github.com/itsjavi/storylite/tree/main/examples/react) directory to 71 | learn how to integrate it in your project. 72 | 73 | > While all examples here show how to integrate it with Vite, StoryLite can be used with any bundler 74 | > since it is a client-side React app that can be mounted anywhere in your project. 75 | 76 | ### Adding MDX support 77 | 78 | Check the `docs` package to see how to add MDX support to your project: 79 | 80 | - [vite.config.ts](https://github.com/itsjavi/storylite/tree/main/packages/docs/vite.config.ts) 81 | - [stories/index.stories.mdx](https://github.com/itsjavi/storylite/tree/main/packages/docs/stories/index.stories.mdx) 82 | 83 | With this setup you can: 84 | 85 | - Import modules from JS and JSX files. 86 | - Define the default story metadata in the MDX's frontmatter block. The body of the MDX will be used 87 | as the default story component. 88 | - You can import other MD/MDX files in your stories, they will be ready to be used as JSX 89 | components. 90 | - You can export JSX components to define new story variants. 91 | 92 | You currently can't: 93 | 94 | - You cannot import TS/TSX files in your MDXs. This is a limitation of Vite's MDX plugin. 95 | - You currently cannot export Story objects, instead you can only export JSX components. 96 | 97 | ## Current Focus and Future 98 | 99 | While StoryLite is geared towards React components at the moment, the potential exists for broader 100 | compatibility with other frameworks supported by Vite. We're continuously enhancing the tool and 101 | looking to: 102 | 103 | - Better story interoperability with StoryBook. 104 | - Better extensibility and configuration options. 105 | - Expand addons to support multiple resizable viewports, and tools for zoom, accessibility, etc. 106 | - Ability to generate code snippets for each story. 107 | - Ability to edit component props and state in the UI. 108 | - Improve documentation and mobile experience of the UI. 109 | - Full test coverage. 110 | - Study the possibility of supporting other frameworks (Vue, Svelte, Solid, etc.) 111 | 112 | ## Contributing 113 | 114 | Contributions are encouraged; Please check out our 115 | [contributing guidelines](https://github.com/itsjavi/storylite/tree/main/CONTRIBUTING.md) before 116 | submitting a PR. 117 | 118 | ## License 119 | 120 | [MIT License](https://github.com/itsjavi/storylite/tree/main/LICENSE) 121 | 122 | ## Acknowledgements 123 | 124 | Inspired by: 125 | 126 | - [StoryBook](https://storybook.js.org/) 127 | - [Ladle](https://ladle.dev/) 128 | 129 | Built with: 130 | 131 | - [Vite](https://vitejs.dev/) 132 | - [React](https://react.dev/) 133 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.6.4/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "formatter": { 7 | "enabled": true, 8 | "formatWithErrors": false, 9 | "indentStyle": "space", 10 | "indentWidth": 2, 11 | "lineWidth": 120, 12 | "ignore": [ 13 | "!.*.js", 14 | "**/node_modules/**", 15 | "**/.next/**", 16 | "**/.turbo/**", 17 | "**/dist/**", 18 | "packages/*/dist/**", 19 | "**/public/**", 20 | "**/coverage/**", 21 | "**/.coverage/**" 22 | ] 23 | }, 24 | "json": { 25 | "parser": { 26 | "allowComments": true 27 | } 28 | }, 29 | "javascript": { 30 | "formatter": { 31 | "quoteStyle": "single", 32 | "quoteProperties": "asNeeded", 33 | "semicolons": "asNeeded", 34 | "trailingComma": "all" 35 | } 36 | }, 37 | "linter": { 38 | "enabled": true, 39 | "rules": { 40 | "recommended": true, 41 | "a11y": { 42 | "useAltText": "error", 43 | "useKeyWithClickEvents": "off", 44 | "useValidAnchor": "off" 45 | }, 46 | "correctness": { 47 | "noUnusedVariables": "warn", 48 | "noVoidTypeReturn": "off", 49 | "useExhaustiveDependencies": "off" 50 | }, 51 | "complexity": { 52 | "noBannedTypes": "off", 53 | "noForEach": "off" 54 | }, 55 | "security": { 56 | "noDangerouslySetInnerHtml": "off" 57 | }, 58 | "suspicious": { 59 | "noArrayIndexKey": "off", 60 | "noConsoleLog": "off", 61 | "noExplicitAny": "off" 62 | }, 63 | "style": { 64 | "all": false 65 | } 66 | }, 67 | "ignore": [ 68 | "!.*.js", 69 | "**/node_modules/**", 70 | "**/.next/**", 71 | "**/.turbo/**", 72 | "**/dist/**", 73 | "packages/*/dist/**", 74 | "**/public/**", 75 | "**/coverage/**", 76 | "**/.coverage/**" 77 | ] 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /examples/react/.storylite/canvas.tsx: -------------------------------------------------------------------------------- 1 | import '@storylite/storylite/styles.css' 2 | // 3 | import '../src/styles/components.css' 4 | // 5 | import '../src/styles/storylite-iframe.css' 6 | import '../src/styles/storylite-ui.css' 7 | 8 | // import iframe-scope styles here 9 | 10 | import stories from '@storylite/vite-plugin:stories' 11 | import { renderStoryLiteApp } from '@storylite/storylite' 12 | 13 | import config from './config' 14 | 15 | const rootElement = document.getElementById('root') as HTMLElement 16 | 17 | renderStoryLiteApp(rootElement, stories, config) 18 | -------------------------------------------------------------------------------- /examples/react/.storylite/config.tsx: -------------------------------------------------------------------------------- 1 | import type { SLAddonPropsWithoutId, SLAppComponentProps } from '@storylite/storylite' 2 | 3 | const config: Partial = { 4 | title: ' ⚡️ StoryLite React', 5 | defaultStory: 'index-default', 6 | useIframeStyles: false, 7 | iframeProps: { 8 | style: { 9 | // padding: '10px', 10 | }, 11 | }, 12 | addons: [ 13 | // ['id-of-addon-to-exclude', false], 14 | [ 15 | 'custom-addon', 16 | { 17 | defaultContent: 👋, 18 | stateful: false, 19 | onClick: (ctx) => { 20 | console.log('custom-addon context', ctx) 21 | alert('You clicked the custom addon!') 22 | }, 23 | } satisfies SLAddonPropsWithoutId, 24 | ], 25 | ], 26 | } 27 | 28 | export default config 29 | -------------------------------------------------------------------------------- /examples/react/.storylite/index.tsx: -------------------------------------------------------------------------------- 1 | import '@storylite/storylite/styles.css' 2 | import '../src/styles/storylite-ui.css' 3 | 4 | // import other StoryLite UI styles here 5 | 6 | import stories from '@storylite/vite-plugin:stories' 7 | import { renderStoryLiteApp } from '@storylite/storylite' 8 | 9 | import config from './config' 10 | 11 | const rootElement = document.getElementById('root') as HTMLElement 12 | 13 | renderStoryLiteApp(rootElement, stories, config) 14 | -------------------------------------------------------------------------------- /examples/react/README.md: -------------------------------------------------------------------------------- 1 | # storylite-examples-react 2 | 3 | ## Reference files 4 | 5 | Basic setup of StoryLite with React: 6 | 7 | - public/\* 8 | - stories/\* 9 | - .storylite/\* (storylite entry points and setup) 10 | - src/styles/storylite-overrides.css 11 | - index.html (Vite entry point) 12 | - canvas.html (Vite entry point for the iframe) 13 | - package.json 14 | - tsconfig.json 15 | - vite.config.ts 16 | -------------------------------------------------------------------------------- /examples/react/canvas.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | StoryLite - Design System (React) - IFRAME 8 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /examples/react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | StoryLite - Design System (React) 8 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /examples/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storylite-examples-react", 3 | "private": true, 4 | "description": "StoryLite example integration with React", 5 | "type": "module", 6 | "scripts": { 7 | "build": "vite build", 8 | "dev": "vite --port=7007 --host=0.0.0.0 --open", 9 | "preview": "vite preview --port=7080 --host=0.0.0.0 --open" 10 | }, 11 | "dependencies": { 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0" 14 | }, 15 | "devDependencies": { 16 | "@storylite/storylite": "workspace:*", 17 | "@storylite/vite-plugin": "workspace:*", 18 | "@types/react": "^18.2.77", 19 | "@types/react-dom": "^18.2.25", 20 | "@vitejs/plugin-react-swc": "^3.6.0", 21 | "typescript": "^5.4.5", 22 | "vite": "^5.2.8" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/react/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /examples/react/src/components/ForwardRefButton.tsx: -------------------------------------------------------------------------------- 1 | import { type ComponentPropsWithRef, forwardRef } from 'react' 2 | 3 | export interface ButtonProps extends ComponentPropsWithRef<'button'> { 4 | variant?: 'red' | 'green' 5 | } 6 | 7 | const ForwardRefButton = forwardRef(function ForwardRefButton( 8 | { variant = 'red', children, ...rest }, 9 | ref, 10 | ) { 11 | return ( 12 | 15 | ) 16 | }) 17 | 18 | export { ForwardRefButton } 19 | -------------------------------------------------------------------------------- /examples/react/src/components/LinkableBtn.tsx: -------------------------------------------------------------------------------- 1 | import type { HTMLAttributes } from 'react' 2 | 3 | export type LinkableBtnProps = { 4 | isActive?: boolean 5 | href?: string 6 | target?: string 7 | hoverable?: boolean 8 | primary?: boolean 9 | } & HTMLAttributes 10 | 11 | export function LinkableBtn({ 12 | className, 13 | isActive, 14 | primary, 15 | hoverable, 16 | children, 17 | href, 18 | target, 19 | ...rest 20 | }: LinkableBtnProps) { 21 | const classNames = [ 22 | 'btn', 23 | isActive ? 'btn-active' : '', 24 | hoverable ? 'btn-hoverable' : 'btn-not-hoverable', 25 | primary ? 'btn-primary' : 'btn-default', 26 | className, 27 | ] 28 | .filter(Boolean) 29 | .join(' ') 30 | 31 | if (href !== undefined) { 32 | return ( 33 | 34 | {children} 35 | 36 | ) 37 | } 38 | 39 | return ( 40 | 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /examples/react/src/styles/components.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Styles for the example components and stories: 3 | */ 4 | .story-1 { 5 | color: green; 6 | font-size: 28px; 7 | } 8 | 9 | .btn { 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | line-height: 1; 14 | text-decoration: none; 15 | min-width: 200px; 16 | max-width: 100%; 17 | height: auto; 18 | font-size: 1.1rem; 19 | border-radius: 2rem; 20 | padding: 0.8rem 1.6rem; 21 | font-weight: bold; 22 | color: #333; 23 | border: 2px solid rgba(0, 0, 0, 0.2); 24 | transition: 0.3s; 25 | cursor: pointer; 26 | } 27 | 28 | .btn-default { 29 | background-color: #888; 30 | box-shadow: 0px 6px 0px -2px #555; 31 | } 32 | 33 | .btn-primary { 34 | box-shadow: 0px 6px 0px -2px #4391d1; 35 | background-color: #90caf9; 36 | } 37 | 38 | .btn:where(.btn-hoverable):hover { 39 | border-color: #000; 40 | transform: translateY(-2px); 41 | } 42 | 43 | .btn:active, 44 | .btn:where(.btn-hoverable):active:hover { 45 | box-shadow: 0 0 #fff; 46 | transform: translateY(2px); 47 | } 48 | -------------------------------------------------------------------------------- /examples/react/src/styles/storylite-iframe.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root { 4 | position: relative; 5 | padding: 0; 6 | margin: 0; 7 | border: none; 8 | box-sizing: border-box; 9 | width: 100%; 10 | height: 100%; 11 | max-height: 100vh; 12 | max-height: 100dvh; 13 | background: transparent; 14 | font-size: 16px; 15 | line-height: 1.6; 16 | tab-size: 4; 17 | -webkit-text-size-adjust: 100%; 18 | overflow-x: hidden; 19 | border: none; 20 | font-family: 21 | system-ui, 22 | -apple-system, 23 | BlinkMacSystemFont, 24 | 'Segoe UI', 25 | Roboto, 26 | Oxygen, 27 | Ubuntu, 28 | Cantarell, 29 | 'Open Sans', 30 | 'Helvetica Neue', 31 | sans-serif; 32 | background: transparent; 33 | } 34 | 35 | html, 36 | body { 37 | color: #111; 38 | } 39 | 40 | [data-theme='dark'] { 41 | color: #eee; 42 | } 43 | 44 | .storylite-iframe, 45 | .storylite-story { 46 | position: relative; 47 | max-width: 100vw; 48 | min-height: 100vh; 49 | /*noinspection CssInvalidPropertyValue*/ 50 | min-height: -webkit-fill-available; 51 | min-height: 100dvh; 52 | padding: 0; 53 | margin: 0; 54 | } 55 | 56 | .storylite-story { 57 | padding: 1rem; 58 | } 59 | 60 | .storylite-story--standalone { 61 | background: #eee; 62 | } 63 | [data-theme='dark'] .storylite-story--standalone { 64 | background: #111; 65 | } 66 | 67 | svg { 68 | color: inherit; 69 | } 70 | 71 | a { 72 | color: var(--storylite-color-primary); 73 | text-decoration: underline; 74 | } 75 | -------------------------------------------------------------------------------- /examples/react/src/styles/storylite-ui.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Custom StoryLite iframe styles 3 | */ 4 | 5 | .storylite-iframe { 6 | font-family: 'Courier New', Courier, monospace; 7 | font-size: 16px; 8 | background: transparent; 9 | } 10 | 11 | .storylite-main { 12 | font-size: 1rem; 13 | } 14 | 15 | .storylite-main :where(h1, h2, h3, h4, h5, h6) { 16 | font-weight: 600; 17 | margin: 0 0 0.5rem 0; 18 | } 19 | 20 | [data-sl-theme='dark']:where(.storylite-iframe--standalone) { 21 | color: var(--storylite-font-color); 22 | background-color: var(--storylite-bg-color); 23 | } 24 | 25 | .storylite-addon-toolbar a { 26 | text-decoration: none; 27 | } 28 | 29 | /* 30 | * DARK-THEME 31 | */ 32 | [data-sl-theme='dark']:where(.storylite-iframe) { 33 | /* General */ 34 | --storylite-font-color: #dfdfd7; 35 | --storylite-bg-color: #252529; 36 | 37 | --storylite-border-color: #29292c; 38 | 39 | /* Muted */ 40 | --storylite-font-color-muted: #999999; 41 | --storylite-border-color-muted: #484848; 42 | 43 | /* Panels */ 44 | --storylite-font-color-panel: #dfdfd7; 45 | --storylite-bg-color-panel: #161618; 46 | 47 | /* Toolbars */ 48 | --storylite-font-color-toolbar: #999999; 49 | --storylite-bg-color-toolbar: #1e1e20; 50 | } 51 | 52 | @media (prefers-color-scheme: dark) { 53 | [data-sl-theme='auto']:where(.storylite-iframe) { 54 | /* General */ 55 | --storylite-font-color: #dfdfd7; 56 | --storylite-bg-color: #252529; 57 | 58 | --storylite-border-color: #29292c; 59 | 60 | /* Muted */ 61 | --storylite-font-color-muted: #999999; 62 | --storylite-border-color-muted: #484848; 63 | 64 | /* Panels */ 65 | --storylite-font-color-panel: #dfdfd7; 66 | --storylite-bg-color-panel: #161618; 67 | 68 | /* Toolbars */ 69 | --storylite-font-color-toolbar: #999999; 70 | --storylite-bg-color-toolbar: #1e1e20; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /examples/react/stories/buttons.stories.tsx: -------------------------------------------------------------------------------- 1 | import { SLCoreAddon, type Story } from '@storylite/storylite' 2 | 3 | import { LinkableBtn } from '../src/components/LinkableBtn' 4 | 5 | import '../src/styles/components.css' 6 | 7 | type StoryType = Story 8 | 9 | // all properties (except navigation) defined in the 'default' export are inherited by 10 | // all other stories in this file 11 | export default { 12 | title: 'Feature Showcase', 13 | component: LinkableBtn, 14 | decorators: [ 15 | (Story, context) => { 16 | return ( 17 |
18 | 19 |
20 | ) 21 | }, 22 | ], 23 | args: { 24 | // default args 25 | children: 'My Button', 26 | }, 27 | parameters: { 28 | // overriden addon parameters 29 | }, 30 | navigation: { 31 | // overriden navigation options 32 | icon: , 33 | hidden: true, // hidden in sub-menu 34 | }, 35 | } satisfies StoryType 36 | 37 | export const WithComponentProps: StoryType = { 38 | title: 'This title is overriden by "name"', 39 | name: 'With Args', 40 | args: { 41 | hoverable: true, 42 | primary: true, 43 | children: 'Primary Button', 44 | }, 45 | } 46 | 47 | export const WithGridAddonOn: StoryType = { 48 | args: { 49 | hoverable: true, 50 | primary: false, 51 | children: 'With Grid Addon ON', 52 | }, 53 | parameters: { 54 | [SLCoreAddon.Grid]: { 55 | value: true, // Turns on the grid for this story 56 | }, 57 | }, 58 | } 59 | 60 | export const WithCustomDecorators: StoryType = { 61 | args: { 62 | hoverable: true, 63 | primary: false, 64 | children: 'With Custom Decorators', 65 | }, 66 | decorators: [ 67 | (Story, context) => { 68 | return ( 69 |
70 | 71 |
72 | ) 73 | }, 74 | (Story, context) => { 75 | return ( 76 |
77 | 78 |
79 | ) 80 | }, 81 | ], 82 | } 83 | 84 | export const WithCustomRender: StoryType = { 85 | args: { 86 | hoverable: true, 87 | primary: false, 88 | children: 'With Custom Render Function', 89 | }, 90 | render: (args) => { 91 | const Story = LinkableBtn 92 | 93 | return ( 94 |
95 | 96 |
97 |
Rendered with a custom render. Args:
98 |
{JSON.stringify(args, null, 2)}
99 |
100 | ) 101 | }, 102 | } 103 | 104 | export const WithScrollbar: StoryType = { 105 | args: { 106 | hoverable: true, 107 | primary: true, 108 | children: 'Primary Button', 109 | }, 110 | decorators: [ 111 | (Story, context) => { 112 | return ( 113 |
120 | 121 |
122 | ) 123 | }, 124 | ], 125 | } 126 | -------------------------------------------------------------------------------- /examples/react/stories/forwardref.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Story } from '@storylite/storylite' 2 | 3 | import { ForwardRefButton } from '../src/components/ForwardRefButton' 4 | 5 | import '../src/styles/components.css' 6 | 7 | type StoryType = Story 8 | 9 | // all properties (except navigation) defined in the 'default' export are inherited by 10 | // all other stories in this file 11 | export default { 12 | title: 'ForwardRefButton', 13 | component: ForwardRefButton, 14 | decorators: [ 15 | (Story, context) => { 16 | return ( 17 |
18 | 19 |
20 | ) 21 | }, 22 | ], 23 | args: { 24 | // default args 25 | children: 'My Button', 26 | }, 27 | navigation: { 28 | // overriden navigation options 29 | icon: , 30 | hidden: true, // hidden in sub-menu 31 | }, 32 | } satisfies StoryType 33 | 34 | export const DefaultStory: StoryType = { 35 | name: 'Default', 36 | } 37 | 38 | export const WithComponentProps: StoryType = { 39 | args: { 40 | variant: 'green', 41 | children: 'Green Variant', 42 | }, 43 | } 44 | -------------------------------------------------------------------------------- /examples/react/stories/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Story } from '@storylite/storylite' 2 | 3 | import '../src/styles/components.css' 4 | 5 | export default { 6 | title: 'Welcome', 7 | navigation: { 8 | icon: 🏠, 9 | // iconExpanded: -🏠, 10 | order: 0, // put on top 11 | hidden: false, // dont show the default export component in the navigation 12 | }, 13 | component: () =>
This is a default component defined in the default export.
, 14 | } satisfies Story 15 | -------------------------------------------------------------------------------- /examples/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["*.ts", "*.tsx", "src/**/*", "stories/**/*", ".storylite/**/*"], 4 | "compilerOptions": { 5 | "jsx": "react-jsx", 6 | "allowSyntheticDefaultImports": true, 7 | "types": ["node", "react", "react-dom", "@storylite/vite-plugin/virtual"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/react/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { resolve } from 'node:path' 3 | 4 | /** 5 | * @see https://vitejs.dev/config 6 | * @see https://vitejs.dev/guide/build.html#multi-page-app 7 | */ 8 | 9 | import storylitePlugin from '@storylite/vite-plugin' 10 | import react from '@vitejs/plugin-react-swc' 11 | import { defineConfig } from 'vite' 12 | 13 | export default defineConfig({ 14 | build: { 15 | rollupOptions: { 16 | input: { 17 | main: resolve(__dirname, 'index.html'), 18 | nested: resolve(__dirname, 'canvas.html'), 19 | }, 20 | }, 21 | }, 22 | plugins: [ 23 | storylitePlugin({ 24 | stories: 'stories/**/*.stories.tsx', // relative to process.cwd() 25 | }), 26 | react(), 27 | ], 28 | }) 29 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // 'types.ts', 'index.ts', 'index.tsx' are supposed to not have any logic, just type definitions and exports 2 | const coveragePathIgnorePatterns = ['types.ts', 'index.ts', 'index.tsx', '.d.ts', '/node_modules/'] 3 | 4 | /** 5 | * @type {import('@jest/types').Config.InitialOptions} 6 | */ 7 | module.exports = { 8 | coverageProvider: 'v8', 9 | collectCoverageFrom: ['**/src/**/*.{js,jsx,ts,tsx}'], 10 | projects: [ 11 | { 12 | displayName: 'DOM', 13 | coveragePathIgnorePatterns, 14 | testEnvironment: `${__dirname}/jest.env-browser.js`, 15 | testMatch: ['**/*.test.ts', '**/*.test.tsx'], 16 | testPathIgnorePatterns: ['/node_modules/', '/.local/', '/.ignore/', '/dist/'], 17 | // setupFilesAfterEnv: ['./jest.setup-browser.js'], 18 | transform: { 19 | '^.+\\.(t|j)sx?$': [ 20 | '@swc/jest', 21 | { 22 | jsc: { 23 | transform: { 24 | react: { 25 | runtime: 'automatic', 26 | }, 27 | }, 28 | }, 29 | }, 30 | ], 31 | }, 32 | transformIgnorePatterns: ['/node_modules/', '^.+\\.module\\.(css|sass|scss)$'], 33 | }, 34 | { 35 | displayName: 'NodeJS', 36 | coveragePathIgnorePatterns, 37 | testEnvironment: 'node', 38 | testMatch: ['**/*.test.ts'], 39 | testPathIgnorePatterns: ['/node_modules/', '/.local/', '/.ignore/', '/dist/', '.dom.test.ts'], 40 | transform: { 41 | '^.+\\.(t|j)s$': ['@swc/jest'], 42 | }, 43 | transformIgnorePatterns: ['/node_modules/', '^.+\\.module\\.(css|sass|scss)$'], 44 | }, 45 | ], 46 | } 47 | -------------------------------------------------------------------------------- /jest.env-browser.js: -------------------------------------------------------------------------------- 1 | const JSDOMEnvironment = require('jest-environment-jsdom').TestEnvironment 2 | 3 | // https://github.com/facebook/jest/blob/v29.4.3/website/versioned_docs/version-29.4/Configuration.md#testenvironment-string 4 | class BrowserEnvironment extends JSDOMEnvironment { 5 | /** 6 | * @param {ConstructorParameters} args 7 | */ 8 | constructor(...args) { 9 | super(...args) 10 | 11 | // https://github.com/jsdom/jsdom/issues/1724#issuecomment-1446858041 12 | this.global.fetch = fetch 13 | this.global.Headers = Headers 14 | this.global.Request = Request 15 | this.global.Response = Response 16 | 17 | // TextEncoder and TextDecoder are not available in JSDOM 18 | const commonJsUtils = require('node:util') 19 | this.global.TextEncoder = commonJsUtils.TextEncoder 20 | this.global.TextDecoder = commonJsUtils.TextDecoder 21 | 22 | // Some stream classes are different in JSDOM 23 | const commonJsBuffer = require('node:buffer') 24 | this.global.ArrayBuffer = ArrayBuffer 25 | this.global.Blob = commonJsBuffer.Blob 26 | this.global.File = commonJsBuffer.File 27 | 28 | // URL implementation is different in JSDOM 29 | this.global.URL = require('node:url').URL 30 | 31 | // Match Crypto implementation in Node.js 32 | // globalThis.crypto = require('crypto').webcrypto 33 | // this.global.Crypto = globalThis.Crypto 34 | // this.global.CryptoKey = globalThis.CryptoKey 35 | } 36 | } 37 | 38 | module.exports = BrowserEnvironment 39 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json", 3 | "version": "0.15.1", 4 | "npmClient": "pnpm", 5 | "packages": ["packages/*", "examples/*"] 6 | } 7 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "targetDefaults": { 3 | "build": { 4 | "cache": true, 5 | "dependsOn": ["^build"], 6 | "outputs": ["{projectRoot}/examples/*/dist", "{projectRoot}/packages/*/dist"] 7 | }, 8 | "dev": { 9 | "dependsOn": [] 10 | }, 11 | "preview": { 12 | "dependsOn": ["^build"] 13 | }, 14 | "lint": { 15 | "cache": true, 16 | "dependsOn": ["^lint"] 17 | }, 18 | "lint-fix": { 19 | "dependsOn": [] 20 | }, 21 | "publint-check": { 22 | "cache": true, 23 | "dependsOn": [] 24 | }, 25 | "type-check": { 26 | "cache": true, 27 | "dependsOn": [] 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "private": true, 4 | "homepage": "https://github.com/itsjavi/storylite#readme", 5 | "bugs": { 6 | "url": "https://github.com/itsjavi/storylite/issues" 7 | }, 8 | "repository": "git@github.com:itsjavi/storylite.git", 9 | "license": "MIT", 10 | "author": "Javi Aguilar https://itsjavi.com", 11 | "scripts": { 12 | "build": "lerna run build", 13 | "clear-cache": "rm -rf packages/*/.next && jest --clearCache", 14 | "dev": "lerna run dev --parallel", 15 | "format": "pnpm lint-fix", 16 | "format-pkg": "pnpm sort-package-json package.json packages/*/package.json examples/*/package.json", 17 | "lerna-publish": "pnpm pre-versioning && lerna publish --no-private --conventional-commits --create-release github", 18 | "lerna-release": "pnpm lerna-publish", 19 | "lint": "pnpm lint-biome", 20 | "lint-biome": "biome lint . --max-diagnostics 50", 21 | "lint-biome-fix": "biome check --max-diagnostics 50 --apply .", 22 | "lint-fix": "pnpm format-pkg && pnpm lint-biome-fix", 23 | "pre-versioning": "pnpm type-check && pnpm lint && pnpm build && pnpm publint && pnpm test:ci", 24 | "prepare": "pnpm format-pkg", 25 | "pretty": "pnpm run format", 26 | "preview": "pnpm build && http-server ./examples/react/dist", 27 | "publint": "lerna run publint-check", 28 | "quality-checks": "pnpm type-check && pnpm lint && pnpm build && pnpm publint && pnpm test:ci", 29 | "test": "jest", 30 | "test:ci": "jest --ci --coverage", 31 | "test:coverage": "jest --coverage", 32 | "type-check": "lerna run type-check" 33 | }, 34 | "devDependencies": { 35 | "@biomejs/biome": "^1.6.4", 36 | "@swc/core": "^1.4.13", 37 | "@swc/jest": "^0.2.36", 38 | "@testing-library/jest-dom": "^6.4.2", 39 | "@testing-library/react": "^15.0.1", 40 | "@types/jest": "^29.5.12", 41 | "@types/node": "^20.12.7", 42 | "http-server": "^14.1.1", 43 | "jest": "^29.7.0", 44 | "jest-environment-jsdom": "^29.7.0", 45 | "lerna": "^8.1.2", 46 | "sort-package-json": "^2.10.0", 47 | "typescript": "^5.4.5" 48 | }, 49 | "packageManager": "pnpm@8.8.0", 50 | "engines": { 51 | "node": ">=18.5.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/docs/.storylite/canvas.tsx: -------------------------------------------------------------------------------- 1 | import '@storylite/storylite/styles.css' 2 | // 3 | import '../src/styles/components.css' 4 | // 5 | import '../src/styles/storylite-iframe.css' 6 | import '../src/styles/storylite-ui.css' 7 | 8 | // import iframe-scope styles here 9 | 10 | import stories from '@storylite/vite-plugin:stories' 11 | import { renderStoryLiteApp } from '@storylite/storylite' 12 | 13 | import config from './config' 14 | 15 | const rootElement = document.getElementById('root') as HTMLElement 16 | 17 | renderStoryLiteApp(rootElement, stories, config) 18 | -------------------------------------------------------------------------------- /packages/docs/.storylite/config.tsx: -------------------------------------------------------------------------------- 1 | import type { SLAddonPropsWithoutId, SLAppComponentProps } from '@storylite/storylite' 2 | import { BoxIcon, GithubIcon, ZapIcon } from 'lucide-react' 3 | 4 | const config: Partial = { 5 | title: ' ⚡️ StoryLite', 6 | defaultStory: 'index-default', 7 | useIframeStyles: false, 8 | iframeProps: { 9 | style: { 10 | // padding: '10px', 11 | }, 12 | }, 13 | addons: [ 14 | [ 15 | 'github-link', 16 | { 17 | defaultContent: ( 18 | 19 | 20 | 21 | ), 22 | tooltip: 'Github Repository', 23 | placement: 'right', 24 | stateful: false, 25 | getHref: () => { 26 | return 'https://github.com/itsjavi/storylite/releases' 27 | }, 28 | hrefTarget: '_blank', 29 | } satisfies SLAddonPropsWithoutId, 30 | ], 31 | [ 32 | 'npm-link', 33 | { 34 | defaultContent: ( 35 | 36 | 37 | 38 | ), 39 | placement: 'right', 40 | stateful: false, 41 | tooltip: 'NPM Package', 42 | getHref: () => { 43 | return 'https://www.npmjs.com/package/@storylite/storylite' 44 | }, 45 | hrefTarget: '_blank', 46 | } satisfies SLAddonPropsWithoutId, 47 | ], 48 | [ 49 | 'stackblitz-link', 50 | { 51 | defaultContent: ( 52 | 53 | 54 | 55 | ), 56 | placement: 'right', 57 | stateful: false, 58 | getHref: () => { 59 | return 'https://stackblitz.com/edit/storylite-demo?file=stories/index.stories.tsx' 60 | }, 61 | hrefTarget: '_blank', 62 | } satisfies SLAddonPropsWithoutId, 63 | ], 64 | ], 65 | } 66 | 67 | export default config 68 | -------------------------------------------------------------------------------- /packages/docs/.storylite/index.tsx: -------------------------------------------------------------------------------- 1 | import '@storylite/storylite/styles.css' 2 | import '../src/styles/storylite-ui.css' 3 | 4 | // import other StoryLite UI styles here 5 | 6 | import stories from '@storylite/vite-plugin:stories' 7 | import { renderStoryLiteApp } from '@storylite/storylite' 8 | 9 | import config from './config' 10 | 11 | const rootElement = document.getElementById('root') as HTMLElement 12 | 13 | renderStoryLiteApp(rootElement, stories, config) 14 | -------------------------------------------------------------------------------- /packages/docs/README.md: -------------------------------------------------------------------------------- 1 | # StoryLite, a lightweight alternative to StoryBook. 2 | 3 | **StoryLite** is a modern and lightweight toolkit for crafting and managing design systems and 4 | components with ease. Inspired by the popular StoryBook UI and powered by Vite⚡️, StoryLite offers 5 | a streamlined and familiar developer experience. 6 | 7 | With StoryLite, you can swiftly create, test, and refine UI components in isolation, ensuring that 8 | your application maintains a consistent look and feel. 9 | 10 | Tailored for smaller projects that crave simplicity without the overhead of a full StoryBook setup, 11 | StoryLite provides an intuitive UI that's customizable to your unique needs. 12 | -------------------------------------------------------------------------------- /packages/docs/canvas.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | StoryLite - Design System (React) - IFRAME 8 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /packages/docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | StoryLite - Design System (React) 8 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /packages/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storylite-docs", 3 | "version": "0.14.0", 4 | "private": true, 5 | "description": "StoryLite docs built with StoryLite itself and React", 6 | "type": "module", 7 | "scripts": { 8 | "build": "vite build --base=/storylite/", 9 | "dev": "vite --base=/storylite/ --port=7087 --host=0.0.0.0 --open", 10 | "preview": "vite --base=/storylite/ preview --port=7088 --host=0.0.0.0 --open" 11 | }, 12 | "dependencies": { 13 | "lucide-react": "^0.367.0", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0" 16 | }, 17 | "devDependencies": { 18 | "@mdx-js/rollup": "^3.0.1", 19 | "@storylite/storylite": "workspace:*", 20 | "@storylite/vite-plugin": "workspace:*", 21 | "@types/react": "^18.2.77", 22 | "@types/react-dom": "^18.2.25", 23 | "@vitejs/plugin-react-swc": "^3.6.0", 24 | "remark-frontmatter": "^5.0.0", 25 | "remark-mdx-frontmatter": "^4.0.0", 26 | "typescript": "^5.4.5", 27 | "vite": "^5.2.8" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/docs/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /packages/docs/src/components/LinkableBtn.tsx: -------------------------------------------------------------------------------- 1 | import type { HTMLAttributes } from 'react' 2 | 3 | export type LinkableBtnProps = { 4 | isActive?: boolean 5 | href?: string 6 | target?: string 7 | hoverable?: boolean 8 | primary?: boolean 9 | } & HTMLAttributes 10 | 11 | export function LinkableBtn({ 12 | className, 13 | isActive, 14 | primary, 15 | hoverable, 16 | children, 17 | href, 18 | target, 19 | ...rest 20 | }: LinkableBtnProps) { 21 | const classNames = [ 22 | 'btn', 23 | isActive ? 'btn-active' : '', 24 | hoverable ? 'btn-hoverable' : 'btn-not-hoverable', 25 | primary ? 'btn-primary' : 'btn-default', 26 | className, 27 | ] 28 | .filter(Boolean) 29 | .join(' ') 30 | 31 | if (href !== undefined) { 32 | return ( 33 | 34 | {children} 35 | 36 | ) 37 | } 38 | 39 | return ( 40 | 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /packages/docs/src/styles/components.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Styles for the example components and stories: 3 | */ 4 | .story-1 { 5 | color: green; 6 | font-size: 28px; 7 | } 8 | 9 | .btn { 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | line-height: 1; 14 | text-decoration: none; 15 | min-width: 200px; 16 | max-width: 100%; 17 | height: auto; 18 | font-size: 1.1rem; 19 | border-radius: 2rem; 20 | padding: 0.8rem 1.6rem; 21 | font-weight: bold; 22 | color: #333; 23 | border: 2px solid rgba(0, 0, 0, 0.2); 24 | transition: 0.3s; 25 | cursor: pointer; 26 | } 27 | 28 | .btn-default { 29 | background-color: #888; 30 | box-shadow: 0px 6px 0px -2px #555; 31 | } 32 | 33 | .btn-primary { 34 | box-shadow: 0px 6px 0px -2px #4391d1; 35 | background-color: #90caf9; 36 | } 37 | 38 | .btn:where(.btn-hoverable):hover { 39 | border-color: #000; 40 | transform: translateY(-2px); 41 | } 42 | 43 | .btn:active, 44 | .btn:where(.btn-hoverable):active:hover { 45 | box-shadow: 0 0 #fff; 46 | transform: translateY(2px); 47 | } 48 | -------------------------------------------------------------------------------- /packages/docs/src/styles/storylite-iframe.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root { 4 | position: relative; 5 | padding: 0; 6 | margin: 0; 7 | border: none; 8 | box-sizing: border-box; 9 | width: 100%; 10 | height: 100%; 11 | max-height: 100vh; 12 | max-height: 100dvh; 13 | background: transparent; 14 | font-size: 16px; 15 | line-height: 1.6; 16 | tab-size: 4; 17 | -webkit-text-size-adjust: 100%; 18 | overflow-x: hidden; 19 | border: none; 20 | font-family: 21 | system-ui, 22 | -apple-system, 23 | BlinkMacSystemFont, 24 | 'Segoe UI', 25 | Roboto, 26 | Oxygen, 27 | Ubuntu, 28 | Cantarell, 29 | 'Open Sans', 30 | 'Helvetica Neue', 31 | sans-serif; 32 | background: transparent; 33 | } 34 | 35 | html, 36 | body { 37 | color: #111; 38 | } 39 | 40 | [data-theme='dark'] { 41 | color: #eee; 42 | } 43 | 44 | .storylite-iframe, 45 | .storylite-story { 46 | position: relative; 47 | max-width: 100vw; 48 | min-height: 100vh; 49 | /*noinspection CssInvalidPropertyValue*/ 50 | min-height: -webkit-fill-available; 51 | min-height: 100dvh; 52 | padding: 0; 53 | margin: 0; 54 | } 55 | 56 | .storylite-story { 57 | padding: 1rem; 58 | } 59 | 60 | .storylite-story--standalone { 61 | background: #eee; 62 | } 63 | [data-theme='dark'] .storylite-story--standalone { 64 | background: #111; 65 | } 66 | 67 | svg { 68 | color: inherit; 69 | } 70 | 71 | a { 72 | color: var(--storylite-color-primary); 73 | text-decoration: underline; 74 | } 75 | -------------------------------------------------------------------------------- /packages/docs/src/styles/storylite-ui.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Custom StoryLite iframe styles 3 | */ 4 | 5 | .ext-icon-npm { 6 | color: #cb0001; 7 | } 8 | 9 | .storylite-iframe { 10 | font-family: 'Courier New', Courier, monospace; 11 | font-size: 16px; 12 | background: transparent; 13 | } 14 | 15 | .storylite-main { 16 | font-size: 1rem; 17 | } 18 | 19 | .storylite-main :where(h1, h2, h3, h4, h5, h6) { 20 | font-weight: 600; 21 | margin: 0 0 0.5rem 0; 22 | } 23 | 24 | [data-sl-theme='dark']:where(.storylite-iframe--standalone) { 25 | color: var(--storylite-font-color); 26 | background-color: var(--storylite-bg-color); 27 | } 28 | 29 | .storylite-addon-toolbar a { 30 | text-decoration: none; 31 | } 32 | 33 | /* 34 | * DARK-THEME 35 | */ 36 | [data-sl-theme='dark']:where(.storylite-iframe) { 37 | /* General */ 38 | --storylite-font-color: #dfdfd7; 39 | --storylite-bg-color: #252529; 40 | 41 | --storylite-border-color: #29292c; 42 | 43 | /* Muted */ 44 | --storylite-font-color-muted: #999999; 45 | --storylite-border-color-muted: #484848; 46 | 47 | /* Panels */ 48 | --storylite-font-color-panel: #dfdfd7; 49 | --storylite-bg-color-panel: #161618; 50 | 51 | /* Toolbars */ 52 | --storylite-font-color-toolbar: #999999; 53 | --storylite-bg-color-toolbar: #1e1e20; 54 | } 55 | 56 | @media (prefers-color-scheme: dark) { 57 | [data-sl-theme='auto']:where(.storylite-iframe) { 58 | /* General */ 59 | --storylite-font-color: #dfdfd7; 60 | --storylite-bg-color: #252529; 61 | 62 | --storylite-border-color: #29292c; 63 | 64 | /* Muted */ 65 | --storylite-font-color-muted: #999999; 66 | --storylite-border-color-muted: #484848; 67 | 68 | /* Panels */ 69 | --storylite-font-color-panel: #dfdfd7; 70 | --storylite-bg-color-panel: #161618; 71 | 72 | /* Toolbars */ 73 | --storylite-font-color-toolbar: #999999; 74 | --storylite-bg-color-toolbar: #1e1e20; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /packages/docs/stories/buttons.stories.tsx: -------------------------------------------------------------------------------- 1 | import { SLCoreAddon, type Story } from '@storylite/storylite' 2 | 3 | import { LinkableBtn } from '../src/components/LinkableBtn' 4 | 5 | import '../src/styles/components.css' 6 | 7 | type StoryType = Story 8 | 9 | // all properties (except navigation) defined in the 'default' export are inherited by 10 | // all other stories in this file 11 | export default { 12 | title: 'Feature Showcase', 13 | component: LinkableBtn, 14 | decorators: [ 15 | (Story, context) => { 16 | return ( 17 |
18 | 19 |
20 | ) 21 | }, 22 | ], 23 | args: { 24 | // default args 25 | children: 'Default Button children', 26 | }, 27 | parameters: { 28 | // overriden addon parameters 29 | }, 30 | navigation: { 31 | // overriden navigation options 32 | icon: , 33 | hidden: true, // hidden in sub-menu 34 | }, 35 | } satisfies StoryType 36 | 37 | export const WithInheritedDefaultArgs: StoryType = { 38 | title: 'With Inherited Defaults', 39 | args: { 40 | hoverable: true, 41 | primary: true, 42 | // children is inherited from the default export args 43 | }, 44 | decorators: [ 45 | (Story, context) => { 46 | return ( 47 |
48 |

This story inherits the default args from the default export.

49 | 50 |
51 | ) 52 | }, 53 | ], 54 | } 55 | 56 | export const WithComponentProps: StoryType = { 57 | title: '---This title is overriden by "name"---', 58 | name: 'With Args', 59 | args: { 60 | hoverable: true, 61 | primary: true, 62 | children: 'Primary Button', 63 | }, 64 | } 65 | 66 | export const WithGridAddonOn: StoryType = { 67 | args: { 68 | hoverable: true, 69 | primary: false, 70 | children: 'With Grid Addon ON', 71 | }, 72 | parameters: { 73 | [SLCoreAddon.Grid]: { 74 | value: true, // Turns on the grid for this story 75 | }, 76 | }, 77 | } 78 | 79 | export const WithCustomDecorators: StoryType = { 80 | args: { 81 | hoverable: true, 82 | primary: false, 83 | children: 'With Custom Decorators', 84 | }, 85 | decorators: [ 86 | (Story, context) => { 87 | return ( 88 |
89 | 90 |
91 | ) 92 | }, 93 | (Story, context) => { 94 | return ( 95 |
96 | 97 |
98 | ) 99 | }, 100 | ], 101 | } 102 | 103 | export const WithCustomRender: StoryType = { 104 | args: { 105 | hoverable: true, 106 | primary: false, 107 | children: 'With Custom Render Function', 108 | }, 109 | render: (args) => { 110 | const Story = LinkableBtn 111 | 112 | return ( 113 |
114 | 115 |
116 |
Rendered with a custom render. Args:
117 |
{JSON.stringify(args, null, 2)}
118 |
119 | ) 120 | }, 121 | } 122 | 123 | export const WithScrollbar: StoryType = { 124 | args: { 125 | hoverable: true, 126 | primary: true, 127 | children: 'Primary Button', 128 | }, 129 | decorators: [ 130 | (Story, context) => { 131 | return ( 132 |
139 | 140 |
141 | ) 142 | }, 143 | ], 144 | } 145 | -------------------------------------------------------------------------------- /packages/docs/stories/index.stories.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Welcome 3 | renderFrame: root 4 | navigation: 5 | icon: 🏠 6 | order: 0 7 | hidden: true # hidden in the submenu 8 | --- 9 | 10 | import BaseReadme from '../README.md' 11 | 12 | StoryLite Logo 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["*.ts", "*.tsx", "src/**/*", "stories/**/*", ".storylite/**/*"], 4 | "compilerOptions": { 5 | "jsx": "react-jsx", 6 | "allowSyntheticDefaultImports": true, 7 | "types": ["node", "react", "react-dom", "@storylite/vite-plugin/virtual"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/docs/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { resolve } from 'node:path' 3 | 4 | import mdx from '@mdx-js/rollup' 5 | import storylite from '@storylite/vite-plugin' 6 | import react from '@vitejs/plugin-react-swc' 7 | import remarkFrontmatter from 'remark-frontmatter' 8 | import remarkMdxFrontmatter from 'remark-mdx-frontmatter' 9 | import { defineConfig } from 'vite' 10 | 11 | /** 12 | * @see https://vitejs.dev/config 13 | * @see https://vitejs.dev/guide/build.html#multi-page-app 14 | */ 15 | 16 | export default defineConfig({ 17 | build: { 18 | rollupOptions: { 19 | input: { 20 | main: resolve(__dirname, 'index.html'), 21 | nested: resolve(__dirname, 'canvas.html'), 22 | }, 23 | }, 24 | }, 25 | plugins: [ 26 | { 27 | enforce: 'pre', // this ensures that .md/mdx files are processed before react & storylite plugins 28 | ...mdx({ 29 | remarkPlugins: [remarkFrontmatter, remarkMdxFrontmatter], 30 | }), 31 | }, 32 | storylite({ 33 | stories: 'stories/**/*.stories.{tsx,md,mdx}', // relative to process.cwd() 34 | }), 35 | react(), 36 | ], 37 | }) 38 | -------------------------------------------------------------------------------- /packages/storylite/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | vite.config.ts.timestamp-* 26 | 27 | README.md 28 | -------------------------------------------------------------------------------- /packages/storylite/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.15.1](https://github.com/itsjavi/storylite/compare/v0.15.0...v0.15.1) (2024-03-27) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * make base and canvas paths configurable ([9dfc797](https://github.com/itsjavi/storylite/commit/9dfc7975fb8ff4bdfc5f62c4a5f5d13b731932eb)) 12 | 13 | 14 | 15 | 16 | 17 | # [0.15.0](https://github.com/itsjavi/storylite/compare/v0.14.2...v0.15.0) (2024-03-27) 18 | 19 | 20 | ### Features 21 | 22 | * add support for React Server Components ([410ef23](https://github.com/itsjavi/storylite/commit/410ef234adfb33f161dd35ecb0228312d3ee15f1)) 23 | -------------------------------------------------------------------------------- /packages/storylite/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Javi Aguilar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/storylite/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /packages/storylite/assets/lucide/LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022. 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /packages/storylite/assets/lucide/svg/bookmark.svg: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/storylite/assets/lucide/svg/box-select.svg: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /packages/storylite/assets/lucide/svg/box.svg: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/storylite/assets/lucide/svg/contrast.svg: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/storylite/assets/lucide/svg/expand.svg: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/storylite/assets/lucide/svg/external-link.svg: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/storylite/assets/lucide/svg/github.svg: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/storylite/assets/lucide/svg/grid-3x3.svg: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/storylite/assets/lucide/svg/minus-square.svg: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/storylite/assets/lucide/svg/monitor-smartphone.svg: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/storylite/assets/lucide/svg/moon.svg: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/storylite/assets/lucide/svg/plus-square.svg: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/storylite/assets/lucide/svg/sun.svg: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/storylite/assets/lucide/svg/x-circle.svg: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/storylite/assets/lucide/svg/zap.svg: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/storylite/declaration.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const content: string 3 | export default content 4 | } 5 | -------------------------------------------------------------------------------- /packages/storylite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@storylite/storylite", 3 | "version": "0.15.1", 4 | "description": "A lightweight alternative to StoryBook, built on top of Vite⚡️. This package contains the React app.", 5 | "license": "MIT", 6 | "type": "module", 7 | "exports": { 8 | ".": { 9 | "types": "./dist/index.d.ts", 10 | "import": "./dist/index.js", 11 | "default": "./dist/index.cjs" 12 | }, 13 | "./styles.css": "./dist/index.css" 14 | }, 15 | "main": "./dist/index.cjs", 16 | "types": "./dist/index.d.ts", 17 | "files": ["dist", "README.md", "LICENSE"], 18 | "scripts": { 19 | "build": "tsup --clean", 20 | "dev": "tsup --watch", 21 | "lint": "biome lint .", 22 | "lint-fix": "biome lint --apply .", 23 | "prepublishOnly": "pnpm run build", 24 | "publint-check": "pnpm publint", 25 | "type-check": "tsc --noEmit" 26 | }, 27 | "dependencies": { 28 | "clsx": "^2.1.0", 29 | "zustand": "^4.5.2" 30 | }, 31 | "devDependencies": { 32 | "@types/node": "^20.12.7", 33 | "@types/react": "^18.2.77", 34 | "@types/react-dom": "^18.2.25", 35 | "publint": "^0.2.7", 36 | "react": "^18.2.0", 37 | "react-dom": "^18.2.0", 38 | "tsup": "^8.0.2", 39 | "typescript": "^5.4.5" 40 | }, 41 | "peerDependencies": { 42 | "@types/node": "^20.4.9", 43 | "@types/react": "^18.2.19", 44 | "@types/react-dom": "^18.2.7", 45 | "react": "^18.2.0", 46 | "react-dom": "^18.2.0", 47 | "typescript": "^5.1.6" 48 | }, 49 | "peerDependenciesMeta": { 50 | "react": { 51 | "optional": false 52 | }, 53 | "react-dom": { 54 | "optional": false 55 | }, 56 | "typescript": { 57 | "optional": false 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/storylite/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsjavi/storylite/640c2cb278cbd1fef18eece7fa27a1a88ee1ecc7/packages/storylite/screenshot.png -------------------------------------------------------------------------------- /packages/storylite/src/app/index.test.tsx: -------------------------------------------------------------------------------- 1 | it('passes', () => { 2 | expect(true).toBe(true) 3 | }) 4 | -------------------------------------------------------------------------------- /packages/storylite/src/app/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | 3 | import { RouteRenderer, useRouterStore } from '..' 4 | import type { SLAppComponentProps, StoryModuleMap } from '../types' 5 | import { useStoryLiteStore } from './stores/global' 6 | 7 | export type StoryLiteAppProps = { 8 | config?: Partial 9 | stories: StoryModuleMap 10 | children?: React.ReactNode 11 | } 12 | 13 | export const StoryLiteApp = (props: StoryLiteAppProps) => { 14 | const { config, stories, children } = props 15 | const [initialize, setCurrentStoryId] = useStoryLiteStore((state) => [state.initialize, state.setCurrentStoryId]) 16 | 17 | const [route, getFallback, initializeRouter, routeParams] = useRouterStore((state) => [ 18 | state.route, 19 | state.getFallback, 20 | state.initialize, 21 | state.params, 22 | ]) 23 | 24 | useEffect(() => { 25 | initialize(config || {}, stories) 26 | initializeRouter() 27 | }, []) 28 | 29 | useEffect(() => { 30 | if (routeParams.storyId) { 31 | setCurrentStoryId(routeParams.storyId) 32 | } 33 | }, [routeParams]) 34 | 35 | return ( 36 | 37 | {children} 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /packages/storylite/src/app/renderApp.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | 4 | import type { SLAppComponentProps, StoryModuleMap } from '@/types' 5 | 6 | import { StoryLiteApp } from '.' 7 | 8 | /** 9 | * Use this method if you don't want to mount the StoryLite app component yourself. 10 | */ 11 | export function renderStoryLiteApp(root: HTMLElement, stories: StoryModuleMap, config?: Partial) { 12 | ReactDOM.createRoot(root).render( 13 | 14 | 15 | {config?.children} 16 | 17 | , 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /packages/storylite/src/app/routes.ts: -------------------------------------------------------------------------------- 1 | // 2 | // We have to import all pages manually, we are not in the Vite context here since it is a SPA, 3 | // so we cannot use import.meta.glob like with Vite: 4 | // const pages = import.meta.glob('./pages/**/*.tsx', { eager: true }) as Record 5 | // 6 | import * as IndexPage from '@/pages/index' 7 | import * as SandboxIndexPage from '@/pages/preview/index' 8 | import * as SandboxStoryPage from '@/pages/preview/story' 9 | import * as StoryPage from '@/pages/stories/story' 10 | import type { RouterPage } from '@/services/router/router.types' 11 | 12 | const appRoutes: Record = { 13 | '/': IndexPage, 14 | '/preview': SandboxIndexPage, 15 | '/preview/stories/[storyId]': SandboxStoryPage, 16 | '/stories/[storyId]': StoryPage, 17 | } 18 | 19 | export { appRoutes } 20 | -------------------------------------------------------------------------------- /packages/storylite/src/app/stores/DefaultTitle.tsx: -------------------------------------------------------------------------------- 1 | import logo from '@/assets/logo.svg' 2 | import { InlineHtml } from '@/components/InlineHtml' 3 | 4 | export function DefaultTitle(): JSX.Element { 5 | return ( 6 | <> 7 | {logo} StoryLite 8 | 9 | ) 10 | } 11 | 12 | export function getDefaultTitle(): JSX.Element { 13 | return 14 | } 15 | -------------------------------------------------------------------------------- /packages/storylite/src/app/stores/global.ts: -------------------------------------------------------------------------------- 1 | import { shallow } from 'zustand/shallow' 2 | import { createWithEqualityFn } from 'zustand/traditional' 3 | 4 | import { 5 | getDefaultToolbarAddons, 6 | getToolbarAddonsAsParameters, 7 | resolveToolbarAddons, 8 | } from '@/components/addons/getToolbarAddons' 9 | import type { SLAddonsMap, SLParameters, SLUserDefinedAddons, StoryMap, StoryModuleMap } from '@/types' 10 | 11 | import { getDefaultTitle } from './DefaultTitle' 12 | import type { StoryLiteActions, StoryLiteState } from './global.types' 13 | 14 | const builtinAddons: SLAddonsMap = new Map( 15 | Array.from(getDefaultToolbarAddons().entries()).map(([id, addon]) => [id, { ...addon, id }]), 16 | ) 17 | 18 | // @ts-ignore 19 | const basePath = import.meta?.env?.BASE_URL ? import.meta.env.BASE_URL : '/' 20 | 21 | const defaultState: StoryLiteState = { 22 | canvas: { 23 | element: null, 24 | standalone: false, 25 | }, 26 | config: { 27 | title: getDefaultTitle(), 28 | basePath: basePath, 29 | canvasPath: `${basePath}/canvas.html`.replace(/[\/]+/g, '/'), 30 | defaultStory: 'index', 31 | iframeProps: {}, 32 | useIframeStyles: true, 33 | themeAttribute: 'data-theme', 34 | localStorageKey: 'storylite', 35 | children: null, 36 | addons: [], 37 | }, 38 | parameters: {}, 39 | addons: builtinAddons, 40 | stories: new Map(), 41 | storyModuleMap: new Map(), 42 | currentStoryId: undefined, 43 | } 44 | 45 | const updateParameters = ( 46 | state: StoryLiteState, 47 | params: SLParameters, 48 | options = { persist: false, crossWindow: false }, 49 | ): StoryLiteState => { 50 | if (options.persist) { 51 | localStorage.setItem(`${state.config.localStorageKey}.parameters`, JSON.stringify(params)) 52 | } 53 | 54 | return { 55 | ...state, 56 | parameters: params, 57 | } 58 | } 59 | 60 | const resolveParams = (): SLParameters => { 61 | const localStorageParams = localStorage.getItem(`${defaultState.config.localStorageKey}.parameters`) 62 | 63 | const builtinAddonParameters = getToolbarAddonsAsParameters(builtinAddons) 64 | 65 | const resolvedParams: SLParameters = localStorageParams ? JSON.parse(localStorageParams) : builtinAddonParameters 66 | 67 | return resolvedParams 68 | } 69 | 70 | export const useStoryLiteStore = createWithEqualityFn((set) => { 71 | const createStoryMap = (moduleMap: StoryModuleMap): StoryMap => { 72 | const storyMap: StoryMap = new Map() 73 | 74 | Array.from(moduleMap.entries()).forEach(([, modules]) => { 75 | Object.entries(modules).forEach(([, story]) => { 76 | storyMap.set(story.id, story) 77 | }) 78 | }) 79 | 80 | return storyMap 81 | } 82 | 83 | return { 84 | ...defaultState, 85 | parameters: resolveParams(), 86 | setAddons(addons: SLUserDefinedAddons) { 87 | set((state) => { 88 | return { 89 | ...state, 90 | addons: resolveToolbarAddons(builtinAddons, addons), 91 | } 92 | }) 93 | }, 94 | setStories(stories) { 95 | set((state) => { 96 | return { 97 | ...state, 98 | storyModuleMap: new Map(stories.entries()), 99 | } 100 | }) 101 | }, 102 | setConfig(config) { 103 | set((state) => { 104 | return { 105 | ...state, 106 | config: { 107 | ...state.config, 108 | ...config, 109 | }, 110 | } 111 | }) 112 | }, 113 | initialize(config, storyModules) { 114 | const moduleMap = new Map(storyModules.entries()) 115 | const storyMap = createStoryMap(moduleMap) 116 | 117 | set((state) => { 118 | return { 119 | ...state, 120 | config: { 121 | ...state.config, 122 | ...config, 123 | }, 124 | addons: resolveToolbarAddons(builtinAddons, config.addons), 125 | storyModuleMap: moduleMap, 126 | stories: storyMap, 127 | } 128 | }) 129 | }, 130 | setCurrentStoryId(storyId) { 131 | set((state) => { 132 | return { 133 | ...state, 134 | currentStoryId: storyId, 135 | } 136 | }) 137 | }, 138 | setParameter(key, value, options = { persist: false, crossWindow: false }) { 139 | set((state) => 140 | updateParameters( 141 | state, 142 | { 143 | ...state.parameters, 144 | [key]: { 145 | ...state.parameters[key], 146 | value, 147 | }, 148 | }, 149 | { 150 | persist: options.persist || false, 151 | crossWindow: options.crossWindow || false, 152 | }, 153 | ), 154 | ) 155 | }, 156 | setParameters(data, options = { persist: false, crossWindow: false }) { 157 | set((state) => 158 | updateParameters(state, data, { 159 | persist: options.persist || false, 160 | crossWindow: options.crossWindow || false, 161 | }), 162 | ) 163 | }, 164 | setCanvasElement(element) { 165 | set((state) => { 166 | return { 167 | ...state, 168 | canvas: { 169 | ...state.canvas, 170 | element, 171 | }, 172 | } 173 | }) 174 | }, 175 | } 176 | }, shallow) 177 | 178 | export const useStoryLiteIframe = (): { 179 | iframe: HTMLIFrameElement | null 180 | loaded: boolean 181 | setIframe: (element: HTMLIFrameElement | null) => void 182 | window: Window | null 183 | document: Document | null 184 | } => { 185 | const [canvas, setElement] = useStoryLiteStore((state) => [state.canvas, state.setCanvasElement]) 186 | 187 | if (!canvas.element) { 188 | return { 189 | iframe: null, 190 | loaded: false, 191 | setIframe: setElement, 192 | window: null, 193 | document: null, 194 | } 195 | } 196 | 197 | const win = canvas.element.contentWindow ?? null 198 | const doc = canvas.element.contentDocument ?? canvas.element.contentWindow?.document ?? null 199 | 200 | return { 201 | iframe: canvas.element, 202 | setIframe: setElement, 203 | loaded: true, 204 | window: win, 205 | document: doc, 206 | } 207 | } 208 | 209 | /** 210 | * Returns a non-reactive fresh state of the store. 211 | */ 212 | export function getStoryLiteState() { 213 | return useStoryLiteStore.getState() 214 | } 215 | -------------------------------------------------------------------------------- /packages/storylite/src/app/stores/global.types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | SLAddonsMap, 3 | SLAppComponentProps, 4 | SLParameters, 5 | SLUserDefinedAddons, 6 | StoryMap, 7 | StoryModuleMap, 8 | } from '@/types' 9 | 10 | export type StoryLiteCanvasState = { 11 | element: HTMLIFrameElement | null 12 | standalone: boolean 13 | } 14 | 15 | export type StoryLiteParamValue = string | string[] | number | number[] | boolean | undefined | null 16 | 17 | export type StoryLiteState = { 18 | config: Required 19 | canvas: StoryLiteCanvasState 20 | parameters: SLParameters 21 | addons: SLAddonsMap 22 | stories: StoryMap 23 | storyModuleMap: StoryModuleMap 24 | currentStoryId?: string 25 | } 26 | 27 | export type StoryLiteActions = { 28 | setParameter: ( 29 | key: string, 30 | value: StoryLiteParamValue, 31 | options?: { persist?: boolean; crossWindow?: boolean }, 32 | ) => void 33 | setParameters: (data: SLParameters, options?: { persist?: boolean; crossWindow?: boolean }) => void 34 | setCurrentStoryId: (storyId: string) => void 35 | setAddons: (addons: SLUserDefinedAddons) => void 36 | setStories: (stories: StoryModuleMap) => void 37 | setConfig: (config: Partial) => void 38 | initialize: (config: Partial, storyModules: StoryModuleMap) => void 39 | setCanvasElement: (element: HTMLIFrameElement | null) => void 40 | } 41 | 42 | export type StoryLiteStore = StoryLiteState & StoryLiteActions 43 | -------------------------------------------------------------------------------- /packages/storylite/src/components/Btn.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/utility' 2 | import type { HTMLAttributes } from 'react' 3 | 4 | export type BtnProps = { 5 | isActive?: boolean 6 | href?: string 7 | target?: string 8 | hoverable?: boolean 9 | } & HTMLAttributes 10 | 11 | export function Btn({ className, isActive, hoverable, children, href, target, ...rest }: BtnProps) { 12 | const classNames = cn( 13 | 'storylite-btn', 14 | { 15 | 'storylite-btn--active': isActive, 16 | 'storylite-btn--hoverable': hoverable, 17 | }, 18 | className, 19 | ) 20 | 21 | if (href !== undefined) { 22 | return ( 23 | 24 | {children} 25 | 26 | ) 27 | } 28 | 29 | return ( 30 | 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /packages/storylite/src/components/InlineHtml.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/utility' 2 | import type { HTMLProps } from 'react' 3 | 4 | type InlineHtmlProps = { 5 | children: string 6 | } & HTMLProps 7 | 8 | export function InlineHtml({ children, className, ...rest }: InlineHtmlProps): JSX.Element { 9 | return ( 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /packages/storylite/src/components/Story.tsx: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | 3 | import { useStoryLiteStore } from '@/app/stores/global' 4 | import { renderStory } from '@/services/renderer/react' 5 | import { useRouterQuery } from '@/services/router' 6 | import { cn } from '@/utility' 7 | 8 | function StoryWrapper({ children }: { children: React.ReactNode }) { 9 | const searchParams = useRouterQuery() 10 | const isStandalone = searchParams.standalone ? true : false 11 | 12 | return ( 13 |
14 |
15 |
{children}
16 |
17 |
18 | ) 19 | } 20 | 21 | export function Story({ storyId }: { storyId: string }): JSX.Element { 22 | const stories = useStoryLiteStore((state) => state.stories) 23 | const story = stories.get(storyId) 24 | 25 | if (!story) { 26 | return ( 27 | 28 |

29 | Story not found: {storyId} 30 |

31 |
32 | ) 33 | } 34 | 35 | if (!story.component) { 36 | return ( 37 | 38 |

39 | Story has no component: {storyId} 40 |

41 |
42 | ) 43 | } 44 | 45 | return {renderStory(story.component, story)} 46 | } 47 | -------------------------------------------------------------------------------- /packages/storylite/src/components/canvas/CanvasIframe.tsx: -------------------------------------------------------------------------------- 1 | import { type HTMLProps, useEffect } from 'react' 2 | 3 | import { useStoryLiteIframe, useStoryLiteStore } from '@/app/stores/global' 4 | import { getStoryUrl } from '@/services/csf-api/navigation' 5 | import { cn } from '@/utility' 6 | import { parametersToDataProps } from '@/utility/parametersToDataProps' 7 | 8 | const allowList = [ 9 | 'autoplay', 10 | 'camera', 11 | 'encrypted-media', 12 | 'fullscreen', 13 | 'geolocation', 14 | 'microphone', 15 | 'midi', 16 | 'payment', 17 | 'picture-in-picture', 18 | 'clipboard-write', 19 | 'accelerometer', 20 | 'gyroscope', 21 | 'magnetometer', 22 | 'xr-spatial-tracking', 23 | 'usb', 24 | ] 25 | 26 | export type CanvasIframeProps = { 27 | storyId?: string 28 | } & Omit, 'src'> 29 | 30 | export function CanvasIframe(props: CanvasIframeProps) { 31 | const { storyId, className, ...rest } = props 32 | const iframeState = useStoryLiteIframe() 33 | const [userConfig, stories, currentParams, setParameters] = useStoryLiteStore((state) => [ 34 | state.config, 35 | state.stories, 36 | state.parameters, 37 | state.setParameters, 38 | ]) 39 | 40 | const onIframeLoad = (e: React.SyntheticEvent) => { 41 | iframeState?.setIframe(e.currentTarget) 42 | } 43 | 44 | useEffect(() => { 45 | if (!storyId || !iframeState.loaded || !iframeState.iframe) { 46 | return 47 | } 48 | 49 | const story = stories.get(storyId) 50 | const hasParams = Object.keys(story?.parameters ?? {}).length > 0 51 | 52 | if (story && hasParams) { 53 | // If the story has parameters, set them when the iframe loads 54 | setParameters( 55 | { 56 | ...currentParams, 57 | ...story.parameters, 58 | }, 59 | { crossWindow: true, persist: false }, 60 | ) 61 | 62 | return () => { 63 | setParameters(currentParams, { crossWindow: true, persist: false }) 64 | } 65 | } 66 | }, [storyId, stories, iframeState.loaded, iframeState.iframe]) 67 | 68 | const userProps = userConfig?.iframeProps || {} 69 | const { className: userClassName, ...userRest } = userProps 70 | const paramsDataProps = parametersToDataProps(currentParams) 71 | const iframeSrc = getStoryUrl(storyId, { 72 | target: 'iframe', 73 | standalone: false, 74 | }) 75 | 76 | return ( 77 |