commit 44406b72db2ea886b49f993810c8ecd54d438d0c Author: liangxu Date: Tue Jul 7 23:15:37 2026 +0800 Initial commit: img-infinite-canvas AI design workbench MVP Moteva-style AI design workbench replica. Home prompt creates a project; the project page provides an infinite canvas with node drag, zoom/pan, a design chat panel, history replay, image asset upload, and project save/regenerate. - Backend: go-zero, DDD layering, sqlc/pgx, optional PostgreSQL; memory or Redis cache; asynq job queue; MinIO/S3/R2/OSS object storage; sky-valley/pi agent runtime adapter. - Frontend: Next.js App Router + Vite artifact build, TypeScript, i18n, shadcn/ui components, auth (OTP/Turnstile/Google/WeChat). - Deploy: Docker Compose and k3s manifests. Co-Authored-By: Claude Opus 4.8 (1M context) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6a3ff12 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +.playwright-cli/* +.playwright-mcp/* +frontend/node_modules/* +frontend/dist/* +frontend/output/* +frontend/.next/* +frontend/.vercel/* +frontend/.vercel_build_output/* +output/* +.DS_Store +.tools/bin/* + +# Embedded external tool/skill repos (checked out locally, not vendored) +.ai-context/ai-context/ +.ai-context/zero-skills/ +.tools/mcp-zero/ \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..26911e2 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,61 @@ +DO NOT send optional commentary +- Spend time on thinking: you do not need to use the commentary channel to report progress to me. +- Follow the existing code style. +- Run tests before finalizing changes when possible. +- Do not change unrelated files. + +# go-zero Development Instructions + +You are working in a project configured with the go-zero AI context. + +## Context Sources + +- Start with `.ai-context/ai-context/00-instructions.md` for workflow rules and decision trees. +- Use `.ai-context/ai-context/workflows.md` for task-specific flows. +- Use `.ai-context/ai-context/tools.md` for exact `goctl` commands. +- Use `.ai-context/ai-context/patterns.md` for quick code patterns. +- Load detailed references from `.ai-context/zero-skills/` only when needed. + +## Core Rules + +- Create or update `.api` specs before generating REST API code. +- Create or update `.proto` files before generating RPC code. +- Prefer the local `mcp-zero` MCP server for generation when available. +- `mcp-zero` runs over stdio (`stdin`/`stdout`) with JSON-RPC, not HTTP. +- Use `goctl` directly only as the underlying generator or fallback. +- After generation, run `go mod tidy`, verify imports, and run `go build ./...`. +- Keep handlers thin. Put business logic in `internal/logic/`. +- Inject dependencies through `internal/svc/ServiceContext`. +- Pass `context.Context` through all layers. +- Use configuration files for ports, hosts, credentials, and secrets. +- Generate complete logic, tests, and service documentation for new services. + +## Detailed References + +- REST API patterns: `.ai-context/zero-skills/references/rest-api-patterns.md` +- RPC patterns: `.ai-context/zero-skills/references/rpc-patterns.md` +- Database patterns: `.ai-context/zero-skills/references/database-patterns.md` +- Resilience patterns: `.ai-context/zero-skills/references/resilience-patterns.md` +- goctl command reference: `.ai-context/zero-skills/references/goctl-commands.md` +- Troubleshooting: `.ai-context/zero-skills/troubleshooting/common-issues.md` +- Production best practices: `.ai-context/zero-skills/best-practices/overview.md` + +## Generation Defaults + +- Local MCP server binary: `.tools/bin/mcp-zero` +- MCP transport: stdio via `mcp.StdioTransport{}` +- Set `GOCTL_PATH=/Users/liangxu/go/bin/goctl` when launching `mcp-zero`. +- Prefer MCP tools: `create_api_service`, `create_api_spec`, `generate_api_from_spec`, `create_rpc_service`, `generate_model`, `generate_template`, `analyze_project`. +- Prefer `--style go_zero` when creating new go-zero services unless existing code uses another style. +- Validate API specs with `goctl api validate -api .api`. +- Generate REST APIs with `goctl api go -api .api -dir . --style go_zero`. +- Generate RPC services with `goctl rpc protoc .proto --go_out=. --go-grpc_out=. --zrpc_out=. --style go_zero`. +- Generate database models with `goctl model mysql/pg/mongo` rather than hand-written models. + +## Avoid + +- Business logic in handlers. +- Manual edits to generated routing and boilerplate when a spec change plus regeneration is appropriate. +- Hard-coded configuration or credentials. +- Stub-only implementations for requested features. +- Skipping validation, tests, docs, `go mod tidy`, or build verification after generation. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4c162dc --- /dev/null +++ b/README.md @@ -0,0 +1,112 @@ +# img-infinite-canvas + +Moteva 风格的 AI 设计工作台复刻 MVP:主页 Prompt 创建项目,项目页提供无限画布、节点拖拽、缩放平移、右侧设计对话、历史回放、图片资产上传、项目保存与再生成。 + +## Stack + +- Backend: go-zero, DDD layering, sqlc, pgx, PostgreSQL optional persistence +- Cache: memory or Redis, default memory +- Agent: sky-valley/pi Go agent runtime adapter, local desktop agent disabled +- Image storage: memory, MinIO, AWS S3, Cloudflare R2, Aliyun OSS +- Frontend: Next.js App Router, pnpm, Vite artifact build, TypeScript, gzip, i18n +- Deploy: local binary, Docker Compose, k3s manifests + +## Run Locally + +Backend: + +```bash +cd server +go mod tidy +go run . -f etc/config.yaml +``` + +Frontend: + +```bash +cd frontend +pnpm install +pnpm dev +``` + +Open `http://localhost:5173`. Next.js rewrites `/api` to `http://localhost:8888` in development. `pnpm build` runs `next build` and `vite build`, producing gzip assets in `frontend/dist`. + +## Cache Config + +Default in-memory cache: + +```yaml +Cache: + Driver: memory + TTLSeconds: 300 +``` + +Redis cache: + +```yaml +Cache: + Driver: redis + Addr: localhost:6379 + Password: "" + DB: 0 + TTLSeconds: 300 +``` + +## Storage Config + +Default memory storage is useful for local UI work: + +```yaml +Storage: + Driver: memory +``` + +## Image Object Storage + +Default memory mode is useful for local prototyping. Production can use MinIO, AWS S3, Cloudflare R2, or Aliyun OSS: + +```yaml +ObjectStorage: + Driver: minio # memory|minio|s3|aws|r2|aliyun + Bucket: canvas-assets + Endpoint: localhost:9000 + Region: us-east-1 + AccessKey: canvas + SecretKey: canvas-secret + PublicBase: http://localhost:9000/canvas-assets + UseSSL: false + ExpiresIn: 900 +``` + +PostgreSQL storage: + +```yaml +Storage: + Driver: postgres + DataSource: postgres://canvas:canvas@localhost:5432/canvas?sslmode=disable +``` + +## Docker Compose + +```bash +cd deploy +docker compose up --build +``` + +Open `http://localhost:3000`. + +## k3s + +Build and load/push these images first: + +```bash +docker build -t img-infinite-canvas-api:latest server +docker build -t img-infinite-canvas-web:latest frontend +kubectl apply -f deploy/k3s/img-infinite-canvas.yaml +``` + +The sample ingress host is `canvas.local`. + +## API + +See [server/API.md](server/API.md). diff --git a/deploy/config/api.docker.yaml b/deploy/config/api.docker.yaml new file mode 100644 index 0000000..8466b8a --- /dev/null +++ b/deploy/config/api.docker.yaml @@ -0,0 +1,96 @@ +Name: img_infinite_canvas-api +Host: 0.0.0.0 +Port: 8888 +Timeout: 1200000 +MaxBytes: 33554432 +Storage: + Driver: postgres + DataSource: postgres://canvas:canvas@postgres:5432/canvas?sslmode=disable +Cache: + Driver: redis + Addr: redis:6379 + Password: "" + DB: 0 + TTLSeconds: 300 +JobQueue: + Driver: asynq + Addr: redis:6379 + Password: "" + DB: 0 + Queue: default + Concurrency: 10 + MaxRetry: 3 + TimeoutSeconds: 900 + ShutdownTimeoutSeconds: 30 +Realtime: + Driver: redis + Addr: redis:6379 + Password: "" + DB: 0 + ChannelPrefix: canvas:realtime + FallbackPollMillis: 10000 +Outbox: + Driver: postgres + DataSource: postgres://canvas:canvas@postgres:5432/canvas?sslmode=disable + PollMillis: 1000 + BatchSize: 100 +Auth: + Region: global + RegionMode: manual + TokenSecret: local-dev-change-me + TokenTTLSeconds: 604800 + RefreshTTLSeconds: 2592000 + CodeTTLSeconds: 600 + DevReturnCode: true + Email: + Driver: none # none|smtp + Host: "" + Port: 587 + Username: "" + Password: "" + FromName: hello + FromEmail: hello@auth.moteva.local + BrandName: Moteva + UseTLS: false + StartTLS: true + InsecureSkipVerify: false + TimeoutSeconds: 10 + Turnstile: + Enabled: true + SiteKey: "1x00000000000000000000AA" + SecretKey: "1x0000000000000000000000000000000AA" + SecretKeyEnv: TURNSTILE_SECRET_KEY + SiteVerifyURL: https://challenges.cloudflare.com/turnstile/v0/siteverify + TimeoutSeconds: 5 + Google: + ClientID: "" + TokenInfoURL: https://oauth2.googleapis.com/tokeninfo + TimeoutSeconds: 8 + Wechat: + AppID: "" + AppSecret: "" + RedirectURI: "" + Scope: snsapi_login + AuthURL: https://open.weixin.qq.com/connect/qrconnect + TokenURL: https://api.weixin.qq.com/sns/oauth2/access_token + TimeoutSeconds: 8 +Agent: + Driver: pi + Text: + Driver: vision + Model: gpt-5.4-mini + TimeoutSeconds: 240 + Planner: + Driver: vision + Model: gpt-5.5 + TimeoutSeconds: 240 +ObjectStorage: + Driver: minio + Bucket: canvas-assets + Endpoint: minio:9000 + Region: us-east-1 + AccessKey: canvas + SecretKey: canvas-secret + PublicBase: http://localhost:19000/canvas-assets + UseSSL: false + ExpiresIn: 900 diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml new file mode 100644 index 0000000..24c2cd0 --- /dev/null +++ b/deploy/docker-compose.yml @@ -0,0 +1,91 @@ +services: + postgres: + image: postgres:18-alpine + environment: + POSTGRES_DB: canvas + POSTGRES_USER: canvas + POSTGRES_PASSWORD: canvas + ports: + - "15432:5432" + volumes: + - postgres18-data:/var/lib/postgresql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U canvas -d canvas"] + interval: 5s + timeout: 5s + retries: 20 + + redis: + image: redis:8-alpine + command: ["redis-server", "--appendonly", "yes"] + ports: + - "16379:6379" + volumes: + - redis-data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 20 + + minio: + image: minio/minio:RELEASE.2025-09-07T16-13-09Z + command: ["server", "/data", "--console-address", ":9001"] + environment: + MINIO_ROOT_USER: canvas + MINIO_ROOT_PASSWORD: canvas-secret + volumes: + - minio-data:/data + ports: + - "19000:9000" + - "19001:9001" + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 5s + timeout: 5s + retries: 20 + + api: + build: + context: ../server + command: ["/app/img-infinite-canvas", "-f", "/app/etc/config.yaml", "-mode", "api"] + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + minio: + condition: service_healthy + volumes: + - ./config/api.docker.yaml:/app/etc/config.yaml:ro + ports: + - "8888:8888" + + worker: + build: + context: ../server + command: ["/app/img-infinite-canvas", "-f", "/app/etc/config.yaml", "-mode", "worker"] + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + minio: + condition: service_healthy + volumes: + - ./config/api.docker.yaml:/app/etc/config.yaml:ro + + web: + build: + context: ../frontend + environment: + NEXT_PUBLIC_API_PROXY: http://api:8888 + depends_on: + - api + ports: + - "3000:3000" + +volumes: + postgres18-data: + redis-data: + minio-data: diff --git a/deploy/k3s/img-infinite-canvas.yaml b/deploy/k3s/img-infinite-canvas.yaml new file mode 100644 index 0000000..2249bee --- /dev/null +++ b/deploy/k3s/img-infinite-canvas.yaml @@ -0,0 +1,342 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: img-infinite-canvas +--- +apiVersion: v1 +kind: Secret +metadata: + name: turnstile-secret + namespace: img-infinite-canvas +type: Opaque +stringData: + TURNSTILE_SECRET_KEY: 1x0000000000000000000000000000000AA +--- +apiVersion: v1 +kind: Secret +metadata: + name: postgres-secret + namespace: img-infinite-canvas +type: Opaque +stringData: + POSTGRES_DB: canvas + POSTGRES_USER: canvas + POSTGRES_PASSWORD: canvas +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: api-config + namespace: img-infinite-canvas +data: + config.yaml: | + Name: img_infinite_canvas-api + Host: 0.0.0.0 + Port: 8888 + Timeout: 1200000 + Storage: + Driver: postgres + DataSource: postgres://canvas:canvas@postgres:5432/canvas?sslmode=disable + Cache: + Driver: redis + Addr: redis:6379 + Password: "" + DB: 0 + TTLSeconds: 300 + JobQueue: + Driver: asynq + Addr: redis:6379 + Password: "" + DB: 0 + Queue: default + Concurrency: 10 + MaxRetry: 3 + TimeoutSeconds: 900 + ShutdownTimeoutSeconds: 30 + Agent: + Driver: pi + Auth: + Region: global + RegionMode: manual + TokenSecret: local-dev-change-me + TokenTTLSeconds: 604800 + RefreshTTLSeconds: 2592000 + CodeTTLSeconds: 600 + DevReturnCode: true + Email: + Driver: none + Host: "" + Port: 587 + Username: "" + Password: "" + FromName: hello + FromEmail: hello@auth.moteva.local + BrandName: Moteva + UseTLS: false + StartTLS: true + InsecureSkipVerify: false + TimeoutSeconds: 10 + Turnstile: + Enabled: true + SiteKey: 1x00000000000000000000AA + SecretKey: "" + SecretKeyEnv: TURNSTILE_SECRET_KEY + SiteVerifyURL: https://challenges.cloudflare.com/turnstile/v0/siteverify + TimeoutSeconds: 5 + ObjectStorage: + Driver: minio + Bucket: canvas-assets + Endpoint: minio:9000 + Region: us-east-1 + AccessKey: canvas + SecretKey: canvas-secret + PublicBase: http://minio:9000/canvas-assets + UseSSL: false + ExpiresIn: 900 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgres + namespace: img-infinite-canvas +spec: + replicas: 1 + selector: + matchLabels: + app: postgres + template: + metadata: + labels: + app: postgres + spec: + containers: + - name: postgres + image: postgres:18-alpine + envFrom: + - secretRef: + name: postgres-secret + ports: + - containerPort: 5432 +--- +apiVersion: v1 +kind: Service +metadata: + name: postgres + namespace: img-infinite-canvas +spec: + selector: + app: postgres + ports: + - port: 5432 + targetPort: 5432 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis + namespace: img-infinite-canvas +spec: + replicas: 1 + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: redis:8-alpine + args: ["redis-server", "--appendonly", "yes"] + ports: + - containerPort: 6379 +--- +apiVersion: v1 +kind: Service +metadata: + name: redis + namespace: img-infinite-canvas +spec: + selector: + app: redis + ports: + - port: 6379 + targetPort: 6379 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: minio + namespace: img-infinite-canvas +spec: + replicas: 1 + selector: + matchLabels: + app: minio + template: + metadata: + labels: + app: minio + spec: + containers: + - name: minio + image: minio/minio:RELEASE.2025-09-07T16-13-09Z + args: ["server", "/data", "--console-address", ":9001"] + env: + - name: MINIO_ROOT_USER + value: canvas + - name: MINIO_ROOT_PASSWORD + value: canvas-secret + ports: + - containerPort: 9000 + - containerPort: 9001 +--- +apiVersion: v1 +kind: Service +metadata: + name: minio + namespace: img-infinite-canvas +spec: + selector: + app: minio + ports: + - name: api + port: 9000 + targetPort: 9000 + - name: console + port: 9001 + targetPort: 9001 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: api + namespace: img-infinite-canvas +spec: + replicas: 1 + selector: + matchLabels: + app: api + template: + metadata: + labels: + app: api + spec: + containers: + - name: api + image: img-infinite-canvas-api:latest + imagePullPolicy: IfNotPresent + args: ["-f", "/app/etc/config.yaml", "-mode", "api"] + ports: + - containerPort: 8888 + envFrom: + - secretRef: + name: turnstile-secret + volumeMounts: + - name: api-config + mountPath: /app/etc/config.yaml + subPath: config.yaml + volumes: + - name: api-config + configMap: + name: api-config +--- +apiVersion: v1 +kind: Service +metadata: + name: api + namespace: img-infinite-canvas +spec: + selector: + app: api + ports: + - port: 8888 + targetPort: 8888 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: worker + namespace: img-infinite-canvas +spec: + replicas: 1 + selector: + matchLabels: + app: worker + template: + metadata: + labels: + app: worker + spec: + containers: + - name: worker + image: img-infinite-canvas-api:latest + imagePullPolicy: IfNotPresent + args: ["-f", "/app/etc/config.yaml", "-mode", "worker"] + envFrom: + - secretRef: + name: turnstile-secret + volumeMounts: + - name: api-config + mountPath: /app/etc/config.yaml + subPath: config.yaml + volumes: + - name: api-config + configMap: + name: api-config +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: web + namespace: img-infinite-canvas +spec: + replicas: 1 + selector: + matchLabels: + app: web + template: + metadata: + labels: + app: web + spec: + containers: + - name: web + image: img-infinite-canvas-web:latest + imagePullPolicy: IfNotPresent + env: + - name: NEXT_PUBLIC_API_PROXY + value: http://api:8888 + ports: + - containerPort: 3000 +--- +apiVersion: v1 +kind: Service +metadata: + name: web + namespace: img-infinite-canvas +spec: + selector: + app: web + ports: + - port: 80 + targetPort: 3000 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: web + namespace: img-infinite-canvas +spec: + rules: + - host: canvas.local + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: web + port: + number: 80 diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..38ca961 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,21 @@ +FROM node:22-alpine AS build + +WORKDIR /src +RUN corepack enable +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile + +COPY . . +RUN pnpm build + +FROM node:22-alpine + +WORKDIR /app +ENV NODE_ENV=production +ENV NEXT_PUBLIC_API_PROXY=http://api:8888 + +COPY --from=build /src/.next/standalone ./ +COPY --from=build /src/.next/static ./.next/static + +EXPOSE 3000 +CMD ["node", "server.js"] diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..12f082d --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + Moteva Infinite Canvas + + +
+ + + diff --git a/frontend/next-env.d.ts b/frontend/next-env.d.ts new file mode 100644 index 0000000..9edff1c --- /dev/null +++ b/frontend/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +import "./.next/types/routes.d.ts"; + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/frontend/next.config.ts b/frontend/next.config.ts new file mode 100644 index 0000000..f9a1988 --- /dev/null +++ b/frontend/next.config.ts @@ -0,0 +1,8 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + output: "standalone", + compress: true +}; + +export default nextConfig; diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..e08a747 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,34 @@ +{ + "name": "img-infinite-canvas-frontend", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "next dev -H 0.0.0.0 -p 5173", + "dev:vite": "vite --host 0.0.0.0", + "build": "tsc -b && next build && vite build", + "start": "next start -H 0.0.0.0 -p 3000", + "preview": "vite preview --host 0.0.0.0" + }, + "dependencies": { + "@radix-ui/react-context-menu": "^2.3.2", + "@radix-ui/react-dropdown-menu": "^2.1.19", + "@radix-ui/react-popover": "^1.1.18", + "@radix-ui/react-scroll-area": "^1.2.13", + "@radix-ui/react-switch": "^1.3.2", + "@radix-ui/react-tabs": "^1.1.16", + "lucide-react": "^1.23.0", + "next": "^16.2.10", + "react": "^19.2.7", + "react-dom": "^19.2.7" + }, + "devDependencies": { + "@types/node": "26.1.0", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.3", + "typescript": "^6.0.3", + "vite": "^8.1.3", + "vite-plugin-compression2": "^2.5.3" + } +} diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml new file mode 100644 index 0000000..563ff63 --- /dev/null +++ b/frontend/pnpm-lock.yaml @@ -0,0 +1,1787 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@radix-ui/react-context-menu': + specifier: ^2.3.2 + version: 2.3.2(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.19 + version: 2.1.19(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-popover': + specifier: ^1.1.18 + version: 1.1.18(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-scroll-area': + specifier: ^1.2.13 + version: 1.2.13(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-switch': + specifier: ^1.3.2 + version: 1.3.2(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-tabs': + specifier: ^1.1.16 + version: 1.1.16(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + lucide-react: + specifier: ^1.23.0 + version: 1.23.0(react@19.2.7) + next: + specifier: ^16.2.10 + version: 16.2.10(react-dom@19.2.7)(react@19.2.7) + react: + specifier: ^19.2.7 + version: 19.2.7 + react-dom: + specifier: ^19.2.7 + version: 19.2.7(react@19.2.7) + +devDependencies: + '@types/node': + specifier: 26.1.0 + version: 26.1.0 + '@types/react': + specifier: ^19.2.17 + version: 19.2.17 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.17) + '@vitejs/plugin-react': + specifier: ^6.0.3 + version: 6.0.3(vite@8.1.3) + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.3 + version: 8.1.3(@types/node@26.1.0) + vite-plugin-compression2: + specifier: ^2.5.3 + version: 2.5.3 + +packages: + + /@emnapi/core@1.11.1: + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + requiresBuild: true + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + dev: true + optional: true + + /@emnapi/runtime@1.11.1: + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + requiresBuild: true + dependencies: + tslib: 2.8.1 + optional: true + + /@emnapi/wasi-threads@1.2.2: + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + requiresBuild: true + dependencies: + tslib: 2.8.1 + dev: true + optional: true + + /@floating-ui/core@1.7.5: + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + dependencies: + '@floating-ui/utils': 0.2.11 + dev: false + + /@floating-ui/dom@1.7.6: + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + dev: false + + /@floating-ui/react-dom@2.1.8(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + '@floating-ui/dom': 1.7.6 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + dev: false + + /@floating-ui/utils@0.2.11: + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + dev: false + + /@img/colour@1.1.0: + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + requiresBuild: true + dev: false + optional: true + + /@img/sharp-darwin-arm64@0.34.5: + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + dev: false + optional: true + + /@img/sharp-darwin-x64@0.34.5: + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + dev: false + optional: true + + /@img/sharp-libvips-darwin-arm64@1.2.4: + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-libvips-darwin-x64@1.2.4: + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-libvips-linux-arm64@1.2.4: + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-libvips-linux-arm@1.2.4: + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-libvips-linux-ppc64@1.2.4: + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-libvips-linux-riscv64@1.2.4: + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-libvips-linux-s390x@1.2.4: + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-libvips-linux-x64@1.2.4: + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-libvips-linuxmusl-arm64@1.2.4: + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-libvips-linuxmusl-x64@1.2.4: + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-linux-arm64@0.34.5: + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + dev: false + optional: true + + /@img/sharp-linux-arm@0.34.5: + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + dev: false + optional: true + + /@img/sharp-linux-ppc64@0.34.5: + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + dev: false + optional: true + + /@img/sharp-linux-riscv64@0.34.5: + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + dev: false + optional: true + + /@img/sharp-linux-s390x@0.34.5: + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + dev: false + optional: true + + /@img/sharp-linux-x64@0.34.5: + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + dev: false + optional: true + + /@img/sharp-linuxmusl-arm64@0.34.5: + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + dev: false + optional: true + + /@img/sharp-linuxmusl-x64@0.34.5: + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + dev: false + optional: true + + /@img/sharp-wasm32@0.34.5: + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + requiresBuild: true + dependencies: + '@emnapi/runtime': 1.11.1 + dev: false + optional: true + + /@img/sharp-win32-arm64@0.34.5: + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-win32-ia32@0.34.5: + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-win32-x64@0.34.5: + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1): + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + requiresBuild: true + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + dev: true + optional: true + + /@next/env@16.2.10: + resolution: {integrity: sha512-zLPxg9M0MEHmygpj5OuxjQ+vHMiy/K7cSp74G8ecYolmgUWw0RwN02tF56npup/+qaI8JB97hQgS/r2Hb6QwVA==} + dev: false + + /@next/swc-darwin-arm64@16.2.10: + resolution: {integrity: sha512-v9IdJCa0H0mbo+8z5zwUpOk1Vj7RjkcI5uNYf5Ws1y6szf/p3Mzl9hLaST8SCt6L9h8NGnruZcd2+o0NTNwDhA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@next/swc-darwin-x64@16.2.10: + resolution: {integrity: sha512-17IS0jJRViROGmA9uGdNR8VPJpfbnaVG7E9qhso5jDLkmyd0lSDORWxbcKINzcFqzZqGwGtMSnrFRxBpuUYjLQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@next/swc-linux-arm64-gnu@16.2.10: + resolution: {integrity: sha512-GRQRsRtuciNJvB54AvvuQTiq0oZtFwa1owQqtZD8wwnGpM2L39MV22kpI72YSXLKIyY40LC66EiLFv4PiicXxg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@next/swc-linux-arm64-musl@16.2.10: + resolution: {integrity: sha512-zkN9MQYS7UQBro+FnISUq1itaQjXI9xqISzuQ+2bc921NcJ1x4yPCqrn77tVN6/dOOXaaWVX3k6/bR07pPwK+A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@next/swc-linux-x64-gnu@16.2.10: + resolution: {integrity: sha512-iCVJnwvrPYECvA6WM/7+oo+OiTvedIKLxtCLAZP4xZR3nXa1zmzZyLPbYCmWvpd4CvMYF1EMTafd0ii3DygLvA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@next/swc-linux-x64-musl@16.2.10: + resolution: {integrity: sha512-ov2g4H0dHY9bPoOU83m91hWT7Iq5qy13bUnyyshLU3HGR1Ownn0X9QpmDPc5iIUaahTp7f7LeGAhV4DSFtackw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@next/swc-win32-arm64-msvc@16.2.10: + resolution: {integrity: sha512-DwAnhLX76HQiFFQNgWlcK+JzlnD1rZ+UK/WY0ZMI/deXpvgnesjNYrqcfo1JzBuz4Kf7o3brIBL0glI1junatA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@next/swc-win32-x64-msvc@16.2.10: + resolution: {integrity: sha512-0JXq3b85Jk9Jg4ntLUbXSPvoDw3gpZou7twuKdoFG2jOw635v7+IiXfTaa0TxVMyx78pUjnrVYwLgjKfX4e6/A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@oxc-project/types@0.138.0: + resolution: {integrity: sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==} + dev: true + + /@radix-ui/number@1.1.2: + resolution: {integrity: sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==} + dev: false + + /@radix-ui/primitive@1.1.4: + resolution: {integrity: sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==} + dev: false + + /@radix-ui/react-arrow@1.1.11(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-Kdil9BB1rIFC/khmf4hC35bn8701AJcizTU7G7cUbEbk5XqqbjDuHW60uUfKqO5WojjZcbAW51Q7P0hRmMLw8A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + dev: false + + /@radix-ui/react-collection@1.1.11(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-djW9+zeg137KQdlPtmE8xnaD+K2rcXXMWFrSg0hsmYZ6HRbdTA7tDHFgpaW9+huWVEu0RCabL+985T4TA0BE7g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + dev: false + + /@radix-ui/react-compose-refs@1.1.3(@types/react@19.2.17)(react@19.2.7): + resolution: {integrity: sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 19.2.17 + react: 19.2.7 + dev: false + + /@radix-ui/react-context-menu@2.3.2(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-qzsA/ZPhF6yMxBOTIk1nlCkoy2mswSbwYL+ErBa2iP0s4WWrlxmczArYqMcpVfEjmM7KJj/ADPXky0yZfbSxtQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-menu': 2.1.19(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + dev: false + + /@radix-ui/react-context@1.1.4(@types/react@19.2.17)(react@19.2.7): + resolution: {integrity: sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 19.2.17 + react: 19.2.7 + dev: false + + /@radix-ui/react-direction@1.1.2(@types/react@19.2.17)(react@19.2.7): + resolution: {integrity: sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 19.2.17 + react: 19.2.7 + dev: false + + /@radix-ui/react-dismissable-layer@1.1.14(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-4lUhWTWAjbDIqFrAPWJ3WqBOpO5YchVZ88X3nh6H9Lu5AFi5nCUeTPj3D8FSDmabmFeRe9ME0BDA4MwKTha5GQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + dev: false + + /@radix-ui/react-dropdown-menu@2.1.19(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-HZccBkbK0LOi8nYKIp5jll/zIRW0cCOmG6WWyqsSpmXCU+ZlcBbTqIwlBvPCu886C5RVu6c/kHV7xSP8IgYNHw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-menu': 2.1.19(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + dev: false + + /@radix-ui/react-focus-guards@1.1.4(@types/react@19.2.17)(react@19.2.7): + resolution: {integrity: sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 19.2.17 + react: 19.2.7 + dev: false + + /@radix-ui/react-focus-scope@1.1.11(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-Mn88Vg2whaRocGJNOH+DKFqYm6ySFPQaiwHNxZPyjn99B52KAEJWWY9NP83+nWdk2HM3rdov+STu9AG471Rt9w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + dev: false + + /@radix-ui/react-id@1.1.2(@types/react@19.2.17)(react@19.2.7): + resolution: {integrity: sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + react: 19.2.7 + dev: false + + /@radix-ui/react-menu@2.1.19(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-Mht9BVd1AIsNFVQr4KG3bIK7XQn5IXF0TL/2ObsrzOdc1loaly/+kBDL5roSCYn8j8XZkvpOD0WYLz2FQtH1Eg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.11(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.11(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-popper': 1.3.2(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.14(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + aria-hidden: 1.2.6 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) + dev: false + + /@radix-ui/react-popover@1.1.18(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-qdXDes+eHlnMUGlBAAAe5EG7oOQvqsXuq4mq585diMudg80iB+jHbsSeG3+Q4eWNsogNyhqU2p/3i+Y0iEepqg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.14(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.11(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-popper': 1.3.2(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + aria-hidden: 1.2.6 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) + dev: false + + /@radix-ui/react-popper@1.3.2(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-3QXNeMkdshed1MR3LNoiCirBywRFPkD8ETJa/HlPuLwSajaQixf2ro+isoDNJlGABg9ug41XuZpINZJIle4XWg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-arrow': 1.1.11(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-rect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/rect': 1.1.2 + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + dev: false + + /@radix-ui/react-portal@1.1.13(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-z3oXfmaHLJTF1wktbjgD6cn9jiEbq3WSondB10LIuIt2m2Ym4iJlrW04/euMwENDdWDdE7z+OuY7Qyp1YpRSwA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + dev: false + + /@radix-ui/react-presence@1.1.6(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + dev: false + + /@radix-ui/react-primitive@2.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + dev: false + + /@radix-ui/react-roving-focus@1.1.14(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-8Qcnx9447tx/aCBgw6Jenfqg4Skq+vqab9mCBmuGNipIS5YXvL275wbKEu7+ICYHIlAPgCduUMJH1XOYewKF6Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.11(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + dev: false + + /@radix-ui/react-scroll-area@1.2.13(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-7tncSubo2G0UY1e8rk+72qe3XRzrGnOLtZQ1PL1KoBfRUNX0NrJT5akb+0kfwSCc3gVR4wdHqyhAQBDpDNOwDw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/number': 1.1.2 + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + dev: false + + /@radix-ui/react-slot@1.3.0(@types/react@19.2.17)(react@19.2.7): + resolution: {integrity: sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + react: 19.2.7 + dev: false + + /@radix-ui/react-switch@1.3.2(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-tgRBI3DdNwAJYE4BBZyZcz/HRRCvAsPkRvG1wvKc+41tBGMxPn/a87T/wikXAvyDypNQ9kaZwHbeZe+veHCGpA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + dev: false + + /@radix-ui/react-tabs@1.1.16(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-v3Ab2l7z6U7tRB4xA0IyKdq0OsqaO1o9ZjsIEoKKnSZ/l96mZz8aCTX0NCXw+YVHJXr8Km4d+Mn6/Q8YjXa+gw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.14(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + dev: false + + /@radix-ui/react-use-callback-ref@1.1.2(@types/react@19.2.17)(react@19.2.7): + resolution: {integrity: sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 19.2.17 + react: 19.2.7 + dev: false + + /@radix-ui/react-use-controllable-state@1.2.3(@types/react@19.2.17)(react@19.2.7): + resolution: {integrity: sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + react: 19.2.7 + dev: false + + /@radix-ui/react-use-effect-event@0.0.3(@types/react@19.2.17)(react@19.2.7): + resolution: {integrity: sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + react: 19.2.7 + dev: false + + /@radix-ui/react-use-layout-effect@1.1.2(@types/react@19.2.17)(react@19.2.7): + resolution: {integrity: sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 19.2.17 + react: 19.2.7 + dev: false + + /@radix-ui/react-use-previous@1.1.2(@types/react@19.2.17)(react@19.2.7): + resolution: {integrity: sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 19.2.17 + react: 19.2.7 + dev: false + + /@radix-ui/react-use-rect@1.1.2(@types/react@19.2.17)(react@19.2.7): + resolution: {integrity: sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/rect': 1.1.2 + '@types/react': 19.2.17 + react: 19.2.7 + dev: false + + /@radix-ui/react-use-size@1.1.2(@types/react@19.2.17)(react@19.2.7): + resolution: {integrity: sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + react: 19.2.7 + dev: false + + /@radix-ui/rect@1.1.2: + resolution: {integrity: sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==} + dev: false + + /@rolldown/binding-android-arm64@1.1.4: + resolution: {integrity: sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rolldown/binding-darwin-arm64@1.1.4: + resolution: {integrity: sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rolldown/binding-darwin-x64@1.1.4: + resolution: {integrity: sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rolldown/binding-freebsd-x64@1.1.4: + resolution: {integrity: sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@rolldown/binding-linux-arm-gnueabihf@1.1.4: + resolution: {integrity: sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rolldown/binding-linux-arm64-gnu@1.1.4: + resolution: {integrity: sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rolldown/binding-linux-arm64-musl@1.1.4: + resolution: {integrity: sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rolldown/binding-linux-ppc64-gnu@1.1.4: + resolution: {integrity: sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rolldown/binding-linux-s390x-gnu@1.1.4: + resolution: {integrity: sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rolldown/binding-linux-x64-gnu@1.1.4: + resolution: {integrity: sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rolldown/binding-linux-x64-musl@1.1.4: + resolution: {integrity: sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rolldown/binding-openharmony-arm64@1.1.4: + resolution: {integrity: sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + requiresBuild: true + dev: true + optional: true + + /@rolldown/binding-wasm32-wasi@1.1.4: + resolution: {integrity: sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + requiresBuild: true + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + dev: true + optional: true + + /@rolldown/binding-win32-arm64-msvc@1.1.4: + resolution: {integrity: sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rolldown/binding-win32-x64-msvc@1.1.4: + resolution: {integrity: sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rolldown/pluginutils@1.0.1: + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + dev: true + + /@rollup/pluginutils@5.4.0: + resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@types/estree': 1.0.9 + estree-walker: 2.0.2 + picomatch: 4.0.5 + dev: true + + /@swc/helpers@0.5.15: + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + dependencies: + tslib: 2.8.1 + dev: false + + /@tybys/wasm-util@0.10.3: + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + requiresBuild: true + dependencies: + tslib: 2.8.1 + dev: true + optional: true + + /@types/estree@1.0.9: + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + dev: true + + /@types/node@26.1.0: + resolution: {integrity: sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==} + dependencies: + undici-types: 8.3.0 + dev: true + + /@types/react-dom@19.2.3(@types/react@19.2.17): + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + dependencies: + '@types/react': 19.2.17 + + /@types/react@19.2.17: + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + dependencies: + csstype: 3.2.3 + + /@vitejs/plugin-react@6.0.3(vite@8.1.3): + resolution: {integrity: sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.1.3(@types/node@26.1.0) + dev: true + + /aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + dependencies: + tslib: 2.8.1 + dev: false + + /baseline-browser-mapping@2.10.41: + resolution: {integrity: sha512-WwS7MHhqGHHlaVsqRZnhvCEMS0owDX+SxRlve7JkuH7My1Ara3ZriTmCQupPfYjxMZ8I/tgxtJYr2t7taHaH4A==} + engines: {node: '>=6.0.0'} + hasBin: true + dev: false + + /caniuse-lite@1.0.30001800: + resolution: {integrity: sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==} + dev: false + + /client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + dev: false + + /csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + /detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + /detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + dev: false + + /estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + dev: true + + /fdir@6.5.0(picomatch@4.0.5): + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + dependencies: + picomatch: 4.0.5 + dev: true + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + dev: false + + /lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + dev: true + + /lucide-react@1.23.0(react@19.2.7): + resolution: {integrity: sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + dependencies: + react: 19.2.7 + dev: false + + /nanoid@3.3.15: + resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + /next@16.2.10(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-2som5AVXb3kE6Yjine3/mNbBayYF58eguBWIVVUdr1y/L426xyVEgYxgBG+1QC34P2x5E+tcDup6XkuOAX3dCA==} + engines: {node: '>=20.9.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + dependencies: + '@next/env': 16.2.10 + '@swc/helpers': 0.5.15 + baseline-browser-mapping: 2.10.41 + caniuse-lite: 1.0.30001800 + postcss: 8.4.31 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + styled-jsx: 5.1.6(react@19.2.7) + optionalDependencies: + '@next/swc-darwin-arm64': 16.2.10 + '@next/swc-darwin-x64': 16.2.10 + '@next/swc-linux-arm64-gnu': 16.2.10 + '@next/swc-linux-arm64-musl': 16.2.10 + '@next/swc-linux-x64-gnu': 16.2.10 + '@next/swc-linux-x64-musl': 16.2.10 + '@next/swc-win32-arm64-msvc': 16.2.10 + '@next/swc-win32-x64-msvc': 16.2.10 + sharp: 0.34.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + dev: false + + /picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + /picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + dev: true + + /postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.15 + picocolors: 1.1.1 + source-map-js: 1.2.1 + dev: false + + /postcss@8.5.16: + resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.15 + picocolors: 1.1.1 + source-map-js: 1.2.1 + dev: true + + /react-dom@19.2.7(react@19.2.7): + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} + peerDependencies: + react: ^19.2.7 + dependencies: + react: 19.2.7 + scheduler: 0.27.0 + dev: false + + /react-remove-scroll-bar@2.3.8(@types/react@19.2.17)(react@19.2.7): + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 19.2.17 + react: 19.2.7 + react-style-singleton: 2.2.3(@types/react@19.2.17)(react@19.2.7) + tslib: 2.8.1 + dev: false + + /react-remove-scroll@2.7.2(@types/react@19.2.17)(react@19.2.7): + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 19.2.17 + react: 19.2.7 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.17)(react@19.2.7) + react-style-singleton: 2.2.3(@types/react@19.2.17)(react@19.2.7) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.17)(react@19.2.7) + use-sidecar: 1.1.3(@types/react@19.2.17)(react@19.2.7) + dev: false + + /react-style-singleton@2.2.3(@types/react@19.2.17)(react@19.2.7): + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 19.2.17 + get-nonce: 1.0.1 + react: 19.2.7 + tslib: 2.8.1 + dev: false + + /react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} + engines: {node: '>=0.10.0'} + dev: false + + /rolldown@1.1.4: + resolution: {integrity: sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + dependencies: + '@oxc-project/types': 0.138.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.1.4 + '@rolldown/binding-darwin-arm64': 1.1.4 + '@rolldown/binding-darwin-x64': 1.1.4 + '@rolldown/binding-freebsd-x64': 1.1.4 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.4 + '@rolldown/binding-linux-arm64-gnu': 1.1.4 + '@rolldown/binding-linux-arm64-musl': 1.1.4 + '@rolldown/binding-linux-ppc64-gnu': 1.1.4 + '@rolldown/binding-linux-s390x-gnu': 1.1.4 + '@rolldown/binding-linux-x64-gnu': 1.1.4 + '@rolldown/binding-linux-x64-musl': 1.1.4 + '@rolldown/binding-openharmony-arm64': 1.1.4 + '@rolldown/binding-wasm32-wasi': 1.1.4 + '@rolldown/binding-win32-arm64-msvc': 1.1.4 + '@rolldown/binding-win32-x64-msvc': 1.1.4 + dev: true + + /scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + dev: false + + /semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + requiresBuild: true + dev: false + optional: true + + /sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + requiresBuild: true + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + dev: false + optional: true + + /source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + /styled-jsx@5.1.6(react@19.2.7): + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + dependencies: + client-only: 0.0.1 + react: 19.2.7 + dev: false + + /tar-mini@0.2.0: + resolution: {integrity: sha512-+qfUHz700DWnRutdUsxRRVZ38G1Qr27OetwaMYTdg8hcPxf46U0S1Zf76dQMWRBmusOt2ZCK5kbIaiLkoGO7WQ==} + dev: true + + /tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + dev: true + + /tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + requiresBuild: true + + /typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + dev: true + + /undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + dev: true + + /use-callback-ref@1.3.3(@types/react@19.2.17)(react@19.2.7): + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 19.2.17 + react: 19.2.7 + tslib: 2.8.1 + dev: false + + /use-sidecar@1.1.3(@types/react@19.2.17)(react@19.2.7): + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 19.2.17 + detect-node-es: 1.1.0 + react: 19.2.7 + tslib: 2.8.1 + dev: false + + /vite-plugin-compression2@2.5.3: + resolution: {integrity: sha512-ItPgqQWkcnBbVw7is9OKwiZ8v6+ju9rYROl5Lp6QfQDEx/d55AwJQb/KLpsQqsU9HoigYBsZ8tK6I02UwJNvEw==} + dependencies: + '@rollup/pluginutils': 5.4.0 + tar-mini: 0.2.0 + transitivePeerDependencies: + - rollup + dev: true + + /vite@8.1.3(@types/node@26.1.0): + resolution: {integrity: sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.3.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + dependencies: + '@types/node': 26.1.0 + lightningcss: 1.32.0 + picomatch: 4.0.5 + postcss: 8.5.16 + rolldown: 1.1.4 + tinyglobby: 0.2.17 + optionalDependencies: + fsevents: 2.3.3 + dev: true diff --git a/frontend/src/app/api/[...path]/route.ts b/frontend/src/app/api/[...path]/route.ts new file mode 100644 index 0000000..97dae30 --- /dev/null +++ b/frontend/src/app/api/[...path]/route.ts @@ -0,0 +1,56 @@ +import type { NextRequest } from "next/server"; + +export const dynamic = "force-dynamic"; +export const maxDuration = 120; +export const runtime = "nodejs"; + +const apiBase = () => process.env.NEXT_PUBLIC_API_PROXY ?? "http://localhost:8888"; + +const hopByHopHeaders = new Set([ + "connection", + "content-encoding", + "content-length", + "host", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade" +]); + +type RouteContext = { + params: Promise<{ + path?: string[]; + }>; +}; + +async function proxy(request: NextRequest, context: RouteContext) { + const { path = [] } = await context.params; + const target = new URL(`/api/${path.join("/")}${request.nextUrl.search}`, apiBase()); + const headers = new Headers(request.headers); + hopByHopHeaders.forEach((header) => headers.delete(header)); + + const response = await fetch(target, { + method: request.method, + headers, + body: ["GET", "HEAD"].includes(request.method) ? undefined : await request.arrayBuffer(), + cache: "no-store" + }); + + const responseHeaders = new Headers(response.headers); + hopByHopHeaders.forEach((header) => responseHeaders.delete(header)); + + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers: responseHeaders + }); +} + +export const GET = proxy; +export const POST = proxy; +export const PUT = proxy; +export const PATCH = proxy; +export const DELETE = proxy; diff --git a/frontend/src/app/icon.svg b/frontend/src/app/icon.svg new file mode 100644 index 0000000..04f5679 --- /dev/null +++ b/frontend/src/app/icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx new file mode 100644 index 0000000..4baa75a --- /dev/null +++ b/frontend/src/app/layout.tsx @@ -0,0 +1,19 @@ +import type { Metadata } from "next"; +import type { ReactNode } from "react"; +import "@/ui/styles.css"; + +export const metadata: Metadata = { + title: "Moteva Infinite Canvas", + description: "AI design workspace", + icons: { + icon: "/icon.svg" + } +}; + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx new file mode 100644 index 0000000..4ab06a6 --- /dev/null +++ b/frontend/src/app/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ClientRoot } from "@/app/root"; + +export default function Page() { + return ; +} diff --git a/frontend/src/app/root.tsx b/frontend/src/app/root.tsx new file mode 100644 index 0000000..02a2d8f --- /dev/null +++ b/frontend/src/app/root.tsx @@ -0,0 +1,18 @@ +"use client"; + +import { I18nProvider } from "@/i18n/i18n"; +import { App } from "@/ui/App"; +import { AuthProvider } from "@/ui/auth/AuthProvider"; +import { GlobalToastProvider } from "@/ui/components/CanvasToast"; + +export function ClientRoot() { + return ( + + + + + + + + ); +} diff --git a/frontend/src/application/route.ts b/frontend/src/application/route.ts new file mode 100644 index 0000000..da55163 --- /dev/null +++ b/frontend/src/application/route.ts @@ -0,0 +1,34 @@ +export type AppRoute = + | { name: "home" } + | { name: "projects" } + | { name: "project"; id: string }; + +export function readRoute(): AppRoute { + if (typeof window === "undefined") { + return { name: "home" }; + } + const hash = window.location.hash.replace(/^#/, ""); + if (hash === "/projects") { + return { name: "projects" }; + } + const match = hash.match(/^\/project\/([^/]+)$/); + if (match) { + return { name: "project", id: decodeURIComponent(match[1]) }; + } + return { name: "home" }; +} + +export function openHome() { + if (typeof window === "undefined") return; + window.location.hash = "/"; +} + +export function openProject(id: string) { + if (typeof window === "undefined") return; + window.location.hash = `/project/${encodeURIComponent(id)}`; +} + +export function openProjects() { + if (typeof window === "undefined") return; + window.location.hash = "/projects"; +} diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx new file mode 100644 index 0000000..c813b70 --- /dev/null +++ b/frontend/src/components/ui/button.tsx @@ -0,0 +1,9 @@ +"use client"; + +import type { ButtonHTMLAttributes } from "react"; + +type ButtonProps = ButtonHTMLAttributes; + +export function Button({ className = "", type = "button", ...props }: ButtonProps) { + return + +
+ +
+ + {step === "email" ? ( +
+

{t("loginWelcome")}

+

{t("loginSubtitle")}

+ + + +
+ +
{ + event.preventDefault(); + void requestEmailCode(); + }} + > + setEmail(event.target.value)} + /> + {turnstileEnabled && turnstileSiteKey && ( + + )} + + + +

{t("loginTerms")}

+
+ ) : ( +
+ +

{t("loginCheckEmail")}

+

+ {t("loginCodeSent")} + {codeResponse?.target || normalizedEmail} +

+ { + setCode(value); + if (codeError) setCodeError(""); + }} + onComplete={(value) => void verifyEmailCode(value)} + /> +
+ {codeError && ( +
+ + {codeError} +
+ )} +
+ +
+ )} + + {error &&
{error}
} + +
+ ); +} + +function GoogleIcon() { + return ( + + ); +} + +function loadGoogleIdentityScript() { + if (window.google?.accounts?.id) return Promise.resolve(); + if (!googleScriptPromise) { + googleScriptPromise = new Promise((resolve, reject) => { + const existing = document.querySelector('script[src="https://accounts.google.com/gsi/client"]'); + if (existing) { + existing.addEventListener("load", () => resolve(), { once: true }); + existing.addEventListener("error", () => reject(new Error("Google login failed to load")), { once: true }); + return; + } + const script = document.createElement("script"); + script.src = "https://accounts.google.com/gsi/client"; + script.async = true; + script.defer = true; + script.onload = () => resolve(); + script.onerror = () => reject(new Error("Google login failed to load")); + document.head.appendChild(script); + }); + } + return googleScriptPromise; +} diff --git a/frontend/src/ui/components/AccountManagementDialog/index.css b/frontend/src/ui/components/AccountManagementDialog/index.css new file mode 100644 index 0000000..c74feb9 --- /dev/null +++ b/frontend/src/ui/components/AccountManagementDialog/index.css @@ -0,0 +1,607 @@ +.account-management-backdrop { + position: fixed; + inset: 0; + z-index: 120; + display: grid; + place-items: center; + padding: 40px 16px; + overflow: hidden; + background: rgba(17, 24, 39, 0.14); + overscroll-behavior: contain; +} + +.account-management-dialog { + position: relative; + width: min(1160px, calc(100vw - 32px)); + height: min(calc(100vh - 80px), 720px); + min-height: 0; + overflow: hidden; + border: 1px solid #e5e7eb; + border-radius: 16px; + background: #fff; + box-shadow: 0 24px 70px rgba(17, 24, 39, 0.18); + overscroll-behavior: contain; +} + +.account-management-close { + position: absolute; + top: 16px; + right: 16px; + z-index: 2; + width: 32px; + height: 32px; + display: grid; + place-items: center; + border-radius: 8px; + color: #333842; + background: transparent; +} + +.account-management-close:hover { + background: rgba(17, 24, 39, 0.06); +} + +.account-management-layout { + height: 100%; + min-height: 0; + display: flex; + overflow: hidden; + background: #fff; +} + +.account-management-sidebar { + width: 200px; + flex: 0 0 auto; + display: flex; + flex-direction: column; + gap: 2px; + padding: 12px; + background: linear-gradient(rgba(0, 0, 0, 0.04), rgba(0, 0, 0, 0.04)), #fff; +} + +.account-management-sidebar-title { + height: 44px; + display: flex; + align-items: center; + overflow: hidden; + padding: 0 12px; + border-radius: 10px; + color: #20242c; + font-size: 14px; + line-height: 20px; + font-weight: 600; + white-space: nowrap; +} + +.account-management-nav { + width: 100%; + height: 36px; + display: flex; + align-items: center; + gap: 8px; + overflow: hidden; + padding: 0 12px; + border-radius: 10px; + color: #262a31; + background: transparent; + font-size: 14px; + line-height: 20px; + font-weight: 400; + text-align: left; +} + +.account-management-nav svg { + flex: 0 0 auto; +} + +.account-management-nav span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.account-management-nav.active { + background: rgba(17, 24, 39, 0.08); +} + +.account-management-nav:hover { + background: rgba(17, 24, 39, 0.06); +} + +.account-management-scroll { + min-width: 0; + flex: 1; + overflow-y: auto; + overscroll-behavior: contain; + -ms-overflow-style: none; + scrollbar-color: transparent transparent; + scrollbar-gutter: auto; + scrollbar-width: none; +} + +.account-management-scroll.is-scrolling { + scrollbar-color: transparent transparent; +} + +.account-management-scroll::-webkit-scrollbar { + display: none; + width: 0; + height: 0; +} + +.account-management-scroll::-webkit-scrollbar-track { + background: transparent; +} + +.account-management-scroll::-webkit-scrollbar-thumb { + border: 2px solid transparent; + border-radius: 999px; + background: transparent; + background-clip: content-box; +} + +.account-management-scroll.is-scrolling::-webkit-scrollbar-thumb { + background-color: #b8bec8; +} + +.account-management-content { + width: 100%; + max-width: 760px; + margin: 0 auto; + padding: 24px 60px 40px; +} + +.account-management-sections { + display: flex; + flex-direction: column; + gap: 48px; +} + +.account-management-section { + display: flex; + flex-direction: column; + gap: 24px; +} + +.account-management-section-heading { + display: flex; + align-items: center; + padding-bottom: 12px; + border-bottom: 1px solid #e5e7eb; +} + +.account-management-section-heading h3 { + margin: 0; + color: #20242c; + font-size: 16px; + line-height: 24px; + font-weight: 600; +} + +.account-management-profile-row, +.account-management-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + min-width: 0; + overflow: hidden; +} + +.account-management-profile { + min-width: 0; + display: flex; + align-items: center; + gap: 12px; +} + +.account-management-avatar { + width: 54px; + height: 54px; + flex: 0 0 auto; + display: grid; + place-items: center; + border-radius: 999px; + object-fit: cover; + background: #20242c; +} + +.account-management-avatar-fallback { + color: #fff; + font-size: 17px; + font-weight: 850; + text-transform: uppercase; +} + +.account-management-file-input { + display: none; +} + +.account-management-profile-text { + min-width: 0; + display: flex; + flex-direction: column; +} + +.account-management-profile-text strong { + color: #20242c; + font-size: 14px; + line-height: 20px; + font-weight: 600; +} + +.account-management-profile-text span { + color: #737a86; + font-size: 13px; + line-height: 18px; +} + +.account-management-label { + flex: 0 0 auto; + color: #20242c; + font-size: 14px; + line-height: 20px; + font-weight: 600; +} + +.account-management-value, +.account-management-inline-edit { + min-width: 0; + color: #20242c; + font-size: 14px; + line-height: 20px; +} + +.account-management-value.muted { + color: #737a86; +} + +.account-management-inline-edit { + display: inline-flex; + align-items: center; + gap: 6px; + overflow: hidden; + background: transparent; +} + +.account-management-inline-edit span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.account-management-inline-edit svg { + flex: 0 0 auto; + color: #343943; +} + +.account-management-name-form { + min-width: 0; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; +} + +.account-management-name-form input { + width: min(240px, 42vw); + height: 36px; + padding: 0 11px; + border: 1px solid #d9dde4; + border-radius: 8px; + outline: none; + color: #20242c; + background: #fff; + font: inherit; + font-size: 15px; +} + +.account-management-name-form input:focus { + border-color: #20242c; + box-shadow: 0 0 0 3px rgba(17, 24, 39, 0.08); +} + +.account-management-name-form button { + height: 32px; + padding: 0 10px; + border: 1px solid #e0e3e8; + border-radius: 8px; + color: #20242c; + background: #fff; + font-size: 13px; + white-space: nowrap; +} + +.account-management-name-form button[type="submit"] { + color: #fff; + border-color: #20242c; + background: #20242c; +} + +.account-management-feedback { + position: sticky; + top: 10px; + z-index: 1; + width: fit-content; + max-width: 100%; + margin: 0 0 14px; + padding: 6px 10px; + border-radius: 8px; + color: #1c9d55; + background: rgba(49, 207, 116, 0.1); + font-size: 13px; + line-height: 18px; + font-weight: 600; +} + +.account-management-feedback.error { + color: #d64d34; + background: rgba(214, 77, 52, 0.1); +} + +.account-management-button { + min-width: 80px; + height: 36px; + flex: 0 0 auto; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 12px; + border: 1px solid #e0e3e8; + border-radius: 8px; + color: #20242c; + background: transparent; + font-size: 14px; + line-height: 20px; + font-weight: 500; + white-space: nowrap; +} + +.account-management-button:hover:not(:disabled) { + background: rgba(17, 24, 39, 0.05); +} + +.account-management-button:disabled { + border-color: #e6e8ec; + color: #aeb4bd; + background: rgba(17, 24, 39, 0.05); + opacity: 1; +} + +.account-management-button.danger { + border-color: transparent; + color: #8d939d; + background: rgba(17, 24, 39, 0.06); +} + +.account-management-button.danger:hover:not(:disabled) { + color: #d64d34; + background: rgba(214, 77, 52, 0.1); +} + +.account-management-button.small { + min-width: auto; + height: 28px; + padding: 0 10px; + font-size: 13px; + line-height: 18px; +} + +.account-management-button.current { + color: #aeb4bd; + background: rgba(17, 24, 39, 0.03); +} + +.account-management-device-action { + display: flex; + align-items: center; + justify-content: space-between; + gap: 32px; + min-width: 0; +} + +.account-management-device-action div { + min-width: 0; +} + +.account-management-device-action strong, +.account-management-logout-copy strong { + display: block; + color: #20242c; + font-size: 14px; + line-height: 20px; + font-weight: 600; +} + +.account-management-device-action p { + margin: 2px 0 0; + color: #737a86; + font-size: 13px; + line-height: 19px; + font-weight: 400; +} + +.account-management-device-table { + display: grid; + grid-template-columns: 100px minmax(0, 1fr) 120px; + overflow: hidden; + border-top: 1px solid #e5e7eb; +} + +.account-management-device-col { + min-width: 0; + display: flex; + flex-direction: column; +} + +.account-management-device-head, +.account-management-device-cell { + display: flex; + align-items: center; + border-bottom: 1px solid #e5e7eb; +} + +.account-management-device-head { + height: 36px; + color: #737a86; + font-size: 12px; + line-height: 16px; + font-weight: 500; +} + +.account-management-device-col.info .account-management-device-head, +.account-management-device-col.info .account-management-device-cell { + padding: 0 12px; +} + +.account-management-device-col.action .account-management-device-cell { + justify-content: flex-end; +} + +.account-management-device-cell { + height: 48px; + color: #20242c; + font-size: 12px; + line-height: 16px; +} + +.account-management-device-cell span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.account-management-online-dot { + width: 7px; + height: 7px; + margin-left: 9px; + border-radius: 999px; + background: #31cf74; +} + +.account-management-logout-copy { + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; +} + +.account-management-logout-copy span { + color: #737a86; + font-size: 13px; + line-height: 19px; +} + +.account-management-confirm-backdrop { + position: absolute; + inset: 0; + z-index: 4; + display: grid; + place-items: center; + padding: 24px; + background: rgba(17, 24, 39, 0.18); +} + +.account-management-confirm { + width: min(360px, 100%); + padding: 22px; + border: 1px solid #e5e7eb; + border-radius: 12px; + background: #fff; + box-shadow: 0 24px 70px rgba(17, 24, 39, 0.22); +} + +.account-management-confirm h4 { + margin: 0; + color: #20242c; + font-size: 17px; + line-height: 24px; + font-weight: 600; +} + +.account-management-confirm p { + margin: 8px 0 20px; + color: #737a86; + font-size: 14px; + line-height: 22px; +} + +.account-management-confirm div { + display: flex; + justify-content: flex-end; + gap: 10px; +} + +.account-management-confirm button { + height: 36px; + padding: 0 14px; + border: 1px solid #e0e3e8; + border-radius: 8px; + color: #20242c; + background: #fff; + font-size: 14px; + font-weight: 600; +} + +.account-management-confirm button.danger { + color: #fff; + border-color: #d64d34; + background: #d64d34; +} + +@media (max-width: 820px) { + .account-management-backdrop { + padding: 16px; + } + + .account-management-dialog { + height: min(calc(100vh - 32px), 720px); + } + + .account-management-layout { + flex-direction: column; + } + + .account-management-sidebar { + width: 100%; + flex-direction: row; + overflow-x: auto; + padding-right: 56px; + } + + .account-management-sidebar-title, + .account-management-nav { + flex: 0 0 auto; + } + + .account-management-content { + max-width: none; + padding: 24px; + } + + .account-management-profile-row, + .account-management-row, + .account-management-device-action { + align-items: flex-start; + flex-direction: column; + gap: 14px; + } + + .account-management-device-table { + grid-template-columns: 76px minmax(220px, 1fr) 106px; + overflow-x: auto; + } + + .account-management-name-form { + width: 100%; + justify-content: flex-start; + flex-wrap: wrap; + } + + .account-management-name-form input { + width: min(320px, 100%); + } +} diff --git a/frontend/src/ui/components/AccountManagementDialog/index.tsx b/frontend/src/ui/components/AccountManagementDialog/index.tsx new file mode 100644 index 0000000..e895e89 --- /dev/null +++ b/frontend/src/ui/components/AccountManagementDialog/index.tsx @@ -0,0 +1,417 @@ +"use client"; + +import { CircleUserRound, PencilLine, ReceiptText, X, Zap } from "lucide-react"; +import { useEffect, useMemo, useRef, useState, type ChangeEvent, type FormEvent, type ReactNode } from "react"; +import { createPortal } from "react-dom"; +import type { AccountDevice, AuthUser } from "@/domain/auth"; +import { useI18n } from "@/i18n/i18n"; +import { authGateway } from "@/infrastructure/authGateway"; + +type AccountManagementDialogProps = { + open: boolean; + user: AuthUser | null; + onClose: () => void; + onLogout: () => void | Promise; + onUserUpdate: (userPatch: Partial) => Promise; +}; + +export function AccountManagementDialog({ open, user, onClose, onLogout, onUserUpdate }: AccountManagementDialogProps) { + const { t } = useI18n(); + const avatarInputRef = useRef(null); + const scrollRevealTimerRef = useRef | null>(null); + const [confirmDeviceRemoval, setConfirmDeviceRemoval] = useState(false); + const [devices, setDevices] = useState([]); + const [editingName, setEditingName] = useState(false); + const [nameDraft, setNameDraft] = useState(""); + const [feedback, setFeedback] = useState<{ tone: "success" | "error"; message: string } | null>(null); + const [profileSaving, setProfileSaving] = useState(false); + const [scrollbarVisible, setScrollbarVisible] = useState(false); + const displayName = user?.name?.trim() || user?.email?.split("@")[0] || t("account"); + const displayEmail = user?.email?.trim() || ""; + const displayAvatar = cleanAvatarUrl(user?.avatarUrl); + const avatarLabel = avatarInitials(displayName || displayEmail); + const loginTimestamp = useMemo(() => formatUtcTimestamp(new Date()), []); + const accountDevices = devices.length > 0 ? devices : [defaultCurrentDevice(loginTimestamp)]; + const hasRemovableDevices = accountDevices.some((device) => !device.current); + + useEffect(() => { + if (!open) return; + + const closeOnEscape = (event: KeyboardEvent) => { + if (event.key === "Escape") onClose(); + }; + const previousOverflow = document.body.style.overflow; + const previousDocumentOverflow = document.documentElement.style.overflow; + + document.body.style.overflow = "hidden"; + document.documentElement.style.overflow = "hidden"; + document.addEventListener("keydown", closeOnEscape); + return () => { + document.body.style.overflow = previousOverflow; + document.documentElement.style.overflow = previousDocumentOverflow; + document.removeEventListener("keydown", closeOnEscape); + }; + }, [onClose, open]); + + useEffect(() => { + if (!open) return; + let active = true; + authGateway + .devices() + .then(({ devices: nextDevices }) => { + if (active) setDevices(nextDevices); + }) + .catch(() => { + if (active) setDevices([defaultCurrentDevice(loginTimestamp)]); + }); + return () => { + active = false; + }; + }, [loginTimestamp, open]); + + useEffect(() => { + if (open) return; + setConfirmDeviceRemoval(false); + setDevices([]); + setEditingName(false); + setFeedback(null); + setProfileSaving(false); + setScrollbarVisible(false); + if (scrollRevealTimerRef.current) { + clearTimeout(scrollRevealTimerRef.current); + scrollRevealTimerRef.current = null; + } + }, [open]); + + useEffect(() => { + return () => { + if (scrollRevealTimerRef.current) clearTimeout(scrollRevealTimerRef.current); + }; + }, []); + + useEffect(() => { + if (!editingName) setNameDraft(displayName); + }, [displayName, editingName]); + + if (!open || typeof document === "undefined") return null; + + const logoutAndClose = async () => { + await onLogout(); + onClose(); + }; + + const showFeedback = (tone: "success" | "error", message: string) => { + setFeedback({ tone, message }); + }; + + const changeAvatar = async (event: ChangeEvent) => { + const file = event.target.files?.[0]; + event.target.value = ""; + if (!file) return; + if (!file.type.startsWith("image/")) { + showFeedback("error", t("avatarImageRequired")); + return; + } + if (file.size > 2 * 1024 * 1024) { + showFeedback("error", t("avatarTooLarge")); + return; + } + + setProfileSaving(true); + try { + const avatarUrl = await authGateway.uploadAvatar(file); + await onUserUpdate({ avatarUrl }); + showFeedback("success", t("avatarUpdated")); + } catch { + showFeedback("error", t("avatarUpdateFailed")); + } finally { + setProfileSaving(false); + } + }; + + const saveName = async (event: FormEvent) => { + event.preventDefault(); + const nextName = nameDraft.trim(); + if (!nextName) { + showFeedback("error", t("usernameRequired")); + return; + } + setProfileSaving(true); + try { + await onUserUpdate({ name: nextName }); + setEditingName(false); + showFeedback("success", t("usernameUpdated")); + } catch { + showFeedback("error", t("usernameUpdateFailed")); + } finally { + setProfileSaving(false); + } + }; + + const removeAllDevices = async () => { + setConfirmDeviceRemoval(false); + const response = await authGateway.removeDevices(); + const { devices: nextDevices } = await authGateway.devices(); + setDevices(nextDevices); + showFeedback("success", t("devicesRemoved", { count: response.removed })); + }; + + const revealScrollbarWhileScrolling = () => { + setScrollbarVisible(true); + if (scrollRevealTimerRef.current) clearTimeout(scrollRevealTimerRef.current); + scrollRevealTimerRef.current = setTimeout(() => { + setScrollbarVisible(false); + scrollRevealTimerRef.current = null; + }, 700); + }; + + return createPortal( +
event.stopPropagation()} + onMouseDown={(event) => { + if (event.target === event.currentTarget) onClose(); + }} + > +
+ + +
+ + +
+
+ {feedback && ( +

+ {feedback.message} +

+ )} +
+
+
+

{t("accountInfo")}

+
+
+
+ {displayAvatar ? ( + + ) : ( + {avatarLabel} + )} +
+ {displayName} + {displayEmail && {displayEmail}} +
+
+ + +
+
+ {t("username")} + {editingName ? ( +
+ setNameDraft(event.target.value)} + aria-label={t("username")} + /> + + +
+ ) : ( + + )} +
+
+ {t("email")} + {displayEmail || "-"} +
+
+ +
+
+

{t("deviceManagement")}

+
+
+
+ {t("removeAllDevices")} +

{t("removeAllDevicesDescription")}

+
+ +
+ +
+ )} + /> + { + const info = deviceInfoText(device, t); + return ( + + {info} + + ); + })} + /> + + device.current ? ( + + ) : ( + + ) + )} + /> +
+
+ +
+
+

{t("system")}

+
+
+
+ {t("logout")} + {t("currentLoginAccount", { name: displayName })} +
+ +
+
+
+
+
+
+ {confirmDeviceRemoval && ( +
+
+

{t("removeAllDevicesConfirmTitle")}

+

{t("removeAllDevicesConfirmText")}

+
+ + +
+
+
+ )} +
+
, + document.body + ); +} + +function formatUtcTimestamp(date: Date) { + const year = date.getUTCFullYear(); + const month = String(date.getUTCMonth() + 1).padStart(2, "0"); + const day = String(date.getUTCDate()).padStart(2, "0"); + const hour = String(date.getUTCHours()).padStart(2, "0"); + const minute = String(date.getUTCMinutes()).padStart(2, "0"); + + return `${year}-${month}-${day} ${hour}:${minute}(UTC)`; +} + +function defaultCurrentDevice(lastSeenAt: string): AccountDevice { + return { + id: "current", + online: true, + current: true, + deviceType: "desktop", + system: "unknown", + browser: "unknown", + client: "PC-WEB", + lastSeenAt + }; +} + +function deviceInfoText(device: AccountDevice, t: (key: string) => string) { + const deviceType = device.deviceType === "desktop" ? t("accountDeviceDesktop") : device.deviceType || t("accountDeviceDesktop"); + const system = device.system === "unknown" ? t("accountDeviceUnknownSystem") : device.system || t("accountDeviceUnknownSystem"); + const browser = device.browser === "unknown" ? t("accountDeviceUnknownBrowser") : device.browser || t("accountDeviceUnknownBrowser"); + + return `${deviceType} | ${system} | ${browser} | ${device.client || "PC-WEB"} | | ${device.lastSeenAt}`; +} + +function avatarInitials(value: string) { + const cleaned = value.trim(); + if (!cleaned) return "M"; + const words = cleaned.split(/\s+/).filter(Boolean); + if (words.length >= 2) return `${words[0][0] ?? ""}${words[1][0] ?? ""}`.toUpperCase(); + return Array.from(cleaned)[0]?.toUpperCase() ?? "M"; +} + +function cleanAvatarUrl(value: string | undefined) { + const avatarUrl = value?.trim() || ""; + const lowerAvatarUrl = avatarUrl.toLowerCase(); + if (lowerAvatarUrl.includes("assets-persist.") && lowerAvatarUrl.includes("/img/") && lowerAvatarUrl.includes("-avatar.png")) return ""; + return avatarUrl; +} + +function DeviceColumn({ title, className, cells }: { title: string; className: string; cells: ReactNode[] }) { + return ( +
+
+ {title} +
+ {cells.map((cell, index) => ( +
+ {cell} +
+ ))} +
+ ); +} diff --git a/frontend/src/ui/components/ArtboardPreview/index.css b/frontend/src/ui/components/ArtboardPreview/index.css new file mode 100644 index 0000000..607a259 --- /dev/null +++ b/frontend/src/ui/components/ArtboardPreview/index.css @@ -0,0 +1,500 @@ +.artboard-image, +.artboard-preview { + width: 100%; + height: 100%; +} + +.artboard-image { + display: block; + object-fit: contain; + object-position: center center; + user-select: none; + -webkit-user-drag: none; +} + +.transparent-artboard-preview { + width: 100%; + height: 100%; + overflow: hidden; + background: transparent; +} + +.adjusted-artboard-image { + --adjust-warm-opacity: 0; + --adjust-cool-opacity: 0; + --adjust-magenta-opacity: 0; + --adjust-green-opacity: 0; + --adjust-highlight-lift: 0; + --adjust-highlight-recover: 0; + --adjust-shadow-lift: 0; + --adjust-shadow-crush: 0; + --adjust-vignette-opacity: 0; + --adjust-vignette-color: 0, 0, 0; + --adjust-grain-opacity: 0; + --adjust-bloom-opacity: 0; + --adjust-bloom-blur: 0; + position: relative; + width: 100%; + height: 100%; + overflow: hidden; +} + +.adjusted-artboard-image .artboard-image, +.adjustment-bloom-image, +.adjusted-artboard-image span { + position: absolute; + inset: 0; + width: 100%; + height: 100%; +} + +.adjustment-bloom-image { + object-fit: contain; + object-position: center center; + opacity: var(--adjust-bloom-opacity); + filter: blur(var(--adjust-bloom-blur)) brightness(1.18) saturate(1.08); + mix-blend-mode: screen; + pointer-events: none; +} + +.adjusted-artboard-image span { + pointer-events: none; +} + +.adjustment-light-overlay { + background: + linear-gradient(rgba(255, 255, 255, var(--adjust-highlight-lift)), rgba(255, 255, 255, var(--adjust-highlight-lift))), + linear-gradient(rgba(0, 0, 0, var(--adjust-highlight-recover)), rgba(0, 0, 0, var(--adjust-highlight-recover))), + linear-gradient(rgba(255, 255, 255, var(--adjust-shadow-lift)), rgba(255, 255, 255, var(--adjust-shadow-lift))), + linear-gradient(rgba(0, 0, 0, var(--adjust-shadow-crush)), rgba(0, 0, 0, var(--adjust-shadow-crush))); + mix-blend-mode: soft-light; +} + +.adjustment-temperature-overlay { + background: + linear-gradient(rgba(255, 190, 74, var(--adjust-warm-opacity)), rgba(255, 190, 74, var(--adjust-warm-opacity))), + linear-gradient(rgba(72, 150, 255, var(--adjust-cool-opacity)), rgba(72, 150, 255, var(--adjust-cool-opacity))), + linear-gradient(rgba(255, 78, 205, var(--adjust-magenta-opacity)), rgba(255, 78, 205, var(--adjust-magenta-opacity))), + linear-gradient(rgba(42, 190, 104, var(--adjust-green-opacity)), rgba(42, 190, 104, var(--adjust-green-opacity))); + mix-blend-mode: overlay; +} + +.adjustment-vignette-overlay { + background: radial-gradient(circle at center, transparent 50%, rgba(var(--adjust-vignette-color), var(--adjust-vignette-opacity)) 100%); + mix-blend-mode: multiply; +} + +.adjustment-grain-overlay { + opacity: var(--adjust-grain-opacity); + background-image: + radial-gradient(circle at 25% 30%, rgba(255, 255, 255, 0.75) 0 0.6px, transparent 0.8px), + radial-gradient(circle at 72% 64%, rgba(0, 0, 0, 0.68) 0 0.55px, transparent 0.75px); + background-size: 4px 4px, 5px 5px; + mix-blend-mode: overlay; +} + +.pen-stroke-node .artboard-image { + object-fit: fill; +} + +.manual-frame-preview { + width: 100%; + height: 100%; + box-sizing: border-box; +} + +.image-generator-preview { + position: relative; + width: 100%; + height: 100%; + overflow: visible; +} + +.image-generator-label { + position: absolute; + left: 0; + top: -28px; + display: inline-flex; + align-items: center; + gap: 6px; + color: #2f80ff; + font-size: 15px; + font-weight: 500; + line-height: 22px; + white-space: nowrap; +} + +.image-generator-label svg { + width: 15px; + height: 15px; + fill: #2f80ff; + stroke: #2f80ff; +} + +.image-generator-frame { + width: 100%; + height: 100%; + display: grid; + place-items: center; + border: 2px solid #2f80ff; + background: #eeeeed; +} + +.image-generator-frame svg { + color: #cfd0d2; + fill: #cfd0d2; + stroke: #cfd0d2; +} + +.generation-skeleton { + --canvas-work-ui-scale: 1; + --canvas-work-label-font: 18px; + --canvas-work-label-pad-x: 18px; + --canvas-work-label-pad-y: 8px; + --canvas-work-label-bottom: 18px; + position: relative; + width: 100%; + height: 100%; + display: grid; + align-content: end; + gap: 7%; + padding: 9%; + background: #f5f6f8; + container-type: inline-size; +} + +.generation-skeleton-image, +.generation-skeleton-lines span { + position: relative; + overflow: hidden; + background: #e6e9ee; +} + +.generation-skeleton-image::after, +.generation-skeleton-lines span::after { + content: ""; + position: absolute; + inset: 0; + transform: translateX(-100%); + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.72), transparent); + animation: skeleton-shimmer 1.35s infinite; +} + +.generation-skeleton-image { + min-height: 0; + aspect-ratio: 1; + border-radius: 10px; +} + +.generation-skeleton-lines { + display: grid; + gap: 10px; +} + +.generation-skeleton-lines span { + height: 14px; + border-radius: 999px; +} + +.generation-skeleton-lines span:nth-child(2) { + width: 72%; +} + +.generation-skeleton-lines span:nth-child(3) { + width: 48%; +} + +.generation-skeleton-label { + position: absolute; + left: 50%; + bottom: calc(var(--canvas-work-label-bottom) * var(--canvas-work-ui-scale)); + z-index: 2; + max-width: calc(100% - calc(56px * var(--canvas-work-ui-scale))); + min-width: calc(64px * var(--canvas-work-ui-scale)); + display: inline-flex; + align-items: center; + justify-content: center; + overflow: hidden; + padding: calc(var(--canvas-work-label-pad-y) * var(--canvas-work-ui-scale)) calc(var(--canvas-work-label-pad-x) * var(--canvas-work-ui-scale)); + border-radius: 999px; + color: rgba(255, 255, 255, 0.94); + background: rgba(87, 94, 94, 0.5); + backdrop-filter: blur(12px); + font-size: calc(var(--canvas-work-label-font) * var(--canvas-work-ui-scale)); + font-weight: 520; + line-height: 1.1; + letter-spacing: 0; + white-space: nowrap; + text-overflow: ellipsis; + transform: translateX(-50%); + will-change: transform, background-color; + animation: generation-label-float 2.4s ease-in-out infinite; +} + +.generation-skeleton-label::after { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient(110deg, transparent 0%, rgba(255, 255, 255, 0.2) 38%, rgba(255, 255, 255, 0.5) 50%, rgba(255, 255, 255, 0.16) 62%, transparent 100%); + transform: translateX(-115%) skewX(-16deg); + animation: generation-label-sheen 1.9s ease-in-out infinite; + pointer-events: none; +} + +@keyframes skeleton-shimmer { + 100% { + transform: translateX(100%); + } +} + +@keyframes generation-label-float { + 0%, + 100% { + background: rgba(87, 94, 94, 0.48); + transform: translateX(-50%) translateY(0) scale(1); + } + + 50% { + background: rgba(87, 94, 94, 0.56); + transform: translateX(-50%) translateY(calc(-1px * var(--canvas-work-ui-scale))) scale(1.012); + } +} + +@keyframes generation-label-sheen { + 0% { + transform: translateX(-115%) skewX(-16deg); + } + + 58%, + 100% { + transform: translateX(115%) skewX(-16deg); + } +} + +.artboard-preview { + overflow: hidden; + color: #171717; + background: #fff; + container-type: inline-size; +} + +.artboard-nav { + height: 9%; + min-height: 22px; + display: flex; + align-items: center; + gap: 4%; + padding: 0 4%; + color: #18191b; + background: #fff9f2; + font-size: clamp(7px, 1.6%, 11px); + letter-spacing: 0; +} + +.artboard-nav b { + margin-right: auto; +} + +.wide-hero { + position: relative; + height: 91%; + display: grid; + grid-template-columns: 0.9fr 1fr 0.8fr; + overflow: hidden; + background: + radial-gradient(circle at 76% 22%, #ffce26 0 28%, transparent 29%), + radial-gradient(circle at 63% -18%, #0067d8 0 30%, transparent 31%), + linear-gradient(135deg, #ff4d3f 0 43%, #ffe9e2 43% 62%, #ffc928 62% 100%); +} + +.hero-copyline { + z-index: 1; + align-self: center; + padding-left: 12%; +} + +.hero-copyline strong { + display: block; + color: #111; + font-size: clamp(28px, 14.5cqw, 78px); + line-height: 0.84; + letter-spacing: 0; +} + +.hero-copyline small { + display: block; + margin-top: 7%; + font-size: clamp(7px, 2cqw, 12px); + font-weight: 850; +} + +.hero-model { + position: absolute; + left: 48%; + bottom: -8%; + width: 26%; + height: 93%; + transform: translateX(-50%); + border-radius: 46% 46% 12% 12%; + background: + radial-gradient(circle at 50% 12%, #9a6a50 0 9%, transparent 10%), + linear-gradient(75deg, transparent 0 27%, #17285b 28% 70%, transparent 71%), + linear-gradient(95deg, transparent 0 38%, #0f214d 39% 62%, transparent 63%); + filter: drop-shadow(0 10px 12px rgba(15, 23, 42, 0.18)); +} + +.hero-note { + z-index: 1; + align-self: center; + max-width: 74%; + display: grid; + gap: 7px; + color: #1b1b1b; + font-size: clamp(8px, 2.2cqw, 13px); +} + +.hero-note span { + display: -webkit-box; + overflow: hidden; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + +.artboard-tall { + display: grid; + grid-template-rows: 28% 32% 27% 13%; +} + +.artboard-tall .artboard-wide { + min-height: 0; +} + +.mood-section { + display: grid; + grid-template-rows: 26% 1fr; + background: #fff; +} + +.mood-section header { + display: flex; + align-items: center; + gap: 8%; + padding: 0 6%; + font-size: clamp(8px, 2.4cqw, 14px); +} + +.mood-section header b { + margin-right: auto; + font-size: clamp(18px, 6cqw, 34px); + line-height: 1; +} + +.mood-section header span { + color: #868686; +} + +.color-cards { + display: grid; + grid-template-columns: repeat(4, 1fr); +} + +.color-cards div { + position: relative; + display: grid; + align-content: end; + justify-items: center; + overflow: hidden; + padding-bottom: 8%; +} + +.color-cards div:nth-child(1) { + background: #ff5545; +} + +.color-cards div:nth-child(2) { + background: #ffd72f; +} + +.color-cards div:nth-child(3) { + background: #77c47c; +} + +.color-cards div:nth-child(4) { + background: #2382dc; +} + +.color-cards i { + width: 48%; + height: 54%; + margin-bottom: 6%; + border-radius: 42% 42% 18% 18%; + background: + radial-gradient(circle at 50% 10%, rgba(255, 255, 255, 0.36) 0 11%, transparent 12%), + linear-gradient(145deg, rgba(255, 255, 255, 0.36), rgba(0, 0, 0, 0.12)); + box-shadow: 0 10px 12px rgba(17, 24, 39, 0.18); +} + +.color-cards strong { + color: #111; + font-size: clamp(10px, 3.6cqw, 24px); + line-height: 0.9; +} + +.product-section { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 4%; + padding: 6%; + background: #fbfaf7; +} + +.product-section span, +.neutral-grid i { + border-radius: 2px; + background: + radial-gradient(circle at 50% 22%, #f2eee6 0 25%, transparent 26%), + linear-gradient(#d7cec2, #f8f4eb); +} + +.artboard-tall footer, +.artboard-square footer { + display: grid; + place-items: center; + color: #fff; + background: #111; + font-size: clamp(10px, 3.1cqw, 18px); + font-weight: 850; +} + +.artboard-square { + display: grid; + grid-template-rows: 32% 53% 15%; + background: #f8f5ee; +} + +.neutral-hero { + display: grid; + align-content: center; + gap: 6px; + padding: 0 9%; + background: #eee8dc; +} + +.neutral-hero strong { + font-family: Georgia, "Times New Roman", serif; + font-size: clamp(20px, 8cqw, 44px); + line-height: 0.95; + font-weight: 500; +} + +.neutral-hero span { + color: #6e665d; + font-size: clamp(8px, 2.6cqw, 14px); +} + +.neutral-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 5%; + padding: 6%; +} diff --git a/frontend/src/ui/components/ArtboardPreview/index.tsx b/frontend/src/ui/components/ArtboardPreview/index.tsx new file mode 100644 index 0000000..a40cb1d --- /dev/null +++ b/frontend/src/ui/components/ArtboardPreview/index.tsx @@ -0,0 +1,231 @@ +"use client"; + +import { Circle, Image as ImageIcon } from "lucide-react"; +import type { CSSProperties } from "react"; +import type { CanvasNode } from "@/domain/design"; +import { useI18n } from "@/i18n/i18n"; +import { CanvasWorkOverlay } from "@/ui/components/CanvasWorkOverlay"; +import { LayerMergeSkeleton } from "@/ui/components/LayerMergeSkeleton"; +import { imageAdjustmentImageStyle, imageAdjustmentLayerStyle, isDefaultImageAdjustments, parseImageAdjustments } from "@/ui/lib/imageAdjustments"; +import { canvasImageUrl } from "@/ui/lib/imageDelivery"; +import { isImageUrl } from "@/ui/pages/CanvasWorkspace/canvas/canvasNodeOps"; +import { framePreviewStyle } from "@/ui/pages/CanvasWorkspace/canvas/frameStyle"; +import "./index.css"; + +export function ArtboardPreview({ node, viewportScale = 1 }: { node: CanvasNode; viewportScale?: number }) { + const { t } = useI18n(); + const overlayUiStyle = workOverlayUiStyle(viewportScale, node); + + if (node.status === "generating" && node.layerRole === "merge-layer") { + return ( +
+ {isImageUrl(node.content) && {node.title}} + +
+ ); + } + + if (node.status === "generating" && node.type === "image" && isImageUrl(node.content)) { + return ; + } + + if (node.status === "generating") { + return ; + } + + if (node.status === "text-extracting" && node.type === "image" && isImageUrl(node.content)) { + return ; + } + + if (node.status === "mockup-modeling" && node.type === "image" && isImageUrl(node.content)) { + return ; + } + + if (node.status === "uploading" && node.type === "image" && isImageUrl(node.content)) { + return ; + } + + if (node.type === "image" && isImageUrl(node.content)) { + const adjustments = parseImageAdjustments(node.imageAdjustments); + if (!isDefaultImageAdjustments(adjustments)) { + return ; + } + if (node.layerRole === "background-removed" || node.tone === "transparent-image") { + return ; + } + return {node.title}; + } + + if (node.type === "image" && node.layerRole === "image-generator") { + return ; + } + + if (node.type === "frame" && node.layerRole === "manual-frame") { + return
; + } + + const kind = artboardKind(node); + if (kind === "wide") return ; + if (kind === "tall") return ; + return ; +} + +function TransparentImagePreview({ node }: { node: CanvasNode }) { + return ( +
+ {node.title} +
+ ); +} + +function AdjustedImagePreview({ node }: { node: CanvasNode }) { + const adjustments = parseImageAdjustments(node.imageAdjustments); + const source = canvasImageUrl(node.content); + return ( +
+ {node.title} + + + + + +
+ ); +} + +function workOverlayUiStyle(viewportScale: number, node: CanvasNode) { + const scale = Math.max(viewportScale, 0.01); + const screenMin = Math.max(1, Math.min(node.width * scale, node.height * scale)); + const labelFont = clampNumber(screenMin * 0.038, 10, 15); + return { + "--canvas-work-ui-scale": `${1 / scale}`, + "--canvas-work-label-font": `${labelFont}px`, + "--canvas-work-label-pad-y": `${clampNumber(labelFont * 0.34, 4, 6)}px`, + "--canvas-work-label-pad-x": `${clampNumber(labelFont * 0.82, 9, 14)}px`, + "--canvas-work-label-bottom": `${clampNumber(screenMin * 0.055, 10, 18)}px` + } as CSSProperties; +} + +function clampNumber(value: number, min: number, max: number) { + return Math.min(Math.max(value, min), max); +} + +function artboardKind(node: CanvasNode) { + if (node.height > node.width * 1.24) return "tall"; + if (node.width > node.height * 1.35) return "wide"; + return "square"; +} + +function ImageGeneratorPreview({ node }: { node: CanvasNode }) { + return ( +
+
+ + {node.title} +
+
+ +
+
+ ); +} + +function WorkingImagePreview({ node, label, style }: { node: CanvasNode; label: string; style?: CSSProperties }) { + return ( + + {node.title} + + ); +} + +function GeneratingArtboardSkeleton({ title, style }: { title: string; style?: CSSProperties }) { + return ( +
+
+
+ + + +
+ {title} +
+ ); +} + +function WideCommerceBoard({ node }: { node: CanvasNode }) { + return ( +
+
+ still day + HOME + SHOP + COLLECTION + +
+
+
+ + Wear +
+ Your +
+ Mood. +
+ {node.title} +
+
+
+ Every color tells a story. + {node.content.slice(0, 52)} +
+
+
+ ); +} + +function TallCommerceBoard({ node }: { node: CanvasNode }) { + return ( +
+ +
+
+ MOOD × COLOR + drop 04 +
+
+ {["ENERGETIC", "HAPPY", "CALM", "BOLD"].map((label) => ( +
+ + {label} +
+ ))} +
+
+
+ {Array.from({ length: 8 }).map((_, index) => ( + + ))} +
+
+ Born from every mood. +
+
+ ); +} + +function SquareCommerceBoard({ node }: { node: CanvasNode }) { + return ( +
+
+ Wear Your Mood. + {node.title} +
+
+ {Array.from({ length: 8 }).map((_, index) => ( + + ))} +
+
Born from every mood.
+
+ ); +} diff --git a/frontend/src/ui/components/BrandMark.tsx b/frontend/src/ui/components/BrandMark.tsx new file mode 100644 index 0000000..a309767 --- /dev/null +++ b/frontend/src/ui/components/BrandMark.tsx @@ -0,0 +1,16 @@ +const leftPath = + "M 42.63 409.42 A 0.78 0.77 47.7 0 0 43.65 409.47 Q 44.14 409.09 44.45 408.55 Q 70.73 362.99 80.23 346.56 C 80.95 345.32 81.71 344.08 83.21 344.04 Q 84.84 344.00 85.49 345.54 Q 91.39 359.81 91.65 360.74 Q 92.76 364.68 92.27 368.28 Q 91.95 370.67 89.28 375.18 Q 55.65 432.31 47.17 446.75 C 44.63 451.08 40.18 454.53 35.21 453.22 C 23.65 450.21 22.78 433.75 22.78 424.14 Q 22.72 274.61 22.77 215.37 C 22.78 205.72 21.87 197.86 26.17 191.34 C 29.34 186.53 35.51 186.89 38.31 191.92 Q 73.85 256.06 90.76 286.51 C 93.21 290.92 92.29 296.56 89.85 301.88 Q 88.22 305.46 85.62 312.37 Q 84.94 314.17 83.50 314.55 A 2.56 2.55 66.0 0 1 80.72 313.47 Q 79.33 311.35 77.57 308.18 Q 56.93 270.88 43.95 247.58 A 1.01 1.01 0.0 0 0 42.31 247.40 C 40.61 249.29 41.00 255.03 41.01 257.69 Q 41.15 380.51 41.01 405.19 Q 40.99 407.86 42.63 409.42 Z"; +const rightPath = + "M 171.40 409.52 Q 173.04 407.96 173.02 405.28 Q 172.88 380.57 173.02 257.61 C 173.03 254.95 173.42 249.20 171.72 247.31 A 1.01 1.01 0.0 0 0 170.08 247.49 Q 157.08 270.82 136.42 308.16 Q 134.66 311.34 133.26 313.46 A 2.57 2.55 -66.0 0 1 130.48 314.54 Q 129.04 314.16 128.36 312.36 Q 125.76 305.44 124.12 301.86 C 121.68 296.53 120.76 290.88 123.21 286.47 Q 140.14 255.98 175.72 191.77 C 178.52 186.73 184.70 186.37 187.87 191.19 C 192.18 197.72 191.27 205.58 191.28 215.25 Q 191.33 274.55 191.27 424.25 C 191.27 433.88 190.40 450.35 178.83 453.37 C 173.85 454.68 169.40 451.23 166.85 446.89 Q 158.36 432.43 124.69 375.24 Q 122.02 370.72 121.70 368.33 Q 121.21 364.73 122.32 360.78 Q 122.58 359.85 128.49 345.57 Q 129.14 344.02 130.77 344.06 C 132.27 344.10 133.03 345.35 133.75 346.59 Q 143.27 363.04 169.58 408.65 Q 169.89 409.19 170.38 409.57 A 0.78 0.78 0.0 0 0 171.40 409.52 Z"; + +export function BrandMark({ className }: { className?: string }) { + return ( + + ); +} diff --git a/frontend/src/ui/components/CanvasAgentComposer/index.css b/frontend/src/ui/components/CanvasAgentComposer/index.css new file mode 100644 index 0000000..e649841 --- /dev/null +++ b/frontend/src/ui/components/CanvasAgentComposer/index.css @@ -0,0 +1,568 @@ +.assistant-composer-stack { + position: relative; + width: 100%; + flex: 0 0 auto; + padding: 0 8px; +} + +.assistant-composer-stack.has-upgrade { + padding-top: 48px; +} + +.assistant-composer { + position: relative; + z-index: 2; + width: 100%; + min-height: 150px; + flex: 0 0 auto; + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 8px; + padding: 8px; + border: 1px solid #e1e3e8; + border-radius: 24px; + background: #fff; + box-shadow: 0 8px 24px rgba(17, 24, 39, 0.1); +} + +.assistant-composer.floating-composer { + margin: 0; + border-color: #cbd2dc; + border-radius: 28px; + background: rgba(255, 255, 255, 0.98); + box-shadow: 0 18px 45px rgba(17, 24, 39, 0.14); +} + +.assistant-composer .prompt-rich-editor { + width: 100%; + min-height: 48px; + max-height: 240px; + padding: 0 4px; + overflow-x: hidden; + overflow-y: auto; + border: 0; + outline: 0; + resize: none; + color: #141414; + font-size: 14px; + font-weight: 500; + line-height: 1.8; + white-space: pre-wrap; + word-break: break-word; + scrollbar-width: none; +} + +.assistant-composer .prompt-rich-editor::-webkit-scrollbar { + display: none; +} + +.assistant-composer .reference-chip { + gap: 4px; + max-width: 116px; + height: 24px; + padding: 0 4px; + border: 0.5px solid rgba(0, 0, 0, 0.15); + border-radius: 8px; + color: #363636; + background: #fff; + font-size: 13px; + font-weight: 400; +} + +.assistant-composer .prompt-reference-chip { + margin: 1px 4px 5px 0; +} + +.assistant-composer .reference-chip:hover { + background: rgba(12, 12, 13, 0.04); +} + +.assistant-composer .reference-chip-thumb { + width: 16px; + height: 16px; + border-radius: 4px; +} + +.assistant-composer .reference-chip > span { + max-width: 86px; +} + +.assistant-composer.image-mode .prompt-rich-editor { + min-height: 52px; + padding-top: 0; +} + +.assistant-composer.floating-composer.image-mode .prompt-rich-editor { + min-height: 86px; + padding: 14px 32px 2px; + font-size: 18px; + line-height: 1.42; +} + +.assistant-composer .prompt-rich-editor[data-empty="true"]::before { + top: 0; + left: 4px; + color: #9ca0a8; +} + +.assistant-composer.image-mode .prompt-rich-editor[data-empty="true"]::before { + top: 0; +} + +.assistant-composer.floating-composer.image-mode .prompt-rich-editor[data-empty="true"]::before { + top: 14px; + left: 32px; +} + +.assistant-composer-toolbar { + min-width: 0; + display: flex; + align-items: center; + gap: 2px; + padding: 0; + overflow: hidden; +} + +.assistant-composer.floating-composer .assistant-composer-toolbar { + padding: 8px 14px 13px 28px; +} + +.composer-upgrade { + position: absolute; + left: 8px; + right: 8px; + top: 0; + z-index: 1; + min-height: 88px; + display: flex; + align-items: flex-start; + padding: 0 16px 40px; + border: 0; + border-radius: 24px 24px 0 0; + color: #363636; + background: #f7f7f4; + font-size: 13px; + box-shadow: 0 8px 24px rgba(17, 24, 39, 0.08); +} + +.composer-upgrade-row { + min-width: 0; + width: 100%; + min-height: 36px; + display: flex; + align-items: center; + gap: 8px; +} + +.composer-upgrade-icon { + width: 18px; + height: 18px; + display: grid; + place-items: center; + flex: 0 0 auto; + border-radius: 999px; + color: #151515; + background: #eef94f; +} + +.composer-upgrade-copy { + min-width: 0; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.composer-upgrade button { + width: 18px; + height: 18px; + display: grid; + place-items: center; + flex: 0 0 auto; + border-radius: 999px; + color: #363636; + background: transparent; + transition: background 0.15s ease, transform 0.15s ease; +} + +.composer-upgrade button:hover { + background: rgba(12, 12, 13, 0.06); + transform: scale(1.08); +} + +.composer-reference-tile { + width: 72px; + height: 72px; + display: grid; + place-items: center; + gap: 6px; + margin: 12px 0 0 16px; + border-radius: 20px; + color: #a6abb5; + background: #f4f4f4; + font-size: 12px; +} + +.assistant-composer.floating-composer .composer-reference-tile { + width: 72px; + height: 72px; + margin: 14px 0 0 116px; + border-radius: 20px; +} + +.composer-reference-tile span { + display: block; +} + +.composer-mode-button, +.composer-settings-chip { + height: 32px; + display: inline-flex; + align-items: center; + gap: 5px; + flex: 0 0 auto; + border-radius: 999px; + color: #2f3746; + background: #fff; + font-size: 14px; + font-weight: 500; +} + +.composer-mode-button { + padding: 0 8px; +} + +.composer-mode-button.open, +.composer-mode-button:hover, +.composer-settings-chip:hover, +.composer-icon-button:hover { + background: #f3f4f6; +} + +.composer-settings-chip { + max-width: 112px; + padding: 0 8px; + overflow: hidden; + white-space: nowrap; +} + +.composer-icon-button, +.composer-model-button { + width: 32px; + height: 32px; + display: grid; + place-items: center; + flex: 0 0 auto; + border-radius: 999px; + color: #4c5563; + background: #fff; +} + +.composer-icon-button.plus-button { + color: #363636; +} + +.composer-model-button { + color: #2f3746; + background: #fff; +} + +.composer-model-button:hover { + background: #e7e7e7; +} + +.composer-spacer { + flex: 1; + min-width: 6px; +} + +.assistant-composer .send-button.small { + width: 32px; + height: 32px; + min-width: 32px; + border-radius: 999px; + box-shadow: none; +} + +.composer-menu-popover { + width: 250px; + padding: 8px; + border-radius: 16px; + background: #fff; +} + +.composer-menu-popover .ui-dropdown-menu-item, +.composer-model-popover .ui-dropdown-menu-item { + width: 100%; + min-height: 38px; + display: flex; + align-items: center; + gap: 10px; + padding: 9px 12px; + border-radius: 12px; + color: #252a33; + background: transparent; + font-size: 15px; + font-weight: 560; + text-align: left; + cursor: pointer; +} + +.composer-menu-popover .ui-dropdown-menu-item:hover, +.composer-menu-popover .ui-dropdown-menu-item[data-highlighted], +.composer-model-popover .ui-dropdown-menu-item:hover, +.composer-model-popover .ui-dropdown-menu-item[data-highlighted] { + background: #f4f5f7; +} + +.composer-menu-popover .ui-dropdown-menu-item[data-disabled], +.composer-model-popover .ui-dropdown-menu-item[data-disabled] { + color: #a4a8b0; + cursor: not-allowed; +} + +.composer-menu-popover .ui-dropdown-menu-item[data-disabled] svg { + color: #b5bac3; +} + +.composer-menu-popover .ui-dropdown-menu-item > span, +.composer-model-popover .ui-dropdown-menu-item > span { + min-width: 0; + flex: 1; +} + +.composer-switch-row > .ui-switch { + margin-left: auto; + flex: 0 0 46px; +} + +.composer-switch-row { + min-height: 52px; + background: #f5f6f8; +} + +.composer-switch-row[data-highlighted] { + background: #f1f3f6; +} + +.composer-web-switch.ui-switch { + position: relative; + width: 46px; + height: 28px; + display: block; + flex: 0 0 46px; + border-radius: 999px; + background: #dfe3e9; + transition: background 0.18s ease, box-shadow 0.18s ease; +} + +.composer-web-switch.ui-switch[data-state="checked"] { + background: #303948; +} + +.composer-web-switch .ui-switch-thumb { + position: absolute; + top: 3px; + left: 3px; + width: 22px; + height: 22px; + margin: 0; + transform: translateX(0); + border-radius: 999px; + background: #fff; + box-shadow: 0 1px 3px rgba(17, 24, 39, 0.2); +} + +.composer-web-switch.ui-switch[data-state="checked"] .ui-switch-thumb { + transform: translateX(18px); +} + +.composer-mode-popover .ui-dropdown-menu-item svg:last-child, +.composer-model-popover .ui-dropdown-menu-item svg:last-child { + margin-left: auto; +} + +.composer-model-popover { + width: 306px; + padding: 9px; + border-radius: 16px; + background: #fff; +} + +.composer-model-popover .ui-dropdown-menu-item { + min-height: 42px; + font-size: 15px; +} + +.composer-model-popover small { + color: #8b9098; + font-size: 12px; +} + +.composer-settings-popover { + width: min(386px, calc(100vw - 24px)); + max-height: min(74vh, 720px); + overflow: auto; + padding: 18px; + border-radius: 16px; +} + +.composer-settings-panel { + display: grid; + gap: 20px; +} + +.composer-settings-panel section { + display: grid; + gap: 12px; +} + +.composer-settings-panel h4 { + margin: 0; + color: #242a34; + font-size: 14px; + font-weight: 600; +} + +.composer-quality-control { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + overflow: hidden; + border-radius: 999px; + background: #f4f4f4; +} + +.composer-quality-control button { + height: 40px; + border-radius: 999px; + color: #3a414d; + background: transparent; + font-size: 14px; +} + +.composer-quality-control button.selected { + border: 1px solid #c9ced7; + background: #fff; +} + +.composer-dimensions { + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: center; + gap: 12px; +} + +.composer-dimensions label { + min-width: 0; + height: 44px; + display: flex; + align-items: center; + gap: 8px; + padding: 0 14px; + border-radius: 11px; + background: #f5f5f5; +} + +.composer-dimensions span { + color: #767b84; + font-size: 13px; +} + +.composer-dimensions input { + min-width: 0; + width: 100%; + border: 0; + outline: 0; + background: transparent; + color: #242a34; + font: inherit; + font-size: 16px; +} + +.composer-dimensions i { + color: #767b84; + font-style: normal; +} + +.composer-ratio-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; +} + +.composer-ratio-grid button { + aspect-ratio: 1; + display: grid; + place-items: center; + gap: 7px; + border: 1px solid #d8dbe1; + border-radius: 10px; + color: #545b66; + background: #fff; + font-size: 14px; +} + +.composer-ratio-grid button.selected, +.composer-count-grid button.selected { + border-color: #c5c8cf; + background: #eeeeee; + color: #1f242d; +} + +.ratio-glyph { + width: 28px; + height: 28px; + display: block; + border: 2px solid #5a5f68; + border-radius: 4px; +} + +.ratio-glyph.wide { + width: 34px; + height: 22px; +} + +.ratio-glyph.portrait { + width: 22px; + height: 34px; +} + +.composer-count-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; +} + +.composer-count-grid button { + height: 40px; + border: 1px solid #d8dbe1; + border-radius: 10px; + color: #535a65; + background: #fff; + font-size: 14px; +} + +.send-button.small.with-cost { + width: 56px; + border-radius: 999px; + color: #ffffff; + background: #111111; + font-size: 13px; + font-weight: 800; +} + +.assistant-composer .send-button.small:disabled { + color: #a5a9b0; + background: #efefef; + opacity: 1; +} + +.send-button.small.is-stopping { + color: #ffffff; + background: #111111; +} + +.send-button.small.is-stopping:hover { + background: #b42318; +} diff --git a/frontend/src/ui/components/CanvasAgentComposer/index.tsx b/frontend/src/ui/components/CanvasAgentComposer/index.tsx new file mode 100644 index 0000000..ce36c15 --- /dev/null +++ b/frontend/src/ui/components/CanvasAgentComposer/index.tsx @@ -0,0 +1,414 @@ +"use client"; + +import { + ArrowUp, + Bot, + Check, + ChevronDown, + ChevronUp, + Globe2, + Image as ImageIcon, + ImagePlus, + Library, + Paperclip, + Plus, + Sparkles, + Square, + Video, + X +} from "lucide-react"; +import { forwardRef, type ReactNode, useMemo, useRef, useState } from "react"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Switch } from "@/components/ui/switch"; +import { useI18n } from "@/i18n/i18n"; +import { PromptComposer, type PromptComposerHandle, type UploadedReferenceImage, type CanvasAnnotation, type AnnotationPreviewRequest } from "@/ui/components/PromptComposer"; + +export type ComposerMode = "agent" | "image"; +export type ComposerImageQuality = "auto" | "high" | "medium" | "low"; +export type ComposerImageRatio = "1:1" | "3:2" | "2:3" | "4:3" | "3:4" | "9:16" | "1:1-2k" | "16:9-2k" | "9:16-2k" | "16:9-4k" | "9:16-4k" | "auto"; + +export type ComposerImageSettings = { + quality: ComposerImageQuality; + ratio: ComposerImageRatio; + width: number; + height: number; + count: number; +}; + +export type CanvasComposerModel = { + id: string; + label: string; + description?: string; + duration?: string; + credits: number; +}; + +export type CanvasAgentComposerVariant = "panel" | "floating"; + +export type CanvasAgentComposerProps = { + text: string; + mode: ComposerMode; + models: CanvasComposerModel[]; + selectedModel: CanvasComposerModel; + settings: ComposerImageSettings; + webSearchEnabled: boolean; + generating: boolean; + variant?: CanvasAgentComposerVariant; + references?: UploadedReferenceImage[]; + annotations?: CanvasAnnotation[]; + previewSlot?: ReactNode; + onTextChange: (text: string) => void; + onModeChange: (mode: ComposerMode) => void; + onSettingsChange: (settings: ComposerImageSettings) => void; + onModelChange: (modelId: string) => void; + onWebSearchChange: (enabled: boolean) => void; + onUploadFile: (file: File | null) => void | Promise; + onReferencesChange: (references: UploadedReferenceImage[]) => void; + onReferenceRemoved: (reference: UploadedReferenceImage) => void; + onAnnotationsChange?: (annotations: CanvasAnnotation[]) => void; + onAnnotationPreviewChange?: (preview: AnnotationPreviewRequest | null) => void; + onSubmit: () => void; + onStop?: () => void; +}; + +const qualityOptions: Array<{ id: ComposerImageQuality; labelKey: string }> = [ + { id: "auto", labelKey: "qualityAuto" }, + { id: "high", labelKey: "qualityHigh" }, + { id: "medium", labelKey: "qualityMedium" }, + { id: "low", labelKey: "qualityLow" } +]; + +export const composerRatioOptions: Array<{ id: ComposerImageRatio; label: string; width: number; height: number }> = [ + { id: "1:1", label: "1:1", width: 1024, height: 1024 }, + { id: "3:2", label: "3:2", width: 1536, height: 1024 }, + { id: "2:3", label: "2:3", width: 1024, height: 1536 }, + { id: "4:3", label: "4:3", width: 1365, height: 1024 }, + { id: "3:4", label: "3:4", width: 1024, height: 1365 }, + { id: "9:16", label: "9:16", width: 1024, height: 1820 }, + { id: "1:1-2k", label: "1:1(2k)", width: 2048, height: 2048 }, + { id: "16:9-2k", label: "16:9(2k)", width: 2048, height: 1152 }, + { id: "9:16-2k", label: "9:16(2k)", width: 1152, height: 2048 }, + { id: "16:9-4k", label: "16:9(4k)", width: 4096, height: 2304 }, + { id: "9:16-4k", label: "9:16(4k)", width: 2304, height: 4096 }, + { id: "auto", label: "auto", width: 1024, height: 1024 } +]; + +export const defaultComposerImageSettings: ComposerImageSettings = { + quality: "auto", + ratio: "1:1", + width: 1024, + height: 1024, + count: 1 +}; + +export function composerImageSize(settings: ComposerImageSettings) { + return `${Math.max(1, Math.round(settings.width))}x${Math.max(1, Math.round(settings.height))}`; +} + +export function composerSettingsSummary(settings: ComposerImageSettings) { + const ratioLabel = composerRatioOptions.find((option) => option.id === settings.ratio)?.label ?? settings.ratio; + return `${qualityLabel(settings.quality)} · ${ratioLabel} · ${settings.count} img`; +} + +function qualityLabel(quality: ComposerImageQuality) { + if (quality === "high") return "高"; + if (quality === "medium") return "中"; + if (quality === "low") return "低"; + return "自动"; +} + +function hasComposerPromptText(value: string) { + return value.replace(/\u00a0/g, " ").trim().length > 0; +} + +export const CanvasAgentComposer = forwardRef(function CanvasAgentComposer( + { + text, + mode, + models, + selectedModel, + settings, + webSearchEnabled, + generating, + variant = "panel", + references, + annotations, + previewSlot, + onTextChange, + onModeChange, + onSettingsChange, + onModelChange, + onWebSearchChange, + onUploadFile, + onReferencesChange, + onReferenceRemoved, + onAnnotationsChange, + onAnnotationPreviewChange, + onSubmit, + onStop + }, + ref +) { + const { t } = useI18n(); + const fileInputRef = useRef(null); + const [assetMenuOpen, setAssetMenuOpen] = useState(false); + const [modeMenuOpen, setModeMenuOpen] = useState(false); + const [settingsOpen, setSettingsOpen] = useState(false); + const [modelOpen, setModelOpen] = useState(false); + const [upgradeVisible, setUpgradeVisible] = useState(true); + const sendCost = useMemo(() => Math.max(1, selectedModel.credits * Math.max(1, settings.count)), [selectedModel.credits, settings.count]); + const modeLabel = mode === "image" ? t("imageMode") : t("agentShort"); + const ModeIcon = mode === "image" ? ImagePlus : Bot; + const canSubmit = hasComposerPromptText(text); + + const patchSettings = (patch: Partial) => { + onSettingsChange({ ...settings, ...patch }); + }; + + const selectRatio = (ratio: ComposerImageRatio) => { + const nextRatio = composerRatioOptions.find((option) => option.id === ratio) ?? composerRatioOptions[0]; + patchSettings({ ratio, width: nextRatio.width, height: nextRatio.height }); + }; + + const composerCard = ( +
+ {mode === "image"} + + {previewSlot} +
+ { + const file = event.target.files?.[0] ?? null; + event.currentTarget.value = ""; + void onUploadFile(file); + setAssetMenuOpen(false); + }} + /> + + + + + + fileInputRef.current?.click()}> + + {t("uploadFile")} + + + + {t("chooseFromLibrary")} + + { + event.preventDefault(); + onWebSearchChange(!webSearchEnabled); + }} + > + + {t("webSearch")} + event.stopPropagation()} onCheckedChange={onWebSearchChange} /> + + + + + + + + + + { + onModeChange("agent"); + setModeMenuOpen(false); + }} + > + + {t("agentShort")} + {mode === "agent" && } + + { + onModeChange("image"); + setModeMenuOpen(false); + }} + > + + {t("imageMode")} + {mode === "image" && } + + + + + + + {mode === "image" && ( + + + + + + + + + )} + +
+ + + + + + {models.map((model) => ( + { + onModelChange(model.id); + setModelOpen(false); + }} + > + + {model.label} + ⚡{model.credits} + {model.id === selectedModel.id && } + + ))} + + + +
+
+ ); + + if (variant === "floating") return composerCard; + + return ( +
+ {upgradeVisible && ( +
+
+ + {t("unlockModels")} + +
+
+ )} + {composerCard} +
+ ); +}); + +export function ComposerSettingsPanel({ + settings, + onPatch, + onSelectRatio +}: { + settings: ComposerImageSettings; + onPatch: (patch: Partial) => void; + onSelectRatio: (ratio: ComposerImageRatio) => void; +}) { + const { t } = useI18n(); + + return ( +
+
+

{t("quality")}

+
+ {qualityOptions.map((option) => ( + + ))} +
+
+
+

{t("imageSizeLabel")}

+
+ + + +
+
+ {composerRatioOptions.map((option) => ( + + ))} +
+
+
+

Image

+
+ {Array.from({ length: 10 }, (_, index) => index + 1).map((count) => ( + + ))} +
+
+
+ ); +} + +function RatioGlyph({ ratio }: { ratio: ComposerImageRatio }) { + const portrait = ratio.startsWith("2:3") || ratio.startsWith("3:4") || ratio.startsWith("9:16"); + const wide = ratio.startsWith("3:2") || ratio.startsWith("4:3") || ratio.startsWith("16:9"); + return