├── .commitlintrc.ts ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── config.yml ├── pull_request_template.md └── workflows │ └── main.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .lintstagedrc ├── .prettierignore ├── .prettierrc ├── .storybook ├── main.ts ├── manager.ts ├── preview.tsx └── theme.ts ├── LICENSE ├── README.md ├── eslint.config.mjs ├── misc └── demo-tab.gif ├── package-lock.json ├── package.json ├── scripts ├── rollup.config.ts ├── tsconfig.base.json ├── tsconfig.build.json └── tsconfig.dev.json ├── src ├── index.ts ├── react-demo-tab │ ├── DemoTab.tsx │ ├── img │ │ ├── css.png │ │ ├── jsx.png │ │ └── sass.png │ ├── types.public.ts │ └── types.ts └── types │ └── images.d.ts ├── stories ├── ButtonGreen │ ├── ButtonGreen.css │ ├── ButtonGreen.tsx │ └── _ButtonGreen.tsx ├── ButtonRed │ ├── ButtonRed.scss │ ├── ButtonRed.tsx │ └── _ButtonRed.tsx ├── Counter │ ├── Counter.tsx │ └── _Counter.tsx ├── Default │ ├── Default.jsx │ └── _Default.tsx ├── DemoTab.scss └── DemoTab.stories.tsx └── tsconfig.json /.commitlintrc.ts: -------------------------------------------------------------------------------- 1 | import type { UserConfig } from '@commitlint/types'; 2 | 3 | const commitlintConfig: UserConfig = { 4 | extends: ['@commitlint/config-conventional'], 5 | }; 6 | 7 | module.exports = commitlintConfig; 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 'Bug Report' 2 | description: 'File a bug report' 3 | body: 4 | - type: 'textarea' 5 | id: 'description' 6 | attributes: 7 | label: 'Description' 8 | description: 'A clear and concise description of what the bug is.' 9 | placeholder: | 10 | Bug description 11 | validations: 12 | required: true 13 | - type: 'input' 14 | id: 'reproduction_link' 15 | attributes: 16 | label: 'Link to Reproduction' 17 | description: | 18 | Please provide demo in online code editor [CodeSandbox](https://codesandbox.io/) or similar. 19 | Issues without a reproduction link are likely to stall. 20 | placeholder: 'https://codesandbox.io/' 21 | validations: 22 | required: true 23 | - type: 'textarea' 24 | id: 'reproduction_steps' 25 | attributes: 26 | label: 'Steps to reproduce' 27 | description: | 28 | Steps to reproduce the behavior: 29 | value: | 30 | 1. Go to '...' 31 | 2. Click on '...' 32 | 3. Scroll down to '...' 33 | 4. See error 34 | - type: 'textarea' 35 | id: 'expected_behavior' 36 | attributes: 37 | label: 'Expected behavior' 38 | description: 'A clear and concise description of what you expected to happen.' 39 | validations: 40 | required: true 41 | - type: 'textarea' 42 | id: 'code_snippets' 43 | attributes: 44 | label: 'Code snippets' 45 | description: | 46 | If applicable, add code samples to help explain your problem. 47 | value: | 48 | ```jsx 49 | 50 | 51 | 52 | ``` 53 | - type: 'input' 54 | id: 'lib-version' 55 | attributes: 56 | label: 'React DemoTab Version' 57 | description: 'The version of library you use.' 58 | placeholder: '1.5.36' 59 | validations: 60 | required: true 61 | - type: 'input' 62 | id: 'browser' 63 | attributes: 64 | label: 'Browser' 65 | description: 'The browser this issue occurred with.' 66 | placeholder: 'Google Chrome 93' 67 | - type: 'checkboxes' 68 | id: 'operating-system' 69 | attributes: 70 | label: 'Operating System' 71 | description: 'The operating system(s) this issue occurred with.' 72 | options: 73 | - label: 'macOS' 74 | - label: 'Windows' 75 | - label: 'Linux' 76 | - type: 'textarea' 77 | id: 'additional-information' 78 | attributes: 79 | label: 'Additional Information' 80 | description: | 81 | Add any other context about the problem here. 82 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask a question 4 | url: https://github.com/demozap/react-demo-tab/discussions 5 | about: Ask questions and discuss topics with other community members 6 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### 🚀 Pull Request Template 2 | 3 | **Description** 4 | 5 | Feature Proposal ... 6 | 7 | OR 8 | 9 | Fixes a bug where '...' happened when '...' 10 | 11 | **Checklist** 12 | 13 | - [ ] Commit messages should follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) as much as possible. After staging your changes please run `npm run commit` 14 | - [ ] Lint passing - `npm run lint` 15 | - [ ] Extended the Storybook demo page / README / documentation, if necessary 16 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | env: 9 | NODE_VERSION: '24.x' 10 | FOLDER_PATH_STORYBOOK_BUILD: ./build-storybook-static 11 | 12 | jobs: 13 | build: 14 | name: Build 🏗️ 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout 🛎️ 19 | uses: actions/checkout@v4 20 | 21 | - uses: actions/setup-node@v4 22 | with: 23 | node-version: ${{ env.NODE_VERSION }} 24 | 25 | - name: Cache dependencies ⚡ 26 | id: cache_dependencies 27 | uses: actions/cache@v4 28 | with: 29 | path: node_modules 30 | key: node-modules-${{ hashFiles('package-lock.json') }} 31 | 32 | - name: Install dependencies 🔧 33 | if: steps.cache_dependencies.outputs.cache-hit != 'true' 34 | run: npm ci 35 | 36 | - name: Build 🏗️ 37 | run: npm run build 38 | 39 | lint: 40 | name: Lint ✅ 41 | runs-on: ubuntu-latest 42 | 43 | steps: 44 | - name: Checkout 🛎️ 45 | uses: actions/checkout@v4 46 | 47 | - uses: actions/setup-node@v4 48 | with: 49 | node-version: ${{ env.NODE_VERSION }} 50 | 51 | - name: Cache dependencies ⚡ 52 | id: cache_dependencies 53 | uses: actions/cache@v4 54 | with: 55 | path: node_modules 56 | key: node-modules-${{ hashFiles('package-lock.json') }} 57 | 58 | - name: Install dependencies 🔧 59 | if: steps.cache_dependencies.outputs.cache-hit != 'true' 60 | run: npm ci 61 | 62 | - name: Lint ✅ 63 | run: npm run lint 64 | 65 | tsc: 66 | name: TypeScript Compiler 🔎 67 | runs-on: ubuntu-latest 68 | 69 | steps: 70 | - name: Checkout 🛎️ 71 | uses: actions/checkout@v4 72 | 73 | - uses: actions/setup-node@v4 74 | with: 75 | node-version: ${{ env.NODE_VERSION }} 76 | 77 | - name: Cache dependencies ⚡ 78 | id: cache_dependencies 79 | uses: actions/cache@v4 80 | with: 81 | path: node_modules 82 | key: node-modules-${{ hashFiles('package-lock.json') }} 83 | 84 | - name: Install dependencies 🔧 85 | if: steps.cache_dependencies.outputs.cache-hit != 'true' 86 | run: npm ci 87 | 88 | - name: TypeScript Compiler 🔎 89 | run: npm run tsc 90 | 91 | storybook: 92 | name: Deploy Storybook 🚀 93 | runs-on: ubuntu-latest 94 | needs: [build, lint, tsc] 95 | if: github.ref == 'refs/heads/main' 96 | 97 | steps: 98 | - name: Checkout 🛎️ 99 | uses: actions/checkout@v4 100 | 101 | - uses: actions/setup-node@v4 102 | with: 103 | node-version: ${{ env.NODE_VERSION }} 104 | 105 | - name: Cache dependencies ⚡ 106 | id: cache_dependencies 107 | uses: actions/cache@v4 108 | with: 109 | path: node_modules 110 | key: node-modules-${{ hashFiles('package-lock.json') }} 111 | 112 | - name: Install dependencies 🔧 113 | if: steps.cache_dependencies.outputs.cache-hit != 'true' 114 | run: npm ci 115 | 116 | - name: Build 🏗️ 117 | run: npm run storybook-build 118 | 119 | - name: Deploy 🚀 120 | uses: JamesIves/github-pages-deploy-action@v4 121 | with: 122 | branch: gh-pages 123 | folder: ${{ env.FOLDER_PATH_STORYBOOK_BUILD }} 124 | clean: true 125 | 126 | release-npm: 127 | name: Release npm 🚀 128 | runs-on: ubuntu-latest 129 | needs: [build, lint, tsc] 130 | if: github.ref == 'refs/heads/main' 131 | env: 132 | NODE_ENV: 'production' 133 | 134 | steps: 135 | - name: Checkout 🛎️ 136 | uses: actions/checkout@v4 137 | 138 | - uses: actions/setup-node@v4 139 | with: 140 | node-version: ${{ env.NODE_VERSION }} 141 | 142 | - name: Cache dependencies ⚡ 143 | id: cache_dependencies 144 | uses: actions/cache@v4 145 | with: 146 | path: node_modules 147 | key: node-modules-${{ hashFiles('package-lock.json') }} 148 | 149 | - name: Install dependencies 🔧 150 | if: steps.cache_dependencies.outputs.cache-hit != 'true' 151 | run: npm ci 152 | 153 | - name: Build 🏗️ 154 | run: npm run build 155 | 156 | - name: Release 🚀 157 | env: 158 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 159 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 160 | run: npm run release 161 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | build-storybook-static 5 | compiled 6 | 7 | bundle-analysis.html 8 | 9 | .rpt2_cache 10 | 11 | npm-debug.log* 12 | .DS_Store 13 | .env 14 | *.log 15 | .vscode 16 | .idea -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no-install commitlint --edit "$1" -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run lint-staged-husky -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*": ["prettier --write --ignore-unknown"], 3 | "*.{js,jsx,ts,tsx}": ["eslint --max-warnings 0"] 4 | } 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "semi": true, 4 | "singleQuote": true, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import path, { dirname } from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | 4 | import type { StorybookConfig } from '@storybook/react-webpack5'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = dirname(__filename); 8 | 9 | const storybookConfig: StorybookConfig = { 10 | stories: ['../stories/**/*.stories.tsx'], 11 | framework: { 12 | name: '@storybook/react-webpack5', 13 | options: {}, 14 | }, 15 | 16 | // JSX transform no longer requires importing React explicitly in every file. 17 | swc: () => ({ 18 | jsc: { 19 | transform: { 20 | react: { 21 | runtime: 'automatic', 22 | }, 23 | }, 24 | }, 25 | }), 26 | 27 | webpackFinal: (config) => { 28 | config.module?.rules?.push({ 29 | test: /\.scss$/, 30 | use: ['style-loader', 'css-loader', 'sass-loader'], 31 | include: path.resolve(__dirname, '../'), 32 | }); 33 | 34 | // Resolve absolute imports 35 | config.resolve?.modules?.push(path.resolve(process.cwd(), 'src')); 36 | 37 | return config; 38 | }, 39 | 40 | addons: ['@storybook/addon-webpack5-compiler-swc', '@storybook/addon-styling'], 41 | }; 42 | 43 | // eslint-disable-next-line import-x/no-default-export 44 | export default storybookConfig; 45 | -------------------------------------------------------------------------------- /.storybook/manager.ts: -------------------------------------------------------------------------------- 1 | import { addons } from '@storybook/manager-api'; 2 | 3 | import { theme } from './theme'; 4 | 5 | addons.setConfig({ 6 | theme, 7 | toolbar: { 8 | title: { hidden: false }, 9 | zoom: { hidden: false }, 10 | eject: { hidden: false }, 11 | copy: { hidden: false }, 12 | fullscreen: { hidden: false }, 13 | }, 14 | bottomPanelHeight: 0, 15 | panelPosition: false, 16 | }); 17 | -------------------------------------------------------------------------------- /.storybook/preview.tsx: -------------------------------------------------------------------------------- 1 | import type { Preview } from '@storybook/react'; 2 | 3 | const preview: Preview = { 4 | parameters: { 5 | actions: { argTypesRegex: '^on[A-Z].*' }, 6 | controls: { 7 | // Description toggle 8 | // expanded: true, 9 | matchers: { 10 | color: /(background|color)$/i, 11 | date: /Date$/, 12 | }, 13 | }, 14 | }, 15 | }; 16 | 17 | // eslint-disable-next-line import-x/no-default-export 18 | export default preview; 19 | -------------------------------------------------------------------------------- /.storybook/theme.ts: -------------------------------------------------------------------------------- 1 | import { create } from '@storybook/theming'; 2 | 3 | export const theme = create({ 4 | base: 'light', 5 | brandTitle: 'React DemoTab 📑', 6 | brandUrl: 'https://github.com/demozap/react-demo-tab', 7 | }); 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Marko Kosir 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 | # React DemoTab 📑 2 | 3 | [![npm version][npm-badge]][npm-url] 4 | [![CI][build-badge]][build-url] 5 | [![semantic-release][semantic-badge]][semantic-url] 6 | [![TypeScript][typescript-badge]][typescript-url] 7 | 8 | _Easily create React demo components_ 9 | 10 | ## [Demo](https://demozap.github.io/react-demo-tab) 11 | 12 | ## Install 13 | 14 | ```bash 15 | npm install react-demo-tab 16 | ``` 17 | 18 | ## Example 19 | 20 | [![](misc/demo-tab.gif)](https://demozap.github.io/react-demo-tab/?path=/story/examples--button-green) 21 | 22 | ```jsx 23 | import ReactDOM from 'react-dom'; 24 | import DemoTab from 'react-demo-tab'; 25 | import DemoComponent from './ButtonGreen'; 26 | 27 | const demoCode = ` 28 | import './ButtonGreen.css'; 29 | 30 | const ButtonGreen = () => ; 31 | export default ButtonGreen;`; 32 | 33 | const demoStyle = ` 34 | .btn-green { 35 | background-color: green; 36 | font-size: 14px; 37 | padding: 12px 26px; 38 | border-radius: 6px; 39 | }`; 40 | 41 | const App = () => { 42 | return ( 43 | 44 | 45 | 46 | ); 47 | }; 48 | 49 | ReactDOM.render(, document.getElementById('root')); 50 | ``` 51 | 52 | ## Props 53 | 54 | Create demo of component that is passed as a child. 55 | 56 | Below is the complete list of possible props and their options: 57 | 58 | > ▶︎ indicates optional prop with default value 59 | 60 | **code**: string 61 | Demo code. Required. 62 | 63 | **style**: string ▶︎ `undefined` 64 | Demo style. 65 | 66 | **codeExt**: 'jsx' | 'tsx' ▶︎ `jsx` 67 | Code file extension for image to be displayed. 68 | 69 | **styleExt**: 'css' | 'scss' ▶︎ `css` 70 | Style file extension for image to be displayed. 71 | 72 | ## Create demos with CLI 73 | 74 | Instead of manually creating demos, automate the process with [DemoZap CLI](https://github.com/demozap/demozap). 75 | 76 | ## Development 77 | 78 | _Easily set up a local development environment!_ 79 | 80 | Build project and start storybook on [localhost](http://localhost:9009): 81 | 82 | - clone 83 | - `npm install` 84 | - `npm start` 85 | 86 | **Start coding!** 🎉 87 | 88 | ## Built with DemoTab 89 | 90 | - [React Tilt](https://github.com/mkosir/react-parallax-tilt) - [DemoTab](https://mkosir.github.io/react-parallax-tilt) 91 | - [Mighty Mouse](https://github.com/mkosir/react-hook-mighty-mouse) - [DemoTab](https://mkosir.github.io/react-hook-mighty-mouse) 92 | - [Magnetic Board](https://github.com/mkosir/react-magnetic-board) - [DemoTab](https://mkosir.github.io/react-magnetic-board) 93 | 94 | ## Contributing 95 | 96 | All contributions are welcome! 97 | 98 | [npm-url]: https://www.npmjs.com/package/react-demo-tab 99 | [npm-badge]: https://img.shields.io/npm/v/react-demo-tab.svg 100 | [build-badge]: https://github.com/demozap/react-demo-tab/actions/workflows/main.yml/badge.svg 101 | [build-url]: https://github.com/demozap/react-demo-tab/actions/workflows/main.yml 102 | [semantic-badge]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg 103 | [semantic-url]: https://github.com/semantic-release/semantic-release 104 | [typescript-badge]: https://badges.frapsoft.com/typescript/code/typescript.svg?v=101 105 | [typescript-url]: https://github.com/microsoft/TypeScript 106 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from '@eslint/js'; 2 | import eslintConfigPrettier from 'eslint-config-prettier'; 3 | import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'; 4 | import * as eslintPluginImportX from 'eslint-plugin-import-x'; 5 | import eslintPluginJsxA11y from 'eslint-plugin-jsx-a11y'; 6 | import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; 7 | import eslintPluginReact from 'eslint-plugin-react'; 8 | import * as eslintPluginReactHooks from 'eslint-plugin-react-hooks'; 9 | import * as tseslint from 'typescript-eslint'; 10 | 11 | export default tseslint.config( 12 | eslint.configs.recommended, 13 | 14 | eslintPluginImportX.flatConfigs.recommended, 15 | eslintPluginImportX.flatConfigs.typescript, 16 | ...tseslint.configs.strictTypeChecked, 17 | ...tseslint.configs.stylisticTypeChecked, 18 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access 19 | eslintPluginJsxA11y.flatConfigs.recommended, 20 | eslintPluginReact.configs.flat.recommended, 21 | eslintPluginPrettierRecommended, 22 | eslintConfigPrettier, 23 | // https://eslint.org/docs/latest/use/configure/configuration-files#excluding-files-with-ignores 24 | // When in their own config block without files, it tells ESLint to ignore those files. 25 | // When in a config block with files, it stops that specific config block from affecting those ignored files. 26 | { 27 | ignores: ['!.*', 'node_modules', 'dist', 'compiled', 'build'], 28 | }, 29 | { 30 | languageOptions: { 31 | parserOptions: { 32 | projectService: true, 33 | tsconfigRootDir: import.meta.dirname, 34 | }, 35 | }, 36 | settings: { 37 | 'import-x/resolver-next': [createTypeScriptImportResolver()], 38 | react: { version: 'detect' }, 39 | }, 40 | }, 41 | { 42 | files: ['**/*.{js,ts,jsx,tsx}'], 43 | 44 | plugins: { 45 | 'react-hooks': eslintPluginReactHooks, 46 | }, 47 | 48 | rules: { 49 | ...eslintPluginReactHooks.configs.recommended.rules, 50 | 51 | 'react/react-in-jsx-scope': 'off', 52 | 53 | 'prefer-template': 'error', 54 | 'no-nested-ternary': 'error', 55 | 'no-unneeded-ternary': 'error', 56 | 'spaced-comment': 'error', 57 | 'no-console': 'error', 58 | 59 | '@typescript-eslint/ban-ts-comment': ['error', { 'ts-expect-error': 'allow-with-description' }], 60 | '@typescript-eslint/consistent-type-definitions': ['error', 'type'], 61 | '@typescript-eslint/array-type': ['error', { default: 'generic' }], 62 | '@typescript-eslint/prefer-nullish-coalescing': 'error', 63 | '@typescript-eslint/no-unnecessary-condition': 'error', 64 | '@typescript-eslint/no-confusing-void-expression': ['error', { ignoreArrowShorthand: true }], 65 | '@typescript-eslint/restrict-plus-operands': 'off', 66 | '@typescript-eslint/restrict-template-expressions': 'off', 67 | '@typescript-eslint/consistent-type-imports': 'error', 68 | '@typescript-eslint/naming-convention': [ 69 | 'error', 70 | { 71 | selector: 'typeAlias', 72 | format: ['PascalCase'], 73 | }, 74 | { 75 | // Generic type parameter must start with letter T, followed by any uppercase letter. 76 | selector: 'typeParameter', 77 | format: ['PascalCase'], 78 | custom: { regex: '^T[A-Z]', match: true }, 79 | }, 80 | ], 81 | 82 | 'import-x/no-default-export': 'error', 83 | 'import-x/order': [ 84 | 'error', 85 | { 86 | groups: ['builtin', 'external', 'internal', 'parent', 'sibling'], 87 | 'newlines-between': 'always', 88 | alphabetize: { 89 | order: 'asc', 90 | caseInsensitive: true, 91 | }, 92 | }, 93 | ], 94 | }, 95 | }, 96 | { 97 | files: ['stories/**/*'], 98 | rules: { 99 | 'import-x/no-default-export': 'off', 100 | }, 101 | }, 102 | ); 103 | -------------------------------------------------------------------------------- /misc/demo-tab.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demozap/react-demo-tab/c183c188aaf8896d6d39eb7c4737690625c9085f/misc/demo-tab.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-demo-tab", 3 | "version": "0.0.0", 4 | "description": "Easily create React demo components", 5 | "main": "./dist/index.umd.js", 6 | "module": "./dist/index.esm.js", 7 | "types": "./dist/index.d.ts", 8 | "files": [ 9 | "dist", 10 | "README.md" 11 | ], 12 | "scripts": { 13 | "start": "npm run storybook", 14 | "prepare": "husky", 15 | "build": "rm -rf dist && rollup --config scripts/rollup.config.ts --configPlugin typescript --configImportAttributesKey with", 16 | "lint": "eslint --report-unused-disable-directives --max-warnings 0 .", 17 | "lint-fix": "eslint --fix .", 18 | "lint-staged-husky": "lint-staged", 19 | "tsc": "tsc -p scripts/tsconfig.dev.json", 20 | "format-lint": "prettier --config .prettierrc --check --ignore-unknown .", 21 | "format-fix": "prettier --config .prettierrc --write --ignore-unknown -l .", 22 | "commit": "git-cz", 23 | "storybook": "TS_NODE_PROJECT=scripts/tsconfig.dev.json storybook dev -p 9009 --quiet", 24 | "storybook-build": "storybook build -o build-storybook-static", 25 | "release": "semantic-release --branches main", 26 | "clean": "rm -rf node_modules coverage dist compiled build-storybook-static" 27 | }, 28 | "devDependencies": { 29 | "@commitlint/cli": "19.8.1", 30 | "@commitlint/config-conventional": "19.8.1", 31 | "@eslint/js": "9.26.0", 32 | "@rollup/plugin-commonjs": "28.0.3", 33 | "@rollup/plugin-image": "3.0.3", 34 | "@rollup/plugin-terser": "0.4.4", 35 | "@rollup/plugin-typescript": "11.1.6", 36 | "@storybook/addon-styling": "2.0.0", 37 | "@storybook/addon-webpack5-compiler-swc": "1.0.5", 38 | "@storybook/manager-api": "8.4.6", 39 | "@storybook/react": "8.4.6", 40 | "@storybook/react-webpack5": "8.4.6", 41 | "@storybook/theming": "8.4.6", 42 | "@types/eslint": "9.6.1", 43 | "@types/react": "19.1.3", 44 | "@types/react-dom": "19.1.3", 45 | "@types/react-syntax-highlighter": "15.5.13", 46 | "commitizen": "4.3.1", 47 | "eslint": "9.26.0", 48 | "eslint-config-prettier": "10.1.5", 49 | "eslint-import-resolver-typescript": "4.3.4", 50 | "eslint-plugin-import-x": "4.11.1", 51 | "eslint-plugin-jsx-a11y": "6.10.2", 52 | "eslint-plugin-no-array-reduce": "1.0.62", 53 | "eslint-plugin-prettier": "5.4.0", 54 | "eslint-plugin-react": "7.37.5", 55 | "eslint-plugin-react-hooks": "5.2.0", 56 | "husky": "9.1.7", 57 | "lint-staged": "16.0.0", 58 | "prettier": "3.5.3", 59 | "react": "18.3.1", 60 | "react-dom": "18.3.1", 61 | "rollup": "4.40.2", 62 | "rollup-plugin-dts": "6.2.1", 63 | "sass": "1.88.0", 64 | "sass-loader": "16.0.5", 65 | "semantic-release": "24.2.3", 66 | "storybook": "8.4.6", 67 | "typescript": "5.8.3", 68 | "typescript-eslint": "8.32.0" 69 | }, 70 | "peerDependencies": { 71 | "react": ">=18.0.0", 72 | "react-dom": ">=18.0.0" 73 | }, 74 | "dependencies": { 75 | "@rehooks/local-storage": "2.4.5", 76 | "react-syntax-highlighter": "15.6.1", 77 | "react-tabs": "6.1.0" 78 | }, 79 | "license": "MIT", 80 | "author": "Marko Kosir ", 81 | "homepage": "https://github.com/demozap/react-demo-tab", 82 | "repository": { 83 | "type": "git", 84 | "url": "git+https://github.com/demozap/react-demo-tab.git" 85 | }, 86 | "bugs": { 87 | "url": "https://github.com/demozap/react-demo-tab/issues" 88 | }, 89 | "keywords": [ 90 | "react", 91 | "react-component", 92 | "demo-component", 93 | "demozap", 94 | "demo", 95 | "demo-tab", 96 | "demotab", 97 | "code", 98 | "tab" 99 | ] 100 | } 101 | -------------------------------------------------------------------------------- /scripts/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | import image from '@rollup/plugin-image'; 3 | import terser from '@rollup/plugin-terser'; 4 | import typescript from '@rollup/plugin-typescript'; 5 | import { defineConfig } from 'rollup'; 6 | import { dts } from 'rollup-plugin-dts'; 7 | 8 | import packageJson from '../package.json' with { type: 'json' }; 9 | 10 | import tsConfig from './tsconfig.base.json' with { type: 'json' }; 11 | 12 | const IS_PRODUCTION = process.env.NODE_ENV === 'production'; 13 | 14 | const PATH_INPUT_FILE = 'src/index.ts'; 15 | const PATH_TSCONFIG_BUILD = 'scripts/tsconfig.build.json'; 16 | 17 | const rollupConfig = defineConfig([ 18 | { 19 | input: PATH_INPUT_FILE, 20 | output: [ 21 | { 22 | file: packageJson.main, 23 | name: packageJson.name, 24 | format: 'umd', 25 | sourcemap: !IS_PRODUCTION, 26 | globals: { 27 | react: 'React', 28 | 'react-dom': 'ReactDOM', 29 | 'react-tabs': 'react-tabs', 30 | 'react-syntax-highlighter': 'react-syntax-highlighter', 31 | '@rehooks/local-storage': '@rehooks/local-storage', 32 | 'react/jsx-runtime': 'jsxRuntime', 33 | 'react-syntax-highlighter/dist/esm/styles/prism': 'prism', 34 | }, 35 | }, 36 | { 37 | file: packageJson.module, 38 | format: 'esm', 39 | sourcemap: !IS_PRODUCTION, 40 | }, 41 | ], 42 | plugins: [ 43 | commonjs(), 44 | typescript({ 45 | tsconfig: PATH_TSCONFIG_BUILD, 46 | sourceMap: !IS_PRODUCTION, 47 | }), 48 | terser({ 49 | output: { comments: false }, 50 | compress: { 51 | pure_getters: true, 52 | }, 53 | toplevel: true, 54 | }), 55 | image(), 56 | ], 57 | external: [ 58 | // Ensure dependencies are not bundled with the library 59 | ...Object.keys(packageJson.peerDependencies), 60 | ...Object.keys(packageJson.dependencies), 61 | 'react-tabs/style/react-tabs.css', 62 | 'react/jsx-runtime', 63 | 'react-syntax-highlighter/dist/esm/styles/prism', 64 | ], 65 | }, 66 | { 67 | input: PATH_INPUT_FILE, 68 | output: { file: packageJson.types, format: 'esm' }, 69 | plugins: [ 70 | dts({ 71 | compilerOptions: { 72 | baseUrl: './src', 73 | paths: tsConfig.compilerOptions.paths, 74 | }, 75 | }), 76 | ], 77 | external: ['react-tabs/style/react-tabs.css'], 78 | }, 79 | ]); 80 | 81 | // eslint-disable-next-line import-x/no-default-export 82 | export default rollupConfig; 83 | -------------------------------------------------------------------------------- /scripts/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "outDir": "../compiled", 5 | "baseUrl": "../src", 6 | "paths": { 7 | ".": ["."] 8 | }, 9 | "importHelpers": true, 10 | "jsx": "react-jsx", 11 | "lib": ["ESNext", "DOM"], 12 | "module": "ESNext", 13 | "moduleResolution": "Bundler", 14 | "target": "ES2020", 15 | "allowSyntheticDefaultImports": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "resolveJsonModule": true, 19 | "pretty": true, 20 | "sourceMap": false, 21 | "strict": true, 22 | "stripInternal": true, 23 | "forceConsistentCasingInFileNames": true, 24 | "skipLibCheck": true, 25 | "esModuleInterop": true, 26 | "allowJs": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /scripts/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": ["../src/**/*"], 4 | "exclude": ["../src/**/*.test.ts", "../src/**/*.test.tsx"] 5 | } 6 | -------------------------------------------------------------------------------- /scripts/tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | // Type check and lint all files and folders. 4 | // Explicitly include those starting with a dot (.) as they are ignored by default in glob patterns. 5 | "include": ["../.", "../.storybook/**/*", "../.commitlintrc.ts"], 6 | "exclude": ["../dist", "../node_modules"] 7 | } 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { DemoTab } from 'react-demo-tab/DemoTab'; 2 | 3 | // Public exposed library types 4 | export * from 'react-demo-tab/types.public'; 5 | -------------------------------------------------------------------------------- /src/react-demo-tab/DemoTab.tsx: -------------------------------------------------------------------------------- 1 | import { useLocalStorage } from '@rehooks/local-storage'; 2 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; 3 | import { prism } from 'react-syntax-highlighter/dist/esm/styles/prism'; 4 | import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; 5 | import 'react-tabs/style/react-tabs.css'; 6 | 7 | import styleImgCSS from './img/css.png'; 8 | import codeImg from './img/jsx.png'; 9 | import styleImgSass from './img/sass.png'; 10 | import type { LocalStorage } from './types'; 11 | import type { DemoTabProps } from './types.public'; 12 | 13 | const localStorageKey = 'react-demo-tab'; 14 | 15 | export const DemoTab = ({ code, style, codeExt = 'jsx', styleExt = 'css', children }: DemoTabProps) => { 16 | const [tabIndex, setTabIndex] = useLocalStorage(localStorageKey, { mainTabIndex: 0, codeTabIndex: 0 }); 17 | 18 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 19 | const styleImg = styleExt === 'css' ? styleImgCSS : styleImgSass; 20 | 21 | return ( 22 | setTabIndex({ mainTabIndex: index, codeTabIndex: tabIndex.codeTabIndex })} 25 | > 26 | 27 | Demo 28 | Code 29 | 30 | {children} 31 | 32 | setTabIndex({ mainTabIndex: tabIndex.mainTabIndex, codeTabIndex: index })} 35 | > 36 | 37 | 38 | {/* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment */} 39 | code-img 40 | 41 | {style && ( 42 | 43 | {/* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment */} 44 | style-img 45 | 46 | )} 47 | 48 | 49 | 50 | {code} 51 | 52 | 53 | {style && ( 54 | 55 | 56 | {style} 57 | 58 | 59 | )} 60 | 61 | 62 | 63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /src/react-demo-tab/img/css.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demozap/react-demo-tab/c183c188aaf8896d6d39eb7c4737690625c9085f/src/react-demo-tab/img/css.png -------------------------------------------------------------------------------- /src/react-demo-tab/img/jsx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demozap/react-demo-tab/c183c188aaf8896d6d39eb7c4737690625c9085f/src/react-demo-tab/img/jsx.png -------------------------------------------------------------------------------- /src/react-demo-tab/img/sass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demozap/react-demo-tab/c183c188aaf8896d6d39eb7c4737690625c9085f/src/react-demo-tab/img/sass.png -------------------------------------------------------------------------------- /src/react-demo-tab/types.public.ts: -------------------------------------------------------------------------------- 1 | export type DemoTabProps = { 2 | /** 3 | * Demo component. 4 | */ 5 | children: React.ReactNode; 6 | /** 7 | * Demo code. 8 | */ 9 | code: string; 10 | /** 11 | * Demo style. 12 | */ 13 | style?: string; 14 | /** 15 | * Code file extension for image to be displayed. 16 | */ 17 | codeExt?: 'jsx' | 'tsx'; 18 | /** 19 | * Style file extension for image to be displayed. 20 | */ 21 | styleExt?: 'css' | 'scss'; 22 | }; 23 | -------------------------------------------------------------------------------- /src/react-demo-tab/types.ts: -------------------------------------------------------------------------------- 1 | export type LocalStorage = { 2 | mainTabIndex: number; 3 | codeTabIndex: number; 4 | }; 5 | -------------------------------------------------------------------------------- /src/types/images.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png'; 2 | -------------------------------------------------------------------------------- /stories/ButtonGreen/ButtonGreen.css: -------------------------------------------------------------------------------- 1 | .btn-green { 2 | background-color: green; 3 | font-size: 14px; 4 | padding: 12px 26px; 5 | border-radius: 6px; 6 | cursor: pointer; 7 | -webkit-transition-duration: 0.2s; 8 | transition-duration: 0.2s; 9 | } 10 | 11 | .btn-green:hover { 12 | background-color: #4caf50; 13 | } 14 | -------------------------------------------------------------------------------- /stories/ButtonGreen/ButtonGreen.tsx: -------------------------------------------------------------------------------- 1 | import './ButtonGreen.css'; 2 | 3 | const ButtonGreen = () => ; 4 | 5 | export default ButtonGreen; 6 | -------------------------------------------------------------------------------- /stories/ButtonGreen/_ButtonGreen.tsx: -------------------------------------------------------------------------------- 1 | import { DemoTab } from 'index'; 2 | 3 | import Demo from './ButtonGreen'; 4 | 5 | const code = `import './ButtonGreen.css'; 6 | 7 | const ButtonGreen = () => ; 8 | 9 | export default ButtonGreen;`; 10 | 11 | const style = `.btn-green { 12 | background-color: green; 13 | font-size: 14px; 14 | padding: 12px 26px; 15 | border-radius: 6px; 16 | cursor: pointer; 17 | -webkit-transition-duration: 0.2s; 18 | transition-duration: 0.2s; 19 | } 20 | 21 | .btn-green:hover { 22 | background-color: #4caf50; 23 | }`; 24 | 25 | export const _ButtonGreen = () => ( 26 | 27 | 28 | 29 | ); 30 | -------------------------------------------------------------------------------- /stories/ButtonRed/ButtonRed.scss: -------------------------------------------------------------------------------- 1 | .btn-red { 2 | background-color: red; 3 | font-size: 14px; 4 | padding: 12px 26px; 5 | border-radius: 6px; 6 | cursor: pointer; 7 | -webkit-transition-duration: 0.2s; 8 | transition-duration: 0.2s; 9 | 10 | &:hover { 11 | background-color: #ca1f1f; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /stories/ButtonRed/ButtonRed.tsx: -------------------------------------------------------------------------------- 1 | import './ButtonRed.scss'; 2 | 3 | const ButtonRed = () => ; 4 | 5 | export default ButtonRed; 6 | -------------------------------------------------------------------------------- /stories/ButtonRed/_ButtonRed.tsx: -------------------------------------------------------------------------------- 1 | import { DemoTab } from 'index'; 2 | 3 | import Demo from './ButtonRed'; 4 | 5 | const code = `import './ButtonRed.scss'; 6 | 7 | const ButtonRed = () => ; 8 | 9 | export default ButtonRed;`; 10 | 11 | const style = `.btn-red { 12 | background-color: red; 13 | font-size: 14px; 14 | padding: 12px 26px; 15 | border-radius: 6px; 16 | cursor: pointer; 17 | -webkit-transition-duration: 0.2s; 18 | transition-duration: 0.2s; 19 | 20 | &:hover { 21 | background-color: #ca1f1f; 22 | } 23 | }`; 24 | 25 | export const _ButtonRed = () => ( 26 | 27 | 28 | 29 | ); 30 | -------------------------------------------------------------------------------- /stories/Counter/Counter.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | const Counter = () => { 4 | const [count, setCount] = useState(0); 5 | 6 | const onCountInc = () => setCount(count + 1); 7 | 8 | const msg = `Button clicked ${count.toString()} ${count > 1 ? 'times' : 'time'}`; 9 | const marginBottom = 10; 10 | 11 | return ( 12 | <> 13 | 19 |
{msg}
20 | 21 | ); 22 | }; 23 | 24 | export default Counter; 25 | -------------------------------------------------------------------------------- /stories/Counter/_Counter.tsx: -------------------------------------------------------------------------------- 1 | import { DemoTab } from 'index'; 2 | 3 | import Demo from './Counter'; 4 | 5 | const code = `import { useState } from 'react'; 6 | 7 | const Counter = () => { 8 | const [count, setCount] = useState(0); 9 | 10 | const onCountInc = () => setCount(count + 1); 11 | 12 | const msg = \`Button clicked \${count.toString()} \${count > 1 ? 'times' : 'time'}\`; 13 | const marginBottom = 10; 14 | 15 | return ( 16 | <> 17 | 23 |
{msg}
24 | 25 | ); 26 | }; 27 | 28 | export default Counter;`; 29 | 30 | export const _Counter = () => ( 31 | 32 | 33 | 34 | ); 35 | -------------------------------------------------------------------------------- /stories/Default/Default.jsx: -------------------------------------------------------------------------------- 1 | const Default = () =>

