Spaces:
Running
Running
feat: added cyberpunk example
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- examples/cyberpunk-standalone/.gitignore +24 -0
- examples/cyberpunk-standalone/README.md +69 -0
- examples/cyberpunk-standalone/eslint.config.js +23 -0
- examples/cyberpunk-standalone/index.html +36 -0
- examples/cyberpunk-standalone/package.json +78 -0
- examples/cyberpunk-standalone/pnpm-lock.yaml +0 -0
- examples/cyberpunk-standalone/postcss.config.mjs +8 -0
- examples/cyberpunk-standalone/public/fonts/Geist-Bold.woff2 +0 -0
- examples/cyberpunk-standalone/public/fonts/Geist-Regular.woff2 +0 -0
- examples/cyberpunk-standalone/public/fonts/Geist-SemiBold.woff2 +0 -0
- examples/cyberpunk-standalone/public/fonts/GeistMono-Bold.woff2 +0 -0
- examples/cyberpunk-standalone/public/fonts/GeistMono-Regular.woff2 +0 -0
- examples/cyberpunk-standalone/public/fonts/GeistMono-SemiBold.woff2 +0 -0
- examples/cyberpunk-standalone/public/partabot-logo.png +0 -0
- examples/cyberpunk-standalone/src/App.css +42 -0
- examples/cyberpunk-standalone/src/App.tsx +222 -0
- examples/cyberpunk-standalone/src/components/VirtualKey.tsx +27 -0
- examples/cyberpunk-standalone/src/components/boot-sequence.tsx +61 -0
- examples/cyberpunk-standalone/src/components/calibration-view.tsx +108 -0
- examples/cyberpunk-standalone/src/components/device-dashboard.tsx +174 -0
- examples/cyberpunk-standalone/src/components/docs-section.tsx +150 -0
- examples/cyberpunk-standalone/src/components/edit-robot-dialog.tsx +78 -0
- examples/cyberpunk-standalone/src/components/footer.tsx +41 -0
- examples/cyberpunk-standalone/src/components/grid-background.tsx +22 -0
- examples/cyberpunk-standalone/src/components/hardware-support-section.tsx +140 -0
- examples/cyberpunk-standalone/src/components/header.tsx +27 -0
- examples/cyberpunk-standalone/src/components/hud-corners.tsx +46 -0
- examples/cyberpunk-standalone/src/components/motor-calibration-visual.tsx +32 -0
- examples/cyberpunk-standalone/src/components/roadmap-section.tsx +179 -0
- examples/cyberpunk-standalone/src/components/setup-cards.tsx +160 -0
- examples/cyberpunk-standalone/src/components/teleoperation-view.tsx +240 -0
- examples/cyberpunk-standalone/src/components/theme-provider.tsx +8 -0
- examples/cyberpunk-standalone/src/components/theme-toggle.tsx +39 -0
- examples/cyberpunk-standalone/src/components/ui/accordion.tsx +58 -0
- examples/cyberpunk-standalone/src/components/ui/alert-dialog.tsx +141 -0
- examples/cyberpunk-standalone/src/components/ui/alert.tsx +59 -0
- examples/cyberpunk-standalone/src/components/ui/aspect-ratio.tsx +7 -0
- examples/cyberpunk-standalone/src/components/ui/avatar.tsx +50 -0
- examples/cyberpunk-standalone/src/components/ui/badge.tsx +36 -0
- examples/cyberpunk-standalone/src/components/ui/breadcrumb.tsx +115 -0
- examples/cyberpunk-standalone/src/components/ui/button.tsx +56 -0
- examples/cyberpunk-standalone/src/components/ui/calendar.tsx +66 -0
- examples/cyberpunk-standalone/src/components/ui/card.tsx +48 -0
- examples/cyberpunk-standalone/src/components/ui/carousel.tsx +262 -0
- examples/cyberpunk-standalone/src/components/ui/chart.tsx +365 -0
- examples/cyberpunk-standalone/src/components/ui/checkbox.tsx +30 -0
- examples/cyberpunk-standalone/src/components/ui/collapsible.tsx +11 -0
- examples/cyberpunk-standalone/src/components/ui/command.tsx +153 -0
- examples/cyberpunk-standalone/src/components/ui/context-menu.tsx +200 -0
- examples/cyberpunk-standalone/src/components/ui/dialog.tsx +122 -0
examples/cyberpunk-standalone/.gitignore
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Logs
|
2 |
+
logs
|
3 |
+
*.log
|
4 |
+
npm-debug.log*
|
5 |
+
yarn-debug.log*
|
6 |
+
yarn-error.log*
|
7 |
+
pnpm-debug.log*
|
8 |
+
lerna-debug.log*
|
9 |
+
|
10 |
+
node_modules
|
11 |
+
dist
|
12 |
+
dist-ssr
|
13 |
+
*.local
|
14 |
+
|
15 |
+
# Editor directories and files
|
16 |
+
.vscode/*
|
17 |
+
!.vscode/extensions.json
|
18 |
+
.idea
|
19 |
+
.DS_Store
|
20 |
+
*.suo
|
21 |
+
*.ntvs*
|
22 |
+
*.njsproj
|
23 |
+
*.sln
|
24 |
+
*.sw?
|
examples/cyberpunk-standalone/README.md
ADDED
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# React + TypeScript + Vite
|
2 |
+
|
3 |
+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
4 |
+
|
5 |
+
Currently, two official plugins are available:
|
6 |
+
|
7 |
+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
|
8 |
+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
9 |
+
|
10 |
+
## Expanding the ESLint configuration
|
11 |
+
|
12 |
+
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
13 |
+
|
14 |
+
```js
|
15 |
+
export default tseslint.config([
|
16 |
+
globalIgnores(['dist']),
|
17 |
+
{
|
18 |
+
files: ['**/*.{ts,tsx}'],
|
19 |
+
extends: [
|
20 |
+
// Other configs...
|
21 |
+
|
22 |
+
// Remove tseslint.configs.recommended and replace with this
|
23 |
+
...tseslint.configs.recommendedTypeChecked,
|
24 |
+
// Alternatively, use this for stricter rules
|
25 |
+
...tseslint.configs.strictTypeChecked,
|
26 |
+
// Optionally, add this for stylistic rules
|
27 |
+
...tseslint.configs.stylisticTypeChecked,
|
28 |
+
|
29 |
+
// Other configs...
|
30 |
+
],
|
31 |
+
languageOptions: {
|
32 |
+
parserOptions: {
|
33 |
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
34 |
+
tsconfigRootDir: import.meta.dirname,
|
35 |
+
},
|
36 |
+
// other options...
|
37 |
+
},
|
38 |
+
},
|
39 |
+
])
|
40 |
+
```
|
41 |
+
|
42 |
+
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
43 |
+
|
44 |
+
```js
|
45 |
+
// eslint.config.js
|
46 |
+
import reactX from 'eslint-plugin-react-x'
|
47 |
+
import reactDom from 'eslint-plugin-react-dom'
|
48 |
+
|
49 |
+
export default tseslint.config([
|
50 |
+
globalIgnores(['dist']),
|
51 |
+
{
|
52 |
+
files: ['**/*.{ts,tsx}'],
|
53 |
+
extends: [
|
54 |
+
// Other configs...
|
55 |
+
// Enable lint rules for React
|
56 |
+
reactX.configs['recommended-typescript'],
|
57 |
+
// Enable lint rules for React DOM
|
58 |
+
reactDom.configs.recommended,
|
59 |
+
],
|
60 |
+
languageOptions: {
|
61 |
+
parserOptions: {
|
62 |
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
63 |
+
tsconfigRootDir: import.meta.dirname,
|
64 |
+
},
|
65 |
+
// other options...
|
66 |
+
},
|
67 |
+
},
|
68 |
+
])
|
69 |
+
```
|
examples/cyberpunk-standalone/eslint.config.js
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import js from '@eslint/js'
|
2 |
+
import globals from 'globals'
|
3 |
+
import reactHooks from 'eslint-plugin-react-hooks'
|
4 |
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
5 |
+
import tseslint from 'typescript-eslint'
|
6 |
+
import { globalIgnores } from 'eslint/config'
|
7 |
+
|
8 |
+
export default tseslint.config([
|
9 |
+
globalIgnores(['dist']),
|
10 |
+
{
|
11 |
+
files: ['**/*.{ts,tsx}'],
|
12 |
+
extends: [
|
13 |
+
js.configs.recommended,
|
14 |
+
tseslint.configs.recommended,
|
15 |
+
reactHooks.configs['recommended-latest'],
|
16 |
+
reactRefresh.configs.vite,
|
17 |
+
],
|
18 |
+
languageOptions: {
|
19 |
+
ecmaVersion: 2020,
|
20 |
+
globals: globals.browser,
|
21 |
+
},
|
22 |
+
},
|
23 |
+
])
|
examples/cyberpunk-standalone/index.html
ADDED
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8" />
|
5 |
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
7 |
+
<title>Cyberpunk Standalone</title>
|
8 |
+
|
9 |
+
<!-- Preload critical fonts -->
|
10 |
+
<link
|
11 |
+
rel="preload"
|
12 |
+
href="/fonts/Geist-Regular.woff2"
|
13 |
+
as="font"
|
14 |
+
type="font/woff2"
|
15 |
+
crossorigin
|
16 |
+
/>
|
17 |
+
<link
|
18 |
+
rel="preload"
|
19 |
+
href="/fonts/Geist-SemiBold.woff2"
|
20 |
+
as="font"
|
21 |
+
type="font/woff2"
|
22 |
+
crossorigin
|
23 |
+
/>
|
24 |
+
<link
|
25 |
+
rel="preload"
|
26 |
+
href="/fonts/GeistMono-Regular.woff2"
|
27 |
+
as="font"
|
28 |
+
type="font/woff2"
|
29 |
+
crossorigin
|
30 |
+
/>
|
31 |
+
</head>
|
32 |
+
<body>
|
33 |
+
<div id="root"></div>
|
34 |
+
<script type="module" src="/src/main.tsx"></script>
|
35 |
+
</body>
|
36 |
+
</html>
|
examples/cyberpunk-standalone/package.json
ADDED
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "cyberpunk-standalone",
|
3 |
+
"private": true,
|
4 |
+
"version": "0.0.0",
|
5 |
+
"type": "module",
|
6 |
+
"scripts": {
|
7 |
+
"dev": "vite",
|
8 |
+
"build": "tsc -b && vite build",
|
9 |
+
"lint": "eslint .",
|
10 |
+
"preview": "vite preview"
|
11 |
+
},
|
12 |
+
"dependencies": {
|
13 |
+
"@hookform/resolvers": "^3.9.1",
|
14 |
+
"@radix-ui/react-accordion": "1.2.2",
|
15 |
+
"@radix-ui/react-alert-dialog": "1.1.4",
|
16 |
+
"@radix-ui/react-aspect-ratio": "1.1.1",
|
17 |
+
"@radix-ui/react-avatar": "1.1.2",
|
18 |
+
"@radix-ui/react-checkbox": "1.1.3",
|
19 |
+
"@radix-ui/react-collapsible": "1.1.2",
|
20 |
+
"@radix-ui/react-context-menu": "2.2.4",
|
21 |
+
"@radix-ui/react-dialog": "1.1.4",
|
22 |
+
"@radix-ui/react-dropdown-menu": "2.1.4",
|
23 |
+
"@radix-ui/react-hover-card": "1.1.4",
|
24 |
+
"@radix-ui/react-label": "2.1.1",
|
25 |
+
"@radix-ui/react-menubar": "1.1.4",
|
26 |
+
"@radix-ui/react-navigation-menu": "1.2.3",
|
27 |
+
"@radix-ui/react-popover": "1.1.4",
|
28 |
+
"@radix-ui/react-progress": "1.1.1",
|
29 |
+
"@radix-ui/react-radio-group": "1.2.2",
|
30 |
+
"@radix-ui/react-scroll-area": "1.2.2",
|
31 |
+
"@radix-ui/react-select": "2.1.4",
|
32 |
+
"@radix-ui/react-separator": "1.1.1",
|
33 |
+
"@radix-ui/react-slider": "latest",
|
34 |
+
"@radix-ui/react-slot": "1.1.1",
|
35 |
+
"@radix-ui/react-switch": "1.1.2",
|
36 |
+
"@radix-ui/react-tabs": "1.1.2",
|
37 |
+
"@radix-ui/react-toast": "1.2.4",
|
38 |
+
"@radix-ui/react-toggle": "1.1.1",
|
39 |
+
"@radix-ui/react-toggle-group": "1.1.1",
|
40 |
+
"@radix-ui/react-tooltip": "1.1.6",
|
41 |
+
"autoprefixer": "^10.4.20",
|
42 |
+
"class-variance-authority": "^0.7.1",
|
43 |
+
"clsx": "^2.1.1",
|
44 |
+
"cmdk": "1.0.4",
|
45 |
+
"date-fns": "4.1.0",
|
46 |
+
"embla-carousel-react": "8.5.1",
|
47 |
+
"input-otp": "1.4.1",
|
48 |
+
"lucide-react": "^0.454.0",
|
49 |
+
"next-themes": "^0.4.6",
|
50 |
+
"react": "^19",
|
51 |
+
"react-day-picker": "8.10.1",
|
52 |
+
"react-dom": "^19",
|
53 |
+
"react-hook-form": "^7.54.1",
|
54 |
+
"react-resizable-panels": "^2.1.7",
|
55 |
+
"recharts": "2.15.0",
|
56 |
+
"sonner": "^1.7.1",
|
57 |
+
"tailwind-merge": "^2.5.5",
|
58 |
+
"tailwindcss-animate": "^1.0.7",
|
59 |
+
"vaul": "^0.9.6",
|
60 |
+
"zod": "^3.24.1"
|
61 |
+
},
|
62 |
+
"devDependencies": {
|
63 |
+
"@eslint/js": "^9.29.0",
|
64 |
+
"@types/node": "^22",
|
65 |
+
"@types/react": "^19.1.8",
|
66 |
+
"@types/react-dom": "^19.1.6",
|
67 |
+
"@vitejs/plugin-react": "^4.5.2",
|
68 |
+
"eslint": "^9.29.0",
|
69 |
+
"eslint-plugin-react-hooks": "^5.2.0",
|
70 |
+
"eslint-plugin-react-refresh": "^0.4.20",
|
71 |
+
"globals": "^16.2.0",
|
72 |
+
"postcss": "^8.5",
|
73 |
+
"tailwindcss": "^3.4.17",
|
74 |
+
"typescript": "^5",
|
75 |
+
"typescript-eslint": "^8.34.1",
|
76 |
+
"vite": "^7.0.0"
|
77 |
+
}
|
78 |
+
}
|
examples/cyberpunk-standalone/pnpm-lock.yaml
ADDED
The diff for this file is too large to render.
See raw diff
|
|
examples/cyberpunk-standalone/postcss.config.mjs
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/** @type {import('postcss-load-config').Config} */
|
2 |
+
const config = {
|
3 |
+
plugins: {
|
4 |
+
tailwindcss: {},
|
5 |
+
},
|
6 |
+
};
|
7 |
+
|
8 |
+
export default config;
|
examples/cyberpunk-standalone/public/fonts/Geist-Bold.woff2
ADDED
Binary file (42.3 kB). View file
|
|
examples/cyberpunk-standalone/public/fonts/Geist-Regular.woff2
ADDED
Binary file (41.2 kB). View file
|
|
examples/cyberpunk-standalone/public/fonts/Geist-SemiBold.woff2
ADDED
Binary file (42 kB). View file
|
|
examples/cyberpunk-standalone/public/fonts/GeistMono-Bold.woff2
ADDED
Binary file (48 kB). View file
|
|
examples/cyberpunk-standalone/public/fonts/GeistMono-Regular.woff2
ADDED
Binary file (47 kB). View file
|
|
examples/cyberpunk-standalone/public/fonts/GeistMono-SemiBold.woff2
ADDED
Binary file (48 kB). View file
|
|
examples/cyberpunk-standalone/public/partabot-logo.png
ADDED
![]() |
examples/cyberpunk-standalone/src/App.css
ADDED
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#root {
|
2 |
+
max-width: 1280px;
|
3 |
+
margin: 0 auto;
|
4 |
+
padding: 2rem;
|
5 |
+
text-align: center;
|
6 |
+
}
|
7 |
+
|
8 |
+
.logo {
|
9 |
+
height: 6em;
|
10 |
+
padding: 1.5em;
|
11 |
+
will-change: filter;
|
12 |
+
transition: filter 300ms;
|
13 |
+
}
|
14 |
+
.logo:hover {
|
15 |
+
filter: drop-shadow(0 0 2em #646cffaa);
|
16 |
+
}
|
17 |
+
.logo.react:hover {
|
18 |
+
filter: drop-shadow(0 0 2em #61dafbaa);
|
19 |
+
}
|
20 |
+
|
21 |
+
@keyframes logo-spin {
|
22 |
+
from {
|
23 |
+
transform: rotate(0deg);
|
24 |
+
}
|
25 |
+
to {
|
26 |
+
transform: rotate(360deg);
|
27 |
+
}
|
28 |
+
}
|
29 |
+
|
30 |
+
@media (prefers-reduced-motion: no-preference) {
|
31 |
+
a:nth-of-type(2) .logo {
|
32 |
+
animation: logo-spin infinite 20s linear;
|
33 |
+
}
|
34 |
+
}
|
35 |
+
|
36 |
+
.card {
|
37 |
+
padding: 2em;
|
38 |
+
}
|
39 |
+
|
40 |
+
.read-the-docs {
|
41 |
+
color: #888;
|
42 |
+
}
|
examples/cyberpunk-standalone/src/App.tsx
ADDED
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useState, useEffect, useRef } from "react";
|
2 |
+
import { ChevronLeft } from "lucide-react";
|
3 |
+
import { Header } from "@/components/header";
|
4 |
+
import { Footer } from "@/components/footer";
|
5 |
+
import { EditRobotDialog } from "@/components/edit-robot-dialog";
|
6 |
+
import { DeviceDashboard } from "@/components/device-dashboard";
|
7 |
+
import { CalibrationView } from "@/components/calibration-view";
|
8 |
+
import { TeleoperationView } from "@/components/teleoperation-view";
|
9 |
+
import { SetupCards } from "@/components/setup-cards";
|
10 |
+
import { DocsSection } from "@/components/docs-section";
|
11 |
+
import { RoadmapSection } from "@/components/roadmap-section";
|
12 |
+
import { HardwareSupportSection } from "@/components/hardware-support-section";
|
13 |
+
import { lerobot } from "@/lib/mock-api";
|
14 |
+
import { useLocalStorage } from "@/hooks/use-local-storage";
|
15 |
+
import type { RobotConnection } from "@/types/robot";
|
16 |
+
|
17 |
+
function App() {
|
18 |
+
const [view, setView] = useState<
|
19 |
+
"dashboard" | "calibrating" | "teleoperating"
|
20 |
+
>("dashboard");
|
21 |
+
const [robots, setRobots] = useLocalStorage<RobotConnection[]>(
|
22 |
+
"connected-robots",
|
23 |
+
[]
|
24 |
+
);
|
25 |
+
const [selectedRobot, setSelectedRobot] = useState<RobotConnection | null>(
|
26 |
+
null
|
27 |
+
);
|
28 |
+
const [editingRobot, setEditingRobot] = useState<RobotConnection | null>(
|
29 |
+
null
|
30 |
+
);
|
31 |
+
const [logs, setLogs] = useState<string[]>([]);
|
32 |
+
const [isConnecting, setIsConnecting] = useState(false);
|
33 |
+
const hardwareSectionRef = useRef<HTMLDivElement>(null);
|
34 |
+
|
35 |
+
useEffect(() => {
|
36 |
+
const loadSavedRobots = async () => {
|
37 |
+
const robotConfigs = robots.map(({ port, ...config }) => config);
|
38 |
+
if (robotConfigs.length > 0) {
|
39 |
+
const findPortProcess = await lerobot.findPort({
|
40 |
+
robotConfigs,
|
41 |
+
onMessage: (msg) => setLogs((prev) => [...prev, msg]),
|
42 |
+
});
|
43 |
+
const reconnectedRobots = await findPortProcess.result;
|
44 |
+
setRobots(reconnectedRobots);
|
45 |
+
}
|
46 |
+
setIsConnecting(false);
|
47 |
+
};
|
48 |
+
loadSavedRobots();
|
49 |
+
}, []);
|
50 |
+
|
51 |
+
const handleFindNewRobots = async () => {
|
52 |
+
setIsConnecting(true);
|
53 |
+
const findPortProcess = await lerobot.findPort({
|
54 |
+
onMessage: (msg) => setLogs((prev) => [...prev, msg]),
|
55 |
+
});
|
56 |
+
const newRobots = await findPortProcess.result;
|
57 |
+
setRobots((prev) => {
|
58 |
+
const existingIds = new Set(prev.map((r) => r.robotId));
|
59 |
+
const uniqueNewRobots = newRobots.filter(
|
60 |
+
(r) => !existingIds.has(r.robotId)
|
61 |
+
);
|
62 |
+
return [...prev, ...uniqueNewRobots];
|
63 |
+
});
|
64 |
+
if (newRobots.length > 0) {
|
65 |
+
setEditingRobot(newRobots[0]);
|
66 |
+
}
|
67 |
+
setIsConnecting(false);
|
68 |
+
};
|
69 |
+
|
70 |
+
const handleUpdateRobot = (updatedRobot: RobotConnection) => {
|
71 |
+
setRobots((prev) =>
|
72 |
+
prev.map((r) => (r.robotId === updatedRobot.robotId ? updatedRobot : r))
|
73 |
+
);
|
74 |
+
setEditingRobot(null);
|
75 |
+
};
|
76 |
+
|
77 |
+
const handleRemoveRobot = (robotId: string) => {
|
78 |
+
setRobots((prev) => prev.filter((r) => r.robotId !== robotId));
|
79 |
+
};
|
80 |
+
|
81 |
+
const handleCalibrate = (robot: RobotConnection) => {
|
82 |
+
setSelectedRobot(robot);
|
83 |
+
setView("calibrating");
|
84 |
+
};
|
85 |
+
|
86 |
+
const handleTeleoperate = (robot: RobotConnection) => {
|
87 |
+
setSelectedRobot(robot);
|
88 |
+
setView("teleoperating");
|
89 |
+
};
|
90 |
+
|
91 |
+
const handleCloseSubView = () => {
|
92 |
+
setSelectedRobot(null);
|
93 |
+
setView("dashboard");
|
94 |
+
};
|
95 |
+
|
96 |
+
const scrollToHardware = () => {
|
97 |
+
hardwareSectionRef.current?.scrollIntoView({ behavior: "smooth" });
|
98 |
+
};
|
99 |
+
|
100 |
+
const renderView = () => {
|
101 |
+
switch (view) {
|
102 |
+
case "calibrating":
|
103 |
+
return selectedRobot && <CalibrationView robot={selectedRobot} />;
|
104 |
+
case "teleoperating":
|
105 |
+
return selectedRobot && <TeleoperationView robot={selectedRobot} />;
|
106 |
+
case "dashboard":
|
107 |
+
default:
|
108 |
+
return (
|
109 |
+
<div className="space-y-12">
|
110 |
+
<DeviceDashboard
|
111 |
+
robots={robots}
|
112 |
+
onCalibrate={handleCalibrate}
|
113 |
+
onTeleoperate={handleTeleoperate}
|
114 |
+
onRemove={handleRemoveRobot}
|
115 |
+
onEdit={setEditingRobot}
|
116 |
+
onFindNew={handleFindNewRobots}
|
117 |
+
isConnecting={isConnecting}
|
118 |
+
onScrollToHardware={scrollToHardware}
|
119 |
+
/>
|
120 |
+
<div>
|
121 |
+
<div className="mb-6">
|
122 |
+
<h2 className="text-2xl font-bold text-primary font-mono tracking-wider mb-2 uppercase">
|
123 |
+
install
|
124 |
+
</h2>
|
125 |
+
<p className="text-sm text-muted-foreground font-mono">
|
126 |
+
Choose your preferred development environment
|
127 |
+
</p>
|
128 |
+
</div>
|
129 |
+
<SetupCards />
|
130 |
+
</div>
|
131 |
+
<DocsSection />
|
132 |
+
<RoadmapSection />
|
133 |
+
<div ref={hardwareSectionRef}>
|
134 |
+
<HardwareSupportSection />
|
135 |
+
</div>
|
136 |
+
</div>
|
137 |
+
);
|
138 |
+
}
|
139 |
+
};
|
140 |
+
|
141 |
+
const PageHeader = () => {
|
142 |
+
let title = "DASHBOARD";
|
143 |
+
if (view === "calibrating" && selectedRobot) {
|
144 |
+
title = `CALIBRATE: ${selectedRobot.name.toUpperCase()}`;
|
145 |
+
} else if (view === "teleoperating" && selectedRobot) {
|
146 |
+
title = `TELEOPERATE: ${selectedRobot.name.toUpperCase()}`;
|
147 |
+
}
|
148 |
+
|
149 |
+
return (
|
150 |
+
<div className="flex items-center justify-between mb-12">
|
151 |
+
<div className="flex items-center gap-4">
|
152 |
+
<div>
|
153 |
+
{view === "calibrating" && selectedRobot ? (
|
154 |
+
<h1 className="font-mono text-4xl font-bold tracking-wider">
|
155 |
+
<span className="text-muted-foreground uppercase">
|
156 |
+
calibrate:
|
157 |
+
</span>{" "}
|
158 |
+
<span
|
159 |
+
className="text-primary text-glitch uppercase"
|
160 |
+
data-text={selectedRobot.name}
|
161 |
+
>
|
162 |
+
{selectedRobot.name.toUpperCase()}
|
163 |
+
</span>
|
164 |
+
</h1>
|
165 |
+
) : view === "teleoperating" && selectedRobot ? (
|
166 |
+
<h1 className="font-mono text-4xl font-bold tracking-wider">
|
167 |
+
<span className="text-muted-foreground uppercase">
|
168 |
+
teleoperate:
|
169 |
+
</span>{" "}
|
170 |
+
<span
|
171 |
+
className="text-primary text-glitch uppercase"
|
172 |
+
data-text={selectedRobot.name}
|
173 |
+
>
|
174 |
+
{selectedRobot.name.toUpperCase()}
|
175 |
+
</span>
|
176 |
+
</h1>
|
177 |
+
) : (
|
178 |
+
<h1
|
179 |
+
className="font-mono text-4xl font-bold text-primary tracking-wider text-glitch uppercase"
|
180 |
+
data-text="dashboard"
|
181 |
+
>
|
182 |
+
DASHBOARD
|
183 |
+
</h1>
|
184 |
+
)}
|
185 |
+
<div className="h-6 flex items-center">
|
186 |
+
{view !== "dashboard" ? (
|
187 |
+
<button
|
188 |
+
onClick={handleCloseSubView}
|
189 |
+
className="flex items-center gap-2 text-sm text-muted-foreground font-mono hover:text-primary transition-colors"
|
190 |
+
>
|
191 |
+
<ChevronLeft className="w-4 h-4" />
|
192 |
+
<span className="uppercase">back to dashboard</span>
|
193 |
+
</button>
|
194 |
+
) : (
|
195 |
+
<p className="text-sm text-muted-foreground font-mono">{""} </p>
|
196 |
+
)}
|
197 |
+
</div>
|
198 |
+
</div>
|
199 |
+
</div>
|
200 |
+
</div>
|
201 |
+
);
|
202 |
+
};
|
203 |
+
|
204 |
+
return (
|
205 |
+
<div className="flex flex-col min-h-screen font-sans scanline-overlay">
|
206 |
+
<Header />
|
207 |
+
<main className="flex-grow container mx-auto py-12 px-4 md:px-6">
|
208 |
+
<PageHeader />
|
209 |
+
{renderView()}
|
210 |
+
<EditRobotDialog
|
211 |
+
robot={editingRobot}
|
212 |
+
isOpen={!!editingRobot}
|
213 |
+
onOpenChange={(open) => !open && setEditingRobot(null)}
|
214 |
+
onSave={handleUpdateRobot}
|
215 |
+
/>
|
216 |
+
</main>
|
217 |
+
<Footer />
|
218 |
+
</div>
|
219 |
+
);
|
220 |
+
}
|
221 |
+
|
222 |
+
export default App;
|
examples/cyberpunk-standalone/src/components/VirtualKey.tsx
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { cn } from "@/lib/utils"
|
2 |
+
|
3 |
+
interface VirtualKeyProps {
|
4 |
+
label: string
|
5 |
+
subLabel?: string
|
6 |
+
isPressed?: boolean
|
7 |
+
}
|
8 |
+
|
9 |
+
const VirtualKey = ({ label, subLabel, isPressed }: VirtualKeyProps) => {
|
10 |
+
return (
|
11 |
+
<div className="flex flex-col items-center">
|
12 |
+
<div
|
13 |
+
className={cn(
|
14 |
+
"w-12 h-12 border rounded-md flex items-center justify-center font-bold transition-all duration-100",
|
15 |
+
isPressed
|
16 |
+
? "bg-primary text-primary-foreground scale-110 border-primary"
|
17 |
+
: "bg-black/30 text-muted-foreground border-white/10",
|
18 |
+
)}
|
19 |
+
>
|
20 |
+
{label}
|
21 |
+
</div>
|
22 |
+
{subLabel && <span className="text-xs text-muted-foreground mt-1 font-mono">{subLabel}</span>}
|
23 |
+
</div>
|
24 |
+
)
|
25 |
+
}
|
26 |
+
|
27 |
+
export default VirtualKey
|
examples/cyberpunk-standalone/src/components/boot-sequence.tsx
ADDED
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { useState, useEffect } from "react"
|
4 |
+
import { cn } from "@/lib/utils"
|
5 |
+
|
6 |
+
const bootLines = [
|
7 |
+
"INITIALIZING BIOS...",
|
8 |
+
"CHECKING MEMORY: 1024MB OK",
|
9 |
+
"DETECTING PRIMARY BUS...",
|
10 |
+
"LEROBOT.JS CORE v1.0.2",
|
11 |
+
"LOADING OPERATOR CONSOLE...",
|
12 |
+
"CONNECTION PROTOCOL: WEBSERIAL",
|
13 |
+
"ENCRYPTION: AES-256",
|
14 |
+
"SYSTEM STATUS: NOMINAL",
|
15 |
+
"BOOT SEQUENCE COMPLETE.",
|
16 |
+
]
|
17 |
+
|
18 |
+
export function BootSequence({ onComplete }: { onComplete: () => void }) {
|
19 |
+
const [visibleLines, setVisibleLines] = useState<string[]>([])
|
20 |
+
const [show, setShow] = useState(true)
|
21 |
+
|
22 |
+
useEffect(() => {
|
23 |
+
const timeouts: NodeJS.Timeout[] = []
|
24 |
+
bootLines.forEach((line, index) => {
|
25 |
+
timeouts.push(
|
26 |
+
setTimeout(() => {
|
27 |
+
setVisibleLines((prev) => [...prev, line])
|
28 |
+
}, index * 150),
|
29 |
+
)
|
30 |
+
})
|
31 |
+
|
32 |
+
timeouts.push(
|
33 |
+
setTimeout(
|
34 |
+
() => {
|
35 |
+
setShow(false)
|
36 |
+
setTimeout(onComplete, 500) // Wait for fade out
|
37 |
+
},
|
38 |
+
bootLines.length * 150 + 500,
|
39 |
+
),
|
40 |
+
)
|
41 |
+
|
42 |
+
return () => timeouts.forEach(clearTimeout)
|
43 |
+
}, [onComplete])
|
44 |
+
|
45 |
+
return (
|
46 |
+
<div
|
47 |
+
className={cn(
|
48 |
+
"fixed inset-0 z-50 flex items-center justify-center bg-black transition-opacity duration-500",
|
49 |
+
show ? "opacity-100" : "opacity-0 pointer-events-none",
|
50 |
+
)}
|
51 |
+
>
|
52 |
+
<div className="font-mono text-primary text-sm md:text-base p-4">
|
53 |
+
{visibleLines.map((line, index) => (
|
54 |
+
<p key={index} className="text-glitch" data-text={line}>
|
55 |
+
> {line}
|
56 |
+
</p>
|
57 |
+
))}
|
58 |
+
</div>
|
59 |
+
</div>
|
60 |
+
)
|
61 |
+
}
|
examples/cyberpunk-standalone/src/components/calibration-view.tsx
ADDED
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
import { useState, useMemo } from "react"
|
3 |
+
import { Download } from "lucide-react"
|
4 |
+
import { Button } from "@/components/ui/button"
|
5 |
+
import { Card } from "@/components/ui/card"
|
6 |
+
import { lerobot } from "@/lib/mock-api"
|
7 |
+
import { useLocalStorage } from "@/hooks/use-local-storage"
|
8 |
+
import { MotorCalibrationVisual } from "@/components/motor-calibration-visual"
|
9 |
+
import type { RobotConnection, LiveCalibrationData, WebCalibrationResults } from "@/types/robot"
|
10 |
+
|
11 |
+
interface CalibrationViewProps {
|
12 |
+
robot: RobotConnection
|
13 |
+
}
|
14 |
+
|
15 |
+
export function CalibrationView({ robot }: CalibrationViewProps) {
|
16 |
+
const [status, setStatus] = useState("Ready to calibrate.")
|
17 |
+
const [liveData, setLiveData] = useState<LiveCalibrationData | null>(null)
|
18 |
+
const [isCalibrating, setIsCalibrating] = useState(false)
|
19 |
+
const [calibrationProcess, setCalibrationProcess] = useState<{
|
20 |
+
stop: () => void
|
21 |
+
result: Promise<WebCalibrationResults>
|
22 |
+
} | null>(null)
|
23 |
+
const [calibrationResults, setCalibrationResults] = useLocalStorage<WebCalibrationResults | null>(
|
24 |
+
`calibration-${robot.robotId}`,
|
25 |
+
null,
|
26 |
+
)
|
27 |
+
|
28 |
+
const handleStart = async () => {
|
29 |
+
setIsCalibrating(true)
|
30 |
+
const process = await lerobot.calibrate(robot, {
|
31 |
+
onLiveUpdate: setLiveData,
|
32 |
+
onProgress: setStatus,
|
33 |
+
})
|
34 |
+
setCalibrationProcess(process)
|
35 |
+
}
|
36 |
+
|
37 |
+
const handleFinish = async () => {
|
38 |
+
if (calibrationProcess) {
|
39 |
+
calibrationProcess.stop()
|
40 |
+
const results = await calibrationProcess.result
|
41 |
+
setCalibrationResults(results)
|
42 |
+
setIsCalibrating(false)
|
43 |
+
setCalibrationProcess(null)
|
44 |
+
}
|
45 |
+
}
|
46 |
+
|
47 |
+
const downloadJson = () => {
|
48 |
+
if (!calibrationResults) return
|
49 |
+
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(calibrationResults, null, 2))
|
50 |
+
const downloadAnchorNode = document.createElement("a")
|
51 |
+
downloadAnchorNode.setAttribute("href", dataStr)
|
52 |
+
downloadAnchorNode.setAttribute("download", `${robot.robotId}_calibration.json`)
|
53 |
+
document.body.appendChild(downloadAnchorNode)
|
54 |
+
downloadAnchorNode.click()
|
55 |
+
downloadAnchorNode.remove()
|
56 |
+
}
|
57 |
+
|
58 |
+
const motorData = useMemo(
|
59 |
+
() =>
|
60 |
+
liveData
|
61 |
+
? Object.entries(liveData)
|
62 |
+
: lerobot.MOCK_MOTOR_NAMES.map((name) => [name, { current: 0, min: 4095, max: 0, range: 0 }]),
|
63 |
+
[liveData],
|
64 |
+
)
|
65 |
+
|
66 |
+
return (
|
67 |
+
<Card className="border-0 rounded-none">
|
68 |
+
<div className="p-4 border-b border-white/10 flex items-center justify-between">
|
69 |
+
<div className="flex items-center gap-4">
|
70 |
+
<div className="w-1 h-8 bg-primary"></div>
|
71 |
+
<div>
|
72 |
+
<h3 className="text-xl font-bold text-foreground font-mono tracking-wider uppercase">motor calibration</h3>
|
73 |
+
<p className="text-sm text-muted-foreground font-mono">move all joints to their limits</p>
|
74 |
+
</div>
|
75 |
+
</div>
|
76 |
+
<div className="flex gap-4">
|
77 |
+
{!isCalibrating ? (
|
78 |
+
<Button onClick={handleStart} size="lg">
|
79 |
+
Start Calibration
|
80 |
+
</Button>
|
81 |
+
) : (
|
82 |
+
<Button onClick={handleFinish} variant="destructive" size="lg">
|
83 |
+
Finish Recording
|
84 |
+
</Button>
|
85 |
+
)}
|
86 |
+
<Button onClick={downloadJson} variant="outline" size="lg" disabled={!calibrationResults}>
|
87 |
+
<Download className="w-4 h-4 mr-2" /> Download JSON
|
88 |
+
</Button>
|
89 |
+
</div>
|
90 |
+
</div>
|
91 |
+
<div className="pt-6 p-6">
|
92 |
+
<div className="flex items-center gap-4 py-2 px-4 text-sm font-sans text-muted-foreground">
|
93 |
+
<div className="w-40">Motor Name</div>
|
94 |
+
<div className="flex-1">Visual Range</div>
|
95 |
+
<div className="w-16 text-right">Current</div>
|
96 |
+
<div className="w-16 text-right">Min</div>
|
97 |
+
<div className="w-16 text-right">Max</div>
|
98 |
+
<div className="w-16 text-right">Range</div>
|
99 |
+
</div>
|
100 |
+
<div className="border-t border-white/10">
|
101 |
+
{motorData.map(([name, data]) => (
|
102 |
+
<MotorCalibrationVisual key={name} name={name} data={data} />
|
103 |
+
))}
|
104 |
+
</div>
|
105 |
+
</div>
|
106 |
+
</Card>
|
107 |
+
)
|
108 |
+
}
|
examples/cyberpunk-standalone/src/components/device-dashboard.tsx
ADDED
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { Settings, Gamepad2, Trash2, Pencil, Plus, ExternalLink } from "lucide-react"
|
4 |
+
import { Button } from "@/components/ui/button"
|
5 |
+
import { Card, CardFooter } from "@/components/ui/card"
|
6 |
+
import { Badge } from "@/components/ui/badge"
|
7 |
+
import { cn } from "@/lib/utils"
|
8 |
+
import HudCorners from "@/components/hud-corners"
|
9 |
+
import type { RobotConnection } from "@/types/robot"
|
10 |
+
|
11 |
+
interface DeviceDashboardProps {
|
12 |
+
robots: RobotConnection[]
|
13 |
+
onCalibrate: (robot: RobotConnection) => void
|
14 |
+
onTeleoperate: (robot: RobotConnection) => void
|
15 |
+
onRemove: (robotId: string) => void
|
16 |
+
onEdit: (robot: RobotConnection) => void
|
17 |
+
onFindNew: () => void
|
18 |
+
isConnecting: boolean
|
19 |
+
onScrollToHardware: () => void
|
20 |
+
}
|
21 |
+
|
22 |
+
export function DeviceDashboard({
|
23 |
+
robots,
|
24 |
+
onCalibrate,
|
25 |
+
onTeleoperate,
|
26 |
+
onRemove,
|
27 |
+
onEdit,
|
28 |
+
onFindNew,
|
29 |
+
isConnecting,
|
30 |
+
onScrollToHardware,
|
31 |
+
}: DeviceDashboardProps) {
|
32 |
+
return (
|
33 |
+
<Card className="border-0 rounded-none">
|
34 |
+
<div className="p-4 border-b border-white/10 flex items-center justify-between">
|
35 |
+
<div className="flex items-center gap-4">
|
36 |
+
<div className="w-1 h-8 bg-primary"></div>
|
37 |
+
<div>
|
38 |
+
<h3 className="text-xl font-bold text-foreground font-mono tracking-wider uppercase">device registry</h3>
|
39 |
+
<div className="flex items-center gap-2 mt-1">
|
40 |
+
<span className="text-xs text-muted-foreground font-mono">currently supports SO-100 </span>
|
41 |
+
<button
|
42 |
+
onClick={onScrollToHardware}
|
43 |
+
className="text-xs text-primary hover:text-accent transition-colors underline font-mono flex items-center gap-1"
|
44 |
+
>
|
45 |
+
<ExternalLink className="w-3 h-3" />
|
46 |
+
add more devices
|
47 |
+
</button>
|
48 |
+
</div>
|
49 |
+
</div>
|
50 |
+
</div>
|
51 |
+
{robots.length > 0 && (
|
52 |
+
<Button onClick={onFindNew} disabled={isConnecting} size="lg" className="font-mono uppercase">
|
53 |
+
<Plus className="w-4 h-4 mr-2" />
|
54 |
+
add unit
|
55 |
+
</Button>
|
56 |
+
)}
|
57 |
+
</div>
|
58 |
+
|
59 |
+
<div className="pt-6 p-6">
|
60 |
+
{robots.length === 0 ? (
|
61 |
+
<div className="relative">
|
62 |
+
<HudCorners className="p-16">
|
63 |
+
<div className="text-center font-mono">
|
64 |
+
<div className="mb-6">
|
65 |
+
{isConnecting ? (
|
66 |
+
<>
|
67 |
+
<div className="w-16 h-16 mx-auto mb-4 border-2 border-primary/50 rounded-lg flex items-center justify-center animate-pulse">
|
68 |
+
<Plus className="w-8 h-8 text-primary animate-spin" />
|
69 |
+
</div>
|
70 |
+
<h4 className="text-xl text-primary mb-2 tracking-wider uppercase">scanning for units</h4>
|
71 |
+
<p className="text-sm text-muted-foreground mb-8">searching for available devices...</p>
|
72 |
+
</>
|
73 |
+
) : (
|
74 |
+
<>
|
75 |
+
<div className="w-16 h-16 mx-auto mb-4 border-2 border-dashed border-primary/50 rounded-lg flex items-center justify-center">
|
76 |
+
<Plus className="w-8 h-8 text-primary/50" />
|
77 |
+
</div>
|
78 |
+
<h4 className="text-xl text-primary mb-2 tracking-wider uppercase">no units detected</h4>
|
79 |
+
|
80 |
+
<Button onClick={onFindNew} size="lg" className="font-mono uppercase">
|
81 |
+
<Plus className="w-4 h-4 mr-2" />
|
82 |
+
add unit
|
83 |
+
</Button>
|
84 |
+
</>
|
85 |
+
)}
|
86 |
+
</div>
|
87 |
+
</div>
|
88 |
+
</HudCorners>
|
89 |
+
</div>
|
90 |
+
) : (
|
91 |
+
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
92 |
+
{robots.map((robot) => (
|
93 |
+
<HudCorners key={robot.robotId}>
|
94 |
+
<Card className="flex flex-col h-full">
|
95 |
+
<div className="p-4 border-b border-white/10">
|
96 |
+
<div className="flex items-start justify-between mb-3">
|
97 |
+
<div className="flex-1">
|
98 |
+
<h4 className="text-xl font-bold text-primary font-mono tracking-wider">{robot.name}</h4>
|
99 |
+
<div className="flex items-center gap-2 mt-1">
|
100 |
+
<span className="text-xs text-muted-foreground font-mono">{robot.serialNumber}</span>
|
101 |
+
<span className="text-xs text-muted-foreground">•</span>
|
102 |
+
<span className="text-xs font-mono uppercase text-muted-foreground">{robot.robotType}</span>
|
103 |
+
</div>
|
104 |
+
</div>
|
105 |
+
<Badge
|
106 |
+
variant="outline"
|
107 |
+
className={cn(
|
108 |
+
"border-primary/50 bg-primary/20 text-primary font-mono text-xs",
|
109 |
+
robot.isConnected && "animate-pulse-slow",
|
110 |
+
)}
|
111 |
+
>
|
112 |
+
{robot.isConnected ? "ONLINE" : "OFFLINE"}
|
113 |
+
</Badge>
|
114 |
+
</div>
|
115 |
+
</div>
|
116 |
+
|
117 |
+
<div className="flex-grow p-4">
|
118 |
+
<div className="grid grid-cols-2 gap-4 text-xs font-mono">
|
119 |
+
<div>
|
120 |
+
<span className="text-muted-foreground uppercase">status:</span>
|
121 |
+
<div className="text-primary uppercase">
|
122 |
+
{robot.isConnected ? "operational" : "disconnected"}
|
123 |
+
</div>
|
124 |
+
</div>
|
125 |
+
<div>
|
126 |
+
<span className="text-muted-foreground uppercase">type:</span>
|
127 |
+
<div className="text-accent uppercase">{robot.robotType}</div>
|
128 |
+
</div>
|
129 |
+
</div>
|
130 |
+
</div>
|
131 |
+
|
132 |
+
<CardFooter className="p-4 border-t border-white/10 flex flex-wrap gap-2">
|
133 |
+
<Button
|
134 |
+
variant="outline"
|
135 |
+
size="sm"
|
136 |
+
onClick={() => onEdit(robot)}
|
137 |
+
className="font-mono text-xs uppercase"
|
138 |
+
>
|
139 |
+
<Pencil className="w-3 h-3 mr-1" /> edit
|
140 |
+
</Button>
|
141 |
+
<Button
|
142 |
+
variant="outline"
|
143 |
+
size="sm"
|
144 |
+
onClick={() => onCalibrate(robot)}
|
145 |
+
className="font-mono text-xs uppercase"
|
146 |
+
>
|
147 |
+
<Settings className="w-3 h-3 mr-1" /> calibrate
|
148 |
+
</Button>
|
149 |
+
<Button
|
150 |
+
variant="outline"
|
151 |
+
size="sm"
|
152 |
+
onClick={() => onTeleoperate(robot)}
|
153 |
+
className="font-mono text-xs uppercase"
|
154 |
+
>
|
155 |
+
<Gamepad2 className="w-3 h-3 mr-1" /> control
|
156 |
+
</Button>
|
157 |
+
<Button
|
158 |
+
variant="destructive"
|
159 |
+
size="sm"
|
160 |
+
onClick={() => onRemove(robot.robotId)}
|
161 |
+
className="font-mono text-xs uppercase"
|
162 |
+
>
|
163 |
+
<Trash2 className="w-3 h-3 mr-1" /> remove
|
164 |
+
</Button>
|
165 |
+
</CardFooter>
|
166 |
+
</Card>
|
167 |
+
</HudCorners>
|
168 |
+
))}
|
169 |
+
</div>
|
170 |
+
)}
|
171 |
+
</div>
|
172 |
+
</Card>
|
173 |
+
)
|
174 |
+
}
|
examples/cyberpunk-standalone/src/components/docs-section.tsx
ADDED
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import type React from "react"
|
4 |
+
import { Book, Code2, Terminal, Copy, Check } from "lucide-react"
|
5 |
+
import { Button } from "@/components/ui/button"
|
6 |
+
import { useState } from "react"
|
7 |
+
|
8 |
+
const CodeBlock = ({ children }: { children: React.ReactNode }) => {
|
9 |
+
const [copied, setCopied] = useState(false)
|
10 |
+
|
11 |
+
const copyToClipboard = async (text: string) => {
|
12 |
+
try {
|
13 |
+
await navigator.clipboard.writeText(text)
|
14 |
+
setCopied(true)
|
15 |
+
setTimeout(() => setCopied(false), 2000)
|
16 |
+
} catch (err) {
|
17 |
+
console.error("Failed to copy:", err)
|
18 |
+
}
|
19 |
+
}
|
20 |
+
|
21 |
+
const codeText = typeof children === "string" ? children : children?.toString() || ""
|
22 |
+
|
23 |
+
return (
|
24 |
+
<div className="bg-muted/50 dark:bg-black/40 border border-border dark:border-white/10 rounded-md overflow-hidden my-4 relative">
|
25 |
+
<pre className="p-4 text-sm overflow-x-auto">
|
26 |
+
<code>{children}</code>
|
27 |
+
</pre>
|
28 |
+
<Button
|
29 |
+
variant="outline"
|
30 |
+
size="sm"
|
31 |
+
onClick={() => copyToClipboard(codeText)}
|
32 |
+
className="absolute top-2 right-2 h-7 w-7 p-0 transition-all"
|
33 |
+
>
|
34 |
+
{copied ? <Check className="w-3 h-3" /> : <Copy className="w-3 h-3" />}
|
35 |
+
</Button>
|
36 |
+
</div>
|
37 |
+
)
|
38 |
+
}
|
39 |
+
|
40 |
+
export function DocsSection() {
|
41 |
+
return (
|
42 |
+
<div className="font-mono">
|
43 |
+
<div className="mb-6">
|
44 |
+
<h2 className="text-2xl font-bold text-primary tracking-wider mb-2 uppercase flex items-center gap-3">
|
45 |
+
<Book className="w-6 h-6" />
|
46 |
+
Docs
|
47 |
+
</h2>
|
48 |
+
<p className="text-sm text-muted-foreground">A quick guide to get started with the @lerobot/web API.</p>
|
49 |
+
</div>
|
50 |
+
|
51 |
+
<div className="bg-muted/40 dark:bg-black/30 border-l-4 border-cyan-500 dark:border-accent-cyan p-6 md:p-8 rounded-r-lg space-y-8">
|
52 |
+
{/* Getting Started */}
|
53 |
+
<div>
|
54 |
+
<h3 className="text-xl font-bold text-cyan-600 dark:text-accent-cyan tracking-wider uppercase flex items-center gap-2">
|
55 |
+
<Terminal className="w-5 h-5" />
|
56 |
+
Getting Started
|
57 |
+
</h3>
|
58 |
+
<p className="text-muted-foreground text-sm mt-2 mb-4">
|
59 |
+
First, import the API from the library. Then you can call its methods to find and control your robot.
|
60 |
+
</p>
|
61 |
+
<CodeBlock>
|
62 |
+
{`import { lerobot } from '@lerobot/web';
|
63 |
+
|
64 |
+
async function connectToRobot() {
|
65 |
+
const { result } = await lerobot.findPort({
|
66 |
+
onMessage: (message) => console.log('Status:', message),
|
67 |
+
});
|
68 |
+
|
69 |
+
const robots = await result;
|
70 |
+
console.log('Found robots:', robots);
|
71 |
+
}`}
|
72 |
+
</CodeBlock>
|
73 |
+
</div>
|
74 |
+
|
75 |
+
{/* API Reference */}
|
76 |
+
<div>
|
77 |
+
<h3 className="text-xl font-bold text-cyan-600 dark:text-accent-cyan tracking-wider uppercase flex items-center gap-2">
|
78 |
+
<Code2 className="w-5 h-5" />
|
79 |
+
API Reference
|
80 |
+
</h3>
|
81 |
+
<div className="space-y-6 mt-4">
|
82 |
+
{/* findPort */}
|
83 |
+
<div>
|
84 |
+
<h4 className="font-bold text-primary">lerobot.findPort(options)</h4>
|
85 |
+
<p className="text-muted-foreground text-sm mt-1">Scans for available robot connections via WebSerial.</p>
|
86 |
+
<CodeBlock>
|
87 |
+
{`// Options
|
88 |
+
{
|
89 |
+
robotConfigs?: RobotConfig[], // Optional: Saved configs to try reconnecting
|
90 |
+
onMessage?: (msg: string) => void // Callback for status updates
|
91 |
+
}
|
92 |
+
|
93 |
+
// Returns
|
94 |
+
{
|
95 |
+
result: Promise<RobotConnection[]> // A promise that resolves with found robots
|
96 |
+
}`}
|
97 |
+
</CodeBlock>
|
98 |
+
</div>
|
99 |
+
|
100 |
+
{/* calibrate */}
|
101 |
+
<div>
|
102 |
+
<h4 className="font-bold text-primary">lerobot.calibrate(robot, options)</h4>
|
103 |
+
<p className="text-muted-foreground text-sm mt-1">Starts the calibration process for a specific robot.</p>
|
104 |
+
<CodeBlock>
|
105 |
+
{`// Parameters
|
106 |
+
robot: RobotConnection // The robot instance to calibrate
|
107 |
+
|
108 |
+
// Options
|
109 |
+
{
|
110 |
+
onLiveUpdate: (data: LiveCalibrationData) => void, // Callback for real-time joint data
|
111 |
+
onProgress: (msg: string) => void // Callback for status messages
|
112 |
+
}
|
113 |
+
|
114 |
+
// Returns
|
115 |
+
{
|
116 |
+
result: Promise<WebCalibrationResults>, // A promise that resolves with final calibration
|
117 |
+
stop: () => void // Function to stop the calibration recording
|
118 |
+
}`}
|
119 |
+
</CodeBlock>
|
120 |
+
</div>
|
121 |
+
|
122 |
+
{/* teleoperate */}
|
123 |
+
<div>
|
124 |
+
<h4 className="font-bold text-primary">lerobot.teleoperate(robot, options)</h4>
|
125 |
+
<p className="text-muted-foreground text-sm mt-1">Initializes the teleoperation interface for a robot.</p>
|
126 |
+
<CodeBlock>
|
127 |
+
{`// Parameters
|
128 |
+
robot: RobotConnection,
|
129 |
+
calibrationData: WebCalibrationResults
|
130 |
+
|
131 |
+
// Options
|
132 |
+
{
|
133 |
+
onStateUpdate: (state: TeleoperationState) => void // Callback for real-time state updates
|
134 |
+
}
|
135 |
+
|
136 |
+
// Returns
|
137 |
+
{
|
138 |
+
start: () => void,
|
139 |
+
stop: () => void,
|
140 |
+
updateKeyState: (key: string, pressed: boolean) => void,
|
141 |
+
moveMotor: (motorName: string, position: number) => void
|
142 |
+
}`}
|
143 |
+
</CodeBlock>
|
144 |
+
</div>
|
145 |
+
</div>
|
146 |
+
</div>
|
147 |
+
</div>
|
148 |
+
</div>
|
149 |
+
)
|
150 |
+
}
|
examples/cyberpunk-standalone/src/components/edit-robot-dialog.tsx
ADDED
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
import { useState, useEffect } from "react"
|
3 |
+
import { Button } from "@/components/ui/button"
|
4 |
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose } from "@/components/ui/dialog"
|
5 |
+
import { Input } from "@/components/ui/input"
|
6 |
+
import { Label } from "@/components/ui/label"
|
7 |
+
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
8 |
+
import type { RobotConnection } from "@/types/robot"
|
9 |
+
|
10 |
+
interface EditRobotDialogProps {
|
11 |
+
robot: RobotConnection | null
|
12 |
+
isOpen: boolean
|
13 |
+
onOpenChange: (open: boolean) => void
|
14 |
+
onSave: (updatedRobot: RobotConnection) => void
|
15 |
+
}
|
16 |
+
|
17 |
+
export function EditRobotDialog({ robot, isOpen, onOpenChange, onSave }: EditRobotDialogProps) {
|
18 |
+
const [name, setName] = useState("")
|
19 |
+
const [type, setType] = useState<"so100_follower" | "so100_leader">("so100_follower")
|
20 |
+
|
21 |
+
useEffect(() => {
|
22 |
+
if (robot) {
|
23 |
+
setName(robot.name)
|
24 |
+
setType(robot.robotType || "so100_follower")
|
25 |
+
}
|
26 |
+
}, [robot])
|
27 |
+
|
28 |
+
const handleSave = () => {
|
29 |
+
if (robot) {
|
30 |
+
onSave({ ...robot, name, robotType: type })
|
31 |
+
}
|
32 |
+
}
|
33 |
+
|
34 |
+
if (!robot) return null
|
35 |
+
|
36 |
+
return (
|
37 |
+
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
38 |
+
<DialogContent className="font-sans bg-background/80 backdrop-blur-sm border-primary/20">
|
39 |
+
<DialogHeader>
|
40 |
+
<DialogTitle>Configure Robot</DialogTitle>
|
41 |
+
</DialogHeader>
|
42 |
+
<div className="grid gap-4 py-4">
|
43 |
+
<div className="grid grid-cols-4 items-center gap-4">
|
44 |
+
<Label htmlFor="name" className="text-right">
|
45 |
+
Name
|
46 |
+
</Label>
|
47 |
+
<Input id="name" value={name} onChange={(e) => setName(e.target.value)} className="col-span-3 font-mono" />
|
48 |
+
</div>
|
49 |
+
<div className="grid grid-cols-4 items-center gap-4">
|
50 |
+
<Label className="text-right">Type</Label>
|
51 |
+
<RadioGroup value={type} onValueChange={(value) => setType(value as any)} className="col-span-3">
|
52 |
+
<div className="flex items-center space-x-2">
|
53 |
+
<RadioGroupItem value="so100_follower" id="r1" />
|
54 |
+
<Label htmlFor="r1">SO-100 Follower</Label>
|
55 |
+
</div>
|
56 |
+
<div className="flex items-center space-x-2">
|
57 |
+
<RadioGroupItem value="so100_leader" id="r2" />
|
58 |
+
<Label htmlFor="r2">SO-100 Leader</Label>
|
59 |
+
</div>
|
60 |
+
</RadioGroup>
|
61 |
+
</div>
|
62 |
+
</div>
|
63 |
+
<DialogFooter>
|
64 |
+
<DialogClose asChild>
|
65 |
+
<Button type="button" variant="secondary">
|
66 |
+
Cancel
|
67 |
+
</Button>
|
68 |
+
</DialogClose>
|
69 |
+
<DialogClose asChild>
|
70 |
+
<Button type="submit" onClick={handleSave}>
|
71 |
+
Save Changes
|
72 |
+
</Button>
|
73 |
+
</DialogClose>
|
74 |
+
</DialogFooter>
|
75 |
+
</DialogContent>
|
76 |
+
</Dialog>
|
77 |
+
)
|
78 |
+
}
|
examples/cyberpunk-standalone/src/components/footer.tsx
ADDED
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Github } from "lucide-react"
|
2 |
+
|
3 |
+
export function Footer() {
|
4 |
+
return (
|
5 |
+
<footer className="w-full border-t">
|
6 |
+
<div className="container mx-auto flex h-16 items-center justify-center px-4 md:px-6">
|
7 |
+
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
8 |
+
<span>
|
9 |
+
Created with ❤️ by{" "}
|
10 |
+
<a
|
11 |
+
href="https://github.com/timpietrusky"
|
12 |
+
target="_blank"
|
13 |
+
rel="noopener noreferrer"
|
14 |
+
className="text-primary hover:text-accent transition-colors underline"
|
15 |
+
>
|
16 |
+
Tim Pietrusky
|
17 |
+
</a>{" "}
|
18 |
+
under{" "}
|
19 |
+
<a
|
20 |
+
href="https://github.com/timpietrusky/lerobot.js/blob/main/LICENSE"
|
21 |
+
target="_blank"
|
22 |
+
rel="noopener noreferrer"
|
23 |
+
className="text-primary hover:text-accent transition-colors underline"
|
24 |
+
>
|
25 |
+
Apache 2.0
|
26 |
+
</a>
|
27 |
+
</span>
|
28 |
+
<a
|
29 |
+
href="https://github.com/timpietrusky/lerobot.js"
|
30 |
+
target="_blank"
|
31 |
+
rel="noopener noreferrer"
|
32 |
+
className="flex items-center gap-1 hover:text-foreground transition-colors"
|
33 |
+
>
|
34 |
+
<Github className="h-4 w-4" />
|
35 |
+
GitHub
|
36 |
+
</a>
|
37 |
+
</div>
|
38 |
+
</div>
|
39 |
+
</footer>
|
40 |
+
)
|
41 |
+
}
|
examples/cyberpunk-standalone/src/components/grid-background.tsx
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import type React from "react"
|
4 |
+
|
5 |
+
import { useEffect } from "react"
|
6 |
+
|
7 |
+
export function GridBackground({ children }: { children: React.ReactNode }) {
|
8 |
+
useEffect(() => {
|
9 |
+
const handleMouseMove = (e: MouseEvent) => {
|
10 |
+
document.body.style.setProperty("--mouse-x", `${e.clientX}px`)
|
11 |
+
document.body.style.setProperty("--mouse-y", `${e.clientY}px`)
|
12 |
+
}
|
13 |
+
|
14 |
+
window.addEventListener("mousemove", handleMouseMove)
|
15 |
+
|
16 |
+
return () => {
|
17 |
+
window.removeEventListener("mousemove", handleMouseMove)
|
18 |
+
}
|
19 |
+
}, [])
|
20 |
+
|
21 |
+
return <>{children}</>
|
22 |
+
}
|
examples/cyberpunk-standalone/src/components/hardware-support-section.tsx
ADDED
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client";
|
2 |
+
|
3 |
+
import { Cpu, Mail, Plus, ExternalLink } from "lucide-react";
|
4 |
+
import { Button } from "@/components/ui/button";
|
5 |
+
import HudCorners from "@/components/hud-corners";
|
6 |
+
|
7 |
+
const supportedHardware = [
|
8 |
+
{
|
9 |
+
name: "SO-100",
|
10 |
+
status: "supported",
|
11 |
+
description:
|
12 |
+
"6-DOF robotic arm with gripper, leader/follower configuration",
|
13 |
+
},
|
14 |
+
];
|
15 |
+
|
16 |
+
export function HardwareSupportSection() {
|
17 |
+
return (
|
18 |
+
<div className="font-mono">
|
19 |
+
<div className="mb-6">
|
20 |
+
<h2 className="text-2xl font-bold text-primary tracking-wider mb-2 uppercase flex items-center gap-3">
|
21 |
+
<Cpu className="w-6 h-6" />
|
22 |
+
Hardware Support
|
23 |
+
</h2>
|
24 |
+
<p className="text-sm text-muted-foreground">
|
25 |
+
{"making sure that robotic ai is accessible to everyone"}
|
26 |
+
</p>
|
27 |
+
</div>
|
28 |
+
|
29 |
+
<div className="space-y-8">
|
30 |
+
{/* Currently Supported */}
|
31 |
+
<HudCorners color="primary" size="sm" layer="front">
|
32 |
+
<div className="bg-muted/40 dark:bg-black/30 border border-primary/30 p-6 rounded-lg">
|
33 |
+
<h3 className="text-xl font-bold text-cyan-600 dark:text-accent-cyan tracking-wider uppercase mb-4 flex items-center gap-2">
|
34 |
+
<Cpu className="w-5 h-5" />
|
35 |
+
Currently Supported
|
36 |
+
</h3>
|
37 |
+
|
38 |
+
<div className="space-y-4">
|
39 |
+
{supportedHardware.map((robot, index) => (
|
40 |
+
<div
|
41 |
+
key={index}
|
42 |
+
className="flex items-center justify-between p-4 bg-muted/50 dark:bg-black/20 rounded-lg border border-border dark:border-white/10"
|
43 |
+
>
|
44 |
+
<div className="flex-1">
|
45 |
+
<div className="flex items-center gap-3 mb-2">
|
46 |
+
<h4 className="font-bold text-primary">{robot.name}</h4>
|
47 |
+
</div>
|
48 |
+
<p className="text-muted-foreground text-xs">
|
49 |
+
{robot.description}
|
50 |
+
</p>
|
51 |
+
</div>
|
52 |
+
|
53 |
+
{/* Sponsored By Section */}
|
54 |
+
<div className="flex flex-col items-end gap-2 ml-6">
|
55 |
+
<span className="text-xs text-muted-foreground uppercase tracking-wider">
|
56 |
+
Sponsored by
|
57 |
+
</span>
|
58 |
+
<a
|
59 |
+
href="https://partabot.com"
|
60 |
+
target="_blank"
|
61 |
+
rel="noopener noreferrer"
|
62 |
+
className="inline-block hover:opacity-80 transition-opacity"
|
63 |
+
>
|
64 |
+
<img
|
65 |
+
src="partabot-logo.png"
|
66 |
+
alt="Partabot.com"
|
67 |
+
width="100"
|
68 |
+
height="32"
|
69 |
+
className="h-6 w-auto invert dark:invert-0"
|
70 |
+
/>
|
71 |
+
</a>
|
72 |
+
</div>
|
73 |
+
</div>
|
74 |
+
))}
|
75 |
+
</div>
|
76 |
+
</div>
|
77 |
+
</HudCorners>
|
78 |
+
|
79 |
+
{/* Add New Hardware */}
|
80 |
+
<HudCorners color="whiteMuted" size="sm" layer="front">
|
81 |
+
<div className="bg-muted/40 dark:bg-black/30 border border-muted/30 p-6 rounded-lg">
|
82 |
+
<h3 className="text-xl font-bold text-cyan-600 dark:text-accent-cyan tracking-wider uppercase mb-4 flex items-center gap-2">
|
83 |
+
<Plus className="w-5 h-5" />
|
84 |
+
Add New Hardware Support
|
85 |
+
</h3>
|
86 |
+
|
87 |
+
<div className="space-y-6">
|
88 |
+
<div>
|
89 |
+
<p className="text-muted-foreground mb-4">
|
90 |
+
please provide us with access to different robot hardware, so
|
91 |
+
we can add them to lerobot.js
|
92 |
+
</p>
|
93 |
+
|
94 |
+
<div className="bg-muted/60 dark:bg-black/40 border border-border dark:border-white/10 rounded-lg p-4 mb-4">
|
95 |
+
<h4 className="font-bold text-primary mb-2">
|
96 |
+
How to contribute hardware support:
|
97 |
+
</h4>
|
98 |
+
<ul className="text-foreground/80 dark:text-muted-foreground text-sm space-y-1 list-disc list-inside">
|
99 |
+
<li>Loan or donate hardware for development</li>
|
100 |
+
<li>Provide technical documentation and APIs</li>
|
101 |
+
<li>Contribute code for new robot integrations</li>
|
102 |
+
<li>Help with testing and validation</li>
|
103 |
+
</ul>
|
104 |
+
</div>
|
105 |
+
|
106 |
+
<div className="flex flex-col sm:flex-row gap-4">
|
107 |
+
<Button
|
108 |
+
className="font-mono uppercase flex items-center gap-2"
|
109 |
+
onClick={() =>
|
110 |
+
window.open(
|
111 |
+
"mailto:[email protected]?subject=LeRobot.js Hardware Support",
|
112 |
+
"_blank"
|
113 |
+
)
|
114 |
+
}
|
115 |
+
>
|
116 |
+
<Mail className="w-4 h-4" />
|
117 |
+
Contact Tim
|
118 |
+
</Button>
|
119 |
+
<Button
|
120 |
+
variant="outline"
|
121 |
+
className="font-mono uppercase flex items-center gap-2 bg-transparent"
|
122 |
+
onClick={() =>
|
123 |
+
window.open(
|
124 |
+
"https://github.com/timpietrusky/lerobot.js",
|
125 |
+
"_blank"
|
126 |
+
)
|
127 |
+
}
|
128 |
+
>
|
129 |
+
<ExternalLink className="w-4 h-4" />
|
130 |
+
GitHub
|
131 |
+
</Button>
|
132 |
+
</div>
|
133 |
+
</div>
|
134 |
+
</div>
|
135 |
+
</div>
|
136 |
+
</HudCorners>
|
137 |
+
</div>
|
138 |
+
</div>
|
139 |
+
);
|
140 |
+
}
|
examples/cyberpunk-standalone/src/components/header.tsx
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ThemeToggle } from "./theme-toggle"
|
2 |
+
|
3 |
+
export function Header() {
|
4 |
+
return (
|
5 |
+
<header className="w-full border-b">
|
6 |
+
<div className="container mx-auto flex h-16 items-center justify-between px-4 md:px-6">
|
7 |
+
<div className="flex items-center gap-6">
|
8 |
+
<h1 className="text-2xl font-bold">LeRobot.js</h1>
|
9 |
+
<div className="flex items-center gap-2 text-primary font-mono text-sm">
|
10 |
+
<span className="text-xs text-muted-foreground font-mono">
|
11 |
+
interact with your robot in JS, inspired by{" "}
|
12 |
+
<a
|
13 |
+
href="https://huggingface.co/docs/lerobot"
|
14 |
+
target="_blank"
|
15 |
+
rel="noopener noreferrer"
|
16 |
+
className="text-primary hover:text-accent transition-colors underline"
|
17 |
+
>
|
18 |
+
LeRobot
|
19 |
+
</a>
|
20 |
+
</span>
|
21 |
+
</div>
|
22 |
+
</div>
|
23 |
+
<ThemeToggle />
|
24 |
+
</div>
|
25 |
+
</header>
|
26 |
+
)
|
27 |
+
}
|
examples/cyberpunk-standalone/src/components/hud-corners.tsx
ADDED
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type React from "react"
|
2 |
+
import { cn } from "@/lib/utils"
|
3 |
+
|
4 |
+
const HudCorners = ({
|
5 |
+
children,
|
6 |
+
className,
|
7 |
+
color = "primary",
|
8 |
+
size = "md",
|
9 |
+
layer = "behind",
|
10 |
+
}: {
|
11 |
+
children: React.ReactNode
|
12 |
+
className?: string
|
13 |
+
color?: "primary" | "whiteMuted"
|
14 |
+
size?: "sm" | "md" | "lg" | "xl"
|
15 |
+
layer?: "behind" | "front" | "top"
|
16 |
+
}) => {
|
17 |
+
// Define size variants for corner dimensions
|
18 |
+
const sizeClasses = {
|
19 |
+
sm: "w-4 h-4", // 16px × 16px - small corners
|
20 |
+
md: "w-6 h-6", // 24px × 24px - default
|
21 |
+
lg: "w-8 h-8", // 32px × 32px - large corners
|
22 |
+
xl: "w-12 h-12", // 48px × 48px - extra large corners
|
23 |
+
}
|
24 |
+
|
25 |
+
// Define layer/z-index variants
|
26 |
+
const layerClasses = {
|
27 |
+
behind: "z-0", // Behind content (default behavior)
|
28 |
+
front: "z-10", // Above most content
|
29 |
+
top: "z-50", // Above almost everything (modals, dropdowns, etc.)
|
30 |
+
}
|
31 |
+
|
32 |
+
const cornerClasses = `absolute ${sizeClasses[size]} ${layerClasses[layer]}`
|
33 |
+
const colorClasses = color === "primary" ? "border-primary/80" : "border-muted-foreground/30"
|
34 |
+
|
35 |
+
return (
|
36 |
+
<div className={cn("relative", className)}>
|
37 |
+
<div className={`${cornerClasses} ${colorClasses} top-0 left-0 border-t border-l`}></div>
|
38 |
+
<div className={`${cornerClasses} ${colorClasses} top-0 right-0 border-t border-r`}></div>
|
39 |
+
<div className={`${cornerClasses} ${colorClasses} bottom-0 left-0 border-b border-l`}></div>
|
40 |
+
<div className={`${cornerClasses} ${colorClasses} bottom-0 right-0 border-b border-r`}></div>
|
41 |
+
{children}
|
42 |
+
</div>
|
43 |
+
)
|
44 |
+
}
|
45 |
+
|
46 |
+
export default HudCorners
|
examples/cyberpunk-standalone/src/components/motor-calibration-visual.tsx
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
interface MotorCalibrationVisualProps {
|
2 |
+
name: string
|
3 |
+
data: { current: number; min: number; max: number; range: number }
|
4 |
+
}
|
5 |
+
|
6 |
+
export function MotorCalibrationVisual({ name, data }: MotorCalibrationVisualProps) {
|
7 |
+
const totalRange = 4095 // Standard range for many servos
|
8 |
+
const toPercent = (val: number) => (val / totalRange) * 100
|
9 |
+
|
10 |
+
const currentPos = toPercent(data.current)
|
11 |
+
const minPos = toPercent(data.min)
|
12 |
+
const maxPos = toPercent(data.max)
|
13 |
+
const rangeWidth = maxPos - minPos
|
14 |
+
|
15 |
+
return (
|
16 |
+
<div className="flex items-center gap-4 py-2">
|
17 |
+
<div className="w-40 font-mono text-muted-foreground">{name}</div>
|
18 |
+
<div className="flex-1">
|
19 |
+
<div className="h-6 bg-black/30 rounded-sm relative overflow-hidden">
|
20 |
+
{data.range > 0 && (
|
21 |
+
<div className="absolute h-full bg-primary/20" style={{ left: `${minPos}%`, width: `${rangeWidth}%` }} />
|
22 |
+
)}
|
23 |
+
<div className="absolute top-0 h-full w-1 bg-accent" style={{ left: `calc(${currentPos}% - 2px)` }} />
|
24 |
+
</div>
|
25 |
+
</div>
|
26 |
+
<div className="w-16 text-right font-mono text-lg">{data.current}</div>
|
27 |
+
<div className="w-16 text-right font-mono text-lg text-primary/80">{data.min === 4095 ? "N/A" : data.min}</div>
|
28 |
+
<div className="w-16 text-right font-mono text-lg text-primary/80">{data.max === 0 ? "N/A" : data.max}</div>
|
29 |
+
<div className="w-16 text-right font-mono text-lg text-accent">{data.range}</div>
|
30 |
+
</div>
|
31 |
+
)
|
32 |
+
}
|
examples/cyberpunk-standalone/src/components/roadmap-section.tsx
ADDED
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
import { CheckCircle, Clock, Target } from "lucide-react"
|
3 |
+
import { Badge } from "@/components/ui/badge"
|
4 |
+
import { cn } from "@/lib/utils"
|
5 |
+
|
6 |
+
interface RoadmapItem {
|
7 |
+
title: string
|
8 |
+
description: string
|
9 |
+
status: "completed" | "planned"
|
10 |
+
}
|
11 |
+
|
12 |
+
const roadmapItems: RoadmapItem[] = [
|
13 |
+
{
|
14 |
+
title: "findPort",
|
15 |
+
description: "WebSerial-based robot detection and connection management",
|
16 |
+
status: "completed",
|
17 |
+
},
|
18 |
+
{
|
19 |
+
title: "calibrate",
|
20 |
+
description: "Real-time joint calibration with visual feedback and data export",
|
21 |
+
status: "completed",
|
22 |
+
},
|
23 |
+
{
|
24 |
+
title: "teleoperate",
|
25 |
+
description: "Manual robot control with keyboard and slider inputs",
|
26 |
+
status: "completed",
|
27 |
+
},
|
28 |
+
{
|
29 |
+
title: "record",
|
30 |
+
description: "Record robot trajectories and sensor data to create datasets",
|
31 |
+
status: "planned",
|
32 |
+
},
|
33 |
+
{
|
34 |
+
title: "replay",
|
35 |
+
description: "Replay any recorded episode or episodes from existing datasets",
|
36 |
+
status: "planned",
|
37 |
+
},
|
38 |
+
{
|
39 |
+
title: "train",
|
40 |
+
description: "Run training based on a given dataset to create a policy",
|
41 |
+
status: "planned",
|
42 |
+
},
|
43 |
+
{
|
44 |
+
title: "eval",
|
45 |
+
description: "Run inference using trained policies for autonomous operation",
|
46 |
+
status: "planned",
|
47 |
+
},
|
48 |
+
]
|
49 |
+
|
50 |
+
const statusConfig = {
|
51 |
+
completed: {
|
52 |
+
icon: CheckCircle,
|
53 |
+
label: "COMPLETED",
|
54 |
+
dotColor: "bg-green-500 dark:bg-green-400",
|
55 |
+
textColor: "text-green-600 dark:text-green-400",
|
56 |
+
bgColor: "bg-green-500/10 dark:bg-green-400/5",
|
57 |
+
borderColor: "border-green-500/30 dark:border-green-400/20",
|
58 |
+
},
|
59 |
+
planned: {
|
60 |
+
icon: Clock,
|
61 |
+
label: "PLANNED",
|
62 |
+
dotColor: "bg-slate-500 dark:bg-muted-foreground",
|
63 |
+
textColor: "text-slate-600 dark:text-muted-foreground",
|
64 |
+
bgColor: "bg-slate-500/10 dark:bg-muted-foreground/5",
|
65 |
+
borderColor: "border-slate-500/30 dark:border-muted-foreground/20",
|
66 |
+
},
|
67 |
+
}
|
68 |
+
|
69 |
+
export function RoadmapSection() {
|
70 |
+
const completedCount = roadmapItems.filter((item) => item.status === "completed").length
|
71 |
+
const totalCount = roadmapItems.length
|
72 |
+
|
73 |
+
return (
|
74 |
+
<div className="font-mono">
|
75 |
+
<div className="mb-6">
|
76 |
+
<h2 className="text-2xl font-bold text-primary tracking-wider mb-2 uppercase flex items-center gap-3">
|
77 |
+
<Target className="w-6 h-6" />
|
78 |
+
Roadmap
|
79 |
+
</h2>
|
80 |
+
<p className="text-sm text-muted-foreground">lfg o7</p>
|
81 |
+
</div>
|
82 |
+
|
83 |
+
<div className="bg-gradient-to-br from-muted/60 to-muted/40 dark:from-black/40 dark:to-black/20 border border-primary/20 rounded-lg overflow-hidden">
|
84 |
+
{/* Header */}
|
85 |
+
<div className="bg-primary/30 dark:bg-primary/10 border-b border-primary/20 p-4">
|
86 |
+
<div className="flex items-center justify-between">
|
87 |
+
<div className="flex items-center gap-4">
|
88 |
+
<div className="text-primary font-bold text-lg tracking-wider">SYSTEM OBJECTIVES</div>
|
89 |
+
<div className="flex items-center gap-2">
|
90 |
+
<div className="w-2 h-2 bg-green-500 dark:bg-green-400 rounded-full animate-pulse"></div>
|
91 |
+
<span className="text-green-600 dark:text-green-400 text-sm">{completedCount} ACTIVE</span>
|
92 |
+
<div className="w-2 h-2 bg-slate-500 dark:bg-muted-foreground rounded-full"></div>
|
93 |
+
<span className="text-slate-600 dark:text-muted-foreground text-sm">
|
94 |
+
{totalCount - completedCount} QUEUED
|
95 |
+
</span>
|
96 |
+
</div>
|
97 |
+
</div>
|
98 |
+
<div className="text-xs text-muted-foreground">
|
99 |
+
PROGRESS: {Math.round((completedCount / totalCount) * 100)}%
|
100 |
+
</div>
|
101 |
+
</div>
|
102 |
+
</div>
|
103 |
+
|
104 |
+
{/* Progress Bar */}
|
105 |
+
<div className="bg-muted/50 dark:bg-black/30 p-4 border-b border-border dark:border-white/10">
|
106 |
+
<div className="w-full bg-muted/80 dark:bg-black/40 rounded-full h-2 overflow-hidden">
|
107 |
+
<div
|
108 |
+
className="h-full bg-gradient-to-r from-green-500 to-primary dark:from-green-400 dark:to-primary transition-all duration-1000 ease-out"
|
109 |
+
style={{ width: `${(completedCount / totalCount) * 100}%` }}
|
110 |
+
></div>
|
111 |
+
</div>
|
112 |
+
</div>
|
113 |
+
|
114 |
+
{/* Items List */}
|
115 |
+
<div className="p-6">
|
116 |
+
<div className="space-y-3">
|
117 |
+
{roadmapItems.map((item, index) => {
|
118 |
+
const config = statusConfig[item.status]
|
119 |
+
const StatusIcon = config.icon
|
120 |
+
|
121 |
+
return (
|
122 |
+
<div
|
123 |
+
key={index}
|
124 |
+
className={cn(
|
125 |
+
"flex items-center gap-4 p-4 rounded border transition-all hover:bg-muted/30 dark:hover:bg-white/5",
|
126 |
+
config.bgColor,
|
127 |
+
config.borderColor,
|
128 |
+
)}
|
129 |
+
>
|
130 |
+
{/* Number */}
|
131 |
+
<div className="text-muted-foreground/50 text-xs font-mono min-w-[2rem] text-right">
|
132 |
+
{String(index + 1).padStart(2, "0")}
|
133 |
+
</div>
|
134 |
+
|
135 |
+
{/* Content */}
|
136 |
+
<div className="flex-1 min-w-0">
|
137 |
+
<h4 className={cn("font-bold text-lg", config.textColor)}>{item.title}()</h4>
|
138 |
+
<p className="text-muted-foreground text-sm mt-1">{item.description}</p>
|
139 |
+
</div>
|
140 |
+
|
141 |
+
{/* Status Badge */}
|
142 |
+
<Badge
|
143 |
+
variant="outline"
|
144 |
+
className={cn(
|
145 |
+
"text-xs font-bold tracking-wider border-0 px-3 py-1 flex-shrink-0",
|
146 |
+
config.textColor,
|
147 |
+
config.bgColor,
|
148 |
+
)}
|
149 |
+
>
|
150 |
+
{config.label}
|
151 |
+
</Badge>
|
152 |
+
|
153 |
+
{/* Status Icon */}
|
154 |
+
<StatusIcon className={cn("w-4 h-4 flex-shrink-0 ml-3", config.textColor)} />
|
155 |
+
</div>
|
156 |
+
)
|
157 |
+
})}
|
158 |
+
</div>
|
159 |
+
</div>
|
160 |
+
|
161 |
+
{/* Footer */}
|
162 |
+
<div className="bg-muted/50 dark:bg-black/30 border-t border-border dark:border-white/10 p-4">
|
163 |
+
<p className="text-muted-foreground text-sm">
|
164 |
+
<span className="text-cyan-600 dark:text-accent-cyan">REFERENCE:</span> functions based on the original{" "}
|
165 |
+
<a
|
166 |
+
href="https://huggingface.co/docs/lerobot"
|
167 |
+
target="_blank"
|
168 |
+
rel="noopener noreferrer"
|
169 |
+
className="text-primary hover:text-accent transition-colors underline"
|
170 |
+
>
|
171 |
+
LeRobot
|
172 |
+
</a>{" "}
|
173 |
+
(python) and adapted for web robotic ai
|
174 |
+
</p>
|
175 |
+
</div>
|
176 |
+
</div>
|
177 |
+
</div>
|
178 |
+
)
|
179 |
+
}
|
examples/cyberpunk-standalone/src/components/setup-cards.tsx
ADDED
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
import { useState } from "react"
|
3 |
+
import { Copy, Package, Clock, Check, Terminal } from "lucide-react"
|
4 |
+
import { Button } from "@/components/ui/button"
|
5 |
+
import { Card } from "@/components/ui/card"
|
6 |
+
import { Badge } from "@/components/ui/badge"
|
7 |
+
import HudCorners from "@/components/hud-corners"
|
8 |
+
import { cn } from "@/lib/utils"
|
9 |
+
|
10 |
+
type PackageManager = "npm" | "yarn" | "pnpm" | "bun"
|
11 |
+
|
12 |
+
interface PackageInstallerProps {
|
13 |
+
packageName: string
|
14 |
+
disabled?: boolean
|
15 |
+
}
|
16 |
+
|
17 |
+
function PackageInstaller({ packageName, disabled = false }: PackageInstallerProps) {
|
18 |
+
const [selectedPM, setSelectedPM] = useState<PackageManager>("pnpm")
|
19 |
+
const [copied, setCopied] = useState(false)
|
20 |
+
|
21 |
+
const packageManagers: { value: PackageManager; label: string; command: string }[] = [
|
22 |
+
{ value: "pnpm", label: "pnpm", command: `pnpm add ${packageName}` },
|
23 |
+
{ value: "npm", label: "npm", command: `npm i ${packageName}` },
|
24 |
+
{ value: "yarn", label: "yarn", command: `yarn add ${packageName}` },
|
25 |
+
]
|
26 |
+
|
27 |
+
const copyToClipboard = async (text: string) => {
|
28 |
+
try {
|
29 |
+
await navigator.clipboard.writeText(text)
|
30 |
+
setCopied(true)
|
31 |
+
setTimeout(() => setCopied(false), 2000)
|
32 |
+
} catch (err) {
|
33 |
+
console.error("Failed to copy:", err)
|
34 |
+
}
|
35 |
+
}
|
36 |
+
|
37 |
+
const currentCommand = packageManagers.find((pm) => pm.value === selectedPM)?.command || ""
|
38 |
+
|
39 |
+
return (
|
40 |
+
<div className="max-w-md">
|
41 |
+
<div className="space-y-3">
|
42 |
+
<div className="flex gap-1 w-fit">
|
43 |
+
{packageManagers.map((pm) => (
|
44 |
+
<Button
|
45 |
+
key={pm.value}
|
46 |
+
variant="ghost"
|
47 |
+
size="sm"
|
48 |
+
onClick={() => setSelectedPM(pm.value)}
|
49 |
+
disabled={disabled}
|
50 |
+
className={cn(
|
51 |
+
"font-mono text-xs px-3 min-w-[60px] h-8 border transition-colors",
|
52 |
+
selectedPM === pm.value
|
53 |
+
? "bg-primary text-primary-foreground border-primary"
|
54 |
+
: "bg-transparent border-input hover:bg-accent hover:text-accent-foreground",
|
55 |
+
)}
|
56 |
+
>
|
57 |
+
{pm.label}
|
58 |
+
</Button>
|
59 |
+
))}
|
60 |
+
</div>
|
61 |
+
|
62 |
+
<div className="relative">
|
63 |
+
<div
|
64 |
+
className={cn(
|
65 |
+
"border rounded-md p-3 font-mono text-sm transition-colors",
|
66 |
+
disabled
|
67 |
+
? "bg-muted/60 dark:bg-black/20 border-dashed border-muted/40 dark:border-muted/20 text-muted-foreground/70 dark:text-muted-foreground/50"
|
68 |
+
: "bg-muted/60 dark:bg-black/40 border-border dark:border-white/10 text-foreground dark:text-primary",
|
69 |
+
)}
|
70 |
+
>
|
71 |
+
{currentCommand}
|
72 |
+
</div>
|
73 |
+
<Button
|
74 |
+
variant="outline"
|
75 |
+
size="sm"
|
76 |
+
onClick={() => copyToClipboard(currentCommand)}
|
77 |
+
disabled={disabled}
|
78 |
+
className="absolute right-2 top-2 h-7 w-7 p-0 transition-all"
|
79 |
+
>
|
80 |
+
{disabled ? (
|
81 |
+
<Clock className="w-3 h-3" />
|
82 |
+
) : copied ? (
|
83 |
+
<Check className="w-3 h-3" />
|
84 |
+
) : (
|
85 |
+
<Copy className="w-3 h-3" />
|
86 |
+
)}
|
87 |
+
</Button>
|
88 |
+
</div>
|
89 |
+
</div>
|
90 |
+
</div>
|
91 |
+
)
|
92 |
+
}
|
93 |
+
|
94 |
+
export function SetupCards() {
|
95 |
+
return (
|
96 |
+
<div className="grid md:grid-cols-2 gap-6 mb-8">
|
97 |
+
{/* Web Installation Card */}
|
98 |
+
{/* HudCorners creates 4 absolutely positioned corner elements with primary color borders */}
|
99 |
+
<HudCorners color="primary" size="sm" layer="front">
|
100 |
+
<Card className="h-full border-dashed border border-muted/30">
|
101 |
+
{/* This div contains the card content and sits above the corner elements due to relative positioning */}
|
102 |
+
<div className="p-6">
|
103 |
+
<div className="flex items-center gap-3 mb-4">
|
104 |
+
<div className="w-12 h-12 bg-primary/20 rounded-lg flex items-center justify-center">
|
105 |
+
<Package className="w-6 h-6 text-primary" />
|
106 |
+
</div>
|
107 |
+
<div>
|
108 |
+
<h3 className="text-xl font-bold text-primary font-mono tracking-wider uppercase">web</h3>
|
109 |
+
<p className="text-sm text-muted-foreground font-mono">run lerobot in the browser</p>
|
110 |
+
</div>
|
111 |
+
</div>
|
112 |
+
|
113 |
+
<PackageInstaller packageName="@lerobot/web" />
|
114 |
+
</div>
|
115 |
+
</Card>
|
116 |
+
</HudCorners>
|
117 |
+
|
118 |
+
{/* Node.js Card - Coming Soon */}
|
119 |
+
<HudCorners color="whiteMuted" size="sm" layer="front">
|
120 |
+
<Card className="h-full opacity-60 relative overflow-hidden border-dashed border border-muted/30">
|
121 |
+
{/* Disabled overlay effect */}
|
122 |
+
<div className="absolute inset-0 bg-gradient-to-br from-transparent via-muted/20 to-muted/40 dark:via-black/20 dark:to-black/40 pointer-events-none" />
|
123 |
+
|
124 |
+
<div className="p-6 relative h-full">
|
125 |
+
<div className="flex items-center justify-between mb-4">
|
126 |
+
<div className="flex items-center gap-3">
|
127 |
+
<div className="w-12 h-12 bg-muted/20 rounded-lg flex items-center justify-center border border-dashed border-muted/30">
|
128 |
+
<Terminal className="w-6 h-6 text-muted-foreground" />
|
129 |
+
</div>
|
130 |
+
<div>
|
131 |
+
<h3 className="text-xl font-bold text-muted-foreground font-mono tracking-wider uppercase">node</h3>
|
132 |
+
<p className="text-sm text-muted-foreground/70 font-mono">run lerobot on the server</p>
|
133 |
+
</div>
|
134 |
+
</div>
|
135 |
+
<Badge
|
136 |
+
variant="outline"
|
137 |
+
className="border-dashed border-muted/30 bg-muted/10 text-muted-foreground font-mono text-xs animate-pulse"
|
138 |
+
>
|
139 |
+
coming soon
|
140 |
+
</Badge>
|
141 |
+
</div>
|
142 |
+
|
143 |
+
<PackageInstaller packageName="@lerobot/node" disabled />
|
144 |
+
|
145 |
+
{/* Cyberpunk scan lines effect */}
|
146 |
+
<div className="absolute inset-0 pointer-events-none">
|
147 |
+
<div
|
148 |
+
className="absolute inset-0 bg-gradient-to-b from-transparent via-primary/5 to-transparent"
|
149 |
+
style={{
|
150 |
+
backgroundImage:
|
151 |
+
"repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(255,255,255,0.03) 2px, rgba(255,255,255,0.03) 4px)",
|
152 |
+
}}
|
153 |
+
/>
|
154 |
+
</div>
|
155 |
+
</div>
|
156 |
+
</Card>
|
157 |
+
</HudCorners>
|
158 |
+
</div>
|
159 |
+
)
|
160 |
+
}
|
examples/cyberpunk-standalone/src/components/teleoperation-view.tsx
ADDED
@@ -0,0 +1,240 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
import { useState, useEffect, useMemo } from "react"
|
3 |
+
import { Power, PowerOff, Keyboard } from "lucide-react"
|
4 |
+
import { Button } from "@/components/ui/button"
|
5 |
+
import { Card } from "@/components/ui/card"
|
6 |
+
import { Badge } from "@/components/ui/badge"
|
7 |
+
import { Slider } from "@/components/ui/slider"
|
8 |
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
9 |
+
import { cn } from "@/lib/utils"
|
10 |
+
import { lerobot } from "@/lib/mock-api"
|
11 |
+
import { useLocalStorage } from "@/hooks/use-local-storage"
|
12 |
+
import VirtualKey from "@/components/VirtualKey"
|
13 |
+
import type { RobotConnection, WebCalibrationResults, TeleoperationState } from "@/types/robot"
|
14 |
+
|
15 |
+
interface TeleoperationViewProps {
|
16 |
+
robot: RobotConnection
|
17 |
+
}
|
18 |
+
|
19 |
+
export function TeleoperationView({ robot }: TeleoperationViewProps) {
|
20 |
+
const [localCalibrationData] = useLocalStorage<WebCalibrationResults | null>(`calibration-${robot.robotId}`, null)
|
21 |
+
const [teleopState, setTeleopState] = useState<TeleoperationState | null>(null)
|
22 |
+
const [teleopProcess, setTeleopProcess] = useState<{
|
23 |
+
start: () => void
|
24 |
+
stop: () => void
|
25 |
+
updateKeyState: (key: string, pressed: boolean) => void
|
26 |
+
moveMotor: (motorName: string, position: number) => void
|
27 |
+
} | null>(null)
|
28 |
+
|
29 |
+
const calibrationData = useMemo(() => {
|
30 |
+
if (localCalibrationData) return localCalibrationData
|
31 |
+
return lerobot.MOCK_MOTOR_NAMES.reduce((acc, name) => {
|
32 |
+
acc[name] = { min: 1000, max: 3000 }
|
33 |
+
return acc
|
34 |
+
}, {} as WebCalibrationResults)
|
35 |
+
}, [localCalibrationData])
|
36 |
+
|
37 |
+
useEffect(() => {
|
38 |
+
let process: Awaited<ReturnType<typeof lerobot.teleoperate>>
|
39 |
+
const setup = async () => {
|
40 |
+
process = await lerobot.teleoperate(robot, {
|
41 |
+
calibrationData,
|
42 |
+
onStateUpdate: setTeleopState,
|
43 |
+
})
|
44 |
+
setTeleopProcess(process)
|
45 |
+
}
|
46 |
+
setup()
|
47 |
+
|
48 |
+
const handleKeyDown = (e: KeyboardEvent) => process?.updateKeyState(e.key, true)
|
49 |
+
const handleKeyUp = (e: KeyboardEvent) => process?.updateKeyState(e.key, false)
|
50 |
+
|
51 |
+
window.addEventListener("keydown", handleKeyDown)
|
52 |
+
window.addEventListener("keyup", handleKeyUp)
|
53 |
+
|
54 |
+
return () => {
|
55 |
+
process?.stop()
|
56 |
+
window.removeEventListener("keydown", handleKeyDown)
|
57 |
+
window.removeEventListener("keyup", handleKeyUp)
|
58 |
+
}
|
59 |
+
}, [robot, calibrationData])
|
60 |
+
|
61 |
+
const motorConfigs =
|
62 |
+
teleopState?.motorConfigs ??
|
63 |
+
lerobot.MOCK_MOTOR_NAMES.map((name) => ({
|
64 |
+
name,
|
65 |
+
currentPosition: 2048,
|
66 |
+
minPosition: calibrationData[name]?.min ?? 0,
|
67 |
+
maxPosition: calibrationData[name]?.max ?? 4095,
|
68 |
+
}))
|
69 |
+
const keyStates = teleopState?.keyStates ?? {}
|
70 |
+
const controls = lerobot.SO100_KEYBOARD_CONTROLS
|
71 |
+
|
72 |
+
return (
|
73 |
+
<Card className="border-0 rounded-none">
|
74 |
+
<div className="p-4 border-b border-white/10">
|
75 |
+
<div className="flex items-center justify-between">
|
76 |
+
<div className="flex items-center gap-4">
|
77 |
+
<div className="w-1 h-8 bg-primary"></div>
|
78 |
+
<div>
|
79 |
+
<h3 className="text-xl font-bold text-foreground font-mono tracking-wider uppercase">robot control</h3>
|
80 |
+
<p className="text-sm text-muted-foreground font-mono">
|
81 |
+
manual <span className="text-muted-foreground">teleoperate</span> interface
|
82 |
+
</p>
|
83 |
+
</div>
|
84 |
+
</div>
|
85 |
+
<div className="flex items-center gap-6">
|
86 |
+
<div className="border-l border-white/10 pl-6 flex items-center gap-4">
|
87 |
+
{teleopState?.isActive ? (
|
88 |
+
<Button onClick={() => teleopProcess?.stop()} variant="destructive" size="lg">
|
89 |
+
<PowerOff className="w-5 h-5 mr-2" /> Stop Control
|
90 |
+
</Button>
|
91 |
+
) : (
|
92 |
+
<Button onClick={() => teleopProcess?.start()} size="lg">
|
93 |
+
<Power className="w-5 h-5 mr-2" /> Control Robot
|
94 |
+
</Button>
|
95 |
+
)}
|
96 |
+
<div className="flex items-center gap-2">
|
97 |
+
<span className="text-sm font-mono text-muted-foreground uppercase">status:</span>
|
98 |
+
<Badge
|
99 |
+
variant="outline"
|
100 |
+
className={cn(
|
101 |
+
"border-primary/50 bg-primary/20 text-primary font-mono text-xs",
|
102 |
+
teleopState?.isActive && "animate-pulse-slow",
|
103 |
+
)}
|
104 |
+
>
|
105 |
+
{teleopState?.isActive ? "ACTIVE" : "STOPPED"}
|
106 |
+
</Badge>
|
107 |
+
</div>
|
108 |
+
</div>
|
109 |
+
</div>
|
110 |
+
</div>
|
111 |
+
</div>
|
112 |
+
<div className="pt-6 p-6 grid md:grid-cols-2 gap-8">
|
113 |
+
<div>
|
114 |
+
<h3 className="font-sans font-semibold mb-4 text-xl">Motor Control</h3>
|
115 |
+
<div className="space-y-6">
|
116 |
+
{motorConfigs.map((motor) => (
|
117 |
+
<div key={motor.name}>
|
118 |
+
<label className="text-sm font-mono text-muted-foreground">{motor.name}</label>
|
119 |
+
<div className="flex items-center gap-4">
|
120 |
+
<Slider
|
121 |
+
value={[motor.currentPosition]}
|
122 |
+
min={motor.minPosition}
|
123 |
+
max={motor.maxPosition}
|
124 |
+
step={1}
|
125 |
+
onValueChange={(val) => teleopProcess?.moveMotor(motor.name, val[0])}
|
126 |
+
disabled={!teleopState?.isActive}
|
127 |
+
/>
|
128 |
+
<span className="text-lg font-mono w-16 text-right text-accent">
|
129 |
+
{Math.round(motor.currentPosition)}
|
130 |
+
</span>
|
131 |
+
</div>
|
132 |
+
</div>
|
133 |
+
))}
|
134 |
+
</div>
|
135 |
+
</div>
|
136 |
+
<div>
|
137 |
+
<h3 className="font-sans font-semibold mb-4 text-xl">Keyboard Layout & Status</h3>
|
138 |
+
<div className="p-4 bg-black/30 rounded-lg space-y-4">
|
139 |
+
<div className="flex justify-around items-end">
|
140 |
+
<div className="flex flex-col items-center gap-2">
|
141 |
+
<VirtualKey
|
142 |
+
label="↑"
|
143 |
+
subLabel="Lift+"
|
144 |
+
isPressed={!!keyStates[controls.shoulder_lift.positive]?.pressed}
|
145 |
+
/>
|
146 |
+
<div className="flex gap-2">
|
147 |
+
<VirtualKey
|
148 |
+
label="←"
|
149 |
+
subLabel="Pan-"
|
150 |
+
isPressed={!!keyStates[controls.shoulder_pan.negative]?.pressed}
|
151 |
+
/>
|
152 |
+
<VirtualKey
|
153 |
+
label="↓"
|
154 |
+
subLabel="Lift-"
|
155 |
+
isPressed={!!keyStates[controls.shoulder_lift.negative]?.pressed}
|
156 |
+
/>
|
157 |
+
<VirtualKey
|
158 |
+
label="→"
|
159 |
+
subLabel="Pan+"
|
160 |
+
isPressed={!!keyStates[controls.shoulder_pan.positive]?.pressed}
|
161 |
+
/>
|
162 |
+
</div>
|
163 |
+
<span className="font-bold text-sm font-sans">Shoulder</span>
|
164 |
+
</div>
|
165 |
+
<div className="flex flex-col items-center gap-2">
|
166 |
+
<VirtualKey
|
167 |
+
label="W"
|
168 |
+
subLabel="Elbow+"
|
169 |
+
isPressed={!!keyStates[controls.elbow_flex.positive]?.pressed}
|
170 |
+
/>
|
171 |
+
<div className="flex gap-2">
|
172 |
+
<VirtualKey
|
173 |
+
label="A"
|
174 |
+
subLabel="Wrist+"
|
175 |
+
isPressed={!!keyStates[controls.wrist_flex.positive]?.pressed}
|
176 |
+
/>
|
177 |
+
<VirtualKey
|
178 |
+
label="S"
|
179 |
+
subLabel="Elbow-"
|
180 |
+
isPressed={!!keyStates[controls.elbow_flex.negative]?.pressed}
|
181 |
+
/>
|
182 |
+
<VirtualKey
|
183 |
+
label="D"
|
184 |
+
subLabel="Wrist-"
|
185 |
+
isPressed={!!keyStates[controls.wrist_flex.negative]?.pressed}
|
186 |
+
/>
|
187 |
+
</div>
|
188 |
+
<span className="font-bold text-sm font-sans">Elbow/Wrist</span>
|
189 |
+
</div>
|
190 |
+
<div className="flex flex-col items-center gap-2">
|
191 |
+
<div className="flex gap-2">
|
192 |
+
<VirtualKey
|
193 |
+
label="Q"
|
194 |
+
subLabel="Roll+"
|
195 |
+
isPressed={!!keyStates[controls.wrist_roll.positive]?.pressed}
|
196 |
+
/>
|
197 |
+
<VirtualKey
|
198 |
+
label="E"
|
199 |
+
subLabel="Roll-"
|
200 |
+
isPressed={!!keyStates[controls.wrist_roll.negative]?.pressed}
|
201 |
+
/>
|
202 |
+
</div>
|
203 |
+
<div className="flex gap-2">
|
204 |
+
<VirtualKey label="O" subLabel="Grip+" isPressed={!!keyStates[controls.gripper.positive]?.pressed} />
|
205 |
+
<VirtualKey label="C" subLabel="Grip-" isPressed={!!keyStates[controls.gripper.negative]?.pressed} />
|
206 |
+
</div>
|
207 |
+
<span className="font-bold text-sm font-sans">Roll/Grip</span>
|
208 |
+
</div>
|
209 |
+
</div>
|
210 |
+
<div className="pt-4 border-t border-white/10">
|
211 |
+
<div className="flex justify-between items-center font-mono text-sm">
|
212 |
+
<div className="flex items-center gap-2 text-muted-foreground">
|
213 |
+
<Keyboard className="w-4 h-4" />
|
214 |
+
<span>Active Keys: {Object.values(keyStates).filter((k) => k.pressed).length}</span>
|
215 |
+
</div>
|
216 |
+
<TooltipProvider>
|
217 |
+
<Tooltip>
|
218 |
+
<TooltipTrigger asChild>
|
219 |
+
<div
|
220 |
+
className={cn(
|
221 |
+
"w-10 h-6 border rounded-md flex items-center justify-center font-mono text-xs transition-all",
|
222 |
+
!!keyStates[controls.stop]?.pressed
|
223 |
+
? "bg-destructive text-destructive-foreground border-destructive"
|
224 |
+
: "bg-background",
|
225 |
+
)}
|
226 |
+
>
|
227 |
+
ESC
|
228 |
+
</div>
|
229 |
+
</TooltipTrigger>
|
230 |
+
<TooltipContent>Emergency Stop</TooltipContent>
|
231 |
+
</Tooltip>
|
232 |
+
</TooltipProvider>
|
233 |
+
</div>
|
234 |
+
</div>
|
235 |
+
</div>
|
236 |
+
</div>
|
237 |
+
</div>
|
238 |
+
</Card>
|
239 |
+
)
|
240 |
+
}
|
examples/cyberpunk-standalone/src/components/theme-provider.tsx
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {
|
2 |
+
ThemeProvider as NextThemesProvider,
|
3 |
+
type ThemeProviderProps,
|
4 |
+
} from "next-themes";
|
5 |
+
|
6 |
+
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
7 |
+
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
8 |
+
}
|
examples/cyberpunk-standalone/src/components/theme-toggle.tsx
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
import { Moon, Sun } from "lucide-react"
|
3 |
+
import { useTheme } from "next-themes"
|
4 |
+
import { useEffect, useState } from "react"
|
5 |
+
|
6 |
+
import { Button } from "@/components/ui/button"
|
7 |
+
|
8 |
+
export function ThemeToggle() {
|
9 |
+
const { setTheme, resolvedTheme } = useTheme()
|
10 |
+
const [mounted, setMounted] = useState(false)
|
11 |
+
|
12 |
+
useEffect(() => {
|
13 |
+
setMounted(true)
|
14 |
+
}, [])
|
15 |
+
|
16 |
+
if (!mounted) {
|
17 |
+
return (
|
18 |
+
<Button variant="outline" size="icon">
|
19 |
+
<Sun className="h-[1.2rem] w-[1.2rem]" />
|
20 |
+
<span className="sr-only">Toggle theme</span>
|
21 |
+
</Button>
|
22 |
+
)
|
23 |
+
}
|
24 |
+
|
25 |
+
const handleToggle = () => {
|
26 |
+
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
27 |
+
}
|
28 |
+
|
29 |
+
return (
|
30 |
+
<Button variant="outline" size="icon" onClick={handleToggle}>
|
31 |
+
{resolvedTheme === "dark" ? (
|
32 |
+
<Sun className="h-[1.2rem] w-[1.2rem]" />
|
33 |
+
) : (
|
34 |
+
<Moon className="h-[1.2rem] w-[1.2rem]" />
|
35 |
+
)}
|
36 |
+
<span className="sr-only">Toggle theme</span>
|
37 |
+
</Button>
|
38 |
+
)
|
39 |
+
}
|
examples/cyberpunk-standalone/src/components/ui/accordion.tsx
ADDED
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
5 |
+
import { ChevronDown } from "lucide-react"
|
6 |
+
|
7 |
+
import { cn } from "@/lib/utils"
|
8 |
+
|
9 |
+
const Accordion = AccordionPrimitive.Root
|
10 |
+
|
11 |
+
const AccordionItem = React.forwardRef<
|
12 |
+
React.ElementRef<typeof AccordionPrimitive.Item>,
|
13 |
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
14 |
+
>(({ className, ...props }, ref) => (
|
15 |
+
<AccordionPrimitive.Item
|
16 |
+
ref={ref}
|
17 |
+
className={cn("border-b", className)}
|
18 |
+
{...props}
|
19 |
+
/>
|
20 |
+
))
|
21 |
+
AccordionItem.displayName = "AccordionItem"
|
22 |
+
|
23 |
+
const AccordionTrigger = React.forwardRef<
|
24 |
+
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
25 |
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
26 |
+
>(({ className, children, ...props }, ref) => (
|
27 |
+
<AccordionPrimitive.Header className="flex">
|
28 |
+
<AccordionPrimitive.Trigger
|
29 |
+
ref={ref}
|
30 |
+
className={cn(
|
31 |
+
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
32 |
+
className
|
33 |
+
)}
|
34 |
+
{...props}
|
35 |
+
>
|
36 |
+
{children}
|
37 |
+
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
38 |
+
</AccordionPrimitive.Trigger>
|
39 |
+
</AccordionPrimitive.Header>
|
40 |
+
))
|
41 |
+
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
42 |
+
|
43 |
+
const AccordionContent = React.forwardRef<
|
44 |
+
React.ElementRef<typeof AccordionPrimitive.Content>,
|
45 |
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
46 |
+
>(({ className, children, ...props }, ref) => (
|
47 |
+
<AccordionPrimitive.Content
|
48 |
+
ref={ref}
|
49 |
+
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
50 |
+
{...props}
|
51 |
+
>
|
52 |
+
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
53 |
+
</AccordionPrimitive.Content>
|
54 |
+
))
|
55 |
+
|
56 |
+
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
57 |
+
|
58 |
+
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
examples/cyberpunk-standalone/src/components/ui/alert-dialog.tsx
ADDED
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
5 |
+
|
6 |
+
import { cn } from "@/lib/utils"
|
7 |
+
import { buttonVariants } from "@/components/ui/button"
|
8 |
+
|
9 |
+
const AlertDialog = AlertDialogPrimitive.Root
|
10 |
+
|
11 |
+
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
12 |
+
|
13 |
+
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
14 |
+
|
15 |
+
const AlertDialogOverlay = React.forwardRef<
|
16 |
+
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
17 |
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
18 |
+
>(({ className, ...props }, ref) => (
|
19 |
+
<AlertDialogPrimitive.Overlay
|
20 |
+
className={cn(
|
21 |
+
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
22 |
+
className
|
23 |
+
)}
|
24 |
+
{...props}
|
25 |
+
ref={ref}
|
26 |
+
/>
|
27 |
+
))
|
28 |
+
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
29 |
+
|
30 |
+
const AlertDialogContent = React.forwardRef<
|
31 |
+
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
32 |
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
33 |
+
>(({ className, ...props }, ref) => (
|
34 |
+
<AlertDialogPortal>
|
35 |
+
<AlertDialogOverlay />
|
36 |
+
<AlertDialogPrimitive.Content
|
37 |
+
ref={ref}
|
38 |
+
className={cn(
|
39 |
+
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
40 |
+
className
|
41 |
+
)}
|
42 |
+
{...props}
|
43 |
+
/>
|
44 |
+
</AlertDialogPortal>
|
45 |
+
))
|
46 |
+
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
47 |
+
|
48 |
+
const AlertDialogHeader = ({
|
49 |
+
className,
|
50 |
+
...props
|
51 |
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
52 |
+
<div
|
53 |
+
className={cn(
|
54 |
+
"flex flex-col space-y-2 text-center sm:text-left",
|
55 |
+
className
|
56 |
+
)}
|
57 |
+
{...props}
|
58 |
+
/>
|
59 |
+
)
|
60 |
+
AlertDialogHeader.displayName = "AlertDialogHeader"
|
61 |
+
|
62 |
+
const AlertDialogFooter = ({
|
63 |
+
className,
|
64 |
+
...props
|
65 |
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
66 |
+
<div
|
67 |
+
className={cn(
|
68 |
+
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
69 |
+
className
|
70 |
+
)}
|
71 |
+
{...props}
|
72 |
+
/>
|
73 |
+
)
|
74 |
+
AlertDialogFooter.displayName = "AlertDialogFooter"
|
75 |
+
|
76 |
+
const AlertDialogTitle = React.forwardRef<
|
77 |
+
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
78 |
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
79 |
+
>(({ className, ...props }, ref) => (
|
80 |
+
<AlertDialogPrimitive.Title
|
81 |
+
ref={ref}
|
82 |
+
className={cn("text-lg font-semibold", className)}
|
83 |
+
{...props}
|
84 |
+
/>
|
85 |
+
))
|
86 |
+
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
87 |
+
|
88 |
+
const AlertDialogDescription = React.forwardRef<
|
89 |
+
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
90 |
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
91 |
+
>(({ className, ...props }, ref) => (
|
92 |
+
<AlertDialogPrimitive.Description
|
93 |
+
ref={ref}
|
94 |
+
className={cn("text-sm text-muted-foreground", className)}
|
95 |
+
{...props}
|
96 |
+
/>
|
97 |
+
))
|
98 |
+
AlertDialogDescription.displayName =
|
99 |
+
AlertDialogPrimitive.Description.displayName
|
100 |
+
|
101 |
+
const AlertDialogAction = React.forwardRef<
|
102 |
+
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
103 |
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
104 |
+
>(({ className, ...props }, ref) => (
|
105 |
+
<AlertDialogPrimitive.Action
|
106 |
+
ref={ref}
|
107 |
+
className={cn(buttonVariants(), className)}
|
108 |
+
{...props}
|
109 |
+
/>
|
110 |
+
))
|
111 |
+
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
112 |
+
|
113 |
+
const AlertDialogCancel = React.forwardRef<
|
114 |
+
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
115 |
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
116 |
+
>(({ className, ...props }, ref) => (
|
117 |
+
<AlertDialogPrimitive.Cancel
|
118 |
+
ref={ref}
|
119 |
+
className={cn(
|
120 |
+
buttonVariants({ variant: "outline" }),
|
121 |
+
"mt-2 sm:mt-0",
|
122 |
+
className
|
123 |
+
)}
|
124 |
+
{...props}
|
125 |
+
/>
|
126 |
+
))
|
127 |
+
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
128 |
+
|
129 |
+
export {
|
130 |
+
AlertDialog,
|
131 |
+
AlertDialogPortal,
|
132 |
+
AlertDialogOverlay,
|
133 |
+
AlertDialogTrigger,
|
134 |
+
AlertDialogContent,
|
135 |
+
AlertDialogHeader,
|
136 |
+
AlertDialogFooter,
|
137 |
+
AlertDialogTitle,
|
138 |
+
AlertDialogDescription,
|
139 |
+
AlertDialogAction,
|
140 |
+
AlertDialogCancel,
|
141 |
+
}
|
examples/cyberpunk-standalone/src/components/ui/alert.tsx
ADDED
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from "react"
|
2 |
+
import { cva, type VariantProps } from "class-variance-authority"
|
3 |
+
|
4 |
+
import { cn } from "@/lib/utils"
|
5 |
+
|
6 |
+
const alertVariants = cva(
|
7 |
+
"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",
|
8 |
+
{
|
9 |
+
variants: {
|
10 |
+
variant: {
|
11 |
+
default: "bg-background text-foreground",
|
12 |
+
destructive:
|
13 |
+
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
14 |
+
},
|
15 |
+
},
|
16 |
+
defaultVariants: {
|
17 |
+
variant: "default",
|
18 |
+
},
|
19 |
+
}
|
20 |
+
)
|
21 |
+
|
22 |
+
const Alert = React.forwardRef<
|
23 |
+
HTMLDivElement,
|
24 |
+
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
25 |
+
>(({ className, variant, ...props }, ref) => (
|
26 |
+
<div
|
27 |
+
ref={ref}
|
28 |
+
role="alert"
|
29 |
+
className={cn(alertVariants({ variant }), className)}
|
30 |
+
{...props}
|
31 |
+
/>
|
32 |
+
))
|
33 |
+
Alert.displayName = "Alert"
|
34 |
+
|
35 |
+
const AlertTitle = React.forwardRef<
|
36 |
+
HTMLParagraphElement,
|
37 |
+
React.HTMLAttributes<HTMLHeadingElement>
|
38 |
+
>(({ className, ...props }, ref) => (
|
39 |
+
<h5
|
40 |
+
ref={ref}
|
41 |
+
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
42 |
+
{...props}
|
43 |
+
/>
|
44 |
+
))
|
45 |
+
AlertTitle.displayName = "AlertTitle"
|
46 |
+
|
47 |
+
const AlertDescription = React.forwardRef<
|
48 |
+
HTMLParagraphElement,
|
49 |
+
React.HTMLAttributes<HTMLParagraphElement>
|
50 |
+
>(({ className, ...props }, ref) => (
|
51 |
+
<div
|
52 |
+
ref={ref}
|
53 |
+
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
54 |
+
{...props}
|
55 |
+
/>
|
56 |
+
))
|
57 |
+
AlertDescription.displayName = "AlertDescription"
|
58 |
+
|
59 |
+
export { Alert, AlertTitle, AlertDescription }
|
examples/cyberpunk-standalone/src/components/ui/aspect-ratio.tsx
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
|
4 |
+
|
5 |
+
const AspectRatio = AspectRatioPrimitive.Root
|
6 |
+
|
7 |
+
export { AspectRatio }
|
examples/cyberpunk-standalone/src/components/ui/avatar.tsx
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
5 |
+
|
6 |
+
import { cn } from "@/lib/utils"
|
7 |
+
|
8 |
+
const Avatar = React.forwardRef<
|
9 |
+
React.ElementRef<typeof AvatarPrimitive.Root>,
|
10 |
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
11 |
+
>(({ className, ...props }, ref) => (
|
12 |
+
<AvatarPrimitive.Root
|
13 |
+
ref={ref}
|
14 |
+
className={cn(
|
15 |
+
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
16 |
+
className
|
17 |
+
)}
|
18 |
+
{...props}
|
19 |
+
/>
|
20 |
+
))
|
21 |
+
Avatar.displayName = AvatarPrimitive.Root.displayName
|
22 |
+
|
23 |
+
const AvatarImage = React.forwardRef<
|
24 |
+
React.ElementRef<typeof AvatarPrimitive.Image>,
|
25 |
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
26 |
+
>(({ className, ...props }, ref) => (
|
27 |
+
<AvatarPrimitive.Image
|
28 |
+
ref={ref}
|
29 |
+
className={cn("aspect-square h-full w-full", className)}
|
30 |
+
{...props}
|
31 |
+
/>
|
32 |
+
))
|
33 |
+
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
34 |
+
|
35 |
+
const AvatarFallback = React.forwardRef<
|
36 |
+
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
37 |
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
38 |
+
>(({ className, ...props }, ref) => (
|
39 |
+
<AvatarPrimitive.Fallback
|
40 |
+
ref={ref}
|
41 |
+
className={cn(
|
42 |
+
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
43 |
+
className
|
44 |
+
)}
|
45 |
+
{...props}
|
46 |
+
/>
|
47 |
+
))
|
48 |
+
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
49 |
+
|
50 |
+
export { Avatar, AvatarImage, AvatarFallback }
|
examples/cyberpunk-standalone/src/components/ui/badge.tsx
ADDED
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from "react"
|
2 |
+
import { cva, type VariantProps } from "class-variance-authority"
|
3 |
+
|
4 |
+
import { cn } from "@/lib/utils"
|
5 |
+
|
6 |
+
const badgeVariants = cva(
|
7 |
+
"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",
|
8 |
+
{
|
9 |
+
variants: {
|
10 |
+
variant: {
|
11 |
+
default:
|
12 |
+
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
13 |
+
secondary:
|
14 |
+
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
15 |
+
destructive:
|
16 |
+
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
17 |
+
outline: "text-foreground",
|
18 |
+
},
|
19 |
+
},
|
20 |
+
defaultVariants: {
|
21 |
+
variant: "default",
|
22 |
+
},
|
23 |
+
}
|
24 |
+
)
|
25 |
+
|
26 |
+
export interface BadgeProps
|
27 |
+
extends React.HTMLAttributes<HTMLDivElement>,
|
28 |
+
VariantProps<typeof badgeVariants> {}
|
29 |
+
|
30 |
+
function Badge({ className, variant, ...props }: BadgeProps) {
|
31 |
+
return (
|
32 |
+
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
33 |
+
)
|
34 |
+
}
|
35 |
+
|
36 |
+
export { Badge, badgeVariants }
|
examples/cyberpunk-standalone/src/components/ui/breadcrumb.tsx
ADDED
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from "react"
|
2 |
+
import { Slot } from "@radix-ui/react-slot"
|
3 |
+
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
4 |
+
|
5 |
+
import { cn } from "@/lib/utils"
|
6 |
+
|
7 |
+
const Breadcrumb = React.forwardRef<
|
8 |
+
HTMLElement,
|
9 |
+
React.ComponentPropsWithoutRef<"nav"> & {
|
10 |
+
separator?: React.ReactNode
|
11 |
+
}
|
12 |
+
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
|
13 |
+
Breadcrumb.displayName = "Breadcrumb"
|
14 |
+
|
15 |
+
const BreadcrumbList = React.forwardRef<
|
16 |
+
HTMLOListElement,
|
17 |
+
React.ComponentPropsWithoutRef<"ol">
|
18 |
+
>(({ className, ...props }, ref) => (
|
19 |
+
<ol
|
20 |
+
ref={ref}
|
21 |
+
className={cn(
|
22 |
+
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
23 |
+
className
|
24 |
+
)}
|
25 |
+
{...props}
|
26 |
+
/>
|
27 |
+
))
|
28 |
+
BreadcrumbList.displayName = "BreadcrumbList"
|
29 |
+
|
30 |
+
const BreadcrumbItem = React.forwardRef<
|
31 |
+
HTMLLIElement,
|
32 |
+
React.ComponentPropsWithoutRef<"li">
|
33 |
+
>(({ className, ...props }, ref) => (
|
34 |
+
<li
|
35 |
+
ref={ref}
|
36 |
+
className={cn("inline-flex items-center gap-1.5", className)}
|
37 |
+
{...props}
|
38 |
+
/>
|
39 |
+
))
|
40 |
+
BreadcrumbItem.displayName = "BreadcrumbItem"
|
41 |
+
|
42 |
+
const BreadcrumbLink = React.forwardRef<
|
43 |
+
HTMLAnchorElement,
|
44 |
+
React.ComponentPropsWithoutRef<"a"> & {
|
45 |
+
asChild?: boolean
|
46 |
+
}
|
47 |
+
>(({ asChild, className, ...props }, ref) => {
|
48 |
+
const Comp = asChild ? Slot : "a"
|
49 |
+
|
50 |
+
return (
|
51 |
+
<Comp
|
52 |
+
ref={ref}
|
53 |
+
className={cn("transition-colors hover:text-foreground", className)}
|
54 |
+
{...props}
|
55 |
+
/>
|
56 |
+
)
|
57 |
+
})
|
58 |
+
BreadcrumbLink.displayName = "BreadcrumbLink"
|
59 |
+
|
60 |
+
const BreadcrumbPage = React.forwardRef<
|
61 |
+
HTMLSpanElement,
|
62 |
+
React.ComponentPropsWithoutRef<"span">
|
63 |
+
>(({ className, ...props }, ref) => (
|
64 |
+
<span
|
65 |
+
ref={ref}
|
66 |
+
role="link"
|
67 |
+
aria-disabled="true"
|
68 |
+
aria-current="page"
|
69 |
+
className={cn("font-normal text-foreground", className)}
|
70 |
+
{...props}
|
71 |
+
/>
|
72 |
+
))
|
73 |
+
BreadcrumbPage.displayName = "BreadcrumbPage"
|
74 |
+
|
75 |
+
const BreadcrumbSeparator = ({
|
76 |
+
children,
|
77 |
+
className,
|
78 |
+
...props
|
79 |
+
}: React.ComponentProps<"li">) => (
|
80 |
+
<li
|
81 |
+
role="presentation"
|
82 |
+
aria-hidden="true"
|
83 |
+
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
|
84 |
+
{...props}
|
85 |
+
>
|
86 |
+
{children ?? <ChevronRight />}
|
87 |
+
</li>
|
88 |
+
)
|
89 |
+
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
|
90 |
+
|
91 |
+
const BreadcrumbEllipsis = ({
|
92 |
+
className,
|
93 |
+
...props
|
94 |
+
}: React.ComponentProps<"span">) => (
|
95 |
+
<span
|
96 |
+
role="presentation"
|
97 |
+
aria-hidden="true"
|
98 |
+
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
99 |
+
{...props}
|
100 |
+
>
|
101 |
+
<MoreHorizontal className="h-4 w-4" />
|
102 |
+
<span className="sr-only">More</span>
|
103 |
+
</span>
|
104 |
+
)
|
105 |
+
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
|
106 |
+
|
107 |
+
export {
|
108 |
+
Breadcrumb,
|
109 |
+
BreadcrumbList,
|
110 |
+
BreadcrumbItem,
|
111 |
+
BreadcrumbLink,
|
112 |
+
BreadcrumbPage,
|
113 |
+
BreadcrumbSeparator,
|
114 |
+
BreadcrumbEllipsis,
|
115 |
+
}
|
examples/cyberpunk-standalone/src/components/ui/button.tsx
ADDED
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from "react"
|
2 |
+
import { Slot } from "@radix-ui/react-slot"
|
3 |
+
import { cva, type VariantProps } from "class-variance-authority"
|
4 |
+
|
5 |
+
import { cn } from "@/lib/utils"
|
6 |
+
|
7 |
+
const buttonVariants = cva(
|
8 |
+
"inline-flex items-center justify-center gap-2 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 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
9 |
+
{
|
10 |
+
variants: {
|
11 |
+
variant: {
|
12 |
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
13 |
+
destructive:
|
14 |
+
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
15 |
+
outline:
|
16 |
+
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
17 |
+
secondary:
|
18 |
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
19 |
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
20 |
+
link: "text-primary underline-offset-4 hover:underline",
|
21 |
+
},
|
22 |
+
size: {
|
23 |
+
default: "h-10 px-4 py-2",
|
24 |
+
sm: "h-9 rounded-md px-3",
|
25 |
+
lg: "h-11 rounded-md px-8",
|
26 |
+
icon: "h-10 w-10",
|
27 |
+
},
|
28 |
+
},
|
29 |
+
defaultVariants: {
|
30 |
+
variant: "default",
|
31 |
+
size: "default",
|
32 |
+
},
|
33 |
+
}
|
34 |
+
)
|
35 |
+
|
36 |
+
export interface ButtonProps
|
37 |
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
38 |
+
VariantProps<typeof buttonVariants> {
|
39 |
+
asChild?: boolean
|
40 |
+
}
|
41 |
+
|
42 |
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
43 |
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
44 |
+
const Comp = asChild ? Slot : "button"
|
45 |
+
return (
|
46 |
+
<Comp
|
47 |
+
className={cn(buttonVariants({ variant, size, className }))}
|
48 |
+
ref={ref}
|
49 |
+
{...props}
|
50 |
+
/>
|
51 |
+
)
|
52 |
+
}
|
53 |
+
)
|
54 |
+
Button.displayName = "Button"
|
55 |
+
|
56 |
+
export { Button, buttonVariants }
|
examples/cyberpunk-standalone/src/components/ui/calendar.tsx
ADDED
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import { ChevronLeft, ChevronRight } from "lucide-react"
|
5 |
+
import { DayPicker } from "react-day-picker"
|
6 |
+
|
7 |
+
import { cn } from "@/lib/utils"
|
8 |
+
import { buttonVariants } from "@/components/ui/button"
|
9 |
+
|
10 |
+
export type CalendarProps = React.ComponentProps<typeof DayPicker>
|
11 |
+
|
12 |
+
function Calendar({
|
13 |
+
className,
|
14 |
+
classNames,
|
15 |
+
showOutsideDays = true,
|
16 |
+
...props
|
17 |
+
}: CalendarProps) {
|
18 |
+
return (
|
19 |
+
<DayPicker
|
20 |
+
showOutsideDays={showOutsideDays}
|
21 |
+
className={cn("p-3", className)}
|
22 |
+
classNames={{
|
23 |
+
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
24 |
+
month: "space-y-4",
|
25 |
+
caption: "flex justify-center pt-1 relative items-center",
|
26 |
+
caption_label: "text-sm font-medium",
|
27 |
+
nav: "space-x-1 flex items-center",
|
28 |
+
nav_button: cn(
|
29 |
+
buttonVariants({ variant: "outline" }),
|
30 |
+
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
31 |
+
),
|
32 |
+
nav_button_previous: "absolute left-1",
|
33 |
+
nav_button_next: "absolute right-1",
|
34 |
+
table: "w-full border-collapse space-y-1",
|
35 |
+
head_row: "flex",
|
36 |
+
head_cell:
|
37 |
+
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
|
38 |
+
row: "flex w-full mt-2",
|
39 |
+
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
|
40 |
+
day: cn(
|
41 |
+
buttonVariants({ variant: "ghost" }),
|
42 |
+
"h-9 w-9 p-0 font-normal aria-selected:opacity-100"
|
43 |
+
),
|
44 |
+
day_range_end: "day-range-end",
|
45 |
+
day_selected:
|
46 |
+
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
47 |
+
day_today: "bg-accent text-accent-foreground",
|
48 |
+
day_outside:
|
49 |
+
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
|
50 |
+
day_disabled: "text-muted-foreground opacity-50",
|
51 |
+
day_range_middle:
|
52 |
+
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
53 |
+
day_hidden: "invisible",
|
54 |
+
...classNames,
|
55 |
+
}}
|
56 |
+
components={{
|
57 |
+
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
|
58 |
+
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
|
59 |
+
}}
|
60 |
+
{...props}
|
61 |
+
/>
|
62 |
+
)
|
63 |
+
}
|
64 |
+
Calendar.displayName = "Calendar"
|
65 |
+
|
66 |
+
export { Calendar }
|
examples/cyberpunk-standalone/src/components/ui/card.tsx
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from "react"
|
2 |
+
|
3 |
+
import { cn } from "@/lib/utils"
|
4 |
+
|
5 |
+
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
|
6 |
+
<div
|
7 |
+
ref={ref}
|
8 |
+
className={cn(
|
9 |
+
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
10 |
+
"relative bg-card/60 border-white/10",
|
11 |
+
className,
|
12 |
+
)}
|
13 |
+
>
|
14 |
+
<div className="absolute inset-0 bg-hex-pattern opacity-50"></div>
|
15 |
+
<div className="relative z-10 h-full">{props.children}</div>
|
16 |
+
</div>
|
17 |
+
))
|
18 |
+
Card.displayName = "Card"
|
19 |
+
|
20 |
+
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
21 |
+
({ className, ...props }, ref) => (
|
22 |
+
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
23 |
+
),
|
24 |
+
)
|
25 |
+
CardHeader.displayName = "CardHeader"
|
26 |
+
|
27 |
+
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
28 |
+
({ className, ...props }, ref) => (
|
29 |
+
<h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} />
|
30 |
+
),
|
31 |
+
)
|
32 |
+
CardTitle.displayName = "CardTitle"
|
33 |
+
|
34 |
+
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
35 |
+
({ className, ...props }, ref) => (
|
36 |
+
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
37 |
+
),
|
38 |
+
)
|
39 |
+
CardDescription.displayName = "CardDescription"
|
40 |
+
|
41 |
+
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
42 |
+
({ className, ...props }, ref) => (
|
43 |
+
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
|
44 |
+
),
|
45 |
+
)
|
46 |
+
CardFooter.displayName = "CardFooter"
|
47 |
+
|
48 |
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription }
|
examples/cyberpunk-standalone/src/components/ui/carousel.tsx
ADDED
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import useEmblaCarousel, {
|
5 |
+
type UseEmblaCarouselType,
|
6 |
+
} from "embla-carousel-react"
|
7 |
+
import { ArrowLeft, ArrowRight } from "lucide-react"
|
8 |
+
|
9 |
+
import { cn } from "@/lib/utils"
|
10 |
+
import { Button } from "@/components/ui/button"
|
11 |
+
|
12 |
+
type CarouselApi = UseEmblaCarouselType[1]
|
13 |
+
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
14 |
+
type CarouselOptions = UseCarouselParameters[0]
|
15 |
+
type CarouselPlugin = UseCarouselParameters[1]
|
16 |
+
|
17 |
+
type CarouselProps = {
|
18 |
+
opts?: CarouselOptions
|
19 |
+
plugins?: CarouselPlugin
|
20 |
+
orientation?: "horizontal" | "vertical"
|
21 |
+
setApi?: (api: CarouselApi) => void
|
22 |
+
}
|
23 |
+
|
24 |
+
type CarouselContextProps = {
|
25 |
+
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
|
26 |
+
api: ReturnType<typeof useEmblaCarousel>[1]
|
27 |
+
scrollPrev: () => void
|
28 |
+
scrollNext: () => void
|
29 |
+
canScrollPrev: boolean
|
30 |
+
canScrollNext: boolean
|
31 |
+
} & CarouselProps
|
32 |
+
|
33 |
+
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
34 |
+
|
35 |
+
function useCarousel() {
|
36 |
+
const context = React.useContext(CarouselContext)
|
37 |
+
|
38 |
+
if (!context) {
|
39 |
+
throw new Error("useCarousel must be used within a <Carousel />")
|
40 |
+
}
|
41 |
+
|
42 |
+
return context
|
43 |
+
}
|
44 |
+
|
45 |
+
const Carousel = React.forwardRef<
|
46 |
+
HTMLDivElement,
|
47 |
+
React.HTMLAttributes<HTMLDivElement> & CarouselProps
|
48 |
+
>(
|
49 |
+
(
|
50 |
+
{
|
51 |
+
orientation = "horizontal",
|
52 |
+
opts,
|
53 |
+
setApi,
|
54 |
+
plugins,
|
55 |
+
className,
|
56 |
+
children,
|
57 |
+
...props
|
58 |
+
},
|
59 |
+
ref
|
60 |
+
) => {
|
61 |
+
const [carouselRef, api] = useEmblaCarousel(
|
62 |
+
{
|
63 |
+
...opts,
|
64 |
+
axis: orientation === "horizontal" ? "x" : "y",
|
65 |
+
},
|
66 |
+
plugins
|
67 |
+
)
|
68 |
+
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
69 |
+
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
70 |
+
|
71 |
+
const onSelect = React.useCallback((api: CarouselApi) => {
|
72 |
+
if (!api) {
|
73 |
+
return
|
74 |
+
}
|
75 |
+
|
76 |
+
setCanScrollPrev(api.canScrollPrev())
|
77 |
+
setCanScrollNext(api.canScrollNext())
|
78 |
+
}, [])
|
79 |
+
|
80 |
+
const scrollPrev = React.useCallback(() => {
|
81 |
+
api?.scrollPrev()
|
82 |
+
}, [api])
|
83 |
+
|
84 |
+
const scrollNext = React.useCallback(() => {
|
85 |
+
api?.scrollNext()
|
86 |
+
}, [api])
|
87 |
+
|
88 |
+
const handleKeyDown = React.useCallback(
|
89 |
+
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
90 |
+
if (event.key === "ArrowLeft") {
|
91 |
+
event.preventDefault()
|
92 |
+
scrollPrev()
|
93 |
+
} else if (event.key === "ArrowRight") {
|
94 |
+
event.preventDefault()
|
95 |
+
scrollNext()
|
96 |
+
}
|
97 |
+
},
|
98 |
+
[scrollPrev, scrollNext]
|
99 |
+
)
|
100 |
+
|
101 |
+
React.useEffect(() => {
|
102 |
+
if (!api || !setApi) {
|
103 |
+
return
|
104 |
+
}
|
105 |
+
|
106 |
+
setApi(api)
|
107 |
+
}, [api, setApi])
|
108 |
+
|
109 |
+
React.useEffect(() => {
|
110 |
+
if (!api) {
|
111 |
+
return
|
112 |
+
}
|
113 |
+
|
114 |
+
onSelect(api)
|
115 |
+
api.on("reInit", onSelect)
|
116 |
+
api.on("select", onSelect)
|
117 |
+
|
118 |
+
return () => {
|
119 |
+
api?.off("select", onSelect)
|
120 |
+
}
|
121 |
+
}, [api, onSelect])
|
122 |
+
|
123 |
+
return (
|
124 |
+
<CarouselContext.Provider
|
125 |
+
value={{
|
126 |
+
carouselRef,
|
127 |
+
api: api,
|
128 |
+
opts,
|
129 |
+
orientation:
|
130 |
+
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
131 |
+
scrollPrev,
|
132 |
+
scrollNext,
|
133 |
+
canScrollPrev,
|
134 |
+
canScrollNext,
|
135 |
+
}}
|
136 |
+
>
|
137 |
+
<div
|
138 |
+
ref={ref}
|
139 |
+
onKeyDownCapture={handleKeyDown}
|
140 |
+
className={cn("relative", className)}
|
141 |
+
role="region"
|
142 |
+
aria-roledescription="carousel"
|
143 |
+
{...props}
|
144 |
+
>
|
145 |
+
{children}
|
146 |
+
</div>
|
147 |
+
</CarouselContext.Provider>
|
148 |
+
)
|
149 |
+
}
|
150 |
+
)
|
151 |
+
Carousel.displayName = "Carousel"
|
152 |
+
|
153 |
+
const CarouselContent = React.forwardRef<
|
154 |
+
HTMLDivElement,
|
155 |
+
React.HTMLAttributes<HTMLDivElement>
|
156 |
+
>(({ className, ...props }, ref) => {
|
157 |
+
const { carouselRef, orientation } = useCarousel()
|
158 |
+
|
159 |
+
return (
|
160 |
+
<div ref={carouselRef} className="overflow-hidden">
|
161 |
+
<div
|
162 |
+
ref={ref}
|
163 |
+
className={cn(
|
164 |
+
"flex",
|
165 |
+
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
166 |
+
className
|
167 |
+
)}
|
168 |
+
{...props}
|
169 |
+
/>
|
170 |
+
</div>
|
171 |
+
)
|
172 |
+
})
|
173 |
+
CarouselContent.displayName = "CarouselContent"
|
174 |
+
|
175 |
+
const CarouselItem = React.forwardRef<
|
176 |
+
HTMLDivElement,
|
177 |
+
React.HTMLAttributes<HTMLDivElement>
|
178 |
+
>(({ className, ...props }, ref) => {
|
179 |
+
const { orientation } = useCarousel()
|
180 |
+
|
181 |
+
return (
|
182 |
+
<div
|
183 |
+
ref={ref}
|
184 |
+
role="group"
|
185 |
+
aria-roledescription="slide"
|
186 |
+
className={cn(
|
187 |
+
"min-w-0 shrink-0 grow-0 basis-full",
|
188 |
+
orientation === "horizontal" ? "pl-4" : "pt-4",
|
189 |
+
className
|
190 |
+
)}
|
191 |
+
{...props}
|
192 |
+
/>
|
193 |
+
)
|
194 |
+
})
|
195 |
+
CarouselItem.displayName = "CarouselItem"
|
196 |
+
|
197 |
+
const CarouselPrevious = React.forwardRef<
|
198 |
+
HTMLButtonElement,
|
199 |
+
React.ComponentProps<typeof Button>
|
200 |
+
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
201 |
+
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
202 |
+
|
203 |
+
return (
|
204 |
+
<Button
|
205 |
+
ref={ref}
|
206 |
+
variant={variant}
|
207 |
+
size={size}
|
208 |
+
className={cn(
|
209 |
+
"absolute h-8 w-8 rounded-full",
|
210 |
+
orientation === "horizontal"
|
211 |
+
? "-left-12 top-1/2 -translate-y-1/2"
|
212 |
+
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
213 |
+
className
|
214 |
+
)}
|
215 |
+
disabled={!canScrollPrev}
|
216 |
+
onClick={scrollPrev}
|
217 |
+
{...props}
|
218 |
+
>
|
219 |
+
<ArrowLeft className="h-4 w-4" />
|
220 |
+
<span className="sr-only">Previous slide</span>
|
221 |
+
</Button>
|
222 |
+
)
|
223 |
+
})
|
224 |
+
CarouselPrevious.displayName = "CarouselPrevious"
|
225 |
+
|
226 |
+
const CarouselNext = React.forwardRef<
|
227 |
+
HTMLButtonElement,
|
228 |
+
React.ComponentProps<typeof Button>
|
229 |
+
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
230 |
+
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
231 |
+
|
232 |
+
return (
|
233 |
+
<Button
|
234 |
+
ref={ref}
|
235 |
+
variant={variant}
|
236 |
+
size={size}
|
237 |
+
className={cn(
|
238 |
+
"absolute h-8 w-8 rounded-full",
|
239 |
+
orientation === "horizontal"
|
240 |
+
? "-right-12 top-1/2 -translate-y-1/2"
|
241 |
+
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
242 |
+
className
|
243 |
+
)}
|
244 |
+
disabled={!canScrollNext}
|
245 |
+
onClick={scrollNext}
|
246 |
+
{...props}
|
247 |
+
>
|
248 |
+
<ArrowRight className="h-4 w-4" />
|
249 |
+
<span className="sr-only">Next slide</span>
|
250 |
+
</Button>
|
251 |
+
)
|
252 |
+
})
|
253 |
+
CarouselNext.displayName = "CarouselNext"
|
254 |
+
|
255 |
+
export {
|
256 |
+
type CarouselApi,
|
257 |
+
Carousel,
|
258 |
+
CarouselContent,
|
259 |
+
CarouselItem,
|
260 |
+
CarouselPrevious,
|
261 |
+
CarouselNext,
|
262 |
+
}
|
examples/cyberpunk-standalone/src/components/ui/chart.tsx
ADDED
@@ -0,0 +1,365 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as RechartsPrimitive from "recharts"
|
5 |
+
|
6 |
+
import { cn } from "@/lib/utils"
|
7 |
+
|
8 |
+
// Format: { THEME_NAME: CSS_SELECTOR }
|
9 |
+
const THEMES = { light: "", dark: ".dark" } as const
|
10 |
+
|
11 |
+
export type ChartConfig = {
|
12 |
+
[k in string]: {
|
13 |
+
label?: React.ReactNode
|
14 |
+
icon?: React.ComponentType
|
15 |
+
} & (
|
16 |
+
| { color?: string; theme?: never }
|
17 |
+
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
18 |
+
)
|
19 |
+
}
|
20 |
+
|
21 |
+
type ChartContextProps = {
|
22 |
+
config: ChartConfig
|
23 |
+
}
|
24 |
+
|
25 |
+
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
26 |
+
|
27 |
+
function useChart() {
|
28 |
+
const context = React.useContext(ChartContext)
|
29 |
+
|
30 |
+
if (!context) {
|
31 |
+
throw new Error("useChart must be used within a <ChartContainer />")
|
32 |
+
}
|
33 |
+
|
34 |
+
return context
|
35 |
+
}
|
36 |
+
|
37 |
+
const ChartContainer = React.forwardRef<
|
38 |
+
HTMLDivElement,
|
39 |
+
React.ComponentProps<"div"> & {
|
40 |
+
config: ChartConfig
|
41 |
+
children: React.ComponentProps<
|
42 |
+
typeof RechartsPrimitive.ResponsiveContainer
|
43 |
+
>["children"]
|
44 |
+
}
|
45 |
+
>(({ id, className, children, config, ...props }, ref) => {
|
46 |
+
const uniqueId = React.useId()
|
47 |
+
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
48 |
+
|
49 |
+
return (
|
50 |
+
<ChartContext.Provider value={{ config }}>
|
51 |
+
<div
|
52 |
+
data-chart={chartId}
|
53 |
+
ref={ref}
|
54 |
+
className={cn(
|
55 |
+
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
56 |
+
className
|
57 |
+
)}
|
58 |
+
{...props}
|
59 |
+
>
|
60 |
+
<ChartStyle id={chartId} config={config} />
|
61 |
+
<RechartsPrimitive.ResponsiveContainer>
|
62 |
+
{children}
|
63 |
+
</RechartsPrimitive.ResponsiveContainer>
|
64 |
+
</div>
|
65 |
+
</ChartContext.Provider>
|
66 |
+
)
|
67 |
+
})
|
68 |
+
ChartContainer.displayName = "Chart"
|
69 |
+
|
70 |
+
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
71 |
+
const colorConfig = Object.entries(config).filter(
|
72 |
+
([_, config]) => config.theme || config.color
|
73 |
+
)
|
74 |
+
|
75 |
+
if (!colorConfig.length) {
|
76 |
+
return null
|
77 |
+
}
|
78 |
+
|
79 |
+
return (
|
80 |
+
<style
|
81 |
+
dangerouslySetInnerHTML={{
|
82 |
+
__html: Object.entries(THEMES)
|
83 |
+
.map(
|
84 |
+
([theme, prefix]) => `
|
85 |
+
${prefix} [data-chart=${id}] {
|
86 |
+
${colorConfig
|
87 |
+
.map(([key, itemConfig]) => {
|
88 |
+
const color =
|
89 |
+
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
90 |
+
itemConfig.color
|
91 |
+
return color ? ` --color-${key}: ${color};` : null
|
92 |
+
})
|
93 |
+
.join("\n")}
|
94 |
+
}
|
95 |
+
`
|
96 |
+
)
|
97 |
+
.join("\n"),
|
98 |
+
}}
|
99 |
+
/>
|
100 |
+
)
|
101 |
+
}
|
102 |
+
|
103 |
+
const ChartTooltip = RechartsPrimitive.Tooltip
|
104 |
+
|
105 |
+
const ChartTooltipContent = React.forwardRef<
|
106 |
+
HTMLDivElement,
|
107 |
+
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
108 |
+
React.ComponentProps<"div"> & {
|
109 |
+
hideLabel?: boolean
|
110 |
+
hideIndicator?: boolean
|
111 |
+
indicator?: "line" | "dot" | "dashed"
|
112 |
+
nameKey?: string
|
113 |
+
labelKey?: string
|
114 |
+
}
|
115 |
+
>(
|
116 |
+
(
|
117 |
+
{
|
118 |
+
active,
|
119 |
+
payload,
|
120 |
+
className,
|
121 |
+
indicator = "dot",
|
122 |
+
hideLabel = false,
|
123 |
+
hideIndicator = false,
|
124 |
+
label,
|
125 |
+
labelFormatter,
|
126 |
+
labelClassName,
|
127 |
+
formatter,
|
128 |
+
color,
|
129 |
+
nameKey,
|
130 |
+
labelKey,
|
131 |
+
},
|
132 |
+
ref
|
133 |
+
) => {
|
134 |
+
const { config } = useChart()
|
135 |
+
|
136 |
+
const tooltipLabel = React.useMemo(() => {
|
137 |
+
if (hideLabel || !payload?.length) {
|
138 |
+
return null
|
139 |
+
}
|
140 |
+
|
141 |
+
const [item] = payload
|
142 |
+
const key = `${labelKey || item.dataKey || item.name || "value"}`
|
143 |
+
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
144 |
+
const value =
|
145 |
+
!labelKey && typeof label === "string"
|
146 |
+
? config[label as keyof typeof config]?.label || label
|
147 |
+
: itemConfig?.label
|
148 |
+
|
149 |
+
if (labelFormatter) {
|
150 |
+
return (
|
151 |
+
<div className={cn("font-medium", labelClassName)}>
|
152 |
+
{labelFormatter(value, payload)}
|
153 |
+
</div>
|
154 |
+
)
|
155 |
+
}
|
156 |
+
|
157 |
+
if (!value) {
|
158 |
+
return null
|
159 |
+
}
|
160 |
+
|
161 |
+
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
162 |
+
}, [
|
163 |
+
label,
|
164 |
+
labelFormatter,
|
165 |
+
payload,
|
166 |
+
hideLabel,
|
167 |
+
labelClassName,
|
168 |
+
config,
|
169 |
+
labelKey,
|
170 |
+
])
|
171 |
+
|
172 |
+
if (!active || !payload?.length) {
|
173 |
+
return null
|
174 |
+
}
|
175 |
+
|
176 |
+
const nestLabel = payload.length === 1 && indicator !== "dot"
|
177 |
+
|
178 |
+
return (
|
179 |
+
<div
|
180 |
+
ref={ref}
|
181 |
+
className={cn(
|
182 |
+
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
183 |
+
className
|
184 |
+
)}
|
185 |
+
>
|
186 |
+
{!nestLabel ? tooltipLabel : null}
|
187 |
+
<div className="grid gap-1.5">
|
188 |
+
{payload.map((item, index) => {
|
189 |
+
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
190 |
+
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
191 |
+
const indicatorColor = color || item.payload.fill || item.color
|
192 |
+
|
193 |
+
return (
|
194 |
+
<div
|
195 |
+
key={item.dataKey}
|
196 |
+
className={cn(
|
197 |
+
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
198 |
+
indicator === "dot" && "items-center"
|
199 |
+
)}
|
200 |
+
>
|
201 |
+
{formatter && item?.value !== undefined && item.name ? (
|
202 |
+
formatter(item.value, item.name, item, index, item.payload)
|
203 |
+
) : (
|
204 |
+
<>
|
205 |
+
{itemConfig?.icon ? (
|
206 |
+
<itemConfig.icon />
|
207 |
+
) : (
|
208 |
+
!hideIndicator && (
|
209 |
+
<div
|
210 |
+
className={cn(
|
211 |
+
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
|
212 |
+
{
|
213 |
+
"h-2.5 w-2.5": indicator === "dot",
|
214 |
+
"w-1": indicator === "line",
|
215 |
+
"w-0 border-[1.5px] border-dashed bg-transparent":
|
216 |
+
indicator === "dashed",
|
217 |
+
"my-0.5": nestLabel && indicator === "dashed",
|
218 |
+
}
|
219 |
+
)}
|
220 |
+
style={
|
221 |
+
{
|
222 |
+
"--color-bg": indicatorColor,
|
223 |
+
"--color-border": indicatorColor,
|
224 |
+
} as React.CSSProperties
|
225 |
+
}
|
226 |
+
/>
|
227 |
+
)
|
228 |
+
)}
|
229 |
+
<div
|
230 |
+
className={cn(
|
231 |
+
"flex flex-1 justify-between leading-none",
|
232 |
+
nestLabel ? "items-end" : "items-center"
|
233 |
+
)}
|
234 |
+
>
|
235 |
+
<div className="grid gap-1.5">
|
236 |
+
{nestLabel ? tooltipLabel : null}
|
237 |
+
<span className="text-muted-foreground">
|
238 |
+
{itemConfig?.label || item.name}
|
239 |
+
</span>
|
240 |
+
</div>
|
241 |
+
{item.value && (
|
242 |
+
<span className="font-mono font-medium tabular-nums text-foreground">
|
243 |
+
{item.value.toLocaleString()}
|
244 |
+
</span>
|
245 |
+
)}
|
246 |
+
</div>
|
247 |
+
</>
|
248 |
+
)}
|
249 |
+
</div>
|
250 |
+
)
|
251 |
+
})}
|
252 |
+
</div>
|
253 |
+
</div>
|
254 |
+
)
|
255 |
+
}
|
256 |
+
)
|
257 |
+
ChartTooltipContent.displayName = "ChartTooltip"
|
258 |
+
|
259 |
+
const ChartLegend = RechartsPrimitive.Legend
|
260 |
+
|
261 |
+
const ChartLegendContent = React.forwardRef<
|
262 |
+
HTMLDivElement,
|
263 |
+
React.ComponentProps<"div"> &
|
264 |
+
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
265 |
+
hideIcon?: boolean
|
266 |
+
nameKey?: string
|
267 |
+
}
|
268 |
+
>(
|
269 |
+
(
|
270 |
+
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
|
271 |
+
ref
|
272 |
+
) => {
|
273 |
+
const { config } = useChart()
|
274 |
+
|
275 |
+
if (!payload?.length) {
|
276 |
+
return null
|
277 |
+
}
|
278 |
+
|
279 |
+
return (
|
280 |
+
<div
|
281 |
+
ref={ref}
|
282 |
+
className={cn(
|
283 |
+
"flex items-center justify-center gap-4",
|
284 |
+
verticalAlign === "top" ? "pb-3" : "pt-3",
|
285 |
+
className
|
286 |
+
)}
|
287 |
+
>
|
288 |
+
{payload.map((item) => {
|
289 |
+
const key = `${nameKey || item.dataKey || "value"}`
|
290 |
+
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
291 |
+
|
292 |
+
return (
|
293 |
+
<div
|
294 |
+
key={item.value}
|
295 |
+
className={cn(
|
296 |
+
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
|
297 |
+
)}
|
298 |
+
>
|
299 |
+
{itemConfig?.icon && !hideIcon ? (
|
300 |
+
<itemConfig.icon />
|
301 |
+
) : (
|
302 |
+
<div
|
303 |
+
className="h-2 w-2 shrink-0 rounded-[2px]"
|
304 |
+
style={{
|
305 |
+
backgroundColor: item.color,
|
306 |
+
}}
|
307 |
+
/>
|
308 |
+
)}
|
309 |
+
{itemConfig?.label}
|
310 |
+
</div>
|
311 |
+
)
|
312 |
+
})}
|
313 |
+
</div>
|
314 |
+
)
|
315 |
+
}
|
316 |
+
)
|
317 |
+
ChartLegendContent.displayName = "ChartLegend"
|
318 |
+
|
319 |
+
// Helper to extract item config from a payload.
|
320 |
+
function getPayloadConfigFromPayload(
|
321 |
+
config: ChartConfig,
|
322 |
+
payload: unknown,
|
323 |
+
key: string
|
324 |
+
) {
|
325 |
+
if (typeof payload !== "object" || payload === null) {
|
326 |
+
return undefined
|
327 |
+
}
|
328 |
+
|
329 |
+
const payloadPayload =
|
330 |
+
"payload" in payload &&
|
331 |
+
typeof payload.payload === "object" &&
|
332 |
+
payload.payload !== null
|
333 |
+
? payload.payload
|
334 |
+
: undefined
|
335 |
+
|
336 |
+
let configLabelKey: string = key
|
337 |
+
|
338 |
+
if (
|
339 |
+
key in payload &&
|
340 |
+
typeof payload[key as keyof typeof payload] === "string"
|
341 |
+
) {
|
342 |
+
configLabelKey = payload[key as keyof typeof payload] as string
|
343 |
+
} else if (
|
344 |
+
payloadPayload &&
|
345 |
+
key in payloadPayload &&
|
346 |
+
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
347 |
+
) {
|
348 |
+
configLabelKey = payloadPayload[
|
349 |
+
key as keyof typeof payloadPayload
|
350 |
+
] as string
|
351 |
+
}
|
352 |
+
|
353 |
+
return configLabelKey in config
|
354 |
+
? config[configLabelKey]
|
355 |
+
: config[key as keyof typeof config]
|
356 |
+
}
|
357 |
+
|
358 |
+
export {
|
359 |
+
ChartContainer,
|
360 |
+
ChartTooltip,
|
361 |
+
ChartTooltipContent,
|
362 |
+
ChartLegend,
|
363 |
+
ChartLegendContent,
|
364 |
+
ChartStyle,
|
365 |
+
}
|
examples/cyberpunk-standalone/src/components/ui/checkbox.tsx
ADDED
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
5 |
+
import { Check } from "lucide-react"
|
6 |
+
|
7 |
+
import { cn } from "@/lib/utils"
|
8 |
+
|
9 |
+
const Checkbox = React.forwardRef<
|
10 |
+
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
11 |
+
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
12 |
+
>(({ className, ...props }, ref) => (
|
13 |
+
<CheckboxPrimitive.Root
|
14 |
+
ref={ref}
|
15 |
+
className={cn(
|
16 |
+
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
17 |
+
className
|
18 |
+
)}
|
19 |
+
{...props}
|
20 |
+
>
|
21 |
+
<CheckboxPrimitive.Indicator
|
22 |
+
className={cn("flex items-center justify-center text-current")}
|
23 |
+
>
|
24 |
+
<Check className="h-4 w-4" />
|
25 |
+
</CheckboxPrimitive.Indicator>
|
26 |
+
</CheckboxPrimitive.Root>
|
27 |
+
))
|
28 |
+
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
29 |
+
|
30 |
+
export { Checkbox }
|
examples/cyberpunk-standalone/src/components/ui/collapsible.tsx
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
4 |
+
|
5 |
+
const Collapsible = CollapsiblePrimitive.Root
|
6 |
+
|
7 |
+
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
8 |
+
|
9 |
+
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
10 |
+
|
11 |
+
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
examples/cyberpunk-standalone/src/components/ui/command.tsx
ADDED
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import { type DialogProps } from "@radix-ui/react-dialog"
|
5 |
+
import { Command as CommandPrimitive } from "cmdk"
|
6 |
+
import { Search } from "lucide-react"
|
7 |
+
|
8 |
+
import { cn } from "@/lib/utils"
|
9 |
+
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
10 |
+
|
11 |
+
const Command = React.forwardRef<
|
12 |
+
React.ElementRef<typeof CommandPrimitive>,
|
13 |
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
14 |
+
>(({ className, ...props }, ref) => (
|
15 |
+
<CommandPrimitive
|
16 |
+
ref={ref}
|
17 |
+
className={cn(
|
18 |
+
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
19 |
+
className
|
20 |
+
)}
|
21 |
+
{...props}
|
22 |
+
/>
|
23 |
+
))
|
24 |
+
Command.displayName = CommandPrimitive.displayName
|
25 |
+
|
26 |
+
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
27 |
+
return (
|
28 |
+
<Dialog {...props}>
|
29 |
+
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
30 |
+
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
31 |
+
{children}
|
32 |
+
</Command>
|
33 |
+
</DialogContent>
|
34 |
+
</Dialog>
|
35 |
+
)
|
36 |
+
}
|
37 |
+
|
38 |
+
const CommandInput = React.forwardRef<
|
39 |
+
React.ElementRef<typeof CommandPrimitive.Input>,
|
40 |
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
41 |
+
>(({ className, ...props }, ref) => (
|
42 |
+
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
43 |
+
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
44 |
+
<CommandPrimitive.Input
|
45 |
+
ref={ref}
|
46 |
+
className={cn(
|
47 |
+
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
48 |
+
className
|
49 |
+
)}
|
50 |
+
{...props}
|
51 |
+
/>
|
52 |
+
</div>
|
53 |
+
))
|
54 |
+
|
55 |
+
CommandInput.displayName = CommandPrimitive.Input.displayName
|
56 |
+
|
57 |
+
const CommandList = React.forwardRef<
|
58 |
+
React.ElementRef<typeof CommandPrimitive.List>,
|
59 |
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
60 |
+
>(({ className, ...props }, ref) => (
|
61 |
+
<CommandPrimitive.List
|
62 |
+
ref={ref}
|
63 |
+
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
64 |
+
{...props}
|
65 |
+
/>
|
66 |
+
))
|
67 |
+
|
68 |
+
CommandList.displayName = CommandPrimitive.List.displayName
|
69 |
+
|
70 |
+
const CommandEmpty = React.forwardRef<
|
71 |
+
React.ElementRef<typeof CommandPrimitive.Empty>,
|
72 |
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
73 |
+
>((props, ref) => (
|
74 |
+
<CommandPrimitive.Empty
|
75 |
+
ref={ref}
|
76 |
+
className="py-6 text-center text-sm"
|
77 |
+
{...props}
|
78 |
+
/>
|
79 |
+
))
|
80 |
+
|
81 |
+
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
82 |
+
|
83 |
+
const CommandGroup = React.forwardRef<
|
84 |
+
React.ElementRef<typeof CommandPrimitive.Group>,
|
85 |
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
86 |
+
>(({ className, ...props }, ref) => (
|
87 |
+
<CommandPrimitive.Group
|
88 |
+
ref={ref}
|
89 |
+
className={cn(
|
90 |
+
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
91 |
+
className
|
92 |
+
)}
|
93 |
+
{...props}
|
94 |
+
/>
|
95 |
+
))
|
96 |
+
|
97 |
+
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
98 |
+
|
99 |
+
const CommandSeparator = React.forwardRef<
|
100 |
+
React.ElementRef<typeof CommandPrimitive.Separator>,
|
101 |
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
102 |
+
>(({ className, ...props }, ref) => (
|
103 |
+
<CommandPrimitive.Separator
|
104 |
+
ref={ref}
|
105 |
+
className={cn("-mx-1 h-px bg-border", className)}
|
106 |
+
{...props}
|
107 |
+
/>
|
108 |
+
))
|
109 |
+
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
110 |
+
|
111 |
+
const CommandItem = React.forwardRef<
|
112 |
+
React.ElementRef<typeof CommandPrimitive.Item>,
|
113 |
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
114 |
+
>(({ className, ...props }, ref) => (
|
115 |
+
<CommandPrimitive.Item
|
116 |
+
ref={ref}
|
117 |
+
className={cn(
|
118 |
+
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
119 |
+
className
|
120 |
+
)}
|
121 |
+
{...props}
|
122 |
+
/>
|
123 |
+
))
|
124 |
+
|
125 |
+
CommandItem.displayName = CommandPrimitive.Item.displayName
|
126 |
+
|
127 |
+
const CommandShortcut = ({
|
128 |
+
className,
|
129 |
+
...props
|
130 |
+
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
131 |
+
return (
|
132 |
+
<span
|
133 |
+
className={cn(
|
134 |
+
"ml-auto text-xs tracking-widest text-muted-foreground",
|
135 |
+
className
|
136 |
+
)}
|
137 |
+
{...props}
|
138 |
+
/>
|
139 |
+
)
|
140 |
+
}
|
141 |
+
CommandShortcut.displayName = "CommandShortcut"
|
142 |
+
|
143 |
+
export {
|
144 |
+
Command,
|
145 |
+
CommandDialog,
|
146 |
+
CommandInput,
|
147 |
+
CommandList,
|
148 |
+
CommandEmpty,
|
149 |
+
CommandGroup,
|
150 |
+
CommandItem,
|
151 |
+
CommandShortcut,
|
152 |
+
CommandSeparator,
|
153 |
+
}
|
examples/cyberpunk-standalone/src/components/ui/context-menu.tsx
ADDED
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
5 |
+
import { Check, ChevronRight, Circle } from "lucide-react"
|
6 |
+
|
7 |
+
import { cn } from "@/lib/utils"
|
8 |
+
|
9 |
+
const ContextMenu = ContextMenuPrimitive.Root
|
10 |
+
|
11 |
+
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
|
12 |
+
|
13 |
+
const ContextMenuGroup = ContextMenuPrimitive.Group
|
14 |
+
|
15 |
+
const ContextMenuPortal = ContextMenuPrimitive.Portal
|
16 |
+
|
17 |
+
const ContextMenuSub = ContextMenuPrimitive.Sub
|
18 |
+
|
19 |
+
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
|
20 |
+
|
21 |
+
const ContextMenuSubTrigger = React.forwardRef<
|
22 |
+
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
|
23 |
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
|
24 |
+
inset?: boolean
|
25 |
+
}
|
26 |
+
>(({ className, inset, children, ...props }, ref) => (
|
27 |
+
<ContextMenuPrimitive.SubTrigger
|
28 |
+
ref={ref}
|
29 |
+
className={cn(
|
30 |
+
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
31 |
+
inset && "pl-8",
|
32 |
+
className
|
33 |
+
)}
|
34 |
+
{...props}
|
35 |
+
>
|
36 |
+
{children}
|
37 |
+
<ChevronRight className="ml-auto h-4 w-4" />
|
38 |
+
</ContextMenuPrimitive.SubTrigger>
|
39 |
+
))
|
40 |
+
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
|
41 |
+
|
42 |
+
const ContextMenuSubContent = React.forwardRef<
|
43 |
+
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
|
44 |
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
|
45 |
+
>(({ className, ...props }, ref) => (
|
46 |
+
<ContextMenuPrimitive.SubContent
|
47 |
+
ref={ref}
|
48 |
+
className={cn(
|
49 |
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
50 |
+
className
|
51 |
+
)}
|
52 |
+
{...props}
|
53 |
+
/>
|
54 |
+
))
|
55 |
+
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
|
56 |
+
|
57 |
+
const ContextMenuContent = React.forwardRef<
|
58 |
+
React.ElementRef<typeof ContextMenuPrimitive.Content>,
|
59 |
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
|
60 |
+
>(({ className, ...props }, ref) => (
|
61 |
+
<ContextMenuPrimitive.Portal>
|
62 |
+
<ContextMenuPrimitive.Content
|
63 |
+
ref={ref}
|
64 |
+
className={cn(
|
65 |
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
66 |
+
className
|
67 |
+
)}
|
68 |
+
{...props}
|
69 |
+
/>
|
70 |
+
</ContextMenuPrimitive.Portal>
|
71 |
+
))
|
72 |
+
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
|
73 |
+
|
74 |
+
const ContextMenuItem = React.forwardRef<
|
75 |
+
React.ElementRef<typeof ContextMenuPrimitive.Item>,
|
76 |
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
|
77 |
+
inset?: boolean
|
78 |
+
}
|
79 |
+
>(({ className, inset, ...props }, ref) => (
|
80 |
+
<ContextMenuPrimitive.Item
|
81 |
+
ref={ref}
|
82 |
+
className={cn(
|
83 |
+
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
84 |
+
inset && "pl-8",
|
85 |
+
className
|
86 |
+
)}
|
87 |
+
{...props}
|
88 |
+
/>
|
89 |
+
))
|
90 |
+
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
|
91 |
+
|
92 |
+
const ContextMenuCheckboxItem = React.forwardRef<
|
93 |
+
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
|
94 |
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
|
95 |
+
>(({ className, children, checked, ...props }, ref) => (
|
96 |
+
<ContextMenuPrimitive.CheckboxItem
|
97 |
+
ref={ref}
|
98 |
+
className={cn(
|
99 |
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
100 |
+
className
|
101 |
+
)}
|
102 |
+
checked={checked}
|
103 |
+
{...props}
|
104 |
+
>
|
105 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
106 |
+
<ContextMenuPrimitive.ItemIndicator>
|
107 |
+
<Check className="h-4 w-4" />
|
108 |
+
</ContextMenuPrimitive.ItemIndicator>
|
109 |
+
</span>
|
110 |
+
{children}
|
111 |
+
</ContextMenuPrimitive.CheckboxItem>
|
112 |
+
))
|
113 |
+
ContextMenuCheckboxItem.displayName =
|
114 |
+
ContextMenuPrimitive.CheckboxItem.displayName
|
115 |
+
|
116 |
+
const ContextMenuRadioItem = React.forwardRef<
|
117 |
+
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
|
118 |
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
|
119 |
+
>(({ className, children, ...props }, ref) => (
|
120 |
+
<ContextMenuPrimitive.RadioItem
|
121 |
+
ref={ref}
|
122 |
+
className={cn(
|
123 |
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
124 |
+
className
|
125 |
+
)}
|
126 |
+
{...props}
|
127 |
+
>
|
128 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
129 |
+
<ContextMenuPrimitive.ItemIndicator>
|
130 |
+
<Circle className="h-2 w-2 fill-current" />
|
131 |
+
</ContextMenuPrimitive.ItemIndicator>
|
132 |
+
</span>
|
133 |
+
{children}
|
134 |
+
</ContextMenuPrimitive.RadioItem>
|
135 |
+
))
|
136 |
+
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
|
137 |
+
|
138 |
+
const ContextMenuLabel = React.forwardRef<
|
139 |
+
React.ElementRef<typeof ContextMenuPrimitive.Label>,
|
140 |
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
|
141 |
+
inset?: boolean
|
142 |
+
}
|
143 |
+
>(({ className, inset, ...props }, ref) => (
|
144 |
+
<ContextMenuPrimitive.Label
|
145 |
+
ref={ref}
|
146 |
+
className={cn(
|
147 |
+
"px-2 py-1.5 text-sm font-semibold text-foreground",
|
148 |
+
inset && "pl-8",
|
149 |
+
className
|
150 |
+
)}
|
151 |
+
{...props}
|
152 |
+
/>
|
153 |
+
))
|
154 |
+
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
|
155 |
+
|
156 |
+
const ContextMenuSeparator = React.forwardRef<
|
157 |
+
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
|
158 |
+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
|
159 |
+
>(({ className, ...props }, ref) => (
|
160 |
+
<ContextMenuPrimitive.Separator
|
161 |
+
ref={ref}
|
162 |
+
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
163 |
+
{...props}
|
164 |
+
/>
|
165 |
+
))
|
166 |
+
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
|
167 |
+
|
168 |
+
const ContextMenuShortcut = ({
|
169 |
+
className,
|
170 |
+
...props
|
171 |
+
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
172 |
+
return (
|
173 |
+
<span
|
174 |
+
className={cn(
|
175 |
+
"ml-auto text-xs tracking-widest text-muted-foreground",
|
176 |
+
className
|
177 |
+
)}
|
178 |
+
{...props}
|
179 |
+
/>
|
180 |
+
)
|
181 |
+
}
|
182 |
+
ContextMenuShortcut.displayName = "ContextMenuShortcut"
|
183 |
+
|
184 |
+
export {
|
185 |
+
ContextMenu,
|
186 |
+
ContextMenuTrigger,
|
187 |
+
ContextMenuContent,
|
188 |
+
ContextMenuItem,
|
189 |
+
ContextMenuCheckboxItem,
|
190 |
+
ContextMenuRadioItem,
|
191 |
+
ContextMenuLabel,
|
192 |
+
ContextMenuSeparator,
|
193 |
+
ContextMenuShortcut,
|
194 |
+
ContextMenuGroup,
|
195 |
+
ContextMenuPortal,
|
196 |
+
ContextMenuSub,
|
197 |
+
ContextMenuSubContent,
|
198 |
+
ContextMenuSubTrigger,
|
199 |
+
ContextMenuRadioGroup,
|
200 |
+
}
|
examples/cyberpunk-standalone/src/components/ui/dialog.tsx
ADDED
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
5 |
+
import { X } from "lucide-react"
|
6 |
+
|
7 |
+
import { cn } from "@/lib/utils"
|
8 |
+
|
9 |
+
const Dialog = DialogPrimitive.Root
|
10 |
+
|
11 |
+
const DialogTrigger = DialogPrimitive.Trigger
|
12 |
+
|
13 |
+
const DialogPortal = DialogPrimitive.Portal
|
14 |
+
|
15 |
+
const DialogClose = DialogPrimitive.Close
|
16 |
+
|
17 |
+
const DialogOverlay = React.forwardRef<
|
18 |
+
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
19 |
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
20 |
+
>(({ className, ...props }, ref) => (
|
21 |
+
<DialogPrimitive.Overlay
|
22 |
+
ref={ref}
|
23 |
+
className={cn(
|
24 |
+
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
25 |
+
className
|
26 |
+
)}
|
27 |
+
{...props}
|
28 |
+
/>
|
29 |
+
))
|
30 |
+
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
31 |
+
|
32 |
+
const DialogContent = React.forwardRef<
|
33 |
+
React.ElementRef<typeof DialogPrimitive.Content>,
|
34 |
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
35 |
+
>(({ className, children, ...props }, ref) => (
|
36 |
+
<DialogPortal>
|
37 |
+
<DialogOverlay />
|
38 |
+
<DialogPrimitive.Content
|
39 |
+
ref={ref}
|
40 |
+
className={cn(
|
41 |
+
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
42 |
+
className
|
43 |
+
)}
|
44 |
+
{...props}
|
45 |
+
>
|
46 |
+
{children}
|
47 |
+
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
48 |
+
<X className="h-4 w-4" />
|
49 |
+
<span className="sr-only">Close</span>
|
50 |
+
</DialogPrimitive.Close>
|
51 |
+
</DialogPrimitive.Content>
|
52 |
+
</DialogPortal>
|
53 |
+
))
|
54 |
+
DialogContent.displayName = DialogPrimitive.Content.displayName
|
55 |
+
|
56 |
+
const DialogHeader = ({
|
57 |
+
className,
|
58 |
+
...props
|
59 |
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
60 |
+
<div
|
61 |
+
className={cn(
|
62 |
+
"flex flex-col space-y-1.5 text-center sm:text-left",
|
63 |
+
className
|
64 |
+
)}
|
65 |
+
{...props}
|
66 |
+
/>
|
67 |
+
)
|
68 |
+
DialogHeader.displayName = "DialogHeader"
|
69 |
+
|
70 |
+
const DialogFooter = ({
|
71 |
+
className,
|
72 |
+
...props
|
73 |
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
74 |
+
<div
|
75 |
+
className={cn(
|
76 |
+
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
77 |
+
className
|
78 |
+
)}
|
79 |
+
{...props}
|
80 |
+
/>
|
81 |
+
)
|
82 |
+
DialogFooter.displayName = "DialogFooter"
|
83 |
+
|
84 |
+
const DialogTitle = React.forwardRef<
|
85 |
+
React.ElementRef<typeof DialogPrimitive.Title>,
|
86 |
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
87 |
+
>(({ className, ...props }, ref) => (
|
88 |
+
<DialogPrimitive.Title
|
89 |
+
ref={ref}
|
90 |
+
className={cn(
|
91 |
+
"text-lg font-semibold leading-none tracking-tight",
|
92 |
+
className
|
93 |
+
)}
|
94 |
+
{...props}
|
95 |
+
/>
|
96 |
+
))
|
97 |
+
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
98 |
+
|
99 |
+
const DialogDescription = React.forwardRef<
|
100 |
+
React.ElementRef<typeof DialogPrimitive.Description>,
|
101 |
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
102 |
+
>(({ className, ...props }, ref) => (
|
103 |
+
<DialogPrimitive.Description
|
104 |
+
ref={ref}
|
105 |
+
className={cn("text-sm text-muted-foreground", className)}
|
106 |
+
{...props}
|
107 |
+
/>
|
108 |
+
))
|
109 |
+
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
110 |
+
|
111 |
+
export {
|
112 |
+
Dialog,
|
113 |
+
DialogPortal,
|
114 |
+
DialogOverlay,
|
115 |
+
DialogClose,
|
116 |
+
DialogTrigger,
|
117 |
+
DialogContent,
|
118 |
+
DialogHeader,
|
119 |
+
DialogFooter,
|
120 |
+
DialogTitle,
|
121 |
+
DialogDescription,
|
122 |
+
}
|