NERDDISCO commited on
Commit
0d9f1af
·
1 Parent(s): ec936d5

feat: added react / tailwind ui

Browse files
index.html CHANGED
@@ -3,30 +3,79 @@
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>🤖 lerobot.js - Robotics in the Browser</title>
7
  <meta
8
  name="description"
9
- content="State-of-the-art AI for real-world robotics in JavaScript/TypeScript"
10
  />
11
  <style>
12
- /* Prevent flash of unstyled content */
13
- body {
14
- opacity: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  }
16
- body.loaded {
17
- opacity: 1;
18
- transition: opacity 0.3s ease;
 
 
 
 
 
19
  }
20
  </style>
21
  </head>
22
  <body>
23
- <div id="app"></div>
24
- <script type="module" src="/src/main.ts"></script>
25
- <script>
26
- // Add loaded class when page is ready
27
- window.addEventListener("load", () => {
28
- document.body.classList.add("loaded");
29
- });
30
- </script>
31
  </body>
32
  </html>
 
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>🤖 lerobot.js - React Demo</title>
7
  <meta
8
  name="description"
9
+ content="State-of-the-art AI for real-world robotics in JavaScript/TypeScript - Interactive React Demo"
10
  />
11
  <style>
12
+ :root {
13
+ --background: 0 0% 100%;
14
+ --foreground: 240 10% 3.9%;
15
+ --card: 0 0% 100%;
16
+ --card-foreground: 240 10% 3.9%;
17
+ --popover: 0 0% 100%;
18
+ --popover-foreground: 240 10% 3.9%;
19
+ --primary: 240 9% 17%;
20
+ --primary-foreground: 0 0% 98%;
21
+ --secondary: 240 4.8% 95.9%;
22
+ --secondary-foreground: 240 5.9% 10%;
23
+ --muted: 240 4.8% 95.9%;
24
+ --muted-foreground: 240 3.8% 46.1%;
25
+ --accent: 240 4.8% 95.9%;
26
+ --accent-foreground: 240 5.9% 10%;
27
+ --destructive: 0 84.2% 60.2%;
28
+ --destructive-foreground: 0 0% 98%;
29
+ --border: 240 5.9% 90%;
30
+ --input: 240 5.9% 90%;
31
+ --ring: 240 10% 3.9%;
32
+ --chart-1: 12 76% 61%;
33
+ --chart-2: 173 58% 39%;
34
+ --chart-3: 197 37% 24%;
35
+ --chart-4: 43 74% 66%;
36
+ --chart-5: 27 87% 67%;
37
+ --radius: 0.5rem;
38
+ }
39
+
40
+ .dark {
41
+ --background: 240 10% 3.9%;
42
+ --foreground: 0 0% 98%;
43
+ --card: 240 10% 3.9%;
44
+ --card-foreground: 0 0% 98%;
45
+ --popover: 240 10% 3.9%;
46
+ --popover-foreground: 0 0% 98%;
47
+ --primary: 0 0% 98%;
48
+ --primary-foreground: 240 5.9% 10%;
49
+ --secondary: 240 3.7% 15.9%;
50
+ --secondary-foreground: 0 0% 98%;
51
+ --muted: 240 3.7% 15.9%;
52
+ --muted-foreground: 240 5% 64.9%;
53
+ --accent: 240 3.7% 15.9%;
54
+ --accent-foreground: 0 0% 98%;
55
+ --destructive: 0 62.8% 30.6%;
56
+ --destructive-foreground: 0 0% 98%;
57
+ --border: 240 3.7% 15.9%;
58
+ --input: 240 3.7% 15.9%;
59
+ --ring: 240 4.9% 83.9%;
60
+ --chart-1: 220 70% 50%;
61
+ --chart-2: 160 60% 45%;
62
+ --chart-3: 30 80% 55%;
63
+ --chart-4: 280 65% 60%;
64
+ --chart-5: 340 75% 55%;
65
  }
66
+
67
+ * {
68
+ border-color: hsl(var(--border));
69
+ }
70
+
71
+ body {
72
+ background-color: hsl(var(--background));
73
+ color: hsl(var(--foreground));
74
  }
75
  </style>
76
  </head>
77
  <body>
78
+ <div id="root"></div>
79
+ <script type="module" src="/src/demo/main.tsx"></script>
 
 
 
 
 
 
80
  </body>
81
  </html>
package.json CHANGED
@@ -19,10 +19,13 @@
19
  "lerobot"
20
  ],
21
  "scripts": {
22
- "dev": "vite",
 
 
23
  "build": "pnpm run build:cli",
24
  "build:cli": "tsc --project tsconfig.cli.json",
25
- "build:web": "tsc && vite build",
 
26
  "preview": "vite preview",
27
  "cli:find-port": "tsx src/cli/index.ts find-port",
28
  "cli:calibrate": "tsx src/cli/index.ts calibrate",
@@ -35,6 +38,18 @@
35
  },
36
  "devDependencies": {
37
  "@types/node": "^22.10.5",
 
 
 
 
 
 
 
 
 
 
 
 
38
  "tsx": "^4.19.2",
39
  "typescript": "~5.8.3",
40
  "vite": "^6.3.5"
 
19
  "lerobot"
20
  ],
21
  "scripts": {
22
+ "dev": "vite --mode demo",
23
+ "dev:vanilla": "vite --mode vanilla",
24
+ "dev:lib": "vite --mode lib",
25
  "build": "pnpm run build:cli",
26
  "build:cli": "tsc --project tsconfig.cli.json",
27
+ "build:web": "tsc && vite build --mode lib",
28
+ "build:demo": "tsc && vite build --mode demo",
29
  "preview": "vite preview",
30
  "cli:find-port": "tsx src/cli/index.ts find-port",
31
  "cli:calibrate": "tsx src/cli/index.ts calibrate",
 
38
  },
39
  "devDependencies": {
40
  "@types/node": "^22.10.5",
41
+ "@types/react": "^18.2.79",
42
+ "@types/react-dom": "^18.2.25",
43
+ "@vitejs/plugin-react": "^4.2.1",
44
+ "autoprefixer": "^10.4.19",
45
+ "class-variance-authority": "^0.7.0",
46
+ "clsx": "^2.1.1",
47
+ "lucide-react": "^0.400.0",
48
+ "postcss": "^8.4.38",
49
+ "react": "^18.2.0",
50
+ "react-dom": "^18.2.0",
51
+ "tailwind-merge": "^2.3.0",
52
+ "tailwindcss": "^3.4.0",
53
  "tsx": "^4.19.2",
54
  "typescript": "~5.8.3",
55
  "vite": "^6.3.5"
pnpm-lock.yaml CHANGED
@@ -18,6 +18,42 @@ importers:
18
  '@types/node':
19
  specifier: ^22.10.5
20
  version: 22.15.31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  tsx:
22
  specifier: ^4.19.2
23
  version: 4.20.3
@@ -26,10 +62,97 @@ importers:
26
  version: 5.8.3
27
  vite:
28
  specifier: ^6.3.5
29
- version: 6.3.5(@types/[email protected])([email protected])
30
 
31
  packages:
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  '@esbuild/[email protected]':
34
  resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==}
35
  engines: {node: '>=18'}
@@ -180,6 +303,47 @@ packages:
180
  cpu: [x64]
181
  os: [win32]
182
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  '@rollup/[email protected]':
184
  resolution: {integrity: sha512-Krjy9awJl6rKbruhQDgivNbD1WuLb8xAclM4IR4cN5pHGAs2oIMMQJEiC3IC/9TZJ+QZkmZhlMO/6MBGxPidpw==}
185
  cpu: [arm]
@@ -344,28 +508,145 @@ packages:
344
  resolution: {integrity: sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==}
345
  engines: {node: '>=12.0.0'}
346
 
 
 
 
 
 
 
 
 
 
 
 
 
347
  '@types/[email protected]':
348
  resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
349
 
350
  '@types/[email protected]':
351
  resolution: {integrity: sha512-jnVe5ULKl6tijxUhvQeNbQG/84fHfg+yMak02cT8QVhBx/F05rAVxCGBYYTh2EKz22D6JF5ktXuNwdx7b9iEGw==}
352
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
354
  resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==}
355
  engines: {node: '>=18'}
356
 
 
 
 
 
357
358
  resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==}
359
  engines: {node: '>=12'}
360
 
 
 
 
 
361
362
  resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
363
  engines: {node: '>=12'}
364
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
366
  resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
367
  engines: {node: '>=18'}
368
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
370
  resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
371
  engines: {node: '>=6.0'}
@@ -375,9 +656,27 @@ packages:
375
  supports-color:
376
  optional: true
377
 
 
 
 
 
 
 
 
 
 
 
 
 
378
379
  resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==}
380
 
 
 
 
 
 
 
381
382
  resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
383
  engines: {node: '>=18'}
@@ -387,6 +686,17 @@ packages:
387
  engines: {node: '>=18'}
388
  hasBin: true
389
 
 
 
 
 
 
 
 
 
 
 
 
390
391
  resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==}
392
  peerDependencies:
@@ -395,11 +705,29 @@ packages:
395
  picomatch:
396
  optional: true
397
 
 
 
 
 
 
 
 
 
 
 
 
398
399
  resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
400
  engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
401
  os: [darwin]
402
 
 
 
 
 
 
 
 
403
404
  resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==}
405
  engines: {node: '>=18'}
@@ -407,21 +735,129 @@ packages:
407
408
  resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
409
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
411
  resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==}
412
  engines: {node: '>=18'}
413
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
415
  resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==}
416
  engines: {node: '>=18'}
417
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
419
  resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
420
  engines: {node: '>=18'}
421
 
 
 
 
 
 
 
 
 
422
423
  resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
424
 
 
 
 
425
426
  resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
427
  engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -434,37 +870,169 @@ packages:
434
  resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==}
435
  hasBin: true
436
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
437
438
  resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
439
  engines: {node: '>=18'}
440
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
441
442
  resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
443
 
 
 
 
 
444
445
  resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
446
  engines: {node: '>=12'}
447
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
448
449
  resolution: {integrity: sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==}
450
  engines: {node: ^10 || ^12 || >=14}
451
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
452
453
  resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
454
 
 
 
 
 
 
455
456
  resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
457
  engines: {node: '>=18'}
458
 
 
 
 
 
459
460
  resolution: {integrity: sha512-wdN2Kd3Twh8MAEOEJZsuxuLKCsBEo4PVNLK6tQWAn10VhsVewQLzcucMgLolRlhFybGxfclbPeEYBaP6RvUFGg==}
461
  engines: {node: '>=18.0.0', npm: '>=8.0.0'}
462
  hasBin: true
463
 
 
 
 
 
 
 
 
 
 
 
464
465
  resolution: {integrity: sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA==}
466
  engines: {node: '>=16.0.0'}
467
 
 
 
 
 
 
 
 
 
468
469
  resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
470
  engines: {node: '>=14'}
@@ -477,18 +1045,61 @@ packages:
477
  resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
478
  engines: {node: '>=0.10.0'}
479
 
 
 
 
 
 
 
 
 
480
481
  resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
482
  engines: {node: '>=18'}
483
 
 
 
 
 
484
485
  resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
486
  engines: {node: '>=12'}
487
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
488
489
  resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
490
  engines: {node: '>=12.0.0'}
491
 
 
 
 
 
 
 
 
492
493
  resolution: {integrity: sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==}
494
  engines: {node: '>=18.0.0'}
@@ -502,6 +1113,15 @@ packages:
502
503
  resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
504
 
 
 
 
 
 
 
 
 
 
505
506
  resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==}
507
  engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@@ -542,12 +1162,150 @@ packages:
542
  yaml:
543
  optional: true
544
 
 
 
 
 
 
 
 
 
 
 
 
 
 
545
546
  resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==}
547
  engines: {node: '>=18'}
548
 
 
 
 
 
 
 
 
 
549
  snapshots:
550
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
  '@esbuild/[email protected]':
552
  optional: true
553
 
@@ -623,6 +1381,49 @@ snapshots:
623
  '@esbuild/[email protected]':
624
  optional: true
625
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
626
  '@rollup/[email protected]':
627
  optional: true
628
 
@@ -737,30 +1538,172 @@ snapshots:
737
  transitivePeerDependencies:
738
  - supports-color
739
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
740
  '@types/[email protected]': {}
741
 
742
  '@types/[email protected]':
743
  dependencies:
744
  undici-types: 6.21.0
745
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
746
747
  dependencies:
748
  environment: 1.1.0
749
 
 
 
750
751
 
 
 
 
 
752
753
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
754
755
  dependencies:
756
  restore-cursor: 5.1.0
757
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
758
759
  dependencies:
760
  ms: 2.1.2
761
 
 
 
 
 
 
 
 
 
762
763
 
 
 
 
 
764
765
 
766
@@ -791,23 +1734,113 @@ snapshots:
791
  '@esbuild/win32-ia32': 0.25.5
792
  '@esbuild/win32-x64': 0.25.5
793
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
794
795
  optionalDependencies:
796
  picomatch: 4.0.2
797
 
 
 
 
 
 
 
 
 
 
 
 
798
799
  optional: true
800
 
 
 
 
 
801
802
 
803
804
  dependencies:
805
  resolve-pkg-maps: 1.0.0
806
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
807
808
  dependencies:
809
  get-east-asian-width: 1.3.0
810
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
811
812
  dependencies:
813
  ansi-escapes: 7.0.0
@@ -816,37 +1849,158 @@ snapshots:
816
  strip-ansi: 7.1.0
817
  wrap-ansi: 9.0.0
818
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
819
820
 
 
 
 
 
 
 
821
822
 
 
 
 
 
 
 
823
824
 
825
826
 
827
828
 
 
 
 
 
 
 
 
 
 
 
829
830
  dependencies:
831
  mimic-function: 5.0.1
832
 
 
 
 
 
 
 
 
 
 
 
 
833
834
 
 
 
835
836
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
837
838
  dependencies:
839
  nanoid: 3.3.11
840
  picocolors: 1.1.1
841
  source-map-js: 1.2.1
842
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
843
844
 
 
 
 
 
 
 
845
846
  dependencies:
847
  onetime: 7.0.0
848
  signal-exit: 4.1.0
849
 
 
 
850
851
  dependencies:
852
  '@types/estree': 1.0.7
@@ -873,6 +2027,16 @@ snapshots:
873
  '@rollup/rollup-win32-x64-msvc': 4.43.0
874
  fsevents: 2.3.3
875
 
 
 
 
 
 
 
 
 
 
 
876
877
  dependencies:
878
  '@serialport/binding-mock': 10.2.2
@@ -892,6 +2056,12 @@ snapshots:
892
  transitivePeerDependencies:
893
  - supports-color
894
 
 
 
 
 
 
 
895
896
 
897
@@ -901,21 +2071,92 @@ snapshots:
901
 
902
903
 
 
 
 
 
 
 
 
 
 
 
 
 
904
905
  dependencies:
906
  emoji-regex: 10.4.0
907
  get-east-asian-width: 1.3.0
908
  strip-ansi: 7.1.0
909
 
 
 
 
 
910
911
  dependencies:
912
  ansi-regex: 6.1.0
913
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
914
915
  dependencies:
916
  fdir: 6.4.6([email protected])
917
  picomatch: 4.0.2
918
 
 
 
 
 
 
 
919
920
  dependencies:
921
  esbuild: 0.25.5
@@ -927,7 +2168,15 @@ snapshots:
927
 
928
929
 
930
- vite@6.3.5(@types/[email protected])(tsx@4.20.3):
 
 
 
 
 
 
 
 
931
  dependencies:
932
  esbuild: 0.25.5
933
  fdir: 6.4.6([email protected])
@@ -938,10 +2187,32 @@ snapshots:
938
  optionalDependencies:
939
  '@types/node': 22.15.31
940
  fsevents: 2.3.3
 
941
  tsx: 4.20.3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
942
 
943
944
  dependencies:
945
  ansi-styles: 6.2.1
946
  string-width: 7.2.0
947
  strip-ansi: 7.1.0
 
 
 
 
 
18
  '@types/node':
19
  specifier: ^22.10.5
20
  version: 22.15.31
21
+ '@types/react':
22
+ specifier: ^18.2.79
23
+ version: 18.3.23
24
+ '@types/react-dom':
25
+ specifier: ^18.2.25
26
+ version: 18.3.7(@types/[email protected])
27
+ '@vitejs/plugin-react':
28
+ specifier: ^4.2.1
29
30
+ autoprefixer:
31
+ specifier: ^10.4.19
32
+ version: 10.4.21([email protected])
33
+ class-variance-authority:
34
+ specifier: ^0.7.0
35
+ version: 0.7.1
36
+ clsx:
37
+ specifier: ^2.1.1
38
+ version: 2.1.1
39
+ lucide-react:
40
+ specifier: ^0.400.0
41
+ version: 0.400.0([email protected])
42
+ postcss:
43
+ specifier: ^8.4.38
44
+ version: 8.5.5
45
+ react:
46
+ specifier: ^18.2.0
47
+ version: 18.3.1
48
+ react-dom:
49
+ specifier: ^18.2.0
50
+ version: 18.3.1([email protected])
51
+ tailwind-merge:
52
+ specifier: ^2.3.0
53
+ version: 2.6.0
54
+ tailwindcss:
55
+ specifier: ^3.4.0
56
+ version: 3.4.17
57
  tsx:
58
  specifier: ^4.19.2
59
  version: 4.20.3
 
62
  version: 5.8.3
63
  vite:
64
  specifier: ^6.3.5
65
66
 
67
  packages:
68
 
