├── .husky ├── .gitignore └── pre-commit ├── packages ├── leva │ ├── .npmignore │ ├── README.md │ ├── src │ │ ├── components │ │ │ ├── Button │ │ │ │ ├── index.ts │ │ │ │ ├── StyledButton.ts │ │ │ │ └── Button.tsx │ │ │ ├── Control │ │ │ │ ├── index.ts │ │ │ │ ├── Control.tsx │ │ │ │ └── ControlInput.tsx │ │ │ ├── Monitor │ │ │ │ ├── index.ts │ │ │ │ └── StyledMonitor.ts │ │ │ ├── ButtonGroup │ │ │ │ ├── index.ts │ │ │ │ ├── StyledButtonGroup.tsx │ │ │ │ ├── StyledButtonGroupButton.ts │ │ │ │ └── ButtonGroup.tsx │ │ │ ├── ValueInput │ │ │ │ ├── index.ts │ │ │ │ └── StyledInput.ts │ │ │ ├── Leva │ │ │ │ ├── index.ts │ │ │ │ ├── tree.ts │ │ │ │ ├── LevaPanel.tsx │ │ │ │ ├── StyledRoot.ts │ │ │ │ └── StyledFilter.ts │ │ │ ├── Folder │ │ │ │ ├── index.ts │ │ │ │ └── FolderTitle.tsx │ │ │ └── UI │ │ │ │ ├── index.ts │ │ │ │ ├── Row.tsx │ │ │ │ ├── Misc.tsx │ │ │ │ └── Chevron.tsx │ │ ├── plugins │ │ │ ├── Vector │ │ │ │ ├── index.ts │ │ │ │ └── vector-utils.ts │ │ │ ├── Boolean │ │ │ │ ├── boolean-types.ts │ │ │ │ ├── boolean-plugin.ts │ │ │ │ ├── index.ts │ │ │ │ ├── StyledBoolean.ts │ │ │ │ └── Boolean.tsx │ │ │ ├── Image │ │ │ │ ├── image-types.ts │ │ │ │ ├── index.ts │ │ │ │ ├── image-plugin.ts │ │ │ │ ├── Image.tsx │ │ │ │ └── StyledImage.ts │ │ │ ├── Color │ │ │ │ ├── index.ts │ │ │ │ ├── color-types.ts │ │ │ │ ├── StyledColor.ts │ │ │ │ └── color-plugin.ts │ │ │ ├── Select │ │ │ │ ├── index.ts │ │ │ │ ├── select-types.ts │ │ │ │ ├── StyledSelect.ts │ │ │ │ ├── select-plugin.ts │ │ │ │ └── Select.tsx │ │ │ ├── String │ │ │ │ ├── index.ts │ │ │ │ ├── string-types.ts │ │ │ │ ├── string-plugin.ts │ │ │ │ └── String.tsx │ │ │ ├── Interval │ │ │ │ ├── index.ts │ │ │ │ ├── interval-types.ts │ │ │ │ └── interval-plugin.ts │ │ │ ├── Vector3d │ │ │ │ ├── index.ts │ │ │ │ ├── vector3d-types.ts │ │ │ │ └── Vector3d.tsx │ │ │ ├── Vector2d │ │ │ │ ├── vector2d-types.ts │ │ │ │ ├── index.ts │ │ │ │ ├── Vector2d.tsx │ │ │ │ └── StyledJoystick.ts │ │ │ └── Number │ │ │ │ ├── StyledNumber.ts │ │ │ │ ├── index.ts │ │ │ │ ├── number-types.ts │ │ │ │ ├── StyledRange.ts │ │ │ │ └── RangeSlider.tsx │ │ ├── types │ │ │ ├── index.ts │ │ │ ├── utils.ts │ │ │ └── v8n.d.ts │ │ ├── utils │ │ │ ├── event.ts │ │ │ ├── index.ts │ │ │ ├── path.ts │ │ │ ├── fn.ts │ │ │ ├── object.ts │ │ │ └── react.ts │ │ ├── helpers │ │ │ ├── index.ts │ │ │ ├── buttonGroup.ts │ │ │ ├── monitor.ts │ │ │ ├── folder.ts │ │ │ └── button.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ └── useCanvas.ts │ │ ├── context.tsx │ │ ├── styles │ │ │ └── index.ts │ │ ├── eventEmitter.ts │ │ ├── index.ts │ │ ├── plugin │ │ │ └── index.ts │ │ └── headless │ │ │ └── index.ts │ ├── headless │ │ └── package.json │ ├── plugin │ │ └── package.json │ ├── stories │ │ ├── components │ │ │ └── decorator-reset.tsx │ │ ├── inputs │ │ │ ├── Image.stories.tsx │ │ │ ├── Interval.stories.tsx │ │ │ ├── Button.stories.tsx │ │ │ ├── Select.stories.tsx │ │ │ ├── Vector.stories.tsx │ │ │ └── Color.stories.tsx │ │ ├── advanced │ │ │ ├── Minimal.stories.tsx │ │ │ ├── Transient.stories.tsx │ │ │ ├── AdvancedPanels.stories.tsx │ │ │ ├── Scroll.stories.tsx │ │ │ └── CustomPlugin.stories.tsx │ │ └── caching.stories.tsx │ └── package.json ├── plugin-dates │ ├── src │ │ ├── date-utils.ts │ │ ├── index.ts │ │ ├── date-types.ts │ │ ├── date-plugin.ts │ │ ├── Date.stories.tsx │ │ ├── StyledDate.ts │ │ └── Date.tsx │ ├── README.md │ ├── CHANGELOG.md │ └── package.json ├── plugin-spring │ ├── src │ │ ├── index.ts │ │ ├── StyledSpring.ts │ │ ├── Spring.tsx │ │ ├── spring-types.ts │ │ ├── Spring.stories.tsx │ │ ├── math.ts │ │ └── spring-plugin.ts │ ├── README.md │ └── package.json ├── plugin-plot │ ├── src │ │ ├── index.ts │ │ ├── plot-types.ts │ │ ├── plot-plugin.ts │ │ ├── Plot.tsx │ │ ├── StyledPlot.ts │ │ ├── plot-utils.ts │ │ └── Plot.stories.tsx │ ├── README.md │ └── package.json └── plugin-bezier │ ├── src │ ├── Bezier.stories.css │ ├── index.ts │ ├── bezier-types.ts │ ├── Bezier.stories.tsx │ ├── BezierPreview.tsx │ ├── StyledBezier.ts │ ├── Bezier.tsx │ └── bezier-plugin.ts │ ├── package.json │ ├── README.md │ └── CHANGELOG.md ├── .eslintignore ├── hero.png ├── .vscode └── settings.json ├── .changeset ├── many-games-eat.md ├── kind-owls-grin.md ├── config.json └── README.md ├── demo ├── serve.json ├── src │ ├── sandboxes │ │ ├── leva-busy │ │ │ ├── src │ │ │ │ ├── styles.module.css │ │ │ │ ├── index.tsx │ │ │ │ └── index.css │ │ │ ├── package.json │ │ │ └── public │ │ │ │ └── index.html │ │ ├── leva-scroll │ │ │ ├── src │ │ │ │ ├── index.jsx │ │ │ │ ├── index.css │ │ │ │ └── App.jsx │ │ │ ├── package.json │ │ │ └── public │ │ │ │ └── index.html │ │ ├── leva-theme │ │ │ ├── src │ │ │ │ ├── index.jsx │ │ │ │ └── index.css │ │ │ ├── package.json │ │ │ └── public │ │ │ │ └── index.html │ │ ├── leva-ui │ │ │ ├── src │ │ │ │ ├── index.jsx │ │ │ │ ├── index.css │ │ │ │ └── styles.css │ │ │ ├── package.json │ │ │ └── public │ │ │ │ └── index.html │ │ ├── leva-minimal │ │ │ ├── src │ │ │ │ ├── index.jsx │ │ │ │ ├── index.css │ │ │ │ └── App.jsx │ │ │ ├── package.json │ │ │ └── public │ │ │ │ └── index.html │ │ ├── leva-plugin-plot │ │ │ ├── src │ │ │ │ ├── index.tsx │ │ │ │ ├── index.css │ │ │ │ └── App.tsx │ │ │ ├── package.json │ │ │ └── public │ │ │ │ └── index.html │ │ ├── leva-transient │ │ │ ├── src │ │ │ │ ├── index.jsx │ │ │ │ ├── index.css │ │ │ │ └── App.jsx │ │ │ ├── package.json │ │ │ └── public │ │ │ │ └── index.html │ │ ├── leva-advanced-panels │ │ │ ├── src │ │ │ │ ├── index.jsx │ │ │ │ ├── index.css │ │ │ │ └── App.jsx │ │ │ ├── package.json │ │ │ └── public │ │ │ │ └── index.html │ │ ├── leva-custom-plugin │ │ │ ├── src │ │ │ │ ├── index.tsx │ │ │ │ ├── index.css │ │ │ │ └── App.tsx │ │ │ ├── package.json │ │ │ └── public │ │ │ │ └── index.html │ │ ├── leva-plugin-bezier │ │ │ ├── src │ │ │ │ ├── index.tsx │ │ │ │ ├── style.css │ │ │ │ ├── index.css │ │ │ │ └── App.tsx │ │ │ ├── package.json │ │ │ └── public │ │ │ │ └── index.html │ │ ├── leva-plugin-dates │ │ │ ├── src │ │ │ │ ├── index.tsx │ │ │ │ ├── index.css │ │ │ │ └── App.tsx │ │ │ ├── package.json │ │ │ └── public │ │ │ │ └── index.html │ │ └── leva-plugin-spring │ │ │ ├── src │ │ │ ├── index.tsx │ │ │ ├── index.css │ │ │ └── App.tsx │ │ │ ├── package.json │ │ │ └── public │ │ │ └── index.html │ ├── index.jsx │ ├── index.css │ └── styles.module.css ├── vite.config.js ├── index.html ├── tsconfig.json └── package.json ├── docs ├── advanced │ └── circle-drag.gif └── getting-started │ └── special-inputs.md ├── .eslintrc.json ├── vitest.setup.ts ├── contributing.md ├── pnpm-workspace.yaml ├── .prettierignore ├── babel.config.js ├── vitest.config.ts ├── .yarnrc.yml ├── .storybook └── preview.js ├── .gitignore ├── .codesandbox └── ci.json ├── .github └── workflows │ ├── chromatic.yml │ ├── docs.yml │ ├── release.yml │ └── main.yml └── LICENSE /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /packages/leva/.npmignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/leva/README.md: -------------------------------------------------------------------------------- 1 | ../../README.md -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | .yarn/ -------------------------------------------------------------------------------- /hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/leva/HEAD/hero.png -------------------------------------------------------------------------------- /packages/leva/src/components/Button/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Button' 2 | -------------------------------------------------------------------------------- /packages/leva/src/components/Control/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Control' 2 | -------------------------------------------------------------------------------- /packages/leva/src/components/Monitor/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Monitor' 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /packages/leva/src/components/ButtonGroup/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ButtonGroup' 2 | -------------------------------------------------------------------------------- /packages/leva/src/components/ValueInput/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ValueInput' 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint 5 | -------------------------------------------------------------------------------- /.changeset/many-games-eat.md: -------------------------------------------------------------------------------- 1 | --- 2 | 'leva': patch 3 | --- 4 | 5 | Export RangeSlider 6 | -------------------------------------------------------------------------------- /demo/serve.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [{ "source": "/*", "destination": "/index.html" }] 3 | } 4 | -------------------------------------------------------------------------------- /packages/leva/src/components/Leva/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Leva' 2 | export * from './LevaPanel' 3 | -------------------------------------------------------------------------------- /docs/advanced/circle-drag.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/leva/HEAD/docs/advanced/circle-drag.gif -------------------------------------------------------------------------------- /packages/leva/src/components/Folder/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Folder' 2 | export * from './FolderTitle' 3 | -------------------------------------------------------------------------------- /packages/leva/src/plugins/Vector/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Vector' 2 | export * from './vector-plugin' 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["react-app"], 3 | "rules": { 4 | "no-console": "warn" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.changeset/kind-owls-grin.md: -------------------------------------------------------------------------------- 1 | --- 2 | "leva": minor 3 | "demo": patch 4 | --- 5 | 6 | feat(headless): add headless mode 7 | -------------------------------------------------------------------------------- /packages/leva/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './internal' 2 | export * from './public' 3 | export * from './utils' 4 | -------------------------------------------------------------------------------- /packages/leva/src/utils/event.ts: -------------------------------------------------------------------------------- 1 | export const multiplyStep = (event: any) => (event.shiftKey ? 5 : event.altKey ? 1 / 5 : 1) 2 | -------------------------------------------------------------------------------- /packages/leva/headless/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "dist/leva-headless.cjs.js", 3 | "module": "dist/leva-headless.esm.js" 4 | } 5 | -------------------------------------------------------------------------------- /vitest.setup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Vitest setup file 3 | * Configures the testing environment 4 | */ 5 | 6 | import '@testing-library/jest-dom' 7 | -------------------------------------------------------------------------------- /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/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './folder' 2 | export * from './button' 3 | export * from './buttonGroup' 4 | export * from './monitor' 5 | -------------------------------------------------------------------------------- /packages/leva/src/plugins/Boolean/boolean-types.ts: -------------------------------------------------------------------------------- 1 | import type { LevaInputProps } from '../../types' 2 | 3 | export type BooleanProps = LevaInputProps 4 | -------------------------------------------------------------------------------- /packages/leva/plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "dist/leva-plugin.cjs.js", 3 | "module": "dist/leva-plugin.esm.js", 4 | "types": "dist/leva-plugin.cjs.d.ts" 5 | } 6 | -------------------------------------------------------------------------------- /packages/leva/src/plugins/Image/image-types.ts: -------------------------------------------------------------------------------- 1 | import type { LevaInputProps } from '../../types' 2 | 3 | export type ImageProps = LevaInputProps 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/getting-started/special-inputs.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Special Inputs 3 | description: 4 | nav: 0 5 | --- 6 | 7 | # Special Inputs 8 | 9 | ### Button 10 | 11 | ### Monitor 12 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | # all packages in subdirs of packages/ 3 | - 'packages/*' 4 | - 'demo' 5 | 6 | catalog: 7 | typescript: ^5.7.2 8 | '@use-gesture/react': ^10.3.1 9 | -------------------------------------------------------------------------------- /.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 | pnpm-lock.yaml -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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/plugins/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/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/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-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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/plugins/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/plugins/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/plugins/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/plugins/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/plugins/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/leva/src/plugins/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 | -------------------------------------------------------------------------------- /demo/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const rootElement = document.getElementById('root') 8 | const root = createRoot(rootElement) 9 | root.render( 10 | 11 | 12 | 13 | ) 14 | -------------------------------------------------------------------------------- /packages/leva/src/plugins/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 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-busy/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const rootElement = document.getElementById('root') 8 | const root = createRoot(rootElement) 9 | root.render( 10 | 11 | 12 | 13 | ) 14 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-scroll/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const rootElement = document.getElementById('root') 8 | const root = createRoot(rootElement) 9 | root.render( 10 | 11 | 12 | 13 | ) 14 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-theme/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const rootElement = document.getElementById('root') 8 | const root = createRoot(rootElement) 9 | root.render( 10 | 11 | 12 | 13 | ) 14 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-ui/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const rootElement = document.getElementById('root') 8 | const root = createRoot(rootElement) 9 | root.render( 10 | 11 | 12 | 13 | ) 14 | -------------------------------------------------------------------------------- /packages/leva/src/components/UI/Row.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StyledRow, StyledInputRow } from './StyledUI' 3 | 4 | type RowProps = React.ComponentProps & { input?: boolean } 5 | 6 | export function Row({ input, ...props }: RowProps) { 7 | if (input) return 8 | return 9 | } 10 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-minimal/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const rootElement = document.getElementById('root') 8 | const root = createRoot(rootElement) 9 | root.render( 10 | 11 | 12 | 13 | ) 14 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-plugin-plot/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const rootElement = document.getElementById('root') 8 | const root = createRoot(rootElement) 9 | root.render( 10 | 11 | 12 | 13 | ) 14 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-transient/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const rootElement = document.getElementById('root') 8 | const root = createRoot(rootElement) 9 | root.render( 10 | 11 | 12 | 13 | ) 14 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | environment: 'jsdom', 7 | setupFiles: ['./vitest.setup.ts'], 8 | include: ['**/*.{test,spec}.{ts,tsx}'], 9 | exclude: ['**/node_modules/**', '**/dist/**', '**/packages/leva/src/types/public.test.ts'], 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-advanced-panels/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const rootElement = document.getElementById('root') 8 | const root = createRoot(rootElement) 9 | root.render( 10 | 11 | 12 | 13 | ) 14 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-custom-plugin/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const rootElement = document.getElementById('root') 8 | const root = createRoot(rootElement) 9 | root.render( 10 | 11 | 12 | 13 | ) 14 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-plugin-bezier/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const rootElement = document.getElementById('root') 8 | const root = createRoot(rootElement) 9 | root.render( 10 | 11 | 12 | 13 | ) 14 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-plugin-dates/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const rootElement = document.getElementById('root') 8 | const root = createRoot(rootElement) 9 | root.render( 10 | 11 | 12 | 13 | ) 14 | -------------------------------------------------------------------------------- /demo/src/sandboxes/leva-plugin-spring/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const rootElement = document.getElementById('root') 8 | const root = createRoot(rootElement) 9 | root.render( 10 | 11 | 12 | 13 | ) 14 | -------------------------------------------------------------------------------- /packages/leva/src/plugins/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 5 | export type Vector3dProps = LevaInputProps 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /packages/leva/src/plugins/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 6 | export type StringProps = LevaInputProps 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/leva/src/plugins/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 & { 5 | joystick: boolean | 'invertY' 6 | } 7 | export type Vector2dProps = LevaInputProps 8 | -------------------------------------------------------------------------------- /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-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-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-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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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-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-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 | -------------------------------------------------------------------------------- /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/plugins/Select/select-types.ts: -------------------------------------------------------------------------------- 1 | import type { LevaInputProps } from '../../types' 2 | 3 | export type SelectSettings = { options: Record | U[] } 4 | export type InternalSelectSettings = { keys: string[]; values: any[] } 5 | 6 | export type SelectInput

