with grid-column: 1 / 3; can
567 | * be used.
568 | */
569 | return (
570 |
571 |
572 | setCurrentValue(e.target.value)}
577 | />
578 |
579 |
580 | );
581 | };
582 | ```
583 |
--------------------------------------------------------------------------------
/example/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .cache
3 | dist
--------------------------------------------------------------------------------
/example/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "singleQuote": true,
4 | "semi": true,
5 | "arrowParens": "avoid"
6 | }
7 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
Playground
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/example/index.scss:
--------------------------------------------------------------------------------
1 | @import '../src/VarUI.scss';
2 |
3 | body {
4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
5 | Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
6 | background: #dddddd;
7 | margin: 0;
8 | }
9 |
10 | h1,
11 | .values {
12 | margin-left: 20px;
13 | }
14 |
15 | .values {
16 | margin-top: 20px;
17 | }
18 |
19 | .example {
20 | display: block;
21 | grid-gap: 20px;
22 | }
23 |
24 | .react-var-ui {
25 | background: #11111a;
26 | }
27 |
28 | @media screen and (min-width: 960px) {
29 | .example {
30 | padding: 0 20px;
31 | display: grid;
32 | grid-template-columns: 400px 1fr;
33 | }
34 |
35 | .values {
36 | margin-top: 0;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/example/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as ReactDOM from 'react-dom';
3 |
4 | import './index.scss';
5 |
6 | import {
7 | VarUI,
8 | VarColor,
9 | VarToggle,
10 | VarSelect,
11 | VarSlider,
12 | VarXY,
13 | VarCategory,
14 | VarButton,
15 | VarString,
16 | VarAngle,
17 | VarDisplay,
18 | VarNumber,
19 | VarImage,
20 | VarFile,
21 | VarMedia,
22 | } from '../src';
23 |
24 | const App = () => {
25 | const [values, setValues] = React.useState({
26 | toggle: true,
27 | color: '#FF0000',
28 | colorAlpha: '#FF0000DD',
29 | select: 'zero',
30 | slider: 0.4,
31 | number: 1,
32 | string: 'test',
33 | angle: 0,
34 | xy: [0, 0],
35 | image: undefined,
36 | file: undefined,
37 | media: undefined,
38 | });
39 |
40 | return (
41 |
42 |
VarUI example
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
59 |
67 |
77 |
85 |
93 |
101 |
102 |
103 |
108 |
109 |
110 | alert('clicked!')}
113 | />
114 | alert('clicked!')}
118 | />
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
Values:
127 |
128 | {Object.entries(values).map(([key, value]) => (
129 |
130 | {key}
131 |
132 | {typeof value === 'boolean'
133 | ? value
134 | ? 'true'
135 | : 'false'
136 | : Array.isArray(value)
137 | ? value.join(', ')
138 | : value?.toString()}
139 |
140 |
141 | ))}
142 |
143 |
144 |
145 |
146 | );
147 | };
148 |
149 | ReactDOM.render(
, document.getElementById('root'));
150 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "license": "MIT",
5 | "type": "module",
6 | "scripts": {
7 | "start": "vite",
8 | "build": "vite build"
9 | },
10 | "devDependencies": {
11 | "@vitejs/plugin-react": "^4.0.4",
12 | "react": "^18.2.0",
13 | "react-dom": "^18.2.0",
14 | "typescript": "^5.2.2",
15 | "vite": "^4.4.9"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "declaration": true,
7 | "lib": ["ES2020", "DOM"],
8 | "downlevelIteration": true,
9 | "strict": true,
10 | "esModuleInterop": true,
11 | "jsx": "react"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/example/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig(() => ({
6 | build: {
7 | outDir: './build',
8 | },
9 | plugins: [react()],
10 | }));
11 |
--------------------------------------------------------------------------------
/example/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | "@ampproject/remapping@^2.2.0":
6 | version "2.2.1"
7 | resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630"
8 | integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==
9 | dependencies:
10 | "@jridgewell/gen-mapping" "^0.3.0"
11 | "@jridgewell/trace-mapping" "^0.3.9"
12 |
13 | "@babel/code-frame@^7.22.13":
14 | version "7.22.13"
15 | resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e"
16 | integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==
17 | dependencies:
18 | "@babel/highlight" "^7.22.13"
19 | chalk "^2.4.2"
20 |
21 | "@babel/compat-data@^7.22.9":
22 | version "7.22.9"
23 | resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.9.tgz#71cdb00a1ce3a329ce4cbec3a44f9fef35669730"
24 | integrity sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==
25 |
26 | "@babel/core@^7.22.9":
27 | version "7.22.15"
28 | resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.15.tgz#15d4fd03f478a459015a4b94cfbb3bd42c48d2f4"
29 | integrity sha512-PtZqMmgRrvj8ruoEOIwVA3yoF91O+Hgw9o7DAUTNBA6Mo2jpu31clx9a7Nz/9JznqetTR6zwfC4L3LAjKQXUwA==
30 | dependencies:
31 | "@ampproject/remapping" "^2.2.0"
32 | "@babel/code-frame" "^7.22.13"
33 | "@babel/generator" "^7.22.15"
34 | "@babel/helper-compilation-targets" "^7.22.15"
35 | "@babel/helper-module-transforms" "^7.22.15"
36 | "@babel/helpers" "^7.22.15"
37 | "@babel/parser" "^7.22.15"
38 | "@babel/template" "^7.22.15"
39 | "@babel/traverse" "^7.22.15"
40 | "@babel/types" "^7.22.15"
41 | convert-source-map "^1.7.0"
42 | debug "^4.1.0"
43 | gensync "^1.0.0-beta.2"
44 | json5 "^2.2.3"
45 | semver "^6.3.1"
46 |
47 | "@babel/generator@^7.22.15":
48 | version "7.22.15"
49 | resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.15.tgz#1564189c7ec94cb8f77b5e8a90c4d200d21b2339"
50 | integrity sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==
51 | dependencies:
52 | "@babel/types" "^7.22.15"
53 | "@jridgewell/gen-mapping" "^0.3.2"
54 | "@jridgewell/trace-mapping" "^0.3.17"
55 | jsesc "^2.5.1"
56 |
57 | "@babel/helper-compilation-targets@^7.22.15":
58 | version "7.22.15"
59 | resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz#0698fc44551a26cf29f18d4662d5bf545a6cfc52"
60 | integrity sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==
61 | dependencies:
62 | "@babel/compat-data" "^7.22.9"
63 | "@babel/helper-validator-option" "^7.22.15"
64 | browserslist "^4.21.9"
65 | lru-cache "^5.1.1"
66 | semver "^6.3.1"
67 |
68 | "@babel/helper-environment-visitor@^7.22.5":
69 | version "7.22.5"
70 | resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz#f06dd41b7c1f44e1f8da6c4055b41ab3a09a7e98"
71 | integrity sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==
72 |
73 | "@babel/helper-function-name@^7.22.5":
74 | version "7.22.5"
75 | resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz#ede300828905bb15e582c037162f99d5183af1be"
76 | integrity sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==
77 | dependencies:
78 | "@babel/template" "^7.22.5"
79 | "@babel/types" "^7.22.5"
80 |
81 | "@babel/helper-hoist-variables@^7.22.5":
82 | version "7.22.5"
83 | resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb"
84 | integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==
85 | dependencies:
86 | "@babel/types" "^7.22.5"
87 |
88 | "@babel/helper-module-imports@^7.22.15":
89 | version "7.22.15"
90 | resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0"
91 | integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==
92 | dependencies:
93 | "@babel/types" "^7.22.15"
94 |
95 | "@babel/helper-module-transforms@^7.22.15":
96 | version "7.22.15"
97 | resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.22.15.tgz#40ad2f6950f143900e9c1c72363c0b431a606082"
98 | integrity sha512-l1UiX4UyHSFsYt17iQ3Se5pQQZZHa22zyIXURmvkmLCD4t/aU+dvNWHatKac/D9Vm9UES7nvIqHs4jZqKviUmQ==
99 | dependencies:
100 | "@babel/helper-environment-visitor" "^7.22.5"
101 | "@babel/helper-module-imports" "^7.22.15"
102 | "@babel/helper-simple-access" "^7.22.5"
103 | "@babel/helper-split-export-declaration" "^7.22.6"
104 | "@babel/helper-validator-identifier" "^7.22.15"
105 |
106 | "@babel/helper-plugin-utils@^7.22.5":
107 | version "7.22.5"
108 | resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295"
109 | integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==
110 |
111 | "@babel/helper-simple-access@^7.22.5":
112 | version "7.22.5"
113 | resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de"
114 | integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==
115 | dependencies:
116 | "@babel/types" "^7.22.5"
117 |
118 | "@babel/helper-split-export-declaration@^7.22.6":
119 | version "7.22.6"
120 | resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c"
121 | integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==
122 | dependencies:
123 | "@babel/types" "^7.22.5"
124 |
125 | "@babel/helper-string-parser@^7.22.5":
126 | version "7.22.5"
127 | resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f"
128 | integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==
129 |
130 | "@babel/helper-validator-identifier@^7.22.15", "@babel/helper-validator-identifier@^7.22.5":
131 | version "7.22.15"
132 | resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.15.tgz#601fa28e4cc06786c18912dca138cec73b882044"
133 | integrity sha512-4E/F9IIEi8WR94324mbDUMo074YTheJmd7eZF5vITTeYchqAi6sYXRLHUVsmkdmY4QjfKTcB2jB7dVP3NaBElQ==
134 |
135 | "@babel/helper-validator-option@^7.22.15":
136 | version "7.22.15"
137 | resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz#694c30dfa1d09a6534cdfcafbe56789d36aba040"
138 | integrity sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==
139 |
140 | "@babel/helpers@^7.22.15":
141 | version "7.22.15"
142 | resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.15.tgz#f09c3df31e86e3ea0b7ff7556d85cdebd47ea6f1"
143 | integrity sha512-7pAjK0aSdxOwR+CcYAqgWOGy5dcfvzsTIfFTb2odQqW47MDfv14UaJDY6eng8ylM2EaeKXdxaSWESbkmaQHTmw==
144 | dependencies:
145 | "@babel/template" "^7.22.15"
146 | "@babel/traverse" "^7.22.15"
147 | "@babel/types" "^7.22.15"
148 |
149 | "@babel/highlight@^7.22.13":
150 | version "7.22.13"
151 | resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.13.tgz#9cda839e5d3be9ca9e8c26b6dd69e7548f0cbf16"
152 | integrity sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==
153 | dependencies:
154 | "@babel/helper-validator-identifier" "^7.22.5"
155 | chalk "^2.4.2"
156 | js-tokens "^4.0.0"
157 |
158 | "@babel/parser@^7.22.15":
159 | version "7.22.16"
160 | resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.16.tgz#180aead7f247305cce6551bea2720934e2fa2c95"
161 | integrity sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==
162 |
163 | "@babel/plugin-transform-react-jsx-self@^7.22.5":
164 | version "7.22.5"
165 | resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.22.5.tgz#ca2fdc11bc20d4d46de01137318b13d04e481d8e"
166 | integrity sha512-nTh2ogNUtxbiSbxaT4Ds6aXnXEipHweN9YRgOX/oNXdf0cCrGn/+2LozFa3lnPV5D90MkjhgckCPBrsoSc1a7g==
167 | dependencies:
168 | "@babel/helper-plugin-utils" "^7.22.5"
169 |
170 | "@babel/plugin-transform-react-jsx-source@^7.22.5":
171 | version "7.22.5"
172 | resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.22.5.tgz#49af1615bfdf6ed9d3e9e43e425e0b2b65d15b6c"
173 | integrity sha512-yIiRO6yobeEIaI0RTbIr8iAK9FcBHLtZq0S89ZPjDLQXBA4xvghaKqI0etp/tF3htTM0sazJKKLz9oEiGRtu7w==
174 | dependencies:
175 | "@babel/helper-plugin-utils" "^7.22.5"
176 |
177 | "@babel/template@^7.22.15", "@babel/template@^7.22.5":
178 | version "7.22.15"
179 | resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38"
180 | integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==
181 | dependencies:
182 | "@babel/code-frame" "^7.22.13"
183 | "@babel/parser" "^7.22.15"
184 | "@babel/types" "^7.22.15"
185 |
186 | "@babel/traverse@^7.22.15":
187 | version "7.22.15"
188 | resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.15.tgz#75be4d2d6e216e880e93017f4e2389aeb77ef2d9"
189 | integrity sha512-DdHPwvJY0sEeN4xJU5uRLmZjgMMDIvMPniLuYzUVXj/GGzysPl0/fwt44JBkyUIzGJPV8QgHMcQdQ34XFuKTYQ==
190 | dependencies:
191 | "@babel/code-frame" "^7.22.13"
192 | "@babel/generator" "^7.22.15"
193 | "@babel/helper-environment-visitor" "^7.22.5"
194 | "@babel/helper-function-name" "^7.22.5"
195 | "@babel/helper-hoist-variables" "^7.22.5"
196 | "@babel/helper-split-export-declaration" "^7.22.6"
197 | "@babel/parser" "^7.22.15"
198 | "@babel/types" "^7.22.15"
199 | debug "^4.1.0"
200 | globals "^11.1.0"
201 |
202 | "@babel/types@^7.22.15", "@babel/types@^7.22.5":
203 | version "7.22.15"
204 | resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.15.tgz#266cb21d2c5fd0b3931e7a91b6dd72d2f617d282"
205 | integrity sha512-X+NLXr0N8XXmN5ZsaQdm9U2SSC3UbIYq/doL++sueHOTisgZHoKaQtZxGuV2cUPQHMfjKEfg/g6oy7Hm6SKFtA==
206 | dependencies:
207 | "@babel/helper-string-parser" "^7.22.5"
208 | "@babel/helper-validator-identifier" "^7.22.15"
209 | to-fast-properties "^2.0.0"
210 |
211 | "@esbuild/android-arm64@0.18.20":
212 | version "0.18.20"
213 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622"
214 | integrity sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==
215 |
216 | "@esbuild/android-arm@0.18.20":
217 | version "0.18.20"
218 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682"
219 | integrity sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==
220 |
221 | "@esbuild/android-x64@0.18.20":
222 | version "0.18.20"
223 | resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz#35cf419c4cfc8babe8893d296cd990e9e9f756f2"
224 | integrity sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==
225 |
226 | "@esbuild/darwin-arm64@0.18.20":
227 | version "0.18.20"
228 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz#08172cbeccf95fbc383399a7f39cfbddaeb0d7c1"
229 | integrity sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==
230 |
231 | "@esbuild/darwin-x64@0.18.20":
232 | version "0.18.20"
233 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz#d70d5790d8bf475556b67d0f8b7c5bdff053d85d"
234 | integrity sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==
235 |
236 | "@esbuild/freebsd-arm64@0.18.20":
237 | version "0.18.20"
238 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz#98755cd12707f93f210e2494d6a4b51b96977f54"
239 | integrity sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==
240 |
241 | "@esbuild/freebsd-x64@0.18.20":
242 | version "0.18.20"
243 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz#c1eb2bff03915f87c29cece4c1a7fa1f423b066e"
244 | integrity sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==
245 |
246 | "@esbuild/linux-arm64@0.18.20":
247 | version "0.18.20"
248 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz#bad4238bd8f4fc25b5a021280c770ab5fc3a02a0"
249 | integrity sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==
250 |
251 | "@esbuild/linux-arm@0.18.20":
252 | version "0.18.20"
253 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz#3e617c61f33508a27150ee417543c8ab5acc73b0"
254 | integrity sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==
255 |
256 | "@esbuild/linux-ia32@0.18.20":
257 | version "0.18.20"
258 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz#699391cccba9aee6019b7f9892eb99219f1570a7"
259 | integrity sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==
260 |
261 | "@esbuild/linux-loong64@0.18.20":
262 | version "0.18.20"
263 | resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d"
264 | integrity sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==
265 |
266 | "@esbuild/linux-mips64el@0.18.20":
267 | version "0.18.20"
268 | resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz#eeff3a937de9c2310de30622a957ad1bd9183231"
269 | integrity sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==
270 |
271 | "@esbuild/linux-ppc64@0.18.20":
272 | version "0.18.20"
273 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz#2f7156bde20b01527993e6881435ad79ba9599fb"
274 | integrity sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==
275 |
276 | "@esbuild/linux-riscv64@0.18.20":
277 | version "0.18.20"
278 | resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz#6628389f210123d8b4743045af8caa7d4ddfc7a6"
279 | integrity sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==
280 |
281 | "@esbuild/linux-s390x@0.18.20":
282 | version "0.18.20"
283 | resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz#255e81fb289b101026131858ab99fba63dcf0071"
284 | integrity sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==
285 |
286 | "@esbuild/linux-x64@0.18.20":
287 | version "0.18.20"
288 | resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz#c7690b3417af318a9b6f96df3031a8865176d338"
289 | integrity sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==
290 |
291 | "@esbuild/netbsd-x64@0.18.20":
292 | version "0.18.20"
293 | resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz#30e8cd8a3dded63975e2df2438ca109601ebe0d1"
294 | integrity sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==
295 |
296 | "@esbuild/openbsd-x64@0.18.20":
297 | version "0.18.20"
298 | resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz#7812af31b205055874c8082ea9cf9ab0da6217ae"
299 | integrity sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==
300 |
301 | "@esbuild/sunos-x64@0.18.20":
302 | version "0.18.20"
303 | resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz#d5c275c3b4e73c9b0ecd38d1ca62c020f887ab9d"
304 | integrity sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==
305 |
306 | "@esbuild/win32-arm64@0.18.20":
307 | version "0.18.20"
308 | resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz#73bc7f5a9f8a77805f357fab97f290d0e4820ac9"
309 | integrity sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==
310 |
311 | "@esbuild/win32-ia32@0.18.20":
312 | version "0.18.20"
313 | resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz#ec93cbf0ef1085cc12e71e0d661d20569ff42102"
314 | integrity sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==
315 |
316 | "@esbuild/win32-x64@0.18.20":
317 | version "0.18.20"
318 | resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d"
319 | integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==
320 |
321 | "@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2":
322 | version "0.3.3"
323 | resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098"
324 | integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==
325 | dependencies:
326 | "@jridgewell/set-array" "^1.0.1"
327 | "@jridgewell/sourcemap-codec" "^1.4.10"
328 | "@jridgewell/trace-mapping" "^0.3.9"
329 |
330 | "@jridgewell/resolve-uri@^3.1.0":
331 | version "3.1.1"
332 | resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721"
333 | integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==
334 |
335 | "@jridgewell/set-array@^1.0.1":
336 | version "1.1.2"
337 | resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
338 | integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
339 |
340 | "@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14":
341 | version "1.4.15"
342 | resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
343 | integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
344 |
345 | "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9":
346 | version "0.3.19"
347 | resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811"
348 | integrity sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==
349 | dependencies:
350 | "@jridgewell/resolve-uri" "^3.1.0"
351 | "@jridgewell/sourcemap-codec" "^1.4.14"
352 |
353 | "@vitejs/plugin-react@^4.0.4":
354 | version "4.0.4"
355 | resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.0.4.tgz#31c3f779dc534e045c4b134e7cf7b150af0a7646"
356 | integrity sha512-7wU921ABnNYkETiMaZy7XqpueMnpu5VxvVps13MjmCo+utBdD79sZzrApHawHtVX66cCJQQTXFcjH0y9dSUK8g==
357 | dependencies:
358 | "@babel/core" "^7.22.9"
359 | "@babel/plugin-transform-react-jsx-self" "^7.22.5"
360 | "@babel/plugin-transform-react-jsx-source" "^7.22.5"
361 | react-refresh "^0.14.0"
362 |
363 | ansi-styles@^3.2.1:
364 | version "3.2.1"
365 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
366 | integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
367 | dependencies:
368 | color-convert "^1.9.0"
369 |
370 | browserslist@^4.21.9:
371 | version "4.21.10"
372 | resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.10.tgz#dbbac576628c13d3b2231332cb2ec5a46e015bb0"
373 | integrity sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==
374 | dependencies:
375 | caniuse-lite "^1.0.30001517"
376 | electron-to-chromium "^1.4.477"
377 | node-releases "^2.0.13"
378 | update-browserslist-db "^1.0.11"
379 |
380 | caniuse-lite@^1.0.30001517:
381 | version "1.0.30001528"
382 | resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001528.tgz#479972fc705b996f1114336c0032418a215fd0aa"
383 | integrity sha512-0Db4yyjR9QMNlsxh+kKWzQtkyflkG/snYheSzkjmvdEtEXB1+jt7A2HmSEiO6XIJPIbo92lHNGNySvE5pZcs5Q==
384 |
385 | chalk@^2.4.2:
386 | version "2.4.2"
387 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
388 | integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
389 | dependencies:
390 | ansi-styles "^3.2.1"
391 | escape-string-regexp "^1.0.5"
392 | supports-color "^5.3.0"
393 |
394 | color-convert@^1.9.0:
395 | version "1.9.3"
396 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
397 | integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
398 | dependencies:
399 | color-name "1.1.3"
400 |
401 | color-name@1.1.3:
402 | version "1.1.3"
403 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
404 | integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
405 |
406 | convert-source-map@^1.7.0:
407 | version "1.9.0"
408 | resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f"
409 | integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==
410 |
411 | debug@^4.1.0:
412 | version "4.3.4"
413 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
414 | integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
415 | dependencies:
416 | ms "2.1.2"
417 |
418 | electron-to-chromium@^1.4.477:
419 | version "1.4.510"
420 | resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.510.tgz#446c50d7533c1e71a84b00a3b37ab06dd601d890"
421 | integrity sha512-xPfLIPFcN/WLXBpQ/K4UgE98oUBO5Tia6BD4rkSR0wE7ep/PwBVlgvPJQrIBpmJGVAmUzwPKuDbVt9XV6+uC2g==
422 |
423 | esbuild@^0.18.10:
424 | version "0.18.20"
425 | resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.18.20.tgz#4709f5a34801b43b799ab7d6d82f7284a9b7a7a6"
426 | integrity sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==
427 | optionalDependencies:
428 | "@esbuild/android-arm" "0.18.20"
429 | "@esbuild/android-arm64" "0.18.20"
430 | "@esbuild/android-x64" "0.18.20"
431 | "@esbuild/darwin-arm64" "0.18.20"
432 | "@esbuild/darwin-x64" "0.18.20"
433 | "@esbuild/freebsd-arm64" "0.18.20"
434 | "@esbuild/freebsd-x64" "0.18.20"
435 | "@esbuild/linux-arm" "0.18.20"
436 | "@esbuild/linux-arm64" "0.18.20"
437 | "@esbuild/linux-ia32" "0.18.20"
438 | "@esbuild/linux-loong64" "0.18.20"
439 | "@esbuild/linux-mips64el" "0.18.20"
440 | "@esbuild/linux-ppc64" "0.18.20"
441 | "@esbuild/linux-riscv64" "0.18.20"
442 | "@esbuild/linux-s390x" "0.18.20"
443 | "@esbuild/linux-x64" "0.18.20"
444 | "@esbuild/netbsd-x64" "0.18.20"
445 | "@esbuild/openbsd-x64" "0.18.20"
446 | "@esbuild/sunos-x64" "0.18.20"
447 | "@esbuild/win32-arm64" "0.18.20"
448 | "@esbuild/win32-ia32" "0.18.20"
449 | "@esbuild/win32-x64" "0.18.20"
450 |
451 | escalade@^3.1.1:
452 | version "3.1.1"
453 | resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
454 | integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
455 |
456 | escape-string-regexp@^1.0.5:
457 | version "1.0.5"
458 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
459 | integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
460 |
461 | fsevents@~2.3.2:
462 | version "2.3.3"
463 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
464 | integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
465 |
466 | gensync@^1.0.0-beta.2:
467 | version "1.0.0-beta.2"
468 | resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
469 | integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
470 |
471 | globals@^11.1.0:
472 | version "11.12.0"
473 | resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
474 | integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
475 |
476 | has-flag@^3.0.0:
477 | version "3.0.0"
478 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
479 | integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==
480 |
481 | "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
482 | version "4.0.0"
483 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
484 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
485 |
486 | jsesc@^2.5.1:
487 | version "2.5.2"
488 | resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
489 | integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
490 |
491 | json5@^2.2.3:
492 | version "2.2.3"
493 | resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
494 | integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
495 |
496 | loose-envify@^1.1.0:
497 | version "1.4.0"
498 | resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
499 | integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
500 | dependencies:
501 | js-tokens "^3.0.0 || ^4.0.0"
502 |
503 | lru-cache@^5.1.1:
504 | version "5.1.1"
505 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
506 | integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
507 | dependencies:
508 | yallist "^3.0.2"
509 |
510 | ms@2.1.2:
511 | version "2.1.2"
512 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
513 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
514 |
515 | nanoid@^3.3.6:
516 | version "3.3.6"
517 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
518 | integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
519 |
520 | node-releases@^2.0.13:
521 | version "2.0.13"
522 | resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d"
523 | integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==
524 |
525 | picocolors@^1.0.0:
526 | version "1.0.0"
527 | resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
528 | integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
529 |
530 | postcss@^8.4.27:
531 | version "8.4.29"
532 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.29.tgz#33bc121cf3b3688d4ddef50be869b2a54185a1dd"
533 | integrity sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==
534 | dependencies:
535 | nanoid "^3.3.6"
536 | picocolors "^1.0.0"
537 | source-map-js "^1.0.2"
538 |
539 | react-dom@^18.2.0:
540 | version "18.2.0"
541 | resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
542 | integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
543 | dependencies:
544 | loose-envify "^1.1.0"
545 | scheduler "^0.23.0"
546 |
547 | react-refresh@^0.14.0:
548 | version "0.14.0"
549 | resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e"
550 | integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==
551 |
552 | react@^18.2.0:
553 | version "18.2.0"
554 | resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
555 | integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
556 | dependencies:
557 | loose-envify "^1.1.0"
558 |
559 | rollup@^3.27.1:
560 | version "3.29.0"
561 | resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.0.tgz#1b40e64818afc979c7e5bef93de675829288986b"
562 | integrity sha512-nszM8DINnx1vSS+TpbWKMkxem0CDWk3cSit/WWCBVs9/JZ1I/XLwOsiUglYuYReaeWWSsW9kge5zE5NZtf/a4w==
563 | optionalDependencies:
564 | fsevents "~2.3.2"
565 |
566 | scheduler@^0.23.0:
567 | version "0.23.0"
568 | resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
569 | integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
570 | dependencies:
571 | loose-envify "^1.1.0"
572 |
573 | semver@^6.3.1:
574 | version "6.3.1"
575 | resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
576 | integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
577 |
578 | source-map-js@^1.0.2:
579 | version "1.0.2"
580 | resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
581 | integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
582 |
583 | supports-color@^5.3.0:
584 | version "5.5.0"
585 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
586 | integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
587 | dependencies:
588 | has-flag "^3.0.0"
589 |
590 | to-fast-properties@^2.0.0:
591 | version "2.0.0"
592 | resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
593 | integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==
594 |
595 | typescript@^5.2.2:
596 | version "5.2.2"
597 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78"
598 | integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==
599 |
600 | update-browserslist-db@^1.0.11:
601 | version "1.0.11"
602 | resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940"
603 | integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==
604 | dependencies:
605 | escalade "^3.1.1"
606 | picocolors "^1.0.0"
607 |
608 | vite@^4.4.9:
609 | version "4.4.9"
610 | resolved "https://registry.yarnpkg.com/vite/-/vite-4.4.9.tgz#1402423f1a2f8d66fd8d15e351127c7236d29d3d"
611 | integrity sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==
612 | dependencies:
613 | esbuild "^0.18.10"
614 | postcss "^8.4.27"
615 | rollup "^3.27.1"
616 | optionalDependencies:
617 | fsevents "~2.3.2"
618 |
619 | yallist@^3.0.2:
620 | version "3.1.1"
621 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
622 | integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
623 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.5.2",
3 | "license": "BSD-3-Clause-Clear",
4 | "exports": {
5 | ".": {
6 | "import": "./dist/react-var-ui.js",
7 | "types": "./dist/index.d.ts"
8 | },
9 | "./dist/index.css": {
10 | "require": "./dist/index.css",
11 | "import": "./dist/index.css"
12 | },
13 | "./index.css": {
14 | "require": "./dist/index.css",
15 | "import": "./dist/index.css"
16 | }
17 | },
18 | "type": "module",
19 | "main": "dist/react-var-ui.js",
20 | "typings": "dist/index.d.ts",
21 | "module": "dist/react-var-ui.js",
22 | "files": [
23 | "dist"
24 | ],
25 | "engines": {
26 | "node": ">=10"
27 | },
28 | "scripts": {
29 | "build": "vite build",
30 | "test": "vitest",
31 | "lint": "eslint src",
32 | "prepare": "vite build",
33 | "chromatic": "npx chromatic --exit-zero-on-changes",
34 | "storybook": "storybook dev -p 6006",
35 | "build-storybook": "storybook build",
36 | "coverage": "vitest --coverage --run"
37 | },
38 | "peerDependencies": {
39 | "react": ">=16"
40 | },
41 | "repository": {
42 | "type": "git",
43 | "url": "https://github.com/mat-sz/react-var-ui.git"
44 | },
45 | "bugs": {
46 | "url": "https://github.com/mat-sz/react-var-ui/issues"
47 | },
48 | "homepage": "https://github.com/mat-sz/react-var-ui",
49 | "name": "react-var-ui",
50 | "author": "Mat Sz
",
51 | "keywords": [
52 | "react",
53 | "dat.gui",
54 | "react-component",
55 | "react-slider",
56 | "slider",
57 | "input",
58 | "range",
59 | "library",
60 | "typescript"
61 | ],
62 | "resolutions": {
63 | "string-width": "4.2.3",
64 | "strip-ansi": "6.0.1",
65 | "wrap-ansi": "7.0.0"
66 | },
67 | "devDependencies": {
68 | "@rollup/plugin-typescript": "^11.1.3",
69 | "@storybook/addon-essentials": "^7.4.0",
70 | "@storybook/addon-interactions": "^7.4.0",
71 | "@storybook/addon-links": "^7.4.0",
72 | "@storybook/addon-onboarding": "^1.0.8",
73 | "@storybook/blocks": "^7.4.0",
74 | "@storybook/preview-api": "^7.5.3",
75 | "@storybook/react": "^7.4.0",
76 | "@storybook/react-vite": "^7.4.0",
77 | "@storybook/testing-library": "^0.2.0",
78 | "@testing-library/jest-dom": "^6.1.3",
79 | "@testing-library/react": "^14.0.0",
80 | "@types/react": "^18.2.21",
81 | "@types/react-dom": "^18.2.7",
82 | "@types/testing-library__jest-dom": "^5.14.1",
83 | "@vitejs/plugin-react": "^4.0.4",
84 | "@vitest/coverage-v8": "^0.34.6",
85 | "canvas": "^2.11.2",
86 | "chromatic": "^7.1.0",
87 | "husky": "^6.0.0",
88 | "jsdom": "^22.1.0",
89 | "react": "^18.2.0",
90 | "react-dom": "^18.2.0",
91 | "sass": "^1.34.1",
92 | "storybook": "^7.4.0",
93 | "typescript": "^5.2.2",
94 | "vite": "^4.4.9",
95 | "vitest": "^0.34.3"
96 | },
97 | "dependencies": {
98 | "@uiw/react-color-sketch": "^2.0.5",
99 | "filesize": "^8.0.6",
100 | "radash": "^11.0.0",
101 | "react-use-pointer-drag": "^0.1.1"
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mat-sz/react-var-ui/c9fd00c9a9865aedd93a68c3e2fc5d7381fbea61/screenshot.png
--------------------------------------------------------------------------------
/screenshots/VarAngle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mat-sz/react-var-ui/c9fd00c9a9865aedd93a68c3e2fc5d7381fbea61/screenshots/VarAngle.png
--------------------------------------------------------------------------------
/screenshots/VarButton.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mat-sz/react-var-ui/c9fd00c9a9865aedd93a68c3e2fc5d7381fbea61/screenshots/VarButton.png
--------------------------------------------------------------------------------
/screenshots/VarCategory.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mat-sz/react-var-ui/c9fd00c9a9865aedd93a68c3e2fc5d7381fbea61/screenshots/VarCategory.png
--------------------------------------------------------------------------------
/screenshots/VarColor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mat-sz/react-var-ui/c9fd00c9a9865aedd93a68c3e2fc5d7381fbea61/screenshots/VarColor.png
--------------------------------------------------------------------------------
/screenshots/VarDisplay.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mat-sz/react-var-ui/c9fd00c9a9865aedd93a68c3e2fc5d7381fbea61/screenshots/VarDisplay.png
--------------------------------------------------------------------------------
/screenshots/VarFile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mat-sz/react-var-ui/c9fd00c9a9865aedd93a68c3e2fc5d7381fbea61/screenshots/VarFile.png
--------------------------------------------------------------------------------
/screenshots/VarImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mat-sz/react-var-ui/c9fd00c9a9865aedd93a68c3e2fc5d7381fbea61/screenshots/VarImage.png
--------------------------------------------------------------------------------
/screenshots/VarMedia.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mat-sz/react-var-ui/c9fd00c9a9865aedd93a68c3e2fc5d7381fbea61/screenshots/VarMedia.png
--------------------------------------------------------------------------------
/screenshots/VarNumber.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mat-sz/react-var-ui/c9fd00c9a9865aedd93a68c3e2fc5d7381fbea61/screenshots/VarNumber.png
--------------------------------------------------------------------------------
/screenshots/VarSelect.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mat-sz/react-var-ui/c9fd00c9a9865aedd93a68c3e2fc5d7381fbea61/screenshots/VarSelect.png
--------------------------------------------------------------------------------
/screenshots/VarSlider.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mat-sz/react-var-ui/c9fd00c9a9865aedd93a68c3e2fc5d7381fbea61/screenshots/VarSlider.png
--------------------------------------------------------------------------------
/screenshots/VarString.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mat-sz/react-var-ui/c9fd00c9a9865aedd93a68c3e2fc5d7381fbea61/screenshots/VarString.png
--------------------------------------------------------------------------------
/screenshots/VarToggle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mat-sz/react-var-ui/c9fd00c9a9865aedd93a68c3e2fc5d7381fbea61/screenshots/VarToggle.png
--------------------------------------------------------------------------------
/screenshots/VarXY.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mat-sz/react-var-ui/c9fd00c9a9865aedd93a68c3e2fc5d7381fbea61/screenshots/VarXY.png
--------------------------------------------------------------------------------
/src/VarAngle.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useMemo, useRef } from 'react';
2 | import { usePointerDrag } from 'react-use-pointer-drag';
3 |
4 | import { useVarUIValue } from './common/VarUIContext';
5 | import { IVarBaseInputProps, VarBase } from './VarBase';
6 |
7 | const PI2 = Math.PI * 2;
8 |
9 | function wrap(angle: number) {
10 | return (PI2 + (angle % PI2)) % PI2;
11 | }
12 |
13 | export interface IVarAngleProps extends IVarBaseInputProps {}
14 |
15 | /**
16 | * Angle picker component. Accepts and provides numbers (radians).
17 | */
18 | export const VarAngle = ({
19 | label,
20 | path,
21 | value,
22 | onChange,
23 | disabled,
24 | readOnly,
25 | defaultValue = 0,
26 | className,
27 | error,
28 | errorPath,
29 | }: IVarAngleProps): JSX.Element => {
30 | const controlRef = useRef(null);
31 | const [currentValue, setCurrentValue, currentError] = useVarUIValue({
32 | path,
33 | fallbackValue: value,
34 | onChange,
35 | error,
36 | errorPath,
37 | });
38 | const degrees = useMemo(
39 | () => Math.round(wrap(currentValue) * (180 / Math.PI)),
40 | [currentValue]
41 | );
42 |
43 | const { dragProps } = usePointerDrag({
44 | onMove: ({ x, y }) => {
45 | const div = controlRef.current!;
46 | const rect = div.getBoundingClientRect();
47 | const centerX = rect.left + rect.width / 2;
48 | const centerY = rect.top + rect.height / 2;
49 | setCurrentValue(wrap(Math.atan2(y - centerY, x - centerX) + Math.PI / 2));
50 | },
51 | });
52 |
53 | useEffect(() => {
54 | controlRef.current?.addEventListener('wheel', e => e.preventDefault());
55 | }, []);
56 |
57 | return (
58 |
65 | {degrees}°
66 |
67 |
72 | typeof defaultValue !== 'undefined' && setCurrentValue(defaultValue)
73 | }
74 | onWheel={e => {
75 | setCurrentValue(wrap(currentValue + 0.5 * Math.sign(e.deltaY)));
76 | }}
77 | title="Angle"
78 | {...dragProps()}
79 | >
80 |
81 |
82 | );
83 | };
84 |
--------------------------------------------------------------------------------
/src/VarArray.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react';
2 | import { clone, get, set } from 'radash';
3 |
4 | import { useVarUIValue, VarUIContext } from './common/VarUIContext';
5 | import { IVarBaseInputProps, VarBase } from './VarBase';
6 |
7 | export interface IVarArrayProps
8 | extends Omit, 'label' | 'children' | 'readOnly'> {
9 | children?: ReactNode | ((element: T, index: number, array: T[]) => ReactNode);
10 | }
11 |
12 | /**
13 | * Array input component.
14 | */
15 | export const VarArray = ({
16 | path,
17 | value,
18 | onChange,
19 | disabled,
20 | className,
21 | children,
22 | error,
23 | errorPath,
24 | }: IVarArrayProps): JSX.Element => {
25 | const [currentValue, setCurrentValue, currentError] = useVarUIValue({
26 | path,
27 | fallbackValue: value,
28 | onChange,
29 | error,
30 | errorPath,
31 | });
32 |
33 | return (
34 |
41 | {currentValue?.map((element, index, array) => {
42 | return (
43 |
47 | typeof path === 'string' ? get(element, path) : undefined,
48 | setValue: (path: string, newValue: any) => {
49 | const newArray = [...currentValue];
50 | newArray[index] =
51 | path === '' ? newValue : set(clone(element), path, newValue);
52 | setCurrentValue(newArray);
53 | },
54 | getError: (path?: string) => {
55 | const elementError = currentError?.[index];
56 | return elementError && path
57 | ? get(elementError, path)
58 | : undefined;
59 | },
60 | }}
61 | key={index}
62 | >
63 | {typeof children === 'function'
64 | ? children(element, index, array)
65 | : children}
66 |
67 | );
68 | })}
69 |
70 | );
71 | };
72 |
--------------------------------------------------------------------------------
/src/VarBase.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react';
2 |
3 | export interface IVarBaseProps {
4 | /**
5 | * Label to be shown left to the input.
6 | */
7 | label?: ReactNode;
8 |
9 | /**
10 | * Additional class names on the wrapping div element.
11 | */
12 | className?: string;
13 |
14 | /**
15 | * Should the component be disabled.
16 | */
17 | disabled?: boolean;
18 |
19 | /**
20 | * Should the component be read-only.
21 | */
22 | readOnly?: boolean;
23 |
24 | /**
25 | * Children. Only rendered when provided directly to the VarBase component.
26 | */
27 | children?: ReactNode;
28 |
29 | /**
30 | * Should keep children in a column, with every child having a width of 100%.
31 | */
32 | column?: boolean;
33 |
34 | /**
35 | * Error to display.
36 | */
37 | error?: string;
38 | }
39 |
40 | export interface IVarBaseValueProps {
41 | /**
42 | * Variable path in the data object.
43 | */
44 | path?: string;
45 |
46 | /**
47 | * Current value (only used if context and path aren't available).
48 | * In most cases you aren't going to need this.
49 | */
50 | value?: T;
51 |
52 | /**
53 | * Default value for components that support resetting (on double click for example).
54 | */
55 | defaultValue?: T;
56 |
57 | /**
58 | * On change event, called with the new value if provided.
59 | * In most cases you aren't going to need this.
60 | */
61 | onChange?: (value: T) => void;
62 |
63 | /**
64 | * Error path to resolve in object. (default: same as path)
65 | */
66 | errorPath?: string;
67 |
68 | error?: string;
69 | }
70 |
71 | export interface IVarBaseInputProps
72 | extends IVarBaseProps,
73 | IVarBaseValueProps {}
74 |
75 | /**
76 | * Base VarUI input component. Doesn't do anything besides displaying the label.
77 | * Used to construct other components from.
78 | */
79 | export const VarBase = ({
80 | label,
81 | children,
82 | className,
83 | disabled,
84 | readOnly,
85 | column = false,
86 | error,
87 | }: IVarBaseProps): JSX.Element => {
88 | return (
89 |
101 | {!!label &&
{label} }
102 | {children}
103 | {error ?
{error}
: null}
104 |
105 | );
106 | };
107 |
--------------------------------------------------------------------------------
/src/VarButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react';
2 |
3 | import { IVarBaseProps, VarBase } from './VarBase';
4 |
5 | export interface IVarButtonProps extends IVarBaseProps {
6 | /**
7 | * Called when the button is clicked.
8 | */
9 | onClick?: () => void;
10 |
11 | /**
12 | * Text for the button.
13 | */
14 | buttonLabel: ReactNode;
15 |
16 | /**
17 | * Should the component be disabled.
18 | */
19 | disabled?: boolean;
20 | }
21 |
22 | /**
23 | * Button component. Only provides a onClick property.
24 | */
25 | export const VarButton = ({
26 | label,
27 | onClick,
28 | buttonLabel,
29 | disabled,
30 | className,
31 | error,
32 | }: IVarButtonProps): JSX.Element => {
33 | return (
34 |
40 |
41 | onClick?.()} disabled={disabled}>
42 | {buttonLabel}
43 |
44 |
45 |
46 | );
47 | };
48 |
--------------------------------------------------------------------------------
/src/VarCategory.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, useState } from 'react';
2 | import { IconDown } from './icons/IconDown';
3 | import { IconUp } from './icons/IconUp';
4 |
5 | export interface IVarCategoryProps {
6 | /**
7 | * Category label.
8 | */
9 | label: ReactNode;
10 |
11 | /**
12 | * Additional class names on the wrapping div element.
13 | */
14 | className?: string;
15 |
16 | /**
17 | * Allows the category to be collapsed if true.
18 | */
19 | collapsible?: boolean;
20 |
21 | children?: React.ReactNode;
22 | }
23 |
24 | /**
25 | * Category component for grouping inputs.
26 | */
27 | export const VarCategory = ({
28 | label,
29 | className,
30 | children,
31 | collapsible,
32 | }: IVarCategoryProps): JSX.Element => {
33 | const [isCollapsed, setCollapsed] = useState(false);
34 |
35 | return (
36 |
37 |
38 | {label}
39 | {collapsible && (
40 | setCollapsed(isCollapsed => !isCollapsed)}
44 | >
45 | {isCollapsed ? : }
46 |
47 | )}
48 |
49 | {(!collapsible || !isCollapsed) &&
{children}
}
50 |
51 | );
52 | };
53 |
--------------------------------------------------------------------------------
/src/VarColor.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useState } from 'react';
2 | import ColorPicker from '@uiw/react-color-sketch';
3 |
4 | import { useVarUIValue } from './common/VarUIContext';
5 | import { IVarBaseInputProps, VarBase } from './VarBase';
6 |
7 | export interface IVarColorProps extends IVarBaseInputProps {
8 | /**
9 | * Should allow picking alpha values?
10 | * If true, the result hex code will contain extra two characters representing the alpha value, from 00 to FF.
11 | */
12 | alpha?: boolean;
13 | }
14 |
15 | /**
16 | * Color picker component. Returns and accepts values in form of hex color strings.
17 | */
18 | export const VarColor = ({
19 | label,
20 | path,
21 | value,
22 | onChange,
23 | alpha,
24 | disabled,
25 | readOnly,
26 | className,
27 | error,
28 | errorPath,
29 | }: IVarColorProps): JSX.Element => {
30 | const [currentValue, setCurrentValue, currentError] = useVarUIValue({
31 | path,
32 | fallbackValue: value,
33 | onChange,
34 | error,
35 | errorPath,
36 | });
37 |
38 | const [show, setShow] = useState(false);
39 |
40 | return (
41 |
48 |
49 | {currentValue}
50 |
51 |
setShow(show => !show)}
54 | >
55 |
60 |
61 | {show ? (
62 |
63 |
setShow(false)}
66 | />
67 | {
70 | if (alpha) {
71 | setCurrentValue(result.hexa);
72 | } else {
73 | setCurrentValue(result.hex);
74 | }
75 | }}
76 | disableAlpha={!alpha}
77 | />
78 |
79 | ) : null}
80 |
81 |
82 |
83 | );
84 | };
85 |
--------------------------------------------------------------------------------
/src/VarDisplay.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { useVarUIValue } from './common/VarUIContext';
4 | import { IVarBaseProps, VarBase } from './VarBase';
5 |
6 | export interface IVarDisplayProps extends IVarBaseProps {
7 | /**
8 | * Variable path in the data object.
9 | */
10 | path?: string;
11 |
12 | /**
13 | * Current value (only used if context and path aren't available).
14 | * In most cases you aren't going to need this.
15 | */
16 | value?: string | number;
17 |
18 | errorPath?: string;
19 |
20 | unit?: string;
21 | }
22 |
23 | /**
24 | * A simple component that displays a string or a numeric value.
25 | */
26 | export const VarDisplay = ({
27 | label,
28 | path,
29 | value,
30 | disabled,
31 | readOnly,
32 | className,
33 | error,
34 | errorPath,
35 | unit,
36 | }: IVarDisplayProps): JSX.Element => {
37 | const [currentValue, _, currentError] = useVarUIValue({
38 | path,
39 | fallbackValue: value,
40 | error,
41 | errorPath,
42 | });
43 |
44 | return (
45 |
52 |
53 | {currentValue}
54 | {unit}
55 |
56 |
57 | );
58 | };
59 |
--------------------------------------------------------------------------------
/src/VarFile.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import filesize from 'filesize';
3 |
4 | import { useVarUIValue } from './common/VarUIContext';
5 | import { IconUpload } from './icons/IconUpload';
6 | import { IVarBaseInputProps, VarBase } from './VarBase';
7 |
8 | export interface IVarFileProps extends IVarBaseInputProps
{
9 | /**
10 | * Accepted file types.
11 | */
12 | accept?: string;
13 |
14 | /**
15 | * Show metadata.
16 | * Default: true.
17 | */
18 | displayMetadata?: boolean;
19 | }
20 |
21 | /**
22 | * File input component. Accepts and provides a File instance.
23 | */
24 | export const VarFile = ({
25 | label,
26 | path,
27 | value,
28 | onChange,
29 | disabled,
30 | readOnly,
31 | className,
32 | accept,
33 | displayMetadata = true,
34 | error,
35 | errorPath,
36 | }: IVarFileProps): JSX.Element => {
37 | const [currentValue, setCurrentValue, currentError] = useVarUIValue({
38 | path,
39 | fallbackValue: value,
40 | onChange,
41 | error,
42 | errorPath,
43 | });
44 |
45 | const onFileChange = useCallback(
46 | (e: React.ChangeEvent) => {
47 | if (!e.target.files?.length) {
48 | return;
49 | }
50 |
51 | const file = e.target.files[0];
52 | setCurrentValue(file);
53 | },
54 | [setCurrentValue]
55 | );
56 |
57 | return (
58 |
65 | {currentValue?.name}
66 |
67 | {displayMetadata && !!currentValue && (
68 |
69 |
Size: {filesize(currentValue.size)}
70 |
Type: {currentValue.type || 'unknown'}
71 |
72 | )}
73 |
74 |
80 |
81 |
82 | );
83 | };
84 |
--------------------------------------------------------------------------------
/src/VarImage.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 |
3 | import { useVarUIValue } from './common/VarUIContext';
4 | import { IconUpload } from './icons/IconUpload';
5 | import { IVarBaseInputProps, VarBase } from './VarBase';
6 |
7 | export interface IVarImageProps extends IVarBaseInputProps {}
8 |
9 | /**
10 | * Image input component. Accepts and provides a blob URL.
11 | */
12 | export const VarImage = ({
13 | label,
14 | path,
15 | value,
16 | onChange,
17 | disabled,
18 | readOnly,
19 | className,
20 | error,
21 | errorPath,
22 | }: IVarImageProps): JSX.Element => {
23 | const [currentValue, setCurrentValue, currentError] = useVarUIValue({
24 | path,
25 | fallbackValue: value,
26 | onChange,
27 | error,
28 | errorPath,
29 | });
30 |
31 | const onFileChange = useCallback(
32 | (e: React.ChangeEvent) => {
33 | if (!e.target.files?.length) {
34 | return;
35 | }
36 |
37 | const file = e.target.files[0];
38 | const url = URL.createObjectURL(file);
39 | setCurrentValue(url);
40 | },
41 | [setCurrentValue]
42 | );
43 |
44 | return (
45 |
53 |
71 |
72 | );
73 | };
74 |
--------------------------------------------------------------------------------
/src/VarMedia.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useMemo, useState } from 'react';
2 |
3 | import { useVarUIValue } from './common/VarUIContext';
4 | import { IconUpload } from './icons/IconUpload';
5 | import { IVarBaseInputProps, VarBase } from './VarBase';
6 |
7 | export interface IVarMediaProps extends IVarBaseInputProps {
8 | /**
9 | * Whether the component should accept image/* files.
10 | */
11 | acceptImage?: boolean;
12 |
13 | /**
14 | * Whether the component should accept audio/* files.
15 | */
16 | acceptAudio?: boolean;
17 |
18 | /**
19 | * Whether the component should accept video/* files.
20 | */
21 | acceptVideo?: boolean;
22 | }
23 |
24 | /**
25 | * Media (audio/video/image) input component. Accepts and provides a blob URL.
26 | *
27 | * If acceptImage, acceptAudio and acceptVideo are all false, the component will accept all 3.
28 | */
29 | export const VarMedia = ({
30 | label,
31 | path,
32 | value,
33 | onChange,
34 | disabled,
35 | readOnly,
36 | className,
37 | acceptImage,
38 | acceptAudio,
39 | acceptVideo,
40 | error,
41 | errorPath,
42 | }: IVarMediaProps): JSX.Element => {
43 | const [currentValue, setCurrentValue, currentError] = useVarUIValue({
44 | path,
45 | fallbackValue: value,
46 | onChange,
47 | error,
48 | errorPath,
49 | });
50 | const [type, setType] = useState();
51 | const accept = useMemo(() => {
52 | let accept = '';
53 |
54 | if (acceptImage) {
55 | accept += 'image/*,';
56 | }
57 |
58 | if (acceptAudio) {
59 | accept += 'audio/*,';
60 | }
61 |
62 | if (acceptVideo) {
63 | accept += 'video/*,';
64 | }
65 |
66 | if (accept.endsWith(',')) {
67 | accept = accept.slice(0, -1);
68 | }
69 |
70 | if (!accept) {
71 | accept = 'image/*,audio/*,video/*';
72 | }
73 |
74 | return accept;
75 | }, [acceptImage, acceptAudio, acceptVideo]);
76 |
77 | const onFileChange = useCallback(
78 | (e: React.ChangeEvent) => {
79 | if (!e.target.files?.length) {
80 | return;
81 | }
82 |
83 | const file = e.target.files[0];
84 | const url = URL.createObjectURL(file);
85 | setCurrentValue(url);
86 | },
87 | [setCurrentValue, setType]
88 | );
89 |
90 | const updatePreview = useCallback(
91 | async (url: string) => {
92 | if (!url) {
93 | setType(undefined);
94 | return;
95 | }
96 |
97 | const res = await fetch(url);
98 | setType(res?.headers?.get('Content-Type')?.split('/')?.[0]);
99 | },
100 | [setType]
101 | );
102 |
103 | useEffect(() => {
104 | updatePreview(currentValue);
105 | }, [currentValue]);
106 |
107 | let preview = (
108 |
109 | {currentValue ? 'Unsupported file type.' : ''}
110 |
111 | );
112 |
113 | switch (type) {
114 | case 'image':
115 | preview = (
116 |
123 | );
124 | break;
125 | case 'video':
126 | preview = (
127 |
135 | );
136 | break;
137 | case 'audio':
138 | preview = (
139 |
146 | );
147 | break;
148 | }
149 |
150 | return (
151 |
159 |
160 | {preview}
161 |
162 |
168 |
169 |
170 | );
171 | };
172 |
--------------------------------------------------------------------------------
/src/VarNumber.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useMemo } from 'react';
2 |
3 | import { useVarUIValue } from './common/VarUIContext';
4 | import { roundValue } from './common/roundValue';
5 | import { IVarBaseInputProps, VarBase } from './VarBase';
6 | import { IconDown } from './icons/IconDown';
7 | import { IconUp } from './icons/IconUp';
8 | import { Number } from './common/Number';
9 |
10 | export interface IVarNumberProps extends IVarBaseInputProps {
11 | /**
12 | * Minimum value.
13 | */
14 | min?: number;
15 |
16 | /**
17 | * Maximum value.
18 | */
19 | max?: number;
20 |
21 | /**
22 | * Step.
23 | */
24 | step?: number;
25 |
26 | /**
27 | * Should the end result be rounded to an integer value.
28 | */
29 | integer?: boolean;
30 |
31 | /**
32 | * If true will display buttons that increase and decrease the value by step.
33 | * Step must be set.
34 | */
35 | showButtons?: boolean;
36 |
37 | /**
38 | * Unit to display to the right of the input field.
39 | */
40 | unit?: string;
41 | }
42 |
43 | /**
44 | * Integer/float number component. Accepts and provides numbers.
45 | */
46 | export const VarNumber = ({
47 | label,
48 | path,
49 | value,
50 | onChange,
51 | min,
52 | max,
53 | step = 1,
54 | integer,
55 | showButtons,
56 | disabled,
57 | readOnly,
58 | className,
59 | error,
60 | errorPath,
61 | unit,
62 | }: IVarNumberProps): JSX.Element => {
63 | const [currentValue, setCurrentValue, currentError] = useVarUIValue({
64 | path,
65 | fallbackValue: value,
66 | onChange,
67 | error,
68 | errorPath,
69 | });
70 | const round = useCallback(
71 | (value: number) => roundValue(value, min, max, step, !!integer),
72 | [min, max, step, integer]
73 | );
74 | const rounded = useMemo(() => round(currentValue), [currentValue, round]);
75 |
76 | const setValue = useCallback(
77 | (value: number) => {
78 | value = round(value);
79 | setCurrentValue(value);
80 | },
81 | [round, setCurrentValue]
82 | );
83 |
84 | return (
85 |
92 |
93 |
105 | {showButtons && (
106 | <>
107 | setValue(currentValue + step)}
110 | disabled={disabled || readOnly}
111 | >
112 |
113 |
114 | setValue(currentValue - step)}
117 | disabled={disabled || readOnly}
118 | >
119 |
120 |
121 | >
122 | )}
123 |
124 |
125 | );
126 | };
127 |
--------------------------------------------------------------------------------
/src/VarScope.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { clone, get, set } from 'radash';
3 |
4 | import { VarUIContext, useVarUIValue } from './common/VarUIContext';
5 | import { IVarBaseValueProps } from './VarBase';
6 |
7 | /**
8 | * Utility component that creates a context with a certain path as base.
9 | */
10 | export const VarScope = ({
11 | path,
12 | value,
13 | onChange,
14 | children,
15 | error,
16 | errorPath,
17 | }: React.PropsWithChildren>): JSX.Element => {
18 | const [currentValue, setCurrentValue, currentError] = useVarUIValue({
19 | path,
20 | fallbackValue: value,
21 | onChange,
22 | error,
23 | errorPath,
24 | });
25 |
26 | return (
27 |
31 | typeof subpath === 'string' ? get(currentValue, subpath) : undefined,
32 | setValue: (subpath: string, newValue: any) => {
33 | const newObject = set(clone(currentValue), subpath, newValue);
34 | setCurrentValue(newObject);
35 | },
36 | getError: (subpath?: string) => {
37 | return currentError && subpath
38 | ? get(currentError, subpath)
39 | : undefined;
40 | },
41 | }}
42 | >
43 | {children}
44 |
45 | );
46 | };
47 |
--------------------------------------------------------------------------------
/src/VarSelect.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react';
2 |
3 | import { useVarUIValue } from './common/VarUIContext';
4 | import { IVarBaseInputProps, VarBase } from './VarBase';
5 |
6 | export interface IVarSelectOption {
7 | /**
8 | * Key for the option. Also used as value if `value` is not specified.
9 | */
10 | key: string | number;
11 |
12 | /**
13 | * Option label.
14 | */
15 | label: string;
16 |
17 | /**
18 | * Option value. Key will be used if not specified.
19 | * Note: Will be serialized to JSON and deserialized when selected.
20 | */
21 | value?: any;
22 | }
23 |
24 | export interface IVarSelectProps extends IVarBaseInputProps {
25 | /**
26 | * Options to be displayed.
27 | */
28 | options: IVarSelectOption[];
29 | }
30 |
31 | /**
32 | * Select component. Returns and accepts either `value` from option object or `key` when `value` is not provided.
33 | */
34 | export const VarSelect = ({
35 | label,
36 | path,
37 | value,
38 | onChange,
39 | options,
40 | disabled,
41 | readOnly,
42 | className,
43 | error,
44 | errorPath,
45 | }: IVarSelectProps): JSX.Element => {
46 | const [currentValue, setCurrentValue, currentError] = useVarUIValue({
47 | path,
48 | fallbackValue: value,
49 | onChange,
50 | error,
51 | errorPath,
52 | });
53 |
54 | const serializedCurrentValue = useMemo(
55 | () => JSON.stringify(currentValue),
56 | [currentValue]
57 | );
58 |
59 | const valueInOptions = !!options.find(
60 | option =>
61 | JSON.stringify(option.value ?? option.key) === serializedCurrentValue
62 | );
63 |
64 | return (
65 |
72 |
73 | setCurrentValue(JSON.parse(e.target.value))}
75 | value={serializedCurrentValue}
76 | title="Select options"
77 | disabled={disabled || readOnly}
78 | >
79 | {!valueInOptions && (
80 |
81 | {String(currentValue)}
82 |
83 | )}
84 | {options.map(option => {
85 | const serializedValue = JSON.stringify(option.value ?? option.key);
86 |
87 | return (
88 |
89 | {option.label}
90 |
91 | );
92 | })}
93 |
94 |
95 |
96 | );
97 | };
98 |
--------------------------------------------------------------------------------
/src/VarSlider.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useMemo, useRef } from 'react';
2 | import { usePointerDrag } from 'react-use-pointer-drag';
3 |
4 | import { Number } from './common/Number';
5 | import { useVarUIValue } from './common/VarUIContext';
6 | import { roundValue } from './common/roundValue';
7 | import { IVarBaseInputProps, VarBase } from './VarBase';
8 | import { IconDown } from './icons/IconDown';
9 | import { IconUp } from './icons/IconUp';
10 |
11 | export interface IVarSliderProps extends IVarBaseInputProps {
12 | /**
13 | * Minimum value.
14 | */
15 | min: number;
16 |
17 | /**
18 | * Maximum value.
19 | */
20 | max: number;
21 |
22 | /**
23 | * Step.
24 | */
25 | step: number;
26 |
27 | /**
28 | * Minimum value (for the input field, if not defined, will use `min`).
29 | */
30 | inputMin?: number;
31 |
32 | /**
33 | * Maximum value (for the input field, if not defined, will use `max`).
34 | */
35 | inputMax?: number;
36 |
37 | /**
38 | * Step (for the input field, if not defined, will use `step`).
39 | */
40 | inputStep?: number;
41 |
42 | /**
43 | * Should the end result be rounded to an integer value.
44 | */
45 | integer?: boolean;
46 |
47 | /**
48 | * If true will display an editable input, otherwise shows a read only value.
49 | */
50 | showInput?: boolean;
51 |
52 | /**
53 | * If true will display buttons that increase and decrease the value by step.
54 | */
55 | showButtons?: boolean;
56 |
57 | /**
58 | * Unit to display to the right of the input field.
59 | */
60 | unit?: string;
61 | }
62 |
63 | /**
64 | * Integer/float slider component. Accepts and provides numbers.
65 | */
66 | export const VarSlider = ({
67 | label,
68 | path,
69 | value,
70 | onChange,
71 | min,
72 | max,
73 | step,
74 | inputMin = min,
75 | inputMax = max,
76 | inputStep = step,
77 | integer,
78 | defaultValue,
79 | showInput,
80 | showButtons,
81 | disabled,
82 | readOnly,
83 | className,
84 | error,
85 | errorPath,
86 | unit,
87 | }: IVarSliderProps): JSX.Element => {
88 | const sliderRef = useRef(null);
89 | const [currentValue, setCurrentValue, currentError] = useVarUIValue({
90 | path,
91 | fallbackValue: value,
92 | onChange,
93 | error,
94 | errorPath,
95 | });
96 | const round = useCallback(
97 | (value: number) => roundValue(value, min, max, step, !!integer),
98 | [min, max, step, integer]
99 | );
100 | const roundInput = useCallback(
101 | (value: number) =>
102 | roundValue(value, inputMin, inputMax, inputStep, !!integer),
103 | [inputMin, inputMax, inputStep, integer]
104 | );
105 | const rounded = useMemo(() => round(currentValue), [currentValue, round]);
106 | const percent = useMemo(
107 | () => ((rounded - min) / (max - min)) * 100,
108 | [rounded, min, max]
109 | );
110 |
111 | const setValue = useCallback(
112 | (value: number) => {
113 | setCurrentValue(round(value));
114 | },
115 | [round, setCurrentValue]
116 | );
117 |
118 | const setValueInput = useCallback(
119 | (value: number) => {
120 | setCurrentValue(roundInput(value));
121 | },
122 | [roundInput, setCurrentValue]
123 | );
124 |
125 | const updatePosition = useCallback(
126 | (x: number) => {
127 | const div = sliderRef.current!;
128 | const rect = div.getBoundingClientRect();
129 | const percent = (x - rect.left) / rect.width;
130 | setValue(min + (max - min) * percent);
131 | },
132 | [setValue, integer, min, max, step]
133 | );
134 |
135 | const { dragProps } = usePointerDrag({
136 | onMove: ({ x }) => updatePosition(x),
137 | });
138 |
139 | useEffect(() => {
140 | sliderRef.current?.addEventListener('wheel', e => e.preventDefault());
141 | }, []);
142 |
143 | return (
144 |
151 |
152 |
updatePosition(e.clientX)}
156 | onDoubleClick={() =>
157 | typeof defaultValue !== 'undefined' && setValue(defaultValue)
158 | }
159 | onWheel={e => setValue(currentValue - step * Math.sign(e.deltaY))}
160 | title="Slider"
161 | {...dragProps()}
162 | >
163 |
167 |
168 | {showInput ? (
169 |
181 | ) : (
182 |
183 | {rounded.toString()}
184 | {unit}
185 |
186 | )}
187 | {showButtons && (
188 | <>
189 |
setValue(currentValue + step)}
192 | disabled={disabled || readOnly}
193 | >
194 |
195 |
196 |
setValue(currentValue - step)}
199 | disabled={disabled || readOnly}
200 | >
201 |
202 |
203 | >
204 | )}
205 |
206 |
207 | );
208 | };
209 |
--------------------------------------------------------------------------------
/src/VarString.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties, useMemo } from 'react';
2 |
3 | import { useVarUIValue } from './common/VarUIContext';
4 | import { IVarBaseInputProps, VarBase } from './VarBase';
5 |
6 | export interface IVarStringProps extends IVarBaseInputProps {
7 | /**
8 | * Maximum length of the text.
9 | */
10 | maxLength?: number;
11 |
12 | /**
13 | * Should the field be a textarea?
14 | */
15 | multiline?: boolean;
16 |
17 | /**
18 | * Should the text field auto expand?
19 | * Only works with multiline instances.
20 | */
21 | autoexpand?: boolean;
22 | }
23 |
24 | /**
25 | * String input component. Accepts and provides a string value.
26 | */
27 | export const VarString = ({
28 | label,
29 | path,
30 | value,
31 | onChange,
32 | maxLength,
33 | multiline,
34 | autoexpand,
35 | disabled,
36 | readOnly,
37 | className,
38 | error,
39 | errorPath,
40 | }: IVarStringProps): JSX.Element => {
41 | const [currentValue, setCurrentValue, currentError] = useVarUIValue({
42 | path,
43 | fallbackValue: value,
44 | onChange,
45 | error,
46 | errorPath,
47 | });
48 |
49 | const autoexpandOnInput = (event: React.FormEvent) => {
50 | const textarea = event.currentTarget;
51 | textarea.style.height = '0';
52 | textarea.style.height = `${textarea.scrollHeight}px`;
53 | };
54 |
55 | const textareaStyle: CSSProperties | undefined = useMemo(
56 | () => (autoexpand ? { overflow: 'hidden', resize: 'none' } : undefined),
57 | [autoexpand]
58 | );
59 |
60 | return (
61 |
69 | {multiline ? (
70 |
92 | );
93 | };
94 |
--------------------------------------------------------------------------------
/src/VarToggle.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { useVarUIValue } from './common/VarUIContext';
4 | import { IVarBaseInputProps, VarBase } from './VarBase';
5 |
6 | export interface IVarToggleProps extends IVarBaseInputProps {}
7 |
8 | /**
9 | * Checkbox/toggle component. Accepts and returns a boolean (true/false).
10 | */
11 | export const VarToggle = ({
12 | label,
13 | path,
14 | value,
15 | onChange,
16 | disabled,
17 | readOnly,
18 | className,
19 | error,
20 | errorPath,
21 | }: IVarToggleProps): JSX.Element => {
22 | const [currentValue, setCurrentValue, currentError] = useVarUIValue({
23 | path,
24 | fallbackValue: value,
25 | onChange,
26 | error,
27 | errorPath,
28 | });
29 |
30 | return (
31 |
38 |
39 |
40 | setCurrentValue(e.target.checked)}
44 | disabled={disabled}
45 | readOnly={readOnly}
46 | />
47 |
48 |
49 |
50 |
51 | );
52 | };
53 |
--------------------------------------------------------------------------------
/src/VarUI.scss:
--------------------------------------------------------------------------------
1 | $foreground-color: var(--react-var-ui-foreground-color, #ddd);
2 | $accent-color: var(--react-var-ui-accent-color, #77f);
3 | $input-background-color: var(--react-var-ui-input-background-color, #353542);
4 | $input-background-hover-color: var(
5 | --react-var-ui-input-background-hover-color,
6 | #424253
7 | );
8 | $input-background-pressed-color: var(
9 | --react-var-ui-input-background-pressed-color,
10 | #2b2b37
11 | );
12 | $label-background-normal-color: var(
13 | --react-var-ui-label-background-normal-color,
14 | #22222a
15 | );
16 | $label-background-hover-color: var(
17 | --react-var-ui-label-background-hover-color,
18 | #2a2a33
19 | );
20 | $label-border-color: var(--react-var-ui-label-border-color, #33333a);
21 | $transition-time: 0.2s;
22 | $error-color: var(--react-var-ui-error-color, #f00);
23 |
24 | .react-var-ui {
25 | &-disabled {
26 | pointer-events: none;
27 |
28 | & > * {
29 | opacity: 0.5;
30 | }
31 | }
32 |
33 | &-read-only {
34 | .react-var-ui-interactive {
35 | pointer-events: none;
36 | }
37 | }
38 |
39 | &-category {
40 | &-title {
41 | display: flex;
42 | align-items: center;
43 | justify-content: space-between;
44 | color: $foreground-color;
45 | text-transform: uppercase;
46 | font-size: 0.7rem;
47 | padding: 0.4rem;
48 | user-select: none;
49 | font-weight: bold;
50 | }
51 |
52 | &-collapse {
53 | background: none;
54 | border: none;
55 | color: $foreground-color;
56 | font-size: 0;
57 | cursor: pointer;
58 | }
59 |
60 | padding-bottom: 1rem;
61 | }
62 |
63 | &-color {
64 | display: flex;
65 | align-items: center;
66 | position: relative;
67 | margin-left: 1rem;
68 |
69 | &-popover {
70 | position: absolute;
71 | z-index: 4;
72 | right: 0;
73 | bottom: 0;
74 | }
75 |
76 | .w-color-sketch {
77 | position: absolute;
78 | top: 0.5rem;
79 | right: 0;
80 | }
81 |
82 | &-color {
83 | position: relative;
84 | width: 2.5rem;
85 | height: 1.2rem;
86 | border-radius: 0.2rem;
87 | }
88 |
89 | &-swatch {
90 | border-radius: 0.2rem;
91 | display: inline-block;
92 | cursor: pointer;
93 | background: white
94 | url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAAAAxSURBVDhPY2RgYBABYnzgDT5JxlEDGIZJGBBIBvjTCSgMCAG8CW3UAAZwQiIEaBuIACBfCLFwf0bOAAAAAElFTkSuQmCC')
95 | center center;
96 | }
97 |
98 | &-cover {
99 | position: fixed;
100 | top: 0;
101 | right: 0;
102 | bottom: 0;
103 | left: 0;
104 | }
105 | }
106 |
107 | &-toggle {
108 | position: relative;
109 | width: 2.5rem;
110 | height: 1.2rem;
111 |
112 | &-helper {
113 | position: absolute;
114 | top: 0;
115 | left: 0;
116 | right: 0;
117 | bottom: 0;
118 | background: $input-background-color;
119 | border-radius: 0.2rem;
120 | transition: $transition-time ease-in-out all;
121 | cursor: pointer;
122 |
123 | &:before {
124 | position: absolute;
125 | height: 0.8rem;
126 | width: 0.8rem;
127 | left: 0.3rem;
128 | top: 0.2rem;
129 | border-radius: 1rem;
130 | background-color: $foreground-color;
131 | content: '';
132 | transition: $transition-time ease-in-out all;
133 | }
134 | }
135 |
136 | input {
137 | width: 0;
138 | height: 0;
139 | opacity: 0.01;
140 |
141 | &:checked + .react-var-ui-toggle-helper {
142 | background: $accent-color !important;
143 |
144 | &:before {
145 | transform: translateX(1.1rem);
146 | }
147 | }
148 | }
149 | }
150 |
151 | &-select {
152 | select {
153 | width: 100%;
154 | background: $input-background-color;
155 | border: none;
156 | border-radius: 0.2rem;
157 | color: $foreground-color;
158 | padding: 0.2rem 0.5rem;
159 |
160 | &:hover,
161 | &:focus {
162 | background: $input-background-hover-color;
163 | }
164 | }
165 | }
166 |
167 | &-string {
168 | &-multiline,
169 | input {
170 | width: 100%;
171 | min-width: 100%;
172 | max-width: 100%;
173 | background: $input-background-color;
174 | border: none;
175 | border-radius: 0.2rem;
176 | color: $foreground-color;
177 | padding: 0.2rem 0.5rem;
178 | outline: none !important;
179 | box-sizing: border-box;
180 | border-radius: 0.4rem;
181 |
182 | &:hover,
183 | &:focus {
184 | background: $input-background-hover-color;
185 | }
186 | }
187 |
188 | &-multiline {
189 | margin-top: 0.4rem;
190 | font-family: inherit;
191 | padding: 0.5rem;
192 | font-size: 0.8rem;
193 | min-height: 2rem;
194 | }
195 | }
196 |
197 | &-label {
198 | display: grid;
199 | grid-template-columns: 1fr 2fr;
200 | padding: 0.5rem 1rem;
201 | font-size: 0.8rem;
202 | color: $foreground-color;
203 | background: $label-background-normal-color;
204 | border-bottom: 1px solid $label-border-color;
205 | user-select: none;
206 |
207 | &-column {
208 | display: block;
209 | }
210 |
211 | &:first-child {
212 | border-top: 1px solid $label-border-color;
213 | }
214 |
215 | &:hover {
216 | background: $label-background-hover-color;
217 | }
218 |
219 | & > span {
220 | display: flex;
221 | align-items: center;
222 |
223 | &:last-child {
224 | justify-content: flex-end;
225 | }
226 | }
227 |
228 | &-has-label {
229 | & > span {
230 | &:first-child {
231 | margin-right: 0.2rem;
232 | }
233 | }
234 | }
235 |
236 | &-no-label {
237 | grid-template-columns: 1fr;
238 | }
239 | }
240 |
241 | &-slider,
242 | &-number {
243 | display: flex;
244 | align-items: center;
245 | justify-content: flex-end;
246 | width: 100%;
247 |
248 | &-track {
249 | background: $input-background-color;
250 | position: relative;
251 | flex: 1;
252 | height: 1.3rem;
253 | border-radius: 0.2rem;
254 | overflow: hidden;
255 | cursor: pointer;
256 | margin-right: 0.4rem;
257 | touch-action: none;
258 | }
259 |
260 | &-content {
261 | background: $accent-color;
262 | position: absolute;
263 | top: 0;
264 | bottom: 0;
265 | left: 0;
266 | pointer-events: none;
267 | }
268 |
269 | input {
270 | width: 2.5rem;
271 | background: $label-background-normal-color;
272 | border: none;
273 | -moz-appearance: textfield;
274 | color: $foreground-color;
275 | outline: none !important;
276 | height: 1.3rem;
277 | padding: 0 0.2rem;
278 | text-align: right;
279 | border-radius: 0.25rem;
280 |
281 | &::-webkit-outer-spin-button,
282 | &::-webkit-inner-spin-button {
283 | -webkit-appearance: none;
284 | margin: 0;
285 | }
286 | }
287 |
288 | span {
289 | width: 2.5rem;
290 | text-align: right;
291 | }
292 |
293 | button {
294 | background: $label-background-normal-color;
295 | border: none;
296 | cursor: pointer;
297 | color: $foreground-color;
298 | height: 1.3rem;
299 | margin-left: 0.4rem;
300 | border-radius: 0.25rem;
301 |
302 | display: flex;
303 | justify-content: center;
304 | align-items: center;
305 | }
306 | }
307 |
308 | &-number {
309 | input {
310 | flex: 1;
311 | max-width: 10rem;
312 | background: $input-background-color;
313 |
314 | &:hover,
315 | &:focus {
316 | background: $input-background-hover-color;
317 | }
318 | }
319 | }
320 |
321 | &-xy {
322 | &-value {
323 | display: inline-block !important;
324 | text-align: right;
325 | }
326 |
327 | &-space {
328 | margin-top: 0.5rem;
329 | margin-bottom: 0.3rem;
330 | width: 100%;
331 | height: 250px;
332 | background: $input-background-color;
333 | position: relative;
334 | cursor: pointer;
335 | border-radius: 0.4rem;
336 | overflow: hidden;
337 | touch-action: none;
338 | }
339 |
340 | &-control {
341 | background: $accent-color;
342 | width: 1rem;
343 | height: 1rem;
344 | border-radius: 100%;
345 | transform: translate(-50%, -50%);
346 | position: absolute;
347 | pointer-events: none;
348 |
349 | &:after,
350 | &:before {
351 | content: '';
352 | position: absolute;
353 | background: $accent-color;
354 | opacity: 0.5;
355 | }
356 |
357 | &:before {
358 | top: -100rem;
359 | bottom: -100rem;
360 | left: calc(50% - 1px);
361 | width: 1px;
362 | }
363 |
364 | &:after {
365 | left: -100rem;
366 | right: -100rem;
367 | top: calc(50% - 1px);
368 | height: 1px;
369 | }
370 | }
371 | }
372 |
373 | &-angle {
374 | display: flex;
375 | justify-content: center;
376 | align-items: center;
377 | padding-top: 1rem;
378 |
379 | &-value {
380 | justify-content: flex-end;
381 | }
382 |
383 | &-control {
384 | width: 6rem;
385 | height: 6rem;
386 | background: $input-background-color;
387 | position: relative;
388 | border-radius: 100%;
389 | cursor: pointer;
390 | touch-action: none;
391 |
392 | &:after {
393 | display: block;
394 | position: absolute;
395 | left: calc(50% - 0.5rem);
396 | top: 0.5rem;
397 | width: 1rem;
398 | height: 1rem;
399 | background: $accent-color;
400 | border-radius: 100%;
401 | content: '';
402 | }
403 | }
404 | }
405 |
406 | &-image,
407 | &-media,
408 | &-file {
409 | margin-top: 0.5rem;
410 | margin-bottom: 0.3rem;
411 | width: 100%;
412 | height: 150px;
413 | background-color: $input-background-color;
414 | position: relative;
415 | border-radius: 0.4rem;
416 | overflow: hidden;
417 | touch-action: none;
418 | display: flex;
419 | justify-content: center;
420 | align-items: center;
421 | cursor: pointer;
422 |
423 | &-value {
424 | display: inline-block !important;
425 | text-align: right;
426 | text-overflow: ellipsis;
427 | white-space: nowrap;
428 | overflow: hidden;
429 | }
430 |
431 | &-background,
432 | &-metadata,
433 | &-audio {
434 | position: absolute;
435 | top: 0;
436 | left: 0;
437 | right: 0;
438 | bottom: 0;
439 | pointer-events: none;
440 | z-index: 0;
441 | }
442 |
443 | video {
444 | width: 100%;
445 | height: 100%;
446 | object-fit: cover;
447 | }
448 |
449 | &-audio {
450 | display: block;
451 | top: auto;
452 | width: 100%;
453 | height: 50px;
454 | z-index: 5;
455 | pointer-events: all;
456 | }
457 |
458 | &-background {
459 | background-size: cover;
460 | background-position: 50% 50%;
461 | filter: brightness(0.5) grayscale(0.5);
462 | transition: $transition-time ease-in-out all;
463 | transform: scale3d(1, 1, 1);
464 | }
465 |
466 | &-metadata {
467 | padding: 10px;
468 | }
469 |
470 | &:hover &-background {
471 | filter: brightness(0.7) grayscale(0.2);
472 | transform: scale3d(1.05, 1.05, 1);
473 | }
474 |
475 | svg {
476 | width: 25px;
477 | height: 25px;
478 | z-index: 1;
479 | pointer-events: none;
480 | }
481 |
482 | input {
483 | position: absolute;
484 | top: 0;
485 | left: 0;
486 | right: 0;
487 | bottom: 0;
488 | cursor: pointer;
489 | opacity: 0.001;
490 | width: 100%;
491 | z-index: 2;
492 | }
493 | }
494 |
495 | &-file,
496 | &-angle,
497 | &-xy {
498 | grid-column: 1 / 3;
499 | }
500 |
501 | &-button {
502 | button {
503 | width: 100%;
504 | color: $foreground-color;
505 | background: $input-background-color;
506 | border: none;
507 | border-radius: 0.4rem;
508 | padding: 0.4rem 1rem;
509 | cursor: pointer;
510 |
511 | &:hover {
512 | background: $input-background-hover-color;
513 | }
514 |
515 | &:active {
516 | background: $input-background-pressed-color;
517 | }
518 | }
519 | }
520 |
521 | &-error {
522 | color: $error-color;
523 | grid-column: 1 / 3;
524 | }
525 | }
526 |
--------------------------------------------------------------------------------
/src/VarUI.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, useMemo, useCallback } from 'react';
2 | import { clone, set, get } from 'radash';
3 |
4 | import { VarUIContext } from './common/VarUIContext';
5 |
6 | export interface IVarUIProps {
7 | /**
8 | * A JavaScript object or array to be mutated by the input components.
9 | */
10 | values: T;
11 |
12 | /**
13 | * A JavaScript object holding error information.
14 | */
15 | errors?: any;
16 |
17 | /**
18 | * @deprecated Replaced by onChange
19 | */
20 | updateValues?: (values: T) => void;
21 |
22 | /**
23 | * The function to be called with the entire changed object.
24 | */
25 | onChange?: (values: T) => void;
26 |
27 | /**
28 | * The function to be called when one value is changed.
29 | */
30 | onChangeValue?: (path: string, newValue: any) => void;
31 |
32 | /**
33 | * Additional class names for the wrapper object.
34 | */
35 | className?: string;
36 |
37 | /**
38 | * Input components (or any other children).
39 | */
40 | children?: ReactNode;
41 | }
42 |
43 | /**
44 | * This is the main component which provides a Context for other components.
45 | * It is not required to use this component - other components accept
46 | * `onChange` and `value` properties which provide a similar functionality.
47 | */
48 | export const VarUI: (
49 | props: IVarUIProps
50 | ) => JSX.Element = ({
51 | values,
52 | errors,
53 | updateValues,
54 | onChange,
55 | onChangeValue,
56 | className,
57 | children,
58 | }) => {
59 | const getValue = useCallback(
60 | (path?: string) =>
61 | typeof path === 'string' ? get(values, path) : undefined,
62 | [values]
63 | );
64 |
65 | const setValue = useCallback(
66 | (path: string, value: any) => {
67 | onChangeValue?.(path, value);
68 | const newValues = path === '' ? value : set(clone(values), path, value);
69 | updateValues?.(newValues);
70 | onChange?.(newValues);
71 | },
72 | [values, updateValues, onChange, onChangeValue]
73 | );
74 |
75 | const getError = useCallback(
76 | (path?: string) =>
77 | errors && typeof path === 'string' ? get(errors, path) : undefined,
78 | [errors]
79 | );
80 |
81 | const contextValue = useMemo(
82 | () => ({ values, getValue, setValue, getError }),
83 | [values, getValue, setValue]
84 | );
85 |
86 | return (
87 |
88 | {children}
89 |
90 | );
91 | };
92 |
--------------------------------------------------------------------------------
/src/VarXY.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useMemo, useRef } from 'react';
2 | import { usePointerDrag } from 'react-use-pointer-drag';
3 |
4 | import { useVarUIValue } from './common/VarUIContext';
5 | import { IVarBaseInputProps, VarBase } from './VarBase';
6 |
7 | export type IVarXYValue = [number, number];
8 |
9 | export interface IVarXYProps extends IVarBaseInputProps {
10 | /**
11 | * Minimum value.
12 | */
13 | min?: IVarXYValue;
14 |
15 | /**
16 | * Maximum value.
17 | */
18 | max?: IVarXYValue;
19 |
20 | /**
21 | * Step.
22 | */
23 | step?: IVarXYValue;
24 | }
25 |
26 | function roundValue(
27 | value: IVarXYValue,
28 | min: IVarXYValue,
29 | max: IVarXYValue,
30 | step: IVarXYValue
31 | ): IVarXYValue {
32 | const result: IVarXYValue = [0, 0];
33 |
34 | if (!value || !Array.isArray(value) || value.length < 2) {
35 | return result;
36 | }
37 |
38 | for (let i = 0; i < step.length; i++) {
39 | const decimalPlaces = step[i].toString().split('.')[1]?.length || 0;
40 | result[i] = Math.round(value[i] / step[i]) * step[i];
41 | result[i] = Math.max(min[i], result[i]);
42 | result[i] = Math.min(max[i], result[i]);
43 |
44 | result[i] = parseFloat(result[i].toFixed(decimalPlaces));
45 | }
46 |
47 | return result;
48 | }
49 |
50 | function percentValue(
51 | value: IVarXYValue,
52 | min: IVarXYValue,
53 | max: IVarXYValue
54 | ): IVarXYValue {
55 | if (!value) {
56 | return [50, 50];
57 | }
58 |
59 | const result: IVarXYValue = [0, 0];
60 |
61 | for (let i = 0; i < value.length; i++) {
62 | result[i] = ((value[i] - min[i]) / (max[i] - min[i])) * 100;
63 | }
64 |
65 | return result;
66 | }
67 |
68 | /**
69 | * XY offset picker. Accepts and provides an array in form of [x, y].
70 | */
71 | export const VarXY = ({
72 | label,
73 | path,
74 | value,
75 | onChange,
76 | disabled,
77 | readOnly,
78 | className,
79 | defaultValue = [0, 0],
80 | min = [-1.0, -1.0],
81 | max = [1.0, 1.0],
82 | step = [0.01, 0.01],
83 | error,
84 | errorPath,
85 | }: IVarXYProps): JSX.Element => {
86 | const sliderRef = useRef(null);
87 | const [currentValue, setCurrentValue, currentError] = useVarUIValue({
88 | path,
89 | fallbackValue: value,
90 | onChange,
91 | error,
92 | errorPath,
93 | });
94 | const rounded = useMemo(
95 | () => roundValue(currentValue, min, max, step),
96 | [currentValue, min, max, step]
97 | );
98 | const percent = useMemo(
99 | () => percentValue(rounded, min, max),
100 | [rounded, min, max]
101 | );
102 |
103 | const updatePosition = useCallback(
104 | (x: number, y: number) => {
105 | if (!sliderRef.current) {
106 | return;
107 | }
108 |
109 | const div = sliderRef.current;
110 | const rect = div.getBoundingClientRect();
111 |
112 | const percentX = (x - rect.left) / rect.width;
113 | const percentY = (y - rect.top) / rect.height;
114 |
115 | const value = roundValue(
116 | [
117 | min[0] + (max[0] - min[0]) * percentX,
118 | min[1] + (max[1] - min[1]) * percentY,
119 | ],
120 | min,
121 | max,
122 | step
123 | );
124 | setCurrentValue(value);
125 | },
126 | [setCurrentValue, min, max, step]
127 | );
128 |
129 | const { dragProps } = usePointerDrag({
130 | onMove: ({ x, y }) => updatePosition(x, y),
131 | });
132 |
133 | const reset = useCallback(() => {
134 | if (typeof defaultValue !== 'undefined') {
135 | setCurrentValue(defaultValue);
136 | }
137 | }, [defaultValue, setCurrentValue]);
138 |
139 | return (
140 |
147 |
148 | {rounded[0]}, {rounded[1]}
149 |
150 |
151 |
updatePosition(e.clientX, e.clientY)}
155 | onDoubleClick={reset}
156 | {...dragProps()}
157 | >
158 |
162 |
163 |
164 |
165 | );
166 | };
167 |
--------------------------------------------------------------------------------
/src/common/Number.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useRef } from 'react';
2 |
3 | export interface INumberProps {
4 | value: number;
5 | onChange: (value: number) => void;
6 | round: (value: number) => number;
7 | min?: number;
8 | max?: number;
9 | step: number;
10 | className?: string;
11 | disabled?: boolean;
12 | readOnly?: boolean;
13 | unit?: string;
14 | }
15 |
16 | /**
17 | * Integer/float slider component. Accepts and provides numbers.
18 | */
19 | export const Number = ({
20 | value,
21 | onChange,
22 | min,
23 | max,
24 | step,
25 | className,
26 | disabled,
27 | readOnly,
28 | round,
29 | unit,
30 | }: INumberProps): JSX.Element => {
31 | const inputRef = useRef(null);
32 |
33 | const setValue = useCallback(
34 | (value: number) => {
35 | if (inputRef.current) {
36 | inputRef.current.value = String(round(value));
37 | }
38 |
39 | onChange(value);
40 | },
41 | [onChange, round]
42 | );
43 |
44 | useEffect(() => {
45 | if (inputRef.current) {
46 | inputRef.current.value = String(value);
47 | }
48 | }, [value]);
49 |
50 | const updateValueFromInput = () => {
51 | const input = inputRef.current;
52 | if (!input) {
53 | return;
54 | }
55 |
56 | setValue(parseFloat(input.value));
57 | };
58 |
59 | const events =
60 | disabled || readOnly
61 | ? {}
62 | : {
63 | onBlur: updateValueFromInput,
64 | onKeyDown: (e: React.KeyboardEvent) => {
65 | const input = inputRef.current!;
66 | switch (e.key) {
67 | case 'ArrowUp':
68 | e.preventDefault();
69 | setValue(parseFloat(input.value) + step);
70 | break;
71 | case 'ArrowDown':
72 | e.preventDefault();
73 | setValue(parseFloat(input.value) - step);
74 | break;
75 | }
76 | },
77 | onKeyUp: (e: React.KeyboardEvent) => {
78 | if (e.key === 'Enter') {
79 | e.preventDefault();
80 | updateValueFromInput();
81 | }
82 | },
83 | };
84 |
85 | return (
86 |
87 |
97 | {unit ? {unit} : null}
98 |
99 | );
100 | };
101 |
--------------------------------------------------------------------------------
/src/common/VarUIContext.ts:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useMemo, useCallback } from 'react';
2 |
3 | export interface IVarUIContext {
4 | values: any;
5 | getValue: (path?: string) => any;
6 | setValue: (path: string, value: any) => void;
7 | getError: (path?: string) => any;
8 | }
9 |
10 | export const VarUIContext = createContext(undefined);
11 |
12 | export interface UseVarUIValueOptions {
13 | path?: string;
14 | fallbackValue?: T;
15 | onChange?: (value: T) => void;
16 | errorPath?: string;
17 | error?: string;
18 | }
19 |
20 | /**
21 | * Simple function used for custom input components.
22 | * @param path
23 | * @param fallbackValue
24 | * @param onChange
25 | * @returns [value: T, setValue: (value: T) => void]
26 | */
27 | export function useVarUIValue({
28 | path,
29 | fallbackValue,
30 | onChange,
31 | errorPath,
32 | error,
33 | }: UseVarUIValueOptions): [T, (value: T) => void, string | undefined] {
34 | const context = useContext(VarUIContext);
35 | const value = useMemo(
36 | () => context?.getValue(path) ?? fallbackValue,
37 | [context, path, fallbackValue]
38 | );
39 | const setValue = useCallback(
40 | (value: T) => {
41 | if (typeof path === 'string' && context) {
42 | context.setValue(path, value);
43 | }
44 |
45 | onChange?.(value);
46 | },
47 | [path, context, onChange]
48 | );
49 | error ||= context?.getError(errorPath || path);
50 |
51 | return [value, setValue, error];
52 | }
53 |
--------------------------------------------------------------------------------
/src/common/roundValue.ts:
--------------------------------------------------------------------------------
1 | export function roundValue(
2 | value: number,
3 | min?: number,
4 | max?: number,
5 | step?: number,
6 | integer?: boolean
7 | ): number {
8 | if (!isFinite(value)) {
9 | value = min ?? 0;
10 | }
11 |
12 | let decimalPlaces = 2;
13 | if (typeof step === 'number') {
14 | decimalPlaces = step.toString().split('.')[1]?.length || 0;
15 | value = Math.round(value / step) * step;
16 | }
17 |
18 | if (typeof min === 'number') {
19 | value = Math.max(min, value);
20 | }
21 |
22 | if (typeof max === 'number') {
23 | value = Math.min(max, value);
24 | }
25 |
26 | return integer ? Math.round(value) : parseFloat(value.toFixed(decimalPlaces));
27 | }
28 |
--------------------------------------------------------------------------------
/src/icons/IconDown.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const IconDown = (): JSX.Element => {
4 | return (
5 |
12 |
16 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/src/icons/IconUp.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const IconUp = (): JSX.Element => {
4 | return (
5 |
12 |
16 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/src/icons/IconUpload.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const IconUpload = (): JSX.Element => {
4 | return (
5 |
12 |
13 |
14 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import './VarUI.scss';
2 |
3 | export * from './VarUI';
4 |
5 | // Inputs
6 | export * from './VarAngle';
7 | export * from './VarArray';
8 | export * from './VarBase';
9 | export * from './VarButton';
10 | export * from './VarColor';
11 | export * from './VarDisplay';
12 | export * from './VarFile';
13 | export * from './VarImage';
14 | export * from './VarMedia';
15 | export * from './VarNumber';
16 | export * from './VarScope';
17 | export * from './VarSelect';
18 | export * from './VarSlider';
19 | export * from './VarString';
20 | export * from './VarToggle';
21 | export * from './VarXY';
22 |
23 | // Other
24 | export * from './VarCategory';
25 |
26 | // Utility
27 | export { useVarUIValue } from './common/VarUIContext';
28 |
--------------------------------------------------------------------------------
/stories/VarAngle.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta, StoryFn } from '@storybook/react';
3 | import { useCallback, useState } from '@storybook/preview-api';
4 |
5 | import { VarAngle, IVarAngleProps } from '../src';
6 |
7 | export default {
8 | title: 'VarAngle',
9 | component: VarAngle,
10 | argTypes: {
11 | defaultValue: {
12 | control: { type: 'range', min: 0, max: Math.PI * 2, step: 0.1 },
13 | },
14 | label: { control: { type: 'text' } },
15 | disabled: { control: 'boolean' },
16 | },
17 | args: {
18 | defaultValue: 0,
19 | label: 'VarAngle',
20 | disabled: false,
21 | },
22 | parameters: {
23 | controls: { expanded: true },
24 | actions: { argTypesRegex: '^on.*' },
25 | },
26 | } as Meta;
27 |
28 | const Template: StoryFn = args => {
29 | const [value, setValue] = useState(0);
30 | const onChange = useCallback(
31 | value => {
32 | setValue(value);
33 | args.onChange?.(value);
34 | },
35 | [setValue, args]
36 | );
37 | return ;
38 | };
39 |
40 | export const Default = Template.bind({});
41 | Default.args = {};
42 | Default.storyName = 'default';
43 |
44 | export const Disabled = Template.bind({});
45 | Disabled.args = {
46 | disabled: true,
47 | };
48 | Disabled.storyName = 'disabled: true';
49 |
50 | export const ReadOnly = Template.bind({});
51 | ReadOnly.args = {
52 | readOnly: true,
53 | };
54 | ReadOnly.storyName = 'readOnly: true';
55 |
--------------------------------------------------------------------------------
/stories/VarButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta, StoryFn } from '@storybook/react';
3 |
4 | import { VarButton, IVarButtonProps } from '../src';
5 |
6 | export default {
7 | title: 'VarButton',
8 | component: VarButton,
9 | argTypes: {
10 | label: { control: 'text' },
11 | buttonLabel: { control: 'text' },
12 | disabled: { control: 'boolean' },
13 | },
14 | args: {
15 | min: 0,
16 | max: 10,
17 | step: 1,
18 | label: 'VarButton',
19 | buttonLabel: 'VarButton',
20 | disabled: false,
21 | },
22 | parameters: {
23 | controls: { expanded: true },
24 | actions: { argTypesRegex: '^on.*' },
25 | },
26 | } as Meta;
27 |
28 | const Template: StoryFn = args => ;
29 |
30 | export const Default = Template.bind({});
31 | Default.args = {};
32 | Default.storyName = 'default';
33 |
34 | export const Disabled = Template.bind({});
35 | Disabled.args = {
36 | disabled: true,
37 | };
38 | Disabled.storyName = 'disabled: true';
39 |
40 | export const NoLabel = Template.bind({});
41 | NoLabel.args = {
42 | label: undefined,
43 | };
44 | NoLabel.storyName = 'no label';
45 |
--------------------------------------------------------------------------------
/stories/VarCategory.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta, StoryFn } from '@storybook/react';
3 |
4 | import { VarCategory, IVarCategoryProps, VarString } from '../src';
5 |
6 | export default {
7 | title: 'VarCategory',
8 | component: VarCategory,
9 | argTypes: {
10 | label: { control: 'text' },
11 | collapsible: { control: 'boolean' },
12 | },
13 | args: {
14 | label: 'VarCategory',
15 | collapsible: false,
16 | },
17 | parameters: {
18 | controls: { expanded: true },
19 | },
20 | } as Meta;
21 |
22 | const Template: StoryFn = args => (
23 |
24 |
25 |
26 | );
27 |
28 | export const Default = Template.bind({});
29 | Default.args = {};
30 | Default.storyName = 'default';
31 |
32 | export const Collapsible = Template.bind({});
33 | Collapsible.args = {
34 | collapsible: true,
35 | };
36 | Collapsible.storyName = 'collapsible: true';
37 |
--------------------------------------------------------------------------------
/stories/VarColor.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta, StoryFn } from '@storybook/react';
3 | import { useCallback, useState } from '@storybook/preview-api';
4 |
5 | import { VarColor, IVarColorProps } from '../src';
6 |
7 | export default {
8 | title: 'VarColor',
9 | component: VarColor,
10 | argTypes: {
11 | label: { control: 'text' },
12 | disabled: { control: 'boolean' },
13 | },
14 | args: {
15 | label: 'VarColor',
16 | disabled: false,
17 | },
18 | parameters: {
19 | controls: { expanded: true },
20 | actions: { argTypesRegex: '^on.*' },
21 | },
22 | } as Meta;
23 |
24 | const Template: StoryFn = args => {
25 | const [value, setValue] = useState('#ff0000');
26 | const onChange = useCallback(
27 | value => {
28 | setValue(value);
29 | args.onChange?.(value);
30 | },
31 | [setValue, args]
32 | );
33 | return ;
34 | };
35 |
36 | export const Default = Template.bind({});
37 | Default.args = {};
38 | Default.storyName = 'default';
39 |
40 | export const Disabled = Template.bind({});
41 | Disabled.args = {
42 | disabled: true,
43 | };
44 | Disabled.storyName = 'disabled: true';
45 |
46 | export const Alpha = Template.bind({});
47 | Alpha.args = {
48 | alpha: true,
49 | };
50 | Alpha.storyName = 'alpha: true';
51 |
52 | export const ReadOnly = Template.bind({});
53 | ReadOnly.args = {
54 | readOnly: true,
55 | };
56 | ReadOnly.storyName = 'readOnly: true';
57 |
--------------------------------------------------------------------------------
/stories/VarDisplay.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta, StoryFn } from '@storybook/react';
3 |
4 | import { VarDisplay, IVarDisplayProps } from '../src';
5 |
6 | export default {
7 | title: 'VarDisplay',
8 | component: VarDisplay,
9 | argTypes: {
10 | value: { control: 'text' },
11 | label: { control: 'text' },
12 | disabled: { control: 'boolean' },
13 | },
14 | args: {
15 | label: 'VarDisplay',
16 | disabled: false,
17 | },
18 | parameters: {
19 | controls: { expanded: true },
20 | },
21 | } as Meta;
22 |
23 | const Template: StoryFn = args => ;
24 |
25 | export const Default = Template.bind({});
26 | Default.args = {};
27 | Default.storyName = 'default';
28 |
29 | export const Disabled = Template.bind({});
30 | Disabled.args = {
31 | disabled: true,
32 | };
33 | Disabled.storyName = 'disabled: true';
34 |
35 | export const ReadOnly = Template.bind({});
36 | ReadOnly.args = {
37 | readOnly: true,
38 | };
39 | ReadOnly.storyName = 'readOnly: true';
40 |
--------------------------------------------------------------------------------
/stories/VarFile.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta, StoryFn } from '@storybook/react';
3 | import { useCallback, useState } from '@storybook/preview-api';
4 |
5 | import { VarFile, IVarFileProps } from '../src';
6 |
7 | export default {
8 | title: 'VarFile',
9 | component: VarFile,
10 | argTypes: {
11 | label: { control: 'text' },
12 | disabled: { control: 'boolean' },
13 | displayMetadata: { control: 'boolean' },
14 | },
15 | args: {
16 | label: 'VarFile',
17 | disabled: false,
18 | displayMetadata: true,
19 | },
20 | parameters: {
21 | controls: { expanded: true },
22 | actions: { argTypesRegex: '^on.*' },
23 | },
24 | } as Meta;
25 |
26 | const Template: StoryFn = args => {
27 | const [value, setValue] = useState(undefined);
28 | const onChange = useCallback(
29 | value => {
30 | setValue(value);
31 | args.onChange?.(value);
32 | },
33 | [setValue, args]
34 | );
35 | return ;
36 | };
37 |
38 | export const Default = Template.bind({});
39 | Default.args = {};
40 | Default.storyName = 'default';
41 |
42 | export const Disabled = Template.bind({});
43 | Disabled.args = {
44 | disabled: true,
45 | };
46 | Disabled.storyName = 'disabled: true';
47 |
48 | export const ReadOnly = Template.bind({});
49 | ReadOnly.args = {
50 | readOnly: true,
51 | };
52 | ReadOnly.storyName = 'readOnly: true';
53 |
--------------------------------------------------------------------------------
/stories/VarImage.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta, StoryFn } from '@storybook/react';
3 | import { useCallback, useState } from '@storybook/preview-api';
4 |
5 | import { VarImage, IVarImageProps } from '../src';
6 |
7 | export default {
8 | title: 'VarImage',
9 | component: VarImage,
10 | argTypes: {
11 | label: { control: 'text' },
12 | disabled: { control: 'boolean' },
13 | },
14 | args: {
15 | label: 'VarImage',
16 | disabled: false,
17 | },
18 | parameters: {
19 | controls: { expanded: true },
20 | actions: { argTypesRegex: '^on.*' },
21 | },
22 | } as Meta;
23 |
24 | const Template: StoryFn = args => {
25 | const [value, setValue] = useState(undefined);
26 | const onChange = useCallback(
27 | value => {
28 | setValue(value);
29 | args.onChange?.(value);
30 | },
31 | [setValue, args]
32 | );
33 | return ;
34 | };
35 |
36 | export const Default = Template.bind({});
37 | Default.args = {};
38 | Default.storyName = 'default';
39 |
40 | export const Disabled = Template.bind({});
41 | Disabled.args = {
42 | disabled: true,
43 | };
44 | Disabled.storyName = 'disabled: true';
45 |
46 | export const ReadOnly = Template.bind({});
47 | ReadOnly.args = {
48 | readOnly: true,
49 | };
50 | ReadOnly.storyName = 'readOnly: true';
51 |
--------------------------------------------------------------------------------
/stories/VarMedia.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta, StoryFn } from '@storybook/react';
3 | import { useCallback, useState } from '@storybook/preview-api';
4 |
5 | import { VarMedia, IVarMediaProps } from '../src';
6 |
7 | export default {
8 | title: 'VarMedia',
9 | component: VarMedia,
10 | argTypes: {
11 | label: { control: 'text' },
12 | disabled: { control: 'boolean' },
13 | acceptImage: { control: 'boolean' },
14 | acceptVideo: { control: 'boolean' },
15 | acceptAudio: { control: 'boolean' },
16 | },
17 | args: {
18 | label: 'VarMedia',
19 | disabled: false,
20 | acceptImage: true,
21 | acceptVideo: true,
22 | acceptAudio: true,
23 | },
24 | parameters: {
25 | controls: { expanded: true },
26 | actions: { argTypesRegex: '^on.*' },
27 | },
28 | } as Meta;
29 |
30 | const Template: StoryFn = args => {
31 | const [value, setValue] = useState(undefined);
32 | const onChange = useCallback(
33 | value => {
34 | setValue(value);
35 | args.onChange?.(value);
36 | },
37 | [setValue, args]
38 | );
39 | return ;
40 | };
41 |
42 | export const Default = Template.bind({});
43 | Default.args = {};
44 | Default.storyName = 'default';
45 |
46 | export const Disabled = Template.bind({});
47 | Disabled.args = {
48 | disabled: true,
49 | };
50 | Disabled.storyName = 'disabled: true';
51 |
52 | export const ReadOnly = Template.bind({});
53 | ReadOnly.args = {
54 | readOnly: true,
55 | };
56 | ReadOnly.storyName = 'readOnly: true';
57 |
--------------------------------------------------------------------------------
/stories/VarNumber.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta, StoryFn } from '@storybook/react';
3 | import { useCallback, useState } from '@storybook/preview-api';
4 |
5 | import { VarNumber, IVarNumberProps } from '../src';
6 |
7 | export default {
8 | title: 'VarNumber',
9 | component: VarNumber,
10 | argTypes: {
11 | min: { control: { type: 'range', min: 0, max: 10, step: 0.1 } },
12 | max: { control: { type: 'range', min: 0, max: 10, step: 0.1 } },
13 | step: { control: { type: 'range', min: 0, max: 10, step: 0.1 } },
14 | integer: { control: 'boolean' },
15 | showInput: { control: 'boolean' },
16 | showButtons: { control: 'boolean' },
17 | defaultValue: { control: { type: 'range', min: 0, max: 10, step: 0.1 } },
18 | label: { control: 'text' },
19 | disabled: { control: 'boolean' },
20 | },
21 | args: {
22 | min: 0,
23 | max: 10,
24 | step: 1,
25 | label: 'VarNumber',
26 | disabled: false,
27 | showButtons: false,
28 | },
29 | parameters: {
30 | controls: { expanded: true },
31 | actions: { argTypesRegex: '^on.*' },
32 | },
33 | } as Meta;
34 |
35 | const Template: StoryFn = args => {
36 | const [value, setValue] = useState(args.value ?? 0);
37 | const onChange = useCallback(
38 | value => {
39 | setValue(value);
40 | args.onChange?.(value);
41 | },
42 | [setValue, args]
43 | );
44 | return ;
45 | };
46 |
47 | export const Default = Template.bind({});
48 | Default.args = {};
49 | Default.storyName = 'default';
50 |
51 | export const Disabled = Template.bind({});
52 | Disabled.args = {
53 | disabled: true,
54 | };
55 | Disabled.storyName = 'disabled: true';
56 |
57 | export const ReadOnly = Template.bind({});
58 | ReadOnly.args = {
59 | readOnly: true,
60 | };
61 | ReadOnly.storyName = 'readOnly: true';
62 |
63 | export const Buttons = Template.bind({});
64 | Buttons.args = { showButtons: true };
65 | Buttons.storyName = 'showButtons: true';
66 |
--------------------------------------------------------------------------------
/stories/VarSelect.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta, StoryFn } from '@storybook/react';
3 | import { useCallback, useState } from '@storybook/preview-api';
4 |
5 | import { VarSelect, IVarSelectProps } from '../src';
6 |
7 | export default {
8 | title: 'VarSelect',
9 | component: VarSelect,
10 | argTypes: {
11 | label: { control: 'text' },
12 | disabled: { control: 'boolean' },
13 | },
14 | args: {
15 | label: 'VarSelect',
16 | disabled: false,
17 | options: [
18 | {
19 | key: '1',
20 | label: 'Test 1',
21 | },
22 | {
23 | key: '2',
24 | label: 'Test 2',
25 | },
26 | {
27 | key: '3',
28 | label: 'Test 3',
29 | },
30 | ],
31 | },
32 | parameters: {
33 | controls: { expanded: true },
34 | actions: { argTypesRegex: '^on.*' },
35 | },
36 | } as Meta;
37 |
38 | const Template: StoryFn = args => {
39 | const [value, setValue] = useState('1');
40 | const onChange = useCallback(
41 | value => {
42 | setValue(value);
43 | args.onChange?.(value);
44 | },
45 | [setValue, args]
46 | );
47 | return ;
48 | };
49 |
50 | export const Default = Template.bind({});
51 | Default.args = {};
52 | Default.storyName = 'default';
53 |
54 | export const Disabled = Template.bind({});
55 | Disabled.args = {
56 | disabled: true,
57 | };
58 | Disabled.storyName = 'disabled: true';
59 |
60 | export const ReadOnly = Template.bind({});
61 | ReadOnly.args = {
62 | readOnly: true,
63 | };
64 | ReadOnly.storyName = 'readOnly: true';
65 |
--------------------------------------------------------------------------------
/stories/VarSlider.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta, StoryFn } from '@storybook/react';
3 | import { useCallback, useState } from '@storybook/preview-api';
4 |
5 | import { VarSlider, IVarSliderProps } from '../src';
6 |
7 | export default {
8 | title: 'VarSlider',
9 | component: VarSlider,
10 | argTypes: {
11 | min: { control: { type: 'range', min: 0, max: 10, step: 0.1 } },
12 | max: { control: { type: 'range', min: 0, max: 10, step: 0.1 } },
13 | step: { control: { type: 'range', min: 0, max: 10, step: 0.1 } },
14 | integer: { control: 'boolean' },
15 | showInput: { control: 'boolean' },
16 | showButtons: { control: 'boolean' },
17 | defaultValue: { control: { type: 'range', min: 0, max: 10, step: 0.1 } },
18 | label: { control: 'text' },
19 | disabled: { control: 'boolean' },
20 | },
21 | args: {
22 | min: 0,
23 | max: 10,
24 | step: 1,
25 | label: 'VarSlider',
26 | disabled: false,
27 | showButtons: false,
28 | showInput: false,
29 | },
30 | parameters: {
31 | controls: { expanded: true },
32 | actions: { argTypesRegex: '^on.*' },
33 | },
34 | } as Meta;
35 |
36 | const Template: StoryFn = args => {
37 | const [value, setValue] = useState(0);
38 | const onChange = useCallback(
39 | value => {
40 | setValue(value);
41 | args.onChange?.(value);
42 | },
43 | [setValue, args]
44 | );
45 | return ;
46 | };
47 |
48 | export const Default = Template.bind({});
49 | Default.args = {};
50 | Default.storyName = 'default';
51 |
52 | export const Disabled = Template.bind({});
53 | Disabled.args = {
54 | disabled: true,
55 | };
56 | Disabled.storyName = 'disabled: true';
57 |
58 | export const ReadOnly = Template.bind({});
59 | ReadOnly.args = {
60 | readOnly: true,
61 | };
62 | ReadOnly.storyName = 'readOnly: true';
63 |
64 | export const Buttons = Template.bind({});
65 | Buttons.args = { showButtons: true };
66 | Buttons.storyName = 'showButtons: true';
67 |
68 | export const Input = Template.bind({});
69 | Input.args = { showInput: true };
70 | Input.storyName = 'showInput: true';
71 |
72 | export const ButtonsInput = Template.bind({});
73 | ButtonsInput.args = { showButtons: true, showInput: true };
74 | ButtonsInput.storyName = 'showButtons: true, showInput: true';
75 |
--------------------------------------------------------------------------------
/stories/VarString.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta, StoryFn } from '@storybook/react';
3 | import { useCallback, useState } from '@storybook/preview-api';
4 |
5 | import { VarString, IVarStringProps } from '../src';
6 |
7 | export default {
8 | title: 'VarString',
9 | component: VarString,
10 | argTypes: {
11 | maxlength: { control: { type: 'range', min: 0, max: 500, step: 1 } },
12 | label: { control: 'text' },
13 | disabled: { control: 'boolean' },
14 | multiline: { control: 'boolean' },
15 | autoexpand: { control: 'boolean' },
16 | },
17 | args: {
18 | label: 'VarString',
19 | disabled: false,
20 | },
21 | parameters: {
22 | controls: { expanded: true },
23 | actions: { argTypesRegex: '^on.*' },
24 | },
25 | } as Meta;
26 |
27 | const Template: StoryFn = args => {
28 | const [value, setValue] = useState('value');
29 | const onChange = useCallback(
30 | value => {
31 | setValue(value);
32 | args.onChange?.(value);
33 | },
34 | [setValue, args]
35 | );
36 | return ;
37 | };
38 |
39 | export const Default = Template.bind({});
40 | Default.args = {};
41 | Default.storyName = 'default';
42 |
43 | export const Disabled = Template.bind({});
44 | Disabled.args = {
45 | disabled: true,
46 | };
47 | Disabled.storyName = 'disabled: true';
48 |
49 | export const ReadOnly = Template.bind({});
50 | ReadOnly.args = {
51 | readOnly: true,
52 | };
53 | ReadOnly.storyName = 'readOnly: true';
54 |
55 | export const Multiline = Template.bind({});
56 | Multiline.args = {
57 | multiline: true,
58 | };
59 | Multiline.storyName = 'multiline: true';
60 |
61 | export const Autoexpand = Template.bind({});
62 | Autoexpand.args = {
63 | multiline: true,
64 | autoexpand: true,
65 | };
66 | Autoexpand.storyName = 'multiline: true, autoexpand: true';
67 |
--------------------------------------------------------------------------------
/stories/VarToggle.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta, StoryFn } from '@storybook/react';
3 | import { useCallback, useState } from '@storybook/preview-api';
4 |
5 | import { VarToggle, IVarToggleProps } from '../src';
6 |
7 | export default {
8 | title: 'VarToggle',
9 | component: VarToggle,
10 | argTypes: {
11 | label: { control: 'text' },
12 | disabled: { control: 'boolean' },
13 | },
14 | args: {
15 | label: 'VarToggle',
16 | disabled: false,
17 | },
18 | parameters: {
19 | controls: { expanded: true },
20 | actions: { argTypesRegex: '^on.*' },
21 | },
22 | } as Meta;
23 |
24 | const Template: StoryFn = args => {
25 | const [value, setValue] = useState(false);
26 | const onChange = useCallback(
27 | value => {
28 | setValue(value);
29 | args.onChange?.(value);
30 | },
31 | [setValue, args]
32 | );
33 | return ;
34 | };
35 |
36 | export const Default = Template.bind({});
37 | Default.args = {};
38 | Default.storyName = 'default';
39 |
40 | export const Disabled = Template.bind({});
41 | Disabled.args = {
42 | disabled: true,
43 | };
44 | Disabled.storyName = 'disabled: true';
45 |
46 | export const ReadOnly = Template.bind({});
47 | ReadOnly.args = {
48 | readOnly: true,
49 | };
50 | ReadOnly.storyName = 'readOnly: true';
51 |
--------------------------------------------------------------------------------
/stories/VarXY.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta, StoryFn } from '@storybook/react';
3 | import { useCallback, useState } from '@storybook/preview-api';
4 |
5 | import { VarXY, IVarXYProps } from '../src';
6 |
7 | export default {
8 | title: 'VarXY',
9 | component: VarXY,
10 | argTypes: {
11 | label: { control: { type: 'text' } },
12 | disabled: { control: 'boolean' },
13 | },
14 | args: {
15 | label: 'VarXY',
16 | disabled: false,
17 | },
18 | parameters: {
19 | controls: { expanded: true },
20 | actions: { argTypesRegex: '^on.*' },
21 | },
22 | } as Meta;
23 |
24 | const Template: StoryFn = args => {
25 | const [value, setValue] = useState<[number, number]>([0, 0]);
26 | const onChange = useCallback(
27 | value => {
28 | setValue(value);
29 | args.onChange?.(value);
30 | },
31 | [setValue, args]
32 | );
33 | return ;
34 | };
35 |
36 | export const Default = Template.bind({});
37 | Default.args = {};
38 | Default.storyName = 'default';
39 |
40 | export const Disabled = Template.bind({});
41 | Disabled.args = {
42 | disabled: true,
43 | };
44 | Disabled.storyName = 'disabled: true';
45 |
46 | export const ReadOnly = Template.bind({});
47 | ReadOnly.args = {
48 | readOnly: true,
49 | };
50 | ReadOnly.storyName = 'readOnly: true';
51 |
--------------------------------------------------------------------------------
/test/VarAngle.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { fireEvent, render, screen } from '@testing-library/react';
3 |
4 | import { VarAngle } from '../src/VarAngle';
5 | import { VarUI } from '../src/VarUI';
6 |
7 | describe('VarAngle', () => {
8 | it('should render without crashing', () => {
9 | render( );
10 | });
11 |
12 | it('should display value', async () => {
13 | render( );
14 | const value = await screen.findByText('180°');
15 | expect(value).toBeInTheDocument();
16 | });
17 |
18 | it('should update value on drag', async () => {
19 | const fn = vi.fn();
20 | render( );
21 | const angle = await screen.findByTitle('Angle');
22 | fireEvent.pointerDown(angle);
23 | fireEvent.pointerMove(angle, { clientX: 0, clientY: 0 });
24 | fireEvent.pointerMove(angle, { clientX: 0, clientY: 0 });
25 | fireEvent.pointerUp(angle, { clientX: 0, clientY: 0 });
26 | expect(fn).toBeCalledTimes(1);
27 | });
28 |
29 | it('should update value on wheel', async () => {
30 | const fn = vi.fn();
31 | render( );
32 | const angle = await screen.findByTitle('Angle');
33 | fireEvent.wheel(angle, { deltaY: -1 });
34 | expect(fn).toBeCalledTimes(1);
35 | });
36 |
37 | it('should reset value on double click', async () => {
38 | const fn = vi.fn();
39 | render( );
40 | const angle = await screen.findByTitle('Angle');
41 | fireEvent.doubleClick(angle);
42 | expect(fn).toBeCalledWith(0);
43 | });
44 |
45 | it('should update value in context', async () => {
46 | const fn = vi.fn();
47 | const init = {
48 | value: Math.PI,
49 | };
50 | render(
51 |
52 |
53 |
54 | );
55 | const angle = await screen.findByTitle('Angle');
56 | fireEvent.doubleClick(angle);
57 | expect(fn).toBeCalledWith(expect.objectContaining({ value: 0 }));
58 | });
59 |
60 | it('should render error from property', async () => {
61 | render( );
62 | expect(screen.getByText('example error')).toBeInTheDocument();
63 | });
64 |
65 | it('should render error from context', async () => {
66 | render(
67 |
73 |
74 |
75 | );
76 | expect(screen.getByText('example error')).toBeInTheDocument();
77 | });
78 | });
79 |
--------------------------------------------------------------------------------
/test/VarArray.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { fireEvent, render, screen } from '@testing-library/react';
3 |
4 | import { VarArray } from '../src/VarArray';
5 | import { VarString } from '../src/VarString';
6 | import { VarUI } from '../src/VarUI';
7 |
8 | describe('VarArray', () => {
9 | it('should render without crashing', () => {
10 | render( );
11 | });
12 |
13 | it('should display value (node children)', async () => {
14 | render(
15 |
16 |
17 |
18 | );
19 | const value = await screen.findByDisplayValue('b');
20 | expect(value).toBeInTheDocument();
21 | });
22 |
23 | it('should display value (function)', async () => {
24 | render(
25 |
26 | {() => }
27 |
28 | );
29 | const value = await screen.findByDisplayValue('b');
30 | expect(value).toBeInTheDocument();
31 | });
32 |
33 | it('should update value on change', async () => {
34 | const fn = vi.fn();
35 | render(
36 |
40 | {() => }
41 |
42 | );
43 | const value = await screen.findByDisplayValue('b');
44 | fireEvent.change(value, {
45 | target: {
46 | value: 'x',
47 | },
48 | });
49 | expect(fn).toBeCalledWith([{ test: 'a' }, { test: 'x' }, { test: 'c' }]);
50 | });
51 |
52 | it('should render errors from context', async () => {
53 | render(
54 |
58 | {() => }
59 |
60 | );
61 | expect(screen.getByText('example error')).toBeInTheDocument();
62 | });
63 |
64 | it('should display value (empty path)', async () => {
65 | render(
66 |
67 |
68 |
69 | );
70 | const value = await screen.findByDisplayValue('b');
71 | expect(value).toBeInTheDocument();
72 | });
73 |
74 | it('should update value on change (empty path)', async () => {
75 | const fn = vi.fn();
76 | render(
77 |
78 | {() => }
79 |
80 | );
81 | const value = await screen.findByDisplayValue('b');
82 | fireEvent.change(value, {
83 | target: {
84 | value: 'x',
85 | },
86 | });
87 | expect(fn).toBeCalledWith(['a', 'x', 'c']);
88 | });
89 | });
90 |
--------------------------------------------------------------------------------
/test/VarBase.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 |
4 | import { VarBase } from '../src/VarBase';
5 |
6 | describe('VarBase', () => {
7 | it('should render without crashing', () => {
8 | render( );
9 | });
10 |
11 | it('should display label', async () => {
12 | render( );
13 | const base = await screen.findByText('Test');
14 | expect(base).toBeInTheDocument();
15 | });
16 |
17 | it('should render error from property', async () => {
18 | render( );
19 | expect(screen.getByText('example error')).toBeInTheDocument();
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/test/VarButton.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 |
4 | import { VarButton } from '../src/VarButton';
5 |
6 | describe('VarButton', () => {
7 | it('should render without crashing', () => {
8 | render( );
9 | });
10 |
11 | it('should call onClick on click', async () => {
12 | const fn = vi.fn();
13 | render( );
14 | const button = await screen.findByText('Test');
15 | button.click();
16 | expect(fn).toBeCalled();
17 | });
18 |
19 | it('should render error from property', async () => {
20 | render( );
21 | expect(screen.getByText('example error')).toBeInTheDocument();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/test/VarCategory.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { fireEvent, render, screen } from '@testing-library/react';
3 |
4 | import { VarCategory } from '../src/VarCategory';
5 | import { VarDisplay } from '../src/VarDisplay';
6 |
7 | describe('VarCategory', () => {
8 | it('should render without crashing', () => {
9 | render( );
10 | });
11 |
12 | it('should display label', async () => {
13 | render( );
14 | const value = await screen.findByText('Test');
15 | expect(value).toBeInTheDocument();
16 | });
17 |
18 | it('should collapse when collapse button clicked', async () => {
19 | render(
20 |
21 |
22 |
23 | );
24 |
25 | const button = await screen.findByTitle('Collapse');
26 | fireEvent.click(button);
27 |
28 | const value = screen.queryByText('contents');
29 | expect(value).toBeNull();
30 |
31 | fireEvent.click(button);
32 |
33 | expect(screen.getByText('contents')).toBeInTheDocument();
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/test/VarColor.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen, fireEvent, act } from '@testing-library/react';
3 |
4 | import { VarColor } from '../src/VarColor';
5 | import { VarUI } from '../src/VarUI';
6 |
7 | describe('VarColor', () => {
8 | it('should render without crashing', () => {
9 | render( );
10 | });
11 |
12 | it('should display value', async () => {
13 | render( );
14 | const colorPreview = await screen.findByTitle('Color preview');
15 | expect(colorPreview.style.backgroundColor).toBe('rgb(255, 0, 0)');
16 | });
17 |
18 | it('should update value on change', async () => {
19 | const fn = vi.fn();
20 | render( );
21 | const colorPreview = await screen.findByTitle('Color preview');
22 | act(() => {
23 | colorPreview.click();
24 | });
25 | const colorTile = await screen.findByTitle('#D0021B');
26 | act(() => {
27 | colorTile.click();
28 | });
29 | expect(fn).toBeCalledWith('#d0021b');
30 | });
31 |
32 | it('should update value on change (alpha)', async () => {
33 | const fn = vi.fn();
34 | render( );
35 | const colorPreview = await screen.findByTitle('Color preview');
36 | act(() => {
37 | colorPreview.click();
38 | });
39 | const colorAlpha = screen.getByText('A').previousSibling!;
40 | fireEvent.change(colorAlpha, { target: { value: '0' } });
41 | expect(fn).toBeCalledWith('#ff000000');
42 | });
43 |
44 | it('should update value on change (alpha, transparent00 edge case)', async () => {
45 | const fn = vi.fn();
46 | render( );
47 | const colorPreview = await screen.findByTitle('Color preview');
48 | act(() => {
49 | colorPreview.click();
50 | });
51 | const colorAlpha = screen.getByText('A').previousSibling!;
52 | fireEvent.change(colorAlpha, { target: { value: '0' } });
53 | expect(fn).toBeCalledWith('#00000000');
54 | });
55 |
56 | it('should update value in context', async () => {
57 | const fn = vi.fn();
58 | const init = {
59 | value: '#ff0000',
60 | };
61 | render(
62 |
63 |
64 |
65 | );
66 |
67 | const colorPreview = await screen.findByTitle('Color preview');
68 | act(() => {
69 | colorPreview.click();
70 | });
71 | const colorTile = await screen.findByTitle('#D0021B');
72 | act(() => {
73 | colorTile.click();
74 | });
75 | expect(fn).toBeCalledWith(expect.objectContaining({ value: '#d0021b' }));
76 | });
77 |
78 | it('should render error from property', async () => {
79 | render( );
80 | expect(screen.getByText('example error')).toBeInTheDocument();
81 | });
82 |
83 | it('should render error from context', async () => {
84 | render(
85 |
91 |
92 |
93 | );
94 | expect(screen.getByText('example error')).toBeInTheDocument();
95 | });
96 | });
97 |
--------------------------------------------------------------------------------
/test/VarDisplay.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 |
4 | import { VarDisplay } from '../src/VarDisplay';
5 | import { VarUI } from '../src/VarUI';
6 |
7 | describe('VarDisplay', () => {
8 | it('should render without crashing', () => {
9 | render( );
10 | });
11 |
12 | it('should display value', async () => {
13 | render( );
14 | const value = await screen.findByText('Value');
15 | expect(value).toBeInTheDocument();
16 | });
17 |
18 | it('should render error from property', async () => {
19 | render( );
20 | expect(screen.getByText('example error')).toBeInTheDocument();
21 | });
22 |
23 | it('should render error from context', async () => {
24 | render(
25 |
31 |
32 |
33 | );
34 | expect(screen.getByText('example error')).toBeInTheDocument();
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/test/VarFile.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { fireEvent, render, screen } from '@testing-library/react';
3 |
4 | import { VarFile } from '../src/VarFile';
5 | import { VarUI } from '../src/VarUI';
6 |
7 | describe('VarFile', () => {
8 | it('should render without crashing', () => {
9 | render( );
10 | });
11 |
12 | it('should display value', async () => {
13 | render( );
14 | const value = await screen.findByText('test.pdf');
15 | expect(value).toBeInTheDocument();
16 | });
17 |
18 | it('should update value on change', async () => {
19 | global.URL.createObjectURL = vi.fn();
20 | const fn = vi.fn();
21 | render( );
22 | const value = await screen.findByTitle('File upload');
23 | fireEvent.change(value, {
24 | target: {
25 | files: [new File(['test'], 'test.png')],
26 | },
27 | });
28 | expect(fn).toBeCalled();
29 | });
30 |
31 | it('should render error from property', async () => {
32 | render( );
33 | expect(screen.getByText('example error')).toBeInTheDocument();
34 | });
35 |
36 | it('should render error from context', async () => {
37 | render(
38 |
44 |
45 |
46 | );
47 | expect(screen.getByText('example error')).toBeInTheDocument();
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/test/VarImage.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { fireEvent, render, screen } from '@testing-library/react';
3 |
4 | import { VarImage } from '../src/VarImage';
5 | import { VarUI } from '../src/VarUI';
6 |
7 | describe('VarImage', () => {
8 | it('should render without crashing', () => {
9 | render( );
10 | });
11 |
12 | it('should display value', async () => {
13 | render( );
14 | const value = await screen.findByTitle('Image preview');
15 | expect(value.style.backgroundImage).toEqual(
16 | 'url(blob:http://example.com/test)'
17 | );
18 | expect(value).toBeInTheDocument();
19 | });
20 |
21 | it('should update value on change', async () => {
22 | global.URL.createObjectURL = vi.fn();
23 | const fn = vi.fn();
24 | render( );
25 | const value = await screen.findByTitle('Image upload');
26 | fireEvent.change(value, {
27 | target: {
28 | files: [new File(['test'], 'test.png')],
29 | },
30 | });
31 | expect(fn).toBeCalled();
32 | });
33 |
34 | it('should render error from property', async () => {
35 | render( );
36 | expect(screen.getByText('example error')).toBeInTheDocument();
37 | });
38 |
39 | it('should render error from context', async () => {
40 | render(
41 |
47 |
48 |
49 | );
50 | expect(screen.getByText('example error')).toBeInTheDocument();
51 | });
52 | });
53 |
--------------------------------------------------------------------------------
/test/VarMedia.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 |
4 | import { VarMedia } from '../src/VarMedia';
5 | import { VarUI } from '../src/VarUI';
6 |
7 | describe('VarMedia', () => {
8 | it('should render without crashing', () => {
9 | render( );
10 | });
11 |
12 | it('should render error from property', async () => {
13 | render( );
14 | expect(screen.getByText('example error')).toBeInTheDocument();
15 | });
16 |
17 | it('should render error from context', async () => {
18 | render(
19 |
25 |
26 |
27 | );
28 | expect(screen.getByText('example error')).toBeInTheDocument();
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/test/VarNumber.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { fireEvent, render, screen } from '@testing-library/react';
3 |
4 | import { VarNumber } from '../src/VarNumber';
5 | import { VarUI } from '../src/VarUI';
6 |
7 | describe('VarNumber', () => {
8 | it('should render without crashing', () => {
9 | render( );
10 | });
11 |
12 | it('should display value', async () => {
13 | render( );
14 | const value = await screen.findByDisplayValue('1337');
15 | expect(value).toBeInTheDocument();
16 | });
17 |
18 | it('should update value on change', async () => {
19 | const fn = vi.fn();
20 | render( );
21 | const value = await screen.findByDisplayValue('1337');
22 | fireEvent.change(value, {
23 | target: {
24 | value: 2222,
25 | },
26 | });
27 | fireEvent.blur(value);
28 | expect(fn).toBeCalledWith(2222);
29 | });
30 |
31 | it('should update value on change (invalid data)', async () => {
32 | const fn = vi.fn();
33 | render( );
34 | const value = await screen.findByDisplayValue('1337');
35 | fireEvent.change(value, {
36 | target: {
37 | value: 'a',
38 | },
39 | });
40 | fireEvent.blur(value);
41 | expect(fn).toBeCalledWith(0);
42 | });
43 |
44 | it('should update value on increase button click', async () => {
45 | const fn = vi.fn();
46 | render( );
47 | const button = await screen.findByTitle('Increase');
48 | button.click();
49 | expect(fn).toBeCalledWith(1338);
50 | });
51 |
52 | it('should update value on decrease button click', async () => {
53 | const fn = vi.fn();
54 | render( );
55 | const button = await screen.findByTitle('Decrease');
56 | button.click();
57 | expect(fn).toBeCalledWith(1336);
58 | });
59 |
60 | it('should update value in context', async () => {
61 | const fn = vi.fn();
62 | const init = {
63 | value: 1,
64 | };
65 | render(
66 |
67 |
68 |
69 | );
70 | const button = await screen.findByTitle('Increase');
71 | button.click();
72 | expect(fn).toBeCalledWith(expect.objectContaining({ value: 2 }));
73 | });
74 |
75 | it('should display unit', async () => {
76 | render( );
77 | expect(screen.getByText('mm')).toBeInTheDocument();
78 | });
79 |
80 | it('should render error from property', async () => {
81 | render( );
82 | expect(screen.getByText('example error')).toBeInTheDocument();
83 | });
84 |
85 | it('should render error from context', async () => {
86 | render(
87 |
93 |
94 |
95 | );
96 | expect(screen.getByText('example error')).toBeInTheDocument();
97 | });
98 | });
99 |
--------------------------------------------------------------------------------
/test/VarScope.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { fireEvent, render, screen } from '@testing-library/react';
3 |
4 | import { VarScope } from '../src/VarScope';
5 | import { VarString } from '../src/VarString';
6 | import { VarUI } from '../src/VarUI';
7 |
8 | describe('VarScope', () => {
9 | it('should render without crashing', () => {
10 | render( );
11 | });
12 |
13 | it('should display value', async () => {
14 | render(
15 |
16 |
17 |
18 |
19 |
20 | );
21 | const value = await screen.findByDisplayValue('abc');
22 | expect(value).toBeInTheDocument();
23 | });
24 |
25 | it('should update value on change', async () => {
26 | const fn = vi.fn();
27 | render(
28 |
29 |
30 |
31 |
32 |
33 | );
34 | const value = await screen.findByDisplayValue('abc');
35 | fireEvent.change(value, {
36 | target: {
37 | value: 'x',
38 | },
39 | });
40 | expect(fn).toBeCalledWith({ scope: { test: 'x' } });
41 | });
42 |
43 | it('should render errors from context', async () => {
44 | render(
45 |
49 |
50 |
51 |
52 |
53 | );
54 | expect(screen.getByText('example error')).toBeInTheDocument();
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/test/VarSelect.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { fireEvent, render, screen } from '@testing-library/react';
3 |
4 | import { VarSelect } from '../src/VarSelect';
5 | import { VarUI } from '../src/VarUI';
6 |
7 | describe('VarSelect', () => {
8 | it('should render without crashing', () => {
9 | render( );
10 | });
11 |
12 | it('should display value', async () => {
13 | render(
14 |
22 | );
23 | const value = await screen.findAllByDisplayValue('Test 2');
24 | expect(value.length).toBeGreaterThan(0);
25 | });
26 |
27 | it('should display value (value property)', async () => {
28 | render(
29 |
37 | );
38 | const value = await screen.findAllByDisplayValue('Test 2');
39 | expect(value.length).toBeGreaterThan(0);
40 | });
41 |
42 | it('should update value on change', async () => {
43 | const fn = vi.fn();
44 | render(
45 |
54 | );
55 | const select = await screen.findByTitle('Select options');
56 | fireEvent.change(select, { target: { value: '1' } });
57 | expect(fn).toBeCalledWith(1);
58 | });
59 |
60 | it('should update value on change (value property)', async () => {
61 | const fn = vi.fn();
62 | render(
63 |
72 | );
73 | const select = await screen.findByTitle('Select options');
74 | fireEvent.change(select, { target: { value: '"test1"' } });
75 | expect(fn).toBeCalledWith('test1');
76 | });
77 |
78 | it('should update value in context', async () => {
79 | const fn = vi.fn();
80 | const init = {
81 | value: 2,
82 | };
83 | render(
84 |
85 |
93 |
94 | );
95 | const select = await screen.findByTitle('Select options');
96 | fireEvent.change(select, { target: { value: '1' } });
97 | expect(fn).toBeCalledWith(expect.objectContaining({ value: 1 }));
98 | });
99 |
100 | it('should render error from property', async () => {
101 | render(
102 |
111 | );
112 | expect(screen.getByText('example error')).toBeInTheDocument();
113 | });
114 |
115 | it('should render error from context', async () => {
116 | render(
117 |
123 |
131 |
132 | );
133 | expect(screen.getByText('example error')).toBeInTheDocument();
134 | });
135 | });
136 |
--------------------------------------------------------------------------------
/test/VarSlider.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { fireEvent, render, screen } from '@testing-library/react';
3 |
4 | import { VarSlider } from '../src/VarSlider';
5 | import { VarUI } from '../src/VarUI';
6 |
7 | describe('VarSlider', () => {
8 | it('should render without crashing', () => {
9 | render( );
10 | });
11 |
12 | it('should display value', async () => {
13 | render( );
14 | const value = await screen.findAllByText('2');
15 | expect(value.length).toBeGreaterThan(0);
16 | });
17 |
18 | it('should update value on drag', async () => {
19 | const fn = vi.fn();
20 | render( );
21 | const slider = await screen.findByTitle('Slider');
22 | fireEvent.pointerDown(slider);
23 | fireEvent.pointerMove(slider, { clientX: 0, clientY: 0 });
24 | fireEvent.pointerMove(slider, { clientX: 0, clientY: 0 });
25 | fireEvent.pointerUp(slider, { clientX: 0, clientY: 0 });
26 | expect(fn).toBeCalledWith(0);
27 | });
28 |
29 | it('should reset value on double click', async () => {
30 | const fn = vi.fn();
31 | render(
32 |
40 | );
41 | const slider = await screen.findByTitle('Slider');
42 | fireEvent.doubleClick(slider);
43 | expect(fn).toBeCalledWith(1);
44 | });
45 |
46 | it('should update value on increase button click', async () => {
47 | const fn = vi.fn();
48 | render(
49 |
57 | );
58 | const button = await screen.findByTitle('Increase');
59 | button.click();
60 | expect(fn).toBeCalledWith(1.1);
61 | });
62 |
63 | it('should update value on decrease button click', async () => {
64 | const fn = vi.fn();
65 | render(
66 |
74 | );
75 | const button = await screen.findByTitle('Decrease');
76 | button.click();
77 | expect(fn).toBeCalledWith(0.9);
78 | });
79 |
80 | it('should update value on input change', async () => {
81 | const fn = vi.fn();
82 | render(
83 |
84 | );
85 | const input = await screen.findByDisplayValue(1);
86 | fireEvent.change(input, {
87 | target: {
88 | value: 2,
89 | },
90 | });
91 | fireEvent.blur(input);
92 | expect(fn).toBeCalledWith(2);
93 | });
94 |
95 | it('should update value to 0 on invalid input change', async () => {
96 | const fn = vi.fn();
97 | render(
98 |
99 | );
100 | const input = await screen.findByDisplayValue(1);
101 | fireEvent.change(input, {
102 | target: {
103 | value: 'a',
104 | },
105 | });
106 | fireEvent.blur(input);
107 | expect(fn).toBeCalledWith(0);
108 | });
109 |
110 | it('should update value in context', async () => {
111 | const fn = vi.fn();
112 | const init = {
113 | value: 1,
114 | };
115 | render(
116 |
117 |
118 |
119 | );
120 | const button = await screen.findByTitle('Increase');
121 | button.click();
122 | expect(fn).toBeCalledWith(expect.objectContaining({ value: 2 }));
123 | });
124 |
125 | it('should display unit', async () => {
126 | render(
127 |
128 | );
129 | expect(screen.getByText('mm')).toBeInTheDocument();
130 | });
131 |
132 | it('should render error from property', async () => {
133 | render(
134 |
142 | );
143 | expect(screen.getByText('example error')).toBeInTheDocument();
144 | });
145 |
146 | it('should render error from context', async () => {
147 | render(
148 |
154 |
155 |
156 | );
157 | expect(screen.getByText('example error')).toBeInTheDocument();
158 | });
159 |
160 | it('should update value on input change if inputMin/inputMax/inputStep are defined', async () => {
161 | const fn = vi.fn();
162 | render(
163 |
174 | );
175 | const input = await screen.findByDisplayValue(1);
176 | fireEvent.change(input, {
177 | target: {
178 | value: 3,
179 | },
180 | });
181 | fireEvent.blur(input);
182 | expect(fn).toBeCalledWith(3);
183 | });
184 | });
185 |
--------------------------------------------------------------------------------
/test/VarString.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { fireEvent, render, screen } from '@testing-library/react';
3 |
4 | import { VarString } from '../src/VarString';
5 | import { VarUI } from '../src/VarUI';
6 |
7 | describe('VarString', () => {
8 | it('should render without crashing', () => {
9 | render( );
10 | });
11 |
12 | describe('multiline = false', () => {
13 | it('should display value', async () => {
14 | render( );
15 | const value = await screen.findByDisplayValue('Test');
16 | expect(value).toBeInTheDocument();
17 | });
18 |
19 | it('should update value on change', async () => {
20 | const fn = vi.fn();
21 | render( );
22 | const value = await screen.findByDisplayValue('Value');
23 | fireEvent.change(value, {
24 | target: {
25 | value: 'Updated',
26 | },
27 | });
28 | expect(fn).toBeCalledWith('Updated');
29 | });
30 |
31 | it('should update value in context', async () => {
32 | const fn = vi.fn();
33 | const init = {
34 | value: 'Value',
35 | };
36 | render(
37 |
38 |
39 |
40 | );
41 | const value = await screen.findByDisplayValue('Value');
42 | fireEvent.change(value, {
43 | target: {
44 | value: 'Updated',
45 | },
46 | });
47 | expect(fn).toBeCalledWith(expect.objectContaining({ value: 'Updated' }));
48 | });
49 | });
50 |
51 | describe('multiline = true', () => {
52 | it('should display value', async () => {
53 | render( );
54 | const value = await screen.findByDisplayValue('Test');
55 | expect(value).toBeInTheDocument();
56 | });
57 |
58 | it('should update value on change', async () => {
59 | const fn = vi.fn();
60 | render( );
61 | const value = await screen.findByDisplayValue('Value');
62 | fireEvent.change(value, {
63 | target: {
64 | value: 'Updated',
65 | },
66 | });
67 | expect(fn).toBeCalledWith('Updated');
68 | });
69 |
70 | it('should update value in context', async () => {
71 | const fn = vi.fn();
72 | const init = {
73 | value: 'Value',
74 | };
75 | render(
76 |
77 |
78 |
79 | );
80 | const value = await screen.findByDisplayValue('Value');
81 | fireEvent.change(value, {
82 | target: {
83 | value: 'Updated',
84 | },
85 | });
86 | expect(fn).toBeCalledWith(expect.objectContaining({ value: 'Updated' }));
87 | });
88 | });
89 |
90 | it('should render error from property', async () => {
91 | render( );
92 | expect(screen.getByText('example error')).toBeInTheDocument();
93 | });
94 |
95 | it('should render error from context', async () => {
96 | render(
97 |
103 |
104 |
105 | );
106 | expect(screen.getByText('example error')).toBeInTheDocument();
107 | });
108 | });
109 |
--------------------------------------------------------------------------------
/test/VarToggle.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 |
4 | import { VarToggle } from '../src/VarToggle';
5 | import { VarUI } from '../src/VarUI';
6 |
7 | describe('VarToggle', () => {
8 | it('should render without crashing', () => {
9 | render( );
10 | });
11 |
12 | it('should display value (false)', async () => {
13 | render( );
14 | const toggle = await screen.findByTitle('Toggle');
15 | const input = toggle.firstChild as HTMLInputElement;
16 | expect(input.checked).toBe(false);
17 | });
18 |
19 | it('should display value (true)', async () => {
20 | render( );
21 | const toggle = await screen.findByTitle('Toggle');
22 | const input = toggle.firstChild as HTMLInputElement;
23 | expect(input.checked).toBe(true);
24 | });
25 |
26 | it('should toggle value on click (false -> true)', async () => {
27 | const fn = vi.fn();
28 | render( );
29 | const toggle = await screen.findByTitle('Toggle');
30 | toggle.click();
31 | expect(fn).toBeCalledWith(true);
32 | });
33 |
34 | it('should toggle value on click (true -> false)', async () => {
35 | const fn = vi.fn();
36 | render( );
37 | const toggle = await screen.findByTitle('Toggle');
38 | toggle.click();
39 | expect(fn).toBeCalledWith(false);
40 | });
41 |
42 | it('should update value in context', async () => {
43 | const fn = vi.fn();
44 | const init = {
45 | value: false,
46 | };
47 | render(
48 |
49 |
50 |
51 | );
52 | const toggle = await screen.findByTitle('Toggle');
53 | toggle.click();
54 | expect(fn).toBeCalledWith(expect.objectContaining({ value: true }));
55 | });
56 |
57 | it('should render error from property', async () => {
58 | render( );
59 | expect(screen.getByText('example error')).toBeInTheDocument();
60 | });
61 |
62 | it('should render error from context', async () => {
63 | render(
64 |
70 |
71 |
72 | );
73 | expect(screen.getByText('example error')).toBeInTheDocument();
74 | });
75 | });
76 |
--------------------------------------------------------------------------------
/test/VarUI.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 |
4 | import { VarUI } from '../src/VarUI';
5 |
6 | describe('VarUI', () => {
7 | it('should render without crashing', () => {
8 | render( );
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/test/common/VarUIContext.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 |
4 | import { useVarUIValue, VarUIContext } from '../../src/common/VarUIContext';
5 |
6 | describe('VarUIContext', () => {
7 | it('should call context functions', () => {
8 | const values = {};
9 | const getValue = vi.fn(() => 1);
10 | const setValue = vi.fn();
11 | const getError = vi.fn(() => undefined);
12 | const context = {
13 | values,
14 | getValue,
15 | setValue,
16 | getError,
17 | };
18 |
19 | const Component = () => {
20 | const [currentValue, setCurrentValue] = useVarUIValue({ path: 'test' });
21 | expect(currentValue).toBe(1);
22 | setCurrentValue(2);
23 | return <>>;
24 | };
25 |
26 | render(
27 |
28 |
29 |
30 | );
31 |
32 | expect(getValue).toBeCalledWith('test');
33 | expect(setValue).toBeCalledWith('test', 2);
34 | });
35 |
36 | it('should use fallback value', () => {
37 | const values = {};
38 | const getValue = vi.fn(() => undefined);
39 | const setValue = vi.fn();
40 | const getError = vi.fn(() => undefined);
41 | const context = {
42 | values,
43 | getValue,
44 | setValue,
45 | getError,
46 | };
47 |
48 | const Component = () => {
49 | const [currentValue] = useVarUIValue({ path: 'test', fallbackValue: 1 });
50 | expect(currentValue).toBe(1);
51 | return <>>;
52 | };
53 |
54 | render(
55 |
56 |
57 |
58 | );
59 |
60 | expect(getValue).toBeCalledWith('test');
61 | });
62 |
63 | it('should call onChange', () => {
64 | const values = {};
65 | const getValue = vi.fn(() => undefined);
66 | const setValue = vi.fn();
67 | const onChange = vi.fn();
68 | const getError = vi.fn(() => undefined);
69 | const context = {
70 | values,
71 | getValue,
72 | setValue,
73 | getError,
74 | };
75 |
76 | const Component = () => {
77 | const [, setCurrentValue] = useVarUIValue({ path: 'test', onChange });
78 | setCurrentValue(2);
79 | return <>>;
80 | };
81 |
82 | render(
83 |
84 |
85 |
86 | );
87 |
88 | expect(setValue).toBeCalledWith('test', 2);
89 | expect(onChange).toBeCalledWith(2);
90 | });
91 | });
92 |
--------------------------------------------------------------------------------
/test/common/roundValue.test.ts:
--------------------------------------------------------------------------------
1 | import { roundValue } from '../../src/common/roundValue';
2 |
3 | describe('roundValue', () => {
4 | it('should return input when no other arguments are specified', () => {
5 | expect(roundValue(5)).toBe(5);
6 | expect(roundValue(5.05)).toBe(5.05);
7 | expect(roundValue(0.33)).toBe(0.33);
8 | });
9 |
10 | it('should clamp value to min', () => {
11 | expect(roundValue(5, 10)).toBe(10);
12 | expect(roundValue(11, 10)).toBe(11);
13 | });
14 |
15 | it('should clamp value to max', () => {
16 | expect(roundValue(15, undefined, 10)).toBe(10);
17 | expect(roundValue(7, undefined, 10)).toBe(7);
18 | });
19 |
20 | it('should clamp value to min and max', () => {
21 | expect(roundValue(15, 5, 10)).toBe(10);
22 | expect(roundValue(4, 5, 10)).toBe(5);
23 | expect(roundValue(7, 5, 10)).toBe(7);
24 | });
25 |
26 | it('rounds value to step', () => {
27 | expect(roundValue(20.5, undefined, undefined, 1)).toBe(21);
28 | expect(roundValue(20.4, undefined, undefined, 1)).toBe(20);
29 | expect(roundValue(21, undefined, undefined, 1)).toBe(21);
30 | expect(roundValue(20.00000005, undefined, undefined, 1)).toBe(20);
31 | expect(roundValue(20.00000005, undefined, undefined, 0.1)).toBe(20);
32 | expect(roundValue(20.05, undefined, undefined, 0.1)).toBe(20.1);
33 | });
34 |
35 | it('should cast value to integer', () => {
36 | expect(roundValue(20.5, undefined, undefined, undefined, true)).toBe(21);
37 | expect(roundValue(20.1, undefined, undefined, undefined, true)).toBe(20);
38 | });
39 |
40 | it('should clamp value to min and max and rounds value to step', () => {
41 | expect(roundValue(20.5, 19, 21, 0.1)).toBe(20.5);
42 | expect(roundValue(20.49, 19, 21, 0.1)).toBe(20.5);
43 | expect(roundValue(21.5, 19, 21, 0.1)).toBe(21);
44 | expect(roundValue(20.9, 19, 21, 0.1)).toBe(20.9);
45 | expect(roundValue(20.96, 19, 21, 0.1)).toBe(21);
46 | });
47 |
48 | it('should handle NaN and Infinity correctly', () => {
49 | expect(roundValue(Infinity, 19, 21, 0.1)).toBe(19);
50 | expect(roundValue(NaN, 19, 21, 0.1)).toBe(19);
51 | expect(roundValue(Infinity, 19.01, 21, 0.1)).toBe(19);
52 | expect(roundValue(NaN, 19.01, 21, 0.1)).toBe(19);
53 | expect(roundValue(Infinity)).toBe(0);
54 | expect(roundValue(NaN)).toBe(0);
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/test/setupTests.ts:
--------------------------------------------------------------------------------
1 | import { cleanup } from '@testing-library/react';
2 | import { afterEach } from 'vitest';
3 | import '@testing-library/jest-dom/vitest';
4 |
5 | afterEach(cleanup);
6 |
--------------------------------------------------------------------------------
/test/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs
3 | "include": ["."],
4 | "compilerOptions": {
5 | "target": "ES2020",
6 | "module": "ESNext",
7 | "moduleResolution": "Node",
8 | "declaration": true,
9 | "lib": ["ES2020", "DOM"],
10 | "downlevelIteration": true,
11 | "strict": true,
12 | "esModuleInterop": true,
13 | "jsx": "react",
14 | "outDir": "./dist",
15 | "types": ["vitest/globals"]
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs
3 | "include": ["src"],
4 | "compilerOptions": {
5 | "target": "ES2020",
6 | "module": "ESNext",
7 | "moduleResolution": "Node",
8 | "declaration": true,
9 | "lib": ["ES2020", "DOM"],
10 | "downlevelIteration": true,
11 | "strict": true,
12 | "esModuleInterop": true,
13 | "jsx": "react",
14 | "outDir": "./dist"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 | import typescript from '@rollup/plugin-typescript';
4 |
5 | // https://vitejs.dev/config/
6 | export default defineConfig(() => ({
7 | build: {
8 | lib: {
9 | entry: 'src/index.tsx',
10 | formats: ['es'] as any,
11 | },
12 | rollupOptions: {
13 | output: {
14 | assetFileNames: assetInfo => {
15 | return assetInfo.name === 'style.css' ? 'index.css' : assetInfo.name!;
16 | },
17 | },
18 | external: [
19 | 'react',
20 | 'react-dom',
21 | 'filesize',
22 | 'radash',
23 | '@uiw/react-color-sketch',
24 | 'react-use-pointer-drag',
25 | 'react/jsx-runtime',
26 | ],
27 | },
28 | },
29 | plugins: [
30 | {
31 | ...typescript(),
32 | apply: 'build',
33 | } as any,
34 | react({ jsxRuntime: 'classic' }),
35 | ],
36 | }));
37 |
--------------------------------------------------------------------------------
/vite.storybook.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig(() => ({
6 | build: {
7 | lib: {
8 | entry: 'src/index.tsx',
9 | formats: ['es'] as any,
10 | },
11 | rollupOptions: {
12 | output: {
13 | assetFileNames: assetInfo => {
14 | return assetInfo.name === 'style.css' ? 'index.css' : assetInfo.name!;
15 | },
16 | },
17 | external: [
18 | 'react',
19 | 'react-dom',
20 | 'filesize',
21 | 'radash',
22 | 'react-color',
23 | 'react-use-pointer-drag',
24 | 'react/jsx-runtime',
25 | ],
26 | },
27 | },
28 | plugins: [react({ jsxRuntime: 'classic' })],
29 | }));
30 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitest/config';
2 | import react from '@vitejs/plugin-react';
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig(() => ({
6 | name: 'react-var-ui',
7 | test: {
8 | globals: true,
9 | threads: false,
10 | environment: 'jsdom',
11 | setupFiles: 'test/setupTests.ts',
12 | },
13 | plugins: [react()],
14 | }));
15 |
--------------------------------------------------------------------------------