69
+ '@alloc/[email protected]':
70
+ resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
71
+ engines: {node: '>=10'}
72
+
73
+ '@ampproject/[email protected]':
74
+ resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
75
+ engines: {node: '>=6.0.0'}
76
+
77
+ '@babel/[email protected]':
78
+ resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
79
+ engines: {node: '>=6.9.0'}
80
+
81
+ '@babel/[email protected]':
82
+ resolution: {integrity: sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==}
83
+ engines: {node: '>=6.9.0'}
84
+
85
+ '@babel/[email protected]':
86
+ resolution: {integrity: sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==}
87
+ engines: {node: '>=6.9.0'}
88
+
89
+ '@babel/[email protected]':
90
+ resolution: {integrity: sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==}
91
+ engines: {node: '>=6.9.0'}
92
+
93
+ '@babel/[email protected]':
94
+ resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==}
95
+ engines: {node: '>=6.9.0'}
96
+
97
+ '@babel/[email protected]':
98
+ resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==}
99
+ engines: {node: '>=6.9.0'}
100
+
101
+ '@babel/[email protected]':
102
+ resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==}
103
+ engines: {node: '>=6.9.0'}
104
+ peerDependencies:
105
+ '@babel/core': ^7.0.0
106
+
107
+ '@babel/[email protected]':
108
+ resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==}
109
+ engines: {node: '>=6.9.0'}
110
+
111
+ '@babel/[email protected]':
112
+ resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
113
+ engines: {node: '>=6.9.0'}
114
+
115
+ '@babel/[email protected]':
116
+ resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
117
+ engines: {node: '>=6.9.0'}
118
+
119
+ '@babel/[email protected]':
120
+ resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
121
+ engines: {node: '>=6.9.0'}
122
+
123
+ '@babel/[email protected]':
124
+ resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==}
125
+ engines: {node: '>=6.9.0'}
126
+
127
+ '@babel/[email protected]':
128
+ resolution: {integrity: sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==}
129
+ engines: {node: '>=6.0.0'}
130
+ hasBin: true
131
+
132
+ '@babel/[email protected]':
133
+ resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==}
134
+ engines: {node: '>=6.9.0'}
135
+ peerDependencies:
136
+ '@babel/core': ^7.0.0-0
137
+
138
+ '@babel/[email protected]':
139
+ resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==}
140
+ engines: {node: '>=6.9.0'}
141
+ peerDependencies:
142
+ '@babel/core': ^7.0.0-0
143
+
144
+ '@babel/[email protected]':
145
+ resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
146
+ engines: {node: '>=6.9.0'}
147
+
148
+ '@babel/[email protected]':
149
+ resolution: {integrity: sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==}
150
+ engines: {node: '>=6.9.0'}
151
+
152
+ '@babel/[email protected]':
153
+ resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==}
154
+ engines: {node: '>=6.9.0'}
155
+
156
  '@esbuild/[email protected]':
157
  resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==}
158
  engines: {node: '>=18'}
 
303
  cpu: [x64]
304
  os: [win32]
305
 
306
+ '@isaacs/[email protected]':
307
+ resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
308
+ engines: {node: '>=12'}
309
+
310
+ '@jridgewell/[email protected]':
311
+ resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==}
312
+ engines: {node: '>=6.0.0'}
313
+
314
+ '@jridgewell/[email protected]':
315
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
316
+ engines: {node: '>=6.0.0'}
317
+
318
+ '@jridgewell/[email protected]':
319
+ resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
320
+ engines: {node: '>=6.0.0'}
321
+
322
+ '@jridgewell/[email protected]':
323
+ resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
324
+
325
+ '@jridgewell/[email protected]':
326
+ resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
327
+
328
+ '@nodelib/[email protected]':
329
+ resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
330
+ engines: {node: '>= 8'}
331
+
332
+ '@nodelib/[email protected]':
333
+ resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
334
+ engines: {node: '>= 8'}
335
+
336
+ '@nodelib/[email protected]':
337
+ resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
338
+ engines: {node: '>= 8'}
339
+
340
+ '@pkgjs/[email protected]':
341
+ resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
342
+ engines: {node: '>=14'}
343
+
344
+ '@rolldown/[email protected]':
345
+ resolution: {integrity: sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==}
346
+
347
  '@rollup/[email protected]':
348
  resolution: {integrity: sha512-Krjy9awJl6rKbruhQDgivNbD1WuLb8xAclM4IR4cN5pHGAs2oIMMQJEiC3IC/9TZJ+QZkmZhlMO/6MBGxPidpw==}
349
  cpu: [arm]
 
508
  resolution: {integrity: sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==}
509
  engines: {node: '>=12.0.0'}
510
 
511
+ '@types/[email protected]':
512
+ resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
513
+
514
+ '@types/[email protected]':
515
+ resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
516
+
517
+ '@types/[email protected]':
518
+ resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
519
+
520
+ '@types/[email protected]':
521
+ resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==}
522
+
523
  '@types/[email protected]':
524
  resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
525
 
526
  '@types/[email protected]':
527
  resolution: {integrity: sha512-jnVe5ULKl6tijxUhvQeNbQG/84fHfg+yMak02cT8QVhBx/F05rAVxCGBYYTh2EKz22D6JF5ktXuNwdx7b9iEGw==}
528
 
529
+ '@types/[email protected]':
530
+ resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
531
+
532
+ '@types/[email protected]':
533
+ resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==}
534
+ peerDependencies:
535
+ '@types/react': ^18.0.0
536
+
537
+ '@types/[email protected]':
538
+ resolution: {integrity: sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==}
539
+
540
+ '@vitejs/[email protected]':
541
+ resolution: {integrity: sha512-QNVT3/Lxx99nMQWJWF7K4N6apUEuT0KlZA3mx/mVaoGj3smm/8rc8ezz15J1pcbcjDK0V15rpHetVfya08r76Q==}
542
+ engines: {node: ^14.18.0 || >=16.0.0}
543
+ peerDependencies:
544
+ vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0
545
+
546
547
  resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==}
548
  engines: {node: '>=18'}
549
 
550
551
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
552
+ engines: {node: '>=8'}
553
+
554
555
  resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==}
556
  engines: {node: '>=12'}
557
 
558
559
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
560
+ engines: {node: '>=8'}
561
+
562
563
  resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
564
  engines: {node: '>=12'}
565
 
566
567
+ resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
568
+
569
570
+ resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
571
+ engines: {node: '>= 8'}
572
+
573
574
+ resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
575
+
576
577
+ resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==}
578
+ engines: {node: ^10 || ^12 || >=14}
579
+ hasBin: true
580
+ peerDependencies:
581
+ postcss: ^8.1.0
582
+
583
584
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
585
+
586
587
+ resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
588
+ engines: {node: '>=8'}
589
+
590
591
+ resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
592
+
593
594
+ resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
595
+ engines: {node: '>=8'}
596
+
597
598
+ resolution: {integrity: sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==}
599
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
600
+ hasBin: true
601
+
602
603
+ resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
604
+ engines: {node: '>= 6'}
605
+
606
607
+ resolution: {integrity: sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==}
608
+
609
610
+ resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
611
+ engines: {node: '>= 8.10.0'}
612
+
613
614
+ resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
615
+
616
617
  resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
618
  engines: {node: '>=18'}
619
 
620
621
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
622
+ engines: {node: '>=6'}
623
+
624
625
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
626
+ engines: {node: '>=7.0.0'}
627
+
628
629
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
630
+
631
632
+ resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
633
+ engines: {node: '>= 6'}
634
+
635
636
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
637
+
638
639
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
640
+ engines: {node: '>= 8'}
641
+
642
643
+ resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
644
+ engines: {node: '>=4'}
645
+ hasBin: true
646
+
647
648
+ resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
649
+
650
651
  resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
652
  engines: {node: '>=6.0'}
 
656
  supports-color:
657
  optional: true
658
 
659
660
+ resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
661
+
662
663
+ resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
664
+
665
666
+ resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
667
+
668
669
+ resolution: {integrity: sha512-RUNQmFLNIWVW6+z32EJQ5+qx8ci6RGvdtDC0Ls+F89wz6I2AthpXF0w0DIrn2jpLX0/PU9ZCo+Qp7bg/EckJmA==}
670
+
671
672
  resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==}
673
 
674
675
+ resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
676
+
677
678
+ resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
679
+
680
681
  resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
682
  engines: {node: '>=18'}
 
686
  engines: {node: '>=18'}
687
  hasBin: true
688
 
689
690
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
691
+ engines: {node: '>=6'}
692
+
693
694
+ resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
695
+ engines: {node: '>=8.6.0'}
696
+
697
698
+ resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
699
+
700
701
  resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==}
702
  peerDependencies:
 
705
  picomatch:
706
  optional: true
707
 
708
709
+ resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
710
+ engines: {node: '>=8'}
711
+
712
713
+ resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
714
+ engines: {node: '>=14'}
715
+
716
717
+ resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
718
+
719
720
  resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
721
  engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
722
  os: [darwin]
723
 
724
725
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
726
+
727
728
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
729
+ engines: {node: '>=6.9.0'}
730
+
731
732
  resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==}
733
  engines: {node: '>=18'}
 
735
736
  resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
737
 
738
739
+ resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
740
+ engines: {node: '>= 6'}
741
+
742
743
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
744
+ engines: {node: '>=10.13.0'}
745
+
746
747
+ resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
748
+ hasBin: true
749
+
750
751
+ resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
752
+ engines: {node: '>=4'}
753
+
754
755
+ resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
756
+ engines: {node: '>= 0.4'}
757
+
758
759
+ resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
760
+ engines: {node: '>=8'}
761
+
762
763
+ resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
764
+ engines: {node: '>= 0.4'}
765
+
766
767
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
768
+ engines: {node: '>=0.10.0'}
769
+
770
771
+ resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
772
+ engines: {node: '>=8'}
773
+
774
775
  resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==}
776
  engines: {node: '>=18'}
777
 
778
779
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
780
+ engines: {node: '>=0.10.0'}
781
+
782
783
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
784
+ engines: {node: '>=0.12.0'}
785
+
786
787
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
788
+
789
790
+ resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
791
+
792
793
+ resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
794
+ hasBin: true
795
+
796
797
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
798
+
799
800
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
801
+ engines: {node: '>=6'}
802
+ hasBin: true
803
+
804
805
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
806
+ engines: {node: '>=6'}
807
+ hasBin: true
808
+
809
810
+ resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
811
+ engines: {node: '>=14'}
812
+
813
814
+ resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
815
+
816
817
  resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==}
818
  engines: {node: '>=18'}
819
 
820
821
+ resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
822
+ hasBin: true
823
+
824
825
+ resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
826
+
827
828
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
829
+
830
831
+ resolution: {integrity: sha512-rpp7pFHh3Xd93KHixNgB0SqThMHpYNzsGUu69UaQbSZ75Q/J3m5t6EhKyMT3m4w2WOxmJ2mY0tD3vebnXqQryQ==}
832
+ peerDependencies:
833
+ react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
834
+
835
836
+ resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
837
+ engines: {node: '>= 8'}
838
+
839
840
+ resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
841
+ engines: {node: '>=8.6'}
842
+
843
844
  resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
845
  engines: {node: '>=18'}
846
 
847
848
+ resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
849
+ engines: {node: '>=16 || 14 >=14.17'}
850
+
851
852
+ resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
853
+ engines: {node: '>=16 || 14 >=14.17'}
854
+
855
856
  resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
857
 
858
859
+ resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
860
+
861
862
  resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
863
  engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
 
870
  resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==}
871
  hasBin: true
872
 
873
874
+ resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
875
+
876
877
+ resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
878
+ engines: {node: '>=0.10.0'}
879
+
880
881
+ resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
882
+ engines: {node: '>=0.10.0'}
883
+
884
885
+ resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
886
+ engines: {node: '>=0.10.0'}
887
+
888
889
+ resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
890
+ engines: {node: '>= 6'}
891
+
892
893
  resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
894
  engines: {node: '>=18'}
895
 
896
897
+ resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
898
+
899
900
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
901
+ engines: {node: '>=8'}
902
+
903
904
+ resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
905
+
906
907
+ resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
908
+ engines: {node: '>=16 || 14 >=14.18'}
909
+
910
911
  resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
912
 
913
914
+ resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
915
+ engines: {node: '>=8.6'}
916
+
917
918
  resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
919
  engines: {node: '>=12'}
920
 
921
922
+ resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
923
+ engines: {node: '>=0.10.0'}
924
+
925
926
+ resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
927
+ engines: {node: '>= 6'}
928
+
929
930
+ resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
931
+ engines: {node: '>=14.0.0'}
932
+ peerDependencies:
933
+ postcss: ^8.0.0
934
+
935
936
+ resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==}
937
+ engines: {node: ^12 || ^14 || >= 16}
938
+ peerDependencies:
939
+ postcss: ^8.4.21
940
+
941
942
+ resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==}
943
+ engines: {node: '>= 14'}
944
+ peerDependencies:
945
+ postcss: '>=8.0.9'
946
+ ts-node: '>=9.0.0'
947
+ peerDependenciesMeta:
948
+ postcss:
949
+ optional: true
950
+ ts-node:
951
+ optional: true
952
+
953
954
+ resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==}
955
+ engines: {node: '>=12.0'}
956
+ peerDependencies:
957
+ postcss: ^8.2.14
958
+
959
960
+ resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
961
+ engines: {node: '>=4'}
962
+
963
964
+ resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
965
+
966
967
  resolution: {integrity: sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==}
968
  engines: {node: ^10 || ^12 || >=14}
969
 
970
971
+ resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
972
+
973
974
+ resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
975
+ peerDependencies:
976
+ react: ^18.3.1
977
+
978
979
+ resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
980
+ engines: {node: '>=0.10.0'}
981
+
982
983
+ resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
984
+ engines: {node: '>=0.10.0'}
985
+
986
987
+ resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
988
+
989
990
+ resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
991
+ engines: {node: '>=8.10.0'}
992
+
993
994
  resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
995
 
996
997
+ resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==}
998
+ engines: {node: '>= 0.4'}
999
+ hasBin: true
1000
+
1001
1002
  resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
1003
  engines: {node: '>=18'}
1004
 
1005
1006
+ resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
1007
+ engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
1008
+
1009
1010
  resolution: {integrity: sha512-wdN2Kd3Twh8MAEOEJZsuxuLKCsBEo4PVNLK6tQWAn10VhsVewQLzcucMgLolRlhFybGxfclbPeEYBaP6RvUFGg==}
1011
  engines: {node: '>=18.0.0', npm: '>=8.0.0'}
1012
  hasBin: true
1013
 
1014
1015
+ resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
1016
+
1017
1018
+ resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
1019
+
1020
1021
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
1022
+ hasBin: true
1023
+
1024
1025
  resolution: {integrity: sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA==}
1026
  engines: {node: '>=16.0.0'}
1027
 
1028
1029
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
1030
+ engines: {node: '>=8'}
1031
+
1032
1033
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
1034
+ engines: {node: '>=8'}
1035
+
1036
1037
  resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
1038
  engines: {node: '>=14'}
 
1045
  resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
1046
  engines: {node: '>=0.10.0'}
1047
 
1048
1049
+ resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
1050
+ engines: {node: '>=8'}
1051
+
1052
1053
+ resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
1054
+ engines: {node: '>=12'}
1055
+
1056
1057
  resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
1058
  engines: {node: '>=18'}
1059
 
1060
1061
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
1062
+ engines: {node: '>=8'}
1063
+
1064
1065
  resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
1066
  engines: {node: '>=12'}
1067
 
1068
1069
+ resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
1070
+ engines: {node: '>=16 || 14 >=14.17'}
1071
+ hasBin: true
1072
+
1073
1074
+ resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
1075
+ engines: {node: '>= 0.4'}
1076
+
1077
1078
+ resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==}
1079
+
1080
1081
+ resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==}
1082
+ engines: {node: '>=14.0.0'}
1083
+ hasBin: true
1084
+
1085
1086
+ resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
1087
+ engines: {node: '>=0.8'}
1088
+
1089
1090
+ resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
1091
+
1092
1093
  resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
1094
  engines: {node: '>=12.0.0'}
1095
 
1096
1097
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
1098
+ engines: {node: '>=8.0'}
1099
+
1100
1101
+ resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
1102
+
1103
1104
  resolution: {integrity: sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==}
1105
  engines: {node: '>=18.0.0'}
 
1113
1114
  resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
1115
 
1116
1117
+ resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}
1118
+ hasBin: true
1119
+ peerDependencies:
1120
+ browserslist: '>= 4.21.0'
1121
+
1122
1123
+ resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
1124
+
1125
1126
  resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==}
1127
  engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
 
1162
  yaml:
1163
  optional: true
1164
 
1165
1166
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
1167
+ engines: {node: '>= 8'}
1168
+ hasBin: true
1169
+
1170
1171
+ resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
1172
+ engines: {node: '>=10'}
1173
+
1174
1175
+ resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
1176
+ engines: {node: '>=12'}
1177
+
1178
1179
  resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==}
1180
  engines: {node: '>=18'}
1181
 
1182
1183
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
1184
+
1185
1186
+ resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==}
1187
+ engines: {node: '>= 14.6'}
1188
+ hasBin: true
1189
+
1190
  snapshots:
1191
 
