emergent-agent-e1 commited on
Commit ·
7a7199f
1
Parent(s): eec94db
auto-commit for 41db1e46-1e00-4de7-81a3-658988d8d311
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .emergent/emergent.yml +3 -0
- .gitignore +82 -0
- README.md +1 -0
- backend/requirements.txt +27 -0
- backend/server.py +89 -0
- design_guidelines.json +106 -0
- frontend/.gitignore +23 -0
- frontend/README.md +70 -0
- frontend/components.json +21 -0
- frontend/craco.config.js +100 -0
- frontend/jsconfig.json +9 -0
- frontend/package.json +91 -0
- frontend/plugins/health-check/health-endpoints.js +213 -0
- frontend/plugins/health-check/webpack-health-plugin.js +120 -0
- frontend/postcss.config.js +6 -0
- frontend/public/index.html +158 -0
- frontend/src/App.css +34 -0
- frontend/src/App.js +54 -0
- frontend/src/components/ui/accordion.jsx +41 -0
- frontend/src/components/ui/alert-dialog.jsx +97 -0
- frontend/src/components/ui/alert.jsx +47 -0
- frontend/src/components/ui/aspect-ratio.jsx +5 -0
- frontend/src/components/ui/avatar.jsx +33 -0
- frontend/src/components/ui/badge.jsx +34 -0
- frontend/src/components/ui/breadcrumb.jsx +92 -0
- frontend/src/components/ui/button.jsx +48 -0
- frontend/src/components/ui/calendar.jsx +71 -0
- frontend/src/components/ui/card.jsx +50 -0
- frontend/src/components/ui/carousel.jsx +193 -0
- frontend/src/components/ui/checkbox.jsx +22 -0
- frontend/src/components/ui/collapsible.jsx +9 -0
- frontend/src/components/ui/command.jsx +116 -0
- frontend/src/components/ui/context-menu.jsx +156 -0
- frontend/src/components/ui/dialog.jsx +94 -0
- frontend/src/components/ui/drawer.jsx +90 -0
- frontend/src/components/ui/dropdown-menu.jsx +156 -0
- frontend/src/components/ui/form.jsx +133 -0
- frontend/src/components/ui/hover-card.jsx +23 -0
- frontend/src/components/ui/input-otp.jsx +53 -0
- frontend/src/components/ui/input.jsx +19 -0
- frontend/src/components/ui/label.jsx +16 -0
- frontend/src/components/ui/menubar.jsx +198 -0
- frontend/src/components/ui/navigation-menu.jsx +104 -0
- frontend/src/components/ui/pagination.jsx +100 -0
- frontend/src/components/ui/popover.jsx +27 -0
- frontend/src/components/ui/progress.jsx +21 -0
- frontend/src/components/ui/radio-group.jsx +29 -0
- frontend/src/components/ui/resizable.jsx +40 -0
- frontend/src/components/ui/scroll-area.jsx +38 -0
- frontend/src/components/ui/select.jsx +119 -0
.emergent/emergent.yml
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"env_image_name": "fastapi_react_mongo_shadcn_base_image_cloud_arm:release-17042026-1"
|
| 3 |
+
}
|
.gitignore
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
| 2 |
+
|
| 3 |
+
# IDE and editors
|
| 4 |
+
.idea/
|
| 5 |
+
.vscode/
|
| 6 |
+
|
| 7 |
+
# Dependencies
|
| 8 |
+
node_modules/
|
| 9 |
+
/node_modules
|
| 10 |
+
/.pnp
|
| 11 |
+
.pnp.js
|
| 12 |
+
.yarn/install-state.gz
|
| 13 |
+
.yarn/*
|
| 14 |
+
!.yarn/patches
|
| 15 |
+
!.yarn/plugins
|
| 16 |
+
!.yarn/releases
|
| 17 |
+
!.yarn/versions
|
| 18 |
+
|
| 19 |
+
# Testing
|
| 20 |
+
/coverage
|
| 21 |
+
|
| 22 |
+
# Next.js
|
| 23 |
+
/.next/
|
| 24 |
+
/out/
|
| 25 |
+
next-env.d.ts
|
| 26 |
+
*.tsbuildinfo
|
| 27 |
+
|
| 28 |
+
# Production builds
|
| 29 |
+
/build
|
| 30 |
+
dist/
|
| 31 |
+
dist
|
| 32 |
+
|
| 33 |
+
# Environment files (comprehensive coverage)
|
| 34 |
+
|
| 35 |
+
*token.json*
|
| 36 |
+
*credentials.json*
|
| 37 |
+
|
| 38 |
+
# Logs and debug files
|
| 39 |
+
npm-debug.log*
|
| 40 |
+
yarn-debug.log*
|
| 41 |
+
yarn-error.log*
|
| 42 |
+
.pnpm-debug.log*
|
| 43 |
+
dump.rdb
|
| 44 |
+
|
| 45 |
+
# System files
|
| 46 |
+
.DS_Store
|
| 47 |
+
*.pem
|
| 48 |
+
|
| 49 |
+
# Python
|
| 50 |
+
__pycache__/
|
| 51 |
+
*pyc*
|
| 52 |
+
venv/
|
| 53 |
+
.venv/
|
| 54 |
+
|
| 55 |
+
# Development tools
|
| 56 |
+
chainlit.md
|
| 57 |
+
.chainlit
|
| 58 |
+
.ipynb_checkpoints/
|
| 59 |
+
.ac
|
| 60 |
+
|
| 61 |
+
# Deployment
|
| 62 |
+
.vercel
|
| 63 |
+
|
| 64 |
+
# Data and databases
|
| 65 |
+
agenthub/agents/youtube/db
|
| 66 |
+
|
| 67 |
+
# Archive files and large assets
|
| 68 |
+
**/*.zip
|
| 69 |
+
**/*.tar.gz
|
| 70 |
+
**/*.tar
|
| 71 |
+
**/*.tgz
|
| 72 |
+
*.pack
|
| 73 |
+
*.deb
|
| 74 |
+
*.dylib
|
| 75 |
+
|
| 76 |
+
# Build caches
|
| 77 |
+
.cache/
|
| 78 |
+
|
| 79 |
+
memory/test_credentials.md
|
| 80 |
+
|
| 81 |
+
# Mobile development
|
| 82 |
+
android-sdk/
|
README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Here are your Instructions
|
backend/requirements.txt
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.110.1
|
| 2 |
+
uvicorn==0.25.0
|
| 3 |
+
boto3>=1.34.129
|
| 4 |
+
requests-oauthlib>=2.0.0
|
| 5 |
+
cryptography>=42.0.8
|
| 6 |
+
python-dotenv>=1.0.1
|
| 7 |
+
pymongo==4.5.0
|
| 8 |
+
pydantic>=2.6.4
|
| 9 |
+
email-validator>=2.2.0
|
| 10 |
+
pyjwt>=2.10.1
|
| 11 |
+
bcrypt==4.1.3
|
| 12 |
+
passlib>=1.7.4
|
| 13 |
+
tzdata>=2024.2
|
| 14 |
+
motor==3.3.1
|
| 15 |
+
pytest>=8.0.0
|
| 16 |
+
black>=24.1.1
|
| 17 |
+
isort>=5.13.2
|
| 18 |
+
flake8>=7.0.0
|
| 19 |
+
mypy>=1.8.0
|
| 20 |
+
python-jose>=3.3.0
|
| 21 |
+
requests>=2.31.0
|
| 22 |
+
pandas>=2.2.0
|
| 23 |
+
numpy>=1.26.0
|
| 24 |
+
python-multipart>=0.0.9
|
| 25 |
+
jq>=1.6.0
|
| 26 |
+
typer>=0.9.0
|
| 27 |
+
emergentintegrations==0.1.0
|
backend/server.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, APIRouter
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
from starlette.middleware.cors import CORSMiddleware
|
| 4 |
+
from motor.motor_asyncio import AsyncIOMotorClient
|
| 5 |
+
import os
|
| 6 |
+
import logging
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from pydantic import BaseModel, Field, ConfigDict
|
| 9 |
+
from typing import List
|
| 10 |
+
import uuid
|
| 11 |
+
from datetime import datetime, timezone
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
ROOT_DIR = Path(__file__).parent
|
| 15 |
+
load_dotenv(ROOT_DIR / '.env')
|
| 16 |
+
|
| 17 |
+
# MongoDB connection
|
| 18 |
+
mongo_url = os.environ['MONGO_URL']
|
| 19 |
+
client = AsyncIOMotorClient(mongo_url)
|
| 20 |
+
db = client[os.environ['DB_NAME']]
|
| 21 |
+
|
| 22 |
+
# Create the main app without a prefix
|
| 23 |
+
app = FastAPI()
|
| 24 |
+
|
| 25 |
+
# Create a router with the /api prefix
|
| 26 |
+
api_router = APIRouter(prefix="/api")
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
# Define Models
|
| 30 |
+
class StatusCheck(BaseModel):
|
| 31 |
+
model_config = ConfigDict(extra="ignore") # Ignore MongoDB's _id field
|
| 32 |
+
|
| 33 |
+
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
| 34 |
+
client_name: str
|
| 35 |
+
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
| 36 |
+
|
| 37 |
+
class StatusCheckCreate(BaseModel):
|
| 38 |
+
client_name: str
|
| 39 |
+
|
| 40 |
+
# Add your routes to the router instead of directly to app
|
| 41 |
+
@api_router.get("/")
|
| 42 |
+
async def root():
|
| 43 |
+
return {"message": "Hello World"}
|
| 44 |
+
|
| 45 |
+
@api_router.post("/status", response_model=StatusCheck)
|
| 46 |
+
async def create_status_check(input: StatusCheckCreate):
|
| 47 |
+
status_dict = input.model_dump()
|
| 48 |
+
status_obj = StatusCheck(**status_dict)
|
| 49 |
+
|
| 50 |
+
# Convert to dict and serialize datetime to ISO string for MongoDB
|
| 51 |
+
doc = status_obj.model_dump()
|
| 52 |
+
doc['timestamp'] = doc['timestamp'].isoformat()
|
| 53 |
+
|
| 54 |
+
_ = await db.status_checks.insert_one(doc)
|
| 55 |
+
return status_obj
|
| 56 |
+
|
| 57 |
+
@api_router.get("/status", response_model=List[StatusCheck])
|
| 58 |
+
async def get_status_checks():
|
| 59 |
+
# Exclude MongoDB's _id field from the query results
|
| 60 |
+
status_checks = await db.status_checks.find({}, {"_id": 0}).to_list(1000)
|
| 61 |
+
|
| 62 |
+
# Convert ISO string timestamps back to datetime objects
|
| 63 |
+
for check in status_checks:
|
| 64 |
+
if isinstance(check['timestamp'], str):
|
| 65 |
+
check['timestamp'] = datetime.fromisoformat(check['timestamp'])
|
| 66 |
+
|
| 67 |
+
return status_checks
|
| 68 |
+
|
| 69 |
+
# Include the router in the main app
|
| 70 |
+
app.include_router(api_router)
|
| 71 |
+
|
| 72 |
+
app.add_middleware(
|
| 73 |
+
CORSMiddleware,
|
| 74 |
+
allow_credentials=True,
|
| 75 |
+
allow_origins=os.environ.get('CORS_ORIGINS', '*').split(','),
|
| 76 |
+
allow_methods=["*"],
|
| 77 |
+
allow_headers=["*"],
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
# Configure logging
|
| 81 |
+
logging.basicConfig(
|
| 82 |
+
level=logging.INFO,
|
| 83 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 84 |
+
)
|
| 85 |
+
logger = logging.getLogger(__name__)
|
| 86 |
+
|
| 87 |
+
@app.on_event("shutdown")
|
| 88 |
+
async def shutdown_db_client():
|
| 89 |
+
client.close()
|
design_guidelines.json
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"project_name": "ForgeSight",
|
| 3 |
+
"domain": "Manufacturing / AI Quality Control / Developer Tool",
|
| 4 |
+
"theme": "Dark",
|
| 5 |
+
"archetype": "Archetype 4 (Swiss & High-Contrast) - Technical Brutalist variant",
|
| 6 |
+
"mood": "Precise, authoritative, industrial, hardware-optimized, slightly brutalist",
|
| 7 |
+
"colors": {
|
| 8 |
+
"background_base": "#0A0A0A",
|
| 9 |
+
"background_surface": "#141416",
|
| 10 |
+
"primary_accent": "#ED1C24",
|
| 11 |
+
"primary_hover": "#FF3B30",
|
| 12 |
+
"border_default": "rgba(255,255,255,0.1)",
|
| 13 |
+
"text_primary": "#FFFFFF",
|
| 14 |
+
"text_secondary": "#A1A1AA",
|
| 15 |
+
"status": {
|
| 16 |
+
"pass": "#10B981",
|
| 17 |
+
"warn": "#F59E0B",
|
| 18 |
+
"fail": "#ED1C24"
|
| 19 |
+
}
|
| 20 |
+
},
|
| 21 |
+
"typography": {
|
| 22 |
+
"headings": {
|
| 23 |
+
"family": "Chivo",
|
| 24 |
+
"weights": [
|
| 25 |
+
"300",
|
| 26 |
+
"900"
|
| 27 |
+
],
|
| 28 |
+
"classes_h1": "text-4xl sm:text-5xl lg:text-6xl font-black tracking-tighter leading-none text-white",
|
| 29 |
+
"classes_h2": "text-2xl sm:text-3xl lg:text-4xl font-bold tracking-tight text-white",
|
| 30 |
+
"classes_h3": "text-xl sm:text-2xl font-semibold text-white"
|
| 31 |
+
},
|
| 32 |
+
"body": {
|
| 33 |
+
"family": "IBM Plex Sans",
|
| 34 |
+
"weights": [
|
| 35 |
+
"400",
|
| 36 |
+
"500"
|
| 37 |
+
],
|
| 38 |
+
"classes": "text-base leading-relaxed tracking-normal text-zinc-300"
|
| 39 |
+
},
|
| 40 |
+
"mono": {
|
| 41 |
+
"family": "JetBrains Mono",
|
| 42 |
+
"weights": [
|
| 43 |
+
"400",
|
| 44 |
+
"700"
|
| 45 |
+
],
|
| 46 |
+
"classes": "text-sm tracking-tight font-mono text-zinc-300"
|
| 47 |
+
},
|
| 48 |
+
"labels": {
|
| 49 |
+
"classes": "text-xs font-mono uppercase tracking-[0.2em] text-zinc-400"
|
| 50 |
+
}
|
| 51 |
+
},
|
| 52 |
+
"layout_and_spacing": {
|
| 53 |
+
"philosophy": "Industrial-HMI meets Linear. Dense information with purposeful whitespace and strict grid alignments.",
|
| 54 |
+
"bento_grid": {
|
| 55 |
+
"dashboard_mode": "Control Room Grid (Tight gaps: gap-4 or gap-6, col-span logic for metrics vs feeds)",
|
| 56 |
+
"marketing_mode": "Tetris Grid (Asymmetric, wide gaps: gap-8, col-span-full for hero elements)"
|
| 57 |
+
},
|
| 58 |
+
"spacing_scale": "Tailwind standard (p-4, p-6, p-8). Cards must use internal p-6. Footers use py-24."
|
| 59 |
+
},
|
| 60 |
+
"surfaces": {
|
| 61 |
+
"dashboards": {
|
| 62 |
+
"style": "Grid Borders (Technical Look)",
|
| 63 |
+
"rules": "Expose the skeleton. border-collapse layouts, 1px subtle borders (border-white/10), flat backgrounds, no soft shadows."
|
| 64 |
+
},
|
| 65 |
+
"agent_console": {
|
| 66 |
+
"style": "Retro-Futurism / Terminal HMI",
|
| 67 |
+
"rules": "Deep black #000000 background, neon red borders for active states, monospace fonts, CSS repeating-linear-gradient for subtle scanlines."
|
| 68 |
+
},
|
| 69 |
+
"navigation": {
|
| 70 |
+
"style": "Solid / Linear",
|
| 71 |
+
"rules": "Solid #0A0A0A background with hard 1px bottom border (border-white/10). Pill-style route switcher."
|
| 72 |
+
}
|
| 73 |
+
},
|
| 74 |
+
"components_strategy": {
|
| 75 |
+
"html_tailwind": "Use for Marketing/Hero sections (custom pure HTML/Tailwind) for maximum asymmetry.",
|
| 76 |
+
"shadcn_ui": "Use heavily customized Shadcn for App/Dashboard interactions (Tabs, Dropdowns, Tables, Cards). Override soft corners to max rounded-sm (2px) or rounded-none.",
|
| 77 |
+
"buttons": "Flat, angular. Primary CTA uses AMD Red background, white text. No rounded-full (use rounded-none or rounded-sm). Hover state must transition background to lighter red (#FF3B30).",
|
| 78 |
+
"animations": "Minimalist micro-interactions. Blinking cursor for agent streaming. Smooth fade-ins for bento cards."
|
| 79 |
+
},
|
| 80 |
+
"media_and_images": {
|
| 81 |
+
"hero_inspection": {
|
| 82 |
+
"url": "https://static.prod-images.emergentagent.com/jobs/d5829a2e-bc03-4880-adcd-73acc809a3bd/images/184a8bf32b150669152ea3aa72546730d8caad845b1b8eb0233eeb35e4255eeb.png",
|
| 83 |
+
"description": "Macro shot of industrial inspection. Use as background for the main hero section with a bg-black/60 overlay for text contrast."
|
| 84 |
+
},
|
| 85 |
+
"blueprint_architecture": {
|
| 86 |
+
"url": "https://static.prod-images.emergentagent.com/jobs/d5829a2e-bc03-4880-adcd-73acc809a3bd/images/7251062dc0e36ea4218374b05cc959bc4e6c55a2cf4789a8a2cbc38db6392916.png",
|
| 87 |
+
"description": "Abstract 3D AMD MI300X chip architecture. Use in the 'Deployment Blueprint' section."
|
| 88 |
+
},
|
| 89 |
+
"factory_feed": {
|
| 90 |
+
"url": "https://images.unsplash.com/photo-1720036237334-9263cd28c3d4?crop=entropy&cs=srgb&fm=jpg&ixid=M3w3NTY2Njl8MHwxfHNlYXJjaHwyfHxpbmR1c3RyaWFsJTIwbWFudWZhY3R1cmluZyUyMGFzc2VtYmx5JTIwbGluZSUyMGRhcmt8ZW58MHx8fHwxNzc3NjQ5MzQzfDA&ixlib=rb-4.1.0&q=85",
|
| 91 |
+
"description": "Background for Defect Feed empty states or generic manufacturing imagery."
|
| 92 |
+
}
|
| 93 |
+
},
|
| 94 |
+
"icons_and_testing": {
|
| 95 |
+
"icon_library": "lucide-react (Default, fine for technical interfaces)",
|
| 96 |
+
"testing": "All interactive elements MUST have data-testid attributes (e.g. data-testid='upload-inspection-button')."
|
| 97 |
+
},
|
| 98 |
+
"instructions_to_main_agent": [
|
| 99 |
+
"Build the app as a hybrid: Marketing Hero at the top, followed by the Dashboard Console below.",
|
| 100 |
+
"Do NOT use the color teal, purple, or generic SaaS blue anywhere.",
|
| 101 |
+
"Enforce the 'Grid Borders' surface look for the dashboard parts to make it feel like an industrial HMI.",
|
| 102 |
+
"For the Agent Console, implement a typing/streaming effect with JetBrains Mono to simulate live inference.",
|
| 103 |
+
"Use data-testid on every interactive UI piece.",
|
| 104 |
+
"In the Blueprint diagram section, use standard CSS grid to layer the visual elements over the blueprint_architecture image."
|
| 105 |
+
]
|
| 106 |
+
}
|
frontend/.gitignore
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
| 2 |
+
|
| 3 |
+
# dependencies
|
| 4 |
+
/node_modules
|
| 5 |
+
/.pnp
|
| 6 |
+
.pnp.js
|
| 7 |
+
|
| 8 |
+
# testing
|
| 9 |
+
/coverage
|
| 10 |
+
|
| 11 |
+
# production
|
| 12 |
+
/build
|
| 13 |
+
|
| 14 |
+
# misc
|
| 15 |
+
.DS_Store
|
| 16 |
+
.env.local
|
| 17 |
+
.env.development.local
|
| 18 |
+
.env.test.local
|
| 19 |
+
.env.production.local
|
| 20 |
+
|
| 21 |
+
npm-debug.log*
|
| 22 |
+
yarn-debug.log*
|
| 23 |
+
yarn-error.log*
|
frontend/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Getting Started with Create React App
|
| 2 |
+
|
| 3 |
+
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
| 4 |
+
|
| 5 |
+
## Available Scripts
|
| 6 |
+
|
| 7 |
+
In the project directory, you can run:
|
| 8 |
+
|
| 9 |
+
### `npm start`
|
| 10 |
+
|
| 11 |
+
Runs the app in the development mode.\
|
| 12 |
+
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
| 13 |
+
|
| 14 |
+
The page will reload when you make changes.\
|
| 15 |
+
You may also see any lint errors in the console.
|
| 16 |
+
|
| 17 |
+
### `npm test`
|
| 18 |
+
|
| 19 |
+
Launches the test runner in the interactive watch mode.\
|
| 20 |
+
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
| 21 |
+
|
| 22 |
+
### `npm run build`
|
| 23 |
+
|
| 24 |
+
Builds the app for production to the `build` folder.\
|
| 25 |
+
It correctly bundles React in production mode and optimizes the build for the best performance.
|
| 26 |
+
|
| 27 |
+
The build is minified and the filenames include the hashes.\
|
| 28 |
+
Your app is ready to be deployed!
|
| 29 |
+
|
| 30 |
+
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
| 31 |
+
|
| 32 |
+
### `npm run eject`
|
| 33 |
+
|
| 34 |
+
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
| 35 |
+
|
| 36 |
+
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
| 37 |
+
|
| 38 |
+
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
|
| 39 |
+
|
| 40 |
+
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
|
| 41 |
+
|
| 42 |
+
## Learn More
|
| 43 |
+
|
| 44 |
+
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
| 45 |
+
|
| 46 |
+
To learn React, check out the [React documentation](https://reactjs.org/).
|
| 47 |
+
|
| 48 |
+
### Code Splitting
|
| 49 |
+
|
| 50 |
+
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
| 51 |
+
|
| 52 |
+
### Analyzing the Bundle Size
|
| 53 |
+
|
| 54 |
+
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
| 55 |
+
|
| 56 |
+
### Making a Progressive Web App
|
| 57 |
+
|
| 58 |
+
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
| 59 |
+
|
| 60 |
+
### Advanced Configuration
|
| 61 |
+
|
| 62 |
+
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
| 63 |
+
|
| 64 |
+
### Deployment
|
| 65 |
+
|
| 66 |
+
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
| 67 |
+
|
| 68 |
+
### `npm run build` fails to minify
|
| 69 |
+
|
| 70 |
+
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
frontend/components.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://ui.shadcn.com/schema.json",
|
| 3 |
+
"style": "new-york",
|
| 4 |
+
"rsc": false,
|
| 5 |
+
"tsx": false,
|
| 6 |
+
"tailwind": {
|
| 7 |
+
"config": "tailwind.config.js",
|
| 8 |
+
"css": "src/index.css",
|
| 9 |
+
"baseColor": "neutral",
|
| 10 |
+
"cssVariables": true,
|
| 11 |
+
"prefix": ""
|
| 12 |
+
},
|
| 13 |
+
"aliases": {
|
| 14 |
+
"components": "@/components",
|
| 15 |
+
"utils": "@/lib/utils",
|
| 16 |
+
"ui": "@/components/ui",
|
| 17 |
+
"lib": "@/lib",
|
| 18 |
+
"hooks": "@/hooks"
|
| 19 |
+
},
|
| 20 |
+
"iconLibrary": "lucide"
|
| 21 |
+
}
|
frontend/craco.config.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// craco.config.js
|
| 2 |
+
const path = require("path");
|
| 3 |
+
require("dotenv").config();
|
| 4 |
+
|
| 5 |
+
// Check if we're in development/preview mode (not production build)
|
| 6 |
+
// Craco sets NODE_ENV=development for start, NODE_ENV=production for build
|
| 7 |
+
const isDevServer = process.env.NODE_ENV !== "production";
|
| 8 |
+
|
| 9 |
+
// Environment variable overrides
|
| 10 |
+
const config = {
|
| 11 |
+
enableHealthCheck: process.env.ENABLE_HEALTH_CHECK === "true",
|
| 12 |
+
};
|
| 13 |
+
|
| 14 |
+
// Conditionally load health check modules only if enabled
|
| 15 |
+
let WebpackHealthPlugin;
|
| 16 |
+
let setupHealthEndpoints;
|
| 17 |
+
let healthPluginInstance;
|
| 18 |
+
|
| 19 |
+
if (config.enableHealthCheck) {
|
| 20 |
+
WebpackHealthPlugin = require("./plugins/health-check/webpack-health-plugin");
|
| 21 |
+
setupHealthEndpoints = require("./plugins/health-check/health-endpoints");
|
| 22 |
+
healthPluginInstance = new WebpackHealthPlugin();
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
let webpackConfig = {
|
| 26 |
+
eslint: {
|
| 27 |
+
configure: {
|
| 28 |
+
extends: ["plugin:react-hooks/recommended"],
|
| 29 |
+
rules: {
|
| 30 |
+
"react-hooks/rules-of-hooks": "error",
|
| 31 |
+
"react-hooks/exhaustive-deps": "warn",
|
| 32 |
+
},
|
| 33 |
+
},
|
| 34 |
+
},
|
| 35 |
+
webpack: {
|
| 36 |
+
alias: {
|
| 37 |
+
'@': path.resolve(__dirname, 'src'),
|
| 38 |
+
},
|
| 39 |
+
configure: (webpackConfig) => {
|
| 40 |
+
|
| 41 |
+
// Add ignored patterns to reduce watched directories
|
| 42 |
+
webpackConfig.watchOptions = {
|
| 43 |
+
...webpackConfig.watchOptions,
|
| 44 |
+
ignored: [
|
| 45 |
+
'**/node_modules/**',
|
| 46 |
+
'**/.git/**',
|
| 47 |
+
'**/build/**',
|
| 48 |
+
'**/dist/**',
|
| 49 |
+
'**/coverage/**',
|
| 50 |
+
'**/public/**',
|
| 51 |
+
],
|
| 52 |
+
};
|
| 53 |
+
|
| 54 |
+
// Add health check plugin to webpack if enabled
|
| 55 |
+
if (config.enableHealthCheck && healthPluginInstance) {
|
| 56 |
+
webpackConfig.plugins.push(healthPluginInstance);
|
| 57 |
+
}
|
| 58 |
+
return webpackConfig;
|
| 59 |
+
},
|
| 60 |
+
},
|
| 61 |
+
};
|
| 62 |
+
|
| 63 |
+
webpackConfig.devServer = (devServerConfig) => {
|
| 64 |
+
// Add health check endpoints if enabled
|
| 65 |
+
if (config.enableHealthCheck && setupHealthEndpoints && healthPluginInstance) {
|
| 66 |
+
const originalSetupMiddlewares = devServerConfig.setupMiddlewares;
|
| 67 |
+
|
| 68 |
+
devServerConfig.setupMiddlewares = (middlewares, devServer) => {
|
| 69 |
+
// Call original setup if exists
|
| 70 |
+
if (originalSetupMiddlewares) {
|
| 71 |
+
middlewares = originalSetupMiddlewares(middlewares, devServer);
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
// Setup health endpoints
|
| 75 |
+
setupHealthEndpoints(devServer, healthPluginInstance);
|
| 76 |
+
|
| 77 |
+
return middlewares;
|
| 78 |
+
};
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
return devServerConfig;
|
| 82 |
+
};
|
| 83 |
+
|
| 84 |
+
// Wrap with visual edits (automatically adds babel plugin, dev server, and overlay in dev mode)
|
| 85 |
+
if (isDevServer) {
|
| 86 |
+
try {
|
| 87 |
+
const { withVisualEdits } = require("@emergentbase/visual-edits/craco");
|
| 88 |
+
webpackConfig = withVisualEdits(webpackConfig);
|
| 89 |
+
} catch (err) {
|
| 90 |
+
if (err.code === 'MODULE_NOT_FOUND' && err.message.includes('@emergentbase/visual-edits/craco')) {
|
| 91 |
+
console.warn(
|
| 92 |
+
"[visual-edits] @emergentbase/visual-edits not installed — visual editing disabled."
|
| 93 |
+
);
|
| 94 |
+
} else {
|
| 95 |
+
throw err;
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
module.exports = webpackConfig;
|
frontend/jsconfig.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"baseUrl": ".",
|
| 4 |
+
"paths": {
|
| 5 |
+
"@/*": ["src/*"]
|
| 6 |
+
}
|
| 7 |
+
},
|
| 8 |
+
"include": ["src"]
|
| 9 |
+
}
|
frontend/package.json
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "frontend",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"dependencies": {
|
| 6 |
+
"@hookform/resolvers": "^5.0.1",
|
| 7 |
+
"@radix-ui/react-accordion": "^1.2.8",
|
| 8 |
+
"@radix-ui/react-alert-dialog": "^1.1.11",
|
| 9 |
+
"@radix-ui/react-aspect-ratio": "^1.1.4",
|
| 10 |
+
"@radix-ui/react-avatar": "^1.1.7",
|
| 11 |
+
"@radix-ui/react-checkbox": "^1.2.3",
|
| 12 |
+
"@radix-ui/react-collapsible": "^1.1.8",
|
| 13 |
+
"@radix-ui/react-context-menu": "^2.2.12",
|
| 14 |
+
"@radix-ui/react-dialog": "^1.1.11",
|
| 15 |
+
"@radix-ui/react-dropdown-menu": "^2.1.12",
|
| 16 |
+
"@radix-ui/react-hover-card": "^1.1.11",
|
| 17 |
+
"@radix-ui/react-label": "^2.1.4",
|
| 18 |
+
"@radix-ui/react-menubar": "^1.1.12",
|
| 19 |
+
"@radix-ui/react-navigation-menu": "^1.2.10",
|
| 20 |
+
"@radix-ui/react-popover": "^1.1.11",
|
| 21 |
+
"@radix-ui/react-progress": "^1.1.4",
|
| 22 |
+
"@radix-ui/react-radio-group": "^1.3.4",
|
| 23 |
+
"@radix-ui/react-scroll-area": "^1.2.6",
|
| 24 |
+
"@radix-ui/react-select": "^2.2.2",
|
| 25 |
+
"@radix-ui/react-separator": "^1.1.4",
|
| 26 |
+
"@radix-ui/react-slider": "^1.3.2",
|
| 27 |
+
"@radix-ui/react-slot": "^1.2.0",
|
| 28 |
+
"@radix-ui/react-switch": "^1.2.2",
|
| 29 |
+
"@radix-ui/react-tabs": "^1.1.9",
|
| 30 |
+
"@radix-ui/react-toast": "^1.2.11",
|
| 31 |
+
"@radix-ui/react-toggle": "^1.1.6",
|
| 32 |
+
"@radix-ui/react-toggle-group": "^1.1.7",
|
| 33 |
+
"@radix-ui/react-tooltip": "^1.2.4",
|
| 34 |
+
"axios": "^1.8.4",
|
| 35 |
+
"class-variance-authority": "^0.7.1",
|
| 36 |
+
"clsx": "^2.1.1",
|
| 37 |
+
"cmdk": "^1.1.1",
|
| 38 |
+
"cra-template": "1.2.0",
|
| 39 |
+
"date-fns": "^4.1.0",
|
| 40 |
+
"embla-carousel-react": "^8.6.0",
|
| 41 |
+
"input-otp": "^1.4.2",
|
| 42 |
+
"lucide-react": "^0.507.0",
|
| 43 |
+
"next-themes": "^0.4.6",
|
| 44 |
+
"react": "^19.0.0",
|
| 45 |
+
"react-day-picker": "8.10.1",
|
| 46 |
+
"react-dom": "^19.0.0",
|
| 47 |
+
"react-hook-form": "^7.56.2",
|
| 48 |
+
"react-resizable-panels": "^3.0.1",
|
| 49 |
+
"react-router-dom": "^7.5.1",
|
| 50 |
+
"react-scripts": "5.0.1",
|
| 51 |
+
"recharts": "^3.6.0",
|
| 52 |
+
"sonner": "^2.0.3",
|
| 53 |
+
"tailwind-merge": "^3.2.0",
|
| 54 |
+
"tailwindcss-animate": "^1.0.7",
|
| 55 |
+
"vaul": "^1.1.2",
|
| 56 |
+
"zod": "^3.24.4"
|
| 57 |
+
},
|
| 58 |
+
"scripts": {
|
| 59 |
+
"start": "craco start",
|
| 60 |
+
"build": "craco build",
|
| 61 |
+
"test": "craco test"
|
| 62 |
+
},
|
| 63 |
+
"browserslist": {
|
| 64 |
+
"production": [
|
| 65 |
+
">0.2%",
|
| 66 |
+
"not dead",
|
| 67 |
+
"not op_mini all"
|
| 68 |
+
],
|
| 69 |
+
"development": [
|
| 70 |
+
"last 1 chrome version",
|
| 71 |
+
"last 1 firefox version",
|
| 72 |
+
"last 1 safari version"
|
| 73 |
+
]
|
| 74 |
+
},
|
| 75 |
+
"devDependencies": {
|
| 76 |
+
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
| 77 |
+
"@craco/craco": "^7.1.0",
|
| 78 |
+
"@emergentbase/visual-edits": "https://assets.emergent.sh/npm/emergentbase-visual-edits-1.0.8.tgz",
|
| 79 |
+
"@eslint/js": "9.23.0",
|
| 80 |
+
"autoprefixer": "^10.4.20",
|
| 81 |
+
"eslint": "9.23.0",
|
| 82 |
+
"eslint-plugin-import": "2.31.0",
|
| 83 |
+
"eslint-plugin-jsx-a11y": "6.10.2",
|
| 84 |
+
"eslint-plugin-react": "7.37.4",
|
| 85 |
+
"eslint-plugin-react-hooks": "5.2.0",
|
| 86 |
+
"globals": "15.15.0",
|
| 87 |
+
"postcss": "^8.4.49",
|
| 88 |
+
"tailwindcss": "^3.4.17"
|
| 89 |
+
},
|
| 90 |
+
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
| 91 |
+
}
|
frontend/plugins/health-check/health-endpoints.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// health-endpoints.js
|
| 2 |
+
// API endpoints for health checks and monitoring
|
| 3 |
+
|
| 4 |
+
const os = require('os');
|
| 5 |
+
|
| 6 |
+
const SERVER_START_TIME = Date.now();
|
| 7 |
+
|
| 8 |
+
/**
|
| 9 |
+
* Setup health check endpoints on the dev server
|
| 10 |
+
* @param {Object} devServer - Webpack dev server instance
|
| 11 |
+
* @param {Object} healthPlugin - Instance of WebpackHealthPlugin
|
| 12 |
+
*/
|
| 13 |
+
function setupHealthEndpoints(devServer, healthPlugin) {
|
| 14 |
+
if (!devServer || !devServer.app) {
|
| 15 |
+
console.warn('[Health Check] Dev server not available, skipping health endpoints');
|
| 16 |
+
return;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
if (!healthPlugin) {
|
| 20 |
+
console.warn('[Health Check] Health plugin not provided, skipping health endpoints');
|
| 21 |
+
return;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
console.log('[Health Check] Setting up health endpoints...');
|
| 25 |
+
|
| 26 |
+
// ====================================================================
|
| 27 |
+
// GET /health - Detailed health status (JSON)
|
| 28 |
+
// ====================================================================
|
| 29 |
+
devServer.app.get("/health", (req, res) => {
|
| 30 |
+
const webpackStatus = healthPlugin.getStatus();
|
| 31 |
+
const uptime = Date.now() - SERVER_START_TIME;
|
| 32 |
+
const memUsage = process.memoryUsage();
|
| 33 |
+
|
| 34 |
+
res.json({
|
| 35 |
+
status: webpackStatus.isHealthy ? 'healthy' : 'unhealthy',
|
| 36 |
+
timestamp: new Date().toISOString(),
|
| 37 |
+
uptime: {
|
| 38 |
+
seconds: Math.floor(uptime / 1000),
|
| 39 |
+
formatted: formatDuration(uptime),
|
| 40 |
+
},
|
| 41 |
+
webpack: {
|
| 42 |
+
state: webpackStatus.state,
|
| 43 |
+
isHealthy: webpackStatus.isHealthy,
|
| 44 |
+
hasCompiled: webpackStatus.hasCompiled,
|
| 45 |
+
errors: webpackStatus.errorCount,
|
| 46 |
+
warnings: webpackStatus.warningCount,
|
| 47 |
+
lastCompileTime: webpackStatus.lastCompileTime
|
| 48 |
+
? new Date(webpackStatus.lastCompileTime).toISOString()
|
| 49 |
+
: null,
|
| 50 |
+
lastSuccessTime: webpackStatus.lastSuccessTime
|
| 51 |
+
? new Date(webpackStatus.lastSuccessTime).toISOString()
|
| 52 |
+
: null,
|
| 53 |
+
compileDuration: webpackStatus.compileDuration
|
| 54 |
+
? `${webpackStatus.compileDuration}ms`
|
| 55 |
+
: null,
|
| 56 |
+
totalCompiles: webpackStatus.totalCompiles,
|
| 57 |
+
firstCompileTime: webpackStatus.firstCompileTime
|
| 58 |
+
? new Date(webpackStatus.firstCompileTime).toISOString()
|
| 59 |
+
: null,
|
| 60 |
+
},
|
| 61 |
+
server: {
|
| 62 |
+
nodeVersion: process.version,
|
| 63 |
+
platform: os.platform(),
|
| 64 |
+
arch: os.arch(),
|
| 65 |
+
cpus: os.cpus().length,
|
| 66 |
+
memory: {
|
| 67 |
+
heapUsed: formatBytes(memUsage.heapUsed),
|
| 68 |
+
heapTotal: formatBytes(memUsage.heapTotal),
|
| 69 |
+
rss: formatBytes(memUsage.rss),
|
| 70 |
+
external: formatBytes(memUsage.external),
|
| 71 |
+
},
|
| 72 |
+
systemMemory: {
|
| 73 |
+
total: formatBytes(os.totalmem()),
|
| 74 |
+
free: formatBytes(os.freemem()),
|
| 75 |
+
used: formatBytes(os.totalmem() - os.freemem()),
|
| 76 |
+
},
|
| 77 |
+
},
|
| 78 |
+
environment: process.env.NODE_ENV || 'development',
|
| 79 |
+
});
|
| 80 |
+
});
|
| 81 |
+
|
| 82 |
+
// ====================================================================
|
| 83 |
+
// GET /health/simple - Simple text response (OK/COMPILING/ERROR)
|
| 84 |
+
// ====================================================================
|
| 85 |
+
devServer.app.get("/health/simple", (req, res) => {
|
| 86 |
+
const webpackStatus = healthPlugin.getSimpleStatus();
|
| 87 |
+
|
| 88 |
+
if (webpackStatus.state === 'success') {
|
| 89 |
+
res.status(200).send('OK');
|
| 90 |
+
} else if (webpackStatus.state === 'compiling') {
|
| 91 |
+
res.status(200).send('COMPILING');
|
| 92 |
+
} else if (webpackStatus.state === 'idle') {
|
| 93 |
+
res.status(200).send('IDLE');
|
| 94 |
+
} else {
|
| 95 |
+
res.status(503).send('ERROR');
|
| 96 |
+
}
|
| 97 |
+
});
|
| 98 |
+
|
| 99 |
+
// ====================================================================
|
| 100 |
+
// GET /health/ready - Readiness check (Kubernetes/load balancer)
|
| 101 |
+
// ====================================================================
|
| 102 |
+
devServer.app.get("/health/ready", (req, res) => {
|
| 103 |
+
const webpackStatus = healthPlugin.getSimpleStatus();
|
| 104 |
+
|
| 105 |
+
if (webpackStatus.state === 'success') {
|
| 106 |
+
res.status(200).json({
|
| 107 |
+
ready: true,
|
| 108 |
+
state: webpackStatus.state,
|
| 109 |
+
});
|
| 110 |
+
} else {
|
| 111 |
+
res.status(503).json({
|
| 112 |
+
ready: false,
|
| 113 |
+
state: webpackStatus.state,
|
| 114 |
+
reason: webpackStatus.state === 'compiling'
|
| 115 |
+
? 'Compilation in progress'
|
| 116 |
+
: 'Compilation failed',
|
| 117 |
+
});
|
| 118 |
+
}
|
| 119 |
+
});
|
| 120 |
+
|
| 121 |
+
// ====================================================================
|
| 122 |
+
// GET /health/live - Liveness check (Kubernetes)
|
| 123 |
+
// ====================================================================
|
| 124 |
+
devServer.app.get("/health/live", (req, res) => {
|
| 125 |
+
res.status(200).json({
|
| 126 |
+
alive: true,
|
| 127 |
+
timestamp: new Date().toISOString(),
|
| 128 |
+
});
|
| 129 |
+
});
|
| 130 |
+
|
| 131 |
+
// ====================================================================
|
| 132 |
+
// GET /health/errors - Get current errors and warnings
|
| 133 |
+
// ====================================================================
|
| 134 |
+
devServer.app.get("/health/errors", (req, res) => {
|
| 135 |
+
const webpackStatus = healthPlugin.getStatus();
|
| 136 |
+
|
| 137 |
+
res.json({
|
| 138 |
+
errorCount: webpackStatus.errorCount,
|
| 139 |
+
warningCount: webpackStatus.warningCount,
|
| 140 |
+
errors: webpackStatus.errors,
|
| 141 |
+
warnings: webpackStatus.warnings,
|
| 142 |
+
state: webpackStatus.state,
|
| 143 |
+
});
|
| 144 |
+
});
|
| 145 |
+
|
| 146 |
+
// ====================================================================
|
| 147 |
+
// GET /health/stats - Compilation statistics
|
| 148 |
+
// ====================================================================
|
| 149 |
+
devServer.app.get("/health/stats", (req, res) => {
|
| 150 |
+
const webpackStatus = healthPlugin.getStatus();
|
| 151 |
+
const uptime = Date.now() - SERVER_START_TIME;
|
| 152 |
+
|
| 153 |
+
res.json({
|
| 154 |
+
totalCompiles: webpackStatus.totalCompiles,
|
| 155 |
+
averageCompileTime: webpackStatus.totalCompiles > 0
|
| 156 |
+
? `${Math.round(uptime / webpackStatus.totalCompiles)}ms`
|
| 157 |
+
: null,
|
| 158 |
+
lastCompileDuration: webpackStatus.compileDuration
|
| 159 |
+
? `${webpackStatus.compileDuration}ms`
|
| 160 |
+
: null,
|
| 161 |
+
firstCompileTime: webpackStatus.firstCompileTime
|
| 162 |
+
? new Date(webpackStatus.firstCompileTime).toISOString()
|
| 163 |
+
: null,
|
| 164 |
+
serverUptime: formatDuration(uptime),
|
| 165 |
+
});
|
| 166 |
+
});
|
| 167 |
+
|
| 168 |
+
console.log('[Health Check] ✓ Health endpoints ready:');
|
| 169 |
+
console.log(' • GET /health - Detailed status');
|
| 170 |
+
console.log(' • GET /health/simple - Simple OK/ERROR');
|
| 171 |
+
console.log(' • GET /health/ready - Readiness check');
|
| 172 |
+
console.log(' • GET /health/live - Liveness check');
|
| 173 |
+
console.log(' • GET /health/errors - Error details');
|
| 174 |
+
console.log(' • GET /health/stats - Statistics');
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
// ====================================================================
|
| 178 |
+
// Helper Functions
|
| 179 |
+
// ====================================================================
|
| 180 |
+
|
| 181 |
+
/**
|
| 182 |
+
* Format bytes to human-readable string
|
| 183 |
+
* @param {number} bytes
|
| 184 |
+
* @returns {string}
|
| 185 |
+
*/
|
| 186 |
+
function formatBytes(bytes) {
|
| 187 |
+
if (bytes === 0) return '0 B';
|
| 188 |
+
const k = 1024;
|
| 189 |
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
| 190 |
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
| 191 |
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
/**
|
| 195 |
+
* Format duration to human-readable string
|
| 196 |
+
* @param {number} ms - Duration in milliseconds
|
| 197 |
+
* @returns {string}
|
| 198 |
+
*/
|
| 199 |
+
function formatDuration(ms) {
|
| 200 |
+
const seconds = Math.floor(ms / 1000);
|
| 201 |
+
const minutes = Math.floor(seconds / 60);
|
| 202 |
+
const hours = Math.floor(minutes / 60);
|
| 203 |
+
|
| 204 |
+
if (hours > 0) {
|
| 205 |
+
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
|
| 206 |
+
} else if (minutes > 0) {
|
| 207 |
+
return `${minutes}m ${seconds % 60}s`;
|
| 208 |
+
} else {
|
| 209 |
+
return `${seconds}s`;
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
module.exports = setupHealthEndpoints;
|
frontend/plugins/health-check/webpack-health-plugin.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// webpack-health-plugin.js
|
| 2 |
+
// Webpack plugin that tracks compilation state and health metrics
|
| 3 |
+
|
| 4 |
+
class WebpackHealthPlugin {
|
| 5 |
+
constructor() {
|
| 6 |
+
this.status = {
|
| 7 |
+
state: 'idle', // idle, compiling, success, failed
|
| 8 |
+
errors: [],
|
| 9 |
+
warnings: [],
|
| 10 |
+
lastCompileTime: null,
|
| 11 |
+
lastSuccessTime: null,
|
| 12 |
+
compileDuration: 0,
|
| 13 |
+
totalCompiles: 0,
|
| 14 |
+
firstCompileTime: null,
|
| 15 |
+
};
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
apply(compiler) {
|
| 19 |
+
const pluginName = 'WebpackHealthPlugin';
|
| 20 |
+
|
| 21 |
+
// Hook: Compilation started
|
| 22 |
+
compiler.hooks.compile.tap(pluginName, () => {
|
| 23 |
+
const now = Date.now();
|
| 24 |
+
this.status.state = 'compiling';
|
| 25 |
+
this.status.lastCompileTime = now;
|
| 26 |
+
|
| 27 |
+
if (!this.status.firstCompileTime) {
|
| 28 |
+
this.status.firstCompileTime = now;
|
| 29 |
+
}
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
// Hook: Compilation completed
|
| 33 |
+
compiler.hooks.done.tap(pluginName, (stats) => {
|
| 34 |
+
const info = stats.toJson({
|
| 35 |
+
all: false,
|
| 36 |
+
errors: true,
|
| 37 |
+
warnings: true,
|
| 38 |
+
});
|
| 39 |
+
|
| 40 |
+
this.status.totalCompiles++;
|
| 41 |
+
this.status.compileDuration = Date.now() - this.status.lastCompileTime;
|
| 42 |
+
|
| 43 |
+
if (stats.hasErrors()) {
|
| 44 |
+
this.status.state = 'failed';
|
| 45 |
+
this.status.errors = info.errors.map(err => ({
|
| 46 |
+
message: err.message || String(err),
|
| 47 |
+
stack: err.stack,
|
| 48 |
+
moduleName: err.moduleName,
|
| 49 |
+
loc: err.loc,
|
| 50 |
+
}));
|
| 51 |
+
} else {
|
| 52 |
+
this.status.state = 'success';
|
| 53 |
+
this.status.lastSuccessTime = Date.now();
|
| 54 |
+
this.status.errors = [];
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
if (stats.hasWarnings()) {
|
| 58 |
+
this.status.warnings = info.warnings.map(warn => ({
|
| 59 |
+
message: warn.message || String(warn),
|
| 60 |
+
moduleName: warn.moduleName,
|
| 61 |
+
loc: warn.loc,
|
| 62 |
+
}));
|
| 63 |
+
} else {
|
| 64 |
+
this.status.warnings = [];
|
| 65 |
+
}
|
| 66 |
+
});
|
| 67 |
+
|
| 68 |
+
// Hook: Compilation failed
|
| 69 |
+
compiler.hooks.failed.tap(pluginName, (error) => {
|
| 70 |
+
this.status.state = 'failed';
|
| 71 |
+
this.status.errors = [{
|
| 72 |
+
message: error.message,
|
| 73 |
+
stack: error.stack,
|
| 74 |
+
}];
|
| 75 |
+
this.status.compileDuration = Date.now() - this.status.lastCompileTime;
|
| 76 |
+
});
|
| 77 |
+
|
| 78 |
+
// Hook: Invalid (file changed, recompiling)
|
| 79 |
+
compiler.hooks.invalid.tap(pluginName, () => {
|
| 80 |
+
this.status.state = 'compiling';
|
| 81 |
+
});
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
getStatus() {
|
| 85 |
+
return {
|
| 86 |
+
...this.status,
|
| 87 |
+
// Add computed fields
|
| 88 |
+
isHealthy: this.status.state === 'success',
|
| 89 |
+
errorCount: this.status.errors.length,
|
| 90 |
+
warningCount: this.status.warnings.length,
|
| 91 |
+
hasCompiled: this.status.totalCompiles > 0,
|
| 92 |
+
};
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// Get simplified status for quick checks
|
| 96 |
+
getSimpleStatus() {
|
| 97 |
+
return {
|
| 98 |
+
state: this.status.state,
|
| 99 |
+
isHealthy: this.status.state === 'success',
|
| 100 |
+
errorCount: this.status.errors.length,
|
| 101 |
+
warningCount: this.status.warnings.length,
|
| 102 |
+
};
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
// Reset statistics (useful for testing)
|
| 106 |
+
reset() {
|
| 107 |
+
this.status = {
|
| 108 |
+
state: 'idle',
|
| 109 |
+
errors: [],
|
| 110 |
+
warnings: [],
|
| 111 |
+
lastCompileTime: null,
|
| 112 |
+
lastSuccessTime: null,
|
| 113 |
+
compileDuration: 0,
|
| 114 |
+
totalCompiles: 0,
|
| 115 |
+
firstCompileTime: null,
|
| 116 |
+
};
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
module.exports = WebpackHealthPlugin;
|
frontend/postcss.config.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module.exports = {
|
| 2 |
+
plugins: {
|
| 3 |
+
tailwindcss: {},
|
| 4 |
+
autoprefixer: {},
|
| 5 |
+
},
|
| 6 |
+
}
|
frontend/public/index.html
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
+
<meta name="theme-color" content="#000000" />
|
| 7 |
+
<meta name="description" content="A product of emergent.sh" />
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@600&display=swap" rel="stylesheet" />
|
| 11 |
+
<!--
|
| 12 |
+
manifest.json provides metadata used when your web app is installed on a
|
| 13 |
+
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
| 14 |
+
-->
|
| 15 |
+
<!--
|
| 16 |
+
Notice the use of %PUBLIC_URL% in the tags above.
|
| 17 |
+
It will be replaced with the URL of the `public` folder during the build.
|
| 18 |
+
Only files inside the `public` folder can be referenced from the HTML.
|
| 19 |
+
|
| 20 |
+
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
| 21 |
+
work correctly both with client-side routing and a non-root public URL.
|
| 22 |
+
Learn how to configure a non-root public URL by running `npm run build`.
|
| 23 |
+
-->
|
| 24 |
+
<title>Emergent | Fullstack App</title>
|
| 25 |
+
<script>window.addEventListener("error",function(e){if(e.error instanceof DOMException&&e.error.name==="DataCloneError"&&e.message&&e.message.includes("PerformanceServerTiming")){e.stopImmediatePropagation();e.preventDefault()}},true);</script>
|
| 26 |
+
<script src="https://assets.emergent.sh/scripts/emergent-main.js"></script>
|
| 27 |
+
</head>
|
| 28 |
+
<body>
|
| 29 |
+
<noscript>You need to enable JavaScript to run this app.</noscript>
|
| 30 |
+
<div id="root"></div>
|
| 31 |
+
<!--
|
| 32 |
+
This HTML file is a template.
|
| 33 |
+
If you open it directly in the browser, you will see an empty page.
|
| 34 |
+
|
| 35 |
+
You can add webfonts, meta tags, or analytics to this file.
|
| 36 |
+
The build step will place the bundled scripts into the <body> tag.
|
| 37 |
+
|
| 38 |
+
To begin the development, run `npm start` or `yarn start`.
|
| 39 |
+
To create a production bundle, use `npm run build` or `yarn build`.
|
| 40 |
+
-->
|
| 41 |
+
<a
|
| 42 |
+
id="emergent-badge"
|
| 43 |
+
target="_blank"
|
| 44 |
+
href="https://app.emergent.sh/?utm_source=emergent-badge"
|
| 45 |
+
style="
|
| 46 |
+
display: inline-flex !important;
|
| 47 |
+
box-sizing: border-box;
|
| 48 |
+
width: 178px;
|
| 49 |
+
height: 40px;
|
| 50 |
+
padding: 8px 12px 8px 12px;
|
| 51 |
+
align-items: center !important;
|
| 52 |
+
gap: 8px;
|
| 53 |
+
border-radius: 50px !important;
|
| 54 |
+
background: #000 !important;
|
| 55 |
+
position: fixed !important;
|
| 56 |
+
bottom: 16px;
|
| 57 |
+
right: 16px;
|
| 58 |
+
text-decoration: none;
|
| 59 |
+
font-family: -apple-system, BlinkMacSystemFont,
|
| 60 |
+
"Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell,
|
| 61 |
+
"Open Sans", "Helvetica Neue",
|
| 62 |
+
sans-serif !important;
|
| 63 |
+
font-size: 12px !important;
|
| 64 |
+
z-index: 9999 !important;
|
| 65 |
+
"
|
| 66 |
+
>
|
| 67 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
| 68 |
+
<path d="M15.5702 8.13142C15.7729 8.0412 16.0007 8.18878 15.9892 8.4103C15.8374 11.3192 14.0965 14.0405 11.2531 15.3065C8.40964 16.5725 5.2224 16.0453 2.95912 14.2117C2.78676 14.072 2.82955 13.804 3.03219 13.7137L4.95677 12.8568C5.04866 12.8159 5.15446 12.823 5.24204 12.8725C6.73377 13.7153 8.59176 13.8649 10.2772 13.1145C11.9626 12.3641 13.0947 10.8833 13.4665 9.21075C13.4883 9.11256 13.5539 9.02918 13.6457 8.98827L15.5702 8.13142Z" fill="white"/>
|
| 69 |
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.3066 4.74698L15.5067 5.19653C15.5759 5.35178 15.5061 5.53366 15.3508 5.60278L1.29992 11.8586C1.14467 11.9278 0.962794 11.8579 0.893675 11.7027L0.701732 11.2716L0.693457 11.2531C-1.10317 7.21778 0.711626 2.49007 4.74692 0.693443C8.78221 -1.10318 13.51 0.711693 15.3066 4.74698ZM2.82356 8.55367C2.63552 8.63739 2.41991 8.51617 2.40853 8.31065C2.28373 6.05724 3.53858 3.85787 5.72286 2.88536C7.90715 1.91286 10.3813 2.45199 11.9724 4.05256C12.1175 4.19854 12.0633 4.43988 11.8753 4.5236L2.82356 8.55367Z" fill="white"/>
|
| 70 |
+
</svg>
|
| 71 |
+
<p
|
| 72 |
+
style="
|
| 73 |
+
color: #FFF !important;
|
| 74 |
+
font-family: 'Inter', sans-serif !important;
|
| 75 |
+
font-size: 13px !important;
|
| 76 |
+
font-style: normal !important;
|
| 77 |
+
font-weight: 600 !important;
|
| 78 |
+
line-height: 20px !important;
|
| 79 |
+
margin: 0 !important;
|
| 80 |
+
white-space: nowrap !important;
|
| 81 |
+
"
|
| 82 |
+
>
|
| 83 |
+
Made with Emergent
|
| 84 |
+
</p>
|
| 85 |
+
</a>
|
| 86 |
+
<script>
|
| 87 |
+
!(function (t, e) {
|
| 88 |
+
var o, n, p, r;
|
| 89 |
+
e.__SV ||
|
| 90 |
+
((window.posthog = e),
|
| 91 |
+
(e._i = []),
|
| 92 |
+
(e.init = function (i, s, a) {
|
| 93 |
+
function g(t, e) {
|
| 94 |
+
var o = e.split(".");
|
| 95 |
+
2 == o.length && ((t = t[o[0]]), (e = o[1])),
|
| 96 |
+
(t[e] = function () {
|
| 97 |
+
t.push(
|
| 98 |
+
[e].concat(
|
| 99 |
+
Array.prototype.slice.call(
|
| 100 |
+
arguments,
|
| 101 |
+
0,
|
| 102 |
+
),
|
| 103 |
+
),
|
| 104 |
+
);
|
| 105 |
+
});
|
| 106 |
+
}
|
| 107 |
+
((p = t.createElement("script")).type =
|
| 108 |
+
"text/javascript"),
|
| 109 |
+
(p.crossOrigin = "anonymous"),
|
| 110 |
+
(p.async = !0),
|
| 111 |
+
(p.src =
|
| 112 |
+
s.api_host.replace(
|
| 113 |
+
".i.posthog.com",
|
| 114 |
+
"-assets.i.posthog.com",
|
| 115 |
+
) + "/static/array.js"),
|
| 116 |
+
(r =
|
| 117 |
+
t.getElementsByTagName(
|
| 118 |
+
"script",
|
| 119 |
+
)[0]).parentNode.insertBefore(p, r);
|
| 120 |
+
var u = e;
|
| 121 |
+
for (
|
| 122 |
+
void 0 !== a ? (u = e[a] = []) : (a = "posthog"),
|
| 123 |
+
u.people = u.people || [],
|
| 124 |
+
u.toString = function (t) {
|
| 125 |
+
var e = "posthog";
|
| 126 |
+
return (
|
| 127 |
+
"posthog" !== a && (e += "." + a),
|
| 128 |
+
t || (e += " (stub)"),
|
| 129 |
+
e
|
| 130 |
+
);
|
| 131 |
+
},
|
| 132 |
+
u.people.toString = function () {
|
| 133 |
+
return u.toString(1) + ".people (stub)";
|
| 134 |
+
},
|
| 135 |
+
o =
|
| 136 |
+
"init me ws ys ps bs capture je Di ks register register_once register_for_session unregister unregister_for_session Ps getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSurveysLoaded onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey canRenderSurveyAsync identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty Es $s createPersonProfile Is opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing Ss debug xs getPageViewId captureTraceFeedback captureTraceMetric".split(
|
| 137 |
+
" ",
|
| 138 |
+
),
|
| 139 |
+
n = 0;
|
| 140 |
+
n < o.length;
|
| 141 |
+
n++
|
| 142 |
+
)
|
| 143 |
+
g(u, o[n]);
|
| 144 |
+
e._i.push([i, s, a]);
|
| 145 |
+
}),
|
| 146 |
+
(e.__SV = 1));
|
| 147 |
+
})(document, window.posthog || []);
|
| 148 |
+
posthog.init("phc_xAvL2Iq4tFmANRE7kzbKwaSqp1HJjN7x48s3vr0CMjs", {
|
| 149 |
+
api_host: "https://us.i.posthog.com",
|
| 150 |
+
person_profiles: "identified_only", // or 'always' to create profiles for anonymous users as well,
|
| 151 |
+
session_recording: {
|
| 152 |
+
recordCrossOriginIframes: true,
|
| 153 |
+
capturePerformance: false,
|
| 154 |
+
},
|
| 155 |
+
});
|
| 156 |
+
</script>
|
| 157 |
+
</body>
|
| 158 |
+
</html>
|
frontend/src/App.css
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.App-logo {
|
| 2 |
+
height: 40vmin;
|
| 3 |
+
pointer-events: none;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
@media (prefers-reduced-motion: no-preference) {
|
| 7 |
+
.App-logo {
|
| 8 |
+
animation: App-logo-spin infinite 20s linear;
|
| 9 |
+
}
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
.App-header {
|
| 13 |
+
background-color: #0f0f10;
|
| 14 |
+
min-height: 100vh;
|
| 15 |
+
display: flex;
|
| 16 |
+
flex-direction: column;
|
| 17 |
+
align-items: center;
|
| 18 |
+
justify-content: center;
|
| 19 |
+
font-size: calc(10px + 2vmin);
|
| 20 |
+
color: white;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
.App-link {
|
| 24 |
+
color: #61dafb;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
@keyframes App-logo-spin {
|
| 28 |
+
from {
|
| 29 |
+
transform: rotate(0deg);
|
| 30 |
+
}
|
| 31 |
+
to {
|
| 32 |
+
transform: rotate(360deg);
|
| 33 |
+
}
|
| 34 |
+
}
|
frontend/src/App.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect } from "react";
|
| 2 |
+
import "@/App.css";
|
| 3 |
+
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
| 4 |
+
import axios from "axios";
|
| 5 |
+
|
| 6 |
+
const BACKEND_URL = process.env.REACT_APP_BACKEND_URL;
|
| 7 |
+
const API = `${BACKEND_URL}/api`;
|
| 8 |
+
|
| 9 |
+
const Home = () => {
|
| 10 |
+
const helloWorldApi = async () => {
|
| 11 |
+
try {
|
| 12 |
+
const response = await axios.get(`${API}/`);
|
| 13 |
+
console.log(response.data.message);
|
| 14 |
+
} catch (e) {
|
| 15 |
+
console.error(e, `errored out requesting / api`);
|
| 16 |
+
}
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
useEffect(() => {
|
| 20 |
+
helloWorldApi();
|
| 21 |
+
}, []);
|
| 22 |
+
|
| 23 |
+
return (
|
| 24 |
+
<div>
|
| 25 |
+
<header className="App-header">
|
| 26 |
+
<a
|
| 27 |
+
className="App-link"
|
| 28 |
+
href="https://emergent.sh"
|
| 29 |
+
target="_blank"
|
| 30 |
+
rel="noopener noreferrer"
|
| 31 |
+
>
|
| 32 |
+
<img src="https://avatars.githubusercontent.com/in/1201222?s=120&u=2686cf91179bbafbc7a71bfbc43004cf9ae1acea&v=4" />
|
| 33 |
+
</a>
|
| 34 |
+
<p className="mt-5">Building something incredible ~!</p>
|
| 35 |
+
</header>
|
| 36 |
+
</div>
|
| 37 |
+
);
|
| 38 |
+
};
|
| 39 |
+
|
| 40 |
+
function App() {
|
| 41 |
+
return (
|
| 42 |
+
<div className="App">
|
| 43 |
+
<BrowserRouter>
|
| 44 |
+
<Routes>
|
| 45 |
+
<Route path="/" element={<Home />}>
|
| 46 |
+
<Route index element={<Home />} />
|
| 47 |
+
</Route>
|
| 48 |
+
</Routes>
|
| 49 |
+
</BrowserRouter>
|
| 50 |
+
</div>
|
| 51 |
+
);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
export default App;
|
frontend/src/components/ui/accordion.jsx
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
| 3 |
+
import { ChevronDown } from "lucide-react"
|
| 4 |
+
|
| 5 |
+
import { cn } from "@/lib/utils"
|
| 6 |
+
|
| 7 |
+
const Accordion = AccordionPrimitive.Root
|
| 8 |
+
|
| 9 |
+
const AccordionItem = React.forwardRef(({ className, ...props }, ref) => (
|
| 10 |
+
<AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />
|
| 11 |
+
))
|
| 12 |
+
AccordionItem.displayName = "AccordionItem"
|
| 13 |
+
|
| 14 |
+
const AccordionTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
|
| 15 |
+
<AccordionPrimitive.Header className="flex">
|
| 16 |
+
<AccordionPrimitive.Trigger
|
| 17 |
+
ref={ref}
|
| 18 |
+
className={cn(
|
| 19 |
+
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180",
|
| 20 |
+
className
|
| 21 |
+
)}
|
| 22 |
+
{...props}>
|
| 23 |
+
{children}
|
| 24 |
+
<ChevronDown
|
| 25 |
+
className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
| 26 |
+
</AccordionPrimitive.Trigger>
|
| 27 |
+
</AccordionPrimitive.Header>
|
| 28 |
+
))
|
| 29 |
+
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
| 30 |
+
|
| 31 |
+
const AccordionContent = React.forwardRef(({ className, children, ...props }, ref) => (
|
| 32 |
+
<AccordionPrimitive.Content
|
| 33 |
+
ref={ref}
|
| 34 |
+
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
| 35 |
+
{...props}>
|
| 36 |
+
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
| 37 |
+
</AccordionPrimitive.Content>
|
| 38 |
+
))
|
| 39 |
+
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
| 40 |
+
|
| 41 |
+
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
frontend/src/components/ui/alert-dialog.jsx
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
| 3 |
+
|
| 4 |
+
import { cn } from "@/lib/utils"
|
| 5 |
+
import { buttonVariants } from "@/components/ui/button"
|
| 6 |
+
|
| 7 |
+
const AlertDialog = AlertDialogPrimitive.Root
|
| 8 |
+
|
| 9 |
+
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
| 10 |
+
|
| 11 |
+
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
| 12 |
+
|
| 13 |
+
const AlertDialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
|
| 14 |
+
<AlertDialogPrimitive.Overlay
|
| 15 |
+
className={cn(
|
| 16 |
+
"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",
|
| 17 |
+
className
|
| 18 |
+
)}
|
| 19 |
+
{...props}
|
| 20 |
+
ref={ref} />
|
| 21 |
+
))
|
| 22 |
+
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
| 23 |
+
|
| 24 |
+
const AlertDialogContent = React.forwardRef(({ className, ...props }, ref) => (
|
| 25 |
+
<AlertDialogPortal>
|
| 26 |
+
<AlertDialogOverlay />
|
| 27 |
+
<AlertDialogPrimitive.Content
|
| 28 |
+
ref={ref}
|
| 29 |
+
className={cn(
|
| 30 |
+
"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",
|
| 31 |
+
className
|
| 32 |
+
)}
|
| 33 |
+
{...props} />
|
| 34 |
+
</AlertDialogPortal>
|
| 35 |
+
))
|
| 36 |
+
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
| 37 |
+
|
| 38 |
+
const AlertDialogHeader = ({
|
| 39 |
+
className,
|
| 40 |
+
...props
|
| 41 |
+
}) => (
|
| 42 |
+
<div
|
| 43 |
+
className={cn("flex flex-col space-y-2 text-center sm:text-left", className)}
|
| 44 |
+
{...props} />
|
| 45 |
+
)
|
| 46 |
+
AlertDialogHeader.displayName = "AlertDialogHeader"
|
| 47 |
+
|
| 48 |
+
const AlertDialogFooter = ({
|
| 49 |
+
className,
|
| 50 |
+
...props
|
| 51 |
+
}) => (
|
| 52 |
+
<div
|
| 53 |
+
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
| 54 |
+
{...props} />
|
| 55 |
+
)
|
| 56 |
+
AlertDialogFooter.displayName = "AlertDialogFooter"
|
| 57 |
+
|
| 58 |
+
const AlertDialogTitle = React.forwardRef(({ className, ...props }, ref) => (
|
| 59 |
+
<AlertDialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold", className)} {...props} />
|
| 60 |
+
))
|
| 61 |
+
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
| 62 |
+
|
| 63 |
+
const AlertDialogDescription = React.forwardRef(({ className, ...props }, ref) => (
|
| 64 |
+
<AlertDialogPrimitive.Description
|
| 65 |
+
ref={ref}
|
| 66 |
+
className={cn("text-sm text-muted-foreground", className)}
|
| 67 |
+
{...props} />
|
| 68 |
+
))
|
| 69 |
+
AlertDialogDescription.displayName =
|
| 70 |
+
AlertDialogPrimitive.Description.displayName
|
| 71 |
+
|
| 72 |
+
const AlertDialogAction = React.forwardRef(({ className, ...props }, ref) => (
|
| 73 |
+
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
|
| 74 |
+
))
|
| 75 |
+
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
| 76 |
+
|
| 77 |
+
const AlertDialogCancel = React.forwardRef(({ className, ...props }, ref) => (
|
| 78 |
+
<AlertDialogPrimitive.Cancel
|
| 79 |
+
ref={ref}
|
| 80 |
+
className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
|
| 81 |
+
{...props} />
|
| 82 |
+
))
|
| 83 |
+
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
| 84 |
+
|
| 85 |
+
export {
|
| 86 |
+
AlertDialog,
|
| 87 |
+
AlertDialogPortal,
|
| 88 |
+
AlertDialogOverlay,
|
| 89 |
+
AlertDialogTrigger,
|
| 90 |
+
AlertDialogContent,
|
| 91 |
+
AlertDialogHeader,
|
| 92 |
+
AlertDialogFooter,
|
| 93 |
+
AlertDialogTitle,
|
| 94 |
+
AlertDialogDescription,
|
| 95 |
+
AlertDialogAction,
|
| 96 |
+
AlertDialogCancel,
|
| 97 |
+
}
|
frontend/src/components/ui/alert.jsx
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import { cva } from "class-variance-authority";
|
| 3 |
+
|
| 4 |
+
import { cn } from "@/lib/utils"
|
| 5 |
+
|
| 6 |
+
const alertVariants = cva(
|
| 7 |
+
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
|
| 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(({ className, variant, ...props }, ref) => (
|
| 23 |
+
<div
|
| 24 |
+
ref={ref}
|
| 25 |
+
role="alert"
|
| 26 |
+
className={cn(alertVariants({ variant }), className)}
|
| 27 |
+
{...props} />
|
| 28 |
+
))
|
| 29 |
+
Alert.displayName = "Alert"
|
| 30 |
+
|
| 31 |
+
const AlertTitle = React.forwardRef(({ className, ...props }, ref) => (
|
| 32 |
+
<h5
|
| 33 |
+
ref={ref}
|
| 34 |
+
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
| 35 |
+
{...props} />
|
| 36 |
+
))
|
| 37 |
+
AlertTitle.displayName = "AlertTitle"
|
| 38 |
+
|
| 39 |
+
const AlertDescription = React.forwardRef(({ className, ...props }, ref) => (
|
| 40 |
+
<div
|
| 41 |
+
ref={ref}
|
| 42 |
+
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
| 43 |
+
{...props} />
|
| 44 |
+
))
|
| 45 |
+
AlertDescription.displayName = "AlertDescription"
|
| 46 |
+
|
| 47 |
+
export { Alert, AlertTitle, AlertDescription }
|
frontend/src/components/ui/aspect-ratio.jsx
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
|
| 2 |
+
|
| 3 |
+
const AspectRatio = AspectRatioPrimitive.Root
|
| 4 |
+
|
| 5 |
+
export { AspectRatio }
|
frontend/src/components/ui/avatar.jsx
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
| 3 |
+
|
| 4 |
+
import { cn } from "@/lib/utils"
|
| 5 |
+
|
| 6 |
+
const Avatar = React.forwardRef(({ className, ...props }, ref) => (
|
| 7 |
+
<AvatarPrimitive.Root
|
| 8 |
+
ref={ref}
|
| 9 |
+
className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
|
| 10 |
+
{...props} />
|
| 11 |
+
))
|
| 12 |
+
Avatar.displayName = AvatarPrimitive.Root.displayName
|
| 13 |
+
|
| 14 |
+
const AvatarImage = React.forwardRef(({ className, ...props }, ref) => (
|
| 15 |
+
<AvatarPrimitive.Image
|
| 16 |
+
ref={ref}
|
| 17 |
+
className={cn("aspect-square h-full w-full", className)}
|
| 18 |
+
{...props} />
|
| 19 |
+
))
|
| 20 |
+
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
| 21 |
+
|
| 22 |
+
const AvatarFallback = React.forwardRef(({ className, ...props }, ref) => (
|
| 23 |
+
<AvatarPrimitive.Fallback
|
| 24 |
+
ref={ref}
|
| 25 |
+
className={cn(
|
| 26 |
+
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
| 27 |
+
className
|
| 28 |
+
)}
|
| 29 |
+
{...props} />
|
| 30 |
+
))
|
| 31 |
+
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
| 32 |
+
|
| 33 |
+
export { Avatar, AvatarImage, AvatarFallback }
|
frontend/src/components/ui/badge.jsx
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import { cva } from "class-variance-authority";
|
| 3 |
+
|
| 4 |
+
import { cn } from "@/lib/utils"
|
| 5 |
+
|
| 6 |
+
const badgeVariants = cva(
|
| 7 |
+
"inline-flex items-center rounded-md 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 shadow 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 shadow hover:bg-destructive/80",
|
| 17 |
+
outline: "text-foreground",
|
| 18 |
+
},
|
| 19 |
+
},
|
| 20 |
+
defaultVariants: {
|
| 21 |
+
variant: "default",
|
| 22 |
+
},
|
| 23 |
+
}
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
function Badge({
|
| 27 |
+
className,
|
| 28 |
+
variant,
|
| 29 |
+
...props
|
| 30 |
+
}) {
|
| 31 |
+
return (<div className={cn(badgeVariants({ variant }), className)} {...props} />);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
export { Badge, badgeVariants }
|
frontend/src/components/ui/breadcrumb.jsx
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />
|
| 9 |
+
)
|
| 10 |
+
Breadcrumb.displayName = "Breadcrumb"
|
| 11 |
+
|
| 12 |
+
const BreadcrumbList = React.forwardRef(({ className, ...props }, ref) => (
|
| 13 |
+
<ol
|
| 14 |
+
ref={ref}
|
| 15 |
+
className={cn(
|
| 16 |
+
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
| 17 |
+
className
|
| 18 |
+
)}
|
| 19 |
+
{...props} />
|
| 20 |
+
))
|
| 21 |
+
BreadcrumbList.displayName = "BreadcrumbList"
|
| 22 |
+
|
| 23 |
+
const BreadcrumbItem = React.forwardRef(({ className, ...props }, ref) => (
|
| 24 |
+
<li
|
| 25 |
+
ref={ref}
|
| 26 |
+
className={cn("inline-flex items-center gap-1.5", className)}
|
| 27 |
+
{...props} />
|
| 28 |
+
))
|
| 29 |
+
BreadcrumbItem.displayName = "BreadcrumbItem"
|
| 30 |
+
|
| 31 |
+
const BreadcrumbLink = React.forwardRef(({ asChild, className, ...props }, ref) => {
|
| 32 |
+
const Comp = asChild ? Slot : "a"
|
| 33 |
+
|
| 34 |
+
return (
|
| 35 |
+
<Comp
|
| 36 |
+
ref={ref}
|
| 37 |
+
className={cn("transition-colors hover:text-foreground", className)}
|
| 38 |
+
{...props} />
|
| 39 |
+
);
|
| 40 |
+
})
|
| 41 |
+
BreadcrumbLink.displayName = "BreadcrumbLink"
|
| 42 |
+
|
| 43 |
+
const BreadcrumbPage = React.forwardRef(({ className, ...props }, ref) => (
|
| 44 |
+
<span
|
| 45 |
+
ref={ref}
|
| 46 |
+
role="link"
|
| 47 |
+
aria-disabled="true"
|
| 48 |
+
aria-current="page"
|
| 49 |
+
className={cn("font-normal text-foreground", className)}
|
| 50 |
+
{...props} />
|
| 51 |
+
))
|
| 52 |
+
BreadcrumbPage.displayName = "BreadcrumbPage"
|
| 53 |
+
|
| 54 |
+
const BreadcrumbSeparator = ({
|
| 55 |
+
children,
|
| 56 |
+
className,
|
| 57 |
+
...props
|
| 58 |
+
}) => (
|
| 59 |
+
<li
|
| 60 |
+
role="presentation"
|
| 61 |
+
aria-hidden="true"
|
| 62 |
+
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
|
| 63 |
+
{...props}>
|
| 64 |
+
{children ?? <ChevronRight />}
|
| 65 |
+
</li>
|
| 66 |
+
)
|
| 67 |
+
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
|
| 68 |
+
|
| 69 |
+
const BreadcrumbEllipsis = ({
|
| 70 |
+
className,
|
| 71 |
+
...props
|
| 72 |
+
}) => (
|
| 73 |
+
<span
|
| 74 |
+
role="presentation"
|
| 75 |
+
aria-hidden="true"
|
| 76 |
+
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
| 77 |
+
{...props}>
|
| 78 |
+
<MoreHorizontal className="h-4 w-4" />
|
| 79 |
+
<span className="sr-only">More</span>
|
| 80 |
+
</span>
|
| 81 |
+
)
|
| 82 |
+
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
|
| 83 |
+
|
| 84 |
+
export {
|
| 85 |
+
Breadcrumb,
|
| 86 |
+
BreadcrumbList,
|
| 87 |
+
BreadcrumbItem,
|
| 88 |
+
BreadcrumbLink,
|
| 89 |
+
BreadcrumbPage,
|
| 90 |
+
BreadcrumbSeparator,
|
| 91 |
+
BreadcrumbEllipsis,
|
| 92 |
+
}
|
frontend/src/components/ui/button.jsx
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import { Slot } from "@radix-ui/react-slot"
|
| 3 |
+
import { cva } 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 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
| 9 |
+
{
|
| 10 |
+
variants: {
|
| 11 |
+
variant: {
|
| 12 |
+
default:
|
| 13 |
+
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
| 14 |
+
destructive:
|
| 15 |
+
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
| 16 |
+
outline:
|
| 17 |
+
"border border-input shadow-sm hover:bg-accent hover:text-accent-foreground",
|
| 18 |
+
secondary:
|
| 19 |
+
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
| 20 |
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
| 21 |
+
link: "text-primary underline-offset-4 hover:underline",
|
| 22 |
+
},
|
| 23 |
+
size: {
|
| 24 |
+
default: "h-9 px-4 py-2",
|
| 25 |
+
sm: "h-8 rounded-md px-3 text-xs",
|
| 26 |
+
lg: "h-10 rounded-md px-8",
|
| 27 |
+
icon: "h-9 w-9",
|
| 28 |
+
},
|
| 29 |
+
},
|
| 30 |
+
defaultVariants: {
|
| 31 |
+
variant: "default",
|
| 32 |
+
size: "default",
|
| 33 |
+
},
|
| 34 |
+
}
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
|
| 38 |
+
const Comp = asChild ? Slot : "button"
|
| 39 |
+
return (
|
| 40 |
+
<Comp
|
| 41 |
+
className={cn(buttonVariants({ variant, size, className }))}
|
| 42 |
+
ref={ref}
|
| 43 |
+
{...props} />
|
| 44 |
+
);
|
| 45 |
+
})
|
| 46 |
+
Button.displayName = "Button"
|
| 47 |
+
|
| 48 |
+
export { Button, buttonVariants }
|
frontend/src/components/ui/calendar.jsx
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import { ChevronLeft, ChevronRight } from "lucide-react"
|
| 3 |
+
import { DayPicker } from "react-day-picker"
|
| 4 |
+
|
| 5 |
+
import { cn } from "@/lib/utils"
|
| 6 |
+
import { buttonVariants } from "@/components/ui/button"
|
| 7 |
+
|
| 8 |
+
function Calendar({
|
| 9 |
+
className,
|
| 10 |
+
classNames,
|
| 11 |
+
showOutsideDays = true,
|
| 12 |
+
...props
|
| 13 |
+
}) {
|
| 14 |
+
return (
|
| 15 |
+
<DayPicker
|
| 16 |
+
showOutsideDays={showOutsideDays}
|
| 17 |
+
className={cn("p-3", className)}
|
| 18 |
+
classNames={{
|
| 19 |
+
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
| 20 |
+
month: "space-y-4",
|
| 21 |
+
caption: "flex justify-center pt-1 relative items-center",
|
| 22 |
+
caption_label: "text-sm font-medium",
|
| 23 |
+
nav: "space-x-1 flex items-center",
|
| 24 |
+
nav_button: cn(
|
| 25 |
+
buttonVariants({ variant: "outline" }),
|
| 26 |
+
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
| 27 |
+
),
|
| 28 |
+
nav_button_previous: "absolute left-1",
|
| 29 |
+
nav_button_next: "absolute right-1",
|
| 30 |
+
table: "w-full border-collapse space-y-1",
|
| 31 |
+
head_row: "flex",
|
| 32 |
+
head_cell:
|
| 33 |
+
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
|
| 34 |
+
row: "flex w-full mt-2",
|
| 35 |
+
cell: cn(
|
| 36 |
+
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md",
|
| 37 |
+
props.mode === "range"
|
| 38 |
+
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
|
| 39 |
+
: "[&:has([aria-selected])]:rounded-md"
|
| 40 |
+
),
|
| 41 |
+
day: cn(
|
| 42 |
+
buttonVariants({ variant: "ghost" }),
|
| 43 |
+
"h-8 w-8 p-0 font-normal aria-selected:opacity-100"
|
| 44 |
+
),
|
| 45 |
+
day_range_start: "day-range-start",
|
| 46 |
+
day_range_end: "day-range-end",
|
| 47 |
+
day_selected:
|
| 48 |
+
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
| 49 |
+
day_today: "bg-accent text-accent-foreground",
|
| 50 |
+
day_outside:
|
| 51 |
+
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
|
| 52 |
+
day_disabled: "text-muted-foreground opacity-50",
|
| 53 |
+
day_range_middle:
|
| 54 |
+
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
| 55 |
+
day_hidden: "invisible",
|
| 56 |
+
...classNames,
|
| 57 |
+
}}
|
| 58 |
+
components={{
|
| 59 |
+
IconLeft: ({ className, ...props }) => (
|
| 60 |
+
<ChevronLeft className={cn("h-4 w-4", className)} {...props} />
|
| 61 |
+
),
|
| 62 |
+
IconRight: ({ className, ...props }) => (
|
| 63 |
+
<ChevronRight className={cn("h-4 w-4", className)} {...props} />
|
| 64 |
+
),
|
| 65 |
+
}}
|
| 66 |
+
{...props} />
|
| 67 |
+
);
|
| 68 |
+
}
|
| 69 |
+
Calendar.displayName = "Calendar"
|
| 70 |
+
|
| 71 |
+
export { Calendar }
|
frontend/src/components/ui/card.jsx
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
|
| 3 |
+
import { cn } from "@/lib/utils"
|
| 4 |
+
|
| 5 |
+
const Card = React.forwardRef(({ className, ...props }, ref) => (
|
| 6 |
+
<div
|
| 7 |
+
ref={ref}
|
| 8 |
+
className={cn("rounded-xl border bg-card text-card-foreground shadow", className)}
|
| 9 |
+
{...props} />
|
| 10 |
+
))
|
| 11 |
+
Card.displayName = "Card"
|
| 12 |
+
|
| 13 |
+
const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
|
| 14 |
+
<div
|
| 15 |
+
ref={ref}
|
| 16 |
+
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
| 17 |
+
{...props} />
|
| 18 |
+
))
|
| 19 |
+
CardHeader.displayName = "CardHeader"
|
| 20 |
+
|
| 21 |
+
const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
|
| 22 |
+
<div
|
| 23 |
+
ref={ref}
|
| 24 |
+
className={cn("font-semibold leading-none tracking-tight", className)}
|
| 25 |
+
{...props} />
|
| 26 |
+
))
|
| 27 |
+
CardTitle.displayName = "CardTitle"
|
| 28 |
+
|
| 29 |
+
const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
|
| 30 |
+
<div
|
| 31 |
+
ref={ref}
|
| 32 |
+
className={cn("text-sm text-muted-foreground", className)}
|
| 33 |
+
{...props} />
|
| 34 |
+
))
|
| 35 |
+
CardDescription.displayName = "CardDescription"
|
| 36 |
+
|
| 37 |
+
const CardContent = React.forwardRef(({ className, ...props }, ref) => (
|
| 38 |
+
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
| 39 |
+
))
|
| 40 |
+
CardContent.displayName = "CardContent"
|
| 41 |
+
|
| 42 |
+
const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
|
| 43 |
+
<div
|
| 44 |
+
ref={ref}
|
| 45 |
+
className={cn("flex items-center p-6 pt-0", className)}
|
| 46 |
+
{...props} />
|
| 47 |
+
))
|
| 48 |
+
CardFooter.displayName = "CardFooter"
|
| 49 |
+
|
| 50 |
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
frontend/src/components/ui/carousel.jsx
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import useEmblaCarousel from "embla-carousel-react";
|
| 3 |
+
import { ArrowLeft, ArrowRight } from "lucide-react"
|
| 4 |
+
|
| 5 |
+
import { cn } from "@/lib/utils"
|
| 6 |
+
import { Button } from "@/components/ui/button"
|
| 7 |
+
|
| 8 |
+
const CarouselContext = React.createContext(null)
|
| 9 |
+
|
| 10 |
+
function useCarousel() {
|
| 11 |
+
const context = React.useContext(CarouselContext)
|
| 12 |
+
|
| 13 |
+
if (!context) {
|
| 14 |
+
throw new Error("useCarousel must be used within a <Carousel />")
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
return context
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
const Carousel = React.forwardRef((
|
| 21 |
+
{
|
| 22 |
+
orientation = "horizontal",
|
| 23 |
+
opts,
|
| 24 |
+
setApi,
|
| 25 |
+
plugins,
|
| 26 |
+
className,
|
| 27 |
+
children,
|
| 28 |
+
...props
|
| 29 |
+
},
|
| 30 |
+
ref
|
| 31 |
+
) => {
|
| 32 |
+
const [carouselRef, api] = useEmblaCarousel({
|
| 33 |
+
...opts,
|
| 34 |
+
axis: orientation === "horizontal" ? "x" : "y",
|
| 35 |
+
}, plugins)
|
| 36 |
+
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
| 37 |
+
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
| 38 |
+
|
| 39 |
+
const onSelect = React.useCallback((api) => {
|
| 40 |
+
if (!api) {
|
| 41 |
+
return
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
setCanScrollPrev(api.canScrollPrev())
|
| 45 |
+
setCanScrollNext(api.canScrollNext())
|
| 46 |
+
}, [])
|
| 47 |
+
|
| 48 |
+
const scrollPrev = React.useCallback(() => {
|
| 49 |
+
api?.scrollPrev()
|
| 50 |
+
}, [api])
|
| 51 |
+
|
| 52 |
+
const scrollNext = React.useCallback(() => {
|
| 53 |
+
api?.scrollNext()
|
| 54 |
+
}, [api])
|
| 55 |
+
|
| 56 |
+
const handleKeyDown = React.useCallback((event) => {
|
| 57 |
+
if (event.key === "ArrowLeft") {
|
| 58 |
+
event.preventDefault()
|
| 59 |
+
scrollPrev()
|
| 60 |
+
} else if (event.key === "ArrowRight") {
|
| 61 |
+
event.preventDefault()
|
| 62 |
+
scrollNext()
|
| 63 |
+
}
|
| 64 |
+
}, [scrollPrev, scrollNext])
|
| 65 |
+
|
| 66 |
+
React.useEffect(() => {
|
| 67 |
+
if (!api || !setApi) {
|
| 68 |
+
return
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
setApi(api)
|
| 72 |
+
}, [api, setApi])
|
| 73 |
+
|
| 74 |
+
React.useEffect(() => {
|
| 75 |
+
if (!api) {
|
| 76 |
+
return
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
onSelect(api)
|
| 80 |
+
api.on("reInit", onSelect)
|
| 81 |
+
api.on("select", onSelect)
|
| 82 |
+
|
| 83 |
+
return () => {
|
| 84 |
+
api?.off("select", onSelect)
|
| 85 |
+
};
|
| 86 |
+
}, [api, onSelect])
|
| 87 |
+
|
| 88 |
+
return (
|
| 89 |
+
<CarouselContext.Provider
|
| 90 |
+
value={{
|
| 91 |
+
carouselRef,
|
| 92 |
+
api: api,
|
| 93 |
+
opts,
|
| 94 |
+
orientation:
|
| 95 |
+
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
| 96 |
+
scrollPrev,
|
| 97 |
+
scrollNext,
|
| 98 |
+
canScrollPrev,
|
| 99 |
+
canScrollNext,
|
| 100 |
+
}}>
|
| 101 |
+
<div
|
| 102 |
+
ref={ref}
|
| 103 |
+
onKeyDownCapture={handleKeyDown}
|
| 104 |
+
className={cn("relative", className)}
|
| 105 |
+
role="region"
|
| 106 |
+
aria-roledescription="carousel"
|
| 107 |
+
{...props}>
|
| 108 |
+
{children}
|
| 109 |
+
</div>
|
| 110 |
+
</CarouselContext.Provider>
|
| 111 |
+
);
|
| 112 |
+
})
|
| 113 |
+
Carousel.displayName = "Carousel"
|
| 114 |
+
|
| 115 |
+
const CarouselContent = React.forwardRef(({ className, ...props }, ref) => {
|
| 116 |
+
const { carouselRef, orientation } = useCarousel()
|
| 117 |
+
|
| 118 |
+
return (
|
| 119 |
+
<div ref={carouselRef} className="overflow-hidden">
|
| 120 |
+
<div
|
| 121 |
+
ref={ref}
|
| 122 |
+
className={cn(
|
| 123 |
+
"flex",
|
| 124 |
+
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
| 125 |
+
className
|
| 126 |
+
)}
|
| 127 |
+
{...props} />
|
| 128 |
+
</div>
|
| 129 |
+
);
|
| 130 |
+
})
|
| 131 |
+
CarouselContent.displayName = "CarouselContent"
|
| 132 |
+
|
| 133 |
+
const CarouselItem = React.forwardRef(({ className, ...props }, ref) => {
|
| 134 |
+
const { orientation } = useCarousel()
|
| 135 |
+
|
| 136 |
+
return (
|
| 137 |
+
<div
|
| 138 |
+
ref={ref}
|
| 139 |
+
role="group"
|
| 140 |
+
aria-roledescription="slide"
|
| 141 |
+
className={cn(
|
| 142 |
+
"min-w-0 shrink-0 grow-0 basis-full",
|
| 143 |
+
orientation === "horizontal" ? "pl-4" : "pt-4",
|
| 144 |
+
className
|
| 145 |
+
)}
|
| 146 |
+
{...props} />
|
| 147 |
+
);
|
| 148 |
+
})
|
| 149 |
+
CarouselItem.displayName = "CarouselItem"
|
| 150 |
+
|
| 151 |
+
const CarouselPrevious = React.forwardRef(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
| 152 |
+
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
| 153 |
+
|
| 154 |
+
return (
|
| 155 |
+
<Button
|
| 156 |
+
ref={ref}
|
| 157 |
+
variant={variant}
|
| 158 |
+
size={size}
|
| 159 |
+
className={cn("absolute h-8 w-8 rounded-full", orientation === "horizontal"
|
| 160 |
+
? "-left-12 top-1/2 -translate-y-1/2"
|
| 161 |
+
: "-top-12 left-1/2 -translate-x-1/2 rotate-90", className)}
|
| 162 |
+
disabled={!canScrollPrev}
|
| 163 |
+
onClick={scrollPrev}
|
| 164 |
+
{...props}>
|
| 165 |
+
<ArrowLeft className="h-4 w-4" />
|
| 166 |
+
<span className="sr-only">Previous slide</span>
|
| 167 |
+
</Button>
|
| 168 |
+
);
|
| 169 |
+
})
|
| 170 |
+
CarouselPrevious.displayName = "CarouselPrevious"
|
| 171 |
+
|
| 172 |
+
const CarouselNext = React.forwardRef(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
| 173 |
+
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
| 174 |
+
|
| 175 |
+
return (
|
| 176 |
+
<Button
|
| 177 |
+
ref={ref}
|
| 178 |
+
variant={variant}
|
| 179 |
+
size={size}
|
| 180 |
+
className={cn("absolute h-8 w-8 rounded-full", orientation === "horizontal"
|
| 181 |
+
? "-right-12 top-1/2 -translate-y-1/2"
|
| 182 |
+
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90", className)}
|
| 183 |
+
disabled={!canScrollNext}
|
| 184 |
+
onClick={scrollNext}
|
| 185 |
+
{...props}>
|
| 186 |
+
<ArrowRight className="h-4 w-4" />
|
| 187 |
+
<span className="sr-only">Next slide</span>
|
| 188 |
+
</Button>
|
| 189 |
+
);
|
| 190 |
+
})
|
| 191 |
+
CarouselNext.displayName = "CarouselNext"
|
| 192 |
+
|
| 193 |
+
export { Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext };
|
frontend/src/components/ui/checkbox.jsx
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
| 3 |
+
import { Check } from "lucide-react"
|
| 4 |
+
|
| 5 |
+
import { cn } from "@/lib/utils"
|
| 6 |
+
|
| 7 |
+
const Checkbox = React.forwardRef(({ className, ...props }, ref) => (
|
| 8 |
+
<CheckboxPrimitive.Root
|
| 9 |
+
ref={ref}
|
| 10 |
+
className={cn(
|
| 11 |
+
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
| 12 |
+
className
|
| 13 |
+
)}
|
| 14 |
+
{...props}>
|
| 15 |
+
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
|
| 16 |
+
<Check className="h-4 w-4" />
|
| 17 |
+
</CheckboxPrimitive.Indicator>
|
| 18 |
+
</CheckboxPrimitive.Root>
|
| 19 |
+
))
|
| 20 |
+
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
| 21 |
+
|
| 22 |
+
export { Checkbox }
|
frontend/src/components/ui/collapsible.jsx
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
| 2 |
+
|
| 3 |
+
const Collapsible = CollapsiblePrimitive.Root
|
| 4 |
+
|
| 5 |
+
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
| 6 |
+
|
| 7 |
+
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
| 8 |
+
|
| 9 |
+
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
frontend/src/components/ui/command.jsx
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import { Command as CommandPrimitive } from "cmdk"
|
| 3 |
+
import { Search } from "lucide-react"
|
| 4 |
+
|
| 5 |
+
import { cn } from "@/lib/utils"
|
| 6 |
+
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
| 7 |
+
|
| 8 |
+
const Command = React.forwardRef(({ className, ...props }, ref) => (
|
| 9 |
+
<CommandPrimitive
|
| 10 |
+
ref={ref}
|
| 11 |
+
className={cn(
|
| 12 |
+
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
| 13 |
+
className
|
| 14 |
+
)}
|
| 15 |
+
{...props} />
|
| 16 |
+
))
|
| 17 |
+
Command.displayName = CommandPrimitive.displayName
|
| 18 |
+
|
| 19 |
+
const CommandDialog = ({
|
| 20 |
+
children,
|
| 21 |
+
...props
|
| 22 |
+
}) => {
|
| 23 |
+
return (
|
| 24 |
+
<Dialog {...props}>
|
| 25 |
+
<DialogContent className="overflow-hidden p-0">
|
| 26 |
+
<Command
|
| 27 |
+
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">
|
| 28 |
+
{children}
|
| 29 |
+
</Command>
|
| 30 |
+
</DialogContent>
|
| 31 |
+
</Dialog>
|
| 32 |
+
);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
const CommandInput = React.forwardRef(({ className, ...props }, ref) => (
|
| 36 |
+
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
| 37 |
+
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
| 38 |
+
<CommandPrimitive.Input
|
| 39 |
+
ref={ref}
|
| 40 |
+
className={cn(
|
| 41 |
+
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
| 42 |
+
className
|
| 43 |
+
)}
|
| 44 |
+
{...props} />
|
| 45 |
+
</div>
|
| 46 |
+
))
|
| 47 |
+
|
| 48 |
+
CommandInput.displayName = CommandPrimitive.Input.displayName
|
| 49 |
+
|
| 50 |
+
const CommandList = React.forwardRef(({ className, ...props }, ref) => (
|
| 51 |
+
<CommandPrimitive.List
|
| 52 |
+
ref={ref}
|
| 53 |
+
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
| 54 |
+
{...props} />
|
| 55 |
+
))
|
| 56 |
+
|
| 57 |
+
CommandList.displayName = CommandPrimitive.List.displayName
|
| 58 |
+
|
| 59 |
+
const CommandEmpty = React.forwardRef((props, ref) => (
|
| 60 |
+
<CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />
|
| 61 |
+
))
|
| 62 |
+
|
| 63 |
+
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
| 64 |
+
|
| 65 |
+
const CommandGroup = React.forwardRef(({ className, ...props }, ref) => (
|
| 66 |
+
<CommandPrimitive.Group
|
| 67 |
+
ref={ref}
|
| 68 |
+
className={cn(
|
| 69 |
+
"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",
|
| 70 |
+
className
|
| 71 |
+
)}
|
| 72 |
+
{...props} />
|
| 73 |
+
))
|
| 74 |
+
|
| 75 |
+
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
| 76 |
+
|
| 77 |
+
const CommandSeparator = React.forwardRef(({ className, ...props }, ref) => (
|
| 78 |
+
<CommandPrimitive.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} />
|
| 79 |
+
))
|
| 80 |
+
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
| 81 |
+
|
| 82 |
+
const CommandItem = React.forwardRef(({ className, ...props }, ref) => (
|
| 83 |
+
<CommandPrimitive.Item
|
| 84 |
+
ref={ref}
|
| 85 |
+
className={cn(
|
| 86 |
+
"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",
|
| 87 |
+
className
|
| 88 |
+
)}
|
| 89 |
+
{...props} />
|
| 90 |
+
))
|
| 91 |
+
|
| 92 |
+
CommandItem.displayName = CommandPrimitive.Item.displayName
|
| 93 |
+
|
| 94 |
+
const CommandShortcut = ({
|
| 95 |
+
className,
|
| 96 |
+
...props
|
| 97 |
+
}) => {
|
| 98 |
+
return (
|
| 99 |
+
<span
|
| 100 |
+
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
|
| 101 |
+
{...props} />
|
| 102 |
+
);
|
| 103 |
+
}
|
| 104 |
+
CommandShortcut.displayName = "CommandShortcut"
|
| 105 |
+
|
| 106 |
+
export {
|
| 107 |
+
Command,
|
| 108 |
+
CommandDialog,
|
| 109 |
+
CommandInput,
|
| 110 |
+
CommandList,
|
| 111 |
+
CommandEmpty,
|
| 112 |
+
CommandGroup,
|
| 113 |
+
CommandItem,
|
| 114 |
+
CommandShortcut,
|
| 115 |
+
CommandSeparator,
|
| 116 |
+
}
|
frontend/src/components/ui/context-menu.jsx
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
| 3 |
+
import { Check, ChevronRight, Circle } from "lucide-react"
|
| 4 |
+
|
| 5 |
+
import { cn } from "@/lib/utils"
|
| 6 |
+
|
| 7 |
+
const ContextMenu = ContextMenuPrimitive.Root
|
| 8 |
+
|
| 9 |
+
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
|
| 10 |
+
|
| 11 |
+
const ContextMenuGroup = ContextMenuPrimitive.Group
|
| 12 |
+
|
| 13 |
+
const ContextMenuPortal = ContextMenuPrimitive.Portal
|
| 14 |
+
|
| 15 |
+
const ContextMenuSub = ContextMenuPrimitive.Sub
|
| 16 |
+
|
| 17 |
+
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
|
| 18 |
+
|
| 19 |
+
const ContextMenuSubTrigger = React.forwardRef(({ className, inset, children, ...props }, ref) => (
|
| 20 |
+
<ContextMenuPrimitive.SubTrigger
|
| 21 |
+
ref={ref}
|
| 22 |
+
className={cn(
|
| 23 |
+
"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",
|
| 24 |
+
inset && "pl-8",
|
| 25 |
+
className
|
| 26 |
+
)}
|
| 27 |
+
{...props}>
|
| 28 |
+
{children}
|
| 29 |
+
<ChevronRight className="ml-auto h-4 w-4" />
|
| 30 |
+
</ContextMenuPrimitive.SubTrigger>
|
| 31 |
+
))
|
| 32 |
+
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
|
| 33 |
+
|
| 34 |
+
const ContextMenuSubContent = React.forwardRef(({ className, ...props }, ref) => (
|
| 35 |
+
<ContextMenuPrimitive.SubContent
|
| 36 |
+
ref={ref}
|
| 37 |
+
className={cn(
|
| 38 |
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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 origin-[--radix-context-menu-content-transform-origin]",
|
| 39 |
+
className
|
| 40 |
+
)}
|
| 41 |
+
{...props} />
|
| 42 |
+
))
|
| 43 |
+
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
|
| 44 |
+
|
| 45 |
+
const ContextMenuContent = React.forwardRef(({ className, ...props }, ref) => (
|
| 46 |
+
<ContextMenuPrimitive.Portal>
|
| 47 |
+
<ContextMenuPrimitive.Content
|
| 48 |
+
ref={ref}
|
| 49 |
+
className={cn(
|
| 50 |
+
"z-50 max-h-[--radix-context-menu-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-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 origin-[--radix-context-menu-content-transform-origin]",
|
| 51 |
+
className
|
| 52 |
+
)}
|
| 53 |
+
{...props} />
|
| 54 |
+
</ContextMenuPrimitive.Portal>
|
| 55 |
+
))
|
| 56 |
+
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
|
| 57 |
+
|
| 58 |
+
const ContextMenuItem = React.forwardRef(({ className, inset, ...props }, ref) => (
|
| 59 |
+
<ContextMenuPrimitive.Item
|
| 60 |
+
ref={ref}
|
| 61 |
+
className={cn(
|
| 62 |
+
"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",
|
| 63 |
+
inset && "pl-8",
|
| 64 |
+
className
|
| 65 |
+
)}
|
| 66 |
+
{...props} />
|
| 67 |
+
))
|
| 68 |
+
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
|
| 69 |
+
|
| 70 |
+
const ContextMenuCheckboxItem = React.forwardRef(({ className, children, checked, ...props }, ref) => (
|
| 71 |
+
<ContextMenuPrimitive.CheckboxItem
|
| 72 |
+
ref={ref}
|
| 73 |
+
className={cn(
|
| 74 |
+
"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",
|
| 75 |
+
className
|
| 76 |
+
)}
|
| 77 |
+
checked={checked}
|
| 78 |
+
{...props}>
|
| 79 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
| 80 |
+
<ContextMenuPrimitive.ItemIndicator>
|
| 81 |
+
<Check className="h-4 w-4" />
|
| 82 |
+
</ContextMenuPrimitive.ItemIndicator>
|
| 83 |
+
</span>
|
| 84 |
+
{children}
|
| 85 |
+
</ContextMenuPrimitive.CheckboxItem>
|
| 86 |
+
))
|
| 87 |
+
ContextMenuCheckboxItem.displayName =
|
| 88 |
+
ContextMenuPrimitive.CheckboxItem.displayName
|
| 89 |
+
|
| 90 |
+
const ContextMenuRadioItem = React.forwardRef(({ className, children, ...props }, ref) => (
|
| 91 |
+
<ContextMenuPrimitive.RadioItem
|
| 92 |
+
ref={ref}
|
| 93 |
+
className={cn(
|
| 94 |
+
"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",
|
| 95 |
+
className
|
| 96 |
+
)}
|
| 97 |
+
{...props}>
|
| 98 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
| 99 |
+
<ContextMenuPrimitive.ItemIndicator>
|
| 100 |
+
<Circle className="h-4 w-4 fill-current" />
|
| 101 |
+
</ContextMenuPrimitive.ItemIndicator>
|
| 102 |
+
</span>
|
| 103 |
+
{children}
|
| 104 |
+
</ContextMenuPrimitive.RadioItem>
|
| 105 |
+
))
|
| 106 |
+
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
|
| 107 |
+
|
| 108 |
+
const ContextMenuLabel = React.forwardRef(({ className, inset, ...props }, ref) => (
|
| 109 |
+
<ContextMenuPrimitive.Label
|
| 110 |
+
ref={ref}
|
| 111 |
+
className={cn(
|
| 112 |
+
"px-2 py-1.5 text-sm font-semibold text-foreground",
|
| 113 |
+
inset && "pl-8",
|
| 114 |
+
className
|
| 115 |
+
)}
|
| 116 |
+
{...props} />
|
| 117 |
+
))
|
| 118 |
+
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
|
| 119 |
+
|
| 120 |
+
const ContextMenuSeparator = React.forwardRef(({ className, ...props }, ref) => (
|
| 121 |
+
<ContextMenuPrimitive.Separator
|
| 122 |
+
ref={ref}
|
| 123 |
+
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
| 124 |
+
{...props} />
|
| 125 |
+
))
|
| 126 |
+
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
|
| 127 |
+
|
| 128 |
+
const ContextMenuShortcut = ({
|
| 129 |
+
className,
|
| 130 |
+
...props
|
| 131 |
+
}) => {
|
| 132 |
+
return (
|
| 133 |
+
<span
|
| 134 |
+
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
|
| 135 |
+
{...props} />
|
| 136 |
+
);
|
| 137 |
+
}
|
| 138 |
+
ContextMenuShortcut.displayName = "ContextMenuShortcut"
|
| 139 |
+
|
| 140 |
+
export {
|
| 141 |
+
ContextMenu,
|
| 142 |
+
ContextMenuTrigger,
|
| 143 |
+
ContextMenuContent,
|
| 144 |
+
ContextMenuItem,
|
| 145 |
+
ContextMenuCheckboxItem,
|
| 146 |
+
ContextMenuRadioItem,
|
| 147 |
+
ContextMenuLabel,
|
| 148 |
+
ContextMenuSeparator,
|
| 149 |
+
ContextMenuShortcut,
|
| 150 |
+
ContextMenuGroup,
|
| 151 |
+
ContextMenuPortal,
|
| 152 |
+
ContextMenuSub,
|
| 153 |
+
ContextMenuSubContent,
|
| 154 |
+
ContextMenuSubTrigger,
|
| 155 |
+
ContextMenuRadioGroup,
|
| 156 |
+
}
|
frontend/src/components/ui/dialog.jsx
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
| 3 |
+
import { X } from "lucide-react"
|
| 4 |
+
|
| 5 |
+
import { cn } from "@/lib/utils"
|
| 6 |
+
|
| 7 |
+
const Dialog = DialogPrimitive.Root
|
| 8 |
+
|
| 9 |
+
const DialogTrigger = DialogPrimitive.Trigger
|
| 10 |
+
|
| 11 |
+
const DialogPortal = DialogPrimitive.Portal
|
| 12 |
+
|
| 13 |
+
const DialogClose = DialogPrimitive.Close
|
| 14 |
+
|
| 15 |
+
const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
|
| 16 |
+
<DialogPrimitive.Overlay
|
| 17 |
+
ref={ref}
|
| 18 |
+
className={cn(
|
| 19 |
+
"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",
|
| 20 |
+
className
|
| 21 |
+
)}
|
| 22 |
+
{...props} />
|
| 23 |
+
))
|
| 24 |
+
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
| 25 |
+
|
| 26 |
+
const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => (
|
| 27 |
+
<DialogPortal>
|
| 28 |
+
<DialogOverlay />
|
| 29 |
+
<DialogPrimitive.Content
|
| 30 |
+
ref={ref}
|
| 31 |
+
className={cn(
|
| 32 |
+
"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",
|
| 33 |
+
className
|
| 34 |
+
)}
|
| 35 |
+
{...props}>
|
| 36 |
+
{children}
|
| 37 |
+
<DialogPrimitive.Close
|
| 38 |
+
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">
|
| 39 |
+
<X className="h-4 w-4" />
|
| 40 |
+
<span className="sr-only">Close</span>
|
| 41 |
+
</DialogPrimitive.Close>
|
| 42 |
+
</DialogPrimitive.Content>
|
| 43 |
+
</DialogPortal>
|
| 44 |
+
))
|
| 45 |
+
DialogContent.displayName = DialogPrimitive.Content.displayName
|
| 46 |
+
|
| 47 |
+
const DialogHeader = ({
|
| 48 |
+
className,
|
| 49 |
+
...props
|
| 50 |
+
}) => (
|
| 51 |
+
<div
|
| 52 |
+
className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
|
| 53 |
+
{...props} />
|
| 54 |
+
)
|
| 55 |
+
DialogHeader.displayName = "DialogHeader"
|
| 56 |
+
|
| 57 |
+
const DialogFooter = ({
|
| 58 |
+
className,
|
| 59 |
+
...props
|
| 60 |
+
}) => (
|
| 61 |
+
<div
|
| 62 |
+
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
| 63 |
+
{...props} />
|
| 64 |
+
)
|
| 65 |
+
DialogFooter.displayName = "DialogFooter"
|
| 66 |
+
|
| 67 |
+
const DialogTitle = React.forwardRef(({ className, ...props }, ref) => (
|
| 68 |
+
<DialogPrimitive.Title
|
| 69 |
+
ref={ref}
|
| 70 |
+
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
| 71 |
+
{...props} />
|
| 72 |
+
))
|
| 73 |
+
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
| 74 |
+
|
| 75 |
+
const DialogDescription = React.forwardRef(({ className, ...props }, ref) => (
|
| 76 |
+
<DialogPrimitive.Description
|
| 77 |
+
ref={ref}
|
| 78 |
+
className={cn("text-sm text-muted-foreground", className)}
|
| 79 |
+
{...props} />
|
| 80 |
+
))
|
| 81 |
+
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
| 82 |
+
|
| 83 |
+
export {
|
| 84 |
+
Dialog,
|
| 85 |
+
DialogPortal,
|
| 86 |
+
DialogOverlay,
|
| 87 |
+
DialogTrigger,
|
| 88 |
+
DialogClose,
|
| 89 |
+
DialogContent,
|
| 90 |
+
DialogHeader,
|
| 91 |
+
DialogFooter,
|
| 92 |
+
DialogTitle,
|
| 93 |
+
DialogDescription,
|
| 94 |
+
}
|
frontend/src/components/ui/drawer.jsx
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import { Drawer as DrawerPrimitive } from "vaul"
|
| 3 |
+
|
| 4 |
+
import { cn } from "@/lib/utils"
|
| 5 |
+
|
| 6 |
+
const Drawer = ({
|
| 7 |
+
shouldScaleBackground = true,
|
| 8 |
+
...props
|
| 9 |
+
}) => (
|
| 10 |
+
<DrawerPrimitive.Root shouldScaleBackground={shouldScaleBackground} {...props} />
|
| 11 |
+
)
|
| 12 |
+
Drawer.displayName = "Drawer"
|
| 13 |
+
|
| 14 |
+
const DrawerTrigger = DrawerPrimitive.Trigger
|
| 15 |
+
|
| 16 |
+
const DrawerPortal = DrawerPrimitive.Portal
|
| 17 |
+
|
| 18 |
+
const DrawerClose = DrawerPrimitive.Close
|
| 19 |
+
|
| 20 |
+
const DrawerOverlay = React.forwardRef(({ className, ...props }, ref) => (
|
| 21 |
+
<DrawerPrimitive.Overlay
|
| 22 |
+
ref={ref}
|
| 23 |
+
className={cn("fixed inset-0 z-50 bg-black/80", className)}
|
| 24 |
+
{...props} />
|
| 25 |
+
))
|
| 26 |
+
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
|
| 27 |
+
|
| 28 |
+
const DrawerContent = React.forwardRef(({ className, children, ...props }, ref) => (
|
| 29 |
+
<DrawerPortal>
|
| 30 |
+
<DrawerOverlay />
|
| 31 |
+
<DrawerPrimitive.Content
|
| 32 |
+
ref={ref}
|
| 33 |
+
className={cn(
|
| 34 |
+
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
|
| 35 |
+
className
|
| 36 |
+
)}
|
| 37 |
+
{...props}>
|
| 38 |
+
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
| 39 |
+
{children}
|
| 40 |
+
</DrawerPrimitive.Content>
|
| 41 |
+
</DrawerPortal>
|
| 42 |
+
))
|
| 43 |
+
DrawerContent.displayName = "DrawerContent"
|
| 44 |
+
|
| 45 |
+
const DrawerHeader = ({
|
| 46 |
+
className,
|
| 47 |
+
...props
|
| 48 |
+
}) => (
|
| 49 |
+
<div
|
| 50 |
+
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
|
| 51 |
+
{...props} />
|
| 52 |
+
)
|
| 53 |
+
DrawerHeader.displayName = "DrawerHeader"
|
| 54 |
+
|
| 55 |
+
const DrawerFooter = ({
|
| 56 |
+
className,
|
| 57 |
+
...props
|
| 58 |
+
}) => (
|
| 59 |
+
<div className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} />
|
| 60 |
+
)
|
| 61 |
+
DrawerFooter.displayName = "DrawerFooter"
|
| 62 |
+
|
| 63 |
+
const DrawerTitle = React.forwardRef(({ className, ...props }, ref) => (
|
| 64 |
+
<DrawerPrimitive.Title
|
| 65 |
+
ref={ref}
|
| 66 |
+
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
| 67 |
+
{...props} />
|
| 68 |
+
))
|
| 69 |
+
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
|
| 70 |
+
|
| 71 |
+
const DrawerDescription = React.forwardRef(({ className, ...props }, ref) => (
|
| 72 |
+
<DrawerPrimitive.Description
|
| 73 |
+
ref={ref}
|
| 74 |
+
className={cn("text-sm text-muted-foreground", className)}
|
| 75 |
+
{...props} />
|
| 76 |
+
))
|
| 77 |
+
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
|
| 78 |
+
|
| 79 |
+
export {
|
| 80 |
+
Drawer,
|
| 81 |
+
DrawerPortal,
|
| 82 |
+
DrawerOverlay,
|
| 83 |
+
DrawerTrigger,
|
| 84 |
+
DrawerClose,
|
| 85 |
+
DrawerContent,
|
| 86 |
+
DrawerHeader,
|
| 87 |
+
DrawerFooter,
|
| 88 |
+
DrawerTitle,
|
| 89 |
+
DrawerDescription,
|
| 90 |
+
}
|
frontend/src/components/ui/dropdown-menu.jsx
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
| 3 |
+
import { Check, ChevronRight, Circle } from "lucide-react"
|
| 4 |
+
|
| 5 |
+
import { cn } from "@/lib/utils"
|
| 6 |
+
|
| 7 |
+
const DropdownMenu = DropdownMenuPrimitive.Root
|
| 8 |
+
|
| 9 |
+
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
| 10 |
+
|
| 11 |
+
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
| 12 |
+
|
| 13 |
+
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
| 14 |
+
|
| 15 |
+
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
| 16 |
+
|
| 17 |
+
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
| 18 |
+
|
| 19 |
+
const DropdownMenuSubTrigger = React.forwardRef(({ className, inset, children, ...props }, ref) => (
|
| 20 |
+
<DropdownMenuPrimitive.SubTrigger
|
| 21 |
+
ref={ref}
|
| 22 |
+
className={cn(
|
| 23 |
+
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
| 24 |
+
inset && "pl-8",
|
| 25 |
+
className
|
| 26 |
+
)}
|
| 27 |
+
{...props}>
|
| 28 |
+
{children}
|
| 29 |
+
<ChevronRight className="ml-auto" />
|
| 30 |
+
</DropdownMenuPrimitive.SubTrigger>
|
| 31 |
+
))
|
| 32 |
+
DropdownMenuSubTrigger.displayName =
|
| 33 |
+
DropdownMenuPrimitive.SubTrigger.displayName
|
| 34 |
+
|
| 35 |
+
const DropdownMenuSubContent = React.forwardRef(({ className, ...props }, ref) => (
|
| 36 |
+
<DropdownMenuPrimitive.SubContent
|
| 37 |
+
ref={ref}
|
| 38 |
+
className={cn(
|
| 39 |
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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 origin-[--radix-dropdown-menu-content-transform-origin]",
|
| 40 |
+
className
|
| 41 |
+
)}
|
| 42 |
+
{...props} />
|
| 43 |
+
))
|
| 44 |
+
DropdownMenuSubContent.displayName =
|
| 45 |
+
DropdownMenuPrimitive.SubContent.displayName
|
| 46 |
+
|
| 47 |
+
const DropdownMenuContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (
|
| 48 |
+
<DropdownMenuPrimitive.Portal>
|
| 49 |
+
<DropdownMenuPrimitive.Content
|
| 50 |
+
ref={ref}
|
| 51 |
+
sideOffset={sideOffset}
|
| 52 |
+
className={cn(
|
| 53 |
+
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
| 54 |
+
"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 origin-[--radix-dropdown-menu-content-transform-origin]",
|
| 55 |
+
className
|
| 56 |
+
)}
|
| 57 |
+
{...props} />
|
| 58 |
+
</DropdownMenuPrimitive.Portal>
|
| 59 |
+
))
|
| 60 |
+
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
| 61 |
+
|
| 62 |
+
const DropdownMenuItem = React.forwardRef(({ className, inset, ...props }, ref) => (
|
| 63 |
+
<DropdownMenuPrimitive.Item
|
| 64 |
+
ref={ref}
|
| 65 |
+
className={cn(
|
| 66 |
+
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
| 67 |
+
inset && "pl-8",
|
| 68 |
+
className
|
| 69 |
+
)}
|
| 70 |
+
{...props} />
|
| 71 |
+
))
|
| 72 |
+
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
| 73 |
+
|
| 74 |
+
const DropdownMenuCheckboxItem = React.forwardRef(({ className, children, checked, ...props }, ref) => (
|
| 75 |
+
<DropdownMenuPrimitive.CheckboxItem
|
| 76 |
+
ref={ref}
|
| 77 |
+
className={cn(
|
| 78 |
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
| 79 |
+
className
|
| 80 |
+
)}
|
| 81 |
+
checked={checked}
|
| 82 |
+
{...props}>
|
| 83 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
| 84 |
+
<DropdownMenuPrimitive.ItemIndicator>
|
| 85 |
+
<Check className="h-4 w-4" />
|
| 86 |
+
</DropdownMenuPrimitive.ItemIndicator>
|
| 87 |
+
</span>
|
| 88 |
+
{children}
|
| 89 |
+
</DropdownMenuPrimitive.CheckboxItem>
|
| 90 |
+
))
|
| 91 |
+
DropdownMenuCheckboxItem.displayName =
|
| 92 |
+
DropdownMenuPrimitive.CheckboxItem.displayName
|
| 93 |
+
|
| 94 |
+
const DropdownMenuRadioItem = React.forwardRef(({ className, children, ...props }, ref) => (
|
| 95 |
+
<DropdownMenuPrimitive.RadioItem
|
| 96 |
+
ref={ref}
|
| 97 |
+
className={cn(
|
| 98 |
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
| 99 |
+
className
|
| 100 |
+
)}
|
| 101 |
+
{...props}>
|
| 102 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
| 103 |
+
<DropdownMenuPrimitive.ItemIndicator>
|
| 104 |
+
<Circle className="h-2 w-2 fill-current" />
|
| 105 |
+
</DropdownMenuPrimitive.ItemIndicator>
|
| 106 |
+
</span>
|
| 107 |
+
{children}
|
| 108 |
+
</DropdownMenuPrimitive.RadioItem>
|
| 109 |
+
))
|
| 110 |
+
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
| 111 |
+
|
| 112 |
+
const DropdownMenuLabel = React.forwardRef(({ className, inset, ...props }, ref) => (
|
| 113 |
+
<DropdownMenuPrimitive.Label
|
| 114 |
+
ref={ref}
|
| 115 |
+
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
| 116 |
+
{...props} />
|
| 117 |
+
))
|
| 118 |
+
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
| 119 |
+
|
| 120 |
+
const DropdownMenuSeparator = React.forwardRef(({ className, ...props }, ref) => (
|
| 121 |
+
<DropdownMenuPrimitive.Separator
|
| 122 |
+
ref={ref}
|
| 123 |
+
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
| 124 |
+
{...props} />
|
| 125 |
+
))
|
| 126 |
+
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
| 127 |
+
|
| 128 |
+
const DropdownMenuShortcut = ({
|
| 129 |
+
className,
|
| 130 |
+
...props
|
| 131 |
+
}) => {
|
| 132 |
+
return (
|
| 133 |
+
<span
|
| 134 |
+
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
| 135 |
+
{...props} />
|
| 136 |
+
);
|
| 137 |
+
}
|
| 138 |
+
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
| 139 |
+
|
| 140 |
+
export {
|
| 141 |
+
DropdownMenu,
|
| 142 |
+
DropdownMenuTrigger,
|
| 143 |
+
DropdownMenuContent,
|
| 144 |
+
DropdownMenuItem,
|
| 145 |
+
DropdownMenuCheckboxItem,
|
| 146 |
+
DropdownMenuRadioItem,
|
| 147 |
+
DropdownMenuLabel,
|
| 148 |
+
DropdownMenuSeparator,
|
| 149 |
+
DropdownMenuShortcut,
|
| 150 |
+
DropdownMenuGroup,
|
| 151 |
+
DropdownMenuPortal,
|
| 152 |
+
DropdownMenuSub,
|
| 153 |
+
DropdownMenuSubContent,
|
| 154 |
+
DropdownMenuSubTrigger,
|
| 155 |
+
DropdownMenuRadioGroup,
|
| 156 |
+
}
|
frontend/src/components/ui/form.jsx
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import { Slot } from "@radix-ui/react-slot"
|
| 3 |
+
import { Controller, FormProvider, useFormContext } from "react-hook-form";
|
| 4 |
+
|
| 5 |
+
import { cn } from "@/lib/utils"
|
| 6 |
+
import { Label } from "@/components/ui/label"
|
| 7 |
+
|
| 8 |
+
const Form = FormProvider
|
| 9 |
+
|
| 10 |
+
const FormFieldContext = React.createContext({})
|
| 11 |
+
|
| 12 |
+
const FormField = (
|
| 13 |
+
{
|
| 14 |
+
...props
|
| 15 |
+
}
|
| 16 |
+
) => {
|
| 17 |
+
return (
|
| 18 |
+
<FormFieldContext.Provider value={{ name: props.name }}>
|
| 19 |
+
<Controller {...props} />
|
| 20 |
+
</FormFieldContext.Provider>
|
| 21 |
+
);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
const useFormField = () => {
|
| 25 |
+
const fieldContext = React.useContext(FormFieldContext)
|
| 26 |
+
const itemContext = React.useContext(FormItemContext)
|
| 27 |
+
const { getFieldState, formState } = useFormContext()
|
| 28 |
+
|
| 29 |
+
const fieldState = getFieldState(fieldContext.name, formState)
|
| 30 |
+
|
| 31 |
+
if (!fieldContext) {
|
| 32 |
+
throw new Error("useFormField should be used within <FormField>")
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
const { id } = itemContext
|
| 36 |
+
|
| 37 |
+
return {
|
| 38 |
+
id,
|
| 39 |
+
name: fieldContext.name,
|
| 40 |
+
formItemId: `${id}-form-item`,
|
| 41 |
+
formDescriptionId: `${id}-form-item-description`,
|
| 42 |
+
formMessageId: `${id}-form-item-message`,
|
| 43 |
+
...fieldState,
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
const FormItemContext = React.createContext({})
|
| 48 |
+
|
| 49 |
+
const FormItem = React.forwardRef(({ className, ...props }, ref) => {
|
| 50 |
+
const id = React.useId()
|
| 51 |
+
|
| 52 |
+
return (
|
| 53 |
+
<FormItemContext.Provider value={{ id }}>
|
| 54 |
+
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
| 55 |
+
</FormItemContext.Provider>
|
| 56 |
+
);
|
| 57 |
+
})
|
| 58 |
+
FormItem.displayName = "FormItem"
|
| 59 |
+
|
| 60 |
+
const FormLabel = React.forwardRef(({ className, ...props }, ref) => {
|
| 61 |
+
const { error, formItemId } = useFormField()
|
| 62 |
+
|
| 63 |
+
return (
|
| 64 |
+
<Label
|
| 65 |
+
ref={ref}
|
| 66 |
+
className={cn(error && "text-destructive", className)}
|
| 67 |
+
htmlFor={formItemId}
|
| 68 |
+
{...props} />
|
| 69 |
+
);
|
| 70 |
+
})
|
| 71 |
+
FormLabel.displayName = "FormLabel"
|
| 72 |
+
|
| 73 |
+
const FormControl = React.forwardRef(({ ...props }, ref) => {
|
| 74 |
+
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
| 75 |
+
|
| 76 |
+
return (
|
| 77 |
+
<Slot
|
| 78 |
+
ref={ref}
|
| 79 |
+
id={formItemId}
|
| 80 |
+
aria-describedby={
|
| 81 |
+
!error
|
| 82 |
+
? `${formDescriptionId}`
|
| 83 |
+
: `${formDescriptionId} ${formMessageId}`
|
| 84 |
+
}
|
| 85 |
+
aria-invalid={!!error}
|
| 86 |
+
{...props} />
|
| 87 |
+
);
|
| 88 |
+
})
|
| 89 |
+
FormControl.displayName = "FormControl"
|
| 90 |
+
|
| 91 |
+
const FormDescription = React.forwardRef(({ className, ...props }, ref) => {
|
| 92 |
+
const { formDescriptionId } = useFormField()
|
| 93 |
+
|
| 94 |
+
return (
|
| 95 |
+
<p
|
| 96 |
+
ref={ref}
|
| 97 |
+
id={formDescriptionId}
|
| 98 |
+
className={cn("text-[0.8rem] text-muted-foreground", className)}
|
| 99 |
+
{...props} />
|
| 100 |
+
);
|
| 101 |
+
})
|
| 102 |
+
FormDescription.displayName = "FormDescription"
|
| 103 |
+
|
| 104 |
+
const FormMessage = React.forwardRef(({ className, children, ...props }, ref) => {
|
| 105 |
+
const { error, formMessageId } = useFormField()
|
| 106 |
+
const body = error ? String(error?.message ?? "") : children
|
| 107 |
+
|
| 108 |
+
if (!body) {
|
| 109 |
+
return null
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
return (
|
| 113 |
+
<p
|
| 114 |
+
ref={ref}
|
| 115 |
+
id={formMessageId}
|
| 116 |
+
className={cn("text-[0.8rem] font-medium text-destructive", className)}
|
| 117 |
+
{...props}>
|
| 118 |
+
{body}
|
| 119 |
+
</p>
|
| 120 |
+
);
|
| 121 |
+
})
|
| 122 |
+
FormMessage.displayName = "FormMessage"
|
| 123 |
+
|
| 124 |
+
export {
|
| 125 |
+
useFormField,
|
| 126 |
+
Form,
|
| 127 |
+
FormItem,
|
| 128 |
+
FormLabel,
|
| 129 |
+
FormControl,
|
| 130 |
+
FormDescription,
|
| 131 |
+
FormMessage,
|
| 132 |
+
FormField,
|
| 133 |
+
}
|
frontend/src/components/ui/hover-card.jsx
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
| 3 |
+
|
| 4 |
+
import { cn } from "@/lib/utils"
|
| 5 |
+
|
| 6 |
+
const HoverCard = HoverCardPrimitive.Root
|
| 7 |
+
|
| 8 |
+
const HoverCardTrigger = HoverCardPrimitive.Trigger
|
| 9 |
+
|
| 10 |
+
const HoverCardContent = React.forwardRef(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
| 11 |
+
<HoverCardPrimitive.Content
|
| 12 |
+
ref={ref}
|
| 13 |
+
align={align}
|
| 14 |
+
sideOffset={sideOffset}
|
| 15 |
+
className={cn(
|
| 16 |
+
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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 origin-[--radix-hover-card-content-transform-origin]",
|
| 17 |
+
className
|
| 18 |
+
)}
|
| 19 |
+
{...props} />
|
| 20 |
+
))
|
| 21 |
+
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
|
| 22 |
+
|
| 23 |
+
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
frontend/src/components/ui/input-otp.jsx
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import { OTPInput, OTPInputContext } from "input-otp"
|
| 3 |
+
import { Minus } from "lucide-react"
|
| 4 |
+
|
| 5 |
+
import { cn } from "@/lib/utils"
|
| 6 |
+
|
| 7 |
+
const InputOTP = React.forwardRef(({ className, containerClassName, ...props }, ref) => (
|
| 8 |
+
<OTPInput
|
| 9 |
+
ref={ref}
|
| 10 |
+
containerClassName={cn("flex items-center gap-2 has-[:disabled]:opacity-50", containerClassName)}
|
| 11 |
+
className={cn("disabled:cursor-not-allowed", className)}
|
| 12 |
+
{...props} />
|
| 13 |
+
))
|
| 14 |
+
InputOTP.displayName = "InputOTP"
|
| 15 |
+
|
| 16 |
+
const InputOTPGroup = React.forwardRef(({ className, ...props }, ref) => (
|
| 17 |
+
<div ref={ref} className={cn("flex items-center", className)} {...props} />
|
| 18 |
+
))
|
| 19 |
+
InputOTPGroup.displayName = "InputOTPGroup"
|
| 20 |
+
|
| 21 |
+
const InputOTPSlot = React.forwardRef(({ index, className, ...props }, ref) => {
|
| 22 |
+
const inputOTPContext = React.useContext(OTPInputContext)
|
| 23 |
+
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
|
| 24 |
+
|
| 25 |
+
return (
|
| 26 |
+
<div
|
| 27 |
+
ref={ref}
|
| 28 |
+
className={cn(
|
| 29 |
+
"relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
|
| 30 |
+
isActive && "z-10 ring-1 ring-ring",
|
| 31 |
+
className
|
| 32 |
+
)}
|
| 33 |
+
{...props}>
|
| 34 |
+
{char}
|
| 35 |
+
{hasFakeCaret && (
|
| 36 |
+
<div
|
| 37 |
+
className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
| 38 |
+
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
|
| 39 |
+
</div>
|
| 40 |
+
)}
|
| 41 |
+
</div>
|
| 42 |
+
);
|
| 43 |
+
})
|
| 44 |
+
InputOTPSlot.displayName = "InputOTPSlot"
|
| 45 |
+
|
| 46 |
+
const InputOTPSeparator = React.forwardRef(({ ...props }, ref) => (
|
| 47 |
+
<div ref={ref} role="separator" {...props}>
|
| 48 |
+
<Minus />
|
| 49 |
+
</div>
|
| 50 |
+
))
|
| 51 |
+
InputOTPSeparator.displayName = "InputOTPSeparator"
|
| 52 |
+
|
| 53 |
+
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
|
frontend/src/components/ui/input.jsx
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
|
| 3 |
+
import { cn } from "@/lib/utils"
|
| 4 |
+
|
| 5 |
+
const Input = React.forwardRef(({ className, type, ...props }, ref) => {
|
| 6 |
+
return (
|
| 7 |
+
<input
|
| 8 |
+
type={type}
|
| 9 |
+
className={cn(
|
| 10 |
+
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
| 11 |
+
className
|
| 12 |
+
)}
|
| 13 |
+
ref={ref}
|
| 14 |
+
{...props} />
|
| 15 |
+
);
|
| 16 |
+
})
|
| 17 |
+
Input.displayName = "Input"
|
| 18 |
+
|
| 19 |
+
export { Input }
|
frontend/src/components/ui/label.jsx
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as LabelPrimitive from "@radix-ui/react-label"
|
| 3 |
+
import { cva } from "class-variance-authority";
|
| 4 |
+
|
| 5 |
+
import { cn } from "@/lib/utils"
|
| 6 |
+
|
| 7 |
+
const labelVariants = cva(
|
| 8 |
+
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
const Label = React.forwardRef(({ className, ...props }, ref) => (
|
| 12 |
+
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
|
| 13 |
+
))
|
| 14 |
+
Label.displayName = LabelPrimitive.Root.displayName
|
| 15 |
+
|
| 16 |
+
export { Label }
|
frontend/src/components/ui/menubar.jsx
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as MenubarPrimitive from "@radix-ui/react-menubar"
|
| 3 |
+
import { Check, ChevronRight, Circle } from "lucide-react"
|
| 4 |
+
|
| 5 |
+
import { cn } from "@/lib/utils"
|
| 6 |
+
|
| 7 |
+
function MenubarMenu({
|
| 8 |
+
...props
|
| 9 |
+
}) {
|
| 10 |
+
return <MenubarPrimitive.Menu {...props} />;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
function MenubarGroup({
|
| 14 |
+
...props
|
| 15 |
+
}) {
|
| 16 |
+
return <MenubarPrimitive.Group {...props} />;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
function MenubarPortal({
|
| 20 |
+
...props
|
| 21 |
+
}) {
|
| 22 |
+
return <MenubarPrimitive.Portal {...props} />;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
function MenubarRadioGroup({
|
| 26 |
+
...props
|
| 27 |
+
}) {
|
| 28 |
+
return <MenubarPrimitive.RadioGroup {...props} />;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
function MenubarSub({
|
| 32 |
+
...props
|
| 33 |
+
}) {
|
| 34 |
+
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
const Menubar = React.forwardRef(({ className, ...props }, ref) => (
|
| 38 |
+
<MenubarPrimitive.Root
|
| 39 |
+
ref={ref}
|
| 40 |
+
className={cn(
|
| 41 |
+
"flex h-9 items-center space-x-1 rounded-md border bg-background p-1 shadow-sm",
|
| 42 |
+
className
|
| 43 |
+
)}
|
| 44 |
+
{...props} />
|
| 45 |
+
))
|
| 46 |
+
Menubar.displayName = MenubarPrimitive.Root.displayName
|
| 47 |
+
|
| 48 |
+
const MenubarTrigger = React.forwardRef(({ className, ...props }, ref) => (
|
| 49 |
+
<MenubarPrimitive.Trigger
|
| 50 |
+
ref={ref}
|
| 51 |
+
className={cn(
|
| 52 |
+
"flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
| 53 |
+
className
|
| 54 |
+
)}
|
| 55 |
+
{...props} />
|
| 56 |
+
))
|
| 57 |
+
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
|
| 58 |
+
|
| 59 |
+
const MenubarSubTrigger = React.forwardRef(({ className, inset, children, ...props }, ref) => (
|
| 60 |
+
<MenubarPrimitive.SubTrigger
|
| 61 |
+
ref={ref}
|
| 62 |
+
className={cn(
|
| 63 |
+
"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",
|
| 64 |
+
inset && "pl-8",
|
| 65 |
+
className
|
| 66 |
+
)}
|
| 67 |
+
{...props}>
|
| 68 |
+
{children}
|
| 69 |
+
<ChevronRight className="ml-auto h-4 w-4" />
|
| 70 |
+
</MenubarPrimitive.SubTrigger>
|
| 71 |
+
))
|
| 72 |
+
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
|
| 73 |
+
|
| 74 |
+
const MenubarSubContent = React.forwardRef(({ className, ...props }, ref) => (
|
| 75 |
+
<MenubarPrimitive.SubContent
|
| 76 |
+
ref={ref}
|
| 77 |
+
className={cn(
|
| 78 |
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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 origin-[--radix-menubar-content-transform-origin]",
|
| 79 |
+
className
|
| 80 |
+
)}
|
| 81 |
+
{...props} />
|
| 82 |
+
))
|
| 83 |
+
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
|
| 84 |
+
|
| 85 |
+
const MenubarContent = React.forwardRef((
|
| 86 |
+
{ className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
|
| 87 |
+
ref
|
| 88 |
+
) => (
|
| 89 |
+
<MenubarPrimitive.Portal>
|
| 90 |
+
<MenubarPrimitive.Content
|
| 91 |
+
ref={ref}
|
| 92 |
+
align={align}
|
| 93 |
+
alignOffset={alignOffset}
|
| 94 |
+
sideOffset={sideOffset}
|
| 95 |
+
className={cn(
|
| 96 |
+
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in 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 origin-[--radix-menubar-content-transform-origin]",
|
| 97 |
+
className
|
| 98 |
+
)}
|
| 99 |
+
{...props} />
|
| 100 |
+
</MenubarPrimitive.Portal>
|
| 101 |
+
))
|
| 102 |
+
MenubarContent.displayName = MenubarPrimitive.Content.displayName
|
| 103 |
+
|
| 104 |
+
const MenubarItem = React.forwardRef(({ className, inset, ...props }, ref) => (
|
| 105 |
+
<MenubarPrimitive.Item
|
| 106 |
+
ref={ref}
|
| 107 |
+
className={cn(
|
| 108 |
+
"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",
|
| 109 |
+
inset && "pl-8",
|
| 110 |
+
className
|
| 111 |
+
)}
|
| 112 |
+
{...props} />
|
| 113 |
+
))
|
| 114 |
+
MenubarItem.displayName = MenubarPrimitive.Item.displayName
|
| 115 |
+
|
| 116 |
+
const MenubarCheckboxItem = React.forwardRef(({ className, children, checked, ...props }, ref) => (
|
| 117 |
+
<MenubarPrimitive.CheckboxItem
|
| 118 |
+
ref={ref}
|
| 119 |
+
className={cn(
|
| 120 |
+
"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",
|
| 121 |
+
className
|
| 122 |
+
)}
|
| 123 |
+
checked={checked}
|
| 124 |
+
{...props}>
|
| 125 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
| 126 |
+
<MenubarPrimitive.ItemIndicator>
|
| 127 |
+
<Check className="h-4 w-4" />
|
| 128 |
+
</MenubarPrimitive.ItemIndicator>
|
| 129 |
+
</span>
|
| 130 |
+
{children}
|
| 131 |
+
</MenubarPrimitive.CheckboxItem>
|
| 132 |
+
))
|
| 133 |
+
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
|
| 134 |
+
|
| 135 |
+
const MenubarRadioItem = React.forwardRef(({ className, children, ...props }, ref) => (
|
| 136 |
+
<MenubarPrimitive.RadioItem
|
| 137 |
+
ref={ref}
|
| 138 |
+
className={cn(
|
| 139 |
+
"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",
|
| 140 |
+
className
|
| 141 |
+
)}
|
| 142 |
+
{...props}>
|
| 143 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
| 144 |
+
<MenubarPrimitive.ItemIndicator>
|
| 145 |
+
<Circle className="h-4 w-4 fill-current" />
|
| 146 |
+
</MenubarPrimitive.ItemIndicator>
|
| 147 |
+
</span>
|
| 148 |
+
{children}
|
| 149 |
+
</MenubarPrimitive.RadioItem>
|
| 150 |
+
))
|
| 151 |
+
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
|
| 152 |
+
|
| 153 |
+
const MenubarLabel = React.forwardRef(({ className, inset, ...props }, ref) => (
|
| 154 |
+
<MenubarPrimitive.Label
|
| 155 |
+
ref={ref}
|
| 156 |
+
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
| 157 |
+
{...props} />
|
| 158 |
+
))
|
| 159 |
+
MenubarLabel.displayName = MenubarPrimitive.Label.displayName
|
| 160 |
+
|
| 161 |
+
const MenubarSeparator = React.forwardRef(({ className, ...props }, ref) => (
|
| 162 |
+
<MenubarPrimitive.Separator
|
| 163 |
+
ref={ref}
|
| 164 |
+
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
| 165 |
+
{...props} />
|
| 166 |
+
))
|
| 167 |
+
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
|
| 168 |
+
|
| 169 |
+
const MenubarShortcut = ({
|
| 170 |
+
className,
|
| 171 |
+
...props
|
| 172 |
+
}) => {
|
| 173 |
+
return (
|
| 174 |
+
<span
|
| 175 |
+
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
|
| 176 |
+
{...props} />
|
| 177 |
+
);
|
| 178 |
+
}
|
| 179 |
+
MenubarShortcut.displayname = "MenubarShortcut"
|
| 180 |
+
|
| 181 |
+
export {
|
| 182 |
+
Menubar,
|
| 183 |
+
MenubarMenu,
|
| 184 |
+
MenubarTrigger,
|
| 185 |
+
MenubarContent,
|
| 186 |
+
MenubarItem,
|
| 187 |
+
MenubarSeparator,
|
| 188 |
+
MenubarLabel,
|
| 189 |
+
MenubarCheckboxItem,
|
| 190 |
+
MenubarRadioGroup,
|
| 191 |
+
MenubarRadioItem,
|
| 192 |
+
MenubarPortal,
|
| 193 |
+
MenubarSubContent,
|
| 194 |
+
MenubarSubTrigger,
|
| 195 |
+
MenubarGroup,
|
| 196 |
+
MenubarSub,
|
| 197 |
+
MenubarShortcut,
|
| 198 |
+
}
|
frontend/src/components/ui/navigation-menu.jsx
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
| 3 |
+
import { cva } from "class-variance-authority"
|
| 4 |
+
import { ChevronDown } from "lucide-react"
|
| 5 |
+
|
| 6 |
+
import { cn } from "@/lib/utils"
|
| 7 |
+
|
| 8 |
+
const NavigationMenu = React.forwardRef(({ className, children, ...props }, ref) => (
|
| 9 |
+
<NavigationMenuPrimitive.Root
|
| 10 |
+
ref={ref}
|
| 11 |
+
className={cn(
|
| 12 |
+
"relative z-10 flex max-w-max flex-1 items-center justify-center",
|
| 13 |
+
className
|
| 14 |
+
)}
|
| 15 |
+
{...props}>
|
| 16 |
+
{children}
|
| 17 |
+
<NavigationMenuViewport />
|
| 18 |
+
</NavigationMenuPrimitive.Root>
|
| 19 |
+
))
|
| 20 |
+
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
|
| 21 |
+
|
| 22 |
+
const NavigationMenuList = React.forwardRef(({ className, ...props }, ref) => (
|
| 23 |
+
<NavigationMenuPrimitive.List
|
| 24 |
+
ref={ref}
|
| 25 |
+
className={cn(
|
| 26 |
+
"group flex flex-1 list-none items-center justify-center space-x-1",
|
| 27 |
+
className
|
| 28 |
+
)}
|
| 29 |
+
{...props} />
|
| 30 |
+
))
|
| 31 |
+
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
|
| 32 |
+
|
| 33 |
+
const NavigationMenuItem = NavigationMenuPrimitive.Item
|
| 34 |
+
|
| 35 |
+
const navigationMenuTriggerStyle = cva(
|
| 36 |
+
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:text-accent-foreground data-[state=open]:bg-accent/50 data-[state=open]:hover:bg-accent data-[state=open]:focus:bg-accent"
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
const NavigationMenuTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
|
| 40 |
+
<NavigationMenuPrimitive.Trigger
|
| 41 |
+
ref={ref}
|
| 42 |
+
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
| 43 |
+
{...props}>
|
| 44 |
+
{children}{" "}
|
| 45 |
+
<ChevronDown
|
| 46 |
+
className="relative top-[1px] ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180"
|
| 47 |
+
aria-hidden="true" />
|
| 48 |
+
</NavigationMenuPrimitive.Trigger>
|
| 49 |
+
))
|
| 50 |
+
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
|
| 51 |
+
|
| 52 |
+
const NavigationMenuContent = React.forwardRef(({ className, ...props }, ref) => (
|
| 53 |
+
<NavigationMenuPrimitive.Content
|
| 54 |
+
ref={ref}
|
| 55 |
+
className={cn(
|
| 56 |
+
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
|
| 57 |
+
className
|
| 58 |
+
)}
|
| 59 |
+
{...props} />
|
| 60 |
+
))
|
| 61 |
+
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
|
| 62 |
+
|
| 63 |
+
const NavigationMenuLink = NavigationMenuPrimitive.Link
|
| 64 |
+
|
| 65 |
+
const NavigationMenuViewport = React.forwardRef(({ className, ...props }, ref) => (
|
| 66 |
+
<div className={cn("absolute left-0 top-full flex justify-center")}>
|
| 67 |
+
<NavigationMenuPrimitive.Viewport
|
| 68 |
+
className={cn(
|
| 69 |
+
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
|
| 70 |
+
className
|
| 71 |
+
)}
|
| 72 |
+
ref={ref}
|
| 73 |
+
{...props} />
|
| 74 |
+
</div>
|
| 75 |
+
))
|
| 76 |
+
NavigationMenuViewport.displayName =
|
| 77 |
+
NavigationMenuPrimitive.Viewport.displayName
|
| 78 |
+
|
| 79 |
+
const NavigationMenuIndicator = React.forwardRef(({ className, ...props }, ref) => (
|
| 80 |
+
<NavigationMenuPrimitive.Indicator
|
| 81 |
+
ref={ref}
|
| 82 |
+
className={cn(
|
| 83 |
+
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
|
| 84 |
+
className
|
| 85 |
+
)}
|
| 86 |
+
{...props}>
|
| 87 |
+
<div
|
| 88 |
+
className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
| 89 |
+
</NavigationMenuPrimitive.Indicator>
|
| 90 |
+
))
|
| 91 |
+
NavigationMenuIndicator.displayName =
|
| 92 |
+
NavigationMenuPrimitive.Indicator.displayName
|
| 93 |
+
|
| 94 |
+
export {
|
| 95 |
+
navigationMenuTriggerStyle,
|
| 96 |
+
NavigationMenu,
|
| 97 |
+
NavigationMenuList,
|
| 98 |
+
NavigationMenuItem,
|
| 99 |
+
NavigationMenuContent,
|
| 100 |
+
NavigationMenuTrigger,
|
| 101 |
+
NavigationMenuLink,
|
| 102 |
+
NavigationMenuIndicator,
|
| 103 |
+
NavigationMenuViewport,
|
| 104 |
+
}
|
frontend/src/components/ui/pagination.jsx
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
|
| 3 |
+
|
| 4 |
+
import { cn } from "@/lib/utils"
|
| 5 |
+
import { buttonVariants } from "@/components/ui/button";
|
| 6 |
+
|
| 7 |
+
const Pagination = ({
|
| 8 |
+
className,
|
| 9 |
+
...props
|
| 10 |
+
}) => (
|
| 11 |
+
<nav
|
| 12 |
+
role="navigation"
|
| 13 |
+
aria-label="pagination"
|
| 14 |
+
className={cn("mx-auto flex w-full justify-center", className)}
|
| 15 |
+
{...props} />
|
| 16 |
+
)
|
| 17 |
+
Pagination.displayName = "Pagination"
|
| 18 |
+
|
| 19 |
+
const PaginationContent = React.forwardRef(({ className, ...props }, ref) => (
|
| 20 |
+
<ul
|
| 21 |
+
ref={ref}
|
| 22 |
+
className={cn("flex flex-row items-center gap-1", className)}
|
| 23 |
+
{...props} />
|
| 24 |
+
))
|
| 25 |
+
PaginationContent.displayName = "PaginationContent"
|
| 26 |
+
|
| 27 |
+
const PaginationItem = React.forwardRef(({ className, ...props }, ref) => (
|
| 28 |
+
<li ref={ref} className={cn("", className)} {...props} />
|
| 29 |
+
))
|
| 30 |
+
PaginationItem.displayName = "PaginationItem"
|
| 31 |
+
|
| 32 |
+
const PaginationLink = ({
|
| 33 |
+
className,
|
| 34 |
+
isActive,
|
| 35 |
+
size = "icon",
|
| 36 |
+
...props
|
| 37 |
+
}) => (
|
| 38 |
+
<a
|
| 39 |
+
aria-current={isActive ? "page" : undefined}
|
| 40 |
+
className={cn(buttonVariants({
|
| 41 |
+
variant: isActive ? "outline" : "ghost",
|
| 42 |
+
size,
|
| 43 |
+
}), className)}
|
| 44 |
+
{...props} />
|
| 45 |
+
)
|
| 46 |
+
PaginationLink.displayName = "PaginationLink"
|
| 47 |
+
|
| 48 |
+
const PaginationPrevious = ({
|
| 49 |
+
className,
|
| 50 |
+
...props
|
| 51 |
+
}) => (
|
| 52 |
+
<PaginationLink
|
| 53 |
+
aria-label="Go to previous page"
|
| 54 |
+
size="default"
|
| 55 |
+
className={cn("gap-1 pl-2.5", className)}
|
| 56 |
+
{...props}>
|
| 57 |
+
<ChevronLeft className="h-4 w-4" />
|
| 58 |
+
<span>Previous</span>
|
| 59 |
+
</PaginationLink>
|
| 60 |
+
)
|
| 61 |
+
PaginationPrevious.displayName = "PaginationPrevious"
|
| 62 |
+
|
| 63 |
+
const PaginationNext = ({
|
| 64 |
+
className,
|
| 65 |
+
...props
|
| 66 |
+
}) => (
|
| 67 |
+
<PaginationLink
|
| 68 |
+
aria-label="Go to next page"
|
| 69 |
+
size="default"
|
| 70 |
+
className={cn("gap-1 pr-2.5", className)}
|
| 71 |
+
{...props}>
|
| 72 |
+
<span>Next</span>
|
| 73 |
+
<ChevronRight className="h-4 w-4" />
|
| 74 |
+
</PaginationLink>
|
| 75 |
+
)
|
| 76 |
+
PaginationNext.displayName = "PaginationNext"
|
| 77 |
+
|
| 78 |
+
const PaginationEllipsis = ({
|
| 79 |
+
className,
|
| 80 |
+
...props
|
| 81 |
+
}) => (
|
| 82 |
+
<span
|
| 83 |
+
aria-hidden
|
| 84 |
+
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
| 85 |
+
{...props}>
|
| 86 |
+
<MoreHorizontal className="h-4 w-4" />
|
| 87 |
+
<span className="sr-only">More pages</span>
|
| 88 |
+
</span>
|
| 89 |
+
)
|
| 90 |
+
PaginationEllipsis.displayName = "PaginationEllipsis"
|
| 91 |
+
|
| 92 |
+
export {
|
| 93 |
+
Pagination,
|
| 94 |
+
PaginationContent,
|
| 95 |
+
PaginationLink,
|
| 96 |
+
PaginationItem,
|
| 97 |
+
PaginationPrevious,
|
| 98 |
+
PaginationNext,
|
| 99 |
+
PaginationEllipsis,
|
| 100 |
+
}
|
frontend/src/components/ui/popover.jsx
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
| 3 |
+
|
| 4 |
+
import { cn } from "@/lib/utils"
|
| 5 |
+
|
| 6 |
+
const Popover = PopoverPrimitive.Root
|
| 7 |
+
|
| 8 |
+
const PopoverTrigger = PopoverPrimitive.Trigger
|
| 9 |
+
|
| 10 |
+
const PopoverAnchor = PopoverPrimitive.Anchor
|
| 11 |
+
|
| 12 |
+
const PopoverContent = React.forwardRef(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
| 13 |
+
<PopoverPrimitive.Portal>
|
| 14 |
+
<PopoverPrimitive.Content
|
| 15 |
+
ref={ref}
|
| 16 |
+
align={align}
|
| 17 |
+
sideOffset={sideOffset}
|
| 18 |
+
className={cn(
|
| 19 |
+
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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 origin-[--radix-popover-content-transform-origin]",
|
| 20 |
+
className
|
| 21 |
+
)}
|
| 22 |
+
{...props} />
|
| 23 |
+
</PopoverPrimitive.Portal>
|
| 24 |
+
))
|
| 25 |
+
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
| 26 |
+
|
| 27 |
+
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
frontend/src/components/ui/progress.jsx
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
| 3 |
+
|
| 4 |
+
import { cn } from "@/lib/utils"
|
| 5 |
+
|
| 6 |
+
const Progress = React.forwardRef(({ className, value, ...props }, ref) => (
|
| 7 |
+
<ProgressPrimitive.Root
|
| 8 |
+
ref={ref}
|
| 9 |
+
className={cn(
|
| 10 |
+
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
|
| 11 |
+
className
|
| 12 |
+
)}
|
| 13 |
+
{...props}>
|
| 14 |
+
<ProgressPrimitive.Indicator
|
| 15 |
+
className="h-full w-full flex-1 bg-primary transition-all"
|
| 16 |
+
style={{ transform: `translateX(-${100 - (value || 0)}%)` }} />
|
| 17 |
+
</ProgressPrimitive.Root>
|
| 18 |
+
))
|
| 19 |
+
Progress.displayName = ProgressPrimitive.Root.displayName
|
| 20 |
+
|
| 21 |
+
export { Progress }
|
frontend/src/components/ui/radio-group.jsx
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
| 3 |
+
import { Circle } from "lucide-react"
|
| 4 |
+
|
| 5 |
+
import { cn } from "@/lib/utils"
|
| 6 |
+
|
| 7 |
+
const RadioGroup = React.forwardRef(({ className, ...props }, ref) => {
|
| 8 |
+
return (<RadioGroupPrimitive.Root className={cn("grid gap-2", className)} {...props} ref={ref} />);
|
| 9 |
+
})
|
| 10 |
+
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
|
| 11 |
+
|
| 12 |
+
const RadioGroupItem = React.forwardRef(({ className, ...props }, ref) => {
|
| 13 |
+
return (
|
| 14 |
+
<RadioGroupPrimitive.Item
|
| 15 |
+
ref={ref}
|
| 16 |
+
className={cn(
|
| 17 |
+
"aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
| 18 |
+
className
|
| 19 |
+
)}
|
| 20 |
+
{...props}>
|
| 21 |
+
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
| 22 |
+
<Circle className="h-3.5 w-3.5 fill-primary" />
|
| 23 |
+
</RadioGroupPrimitive.Indicator>
|
| 24 |
+
</RadioGroupPrimitive.Item>
|
| 25 |
+
);
|
| 26 |
+
})
|
| 27 |
+
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
|
| 28 |
+
|
| 29 |
+
export { RadioGroup, RadioGroupItem }
|
frontend/src/components/ui/resizable.jsx
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { GripVertical } from "lucide-react"
|
| 2 |
+
import * as ResizablePrimitive from "react-resizable-panels"
|
| 3 |
+
|
| 4 |
+
import { cn } from "@/lib/utils"
|
| 5 |
+
|
| 6 |
+
const ResizablePanelGroup = ({
|
| 7 |
+
className,
|
| 8 |
+
...props
|
| 9 |
+
}) => (
|
| 10 |
+
<ResizablePrimitive.PanelGroup
|
| 11 |
+
className={cn(
|
| 12 |
+
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
| 13 |
+
className
|
| 14 |
+
)}
|
| 15 |
+
{...props} />
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
const ResizablePanel = ResizablePrimitive.Panel
|
| 19 |
+
|
| 20 |
+
const ResizableHandle = ({
|
| 21 |
+
withHandle,
|
| 22 |
+
className,
|
| 23 |
+
...props
|
| 24 |
+
}) => (
|
| 25 |
+
<ResizablePrimitive.PanelResizeHandle
|
| 26 |
+
className={cn(
|
| 27 |
+
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
| 28 |
+
className
|
| 29 |
+
)}
|
| 30 |
+
{...props}>
|
| 31 |
+
{withHandle && (
|
| 32 |
+
<div
|
| 33 |
+
className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
|
| 34 |
+
<GripVertical className="h-2.5 w-2.5" />
|
| 35 |
+
</div>
|
| 36 |
+
)}
|
| 37 |
+
</ResizablePrimitive.PanelResizeHandle>
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
frontend/src/components/ui/scroll-area.jsx
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
| 3 |
+
|
| 4 |
+
import { cn } from "@/lib/utils"
|
| 5 |
+
|
| 6 |
+
const ScrollArea = React.forwardRef(({ className, children, ...props }, ref) => (
|
| 7 |
+
<ScrollAreaPrimitive.Root
|
| 8 |
+
ref={ref}
|
| 9 |
+
className={cn("relative overflow-hidden", className)}
|
| 10 |
+
{...props}>
|
| 11 |
+
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
| 12 |
+
{children}
|
| 13 |
+
</ScrollAreaPrimitive.Viewport>
|
| 14 |
+
<ScrollBar />
|
| 15 |
+
<ScrollAreaPrimitive.Corner />
|
| 16 |
+
</ScrollAreaPrimitive.Root>
|
| 17 |
+
))
|
| 18 |
+
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
| 19 |
+
|
| 20 |
+
const ScrollBar = React.forwardRef(({ className, orientation = "vertical", ...props }, ref) => (
|
| 21 |
+
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
| 22 |
+
ref={ref}
|
| 23 |
+
orientation={orientation}
|
| 24 |
+
className={cn(
|
| 25 |
+
"flex touch-none select-none transition-colors",
|
| 26 |
+
orientation === "vertical" &&
|
| 27 |
+
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
| 28 |
+
orientation === "horizontal" &&
|
| 29 |
+
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
| 30 |
+
className
|
| 31 |
+
)}
|
| 32 |
+
{...props}>
|
| 33 |
+
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
| 34 |
+
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
| 35 |
+
))
|
| 36 |
+
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
| 37 |
+
|
| 38 |
+
export { ScrollArea, ScrollBar }
|
frontend/src/components/ui/select.jsx
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as SelectPrimitive from "@radix-ui/react-select"
|
| 3 |
+
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
| 4 |
+
|
| 5 |
+
import { cn } from "@/lib/utils"
|
| 6 |
+
|
| 7 |
+
const Select = SelectPrimitive.Root
|
| 8 |
+
|
| 9 |
+
const SelectGroup = SelectPrimitive.Group
|
| 10 |
+
|
| 11 |
+
const SelectValue = SelectPrimitive.Value
|
| 12 |
+
|
| 13 |
+
const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
|
| 14 |
+
<SelectPrimitive.Trigger
|
| 15 |
+
ref={ref}
|
| 16 |
+
className={cn(
|
| 17 |
+
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
| 18 |
+
className
|
| 19 |
+
)}
|
| 20 |
+
{...props}>
|
| 21 |
+
{children}
|
| 22 |
+
<SelectPrimitive.Icon asChild>
|
| 23 |
+
<ChevronDown className="h-4 w-4 opacity-50" />
|
| 24 |
+
</SelectPrimitive.Icon>
|
| 25 |
+
</SelectPrimitive.Trigger>
|
| 26 |
+
))
|
| 27 |
+
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
| 28 |
+
|
| 29 |
+
const SelectScrollUpButton = React.forwardRef(({ className, ...props }, ref) => (
|
| 30 |
+
<SelectPrimitive.ScrollUpButton
|
| 31 |
+
ref={ref}
|
| 32 |
+
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
| 33 |
+
{...props}>
|
| 34 |
+
<ChevronUp className="h-4 w-4" />
|
| 35 |
+
</SelectPrimitive.ScrollUpButton>
|
| 36 |
+
))
|
| 37 |
+
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
| 38 |
+
|
| 39 |
+
const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) => (
|
| 40 |
+
<SelectPrimitive.ScrollDownButton
|
| 41 |
+
ref={ref}
|
| 42 |
+
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
| 43 |
+
{...props}>
|
| 44 |
+
<ChevronDown className="h-4 w-4" />
|
| 45 |
+
</SelectPrimitive.ScrollDownButton>
|
| 46 |
+
))
|
| 47 |
+
SelectScrollDownButton.displayName =
|
| 48 |
+
SelectPrimitive.ScrollDownButton.displayName
|
| 49 |
+
|
| 50 |
+
const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => (
|
| 51 |
+
<SelectPrimitive.Portal>
|
| 52 |
+
<SelectPrimitive.Content
|
| 53 |
+
ref={ref}
|
| 54 |
+
className={cn(
|
| 55 |
+
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover 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 origin-[--radix-select-content-transform-origin]",
|
| 56 |
+
position === "popper" &&
|
| 57 |
+
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
| 58 |
+
className
|
| 59 |
+
)}
|
| 60 |
+
position={position}
|
| 61 |
+
{...props}>
|
| 62 |
+
<SelectScrollUpButton />
|
| 63 |
+
<SelectPrimitive.Viewport
|
| 64 |
+
className={cn("p-1", position === "popper" &&
|
| 65 |
+
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]")}>
|
| 66 |
+
{children}
|
| 67 |
+
</SelectPrimitive.Viewport>
|
| 68 |
+
<SelectScrollDownButton />
|
| 69 |
+
</SelectPrimitive.Content>
|
| 70 |
+
</SelectPrimitive.Portal>
|
| 71 |
+
))
|
| 72 |
+
SelectContent.displayName = SelectPrimitive.Content.displayName
|
| 73 |
+
|
| 74 |
+
const SelectLabel = React.forwardRef(({ className, ...props }, ref) => (
|
| 75 |
+
<SelectPrimitive.Label
|
| 76 |
+
ref={ref}
|
| 77 |
+
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
|
| 78 |
+
{...props} />
|
| 79 |
+
))
|
| 80 |
+
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
| 81 |
+
|
| 82 |
+
const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => (
|
| 83 |
+
<SelectPrimitive.Item
|
| 84 |
+
ref={ref}
|
| 85 |
+
className={cn(
|
| 86 |
+
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
| 87 |
+
className
|
| 88 |
+
)}
|
| 89 |
+
{...props}>
|
| 90 |
+
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
| 91 |
+
<SelectPrimitive.ItemIndicator>
|
| 92 |
+
<Check className="h-4 w-4" />
|
| 93 |
+
</SelectPrimitive.ItemIndicator>
|
| 94 |
+
</span>
|
| 95 |
+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
| 96 |
+
</SelectPrimitive.Item>
|
| 97 |
+
))
|
| 98 |
+
SelectItem.displayName = SelectPrimitive.Item.displayName
|
| 99 |
+
|
| 100 |
+
const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => (
|
| 101 |
+
<SelectPrimitive.Separator
|
| 102 |
+
ref={ref}
|
| 103 |
+
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
| 104 |
+
{...props} />
|
| 105 |
+
))
|
| 106 |
+
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
| 107 |
+
|
| 108 |
+
export {
|
| 109 |
+
Select,
|
| 110 |
+
SelectGroup,
|
| 111 |
+
SelectValue,
|
| 112 |
+
SelectTrigger,
|
| 113 |
+
SelectContent,
|
| 114 |
+
SelectLabel,
|
| 115 |
+
SelectItem,
|
| 116 |
+
SelectSeparator,
|
| 117 |
+
SelectScrollUpButton,
|
| 118 |
+
SelectScrollDownButton,
|
| 119 |
+
}
|