DemoTab 📑

; 2 | 3 | export default Default; 4 | -------------------------------------------------------------------------------- /stories/Default/_Default.tsx: -------------------------------------------------------------------------------- 1 | import { DemoTab } from 'index'; 2 | 3 | import Demo from './Default'; 4 | 5 | const code = `const Default = () =>

DemoTab 📑

; 6 | 7 | export default Default;`; 8 | 9 | export const _Default = () => ( 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /stories/DemoTab.scss: -------------------------------------------------------------------------------- 1 | #root { 2 | font-family: Arial, sans-serif; 3 | 4 | .tab-demo.react-tabs__tab-panel--selected { 5 | margin-top: 10vh; 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: center; 9 | align-items: center; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /stories/DemoTab.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from '@storybook/react'; 2 | 3 | import { _ButtonGreen } from './ButtonGreen/_ButtonGreen'; 4 | import { _ButtonRed } from './ButtonRed/_ButtonRed'; 5 | import { _Counter } from './Counter/_Counter'; 6 | import { _Default } from './Default/_Default'; 7 | 8 | import './DemoTab.scss'; 9 | 10 | const meta: Meta = { 11 | title: 'Examples', 12 | }; 13 | 14 | export default meta; 15 | 16 | export const Default = () => <_Default />; 17 | 18 | export const ButtonGreen = () => <_ButtonGreen />; 19 | ButtonGreen.storyName = 'Button Green - css'; 20 | 21 | export const ButtonRed = () => <_ButtonRed />; 22 | ButtonRed.storyName = 'Button Red - scss'; 23 | 24 | export const Counter = () => <_Counter />; 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./scripts/tsconfig.dev.json" 3 | } 4 | --------------------------------------------------------------------------------