1192
+ '@alloc/[email protected]': {}
1193
+
1194
+ '@ampproject/[email protected]':
1195
+ dependencies:
1196
+ '@jridgewell/gen-mapping': 0.3.8
1197
+ '@jridgewell/trace-mapping': 0.3.25
1198
+
1199
+ '@babel/[email protected]':
1200
+ dependencies:
1201
+ '@babel/helper-validator-identifier': 7.27.1
1202
+ js-tokens: 4.0.0
1203
+ picocolors: 1.1.1
1204
+
1205
+ '@babel/[email protected]': {}
1206
+
1207
+ '@babel/[email protected]':
1208
+ dependencies:
1209
+ '@ampproject/remapping': 2.3.0
1210
+ '@babel/code-frame': 7.27.1
1211
+ '@babel/generator': 7.27.5
1212
+ '@babel/helper-compilation-targets': 7.27.2
1213
+ '@babel/helper-module-transforms': 7.27.3(@babel/[email protected])
1214
+ '@babel/helpers': 7.27.6
1215
+ '@babel/parser': 7.27.5
1216
+ '@babel/template': 7.27.2
1217
+ '@babel/traverse': 7.27.4
1218
+ '@babel/types': 7.27.6
1219
+ convert-source-map: 2.0.0
1220
+ debug: 4.3.4
1221
+ gensync: 1.0.0-beta.2
1222
+ json5: 2.2.3
1223
+ semver: 6.3.1
1224
+ transitivePeerDependencies:
1225
+ - supports-color
1226
+
1227
+ '@babel/[email protected]':
1228
+ dependencies:
1229
+ '@babel/parser': 7.27.5
1230
+ '@babel/types': 7.27.6
1231
+ '@jridgewell/gen-mapping': 0.3.8
1232
+ '@jridgewell/trace-mapping': 0.3.25
1233
+ jsesc: 3.1.0
1234
+
1235
+ '@babel/[email protected]':
1236
+ dependencies:
1237
+ '@babel/compat-data': 7.27.5
1238
+ '@babel/helper-validator-option': 7.27.1
1239
+ browserslist: 4.25.0
1240
+ lru-cache: 5.1.1
1241
+ semver: 6.3.1
1242
+
1243
+ '@babel/[email protected]':
1244
+ dependencies:
1245
+ '@babel/traverse': 7.27.4
1246
+ '@babel/types': 7.27.6
1247
+ transitivePeerDependencies:
1248
+ - supports-color
1249
+
1250
1251
+ dependencies:
1252
+ '@babel/core': 7.27.4
1253
+ '@babel/helper-module-imports': 7.27.1
1254
+ '@babel/helper-validator-identifier': 7.27.1
1255
+ '@babel/traverse': 7.27.4
1256
+ transitivePeerDependencies:
1257
+ - supports-color
1258
+
1259
+ '@babel/[email protected]': {}
1260
+
1261
+ '@babel/[email protected]': {}
1262
+
1263
+ '@babel/[email protected]': {}
1264
+
1265
+ '@babel/[email protected]': {}
1266
+
1267
+ '@babel/[email protected]':
1268
+ dependencies:
1269
+ '@babel/template': 7.27.2
1270
+ '@babel/types': 7.27.6
1271
+
1272
+ '@babel/[email protected]':
1273
+ dependencies:
1274
+ '@babel/types': 7.27.6
1275
+
1276
1277
+ dependencies:
1278
+ '@babel/core': 7.27.4
1279
+ '@babel/helper-plugin-utils': 7.27.1
1280
+
1281
1282
+ dependencies:
1283
+ '@babel/core': 7.27.4
1284
+ '@babel/helper-plugin-utils': 7.27.1
1285
+
1286
+ '@babel/[email protected]':
1287
+ dependencies:
1288
+ '@babel/code-frame': 7.27.1
1289
+ '@babel/parser': 7.27.5
1290
+ '@babel/types': 7.27.6
1291
+
1292
+ '@babel/[email protected]':
1293
+ dependencies:
1294
+ '@babel/code-frame': 7.27.1
1295
+ '@babel/generator': 7.27.5
1296
+ '@babel/parser': 7.27.5
1297
+ '@babel/template': 7.27.2
1298
+ '@babel/types': 7.27.6
1299
+ debug: 4.3.4
1300
+ globals: 11.12.0
1301
+ transitivePeerDependencies:
1302
+ - supports-color
1303
+
1304
+ '@babel/[email protected]':
1305
+ dependencies:
1306
+ '@babel/helper-string-parser': 7.27.1
1307
+ '@babel/helper-validator-identifier': 7.27.1
1308
+
1309
  '@esbuild/[email protected]':
1310
  optional: true
1311
 
 
1381
  '@esbuild/[email protected]':
1382
  optional: true
1383
 
1384
+ '@isaacs/[email protected]':
1385
+ dependencies:
1386
+ string-width: 5.1.2
1387
+ string-width-cjs: [email protected]
1388
+ strip-ansi: 7.1.0
1389
+ strip-ansi-cjs: [email protected]
1390
+ wrap-ansi: 8.1.0
1391
+ wrap-ansi-cjs: [email protected]
1392
+
1393
+ '@jridgewell/[email protected]':
1394
+ dependencies:
1395
+ '@jridgewell/set-array': 1.2.1
1396
+ '@jridgewell/sourcemap-codec': 1.5.0
1397
+ '@jridgewell/trace-mapping': 0.3.25
1398
+
1399
+ '@jridgewell/[email protected]': {}
1400
+
1401
+ '@jridgewell/[email protected]': {}
1402
+
1403
+ '@jridgewell/[email protected]': {}
1404
+
1405
+ '@jridgewell/[email protected]':
1406
+ dependencies:
1407
+ '@jridgewell/resolve-uri': 3.1.2
1408
+ '@jridgewell/sourcemap-codec': 1.5.0
1409
+
1410
+ '@nodelib/[email protected]':
1411
+ dependencies:
1412
+ '@nodelib/fs.stat': 2.0.5
1413
+ run-parallel: 1.2.0
1414
+
1415
+ '@nodelib/[email protected]': {}
1416
+
1417
+ '@nodelib/[email protected]':
1418
+ dependencies:
1419
+ '@nodelib/fs.scandir': 2.1.5
1420
+ fastq: 1.19.1
1421
+
1422
+ '@pkgjs/[email protected]':
1423
+ optional: true
1424
+
1425
+ '@rolldown/[email protected]': {}
1426
+
1427
  '@rollup/[email protected]':
1428
  optional: true
1429
 
 
1538
  transitivePeerDependencies:
1539
  - supports-color
1540
 
1541
+ '@types/[email protected]':
1542
+ dependencies:
1543
+ '@babel/parser': 7.27.5
1544
+ '@babel/types': 7.27.6
1545
+ '@types/babel__generator': 7.27.0
1546
+ '@types/babel__template': 7.4.4
1547
+ '@types/babel__traverse': 7.20.7
1548
+
1549
+ '@types/[email protected]':
1550
+ dependencies:
1551
+ '@babel/types': 7.27.6
1552
+
1553
+ '@types/[email protected]':
1554
+ dependencies:
1555
+ '@babel/parser': 7.27.5
1556
+ '@babel/types': 7.27.6
1557
+
1558
+ '@types/[email protected]':
1559
+ dependencies:
1560
+ '@babel/types': 7.27.6
1561
+
1562
  '@types/[email protected]': {}
1563
 
1564
  '@types/[email protected]':
1565
  dependencies:
1566
  undici-types: 6.21.0
1567
 
1568
+ '@types/[email protected]': {}
1569
+
1570
1571
+ dependencies:
1572
+ '@types/react': 18.3.23
1573
+
1574
+ '@types/[email protected]':
1575
+ dependencies:
1576
+ '@types/prop-types': 15.7.15
1577
+ csstype: 3.1.3
1578
+
1579
1580
+ dependencies:
1581
+ '@babel/core': 7.27.4
1582
+ '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/[email protected])
1583
+ '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/[email protected])
1584
+ '@rolldown/pluginutils': 1.0.0-beta.11
1585
+ '@types/babel__core': 7.20.5
1586
+ react-refresh: 0.17.0
1587
1588
+ transitivePeerDependencies:
1589
+ - supports-color
1590
+
1591
1592
  dependencies:
1593
  environment: 1.1.0
1594
 
1595
1596
+
1597
1598
 
1599
1600
+ dependencies:
1601
+ color-convert: 2.0.1
1602
+
1603
1604
 
1605
1606
+
1607
1608
+ dependencies:
1609
+ normalize-path: 3.0.0
1610
+ picomatch: 2.3.1
1611
+
1612
1613
+
1614
1615
+ dependencies:
1616
+ browserslist: 4.25.0
1617
+ caniuse-lite: 1.0.30001723
1618
+ fraction.js: 4.3.7
1619
+ normalize-range: 0.1.2
1620
+ picocolors: 1.1.1
1621
+ postcss: 8.5.5
1622
+ postcss-value-parser: 4.2.0
1623
+
1624
1625
+
1626
1627
+
1628
1629
+ dependencies:
1630
+ balanced-match: 1.0.2
1631
+
1632
1633
+ dependencies:
1634
+ fill-range: 7.1.1
1635
+
1636
1637
+ dependencies:
1638
+ caniuse-lite: 1.0.30001723
1639
+ electron-to-chromium: 1.5.168
1640
+ node-releases: 2.0.19
1641
+ update-browserslist-db: 1.1.3([email protected])
1642
+
1643
1644
+
1645
1646
+
1647
1648
+ dependencies:
1649
+ anymatch: 3.1.3
1650
+ braces: 3.0.3
1651
+ glob-parent: 5.1.2
1652
+ is-binary-path: 2.1.0
1653
+ is-glob: 4.0.3
1654
+ normalize-path: 3.0.0
1655
+ readdirp: 3.6.0
1656
+ optionalDependencies:
1657
+ fsevents: 2.3.3
1658
+
1659
1660
+ dependencies:
1661
+ clsx: 2.1.1
1662
+
1663
1664
  dependencies:
1665
  restore-cursor: 5.1.0
1666
 
1667
1668
+
1669
1670
+ dependencies:
1671
+ color-name: 1.1.4
1672
+
1673
1674
+
1675
1676
+
1677
1678
+
1679
1680
+ dependencies:
1681
+ path-key: 3.1.1
1682
+ shebang-command: 2.0.0
1683
+ which: 2.0.2
1684
+
1685
1686
+
1687
1688
+
1689
1690
  dependencies:
1691
  ms: 2.1.2
1692
 
1693
1694
+
1695
1696
+
1697
1698
+
1699
1700
+
1701
1702
 
1703
1704
+
1705
1706
+
1707
1708
 
1709
 
1734
  '@esbuild/win32-ia32': 0.25.5
1735
  '@esbuild/win32-x64': 0.25.5
1736
 
1737
1738
+
1739
1740
+ dependencies:
1741
+ '@nodelib/fs.stat': 2.0.5
1742
+ '@nodelib/fs.walk': 1.2.8
1743
+ glob-parent: 5.1.2
1744
+ merge2: 1.4.1
1745
+ micromatch: 4.0.8
1746
+
1747
1748
+ dependencies:
1749
+ reusify: 1.1.0
1750
+
1751
1752
  optionalDependencies:
1753
  picomatch: 4.0.2
1754
 
1755
1756
+ dependencies:
1757
+ to-regex-range: 5.0.1
1758
+
1759
1760
+ dependencies:
1761
+ cross-spawn: 7.0.6
1762
+ signal-exit: 4.1.0
1763
+
1764
1765
+
1766
1767
  optional: true
1768
 
1769
1770
+
1771
1772
+
1773
1774
 
1775
1776
  dependencies:
1777
  resolve-pkg-maps: 1.0.0
1778
 
1779
1780
+ dependencies:
1781
+ is-glob: 4.0.3
1782
+
1783
1784
+ dependencies:
1785
+ is-glob: 4.0.3
1786
+
1787
1788
+ dependencies:
1789
+ foreground-child: 3.3.1
1790
+ jackspeak: 3.4.3
1791
+ minimatch: 9.0.5
1792
+ minipass: 7.1.2
1793
+ package-json-from-dist: 1.0.1
1794
+ path-scurry: 1.11.1
1795
+
1796
1797
+
1798
1799
+ dependencies:
1800
+ function-bind: 1.1.2
1801
+
1802
1803
+ dependencies:
1804
+ binary-extensions: 2.3.0
1805
+
1806
1807
+ dependencies:
1808
+ hasown: 2.0.2
1809
+
1810
1811
+
1812
1813
+
1814
1815
  dependencies:
1816
  get-east-asian-width: 1.3.0
1817
 
1818
1819
+ dependencies:
1820
+ is-extglob: 2.1.1
1821
+
1822
1823
+
1824
1825
+
1826
1827
+ dependencies:
1828
+ '@isaacs/cliui': 8.0.2
1829
+ optionalDependencies:
1830
+ '@pkgjs/parseargs': 0.11.0
1831
+
1832
1833
+
1834
1835
+
1836
1837
+
1838
1839
+
1840
1841
+
1842
1843
+
1844
1845
  dependencies:
1846
  ansi-escapes: 7.0.0
 
1849
  strip-ansi: 7.1.0
1850
  wrap-ansi: 9.0.0
1851
 
1852
1853
+ dependencies:
1854
+ js-tokens: 4.0.0
1855
+
1856
1857
+
1858
1859
+ dependencies:
1860
+ yallist: 3.1.1
1861
+
1862
1863
+ dependencies:
1864
+ react: 18.3.1
1865
+
1866
1867
+
1868
1869
+ dependencies:
1870
+ braces: 3.0.3
1871
+ picomatch: 2.3.1
1872
+
1873
1874
 
1875
1876
+ dependencies:
1877
+ brace-expansion: 2.0.2
1878
+
1879
1880
+
1881
1882
 
1883
1884
+ dependencies:
1885
+ any-promise: 1.3.0
1886
+ object-assign: 4.1.1
1887
+ thenify-all: 1.6.0
1888
+
1889
1890
 
1891
1892
 
1893
1894
 
1895
1896
+
1897
1898
+
1899
1900
+
1901
1902
+
1903
1904
+
1905
1906
  dependencies:
1907
  mimic-function: 5.0.1
1908
 
1909
1910
+
1911
1912
+
1913
1914
+
1915
1916
+ dependencies:
1917
+ lru-cache: 10.4.3
1918
+ minipass: 7.1.2
1919
+
1920
1921
 
1922
1923
+
1924
1925
 
1926
1927
+
1928
1929
+
1930
1931
+ dependencies:
1932
+ postcss: 8.5.5
1933
+ postcss-value-parser: 4.2.0
1934
+ read-cache: 1.0.0
1935
+ resolve: 1.22.10
1936
+
1937
1938
+ dependencies:
1939
+ camelcase-css: 2.0.1
1940
+ postcss: 8.5.5
1941
+
1942
1943
+ dependencies:
1944
+ lilconfig: 3.1.3
1945
+ yaml: 2.8.0
1946
+ optionalDependencies:
1947
+ postcss: 8.5.5
1948
+
1949
1950
+ dependencies:
1951
+ postcss: 8.5.5
1952
+ postcss-selector-parser: 6.1.2
1953
+
1954
1955
+ dependencies:
1956
+ cssesc: 3.0.0
1957
+ util-deprecate: 1.0.2
1958
+
1959
1960
+
1961
1962
  dependencies:
1963
  nanoid: 3.3.11
1964
  picocolors: 1.1.1
1965
  source-map-js: 1.2.1
1966
 
1967
1968
+
1969
1970
+ dependencies:
1971
+ loose-envify: 1.4.0
1972
+ react: 18.3.1
1973
+ scheduler: 0.23.2
1974
+
1975
1976
+
1977
1978
+ dependencies:
1979
+ loose-envify: 1.4.0
1980
+
1981
1982
+ dependencies:
1983
+ pify: 2.3.0
1984
+
1985
1986
+ dependencies:
1987
+ picomatch: 2.3.1
1988
+
1989
1990
 
1991
1992
+ dependencies:
1993
+ is-core-module: 2.16.1
1994
+ path-parse: 1.0.7
1995
+ supports-preserve-symlinks-flag: 1.0.0
1996
+
1997
1998
  dependencies:
1999
  onetime: 7.0.0
2000
  signal-exit: 4.1.0
2001
 
2002
2003
+
2004
2005
  dependencies:
2006
  '@types/estree': 1.0.7
 
2027
  '@rollup/rollup-win32-x64-msvc': 4.43.0
2028
  fsevents: 2.3.3
2029
 
2030
2031
+ dependencies:
2032
+ queue-microtask: 1.2.3
2033
+
2034
2035
+ dependencies:
2036
+ loose-envify: 1.4.0
2037
+
2038
2039
+
2040
2041
  dependencies:
2042
  '@serialport/binding-mock': 10.2.2
 
2056
  transitivePeerDependencies:
2057
  - supports-color
2058
 
2059
2060
+ dependencies:
2061
+ shebang-regex: 3.0.0
2062
+
2063
2064
+
2065
2066
 
2067
 
2071
 
2072
2073
 
2074
2075
+ dependencies:
2076
+ emoji-regex: 8.0.0
2077
+ is-fullwidth-code-point: 3.0.0
2078
+ strip-ansi: 6.0.1
2079
+
2080
2081
+ dependencies:
2082
+ eastasianwidth: 0.2.0
2083
+ emoji-regex: 9.2.2
2084
+ strip-ansi: 7.1.0
2085
+
2086
2087
  dependencies:
2088
  emoji-regex: 10.4.0
2089
  get-east-asian-width: 1.3.0
2090
  strip-ansi: 7.1.0
2091
 
2092
2093
+ dependencies:
2094
+ ansi-regex: 5.0.1
2095
+
2096
2097
  dependencies:
2098
  ansi-regex: 6.1.0
2099
 
2100
2101
+ dependencies:
2102
+ '@jridgewell/gen-mapping': 0.3.8
2103
+ commander: 4.1.1
2104
+ glob: 10.4.5
2105
+ lines-and-columns: 1.2.4
2106
+ mz: 2.7.0
2107
+ pirates: 4.0.7
2108
+ ts-interface-checker: 0.1.13
2109
+
2110
2111
+
2112
2113
+
2114
2115
+ dependencies:
2116
+ '@alloc/quick-lru': 5.2.0
2117
+ arg: 5.0.2
2118
+ chokidar: 3.6.0
2119
+ didyoumean: 1.2.2
2120
+ dlv: 1.1.3
2121
+ fast-glob: 3.3.3
2122
+ glob-parent: 6.0.2
2123
+ is-glob: 4.0.3
2124
+ jiti: 1.21.7
2125
+ lilconfig: 3.1.3
2126
+ micromatch: 4.0.8
2127
+ normalize-path: 3.0.0
2128
+ object-hash: 3.0.0
2129
+ picocolors: 1.1.1
2130
+ postcss: 8.5.5
2131
+ postcss-import: 15.1.0([email protected])
2132
+ postcss-js: 4.0.1([email protected])
2133
+ postcss-load-config: 4.0.2([email protected])
2134
+ postcss-nested: 6.2.0([email protected])
2135
+ postcss-selector-parser: 6.1.2
2136
+ resolve: 1.22.10
2137
+ sucrase: 3.35.0
2138
+ transitivePeerDependencies:
2139
+ - ts-node
2140
+
2141
2142
+ dependencies:
2143
+ thenify: 3.3.1
2144
+
2145
2146
+ dependencies:
2147
+ any-promise: 1.3.0
2148
+
2149
2150
  dependencies:
2151
  fdir: 6.4.6([email protected])
2152
  picomatch: 4.0.2
2153
 
2154
2155
+ dependencies:
2156
+ is-number: 7.0.0
2157
+
2158
2159
+
2160
2161
  dependencies:
2162
  esbuild: 0.25.5
 
2168
 
2169
2170
 
2171
+ update-browserslist-db@1.1.3(browserslist@4.25.0):
2172
+ dependencies:
2173
+ browserslist: 4.25.0
2174
+ escalade: 3.2.0
2175
+ picocolors: 1.1.1
2176
+
2177
2178
+
2179
2180
  dependencies:
2181
  esbuild: 0.25.5
2182
  fdir: 6.4.6([email protected])
 
2187
  optionalDependencies:
2188
  '@types/node': 22.15.31
2189
  fsevents: 2.3.3
2190
+ jiti: 1.21.7
2191
  tsx: 4.20.3