= { value?: P } & SelectSettings 7 | 8 | export type SelectProps = LevaInputProps 9 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Sandboxes Leva 8 | 9 | 10 | 11 |

12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /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-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-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-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 | -------------------------------------------------------------------------------- /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 | Function, settings?: MonitorSettings): MonitorInput { 7 | return { type: SpecialInputs.MONITOR, objectOrFn, settings: { ...defaultSettings, ...settings } } 8 | } 9 | -------------------------------------------------------------------------------- /packages/leva/src/plugins/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/stories/components/decorator-reset.tsx: -------------------------------------------------------------------------------- 1 | import type { StoryFn } from '@storybook/react' 2 | import * as React from 'react' 3 | import { levaStore } from '../../src' 4 | 5 | const DefaultStory = (Story: StoryFn) => { 6 | const [_, set] = React.useState(false) 7 | React.useEffect(() => { 8 | levaStore.dispose() 9 | set(true) 10 | }, []) 11 | return _ ? : <> 12 | } 13 | 14 | export default DefaultStory 15 | -------------------------------------------------------------------------------- /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/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/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-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 | -------------------------------------------------------------------------------- /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
{birthday.formattedDate}
15 | } 16 | -------------------------------------------------------------------------------- /packages/leva/src/plugins/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/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(schema: S, settings?: FolderSettings): FolderInput> { 7 | return { 8 | type: SpecialInputs.FOLDER, 9 | schema, 10 | settings: { ...defaultSettings, ...settings }, 11 | } as any 12 | } 13 | -------------------------------------------------------------------------------- /packages/leva/src/plugins/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 9 | 10 | export type ColorProps = LevaInputProps 11 | -------------------------------------------------------------------------------- /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 |
12 |
{JSON.stringify(mySpring, null, '  ')}
13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const parameters = { 4 | actions: { argTypesRegex: "^on[A-Z].*" }, 5 | 6 | "previewTabs": { 7 | 'storybook/docs/panel': { hidden: true } 8 | }, 9 | 10 | options: { 11 | storySort: { 12 | order: ["Inputs", ["String", "Boolean", "Number", "Interval"], "Misc", "Plugins"] 13 | } 14 | }, 15 | 16 | docs: { 17 | codePanel: true 18 | } 19 | } 20 | 21 | export const decorators = [(Story) =>
] 22 | -------------------------------------------------------------------------------- /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 | 11 | {children} 12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /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 |
11 |
12 |
{JSON.stringify(curve, null, '  ')}
13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /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/plugins/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | toggle()}> 14 | 15 |
{name}
16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /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/leva/src/utils/fn.ts: -------------------------------------------------------------------------------- 1 | export const debounce = (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/plugins/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 12 | 13 | export type NumberProps = LevaInputProps 14 | 15 | export type RangeSliderProps = { value: number; onDrag: (v: number) => void } & InternalNumberSettings 16 | -------------------------------------------------------------------------------- /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/plugins/Vector3d/Vector3d.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Vector } from '../Vector' 3 | import { Label, Row } from '../../components/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() 9 | return ( 10 | 11 | 12 | 13 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 15 | 16 | export type PlotProps = LevaInputProps 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | 36 | build-storybook.log -------------------------------------------------------------------------------- /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 10 | 11 | export function Button({ onClick, settings, label }: ButtonProps) { 12 | const store = useStoreContext() 13 | return ( 14 | 15 | onClick(store.get)}> 16 | {label} 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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-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 | -------------------------------------------------------------------------------- /packages/leva/stories/inputs/Image.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StoryFn, Meta } from '@storybook/react' 3 | 4 | import Reset from '../components/decorator-reset' 5 | 6 | import { useControls } from '../../src' 7 | import type { ImageInput } from '../../src/types' 8 | 9 | export default { 10 | title: 'Inputs/Image', 11 | decorators: [Reset], 12 | } as Meta 13 | 14 | const Template: StoryFn = (args) => { 15 | const values = useControls({ foo: args }) 16 | 17 | return
{values.foo && }
18 | } 19 | 20 | export const Image = Template.bind({}) 21 | Image.args = { image: undefined } 22 | -------------------------------------------------------------------------------- /.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": "18" 20 | } 21 | -------------------------------------------------------------------------------- /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() 10 | 11 | return ( 12 | <> 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /packages/leva/src/plugins/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/plugins/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/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 8 | 9 | export type InternalSpringSettings = InternalVectorSettings 10 | 11 | export type SpringProps = LevaInputProps 12 | -------------------------------------------------------------------------------- /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-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-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: , 14 | }, 15 | }) 16 | 17 | return ( 18 | <> 19 | 20 |
{JSON.stringify(data, null, '  ')}
21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /packages/leva/src/plugins/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 14 | 15 | export type IntervalSliderProps = { 16 | value: InternalInterval 17 | onDrag: (v: Partial) => void 18 | } & InternalIntervalSettings 19 | -------------------------------------------------------------------------------- /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 | "@use-gesture/react": "catalog:" 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 | -------------------------------------------------------------------------------- /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): v is { __levaInput: true; path: string } => '__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 | -------------------------------------------------------------------------------- /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": "catalog:" 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 | -------------------------------------------------------------------------------- /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 & { toggled?: boolean }) { 11 | return ( 12 | 19 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /packages/plugin-spring/src/Spring.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StoryFn, Meta } from '@storybook/react' 3 | 4 | // @ts-ignore 5 | import Reset from '../../leva/stories/components/decorator-reset' 6 | import { useControls } from 'leva' 7 | 8 | import { spring } from './index' 9 | 10 | export default { 11 | title: 'Plugins/Spring', 12 | decorators: [Reset], 13 | } as Meta 14 | 15 | const Template: StoryFn> = (args) => { 16 | const values = useControls( 17 | { 18 | bar: spring({ tension: 100, friction: 30 }), 19 | }, 20 | args 21 | ) 22 | 23 | return ( 24 |
25 |
{JSON.stringify(values, null, '  ')}
26 |
27 | ) 28 | } 29 | 30 | export const Spring = Template.bind({}) 31 | Spring.args = {} 32 | -------------------------------------------------------------------------------- /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": "catalog:" 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/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": "catalog:" 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/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": "catalog:" 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/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": "catalog:" 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 | -------------------------------------------------------------------------------- /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 7 | 8 | // TODO: export this upstream 9 | export type DateCalendarContainerProps = React.ComponentProps 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 15 | 16 | export type DateProps = LevaInputProps 17 | -------------------------------------------------------------------------------- /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 6 | 7 | /** 8 | * Functions the same as `` but enables multiple unique panels with their own store. 9 | * 10 | * @example 11 | * const store1 = useCreateStore() 12 | * const store2 = useCreateStore() 13 | * 14 | * return ( 15 | * <> 16 | * 17 | * 18 | * 19 | * ) 20 | */ 21 | export function LevaPanel({ store, ...props }: LevaPanelProps) { 22 | const parentStore = useStoreContext() 23 | const _store = store === undefined ? parentStore : store 24 | return 25 | } 26 | -------------------------------------------------------------------------------- /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": "catalog:" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/leva/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | // ============================================ 2 | // PUBLIC API - For Plugin Development 3 | // ============================================ 4 | export * from './useInput' 5 | export * from './useValue' 6 | export * from './useInputSetters' 7 | export * from './useDrag' 8 | export * from './useTransform' 9 | export * from './useCanvas' 10 | 11 | // ============================================ 12 | // INTERNAL - Store Subscriptions 13 | // ============================================ 14 | export * from './useValuesForPath' 15 | export * from './useVisiblePaths' 16 | 17 | // ============================================ 18 | // INTERNAL - UI/Animation Utilities 19 | // ============================================ 20 | export * from './usePopin' 21 | export * from './useToggle' 22 | export * from './useCompareMemoize' 23 | -------------------------------------------------------------------------------- /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": "catalog:", 16 | "leva": ">=0.10.1", 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 | "devDependencies": { 24 | "@use-gesture/react": "catalog:", 25 | "leva": "workspace:*" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/leva/stories/advanced/Minimal.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Reset from '../components/decorator-reset' 3 | import { Meta } from '@storybook/react' 4 | import { useControls, Leva } from '../../src' 5 | 6 | export default { 7 | title: 'Advanced/Minimal', 8 | decorators: [Reset], 9 | } as Meta 10 | 11 | export const Default = () => { 12 | const data = useControls({ 13 | number: 10, 14 | minmax: { value: 12.5, min: 5.5, max: 30.5, optional: true }, 15 | printSize: { value: 100, min: 80, max: 140, step: 10 }, 16 | color: { 17 | value: '#f00', 18 | hint: 'Hey, we support icons and hinting values and long text will wrap!', 19 | }, 20 | }) 21 | 22 | return ( 23 | <> 24 | 25 |
{JSON.stringify(data, null, '  ')}
26 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /packages/leva/stories/inputs/Interval.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StoryFn, 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: StoryFn = (args) => { 14 | const values = useControls({ 15 | foo: args, 16 | }) 17 | 18 | return ( 19 |
20 |
{JSON.stringify(values, null, '  ')}
21 |
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/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 | "@use-gesture/react": "catalog:", 16 | "leva": ">=0.10.1", 17 | "react": "^18.0.0 || ^19.0.0", 18 | "react-dom": "^18.0.0 || ^19.0.0" 19 | }, 20 | "dependencies": { 21 | "@react-spring/web": "9.4.2" 22 | }, 23 | "devDependencies": { 24 | "@use-gesture/react": "catalog:", 25 | "leva": "workspace:*" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /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 | "@use-gesture/react": "catalog:", 16 | "leva": ">=0.10.1", 17 | "react": "^18.0.0 || ^19.0.0", 18 | "react-dom": "^18.0.0 || ^19.0.0" 19 | }, 20 | "dependencies": { 21 | "react-use-measure": "^2.1.1" 22 | }, 23 | "devDependencies": { 24 | "@use-gesture/react": "catalog:", 25 | "leva": "workspace:*" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /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
29 | } 30 | ``` 31 | -------------------------------------------------------------------------------- /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": "catalog:", 16 | "leva": ">=0.10.1", 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 | "@use-gesture/react": "catalog:", 26 | "leva": "workspace:*" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/leva/src/plugins/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/stories/caching.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Reset from './components/decorator-reset' 3 | import { StoryFn, Meta } from '@storybook/react' 4 | 5 | import { useControls } from '../src' 6 | 7 | export default { 8 | title: 'Dev/Hook/Caching', 9 | decorators: [Reset], 10 | } as Meta 11 | 12 | const Controls = () => { 13 | const values = useControls({ num: 10, color: '#f00' }) 14 | 15 | return ( 16 |
17 |
{JSON.stringify(values, null, '  ')}
18 |
19 | ) 20 | } 21 | 22 | /** 23 | * The hook should keep the state even after unmounting/remounting 24 | */ 25 | const Template: StoryFn = () => { 26 | const [mounted, toggle] = React.useState(true) 27 | return ( 28 |
29 | 30 | {mounted && } 31 |
32 | ) 33 | } 34 | 35 | export const Caching = Template.bind({}) 36 | -------------------------------------------------------------------------------- /packages/leva/src/utils/object.ts: -------------------------------------------------------------------------------- 1 | export function pick(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(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(value: U[], keys: K[]): Record { 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/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 | -------------------------------------------------------------------------------- /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 |
24 | 25 | 26 | 27 | 28 | 29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /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 | 15 | 16 | 17 | ) 18 | } 19 | 20 | export default function App() { 21 | return ( 22 | 26 | 27 | 28 | 29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /packages/leva/stories/inputs/Button.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Meta } from '@storybook/react' 3 | import { expect, within } from 'storybook/test' 4 | 5 | import Reset from '../components/decorator-reset' 6 | 7 | import { useControls, button } from '../../src' 8 | 9 | export default { 10 | title: 'Inputs/Button', 11 | decorators: [Reset], 12 | } as Meta 13 | 14 | export const Button = () => { 15 | const values = useControls({ 16 | number: 3, 17 | foo: button((get) => alert(`Number value is ${get('number').toFixed(2)}`)), 18 | }) 19 | 20 | return ( 21 |
22 |
{JSON.stringify(values, null, '  ')}
23 |
24 | ) 25 | } 26 | 27 | export const DisabledButton = () => { 28 | const values = useControls({ 29 | number: 3, 30 | foo: button((get) => alert(`Number value is ${get('number')}`), { disabled: true }), 31 | }) 32 | 33 | return ( 34 |
35 |
{JSON.stringify(values, null, '  ')}
36 |
37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/chromatic.yml: -------------------------------------------------------------------------------- 1 | name: Chromatic 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | chromatic: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Setup pnpm 21 | uses: pnpm/action-setup@v2 22 | with: 23 | version: 9 24 | 25 | - name: Setup Node.js 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: '20' 29 | cache: 'pnpm' 30 | 31 | - name: Install dependencies 32 | run: pnpm install --frozen-lockfile 33 | 34 | - name: Run Chromatic 35 | uses: chromaui/action@v1 36 | with: 37 | projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} 38 | buildScriptName: build-storybook 39 | token: ${{ secrets.GITHUB_TOKEN }} 40 | exitZeroOnChanges: true 41 | 42 | -------------------------------------------------------------------------------- /packages/leva/stories/advanced/Transient.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Reset from '../components/decorator-reset' 3 | import { Meta } from '@storybook/react' 4 | import { useControls } from '../../src' 5 | 6 | export default { 7 | title: 'Advanced/Transient', 8 | decorators: [Reset], 9 | } as Meta 10 | 11 | export const Default = () => { 12 | const [color, setColor] = React.useState('indianred') 13 | useControls({ 14 | color: { 15 | value: 'indianred', 16 | onChange: (v) => setColor(v), 17 | }, 18 | }) 19 | 20 | return ( 21 |
30 |
39 |
40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 |
32 |
y1(1) = {t1}
33 |
y2(1) = {t2}
34 |
y3(1) = {t3}
35 |
36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /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 | "@leva-ui/plugin-bezier": "workspace:*", 12 | "@leva-ui/plugin-dates": "workspace:*", 13 | "@leva-ui/plugin-plot": "workspace:*", 14 | "@leva-ui/plugin-spring": "workspace:*", 15 | "@radix-ui/react-icons": "^1.0.3", 16 | "@react-three/drei": "^8.8.3", 17 | "@react-three/fiber": "^7.0.26", 18 | "@stitches/react": "1.2.8", 19 | "@use-gesture/react": "catalog:", 20 | "leva": "workspace:*", 21 | "noisejs": "^2.1.0", 22 | "react": "^18.0.0", 23 | "react-dom": "^18.0.0", 24 | "react-dropzone": "^12.0.0", 25 | "react-use": "^17.3.2", 26 | "three": "^0.143.0", 27 | "wouter": "^2.7.5" 28 | }, 29 | "devDependencies": { 30 | "@types/react": "^18.0.0", 31 | "@types/react-dom": "^18.0.0", 32 | "@vitejs/plugin-react-refresh": "^1.3.6", 33 | "typescript": "catalog:", 34 | "vite": "2.7.13" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /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 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 17 | : never 18 | : T 19 | 20 | // adds Iterator to the return type in case it has any 21 | type GetIterator = T extends { [Symbol.iterator]: infer U } ? { [Symbol.iterator]: U } : {} 22 | 23 | export type UnionToIntersection = (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 extends Record ? K : never 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/leva/src/plugins/Vector2d/Vector2d.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { styled } from '../../styles' 3 | import { Vector } from '../Vector' 4 | import { Label, Row } from '../../components/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() 22 | return ( 23 | 24 | 25 | 26 | {settings.joystick && } 27 | 28 | 29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /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/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() { 8 | return useContext(InputContext) as InputContextProps & T 9 | } 10 | 11 | type ThemeContextProps = { theme: FullTheme; className: string } 12 | 13 | export const ThemeContext = createContext(null) 14 | 15 | export const StoreContext = createContext(null) 16 | 17 | export const PanelSettingsContext = createContext(null) 18 | 19 | export function useStoreContext() { 20 | return useContext(StoreContext)! 21 | } 22 | 23 | export function usePanelSettingsContext() { 24 | return useContext(PanelSettingsContext)! 25 | } 26 | 27 | type LevaStoreProviderProps = { 28 | children: React.ReactNode 29 | store: StoreType 30 | } 31 | 32 | export function LevaStoreProvider({ children, store }: LevaStoreProviderProps) { 33 | return {children} 34 | } 35 | -------------------------------------------------------------------------------- /packages/leva/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leva", 3 | "version": "0.10.1", 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 | "headless/index.ts" 19 | ] 20 | }, 21 | "peerDependencies": { 22 | "react": "^18.0.0 || ^19.0.0", 23 | "react-dom": "^18.0.0 || ^19.0.0" 24 | }, 25 | "dependencies": { 26 | "@radix-ui/react-portal": "^1.1.4", 27 | "@radix-ui/react-tooltip": "^1.1.8", 28 | "@stitches/react": "^1.2.8", 29 | "@use-gesture/react": "catalog:", 30 | "colord": "^2.9.2", 31 | "dequal": "^2.0.2", 32 | "merge-value": "^1.0.0", 33 | "react-colorful": "^5.5.1", 34 | "react-dropzone": "^12.0.0", 35 | "v8n": "^1.3.3", 36 | "zustand": "^5.0.8" 37 | }, 38 | "devDependencies": { 39 | "@welldone-software/why-did-you-render": "^6.2.3" 40 | } 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 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 33 | -------------------------------------------------------------------------------- /packages/leva/src/plugins/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 = >( 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/plugins/String/String.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ValueInput, ValueInputProps } from '../../components/ValueInput' 3 | import { Label, Row } from '../../components/UI' 4 | import { useInputContext } from '../../context' 5 | import type { StringProps } from './string-types' 6 | import { styled } from '../../styles' 7 | 8 | type BaseStringProps = Pick & 9 | Omit & { 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 17 | return {displayValue} 18 | } 19 | 20 | export function StringComponent() { 21 | const { label, settings, displayValue, onUpdate, onChange } = useInputContext() 22 | return ( 23 | 24 | 25 | 26 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /packages/leva/stories/advanced/AdvancedPanels.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Reset from '../components/decorator-reset' 3 | import { Meta } from '@storybook/react' 4 | import { useControls, useStoreContext, useCreateStore, LevaPanel, LevaStoreProvider } from '../../src' 5 | 6 | export default { 7 | title: 'Advanced/Advanced Panels', 8 | decorators: [Reset], 9 | } as Meta 10 | 11 | function MyComponent() { 12 | const store = useStoreContext() 13 | useControls({ point: [0, 0] }, { store }) 14 | return null 15 | } 16 | 17 | export const Default = () => { 18 | const store1 = useCreateStore() 19 | const store2 = useCreateStore() 20 | useControls({ color: '#fff' }, { store: store1 }) 21 | useControls({ boolean: true }, { store: store2 }) 22 | return ( 23 |
31 | 32 | 33 | 34 | 35 | 36 |
37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /packages/plugin-bezier/src/Bezier.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StoryFn, Meta } from '@storybook/react' 3 | 4 | // @ts-ignore 5 | import Reset from 'leva/stories/components/decorator-reset' 6 | import { useControls } from 'leva' 7 | 8 | import { bezier } from './index' 9 | import './Bezier.stories.css' 10 | 11 | export default { 12 | title: 'Plugins/Bezier', 13 | decorators: [Reset], 14 | } as Meta 15 | 16 | const Template: StoryFn> = (args: ReturnType) => { 17 | const data = useControls({ curve: args }) 18 | return ( 19 |
20 |
21 |
{JSON.stringify(data, null, '  ')}
22 |
23 | ) 24 | } 25 | 26 | export const DefaultBezier = Template.bind({}) 27 | DefaultBezier.args = bezier(undefined) 28 | 29 | export const WithArguments = Template.bind({}) 30 | WithArguments.args = bezier([0.54, 0.05, 0.6, 0.98]) 31 | 32 | export const WithPreset = Template.bind({}) 33 | WithPreset.args = bezier('in-out-quadratic') 34 | 35 | export const WithOptions = Template.bind({}) 36 | WithOptions.args = bezier({ handles: [0.54, 0.05, 0.6, 0.98], graph: false, preview: false, label: 'no graph' }) 37 | -------------------------------------------------------------------------------- /packages/leva/src/plugins/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/plugins/Boolean/Boolean.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useInputContext } from '../../context' 3 | import { Label, Row } from '../../components/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) { 13 | return ( 14 | 15 | onUpdate(e.currentTarget.checked)} 20 | disabled={disabled} 21 | /> 22 | 27 | 28 | ) 29 | } 30 | 31 | export function BooleanComponent() { 32 | const { label, value, onUpdate, disabled, id } = useInputContext() 33 | 34 | return ( 35 | 36 | 37 | 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /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(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) === '$') _key = value.substr(1) as any 29 | else return value 30 | } 31 | } 32 | 33 | export * from './stitches.config' 34 | -------------------------------------------------------------------------------- /packages/leva/src/eventEmitter.ts: -------------------------------------------------------------------------------- 1 | type Listener = (...args: Array) => 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) => void 7 | } 8 | 9 | /** 10 | * Super simple event emitter. 11 | */ 12 | export const createEventEmitter = (): EventEmitter => { 13 | const listenerMapping = new Map>() 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/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 | if ('value' in data[path]) return data[path].value 30 | return undefined // TODO should throw 31 | } 32 | const value = parseExpression(expression, get) as (v: number) => any 33 | const settings = { ...defaultSettings, ..._settings } 34 | return { value, settings: settings as InternalPlotSettings } 35 | } 36 | -------------------------------------------------------------------------------- /packages/plugin-dates/src/Date.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StoryFn, Meta } from '@storybook/react' 3 | 4 | // @ts-ignore 5 | import Reset from '../../leva/stories/components/decorator-reset' 6 | import { useControls } from 'leva' 7 | 8 | import { date } from './index' 9 | import { DateInput } from './date-types' 10 | 11 | export default { 12 | title: 'Plugins/Dates', 13 | decorators: [Reset], 14 | } as Meta 15 | 16 | const Template: StoryFn = (args: DateInput) => { 17 | const { birthday } = useControls({ birthday: date(args) }) 18 | return
{birthday.formattedDate}
19 | } 20 | 21 | export const DefaultDate = Template.bind({}) 22 | DefaultDate.args = { date: new Date('2025-10-31') } 23 | 24 | export const CustomLocale = Template.bind({}) 25 | CustomLocale.args = { date: new Date('2025-10-31'), locale: 'en-US' } 26 | 27 | export const CustomInputFormat = Template.bind({}) 28 | CustomInputFormat.args = { date: new Date('2025-10-31'), inputFormat: 'yyyy-MM-dd' } 29 | 30 | export const WithOtherFields = () => { 31 | const { birthday, ...values } = useControls({ 32 | text: 'text', 33 | birthday: date({ date: new Date('2025-10-31') }), 34 | number: 0, 35 | }) 36 | return ( 37 |
38 | {birthday.formattedDate} 39 |
40 | {JSON.stringify(values)} 41 |
42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /packages/leva/src/plugins/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 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Build documentation and deploy to GitHub Pages 2 | on: 3 | push: 4 | branches: ['main'] 5 | workflow_dispatch: 6 | 7 | # Cancel previous run (see: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency) 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | build: 14 | uses: pmndrs/docs/.github/workflows/build.yml@v2 15 | with: 16 | mdx: 'docs' 17 | libname: 'leva' 18 | libname_short: 'leva' 19 | home_redirect: '/getting-started/introduction' 20 | icon: '🌋' 21 | # logo: '/logo.jpg' 22 | github: 'https://github.com/pmndrs/leva' 23 | # discord: 'https://discord.com/channels/740090768164651008/740093168770613279' 24 | 25 | deploy: 26 | needs: build 27 | runs-on: ubuntu-latest 28 | 29 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment 30 | permissions: 31 | pages: write # to deploy to Pages 32 | id-token: write # to verify the deployment originates from an appropriate source 33 | 34 | # Deploy to the github-pages environment 35 | environment: 36 | name: github-pages 37 | url: ${{ steps.deployment.outputs.page_url }} 38 | 39 | steps: 40 | - id: deployment 41 | uses: actions/deploy-pages@v4 42 | -------------------------------------------------------------------------------- /packages/leva/src/plugins/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/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() 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 | 27 | 28 | 29 | )} 30 | 31 | 32 | 33 | setSettings({ graph: !graph })}> 34 | 𝑓 35 | 36 | 37 | 38 | 39 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /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) => { 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 | 15 | {plotPoints.map((p, i) => ( 16 | 17 | ))} 18 | 26 | 27 | ) 28 | }) 29 | 30 | export function BezierPreview({ value }: Pick) { 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 36 | } 37 | -------------------------------------------------------------------------------- /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/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/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/index.ts: -------------------------------------------------------------------------------- 1 | import { register } from './plugin' 2 | import number from './plugins/Number' 3 | import select from './plugins/Select' 4 | import color from './plugins/Color' 5 | import string from './plugins/String' 6 | import boolean from './plugins/Boolean' 7 | import vector3d from './plugins/Vector3d' 8 | import vector2d from './plugins/Vector2d' 9 | import image from './plugins/Image' 10 | import interval from './plugins/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/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 | 31 | {label && } 32 | 33 | {Object.entries(opts).map(([label, onClick]) => ( 34 | onClick(store.get)}> 35 | {label} 36 | 37 | ))} 38 | 39 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /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 | 44 | 45 |
46 |
{JSON.stringify(data, null, '  ')}
47 |
48 | 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /.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: '20' 27 | 28 | - name: Cache pnpm modules 29 | uses: actions/cache@v3 30 | with: 31 | path: ~/.pnpm-store 32 | key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} 33 | restore-keys: | 34 | ${{ runner.os }}-pnpm- 35 | 36 | - uses: pnpm/action-setup@v2.0.1 37 | with: 38 | version: 9 39 | run_install: true 40 | 41 | - name: Create Release Pull Request or Publish to npm 42 | id: changesets 43 | uses: changesets/action@v1 44 | with: 45 | version: pnpm ci:version 46 | publish: pnpm ci:publish 47 | commit: 'chore(release): update monorepo packages versions' 48 | title: 'Upcoming Release Changes' 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 52 | -------------------------------------------------------------------------------- /packages/leva/stories/inputs/Select.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StoryFn, 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: StoryFn = (args) => { 14 | const values = useControls({ 15 | foo: args, 16 | }) 17 | 18 | return ( 19 |
20 |
{JSON.stringify(values, null, '  ')}
21 |
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 = () => IconA 53 | const IconB = () => IconB 54 | 55 | export const FunctionAsOptions = () => { 56 | const values = useControls({ 57 | foo: { options: { none: '', IconA, IconB } }, 58 | }) 59 | 60 | return ( 61 |
62 |
{values.foo.toString()}
63 |
64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /packages/leva/src/plugins/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/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-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/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 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 | 52 | ) 53 | }) 54 | -------------------------------------------------------------------------------- /packages/leva/stories/inputs/Vector.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StoryFn, 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: StoryFn = (args) => { 14 | const values = useControls({ 15 | foo: args, 16 | }) 17 | 18 | return ( 19 |
20 |
{JSON.stringify(values, null, '  ')}
21 |
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 |