├── .all-contributorsrc ├── .changeset ├── README.md ├── config.json └── light-tigers-explode.md ├── .codesandbox └── ci.json ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── main.yml │ └── release.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .prettierignore ├── .storybook ├── main.js ├── manager.js └── preview.js ├── .vscode └── settings.json ├── .yarn ├── plugins │ └── @yarnpkg │ │ ├── plugin-interactive-tools.cjs │ │ ├── plugin-version.cjs │ │ └── plugin-workspace-tools.cjs └── releases │ └── yarn-sources.cjs ├── .yarnrc.yml ├── LICENSE ├── README.md ├── babel.config.js ├── code_of_conduct.md ├── contributing.md ├── cypress.json ├── cypress ├── fixtures │ └── example.json ├── integration │ └── spec.js ├── plugins │ └── index.js └── support │ ├── commands.js │ └── index.js ├── demo ├── index.html ├── package.json ├── serve.json ├── src │ ├── App.jsx │ ├── index.css │ ├── index.jsx │ ├── sandboxes │ │ ├── leva-advanced-panels │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── index.html │ │ │ └── src │ │ │ │ ├── App.jsx │ │ │ │ ├── index.css │ │ │ │ └── index.jsx │ │ ├── leva-busy │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── index.html │ │ │ └── src │ │ │ │ ├── App.tsx │ │ │ │ ├── index.css │ │ │ │ ├── index.tsx │ │ │ │ └── styles.module.css │ │ ├── leva-custom-plugin │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── index.html │ │ │ └── src │ │ │ │ ├── App.tsx │ │ │ │ ├── index.css │ │ │ │ └── index.tsx │ │ ├── leva-minimal │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── index.html │ │ │ └── src │ │ │ │ ├── App.jsx │ │ │ │ ├── index.css │ │ │ │ └── index.jsx │ │ ├── leva-plugin-bezier │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── index.html │ │ │ └── src │ │ │ │ ├── App.tsx │ │ │ │ ├── index.css │ │ │ │ ├── index.tsx │ │ │ │ └── style.css │ │ ├── leva-plugin-dates │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── index.html │ │ │ └── src │ │ │ │ ├── App.tsx │ │ │ │ ├── index.css │ │ │ │ └── index.tsx │ │ ├── leva-plugin-plot │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── index.html │ │ │ └── src │ │ │ │ ├── App.tsx │ │ │ │ ├── index.css │ │ │ │ └── index.tsx │ │ ├── leva-plugin-spring │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── index.html │ │ │ └── src │ │ │ │ ├── App.tsx │ │ │ │ ├── index.css │ │ │ │ └── index.tsx │ │ ├── leva-scroll │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── index.html │ │ │ └── src │ │ │ │ ├── App.jsx │ │ │ │ ├── index.css │ │ │ │ └── index.jsx │ │ ├── leva-theme │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── index.html │ │ │ └── src │ │ │ │ ├── App.jsx │ │ │ │ ├── index.css │ │ │ │ └── index.jsx │ │ ├── leva-transient │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── index.html │ │ │ └── src │ │ │ │ ├── App.jsx │ │ │ │ ├── index.css │ │ │ │ └── index.jsx │ │ └── leva-ui │ │ │ ├── package.json │ │ │ ├── public │ │ │ └── index.html │ │ │ └── src │ │ │ ├── App.jsx │ │ │ ├── index.css │ │ │ ├── index.jsx │ │ │ └── styles.css │ └── styles.module.css ├── tsconfig.json └── vite.config.js ├── docs ├── advanced │ ├── circle-drag.gif │ ├── controlled-inputs.md │ └── creating-plugins.md ├── configuration.md ├── getting-started.md ├── inputs.md ├── plugins.md ├── special-inputs.md ├── styling.md └── typescript.md ├── hero.png ├── package.json ├── packages ├── leva │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── plugin │ │ └── package.json │ ├── src │ │ ├── components │ │ │ ├── Boolean │ │ │ │ ├── Boolean.tsx │ │ │ │ ├── StyledBoolean.ts │ │ │ │ ├── boolean-plugin.ts │ │ │ │ ├── boolean-types.ts │ │ │ │ └── index.ts │ │ │ ├── Button │ │ │ │ ├── Button.tsx │ │ │ │ ├── StyledButton.ts │ │ │ │ └── index.ts │ │ │ ├── ButtonGroup │ │ │ │ ├── ButtonGroup.tsx │ │ │ │ ├── StyledButtonGroup.tsx │ │ │ │ ├── StyledButtonGroupButton.ts │ │ │ │ └── index.ts │ │ │ ├── Color │ │ │ │ ├── Color.tsx │ │ │ │ ├── StyledColor.ts │ │ │ │ ├── color-plugin.ts │ │ │ │ ├── color-types.ts │ │ │ │ └── index.ts │ │ │ ├── Control │ │ │ │ ├── Control.tsx │ │ │ │ ├── ControlInput.tsx │ │ │ │ └── index.ts │ │ │ ├── Folder │ │ │ │ ├── Folder.tsx │ │ │ │ ├── FolderTitle.tsx │ │ │ │ ├── StyledFolder.ts │ │ │ │ └── index.ts │ │ │ ├── Image │ │ │ │ ├── Image.tsx │ │ │ │ ├── StyledImage.ts │ │ │ │ ├── image-plugin.ts │ │ │ │ ├── image-types.ts │ │ │ │ └── index.ts │ │ │ ├── Interval │ │ │ │ ├── Interval.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── interval-plugin.ts │ │ │ │ └── interval-types.ts │ │ │ ├── Leva │ │ │ │ ├── Filter.tsx │ │ │ │ ├── Leva.tsx │ │ │ │ ├── LevaPanel.tsx │ │ │ │ ├── LevaRoot.tsx │ │ │ │ ├── StyledFilter.ts │ │ │ │ ├── StyledRoot.ts │ │ │ │ ├── index.ts │ │ │ │ └── tree.ts │ │ │ ├── Monitor │ │ │ │ ├── Monitor.tsx │ │ │ │ ├── StyledMonitor.ts │ │ │ │ └── index.ts │ │ │ ├── Number │ │ │ │ ├── Number.tsx │ │ │ │ ├── RangeSlider.tsx │ │ │ │ ├── StyledNumber.ts │ │ │ │ ├── StyledRange.ts │ │ │ │ ├── index.ts │ │ │ │ ├── number-plugin.ts │ │ │ │ └── number-types.ts │ │ │ ├── Select │ │ │ │ ├── Select.tsx │ │ │ │ ├── StyledSelect.ts │ │ │ │ ├── index.ts │ │ │ │ ├── select-plugin.ts │ │ │ │ └── select-types.ts │ │ │ ├── String │ │ │ │ ├── String.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── string-plugin.ts │ │ │ │ └── string-types.ts │ │ │ ├── UI │ │ │ │ ├── Chevron.tsx │ │ │ │ ├── Label.tsx │ │ │ │ ├── Misc.tsx │ │ │ │ ├── Row.tsx │ │ │ │ ├── StyledUI.ts │ │ │ │ └── index.ts │ │ │ ├── ValueInput │ │ │ │ ├── StyledInput.ts │ │ │ │ ├── ValueInput.tsx │ │ │ │ └── index.ts │ │ │ ├── Vector │ │ │ │ ├── Vector.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── vector-plugin.ts │ │ │ │ ├── vector-types.ts │ │ │ │ └── vector-utils.ts │ │ │ ├── Vector2d │ │ │ │ ├── Joystick.tsx │ │ │ │ ├── StyledJoystick.ts │ │ │ │ ├── Vector2d.tsx │ │ │ │ ├── index.ts │ │ │ │ └── vector2d-types.ts │ │ │ └── Vector3d │ │ │ │ ├── Vector3d.tsx │ │ │ │ ├── index.ts │ │ │ │ └── vector3d-types.ts │ │ ├── context.tsx │ │ ├── eventEmitter.ts │ │ ├── helpers │ │ │ ├── button.ts │ │ │ ├── buttonGroup.ts │ │ │ ├── folder.ts │ │ │ ├── index.ts │ │ │ └── monitor.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── useCanvas.ts │ │ │ ├── useCompareMemoize.ts │ │ │ ├── useDeepMemo.ts │ │ │ ├── useDrag.ts │ │ │ ├── useInput.ts │ │ │ ├── useInputSetters.ts │ │ │ ├── usePopin.ts │ │ │ ├── useShallowMemo.ts │ │ │ ├── useToggle.ts │ │ │ ├── useTransform.ts │ │ │ ├── useValue.ts │ │ │ ├── useValuesForPath.ts │ │ │ └── useVisiblePaths.ts │ │ ├── index.ts │ │ ├── plugin.ts │ │ ├── plugin │ │ │ └── index.ts │ │ ├── store.ts │ │ ├── styles │ │ │ ├── index.ts │ │ │ └── stitches.config.ts │ │ ├── types │ │ │ ├── index.ts │ │ │ ├── internal.ts │ │ │ ├── public.test.ts │ │ │ ├── public.ts │ │ │ ├── utils.ts │ │ │ └── v8n.d.ts │ │ ├── useControls.ts │ │ └── utils │ │ │ ├── data.ts │ │ │ ├── event.ts │ │ │ ├── fn.ts │ │ │ ├── index.ts │ │ │ ├── input.ts │ │ │ ├── log.ts │ │ │ ├── math.ts │ │ │ ├── object.ts │ │ │ ├── path.ts │ │ │ └── react.ts │ └── stories │ │ ├── Folder.stories.tsx │ │ ├── caching.stories.tsx │ │ ├── components │ │ └── decorator-reset.tsx │ │ ├── controlled-inputs.stories.tsx │ │ ├── hook-dependencies.stories.tsx │ │ ├── input-options.stories.tsx │ │ ├── inputs │ │ ├── Boolean.stories.tsx │ │ ├── Button.stories.tsx │ │ ├── ButtonGroup.stories.tsx │ │ ├── Color.stories.tsx │ │ ├── Image.stories.tsx │ │ ├── Interval.stories.tsx │ │ ├── Number.stories.tsx │ │ ├── Select.stories.tsx │ │ ├── String.stories.tsx │ │ └── Vector.stories.tsx │ │ └── panel-options.stories.tsx ├── plugin-bezier │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ │ ├── Bezier.stories.css │ │ ├── Bezier.stories.tsx │ │ ├── Bezier.tsx │ │ ├── BezierPreview.tsx │ │ ├── BezierSvg.tsx │ │ ├── StyledBezier.ts │ │ ├── bezier-plugin.ts │ │ ├── bezier-types.ts │ │ ├── bezier-utils.ts │ │ └── index.ts ├── plugin-dates │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ │ ├── Date.stories.tsx │ │ ├── Date.tsx │ │ ├── StyledDate.ts │ │ ├── date-plugin.ts │ │ ├── date-types.ts │ │ ├── date-utils.ts │ │ └── index.ts ├── plugin-plot │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ │ ├── Plot.stories.tsx │ │ ├── Plot.tsx │ │ ├── PlotCanvas.tsx │ │ ├── StyledPlot.ts │ │ ├── index.ts │ │ ├── plot-plugin.ts │ │ ├── plot-types.ts │ │ └── plot-utils.ts └── plugin-spring │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ ├── Spring.stories.tsx │ ├── Spring.tsx │ ├── SpringCanvas.tsx │ ├── StyledSpring.ts │ ├── index.ts │ ├── math.ts │ ├── spring-plugin.ts │ └── spring-types.ts ├── tsconfig.json └── yarn.lock /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/master/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.6.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "access": "public", 6 | "baseBranch": "main", 7 | "updateInternalDependencies": "patch", 8 | "ignore": ["demo"] 9 | } 10 | -------------------------------------------------------------------------------- /.changeset/light-tigers-explode.md: -------------------------------------------------------------------------------- 1 | --- 2 | "leva": patch 3 | --- 4 | 5 | `@radix-ui/*` upgrades to prevent peerDeps warnings with React 19 6 | -------------------------------------------------------------------------------- /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "sandboxes": [ 6 | "/demo/src/sandboxes/leva-minimal", 7 | "/demo/src/sandboxes/leva-busy", 8 | "/demo/src/sandboxes/leva-scroll", 9 | "/demo/src/sandboxes/leva-advanced-panels", 10 | "/demo/src/sandboxes/leva-ui", 11 | "/demo/src/sandboxes/leva-theme", 12 | "/demo/src/sandboxes/leva-transient", 13 | "/demo/src/sandboxes/leva-plugin-plot", 14 | "/demo/src/sandboxes/leva-plugin-bezier", 15 | "/demo/src/sandboxes/leva-plugin-spring", 16 | "/demo/src/sandboxes/leva-plugin-dates", 17 | "/demo/src/sandboxes/leva-custom-plugin" 18 | ], 19 | "node": "14" 20 | } 21 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | .yarn/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app", 3 | "rules": { 4 | "no-console": "warn" 5 | }, 6 | "plugins": ["cypress"], 7 | "env": { 8 | "cypress/globals": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | paths: 5 | - '.github/**' 6 | - 'packages/**' 7 | - 'package.json' 8 | - 'yarn.lock' 9 | - '!demo/**' 10 | - '!docs/**' 11 | - '!**.md' 12 | - '!.changeset/**' 13 | jobs: 14 | build: 15 | name: Build, lint, and test 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout repo 20 | uses: actions/checkout@v3 21 | 22 | - name: Use Node 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: '18' 26 | 27 | - name: Get yarn cache directory path 28 | id: yarn-cache-dir-path 29 | run: echo "::set-output name=dir::$(yarn config get cacheFolder)" 30 | 31 | - uses: actions/cache@v3 32 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 33 | with: 34 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 35 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 36 | restore-keys: | 37 | ${{ runner.os }}-yarn- 38 | 39 | - name: Install dependencies 40 | run: yarn --silent 41 | 42 | - name: Check types 43 | run: yarn tsc 44 | 45 | - name: Check formatting 46 | run: yarn prettier 47 | 48 | - name: Lint files 49 | run: yarn lint:full 50 | 51 | - uses: actions/cache@v3 52 | name: Setup Yarn build cache 53 | id: yarn-build-cache 54 | with: 55 | path: packages/**/dist 56 | key: ${{ runner.os }}-yarn-build-${{ hashFiles('/packages/**/*') }} 57 | restore-keys: | 58 | ${{ runner.os }}-yarn-build- 59 | 60 | - name: Yarn build without cache 61 | if: steps.yarn-build-cache.outputs.cache-hit != 'true' 62 | run: yarn build 63 | 64 | - name: Cypress run 65 | run: yarn ci:test 66 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - '**/package.json' 9 | - '.changeset/**' 10 | - '.github/workflows/release.yml' 11 | 12 | jobs: 13 | release: 14 | name: Release 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout Repo 18 | uses: actions/checkout@v3 19 | with: 20 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 21 | fetch-depth: 0 22 | 23 | - name: Use Node 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: '18' 27 | 28 | - name: Get yarn cache directory path 29 | id: yarn-cache-dir-path 30 | run: echo "::set-output name=dir::$(yarn config get cacheFolder)" 31 | 32 | - uses: actions/cache@v3 33 | id: yarn-cache 34 | with: 35 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 36 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 37 | restore-keys: | 38 | ${{ runner.os }}-yarn- 39 | 40 | - name: Install dependencies 41 | run: yarn install --silent 42 | 43 | - name: Create Release Pull Request or Publish to npm 44 | id: changesets 45 | uses: changesets/action@v1 46 | with: 47 | version: yarn ci:version 48 | publish: yarn ci:release 49 | commit: 'chore(release): update monorepo packages versions' 50 | title: 'Upcoming Release Changes' 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules/ 5 | .yarn/* 6 | !.yarn/releases 7 | !.yarn/plugins 8 | !.yarn/sdks 9 | !.yarn/versions 10 | .pnp.* 11 | 12 | # testing 13 | /coverage 14 | 15 | # production 16 | build/ 17 | dist/ 18 | .cache/ 19 | .parcel-cache/ 20 | 21 | # misc 22 | .DS_Store 23 | .env.local 24 | .env.development.local 25 | .env.test.local 26 | .env.production.local 27 | 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | storybook-static/ 33 | .idea 34 | cypress/screenshots 35 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .changeset/ 2 | .codesandbox/ 3 | .github/ 4 | .husky/ 5 | .storybook/ 6 | .vscode/ 7 | .yarn/ 8 | dist/ 9 | node_modules/ 10 | patches/ 11 | storybook-static/ 12 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | stories: ['../packages/**/*.stories.mdx', '../packages/**/*.stories.@(js|jsx|ts|tsx)'], 5 | addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-storysource'], 6 | webpackFinal: async (config, { configType }) => { 7 | // `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION' 8 | // You can change the configuration based on that. 9 | // 'PRODUCTION' is used when building the static version of storybook. 10 | 11 | // manually resolve packages from other repos so that vercel builds properly 12 | if (configType === 'PRODUCTION') { 13 | config.resolve.alias['leva'] = path.resolve(__dirname, '../packages/leva/') 14 | } 15 | // Return the altered config 16 | return config 17 | }, 18 | typescript: { 19 | reactDocgen: 'none', // temp fix for TS 4.3.2 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /.storybook/manager.js: -------------------------------------------------------------------------------- 1 | import { addons } from '@storybook/addons'; 2 | import { themes } from '@storybook/theming'; 3 | 4 | addons.setConfig({ 5 | theme: themes.dark, 6 | }); -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | 2 | export const parameters = { 3 | actions: { argTypesRegex: "^on[A-Z].*" }, 4 | "previewTabs": { 5 | 'storybook/docs/panel': { hidden: true } 6 | }, 7 | options: { 8 | storySort: { 9 | order: ["Inputs", ["String", "Boolean", "Number", "Interval"], "Misc", "Plugins"] 10 | } 11 | }, 12 | } 13 | 14 | export const decorators = [(Story) => <div style={{ color: "white" }}><Story /></div>] 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-version.cjs 5 | spec: '@yarnpkg/plugin-version' 6 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 7 | spec: '@yarnpkg/plugin-interactive-tools' 8 | - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs 9 | spec: '@yarnpkg/plugin-workspace-tools' 10 | 11 | yarnPath: .yarn/releases/yarn-sources.cjs 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Poimandres 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 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | comments: false, 3 | presets: [ 4 | [ 5 | '@babel/preset-env', 6 | { 7 | bugfixes: true, 8 | targets: { 9 | esmodules: true, 10 | }, 11 | }, 12 | ], 13 | '@babel/preset-react', 14 | '@babel/preset-typescript', 15 | ], 16 | } 17 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## What we are looking for 4 | 5 | - Input Suggestions 6 | - Plugin Suggestions 7 | - Alternative Themes 8 | - Unit Tests 9 | - Docs 10 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:5000", 3 | "defaultCommandTimeout": 10000, 4 | "video": false 5 | } 6 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /cypress/integration/spec.js: -------------------------------------------------------------------------------- 1 | describe('Number', () => { 2 | it('Works.', () => { 3 | cy.visit('/leva-minimal') 4 | cy.findByLabelText(/number/).should('exist') 5 | 6 | cy.findByLabelText(/number/) 7 | .clear() 8 | .type(123) 9 | .blur() 10 | // expect 123 to be the value 11 | cy.findByLabelText(/number/).should('have.value', 123) 12 | // expect previous value when empty string is used 13 | cy.findByLabelText(/number/) 14 | .focus() 15 | .clear() 16 | .blur() 17 | }) 18 | 19 | it("Doesn't accept non-numbers.", () => { 20 | cy.visit('/leva-minimal') 21 | 22 | // expect previous value when typing invalid characters 23 | cy.findByLabelText(/number/) 24 | .clear() 25 | .type('ABC') 26 | .blur() 27 | cy.findByLabelText(/number/).should('have.value', 10) 28 | }) 29 | }) 30 | 31 | describe('MinMax', () => { 32 | it('Works.', () => { 33 | cy.visit('/leva-minimal') 34 | cy.findByLabelText(/minmax/).should('exist') 35 | 36 | cy.findByLabelText(/number/) 37 | .clear() 38 | .type(13) 39 | .blur() 40 | cy.findByLabelText(/number/).should('have.value', 13) 41 | // expect previous value when empty string is used 42 | cy.findByLabelText(/number/) 43 | .focus() 44 | .clear() 45 | .blur() 46 | }) 47 | 48 | it("Doesn't go over.", () => { 49 | cy.visit('/leva-minimal') 50 | 51 | // since value is over max, it should reset to max 52 | cy.findByLabelText(/minmax/) 53 | .clear() 54 | .type(123) 55 | .blur() 56 | cy.findByLabelText(/minmax/).should('have.value', 30.5) 57 | }) 58 | 59 | it("Doesn't go under.", () => { 60 | cy.visit('/leva-minimal') 61 | 62 | // since value is under min, it should reset to initial 63 | cy.findByLabelText(/minmax/) 64 | .clear() 65 | .type(1) 66 | .blur() 67 | cy.findByLabelText(/minmax/).should('have.value', 5.5) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// <reference types="cypress" /> 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | module.exports = (on, config) => { 19 | // `on` is used to hook into various events Cypress emits 20 | // `config` is the resolved Cypress config 21 | } 22 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/cypress/add-commands' 2 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="UTF-8" /> 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 | <meta http-equiv="X-UA-Compatible" content="ie=edge" /> 7 | <title>Sandboxes Leva</title> 8 | </head> 9 | 10 | <body> 11 | <div id="root"></div> 12 | <script type="module" src="./src/index.jsx"></script> 13 | </body> 14 | </html> 15 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite --port 3300 --host", 7 | "build": "vite build", 8 | "serve": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@radix-ui/react-icons": "^1.0.3", 12 | "@react-three/drei": "^8.8.3", 13 | "@react-three/fiber": "^7.0.26", 14 | "@stitches/react": "1.2.8", 15 | "leva": "*", 16 | "noisejs": "^2.1.0", 17 | "react": "^18.0.0", 18 | "react-dom": "^18.0.0", 19 | "react-use": "^17.3.2", 20 | "three": "^0.143.0", 21 | "wouter": "^2.7.5" 22 | }, 23 | "devDependencies": { 24 | "@types/react": "^18.0.0", 25 | "@types/react-dom": "^18.0.0", 26 | "@vitejs/plugin-react-refresh": "^1.3.6", 27 | "typescript": "^4.5.5", 28 | "vite": "2.7.13" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /demo/serve.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [{ "source": "/*", "destination": "/index.html" }] 3 | } 4 | -------------------------------------------------------------------------------- /demo/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: system-ui, sans-serif; 3 | min-height: 100vh; 4 | background: linear-gradient(140deg, rgb(165, 142, 251), rgb(233, 191, 248)); 5 | margin: 0; 6 | } 7 | 8 | *, 9 | *:after, 10 | *:before { 11 | box-sizing: border-box; 12 | } 13 | 14 | pre { 15 | max-width: 720px; 16 | margin: 20px; 17 | padding: 20px; 18 | border: 10px solid black; 19 | } 20 | -------------------------------------------------------------------------------- /demo/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const rootElement = document.getElementById('root') 8 | ReactDOM.render( 9 | <React.StrictMode> 10 | <App /> 11 | </React.StrictMode>, 12 | rootElement 13 | ) 14 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-advanced-panels/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leva-advanced-panels", 3 | "version": "1.0.0", 4 | "main": "src/index.jsx", 5 | "dependencies": { 6 | "leva": "*", 7 | "react": "^18.0.0", 8 | "react-dom": "^18.0.0", 9 | "react-scripts": "4.0.3" 10 | }, 11 | "scripts": { 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test --env=jsdom", 15 | "eject": "react-scripts eject" 16 | }, 17 | "browserslist": [ 18 | ">0.2%", 19 | "not dead", 20 | "not ie <= 11", 21 | "not op_mini all" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-advanced-panels/public/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="utf-8" /> 5 | <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> 6 | <meta name="theme-color" content="#000000" /> 7 | <!-- 8 | manifest.json provides metadata used when your web app is added to the 9 | homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ 10 | --> 11 | <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> 12 | <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" /> 13 | <!-- 14 | Notice the use of %PUBLIC_URL% in the tags above. 15 | It will be replaced with the URL of the `public` folder during the build. 16 | Only files inside the `public` folder can be referenced from the HTML. 17 | 18 | Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will 19 | work correctly both with client-side routing and a non-root public URL. 20 | Learn how to configure a non-root public URL by running `npm run build`. 21 | --> 22 | <title>Leva Sandbox</title> 23 | </head> 24 | 25 | <body> 26 | <noscript> You need to enable JavaScript to run this app. </noscript> 27 | <div id="root"></div> 28 | <!-- 29 | This HTML file is a template. 30 | If you open it directly in the browser, you will see an empty page. 31 | 32 | You can add webfonts, meta tags, or analytics to this file. 33 | The build step will place the bundled scripts into the <body> tag. 34 | 35 | To begin the development, run `npm start` or `yarn start`. 36 | To create a production bundle, use `npm run build` or `yarn build`. 37 | --> 38 | </body> 39 | </html> 40 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-advanced-panels/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useControls, useStoreContext, useCreateStore, LevaPanel, LevaStoreProvider } from 'leva' 3 | 4 | function MyComponent() { 5 | const store = useStoreContext() 6 | useControls({ point: [0, 0] }, { store }) 7 | return null 8 | } 9 | 10 | export default function App() { 11 | const store1 = useCreateStore() 12 | const store2 = useCreateStore() 13 | useControls({ color: '#fff' }, { store: store1 }) 14 | useControls({ boolean: true }, { store: store2 }) 15 | return ( 16 | <div 17 | style={{ 18 | display: 'grid', 19 | width: 300, 20 | gridRowGap: 10, 21 | padding: 10, 22 | background: '#fff', 23 | }}> 24 | <LevaPanel store={store1} fill flat titleBar={false} /> 25 | <LevaPanel store={store2} fill flat titleBar={false} /> 26 | <LevaStoreProvider store={store1}> 27 | <MyComponent /> 28 | </LevaStoreProvider> 29 | </div> 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-advanced-panels/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: system-ui, sans-serif; 3 | min-height: 100vh; 4 | background: linear-gradient(140deg, rgb(165, 142, 251), rgb(233, 191, 248)); 5 | margin: 0; 6 | } 7 | 8 | *, 9 | *:after, 10 | *:before { 11 | box-sizing: border-box; 12 | } 13 | 14 | pre { 15 | max-width: 720px; 16 | margin: 20px; 17 | padding: 20px; 18 | border: 10px solid black; 19 | } 20 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-advanced-panels/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const rootElement = document.getElementById('root') 8 | ReactDOM.render( 9 | <React.StrictMode> 10 | <App /> 11 | </React.StrictMode>, 12 | rootElement 13 | ) 14 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-busy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leva-busy", 3 | "main": "src/index.jsx", 4 | "version": "1.0.0", 5 | "dependencies": { 6 | "@radix-ui/react-icons": "^1.0.2", 7 | "leva": "*", 8 | "noisejs": "2.1.0", 9 | "react": "^18.0.0", 10 | "react-dom": "^18.0.0", 11 | "react-scripts": "4.0.3", 12 | "react-use": "^17.2.4" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test --env=jsdom", 18 | "eject": "react-scripts eject" 19 | }, 20 | "browserslist": [ 21 | ">0.2%", 22 | "not dead", 23 | "not ie <= 11", 24 | "not op_mini all" 25 | ], 26 | "devDependencies": { 27 | "@types/react": "^18.0.0", 28 | "@types/react-dom": "^18.0.0", 29 | "typescript": "^4.1.5" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-busy/public/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="utf-8" /> 5 | <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> 6 | <meta name="theme-color" content="#000000" /> 7 | <!-- 8 | manifest.json provides metadata used when your web app is added to the 9 | homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ 10 | --> 11 | <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> 12 | <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" /> 13 | <!-- 14 | Notice the use of %PUBLIC_URL% in the tags above. 15 | It will be replaced with the URL of the `public` folder during the build. 16 | Only files inside the `public` folder can be referenced from the HTML. 17 | 18 | Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will 19 | work correctly both with client-side routing and a non-root public URL. 20 | Learn how to configure a non-root public URL by running `npm run build`. 21 | --> 22 | <title>Leva Sandbox</title> 23 | </head> 24 | 25 | <body> 26 | <noscript> You need to enable JavaScript to run this app. </noscript> 27 | <div id="root"></div> 28 | <!-- 29 | This HTML file is a template. 30 | If you open it directly in the browser, you will see an empty page. 31 | 32 | You can add webfonts, meta tags, or analytics to this file. 33 | The build step will place the bundled scripts into the <body> tag. 34 | 35 | To begin the development, run `npm start` or `yarn start`. 36 | To create a production bundle, use `npm run build` or `yarn build`. 37 | --> 38 | </body> 39 | </html> 40 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-busy/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: system-ui, sans-serif; 3 | min-height: 100vh; 4 | background: linear-gradient(140deg, rgb(165, 142, 251), rgb(233, 191, 248)); 5 | margin: 0; 6 | } 7 | 8 | *, 9 | *:after, 10 | *:before { 11 | box-sizing: border-box; 12 | } 13 | 14 | pre { 15 | max-width: 720px; 16 | margin: 20px; 17 | padding: 20px; 18 | border: 10px solid black; 19 | } 20 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-busy/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const rootElement = document.getElementById('root') 8 | ReactDOM.render( 9 | <React.StrictMode> 10 | <App /> 11 | </React.StrictMode>, 12 | rootElement 13 | ) 14 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-busy/src/styles.module.css: -------------------------------------------------------------------------------- 1 | .buttons { 2 | display: flex; 3 | font-size: 14px; 4 | padding: 10px; 5 | align-items: center; 6 | } 7 | 8 | .buttons > * { 9 | margin-left: 4px; 10 | } 11 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-custom-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leva-custom-plugin", 3 | "version": "1.0.0", 4 | "main": "src/index.tsx", 5 | "dependencies": { 6 | "leva": "*", 7 | "react": "^18.0.0", 8 | "react-dom": "^18.0.0", 9 | "react-scripts": "4.0.3" 10 | }, 11 | "devDependencies": { 12 | "@types/react": "^18.0.0", 13 | "@types/react-dom": "^18.0.0", 14 | "typescript": "^4.1.5" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test --env=jsdom", 20 | "eject": "react-scripts eject" 21 | }, 22 | "browserslist": [ 23 | ">0.2%", 24 | "not dead", 25 | "not ie <= 11", 26 | "not op_mini all" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-custom-plugin/public/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="utf-8" /> 5 | <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> 6 | <meta name="theme-color" content="#000000" /> 7 | <!-- 8 | manifest.json provides metadata used when your web app is added to the 9 | homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ 10 | --> 11 | <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> 12 | <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" /> 13 | <!-- 14 | Notice the use of %PUBLIC_URL% in the tags above. 15 | It will be replaced with the URL of the `public` folder during the build. 16 | Only files inside the `public` folder can be referenced from the HTML. 17 | 18 | Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will 19 | work correctly both with client-side routing and a non-root public URL. 20 | Learn how to configure a non-root public URL by running `npm run build`. 21 | --> 22 | <title>Leva Sandbox</title> 23 | </head> 24 | 25 | <body> 26 | <noscript> You need to enable JavaScript to run this app. </noscript> 27 | <div id="root"></div> 28 | <!-- 29 | This HTML file is a template. 30 | If you open it directly in the browser, you will see an empty page. 31 | 32 | You can add webfonts, meta tags, or analytics to this file. 33 | The build step will place the bundled scripts into the <body> tag. 34 | 35 | To begin the development, run `npm start` or `yarn start`. 36 | To create a production bundle, use `npm run build` or `yarn build`. 37 | --> 38 | </body> 39 | </html> 40 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-custom-plugin/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Leva, useControls } from 'leva' 3 | import { createPlugin, useInputContext, LevaInputProps, Components } from 'leva/plugin' 4 | 5 | const { Row, Label, String } = Components 6 | 7 | type GreenOrBlueSettings = { alpha?: number } 8 | type GreenOrBlueType = { color?: string; light: boolean } 9 | type GreenOrBlueInput = GreenOrBlueType & GreenOrBlueSettings 10 | 11 | type GreenOrBlueProps = LevaInputProps<GreenOrBlueType, GreenOrBlueSettings, string> 12 | 13 | function GreenOrBlue() { 14 | const props = useInputContext<GreenOrBlueProps>() 15 | const { label, displayValue, onUpdate, onChange, settings } = props 16 | const background = displayValue 17 | 18 | return ( 19 | <Row input> 20 | <Label style={{ background, opacity: settings.alpha }}>{label}</Label> 21 | <String displayValue={displayValue} onUpdate={onUpdate} onChange={onChange} /> 22 | </Row> 23 | ) 24 | } 25 | 26 | const normalize = ({ color, light, alpha }: GreenOrBlueInput) => { 27 | return { value: { color, light }, settings: { alpha } } 28 | } 29 | 30 | const sanitize = (v: string): GreenOrBlueType => { 31 | if (!['green', 'blue', 'lightgreen', 'lightblue'].includes(v)) throw Error('Invalid value') 32 | // @ts-ignore 33 | const [, isLight, color] = v.match(/(light)?(.*)/) 34 | return { light: !!isLight, color } 35 | } 36 | 37 | const format = (v: GreenOrBlueType) => (v.light ? 'light' : '') + v.color 38 | 39 | const greenOrBlue = createPlugin({ 40 | sanitize, 41 | format, 42 | normalize, 43 | component: GreenOrBlue, 44 | }) 45 | 46 | export default function App() { 47 | const data = useControls({ 48 | myPlugin: greenOrBlue({ color: 'green', light: true, alpha: 0.5 }), 49 | }) 50 | 51 | return ( 52 | <> 53 | <Leva titleBar={false} /> 54 | <pre>{JSON.stringify(data, null, ' ')}</pre> 55 | </> 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-custom-plugin/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: system-ui, sans-serif; 3 | min-height: 100vh; 4 | background: linear-gradient(140deg, rgb(165, 142, 251), rgb(233, 191, 248)); 5 | margin: 0; 6 | } 7 | 8 | *, 9 | *:after, 10 | *:before { 11 | box-sizing: border-box; 12 | } 13 | 14 | pre { 15 | max-width: 720px; 16 | margin: 20px; 17 | padding: 20px; 18 | border: 10px solid black; 19 | } 20 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-custom-plugin/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const rootElement = document.getElementById('root') 8 | ReactDOM.render( 9 | <React.StrictMode> 10 | <App /> 11 | </React.StrictMode>, 12 | rootElement 13 | ) 14 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-minimal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leva-minimal", 3 | "version": "1.0.0", 4 | "main": "src/index.jsx", 5 | "dependencies": { 6 | "@radix-ui/react-icons": "^1.0.2", 7 | "leva": "*", 8 | "react": "^18.0.0", 9 | "react-dom": "^18.0.0", 10 | "react-scripts": "4.0.3" 11 | }, 12 | "scripts": { 13 | "start": "react-scripts start", 14 | "build": "react-scripts build", 15 | "test": "react-scripts test --env=jsdom", 16 | "eject": "react-scripts eject" 17 | }, 18 | "browserslist": [ 19 | ">0.2%", 20 | "not dead", 21 | "not ie <= 11", 22 | "not op_mini all" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-minimal/public/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="utf-8" /> 5 | <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> 6 | <meta name="theme-color" content="#000000" /> 7 | <!-- 8 | manifest.json provides metadata used when your web app is added to the 9 | homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ 10 | --> 11 | <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> 12 | <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" /> 13 | <!-- 14 | Notice the use of %PUBLIC_URL% in the tags above. 15 | It will be replaced with the URL of the `public` folder during the build. 16 | Only files inside the `public` folder can be referenced from the HTML. 17 | 18 | Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will 19 | work correctly both with client-side routing and a non-root public URL. 20 | Learn how to configure a non-root public URL by running `npm run build`. 21 | --> 22 | <title>Leva Sandbox</title> 23 | </head> 24 | 25 | <body> 26 | <noscript> You need to enable JavaScript to run this app. </noscript> 27 | <div id="root"></div> 28 | <!-- 29 | This HTML file is a template. 30 | If you open it directly in the browser, you will see an empty page. 31 | 32 | You can add webfonts, meta tags, or analytics to this file. 33 | The build step will place the bundled scripts into the <body> tag. 34 | 35 | To begin the development, run `npm start` or `yarn start`. 36 | To create a production bundle, use `npm run build` or `yarn build`. 37 | --> 38 | </body> 39 | </html> 40 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-minimal/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useControls, Leva } from 'leva' 3 | import { Half2Icon } from '@radix-ui/react-icons' 4 | 5 | export default function App() { 6 | const data = useControls({ 7 | number: 10, 8 | minmax: { value: 12.5, min: 5.5, max: 30.5, optional: true }, 9 | printSize: { value: 100, min: 80, max: 140, step: 10 }, 10 | color: { 11 | value: '#f00', 12 | hint: 'Hey, we support icons and hinting values and long text will wrap!', 13 | label: <Half2Icon />, 14 | }, 15 | }) 16 | 17 | return ( 18 | <> 19 | <Leva titleBar={false} /> 20 | <pre>{JSON.stringify(data, null, ' ')}</pre> 21 | </> 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-minimal/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: system-ui, sans-serif; 3 | min-height: 100vh; 4 | background: linear-gradient(140deg, rgb(165, 142, 251), rgb(233, 191, 248)); 5 | margin: 0; 6 | } 7 | 8 | *, 9 | *:after, 10 | *:before { 11 | box-sizing: border-box; 12 | } 13 | 14 | pre { 15 | max-width: 720px; 16 | margin: 20px; 17 | padding: 20px; 18 | border: 10px solid black; 19 | } 20 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-minimal/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const rootElement = document.getElementById('root') 8 | ReactDOM.render( 9 | <React.StrictMode> 10 | <App /> 11 | </React.StrictMode>, 12 | rootElement 13 | ) 14 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-plugin-bezier/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leva-plugin-bezier", 3 | "version": "1.0.0", 4 | "keywords": [], 5 | "main": "src/index.jsx", 6 | "dependencies": { 7 | "@leva-ui/plugin-bezier": "*", 8 | "leva": "*", 9 | "react": "^18.0.0", 10 | "react-dom": "^18.0.0", 11 | "react-scripts": "4.0.3" 12 | }, 13 | "devDependencies": { 14 | "@types/react": "^18.0.0", 15 | "@types/react-dom": "^18.0.0", 16 | "typescript": "^4.1.5" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test --env=jsdom", 22 | "eject": "react-scripts eject" 23 | }, 24 | "browserslist": [ 25 | ">0.2%", 26 | "not dead", 27 | "not ie <= 11", 28 | "not op_mini all" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-plugin-bezier/public/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="utf-8" /> 5 | <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> 6 | <meta name="theme-color" content="#000000" /> 7 | <!-- 8 | manifest.json provides metadata used when your web app is added to the 9 | homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ 10 | --> 11 | <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> 12 | <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" /> 13 | <!-- 14 | Notice the use of %PUBLIC_URL% in the tags above. 15 | It will be replaced with the URL of the `public` folder during the build. 16 | Only files inside the `public` folder can be referenced from the HTML. 17 | 18 | Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will 19 | work correctly both with client-side routing and a non-root public URL. 20 | Learn how to configure a non-root public URL by running `npm run build`. 21 | --> 22 | <title>Leva Sandbox</title> 23 | </head> 24 | 25 | <body> 26 | <noscript> You need to enable JavaScript to run this app. </noscript> 27 | <div id="root"></div> 28 | <!-- 29 | This HTML file is a template. 30 | If you open it directly in the browser, you will see an empty page. 31 | 32 | You can add webfonts, meta tags, or analytics to this file. 33 | The build step will place the bundled scripts into the <body> tag. 34 | 35 | To begin the development, run `npm start` or `yarn start`. 36 | To create a production bundle, use `npm run build` or `yarn build`. 37 | --> 38 | </body> 39 | </html> 40 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-plugin-bezier/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useControls } from 'leva' 3 | import { bezier } from '@leva-ui/plugin-bezier' 4 | import './style.css' 5 | 6 | export default function App() { 7 | const { curve } = useControls({ curve: bezier() }) 8 | 9 | return ( 10 | <div className="App"> 11 | <div className="bezier-animated" style={{ animationTimingFunction: curve.cssEasing }} /> 12 | <pre>{JSON.stringify(curve, null, ' ')}</pre> 13 | </div> 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-plugin-bezier/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: system-ui, sans-serif; 3 | min-height: 100vh; 4 | background: linear-gradient(140deg, rgb(165, 142, 251), rgb(233, 191, 248)); 5 | margin: 0; 6 | } 7 | 8 | *, 9 | *:after, 10 | *:before { 11 | box-sizing: border-box; 12 | } 13 | 14 | pre { 15 | max-width: 720px; 16 | margin: 20px; 17 | padding: 20px; 18 | border: 10px solid black; 19 | } 20 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-plugin-bezier/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const rootElement = document.getElementById('root') 8 | ReactDOM.render( 9 | <React.StrictMode> 10 | <App /> 11 | </React.StrictMode>, 12 | rootElement 13 | ) 14 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-plugin-bezier/src/style.css: -------------------------------------------------------------------------------- 1 | @keyframes bezierStoryScale { 2 | 0% { 3 | transform: scaleX(0); 4 | } 5 | 6 | 100% { 7 | transform: scaleX(1); 8 | } 9 | } 10 | 11 | .bezier-animated { 12 | height: 10px; 13 | width: 200px; 14 | background: indianred; 15 | transform-origin: left; 16 | animation: bezierStoryScale 1000ms infinite alternate both; 17 | } 18 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-plugin-dates/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leva-plugin-dates", 3 | "version": "1.0.0", 4 | "keywords": [], 5 | "main": "src/index.jsx", 6 | "dependencies": { 7 | "@leva-ui/plugin-dates": "*", 8 | "leva": "*", 9 | "react": "^18.0.0", 10 | "react-dom": "^18.0.0", 11 | "react-scripts": "4.0.3" 12 | }, 13 | "devDependencies": { 14 | "@types/react": "^18.0.0", 15 | "@types/react-dom": "^18.0.0", 16 | "typescript": "^4.1.5" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test --env=jsdom", 22 | "eject": "react-scripts eject" 23 | }, 24 | "browserslist": [ 25 | ">0.2%", 26 | "not dead", 27 | "not ie <= 11", 28 | "not op_mini all" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-plugin-dates/public/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="utf-8" /> 5 | <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> 6 | <meta name="theme-color" content="#000000" /> 7 | <!-- 8 | manifest.json provides metadata used when your web app is added to the 9 | homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ 10 | --> 11 | <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> 12 | <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" /> 13 | <!-- 14 | Notice the use of %PUBLIC_URL% in the tags above. 15 | It will be replaced with the URL of the `public` folder during the build. 16 | Only files inside the `public` folder can be referenced from the HTML. 17 | 18 | Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will 19 | work correctly both with client-side routing and a non-root public URL. 20 | Learn how to configure a non-root public URL by running `npm run build`. 21 | --> 22 | <title>Leva Sandbox</title> 23 | </head> 24 | 25 | <body> 26 | <noscript> You need to enable JavaScript to run this app. </noscript> 27 | <div id="root"></div> 28 | <!-- 29 | This HTML file is a template. 30 | If you open it directly in the browser, you will see an empty page. 31 | 32 | You can add webfonts, meta tags, or analytics to this file. 33 | The build step will place the bundled scripts into the <body> tag. 34 | 35 | To begin the development, run `npm start` or `yarn start`. 36 | To create a production bundle, use `npm run build` or `yarn build`. 37 | --> 38 | </body> 39 | </html> 40 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-plugin-dates/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { date } from '@leva-ui/plugin-dates' 3 | import { useControls } from 'leva' 4 | 5 | export default function App() { 6 | const { birthday } = useControls({ 7 | birthday: date({ 8 | date: new Date(), 9 | locale: 'en-UK', 10 | inputFormat: 'dd.MM.yyyy', 11 | }), 12 | }) 13 | 14 | return <div className="App">{birthday.formattedDate}</div> 15 | } 16 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-plugin-dates/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: system-ui, sans-serif; 3 | min-height: 100vh; 4 | background: linear-gradient(140deg, rgb(165, 142, 251), rgb(233, 191, 248)); 5 | margin: 0; 6 | } 7 | 8 | *, 9 | *:after, 10 | *:before { 11 | box-sizing: border-box; 12 | } 13 | 14 | pre { 15 | max-width: 720px; 16 | margin: 20px; 17 | padding: 20px; 18 | border: 10px solid black; 19 | } 20 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-plugin-dates/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const rootElement = document.getElementById('root') 8 | ReactDOM.render( 9 | <React.StrictMode> 10 | <App /> 11 | </React.StrictMode>, 12 | rootElement 13 | ) 14 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-plugin-plot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leva-plugin-plot", 3 | "version": "1.0.0", 4 | "keywords": [], 5 | "main": "src/index.jsx", 6 | "dependencies": { 7 | "@leva-ui/plugin-plot": "*", 8 | "leva": "*", 9 | "react": "^18.0.0", 10 | "react-dom": "^18.0.0", 11 | "react-scripts": "4.0.3" 12 | }, 13 | "devDependencies": { 14 | "@types/react": "^18.0.0", 15 | "@types/react-dom": "^18.0.0", 16 | "typescript": "^4.1.5" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test --env=jsdom", 22 | "eject": "react-scripts eject" 23 | }, 24 | "browserslist": [ 25 | ">0.2%", 26 | "not dead", 27 | "not ie <= 11", 28 | "not op_mini all" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-plugin-plot/public/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="utf-8" /> 5 | <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> 6 | <meta name="theme-color" content="#000000" /> 7 | <!-- 8 | manifest.json provides metadata used when your web app is added to the 9 | homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ 10 | --> 11 | <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> 12 | <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" /> 13 | <!-- 14 | Notice the use of %PUBLIC_URL% in the tags above. 15 | It will be replaced with the URL of the `public` folder during the build. 16 | Only files inside the `public` folder can be referenced from the HTML. 17 | 18 | Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will 19 | work correctly both with client-side routing and a non-root public URL. 20 | Learn how to configure a non-root public URL by running `npm run build`. 21 | --> 22 | <title>Leva Sandbox</title> 23 | </head> 24 | 25 | <body> 26 | <noscript> You need to enable JavaScript to run this app. </noscript> 27 | <div id="root"></div> 28 | <!-- 29 | This HTML file is a template. 30 | If you open it directly in the browser, you will see an empty page. 31 | 32 | You can add webfonts, meta tags, or analytics to this file. 33 | The build step will place the bundled scripts into the <body> tag. 34 | 35 | To begin the development, run `npm start` or `yarn start`. 36 | To create a production bundle, use `npm run build` or `yarn build`. 37 | --> 38 | </body> 39 | </html> 40 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-plugin-plot/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useControls, monitor } from 'leva' 3 | import { plot } from '@leva-ui/plugin-plot' 4 | 5 | export default function App() { 6 | const p = React.useRef(performance.now()) 7 | const values = useControls({ 8 | w: 1, 9 | y1: plot({ expression: 'cos(x*w)', boundsX: [-10, 10] }), 10 | y2: plot({ expression: 'x * y1', boundsX: [-100, 100] }), 11 | y3: plot({ expression: 'tan(y2)', boundsX: [-4, 4], boundsY: [-10, 10] }), 12 | }) 13 | 14 | useControls( 15 | { 16 | 'y1(t)': monitor( 17 | () => { 18 | const t = performance.now() - p.current 19 | return values.y1(t / 100) 20 | }, 21 | { graph: true, interval: 30 } 22 | ), 23 | }, 24 | [values.y1] 25 | ) 26 | 27 | const t1 = values.y1(1) 28 | const t2 = values.y2(1) 29 | const t3 = values.y3(1) 30 | return ( 31 | <div className="App"> 32 | <pre>y1(1) = {t1}</pre> 33 | <pre>y2(1) = {t2}</pre> 34 | <pre>y3(1) = {t3}</pre> 35 | </div> 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-plugin-plot/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: system-ui, sans-serif; 3 | min-height: 100vh; 4 | background: linear-gradient(140deg, rgb(165, 142, 251), rgb(233, 191, 248)); 5 | margin: 0; 6 | } 7 | 8 | *, 9 | *:after, 10 | *:before { 11 | box-sizing: border-box; 12 | } 13 | 14 | pre { 15 | max-width: 720px; 16 | margin: 20px; 17 | padding: 20px; 18 | border: 10px solid black; 19 | } 20 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-plugin-plot/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const rootElement = document.getElementById('root') 8 | ReactDOM.render( 9 | <React.StrictMode> 10 | <App /> 11 | </React.StrictMode>, 12 | rootElement 13 | ) 14 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-plugin-spring/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leva-plugin-spring", 3 | "version": "1.0.0", 4 | "keywords": [], 5 | "main": "src/index.jsx", 6 | "dependencies": { 7 | "@leva-ui/plugin-spring": "*", 8 | "leva": "*", 9 | "react": "^18.0.0", 10 | "react-dom": "^18.0.0", 11 | "react-scripts": "4.0.3" 12 | }, 13 | "devDependencies": { 14 | "@types/react": "^18.0.0", 15 | "@types/react-dom": "^18.0.0", 16 | "typescript": "^4.1.5" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test --env=jsdom", 22 | "eject": "react-scripts eject" 23 | }, 24 | "browserslist": [ 25 | ">0.2%", 26 | "not dead", 27 | "not ie <= 11", 28 | "not op_mini all" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-plugin-spring/public/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="utf-8" /> 5 | <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> 6 | <meta name="theme-color" content="#000000" /> 7 | <!-- 8 | manifest.json provides metadata used when your web app is added to the 9 | homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ 10 | --> 11 | <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> 12 | <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" /> 13 | <!-- 14 | Notice the use of %PUBLIC_URL% in the tags above. 15 | It will be replaced with the URL of the `public` folder during the build. 16 | Only files inside the `public` folder can be referenced from the HTML. 17 | 18 | Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will 19 | work correctly both with client-side routing and a non-root public URL. 20 | Learn how to configure a non-root public URL by running `npm run build`. 21 | --> 22 | <title>Leva Sandbox</title> 23 | </head> 24 | 25 | <body> 26 | <noscript> You need to enable JavaScript to run this app. </noscript> 27 | <div id="root"></div> 28 | <!-- 29 | This HTML file is a template. 30 | If you open it directly in the browser, you will see an empty page. 31 | 32 | You can add webfonts, meta tags, or analytics to this file. 33 | The build step will place the bundled scripts into the <body> tag. 34 | 35 | To begin the development, run `npm start` or `yarn start`. 36 | To create a production bundle, use `npm run build` or `yarn build`. 37 | --> 38 | </body> 39 | </html> 40 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-plugin-spring/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useControls } from 'leva' 3 | import { spring } from '@leva-ui/plugin-spring' 4 | 5 | export default function App() { 6 | const { mySpring } = useControls({ 7 | mySpring: spring({ tension: 100, friction: 30, hint: 'spring to use with react-spring' }), 8 | }) 9 | 10 | return ( 11 | <div className="App"> 12 | <pre>{JSON.stringify(mySpring, null, ' ')}</pre> 13 | </div> 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-plugin-spring/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: system-ui, sans-serif; 3 | min-height: 100vh; 4 | background: linear-gradient(140deg, rgb(165, 142, 251), rgb(233, 191, 248)); 5 | margin: 0; 6 | } 7 | 8 | *, 9 | *:after, 10 | *:before { 11 | box-sizing: border-box; 12 | } 13 | 14 | pre { 15 | max-width: 720px; 16 | margin: 20px; 17 | padding: 20px; 18 | border: 10px solid black; 19 | } 20 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-plugin-spring/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const rootElement = document.getElementById('root') 8 | ReactDOM.render( 9 | <React.StrictMode> 10 | <App /> 11 | </React.StrictMode>, 12 | rootElement 13 | ) 14 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-scroll/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leva-scroll", 3 | "version": "1.0.0", 4 | "description": "This sandbox has been generated!", 5 | "keywords": [], 6 | "main": "src/index.jsx", 7 | "dependencies": { 8 | "leva": "*", 9 | "noisejs": "2.1.0", 10 | "react": "^18.0.0", 11 | "react-dom": "^18.0.0", 12 | "react-scripts": "4.0.3" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test --env=jsdom", 18 | "eject": "react-scripts eject" 19 | }, 20 | "browserslist": [ 21 | ">0.2%", 22 | "not dead", 23 | "not ie <= 11", 24 | "not op_mini all" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-scroll/public/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="utf-8" /> 5 | <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> 6 | <meta name="theme-color" content="#000000" /> 7 | <!-- 8 | manifest.json provides metadata used when your web app is added to the 9 | homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ 10 | --> 11 | <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> 12 | <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" /> 13 | <!-- 14 | Notice the use of %PUBLIC_URL% in the tags above. 15 | It will be replaced with the URL of the `public` folder during the build. 16 | Only files inside the `public` folder can be referenced from the HTML. 17 | 18 | Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will 19 | work correctly both with client-side routing and a non-root public URL. 20 | Learn how to configure a non-root public URL by running `npm run build`. 21 | --> 22 | <title>Leva Sandbox</title> 23 | </head> 24 | 25 | <body> 26 | <noscript> You need to enable JavaScript to run this app. </noscript> 27 | <div id="root"></div> 28 | <!-- 29 | This HTML file is a template. 30 | If you open it directly in the browser, you will see an empty page. 31 | 32 | You can add webfonts, meta tags, or analytics to this file. 33 | The build step will place the bundled scripts into the <body> tag. 34 | 35 | To begin the development, run `npm start` or `yarn start`. 36 | To create a production bundle, use `npm run build` or `yarn build`. 37 | --> 38 | </body> 39 | </html> 40 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-scroll/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useControls, folder, button, monitor, Leva } from 'leva' 3 | import { Noise } from 'noisejs' 4 | 5 | const noise = new Noise(Math.random()) 6 | 7 | function frame() { 8 | const t = Date.now() 9 | return noise.simplex2(t / 1000, t / 100) 10 | } 11 | 12 | export default function App() { 13 | const data = useControls({ 14 | first: { value: 0, min: -10, max: 10 }, 15 | image: { image: undefined }, 16 | select: { options: ['x', 'y', ['x', 'y']] }, 17 | interval: { min: -100, max: 100, value: [-10, 10] }, 18 | color: '#ffffff', 19 | refMonitor: monitor(frame, { graph: true, interval: 30 }), 20 | number: { value: 1000, min: 3 }, 21 | folder2: folder({ 22 | boolean: false, 23 | spring: { tension: 100, friction: 30 }, 24 | folder3: folder( 25 | { 26 | // eslint-disable-next-line no-console 27 | 'Hello Button': button(() => console.log('hello')), 28 | folder4: folder({ 29 | pos2d: { x: 3, y: 4 }, 30 | pos2dArr: [100, 200], 31 | pos3d: { x: 0.3, y: 0.1, z: 0.5 }, 32 | pos3dArr: [Math.PI / 2, 20, 4], 33 | }), 34 | }, 35 | { collapsed: false } 36 | ), 37 | }), 38 | colorObj: { r: 1, g: 2, b: 3 }, 39 | }) 40 | 41 | return ( 42 | <> 43 | <Leva oneLineLabels /> 44 | 45 | <div className="App"> 46 | <pre>{JSON.stringify(data, null, ' ')}</pre> 47 | </div> 48 | </> 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-scroll/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: system-ui, sans-serif; 3 | min-height: 100vh; 4 | background: linear-gradient(140deg, rgb(165, 142, 251), rgb(233, 191, 248)); 5 | margin: 0; 6 | } 7 | 8 | *, 9 | *:after, 10 | *:before { 11 | box-sizing: border-box; 12 | } 13 | 14 | pre { 15 | max-width: 720px; 16 | margin: 20px; 17 | padding: 20px; 18 | border: 10px solid black; 19 | } 20 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-scroll/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const rootElement = document.getElementById('root') 8 | ReactDOM.render( 9 | <React.StrictMode> 10 | <App /> 11 | </React.StrictMode>, 12 | rootElement 13 | ) 14 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-theme/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leva-theme", 3 | "version": "1.0.0", 4 | "main": "src/index.jsx", 5 | "dependencies": { 6 | "leva": "*", 7 | "@leva-ui/plugin-spring": "*", 8 | "noisejs": "2.1.0", 9 | "react": "^18.0.0", 10 | "react-dom": "^18.0.0", 11 | "react-scripts": "4.0.3" 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "build": "react-scripts build", 16 | "test": "react-scripts test --env=jsdom", 17 | "eject": "react-scripts eject" 18 | }, 19 | "browserslist": [ 20 | ">0.2%", 21 | "not dead", 22 | "not ie <= 11", 23 | "not op_mini all" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-theme/public/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="utf-8" /> 5 | <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> 6 | <meta name="theme-color" content="#000000" /> 7 | <!-- 8 | manifest.json provides metadata used when your web app is added to the 9 | homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ 10 | --> 11 | <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> 12 | <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" /> 13 | <!-- 14 | Notice the use of %PUBLIC_URL% in the tags above. 15 | It will be replaced with the URL of the `public` folder during the build. 16 | Only files inside the `public` folder can be referenced from the HTML. 17 | 18 | Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will 19 | work correctly both with client-side routing and a non-root public URL. 20 | Learn how to configure a non-root public URL by running `npm run build`. 21 | --> 22 | <title>Leva Sandbox</title> 23 | </head> 24 | 25 | <body> 26 | <noscript> You need to enable JavaScript to run this app. </noscript> 27 | <div id="root"></div> 28 | <!-- 29 | This HTML file is a template. 30 | If you open it directly in the browser, you will see an empty page. 31 | 32 | You can add webfonts, meta tags, or analytics to this file. 33 | The build step will place the bundled scripts into the <body> tag. 34 | 35 | To begin the development, run `npm start` or `yarn start`. 36 | To create a production bundle, use `npm run build` or `yarn build`. 37 | --> 38 | </body> 39 | </html> 40 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-theme/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: system-ui, sans-serif; 3 | min-height: 100vh; 4 | background: linear-gradient(140deg, rgb(165, 142, 251), rgb(233, 191, 248)); 5 | margin: 0; 6 | } 7 | 8 | *, 9 | *:after, 10 | *:before { 11 | box-sizing: border-box; 12 | } 13 | 14 | pre { 15 | max-width: 720px; 16 | margin: 20px; 17 | padding: 20px; 18 | border: 10px solid black; 19 | } 20 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-theme/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const rootElement = document.getElementById('root') 8 | ReactDOM.render( 9 | <React.StrictMode> 10 | <App /> 11 | </React.StrictMode>, 12 | rootElement 13 | ) 14 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-transient/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leva-transient", 3 | "version": "1.0.0", 4 | "main": "src/index.jsx", 5 | "dependencies": { 6 | "@react-three/drei": "^4.3.3", 7 | "@react-three/fiber": "^6.0.21", 8 | "leva": "*", 9 | "react": "^18.0.0", 10 | "react-dom": "^18.0.0", 11 | "react-scripts": "4.0.3", 12 | "three": "^0.143.0" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test --env=jsdom", 18 | "eject": "react-scripts eject" 19 | }, 20 | "browserslist": [ 21 | ">0.2%", 22 | "not dead", 23 | "not ie <= 11", 24 | "not op_mini all" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-transient/public/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="utf-8" /> 5 | <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> 6 | <meta name="theme-color" content="#000000" /> 7 | <!-- 8 | manifest.json provides metadata used when your web app is added to the 9 | homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ 10 | --> 11 | <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> 12 | <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" /> 13 | <!-- 14 | Notice the use of %PUBLIC_URL% in the tags above. 15 | It will be replaced with the URL of the `public` folder during the build. 16 | Only files inside the `public` folder can be referenced from the HTML. 17 | 18 | Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will 19 | work correctly both with client-side routing and a non-root public URL. 20 | Learn how to configure a non-root public URL by running `npm run build`. 21 | --> 22 | <title>Leva Sandbox</title> 23 | </head> 24 | 25 | <body> 26 | <noscript> You need to enable JavaScript to run this app. </noscript> 27 | <div id="root"></div> 28 | <!-- 29 | This HTML file is a template. 30 | If you open it directly in the browser, you will see an empty page. 31 | 32 | You can add webfonts, meta tags, or analytics to this file. 33 | The build step will place the bundled scripts into the <body> tag. 34 | 35 | To begin the development, run `npm start` or `yarn start`. 36 | To create a production bundle, use `npm run build` or `yarn build`. 37 | --> 38 | </body> 39 | </html> 40 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-transient/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react' 2 | import { useControls } from 'leva' 3 | import { Canvas } from '@react-three/fiber' 4 | import { OrbitControls } from '@react-three/drei' 5 | 6 | import * as THREE from 'three' 7 | 8 | const torusknot = new THREE.TorusKnotBufferGeometry(3, 0.8, 256, 16) 9 | 10 | const Mesh = () => { 11 | const matRef = useRef() 12 | useControls({ color: { value: 'indianred', onChange: (v) => matRef.current && matRef.current.color.set(v) } }) 13 | return ( 14 | <mesh geometry={torusknot}> 15 | <meshPhysicalMaterial ref={matRef} attach="material" flatShading /> 16 | </mesh> 17 | ) 18 | } 19 | 20 | export default function App() { 21 | return ( 22 | <Canvas 23 | pixelRatio={[1, 2]} 24 | camera={{ position: [0, 0, 16], fov: 50 }} 25 | style={{ background: 'dimgray', height: '100vh', width: '100vw' }}> 26 | <OrbitControls /> 27 | <directionalLight /> 28 | <Mesh /> 29 | </Canvas> 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-transient/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: system-ui, sans-serif; 3 | min-height: 100vh; 4 | background: linear-gradient(140deg, rgb(165, 142, 251), rgb(233, 191, 248)); 5 | margin: 0; 6 | } 7 | 8 | *, 9 | *:after, 10 | *:before { 11 | box-sizing: border-box; 12 | } 13 | 14 | pre { 15 | max-width: 720px; 16 | margin: 20px; 17 | padding: 20px; 18 | border: 10px solid black; 19 | } 20 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-transient/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const rootElement = document.getElementById('root') 8 | ReactDOM.render( 9 | <React.StrictMode> 10 | <App /> 11 | </React.StrictMode>, 12 | rootElement 13 | ) 14 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leva-ui", 3 | "version": "1.0.0", 4 | "description": "This sandbox has been generated!", 5 | "keywords": [], 6 | "main": "src/index.jsx", 7 | "dependencies": { 8 | "leva": "*", 9 | "react": "^18.0.0", 10 | "react-dom": "^18.0.0", 11 | "react-dropzone": "11.3.1", 12 | "react-scripts": "4.0.3", 13 | "react-use-gesture": "^9.0.0" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test --env=jsdom", 19 | "eject": "react-scripts eject" 20 | }, 21 | "browserslist": [ 22 | ">0.2%", 23 | "not dead", 24 | "not ie <= 11", 25 | "not op_mini all" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-ui/public/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="utf-8" /> 5 | <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> 6 | <meta name="theme-color" content="#000000" /> 7 | <!-- 8 | manifest.json provides metadata used when your web app is added to the 9 | homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ 10 | --> 11 | <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> 12 | <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" /> 13 | <!-- 14 | Notice the use of %PUBLIC_URL% in the tags above. 15 | It will be replaced with the URL of the `public` folder during the build. 16 | Only files inside the `public` folder can be referenced from the HTML. 17 | 18 | Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will 19 | work correctly both with client-side routing and a non-root public URL. 20 | Learn how to configure a non-root public URL by running `npm run build`. 21 | --> 22 | <title>Leva Sandbox</title> 23 | </head> 24 | 25 | <body> 26 | <noscript> You need to enable JavaScript to run this app. </noscript> 27 | <div id="root"></div> 28 | <!-- 29 | This HTML file is a template. 30 | If you open it directly in the browser, you will see an empty page. 31 | 32 | You can add webfonts, meta tags, or analytics to this file. 33 | The build step will place the bundled scripts into the <body> tag. 34 | 35 | To begin the development, run `npm start` or `yarn start`. 36 | To create a production bundle, use `npm run build` or `yarn build`. 37 | --> 38 | </body> 39 | </html> 40 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-ui/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: system-ui, sans-serif; 3 | min-height: 100vh; 4 | background: linear-gradient(140deg, rgb(165, 142, 251), rgb(233, 191, 248)); 5 | margin: 0; 6 | } 7 | 8 | *, 9 | *:after, 10 | *:before { 11 | box-sizing: border-box; 12 | } 13 | 14 | pre { 15 | max-width: 720px; 16 | margin: 20px; 17 | padding: 20px; 18 | border: 10px solid black; 19 | } 20 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-ui/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const rootElement = document.getElementById('root') 8 | ReactDOM.render( 9 | <React.StrictMode> 10 | <App /> 11 | </React.StrictMode>, 12 | rootElement 13 | ) 14 | -------------------------------------------------------------------------------- /demo/src/styles.module.css: -------------------------------------------------------------------------------- 1 | .back { 2 | position: fixed; 3 | left: 10px; 4 | bottom: 10px; 5 | z-index: 100; 6 | padding: 10px; 7 | background: #000; 8 | color: #fff; 9 | font-weight: 500; 10 | font-size: 14px; 11 | text-decoration: none; 12 | border-radius: 2px; 13 | border: 1px solid #333; 14 | } 15 | 16 | .link { 17 | color: inherit; 18 | } 19 | 20 | .linkList { 21 | display: grid; 22 | gap: 10px; 23 | } 24 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "types": ["vite/client"], 6 | "allowJs": true, 7 | "skipLibCheck": false, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react" 18 | }, 19 | "include": ["./src"] 20 | } 21 | -------------------------------------------------------------------------------- /demo/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import reactRefresh from '@vitejs/plugin-react-refresh' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [reactRefresh()], 7 | }) 8 | -------------------------------------------------------------------------------- /docs/advanced/circle-drag.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/leva/f66726534e85e356af64c4556cb5c8b605175598/docs/advanced/circle-drag.gif -------------------------------------------------------------------------------- /docs/advanced/creating-plugins.md: -------------------------------------------------------------------------------- 1 | # Creating Plugins 2 | 3 | @TODO 4 | This page will contain info on how to create custom plugins, 5 | what even qualifies as a plugin, and an idea of what kinds of plugins could be included and maintained in this repo 6 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | ## Configuration 2 | 3 | You can configure Leva by using the `<Leva>` component anywhere in your App: 4 | 5 | ```jsx 6 | import { Leva } from 'leva' 7 | 8 | export default function MyApp() { 9 | return ( 10 | <> 11 | <Leva 12 | theme={myTheme} // you can pass a custom theme (see the styling section) 13 | fill // default = false, true makes the pane fill the parent dom node it's rendered in 14 | flat // default = false, true removes border radius and shadow 15 | oneLineLabels // default = false, alternative layout for labels, with labels and fields on separate rows 16 | hideTitleBar // default = false, hides the GUI header 17 | collapsed // default = false, when true the GUI is collpased 18 | hidden // default = false, when true the GUI is hidden 19 | /> 20 | </> 21 | ) 22 | } 23 | ``` 24 | 25 | - TODO // Add default config for LevaPanel as well 26 | 27 | ### Disabling the GUI 28 | 29 | Each instance of the `useControls` hook will render the panel. If you want to completely disable the GUI based on preferences, you need to explicitly set `hidden` to false. 30 | 31 | ```jsx 32 | import { Leva } from 'leva' 33 | 34 | function MyComponent() { 35 | const { myValue } = useControls({ myValue: 10 }) // Won't be visible because the panel will not render. 36 | 37 | return myValue 38 | } 39 | 40 | export default function MyApp() { 41 | return ( 42 | <> 43 | <Leva {...config} hidden={false} /> 44 | </> 45 | ) 46 | } 47 | ``` 48 | -------------------------------------------------------------------------------- /docs/plugins.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | What are plugins 4 | 5 | ## How to find plugins 6 | 7 | @TODO 8 | 9 | ## How to use plugins 10 | 11 | @TODO 12 | 13 | ## Creating a plugin 14 | -------------------------------------------------------------------------------- /docs/special-inputs.md: -------------------------------------------------------------------------------- 1 | # Special Inputs 2 | 3 | ### Button 4 | 5 | A simple button: 6 | 7 | ```jsx 8 | const button = useControls({ 9 | foo: button(() => console.log('clicked')), 10 | }) 11 | ``` 12 | 13 | ### Monitor 14 | -------------------------------------------------------------------------------- /docs/styling.md: -------------------------------------------------------------------------------- 1 | # Customizing Style 2 | 3 | @todo 4 | Talk about how to use themes to customize styles 5 | -------------------------------------------------------------------------------- /docs/typescript.md: -------------------------------------------------------------------------------- 1 | # Using With Typescript 2 | 3 | @TODO talk about ts specific issues 4 | -------------------------------------------------------------------------------- /hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/leva/f66726534e85e356af64c4556cb5c8b605175598/hero.png -------------------------------------------------------------------------------- /packages/leva/.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/leva/f66726534e85e356af64c4556cb5c8b605175598/packages/leva/.npmignore -------------------------------------------------------------------------------- /packages/leva/README.md: -------------------------------------------------------------------------------- 1 | ../../README.md 2 | -------------------------------------------------------------------------------- /packages/leva/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leva", 3 | "version": "0.10.0", 4 | "main": "dist/leva.cjs.js", 5 | "module": "dist/leva.esm.js", 6 | "types": "dist/leva.cjs.d.ts", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/pmndrs/leva.git", 11 | "directory": "packages/leva" 12 | }, 13 | "bugs": "https://github.com/pmndrs/leva/issues", 14 | "preconstruct": { 15 | "entrypoints": [ 16 | "index.ts", 17 | "plugin/index.ts" 18 | ] 19 | }, 20 | "peerDependencies": { 21 | "react": "^18.0.0 || ^19.0.0", 22 | "react-dom": "^18.0.0 || ^19.0.0" 23 | }, 24 | "dependencies": { 25 | "@radix-ui/react-portal": "^1.1.4", 26 | "@radix-ui/react-tooltip": "^1.1.8", 27 | "@stitches/react": "^1.2.8", 28 | "@use-gesture/react": "^10.2.5", 29 | "colord": "^2.9.2", 30 | "dequal": "^2.0.2", 31 | "merge-value": "^1.0.0", 32 | "react-colorful": "^5.5.1", 33 | "react-dropzone": "^12.0.0", 34 | "v8n": "^1.3.3", 35 | "zustand": "^3.6.9" 36 | }, 37 | "devDependencies": { 38 | "@welldone-software/why-did-you-render": "^6.2.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/leva/plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "dist/leva-plugin.cjs.js", 3 | "module": "dist/leva-plugin.esm.js" 4 | } 5 | -------------------------------------------------------------------------------- /packages/leva/src/components/Boolean/Boolean.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useInputContext } from '../../context' 3 | import { Label, Row } from '../UI' 4 | import { StyledInputWrapper } from './StyledBoolean' 5 | import type { BooleanProps } from './boolean-types' 6 | 7 | export function Boolean({ 8 | value, 9 | onUpdate, 10 | id, 11 | disabled, 12 | }: Pick<BooleanProps, 'value' | 'onUpdate' | 'id' | 'disabled'>) { 13 | return ( 14 | <StyledInputWrapper> 15 | <input 16 | id={id} 17 | type="checkbox" 18 | checked={value} 19 | onChange={(e) => onUpdate(e.currentTarget.checked)} 20 | disabled={disabled} 21 | /> 22 | <label htmlFor={id}> 23 | <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> 24 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> 25 | </svg> 26 | </label> 27 | </StyledInputWrapper> 28 | ) 29 | } 30 | 31 | export function BooleanComponent() { 32 | const { label, value, onUpdate, disabled, id } = useInputContext<BooleanProps>() 33 | 34 | return ( 35 | <Row input> 36 | <Label>{label}</Label> 37 | <Boolean value={value} onUpdate={onUpdate} id={id} disabled={disabled} /> 38 | </Row> 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /packages/leva/src/components/Boolean/StyledBoolean.ts: -------------------------------------------------------------------------------- 1 | import { styled } from '../../styles' 2 | 3 | export const StyledInputWrapper = styled('div', { 4 | position: 'relative', 5 | $flex: '', 6 | height: '$rowHeight', 7 | 8 | input: { 9 | $reset: '', 10 | height: 0, 11 | width: 0, 12 | opacity: 0, 13 | margin: 0, 14 | }, 15 | 16 | label: { 17 | position: 'relative', 18 | $flexCenter: '', 19 | userSelect: 'none', 20 | cursor: 'pointer', 21 | height: '$checkboxSize', 22 | width: '$checkboxSize', 23 | backgroundColor: '$elevation3', 24 | borderRadius: '$sm', 25 | $hover: '', 26 | }, 27 | 28 | 'input:focus + label': { $focusStyle: '' }, 29 | 30 | 'input:focus:checked + label, input:checked + label:hover': { 31 | $hoverStyle: '$accent3', 32 | }, 33 | 34 | 'input + label:active': { 35 | backgroundColor: '$accent1', 36 | }, 37 | 38 | 'input:checked + label:active': { 39 | backgroundColor: '$accent1', 40 | }, 41 | 42 | 'label > svg': { 43 | display: 'none', 44 | width: '90%', 45 | height: '90%', 46 | stroke: '$highlight3', 47 | }, 48 | 49 | 'input:checked + label': { 50 | backgroundColor: '$accent2', 51 | }, 52 | 53 | 'input:checked + label > svg': { 54 | display: 'block', 55 | }, 56 | }) 57 | -------------------------------------------------------------------------------- /packages/leva/src/components/Boolean/boolean-plugin.ts: -------------------------------------------------------------------------------- 1 | import v8n from 'v8n' 2 | 3 | export const schema = (o: any) => v8n().boolean().test(o) 4 | 5 | export const sanitize = (v: any): boolean => { 6 | if (typeof v !== 'boolean') throw Error('Invalid boolean') 7 | return v 8 | } 9 | -------------------------------------------------------------------------------- /packages/leva/src/components/Boolean/boolean-types.ts: -------------------------------------------------------------------------------- 1 | import type { LevaInputProps } from '../../types' 2 | 3 | export type BooleanProps = LevaInputProps<boolean> 4 | -------------------------------------------------------------------------------- /packages/leva/src/components/Boolean/index.ts: -------------------------------------------------------------------------------- 1 | import * as props from './boolean-plugin' 2 | import { BooleanComponent } from './Boolean' 3 | import { createInternalPlugin } from '../../plugin' 4 | 5 | export * from './Boolean' 6 | 7 | export default createInternalPlugin({ 8 | component: BooleanComponent, 9 | ...props, 10 | }) 11 | -------------------------------------------------------------------------------- /packages/leva/src/components/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useStoreContext } from '../..' 3 | import { ButtonInput } from '../../types' 4 | import { Row } from '../UI' 5 | import { StyledButton } from './StyledButton' 6 | 7 | type ButtonProps = { 8 | label: string 9 | } & Omit<ButtonInput, 'type'> 10 | 11 | export function Button({ onClick, settings, label }: ButtonProps) { 12 | const store = useStoreContext() 13 | return ( 14 | <Row> 15 | <StyledButton disabled={settings.disabled} onClick={() => onClick(store.get)}> 16 | {label} 17 | </StyledButton> 18 | </Row> 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /packages/leva/src/components/Button/StyledButton.ts: -------------------------------------------------------------------------------- 1 | import { styled } from '../../styles' 2 | 3 | export const StyledButton = styled('button', { 4 | display: 'block', 5 | $reset: '', 6 | fontWeight: '$button', 7 | height: '$rowHeight', 8 | borderStyle: 'none', 9 | borderRadius: '$sm', 10 | backgroundColor: '$elevation1', 11 | color: '$highlight1', 12 | '&:not(:disabled)': { 13 | color: '$highlight3', 14 | backgroundColor: '$accent2', 15 | cursor: 'pointer', 16 | $hover: '$accent3', 17 | $active: '$accent3 $accent1', 18 | $focus: '', 19 | }, 20 | }) 21 | -------------------------------------------------------------------------------- /packages/leva/src/components/Button/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Button' 2 | -------------------------------------------------------------------------------- /packages/leva/src/components/ButtonGroup/ButtonGroup.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Row, Label } from '../UI' 3 | import { StyledButtonGroup } from './StyledButtonGroup' 4 | import { StyledButtonGroupButton } from './StyledButtonGroupButton' 5 | import { ButtonGroupInputOpts, ButtonGroupOpts } from '../../types' 6 | import { useStoreContext } from '../..' 7 | 8 | export type ButtonGroupInternalOpts = { 9 | label: null | React.JSX.Element | string 10 | opts: ButtonGroupInputOpts 11 | } 12 | 13 | const getOpts = ({ label: _label, opts: _opts }: ButtonGroupInternalOpts) => { 14 | let label = typeof _label === 'string' ? (_label.trim() === '' ? null : _label) : _label 15 | let opts = _opts 16 | if (typeof _opts.opts === 'object') { 17 | if (opts.label !== undefined) { 18 | label = _opts.label as any 19 | } 20 | opts = _opts.opts 21 | } 22 | 23 | return { label, opts: opts as ButtonGroupOpts } 24 | } 25 | 26 | export function ButtonGroup(props: ButtonGroupInternalOpts) { 27 | const { label, opts } = getOpts(props) 28 | const store = useStoreContext() 29 | return ( 30 | <Row input={!!label}> 31 | {label && <Label>{label}</Label>} 32 | <StyledButtonGroup> 33 | {Object.entries(opts).map(([label, onClick]) => ( 34 | <StyledButtonGroupButton key={label} onClick={() => onClick(store.get)}> 35 | {label} 36 | </StyledButtonGroupButton> 37 | ))} 38 | </StyledButtonGroup> 39 | </Row> 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /packages/leva/src/components/ButtonGroup/StyledButtonGroup.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from '../../styles' 2 | 3 | export const StyledButtonGroup = styled('div', { 4 | $flex: '', 5 | justifyContent: 'flex-end', 6 | gap: '$colGap', 7 | }) 8 | -------------------------------------------------------------------------------- /packages/leva/src/components/ButtonGroup/StyledButtonGroupButton.ts: -------------------------------------------------------------------------------- 1 | import { styled } from '../../styles' 2 | 3 | export const StyledButtonGroupButton = styled('button', { 4 | $reset: '', 5 | cursor: 'pointer', 6 | borderRadius: '$xs', 7 | '&:hover': { 8 | backgroundColor: '$elevation3', 9 | }, 10 | }) 11 | -------------------------------------------------------------------------------- /packages/leva/src/components/ButtonGroup/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ButtonGroup' 2 | -------------------------------------------------------------------------------- /packages/leva/src/components/Color/StyledColor.ts: -------------------------------------------------------------------------------- 1 | import { styled } from '../../styles' 2 | 3 | export const ColorPreview = styled('div', { 4 | position: 'relative', 5 | boxSizing: 'border-box', 6 | borderRadius: '$sm', 7 | overflow: 'hidden', 8 | cursor: 'pointer', 9 | height: '$rowHeight', 10 | width: '$rowHeight', 11 | backgroundColor: '#fff', 12 | backgroundImage: `url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill-opacity=".05"><path d="M8 0h8v8H8zM0 8h8v8H0z"/></svg>')`, 13 | $inputStyle: '', 14 | $hover: '', 15 | zIndex: 1, 16 | variants: { 17 | active: { true: { $inputStyle: '$accent1' } }, 18 | }, 19 | '&::before': { 20 | content: '""', 21 | position: 'absolute', 22 | top: 0, 23 | bottom: 0, 24 | right: 0, 25 | left: 0, 26 | backgroundColor: 'currentColor', 27 | zIndex: 1, 28 | }, 29 | }) 30 | 31 | export const PickerContainer = styled('div', { 32 | position: 'relative', 33 | display: 'grid', 34 | gridTemplateColumns: '$sizes$rowHeight auto', 35 | columnGap: '$colGap', 36 | alignItems: 'center', 37 | }) 38 | 39 | export const PickerWrapper = styled('div', { 40 | width: '$colorPickerWidth', 41 | height: '$colorPickerHeight', 42 | 43 | '.react-colorful': { 44 | width: '100%', 45 | height: '100%', 46 | boxShadow: '$level2', 47 | cursor: 'crosshair', 48 | }, 49 | 50 | '.react-colorful__saturation': { 51 | borderRadius: '$sm $sm 0 0', 52 | }, 53 | 54 | '.react-colorful__alpha, .react-colorful__hue': { 55 | height: 10, 56 | }, 57 | 58 | '.react-colorful__last-control': { 59 | borderRadius: '0 0 $sm $sm', 60 | }, 61 | 62 | '.react-colorful__pointer': { 63 | height: 12, 64 | width: 12, 65 | }, 66 | }) 67 | -------------------------------------------------------------------------------- /packages/leva/src/components/Color/color-plugin.ts: -------------------------------------------------------------------------------- 1 | import v8n from 'v8n' 2 | import { Colord, colord, extend, getFormat } from 'colord' 3 | import namesPlugin from 'colord/plugins/names' 4 | import { omit } from '../../utils' 5 | import type { InternalColorSettings, Format, ColorInput } from './color-types' 6 | 7 | extend([namesPlugin]) 8 | 9 | const convertMap = { 10 | rgb: 'toRgb', 11 | hsl: 'toHsl', 12 | hsv: 'toHsv', 13 | hex: 'toHex', 14 | } 15 | 16 | v8n.extend({ 17 | color: () => (value: any) => colord(value).isValid(), 18 | }) 19 | // prettier-ignore 20 | // @ts-expect-error 21 | export const schema = (o: any) => v8n().color().test(o) 22 | 23 | function convert(color: Colord, { format, hasAlpha, isString }: InternalColorSettings) { 24 | const convertFn = convertMap[format] + (isString && format !== 'hex' ? 'String' : '') 25 | // @ts-ignore 26 | const result = color[convertFn]() 27 | return typeof result === 'object' && !hasAlpha ? omit(result, ['a']) : result 28 | } 29 | 30 | export const sanitize = (v: any, settings: InternalColorSettings) => { 31 | const color = colord(v) 32 | if (!color.isValid()) throw Error('Invalid color') 33 | return convert(color, settings) 34 | } 35 | 36 | export const format = (v: any, settings: InternalColorSettings) => { 37 | return convert(colord(v), { ...settings, isString: true, format: 'hex' }) 38 | } 39 | 40 | export const normalize = ({ value }: ColorInput) => { 41 | const _f = getFormat(value) 42 | const format = (_f === 'name' ? 'hex' : _f) as Format 43 | const hasAlpha = 44 | typeof value === 'object' 45 | ? 'a' in value 46 | : (_f === 'hex' && value.length === 8) || /^(rgba)|(hsla)|(hsva)/.test(value) 47 | 48 | const settings = { format, hasAlpha, isString: typeof value === 'string' } 49 | 50 | // by santizing the value we make sure the returned value is parsed and fixed, 51 | // consistent with future updates. 52 | return { value: sanitize(value, settings), settings } 53 | } 54 | -------------------------------------------------------------------------------- /packages/leva/src/components/Color/color-types.ts: -------------------------------------------------------------------------------- 1 | import type { ColorVectorInput, InputWithSettings, LevaInputProps } from '../../types' 2 | 3 | export type Format = 'hex' | 'rgb' | 'hsl' | 'hsv' 4 | 5 | export type Color = string | ColorVectorInput 6 | export type InternalColorSettings = { format: Format; hasAlpha: boolean; isString: boolean } 7 | 8 | export type ColorInput = InputWithSettings<Color> 9 | 10 | export type ColorProps = LevaInputProps<Color, InternalColorSettings, string> 11 | -------------------------------------------------------------------------------- /packages/leva/src/components/Color/index.ts: -------------------------------------------------------------------------------- 1 | import * as props from './color-plugin' 2 | import { ColorComponent } from './Color' 3 | import { createInternalPlugin } from '../../plugin' 4 | 5 | export * from './Color' 6 | 7 | export default createInternalPlugin({ 8 | component: ColorComponent, 9 | ...props, 10 | }) 11 | -------------------------------------------------------------------------------- /packages/leva/src/components/Control/Control.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ControlInput } from './ControlInput' 3 | import { log, LevaErrors } from '../../utils/log' 4 | import { Plugins } from '../../plugin' 5 | import { Button } from '../Button' 6 | import { ButtonGroup } from '../ButtonGroup' 7 | import { Monitor } from '../Monitor' 8 | import { useInput } from '../../hooks' 9 | import { SpecialInputs } from '../../types' 10 | 11 | type ControlProps = { path: string } 12 | 13 | const specialComponents = { 14 | [SpecialInputs.BUTTON]: Button, 15 | [SpecialInputs.BUTTON_GROUP]: ButtonGroup, 16 | [SpecialInputs.MONITOR]: Monitor, 17 | } 18 | 19 | export const Control = React.memo(({ path }: ControlProps) => { 20 | const [input, { set, setSettings, disable, storeId, emitOnEditStart, emitOnEditEnd }] = useInput(path) 21 | if (!input) return null 22 | 23 | const { type, label, key, ...inputProps } = input 24 | 25 | if (type in SpecialInputs) { 26 | // @ts-expect-error 27 | const SpecialInputForType = specialComponents[type] 28 | return <SpecialInputForType label={label} path={path} {...inputProps} /> 29 | } 30 | 31 | if (!(type in Plugins)) { 32 | log(LevaErrors.UNSUPPORTED_INPUT, type, path) 33 | return null 34 | } 35 | 36 | return ( 37 | // @ts-expect-error 38 | <ControlInput 39 | key={storeId + path} 40 | type={type} 41 | label={label} 42 | storeId={storeId} 43 | path={path} 44 | valueKey={key} 45 | setValue={set} 46 | setSettings={setSettings} 47 | disable={disable} 48 | emitOnEditStart={emitOnEditStart} 49 | emitOnEditEnd={emitOnEditEnd} 50 | {...inputProps} 51 | /> 52 | ) 53 | }) 54 | -------------------------------------------------------------------------------- /packages/leva/src/components/Control/ControlInput.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Plugins } from '../../plugin' 3 | import { warn, LevaErrors } from '../../utils/log' 4 | import { InputContext } from '../../context' 5 | import { useInputSetters } from '../../hooks' 6 | import { StyledInputWrapper } from '../UI/StyledUI' 7 | import type { DataInput } from '../../types' 8 | 9 | type ControlInputProps = Omit<DataInput, '__refCount' | 'key'> & { 10 | valueKey: string 11 | path: string 12 | storeId: string 13 | setValue: (value: any) => void 14 | setSettings: (settings: any) => void 15 | disable: (flag: boolean) => void 16 | emitOnEditStart?: (...args: any) => void 17 | emitOnEditEnd?: (...args: any) => void 18 | } 19 | 20 | export function ControlInput({ 21 | type, 22 | label, 23 | path, 24 | valueKey, 25 | value, 26 | settings, 27 | setValue, 28 | disabled, 29 | ...rest 30 | }: ControlInputProps) { 31 | const { displayValue, onChange, onUpdate } = useInputSetters({ type, value, settings, setValue }) 32 | 33 | const Input = Plugins[type].component 34 | if (!Input) { 35 | warn(LevaErrors.NO_COMPONENT_FOR_TYPE, type, path) 36 | return null 37 | } 38 | 39 | return ( 40 | <InputContext.Provider 41 | value={{ 42 | key: valueKey, 43 | path, 44 | id: '' + path, 45 | label, 46 | displayValue, 47 | value, 48 | onChange, 49 | onUpdate, 50 | settings, 51 | setValue, 52 | disabled, 53 | ...rest, 54 | }}> 55 | <StyledInputWrapper disabled={disabled}> 56 | {/* @ts-ignore */} 57 | <Input /> 58 | </StyledInputWrapper> 59 | </InputContext.Provider> 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /packages/leva/src/components/Control/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Control' 2 | -------------------------------------------------------------------------------- /packages/leva/src/components/Folder/FolderTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StyledTitle } from './StyledFolder' 3 | import { Chevron } from '../UI' 4 | 5 | export type FolderTitleProps = { 6 | name?: string 7 | toggled: boolean 8 | toggle: (flag?: boolean) => void 9 | } 10 | 11 | export function FolderTitle({ toggle, toggled, name }: FolderTitleProps) { 12 | return ( 13 | <StyledTitle onClick={() => toggle()}> 14 | <Chevron toggled={toggled} /> 15 | <div>{name}</div> 16 | </StyledTitle> 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /packages/leva/src/components/Folder/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Folder' 2 | export * from './FolderTitle' 3 | -------------------------------------------------------------------------------- /packages/leva/src/components/Image/Image.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react' 2 | import { Label, Portal, Overlay, Row } from '../UI' 3 | import { useDropzone } from 'react-dropzone' 4 | import { DropZone, ImageContainer, ImagePreview, Instructions, ImageLargePreview, Remove } from './StyledImage' 5 | import { useInputContext } from '../../context' 6 | import { usePopin } from '../../hooks' 7 | import type { ImageProps } from './image-types' 8 | 9 | export function ImageComponent() { 10 | const { label, value, onUpdate, disabled } = useInputContext<ImageProps>() 11 | const { popinRef, wrapperRef, shown, show, hide } = usePopin() 12 | 13 | const onDrop = useCallback( 14 | <T extends File>(acceptedFiles: T[]) => { 15 | if (acceptedFiles.length) onUpdate(acceptedFiles[0]) 16 | }, 17 | [onUpdate] 18 | ) 19 | 20 | const clear = useCallback( 21 | (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => { 22 | e.stopPropagation() 23 | onUpdate(undefined) 24 | }, 25 | [onUpdate] 26 | ) 27 | 28 | const { getRootProps, getInputProps, isDragAccept } = useDropzone({ 29 | maxFiles: 1, 30 | accept: 'image/*', 31 | onDrop, 32 | disabled, 33 | }) 34 | 35 | // TODO fix any in DropZone 36 | return ( 37 | <Row input> 38 | <Label>{label}</Label> 39 | <ImageContainer> 40 | <ImagePreview 41 | ref={popinRef} 42 | hasImage={!!value} 43 | onPointerDown={() => !!value && show()} 44 | onPointerUp={hide} 45 | style={{ backgroundImage: value ? `url(${value})` : 'none' }} 46 | /> 47 | {shown && !!value && ( 48 | <Portal> 49 | <Overlay onPointerUp={hide} style={{ cursor: 'pointer' }} /> 50 | <ImageLargePreview ref={wrapperRef} style={{ backgroundImage: `url(${value})` }} /> 51 | </Portal> 52 | )} 53 | <DropZone {...(getRootProps({ isDragAccept }) as any)}> 54 | <input {...getInputProps()} /> 55 | <Instructions>{isDragAccept ? 'drop image' : 'click or drop'}</Instructions> 56 | </DropZone> 57 | <Remove onClick={clear} disabled={!value} /> 58 | </ImageContainer> 59 | </Row> 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /packages/leva/src/components/Image/image-plugin.ts: -------------------------------------------------------------------------------- 1 | import type { ImageInput } from '../../types' 2 | 3 | export const sanitize = (v: any): string | undefined => { 4 | if (v === undefined) return undefined 5 | if (v instanceof File) { 6 | try { 7 | return URL.createObjectURL(v) 8 | } catch (e) { 9 | return undefined 10 | } 11 | } 12 | if (typeof v === 'string' && v.indexOf('blob:') === 0) return v 13 | throw Error(`Invalid image format [undefined | blob | File].`) 14 | } 15 | 16 | export const schema = (_o: any, s: any) => typeof s === 'object' && 'image' in s 17 | 18 | export const normalize = ({ image }: ImageInput) => { 19 | return { value: image } 20 | } 21 | -------------------------------------------------------------------------------- /packages/leva/src/components/Image/image-types.ts: -------------------------------------------------------------------------------- 1 | import type { LevaInputProps } from '../../types' 2 | 3 | export type ImageProps = LevaInputProps<string | undefined> 4 | -------------------------------------------------------------------------------- /packages/leva/src/components/Image/index.ts: -------------------------------------------------------------------------------- 1 | import * as props from './image-plugin' 2 | import { ImageComponent } from './Image' 3 | import { createInternalPlugin } from '../../plugin' 4 | 5 | export * from './Image' 6 | 7 | export default createInternalPlugin({ 8 | component: ImageComponent, 9 | ...props, 10 | }) 11 | -------------------------------------------------------------------------------- /packages/leva/src/components/Interval/index.ts: -------------------------------------------------------------------------------- 1 | import * as props from './interval-plugin' 2 | import { IntervalComponent } from './Interval' 3 | import { createInternalPlugin } from '../../plugin' 4 | 5 | export * from './Interval' 6 | 7 | export default createInternalPlugin({ 8 | component: IntervalComponent, 9 | ...props, 10 | }) 11 | -------------------------------------------------------------------------------- /packages/leva/src/components/Interval/interval-plugin.ts: -------------------------------------------------------------------------------- 1 | import v8n from 'v8n' 2 | import { clamp } from '../../utils' 3 | import { normalizeKeyedNumberSettings } from '../Vector/vector-utils' 4 | import type { IntervalInput } from '../../types' 5 | import type { InternalInterval, InternalIntervalSettings, Interval } from './interval-types' 6 | 7 | const number = v8n().number() 8 | 9 | export const schema = (o: any, s: any) => 10 | v8n().array().length(2).every.number().test(o) && v8n().schema({ min: number, max: number }).test(s) 11 | 12 | export const format = (v: Interval) => ({ min: v[0], max: v[1] }) 13 | 14 | export const sanitize = ( 15 | value: InternalInterval | Interval, 16 | { bounds: [MIN, MAX] }: InternalIntervalSettings, 17 | prevValue: any 18 | ): Interval => { 19 | // value can be passed as an array externally 20 | const _value: InternalInterval = Array.isArray(value) ? format(value as Interval) : value 21 | const _newValue = { min: prevValue[0], max: prevValue[1] } 22 | const { min, max } = { ..._newValue, ..._value } 23 | return [clamp(Number(min), MIN, Math.max(MIN, max)), clamp(Number(max), Math.min(MAX, min), MAX)] 24 | } 25 | 26 | export const normalize = ({ value, min, max }: IntervalInput) => { 27 | const boundsSettings = { min, max } 28 | const _settings = normalizeKeyedNumberSettings(format(value), { min: boundsSettings, max: boundsSettings }) 29 | const bounds: [number, number] = [min, max] 30 | const settings = { ..._settings, bounds } 31 | 32 | // sanitizing value to make sure it's withing interval bounds 33 | const _value = sanitize(format(value), settings, value) 34 | return { value: _value, settings } 35 | } 36 | -------------------------------------------------------------------------------- /packages/leva/src/components/Interval/interval-types.ts: -------------------------------------------------------------------------------- 1 | import type { LevaInputProps, IntervalInput } from '../../types' 2 | import type { InternalNumberSettings } from '../Number/number-types' 3 | 4 | export type Interval = IntervalInput['value'] 5 | export type InternalInterval = { min: number; max: number } 6 | 7 | export type InternalIntervalSettings = { 8 | bounds: [number, number] 9 | min: InternalNumberSettings 10 | max: InternalNumberSettings 11 | } 12 | 13 | export type IntervalProps = LevaInputProps<Interval, InternalIntervalSettings, InternalInterval> 14 | 15 | export type IntervalSliderProps = { 16 | value: InternalInterval 17 | onDrag: (v: Partial<InternalInterval>) => void 18 | } & InternalIntervalSettings 19 | -------------------------------------------------------------------------------- /packages/leva/src/components/Leva/Leva.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { levaStore } from '../../store' 4 | import { LevaRoot, LevaRootProps } from './LevaRoot' 5 | 6 | let rootInitialized = false 7 | let rootEl: HTMLElement | null = null 8 | 9 | type LevaProps = Omit<Partial<LevaRootProps>, 'store'> & { isRoot?: boolean } 10 | 11 | // uses global store 12 | export function Leva({ isRoot = false, ...props }: LevaProps) { 13 | useEffect(() => { 14 | rootInitialized = true 15 | // if this panel was attached somewhere in the app and there is already 16 | // a floating panel, we remove it. 17 | if (!isRoot && rootEl) { 18 | rootEl.remove() 19 | rootEl = null 20 | } 21 | return () => { 22 | if (!isRoot) rootInitialized = false 23 | } 24 | }, [isRoot]) 25 | 26 | return <LevaRoot store={levaStore} {...props} /> 27 | } 28 | 29 | /** 30 | * This hook is used by Leva useControls, and ensures that we spawn a Leva Panel 31 | * without the user having to put it into the component tree. This should only 32 | * happen when using the global store 33 | * @param isGlobalPanel 34 | */ 35 | export function useRenderRoot(isGlobalPanel: boolean) { 36 | useEffect(() => { 37 | if (isGlobalPanel && !rootInitialized) { 38 | if (!rootEl) { 39 | rootEl = 40 | document.getElementById('leva__root') || Object.assign(document.createElement('div'), { id: 'leva__root' }) 41 | if (document.body) { 42 | document.body.appendChild(rootEl) 43 | createRoot(rootEl).render(<Leva isRoot />) 44 | } 45 | } 46 | rootInitialized = true 47 | } 48 | }, [isGlobalPanel]) 49 | } 50 | -------------------------------------------------------------------------------- /packages/leva/src/components/Leva/LevaPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useStoreContext } from '../../context' 3 | import { LevaRoot, LevaRootProps } from './LevaRoot' 4 | 5 | type LevaPanelProps = Partial<LevaRootProps> 6 | 7 | // uses custom store 8 | export function LevaPanel({ store, ...props }: LevaPanelProps) { 9 | const parentStore = useStoreContext() 10 | const _store = store === undefined ? parentStore : store 11 | return <LevaRoot store={_store} {...props} /> 12 | } 13 | -------------------------------------------------------------------------------- /packages/leva/src/components/Leva/StyledRoot.ts: -------------------------------------------------------------------------------- 1 | import { styled } from '../../styles' 2 | import { StyledInputRow } from '../UI/StyledUI' 3 | 4 | export const StyledRoot = styled('div', { 5 | /* position */ 6 | position: 'relative', 7 | fontFamily: '$mono', 8 | fontSize: '$root', 9 | color: '$rootText', 10 | backgroundColor: '$elevation1', 11 | variants: { 12 | fill: { 13 | false: { 14 | position: 'fixed', 15 | top: '10px', 16 | right: '10px', 17 | zIndex: 1000, 18 | width: '$rootWidth', 19 | }, 20 | true: { 21 | position: 'relative', 22 | width: '100%', 23 | }, 24 | }, 25 | flat: { 26 | false: { 27 | borderRadius: '$lg', 28 | boxShadow: '$level1', 29 | }, 30 | }, 31 | oneLineLabels: { 32 | true: { 33 | [`${StyledInputRow}`]: { 34 | gridTemplateColumns: 'auto', 35 | gridAutoColumns: 'minmax(max-content, 1fr)', 36 | gridAutoRows: 'minmax($sizes$rowHeight), auto)', 37 | rowGap: 0, 38 | columnGap: 0, 39 | marginTop: '$rowGap', 40 | }, 41 | }, 42 | }, 43 | hideTitleBar: { 44 | true: { $titleBarHeight: '0px' }, 45 | false: { $titleBarHeight: '$sizes$titleBarHeight' }, 46 | }, 47 | }, 48 | 49 | '&,*,*:after,*:before': { 50 | boxSizing: 'border-box', 51 | }, 52 | 53 | '*::selection': { 54 | backgroundColor: '$accent2', 55 | }, 56 | }) 57 | -------------------------------------------------------------------------------- /packages/leva/src/components/Leva/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Leva' 2 | export * from './LevaPanel' 3 | -------------------------------------------------------------------------------- /packages/leva/src/components/Leva/tree.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error 2 | import merge from 'merge-value' 3 | import { getKeyPath } from '../../utils' 4 | import type { Tree } from '../../types' 5 | 6 | export const isInput = (v: object) => '__levaInput' in v 7 | 8 | export const buildTree = (paths: string[], filter?: string): Tree => { 9 | const tree = {} 10 | const _filter = filter ? filter.toLowerCase() : null 11 | paths.forEach((path) => { 12 | const [valueKey, folderPath] = getKeyPath(path) 13 | if (!_filter || valueKey.toLowerCase().indexOf(_filter) > -1) { 14 | merge(tree, folderPath, { 15 | [valueKey]: { __levaInput: true, path }, 16 | }) 17 | } 18 | }) 19 | return tree 20 | } 21 | -------------------------------------------------------------------------------- /packages/leva/src/components/Monitor/StyledMonitor.ts: -------------------------------------------------------------------------------- 1 | import { styled } from '../../styles' 2 | 3 | export const Canvas = styled('canvas', { 4 | height: '$monitorHeight', 5 | width: '100%', 6 | display: 'block', 7 | borderRadius: '$sm', 8 | }) 9 | -------------------------------------------------------------------------------- /packages/leva/src/components/Monitor/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Monitor' 2 | -------------------------------------------------------------------------------- /packages/leva/src/components/Number/RangeSlider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react' 2 | import { RangeWrapper, Range, Scrubber, Indicator } from './StyledRange' 3 | import { sanitizeStep } from './number-plugin' 4 | import { useDrag } from '../../hooks' 5 | import { invertedRange, range } from '../../utils' 6 | import { useTh } from '../../styles' 7 | import type { RangeSliderProps } from './number-types' 8 | 9 | export function RangeSlider({ value, min, max, onDrag, step, initialValue }: RangeSliderProps) { 10 | const ref = useRef<HTMLDivElement>(null) 11 | const scrubberRef = useRef<HTMLDivElement>(null) 12 | const rangeWidth = useRef<number>(0) 13 | const scrubberWidth = useTh('sizes', 'scrubberWidth') 14 | 15 | const bind = useDrag(({ event, first, xy: [x], movement: [mx], memo }) => { 16 | if (first) { 17 | // rangeWidth is the width of the slider el minus the width of the scrubber el itself 18 | const { width, left } = ref.current!.getBoundingClientRect() 19 | rangeWidth.current = width - parseFloat(scrubberWidth) 20 | 21 | const targetIsScrub = event?.target === scrubberRef.current 22 | // memo is the value where the user clicked on 23 | memo = targetIsScrub ? value : invertedRange((x - left) / width, min, max) 24 | } 25 | const newValue = memo + invertedRange(mx / rangeWidth.current, 0, max - min) 26 | onDrag(sanitizeStep(newValue, { step, initialValue })) 27 | return memo 28 | }) 29 | 30 | const pos = range(value, min, max) 31 | 32 | return ( 33 | <RangeWrapper ref={ref} {...bind()}> 34 | <Range> 35 | <Indicator style={{ left: 0, right: `${(1 - pos) * 100}%` }} /> 36 | </Range> 37 | <Scrubber ref={scrubberRef} style={{ left: `calc(${pos} * (100% - ${scrubberWidth}))` }} /> 38 | </RangeWrapper> 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /packages/leva/src/components/Number/StyledNumber.ts: -------------------------------------------------------------------------------- 1 | import { styled } from '../../styles' 2 | 3 | export const RangeGrid = styled('div', { 4 | variants: { 5 | hasRange: { 6 | true: { 7 | position: 'relative', 8 | display: 'grid', 9 | gridTemplateColumns: 'auto $sizes$numberInputMinWidth', 10 | columnGap: '$colGap', 11 | alignItems: 'center', 12 | }, 13 | }, 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /packages/leva/src/components/Number/StyledRange.ts: -------------------------------------------------------------------------------- 1 | import { styled } from '../../styles' 2 | 3 | export const Range = styled('div', { 4 | position: 'relative', 5 | width: '100%', 6 | height: 2, 7 | borderRadius: '$xs', 8 | backgroundColor: '$elevation1', 9 | }) 10 | 11 | export const Scrubber = styled('div', { 12 | position: 'absolute', 13 | width: '$scrubberWidth', 14 | height: '$scrubberHeight', 15 | borderRadius: '$xs', 16 | boxShadow: '0 0 0 2px $colors$elevation2', 17 | backgroundColor: '$accent2', 18 | cursor: 'pointer', 19 | $active: 'none $accent1', 20 | $hover: 'none $accent3', 21 | variants: { 22 | position: { 23 | left: { 24 | borderTopRightRadius: 0, 25 | borderBottomRightRadius: 0, 26 | transform: 'translateX(calc(-0.5 * ($sizes$scrubberWidth + 4px)))', 27 | }, 28 | right: { 29 | borderTopLeftRadius: 0, 30 | borderBottomLeftRadius: 0, 31 | transform: 'translateX(calc(0.5 * ($sizes$scrubberWidth + 4px)))', 32 | }, 33 | }, 34 | }, 35 | }) 36 | 37 | export const RangeWrapper = styled('div', { 38 | position: 'relative', 39 | $flex: '', 40 | height: '100%', 41 | cursor: 'pointer', 42 | touchAction: 'none', 43 | }) 44 | 45 | export const Indicator = styled('div', { 46 | position: 'absolute', 47 | height: '100%', 48 | backgroundColor: '$accent2', 49 | }) 50 | -------------------------------------------------------------------------------- /packages/leva/src/components/Number/index.ts: -------------------------------------------------------------------------------- 1 | import * as props from './number-plugin' 2 | import { NumberComponent } from './Number' 3 | import { createInternalPlugin } from '../../plugin' 4 | 5 | const { sanitizeStep, ...rest } = props 6 | 7 | export * from './Number' 8 | export * from './StyledNumber' 9 | export * from './StyledRange' 10 | export { sanitizeStep } 11 | 12 | export default createInternalPlugin({ 13 | component: NumberComponent, 14 | ...rest, 15 | }) 16 | -------------------------------------------------------------------------------- /packages/leva/src/components/Number/number-types.ts: -------------------------------------------------------------------------------- 1 | import type { InputWithSettings, LevaInputProps, NumberSettings } from '../../types' 2 | 3 | export type InternalNumberSettings = { 4 | min: number 5 | max: number 6 | step: number 7 | pad: number 8 | initialValue: number 9 | suffix?: string 10 | } 11 | export type NumberInput = InputWithSettings<number | string, NumberSettings> 12 | 13 | export type NumberProps = LevaInputProps<number, InternalNumberSettings> 14 | 15 | export type RangeSliderProps = { value: number; onDrag: (v: number) => void } & InternalNumberSettings 16 | -------------------------------------------------------------------------------- /packages/leva/src/components/Select/Select.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react' 2 | import { useInputContext } from '../../context' 3 | import { Label, Row, Chevron } from '../UI' 4 | import { NativeSelect, PresentationalSelect, SelectContainer } from './StyledSelect' 5 | import type { SelectProps } from './select-types' 6 | 7 | export function Select({ 8 | displayValue, 9 | value, 10 | onUpdate, 11 | id, 12 | settings, 13 | disabled, 14 | }: Pick<SelectProps, 'value' | 'displayValue' | 'onUpdate' | 'id' | 'settings' | 'disabled'>) { 15 | const { keys, values } = settings 16 | const lastDisplayedValue = useRef<any>() 17 | 18 | // in case the value isn't present in values (possibly when changing options 19 | // via deps), remember the last correct display value. 20 | if (value === values[displayValue]) { 21 | lastDisplayedValue.current = keys[displayValue] 22 | } 23 | 24 | return ( 25 | <SelectContainer> 26 | <NativeSelect 27 | id={id} 28 | value={displayValue} 29 | onChange={(e) => onUpdate(values[Number(e.currentTarget.value)])} 30 | disabled={disabled}> 31 | {keys.map((key, index) => ( 32 | <option key={key} value={index}> 33 | {key} 34 | </option> 35 | ))} 36 | </NativeSelect> 37 | <PresentationalSelect>{lastDisplayedValue.current}</PresentationalSelect> 38 | <Chevron toggled /> 39 | </SelectContainer> 40 | ) 41 | } 42 | 43 | export function SelectComponent() { 44 | const { label, value, displayValue, onUpdate, id, disabled, settings } = useInputContext<SelectProps>() 45 | return ( 46 | <Row input> 47 | <Label>{label}</Label> 48 | <Select 49 | id={id} 50 | value={value} 51 | displayValue={displayValue} 52 | onUpdate={onUpdate} 53 | settings={settings} 54 | disabled={disabled} 55 | /> 56 | </Row> 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /packages/leva/src/components/Select/StyledSelect.ts: -------------------------------------------------------------------------------- 1 | import { styled } from '../../styles' 2 | 3 | export const SelectContainer = styled('div', { 4 | $flexCenter: '', 5 | position: 'relative', 6 | '> svg': { 7 | pointerEvents: 'none', 8 | position: 'absolute', 9 | right: '$md', 10 | }, 11 | }) 12 | 13 | export const NativeSelect = styled('select', { 14 | position: 'absolute', 15 | top: 0, 16 | left: 0, 17 | width: '100%', 18 | height: '100%', 19 | opacity: 0, 20 | }) 21 | 22 | export const PresentationalSelect = styled('div', { 23 | display: 'flex', 24 | alignItems: 'center', 25 | width: '100%', 26 | height: '$rowHeight', 27 | backgroundColor: '$elevation3', 28 | borderRadius: '$sm', 29 | padding: '0 $sm', 30 | cursor: 'pointer', 31 | [`${NativeSelect}:focus + &`]: { 32 | $focusStyle: '', 33 | }, 34 | [`${NativeSelect}:hover + &`]: { 35 | $hoverStyle: '', 36 | }, 37 | }) 38 | -------------------------------------------------------------------------------- /packages/leva/src/components/Select/index.ts: -------------------------------------------------------------------------------- 1 | import * as props from './select-plugin' 2 | import { SelectComponent } from './Select' 3 | import { createInternalPlugin } from '../../plugin' 4 | 5 | export * from './Select' 6 | 7 | export default createInternalPlugin({ 8 | component: SelectComponent, 9 | ...props, 10 | }) 11 | -------------------------------------------------------------------------------- /packages/leva/src/components/Select/select-plugin.ts: -------------------------------------------------------------------------------- 1 | import v8n from 'v8n' 2 | import type { SelectInput, InternalSelectSettings } from './select-types' 3 | 4 | // the options attribute is either an key value object or an array 5 | export const schema = (_o: any, s: any) => 6 | v8n() 7 | .schema({ 8 | options: v8n().passesAnyOf(v8n().object(), v8n().array()), 9 | }) 10 | .test(s) 11 | 12 | export const sanitize = (value: any, { values }: InternalSelectSettings) => { 13 | if (values.indexOf(value) < 0) throw Error(`Selected value doesn't match Select options`) 14 | return value 15 | } 16 | 17 | export const format = (value: any, { values }: InternalSelectSettings) => { 18 | return values.indexOf(value) 19 | } 20 | 21 | export const normalize = (input: SelectInput) => { 22 | let { value, options } = input 23 | let keys 24 | let values 25 | 26 | if (Array.isArray(options)) { 27 | values = options 28 | keys = options.map((o) => String(o)) 29 | } else { 30 | values = Object.values(options) 31 | keys = Object.keys(options) 32 | } 33 | 34 | if (!('value' in input)) value = values[0] 35 | else if (!values.includes(value)) { 36 | keys.unshift(String(value)) 37 | values.unshift(value) 38 | } 39 | 40 | if (!Object.values(options).includes(value)) (options as any)[String(value)] = value 41 | return { value, settings: { keys, values } } 42 | } 43 | -------------------------------------------------------------------------------- /packages/leva/src/components/Select/select-types.ts: -------------------------------------------------------------------------------- 1 | import type { LevaInputProps } from '../../types' 2 | 3 | export type SelectSettings<U = unknown> = { options: Record<string, U> | U[] } 4 | export type InternalSelectSettings = { keys: string[]; values: any[] } 5 | 6 | export type SelectInput<P = unknown, U = unknown> = { value?: P } & SelectSettings<U> 7 | 8 | export type SelectProps = LevaInputProps<any, InternalSelectSettings, number> 9 | -------------------------------------------------------------------------------- /packages/leva/src/components/String/String.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ValueInput, ValueInputProps } from '../ValueInput' 3 | import { Label, Row } from '../UI' 4 | import { useInputContext } from '../../context' 5 | import type { StringProps } from './string-types' 6 | import { styled } from '../../styles' 7 | 8 | type BaseStringProps = Pick<StringProps, 'displayValue' | 'onUpdate' | 'onChange'> & 9 | Omit<ValueInputProps, 'value'> & { editable?: boolean } 10 | 11 | const NonEditableString = styled('div', { 12 | whiteSpace: 'pre-wrap', 13 | }) 14 | 15 | export function String({ displayValue, onUpdate, onChange, editable = true, ...props }: BaseStringProps) { 16 | if (editable) return <ValueInput value={displayValue} onUpdate={onUpdate} onChange={onChange} {...props} /> 17 | return <NonEditableString>{displayValue}</NonEditableString> 18 | } 19 | 20 | export function StringComponent() { 21 | const { label, settings, displayValue, onUpdate, onChange } = useInputContext<StringProps>() 22 | return ( 23 | <Row input> 24 | <Label>{label}</Label> 25 | <String displayValue={displayValue} onUpdate={onUpdate} onChange={onChange} {...settings} /> 26 | </Row> 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /packages/leva/src/components/String/index.ts: -------------------------------------------------------------------------------- 1 | import * as props from './string-plugin' 2 | import { StringComponent } from './String' 3 | import { createInternalPlugin } from '../../plugin' 4 | 5 | export * from './String' 6 | 7 | export default createInternalPlugin({ 8 | component: StringComponent, 9 | ...props, 10 | }) 11 | -------------------------------------------------------------------------------- /packages/leva/src/components/String/string-plugin.ts: -------------------------------------------------------------------------------- 1 | import v8n from 'v8n' 2 | import { StringInput } from './string-types' 3 | 4 | export const schema = (o: any) => v8n().string().test(o) 5 | 6 | export const sanitize = (v: any) => { 7 | if (typeof v !== 'string') throw Error(`Invalid string`) 8 | return v 9 | } 10 | 11 | export const normalize = ({ value, editable = true, rows = false }: StringInput) => { 12 | return { 13 | value, 14 | settings: { editable, rows: typeof rows === 'number' ? rows : rows ? 5 : 0 }, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/leva/src/components/String/string-types.ts: -------------------------------------------------------------------------------- 1 | import type { InputWithSettings, LevaInputProps } from '../../types' 2 | 3 | export type StringSettings = { editable?: boolean; rows?: boolean | number } 4 | export type InternalStringSettings = { editable: boolean; rows: number } 5 | export type StringInput = InputWithSettings<string, StringSettings> 6 | export type StringProps = LevaInputProps<string, InternalStringSettings> 7 | -------------------------------------------------------------------------------- /packages/leva/src/components/UI/Chevron.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { styled } from '../../styles' 3 | 4 | // TODO remove as any when this is corrected by stitches 5 | const Svg = styled('svg', { 6 | fill: 'currentColor', 7 | transition: 'transform 350ms ease, fill 250ms ease', 8 | }) as any 9 | 10 | export function Chevron({ toggled, ...props }: React.SVGProps<SVGSVGElement> & { toggled?: boolean }) { 11 | return ( 12 | <Svg 13 | width="9" 14 | height="5" 15 | viewBox="0 0 9 5" 16 | xmlns="http://www.w3.org/2000/svg" 17 | style={{ transform: `rotate(${toggled ? 0 : -90}deg)` }} 18 | {...props}> 19 | <path d="M3.8 4.4c.4.3 1 .3 1.4 0L8 1.7A1 1 0 007.4 0H1.6a1 1 0 00-.7 1.7l3 2.7z" /> 20 | </Svg> 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /packages/leva/src/components/UI/Misc.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import * as P from '@radix-ui/react-portal' 3 | import { ThemeContext } from '../../context' 4 | export { Overlay } from './StyledUI' 5 | 6 | // @ts-ignore 7 | export function Portal({ children, container = globalThis?.document?.body }) { 8 | const { className } = useContext(ThemeContext)! 9 | return ( 10 | <P.Root className={className} container={container}> 11 | {children} 12 | </P.Root> 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /packages/leva/src/components/UI/Row.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StyledRow, StyledInputRow } from './StyledUI' 3 | 4 | type RowProps = React.ComponentProps<typeof StyledRow> & { input?: boolean } 5 | 6 | export function Row({ input, ...props }: RowProps) { 7 | if (input) return <StyledInputRow {...props} /> 8 | return <StyledRow {...props} /> 9 | } 10 | -------------------------------------------------------------------------------- /packages/leva/src/components/UI/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Misc' 2 | export * from './Label' 3 | export * from './Chevron' 4 | export * from './Row' 5 | -------------------------------------------------------------------------------- /packages/leva/src/components/ValueInput/StyledInput.ts: -------------------------------------------------------------------------------- 1 | import { styled } from '../../styles' 2 | 3 | export const StyledInput = styled('input', { 4 | /* input reset */ 5 | $reset: '', 6 | padding: '0 $sm', 7 | width: 0, 8 | minWidth: 0, 9 | flex: 1, 10 | height: '100%', 11 | variants: { 12 | levaType: { number: { textAlign: 'right' } }, 13 | as: { textarea: { padding: '$sm' } }, 14 | }, 15 | }) 16 | 17 | export const InnerLabel = styled('div', { 18 | $draggable: '', 19 | height: '100%', 20 | $flexCenter: '', 21 | position: 'relative', 22 | padding: '0 $xs', 23 | fontSize: '0.8em', 24 | opacity: 0.8, 25 | cursor: 'default', 26 | touchAction: 'none', 27 | [`& + ${StyledInput}`]: { paddingLeft: 0 }, 28 | }) 29 | 30 | export const InnerNumberLabel = styled(InnerLabel, { 31 | cursor: 'ew-resize', 32 | marginRight: '-$xs', 33 | textTransform: 'uppercase', 34 | opacity: 0.3, 35 | '&:hover': { opacity: 1 }, 36 | variants: { 37 | dragging: { true: { backgroundColor: '$accent2', opacity: 1 } }, 38 | }, 39 | }) 40 | 41 | export const InputContainer = styled('div', { 42 | $flex: '', 43 | position: 'relative', 44 | borderRadius: '$sm', 45 | overflow: 'hidden', 46 | color: 'inherit', 47 | height: '$rowHeight', 48 | backgroundColor: '$elevation3', 49 | $inputStyle: '$elevation1', 50 | $hover: '', 51 | $focusWithin: '', 52 | variants: { 53 | textArea: { true: { height: 'auto' } }, 54 | }, 55 | }) 56 | -------------------------------------------------------------------------------- /packages/leva/src/components/ValueInput/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ValueInput' 2 | -------------------------------------------------------------------------------- /packages/leva/src/components/Vector/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Vector' 2 | export * from './vector-plugin' 3 | -------------------------------------------------------------------------------- /packages/leva/src/components/Vector/vector-utils.ts: -------------------------------------------------------------------------------- 1 | import { normalize } from '../Number/number-plugin' 2 | import type { NumberSettings } from '../../types' 3 | import type { InternalNumberSettings } from '../Number/number-types' 4 | 5 | export const normalizeKeyedNumberSettings = <V extends Record<string, number>>( 6 | value: V, 7 | settings: { [key in keyof V]?: NumberSettings } 8 | ) => { 9 | const _settings = {} as { [key in keyof V]: InternalNumberSettings } 10 | 11 | let maxStep = 0 12 | let minPad = Infinity 13 | Object.entries(value).forEach(([key, v]: [keyof V, any]) => { 14 | _settings[key] = normalize({ value: v, ...settings[key] }).settings 15 | maxStep = Math.max(maxStep, _settings[key].step) 16 | minPad = Math.min(minPad, _settings[key].pad) 17 | }) 18 | 19 | // makes sure we get a consistent step and pad on all vector components when 20 | // step is not specified in settings. 21 | for (let key in _settings) { 22 | const { step, min, max } = (settings[key] as any) || {} 23 | if (!isFinite(step) && (!isFinite(min) || !isFinite(max))) { 24 | _settings[key].step = maxStep 25 | _settings[key].pad = minPad 26 | } 27 | } 28 | 29 | return _settings 30 | } 31 | -------------------------------------------------------------------------------- /packages/leva/src/components/Vector2d/StyledJoystick.ts: -------------------------------------------------------------------------------- 1 | import { styled } from '../../styles' 2 | 3 | export const JoystickTrigger = styled('div', { 4 | $flexCenter: '', 5 | position: 'relative', 6 | backgroundColor: '$elevation3', 7 | borderRadius: '$sm', 8 | cursor: 'pointer', 9 | height: '$rowHeight', 10 | width: '$rowHeight', 11 | touchAction: 'none', 12 | $draggable: '', 13 | $hover: '', 14 | 15 | '&:active': { cursor: 'none' }, 16 | 17 | '&::after': { 18 | content: '""', 19 | backgroundColor: '$accent2', 20 | height: 4, 21 | width: 4, 22 | borderRadius: 2, 23 | }, 24 | }) 25 | 26 | export const JoystickPlayground = styled('div', { 27 | $flexCenter: '', 28 | width: '$joystickWidth', 29 | height: '$joystickHeight', 30 | borderRadius: '$sm', 31 | boxShadow: '$level2', 32 | position: 'fixed', 33 | zIndex: 10000, 34 | overflow: 'hidden', 35 | $draggable: '', 36 | transform: 'translate(-50%, -50%)', 37 | 38 | variants: { 39 | isOutOfBounds: { 40 | true: { backgroundColor: '$elevation1' }, 41 | false: { backgroundColor: '$elevation3' }, 42 | }, 43 | }, 44 | '> div': { 45 | position: 'absolute', 46 | $flexCenter: '', 47 | borderStyle: 'solid', 48 | borderWidth: 1, 49 | borderColor: '$highlight1', 50 | backgroundColor: '$elevation3', 51 | width: '80%', 52 | height: '80%', 53 | 54 | '&::after,&::before': { 55 | content: '""', 56 | position: 'absolute', 57 | zindex: 10, 58 | backgroundColor: '$highlight1', 59 | }, 60 | 61 | '&::before': { 62 | width: '100%', 63 | height: 1, 64 | }, 65 | 66 | '&::after': { 67 | height: '100%', 68 | width: 1, 69 | }, 70 | }, 71 | 72 | '> span': { 73 | position: 'relative', 74 | zindex: 100, 75 | width: 10, 76 | height: 10, 77 | backgroundColor: '$accent2', 78 | borderRadius: '50%', 79 | }, 80 | }) 81 | -------------------------------------------------------------------------------- /packages/leva/src/components/Vector2d/Vector2d.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { styled } from '../../styles' 3 | import { Vector } from '../Vector' 4 | import { Label, Row } from '../UI' 5 | import { Joystick } from './Joystick' 6 | import { useInputContext } from '../../context' 7 | import type { Vector2dProps } from './vector2d-types' 8 | 9 | export const Container = styled('div', { 10 | display: 'grid', 11 | columnGap: '$colGap', 12 | variants: { 13 | withJoystick: { 14 | true: { gridTemplateColumns: '$sizes$rowHeight auto' }, 15 | false: { gridTemplateColumns: 'auto' }, 16 | }, 17 | }, 18 | }) 19 | 20 | export function Vector2dComponent() { 21 | const { label, displayValue, onUpdate, settings } = useInputContext<Vector2dProps>() 22 | return ( 23 | <Row input> 24 | <Label>{label}</Label> 25 | <Container withJoystick={!!settings.joystick}> 26 | {settings.joystick && <Joystick value={displayValue} settings={settings} onUpdate={onUpdate} />} 27 | <Vector value={displayValue} settings={settings} onUpdate={onUpdate} /> 28 | </Container> 29 | </Row> 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /packages/leva/src/components/Vector2d/index.ts: -------------------------------------------------------------------------------- 1 | import { Vector2dComponent } from './Vector2d' 2 | import { getVectorPlugin } from '../Vector' 3 | import { createInternalPlugin } from '../../plugin' 4 | import type { InternalVector2dSettings } from './vector2d-types' 5 | 6 | export * from './Vector2d' 7 | 8 | const plugin = getVectorPlugin(['x', 'y']) 9 | const normalize = ({ joystick = true, ...input }: any) => { 10 | const { value, settings } = plugin.normalize(input) 11 | return { value, settings: { ...settings, joystick } as InternalVector2dSettings } 12 | } 13 | 14 | export default createInternalPlugin({ 15 | component: Vector2dComponent, 16 | ...plugin, 17 | normalize, 18 | }) 19 | -------------------------------------------------------------------------------- /packages/leva/src/components/Vector2d/vector2d-types.ts: -------------------------------------------------------------------------------- 1 | import type { LevaInputProps, Vector2d, VectorObj } from '../../types' 2 | import type { InternalVectorSettings } from '../Vector/vector-types' 3 | 4 | export type InternalVector2dSettings = InternalVectorSettings<string, [string, string]> & { 5 | joystick: boolean | 'invertY' 6 | } 7 | export type Vector2dProps = LevaInputProps<Vector2d, InternalVector2dSettings, VectorObj> 8 | -------------------------------------------------------------------------------- /packages/leva/src/components/Vector3d/Vector3d.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Vector } from '../Vector' 3 | import { Label, Row } from '../UI' 4 | import { useInputContext } from '../../context' 5 | import type { Vector3dProps } from './vector3d-types' 6 | 7 | export function Vector3dComponent() { 8 | const { label, displayValue, onUpdate, settings } = useInputContext<Vector3dProps>() 9 | return ( 10 | <Row input> 11 | <Label>{label}</Label> 12 | <Vector value={displayValue} settings={settings} onUpdate={onUpdate} /> 13 | </Row> 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /packages/leva/src/components/Vector3d/index.ts: -------------------------------------------------------------------------------- 1 | import { Vector3dComponent } from './Vector3d' 2 | import { getVectorPlugin } from '../Vector' 3 | import { createInternalPlugin } from '../../plugin' 4 | 5 | export * from './Vector3d' 6 | 7 | export default createInternalPlugin({ 8 | component: Vector3dComponent, 9 | ...getVectorPlugin(['x', 'y', 'z']), 10 | }) 11 | -------------------------------------------------------------------------------- /packages/leva/src/components/Vector3d/vector3d-types.ts: -------------------------------------------------------------------------------- 1 | import type { LevaInputProps, Vector3d, VectorObj } from '../../types' 2 | import type { InternalVectorSettings } from '../Vector/vector-types' 3 | 4 | export type InternalVector3dSettings = InternalVectorSettings<string, [string, string, string]> 5 | export type Vector3dProps = LevaInputProps<Vector3d, InternalVector3dSettings, VectorObj> 6 | -------------------------------------------------------------------------------- /packages/leva/src/context.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext } from 'react' 2 | import type { FullTheme } from './styles' 3 | import type { StoreType, PanelSettingsType, InputContextProps } from './types' 4 | 5 | export const InputContext = createContext({}) 6 | 7 | export function useInputContext<T = {}>() { 8 | return useContext(InputContext) as InputContextProps & T 9 | } 10 | 11 | type ThemeContextProps = { theme: FullTheme; className: string } 12 | 13 | export const ThemeContext = createContext<ThemeContextProps | null>(null) 14 | 15 | export const StoreContext = createContext<StoreType | null>(null) 16 | 17 | export const PanelSettingsContext = createContext<PanelSettingsType | null>(null) 18 | 19 | export function useStoreContext() { 20 | return useContext(StoreContext)! 21 | } 22 | 23 | export function usePanelSettingsContext() { 24 | return useContext(PanelSettingsContext)! 25 | } 26 | 27 | type ReactChild = React.ReactElement | string | number 28 | 29 | type LevaStoreProviderProps = { 30 | children: ReactChild | ReactChild[] | typeof React.Children 31 | store: StoreType 32 | } 33 | 34 | export function LevaStoreProvider({ children, store }: LevaStoreProviderProps) { 35 | // @ts-expect-error portal JSX types are broken upstream 36 | return <StoreContext.Provider value={store}>{children}</StoreContext.Provider> 37 | } 38 | -------------------------------------------------------------------------------- /packages/leva/src/eventEmitter.ts: -------------------------------------------------------------------------------- 1 | type Listener = (...args: Array<any>) => void 2 | 3 | type EventEmitter = { 4 | on: (topic: string, listener: Listener) => void 5 | off: (topic: string, listener: Listener) => void 6 | emit: (event: string, ...args: Array<any>) => void 7 | } 8 | 9 | /** 10 | * Super simple event emitter. 11 | */ 12 | export const createEventEmitter = (): EventEmitter => { 13 | const listenerMapping = new Map<string, Set<Listener>>() 14 | return { 15 | on: (topic, listener) => { 16 | let listeners = listenerMapping.get(topic) 17 | if (listeners === undefined) { 18 | listeners = new Set() 19 | listenerMapping.set(topic, listeners) 20 | } 21 | listeners.add(listener) 22 | }, 23 | off: (topic, listener) => { 24 | const listeners = listenerMapping.get(topic) 25 | if (listeners === undefined) { 26 | return 27 | } 28 | listeners.delete(listener) 29 | if (listeners.size === 0) { 30 | listenerMapping.delete(topic) 31 | } 32 | }, 33 | emit: (topic, ...args) => { 34 | const listeners = listenerMapping.get(topic) 35 | if (listeners === undefined) { 36 | return 37 | } 38 | for (const listener of listeners) { 39 | listener(...args) 40 | } 41 | }, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/leva/src/helpers/button.ts: -------------------------------------------------------------------------------- 1 | import { SpecialInputs } from '../types' 2 | import type { ButtonInput, ButtonSettings } from '../types' 3 | 4 | const defaultSettings = { disabled: false } 5 | 6 | /** 7 | * 8 | * @param name button name 9 | * @param onClick function that executes when the button is clicked 10 | */ 11 | export function button(onClick: ButtonInput['onClick'], settings?: ButtonSettings): ButtonInput { 12 | return { type: SpecialInputs.BUTTON, onClick, settings: { ...defaultSettings, ...settings } } 13 | } 14 | -------------------------------------------------------------------------------- /packages/leva/src/helpers/buttonGroup.ts: -------------------------------------------------------------------------------- 1 | import { SpecialInputs } from '../types' 2 | import type { ButtonGroupInput, ButtonGroupInputOpts } from '../types' 3 | 4 | /** 5 | * 6 | * @param name button name 7 | * @param onClick function that executes when the button is clicked 8 | */ 9 | export function buttonGroup(opts: ButtonGroupInputOpts): ButtonGroupInput { 10 | return { type: SpecialInputs.BUTTON_GROUP, opts } 11 | } 12 | -------------------------------------------------------------------------------- /packages/leva/src/helpers/folder.ts: -------------------------------------------------------------------------------- 1 | import { SpecialInputs } from '../types' 2 | import type { FolderInput, Schema, SchemaToValues, FolderSettings } from '../types' 3 | 4 | const defaultSettings = { collapsed: false } 5 | 6 | export function folder<S extends Schema>(schema: S, settings?: FolderSettings): FolderInput<SchemaToValues<S>> { 7 | return { 8 | type: SpecialInputs.FOLDER, 9 | schema, 10 | settings: { ...defaultSettings, ...settings }, 11 | } as any 12 | } 13 | -------------------------------------------------------------------------------- /packages/leva/src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './folder' 2 | export * from './button' 3 | export * from './buttonGroup' 4 | export * from './monitor' 5 | -------------------------------------------------------------------------------- /packages/leva/src/helpers/monitor.ts: -------------------------------------------------------------------------------- 1 | import { SpecialInputs } from '../types' 2 | import type { MonitorInput, MonitorSettings } from '../types' 3 | 4 | const defaultSettings = { graph: false, interval: 100 } 5 | 6 | export function monitor(objectOrFn: React.MutableRefObject<any> | Function, settings?: MonitorSettings): MonitorInput { 7 | return { type: SpecialInputs.MONITOR, objectOrFn, settings: { ...defaultSettings, ...settings } } 8 | } 9 | -------------------------------------------------------------------------------- /packages/leva/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useInputSetters' 2 | export * from './useDeepMemo' 3 | export * from './useShallowMemo' 4 | export * from './useCompareMemoize' 5 | export * from './useDrag' 6 | export * from './useCanvas' 7 | export * from './useTransform' 8 | export * from './useToggle' 9 | export * from './useVisiblePaths' 10 | export * from './useValuesForPath' 11 | export * from './useInput' 12 | export * from './usePopin' 13 | export * from './useValue' 14 | -------------------------------------------------------------------------------- /packages/leva/src/hooks/useCanvas.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | import { debounce } from '../utils' 3 | 4 | export function useCanvas2d( 5 | fn: Function 6 | ): [React.RefObject<HTMLCanvasElement>, React.RefObject<CanvasRenderingContext2D>] { 7 | const canvas = useRef<HTMLCanvasElement>(null) 8 | const ctx = useRef<CanvasRenderingContext2D | null>(null) 9 | const hasFired = useRef(false) 10 | 11 | // TODO this is pretty much useless in 90% of cases since panels 12 | // have a fixed width 13 | useEffect(() => { 14 | const handleCanvas = debounce(() => { 15 | canvas.current!.width = canvas.current!.offsetWidth * window.devicePixelRatio 16 | canvas.current!.height = canvas.current!.offsetHeight * window.devicePixelRatio 17 | fn(canvas.current, ctx.current) 18 | }, 250) 19 | window.addEventListener('resize', handleCanvas) 20 | if (!hasFired.current) { 21 | handleCanvas() 22 | hasFired.current = true 23 | } 24 | return () => window.removeEventListener('resize', handleCanvas) 25 | }, [fn]) 26 | 27 | useEffect(() => { 28 | ctx.current = canvas.current!.getContext('2d') 29 | }, []) 30 | 31 | return [canvas, ctx] 32 | } 33 | -------------------------------------------------------------------------------- /packages/leva/src/hooks/useCompareMemoize.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react' 2 | import { dequal } from 'dequal/lite' 3 | import shallow from 'zustand/shallow' 4 | 5 | export function useCompareMemoize(value: any, deep: boolean) { 6 | const ref = useRef() 7 | const compare = deep ? dequal : shallow 8 | 9 | if (!compare(value, ref.current)) { 10 | ref.current = value 11 | } 12 | 13 | return ref.current 14 | } 15 | -------------------------------------------------------------------------------- /packages/leva/src/hooks/useDeepMemo.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import { useCompareMemoize } from './useCompareMemoize' 3 | 4 | export function useDeepMemo<T>(fn: () => T, deps: React.DependencyList | undefined) { 5 | // NOTE: useMemo implementation allows undefined, but types do not 6 | // eslint-disable-next-line react-hooks/exhaustive-deps 7 | return useMemo(fn, useCompareMemoize(deps, true)!) 8 | } 9 | -------------------------------------------------------------------------------- /packages/leva/src/hooks/useDrag.ts: -------------------------------------------------------------------------------- 1 | import { useInputContext } from '../context' 2 | import { FullGestureState, useDrag as useDragHook, UserDragConfig } from '@use-gesture/react' 3 | 4 | export function useDrag(handler: (state: FullGestureState<'drag'>) => any, config?: UserDragConfig) { 5 | const { emitOnEditStart, emitOnEditEnd } = useInputContext() 6 | return useDragHook((state) => { 7 | if (state.first) { 8 | document.body.classList.add('leva__panel__dragged') 9 | emitOnEditStart?.() 10 | } 11 | const result = handler(state) 12 | if (state.last) { 13 | document.body.classList.remove('leva__panel__dragged') 14 | emitOnEditEnd?.() 15 | } 16 | return result 17 | }, config) 18 | } 19 | -------------------------------------------------------------------------------- /packages/leva/src/hooks/useInput.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState, useEffect } from 'react' 2 | import shallow from 'zustand/shallow' 3 | import { useStoreContext } from '../context' 4 | import type { Data, DataItem } from '../types' 5 | 6 | const getInputAtPath = (data: Data, path: string) => { 7 | if (!data[path]) return null 8 | const { __refCount, ...input } = data[path] 9 | return input 10 | } 11 | 12 | type Input = Omit<DataItem, '__refCount'> 13 | 14 | /** 15 | * Return all input (value and settings) properties at a given path. 16 | * 17 | * @param path 18 | */ 19 | export function useInput(path: string): [ 20 | Input | null, 21 | { 22 | set: (value: any, onValueChanged?: (value: any) => void) => void 23 | setSettings: (value: any) => void 24 | disable: (flag: boolean) => void 25 | storeId: string 26 | emitOnEditStart: () => void 27 | emitOnEditEnd: () => void 28 | } 29 | ] { 30 | const store = useStoreContext() 31 | const [state, setState] = useState<Input | null>(getInputAtPath(store.getData(), path)) 32 | 33 | const set = useCallback((value: any) => store.setValueAtPath(path, value, true), [path, store]) 34 | const setSettings = useCallback((settings: any) => store.setSettingsAtPath(path, settings), [path, store]) 35 | const disable = useCallback((flag: boolean) => store.disableInputAtPath(path, flag), [path, store]) 36 | const emitOnEditStart = useCallback(() => store.emitOnEditStart(path), [path, store]) 37 | const emitOnEditEnd = useCallback(() => store.emitOnEditEnd(path), [path, store]) 38 | 39 | useEffect(() => { 40 | setState(getInputAtPath(store.getData(), path)) 41 | const unsub = store.useStore.subscribe((s) => getInputAtPath(s.data, path), setState, { equalityFn: shallow }) 42 | return () => unsub() 43 | }, [store, path]) 44 | 45 | return [state, { set, setSettings, disable, storeId: store.storeId, emitOnEditStart, emitOnEditEnd }] 46 | } 47 | -------------------------------------------------------------------------------- /packages/leva/src/hooks/useInputSetters.ts: -------------------------------------------------------------------------------- 1 | import { dequal } from 'dequal/lite' 2 | import { useState, useCallback, useEffect, useRef } from 'react' 3 | import { format } from '../plugin' 4 | 5 | type Props<V, Settings> = { 6 | type: string 7 | value: V 8 | settings?: Settings 9 | setValue: (v: V) => void 10 | } 11 | 12 | export function useInputSetters<V, Settings extends object>({ value, type, settings, setValue }: Props<V, Settings>) { 13 | // the value used by the panel vs the value 14 | const [displayValue, setDisplayValue] = useState(format(type, value, settings)) 15 | const previousValueRef = useRef(value) 16 | const settingsRef = useRef(settings) 17 | settingsRef.current = settings 18 | const setFormat = useCallback((v: V) => setDisplayValue(format(type, v, settingsRef.current)), [type]) 19 | 20 | const onUpdate = useCallback( 21 | (updatedValue: any) => { 22 | try { 23 | setValue(updatedValue) 24 | } catch (error: any) { 25 | const { type, previousValue } = error 26 | // make sure we throw an error if it's not a sanitization error 27 | if (type !== 'LEVA_ERROR') throw error 28 | setFormat(previousValue) 29 | } 30 | }, 31 | [setFormat, setValue] 32 | ) 33 | 34 | useEffect(() => { 35 | if (!dequal(value, previousValueRef.current)) { 36 | setFormat(value) 37 | } 38 | previousValueRef.current = value 39 | }, [value, setFormat]) 40 | 41 | return { displayValue, onChange: setDisplayValue, onUpdate } 42 | } 43 | -------------------------------------------------------------------------------- /packages/leva/src/hooks/usePopin.ts: -------------------------------------------------------------------------------- 1 | import { useState, useRef, useLayoutEffect, useCallback } from 'react' 2 | 3 | export function usePopin(margin = 3) { 4 | const popinRef = useRef<HTMLDivElement>(null) 5 | const wrapperRef = useRef<HTMLDivElement>(null) 6 | 7 | const [shown, setShow] = useState(false) 8 | 9 | const show = useCallback(() => setShow(true), []) 10 | const hide = useCallback(() => setShow(false), []) 11 | 12 | useLayoutEffect(() => { 13 | if (shown) { 14 | const { bottom, top, left } = popinRef.current!.getBoundingClientRect() 15 | const { height } = wrapperRef.current!.getBoundingClientRect() 16 | const direction = bottom + height > window.innerHeight - 40 ? 'up' : 'down' 17 | 18 | wrapperRef.current!.style.position = 'fixed' 19 | wrapperRef.current!.style.zIndex = '10000' 20 | wrapperRef.current!.style.left = left + 'px' 21 | 22 | if (direction === 'down') wrapperRef.current!.style.top = bottom + margin + 'px' 23 | else wrapperRef.current!.style.bottom = window.innerHeight - top + margin + 'px' 24 | } 25 | }, [margin, shown]) 26 | 27 | return { popinRef, wrapperRef, shown, show, hide } 28 | } 29 | -------------------------------------------------------------------------------- /packages/leva/src/hooks/useShallowMemo.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import { useCompareMemoize } from './useCompareMemoize' 3 | 4 | export function useShallowMemo<T>(fn: () => T, deps: React.DependencyList | undefined) { 5 | // NOTE: useMemo implementation allows undefined, but types do not 6 | // eslint-disable-next-line react-hooks/exhaustive-deps 7 | return useMemo(fn, useCompareMemoize(deps, false)!) 8 | } 9 | -------------------------------------------------------------------------------- /packages/leva/src/hooks/useToggle.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect, useLayoutEffect } from 'react' 2 | 3 | export function useToggle(toggled: boolean) { 4 | const wrapperRef = useRef<HTMLDivElement>(null) 5 | const contentRef = useRef<HTMLDivElement>(null) 6 | const firstRender = useRef(true) 7 | 8 | // this should be fine for SSR since the store is set in useEffect and 9 | // therefore the pane doesn't show on first render. 10 | useLayoutEffect(() => { 11 | if (!toggled) { 12 | wrapperRef.current!.style.height = '0px' 13 | wrapperRef.current!.style.overflow = 'hidden' 14 | } 15 | // we only want to do this once so that's ok to break the rules of hooks. 16 | // eslint-disable-next-line react-hooks/exhaustive-deps 17 | }, []) 18 | 19 | useEffect(() => { 20 | // prevents first animation 21 | if (firstRender.current) { 22 | firstRender.current = false 23 | return 24 | } 25 | 26 | let timeout: number 27 | const ref = wrapperRef.current! 28 | 29 | const fixHeight = () => { 30 | if (toggled) { 31 | ref.style.removeProperty('height') 32 | ref.style.removeProperty('overflow') 33 | contentRef.current!.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) 34 | } 35 | } 36 | 37 | ref.addEventListener('transitionend', fixHeight, { once: true }) 38 | 39 | const { height } = contentRef.current!.getBoundingClientRect() 40 | ref.style.height = height + 'px' 41 | if (!toggled) { 42 | ref.style.overflow = 'hidden' 43 | timeout = window.setTimeout(() => (ref.style.height = '0px'), 50) 44 | } 45 | 46 | return () => { 47 | ref.removeEventListener('transitionend', fixHeight) 48 | clearTimeout(timeout) 49 | } 50 | }, [toggled]) 51 | 52 | return { wrapperRef, contentRef } 53 | } 54 | -------------------------------------------------------------------------------- /packages/leva/src/hooks/useTransform.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useCallback } from 'react' 2 | 3 | export function useTransform<T extends HTMLElement>(): [ 4 | React.RefObject<T>, 5 | (point: { x?: number; y?: number }) => void 6 | ] { 7 | const ref = useRef<T>(null) 8 | const local = useRef({ x: 0, y: 0 }) 9 | 10 | const set = useCallback((point: { x?: number; y?: number }) => { 11 | Object.assign(local.current, point) 12 | if (ref.current) ref.current.style.transform = `translate3d(${local.current.x}px, ${local.current.y}px, 0)` 13 | }, []) 14 | 15 | return [ref, set] 16 | } 17 | -------------------------------------------------------------------------------- /packages/leva/src/hooks/useValue.ts: -------------------------------------------------------------------------------- 1 | import shallow from 'zustand/shallow' 2 | import { useStoreContext } from '../context' 3 | 4 | export const useValue = (path: string) => { 5 | return useValues([path])[path] 6 | } 7 | 8 | export const useValues = <T extends string>(paths: T[]) => { 9 | const store = useStoreContext() 10 | const value = store.useStore( 11 | ({ data }) => 12 | paths.reduce((acc, path) => { 13 | // @ts-expect-error 14 | if (data[path] && 'value' in data[path]) return Object.assign(acc, { [path]: data[path].value }) 15 | return acc 16 | }, {} as { [key in T]: any }), 17 | shallow 18 | ) 19 | return value 20 | } 21 | -------------------------------------------------------------------------------- /packages/leva/src/hooks/useValuesForPath.ts: -------------------------------------------------------------------------------- 1 | import shallow from 'zustand/shallow' 2 | import { getValuesForPaths } from '../utils/data' 3 | import type { Data, StoreType } from '../types' 4 | 5 | /** 6 | * Hook that returns the values from the zustand store for the given paths. 7 | * @param paths paths for which to return values 8 | * @param initialData 9 | */ 10 | export function useValuesForPath(store: StoreType, paths: string[], initialData: Data) { 11 | const valuesForPath = store.useStore((s) => { 12 | const data = { ...initialData, ...s.data } 13 | return getValuesForPaths(data, paths) 14 | }, shallow) 15 | 16 | return valuesForPath 17 | } 18 | -------------------------------------------------------------------------------- /packages/leva/src/hooks/useVisiblePaths.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import shallow from 'zustand/shallow' 3 | import type { StoreType } from '../types' 4 | 5 | /** 6 | * Hook used by the root component to get all visible inputs. 7 | */ 8 | export const useVisiblePaths = (store: StoreType) => { 9 | const [paths, setPaths] = useState(store.getVisiblePaths()) 10 | 11 | useEffect(() => { 12 | setPaths(store.getVisiblePaths()) 13 | const unsub = store.useStore.subscribe(store.getVisiblePaths, setPaths, { equalityFn: shallow }) 14 | return () => unsub() 15 | }, [store]) 16 | 17 | return paths 18 | } 19 | -------------------------------------------------------------------------------- /packages/leva/src/index.ts: -------------------------------------------------------------------------------- 1 | import { register } from './plugin' 2 | import number from './components/Number' 3 | import select from './components/Select' 4 | import color from './components/Color' 5 | import string from './components/String' 6 | import boolean from './components/Boolean' 7 | import vector3d from './components/Vector3d' 8 | import vector2d from './components/Vector2d' 9 | import image from './components/Image' 10 | import interval from './components/Interval' 11 | import { LevaInputs } from './types' 12 | 13 | /** 14 | * Register all the primitive inputs. 15 | * @note could potentially be done elsewhere. 16 | */ 17 | 18 | register(LevaInputs.SELECT, select) 19 | register(LevaInputs.IMAGE, image) 20 | register(LevaInputs.NUMBER, number) 21 | register(LevaInputs.COLOR, color) 22 | register(LevaInputs.STRING, string) 23 | register(LevaInputs.BOOLEAN, boolean) 24 | register(LevaInputs.INTERVAL, interval) 25 | register(LevaInputs.VECTOR3D, vector3d) 26 | register(LevaInputs.VECTOR2D, vector2d) 27 | 28 | // main hook 29 | export { useControls } from './useControls' 30 | 31 | // panel components 32 | export { Leva, LevaPanel } from './components/Leva' 33 | 34 | // simplifies passing store as context 35 | export { useStoreContext, LevaStoreProvider } from './context' 36 | 37 | // export the levaStore (default store) 38 | // hook to create custom store 39 | export { levaStore, useCreateStore } from './store' 40 | 41 | // export folder, monitor, button 42 | export * from './helpers' 43 | 44 | export { LevaInputs } 45 | -------------------------------------------------------------------------------- /packages/leva/src/plugin/index.ts: -------------------------------------------------------------------------------- 1 | // used as entrypoint 2 | 3 | // export all components 4 | import { Row, Label, Portal, Overlay } from '../components/UI' 5 | import { String } from '../components/String' 6 | import { Number } from '../components/Number' 7 | import { Boolean } from '../components/Boolean' 8 | import { Select } from '../components/Select' 9 | import { Vector } from '../components/Vector' 10 | import { InnerLabel } from '../components/ValueInput/StyledInput' 11 | 12 | export const Components = { 13 | Row, 14 | Label, 15 | Portal, 16 | Overlay, 17 | String, 18 | Number, 19 | Boolean, 20 | Select, 21 | Vector, 22 | InnerLabel, 23 | } 24 | 25 | export { colord } from 'colord' 26 | export { dequal } from 'dequal/lite' 27 | 28 | export { debounce, clamp, pad, evaluate, range, invertedRange, mergeRefs } from '../utils' 29 | export { normalizeKeyedNumberSettings } from '../components/Vector/vector-utils' 30 | 31 | export { createPlugin } from '../plugin' 32 | 33 | // export vector utilities 34 | export * from '../components/Vector/vector-plugin' 35 | // export useful hooks 36 | export { useDrag, useCanvas2d, useTransform, useInput, useValue, useValues, useInputSetters } from '../hooks' 37 | export { useInputContext, useStoreContext } from '../context' 38 | 39 | // export styling utilities 40 | export { styled, keyframes, useTh } from '../styles' 41 | 42 | // export types 43 | export * from '../types/public' 44 | export type { InternalVectorSettings } from '../components/Vector/vector-types' 45 | export type { InternalNumberSettings } from '../components/Number/number-types' 46 | -------------------------------------------------------------------------------- /packages/leva/src/styles/index.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import { getDefaultTheme, FullTheme, LevaCustomTheme, createTheme } from './stitches.config' 3 | import { ThemeContext } from '../context' 4 | import { warn, LevaErrors } from '../utils' 5 | 6 | export function mergeTheme(newTheme?: LevaCustomTheme): { theme: FullTheme; className: string } { 7 | const defaultTheme = getDefaultTheme() 8 | if (!newTheme) return { theme: defaultTheme, className: '' } 9 | Object.keys(newTheme!).forEach((key) => { 10 | // @ts-ignore 11 | Object.assign(defaultTheme![key], newTheme![key]) 12 | }) 13 | const customTheme = createTheme(defaultTheme) 14 | return { theme: defaultTheme, className: customTheme.className } 15 | } 16 | 17 | export function useTh<C extends keyof FullTheme>(category: C, key: keyof FullTheme[C]) { 18 | const { theme } = useContext(ThemeContext)! 19 | if (!(category in theme!) || !(key in theme![category]!)) { 20 | warn(LevaErrors.THEME_ERROR, category, key) 21 | return '' 22 | } 23 | 24 | let _key = key 25 | while (true) { 26 | // @ts-ignore 27 | let value = theme[category][_key] 28 | if (typeof value === 'string' && value.charAt(0) === '#39;) _key = value.substr(1) as any 29 | else return value 30 | } 31 | } 32 | 33 | export * from './stitches.config' 34 | -------------------------------------------------------------------------------- /packages/leva/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './internal' 2 | export * from './public' 3 | export * from './utils' 4 | -------------------------------------------------------------------------------- /packages/leva/src/types/utils.ts: -------------------------------------------------------------------------------- 1 | // Utils from https://github.com/pmndrs/use-tweaks/blob/92561618abbf43c581fc5950fd35c0f8b21047cd/src/types.ts#L70 2 | /** 3 | * It does nothing but beautify union type 4 | * 5 | * ``` 6 | * type A = { a: 'a' } & { b: 'b' } // { a: 'a' } & { b: 'b' } 7 | * type B = Id<{ a: 'a' } & { b: 'b' }> // { a: 'a', b: 'b' } 8 | * ``` 9 | */ 10 | export type BeautifyUnionType<T> = T extends object 11 | ? T extends Function // if T is a function return it as is 12 | ? T 13 | : any[] extends T // if T is a plain array return it as is 14 | ? T 15 | : T extends infer TT // if T is an object beautify it 16 | ? { [k in keyof TT]: TT[k] } & GetIterator<TT> 17 | : never 18 | : T 19 | 20 | // adds Iterator to the return type in case it has any 21 | type GetIterator<T> = T extends { [Symbol.iterator]: infer U } ? { [Symbol.iterator]: U } : {} 22 | 23 | export type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never 24 | 25 | /** 26 | * Gets keys from Record 27 | */ 28 | export type GetKeys<V> = V extends Record<infer K, number> ? K : never 29 | -------------------------------------------------------------------------------- /packages/leva/src/types/v8n.d.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/DefinitelyTyped/DefinitelyTyped/pull/27449/commits/3afbb600c58f0739410b5f882d35eb323976fe80 2 | 3 | declare module 'v8n' { 4 | const v8n: { 5 | (): Validation 6 | extend(item: Record<string, (item: any) => (value: any) => boolean>) 7 | } 8 | 9 | export default v8n 10 | 11 | interface Validation { 12 | chain: Rule[] 13 | every: Validation 14 | invert?: boolean 15 | extend(newRules: { [key: string]: () => boolean }): void 16 | test(value: any): boolean 17 | check(value: any): never 18 | pattern(pattern: RegExp): Validation 19 | equal(expected: any): Validation 20 | exact(expected: any): Validation 21 | string(): Validation 22 | number(): Validation 23 | boolean(): Validation 24 | undefined(): Validation 25 | null(): Validation 26 | array(): Validation 27 | lowercase(): Validation 28 | vowel(): Validation 29 | object(): Validation 30 | consonant(): Validation 31 | first(item: any): Validation 32 | last(item: any): Validation 33 | empty(): Validation 34 | length(min: number, max?: number): Validation 35 | minLength(min: number): Validation 36 | maxLength(max: number): Validation 37 | negative(): Validation 38 | positive(): Validation 39 | between(min: number, max: number): Validation 40 | range(min: number, max: number): Validation 41 | lessThan(bound: number): Validation 42 | lessThanOrEqual(bound: number): Validation 43 | greaterThan(bound: number): Validation 44 | greaterThanOrEqual(bound: number): Validation 45 | even(): Validation 46 | odd(): Validation 47 | includes(expected: any): Validation 48 | integer(): Validation 49 | schema(item: any): Validation 50 | passesAnyOf(...args: Validation[]): Validation 51 | optional(item: Validation): Validation 52 | } 53 | class Rule { 54 | constructor(name: string, fn: () => boolean, args?: any, invert?: boolean) 55 | name: string 56 | fn: () => boolean 57 | args?: any 58 | invert?: boolean 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/leva/src/utils/data.ts: -------------------------------------------------------------------------------- 1 | import { pick } from '.' 2 | import { Data } from '../types' 3 | 4 | /** 5 | * Takes a data object with { [path.key]: value } and returns { [key]: value }. 6 | * Also warns when two similar keys are being used by the user. 7 | * 8 | * @param data 9 | * @param paths 10 | * @param shouldWarn 11 | */ 12 | export function getValuesForPaths(data: Data, paths: string[]) { 13 | return Object.entries(pick(data, paths)).reduce( 14 | // Typescript complains that SpecialInput type doesn't have a value key. 15 | // But getValuesForPath is only called from paths that are inputs, 16 | // so they always have a value key. 17 | 18 | // @ts-expect-error 19 | (acc, [, { value, disabled, key }]) => { 20 | acc[key] = disabled ? undefined : value 21 | return acc 22 | }, 23 | {} as { [path: string]: any } 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /packages/leva/src/utils/event.ts: -------------------------------------------------------------------------------- 1 | export const multiplyStep = (event: any) => (event.shiftKey ? 5 : event.altKey ? 1 / 5 : 1) 2 | -------------------------------------------------------------------------------- /packages/leva/src/utils/fn.ts: -------------------------------------------------------------------------------- 1 | export const debounce = <F extends Function>(callback: F, wait: number, immediate = false) => { 2 | let timeout: number = 0 3 | 4 | return function () { 5 | const args = arguments as any 6 | const callNow = immediate && !timeout 7 | // @ts-expect-error 8 | const next = () => callback.apply(this, args) 9 | 10 | window.clearTimeout(timeout) 11 | timeout = window.setTimeout(next, wait) 12 | 13 | if (callNow) next() 14 | } as F extends (...args: infer A) => infer B ? (...args: A) => B : never 15 | } 16 | -------------------------------------------------------------------------------- /packages/leva/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './math' 2 | export * from './path' 3 | export * from './object' 4 | export * from './input' 5 | export * from './fn' 6 | export * from './log' 7 | export * from './data' 8 | export * from './event' 9 | export * from './react' 10 | -------------------------------------------------------------------------------- /packages/leva/src/utils/object.ts: -------------------------------------------------------------------------------- 1 | export function pick<K extends string, T extends { [k in K]: unknown }>(object: T, keys: K[]) { 2 | return keys.reduce((obj, key) => { 3 | if (!!object && object.hasOwnProperty(key)) { 4 | obj[key] = object[key] 5 | } 6 | return obj 7 | }, {} as { [k in K]: T[k] }) 8 | } 9 | 10 | export function omit<K extends string, T extends { [k in K]: unknown }>(object: T, keys: K[]) { 11 | const obj = { ...object } 12 | keys.forEach((k) => k in object && delete obj[k]) 13 | return obj 14 | } 15 | export function mapArrayToKeys<U extends any, K extends string>(value: U[], keys: K[]): Record<K, U> { 16 | return value.reduce((acc, v, i) => Object.assign(acc, { [keys[i]]: v }), {} as any) 17 | } 18 | 19 | export function isObject(variable: any) { 20 | return Object.prototype.toString.call(variable) === '[object Object]' 21 | } 22 | 23 | export const isEmptyObject = (obj: Object) => isObject(obj) && Object.keys(obj).length === 0 24 | -------------------------------------------------------------------------------- /packages/leva/src/utils/path.ts: -------------------------------------------------------------------------------- 1 | export const join = (...args: (string | undefined)[]) => args.filter(Boolean).join('.') 2 | 3 | export const prefix = (obj: object, p: string) => 4 | Object.entries(obj).reduce((acc, [key, v]) => ({ ...acc, [join(p, key)]: v }), {}) 5 | 6 | export function getKeyPath(path: string): [string, string | undefined] { 7 | const dir = path.split('.') 8 | return [dir.pop()!, dir.join('.') || undefined] 9 | } 10 | -------------------------------------------------------------------------------- /packages/leva/src/utils/react.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | /* 4 | * https://github.com/gregberge/react-merge-refs 5 | * MIT License 6 | * Copyright (c) 2020 Greg Bergé 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in all 16 | * copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | * SOFTWARE. 25 | */ 26 | 27 | export function mergeRefs<T>( 28 | refs: Array<React.RefCallback<T> | React.RefObject<T> | null | undefined> 29 | ): React.RefCallback<T> { 30 | return (value) => { 31 | refs.forEach((ref) => { 32 | if (typeof ref === 'function') ref(value) 33 | else if (ref != null) { 34 | ;(ref as React.MutableRefObject<T | null>).current = value 35 | } 36 | }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/leva/stories/caching.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Reset from './components/decorator-reset' 3 | import { Story, Meta } from '@storybook/react' 4 | 5 | import { useControls } from '../src' 6 | 7 | export default { 8 | title: 'Hook/Caching', 9 | decorators: [Reset], 10 | } as Meta 11 | 12 | const Controls = () => { 13 | const values = useControls({ num: 10, color: '#f00' }) 14 | 15 | return ( 16 | <div> 17 | <pre>{JSON.stringify(values, null, ' ')}</pre> 18 | </div> 19 | ) 20 | } 21 | 22 | const Template: Story<any> = () => { 23 | const [mounted, toggle] = React.useState(true) 24 | return ( 25 | <div> 26 | <button onClick={() => toggle((t) => !t)}>{mounted ? 'Unmount' : 'Mount'}</button> 27 | {mounted && <Controls />} 28 | </div> 29 | ) 30 | } 31 | 32 | export const Caching = Template.bind({}) 33 | -------------------------------------------------------------------------------- /packages/leva/stories/components/decorator-reset.tsx: -------------------------------------------------------------------------------- 1 | import { StoryFnReactReturnType } from '@storybook/react/dist/ts3.9/client/preview/types' 2 | import * as React from 'react' 3 | import { levaStore } from '../../src' 4 | 5 | const DefaultStory = (Story: () => StoryFnReactReturnType) => { 6 | const [_, set] = React.useState(false) 7 | React.useEffect(() => { 8 | levaStore.dispose() 9 | set(true) 10 | }, []) 11 | return _ ? <Story /> : <></> 12 | } 13 | 14 | export default DefaultStory 15 | -------------------------------------------------------------------------------- /packages/leva/stories/inputs/Boolean.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Story, Meta } from '@storybook/react' 3 | 4 | import Reset from '../components/decorator-reset' 5 | 6 | import { useControls } from '../../src' 7 | 8 | export default { 9 | title: 'Inputs/Boolean', 10 | decorators: [Reset], 11 | } as Meta 12 | 13 | const Template: Story<any> = (args) => { 14 | const values = useControls({ 15 | foo: args, 16 | }) 17 | 18 | return ( 19 | <div> 20 | <pre>{JSON.stringify(values, null, ' ')}</pre> 21 | </div> 22 | ) 23 | } 24 | 25 | export const Default = Template.bind({}) 26 | Default.args = { 27 | value: false, 28 | } 29 | 30 | export const Checked = Template.bind({}) 31 | Checked.args = { 32 | value: true, 33 | } 34 | -------------------------------------------------------------------------------- /packages/leva/stories/inputs/Button.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Meta } from '@storybook/react' 3 | 4 | import Reset from '../components/decorator-reset' 5 | 6 | import { useControls, button } from '../../src' 7 | 8 | export default { 9 | title: 'Inputs/Button', 10 | decorators: [Reset], 11 | } as Meta 12 | 13 | export const Button = () => { 14 | const values = useControls({ 15 | number: 3, 16 | foo: button((get) => alert(`Number value is ${get('number').toFixed(2)}`)), 17 | }) 18 | 19 | return ( 20 | <div> 21 | <pre>{JSON.stringify(values, null, ' ')}</pre> 22 | </div> 23 | ) 24 | } 25 | 26 | export const DisabledButton = () => { 27 | const values = useControls({ 28 | number: 3, 29 | foo: button((get) => alert(`Number value is ${get('number')}`), { disabled: true }), 30 | }) 31 | 32 | return ( 33 | <div> 34 | <pre>{JSON.stringify(values, null, ' ')}</pre> 35 | </div> 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /packages/leva/stories/inputs/Image.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Story, Meta } from '@storybook/react' 3 | 4 | import Reset from '../components/decorator-reset' 5 | 6 | import { useControls } from '../../src' 7 | 8 | export default { 9 | title: 'Inputs/Image', 10 | decorators: [Reset], 11 | } as Meta 12 | 13 | const Template: Story<any> = (args = undefined) => { 14 | const values = useControls({ foo: args }) as any 15 | 16 | return ( 17 | <div> 18 | <img src={values.foo} alt="" width="200" /> 19 | </div> 20 | ) 21 | } 22 | 23 | export const Image = Template.bind({}) 24 | Image.args = { image: undefined } 25 | -------------------------------------------------------------------------------- /packages/leva/stories/inputs/Interval.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Story, Meta } from '@storybook/react' 3 | 4 | import Reset from '../components/decorator-reset' 5 | 6 | import { useControls } from '../../src' 7 | 8 | export default { 9 | title: 'Inputs/Interval', 10 | decorators: [Reset], 11 | } as Meta 12 | 13 | const Template: Story<any> = (args) => { 14 | const values = useControls({ 15 | foo: args, 16 | }) 17 | 18 | return ( 19 | <div> 20 | <pre>{JSON.stringify(values, null, ' ')}</pre> 21 | </div> 22 | ) 23 | } 24 | 25 | export const Simple = Template.bind({}) 26 | Simple.args = { 27 | value: [10, 15], 28 | min: 1, 29 | max: 20, 30 | } 31 | 32 | export const OverflowingValue = Template.bind({}) 33 | OverflowingValue.args = { 34 | value: [-10, 150], 35 | min: 1, 36 | max: 20, 37 | } 38 | -------------------------------------------------------------------------------- /packages/leva/stories/inputs/Number.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Story, Meta } from '@storybook/react' 3 | 4 | import Reset from '../components/decorator-reset' 5 | 6 | import { useControls } from '../../src' 7 | 8 | export default { 9 | title: 'Inputs/Number', 10 | decorators: [Reset], 11 | } as Meta 12 | 13 | const Template: Story<any> = (args) => { 14 | const values = useControls({ 15 | foo: args, 16 | }) 17 | 18 | return ( 19 | <div> 20 | <pre>{JSON.stringify(values, null, ' ')}</pre> 21 | </div> 22 | ) 23 | } 24 | 25 | export const Simple = Template.bind({}) 26 | Simple.args = { 27 | value: 1, 28 | } 29 | 30 | export const MinMax = Template.bind({}) 31 | MinMax.args = { 32 | value: 1, 33 | min: 0, 34 | max: 10, 35 | } 36 | 37 | export const WithValueOverflow = Template.bind({}) 38 | WithValueOverflow.args = { 39 | value: 100, 40 | min: 0, 41 | max: 10, 42 | } 43 | 44 | export const Step = Template.bind({}) 45 | Step.args = { 46 | value: 10, 47 | step: 0.25, 48 | } 49 | 50 | export const Suffix = Template.bind({}) 51 | Suffix.args = { value: '10px' } 52 | 53 | export const Complete = Template.bind({}) 54 | Complete.args = { 55 | value: 5, 56 | min: 0, 57 | max: 10, 58 | step: 1, 59 | suffix: 'px', 60 | } 61 | -------------------------------------------------------------------------------- /packages/leva/stories/inputs/Select.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Story, Meta } from '@storybook/react' 3 | 4 | import Reset from '../components/decorator-reset' 5 | 6 | import { useControls } from '../../src' 7 | 8 | export default { 9 | title: 'Inputs/Select', 10 | decorators: [Reset], 11 | } as Meta 12 | 13 | const Template: Story<any> = (args) => { 14 | const values = useControls({ 15 | foo: args, 16 | }) 17 | 18 | return ( 19 | <div> 20 | <pre>{JSON.stringify(values, null, ' ')}</pre> 21 | </div> 22 | ) 23 | } 24 | 25 | export const Simple = Template.bind({}) 26 | Simple.args = { 27 | value: 'x', 28 | options: ['x', 'y'], 29 | } 30 | 31 | export const CustomLabels = Template.bind({}) 32 | CustomLabels.args = { 33 | value: 'helloWorld', 34 | options: { 35 | 'Hello World': 'helloWorld', 36 | 'Leva is awesome!': 'leva', 37 | }, 38 | } 39 | 40 | export const InferredValueAsOption = Template.bind({}) 41 | InferredValueAsOption.args = { 42 | value: true, 43 | options: [false], 44 | } 45 | 46 | export const DifferentOptionTypes = Template.bind({}) 47 | DifferentOptionTypes.args = { 48 | value: undefined, 49 | options: ['x', 'y', ['x', 'y']], 50 | } 51 | 52 | const IconA = () => <span>IconA</span> 53 | const IconB = () => <span>IconB</span> 54 | 55 | export const FunctionAsOptions = () => { 56 | const values = useControls({ 57 | foo: { options: { none: '', IconA, IconB } }, 58 | }) 59 | 60 | return ( 61 | <div> 62 | <pre>{values.foo.toString()}</pre> 63 | </div> 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /packages/leva/stories/inputs/String.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Story, Meta } from '@storybook/react' 3 | 4 | import Reset from '../components/decorator-reset' 5 | 6 | import { useControls } from '../../src' 7 | 8 | export default { 9 | title: 'Inputs/String', 10 | decorators: [Reset], 11 | } as Meta 12 | 13 | const Template: Story<any> = (args) => { 14 | const values = useControls({ 15 | foo: args, 16 | }) 17 | 18 | return ( 19 | <div> 20 | <pre>{JSON.stringify(values, null, ' ')}</pre> 21 | </div> 22 | ) 23 | } 24 | 25 | export const Simple = Template.bind({}) 26 | Simple.args = { 27 | value: 'Leva is awesome', 28 | } 29 | 30 | export const DefaultRows = Template.bind({}) 31 | DefaultRows.args = { 32 | value: 'Leva also supports <textarea/>\nAllowing for\nmultiple lines', 33 | rows: true, 34 | } 35 | 36 | export const CustomRows = Template.bind({}) 37 | CustomRows.args = { 38 | value: 'You can specify the number of rows you need', 39 | rows: 3, 40 | } 41 | 42 | export const NonEditable = Template.bind({}) 43 | NonEditable.args = { 44 | value: 'This text is not editable but still supports\nline\nbreaks.', 45 | editable: false, 46 | } 47 | -------------------------------------------------------------------------------- /packages/leva/stories/inputs/Vector.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Story, Meta } from '@storybook/react' 3 | 4 | import Reset from '../components/decorator-reset' 5 | 6 | import { useControls } from '../../src' 7 | 8 | export default { 9 | title: 'Inputs/Vector', 10 | decorators: [Reset], 11 | } as Meta 12 | 13 | const Template: Story<any> = (args) => { 14 | const values = useControls({ 15 | foo: args, 16 | }) 17 | 18 | return ( 19 | <div> 20 | <pre>{JSON.stringify(values, null, ' ')}</pre> 21 | </div> 22 | ) 23 | } 24 | 25 | export const Vector2 = Template.bind({}) 26 | Vector2.args = { 27 | value: { x: 0, y: 0 }, 28 | } 29 | 30 | export const Vector2FromArray = Template.bind({}) 31 | Vector2FromArray.args = { 32 | value: [1, 10], 33 | } 34 | 35 | export const Vector2WithLock = Template.bind({}) 36 | Vector2WithLock.args = { 37 | value: [1, 10], 38 | lock: true, 39 | } 40 | 41 | export const Vector2WithoutJoystick = Template.bind({}) 42 | Vector2WithoutJoystick.args = { 43 | value: { x: 0, y: 0 }, 44 | joystick: false, 45 | } 46 | 47 | export const Vector2WithInvertedJoystickY = ({ value, invertY }) => ( 48 | <Template value={value} joystick={invertY ? 'invertY' : undefined} /> 49 | ) 50 | Vector2WithInvertedJoystickY.args = { 51 | value: [0, 0], 52 | invertY: true, 53 | } 54 | 55 | export const Vector3 = Template.bind({}) 56 | Vector3.args = { 57 | value: { x: 0, y: 0, z: 0 }, 58 | } 59 | 60 | export const Vector3FromArray = Template.bind({}) 61 | Vector3FromArray.args = { 62 | value: [1, 10, 0], 63 | } 64 | 65 | export const Vector3WithLock = Template.bind({}) 66 | Vector3WithLock.args = { 67 | value: [1, 10, 0], 68 | lock: true, 69 | } 70 | -------------------------------------------------------------------------------- /packages/plugin-bezier/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @leva-ui/plugin-bezier 2 | 3 | ## 0.10.0 4 | 5 | ### Minor Changes 6 | 7 | - 3d4a620: feat!: React 18 and 19 support 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies [b9c6376] 12 | - Updated dependencies [3d4a620] 13 | - leva@0.10.0 14 | 15 | ## 0.9.19 16 | 17 | ### Patch Changes 18 | 19 | - 3177e59: style: label alignment 20 | - Updated dependencies [3177e59] 21 | - leva@0.9.23 22 | 23 | ## 0.9.18 24 | 25 | ### Patch Changes 26 | 27 | - e45e9de: Feat: pass `get` function to Button and ButtonGroup 28 | - Updated dependencies [e45e9de] 29 | - leva@0.9.18 30 | 31 | ## 0.9.14 32 | 33 | ### Minor Changes 34 | 35 | - 1001f25: Fix version for stitches before moving to 1.x 36 | 37 | ## 0.9.12 38 | 39 | ### Patch Changes 40 | 41 | - Updated dependencies 42 | - leva@0.9.12 43 | 44 | ## 0.9.10 45 | 46 | ### Patch Changes 47 | 48 | - 16e3c14: feat: add `preview` flag to disable dot preview. 49 | - Updated dependencies [16e3c14] 50 | - leva@0.9.10 51 | 52 | ## 0.9.8 53 | 54 | ### Patch Changes 55 | 56 | - f8f7b57: fix: double render issue when using nested components. 57 | - Updated dependencies [f8f7b57] 58 | - leva@0.9.9 59 | 60 | ## 0.0.4 61 | 62 | ### Patch Changes 63 | 64 | - 0511799: styles: remove manual 'leva\_\_' prefix from stitches styles. 65 | - Updated dependencies [0511799] 66 | - leva@0.9.6 67 | 68 | ## 0.0.3 69 | 70 | ### Patch Changes 71 | 72 | - 26ead12: Feat: add cssEasing to returned prop 73 | 74 | ## 0.0.2 75 | 76 | ### Patch Changes 77 | 78 | - c997410: Plugin: add the Bezier plugin 79 | 80 | ```js 81 | import { bezier } from '@leva-ui/plugin-bezier' 82 | useControls({ curve: bezier([0.25, 0.1, 0.25, 1]) }) 83 | ``` 84 | 85 | - Updated dependencies [c997410] 86 | - leva@0.8.1 87 | -------------------------------------------------------------------------------- /packages/plugin-bezier/README.md: -------------------------------------------------------------------------------- 1 | ## Leva Bezier 2 | 3 | ### Installation 4 | 5 | ```bash 6 | npm i @leva-ui/plugin-bezier 7 | ``` 8 | 9 | ### Quick start 10 | 11 | ```jsx 12 | import { useControls } from 'leva' 13 | import { bezier } from '@leva-ui/plugin-bezier' 14 | 15 | function MyComponent() { 16 | const { curve } = useControls({ curve: bezier() }) 17 | // or 18 | const { curve } = useControls({ curve: bezier([0.54, 0.05, 0.6, 0.98]) }) 19 | // or 20 | const { curve } = useControls({ curve: bezier('in-out-quadratic') }) 21 | // or 22 | const { curve } = useControls({ curve: bezier({ handles: [0.54, 0.05, 0.6, 0.98], graph: false }) }) 23 | 24 | // built-in function evaluation 25 | console.log(curve.evaluate(0.3)) 26 | 27 | // inside a css like animation-timing-function 28 | return <div style={{ animationTimingFunction: value.cssEasing }} /> 29 | } 30 | ``` 31 | -------------------------------------------------------------------------------- /packages/plugin-bezier/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@leva-ui/plugin-bezier", 3 | "version": "0.10.0", 4 | "main": "dist/leva-ui-plugin-bezier.cjs.js", 5 | "module": "dist/leva-ui-plugin-bezier.esm.js", 6 | "types": "dist/leva-ui-plugin-bezier.cjs.d.ts", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/pmndrs/leva.git", 11 | "directory": "packages/plugin-beziers" 12 | }, 13 | "bugs": "https://github.com/pmndrs/leva/issues", 14 | "peerDependencies": { 15 | "leva": ">=0.10.0", 16 | "react": "^18.0.0 || ^19.0.0", 17 | "react-dom": "^18.0.0 || ^19.0.0" 18 | }, 19 | "dependencies": { 20 | "react-use-measure": "^2.1.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/plugin-bezier/src/Bezier.stories.css: -------------------------------------------------------------------------------- 1 | @keyframes bezierStoryScale { 2 | 0% { 3 | transform: scaleX(0); 4 | } 5 | 6 | 100% { 7 | transform: scaleX(1); 8 | } 9 | } 10 | 11 | .bezier-animated { 12 | height: 10px; 13 | width: 200px; 14 | background: indianred; 15 | transform-origin: left; 16 | animation: bezierStoryScale 1000ms infinite alternate both; 17 | } 18 | -------------------------------------------------------------------------------- /packages/plugin-bezier/src/Bezier.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Story, Meta } from '@storybook/react' 3 | 4 | import Reset from 'leva/stories/components/decorator-reset' 5 | import { useControls } from 'leva/src' 6 | 7 | import { bezier } from './index' 8 | import './Bezier.stories.css' 9 | 10 | export default { 11 | title: 'Plugins/Bezier', 12 | decorators: [Reset], 13 | } as Meta 14 | 15 | const Template: Story<any> = (args) => { 16 | const data = useControls({ curve: args }) 17 | return ( 18 | <div> 19 | <div className="bezier-animated" style={{ animationTimingFunction: data.curve.cssEasing }} /> 20 | <pre>{JSON.stringify(data, null, ' ')}</pre> 21 | </div> 22 | ) 23 | } 24 | 25 | export const DefaultBezier = Template.bind({}) 26 | DefaultBezier.args = bezier(undefined) 27 | 28 | export const WithArguments = Template.bind({}) 29 | WithArguments.args = bezier([0.54, 0.05, 0.6, 0.98]) 30 | 31 | export const WithPreset = Template.bind({}) 32 | WithPreset.args = bezier('in-out-quadratic') 33 | 34 | export const WithOptions = Template.bind({}) 35 | WithOptions.args = bezier({ handles: [0.54, 0.05, 0.6, 0.98], graph: false, preview: false, label: 'no graph' }) 36 | -------------------------------------------------------------------------------- /packages/plugin-bezier/src/BezierPreview.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useMemo, useReducer } from 'react' 2 | import { debounce } from 'leva/plugin' 3 | import { PreviewSvg } from './StyledBezier' 4 | import type { BezierProps } from './bezier-types' 5 | 6 | const DebouncedBezierPreview = React.memo(({ value }: Pick<BezierProps, 'value'>) => { 7 | // use to forceUpdate on click 8 | const [, forceUpdate] = useReducer((x) => x + 1, 0) 9 | 10 | const plotPoints = Array(21) 11 | .fill(0) 12 | .map((_, i) => 5 + value.evaluate(i / 20) * 90) 13 | return ( 14 | <PreviewSvg onClick={forceUpdate}> 15 | {plotPoints.map((p, i) => ( 16 | <circle key={i + Date.now()} r={3} cx={`${p}%`} style={{ animationDelay: `${i * 50}ms` }} /> 17 | ))} 18 | <circle 19 | key={Date.now() - 1} 20 | r={3} 21 | style={{ 22 | animationTimingFunction: `cubic-bezier(${value.join(',')})`, 23 | animationDuration: `${plotPoints.length * 50}ms`, 24 | }} 25 | /> 26 | </PreviewSvg> 27 | ) 28 | }) 29 | 30 | export function BezierPreview({ value }: Pick<BezierProps, 'value'>) { 31 | const [debouncedValue, set] = useState(value) 32 | const debounceValue = useMemo(() => debounce((v: typeof value) => set(v), 250), []) 33 | useEffect(() => void debounceValue(value), [value, debounceValue]) 34 | 35 | return <DebouncedBezierPreview value={debouncedValue} /> 36 | } 37 | -------------------------------------------------------------------------------- /packages/plugin-bezier/src/StyledBezier.ts: -------------------------------------------------------------------------------- 1 | import { styled, keyframes } from 'leva/plugin' 2 | 3 | export const Svg = styled('svg', { 4 | width: '100%', 5 | height: '$controlWidth', 6 | marginTop: '$rowGap', 7 | overflow: 'visible', 8 | zIndex: 100, 9 | '> path': { 10 | stroke: '$highlight3', 11 | strokeWidth: 2, 12 | }, 13 | g: { 14 | color: '$accent1', 15 | '&:hover': { color: '$accent3' }, 16 | '&:active': { color: '$vivid1' }, 17 | }, 18 | circle: { 19 | fill: 'currentColor', 20 | strokeWidth: 10, 21 | stroke: 'transparent', 22 | cursor: 'pointer', 23 | }, 24 | '> line': { 25 | stroke: '$highlight1', 26 | strokeWidth: 2, 27 | }, 28 | '> g > line': { 29 | stroke: 'currentColor', 30 | }, 31 | variants: { 32 | withPreview: { true: { marginBottom: 0 }, false: { marginBottom: '$rowGap' } }, 33 | }, 34 | }) 35 | 36 | const fadeIn = (o: number) => 37 | keyframes({ 38 | '0%': { opacity: 0 }, 39 | '10%': { opacity: 0.8 }, 40 | '100%': { opacity: o }, 41 | }) 42 | 43 | const move = keyframes({ 44 | '0%': { transform: 'translateX(5%)' }, 45 | '100%': { transform: 'translateX(95%)' }, 46 | }) 47 | 48 | export const PreviewSvg = styled('svg', { 49 | width: '100%', 50 | overflow: 'visible', 51 | height: 6, 52 | '> circle': { 53 | fill: '$vivid1', 54 | cy: '50%', 55 | animation: `${fadeIn(0.3)} 1000ms both`, 56 | '&:first-of-type': { animationName: fadeIn(0.7) }, 57 | '&:last-of-type': { animationName: move }, 58 | }, 59 | }) 60 | 61 | export const SyledInnerLabel = styled('div', { 62 | userSelect: 'none', 63 | $flexCenter: '', 64 | height: 14, 65 | width: 14, 66 | borderRadius: 7, 67 | marginRight: '$sm', 68 | cursor: 'pointer', 69 | fontSize: '0.8em', 70 | variants: { 71 | graph: { true: { backgroundColor: '$elevation1' } }, 72 | }, 73 | }) 74 | 75 | export const Container = styled('div', { 76 | display: 'grid', 77 | gridTemplateColumns: 'auto 1fr', 78 | alignItems: 'center', 79 | }) 80 | -------------------------------------------------------------------------------- /packages/plugin-bezier/src/bezier-plugin.ts: -------------------------------------------------------------------------------- 1 | import { normalizeVector, sanitizeVector } from 'leva/plugin' 2 | import { bezier } from './bezier-utils' 3 | import type { BezierArray, BezierInput, InternalBezierSettings, InternalBezier, BuiltInKeys } from './bezier-types' 4 | 5 | const abscissasSettings = { min: 0, max: 1, step: 0.01 } 6 | const ordinatesSettings = { step: 0.01 } 7 | const defaultSettings = { graph: true, preview: true } 8 | 9 | export const BuiltIn: Record<BuiltInKeys, BezierArray> = { 10 | ease: [0.25, 0.1, 0.25, 1], 11 | linear: [0, 0, 1, 1], 12 | 'ease-in': [0.42, 0, 1, 1], 13 | 'ease-out': [0, 0, 0.58, 1], 14 | 'ease-in-out': [0.42, 0, 0.58, 1], 15 | 'in-out-sine': [0.45, 0.05, 0.55, 0.95], 16 | 'in-out-quadratic': [0.46, 0.03, 0.52, 0.96], 17 | 'in-out-cubic': [0.65, 0.05, 0.36, 1], 18 | 'fast-out-slow-in': [0.4, 0, 0.2, 1], 19 | 'in-out-back': [0.68, -0.55, 0.27, 1.55], 20 | } 21 | 22 | export const normalize = (input: BezierInput = [0.25, 0.1, 0.25, 1]) => { 23 | let { handles, ..._settings } = typeof input === 'object' && 'handles' in input ? input : { handles: input } 24 | handles = typeof handles === 'string' ? BuiltIn[handles] : handles 25 | 26 | const mergedSettings = { x1: abscissasSettings, y1: ordinatesSettings, x2: abscissasSettings, y2: ordinatesSettings } 27 | 28 | const { value: _value, settings } = normalizeVector(handles, mergedSettings, ['x1', 'y1', 'x2', 'y2']) 29 | const value = _value as InternalBezier 30 | value.evaluate = bezier(..._value) 31 | value.cssEasing = `cubic-bezier(${_value.join(',')})` 32 | return { value, settings: { ...settings, ...defaultSettings, ..._settings } as InternalBezierSettings } 33 | } 34 | 35 | export const sanitize = (value: any, settings: InternalBezierSettings, prevValue?: any) => { 36 | const _value = sanitizeVector(value, settings, prevValue) as BezierArray 37 | const newValue = _value as InternalBezier 38 | newValue.evaluate = bezier(..._value) 39 | newValue.cssEasing = `cubic-bezier(${_value.join(',')})` 40 | return newValue 41 | } 42 | -------------------------------------------------------------------------------- /packages/plugin-bezier/src/bezier-types.ts: -------------------------------------------------------------------------------- 1 | import type { LevaInputProps, InternalVectorSettings, MergedInputWithSettings } from 'leva/plugin' 2 | 3 | export type BuiltInKeys = 4 | | 'ease' 5 | | 'linear' 6 | | 'ease-in' 7 | | 'ease-out' 8 | | 'ease-in-out' 9 | | 'in-out-sine' 10 | | 'in-out-quadratic' 11 | | 'in-out-cubic' 12 | | 'fast-out-slow-in' 13 | | 'in-out-back' 14 | 15 | export type BezierArray = [number, number, number, number] 16 | 17 | export type Bezier = BezierArray | BuiltInKeys 18 | 19 | export type BezierSettings = { graph?: boolean; preview?: boolean } 20 | export type BezierInput = MergedInputWithSettings<Bezier, BezierSettings, 'handles'> 21 | 22 | export type InternalBezier = [number, number, number, number] & { evaluate(value: number): number; cssEasing: string } 23 | 24 | export type DisplayValueBezier = { x1: number; y1: number; x2: number; y2: number } 25 | 26 | export type InternalBezierSettings = InternalVectorSettings< 27 | keyof DisplayValueBezier, 28 | (keyof DisplayValueBezier)[], 29 | 'array' 30 | > & { graph: boolean; preview: boolean } 31 | 32 | export type BezierProps = LevaInputProps<InternalBezier, InternalBezierSettings, DisplayValueBezier> 33 | -------------------------------------------------------------------------------- /packages/plugin-bezier/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createPlugin, formatVector } from 'leva/plugin' 2 | import { Bezier } from './Bezier' 3 | import { normalize, sanitize } from './bezier-plugin' 4 | import { InternalBezierSettings } from './bezier-types' 5 | 6 | export const bezier = createPlugin({ 7 | normalize, 8 | sanitize, 9 | format: (value: any, settings: InternalBezierSettings) => formatVector(value, settings), 10 | component: Bezier, 11 | }) 12 | -------------------------------------------------------------------------------- /packages/plugin-dates/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @leva-ui/plugin-dates 2 | 3 | ## 0.10.1 4 | 5 | ### Patch Changes 6 | 7 | - 89764b0: fix(plugin-dates): update React Calendar 8 | 9 | ## 0.10.0 10 | 11 | ### Minor Changes 12 | 13 | - 3d4a620: feat!: React 18 and 19 support 14 | 15 | ### Patch Changes 16 | 17 | - Updated dependencies [b9c6376] 18 | - Updated dependencies [3d4a620] 19 | - leva@0.10.0 20 | 21 | ## 0.9.32 22 | 23 | ### Patch Changes 24 | 25 | - 8b21a5c: fix: scrolling long panels 26 | - Updated dependencies [8b21a5c] 27 | - leva@0.9.34 28 | 29 | ## 0.9.31 30 | 31 | ### Major Changes 32 | 33 | - 14a5605: feat: new date picker plugin 34 | -------------------------------------------------------------------------------- /packages/plugin-dates/README.md: -------------------------------------------------------------------------------- 1 | ## Leva Plot 2 | 3 | ### Installation 4 | 5 | ```bash 6 | npm i @leva-ui/plugin-plot 7 | ``` 8 | 9 | ### Quick start 10 | 11 | ```jsx 12 | import { useControls } from 'leva' 13 | import { plot } from '@leva-ui/plugin-plot' 14 | 15 | function MyComponent() { 16 | const { y } = useControls({ y: plot({ expression: 'cos(x)', graph: true, boundsX: [-10, 10], boundsY: [0, 100] }) }) 17 | return y(Math.PI) 18 | } 19 | ``` 20 | -------------------------------------------------------------------------------- /packages/plugin-dates/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@leva-ui/plugin-dates", 3 | "version": "0.10.1", 4 | "main": "dist/leva-ui-plugin-dates.cjs.js", 5 | "module": "dist/leva-ui-plugin-dates.esm.js", 6 | "types": "dist/leva-ui-plugin-dates.cjs.d.ts", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/pmndrs/leva.git", 11 | "directory": "packages/plugin-dates" 12 | }, 13 | "bugs": "https://github.com/pmndrs/leva/issues", 14 | "peerDependencies": { 15 | "@use-gesture/react": "^10.0.0", 16 | "leva": ">=0.10.0", 17 | "react": "^18.0.0 || ^19.0.0", 18 | "react-dom": "^18.0.0 || ^19.0.0" 19 | }, 20 | "dependencies": { 21 | "react-datepicker": "^7.6.0" 22 | }, 23 | "devDependencies": { 24 | "@types/react-datepicker": "^7.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/plugin-dates/src/Date.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Story, Meta } from '@storybook/react' 3 | 4 | import Reset from '../../leva/stories/components/decorator-reset' 5 | import { useControls } from '../../leva/src' 6 | 7 | import { date } from './index' 8 | import { DateInput } from './date-types' 9 | 10 | export default { 11 | title: 'Plugins/Dates', 12 | decorators: [Reset], 13 | } as Meta 14 | 15 | const Template: Story<DateInput> = (args) => { 16 | const { birthday } = useControls({ birthday: date(args) }) 17 | return <div>{birthday.formattedDate}</div> 18 | } 19 | 20 | export const DefaultDate = Template.bind({}) 21 | DefaultDate.args = { date: new Date() } 22 | 23 | export const CustomLocale = Template.bind({}) 24 | CustomLocale.args = { date: new Date(), locale: 'en-US' } 25 | 26 | export const CustomInputFormat = Template.bind({}) 27 | CustomInputFormat.args = { date: new Date(), inputFormat: 'yyyy-MM-dd' } 28 | 29 | export const WithOtherFields = () => { 30 | const { birthday, ...values } = useControls({ 31 | text: 'text', 32 | birthday: date({ date: new Date() }), 33 | number: 0, 34 | }) 35 | return ( 36 | <div> 37 | {birthday.formattedDate} 38 | <br /> 39 | {JSON.stringify(values)} 40 | </div> 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /packages/plugin-dates/src/Date.tsx: -------------------------------------------------------------------------------- 1 | import { Components, useInputContext } from 'leva/plugin' 2 | import React, { forwardRef, useState } from 'react' 3 | import DatePicker, { CalendarContainer } from 'react-datepicker' 4 | import 'react-datepicker/dist/react-datepicker.css' 5 | import { DateCalendarContainerProps, DateInputProps, DateProps } from './date-types' 6 | import { InputContainer, StyledInput, StyledWrapper } from './StyledDate' 7 | 8 | const { Label, Row } = Components 9 | 10 | const DateCalendarContainer = ({ children }: DateCalendarContainerProps) => { 11 | return ( 12 | <CalendarContainer> 13 | {/* @ts-expect-error portal JSX types are broken upstream */} 14 | <StyledWrapper>{children}</StyledWrapper> 15 | </CalendarContainer> 16 | ) 17 | } 18 | 19 | const DateInput = forwardRef<HTMLInputElement, Partial<DateInputProps>>(({ value, onClick, onChange }, ref) => { 20 | return <StyledInput ref={ref} value={value} onClick={onClick} onChange={onChange} /> 21 | }) 22 | 23 | export function Date() { 24 | const { label, value, onUpdate, settings } = useInputContext<DateProps>() 25 | 26 | const [isOpen, setIsOpen] = useState(false) 27 | 28 | return ( 29 | <Row input style={{ height: isOpen ? 300 : 'auto' }}> 30 | <Label>{label}</Label> 31 | <InputContainer> 32 | <DatePicker 33 | selected={value.date} 34 | onChange={onUpdate} 35 | dateFormat={settings.inputFormat} 36 | calendarContainer={DateCalendarContainer} 37 | customInput={<DateInput />} 38 | onCalendarOpen={() => setIsOpen(true)} 39 | onCalendarClose={() => setIsOpen(false)} 40 | /> 41 | </InputContainer> 42 | </Row> 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /packages/plugin-dates/src/StyledDate.ts: -------------------------------------------------------------------------------- 1 | import { styled } from 'leva/plugin' 2 | 3 | export const StyledInput = styled('input', { 4 | $reset: '', 5 | padding: '0 $sm', 6 | width: '100%', 7 | minWidth: 0, 8 | flex: 1, 9 | height: '100%', 10 | }) 11 | 12 | export const InputContainer = styled('div', { 13 | $flex: '', 14 | position: 'relative', 15 | borderRadius: '$sm', 16 | color: 'inherit', 17 | height: '$rowHeight', 18 | backgroundColor: '$elevation3', 19 | $inputStyle: '$elevation1', 20 | $hover: '', 21 | $focusWithin: '', 22 | variants: { 23 | textArea: { true: { height: 'auto' } }, 24 | }, 25 | }) 26 | 27 | export const StyledWrapper = styled('div', { 28 | position: 'relative', 29 | 30 | '& .react-datepicker__header': { 31 | backgroundColor: '$elevation3', 32 | border: 'none', 33 | }, 34 | 35 | '& .react-datepicker__current-month, .react-datepicker__day, .react-datepicker__day-name': { 36 | color: 'inherit', 37 | }, 38 | 39 | '& .react-datepicker__day': { 40 | transition: 'all 0.2s ease', 41 | }, 42 | 43 | '& .react-datepicker__day--selected': { 44 | backgroundColor: '$accent1', 45 | color: '$highlight3', 46 | }, 47 | 48 | '& .react-datepicker__day--keyboard-selected': { 49 | backgroundColor: 'transparent', 50 | color: 'inherit', 51 | }, 52 | 53 | '& .react-datepicker__day--today': { 54 | backgroundColor: '$accent3', 55 | color: '$highlight3', 56 | }, 57 | 58 | '& .react-datepicker__month-container': { 59 | backgroundColor: '$elevation2', 60 | borderRadius: '$lg', 61 | }, 62 | 63 | '& .react-datepicker__day:hover': { 64 | backgroundColor: '$highlight1', 65 | }, 66 | }) 67 | -------------------------------------------------------------------------------- /packages/plugin-dates/src/date-plugin.ts: -------------------------------------------------------------------------------- 1 | import type { DateInput, DateSettings, InternalDate, InternalDateSettings } from './date-types' 2 | import { formatDate } from './date-utils' 3 | 4 | const defaultSettings = { 5 | inputFormat: 'MM/dd/yyyy', 6 | } 7 | 8 | export const sanitize = (value: Date, settings: DateSettings) => { 9 | return { 10 | date: value, 11 | formattedDate: formatDate(value, settings.locale), 12 | } 13 | } 14 | 15 | export const format = (value: InternalDate, settings: DateSettings) => { 16 | return { 17 | date: value.date, 18 | formattedDate: formatDate(value.date, settings.locale), 19 | } 20 | } 21 | 22 | export const normalize = ({ date, ..._settings }: DateInput) => { 23 | const settings = { ...defaultSettings, ..._settings } 24 | const defaultDate = date ?? new Date() 25 | return { 26 | value: { date: defaultDate, formattedDate: formatDate(defaultDate, settings.locale) }, 27 | settings: settings as InternalDateSettings, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/plugin-dates/src/date-types.ts: -------------------------------------------------------------------------------- 1 | import type { LevaInputProps } from 'leva/plugin' 2 | import { ChangeEventHandler, MouseEventHandler } from 'react' 3 | import { CalendarContainer } from 'react-datepicker' 4 | 5 | export type DateSettings = { locale: string; inputFormat: string } 6 | export type DateInput = { date: Date } & Partial<DateSettings> 7 | 8 | // TODO: export this upstream 9 | export type DateCalendarContainerProps = React.ComponentProps<typeof CalendarContainer> 10 | export type DateInputProps = { value: string; onClick: MouseEventHandler; onChange: ChangeEventHandler } 11 | 12 | export type InternalDate = { date: Date; formattedDate: string } 13 | 14 | export type InternalDateSettings = Required<DateSettings> 15 | 16 | export type DateProps = LevaInputProps<InternalDate, InternalDateSettings, string> 17 | -------------------------------------------------------------------------------- /packages/plugin-dates/src/date-utils.ts: -------------------------------------------------------------------------------- 1 | export function parseDate(date: string, locale: string) { 2 | return new Date(date) 3 | } 4 | 5 | export function formatDate(date: Date, locale?: string) { 6 | return date.toLocaleDateString(locale) 7 | } 8 | -------------------------------------------------------------------------------- /packages/plugin-dates/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createPlugin } from 'leva/plugin' 2 | import { Date } from './Date' 3 | import { sanitize, normalize, format } from './date-plugin' 4 | 5 | export const date = createPlugin({ 6 | sanitize, 7 | format, 8 | normalize, 9 | component: Date, 10 | }) 11 | -------------------------------------------------------------------------------- /packages/plugin-plot/README.md: -------------------------------------------------------------------------------- 1 | ## Leva Plot 2 | 3 | ### Installation 4 | 5 | ```bash 6 | npm i @leva-ui/plugin-plot 7 | ``` 8 | 9 | ### Quick start 10 | 11 | ```jsx 12 | import { useControls } from 'leva' 13 | import { plot } from '@leva-ui/plugin-plot' 14 | 15 | function MyComponent() { 16 | const { y } = useControls({ y: plot({ expression: 'cos(x)', graph: true, boundsX: [-10, 10], boundsY: [0, 100] }) }) 17 | return y(Math.PI) 18 | } 19 | ``` 20 | -------------------------------------------------------------------------------- /packages/plugin-plot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@leva-ui/plugin-plot", 3 | "version": "0.10.0", 4 | "main": "dist/leva-ui-plugin-plot.cjs.js", 5 | "module": "dist/leva-ui-plugin-plot.esm.js", 6 | "types": "dist/leva-ui-plugin-plot.cjs.d.ts", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/pmndrs/leva.git", 11 | "directory": "packages/plugin-plot" 12 | }, 13 | "bugs": "https://github.com/pmndrs/leva/issues", 14 | "peerDependencies": { 15 | "@use-gesture/react": "^10.0.0", 16 | "leva": ">=0.10.0", 17 | "react": "^18.0.0 || ^19.0.0", 18 | "react-dom": "^18.0.0 || ^19.0.0" 19 | }, 20 | "dependencies": { 21 | "mathjs": "^10.1.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/plugin-plot/src/Plot.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Story, Meta } from '@storybook/react' 3 | 4 | import Reset from '../../leva/stories/components/decorator-reset' 5 | import { useControls } from '../../leva/src' 6 | 7 | import { plot } from './index' 8 | 9 | export default { 10 | title: 'Plugins/Plot', 11 | decorators: [Reset], 12 | } as Meta 13 | 14 | const Template: Story<any> = (args) => { 15 | const { y } = useControls({ y: plot(args) }) 16 | return ( 17 | <div> 18 | {[0, 0.5, -1].map((x) => ( 19 | <pre key={x}> 20 | y({x}) = {y(x).toFixed(2)} 21 | </pre> 22 | ))} 23 | </div> 24 | ) 25 | } 26 | 27 | export const DefaultBounds = Template.bind({}) 28 | DefaultBounds.args = { expression: 'x' } 29 | 30 | export const HideGraph = Template.bind({}) 31 | HideGraph.args = { expression: 'x', graph: false } 32 | 33 | export const BoundsX = Template.bind({}) 34 | BoundsX.args = { expression: 'cos(x)', boundsX: [-10, 10] } 35 | 36 | export const BoundsY = Template.bind({}) 37 | BoundsY.args = { expression: 'sin(x) * tan(x)', boundsX: [-10, 10], boundsY: [-1, 1] } 38 | 39 | export const InputAsVariable = () => { 40 | const { y } = useControls({ var: 10, y: plot({ expression: 'cos(x * var)' }) }) 41 | return ( 42 | <div> 43 | {[0, 0.5, -1].map((x) => ( 44 | <pre key={x}> 45 | y({x}) = {y(x).toFixed(2)} 46 | </pre> 47 | ))} 48 | </div> 49 | ) 50 | } 51 | 52 | export const CurveAsVariable = () => { 53 | const { y2 } = useControls({ 54 | var: 10, 55 | y1: plot({ expression: 'cos(x * var)' }), 56 | y2: plot({ expression: 'x * y1' }), 57 | }) 58 | return ( 59 | <div> 60 | {[0, 0.5, -1].map((x) => ( 61 | <pre key={x}> 62 | y2({x}) = {y2(x).toFixed(2)} 63 | </pre> 64 | ))} 65 | </div> 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /packages/plugin-plot/src/Plot.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react' 2 | import { useInputContext, useValues, Components } from 'leva/plugin' 3 | import { PlotCanvas } from './PlotCanvas' 4 | import type { PlotProps } from './plot-types' 5 | import { SyledInnerLabel, Container } from './StyledPlot' 6 | 7 | const { Label, Row, String } = Components 8 | 9 | export function Plot() { 10 | const { label, value, displayValue, settings, onUpdate, onChange, setSettings } = useInputContext<PlotProps>() 11 | 12 | const { graph } = settings 13 | 14 | const scope = useValues(value.__symbols) 15 | const displayRef = useRef(displayValue) 16 | displayRef.current = displayValue 17 | 18 | useEffect(() => { 19 | // recomputes when scope which holds the values of the symbols change 20 | onUpdate(displayRef.current) 21 | }, [scope, onUpdate]) 22 | 23 | return ( 24 | <> 25 | {graph && ( 26 | <Row> 27 | <PlotCanvas value={value} settings={settings} /> 28 | </Row> 29 | )} 30 | <Row input> 31 | <Label>{label}</Label> 32 | <Container> 33 | <SyledInnerLabel graph={graph} onClick={() => setSettings({ graph: !graph })}> 34 | 𝑓 35 | </SyledInnerLabel> 36 | <String displayValue={displayValue} onUpdate={onUpdate} onChange={onChange} /> 37 | </Container> 38 | </Row> 39 | </> 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /packages/plugin-plot/src/StyledPlot.ts: -------------------------------------------------------------------------------- 1 | import { styled } from 'leva/plugin' 2 | 3 | export const Wrapper = styled('div', { 4 | position: 'relative', 5 | height: 80, 6 | width: '100%', 7 | marginBottom: '$sm', 8 | }) 9 | 10 | export const ToolTip = styled('div', { 11 | position: 'absolute', 12 | top: -4, 13 | pointerEvents: 'none', 14 | fontFamily: '$mono', 15 | fontSize: 'calc($fontSizes$root * 0.9)', 16 | padding: '$xs $sm', 17 | color: '$toolTipBackground', 18 | backgroundColor: '$toolTipText', 19 | borderRadius: '$xs', 20 | whiteSpace: 'nowrap', 21 | transform: 'translate(-50%, -100%)', 22 | boxShadow: '$level2', 23 | }) 24 | 25 | export const Canvas = styled('canvas', { 26 | height: '100%', 27 | width: '100%', 28 | }) 29 | 30 | export const Dot = styled('div', { 31 | position: 'absolute', 32 | height: 6, 33 | width: 6, 34 | borderRadius: 6, 35 | backgroundColor: '$highlight3', 36 | pointerEvents: 'none', 37 | }) 38 | 39 | export const SyledInnerLabel = styled('div', { 40 | userSelect: 'none', 41 | $flexCenter: '', 42 | height: 14, 43 | width: 14, 44 | borderRadius: 7, 45 | marginRight: '$sm', 46 | cursor: 'pointer', 47 | fontSize: '0.8em', 48 | variants: { 49 | graph: { true: { backgroundColor: '$elevation1' } }, 50 | }, 51 | }) 52 | 53 | export const Container = styled('div', { 54 | display: 'grid', 55 | gridTemplateColumns: 'auto 1fr', 56 | alignItems: 'center', 57 | }) 58 | -------------------------------------------------------------------------------- /packages/plugin-plot/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createPlugin } from 'leva/plugin' 2 | import { Plot } from './Plot' 3 | import { normalize, sanitize, format } from './plot-plugin' 4 | 5 | export const plot = createPlugin({ 6 | normalize, 7 | sanitize, 8 | format, 9 | component: Plot, 10 | }) 11 | -------------------------------------------------------------------------------- /packages/plugin-plot/src/plot-plugin.ts: -------------------------------------------------------------------------------- 1 | import { Data, StoreType } from 'packages/leva/src/types' 2 | import * as math from 'mathjs' 3 | import { parseExpression } from './plot-utils' 4 | import type { PlotInput, InternalPlot, InternalPlotSettings } from './plot-types' 5 | 6 | export const sanitize = ( 7 | expression: string, 8 | _settings: InternalPlotSettings, 9 | _prevValue: math.MathNode, 10 | _path: string, 11 | store: StoreType 12 | ) => { 13 | if (expression === '') throw Error('Empty mathjs expression') 14 | try { 15 | return parseExpression(expression, store.get) 16 | } catch (e) { 17 | throw Error('Invalid mathjs expression string') 18 | } 19 | } 20 | 21 | export const format = (value: InternalPlot) => { 22 | return value.__parsed.toString() 23 | } 24 | 25 | const defaultSettings = { boundsX: [-1, 1], boundsY: [-Infinity, Infinity], graph: true } 26 | 27 | export const normalize = ({ expression, ..._settings }: PlotInput, _path: string, data: Data) => { 28 | const get = (path: string) => { 29 | // @ts-expect-error 30 | if ('value' in data[path]) return data[path].value 31 | return undefined // TODO should throw 32 | } 33 | const value = parseExpression(expression, get) as (v: number) => any 34 | const settings = { ...defaultSettings, ..._settings } 35 | return { value, settings: settings as InternalPlotSettings } 36 | } 37 | -------------------------------------------------------------------------------- /packages/plugin-plot/src/plot-types.ts: -------------------------------------------------------------------------------- 1 | import type { LevaInputProps } from 'leva/plugin' 2 | 3 | export type Plot = { expression: string } 4 | export type PlotSettings = { boundsX?: [number, number]; boundsY?: [number, number]; graph?: boolean } 5 | export type PlotInput = Plot & PlotSettings 6 | 7 | export type InternalPlot = { 8 | (v: number): any 9 | __parsedScoped: math.MathNode 10 | __parsed: math.MathNode 11 | __symbols: string[] 12 | } 13 | 14 | export type InternalPlotSettings = Required<PlotSettings> 15 | 16 | export type PlotProps = LevaInputProps<InternalPlot, InternalPlotSettings, string> 17 | -------------------------------------------------------------------------------- /packages/plugin-plot/src/plot-utils.ts: -------------------------------------------------------------------------------- 1 | import * as math from 'mathjs' 2 | 3 | export function getSymbols(expr: math.MathNode) { 4 | return expr 5 | .filter((node) => { 6 | if (node instanceof math.SymbolNode && node.isSymbolNode) { 7 | try { 8 | const e = node.evaluate() 9 | return !!e.units 10 | } catch { 11 | return node.name !== 'x' 12 | } 13 | } 14 | return false 15 | }) 16 | .map((node: unknown) => (node as math.SymbolNode).name) 17 | } 18 | 19 | export function parseExpression(expression: string, get: (path: string) => any) { 20 | const parsed = math.parse(expression) 21 | const symbols = getSymbols(parsed) 22 | const scope = symbols.reduce((acc, path) => { 23 | const symbol = get(path) 24 | if (!symbol) throw Error(`Invalid symbol at path \`${path}\``) 25 | return Object.assign(acc, { [path]: symbol }) 26 | }, {} as { [key in keyof typeof symbols]: any }) 27 | 28 | let _formattedString = parsed.toString() 29 | 30 | for (let key in scope) { 31 | const re = new RegExp(`\\b${key}\\b`, 'g') 32 | // TODO check type better than this 33 | const s = typeof scope[key] === 'function' ? scope[key].__parsedScoped.toString() : scope[key] 34 | _formattedString = _formattedString.replace(re, s) 35 | } 36 | 37 | const parsedScoped = math.parse(_formattedString) 38 | const compiled = parsedScoped.compile() 39 | 40 | function expr(v: number) { 41 | return compiled.evaluate({ x: v }) 42 | } 43 | 44 | Object.assign(expr, { 45 | __parsedScoped: parsedScoped, 46 | __parsed: parsed, 47 | __symbols: symbols, 48 | }) 49 | 50 | return expr 51 | } 52 | -------------------------------------------------------------------------------- /packages/plugin-spring/README.md: -------------------------------------------------------------------------------- 1 | ## Leva Spring 2 | 3 | ### Installation 4 | 5 | ```bash 6 | npm i @leva-ui/plugin-spring 7 | ``` 8 | 9 | ### Quick start 10 | 11 | ```jsx 12 | import { useControls } from 'leva' 13 | import { spring } from '@leva-ui/plugin-spring' 14 | 15 | function MyComponent() { 16 | const { mySpring } = useControls({ mySpring: spring({ tension: 100, friction: 30, mass: 1 }) }) 17 | return mySpring.toString() 18 | } 19 | ``` 20 | -------------------------------------------------------------------------------- /packages/plugin-spring/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@leva-ui/plugin-spring", 3 | "version": "0.10.0", 4 | "main": "dist/leva-ui-plugin-spring.cjs.js", 5 | "module": "dist/leva-ui-plugin-spring.esm.js", 6 | "types": "dist/leva-ui-plugin-spring.cjs.d.ts", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/pmndrs/leva.git", 11 | "directory": "packages/plugin-spring" 12 | }, 13 | "bugs": "https://github.com/pmndrs/leva/issues", 14 | "peerDependencies": { 15 | "leva": ">=0.10.0", 16 | "react": "^18.0.0 || ^19.0.0", 17 | "react-dom": "^18.0.0 || ^19.0.0" 18 | }, 19 | "dependencies": { 20 | "@react-spring/web": "9.4.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/plugin-spring/src/Spring.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Story, Meta } from '@storybook/react' 3 | 4 | import Reset from '../../leva/stories/components/decorator-reset' 5 | import { useControls } from '../../leva/src' 6 | 7 | import { spring } from './index' 8 | 9 | export default { 10 | title: 'Plugins/Spring', 11 | decorators: [Reset], 12 | } as Meta 13 | 14 | const Template: Story<any> = (args) => { 15 | const values = useControls( 16 | { 17 | bar: spring({ tension: 100, friction: 30 }), 18 | }, 19 | args 20 | ) 21 | 22 | return ( 23 | <div> 24 | <pre>{JSON.stringify(values, null, ' ')}</pre> 25 | </div> 26 | ) 27 | } 28 | 29 | export const Spring = Template.bind({}) 30 | Spring.args = {} 31 | -------------------------------------------------------------------------------- /packages/plugin-spring/src/Spring.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useInputContext, Components } from 'leva/plugin' 3 | import { SpringCanvas } from './SpringCanvas' 4 | import type { SpringProps } from './spring-types' 5 | 6 | const { Row, Label, Vector } = Components 7 | 8 | export function Spring() { 9 | const { label, displayValue, onUpdate, settings } = useInputContext<SpringProps>() 10 | 11 | return ( 12 | <> 13 | <Row> 14 | <SpringCanvas /> 15 | </Row> 16 | <Row input> 17 | <Label>{label}</Label> 18 | <Vector value={displayValue} settings={settings} onUpdate={onUpdate} /> 19 | </Row> 20 | </> 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /packages/plugin-spring/src/StyledSpring.ts: -------------------------------------------------------------------------------- 1 | import { styled } from 'leva/plugin' 2 | 3 | export const Canvas = styled('canvas', { 4 | height: 80, 5 | width: '100%', 6 | cursor: 'crosshair', 7 | display: 'block', 8 | $draggable: '', 9 | }) 10 | 11 | export const SpringPreview = styled('div', { 12 | position: 'relative', 13 | top: -2, 14 | backgroundColor: '$accent2', 15 | width: '100%', 16 | height: 2, 17 | opacity: 0.2, 18 | borderRadius: 1, 19 | transition: 'opacity 350ms ease', 20 | transformOrigin: 'left', 21 | }) 22 | -------------------------------------------------------------------------------- /packages/plugin-spring/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createPlugin } from 'leva/plugin' 2 | import { Spring } from './Spring' 3 | import { normalize, sanitize } from './spring-plugin' 4 | 5 | export const spring = createPlugin({ 6 | normalize, 7 | sanitize, 8 | component: Spring, 9 | }) 10 | -------------------------------------------------------------------------------- /packages/plugin-spring/src/math.ts: -------------------------------------------------------------------------------- 1 | export function springFn(tension: number, friction: number, mass = 1) { 2 | const w0 = Math.sqrt(tension / mass) / 1000 // angular frequency in rad/ms 3 | const zeta = friction / (2 * Math.sqrt(tension * mass)) // damping ratio 4 | 5 | const w1 = w0 * Math.sqrt(1.0 - zeta * zeta) // exponential decay 6 | const w2 = w0 * Math.sqrt(zeta * zeta - 1.0) // frequency of damped oscillation 7 | 8 | const v_0 = 0 9 | 10 | const to = 1 11 | const from = 0 12 | const x_0 = to - from 13 | 14 | if (zeta < 1) { 15 | // Under damped 16 | return (t: number) => 17 | to - Math.exp(-zeta * w0 * t) * (((-v_0 + zeta * w0 * x_0) / w1) * Math.sin(w1 * t) + x_0 * Math.cos(w1 * t)) 18 | } else if (zeta === 1) { 19 | // Critically damped 20 | return (t: number) => to - Math.exp(-w0 * t) * (x_0 + (-v_0 + w0 * x_0) * t) 21 | } else { 22 | // Overdamped 23 | return (t: number) => 24 | to - 25 | (Math.exp(-zeta * w0 * t) * ((-v_0 + zeta * w0 * x_0) * Math.sinh(w2 * t) + w2 * x_0 * Math.cosh(w2 * t))) / w2 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/plugin-spring/src/spring-plugin.ts: -------------------------------------------------------------------------------- 1 | import { normalizeVector, sanitizeVector } from 'leva/plugin' 2 | import type { InternalSpring, InternalSpringSettings, SpringInput } from './spring-types' 3 | 4 | const defaultTensionSettings = { min: 1, step: 1 } 5 | const defaultFrictionSettings = { min: 1, step: 0.5 } 6 | const defaultMassSettings = { min: 0.1, step: 0.1 } 7 | const defaultValue = { tension: 100, friction: 30, mass: 1 } 8 | 9 | export const normalize = (input: SpringInput = {}) => { 10 | const { value: _value, ..._settings } = 'value' in input ? input : { value: input } 11 | const mergedSettings = { 12 | tension: { ...defaultTensionSettings, ..._settings.tension }, 13 | friction: { ...defaultFrictionSettings, ..._settings.friction }, 14 | mass: { ...defaultMassSettings, ..._settings.mass }, 15 | } 16 | 17 | const { value, settings } = normalizeVector({ ...defaultValue, ..._value }, mergedSettings) 18 | return { value, settings: settings as InternalSpringSettings } 19 | } 20 | 21 | export const sanitize = (value: InternalSpring, settings: InternalSpringSettings, prevValue?: any) => 22 | sanitizeVector(value, settings, prevValue) 23 | -------------------------------------------------------------------------------- /packages/plugin-spring/src/spring-types.ts: -------------------------------------------------------------------------------- 1 | import type { InputWithSettings, NumberSettings, LevaInputProps, InternalVectorSettings } from 'leva/plugin' 2 | 3 | export type Spring = { tension?: number; friction?: number; mass?: number } 4 | export type InternalSpring = { tension: number; friction: number; mass: number } 5 | export type SpringSettings = { [key in keyof Spring]?: NumberSettings } 6 | 7 | export type SpringInput = Spring | InputWithSettings<Spring, SpringSettings> 8 | 9 | export type InternalSpringSettings = InternalVectorSettings<keyof InternalSpring, (keyof InternalSpring)[], 'object'> 10 | 11 | export type SpringProps = LevaInputProps<InternalSpring, InternalSpringSettings, InternalSpring> 12 | --------------------------------------------------------------------------------