2192
+ yaml: 2.8.0
2193
+
2194
2195
+ dependencies:
2196
+ isexe: 2.0.0
2197
+
2198
2199
+ dependencies:
2200
+ ansi-styles: 4.3.0
2201
+ string-width: 4.2.3
2202
+ strip-ansi: 6.0.1
2203
+
2204
2205
+ dependencies:
2206
+ ansi-styles: 6.2.1
2207
+ string-width: 5.1.2
2208
+ strip-ansi: 7.1.0
2209
 
2210
2211
  dependencies:
2212
  ansi-styles: 6.2.1
2213
  string-width: 7.2.0
2214
  strip-ansi: 7.1.0
2215
+
2216
2217
+
2218
postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import tailwindcss from "tailwindcss";
2
+ import autoprefixer from "autoprefixer";
3
+
4
+ export default {
5
+ plugins: [tailwindcss, autoprefixer],
6
+ };
postcss.config.mjs ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import tailwindcss from "tailwindcss";
2
+ import autoprefixer from "autoprefixer";
3
+
4
+ export default {
5
+ plugins: [tailwindcss, autoprefixer],
6
+ };
src/demo/App.tsx ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from "react";
2
+ import { Home } from "./pages/Home";
3
+ import { ErrorBoundary } from "./components/ErrorBoundary";
4
+ import type { ConnectedRobot } from "./types";
5
+
6
+ export function App() {
7
+ const [connectedRobots, setConnectedRobots] = useState<ConnectedRobot[]>([]);
8
+
9
+ return (
10
+ <ErrorBoundary>
11
+ <div className="min-h-screen bg-background">
12
+ <Home
13
+ onGetStarted={() => {}} // No longer needed
14
+ connectedRobots={connectedRobots}
15
+ onConnectedRobotsChange={setConnectedRobots}
16
+ />
17
+ </div>
18
+ </ErrorBoundary>
19
+ );
20
+ }
src/demo/components/CalibrationPanel.tsx ADDED
@@ -0,0 +1,526 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useCallback, useRef } from "react";
2
+ import { Button } from "./ui/button";
3
+ import {
4
+ Card,
5
+ CardContent,
6
+ CardDescription,
7
+ CardHeader,
8
+ CardTitle,
9
+ } from "./ui/card";
10
+ import { Badge } from "./ui/badge";
11
+ import { calibrateWithPort } from "../../lerobot/web/calibrate";
12
+ import type { ConnectedRobot } from "../types";
13
+
14
+ interface CalibrationPanelProps {
15
+ robot: ConnectedRobot;
16
+ onFinish: () => void;
17
+ }
18
+
19
+ interface MotorCalibrationData {
20
+ name: string;
21
+ current: number;
22
+ min: number;
23
+ max: number;
24
+ range: number;
25
+ }
26
+
27
+ export function CalibrationPanel({ robot, onFinish }: CalibrationPanelProps) {
28
+ const [isCalibrating, setIsCalibrating] = useState(false);
29
+ const [motorData, setMotorData] = useState<MotorCalibrationData[]>([]);
30
+ const [calibrationStatus, setCalibrationStatus] =
31
+ useState<string>("Ready to calibrate");
32
+ const [calibrationComplete, setCalibrationComplete] = useState(false);
33
+ const [readCount, setReadCount] = useState(0);
34
+
35
+ const animationFrameRef = useRef<number>();
36
+ const lastReadTime = useRef<number>(0);
37
+ const isReading = useRef<boolean>(false);
38
+
39
+ // Motor names matching Node CLI exactly
40
+ const motorNames = [
41
+ "waist",
42
+ "shoulder",
43
+ "elbow",
44
+ "forearm_roll",
45
+ "wrist_angle",
46
+ "wrist_rotate",
47
+ ];
48
+
49
+ // Initialize motor data with center positions
50
+ const initializeMotorData = useCallback(() => {
51
+ const initialData = motorNames.map((name) => ({
52
+ name,
53
+ current: 2047, // Center position for STS3215 (4095/2)
54
+ min: 2047,
55
+ max: 2047,
56
+ range: 0,
57
+ }));
58
+ setMotorData(initialData);
59
+ setReadCount(0);
60
+ }, []);
61
+
62
+ // Keep track of last known good positions to avoid glitches
63
+ const lastKnownPositions = useRef<number[]>([
64
+ 2047, 2047, 2047, 2047, 2047, 2047,
65
+ ]);
66
+
67
+ // Read actual motor positions with robust error handling
68
+ const readMotorPositions = useCallback(async (): Promise<number[]> => {
69
+ if (!robot.port || !robot.port.readable || !robot.port.writable) {
70
+ throw new Error("Robot port not available for communication");
71
+ }
72
+
73
+ const positions: number[] = [];
74
+ const motorIds = [1, 2, 3, 4, 5, 6];
75
+
76
+ // Get persistent reader/writer for this session
77
+ const reader = robot.port.readable.getReader();
78
+ const writer = robot.port.writable.getWriter();
79
+
80
+ try {
81
+ for (let index = 0; index < motorIds.length; index++) {
82
+ const motorId = motorIds[index];
83
+ let success = false;
84
+ let retries = 2; // Allow 2 retries per motor
85
+
86
+ while (!success && retries > 0) {
87
+ try {
88
+ // Create STS3215 Read Position packet
89
+ const packet = new Uint8Array([
90
+ 0xff,
91
+ 0xff,
92
+ motorId,
93
+ 0x04,
94
+ 0x02,
95
+ 0x38,
96
+ 0x02,
97
+ 0x00,
98
+ ]);
99
+ const checksum = ~(motorId + 0x04 + 0x02 + 0x38 + 0x02) & 0xff;
100
+ packet[7] = checksum;
101
+
102
+ // Write packet
103
+ await writer.write(packet);
104
+
105
+ // Wait for response
106
+ await new Promise((resolve) => setTimeout(resolve, 10));
107
+
108
+ // Read with timeout
109
+ const timeoutPromise = new Promise((_, reject) =>
110
+ setTimeout(() => reject(new Error("Timeout")), 100)
111
+ );
112
+
113
+ const result = (await Promise.race([
114
+ reader.read(),
115
+ timeoutPromise,
116
+ ])) as ReadableStreamReadResult<Uint8Array>;
117
+
118
+ if (
119
+ result &&
120
+ !result.done &&
121
+ result.value &&
122
+ result.value.length >= 7
123
+ ) {
124
+ const response = result.value;
125
+ const responseId = response[2];
126
+ const error = response[4];
127
+
128
+ // Check if this is the response we're looking for
129
+ if (responseId === motorId && error === 0) {
130
+ const position = response[5] | (response[6] << 8);
131
+ positions.push(position);
132
+ lastKnownPositions.current[index] = position; // Update last known good position
133
+ success = true;
134
+ } else {
135
+ // Wrong motor ID or error - might be out of sync, try again
136
+ retries--;
137
+ await new Promise((resolve) => setTimeout(resolve, 5));
138
+ }
139
+ } else {
140
+ retries--;
141
+ await new Promise((resolve) => setTimeout(resolve, 5));
142
+ }
143
+ } catch (error) {
144
+ retries--;
145
+ await new Promise((resolve) => setTimeout(resolve, 5));
146
+ }
147
+ }
148
+
149
+ if (!success) {
150
+ // Use last known good position instead of fallback center position
151
+ positions.push(lastKnownPositions.current[index]);
152
+ }
153
+
154
+ // Small delay between motors
155
+ await new Promise((resolve) => setTimeout(resolve, 2));
156
+ }
157
+ } finally {
158
+ reader.releaseLock();
159
+ writer.releaseLock();
160
+ }
161
+
162
+ return positions;
163
+ }, [robot.port]);
164
+
165
+ // Update motor data with new readings - NO SIMULATION, REAL VALUES ONLY
166
+ const updateMotorData = useCallback(async () => {
167
+ if (!isCalibrating || isReading.current) return;
168
+
169
+ const now = performance.now();
170
+ // Read at ~15Hz to reduce serial communication load (66ms intervals)
171
+ if (now - lastReadTime.current < 66) return;
172
+
173
+ lastReadTime.current = now;
174
+ isReading.current = true;
175
+
176
+ try {
177
+ const positions = await readMotorPositions();
178
+
179
+ // Always update since we're now keeping last known good positions
180
+ // Only show warning if all motors are still at center position (no successful reads yet)
181
+ const allAtCenter = positions.every((pos) => pos === 2047);
182
+ if (allAtCenter && readCount === 0) {
183
+ console.log("No motor data received yet - still trying to connect");
184
+ setCalibrationStatus("Connecting to motors - please wait...");
185
+ }
186
+
187
+ setMotorData((prev) =>
188
+ prev.map((motor, index) => {
189
+ const current = positions[index];
190
+ const min = Math.min(motor.min, current);
191
+ const max = Math.max(motor.max, current);
192
+ const range = max - min;
193
+
194
+ return {
195
+ ...motor,
196
+ current,
197
+ min,
198
+ max,
199
+ range,
200
+ };
201
+ })
202
+ );
203
+
204
+ setReadCount((prev) => prev + 1);
205
+ console.log(`Real motor positions:`, positions);
206
+ } catch (error) {
207
+ console.warn("Failed to read motor positions:", error);
208
+ setCalibrationStatus(
209
+ `Error reading motors: ${
210
+ error instanceof Error ? error.message : error
211
+ }`
212
+ );
213
+ } finally {
214
+ isReading.current = false;
215
+ }
216
+ }, [isCalibrating, readMotorPositions]);
217
+
218
+ // Animation loop using RAF (requestAnimationFrame)
219
+ const animationLoop = useCallback(() => {
220
+ updateMotorData();
221
+
222
+ if (isCalibrating) {
223
+ animationFrameRef.current = requestAnimationFrame(animationLoop);
224
+ }
225
+ }, [isCalibrating, updateMotorData]);
226
+
227
+ useEffect(() => {
228
+ initializeMotorData();
229
+ }, [initializeMotorData]);
230
+
231
+ useEffect(() => {
232
+ if (isCalibrating) {
233
+ animationFrameRef.current = requestAnimationFrame(animationLoop);
234
+ } else {
235
+ if (animationFrameRef.current) {
236
+ cancelAnimationFrame(animationFrameRef.current);
237
+ }
238
+ }
239
+
240
+ return () => {
241
+ if (animationFrameRef.current) {
242
+ cancelAnimationFrame(animationFrameRef.current);
243
+ }
244
+ };
245
+ }, [isCalibrating, animationLoop]);
246
+
247
+ const startCalibration = async () => {
248
+ if (!robot.port || !robot.robotType) {
249
+ setCalibrationStatus("Error: Invalid robot configuration");
250
+ return;
251
+ }
252
+
253
+ setCalibrationStatus(
254
+ "Initializing calibration - reading current positions..."
255
+ );
256
+
257
+ try {
258
+ // Get current positions to use as starting point for min/max
259
+ const currentPositions = await readMotorPositions();
260
+
261
+ // Reset calibration data with current positions as both min and max
262
+ const freshData = motorNames.map((name, index) => ({
263
+ name,
264
+ current: currentPositions[index],
265
+ min: currentPositions[index], // Start with current position
266
+ max: currentPositions[index], // Start with current position
267
+ range: 0, // No range yet
268
+ }));
269
+
270
+ setMotorData(freshData);
271
+ setReadCount(0);
272
+ setIsCalibrating(true);
273
+ setCalibrationComplete(false);
274
+ setCalibrationStatus(
275
+ "Recording ranges of motion - move all joints through their full range..."
276
+ );
277
+ } catch (error) {
278
+ setCalibrationStatus(
279
+ `Error starting calibration: ${
280
+ error instanceof Error ? error.message : error
281
+ }`
282
+ );
283
+ }
284
+ };
285
+
286
+ // Generate calibration config JSON matching Node CLI format
287
+ const generateConfigJSON = () => {
288
+ const calibrationData = {
289
+ homing_offset: motorData.map((motor) => motor.current - 2047), // Center offset
290
+ drive_mode: [3, 3, 3, 3, 3, 3], // SO-100 standard drive mode
291
+ start_pos: motorData.map((motor) => motor.min),
292
+ end_pos: motorData.map((motor) => motor.max),
293
+ calib_mode: ["middle", "middle", "middle", "middle", "middle", "middle"], // SO-100 standard
294
+ motor_names: motorNames,
295
+ };
296
+
297
+ return calibrationData;
298
+ };
299
+
300
+ // Download calibration config as JSON file
301
+ const downloadConfigJSON = () => {
302
+ const configData = generateConfigJSON();
303
+ const jsonString = JSON.stringify(configData, null, 2);
304
+ const blob = new Blob([jsonString], { type: "application/json" });
305
+ const url = URL.createObjectURL(blob);
306
+
307
+ const link = document.createElement("a");
308
+ link.href = url;
309
+ link.download = `${robot.robotId || robot.robotType}_calibration.json`;
310
+ document.body.appendChild(link);
311
+ link.click();
312
+ document.body.removeChild(link);
313
+ URL.revokeObjectURL(url);
314
+ };
315
+
316
+ const finishCalibration = () => {
317
+ setIsCalibrating(false);
318
+ setCalibrationComplete(true);
319
+ setCalibrationStatus(
320
+ `✅ Calibration completed! Recorded ${readCount} position readings.`
321
+ );
322
+
323
+ // Save calibration config to localStorage using serial number
324
+ const configData = generateConfigJSON();
325
+ const serialNumber = (robot as any).serialNumber;
326
+
327
+ if (!serialNumber) {
328
+ console.warn("⚠️ No serial number available for calibration storage");
329
+ setCalibrationStatus(
330
+ `⚠️ Calibration completed but cannot save - no robot serial number`
331
+ );
332
+ return;
333
+ }
334
+
335
+ const calibrationKey = `lerobot-calibration-${serialNumber}`;
336
+ try {
337
+ localStorage.setItem(
338
+ calibrationKey,
339
+ JSON.stringify({
340
+ config: configData,
341
+ timestamp: new Date().toISOString(),
342
+ serialNumber: serialNumber,
343
+ robotId: robot.robotId,
344
+ robotType: robot.robotType,
345
+ readCount: readCount,
346
+ })
347
+ );
348
+ console.log(`💾 Calibration saved for robot serial: ${serialNumber}`);
349
+ } catch (error) {
350
+ console.warn("Failed to save calibration to localStorage:", error);
351
+ setCalibrationStatus(
352
+ `⚠️ Calibration completed but save failed: ${error}`
353
+ );
354
+ }
355
+ };
356
+
357
+ return (
358
+ <div className="space-y-4">
359
+ {/* Calibration Status Card */}
360
+ <Card>
361
+ <CardHeader>
362
+ <div className="flex items-center justify-between">
363
+ <div>
364
+ <CardTitle className="text-lg">
365
+ 🛠️ Calibrating: {robot.robotId}
366
+ </CardTitle>
367
+ <CardDescription>
368
+ {robot.robotType?.replace("_", " ")} • {robot.name}
369
+ </CardDescription>
370
+ </div>
371
+ <Badge
372
+ variant={
373
+ isCalibrating
374
+ ? "default"
375
+ : calibrationComplete
376
+ ? "default"
377
+ : "outline"
378
+ }
379
+ >
380
+ {isCalibrating
381
+ ? "Recording"
382
+ : calibrationComplete
383
+ ? "Complete"
384
+ : "Ready"}
385
+ </Badge>
386
+ </div>
387
+ </CardHeader>
388
+ <CardContent>
389
+ <div className="space-y-4">
390
+ <div className="p-3 bg-blue-50 rounded-lg">
391
+ <p className="text-sm font-medium text-blue-900">Status:</p>
392
+ <p className="text-sm text-blue-800">{calibrationStatus}</p>
393
+ {isCalibrating && (
394
+ <p className="text-xs text-blue-600 mt-1">
395
+ Readings: {readCount} | Press "Finish Calibration" when done
396
+ </p>
397
+ )}
398
+ </div>
399
+
400
+ <div className="flex gap-2">
401
+ {!isCalibrating && !calibrationComplete && (
402
+ <Button onClick={startCalibration}>Start Calibration</Button>
403
+ )}
404
+
405
+ {isCalibrating && (
406
+ <Button onClick={finishCalibration} variant="outline">
407
+ Finish Calibration
408
+ </Button>
409
+ )}
410
+
411
+ {calibrationComplete && (
412
+ <>
413
+ <Button onClick={downloadConfigJSON} variant="outline">
414
+ Download Config JSON
415
+ </Button>
416
+ <Button onClick={onFinish}>Done</Button>
417
+ </>
418
+ )}
419
+ </div>
420
+ </div>
421
+ </CardContent>
422
+ </Card>
423
+
424
+ {/* Configuration JSON Display */}
425
+ {calibrationComplete && (
426
+ <Card>
427
+ <CardHeader>
428
+ <CardTitle className="text-lg">
429
+ 🎯 Calibration Configuration
430
+ </CardTitle>
431
+ <CardDescription>
432
+ Copy this JSON or download it for your robot setup
433
+ </CardDescription>
434
+ </CardHeader>
435
+ <CardContent>
436
+ <div className="space-y-3">
437
+ <pre className="bg-gray-100 p-4 rounded-lg text-sm overflow-x-auto border">
438
+ <code>{JSON.stringify(generateConfigJSON(), null, 2)}</code>
439
+ </pre>
440
+ <div className="flex gap-2">
441
+ <Button onClick={downloadConfigJSON} variant="outline">
442
+ 📄 Download JSON File
443
+ </Button>
444
+ <Button
445
+ onClick={() => {
446
+ navigator.clipboard.writeText(
447
+ JSON.stringify(generateConfigJSON(), null, 2)
448
+ );
449
+ }}
450
+ variant="outline"
451
+ >
452
+ 📋 Copy to Clipboard
453
+ </Button>
454
+ </div>
455
+ </div>
456
+ </CardContent>
457
+ </Card>
458
+ )}
459
+
460
+ {/* Live Position Recording Table (matching Node CLI exactly) */}
461
+ <Card>
462
+ <CardHeader>
463
+ <CardTitle className="text-lg">Live Position Recording</CardTitle>
464
+ <CardDescription>
465
+ Real-time motor position feedback - exactly like Node CLI
466
+ </CardDescription>
467
+ </CardHeader>
468
+ <CardContent>
469
+ <div className="overflow-hidden rounded-lg border">
470
+ <table className="w-full font-mono text-sm">
471
+ <thead className="bg-gray-50">
472
+ <tr>
473
+ <th className="px-4 py-2 text-left font-medium text-gray-900">
474
+ Motor Name
475
+ </th>
476
+ <th className="px-4 py-2 text-right font-medium text-gray-900">
477
+ Current
478
+ </th>
479
+ <th className="px-4 py-2 text-right font-medium text-gray-900">
480
+ Min
481
+ </th>
482
+ <th className="px-4 py-2 text-right font-medium text-gray-900">
483
+ Max
484
+ </th>
485
+ <th className="px-4 py-2 text-right font-medium text-gray-900">
486
+ Range
487
+ </th>
488
+ </tr>
489
+ </thead>
490
+ <tbody className="divide-y divide-gray-200">
491
+ {motorData.map((motor, index) => (
492
+ <tr key={index} className="hover:bg-gray-50">
493
+ <td className="px-4 py-2 font-medium flex items-center gap-2">
494
+ {motor.name}
495
+ {motor.range > 100 && (
496
+ <span className="text-green-600 text-xs">✓</span>
497
+ )}
498
+ </td>
499
+ <td className="px-4 py-2 text-right">{motor.current}</td>
500
+ <td className="px-4 py-2 text-right">{motor.min}</td>
501
+ <td className="px-4 py-2 text-right">{motor.max}</td>
502
+ <td className="px-4 py-2 text-right font-medium">
503
+ <span
504
+ className={
505
+ motor.range > 100 ? "text-green-600" : "text-gray-500"
506
+ }
507
+ >
508
+ {motor.range}
509
+ </span>
510
+ </td>
511
+ </tr>
512
+ ))}
513
+ </tbody>
514
+ </table>
515
+ </div>
516
+
517
+ {isCalibrating && (
518
+ <div className="mt-3 text-center text-sm text-gray-600">
519
+ Move joints through their full range of motion...
520
+ </div>
521
+ )}
522
+ </CardContent>
523
+ </Card>
524
+ </div>
525
+ );
526
+ }
src/demo/components/CalibrationWizard.tsx ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from "react";
2
+ import { Button } from "./ui/button";
3
+ import {
4
+ Card,
5
+ CardContent,
6
+ CardDescription,
7
+ CardHeader,
8
+ CardTitle,
9
+ } from "./ui/card";
10
+ import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
11
+ import { Badge } from "./ui/badge";
12
+ import { calibrateWithPort } from "../../lerobot/web/calibrate";
13
+ import type { ConnectedRobot } from "../types";
14
+
15
+ interface CalibrationWizardProps {
16
+ robot: ConnectedRobot;
17
+ onComplete: () => void;
18
+ onCancel: () => void;
19
+ }
20
+
21
+ interface CalibrationStep {
22
+ id: string;
23
+ title: string;
24
+ description: string;
25
+ status: "pending" | "running" | "complete" | "error";
26
+ message?: string;
27
+ }
28
+
29
+ export function CalibrationWizard({
30
+ robot,
31
+ onComplete,
32
+ onCancel,
33
+ }: CalibrationWizardProps) {
34
+ const [currentStepIndex, setCurrentStepIndex] = useState(0);
35
+ const [steps, setSteps] = useState<CalibrationStep[]>([
36
+ {
37
+ id: "init",
38
+ title: "Initialize Robot",
39
+ description: "Connecting to robot and checking status",
40
+ status: "pending",
41
+ },
42
+ {
43
+ id: "calibrate",
44
+ title: "Calibrate Motors",
45
+ description: "Running calibration sequence",
46
+ status: "pending",
47
+ },
48
+ {
49
+ id: "verify",
50
+ title: "Verify Calibration",
51
+ description: "Testing calibrated positions",
52
+ status: "pending",
53
+ },
54
+ {
55
+ id: "complete",
56
+ title: "Complete",
57
+ description: "Calibration finished successfully",
58
+ status: "pending",
59
+ },
60
+ ]);
61
+
62
+ const [isRunning, setIsRunning] = useState(false);
63
+ const [error, setError] = useState<string | null>(null);
64
+
65
+ useEffect(() => {
66
+ startCalibration();
67
+ }, []);
68
+
69
+ const updateStep = (
70
+ stepId: string,
71
+ status: CalibrationStep["status"],
72
+ message?: string
73
+ ) => {
74
+ setSteps((prev) =>
75
+ prev.map((step) =>
76
+ step.id === stepId ? { ...step, status, message } : step
77
+ )
78
+ );
79
+ };
80
+
81
+ const startCalibration = async () => {
82
+ if (!robot.port || !robot.robotType) {
83
+ setError("Invalid robot configuration");
84
+ return;
85
+ }
86
+
87
+ setIsRunning(true);
88
+ setError(null);
89
+
90
+ try {
91
+ // Step 1: Initialize
92
+ setCurrentStepIndex(0);
93
+ updateStep("init", "running");
94
+ await new Promise((resolve) => setTimeout(resolve, 1000));
95
+ updateStep("init", "complete", "Robot initialized successfully");
96
+
97
+ // Step 2: Calibrate
98
+ setCurrentStepIndex(1);
99
+ updateStep("calibrate", "running");
100
+
101
+ try {
102
+ await calibrateWithPort(robot.port, robot.robotType);
103
+ updateStep("calibrate", "complete", "Motor calibration completed");
104
+ } catch (error) {
105
+ updateStep(
106
+ "calibrate",
107
+ "error",
108
+ error instanceof Error ? error.message : "Calibration failed"
109
+ );
110
+ throw error;
111
+ }
112
+
113
+ // Step 3: Verify
114
+ setCurrentStepIndex(2);
115
+ updateStep("verify", "running");
116
+ await new Promise((resolve) => setTimeout(resolve, 1500));
117
+ updateStep("verify", "complete", "Calibration verified");
118
+
119
+ // Step 4: Complete
120
+ setCurrentStepIndex(3);
121
+ updateStep("complete", "complete", "Robot is ready for use");
122
+
123
+ setTimeout(() => {
124
+ onComplete();
125
+ }, 2000);
126
+ } catch (error) {
127
+ setError(error instanceof Error ? error.message : "Calibration failed");
128
+ } finally {
129
+ setIsRunning(false);
130
+ }
131
+ };
132
+
133
+ const getStepIcon = (status: CalibrationStep["status"]) => {
134
+ switch (status) {
135
+ case "pending":
136
+ return "⏳";
137
+ case "running":
138
+ return "🔄";
139
+ case "complete":
140
+ return "✅";
141
+ case "error":
142
+ return "❌";
143
+ default:
144
+ return "⏳";
145
+ }
146
+ };
147
+
148
+ const getStepBadgeVariant = (status: CalibrationStep["status"]) => {
149
+ switch (status) {
150
+ case "pending":
151
+ return "secondary" as const;
152
+ case "running":
153
+ return "default" as const;
154
+ case "complete":
155
+ return "default" as const;
156
+ case "error":
157
+ return "destructive" as const;
158
+ default:
159
+ return "secondary" as const;
160
+ }
161
+ };
162
+
163
+ return (
164
+ <div className="space-y-6">
165
+ <div className="text-center">
166
+ <h3 className="text-lg font-semibold mb-2">Calibration in Progress</h3>
167
+ <p className="text-muted-foreground">
168
+ Calibrating {robot.robotId} ({robot.robotType?.replace("_", " ")})
169
+ </p>
170
+ </div>
171
+
172
+ {error && (
173
+ <Alert variant="destructive">
174
+ <AlertDescription>{error}</AlertDescription>
175
+ </Alert>
176
+ )}
177
+
178
+ <div className="space-y-4">
179
+ {steps.map((step, index) => (
180
+ <Card
181
+ key={step.id}
182
+ className={index === currentStepIndex ? "ring-2 ring-blue-500" : ""}
183
+ >
184
+ <CardHeader className="pb-3">
185
+ <div className="flex items-center justify-between">
186
+ <div className="flex items-center space-x-3">
187
+ <span className="text-2xl">{getStepIcon(step.status)}</span>
188
+ <div>
189
+ <CardTitle className="text-base">{step.title}</CardTitle>
190
+ <CardDescription className="text-sm">
191
+ {step.description}
192
+ </CardDescription>
193
+ </div>
194
+ </div>
195
+ <Badge variant={getStepBadgeVariant(step.status)}>
196
+ {step.status.charAt(0).toUpperCase() + step.status.slice(1)}
197
+ </Badge>
198
+ </div>
199
+ </CardHeader>
200
+ {step.message && (
201
+ <CardContent className="pt-0">
202
+ <p className="text-sm text-muted-foreground">{step.message}</p>
203
+ </CardContent>
204
+ )}
205
+ </Card>
206
+ ))}
207
+ </div>
208
+
209
+ <div className="flex justify-center space-x-4">
210
+ <Button variant="outline" onClick={onCancel} disabled={isRunning}>
211
+ Cancel
212
+ </Button>
213
+ {error && <Button onClick={startCalibration}>Retry Calibration</Button>}
214
+ </div>
215
+ </div>
216
+ );
217
+ }
src/demo/components/ErrorBoundary.tsx ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { Component, ErrorInfo, ReactNode } from "react";
2
+ import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
3
+ import { Button } from "./ui/button";
4
+
5
+ interface Props {
6
+ children: ReactNode;
7
+ }
8
+
9
+ interface State {
10
+ hasError: boolean;
11
+ error?: Error;
12
+ }
13
+
14
+ export class ErrorBoundary extends Component<Props, State> {
15
+ constructor(props: Props) {
16
+ super(props);
17
+ this.state = { hasError: false };
18
+ }
19
+
20
+ static getDerivedStateFromError(error: Error): State {
21
+ return { hasError: true, error };
22
+ }
23
+
24
+ componentDidCatch(error: Error, errorInfo: ErrorInfo) {
25
+ console.error("ErrorBoundary caught an error:", error, errorInfo);
26
+ }
27
+
28
+ render() {
29
+ if (this.state.hasError) {
30
+ return (
31
+ <div className="min-h-screen flex items-center justify-center p-8">
32
+ <div className="max-w-md w-full">
33
+ <Alert variant="destructive">
34
+ <AlertTitle>Something went wrong</AlertTitle>
35
+ <AlertDescription>
36
+ The application encountered an error. Please try refreshing the
37
+ page or contact support if the problem persists.
38
+ </AlertDescription>
39
+ </Alert>
40
+ <div className="mt-4 flex gap-2">
41
+ <Button onClick={() => window.location.reload()}>
42
+ Refresh Page
43
+ </Button>
44
+ <Button
45
+ variant="outline"
46
+ onClick={() =>
47
+ this.setState({ hasError: false, error: undefined })
48
+ }
49
+ >
50
+ Try Again
51
+ </Button>
52
+ </div>
53
+ {process.env.NODE_ENV === "development" && this.state.error && (
54
+ <div className="mt-4 p-4 bg-gray-100 rounded-md text-xs">
55
+ <pre>{this.state.error.stack}</pre>
56
+ </div>
57
+ )}
58
+ </div>
59
+ </div>
60
+ );
61
+ }
62
+
63
+ return this.props.children;
64
+ }
65
+ }
src/demo/components/PortManager.tsx ADDED
@@ -0,0 +1,1050 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from "react";
2
+ import { Button } from "./ui/button";
3
+ import {
4
+ Card,
5
+ CardContent,
6
+ CardDescription,
7
+ CardHeader,
8
+ CardTitle,
9
+ } from "./ui/card";
10
+ import { Alert, AlertDescription } from "./ui/alert";
11
+ import { Badge } from "./ui/badge";
12
+ import { isWebSerialSupported } from "../../lerobot/web/calibrate";
13
+ import type { ConnectedRobot } from "../types";
14
+
15
+ interface PortManagerProps {
16
+ connectedRobots: ConnectedRobot[];
17
+ onConnectedRobotsChange: (robots: ConnectedRobot[]) => void;
18
+ onCalibrate?: (
19
+ port: SerialPort,
20
+ robotType: "so100_follower" | "so100_leader",
21
+ robotId: string
22
+ ) => void;
23
+ }
24
+
25
+ export function PortManager({
26
+ connectedRobots,
27
+ onConnectedRobotsChange,
28
+ onCalibrate,
29
+ }: PortManagerProps) {
30
+ const [isConnecting, setIsConnecting] = useState(false);
31
+ const [isFindingPorts, setIsFindingPorts] = useState(false);
32
+ const [findPortsLog, setFindPortsLog] = useState<string[]>([]);
33
+ const [error, setError] = useState<string | null>(null);
34
+
35
+ // Load saved port data from localStorage on mount
36
+ useEffect(() => {
37
+ loadSavedPorts();
38
+ }, []);
39
+
40
+ // Save port data to localStorage whenever connectedPorts changes
41
+ useEffect(() => {
42
+ savePortsToStorage();
43
+ }, [connectedRobots]);
44
+
45
+ const loadSavedPorts = async () => {
46
+ try {
47
+ const saved = localStorage.getItem("lerobot-ports");
48
+ if (!saved) return;
49
+
50
+ const savedData = JSON.parse(saved);
51
+ const existingPorts = await navigator.serial.getPorts();
52
+
53
+ const restoredPorts: ConnectedRobot[] = [];
54
+
55
+ for (const port of existingPorts) {
56
+ // Find saved data by matching port info instead of display name
57
+ const portInfo = port.getInfo();
58
+ const savedPort = savedData.find((p: any) => {
59
+ // Try to match by USB vendor/product ID if available
60
+ if (portInfo.usbVendorId && portInfo.usbProductId) {
61
+ return (
62
+ p.usbVendorId === portInfo.usbVendorId &&
63
+ p.usbProductId === portInfo.usbProductId
64
+ );
65
+ }
66
+ // Fallback to name matching
67
+ return p.name === getPortDisplayName(port);
68
+ });
69
+
70
+ // Auto-connect to paired robots
71
+ let isConnected = false;
72
+ try {
73
+ // Check if already open
74
+ if (port.readable !== null && port.writable !== null) {
75
+ isConnected = true;
76
+ } else {
77
+ // Auto-open paired robots
78
+ await port.open({ baudRate: 1000000 });
79
+ isConnected = true;
80
+ }
81
+ } catch (error) {
82
+ console.log("Could not auto-connect to paired robot:", error);
83
+ isConnected = false;
84
+ }
85
+
86
+ restoredPorts.push({
87
+ port,
88
+ name: getPortDisplayName(port),
89
+ isConnected,
90
+ robotType: savedPort?.robotType,
91
+ robotId: savedPort?.robotId,
92
+ });
93
+ }
94
+
95
+ onConnectedRobotsChange(restoredPorts);
96
+ } catch (error) {
97
+ console.error("Failed to load saved ports:", error);
98
+ }
99
+ };
100
+
101
+ const savePortsToStorage = () => {
102
+ try {
103
+ const dataToSave = connectedRobots.map((p) => {
104
+ const portInfo = p.port.getInfo();
105
+ return {
106
+ name: p.name,
107
+ robotType: p.robotType,
108
+ robotId: p.robotId,
109
+ usbVendorId: portInfo.usbVendorId,
110
+ usbProductId: portInfo.usbProductId,
111
+ };
112
+ });
113
+ localStorage.setItem("lerobot-ports", JSON.stringify(dataToSave));
114
+ } catch (error) {
115
+ console.error("Failed to save ports to storage:", error);
116
+ }
117
+ };
118
+
119
+ const getPortDisplayName = (port: SerialPort): string => {
120
+ try {
121
+ const info = port.getInfo();
122
+ if (info.usbVendorId && info.usbProductId) {
123
+ return `USB Port (${info.usbVendorId}:${info.usbProductId})`;
124
+ }
125
+ if (info.usbVendorId) {
126
+ return `Serial Port (VID:${info.usbVendorId
127
+ .toString(16)
128
+ .toUpperCase()})`;
129
+ }
130
+ } catch (error) {
131
+ // getInfo() might not be available
132
+ }
133
+ return `Serial Port ${Date.now()}`;
134
+ };
135
+
136
+ const handleConnect = async () => {
137
+ if (!isWebSerialSupported()) {
138
+ setError("Web Serial API is not supported in this browser");
139
+ return;
140
+ }
141
+
142
+ try {
143
+ setIsConnecting(true);
144
+ setError(null);
145
+
146
+ // Step 1: Request Web Serial port
147
+ console.log("Step 1: Requesting Web Serial port...");
148
+ const port = await navigator.serial.requestPort();
149
+ await port.open({ baudRate: 1000000 });
150
+
151
+ // Step 2: Request WebUSB device for metadata
152
+ console.log(
153
+ "Step 2: Requesting WebUSB device for unique identification..."
154
+ );
155
+ let serialNumber = null;
156
+ let usbMetadata = null;
157
+
158
+ try {
159
+ // Request USB device access for metadata
160
+ const usbDevice = await navigator.usb.requestDevice({
161
+ filters: [
162
+ { vendorId: 0x0403 }, // FTDI
163
+ { vendorId: 0x067b }, // Prolific
164
+ { vendorId: 0x10c4 }, // Silicon Labs
165
+ { vendorId: 0x1a86 }, // QinHeng Electronics (CH340)
166
+ { vendorId: 0x239a }, // Adafruit
167
+ { vendorId: 0x2341 }, // Arduino
168
+ { vendorId: 0x2e8a }, // Raspberry Pi Foundation
169
+ { vendorId: 0x1b4f }, // SparkFun
170
+ ],
171
+ });
172
+
173
+ if (usbDevice) {
174
+ serialNumber =
175
+ usbDevice.serialNumber ||
176
+ `${usbDevice.vendorId}-${usbDevice.productId}-${Date.now()}`;
177
+ usbMetadata = {
178
+ vendorId: `0x${usbDevice.vendorId.toString(16).padStart(4, "0")}`,
179
+ productId: `0x${usbDevice.productId.toString(16).padStart(4, "0")}`,
180
+ serialNumber: usbDevice.serialNumber || "Generated ID",
181
+ manufacturerName: usbDevice.manufacturerName || "Unknown",
182
+ productName: usbDevice.productName || "Unknown",
183
+ usbVersionMajor: usbDevice.usbVersionMajor,
184
+ usbVersionMinor: usbDevice.usbVersionMinor,
185
+ deviceClass: usbDevice.deviceClass,
186
+ deviceSubclass: usbDevice.deviceSubclass,
187
+ deviceProtocol: usbDevice.deviceProtocol,
188
+ };
189
+ console.log("✅ USB device metadata acquired:", usbMetadata);
190
+ }
191
+ } catch (usbError) {
192
+ console.log(
193
+ "⚠️ WebUSB request failed, generating fallback ID:",
194
+ usbError
195
+ );
196
+ // Generate a fallback unique ID if WebUSB fails
197
+ serialNumber = `fallback-${Date.now()}-${Math.random()
198
+ .toString(36)
199
+ .substr(2, 9)}`;
200
+ usbMetadata = {
201
+ vendorId: "Unknown",
202
+ productId: "Unknown",
203
+ serialNumber: serialNumber,
204
+ manufacturerName: "USB Metadata Not Available",
205
+ productName: "Check browser WebUSB support",
206
+ };
207
+ }
208
+
209
+ const portName = getPortDisplayName(port);
210
+
211
+ // Step 3: Check if this robot (by serial number) is already connected
212
+ const existingIndex = connectedRobots.findIndex(
213
+ (robot) => robot.serialNumber === serialNumber
214
+ );
215
+
216
+ if (existingIndex === -1) {
217
+ // New robot - add to list
218
+ const newRobot: ConnectedRobot = {
219
+ port,
220
+ name: portName,
221
+ isConnected: true,
222
+ serialNumber: serialNumber!,
223
+ usbMetadata: usbMetadata || undefined,
224
+ };
225
+
226
+ // Try to load saved robot info by serial number
227
+ try {
228
+ const savedRobotKey = `lerobot-robot-${serialNumber}`;
229
+ const savedData = localStorage.getItem(savedRobotKey);
230
+ if (savedData) {
231
+ const parsed = JSON.parse(savedData);
232
+ newRobot.robotType = parsed.robotType;
233
+ newRobot.robotId = parsed.robotId;
234
+ console.log("📋 Loaded saved robot configuration:", parsed);
235
+ }
236
+ } catch (error) {
237
+ console.warn("Failed to load saved robot data:", error);
238
+ }
239
+
240
+ onConnectedRobotsChange([...connectedRobots, newRobot]);
241
+ console.log("🤖 New robot connected with ID:", serialNumber);
242
+ } else {
243
+ // Existing robot - update port and connection status
244
+ const updatedRobots = connectedRobots.map((robot, index) =>
245
+ index === existingIndex
246
+ ? { ...robot, port, isConnected: true, name: portName }
247
+ : robot
248
+ );
249
+ onConnectedRobotsChange(updatedRobots);
250
+ console.log("🔄 Existing robot reconnected:", serialNumber);
251
+ }
252
+ } catch (error) {
253
+ if (
254
+ error instanceof Error &&
255
+ (error.message.includes("cancelled") ||
256
+ error.message.includes("No port selected by the user") ||
257
+ error.name === "NotAllowedError")
258
+ ) {
259
+ // User cancelled - no error message needed, just log to console
260
+ console.log("Connection cancelled by user");
261
+ return;
262
+ }
263
+ setError(
264
+ error instanceof Error ? error.message : "Failed to connect to robot"
265
+ );
266
+ } finally {
267
+ setIsConnecting(false);
268
+ }
269
+ };
270
+
271
+ const handleDisconnect = async (index: number) => {
272
+ try {
273
+ const portInfo = connectedRobots[index];
274
+ if (portInfo.isConnected) {
275
+ await portInfo.port.close();
276
+ }
277
+
278
+ const updatedRobots = connectedRobots.filter((_, i) => i !== index);
279
+ onConnectedRobotsChange(updatedRobots);
280
+ } catch (error) {
281
+ setError(
282
+ error instanceof Error ? error.message : "Failed to disconnect port"
283
+ );
284
+ }
285
+ };
286
+
287
+ const handleUpdatePortInfo = (
288
+ index: number,
289
+ robotType: "so100_follower" | "so100_leader",
290
+ robotId: string
291
+ ) => {
292
+ const updatedRobots = connectedRobots.map((robot, i) => {
293
+ if (i === index) {
294
+ const updatedRobot = { ...robot, robotType, robotId };
295
+
296
+ // Save robot configuration to localStorage using serial number
297
+ if (updatedRobot.serialNumber) {
298
+ try {
299
+ const robotKey = `lerobot-robot-${updatedRobot.serialNumber}`;
300
+ const robotData = {
301
+ robotType,
302
+ robotId,
303
+ serialNumber: updatedRobot.serialNumber,
304
+ lastUpdated: new Date().toISOString(),
305
+ };
306
+ localStorage.setItem(robotKey, JSON.stringify(robotData));
307
+ console.log(
308
+ "💾 Saved robot configuration for:",
309
+ updatedRobot.serialNumber
310
+ );
311
+ } catch (error) {
312
+ console.warn("Failed to save robot configuration:", error);
313
+ }
314
+ }
315
+
316
+ return updatedRobot;
317
+ }
318
+ return robot;
319
+ });
320
+ onConnectedRobotsChange(updatedRobots);
321
+ };
322
+
323
+ const handleFindPorts = async () => {
324
+ if (!isWebSerialSupported()) {
325
+ setError("Web Serial API is not supported in this browser");
326
+ return;
327
+ }
328
+
329
+ try {
330
+ setIsFindingPorts(true);
331
+ setFindPortsLog([]);
332
+ setError(null);
333
+
334
+ // Get initial ports
335
+ const initialPorts = await navigator.serial.getPorts();
336
+ setFindPortsLog((prev) => [
337
+ ...prev,
338
+ `Found ${initialPorts.length} existing paired port(s)`,
339
+ ]);
340
+
341
+ // Ask user to disconnect
342
+ setFindPortsLog((prev) => [
343
+ ...prev,
344
+ "Please disconnect the USB cable from your robot and click OK",
345
+ ]);
346
+
347
+ // Simple implementation - just show the instruction
348
+ // In a real implementation, we'd monitor port changes
349
+ const confirmed = confirm(
350
+ "Disconnect the USB cable from your robot and click OK when done"
351
+ );
352
+
353
+ if (confirmed) {
354
+ setFindPortsLog((prev) => [...prev, "Reconnect the USB cable now"]);
355
+
356
+ // Request port selection
357
+ const port = await navigator.serial.requestPort();
358
+ await port.open({ baudRate: 1000000 });
359
+
360
+ const portName = getPortDisplayName(port);
361
+ setFindPortsLog((prev) => [...prev, `Identified port: ${portName}`]);
362
+
363
+ // Add to connected ports if not already there
364
+ const existingIndex = connectedRobots.findIndex(
365
+ (p) => p.name === portName
366
+ );
367
+ if (existingIndex === -1) {
368
+ const newPort: ConnectedRobot = {
369
+ port,
370
+ name: portName,
371
+ isConnected: true,
372
+ };
373
+ onConnectedRobotsChange([...connectedRobots, newPort]);
374
+ }
375
+ }
376
+ } catch (error) {
377
+ if (
378
+ error instanceof Error &&
379
+ (error.message.includes("cancelled") ||
380
+ error.name === "NotAllowedError")
381
+ ) {
382
+ // User cancelled - no message needed, just log to console
383
+ console.log("Port identification cancelled by user");
384
+ return;
385
+ }
386
+ setError(error instanceof Error ? error.message : "Failed to find ports");
387
+ } finally {
388
+ setIsFindingPorts(false);
389
+ }
390
+ };
391
+
392
+ const ensurePortIsOpen = async (robotIndex: number) => {
393
+ const robot = connectedRobots[robotIndex];
394
+ if (!robot) return false;
395
+
396
+ try {
397
+ // If port is already open, we're good
398
+ if (robot.port.readable !== null && robot.port.writable !== null) {
399
+ return true;
400
+ }
401
+
402
+ // Try to open the port
403
+ await robot.port.open({ baudRate: 1000000 });
404
+
405
+ // Update the robot's connection status
406
+ const updatedRobots = connectedRobots.map((r, i) =>
407
+ i === robotIndex ? { ...r, isConnected: true } : r
408
+ );
409
+ onConnectedRobotsChange(updatedRobots);
410
+
411
+ return true;
412
+ } catch (error) {
413
+ console.error("Failed to open port for calibration:", error);
414
+ setError(error instanceof Error ? error.message : "Failed to open port");
415
+ return false;
416
+ }
417
+ };
418
+
419
+ const handleCalibrate = async (port: ConnectedRobot) => {
420
+ if (!port.robotType || !port.robotId) {
421
+ setError("Please set robot type and ID before calibrating");
422
+ return;
423
+ }
424
+
425
+ // Find the robot index
426
+ const robotIndex = connectedRobots.findIndex((r) => r.port === port.port);
427
+ if (robotIndex === -1) {
428
+ setError("Robot not found in connected robots list");
429
+ return;
430
+ }
431
+
432
+ // Ensure port is open before calibrating
433
+ const isOpen = await ensurePortIsOpen(robotIndex);
434
+ if (!isOpen) {
435
+ return; // Error already set in ensurePortIsOpen
436
+ }
437
+
438
+ if (onCalibrate) {
439
+ onCalibrate(port.port, port.robotType, port.robotId);
440
+ }
441
+ };
442
+
443
+ return (
444
+ <Card>
445
+ <CardHeader>
446
+ <CardTitle>🔌 Robot Connection Manager</CardTitle>
447
+ <CardDescription>
448
+ Connect, identify, and manage your robot arms
449
+ </CardDescription>
450
+ </CardHeader>
451
+ <CardContent>
452
+ <div className="space-y-6">
453
+ {/* Error Display */}
454
+ {error && (
455
+ <Alert variant="destructive">
456
+ <AlertDescription>{error}</AlertDescription>
457
+ </Alert>
458
+ )}
459
+
460
+ {/* Connection Controls */}
461
+ <div className="flex gap-2">
462
+ <Button
463
+ onClick={handleConnect}
464
+ disabled={isConnecting || !isWebSerialSupported()}
465
+ className="flex-1"
466
+ >
467
+ {isConnecting ? "Connecting..." : "Connect Robot"}
468
+ </Button>
469
+ <Button
470
+ variant="outline"
471
+ onClick={handleFindPorts}
472
+ disabled={isFindingPorts || !isWebSerialSupported()}
473
+ className="flex-1"
474
+ >
475
+ {isFindingPorts ? "Finding..." : "Find Port"}
476
+ </Button>
477
+ </div>
478
+
479
+ {/* Find Ports Log */}
480
+ {findPortsLog.length > 0 && (
481
+ <div className="bg-gray-50 p-3 rounded-md text-sm space-y-1">
482
+ {findPortsLog.map((log, index) => (
483
+ <div key={index} className="text-gray-700">
484
+ {log}
485
+ </div>
486
+ ))}
487
+ </div>
488
+ )}
489
+
490
+ {/* Connected Ports */}
491
+ <div>
492
+ <h4 className="font-semibold mb-3">
493
+ Connected Robots ({connectedRobots.length})
494
+ </h4>
495
+
496
+ {connectedRobots.length === 0 ? (
497
+ <div className="text-center py-8 text-gray-500">
498
+ <div className="text-2xl mb-2">🤖</div>
499
+ <p>No robots connected</p>
500
+ <p className="text-xs">
501
+ Use "Connect Robot" or "Find Port" to add robots
502
+ </p>
503
+ </div>
504
+ ) : (
505
+ <div className="space-y-4">
506
+ {connectedRobots.map((portInfo, index) => (
507
+ <PortCard
508
+ key={index}
509
+ portInfo={portInfo}
510
+ onDisconnect={() => handleDisconnect(index)}
511
+ onUpdateInfo={(robotType, robotId) =>
512
+ handleUpdatePortInfo(index, robotType, robotId)
513
+ }
514
+ onCalibrate={() => handleCalibrate(portInfo)}
515
+ />
516
+ ))}
517
+ </div>
518
+ )}
519
+ </div>
520
+ </div>
521
+ </CardContent>
522
+ </Card>
523
+ );
524
+ }
525
+
526
+ interface PortCardProps {
527
+ portInfo: ConnectedRobot;
528
+ onDisconnect: () => void;
529
+ onUpdateInfo: (
530
+ robotType: "so100_follower" | "so100_leader",
531
+ robotId: string
532
+ ) => void;
533
+ onCalibrate: () => void;
534
+ }
535
+
536
+ function PortCard({
537
+ portInfo,
538
+ onDisconnect,
539
+ onUpdateInfo,
540
+ onCalibrate,
541
+ }: PortCardProps) {
542
+ const [robotType, setRobotType] = useState<"so100_follower" | "so100_leader">(
543
+ portInfo.robotType || "so100_follower"
544
+ );
545
+ const [robotId, setRobotId] = useState(portInfo.robotId || "");
546
+ const [isEditing, setIsEditing] = useState(false);
547
+ const [isScanning, setIsScanning] = useState(false);
548
+ const [motorIDs, setMotorIDs] = useState<number[]>([]);
549
+ const [portMetadata, setPortMetadata] = useState<any>(null);
550
+ const [showDeviceInfo, setShowDeviceInfo] = useState(false);
551
+
552
+ // Check for calibration in localStorage using serial number
553
+ const getCalibrationStatus = () => {
554
+ if (!portInfo.serialNumber) return null;
555
+
556
+ const calibrationKey = `lerobot-calibration-${portInfo.serialNumber}`;
557
+ try {
558
+ const saved = localStorage.getItem(calibrationKey);
559
+ if (saved) {
560
+ const calibrationData = JSON.parse(saved);
561
+ return {
562
+ timestamp: calibrationData.timestamp,
563
+ readCount: calibrationData.readCount,
564
+ };
565
+ }
566
+ } catch (error) {
567
+ console.warn("Failed to read calibration from localStorage:", error);
568
+ }
569
+ return null;
570
+ };
571
+
572
+ const calibrationStatus = getCalibrationStatus();
573
+
574
+ const handleSave = () => {
575
+ if (robotId.trim()) {
576
+ onUpdateInfo(robotType, robotId.trim());
577
+ setIsEditing(false);
578
+ }
579
+ };
580
+
581
+ // Use current values (either from props or local state)
582
+ const currentRobotType = portInfo.robotType || robotType;
583
+ const currentRobotId = portInfo.robotId || robotId;
584
+
585
+ const handleCancel = () => {
586
+ setRobotType(portInfo.robotType || "so100_follower");
587
+ setRobotId(portInfo.robotId || "");
588
+ setIsEditing(false);
589
+ };
590
+
591
+ // Scan for motor IDs and gather USB device metadata
592
+ const scanDeviceInfo = async () => {
593
+ if (!portInfo.port || !portInfo.isConnected) {
594
+ console.warn("Port not connected");
595
+ return;
596
+ }
597
+
598
+ setIsScanning(true);
599
+ setMotorIDs([]);
600
+ setPortMetadata(null);
601
+ const foundIDs: number[] = [];
602
+
603
+ try {
604
+ // Try to get USB device info using WebUSB for better metadata
605
+ let usbDeviceInfo = null;
606
+
607
+ try {
608
+ // First, check if we already have USB device permissions
609
+ let usbDevices = await navigator.usb.getDevices();
610
+ console.log("Already permitted USB devices:", usbDevices);
611
+
612
+ // If no devices found, request permission for USB-to-serial devices
613
+ if (usbDevices.length === 0) {
614
+ console.log(
615
+ "No USB permissions yet, requesting access to USB-to-serial devices..."
616
+ );
617
+
618
+ // Request access to common USB-to-serial chips
619
+ try {
620
+ const device = await navigator.usb.requestDevice({
621
+ filters: [
622
+ { vendorId: 0x0403 }, // FTDI
623
+ { vendorId: 0x067b }, // Prolific
624
+ { vendorId: 0x10c4 }, // Silicon Labs
625
+ { vendorId: 0x1a86 }, // QinHeng Electronics (CH340)
626
+ { vendorId: 0x239a }, // Adafruit
627
+ { vendorId: 0x2341 }, // Arduino
628
+ { vendorId: 0x2e8a }, // Raspberry Pi Foundation
629
+ { vendorId: 0x1b4f }, // SparkFun
630
+ ],
631
+ });
632
+
633
+ if (device) {
634
+ usbDevices = [device];
635
+ console.log("USB device access granted:", device);
636
+ }
637
+ } catch (requestError) {
638
+ console.log(
639
+ "User cancelled USB device selection or no devices found"
640
+ );
641
+ // Try requesting any device as fallback
642
+ try {
643
+ const anyDevice = await navigator.usb.requestDevice({
644
+ filters: [], // Allow any USB device
645
+ });
646
+ if (anyDevice) {
647
+ usbDevices = [anyDevice];
648
+ console.log("Fallback USB device selected:", anyDevice);
649
+ }
650
+ } catch (fallbackError) {
651
+ console.log("No USB device selected");
652
+ }
653
+ }
654
+ }
655
+
656
+ // Try to match with Web Serial port (this is tricky, so we'll take the first available)
657
+ if (usbDevices.length > 0) {
658
+ // Look for common USB-to-serial chip vendor IDs
659
+ const serialChipVendors = [
660
+ 0x0403, // FTDI
661
+ 0x067b, // Prolific
662
+ 0x10c4, // Silicon Labs
663
+ 0x1a86, // QinHeng Electronics (CH340)
664
+ 0x239a, // Adafruit
665
+ 0x2341, // Arduino
666
+ 0x2e8a, // Raspberry Pi Foundation
667
+ 0x1b4f, // SparkFun
668
+ ];
669
+
670
+ const serialDevice =
671
+ usbDevices.find((device) =>
672
+ serialChipVendors.includes(device.vendorId)
673
+ ) || usbDevices[0]; // Fallback to first device
674
+
675
+ if (serialDevice) {
676
+ usbDeviceInfo = {
677
+ vendorId: `0x${serialDevice.vendorId
678
+ .toString(16)
679
+ .padStart(4, "0")}`,
680
+ productId: `0x${serialDevice.productId
681
+ .toString(16)
682
+ .padStart(4, "0")}`,
683
+ serialNumber: serialDevice.serialNumber || "Not available",
684
+ manufacturerName: serialDevice.manufacturerName || "Unknown",
685
+ productName: serialDevice.productName || "Unknown",
686
+ usbVersionMajor: serialDevice.usbVersionMajor,
687
+ usbVersionMinor: serialDevice.usbVersionMinor,
688
+ deviceClass: serialDevice.deviceClass,
689
+ deviceSubclass: serialDevice.deviceSubclass,
690
+ deviceProtocol: serialDevice.deviceProtocol,
691
+ };
692
+ console.log("USB device info:", usbDeviceInfo);
693
+ }
694
+ }
695
+ } catch (usbError) {
696
+ console.log("WebUSB not available or no permissions:", usbError);
697
+ // Fallback to Web Serial API info
698
+ const portInfo_metadata = portInfo.port.getInfo();
699
+ console.log("Serial port metadata fallback:", portInfo_metadata);
700
+ if (Object.keys(portInfo_metadata).length > 0) {
701
+ usbDeviceInfo = {
702
+ vendorId: portInfo_metadata.usbVendorId
703
+ ? `0x${portInfo_metadata.usbVendorId
704
+ .toString(16)
705
+ .padStart(4, "0")}`
706
+ : "Not available",
707
+ productId: portInfo_metadata.usbProductId
708
+ ? `0x${portInfo_metadata.usbProductId
709
+ .toString(16)
710
+ .padStart(4, "0")}`
711
+ : "Not available",
712
+ serialNumber: "Not available via Web Serial",
713
+ manufacturerName: "Not available via Web Serial",
714
+ productName: "Not available via Web Serial",
715
+ };
716
+ }
717
+ }
718
+
719
+ setPortMetadata(usbDeviceInfo);
720
+
721
+ // Get reader/writer for the port
722
+ const reader = portInfo.port.readable?.getReader();
723
+ const writer = portInfo.port.writable?.getWriter();
724
+
725
+ if (!reader || !writer) {
726
+ console.warn("Cannot access port reader/writer");
727
+ setShowDeviceInfo(true);
728
+ return;
729
+ }
730
+
731
+ // Test motor IDs 1-10 (common range for servos)
732
+ for (let motorId = 1; motorId <= 10; motorId++) {
733
+ try {
734
+ // Create STS3215 ping packet
735
+ const packet = new Uint8Array([
736
+ 0xff,
737
+ 0xff,
738
+ motorId,
739
+ 0x02,
740
+ 0x01,
741
+ 0x00,
742
+ ]);
743
+ const checksum = ~(motorId + 0x02 + 0x01) & 0xff;
744
+ packet[5] = checksum;
745
+
746
+ // Send ping
747
+ await writer.write(packet);
748
+
749
+ // Wait a bit for response
750
+ await new Promise((resolve) => setTimeout(resolve, 20));
751
+
752
+ // Try to read response with timeout
753
+ const timeoutPromise = new Promise((_, reject) =>
754
+ setTimeout(() => reject(new Error("Timeout")), 50)
755
+ );
756
+
757
+ try {
758
+ const result = (await Promise.race([
759
+ reader.read(),
760
+ timeoutPromise,
761
+ ])) as ReadableStreamReadResult<Uint8Array>;
762
+
763
+ if (
764
+ result &&
765
+ !result.done &&
766
+ result.value &&
767
+ result.value.length >= 6
768
+ ) {
769
+ const response = result.value;
770
+ const responseId = response[2];
771
+
772
+ // If we got a response with matching ID, motor exists
773
+ if (responseId === motorId) {
774
+ foundIDs.push(motorId);
775
+ }
776
+ }
777
+ } catch (readError) {
778
+ // No response from this motor ID - that's normal
779
+ }
780
+ } catch (error) {
781
+ console.warn(`Error testing motor ID ${motorId}:`, error);
782
+ }
783
+
784
+ // Small delay between tests
785
+ await new Promise((resolve) => setTimeout(resolve, 10));
786
+ }
787
+
788
+ reader.releaseLock();
789
+ writer.releaseLock();
790
+
791
+ setMotorIDs(foundIDs);
792
+ setShowDeviceInfo(true);
793
+ } catch (error) {
794
+ console.error("Device info scan failed:", error);
795
+ } finally {
796
+ setIsScanning(false);
797
+ }
798
+ };
799
+
800
+ return (
801
+ <div className="border rounded-lg p-4 space-y-3">
802
+ {/* Header with port name and status */}
803
+ <div className="flex items-center justify-between">
804
+ <div className="flex items-center space-x-2">
805
+ <div className="flex flex-col">
806
+ <span className="font-medium">{portInfo.name}</span>
807
+ {portInfo.serialNumber && (
808
+ <span className="text-xs text-gray-500 font-mono">
809
+ ID:{" "}
810
+ {portInfo.serialNumber.length > 20
811
+ ? portInfo.serialNumber.substring(0, 20) + "..."
812
+ : portInfo.serialNumber}
813
+ </span>
814
+ )}
815
+ </div>
816
+ <Badge variant={portInfo.isConnected ? "default" : "outline"}>
817
+ {portInfo.isConnected ? "Connected" : "Available"}
818
+ </Badge>
819
+ {portInfo.usbMetadata && (
820
+ <Badge variant="outline" className="text-xs">
821
+ {portInfo.usbMetadata.manufacturerName}
822
+ </Badge>
823
+ )}
824
+ </div>
825
+ <Button variant="destructive" size="sm" onClick={onDisconnect}>
826
+ Remove
827
+ </Button>
828
+ </div>
829
+
830
+ {/* Robot Info Display (when not editing) */}
831
+ {!isEditing && currentRobotType && currentRobotId && (
832
+ <div className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
833
+ <div className="flex items-center space-x-3">
834
+ <div>
835
+ <div className="font-medium text-sm">{currentRobotId}</div>
836
+ <div className="text-xs text-gray-600">
837
+ {currentRobotType.replace("_", " ")}
838
+ </div>
839
+ </div>
840
+ {calibrationStatus && (
841
+ <Badge variant="default" className="bg-green-100 text-green-800">
842
+ ✅ Calibrated
843
+ </Badge>
844
+ )}
845
+ </div>
846
+ <Button
847
+ variant="outline"
848
+ size="sm"
849
+ onClick={() => setIsEditing(true)}
850
+ >
851
+ Edit
852
+ </Button>
853
+ </div>
854
+ )}
855
+
856
+ {/* Setup prompt for unconfigured robots */}
857
+ {!isEditing && (!currentRobotType || !currentRobotId) && (
858
+ <div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
859
+ <div className="text-sm text-blue-800">
860
+ Robot needs configuration before use
861
+ </div>
862
+ <Button
863
+ variant="outline"
864
+ size="sm"
865
+ onClick={() => setIsEditing(true)}
866
+ >
867
+ Configure
868
+ </Button>
869
+ </div>
870
+ )}
871
+
872
+ {/* Robot Configuration Form (when editing) */}
873
+ {isEditing && (
874
+ <div className="space-y-3 p-3 bg-gray-50 rounded-lg">
875
+ <div className="grid grid-cols-2 gap-3">
876
+ <div>
877
+ <label className="text-sm font-medium block mb-1">
878
+ Robot Type
879
+ </label>
880
+ <select
881
+ value={robotType}
882
+ onChange={(e) =>
883
+ setRobotType(
884
+ e.target.value as "so100_follower" | "so100_leader"
885
+ )
886
+ }
887
+ className="w-full px-2 py-1 border rounded text-sm"
888
+ >
889
+ <option value="so100_follower">SO-100 Follower</option>
890
+ <option value="so100_leader">SO-100 Leader</option>
891
+ </select>
892
+ </div>
893
+ <div>
894
+ <label className="text-sm font-medium block mb-1">Robot ID</label>
895
+ <input
896
+ type="text"
897
+ value={robotId}
898
+ onChange={(e) => setRobotId(e.target.value)}
899
+ placeholder="e.g., my_robot"
900
+ className="w-full px-2 py-1 border rounded text-sm"
901
+ />
902
+ </div>
903
+ </div>
904
+
905
+ <div className="flex gap-2">
906
+ <Button size="sm" onClick={handleSave} disabled={!robotId.trim()}>
907
+ Save
908
+ </Button>
909
+ <Button size="sm" variant="outline" onClick={handleCancel}>
910
+ Cancel
911
+ </Button>
912
+ </div>
913
+ </div>
914
+ )}
915
+
916
+ {/* Calibration Status and Action */}
917
+ {currentRobotType && currentRobotId && (
918
+ <div className="space-y-3">
919
+ <div className="flex items-center justify-between">
920
+ <div className="text-sm text-gray-600">
921
+ {calibrationStatus ? (
922
+ <span>
923
+ Last calibrated:{" "}
924
+ {new Date(calibrationStatus.timestamp).toLocaleDateString()}
925
+ <span className="text-xs ml-1">
926
+ ({calibrationStatus.readCount} readings)
927
+ </span>
928
+ </span>
929
+ ) : (
930
+ <span>Not calibrated yet</span>
931
+ )}
932
+ </div>
933
+ <Button
934
+ size="sm"
935
+ variant={calibrationStatus ? "outline" : "default"}
936
+ onClick={onCalibrate}
937
+ disabled={!currentRobotType || !currentRobotId}
938
+ >
939
+ {calibrationStatus ? "Re-calibrate" : "Calibrate"}
940
+ </Button>
941
+ </div>
942
+
943
+ {/* Device Info Scanner */}
944
+ <div className="flex items-center justify-between">
945
+ <div className="text-sm text-gray-600">
946
+ Scan device info and motor IDs
947
+ </div>
948
+ <Button
949
+ size="sm"
950
+ variant="outline"
951
+ onClick={scanDeviceInfo}
952
+ disabled={!portInfo.isConnected || isScanning}
953
+ >
954
+ {isScanning ? "Scanning..." : "Show Device Info"}
955
+ </Button>
956
+ </div>
957
+
958
+ {/* Device Info Results */}
959
+ {showDeviceInfo && (
960
+ <div className="p-3 bg-gray-50 rounded-lg space-y-3">
961
+ {/* USB Device Information */}
962
+ {portMetadata && (
963
+ <div>
964
+ <div className="text-sm font-medium mb-2">
965
+ 📱 USB Device Info:
966
+ </div>
967
+ <div className="space-y-1 text-xs">
968
+ <div className="flex justify-between">
969
+ <span className="text-gray-600">Vendor ID:</span>
970
+ <span className="font-mono">{portMetadata.vendorId}</span>
971
+ </div>
972
+ <div className="flex justify-between">
973
+ <span className="text-gray-600">Product ID:</span>
974
+ <span className="font-mono">
975
+ {portMetadata.productId}
976
+ </span>
977
+ </div>
978
+ <div className="flex justify-between">
979
+ <span className="text-gray-600">Serial Number:</span>
980
+ <span className="font-mono text-green-600 font-semibold">
981
+ {portMetadata.serialNumber}
982
+ </span>
983
+ </div>
984
+ <div className="flex justify-between">
985
+ <span className="text-gray-600">Manufacturer:</span>
986
+ <span>{portMetadata.manufacturerName}</span>
987
+ </div>
988
+ <div className="flex justify-between">
989
+ <span className="text-gray-600">Product:</span>
990
+ <span>{portMetadata.productName}</span>
991
+ </div>
992
+ {portMetadata.usbVersionMajor && (
993
+ <div className="flex justify-between">
994
+ <span className="text-gray-600">USB Version:</span>
995
+ <span>
996
+ {portMetadata.usbVersionMajor}.
997
+ {portMetadata.usbVersionMinor}
998
+ </span>
999
+ </div>
1000
+ )}
1001
+ {portMetadata.deviceClass !== undefined && (
1002
+ <div className="flex justify-between">
1003
+ <span className="text-gray-600">Device Class:</span>
1004
+ <span>
1005
+ 0x
1006
+ {portMetadata.deviceClass
1007
+ .toString(16)
1008
+ .padStart(2, "0")}
1009
+ </span>
1010
+ </div>
1011
+ )}
1012
+ </div>
1013
+ </div>
1014
+ )}
1015
+
1016
+ {/* Motor IDs */}
1017
+ <div>
1018
+ <div className="text-sm font-medium mb-2">
1019
+ 🤖 Found Motor IDs:
1020
+ </div>
1021
+ {motorIDs.length > 0 ? (
1022
+ <div className="flex flex-wrap gap-2">
1023
+ {motorIDs.map((id) => (
1024
+ <Badge key={id} variant="outline" className="text-xs">
1025
+ Motor {id}
1026
+ </Badge>
1027
+ ))}
1028
+ </div>
1029
+ ) : (
1030
+ <div className="text-sm text-gray-500">
1031
+ No motor IDs found. Check connection and power.
1032
+ </div>
1033
+ )}
1034
+ </div>
1035
+
1036
+ <Button
1037
+ size="sm"
1038
+ variant="outline"
1039
+ onClick={() => setShowDeviceInfo(false)}
1040
+ className="mt-2 text-xs"
1041
+ >
1042
+ Hide
1043
+ </Button>
1044
+ </div>
1045
+ )}
1046
+ </div>
1047
+ )}
1048
+ </div>
1049
+ );
1050
+ }
src/demo/components/ui/alert.tsx ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { cn } from "../../lib/utils";
4
+
5
+ const alertVariants = cva(
6
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: "bg-background text-foreground",
11
+ destructive:
12
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
13
+ },
14
+ },
15
+ defaultVariants: {
16
+ variant: "default",
17
+ },
18
+ }
19
+ );
20
+
21
+ const Alert = React.forwardRef<
22
+ HTMLDivElement,
23
+ React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
24
+ >(({ className, variant, ...props }, ref) => (
25
+ <div
26
+ ref={ref}
27
+ role="alert"
28
+ className={cn(alertVariants({ variant }), className)}
29
+ {...props}
30
+ />
31
+ ));
32
+ Alert.displayName = "Alert";
33
+
34
+ const AlertTitle = React.forwardRef<
35
+ HTMLParagraphElement,
36
+ React.HTMLAttributes<HTMLHeadingElement>
37
+ >(({ className, ...props }, ref) => (
38
+ <h5
39
+ ref={ref}
40
+ className={cn("mb-1 font-medium leading-none tracking-tight", className)}
41
+ {...props}
42
+ />
43
+ ));
44
+ AlertTitle.displayName = "AlertTitle";
45
+
46
+ const AlertDescription = React.forwardRef<
47
+ HTMLParagraphElement,
48
+ React.HTMLAttributes<HTMLParagraphElement>
49
+ >(({ className, ...props }, ref) => (
50
+ <div
51
+ ref={ref}
52
+ className={cn("text-sm [&_p]:leading-relaxed", className)}
53
+ {...props}
54
+ />
55
+ ));
56
+ AlertDescription.displayName = "AlertDescription";
57
+
58
+ export { Alert, AlertTitle, AlertDescription };
src/demo/components/ui/badge.tsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { cn } from "../../lib/utils";
4
+
5
+ const badgeVariants = cva(
6
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default:
11
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
12
+ secondary:
13
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
14
+ destructive:
15
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
16
+ outline: "text-foreground",
17
+ },
18
+ },
19
+ defaultVariants: {
20
+ variant: "default",
21
+ },
22
+ }
23
+ );
24
+
25
+ export interface BadgeProps
26
+ extends React.HTMLAttributes<HTMLDivElement>,
27
+ VariantProps<typeof badgeVariants> {}
28
+
29
+ function Badge({ className, variant, ...props }: BadgeProps) {
30
+ return (
31
+ <div className={cn(badgeVariants({ variant }), className)} {...props} />
32
+ );
33
+ }
34
+
35
+ export { Badge, badgeVariants };
src/demo/components/ui/button.tsx ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { cn } from "../../lib/utils";
4
+
5
+ const buttonVariants = cva(
6
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
11
+ destructive:
12
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
13
+ outline:
14
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
15
+ secondary:
16
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
17
+ ghost: "hover:bg-accent hover:text-accent-foreground",
18
+ link: "text-primary underline-offset-4 hover:underline",
19
+ },
20
+ size: {
21
+ default: "h-10 px-4 py-2",
22
+ sm: "h-9 rounded-md px-3",
23
+ lg: "h-11 rounded-md px-8",
24
+ icon: "h-10 w-10",
25
+ },
26
+ },
27
+ defaultVariants: {
28
+ variant: "default",
29
+ size: "default",
30
+ },
31
+ }
32
+ );
33
+
34
+ export interface ButtonProps
35
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
36
+ VariantProps<typeof buttonVariants> {
37
+ asChild?: boolean;
38
+ }
39
+
40
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
41
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
42
+ return (
43
+ <button
44
+ className={cn(buttonVariants({ variant, size, className }))}
45
+ ref={ref}
46
+ {...props}
47
+ />
48
+ );
49
+ }
50
+ );
51
+ Button.displayName = "Button";
52
+
53
+ export { Button, buttonVariants };
src/demo/components/ui/card.tsx ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { cn } from "../../lib/utils";
3
+
4
+ const Card = React.forwardRef<
5
+ HTMLDivElement,
6
+ React.HTMLAttributes<HTMLDivElement>
7
+ >(({ className, ...props }, ref) => (
8
+ <div
9
+ ref={ref}
10
+ className={cn(
11
+ "rounded-lg border bg-card text-card-foreground shadow-sm",
12
+ className
13
+ )}
14
+ {...props}
15
+ />
16
+ ));
17
+ Card.displayName = "Card";
18
+
19
+ const CardHeader = React.forwardRef<
20
+ HTMLDivElement,
21
+ React.HTMLAttributes<HTMLDivElement>
22
+ >(({ className, ...props }, ref) => (
23
+ <div
24
+ ref={ref}
25
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
26
+ {...props}
27
+ />
28
+ ));
29
+ CardHeader.displayName = "CardHeader";
30
+
31
+ const CardTitle = React.forwardRef<
32
+ HTMLParagraphElement,
33
+ React.HTMLAttributes<HTMLHeadingElement>
34
+ >(({ className, ...props }, ref) => (
35
+ <h3
36
+ ref={ref}
37
+ className={cn(
38
+ "text-2xl font-semibold leading-none tracking-tight",
39
+ className
40
+ )}
41
+ {...props}
42
+ />
43
+ ));
44
+ CardTitle.displayName = "CardTitle";
45
+
46
+ const CardDescription = React.forwardRef<
47
+ HTMLParagraphElement,
48
+ React.HTMLAttributes<HTMLParagraphElement>
49
+ >(({ className, ...props }, ref) => (
50
+ <p
51
+ ref={ref}
52
+ className={cn("text-sm text-muted-foreground", className)}
53
+ {...props}
54
+ />
55
+ ));
56
+ CardDescription.displayName = "CardDescription";
57
+
58
+ const CardContent = React.forwardRef<
59
+ HTMLDivElement,
60
+ React.HTMLAttributes<HTMLDivElement>
61
+ >(({ className, ...props }, ref) => (
62
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
63
+ ));
64
+ CardContent.displayName = "CardContent";
65
+
66
+ const CardFooter = React.forwardRef<
67
+ HTMLDivElement,
68
+ React.HTMLAttributes<HTMLDivElement>
69
+ >(({ className, ...props }, ref) => (
70
+ <div
71
+ ref={ref}
72
+ className={cn("flex items-center p-6 pt-0", className)}
73
+ {...props}
74
+ />
75
+ ));
76
+ CardFooter.displayName = "CardFooter";
77
+
78
+ export {
79
+ Card,
80
+ CardHeader,
81
+ CardFooter,
82
+ CardTitle,
83
+ CardDescription,
84
+ CardContent,
85
+ };
src/demo/index.css ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ * {
7
+ @apply border-border;
8
+ }
9
+ body {
10
+ @apply bg-background text-foreground;
11
+ }
12
+ }
src/demo/lib/utils.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
src/demo/main.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import ReactDOM from "react-dom/client";
3
+ import { App } from "./App";
4
+ import "./index.css";
5
+
6
+ ReactDOM.createRoot(document.getElementById("root")!).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>
10
+ );
src/demo/pages/Calibrate.tsx ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from "react";
2
+ import { Button } from "../components/ui/button";
3
+ import {
4
+ Card,
5
+ CardContent,
6
+ CardDescription,
7
+ CardHeader,
8
+ CardTitle,
9
+ } from "../components/ui/card";
10
+ import { Alert, AlertDescription } from "../components/ui/alert";
11
+ import { Badge } from "../components/ui/badge";
12
+ import { CalibrationWizard } from "../components/CalibrationWizard";
13
+ import type { ConnectedRobot } from "../types";
14
+
15
+ interface CalibrateProps {
16
+ selectedRobot: ConnectedRobot;
17
+ onBack: () => void;
18
+ onHome: () => void;
19
+ }
20
+
21
+ export function Calibrate({ selectedRobot, onBack, onHome }: CalibrateProps) {
22
+ const [calibrationStarted, setCalibrationStarted] = useState(false);
23
+
24
+ // Auto-start calibration when component mounts
25
+ useEffect(() => {
26
+ if (selectedRobot && selectedRobot.isConnected) {
27
+ setCalibrationStarted(true);
28
+ }
29
+ }, [selectedRobot]);
30
+
31
+ if (!selectedRobot) {
32
+ return (
33
+ <div className="container mx-auto px-4 py-8">
34
+ <Alert variant="destructive">
35
+ <AlertDescription>
36
+ No robot selected. Please go back to setup.
37
+ </AlertDescription>
38
+ </Alert>
39
+ <div className="mt-4">
40
+ <Button onClick={onBack}>Back to Setup</Button>
41
+ </div>
42
+ </div>
43
+ );
44
+ }
45
+
46
+ return (
47
+ <div className="container mx-auto px-4 py-8 max-w-4xl">
48
+ <div className="space-y-6">
49
+ <div className="text-center space-y-2">
50
+ <h1 className="text-3xl font-bold">Robot Calibration</h1>
51
+ <p className="text-muted-foreground">
52
+ Calibrating: {selectedRobot.robotId}
53
+ </p>
54
+ </div>
55
+
56
+ <Card>
57
+ <CardHeader>
58
+ <div className="flex items-center justify-between">
59
+ <div>
60
+ <CardTitle className="text-xl">
61
+ {selectedRobot.robotId}
62
+ </CardTitle>
63
+ <CardDescription>{selectedRobot.name}</CardDescription>
64
+ </div>
65
+ <div className="flex items-center space-x-2">
66
+ <Badge
67
+ variant={selectedRobot.isConnected ? "default" : "secondary"}
68
+ >
69
+ {selectedRobot.isConnected ? "Connected" : "Disconnected"}
70
+ </Badge>
71
+ <Badge variant="outline">
72
+ {selectedRobot.robotType?.replace("_", " ")}
73
+ </Badge>
74
+ </div>
75
+ </div>
76
+ </CardHeader>
77
+ <CardContent>
78
+ {!selectedRobot.isConnected ? (
79
+ <Alert variant="destructive">
80
+ <AlertDescription>
81
+ Robot is not connected. Please check connection and try again.
82
+ </AlertDescription>
83
+ </Alert>
84
+ ) : calibrationStarted ? (
85
+ <CalibrationWizard
86
+ robot={selectedRobot}
87
+ onComplete={onHome}
88
+ onCancel={onBack}
89
+ />
90
+ ) : (
91
+ <div className="space-y-4">
92
+ <div className="text-center py-8">
93
+ <div className="text-2xl mb-4">🛠️</div>
94
+ <h3 className="text-lg font-semibold mb-2">
95
+ Ready to Calibrate
96
+ </h3>
97
+ <p className="text-muted-foreground mb-4">
98
+ Make sure your robot arm is in a safe position and you have
99
+ a clear workspace.
100
+ </p>
101
+ <Button onClick={() => setCalibrationStarted(true)} size="lg">
102
+ Start Calibration
103
+ </Button>
104
+ </div>
105
+ </div>
106
+ )}
107
+ </CardContent>
108
+ </Card>
109
+
110
+ <div className="flex justify-center space-x-4">
111
+ <Button variant="outline" onClick={onBack}>
112
+ Back to Setup
113
+ </Button>
114
+ <Button variant="outline" onClick={onHome}>
115
+ Back to Home
116
+ </Button>
117
+ </div>
118
+ </div>
119
+ </div>
120
+ );
121
+ }
src/demo/pages/Home.tsx ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from "react";
2
+ import { Button } from "../components/ui/button";
3
+ import {
4
+ Card,
5
+ CardContent,
6
+ CardDescription,
7
+ CardHeader,
8
+ CardTitle,
9
+ } from "../components/ui/card";
10
+ import { Alert, AlertDescription } from "../components/ui/alert";
11
+ import { PortManager } from "../components/PortManager";
12
+ import { CalibrationPanel } from "../components/CalibrationPanel";
13
+ import { isWebSerialSupported } from "../../lerobot/web/calibrate";
14
+ import type { ConnectedRobot } from "../types";
15
+
16
+ interface HomeProps {
17
+ onGetStarted: () => void;
18
+ connectedRobots: ConnectedRobot[];
19
+ onConnectedRobotsChange: (robots: ConnectedRobot[]) => void;
20
+ }
21
+
22
+ export function Home({
23
+ onGetStarted,
24
+ connectedRobots,
25
+ onConnectedRobotsChange,
26
+ }: HomeProps) {
27
+ const [calibratingRobot, setCalibratingRobot] =
28
+ useState<ConnectedRobot | null>(null);
29
+ const isSupported = isWebSerialSupported();
30
+
31
+ const handleCalibrate = (
32
+ port: SerialPort,
33
+ robotType: "so100_follower" | "so100_leader",
34
+ robotId: string
35
+ ) => {
36
+ // Find the robot from connectedRobots
37
+ const robot = connectedRobots.find((r) => r.port === port);
38
+ if (robot) {
39
+ setCalibratingRobot(robot);
40
+ }
41
+ };
42
+
43
+ const handleFinishCalibration = () => {
44
+ setCalibratingRobot(null);
45
+ };
46
+
47
+ return (
48
+ <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
49
+ <div className="container mx-auto px-6 py-12">
50
+ {/* Header */}
51
+ <div className="text-center mb-12">
52
+ <h1 className="text-4xl font-bold text-gray-900 mb-4">
53
+ 🤖 LeRobot.js
54
+ </h1>
55
+ <p className="text-xl text-gray-600 mb-8">
56
+ State-of-the-art AI for real-world robotics in JavaScript
57
+ </p>
58
+
59
+ {!isSupported && (
60
+ <Alert variant="destructive" className="max-w-2xl mx-auto mb-8">
61
+ <AlertDescription>
62
+ Web Serial API is not supported in this browser. Please use
63
+ Chrome, Edge, or another Chromium-based browser to use this
64
+ demo.
65
+ </AlertDescription>
66
+ </Alert>
67
+ )}
68
+ </div>
69
+
70
+ {/* Main Content */}
71
+ {calibratingRobot ? (
72
+ <div className="max-w-6xl mx-auto">
73
+ <div className="mb-4">
74
+ <Button
75
+ variant="outline"
76
+ onClick={() => setCalibratingRobot(null)}
77
+ >
78
+ ← Back to Dashboard
79
+ </Button>
80
+ </div>
81
+ <CalibrationPanel
82
+ robot={calibratingRobot}
83
+ onFinish={handleFinishCalibration}
84
+ />
85
+ </div>
86
+ ) : (
87
+ <div className="max-w-6xl mx-auto">
88
+ <PortManager
89
+ onCalibrate={handleCalibrate}
90
+ connectedRobots={connectedRobots}
91
+ onConnectedRobotsChange={onConnectedRobotsChange}
92
+ />
93
+ </div>
94
+ )}
95
+ </div>
96
+ </div>
97
+ );
98
+ }
src/demo/pages/Setup.tsx ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { Button } from "../components/ui/button";
3
+ import {
4
+ Card,
5
+ CardContent,
6
+ CardDescription,
7
+ CardHeader,
8
+ CardTitle,
9
+ } from "../components/ui/card";
10
+ import { Alert, AlertDescription, AlertTitle } from "../components/ui/alert";
11
+ import { Badge } from "../components/ui/badge";
12
+ import type { ConnectedRobot } from "../types";
13
+
14
+ interface SetupProps {
15
+ connectedRobots: ConnectedRobot[];
16
+ onBack: () => void;
17
+ onNext: (robot: ConnectedRobot) => void;
18
+ }
19
+
20
+ export function Setup({ connectedRobots, onBack, onNext }: SetupProps) {
21
+ const configuredRobots = connectedRobots.filter(
22
+ (r) => r.robotType && r.robotId
23
+ );
24
+
25
+ return (
26
+ <div className="container mx-auto px-4 py-8 max-w-4xl">
27
+ <div className="space-y-6">
28
+ <div className="text-center space-y-2">
29
+ <h1 className="text-3xl font-bold">Robot Setup</h1>
30
+ <p className="text-muted-foreground">
31
+ Select a connected robot to calibrate
32
+ </p>
33
+ </div>
34
+
35
+ <div className="space-y-4">
36
+ <div className="flex items-center justify-between">
37
+ <h2 className="text-xl font-semibold">Connected Robots</h2>
38
+ <Badge variant="outline">{configuredRobots.length} ready</Badge>
39
+ </div>
40
+
41
+ {configuredRobots.length === 0 ? (
42
+ <Card>
43
+ <CardContent className="text-center py-8">
44
+ <div className="text-muted-foreground space-y-2">
45
+ <p>No configured robots found.</p>
46
+ <p className="text-sm">
47
+ Go back to the home page to connect and configure your
48
+ robots.
49
+ </p>
50
+ </div>
51
+ </CardContent>
52
+ </Card>
53
+ ) : (
54
+ <div className="grid gap-4">
55
+ {configuredRobots.map((robot, index) => (
56
+ <Card
57
+ key={index}
58
+ className="cursor-pointer hover:shadow-md transition-shadow"
59
+ >
60
+ <CardHeader>
61
+ <div className="flex items-center justify-between">
62
+ <div>
63
+ <CardTitle className="text-lg">
64
+ {robot.robotId}
65
+ </CardTitle>
66
+ <CardDescription>{robot.name}</CardDescription>
67
+ </div>
68
+ <div className="flex items-center space-x-2">
69
+ <Badge
70
+ variant={robot.isConnected ? "default" : "outline"}
71
+ >
72
+ {robot.isConnected ? "Connected" : "Available"}
73
+ </Badge>
74
+ <Badge variant="outline">
75
+ {robot.robotType?.replace("_", " ")}
76
+ </Badge>
77
+ </div>
78
+ </div>
79
+ </CardHeader>
80
+ <CardContent>
81
+ <Button onClick={() => onNext(robot)} className="w-full">
82
+ Calibrate This Robot
83
+ </Button>
84
+ </CardContent>
85
+ </Card>
86
+ ))}
87
+ </div>
88
+ )}
89
+ </div>
90
+
91
+ <div className="flex justify-center">
92
+ <Button variant="outline" onClick={onBack}>
93
+ Back to Home
94
+ </Button>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ );
99
+ }
src/demo/types.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface ConnectedRobot {
2
+ port: SerialPort;
3
+ name: string;
4
+ isConnected: boolean;
5
+ robotType?: "so100_follower" | "so100_leader";
6
+ robotId?: string;
7
+ serialNumber?: string; // Unique identifier from USB device
8
+ usbMetadata?: {
9
+ vendorId: string;
10
+ productId: string;
11
+ serialNumber: string;
12
+ manufacturerName: string;
13
+ productName: string;
14
+ usbVersionMajor?: number;
15
+ usbVersionMinor?: number;
16
+ deviceClass?: number;
17
+ deviceSubclass?: number;
18
+ deviceProtocol?: number;
19
+ };
20
+ }
src/vite-env.d.ts CHANGED
@@ -1 +1,33 @@
1
  /// <reference types="vite/client" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  /// <reference types="vite/client" />
