@shulya/liquid-glass 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/vue.mjs ADDED
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Vue 3 Adapter for Liquid Glass UI (Component + v-liquid-glass Directive)
3
+ */
4
+ import { defineComponent, h, ref, onMounted, onBeforeUnmount, watch } from 'vue';
5
+ import { LiquidGlassController, OPTICAL_DEFAULTS } from './core.mjs';
6
+
7
+ /**
8
+ * Vue Directive: v-liquid-glass="{ rim: 16, ior: 1.46, height: 18, frosted: 0, fusion: true }"
9
+ */
10
+ export const vLiquidGlass = {
11
+ mounted(el, binding) {
12
+ el.__liquidGlass = new LiquidGlassController(el, binding.value || {});
13
+ },
14
+ updated(el, binding) {
15
+ if (el.__liquidGlass) {
16
+ el.__liquidGlass.update(binding.value || {});
17
+ }
18
+ },
19
+ unmounted(el) {
20
+ if (el.__liquidGlass) {
21
+ el.__liquidGlass.destroy();
22
+ delete el.__liquidGlass;
23
+ }
24
+ }
25
+ };
26
+
27
+ /**
28
+ * <LiquidGlass> Vue 3 Component
29
+ */
30
+ export const LiquidGlass = defineComponent({
31
+ name: 'LiquidGlass',
32
+ props: {
33
+ rim: { type: Number, default: OPTICAL_DEFAULTS.rim },
34
+ ior: { type: Number, default: OPTICAL_DEFAULTS.ior },
35
+ height: { type: Number, default: OPTICAL_DEFAULTS.height },
36
+ frosted: { type: Number, default: OPTICAL_DEFAULTS.frosted },
37
+ fusion: { type: Boolean, default: OPTICAL_DEFAULTS.fusion },
38
+ tag: { type: String, default: 'div' }
39
+ },
40
+ setup(props, { slots, attrs }) {
41
+ const rootRef = ref(null);
42
+ let controller = null;
43
+
44
+ onMounted(() => {
45
+ if (rootRef.value) {
46
+ controller = new LiquidGlassController(rootRef.value, {
47
+ rim: props.rim,
48
+ ior: props.ior,
49
+ height: props.height,
50
+ frosted: props.frosted,
51
+ fusion: props.fusion
52
+ });
53
+ }
54
+ });
55
+
56
+ watch(
57
+ () => [props.rim, props.ior, props.height, props.frosted, props.fusion],
58
+ ([rim, ior, height, frosted, fusion]) => {
59
+ if (controller) {
60
+ controller.update({ rim, ior, height, frosted, fusion });
61
+ }
62
+ }
63
+ );
64
+
65
+ onBeforeUnmount(() => {
66
+ if (controller) {
67
+ controller.destroy();
68
+ controller = null;
69
+ }
70
+ });
71
+
72
+ return () =>
73
+ h(
74
+ props.tag,
75
+ {
76
+ ref: rootRef,
77
+ ...attrs
78
+ },
79
+ slots.default ? slots.default() : null
80
+ );
81
+ }
82
+ });
83
+
84
+ export const LiquidGlassPlugin = {
85
+ install(app) {
86
+ app.component('LiquidGlass', LiquidGlass);
87
+ app.directive('liquid-glass', vLiquidGlass);
88
+ }
89
+ };
90
+
91
+ export { LiquidGlassController, OPTICAL_DEFAULTS, OPTICAL_LIMITS } from './core.mjs';
92
+ export default LiquidGlass;
@@ -0,0 +1,38 @@
1
+ # `@shulya/liquid-glass` — React Interactive Playground (`example/`)
2
+
3
+ - **Live Demo:** [https://liquid.shulya.tech](https://liquid.shulya.tech)
4
+ - **NPM Package:** [https://www.npmjs.com/package/@shulya/liquid-glass](https://www.npmjs.com/package/@shulya/liquid-glass)
5
+ - **GitHub Repository:** [https://github.com/shulya-tech/liquid-glass](https://github.com/shulya-tech/liquid-glass)
6
+
7
+ ![Liquid Glass React Playground](../screen.png)
8
+
9
+ This directory contains a complete **React + Vite** application demonstrating the `<LiquidGlass>` component from `@shulya/liquid-glass/react`.
10
+
11
+ ## Overview
12
+
13
+ - **Three Draggable `<LiquidGlass>` Components:**
14
+ - **Capsule Slider (`136 × 74 px`, radius `37 px`)** positioned over a green switch track.
15
+ - **Rounded Rectangle (`196 × 116 px`, radius `22 px`)** positioned over an electric blue gradient card.
16
+ - **Circular Lens (`112 × 112 px`, radius `56 px`)** positioned over a warm sunset sphere.
17
+ - **High-Contrast Optical Stage:** Features a fine `56 px` architectural hairline grid, a thin diagonal rainbow ribbon, high-contrast black/white stripes, and centered `"Shulya"` typography to inspect edge refraction, chromatic dispersion, and specular bevel highlights.
18
+ - **Liquid Surface Coalescence (`fusion`):** Drag any two or all three `<LiquidGlass>` elements close together to observe real-time liquid bridge coalescence powered by the precomputed empirical lookup tables (`_T0`–`_T4`), or toggle **Liquid Fusion** off in the bottom control shelf to inspect isolated lenses.
19
+ - **Live React State Controls:**
20
+ - **Bevel Width (`rim`):** `6–32 px` (default `16 px`)
21
+ - **Refraction (`ior`):** `1.10–1.70` (default `1.46`)
22
+ - **Thickness (`height`):** `6–34 px` (default `18 px`)
23
+ - **Frosted Blur (`frosted`):** `0–30 px` (default `0 px`)
24
+ - **Liquid Fusion (`fusion`):** `true / false` toggle
25
+
26
+ ## Getting Started
27
+
28
+ ```bash
29
+ # 1. From the root of the repository, build the library dist bundles:
30
+ npm run build
31
+
32
+ # 2. Install example dependencies and start the Vite dev server:
33
+ cd example
34
+ npm install
35
+ npm run dev
36
+ ```
37
+
38
+ Then open **`http://localhost:5173`** in your browser.
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
7
+ <title>Liquid Glass UI — React Interactive Playground</title>
8
+ </head>
9
+
10
+ <body>
11
+ <div id="root"></div>
12
+ <script type="module" src="/src/main.jsx"></script>
13
+ </body>
14
+
15
+ </html>
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "liquid-glass-ui-react-example",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "description": "Interactive React playground for @shulya/liquid-glass (https://liquid.shulya.tech)",
6
+ "homepage": "https://liquid.shulya.tech",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/shulya-tech/liquid-glass.git",
10
+ "directory": "example"
11
+ },
12
+ "type": "module",
13
+ "scripts": {
14
+ "dev": "vite",
15
+ "build": "vite build",
16
+ "preview": "vite preview"
17
+ },
18
+ "dependencies": {
19
+ "liquid-glass-ui": "file:..",
20
+ "react": "^19.0.0",
21
+ "react-dom": "^19.0.0"
22
+ },
23
+ "devDependencies": {
24
+ "@vitejs/plugin-react": "^4.3.4",
25
+ "vite": "^6.2.0"
26
+ }
27
+ }
@@ -0,0 +1,398 @@
1
+ import React, {
2
+ useEffect,
3
+ useLayoutEffect,
4
+ useRef,
5
+ useState,
6
+ useCallback
7
+ } from 'react';
8
+ import {
9
+ LiquidGlass,
10
+ OPTICAL_DEFAULTS,
11
+ scheduleStageRedraw
12
+ } from '@shulya/liquid-glass/react';
13
+
14
+ /**
15
+ * Draws a crisp, high-DPI optical test stage onto the background canvas
16
+ * using its exact rendered getBoundingClientRect() dimensions so desktop
17
+ * and mobile viewports always match 1:1 to the sub-pixel.
18
+ */
19
+ function drawStageBackground(canvas) {
20
+ if (!canvas) return;
21
+ const rect = canvas.getBoundingClientRect();
22
+ const w = Math.max(1, Math.round(rect.width || window.innerWidth));
23
+ const h = Math.max(1, Math.round(rect.height || window.innerHeight));
24
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
25
+ const isMobile = w < 640;
26
+
27
+ canvas.width = Math.floor(w * dpr);
28
+ canvas.height = Math.floor(h * dpr);
29
+
30
+ const ctx = canvas.getContext('2d');
31
+ if (!ctx) return;
32
+
33
+ ctx.save();
34
+ ctx.scale(dpr, dpr);
35
+
36
+ // 1. Clean Light Gray Backdrop (#ebedf2)
37
+ ctx.fillStyle = '#ebedf2';
38
+ ctx.fillRect(0, 0, w, h);
39
+
40
+ // Fine architectural hairline grid (56px step, 0.5px crisp stroke)
41
+ ctx.strokeStyle = 'rgba(0, 0, 0, 0.035)';
42
+ ctx.lineWidth = 0.5;
43
+ const step = isMobile ? 44 : 56;
44
+ ctx.beginPath();
45
+ for (let x = 0; x < w; x += step) {
46
+ ctx.moveTo(x + 0.5, 0);
47
+ ctx.lineTo(x + 0.5, h);
48
+ }
49
+ for (let y = 0; y < h; y += step) {
50
+ ctx.moveTo(0, y + 0.5);
51
+ ctx.lineTo(w, y + 0.5);
52
+ }
53
+ ctx.stroke();
54
+
55
+ // 2. Sleek Green Switch Track (Top-Left)
56
+ const pillX = isMobile ? w * 0.24 : w * 0.16;
57
+ const pillY = isMobile ? h * 0.18 : h * 0.22;
58
+ const pillW = isMobile ? 116 : 160;
59
+ const pillH = isMobile ? 20 : 26;
60
+ ctx.save();
61
+ ctx.beginPath();
62
+ ctx.roundRect(pillX - pillW / 2, pillY - pillH / 2, pillW, pillH, pillH / 2);
63
+ ctx.fillStyle = '#32d74b';
64
+ ctx.fill();
65
+ ctx.restore();
66
+
67
+ // 3. Thin Diagonal Rainbow Gradient Ribbon
68
+ ctx.save();
69
+ ctx.translate(w * 0.5, isMobile ? h * 0.44 : h * 0.51);
70
+ ctx.rotate(-0.2);
71
+ const ribHalfLen = isMobile ? Math.min(w * 0.4, 150) : Math.min(w * 0.28, 310);
72
+ const ribThickness = isMobile ? 18 : 26;
73
+ const ribGrad = ctx.createLinearGradient(-ribHalfLen, 0, ribHalfLen, 0);
74
+ ribGrad.addColorStop(0.0, '#ff2d55');
75
+ ribGrad.addColorStop(0.25, '#ff9500');
76
+ ribGrad.addColorStop(0.5, '#ffcc00');
77
+ ribGrad.addColorStop(0.75, '#5ac8fa');
78
+ ribGrad.addColorStop(1.0, '#5856d6');
79
+ ctx.fillStyle = ribGrad;
80
+ ctx.beginPath();
81
+ ctx.roundRect(
82
+ -ribHalfLen,
83
+ -ribThickness / 2,
84
+ ribHalfLen * 2,
85
+ ribThickness,
86
+ ribThickness / 2
87
+ );
88
+ ctx.fill();
89
+ ctx.restore();
90
+
91
+ // 4. Refined Electric Blue Card (Right Center)
92
+ ctx.save();
93
+ const cardW = isMobile ? 108 : 152;
94
+ const cardH = isMobile ? 64 : 88;
95
+ const cardX = isMobile ? w * 0.74 - cardW * 0.5 : w * 0.67;
96
+ const cardY = isMobile ? h * 0.17 : h * 0.24;
97
+ const blueGrad = ctx.createLinearGradient(cardX, cardY, cardX + cardW, cardY + cardH);
98
+ blueGrad.addColorStop(0, '#007aff');
99
+ blueGrad.addColorStop(1, '#5856d6');
100
+ ctx.fillStyle = blueGrad;
101
+ ctx.beginPath();
102
+ ctx.roundRect(cardX, cardY, cardW, cardH, isMobile ? 16 : 22);
103
+ ctx.fill();
104
+ ctx.restore();
105
+
106
+ // 5. Compact Sunset Orange Sphere (Bottom Left)
107
+ ctx.save();
108
+ const orbX = isMobile ? w * 0.25 : w * 0.26;
109
+ const orbY = isMobile ? h * 0.60 : h * 0.67;
110
+ const orbR = isMobile ? 38 : 54;
111
+ const orbGrad = ctx.createRadialGradient(orbX - 10, orbY - 10, 4, orbX, orbY, orbR);
112
+ orbGrad.addColorStop(0, '#ffcc00');
113
+ orbGrad.addColorStop(0.6, '#ff3b30');
114
+ orbGrad.addColorStop(1, '#c91f66');
115
+ ctx.fillStyle = orbGrad;
116
+ ctx.beginPath();
117
+ ctx.arc(orbX, orbY, orbR, 0, Math.PI * 2);
118
+ ctx.fill();
119
+ ctx.restore();
120
+
121
+ // 6. Fine Optical Stripe Grating (Bottom Right)
122
+ ctx.save();
123
+ const stW = isMobile ? 112 : 160;
124
+ const stH = isMobile ? 68 : 92;
125
+ const stX = isMobile ? w * 0.72 - stW * 0.5 : w * 0.63;
126
+ const stY = isMobile ? h * 0.56 : h * 0.62;
127
+ const stripeWidth = isMobile ? 8 : 10;
128
+ ctx.beginPath();
129
+ ctx.roundRect(stX, stY, stW, stH, isMobile ? 14 : 18);
130
+ ctx.clip();
131
+ for (let i = 0; i < stW / stripeWidth; i++) {
132
+ ctx.fillStyle = i % 2 === 0 ? '#1c1c1e' : '#ffffff';
133
+ ctx.fillRect(stX + i * stripeWidth, stY, stripeWidth, stH);
134
+ }
135
+ ctx.restore();
136
+
137
+ // 7. Sleek Center Typography ("Shulya")
138
+ ctx.save();
139
+ const fontSize = isMobile ? Math.min(w * 0.13, 52) : Math.min(w * 0.082, 92);
140
+ ctx.font = `600 ${fontSize}px -apple-system, BlinkMacSystemFont, "SF Pro Display", "Inter", sans-serif`;
141
+ ctx.textAlign = 'center';
142
+ ctx.textBaseline = 'middle';
143
+ ctx.fillStyle = '#16181d';
144
+ ctx.fillText('Shulya', w * 0.5, isMobile ? h * 0.38 : h * 0.43);
145
+ ctx.restore();
146
+
147
+ ctx.restore();
148
+ }
149
+
150
+ function createInitialShapes(w, h) {
151
+ const isMobile = w < 640;
152
+ if (isMobile) {
153
+ return [
154
+ { id: 'slider', x: w * 0.26, y: h * 0.18, w: 104, h: 58, r: 29 },
155
+ { id: 'rect', x: w * 0.74, y: h * 0.21, w: 132, h: 82, r: 18 },
156
+ { id: 'circle', x: w * 0.26, y: h * 0.60, w: 88, h: 88, r: 44 }
157
+ ];
158
+ }
159
+ return [
160
+ { id: 'slider', x: w * 0.16 + 42, y: h * 0.22, w: 136, h: 74, r: 37 },
161
+ { id: 'rect', x: w * 0.71, y: h * 0.21, w: 196, h: 116, r: 22 },
162
+ { id: 'circle', x: w * 0.22, y: h * 0.65, w: 112, h: 112, r: 56 }
163
+ ];
164
+ }
165
+
166
+ export default function App() {
167
+ const stageRef = useRef(null);
168
+
169
+ // Live optical configuration parameters bound to <LiquidGlass> props
170
+ const [rim, setRim] = useState(OPTICAL_DEFAULTS.rim);
171
+ const [ior, setIor] = useState(OPTICAL_DEFAULTS.ior);
172
+ const [height, setHeight] = useState(OPTICAL_DEFAULTS.height);
173
+ const [frosted, setFrosted] = useState(OPTICAL_DEFAULTS.frosted);
174
+ const [fusion, setFusion] = useState(OPTICAL_DEFAULTS.fusion);
175
+ const [fps, setFps] = useState(60);
176
+
177
+ // Three sleek draggable LiquidGlass bodies
178
+ const [shapes, setShapes] = useState(() => {
179
+ const w = typeof window !== 'undefined' ? window.innerWidth : 1280;
180
+ const h = typeof window !== 'undefined' ? window.innerHeight : 800;
181
+ return createInitialShapes(w, h);
182
+ });
183
+
184
+ const dragState = useRef(null);
185
+ const lastModeRef = useRef(
186
+ typeof window !== 'undefined' ? window.innerWidth < 640 : false
187
+ );
188
+
189
+ // Synchronously redraw stage background before browser paint and on resize
190
+ useLayoutEffect(() => {
191
+ const canvas = stageRef.current;
192
+ if (!canvas) return;
193
+
194
+ const syncStage = () => {
195
+ const rect = canvas.getBoundingClientRect();
196
+ const w = Math.max(1, Math.round(rect.width || window.innerWidth));
197
+ const h = Math.max(1, Math.round(rect.height || window.innerHeight));
198
+ const isMobile = w < 640;
199
+
200
+ if (isMobile !== lastModeRef.current) {
201
+ lastModeRef.current = isMobile;
202
+ setShapes(createInitialShapes(w, h));
203
+ }
204
+
205
+ drawStageBackground(canvas);
206
+ scheduleStageRedraw();
207
+ };
208
+
209
+ syncStage();
210
+
211
+ let ro = null;
212
+ if (typeof ResizeObserver !== 'undefined') {
213
+ ro = new ResizeObserver(syncStage);
214
+ ro.observe(canvas);
215
+ }
216
+ window.addEventListener('resize', syncStage);
217
+
218
+ return () => {
219
+ if (ro) ro.disconnect();
220
+ window.removeEventListener('resize', syncStage);
221
+ };
222
+ }, []);
223
+
224
+ // Trigger optical redraw synchronously after React commits new shape positions or parameters
225
+ useLayoutEffect(() => {
226
+ scheduleStageRedraw();
227
+ }, [shapes, rim, ior, height, frosted, fusion]);
228
+
229
+ // FPS monitor
230
+ useEffect(() => {
231
+ let frameCount = 0;
232
+ let lastTime = performance.now();
233
+ let rafId;
234
+
235
+ const tick = (now) => {
236
+ frameCount++;
237
+ if (now - lastTime >= 500) {
238
+ setFps(Math.min(120, Math.round((frameCount * 1000) / (now - lastTime))));
239
+ frameCount = 0;
240
+ lastTime = now;
241
+ }
242
+ rafId = requestAnimationFrame(tick);
243
+ };
244
+
245
+ rafId = requestAnimationFrame(tick);
246
+ return () => cancelAnimationFrame(rafId);
247
+ }, []);
248
+
249
+ // Pointer dragging handlers
250
+ const handlePointerDown = useCallback((id, e) => {
251
+ e.currentTarget.setPointerCapture(e.pointerId);
252
+ setShapes((prev) => {
253
+ const idx = prev.findIndex((s) => s.id === id);
254
+ if (idx === -1) return prev;
255
+ const target = prev[idx];
256
+ dragState.current = {
257
+ id,
258
+ offX: e.clientX - target.x,
259
+ offY: e.clientY - target.y
260
+ };
261
+ // Move active dragged shape to top of render order
262
+ const next = [...prev];
263
+ const [picked] = next.splice(idx, 1);
264
+ next.push(picked);
265
+ return next;
266
+ });
267
+ }, []);
268
+
269
+ useEffect(() => {
270
+ const handlePointerMove = (e) => {
271
+ if (!dragState.current) return;
272
+ const { id, offX, offY } = dragState.current;
273
+ const nextX = e.clientX - offX;
274
+ const nextY = e.clientY - offY;
275
+
276
+ setShapes((prev) =>
277
+ prev.map((s) => (s.id === id ? { ...s, x: nextX, y: nextY } : s))
278
+ );
279
+ };
280
+
281
+ const handlePointerUp = () => {
282
+ dragState.current = null;
283
+ };
284
+
285
+ window.addEventListener('pointermove', handlePointerMove);
286
+ window.addEventListener('pointerup', handlePointerUp);
287
+ window.addEventListener('pointercancel', handlePointerUp);
288
+ return () => {
289
+ window.removeEventListener('pointermove', handlePointerMove);
290
+ window.removeEventListener('pointerup', handlePointerUp);
291
+ window.removeEventListener('pointercancel', handlePointerUp);
292
+ };
293
+ }, []);
294
+
295
+ return (
296
+ <>
297
+ {/* Background Optical Test Stage */}
298
+ <canvas ref={stageRef} className="stage-canvas" />
299
+
300
+ {/* Three Draggable <LiquidGlass> React Components */}
301
+ {shapes.map((s, index) => (
302
+ <LiquidGlass
303
+ key={s.id}
304
+ rim={rim}
305
+ ior={ior}
306
+ height={height}
307
+ frosted={frosted}
308
+ fusion={fusion}
309
+ className="glass-body"
310
+ data-lg-tint="rgba(255, 255, 255, 0.03)"
311
+ onPointerDown={(e) => handlePointerDown(s.id, e)}
312
+ style={{
313
+ width: `${s.w}px`,
314
+ height: `${s.h}px`,
315
+ borderRadius: `${s.r}px`,
316
+ left: `${Math.round(s.x - s.w * 0.5)}px`,
317
+ top: `${Math.round(s.y - s.h * 0.5)}px`,
318
+ zIndex: 20 + index
319
+ }}
320
+ />
321
+ ))}
322
+
323
+ {/* Bottom Interactive Control Shelf */}
324
+ <div className="shelf-bar">
325
+ <div className="ctrl">
326
+ <div className="ctrl-head">
327
+ <span>Bevel Width (Rim)</span>
328
+ <span className="ctrl-val">{rim} px</span>
329
+ </div>
330
+ <input
331
+ type="range"
332
+ min="6"
333
+ max="32"
334
+ step="1"
335
+ value={rim}
336
+ onChange={(e) => setRim(Number(e.target.value))}
337
+ />
338
+ </div>
339
+
340
+ <div className="ctrl">
341
+ <div className="ctrl-head">
342
+ <span>Refraction (IOR)</span>
343
+ <span className="ctrl-val">{ior.toFixed(2)}</span>
344
+ </div>
345
+ <input
346
+ type="range"
347
+ min="1.10"
348
+ max="1.70"
349
+ step="0.02"
350
+ value={ior}
351
+ onChange={(e) => setIor(Number(e.target.value))}
352
+ />
353
+ </div>
354
+
355
+ <div className="ctrl">
356
+ <div className="ctrl-head">
357
+ <span>Thickness (Height)</span>
358
+ <span className="ctrl-val">{height} px</span>
359
+ </div>
360
+ <input
361
+ type="range"
362
+ min="6"
363
+ max="34"
364
+ step="1"
365
+ value={height}
366
+ onChange={(e) => setHeight(Number(e.target.value))}
367
+ />
368
+ </div>
369
+
370
+ <div className="ctrl">
371
+ <div className="ctrl-head">
372
+ <span>Frosted Blur</span>
373
+ <span className="ctrl-val">{frosted} px</span>
374
+ </div>
375
+ <input
376
+ type="range"
377
+ min="0"
378
+ max="30"
379
+ step="1"
380
+ value={frosted}
381
+ onChange={(e) => setFrosted(Number(e.target.value))}
382
+ />
383
+ </div>
384
+
385
+ <label className="ctrl-toggle">
386
+ <input
387
+ type="checkbox"
388
+ checked={fusion}
389
+ onChange={(e) => setFusion(e.target.checked)}
390
+ />
391
+ <span>Liquid Fusion</span>
392
+ </label>
393
+
394
+ <div className="fps-tag">{fps} FPS</div>
395
+ </div>
396
+ </>
397
+ );
398
+ }
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import App from './App.jsx';
4
+ import './styles.css';
5
+
6
+ createRoot(document.getElementById('root')).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>
10
+ );