├── .eslintrc.js ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .husky └── pre-commit ├── .node-version ├── .prettierrc ├── .storybook ├── main.ts └── preview.ts ├── LICENSE ├── README.md ├── hero.gif ├── package.json ├── pnpm-lock.yaml ├── src ├── Resizable.tsx ├── constants.ts ├── index.ts ├── types.ts └── useResizable.ts ├── stories ├── AxisX.stories.tsx ├── AxisXReverse.stories.tsx ├── AxisXReverseWithContainer.stories.tsx ├── AxisXWithContainer.stories.tsx ├── AxisY.stories.tsx ├── AxisYReverse.stories.tsx ├── AxisYReverseWithContainer.stories.tsx ├── AxisYWithContainer.stories.tsx ├── Callback.stories.tsx ├── Disabled.stories.tsx ├── DraggingState.stories.tsx ├── IdeClone.stories.tsx ├── VirtualSplitter.stories.tsx ├── components │ ├── IdeClone.tsx │ ├── SampleBox.tsx │ └── SampleSeparator.tsx ├── style │ ├── IdeClone.css │ ├── SampleBox.css │ └── SampleSeparator.css └── utils │ └── cn.ts ├── tsconfig.build.json ├── tsconfig.json └── vite.config.ts /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'airbnb', 4 | 'airbnb-typescript', 5 | 'airbnb/hooks', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:@typescript-eslint/eslint-recommended', 8 | 'plugin:storybook/recommended', 9 | 'prettier', 10 | ], 11 | plugins: ['@typescript-eslint', 'jsx-a11y', 'react', 'react-hooks'], 12 | parser: '@typescript-eslint/parser', 13 | env: { 14 | browser: true, 15 | node: true, 16 | es6: true, 17 | }, 18 | parserOptions: { 19 | sourceType: 'module', 20 | ecmaFeatures: { 21 | jsx: true, 22 | }, 23 | project: './tsconfig.json', 24 | }, 25 | settings: { 26 | react: { 27 | version: '18', 28 | }, 29 | }, 30 | rules: { 31 | 'react/prop-types': 'off', 32 | 'react/react-in-jsx-scope': 'off', 33 | 'react/jsx-no-target-blank': 2, 34 | 'react/jsx-props-no-spreading': 'off', 35 | 'react/jsx-sort-props': [ 36 | 'error', 37 | { 38 | shorthandLast: true, 39 | callbacksLast: true, 40 | multiline: 'ignore', 41 | ignoreCase: false, 42 | noSortAlphabetically: true, 43 | reservedFirst: ['key', 'ref'], 44 | }, 45 | ], 46 | 'react/function-component-definition': 'off', 47 | 'react/require-default-props': 'off', 48 | '@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }], 49 | '@typescript-eslint/no-explicit-any': 'off', 50 | '@typescript-eslint/explicit-module-boundary-types': 'off', 51 | '@typescript-eslint/space-before-blocks': 'off', 52 | 'object-shorthand': ['error', 'properties'], 53 | 'no-console': [ 54 | 'error', 55 | { 56 | allow: ['warn', 'error'], 57 | }, 58 | ], 59 | 'no-restricted-exports': 'off', 60 | 'consistent-return': 'off', 61 | 'import/order': [ 62 | 'error', 63 | { 64 | groups: [ 65 | 'builtin', 66 | 'external', 67 | 'internal', 68 | ['parent', 'sibling'], 69 | 'object', 70 | 'index', 71 | 'type', 72 | ], 73 | 'newlines-between': 'always', 74 | alphabetize: { order: 'asc', caseInsensitive: true }, 75 | pathGroups: [ 76 | { 77 | pattern: '{react,react-dom}', 78 | group: 'builtin', 79 | position: 'before', 80 | }, 81 | ], 82 | pathGroupsExcludedImportTypes: ['builtin', 'object'], 83 | }, 84 | ], 85 | 'import/prefer-default-export': 'off', 86 | 'import/no-extraneous-dependencies': 'off', 87 | 'jsx-quotes': ['error', 'prefer-double'], 88 | }, 89 | ignorePatterns: ['.eslintrc.*', 'vite.config.ts'], 90 | overrides: [ 91 | { 92 | files: ['*.stories.tsx'], 93 | rules: { 94 | 'max-lines': 'off', 95 | 'no-console': 'off', 96 | 'no-alert': 'off', 97 | 'react-hooks/rules-of-hooks': 'off', 98 | }, 99 | }, 100 | ], 101 | }; 102 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | pull_request: 4 | types: [opened, synchronize] 5 | jobs: 6 | build: 7 | name: test 8 | runs-on: ubuntu-latest 9 | timeout-minutes: 30 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Setup pnpm 13 | uses: pnpm/action-setup@v4 14 | with: 15 | version: 9 16 | - name: Use Node.js 20.x 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: 20 20 | cache: pnpm 21 | - run: pnpm install 22 | - run: pnpm test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | lib 4 | 5 | # misc 6 | .DS_Store 7 | *.pem 8 | 9 | # debug 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # local env files 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | .env 20 | 21 | #idea 22 | .idea 23 | 24 | #build-storybook 25 | docs 26 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | pnpm lint-staged 5 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 20.18.0 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "quoteProps": "as-needed", 8 | "jsxSingleQuote": false, 9 | "trailingComma": "all", 10 | "bracketSpacing": true, 11 | "bracketSameLine": false, 12 | "arrowParens": "always", 13 | "endOfLine": "lf" 14 | } 15 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import { mergeConfig } from 'vite'; 2 | 3 | import type { StorybookConfig } from '@storybook/react-vite'; 4 | 5 | const config: StorybookConfig = { 6 | stories: ['../stories/**/*.stories.@(ts|tsx)'], 7 | addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-storysource'], 8 | framework: '@storybook/react-vite', 9 | async viteFinal(config) { 10 | return mergeConfig(config, {}); 11 | }, 12 | }; 13 | 14 | export default config; 15 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import '../stories/style/SampleBox.css'; 2 | import '../stories/style/SampleSeparator.css'; 3 | import '../stories/style/IdeClone.css'; 4 | 5 | import { Parameters } from '@storybook/react'; 6 | 7 | export const parameters: Parameters = { 8 | layout: 'fullscreen', 9 | controls: { 10 | matchers: { 11 | color: /(background|color)$/i, 12 | date: /Date$/, 13 | }, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ryo Sogawa 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-resizable-layout 2 | 3 | [![npm version](https://badge.fury.io/js/react-resizable-layout.svg)](http://badge.fury.io/js/react-resizable-layout) 4 | [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE) 5 | [![minziggped size](https://badgen.net/bundlephobia/minzip/react-resizable-layout)](https://bundlephobia.com/package/react-resizable-layout) 6 | [![dependencies count](https://badgen.net/bundlephobia/dependency-count/react-resizable-layout)](https://bundlephobia.com/package/react-resizable-layout) 7 | [![tree shaking](https://badgen.net/bundlephobia/tree-shaking/react-resizable-layout)](https://bundlephobia.com/package/react-resizable-layout) 8 | 9 | A lightweight, accessible headless React component and hook for drag-and-drop resizable layouts. 10 | 11 | ![Hero Image](./hero.gif) 12 | 13 | [![Edit react-resizable-layout](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/react-resizable-layout-jy3vhk?fontsize=14&hidenavigation=1&theme=dark) 14 | 15 | [Storybook Demo](https://react-resizable-layout.vercel.app/) 16 | 17 | ## Features 18 | 19 | - 📦 Lightweight 20 | - 🕳 Headless 21 | - 🫶🏽 Accessible 22 | - 🤏 Drag and Drop Support 23 | - ⌨️ Keyboard Support 24 | - 🫙 Zero Dependencies 25 | 26 | 27 | ## Installation 28 | 29 | Install from npm: 30 | 31 | ``` 32 | # Using npm 33 | npm install react-resizable-layout 34 | 35 | # Using Yarn 36 | yarn add react-resizable-layout 37 | 38 | # Using pnpm 39 | pnpm add react-resizable-layout 40 | ``` 41 | 42 | 43 | ## Usage 44 | ### `Resizable` component 45 | 46 | ```tsx 47 | import Resizable from 'react-resizable-layout'; 48 | 49 | 50 | {({ position, separatorProps }) => ( 51 |
52 |
53 | 54 |
55 |
56 | )} 57 | 58 | ``` 59 | 60 | ### `useResizable` hook 61 | 62 | ```tsx 63 | import { useResizable } from 'react-resizable-layout'; 64 | 65 | const Component = () => { 66 | const { position, separatorProps } = useResizable({ 67 | axis: 'x', 68 | }) 69 | 70 | return ( 71 |
72 |
73 | 74 |
75 |
76 | ) 77 | } 78 | ``` 79 | 80 | 81 | ## Aria Props 82 | The following attributes are added to `separatorProps` in accordance with W3C. 83 | https://www.w3.org/TR/wai-aria-1.2/#separator 84 | 85 | | Attribute | Value | 86 | |------------------|--------------------------------| 87 | | role | `'separator'` | 88 | | aria-valuenow | `position` | 89 | | aria-valuemin | `props.min` | 90 | | aria-valuemax | `props.max` | 91 | | aria-orientation | `'vertical'` or `'horizontal'` | 92 | | aria-disabled | `props.disabled` | 93 | 94 | 95 | ## Configuration 96 | 97 | ### Common Props 98 | 99 | | Name | Type | Default | Required | Description | 100 | |---------------|-------------------------|----------|----------|---------------------------------------------------------------------------| 101 | | axis | 'x' or 'y' | - | ◯ | Resize direction | 102 | | containerRef | ReactRef | - | - | Reference to container for calculating position | 103 | | disabled | boolean | false | - | Disable resize | 104 | | initial | number | 0 | - | Initial size | 105 | | min | number | 0 | - | Minimum size | 106 | | max | number | Infinity | - | Maximum size | 107 | | reverse | boolean | false | - | If true, returns position of the opposite side | 108 | | step | number | 10 | - | Pixel steps when operating with keyboard | 109 | | shiftStep | number | 50 | - | Pixel steps when operating with keyboard while holding down the shift key | 110 | | onResizeStart | function | - | - | Callback on resize start | 111 | | onResizeEnd | function | - | - | Callback on resize end | 112 | 113 | ### `Resizable` component children args 114 | 115 | `useResizable` hook returns same. 116 | 117 | | Name | Type | Description | 118 | |----------------|----------|----------------------------------------------------------------| 119 | | position | number | Separator's position (Width for 'x' axis, height for 'y' axis) | 120 | | endPosition | number | Separator's position at end of drag | 121 | | isDragging | boolean | True if dragging | 122 | | separatorProps | object | Separator's props like onPointerDown | 123 | | setPosition | function | Set separator's position | 124 | 125 | 126 | ## About keyboard support 127 | The following keyboard operations are supported. 128 | 129 | | Key | Operation | 130 | |-----------------------------------|---------------------------------------------| 131 | | Arrow (`↑`,`→`,`↓`,`←`) | Move the separator by 10px (default) | 132 | | `Shift` + Arrow (`↑`,`→`,`↓`,`←`) | Move the separator by 50px (default) | 133 | | `Enter` | Reset the separator to the initial position | 134 | 135 | 136 | ## About mouse support 137 | Double-click on the separator to return it to its initial position. 138 | 139 | 140 | ## Contribution 141 | Feel free to open an issue or make a pull request. 142 | 143 | 144 | ## License 145 | Distributed under the MIT License. See [LICENSE](./LICENSE) for more information. 146 | -------------------------------------------------------------------------------- /hero.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RyoSogawa/react-resizable-layout/d8664d6185dd79d29ee7790e74d0c22be06c63c8/hero.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-resizable-layout", 3 | "description": "Lightweight, accessible headless React component and hook for drag-and-drop resizable layouts.", 4 | "author": "RyoSogawa", 5 | "version": "0.7.2", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+ssh://git@github.com/RyoSogawa/react-resizable-layout.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/RyoSogawa/react-resizable-layout/issues" 13 | }, 14 | "homepage": "https://github.com/RyoSogawa/react-resizable-layout", 15 | "keywords": [ 16 | "react", 17 | "react-hooks", 18 | "react-component", 19 | "headless", 20 | "dnd", 21 | "resize", 22 | "typescript" 23 | ], 24 | "files": [ 25 | "lib" 26 | ], 27 | "main": "./lib/index.js", 28 | "module": "./lib/index.module.js", 29 | "types": "./lib/index.d.ts", 30 | "scripts": { 31 | "prebuild": "rimraf ./lib", 32 | "build": "microbundle --jsx React.createElement --no-sourcemap --compress --tsconfig ./tsconfig.build.json", 33 | "format": "prettier --write \"(src|stories)/*.(js|ts|jsx|tsx)\"", 34 | "lint": "eslint ./src ./stories", 35 | "lint:fix": "pnpm lint --fix", 36 | "test": "tsc --noEmit && pnpm lint", 37 | "prepublishOnly": "pnpm test && pnpm build", 38 | "preversion": "pnpm lint", 39 | "version": "pnpm format && git add -A src", 40 | "postversion": "git push && git push --tags", 41 | "prepare": "husky install", 42 | "storybook": "storybook dev -p 6006", 43 | "build-storybook": "storybook build -o ./docs" 44 | }, 45 | "peerDependencies": { 46 | "react": ">=17.0.0", 47 | "react-dom": ">=17.0.0" 48 | }, 49 | "devDependencies": { 50 | "@storybook/addon-actions": "^8.4.5", 51 | "@storybook/addon-essentials": "^8.4.5", 52 | "@storybook/addon-links": "^8.4.5", 53 | "@storybook/addon-storysource": "^8.4.5", 54 | "@storybook/react": "^8.4.5", 55 | "@storybook/react-vite": "^8.4.5", 56 | "@storybook/test": "^8.4.5", 57 | "@types/react": "^18.3.12", 58 | "@types/react-dom": "^18.3.1", 59 | "@typescript-eslint/eslint-plugin": "^5.62.0", 60 | "@typescript-eslint/parser": "^5.62.0", 61 | "@vitejs/plugin-react": "^4.3.3", 62 | "babel-loader": "^8.4.1", 63 | "eslint": "^8.57.1", 64 | "eslint-config-airbnb": "^19.0.4", 65 | "eslint-config-airbnb-typescript": "^17.1.0", 66 | "eslint-config-prettier": "^8.10.0", 67 | "eslint-plugin-import": "^2.31.0", 68 | "eslint-plugin-jsx-a11y": "^6.10.2", 69 | "eslint-plugin-react": "^7.37.2", 70 | "eslint-plugin-react-hooks": "^4.6.2", 71 | "eslint-plugin-storybook": "0.9.0", 72 | "husky": "^7.0.0", 73 | "lint-staged": "^12.3.3", 74 | "microbundle": "^0.15.0", 75 | "prettier": "^2.8.8", 76 | "react": "^18.3.1", 77 | "react-dom": "^18.3.1", 78 | "rimraf": "^6.0.1", 79 | "storybook": "^8.4.5", 80 | "typescript": "^5.7.2", 81 | "vite": "^4.2.1" 82 | }, 83 | "lint-staged": { 84 | "*.{js,ts,jsx,tsx}": [ 85 | "eslint --fix", 86 | "prettier --write" 87 | ] 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Resizable.tsx: -------------------------------------------------------------------------------- 1 | import useResizable from './useResizable'; 2 | 3 | import type { ResizableProps } from './types'; 4 | 5 | const Resizable = ({ 6 | axis, 7 | disabled = false, 8 | initial = 0, 9 | min = 0, 10 | max = Infinity, 11 | reverse, 12 | onResizeStart, 13 | onResizeEnd, 14 | children, 15 | containerRef, 16 | }: ResizableProps): JSX.Element => { 17 | const resizable = useResizable({ 18 | axis, 19 | disabled, 20 | initial, 21 | min, 22 | max, 23 | reverse, 24 | onResizeStart, 25 | onResizeEnd, 26 | containerRef, 27 | }); 28 | 29 | return children(resizable); 30 | }; 31 | 32 | export default Resizable; 33 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const KEYS_LEFT = ['ArrowLeft', 'Left']; 2 | export const KEYS_RIGHT = ['ArrowRight', 'Right']; 3 | export const KEYS_UP = ['ArrowUp', 'Up']; 4 | export const KEYS_DOWN = ['ArrowDown', 'Down']; 5 | export const KEYS_AXIS_X = [...KEYS_LEFT, ...KEYS_RIGHT]; 6 | export const KEYS_AXIS_Y = [...KEYS_UP, ...KEYS_DOWN]; 7 | export const KEYS_POSITIVE = [...KEYS_RIGHT, ...KEYS_DOWN]; 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Resizable from './Resizable'; 2 | import useResizable from './useResizable'; 3 | 4 | export default Resizable; 5 | export { useResizable }; 6 | export * from './types'; 7 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type React from 'react'; 2 | 3 | export type SeparatorProps = React.ComponentPropsWithoutRef<'hr'>; 4 | 5 | /** 6 | * @deprecated Use SeparatorProps instead 7 | */ 8 | export type SplitterProps = SeparatorProps; 9 | 10 | export type Resizable = { 11 | /** 12 | * border position 13 | */ 14 | position: number; 15 | /** 16 | * position at end of drag 17 | */ 18 | endPosition: number; 19 | /** 20 | * whether the border is dragging 21 | */ 22 | isDragging: boolean; 23 | /** 24 | * props for drag bar 25 | */ 26 | separatorProps: SeparatorProps; 27 | /** 28 | * set border position 29 | */ 30 | setPosition: React.Dispatch>; 31 | /** 32 | * @deprecated Use separatorProps instead 33 | */ 34 | splitterProps: SplitterProps; 35 | }; 36 | 37 | export type ResizeCallbackArgs = { 38 | /** 39 | * position at the time of callback 40 | */ 41 | position: number; 42 | }; 43 | 44 | export type UseResizableProps = { 45 | /** 46 | * direction of resizing 47 | */ 48 | axis: 'x' | 'y'; 49 | /** 50 | * ref of the container element 51 | */ 52 | containerRef?: React.RefObject; 53 | /** 54 | * if true, cannot resize 55 | */ 56 | disabled?: boolean; 57 | /** 58 | * initial border position 59 | */ 60 | initial?: number; 61 | /** 62 | * minimum border position 63 | */ 64 | min?: number; 65 | /** 66 | * maximum border position 67 | */ 68 | max?: number; 69 | /** 70 | * calculate border position from other side 71 | */ 72 | reverse?: boolean; 73 | /** 74 | * resizing step with keyboard 75 | */ 76 | step?: number; 77 | shiftStep?: number; 78 | /** 79 | * callback when border position changes start 80 | */ 81 | onResizeStart?: (args: ResizeCallbackArgs) => void; 82 | /** 83 | * callback when border position changes end 84 | */ 85 | onResizeEnd?: (args: ResizeCallbackArgs) => void; 86 | }; 87 | 88 | export type ResizableProps = UseResizableProps & { 89 | /** 90 | * callback children 91 | */ 92 | children: (props: Resizable) => JSX.Element; 93 | }; 94 | -------------------------------------------------------------------------------- /src/useResizable.ts: -------------------------------------------------------------------------------- 1 | import type React from 'react'; 2 | import { useCallback, useMemo, useRef, useState } from 'react'; 3 | 4 | import { KEYS_AXIS_X, KEYS_AXIS_Y, KEYS_POSITIVE } from './constants'; 5 | 6 | import type { Resizable, SeparatorProps, UseResizableProps } from './types'; 7 | 8 | const useResizable = ({ 9 | axis, 10 | disabled = false, 11 | initial = 0, 12 | min = 0, 13 | max = Infinity, 14 | reverse, 15 | step = 10, 16 | shiftStep = 50, 17 | onResizeStart, 18 | onResizeEnd, 19 | containerRef, 20 | }: UseResizableProps): Resizable => { 21 | const initialPosition = Math.min(Math.max(initial, min), max); 22 | const isResizing = useRef(false); 23 | const [isDragging, setIsDragging] = useState(false); 24 | const [position, setPosition] = useState(initialPosition); 25 | const positionRef = useRef(initialPosition); 26 | const [endPosition, setEndPosition] = useState(initialPosition); 27 | 28 | const ariaProps = useMemo( 29 | () => ({ 30 | role: 'separator', 31 | 'aria-valuenow': position, 32 | 'aria-valuemin': min, 33 | 'aria-valuemax': max, 34 | 'aria-orientation': axis === 'x' ? 'vertical' : 'horizontal', 35 | 'aria-disabled': disabled, 36 | }), 37 | [axis, disabled, max, min, position], 38 | ); 39 | 40 | const handlePointermove = useCallback( 41 | (e: PointerEvent) => { 42 | // exit if not resizing 43 | if (!isResizing.current) return; 44 | 45 | if (disabled) return; 46 | 47 | e.stopPropagation(); 48 | e.preventDefault(); // prevent text selection 49 | 50 | let currentPosition = (() => { 51 | if (axis === 'x') { 52 | if (containerRef?.current) { 53 | const containerNode = containerRef.current; 54 | const { left, width } = containerNode.getBoundingClientRect(); 55 | return reverse ? left + width - e.clientX : e.clientX - left; 56 | } 57 | return reverse ? document.body.offsetWidth - e.clientX : e.clientX; 58 | } 59 | if (containerRef?.current) { 60 | const containerNode = containerRef.current; 61 | const { top, height } = containerNode.getBoundingClientRect(); 62 | return reverse ? top + height - e.clientY : e.clientY - top; 63 | } 64 | return reverse ? document.body.offsetHeight - e.clientY : e.clientY; 65 | })(); 66 | 67 | currentPosition = Math.min(Math.max(currentPosition, min), max); 68 | setPosition(currentPosition); 69 | positionRef.current = currentPosition; 70 | }, 71 | [axis, disabled, max, min, reverse, containerRef], 72 | ); 73 | 74 | const handlePointerup = useCallback( 75 | (e: PointerEvent) => { 76 | if (disabled) return; 77 | 78 | e.stopPropagation(); 79 | isResizing.current = false; 80 | setIsDragging(false); 81 | setEndPosition(positionRef.current); 82 | document.removeEventListener('pointermove', handlePointermove); 83 | document.removeEventListener('pointerup', handlePointerup); 84 | if (onResizeEnd) onResizeEnd({ position: positionRef.current }); 85 | }, 86 | [disabled, handlePointermove, onResizeEnd], 87 | ); 88 | 89 | const handlePointerdown = useCallback( 90 | (e) => { 91 | if (disabled) return; 92 | 93 | e.stopPropagation(); 94 | isResizing.current = true; 95 | setIsDragging(true); 96 | document.addEventListener('pointermove', handlePointermove); 97 | document.addEventListener('pointerup', handlePointerup); 98 | if (onResizeStart) onResizeStart({ position: positionRef.current }); 99 | }, 100 | [disabled, handlePointermove, handlePointerup, onResizeStart], 101 | ); 102 | 103 | const handleKeyDown = useCallback( 104 | (e) => { 105 | if (disabled) return; 106 | 107 | if (e.key === 'Enter') { 108 | setPosition(initial); 109 | positionRef.current = initial; 110 | return; 111 | } 112 | if ( 113 | (axis === 'x' && !KEYS_AXIS_X.includes(e.key)) || 114 | (axis === 'y' && !KEYS_AXIS_Y.includes(e.key)) 115 | ) { 116 | return; 117 | } 118 | 119 | if (onResizeStart) onResizeStart({ position: positionRef.current }); 120 | 121 | const changeStep = e.shiftKey ? shiftStep : step; 122 | const reversed = reverse ? -1 : 1; 123 | const dir = KEYS_POSITIVE.includes(e.key) ? reversed : -1 * reversed; 124 | 125 | const newPosition = position + changeStep * dir; 126 | if (newPosition < min) { 127 | setPosition(min); 128 | positionRef.current = min; 129 | } else if (newPosition > max) { 130 | setPosition(max); 131 | positionRef.current = max; 132 | } else { 133 | setPosition(newPosition); 134 | positionRef.current = newPosition; 135 | } 136 | 137 | if (onResizeEnd) onResizeEnd({ position: positionRef.current }); 138 | }, 139 | // prettier-ignore 140 | [disabled, axis, onResizeStart, shiftStep, step, reverse, position, min, max, onResizeEnd, initial], 141 | ); 142 | 143 | const handleDoubleClick = useCallback(() => { 144 | if (disabled) return; 145 | setPosition(initial); 146 | positionRef.current = initial; 147 | }, [disabled, initial]); 148 | 149 | return { 150 | position, 151 | endPosition, 152 | isDragging, 153 | separatorProps: { 154 | ...ariaProps, 155 | onPointerDown: handlePointerdown, 156 | onKeyDown: handleKeyDown, 157 | onDoubleClick: handleDoubleClick, 158 | }, 159 | setPosition, 160 | // deprecated. next version will remove this. 161 | splitterProps: { 162 | ...ariaProps, 163 | onPointerDown: handlePointerdown, 164 | onKeyDown: handleKeyDown, 165 | onDoubleClick: handleDoubleClick, 166 | }, 167 | }; 168 | }; 169 | 170 | export default useResizable; 171 | -------------------------------------------------------------------------------- /stories/AxisX.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { fn } from '@storybook/test'; 4 | 5 | import SampleBox from './components/SampleBox'; 6 | import SampleSeparator from './components/SampleSeparator'; 7 | import Resizable from '../src/Resizable'; 8 | 9 | import type { Meta, StoryObj } from '@storybook/react'; 10 | 11 | export default { 12 | title: 'Basic', 13 | component: Resizable, 14 | args: { 15 | onResizeStart: fn(), 16 | onResizeEnd: fn(), 17 | }, 18 | } as Meta; 19 | 20 | export const AxisX: StoryObj = { 21 | args: { 22 | axis: 'x', 23 | initial: 200, 24 | min: 100, 25 | max: 500, 26 | }, 27 | render: (props) => ( 28 | 29 | {({ position: x, separatorProps }) => ( 30 |
31 | 32 | 33 | 34 |
35 | )} 36 |
37 | ), 38 | }; 39 | -------------------------------------------------------------------------------- /stories/AxisXReverse.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { fn } from '@storybook/test'; 4 | 5 | import SampleBox from './components/SampleBox'; 6 | import SampleSeparator from './components/SampleSeparator'; 7 | import Resizable from '../src/Resizable'; 8 | 9 | import type { Meta, StoryObj } from '@storybook/react'; 10 | 11 | export default { 12 | title: 'Basic', 13 | component: Resizable, 14 | args: { 15 | onResizeStart: fn(), 16 | onResizeEnd: fn(), 17 | }, 18 | } as Meta; 19 | 20 | export const AxisXReverse: StoryObj = { 21 | args: { 22 | axis: 'x', 23 | initial: 200, 24 | min: 100, 25 | max: 500, 26 | reverse: true, 27 | }, 28 | render: (props) => ( 29 | 30 | {({ position: x, separatorProps }) => ( 31 |
32 | 33 | 34 | 35 |
36 | )} 37 |
38 | ), 39 | }; 40 | -------------------------------------------------------------------------------- /stories/AxisXReverseWithContainer.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | 3 | import { fn } from '@storybook/test'; 4 | 5 | import SampleBox from './components/SampleBox'; 6 | import SampleSeparator from './components/SampleSeparator'; 7 | import Resizable from '../src/Resizable'; 8 | 9 | import type { Meta, StoryObj } from '@storybook/react'; 10 | 11 | export default { 12 | title: 'WithContainer', 13 | component: Resizable, 14 | args: { 15 | onResizeStart: fn(), 16 | onResizeEnd: fn(), 17 | }, 18 | } as Meta; 19 | 20 | export const AxisXReverseWithContainer: StoryObj = { 21 | args: { 22 | axis: 'x', 23 | initial: 200, 24 | min: 100, 25 | max: 500, 26 | reverse: true, 27 | }, 28 | render: (props) => { 29 | const containerRef = useRef(null); 30 | 31 | return ( 32 |
33 |
34 | 35 | {({ position: x, separatorProps }) => ( 36 |
40 | 41 | 42 | 43 |
44 | )} 45 |
46 |
47 |
48 | ); 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /stories/AxisXWithContainer.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | 3 | import { fn } from '@storybook/test'; 4 | 5 | import SampleBox from './components/SampleBox'; 6 | import SampleSeparator from './components/SampleSeparator'; 7 | import Resizable from '../src/Resizable'; 8 | 9 | import type { Meta, StoryObj } from '@storybook/react'; 10 | 11 | export default { 12 | title: 'WithContainer', 13 | component: Resizable, 14 | args: { 15 | onResizeStart: fn(), 16 | onResizeEnd: fn(), 17 | }, 18 | } as Meta; 19 | 20 | export const AxisXWithContainer: StoryObj = { 21 | args: { 22 | axis: 'x', 23 | initial: 200, 24 | min: 100, 25 | max: 500, 26 | }, 27 | render: (props) => { 28 | const containerRef = useRef(null); 29 | 30 | return ( 31 |
32 |
33 | 34 | {({ position: x, separatorProps }) => ( 35 |
40 | 41 | 42 | 43 |
44 | )} 45 |
46 |
47 |
48 | ); 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /stories/AxisY.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { fn } from '@storybook/test'; 4 | 5 | import SampleBox from './components/SampleBox'; 6 | import SampleSeparator from './components/SampleSeparator'; 7 | import Resizable from '../src/Resizable'; 8 | 9 | import type { Meta, StoryObj } from '@storybook/react'; 10 | 11 | export default { 12 | title: 'Basic', 13 | component: Resizable, 14 | args: { 15 | onResizeStart: fn(), 16 | onResizeEnd: fn(), 17 | }, 18 | } as Meta; 19 | 20 | export const AxisY: StoryObj = { 21 | args: { 22 | axis: 'y', 23 | initial: 100, 24 | min: 50, 25 | max: 300, 26 | }, 27 | render: (props) => ( 28 | 29 | {({ position: y, separatorProps }) => ( 30 |
38 | 39 | 40 | 41 |
42 | )} 43 |
44 | ), 45 | }; 46 | -------------------------------------------------------------------------------- /stories/AxisYReverse.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { fn } from '@storybook/test'; 4 | 5 | import SampleBox from './components/SampleBox'; 6 | import SampleSeparator from './components/SampleSeparator'; 7 | import Resizable from '../src/Resizable'; 8 | 9 | import type { Meta, StoryObj } from '@storybook/react'; 10 | 11 | export default { 12 | title: 'Basic', 13 | component: Resizable, 14 | args: { 15 | onResizeStart: fn(), 16 | onResizeEnd: fn(), 17 | }, 18 | } as Meta; 19 | 20 | export const AxisYReverse: StoryObj = { 21 | args: { 22 | axis: 'y', 23 | initial: 100, 24 | min: 50, 25 | max: 300, 26 | reverse: true, 27 | }, 28 | render: (props) => ( 29 | 30 | {({ position: y, separatorProps }) => ( 31 |
39 | 40 | 41 | 42 |
43 | )} 44 |
45 | ), 46 | }; 47 | -------------------------------------------------------------------------------- /stories/AxisYReverseWithContainer.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | 3 | import { fn } from '@storybook/test'; 4 | 5 | import SampleBox from './components/SampleBox'; 6 | import SampleSeparator from './components/SampleSeparator'; 7 | import Resizable from '../src/Resizable'; 8 | 9 | import type { Meta, StoryObj } from '@storybook/react'; 10 | 11 | export default { 12 | title: 'WithContainer', 13 | component: Resizable, 14 | args: { 15 | onResizeStart: fn(), 16 | onResizeEnd: fn(), 17 | }, 18 | } as Meta; 19 | 20 | export const AxisYReverseWithContainer: StoryObj = { 21 | args: { 22 | axis: 'y', 23 | initial: 100, 24 | min: 50, 25 | max: 300, 26 | reverse: true, 27 | }, 28 | render: (props) => { 29 | const containerRef = useRef(null); 30 | 31 | return ( 32 | <> 33 |
34 | 35 | {({ position: y, separatorProps }) => ( 36 |
45 | 46 | 47 | 48 |
49 | )} 50 |
51 |
52 | 53 | ); 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /stories/AxisYWithContainer.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | 3 | import { fn } from '@storybook/test'; 4 | 5 | import SampleBox from './components/SampleBox'; 6 | import SampleSeparator from './components/SampleSeparator'; 7 | import Resizable from '../src/Resizable'; 8 | 9 | import type { Meta, StoryObj } from '@storybook/react'; 10 | 11 | export default { 12 | title: 'WithContainer', 13 | component: Resizable, 14 | args: { 15 | onResizeStart: fn(), 16 | onResizeEnd: fn(), 17 | }, 18 | } as Meta; 19 | 20 | export const AxisYWithContainer: StoryObj = { 21 | args: { 22 | axis: 'y', 23 | initial: 100, 24 | min: 50, 25 | max: 300, 26 | }, 27 | render: (props) => { 28 | const containerRef = useRef(null); 29 | 30 | return ( 31 | <> 32 |
33 | 34 | {({ position: y, separatorProps }) => ( 35 |
44 | 45 | 46 | 47 |
48 | )} 49 |
50 |
51 | 52 | ); 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /stories/Callback.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { fn } from '@storybook/test'; 4 | 5 | import SampleBox from './components/SampleBox'; 6 | import SampleSeparator from './components/SampleSeparator'; 7 | import Resizable from '../src/Resizable'; 8 | 9 | import type { Meta, StoryObj } from '@storybook/react'; 10 | 11 | export default { 12 | title: 'Advanced', 13 | component: Resizable, 14 | args: { 15 | onResizeStart: fn(), 16 | onResizeEnd: fn(), 17 | }, 18 | } as Meta; 19 | 20 | export const Callback: StoryObj = { 21 | args: { 22 | axis: 'x', 23 | initial: 200, 24 | }, 25 | render: (props) => ( 26 | { 29 | console.table(args); 30 | }} 31 | onResizeEnd={(args) => { 32 | console.table(args); 33 | }} 34 | > 35 | {({ position: x, separatorProps }) => ( 36 |
37 | 44 | 45 | 46 |
47 | )} 48 |
49 | ), 50 | }; 51 | -------------------------------------------------------------------------------- /stories/Disabled.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { fn } from '@storybook/test'; 4 | 5 | import SampleBox from './components/SampleBox'; 6 | import SampleSeparator from './components/SampleSeparator'; 7 | import Resizable from '../src/Resizable'; 8 | 9 | import type { Meta, StoryObj } from '@storybook/react'; 10 | 11 | export default { 12 | title: 'Basic', 13 | component: Resizable, 14 | args: { 15 | onResizeStart: fn(), 16 | onResizeEnd: fn(), 17 | }, 18 | } as Meta; 19 | 20 | export const Disabled: StoryObj = { 21 | args: { 22 | axis: 'x', 23 | initial: 200, 24 | min: 100, 25 | max: 500, 26 | disabled: true, 27 | }, 28 | render: (props) => ( 29 | 30 | {({ position: x, separatorProps }) => ( 31 |
32 | 33 | 34 | 35 |
36 | )} 37 |
38 | ), 39 | }; 40 | -------------------------------------------------------------------------------- /stories/DraggingState.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { fn } from '@storybook/test'; 4 | 5 | import SampleBox from './components/SampleBox'; 6 | import SampleSeparator from './components/SampleSeparator'; 7 | import Resizable from '../src/Resizable'; 8 | 9 | import type { Meta, StoryObj } from '@storybook/react'; 10 | 11 | export default { 12 | title: 'Advanced', 13 | component: Resizable, 14 | args: { 15 | onResizeStart: fn(), 16 | onResizeEnd: fn(), 17 | }, 18 | } as Meta; 19 | 20 | export const DraggingState: StoryObj = { 21 | args: { 22 | axis: 'x', 23 | initial: 200, 24 | min: 100, 25 | max: 500, 26 | }, 27 | render: (props) => ( 28 | 29 | {({ position: x, isDragging, separatorProps }) => ( 30 |
31 | 38 | 39 | 45 |
46 | )} 47 |
48 | ), 49 | }; 50 | -------------------------------------------------------------------------------- /stories/IdeClone.stories.tsx: -------------------------------------------------------------------------------- 1 | import IdeClone from './components/IdeClone'; 2 | 3 | import type { Meta, StoryObj } from '@storybook/react'; 4 | 5 | export default { 6 | title: 'Example', 7 | component: IdeClone, 8 | } as Meta; 9 | 10 | export const IDEClone: StoryObj = {}; 11 | -------------------------------------------------------------------------------- /stories/VirtualSplitter.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { fn } from '@storybook/test'; 4 | 5 | import SampleBox from './components/SampleBox'; 6 | import SampleSeparator from './components/SampleSeparator'; 7 | import Resizable from '../src/Resizable'; 8 | 9 | import type { Meta, StoryObj } from '@storybook/react'; 10 | 11 | export default { 12 | title: 'Advanced', 13 | component: Resizable, 14 | args: { 15 | onResizeStart: fn(), 16 | onResizeEnd: fn(), 17 | }, 18 | } as Meta; 19 | 20 | export const VirtualSplitter: StoryObj = { 21 | args: { 22 | axis: 'x', 23 | initial: 200, 24 | min: 100, 25 | max: 500, 26 | }, 27 | render: (props) => ( 28 | 29 | {({ position: x, endPosition: endX, isDragging, separatorProps }) => ( 30 |
39 | 40 | 41 | 52 | 53 |
54 | )} 55 |
56 | ), 57 | }; 58 | -------------------------------------------------------------------------------- /stories/components/IdeClone.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import SampleSeparator from './SampleSeparator'; 4 | import { useResizable } from '../../src'; 5 | import { cn } from '../utils/cn'; 6 | 7 | const IdeClone = (): JSX.Element => { 8 | const { 9 | isDragging: isTerminalDragging, 10 | position: terminalH, 11 | separatorProps: terminalDragBarProps, 12 | } = useResizable({ 13 | axis: 'y', 14 | initial: 150, 15 | min: 50, 16 | reverse: true, 17 | }); 18 | const { 19 | isDragging: isFileDragging, 20 | position: fileW, 21 | separatorProps: fileDragBarProps, 22 | } = useResizable({ 23 | axis: 'x', 24 | initial: 250, 25 | min: 50, 26 | }); 27 | const { 28 | isDragging: isPluginDragging, 29 | position: pluginW, 30 | separatorProps: pluginDragBarProps, 31 | } = useResizable({ 32 | axis: 'x', 33 | initial: 200, 34 | min: 50, 35 | reverse: true, 36 | }); 37 | 38 | return ( 39 |
40 |
41 |
45 | File Tree 46 |
47 | 48 |
49 |
Editor
50 | 51 |
55 | Plugin 56 |
57 |
58 |
59 | 60 |
64 | Terminal 65 |
66 |
67 | ); 68 | }; 69 | 70 | export default IdeClone; 71 | -------------------------------------------------------------------------------- /stories/components/SampleBox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { cn } from '../utils/cn'; 4 | 5 | const SampleBox = ({ id, width, height, theme, size, text }: any) => { 6 | const additionalClass = (() => { 7 | switch (theme) { 8 | case 'blue': 9 | return 'sample-box--blue'; 10 | case 'red': 11 | return 'sample-box--red'; 12 | default: 13 | return null; 14 | } 15 | })(); 16 | 17 | return ( 18 |
27 | {text || 'Drag center bar to resize'} 28 |
29 | {size && `(currentSize : ${size}px)`} 30 |
31 | ); 32 | }; 33 | 34 | export default SampleBox; 35 | -------------------------------------------------------------------------------- /stories/components/SampleSeparator.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import { cn } from '../utils/cn'; 4 | 5 | const SampleSeparator = ({ id = 'drag-bar', dir, isDragging, disabled, ...props }: any) => { 6 | const [isFocused, setIsFocused] = useState(false); 7 | 8 | return ( 9 |
setIsFocused(true)} 20 | onBlur={() => setIsFocused(false)} 21 | {...props} 22 | /> 23 | ); 24 | }; 25 | 26 | export default SampleSeparator; 27 | -------------------------------------------------------------------------------- /stories/style/IdeClone.css: -------------------------------------------------------------------------------- 1 | .flex { 2 | display: flex; 3 | } 4 | 5 | .flex-column { 6 | flex-direction: column; 7 | } 8 | 9 | .shrink-0 { 10 | flex-shrink: 0; 11 | } 12 | 13 | .grow { 14 | flex-grow: 1; 15 | } 16 | 17 | .h-screen { 18 | height: 100vh; 19 | } 20 | 21 | .bg-darker { 22 | background-color: #202020; 23 | } 24 | 25 | .bg-dark { 26 | background-color: #2D3032; 27 | } 28 | 29 | .font-mono { 30 | font-family: monospace; 31 | } 32 | 33 | .color-white { 34 | color: white; 35 | } 36 | 37 | .overflow-hidden { 38 | overflow: hidden; 39 | } 40 | 41 | .contents { 42 | display: grid; 43 | place-items: center; 44 | transition: filter 0.2s ease-out, background-color 0.2s ease-out; 45 | font-size: 16px; 46 | } 47 | 48 | .dragging { 49 | filter: blur(5px); 50 | background-color: #555555; 51 | } 52 | -------------------------------------------------------------------------------- /stories/style/SampleBox.css: -------------------------------------------------------------------------------- 1 | .sample-box{ 2 | display: grid; 3 | place-items: center; 4 | flex-shrink: 0; 5 | } 6 | 7 | .sample-box.sample-box--blue{ 8 | background-color: #f0f9ff; 9 | } 10 | 11 | .sample-box.sample-box--red{ 12 | background-color: #fff1f2; 13 | } 14 | -------------------------------------------------------------------------------- /stories/style/SampleSeparator.css: -------------------------------------------------------------------------------- 1 | .sample-drag-bar { 2 | flex-shrink: 0; 3 | width: 5px; 4 | margin: 0; 5 | border: none; 6 | background-color: #d1d5db; 7 | cursor: col-resize; 8 | transition: background-color 0.15s 0.15s ease-in-out; 9 | } 10 | 11 | .sample-drag-bar.sample-drag-bar--dragging, 12 | .sample-drag-bar:hover{ 13 | background-color: #63B3ED; 14 | } 15 | 16 | .sample-drag-bar.sample-drag-bar--horizontal { 17 | height: 5px; 18 | width: 100%; 19 | cursor: row-resize; 20 | } 21 | 22 | .disabled { 23 | cursor: not-allowed; 24 | } 25 | .disabled:hover { 26 | background-color: #d1d5db; 27 | } 28 | -------------------------------------------------------------------------------- /stories/utils/cn.ts: -------------------------------------------------------------------------------- 1 | export const cn = (...args: any[]) => args.filter(Boolean).join(' '); 2 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src" 5 | }, 6 | "exclude": [ 7 | "lib/**/*", 8 | "stories/**/*" 9 | ], 10 | "include": [ 11 | "src/**/*" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "strict": true, 6 | "skipLibCheck": true, 7 | "declaration": true, 8 | "declarationMap": true, 9 | "pretty": true, 10 | "newLine": "lf", 11 | "outDir": "lib", 12 | "allowJs": true, 13 | "rootDir": "/", 14 | "lib": ["es6", "dom", "es2016", "es2017"], 15 | "jsx": "react", 16 | "allowSyntheticDefaultImports": true, 17 | "moduleResolution": "Node" 18 | }, 19 | "exclude": [ 20 | "lib/**/*" 21 | ], 22 | "include": [ 23 | "src/**/*", 24 | "stories/**/*" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | }); 7 | --------------------------------------------------------------------------------