2
+
3
+ // WebUSB API type declarations
4
+ interface USBDevice {
5
+ vendorId: number;
6
+ productId: number;
7
+ serialNumber?: string;
8
+ manufacturerName?: string;
9
+ productName?: string;
10
+ usbVersionMajor: number;
11
+ usbVersionMinor: number;
12
+ deviceClass: number;
13
+ deviceSubclass: number;
14
+ deviceProtocol: number;
15
+ }
16
+
17
+ interface USBDeviceFilter {
18
+ vendorId?: number;
19
+ productId?: number;
20
+ }
21
+
22
+ interface USBDeviceRequestOptions {
23
+ filters: USBDeviceFilter[];
24
+ }
25
+
26
+ interface USB {
27
+ getDevices(): Promise<USBDevice[]>;
28
+ requestDevice(options: USBDeviceRequestOptions): Promise<USBDevice>;
29
+ }
30
+
31
+ interface Navigator {
32
+ usb: USB;
33
+ }
tailwind.config.js ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: ["./index.html", "./src/demo/**/*.{js,ts,jsx,tsx}"],
4
+ theme: {
5
+ extend: {
6
+ borderRadius: {
7
+ lg: "var(--radius)",
8
+ md: "calc(var(--radius) - 2px)",
9
+ sm: "calc(var(--radius) - 4px)",
10
+ },
11
+ colors: {
12
+ background: "hsl(var(--background))",
13
+ foreground: "hsl(var(--foreground))",
14
+ card: {
15
+ DEFAULT: "hsl(var(--card))",
16
+ foreground: "hsl(var(--foreground))",
17
+ },
18
+ popover: {
19
+ DEFAULT: "hsl(var(--popover))",
20
+ foreground: "hsl(var(--popover-foreground))",
21
+ },
22
+ primary: {
23
+ DEFAULT: "hsl(var(--primary))",
24
+ foreground: "hsl(var(--primary-foreground))",
25
+ },
26
+ secondary: {
27
+ DEFAULT: "hsl(var(--secondary))",
28
+ foreground: "hsl(var(--secondary-foreground))",
29
+ },
30
+ muted: {
31
+ DEFAULT: "hsl(var(--muted))",
32
+ foreground: "hsl(var(--muted-foreground))",
33
+ },
34
+ accent: {
35
+ DEFAULT: "hsl(var(--accent))",
36
+ foreground: "hsl(var(--accent-foreground))",
37
+ },
38
+ destructive: {
39
+ DEFAULT: "hsl(var(--destructive))",
40
+ foreground: "hsl(var(--destructive-foreground))",
41
+ },
42
+ border: "hsl(var(--border))",
43
+ input: "hsl(var(--input))",
44
+ ring: "hsl(var(--ring))",
45
+ chart: {
46
+ 1: "hsl(var(--chart-1))",
47
+ 2: "hsl(var(--chart-2))",
48
+ 3: "hsl(var(--chart-3))",
49
+ 4: "hsl(var(--chart-4))",
50
+ 5: "hsl(var(--chart-5))",
51
+ },
52
+ },
53
+ },
54
+ },
55
+ plugins: [],
56
+ };
vanilla.html ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>🤖 lerobot.js - Vanilla Demo</title>
7
+ <meta
8
+ name="description"
9
+ content="State-of-the-art AI for real-world robotics in JavaScript/TypeScript"
10
+ />
11
+ <style>
12
+ /* Prevent flash of unstyled content */
13
+ body {
14
+ opacity: 0;
15
+ }
16
+ body.loaded {
17
+ opacity: 1;
18
+ transition: opacity 0.3s ease;
19
+ }
20
+ </style>
21
+ </head>
22
+ <body>
23
+ <div id="app"></div>
24
+ <script type="module" src="/src/main.ts"></script>
25
+ <script>
26
+ // Add loaded class when page is ready
27
+ window.addEventListener("load", () => {
28
+ document.body.classList.add("loaded");
29
+ });
30
+ </script>
31
+ </body>
32
+ </html>
vite.config.ts ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+ import { resolve } from "path";
4
+
5
+ export default defineConfig(({ mode }) => {
6
+ const baseConfig = {
7
+ plugins: [],
8
+ resolve: {
9
+ alias: {
10
+ "@": resolve(__dirname, "./src"),
11
+ },
12
+ },
13
+ };
14
+
15
+ if (mode === "demo") {
16
+ // React demo mode - includes React, Tailwind, shadcn/ui
17
+ return {
18
+ ...baseConfig,
19
+ plugins: [react()],
20
+ css: {
21
+ postcss: "./postcss.config.mjs",
22
+ },
23
+ build: {
24
+ outDir: "dist/demo",
25
+ rollupOptions: {
26
+ input: {
27
+ main: resolve(__dirname, "index.html"),
28
+ },
29
+ },
30
+ },
31
+ };
32
+ }
33
+
34
+ if (mode === "vanilla") {
35
+ // Vanilla mode - current implementation without React
36
+ return {
37
+ ...baseConfig,
38
+ build: {
39
+ outDir: "dist/vanilla",
40
+ rollupOptions: {
41
+ input: {
42
+ main: resolve(__dirname, "vanilla.html"),
43
+ },
44
+ },
45
+ },
46
+ };
47
+ }
48
+
49
+ if (mode === "lib") {
50
+ // Library mode - core library without any demo UI
51
+ return {
52
+ ...baseConfig,
53
+ build: {
54
+ lib: {
55
+ entry: resolve(__dirname, "src/main.ts"),
56
+ name: "LeRobot",
57
+ fileName: "lerobot",
58
+ },
59
+ rollupOptions: {
60
+ external: ["serialport", "react", "react-dom"],
61
+ output: {
62
+ globals: {
63
+ serialport: "SerialPort",
64
+ react: "React",
65
+ "react-dom": "ReactDOM",
66
+ },
67
+ },
68
+ },
69
+ },
70
+ };
71
+ }
72
+
73
+ // Default mode (fallback to demo)
74
+ return defineConfig({ mode: "demo" });
75
+ });