Browse Source

feat: initial commit for celestia-trace frontend

bob.yuxinyang 1 tháng trước cách đây
commit
7f887d42ed

+ 4 - 0
.gitignore

@@ -0,0 +1,4 @@
+node_modules
+dist
+.DS_Store
+*.log

+ 20 - 0
Dockerfile

@@ -0,0 +1,20 @@
+# Stage 1: Build Vite React application
+FROM docker.m.daocloud.io/library/node:20-alpine AS builder
+
+WORKDIR /app
+
+COPY package*.json ./
+RUN npm install
+
+COPY . .
+RUN npm run build
+
+# Stage 2: Serve static files via Nginx
+FROM docker.m.daocloud.io/library/nginx:alpine
+
+COPY --from=builder /app/dist /usr/share/nginx/html
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+
+EXPOSE 80
+
+CMD ["nginx", "-g", "daemon off;"]

+ 40 - 0
deploy.sh

@@ -0,0 +1,40 @@
+#!/bin/bash
+# CelestiaTrace Frontend Independent Deployment Script
+# Usage: ./deploy.sh (inside ./frontend)
+
+set -e
+
+SERVER_IP="47.93.193.127"
+PEM_KEY="../ccdw-meishi-1.pem"
+REMOTE_DIR="/opt/celestia-trace-frontend"
+SSH_USER="root"
+
+echo "🚀 Deploying CelestiaTrace Frontend independently..."
+
+# Ensure PEM key permissions
+chmod 400 "$PEM_KEY" 2>/dev/null || true
+
+# Create remote directory
+ssh -i "$PEM_KEY" -o StrictHostKeyChecking=no "$SSH_USER@$SERVER_IP" "mkdir -p $REMOTE_DIR"
+
+# Sync frontend files to server
+rsync -avz --exclude '.git' --exclude 'node_modules' --exclude 'dist' \
+  -e "ssh -i $PEM_KEY -o StrictHostKeyChecking=no" \
+  ./ "$SSH_USER@$SERVER_IP:$REMOTE_DIR/"
+
+# Deploy container on server
+ssh -i "$PEM_KEY" -o StrictHostKeyChecking=no "$SSH_USER@$SERVER_IP" << 'REMOTE_CMDS'
+set -e
+cd /opt/celestia-trace-frontend
+
+echo "📦 Building & starting CelestiaTrace Frontend container..."
+docker rm -f celestia-frontend 2>/dev/null || true
+docker compose down || true
+docker compose up -d --build
+
+echo "✅ Frontend deployment complete!"
+docker compose ps
+REMOTE_CMDS
+
+echo ""
+echo "🎉 Frontend is live at http://celestiatrace.lessncosmos.com"

+ 7 - 0
docker-compose.yml

@@ -0,0 +1,7 @@
+services:
+  frontend:
+    build: .
+    container_name: celestia-frontend
+    restart: unless-stopped
+    ports:
+      - "8081:80"

+ 18 - 0
index.html

@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="zh-CN" class="dark">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/spark-logo.svg" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>星痕 CelestiaTrace - 微光 Spark 智能数据中心</title>
+    <meta name="description" content="星痕 CelestiaTrace 网页端控制台,管理“微光 Spark”绑定的现场记录、存储同步与多端互通管理中心。" />
+    <!-- Google Fonts -->
+    <link rel="preconnect" href="https://fonts.googleapis.com">
+    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
+  </head>
+  <body class="bg-[#0B0E14] text-slate-100 font-sans antialiased min-h-screen selection:bg-spark-cyan/30 selection:text-spark-cyan">
+    <div id="root"></div>
+    <script type="module" src="/src/main.jsx"></script>
+  </body>
+</html>

+ 16 - 0
nginx.conf

@@ -0,0 +1,16 @@
+server {
+    listen 80;
+    server_name localhost;
+
+    root /usr/share/nginx/html;
+    index index.html;
+
+    location / {
+        try_files $uri $uri/ /index.html;
+    }
+
+    error_page 500 502 503 504 /5x.x.html;
+    location = /5x.x.html {
+        root /usr/share/nginx/html;
+    }
+}

+ 2988 - 0
package-lock.json

@@ -0,0 +1,2988 @@
+{
+  "name": "celestia-trace-frontend",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "celestia-trace-frontend",
+      "version": "1.0.0",
+      "dependencies": {
+        "axios": "^1.6.8",
+        "lucide-react": "^0.368.0",
+        "react": "^18.2.0",
+        "react-dom": "^18.2.0"
+      },
+      "devDependencies": {
+        "@types/react": "^18.2.66",
+        "@types/react-dom": "^18.2.22",
+        "@vitejs/plugin-react": "^4.2.1",
+        "autoprefixer": "^10.4.19",
+        "postcss": "^8.4.38",
+        "tailwindcss": "^3.4.3",
+        "vite": "^5.2.0"
+      }
+    },
+    "node_modules/@alloc/quick-lru": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+      "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@babel/code-frame": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.7.tgz",
+      "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-validator-identifier": "^7.29.7",
+        "js-tokens": "^4.0.0",
+        "picocolors": "^1.1.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/compat-data": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.7.tgz",
+      "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/core": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.29.7.tgz",
+      "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.7",
+        "@babel/generator": "^7.29.7",
+        "@babel/helper-compilation-targets": "^7.29.7",
+        "@babel/helper-module-transforms": "^7.29.7",
+        "@babel/helpers": "^7.29.7",
+        "@babel/parser": "^7.29.7",
+        "@babel/template": "^7.29.7",
+        "@babel/traverse": "^7.29.7",
+        "@babel/types": "^7.29.7",
+        "@jridgewell/remapping": "^2.3.5",
+        "convert-source-map": "^2.0.0",
+        "debug": "^4.1.0",
+        "gensync": "^1.0.0-beta.2",
+        "json5": "^2.2.3",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/babel"
+      }
+    },
+    "node_modules/@babel/generator": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.7.tgz",
+      "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.7",
+        "@babel/types": "^7.29.7",
+        "@jridgewell/gen-mapping": "^0.3.12",
+        "@jridgewell/trace-mapping": "^0.3.28",
+        "jsesc": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-compilation-targets": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+      "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/compat-data": "^7.29.7",
+        "@babel/helper-validator-option": "^7.29.7",
+        "browserslist": "^4.24.0",
+        "lru-cache": "^5.1.1",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-globals": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+      "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-imports": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+      "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/traverse": "^7.29.7",
+        "@babel/types": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-transforms": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+      "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-module-imports": "^7.29.7",
+        "@babel/helper-validator-identifier": "^7.29.7",
+        "@babel/traverse": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-plugin-utils": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
+      "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+      "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+      "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-option": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+      "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helpers": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.7.tgz",
+      "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/template": "^7.29.7",
+        "@babel/types": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz",
+      "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.29.7"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-react-jsx-self": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
+      "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-react-jsx-source": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
+      "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/template": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.29.7.tgz",
+      "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.7",
+        "@babel/parser": "^7.29.7",
+        "@babel/types": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/traverse": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.7.tgz",
+      "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.7",
+        "@babel/generator": "^7.29.7",
+        "@babel/helper-globals": "^7.29.7",
+        "@babel/parser": "^7.29.7",
+        "@babel/template": "^7.29.7",
+        "@babel/types": "^7.29.7",
+        "debug": "^4.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz",
+      "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.29.7",
+        "@babel/helper-validator-identifier": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+      "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+      "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+      "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+      "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+      "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+      "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+      "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+      "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+      "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+      "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+      "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+      "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+      "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+      "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+      "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+      "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+      "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+      "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+      "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.13",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.0",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/remapping": {
+      "version": "2.3.5",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+      "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.31",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@rolldown/pluginutils": {
+      "version": "1.0.0-beta.27",
+      "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+      "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
+      "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz",
+      "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz",
+      "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz",
+      "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz",
+      "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz",
+      "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz",
+      "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz",
+      "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz",
+      "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz",
+      "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-gnu": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz",
+      "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-musl": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz",
+      "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz",
+      "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-musl": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz",
+      "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz",
+      "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-musl": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz",
+      "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz",
+      "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz",
+      "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz",
+      "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-openbsd-x64": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz",
+      "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-openharmony-arm64": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz",
+      "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz",
+      "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz",
+      "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-gnu": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz",
+      "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz",
+      "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@types/babel__core": {
+      "version": "7.20.5",
+      "resolved": "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz",
+      "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.20.7",
+        "@babel/types": "^7.20.7",
+        "@types/babel__generator": "*",
+        "@types/babel__template": "*",
+        "@types/babel__traverse": "*"
+      }
+    },
+    "node_modules/@types/babel__generator": {
+      "version": "7.27.0",
+      "resolved": "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+      "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "node_modules/@types/babel__template": {
+      "version": "7.4.4",
+      "resolved": "https://registry.npmmirror.com/@types/babel__template/-/babel__template-7.4.4.tgz",
+      "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.1.0",
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "node_modules/@types/babel__traverse": {
+      "version": "7.28.0",
+      "resolved": "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+      "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.28.2"
+      }
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz",
+      "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/prop-types": {
+      "version": "15.7.15",
+      "resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz",
+      "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/react": {
+      "version": "18.3.31",
+      "resolved": "https://registry.npmmirror.com/@types/react/-/react-18.3.31.tgz",
+      "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/prop-types": "*",
+        "csstype": "^3.2.2"
+      }
+    },
+    "node_modules/@types/react-dom": {
+      "version": "18.3.7",
+      "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-18.3.7.tgz",
+      "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": "^18.0.0"
+      }
+    },
+    "node_modules/@vitejs/plugin-react": {
+      "version": "4.7.0",
+      "resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+      "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/core": "^7.28.0",
+        "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+        "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+        "@rolldown/pluginutils": "1.0.0-beta.27",
+        "@types/babel__core": "^7.20.5",
+        "react-refresh": "^0.17.0"
+      },
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "peerDependencies": {
+        "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+      }
+    },
+    "node_modules/agent-base": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz",
+      "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/any-promise": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz",
+      "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/anymatch": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz",
+      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/arg": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz",
+      "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/autoprefixer": {
+      "version": "10.5.4",
+      "resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.5.4.tgz",
+      "integrity": "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "browserslist": "^4.28.6",
+        "caniuse-lite": "^1.0.30001806",
+        "fraction.js": "^5.3.4",
+        "picocolors": "^1.1.1",
+        "postcss-value-parser": "^4.2.0"
+      },
+      "bin": {
+        "autoprefixer": "bin/autoprefixer"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      },
+      "peerDependencies": {
+        "postcss": "^8.1.0"
+      }
+    },
+    "node_modules/axios": {
+      "version": "1.18.1",
+      "resolved": "https://registry.npmmirror.com/axios/-/axios-1.18.1.tgz",
+      "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.16.0",
+        "form-data": "^4.0.5",
+        "https-proxy-agent": "^5.0.1",
+        "proxy-from-env": "^2.1.0"
+      }
+    },
+    "node_modules/baseline-browser-mapping": {
+      "version": "2.11.1",
+      "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.1.tgz",
+      "integrity": "sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "baseline-browser-mapping": "dist/cli.cjs"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/binary-extensions": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz",
+      "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/braces": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fill-range": "^7.1.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/browserslist": {
+      "version": "4.28.7",
+      "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.7.tgz",
+      "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "baseline-browser-mapping": "^2.10.44",
+        "caniuse-lite": "^1.0.30001806",
+        "electron-to-chromium": "^1.5.393",
+        "node-releases": "^2.0.51",
+        "update-browserslist-db": "^1.2.3"
+      },
+      "bin": {
+        "browserslist": "cli.js"
+      },
+      "engines": {
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/camelcase-css": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/camelcase-css/-/camelcase-css-2.0.1.tgz",
+      "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/caniuse-lite": {
+      "version": "1.0.30001806",
+      "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz",
+      "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "CC-BY-4.0"
+    },
+    "node_modules/chokidar": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz",
+      "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "anymatch": "~3.1.2",
+        "braces": "~3.0.2",
+        "glob-parent": "~5.1.2",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.6.0"
+      },
+      "engines": {
+        "node": ">= 8.10.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/chokidar/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/commander": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz",
+      "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/convert-source-map": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz",
+      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/cssesc": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz",
+      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "cssesc": "bin/cssesc"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/csstype": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
+      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/didyoumean": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz",
+      "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+      "dev": true,
+      "license": "Apache-2.0"
+    },
+    "node_modules/dlv": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz",
+      "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.5.395",
+      "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.395.tgz",
+      "integrity": "sha512-7zt9Aw+SrmxLWLN0zhaTWZQiCdryLVrYTq5R7iZakLvi2UQPYMMsROYV/2qVCzMeCiSXHwKOU+sZ4zOVVlrtKA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+      "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz",
+      "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.21.5",
+        "@esbuild/android-arm": "0.21.5",
+        "@esbuild/android-arm64": "0.21.5",
+        "@esbuild/android-x64": "0.21.5",
+        "@esbuild/darwin-arm64": "0.21.5",
+        "@esbuild/darwin-x64": "0.21.5",
+        "@esbuild/freebsd-arm64": "0.21.5",
+        "@esbuild/freebsd-x64": "0.21.5",
+        "@esbuild/linux-arm": "0.21.5",
+        "@esbuild/linux-arm64": "0.21.5",
+        "@esbuild/linux-ia32": "0.21.5",
+        "@esbuild/linux-loong64": "0.21.5",
+        "@esbuild/linux-mips64el": "0.21.5",
+        "@esbuild/linux-ppc64": "0.21.5",
+        "@esbuild/linux-riscv64": "0.21.5",
+        "@esbuild/linux-s390x": "0.21.5",
+        "@esbuild/linux-x64": "0.21.5",
+        "@esbuild/netbsd-x64": "0.21.5",
+        "@esbuild/openbsd-x64": "0.21.5",
+        "@esbuild/sunos-x64": "0.21.5",
+        "@esbuild/win32-arm64": "0.21.5",
+        "@esbuild/win32-ia32": "0.21.5",
+        "@esbuild/win32-x64": "0.21.5"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/fast-glob": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz",
+      "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.8"
+      },
+      "engines": {
+        "node": ">=8.6.0"
+      }
+    },
+    "node_modules/fast-glob/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fastq": {
+      "version": "1.20.1",
+      "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz",
+      "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "node_modules/fill-range": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.16.0",
+      "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz",
+      "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.6.tgz",
+      "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.4",
+        "mime-types": "^2.1.35"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fraction.js": {
+      "version": "5.3.4",
+      "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-5.3.4.tgz",
+      "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/rawify"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/gensync": {
+      "version": "1.0.0-beta.2",
+      "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz",
+      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/glob-parent": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz",
+      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz",
+      "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+      "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "6",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/is-binary-path": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz",
+      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "binary-extensions": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-core-module": {
+      "version": "2.16.2",
+      "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.2.tgz",
+      "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "hasown": "^2.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/jiti": {
+      "version": "1.21.7",
+      "resolved": "https://registry.npmmirror.com/jiti/-/jiti-1.21.7.tgz",
+      "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "jiti": "bin/jiti.js"
+      }
+    },
+    "node_modules/js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "license": "MIT"
+    },
+    "node_modules/jsesc": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz",
+      "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "jsesc": "bin/jsesc"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/json5": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz",
+      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "json5": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/lilconfig": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.3.tgz",
+      "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antonk52"
+      }
+    },
+    "node_modules/lines-and-columns": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/loose-envify": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
+      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "js-tokens": "^3.0.0 || ^4.0.0"
+      },
+      "bin": {
+        "loose-envify": "cli.js"
+      }
+    },
+    "node_modules/lru-cache": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz",
+      "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^3.0.2"
+      }
+    },
+    "node_modules/lucide-react": {
+      "version": "0.368.0",
+      "resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.368.0.tgz",
+      "integrity": "sha512-soryVrCjheZs8rbXKdINw9B8iPi5OajBJZMJ1HORig89ljcOcEokKKAgGbg3QWxSXel7JwHOfDFUdDHAKyUAMw==",
+      "license": "ISC",
+      "peerDependencies": {
+        "react": "^16.5.1 || ^17.0.0 || ^18.0.0"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/merge2": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz",
+      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/micromatch": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "braces": "^3.0.3",
+        "picomatch": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/mz": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz",
+      "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "any-promise": "^1.0.0",
+        "object-assign": "^4.0.1",
+        "thenify-all": "^1.0.0"
+      }
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.16",
+      "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.16.tgz",
+      "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/node-releases": {
+      "version": "2.0.51",
+      "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.51.tgz",
+      "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/normalize-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-hash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/object-hash/-/object-hash-3.0.0.tgz",
+      "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/path-parse": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz",
+      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz",
+      "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pify": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz",
+      "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/pirates": {
+      "version": "4.0.7",
+      "resolved": "https://registry.npmmirror.com/pirates/-/pirates-4.0.7.tgz",
+      "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.22",
+      "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.22.tgz",
+      "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.16",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/postcss-import": {
+      "version": "15.1.0",
+      "resolved": "https://registry.npmmirror.com/postcss-import/-/postcss-import-15.1.0.tgz",
+      "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "postcss-value-parser": "^4.0.0",
+        "read-cache": "^1.0.0",
+        "resolve": "^1.1.7"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "peerDependencies": {
+        "postcss": "^8.0.0"
+      }
+    },
+    "node_modules/postcss-js": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/postcss-js/-/postcss-js-4.1.0.tgz",
+      "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "camelcase-css": "^2.0.1"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >= 16"
+      },
+      "peerDependencies": {
+        "postcss": "^8.4.21"
+      }
+    },
+    "node_modules/postcss-load-config": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
+      "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "lilconfig": "^3.1.1"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "peerDependencies": {
+        "jiti": ">=1.21.0",
+        "postcss": ">=8.0.9",
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
+      },
+      "peerDependenciesMeta": {
+        "jiti": {
+          "optional": true
+        },
+        "postcss": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/postcss-nested": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmmirror.com/postcss-nested/-/postcss-nested-6.2.0.tgz",
+      "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "postcss-selector-parser": "^6.1.1"
+      },
+      "engines": {
+        "node": ">=12.0"
+      },
+      "peerDependencies": {
+        "postcss": "^8.2.14"
+      }
+    },
+    "node_modules/postcss-selector-parser": {
+      "version": "6.1.4",
+      "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz",
+      "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cssesc": "^3.0.0",
+        "util-deprecate": "^1.0.2"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/postcss-value-parser": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+      "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/proxy-from-env": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+      "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/react": {
+      "version": "18.3.1",
+      "resolved": "https://registry.npmmirror.com/react/-/react-18.3.1.tgz",
+      "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/react-dom": {
+      "version": "18.3.1",
+      "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz",
+      "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.1.0",
+        "scheduler": "^0.23.2"
+      },
+      "peerDependencies": {
+        "react": "^18.3.1"
+      }
+    },
+    "node_modules/react-refresh": {
+      "version": "0.17.0",
+      "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.17.0.tgz",
+      "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/read-cache": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/read-cache/-/read-cache-1.0.0.tgz",
+      "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "pify": "^2.3.0"
+      }
+    },
+    "node_modules/readdirp": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",
+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "picomatch": "^2.2.1"
+      },
+      "engines": {
+        "node": ">=8.10.0"
+      }
+    },
+    "node_modules/resolve": {
+      "version": "1.22.12",
+      "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.12.tgz",
+      "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "is-core-module": "^2.16.1",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      },
+      "bin": {
+        "resolve": "bin/resolve"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/reusify": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz",
+      "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "iojs": ">=1.0.0",
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/rollup": {
+      "version": "4.62.2",
+      "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.62.2.tgz",
+      "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "1.0.9"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@rollup/rollup-android-arm-eabi": "4.62.2",
+        "@rollup/rollup-android-arm64": "4.62.2",
+        "@rollup/rollup-darwin-arm64": "4.62.2",
+        "@rollup/rollup-darwin-x64": "4.62.2",
+        "@rollup/rollup-freebsd-arm64": "4.62.2",
+        "@rollup/rollup-freebsd-x64": "4.62.2",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.62.2",
+        "@rollup/rollup-linux-arm-musleabihf": "4.62.2",
+        "@rollup/rollup-linux-arm64-gnu": "4.62.2",
+        "@rollup/rollup-linux-arm64-musl": "4.62.2",
+        "@rollup/rollup-linux-loong64-gnu": "4.62.2",
+        "@rollup/rollup-linux-loong64-musl": "4.62.2",
+        "@rollup/rollup-linux-ppc64-gnu": "4.62.2",
+        "@rollup/rollup-linux-ppc64-musl": "4.62.2",
+        "@rollup/rollup-linux-riscv64-gnu": "4.62.2",
+        "@rollup/rollup-linux-riscv64-musl": "4.62.2",
+        "@rollup/rollup-linux-s390x-gnu": "4.62.2",
+        "@rollup/rollup-linux-x64-gnu": "4.62.2",
+        "@rollup/rollup-linux-x64-musl": "4.62.2",
+        "@rollup/rollup-openbsd-x64": "4.62.2",
+        "@rollup/rollup-openharmony-arm64": "4.62.2",
+        "@rollup/rollup-win32-arm64-msvc": "4.62.2",
+        "@rollup/rollup-win32-ia32-msvc": "4.62.2",
+        "@rollup/rollup-win32-x64-gnu": "4.62.2",
+        "@rollup/rollup-win32-x64-msvc": "4.62.2",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
+    "node_modules/scheduler": {
+      "version": "0.23.2",
+      "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz",
+      "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.1.0"
+      }
+    },
+    "node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/sucrase": {
+      "version": "3.35.1",
+      "resolved": "https://registry.npmmirror.com/sucrase/-/sucrase-3.35.1.tgz",
+      "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.2",
+        "commander": "^4.0.0",
+        "lines-and-columns": "^1.1.6",
+        "mz": "^2.7.0",
+        "pirates": "^4.0.1",
+        "tinyglobby": "^0.2.11",
+        "ts-interface-checker": "^0.1.9"
+      },
+      "bin": {
+        "sucrase": "bin/sucrase",
+        "sucrase-node": "bin/sucrase-node"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/supports-preserve-symlinks-flag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/tailwindcss": {
+      "version": "3.4.19",
+      "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.19.tgz",
+      "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@alloc/quick-lru": "^5.2.0",
+        "arg": "^5.0.2",
+        "chokidar": "^3.6.0",
+        "didyoumean": "^1.2.2",
+        "dlv": "^1.1.3",
+        "fast-glob": "^3.3.2",
+        "glob-parent": "^6.0.2",
+        "is-glob": "^4.0.3",
+        "jiti": "^1.21.7",
+        "lilconfig": "^3.1.3",
+        "micromatch": "^4.0.8",
+        "normalize-path": "^3.0.0",
+        "object-hash": "^3.0.0",
+        "picocolors": "^1.1.1",
+        "postcss": "^8.4.47",
+        "postcss-import": "^15.1.0",
+        "postcss-js": "^4.0.1",
+        "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
+        "postcss-nested": "^6.2.0",
+        "postcss-selector-parser": "^6.1.2",
+        "resolve": "^1.22.8",
+        "sucrase": "^3.35.0"
+      },
+      "bin": {
+        "tailwind": "lib/cli.js",
+        "tailwindcss": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/thenify": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz",
+      "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "any-promise": "^1.0.0"
+      }
+    },
+    "node_modules/thenify-all": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz",
+      "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "thenify": ">= 3.1.0 < 4"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.17",
+      "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz",
+      "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/tinyglobby/node_modules/fdir": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
+      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/tinyglobby/node_modules/picomatch": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz",
+      "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/ts-interface-checker": {
+      "version": "0.1.13",
+      "resolved": "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+      "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+      "dev": true,
+      "license": "Apache-2.0"
+    },
+    "node_modules/update-browserslist-db": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+      "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "escalade": "^3.2.0",
+        "picocolors": "^1.1.1"
+      },
+      "bin": {
+        "update-browserslist-db": "cli.js"
+      },
+      "peerDependencies": {
+        "browserslist": ">= 4.21.0"
+      }
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/vite": {
+      "version": "5.4.21",
+      "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz",
+      "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.21.3",
+        "postcss": "^8.4.43",
+        "rollup": "^4.20.0"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^18.0.0 || >=20.0.0",
+        "less": "*",
+        "lightningcss": "^1.21.0",
+        "sass": "*",
+        "sass-embedded": "*",
+        "stylus": "*",
+        "sugarss": "*",
+        "terser": "^5.4.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/yallist": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz",
+      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+      "dev": true,
+      "license": "ISC"
+    }
+  }
+}

+ 26 - 0
package.json

@@ -0,0 +1,26 @@
+{
+  "name": "celestia-trace-frontend",
+  "private": true,
+  "version": "1.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "axios": "^1.6.8",
+    "lucide-react": "^0.368.0",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0"
+  },
+  "devDependencies": {
+    "@types/react": "^18.2.66",
+    "@types/react-dom": "^18.2.22",
+    "@vitejs/plugin-react": "^4.2.1",
+    "autoprefixer": "^10.4.19",
+    "postcss": "^8.4.38",
+    "tailwindcss": "^3.4.3",
+    "vite": "^5.2.0"
+  }
+}

+ 6 - 0
postcss.config.js

@@ -0,0 +1,6 @@
+export default {
+  plugins: {
+    tailwindcss: {},
+    autoprefixer: {},
+  },
+}

+ 14 - 0
public/spark-logo.svg

@@ -0,0 +1,14 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="none">
+  <defs>
+    <linearGradient id="sparkGrad" x1="0%" y1="0%" x2="100%" y2="100%">
+      <stop offset="0%" stop-color="#00F2FE"/>
+      <stop offset="100%" stop-color="#7F56D9"/>
+    </linearGradient>
+    <radialGradient id="glow" cx="50%" cy="50%" r="50%">
+      <stop offset="0%" stop-color="#00F2FE" stop-opacity="0.6"/>
+      <stop offset="100%" stop-color="#0B0E14" stop-opacity="0"/>
+    </radialGradient>
+  </defs>
+  <circle cx="50" cy="50" r="45" fill="url(#glow)" />
+  <path d="M50 10 L58 38 L86 38 L63 55 L71 83 L50 66 L29 83 L37 55 L14 38 L42 38 Z" fill="url(#sparkGrad)" />
+</svg>

+ 159 - 0
src/App.jsx

@@ -0,0 +1,159 @@
+import React, { useState } from 'react';
+import { AuthProvider, useAuth } from './context/AuthContext';
+import { ThemeProvider } from './context/ThemeContext';
+import { Navbar } from './components/Navbar';
+import { Sidebar, FloatingTabBar } from './components/Sidebar';
+import { DeviceBindModal } from './components/DeviceBindModal';
+import { RecordDetailModal } from './components/RecordDetailModal';
+import { AuthModal } from './components/AuthModal';
+import { EditProfileModal } from './components/EditProfileModal';
+import { ChangePasswordModal } from './components/ChangePasswordModal';
+import { SyncAuthPromptModal } from './components/SyncAuthPromptModal';
+import { Dashboard } from './pages/Dashboard';
+import { DevicesPage } from './pages/DevicesPage';
+import { RecordsPage } from './pages/RecordsPage';
+import { SyncSettingsPage } from './pages/SyncSettingsPage';
+import { ProfilePage } from './pages/ProfilePage';
+import { Sparkles } from 'lucide-react';
+
+const AppContent = () => {
+  const { user, loading } = useAuth();
+  const [activeTab, setActiveTab] = useState('dashboard');
+  const [isBindModalOpen, setIsBindModalOpen] = useState(false);
+  const [selectedSession, setSelectedSession] = useState(null);
+
+  // Global Modals State
+  const [isAuthModalOpen, setIsAuthModalOpen] = useState(false);
+  const [authModalTab, setAuthModalTab] = useState('login');
+  const [isEditProfileOpen, setIsEditProfileOpen] = useState(false);
+  const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false);
+  const [isSyncAuthPromptOpen, setIsSyncAuthPromptOpen] = useState(false);
+
+  const openAuthModal = (tab = 'login') => {
+    setAuthModalTab(tab);
+    setIsAuthModalOpen(true);
+  };
+
+  if (loading) {
+    return (
+      <div className="min-h-screen business-grid-bg flex items-center justify-center">
+        <div className="text-center space-y-3">
+          <div className="w-12 h-12 rounded-full bg-slate-900 dark:bg-white/10 border border-black/10 dark:border-white/20 flex items-center justify-center mx-auto animate-spin shadow-md">
+            <Sparkles className="w-6 h-6 text-slate-800 dark:text-white" />
+          </div>
+          <div className="text-xs text-slate-500 font-mono">加载星痕 CelestiaTrace 云引擎数据...</div>
+        </div>
+      </div>
+    );
+  }
+
+  return (
+    <div className="min-h-screen business-grid-bg text-slate-900 dark:text-slate-100 flex flex-col font-sans transition-colors">
+      <Navbar 
+        activeTab={activeTab} 
+        setActiveTab={setActiveTab}
+        onOpenAuthModal={() => openAuthModal('login')}
+      />
+
+      <div className="flex flex-1">
+        <Sidebar activeTab={activeTab} setActiveTab={setActiveTab} />
+
+        <main className="flex-1 p-4 sm:p-6 lg:p-8 max-w-6xl mx-auto w-full overflow-x-hidden">
+          {activeTab === 'dashboard' && (
+            <Dashboard
+              setActiveTab={setActiveTab}
+              onSelectSession={(sess) => setSelectedSession(sess)}
+              onOpenBindModal={() => {
+                if (!user) {
+                  openAuthModal('login');
+                } else {
+                  setIsBindModalOpen(true);
+                }
+              }}
+            />
+          )}
+
+          {activeTab === 'records' && (
+            <RecordsPage
+              onSelectSession={(sess) => setSelectedSession(sess)}
+            />
+          )}
+
+          {activeTab === 'devices' && (
+            <DevicesPage
+              onOpenBindModal={() => {
+                if (!user) {
+                  openAuthModal('login');
+                } else {
+                  setIsBindModalOpen(true);
+                }
+              }}
+            />
+          )}
+
+          {activeTab === 'sync' && (
+            <SyncSettingsPage />
+          )}
+
+          {activeTab === 'profile' && (
+            <ProfilePage 
+              onOpenAuthModal={() => openAuthModal('login')}
+              onOpenEditProfileModal={() => setIsEditProfileOpen(true)}
+              onOpenChangePasswordModal={() => setIsChangePasswordOpen(true)}
+              onOpenSyncAuthPrompt={() => setIsSyncAuthPromptOpen(true)}
+              onOpenBindModal={() => setIsBindModalOpen(true)}
+            />
+          )}
+        </main>
+      </div>
+
+      {/* iOS Style Floating Tab Bar */}
+      <FloatingTabBar activeTab={activeTab} setActiveTab={setActiveTab} />
+
+      {/* Global Modals */}
+      <DeviceBindModal
+        isOpen={isBindModalOpen}
+        onClose={() => setIsBindModalOpen(false)}
+        onDeviceBound={(dev) => console.log('Device bound:', dev)}
+      />
+
+      <RecordDetailModal
+        session={selectedSession}
+        onClose={() => setSelectedSession(null)}
+        onEventAdded={(sessId, ev) => console.log('Event added:', ev)}
+      />
+
+      <AuthModal
+        isOpen={isAuthModalOpen}
+        onClose={() => setIsAuthModalOpen(false)}
+        initialTab={authModalTab}
+      />
+
+      <EditProfileModal
+        isOpen={isEditProfileOpen}
+        onClose={() => setIsEditProfileOpen(false)}
+      />
+
+      <ChangePasswordModal
+        isOpen={isChangePasswordOpen}
+        onClose={() => setIsChangePasswordOpen(false)}
+      />
+
+      <SyncAuthPromptModal
+        isOpen={isSyncAuthPromptOpen}
+        onClose={() => setIsSyncAuthPromptOpen(false)}
+        onLoginClick={() => openAuthModal('login')}
+      />
+    </div>
+  );
+};
+
+export default function App() {
+  return (
+    <AuthProvider>
+      <ThemeProvider>
+        <AppContent />
+      </ThemeProvider>
+    </AuthProvider>
+  );
+}

+ 145 - 0
src/api.js

@@ -0,0 +1,145 @@
+import axios from 'axios';
+
+const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/v1';
+
+const api = axios.create({
+  baseURL: API_BASE_URL,
+  headers: {
+    'Content-Type': 'application/json',
+  },
+});
+
+// Interceptor to inject JWT token
+api.interceptors.request.use((config) => {
+  const token = localStorage.getItem('celestia_token');
+  if (token) {
+    config.headers.Authorization = `Bearer ${token}`;
+  }
+  return config;
+});
+
+let refreshPromise = null;
+api.interceptors.response.use(
+  (response) => response,
+  async (error) => {
+    const original = error.config;
+    if (error.response?.status !== 401 || original?._retried || original?.url?.includes('/auth/refresh')) {
+      return Promise.reject(error);
+    }
+    const refreshToken = localStorage.getItem('celestia_refresh_token');
+    if (!refreshToken) return Promise.reject(error);
+    original._retried = true;
+    refreshPromise ||= axios.post(`${API_BASE_URL}/auth/refresh`, { refreshToken })
+      .then((response) => {
+        const payload = response.data?.data;
+        if (!payload?.token || !payload?.refreshToken) throw new Error('Invalid refresh response');
+        localStorage.setItem('celestia_token', payload.token);
+        localStorage.setItem('celestia_refresh_token', payload.refreshToken);
+        return payload.token;
+      })
+      .finally(() => { refreshPromise = null; });
+    const token = await refreshPromise;
+    original.headers.Authorization = `Bearer ${token}`;
+    return api(original);
+  }
+);
+
+export const authAPI = {
+  login: async (identifier, password) => {
+    try {
+      const res = await api.post('/auth/login', { identifier, password });
+      return res.data;
+    } catch (err) {
+      if (err.response) throw err.response.data;
+      throw { message: '网络错误,请稍后重试' };
+    }
+  },
+  register: async (username, identifier, password) => {
+    try {
+      const res = await api.post('/auth/register', { username, identifier, password });
+      return res.data;
+    } catch (err) {
+      if (err.response) throw err.response.data;
+      throw { message: '网络错误,请稍后重试' };
+    }
+  },
+  getProfile: async () => {
+    const res = await api.get('/user/profile');
+    return res.data;
+  },
+  updateProfile: async (data) => {
+    const res = await api.put('/user/profile', data);
+    return res.data;
+  },
+  changePassword: async (oldPassword, newPassword) => {
+    const res = await api.post('/user/change-password', { oldPassword, newPassword });
+    return res.data;
+  },
+  logout: async () => {
+    const res = await api.post('/auth/logout');
+    return res.data;
+  }
+};
+
+export const deviceAPI = {
+  getDevices: async () => {
+    const res = await api.get('/devices');
+    return res.data;
+  },
+  bindDevice: async (deviceData) => {
+    const res = await api.post('/devices', deviceData);
+    return res.data;
+  },
+  updateDevice: async (id, deviceData) => {
+    const res = await api.put(`/devices/${id}`, deviceData);
+    return res.data;
+  },
+  unbindDevice: async (id) => {
+    const res = await api.delete(`/devices/${id}`);
+    return res.data;
+  }
+};
+
+export const sessionAPI = {
+  getSessions: async (search = '') => {
+    const res = await api.get('/sessions', { params: { search } });
+    return res.data;
+  },
+  getSessionDetail: async (id) => {
+    const res = await api.get(`/sessions/${id}`);
+    return res.data;
+  },
+  createSession: async (sessionData) => {
+    const res = await api.post('/sessions', sessionData);
+    return res.data;
+  },
+  addEvent: async (sessionId, eventData) => {
+    const res = await api.post(`/sessions/${sessionId}/events`, eventData);
+    return res.data;
+  },
+  deleteSession: async (id) => {
+    const res = await api.delete(`/sessions/${id}`);
+    return res.data;
+  }
+};
+
+export const syncAPI = {
+  getSettings: async () => {
+    const res = await api.get('/sync/settings');
+    return res.data;
+  },
+  updateSettings: async (settings) => {
+    const res = await api.put('/sync/settings', settings);
+    return res.data;
+  },
+  getSummary: async () => {
+    const res = await api.get('/sync/summary');
+    return res.data;
+  },
+  triggerManualSync: async () => {
+    const res = await api.post('/sync/trigger');
+    return res.data;
+  }
+};
+
+export default api;

+ 209 - 0
src/components/AuthModal.jsx

@@ -0,0 +1,209 @@
+import React, { useState, useEffect } from 'react';
+import { X, User, Mail, Lock, ShieldCheck, Loader2 } from 'lucide-react';
+import { useAuth } from '../context/AuthContext';
+
+export const AuthModal = ({ isOpen, onClose, initialTab = 'login' }) => {
+  const { login, register } = useAuth();
+  const [selectedTab, setSelectedTab] = useState(initialTab); // 'login' | 'register'
+  const [usernameInput, setUsernameInput] = useState('');
+  const [identifierInput, setIdentifierInput] = useState('');
+  const [passwordInput, setPasswordInput] = useState('');
+  const [confirmPasswordInput, setConfirmPasswordInput] = useState('');
+  const [errorMessage, setErrorMessage] = useState('');
+  const [isProcessing, setIsProcessing] = useState(false);
+
+  useEffect(() => {
+    if (isOpen) {
+      setSelectedTab(initialTab);
+      setErrorMessage('');
+      setUsernameInput('');
+      setIdentifierInput('');
+      setPasswordInput('');
+      setConfirmPasswordInput('');
+    }
+  }, [isOpen, initialTab]);
+
+  if (!isOpen) return null;
+
+  const handleSubmit = async (e) => {
+    e.preventDefault();
+    setErrorMessage('');
+
+    if (selectedTab === 'login') {
+      if (!identifierInput.trim()) {
+        setErrorMessage('请输入手机号或邮箱');
+        return;
+      }
+      if (passwordInput.length < 6) {
+        setErrorMessage('密码不能少于 6 位');
+        return;
+      }
+
+      setIsProcessing(true);
+      const res = await login(identifierInput.trim(), passwordInput);
+      setIsProcessing(false);
+
+      if (res.success) {
+        onClose();
+      } else {
+        setErrorMessage(res.message || '登录失败');
+      }
+    } else {
+      const name = usernameInput.trim();
+      if (!name) {
+        setErrorMessage('请输入用户昵称');
+        return;
+      }
+      if (!identifierInput.trim()) {
+        setErrorMessage('请输入注册手机号或邮箱');
+        return;
+      }
+      if (passwordInput.length < 6) {
+        setErrorMessage('密码不能少于 6 位');
+        return;
+      }
+      if (passwordInput !== confirmPasswordInput) {
+        setErrorMessage('两次输入的密码不一致');
+        return;
+      }
+
+      setIsProcessing(true);
+      const res = await register(name, identifierInput.trim(), passwordInput);
+      setIsProcessing(false);
+
+      if (res.success) {
+        onClose();
+      } else {
+        setErrorMessage(res.message || '注册失败');
+      }
+    }
+  };
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/75 backdrop-blur-md animate-fade-in">
+      <div className="relative w-full max-w-md bg-[#0B0C10] border border-white/15 rounded-3xl p-6 sm:p-8 shadow-2xl text-white space-y-6">
+        {/* Header */}
+        <div className="flex items-center justify-between">
+          <div className="w-8" />
+          <div className="text-center space-y-1">
+            <h2 className="text-xl font-bold tracking-tight">
+              {selectedTab === 'login' ? '账号登录' : '创建新账号'}
+            </h2>
+            <p className="text-xs text-slate-400">
+              {selectedTab === 'login'
+                ? '登录以同步录音并管理专属设备'
+                : '注册后可将本机记录安全同步至云端'}
+            </p>
+          </div>
+          <button
+            onClick={onClose}
+            className="p-1 rounded-full text-slate-400 hover:text-white hover:bg-white/10 transition-colors"
+          >
+            <X className="w-5 h-5" />
+          </button>
+        </div>
+
+        {/* Tab Picker */}
+        <div className="flex rounded-xl bg-white/[0.06] p-1 text-xs font-medium border border-white/10">
+          <button
+            type="button"
+            onClick={() => {
+              setSelectedTab('login');
+              setErrorMessage('');
+            }}
+            className={`flex-1 py-2 rounded-lg transition-all ${
+              selectedTab === 'login'
+                ? 'bg-white/15 text-white font-semibold shadow-sm'
+                : 'text-slate-400 hover:text-white'
+            }`}
+          >
+            登录
+          </button>
+          <button
+            type="button"
+            onClick={() => {
+              setSelectedTab('register');
+              setErrorMessage('');
+            }}
+            className={`flex-1 py-2 rounded-lg transition-all ${
+              selectedTab === 'register'
+                ? 'bg-white/15 text-white font-semibold shadow-sm'
+                : 'text-slate-400 hover:text-white'
+            }`}
+          >
+            注册账号
+          </button>
+        </div>
+
+        {/* Form Inputs */}
+        <form onSubmit={handleSubmit} className="space-y-4">
+          {selectedTab === 'register' && (
+            <div className="relative">
+              <User className="w-4 h-4 text-slate-400 absolute left-3.5 top-3.5" />
+              <input
+                type="text"
+                value={usernameInput}
+                onChange={(e) => setUsernameInput(e.target.value)}
+                placeholder="设置昵称 (如: 星痕探索者)"
+                className="w-full pl-10 pr-4 py-2.5 rounded-xl bg-white/[0.04] border border-white/10 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-white/30 transition-all"
+              />
+            </div>
+          )}
+
+          <div className="relative">
+            <Mail className="w-4 h-4 text-slate-400 absolute left-3.5 top-3.5" />
+            <input
+              type="text"
+              value={identifierInput}
+              onChange={(e) => setIdentifierInput(e.target.value)}
+              placeholder="手机号或电子邮箱"
+              className="w-full pl-10 pr-4 py-2.5 rounded-xl bg-white/[0.04] border border-white/10 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-white/30 transition-all"
+            />
+          </div>
+
+          <div className="relative">
+            <Lock className="w-4 h-4 text-slate-400 absolute left-3.5 top-3.5" />
+            <input
+              type="password"
+              value={passwordInput}
+              onChange={(e) => setPasswordInput(e.target.value)}
+              placeholder="密码 (至少 6 位)"
+              className="w-full pl-10 pr-4 py-2.5 rounded-xl bg-white/[0.04] border border-white/10 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-white/30 transition-all"
+            />
+          </div>
+
+          {selectedTab === 'register' && (
+            <div className="relative">
+              <ShieldCheck className="w-4 h-4 text-slate-400 absolute left-3.5 top-3.5" />
+              <input
+                type="password"
+                value={confirmPasswordInput}
+                onChange={(e) => setConfirmPasswordInput(e.target.value)}
+                placeholder="再次确认密码"
+                className="w-full pl-10 pr-4 py-2.5 rounded-xl bg-white/[0.04] border border-white/10 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-white/30 transition-all"
+              />
+            </div>
+          )}
+
+          {errorMessage && (
+            <div className="p-3 rounded-xl bg-rose-500/10 border border-rose-500/30 text-rose-400 text-xs font-mono">
+              {errorMessage}
+            </div>
+          )}
+
+          <button
+            type="submit"
+            disabled={isProcessing}
+            className="w-full py-3 rounded-xl bg-white text-slate-950 font-semibold text-xs shadow-lg hover:bg-slate-100 transition-all flex items-center justify-center space-x-2 disabled:opacity-50"
+          >
+            {isProcessing ? (
+              <Loader2 className="w-4 h-4 animate-spin text-slate-900" />
+            ) : (
+              <span>{selectedTab === 'login' ? '安全登录' : '注册并自动登录'}</span>
+            )}
+          </button>
+        </form>
+      </div>
+    </div>
+  );
+};

+ 144 - 0
src/components/ChangePasswordModal.jsx

@@ -0,0 +1,144 @@
+import React, { useState, useEffect } from 'react';
+import { X, Lock, ShieldCheck, Loader2, Check } from 'lucide-react';
+import { useAuth } from '../context/AuthContext';
+
+export const ChangePasswordModal = ({ isOpen, onClose }) => {
+  const { changePassword } = useAuth();
+  const [oldPasswordInput, setOldPasswordInput] = useState('');
+  const [newPasswordInput, setNewPasswordInput] = useState('');
+  const [confirmPasswordInput, setConfirmPasswordInput] = useState('');
+  const [errorMessage, setErrorMessage] = useState('');
+  const [successToast, setSuccessToast] = useState('');
+  const [isProcessing, setIsProcessing] = useState(false);
+
+  useEffect(() => {
+    if (isOpen) {
+      setOldPasswordInput('');
+      setNewPasswordInput('');
+      setConfirmPasswordInput('');
+      setErrorMessage('');
+      setSuccessToast('');
+    }
+  }, [isOpen]);
+
+  if (!isOpen) return null;
+
+  const handleChangePassword = async (e) => {
+    e.preventDefault();
+    setErrorMessage('');
+    setSuccessToast('');
+
+    if (!oldPasswordInput) {
+      setErrorMessage('请输入原密码');
+      return;
+    }
+    if (newPasswordInput.length < 6) {
+      setErrorMessage('新密码长度不能少于 6 位');
+      return;
+    }
+    if (newPasswordInput !== confirmPasswordInput) {
+      setErrorMessage('两次输入的新密码不一致');
+      return;
+    }
+
+    setIsProcessing(true);
+    const res = await changePassword(oldPasswordInput, newPasswordInput);
+    setIsProcessing(false);
+
+    if (res.success) {
+      setSuccessToast('密码修改成功!请记牢新密码');
+      setTimeout(() => {
+        onClose();
+      }, 1000);
+    } else {
+      setErrorMessage(res.message || '原密码错误或修改失败');
+    }
+  };
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/75 backdrop-blur-md animate-fade-in">
+      <div className="relative w-full max-w-md bg-[#0B0C10] border border-white/15 rounded-3xl p-6 sm:p-8 shadow-2xl text-white space-y-6">
+        {/* Header */}
+        <div className="flex items-center justify-between">
+          <h2 className="text-lg font-bold">修改密码</h2>
+          <button
+            onClick={onClose}
+            className="p-1 rounded-full text-slate-400 hover:text-white hover:bg-white/10 transition-colors"
+          >
+            <X className="w-5 h-5" />
+          </button>
+        </div>
+
+        {/* Inputs */}
+        <form onSubmit={handleChangePassword} className="space-y-4">
+          <div className="space-y-1.5">
+            <label className="text-xs text-slate-400">当前原密码</label>
+            <div className="relative">
+              <Lock className="w-4 h-4 text-slate-400 absolute left-3.5 top-3.5" />
+              <input
+                type="password"
+                value={oldPasswordInput}
+                onChange={(e) => setOldPasswordInput(e.target.value)}
+                placeholder="请输入当前账号原密码"
+                className="w-full pl-10 pr-4 py-2.5 rounded-xl bg-white/[0.04] border border-white/10 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-white/30 transition-all"
+              />
+            </div>
+          </div>
+
+          <div className="space-y-1.5">
+            <label className="text-xs text-slate-400">新密码</label>
+            <div className="relative">
+              <Lock className="w-4 h-4 text-slate-400 absolute left-3.5 top-3.5" />
+              <input
+                type="password"
+                value={newPasswordInput}
+                onChange={(e) => setNewPasswordInput(e.target.value)}
+                placeholder="请输入新密码 (不少于 6 位)"
+                className="w-full pl-10 pr-4 py-2.5 rounded-xl bg-white/[0.04] border border-white/10 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-white/30 transition-all"
+              />
+            </div>
+          </div>
+
+          <div className="space-y-1.5">
+            <label className="text-xs text-slate-400">确认新密码</label>
+            <div className="relative">
+              <ShieldCheck className="w-4 h-4 text-slate-400 absolute left-3.5 top-3.5" />
+              <input
+                type="password"
+                value={confirmPasswordInput}
+                onChange={(e) => setConfirmPasswordInput(e.target.value)}
+                placeholder="请再次输入新密码"
+                className="w-full pl-10 pr-4 py-2.5 rounded-xl bg-white/[0.04] border border-white/10 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-white/30 transition-all"
+              />
+            </div>
+          </div>
+
+          {errorMessage && (
+            <div className="p-3 rounded-xl bg-rose-500/10 border border-rose-500/30 text-rose-400 text-xs font-mono">
+              {errorMessage}
+            </div>
+          )}
+
+          {successToast && (
+            <div className="p-3 rounded-xl bg-emerald-500/10 border border-emerald-500/30 text-emerald-400 text-xs flex items-center space-x-2">
+              <Check className="w-4 h-4" />
+              <span>{successToast}</span>
+            </div>
+          )}
+
+          <button
+            type="submit"
+            disabled={isProcessing}
+            className="w-full py-3 rounded-xl bg-white text-slate-950 font-semibold text-xs shadow-lg hover:bg-slate-100 transition-all flex items-center justify-center space-x-2 disabled:opacity-50"
+          >
+            {isProcessing ? (
+              <Loader2 className="w-4 h-4 animate-spin text-slate-900" />
+            ) : (
+              <span>确认修改密码</span>
+            )}
+          </button>
+        </form>
+      </div>
+    </div>
+  );
+};

+ 107 - 0
src/components/DeviceBindModal.jsx

@@ -0,0 +1,107 @@
+import React, { useState } from 'react';
+import { Radio, X, Wifi } from 'lucide-react';
+import { deviceAPI } from '../api';
+
+export const DeviceBindModal = ({ isOpen, onClose, onDeviceBound }) => {
+  const [name, setName] = useState('');
+  const [hardwareMAC, setHardwareMAC] = useState('');
+  const [firmwareVersion, setFirmwareVersion] = useState('');
+  const [peripheralUUID, setPeripheralUUID] = useState('');
+  const [errorMessage, setErrorMessage] = useState('');
+  const [loading, setLoading] = useState(false);
+
+  if (!isOpen) return null;
+
+  const handleSubmit = async (e) => {
+    e.preventDefault();
+    setLoading(true);
+    try {
+      const res = await deviceAPI.bindDevice({
+        name,
+        hardwareMAC,
+        firmwareVersion,
+        peripheralUUID
+      });
+      if (res.code === 0) {
+        onDeviceBound(res.data);
+        onClose();
+      }
+    } catch (err) {
+      setErrorMessage(err?.message || '设备绑定失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in">
+      <div className="w-full max-w-md business-card p-6 rounded-3xl relative shadow-2xl">
+        <button
+          onClick={onClose}
+          className="absolute top-4 right-4 p-1 text-slate-400 hover:text-slate-900 dark:hover:text-white transition-colors"
+        >
+          <X className="w-5 h-5" />
+        </button>
+
+        <div className="flex items-center space-x-3 mb-5">
+          <div className="p-2 rounded-xl bg-black/5 dark:bg-white/10 border border-black/10 dark:border-white/10 text-slate-900 dark:text-white">
+            <Radio className="w-5 h-5" />
+          </div>
+          <div>
+            <h3 className="text-sm font-bold text-slate-900 dark:text-white">绑定“微光 Spark”新设备</h3>
+            <p className="text-xs text-slate-400">登记与账户关联的 BLE 蓝牙随身麦克风</p>
+          </div>
+        </div>
+
+        <form onSubmit={handleSubmit} className="space-y-4">
+          {errorMessage && <div className="text-xs text-rose-500 font-mono">{errorMessage}</div>}
+          <div>
+            <label className="block text-[11px] font-mono text-slate-400 mb-1">iOS 外围设备 UUID</label>
+            <input type="text" value={peripheralUUID} onChange={(e) => setPeripheralUUID(e.target.value)} required className="w-full px-3 py-2 rounded-xl bg-black/[0.02] dark:bg-slate-900 border border-black/10 dark:border-white/10 text-xs font-mono text-slate-900 dark:text-white focus:outline-none" placeholder="填入 iOS 设备的 UUID..." />
+          </div>
+
+          <div>
+            <label className="block text-[11px] font-mono text-slate-400 mb-1">自定义名称</label>
+            <input
+              type="text"
+              value={name}
+              onChange={(e) => setName(e.target.value)}
+              required
+              className="w-full px-3 py-2 rounded-xl bg-black/[0.02] dark:bg-slate-900 border border-black/10 dark:border-white/10 text-xs text-slate-900 dark:text-white focus:outline-none"
+              placeholder="例如:Spark Pro 便携录音笔"
+            />
+          </div>
+
+          <div>
+            <label className="block text-[11px] font-mono text-slate-400 mb-1">硬件 MAC 地址</label>
+            <input
+              type="text"
+              value={hardwareMAC}
+              onChange={(e) => setHardwareMAC(e.target.value)}
+              required
+              className="w-full px-3 py-2 rounded-xl bg-black/[0.02] dark:bg-slate-900 border border-black/10 dark:border-white/10 text-xs font-mono text-slate-900 dark:text-white focus:outline-none"
+              placeholder="7C:D1:C3:92:44:FA"
+            />
+          </div>
+
+          <div className="flex items-center space-x-3 pt-2">
+            <button
+              type="button"
+              onClick={onClose}
+              className="w-1/2 py-2 rounded-xl border border-black/10 dark:border-white/10 text-xs font-semibold text-slate-500"
+            >
+              取消
+            </button>
+            <button
+              type="submit"
+              disabled={loading}
+              className="w-1/2 py-2 rounded-xl bg-slate-900 text-white dark:bg-white dark:text-slate-900 text-xs font-semibold"
+            >
+              {loading ? '绑定中...' : '确认绑定'}
+            </button>
+          </div>
+        </form>
+      </div>
+    </div>
+  );
+};

+ 135 - 0
src/components/EditProfileModal.jsx

@@ -0,0 +1,135 @@
+import React, { useState, useEffect } from 'react';
+import { X, User, Mail, Phone, Loader2, Check } from 'lucide-react';
+import { useAuth } from '../context/AuthContext';
+
+export const EditProfileModal = ({ isOpen, onClose }) => {
+  const { user, updateProfile } = useAuth();
+  const [usernameInput, setUsernameInput] = useState('');
+  const [emailInput, setEmailInput] = useState('');
+  const [phoneInput, setPhoneInput] = useState('');
+  const [errorMessage, setErrorMessage] = useState('');
+  const [successToast, setSuccessToast] = useState('');
+  const [isProcessing, setIsProcessing] = useState(false);
+
+  useEffect(() => {
+    if (isOpen && user) {
+      setUsernameInput(user.username || '');
+      setEmailInput(user.email || '');
+      setPhoneInput(user.phoneNumber || '');
+      setErrorMessage('');
+      setSuccessToast('');
+    }
+  }, [isOpen, user]);
+
+  if (!isOpen) return null;
+
+  const handleSave = async (e) => {
+    e.preventDefault();
+    setErrorMessage('');
+    setSuccessToast('');
+
+    setIsProcessing(true);
+    const res = await updateProfile({
+      username: usernameInput.trim(),
+      email: emailInput.trim(),
+      phoneNumber: phoneInput.trim(),
+    });
+    setIsProcessing(false);
+
+    if (res.success) {
+      setSuccessToast('个人资料更新成功!');
+      setTimeout(() => {
+        onClose();
+      }, 800);
+    } else {
+      setErrorMessage(res.message || '保存修改失败');
+    }
+  };
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/75 backdrop-blur-md animate-fade-in">
+      <div className="relative w-full max-w-md bg-[#0B0C10] border border-white/15 rounded-3xl p-6 sm:p-8 shadow-2xl text-white space-y-6">
+        {/* Header */}
+        <div className="flex items-center justify-between">
+          <h2 className="text-lg font-bold">编辑个人资料</h2>
+          <button
+            onClick={onClose}
+            className="p-1 rounded-full text-slate-400 hover:text-white hover:bg-white/10 transition-colors"
+          >
+            <X className="w-5 h-5" />
+          </button>
+        </div>
+
+        {/* Inputs */}
+        <form onSubmit={handleSave} className="space-y-4">
+          <div className="space-y-1.5">
+            <label className="text-xs text-slate-400">用户昵称</label>
+            <div className="relative">
+              <User className="w-4 h-4 text-slate-400 absolute left-3.5 top-3.5" />
+              <input
+                type="text"
+                value={usernameInput}
+                onChange={(e) => setUsernameInput(e.target.value)}
+                placeholder="请输入新的用户昵称"
+                className="w-full pl-10 pr-4 py-2.5 rounded-xl bg-white/[0.04] border border-white/10 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-white/30 transition-all"
+              />
+            </div>
+          </div>
+
+          <div className="space-y-1.5">
+            <label className="text-xs text-slate-400">电子邮箱</label>
+            <div className="relative">
+              <Mail className="w-4 h-4 text-slate-400 absolute left-3.5 top-3.5" />
+              <input
+                type="email"
+                value={emailInput}
+                onChange={(e) => setEmailInput(e.target.value)}
+                placeholder="绑定/修改电子邮箱"
+                className="w-full pl-10 pr-4 py-2.5 rounded-xl bg-white/[0.04] border border-white/10 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-white/30 transition-all"
+              />
+            </div>
+          </div>
+
+          <div className="space-y-1.5">
+            <label className="text-xs text-slate-400">手机号码</label>
+            <div className="relative">
+              <Phone className="w-4 h-4 text-slate-400 absolute left-3.5 top-3.5" />
+              <input
+                type="text"
+                value={phoneInput}
+                onChange={(e) => setPhoneInput(e.target.value)}
+                placeholder="绑定/修改手机号码"
+                className="w-full pl-10 pr-4 py-2.5 rounded-xl bg-white/[0.04] border border-white/10 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-white/30 transition-all"
+              />
+            </div>
+          </div>
+
+          {errorMessage && (
+            <div className="p-3 rounded-xl bg-rose-500/10 border border-rose-500/30 text-rose-400 text-xs font-mono">
+              {errorMessage}
+            </div>
+          )}
+
+          {successToast && (
+            <div className="p-3 rounded-xl bg-emerald-500/10 border border-emerald-500/30 text-emerald-400 text-xs flex items-center space-x-2">
+              <Check className="w-4 h-4" />
+              <span>{successToast}</span>
+            </div>
+          )}
+
+          <button
+            type="submit"
+            disabled={isProcessing}
+            className="w-full py-3 rounded-xl bg-white text-slate-950 font-semibold text-xs shadow-lg hover:bg-slate-100 transition-all flex items-center justify-center space-x-2 disabled:opacity-50"
+          >
+            {isProcessing ? (
+              <Loader2 className="w-4 h-4 animate-spin text-slate-900" />
+            ) : (
+              <span>保存修改</span>
+            )}
+          </button>
+        </form>
+      </div>
+    </div>
+  );
+};

+ 125 - 0
src/components/Navbar.jsx

@@ -0,0 +1,125 @@
+import React, { useState } from 'react';
+import { RefreshCw, User, LogOut, CheckCircle2 } from 'lucide-react';
+import { useAuth } from '../context/AuthContext';
+import { syncAPI } from '../api';
+
+export const Navbar = ({ activeTab, setActiveTab, onOpenAuthModal }) => {
+  const { user, logout } = useAuth();
+  const [syncing, setSyncing] = useState(false);
+  const [syncToast, setSyncToast] = useState(false);
+
+  // Format current date matching iOS app format
+  const currentDateFormatted = (() => {
+    const d = new Date();
+    const year = d.getFullYear();
+    const month = d.getMonth() + 1;
+    const date = d.getDate();
+    const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
+    const weekDay = weekDays[d.getDay()];
+    return `${year}年${month}月${date}日 ${weekDay}`;
+  })();
+
+  const handleSyncNow = async () => {
+    if (!user) {
+      if (onOpenAuthModal) onOpenAuthModal();
+      return;
+    }
+    setSyncing(true);
+    try {
+      await syncAPI.triggerManualSync();
+      setSyncToast(true);
+      setTimeout(() => setSyncToast(false), 3000);
+    } catch (e) {
+      console.warn("Manual sync failed:", e);
+    } finally {
+      setSyncing(false);
+    }
+  };
+
+  return (
+    <header className="sticky top-0 z-40 w-full bg-[#F8F9FA]/90 backdrop-blur-md border-b border-black/5 px-4 sm:px-8 py-3.5 flex items-center justify-between">
+      {/* Left Title Section matching iOS TopBar screenshot */}
+      <div 
+        className="flex items-center space-x-3.5 cursor-pointer select-none"
+        onClick={() => setActiveTab('dashboard')}
+      >
+        <div>
+          <div className="flex items-center space-x-2">
+            <h1 className="text-xs font-bold font-mono tracking-widest-tech uppercase text-[#1D1D1F] leading-tight">
+              星痕 现场 记录
+            </h1>
+          </div>
+          <p className="text-[13px] text-[#8E8E93] font-sans leading-tight mt-0.5">
+            {currentDateFormatted}
+          </p>
+        </div>
+      </div>
+
+      {/* Center Status Indicator & Sync Actions */}
+      <div className="flex items-center space-x-3">
+        {/* iOS Ready Status Badge matching screenshot */}
+        <div className="flex items-center space-x-1.5 px-2.5 py-1 rounded-md bg-white border border-black/15 text-[10px] font-mono font-semibold text-[#8E8E93] shadow-2xs">
+          <span className="w-1.5 h-1.5 rounded-full bg-[#34C759]"></span>
+          <span>就绪</span>
+        </div>
+
+        {syncToast && (
+          <div className="hidden sm:flex items-center space-x-1.5 px-3 py-1 rounded-md bg-emerald-50 border border-emerald-200 text-emerald-700 text-xs animate-fade-in">
+            <CheckCircle2 className="w-3.5 h-3.5" />
+            <span>已同步至云端</span>
+          </div>
+        )}
+
+        <button
+          onClick={handleSyncNow}
+          disabled={syncing}
+          className="flex items-center space-x-2 px-3 py-1.5 text-xs font-medium text-slate-700 bg-white hover:bg-slate-50 border border-black/10 rounded-lg shadow-2xs transition-all"
+        >
+          <RefreshCw className={`w-3.5 h-3.5 text-slate-600 ${syncing ? 'animate-spin' : ''}`} />
+          <span className="hidden sm:inline">{syncing ? '同步中...' : '手动同步'}</span>
+        </button>
+      </div>
+
+      {/* Right Controls: User Card / Auth button */}
+      <div className="flex items-center space-x-2">
+        {user ? (
+          <div className="flex items-center space-x-2 bg-white px-2.5 py-1.5 rounded-xl border border-black/10 shadow-2xs">
+            <img
+              src={user?.avatarURL || "/spark-logo.svg"}
+              alt="Avatar"
+              className="w-7 h-7 rounded-lg object-cover ring-1 ring-black/10"
+            />
+            <div className="hidden sm:block text-left">
+              <div className="text-xs font-semibold text-[#1D1D1F] leading-tight">{user.username}</div>
+              <div className="text-[10px] text-[#8E8E93] leading-tight font-mono">{user.phoneNumber || user.email || `ID: ${String(user.id || '').substring(0, 6)}`}</div>
+            </div>
+
+            <button
+              onClick={() => setActiveTab('profile')}
+              className="p-1 text-slate-500 hover:text-slate-900 transition-colors"
+              title="个人主页"
+            >
+              <User className="w-4 h-4" />
+            </button>
+            
+            <button
+              onClick={() => logout()}
+              className="p-1 text-slate-500 hover:text-rose-600 transition-colors"
+              title="退出登录"
+            >
+              <LogOut className="w-4 h-4" />
+            </button>
+          </div>
+        ) : (
+          <button
+            onClick={onOpenAuthModal}
+            className="flex items-center space-x-2 bg-slate-900 text-white dark:bg-white dark:text-slate-950 px-3.5 py-1.5 rounded-xl text-xs font-semibold shadow-2xs hover:opacity-90 transition-all"
+          >
+            <User className="w-3.5 h-3.5" />
+            <span>登录 / 注册</span>
+          </button>
+        )}
+      </div>
+    </header>
+  );
+};

+ 185 - 0
src/components/RecordDetailModal.jsx

@@ -0,0 +1,185 @@
+import React, { useState } from 'react';
+import { X, Play, Pause, FileText, Camera, Bookmark, Mic, Plus } from 'lucide-react';
+import { sessionAPI } from '../api';
+
+export const RecordDetailModal = ({ session, onClose, onEventAdded }) => {
+  const [isPlaying, setIsPlaying] = useState(false);
+  const [playbackTime, setPlaybackTime] = useState(0);
+  const [newNoteText, setNewNoteText] = useState('');
+  const [isSubmitting, setIsSubmitting] = useState(false);
+
+  if (!session) return null;
+
+  const events = session.events || [];
+
+  const formatDuration = (ms) => {
+    const totalSec = Math.floor((ms || 0) / 1000);
+    const m = Math.floor(totalSec / 60);
+    const s = totalSec % 60;
+    return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
+  };
+
+  const handleAddNote = async (e) => {
+    e.preventDefault();
+    if (!newNoteText.trim()) return;
+    setIsSubmitting(true);
+    try {
+      const res = await sessionAPI.addEvent(session.id, {
+        clientId: crypto.randomUUID(),
+        relativeTimeMs: playbackTime * 1000,
+        eventType: 'NOTE',
+        textContent: newNoteText
+      });
+      if (res.code === 0 && onEventAdded) {
+        onEventAdded(session.id, res.data);
+      }
+      setNewNoteText('');
+    } catch (err) {
+      window.alert(err?.message || '添加笔记失败');
+    } finally {
+      setIsSubmitting(false);
+    }
+  };
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in">
+      <div className="w-full max-w-2xl business-card rounded-3xl overflow-hidden shadow-2xl flex flex-col max-h-[85vh]">
+        {/* Modal Header */}
+        <div className="p-5 border-b border-black/10 dark:border-white/10 flex items-start justify-between">
+          <div>
+            <div className="flex items-center space-x-2 mb-1">
+              <span className="px-2 py-0.5 text-[10px] font-mono font-semibold text-slate-600 dark:text-slate-300 bg-black/5 dark:bg-white/10 rounded">
+                现场记录 Timeline
+              </span>
+              <span className="text-xs text-slate-400 font-mono">
+                {new Date(session.startTime).toLocaleString('zh-CN')}
+              </span>
+            </div>
+            <h2 className="text-base font-bold text-slate-900 dark:text-white">{session.title}</h2>
+          </div>
+
+          <button
+            onClick={onClose}
+            className="p-1 text-slate-400 hover:text-slate-900 dark:hover:text-white transition-colors"
+          >
+            <X className="w-5 h-5" />
+          </button>
+        </div>
+
+        {/* Modal Content */}
+        <div className="p-6 overflow-y-auto space-y-6 flex-1">
+          {/* Audio Player Card */}
+          <div className="p-4 rounded-2xl bg-black/[0.02] dark:bg-white/[0.03] border border-black/10 dark:border-white/10 space-y-3">
+            <div className="flex items-center justify-between">
+              <div className="flex items-center space-x-3">
+                <button
+                  onClick={() => setIsPlaying(!isPlaying)}
+                  className="w-10 h-10 rounded-full bg-slate-900 text-white dark:bg-white dark:text-slate-900 flex items-center justify-center shadow-md transition-transform active:scale-95"
+                >
+                  {isPlaying ? <Pause className="w-5 h-5 fill-current" /> : <Play className="w-5 h-5 fill-current ml-0.5" />}
+                </button>
+                <div>
+                  <div className="text-xs font-bold text-slate-900 dark:text-white">现场音频录像</div>
+                  <div className="text-[10px] font-mono text-slate-400">已加密备份至云端</div>
+                </div>
+              </div>
+
+              <div className="text-right font-mono text-xs font-semibold text-slate-900 dark:text-white">
+                {formatDuration(playbackTime * 1000)} / {session.durationFormatted || formatDuration(session.durationMs)}
+              </div>
+            </div>
+
+            {/* Waveform Bar */}
+            <div className="space-y-1 pt-1">
+              <div className="h-8 w-full flex items-center justify-between gap-1">
+                {Array.from({ length: 40 }).map((_, i) => (
+                  <div
+                    key={i}
+                    style={{ height: `${Math.max(20, Math.sin(i * 0.4) * 80 + 30)}%` }}
+                    className={`w-1 rounded-full transition-all ${
+                      i < 16 ? 'bg-slate-900 dark:bg-white' : 'bg-slate-300 dark:bg-slate-700'
+                    }`}
+                  />
+                ))}
+              </div>
+              <input
+                type="range"
+                min="0"
+                max={Math.floor((session.durationMs || 300000) / 1000)}
+                value={playbackTime}
+                onChange={(e) => setPlaybackTime(Number(e.target.value))}
+                className="w-full h-1 bg-slate-200 dark:bg-slate-800 rounded appearance-none cursor-pointer accent-slate-900 dark:accent-white"
+              />
+            </div>
+          </div>
+
+          {/* Timeline Node List */}
+          <div>
+            <h3 className="text-[10px] font-mono font-semibold uppercase tracking-widest text-slate-400 mb-3">
+              事件时间轴 ({events.length})
+            </h3>
+
+            <div className="space-y-3">
+              {events.length === 0 ? (
+                <div className="text-xs font-mono text-slate-400 text-center py-4">
+                  暂无附加事件,可使用下方表单添加随笔
+                </div>
+              ) : (
+                events.map((ev) => (
+                  <div key={ev.id} className="p-3 rounded-xl bg-black/[0.02] dark:bg-white/[0.03] border border-black/5 dark:border-white/5 space-y-1">
+                    <div className="flex items-center justify-between text-xs">
+                      <span className="font-mono text-slate-900 dark:text-white font-semibold">
+                        {formatDuration(ev.relativeTimeMs)}
+                      </span>
+                      <span className="text-[10px] font-mono text-slate-400 uppercase">
+                        {ev.eventType}
+                      </span>
+                    </div>
+
+                    {ev.textContent && (
+                      <p className="text-xs text-slate-700 dark:text-slate-200 leading-relaxed">{ev.textContent}</p>
+                    )}
+                  </div>
+                ))
+              )}
+            </div>
+          </div>
+
+          {/* Add Web Note Form */}
+          <form onSubmit={handleAddNote} className="p-3 rounded-2xl border border-black/10 dark:border-white/10 space-y-2">
+            <div className="flex items-center space-x-1.5 text-xs font-medium text-slate-900 dark:text-white">
+              <Plus className="w-3.5 h-3.5" />
+              <span>添加网页附言笔记</span>
+            </div>
+            <div className="flex space-x-2">
+              <input
+                type="text"
+                value={newNoteText}
+                onChange={(e) => setNewNoteText(e.target.value)}
+                placeholder="输入笔记标记内容..."
+                className="flex-1 px-3 py-1.5 rounded-xl bg-black/[0.02] dark:bg-slate-900 border border-black/10 dark:border-white/10 text-xs text-slate-900 dark:text-white focus:outline-none"
+              />
+              <button
+                type="submit"
+                disabled={isSubmitting}
+                className="px-3.5 py-1.5 rounded-xl bg-slate-900 text-white dark:bg-white dark:text-slate-900 text-xs font-medium"
+              >
+                提交
+              </button>
+            </div>
+          </form>
+        </div>
+
+        {/* Modal Footer */}
+        <div className="p-4 border-t border-black/10 dark:border-white/10 flex justify-end">
+          <button
+            onClick={onClose}
+            className="px-4 py-2 rounded-xl bg-slate-100 dark:bg-slate-800 text-xs font-semibold text-slate-800 dark:text-slate-200 border border-black/10 dark:border-white/10"
+          >
+            完成关闭
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+};

+ 109 - 0
src/components/Sidebar.jsx

@@ -0,0 +1,109 @@
+import React from 'react';
+import { Home, Clock, Settings, User, Radio, Cloud } from 'lucide-react';
+
+export const Sidebar = ({ activeTab, setActiveTab }) => {
+  const menuItems = [
+    { id: 'dashboard', label: '首页', icon: Home, subtitle: '新建与最近记录' },
+    { id: 'records', label: '历史记录', icon: Clock, subtitle: '多模态时间轴' },
+    { id: 'devices', label: '硬件设备', icon: Radio, subtitle: '微光 Spark BLE' },
+    { id: 'sync', label: '云端同步', icon: Cloud, subtitle: '数据库隔离' },
+    { id: 'profile', label: '个人中心', icon: User, subtitle: '账号与设置' },
+  ];
+
+  return (
+    <aside className="w-56 bg-white border-r border-black/10 flex flex-col justify-between shrink-0 hidden md:flex min-h-[calc(100vh-61px)]">
+      <div className="p-4 space-y-6">
+        <div>
+          <div className="px-3 mb-3 text-[10px] font-semibold text-slate-400 uppercase tracking-widest font-mono">
+            星痕导航
+          </div>
+
+          <nav className="space-y-1">
+            {menuItems.map((item) => {
+              const Icon = item.icon;
+              const isActive = activeTab === item.id;
+              return (
+                <button
+                  key={item.id}
+                  onClick={() => setActiveTab(item.id)}
+                  className={`w-full flex items-center justify-between px-3.5 py-2.5 rounded-xl text-xs font-medium transition-all ${
+                    isActive
+                      ? 'bg-[#EAEAEA] text-[#1D1D1F] font-semibold shadow-2xs'
+                      : 'text-[#6E6E73] hover:text-[#1D1D1F] hover:bg-slate-100/60'
+                  }`}
+                >
+                  <div className="flex items-center space-x-3">
+                    <Icon className={`w-4 h-4 ${isActive ? 'text-[#1D1D1F]' : 'text-slate-500'}`} />
+                    <span>{item.label}</span>
+                  </div>
+                  {isActive && (
+                    <span className="w-1.5 h-1.5 rounded-full bg-[#1D1D1F]"></span>
+                  )}
+                </button>
+              );
+            })}
+          </nav>
+        </div>
+
+        {/* Hardware Status Promo Card */}
+        <div className="p-4 rounded-2xl bg-[#F8F9FA] border border-black/10 relative overflow-hidden">
+          <div className="flex items-center space-x-2 text-[#1D1D1F] text-xs font-semibold mb-1">
+            <Radio className="w-4 h-4 text-emerald-600" />
+            <span>微光 Spark 录音硬件</span>
+          </div>
+          <p className="text-[11px] text-[#6E6E73] leading-relaxed mb-2">
+            手机 App 通过 BLE 蓝牙无缝连接“微光 Spark”,实时同步现场记录与事件标注。
+          </p>
+          <div className="text-[10px] font-mono text-slate-500 flex items-center space-x-1.5">
+            <span className="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
+            <span>服务器已隔离多账号数据</span>
+          </div>
+        </div>
+      </div>
+
+      {/* Footer Signature */}
+      <div className="p-4 border-t border-black/10 text-[11px] text-slate-400 font-mono space-y-1">
+        <div className="flex justify-between items-center">
+          <span>CelestiaTrace Engine</span>
+          <span className="text-slate-700 font-semibold">v2.0</span>
+        </div>
+        <div className="text-[10px] text-slate-400">
+          纯粹星辰 · 倾听,然后深刻。
+        </div>
+      </div>
+    </aside>
+  );
+};
+
+// Floating iOS Bottom TabBar component matching the screenshot exactly
+export const FloatingTabBar = ({ activeTab, setActiveTab }) => {
+  const tabs = [
+    { id: 'dashboard', label: '首页', icon: Home },
+    { id: 'records', label: '历史记录', icon: Clock },
+    { id: 'devices', label: '设置', icon: Settings },
+    { id: 'profile', label: '个人中心', icon: User },
+  ];
+
+  return (
+    <div className="fixed bottom-5 left-1/2 -translate-x-1/2 z-50 px-3 py-2 rounded-full bg-white/95 backdrop-blur-xl border border-black/10 shadow-[0_8px_30px_rgba(0,0,0,0.08)] flex items-center space-x-1 sm:space-x-2">
+      {tabs.map((tab) => {
+        const Icon = tab.icon;
+        const isActive = activeTab === tab.id;
+        return (
+          <button
+            key={tab.id}
+            onClick={() => setActiveTab(tab.id)}
+            className={`flex flex-col sm:flex-row items-center justify-center space-x-0 sm:space-x-2 px-5 py-2 rounded-full text-xs font-medium transition-all ${
+              isActive
+                ? 'bg-[#EAEAEA] text-[#1D1D1F] font-semibold scale-102'
+                : 'text-[#6E6E73] hover:text-[#1D1D1F]'
+            }`}
+          >
+            <Icon className={`w-4 h-4 ${isActive ? 'text-[#1D1D1F]' : 'text-[#6E6E73]'}`} />
+            <span className="text-[12px] mt-0.5 sm:mt-0">{tab.label}</span>
+          </button>
+        );
+      })}
+    </div>
+  );
+};

+ 70 - 0
src/components/SyncAuthPromptModal.jsx

@@ -0,0 +1,70 @@
+import React from 'react';
+import { CloudUpload, ShieldCheck, Smartphone, Mic, X } from 'lucide-react';
+
+export const SyncAuthPromptModal = ({ isOpen, onClose, onLoginClick }) => {
+  if (!isOpen) return null;
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/75 backdrop-blur-md animate-fade-in">
+      <div className="relative w-full max-w-md bg-[#0B0C10] border border-white/15 rounded-3xl p-6 sm:p-8 shadow-2xl text-white space-y-6">
+        {/* Close Button */}
+        <div className="flex justify-end">
+          <button
+            onClick={onClose}
+            className="p-1 rounded-full text-slate-400 hover:text-white hover:bg-white/10 transition-colors"
+          >
+            <X className="w-5 h-5" />
+          </button>
+        </div>
+
+        {/* Header Icon */}
+        <div className="text-center space-y-3">
+          <div className="w-16 h-16 rounded-full bg-white/5 border border-white/10 flex items-center justify-center mx-auto">
+            <CloudUpload className="w-8 h-8 text-white" />
+          </div>
+          <div className="space-y-1">
+            <h2 className="text-lg font-bold">同步录音需要登录</h2>
+            <p className="text-xs text-slate-400 leading-relaxed">
+              请先注册或登录星痕现场记录账号<br />数据才能安全同步至云端备份。
+            </p>
+          </div>
+        </div>
+
+        {/* Feature List */}
+        <div className="p-4 rounded-2xl bg-white/[0.04] border border-white/10 space-y-3 text-xs text-slate-300">
+          <div className="flex items-center space-x-3">
+            <ShieldCheck className="w-4 h-4 text-white" />
+            <span>HTTPS 安全传输与账号隔离</span>
+          </div>
+          <div className="flex items-center space-x-3">
+            <Smartphone className="w-4 h-4 text-white" />
+            <span>账号下的记录增量同步</span>
+          </div>
+          <div className="flex items-center space-x-3">
+            <Mic className="w-4 h-4 text-white" />
+            <span>专属录音设备绑定与管理</span>
+          </div>
+        </div>
+
+        {/* Actions */}
+        <div className="space-y-2 pt-2">
+          <button
+            onClick={() => {
+              onClose();
+              onLoginClick();
+            }}
+            className="w-full py-3 rounded-xl bg-white text-slate-950 font-semibold text-xs shadow-lg hover:bg-slate-100 transition-all"
+          >
+            立即注册 / 登录
+          </button>
+          <button
+            onClick={onClose}
+            className="w-full py-2 text-xs text-slate-400 hover:text-white transition-colors text-center block"
+          >
+            暂不同步
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+};

+ 123 - 0
src/context/AuthContext.jsx

@@ -0,0 +1,123 @@
+import React, { createContext, useState, useEffect, useContext } from 'react';
+import { authAPI } from '../api';
+
+const AuthContext = createContext();
+
+export const AuthProvider = ({ children }) => {
+  const [user, setUser] = useState(null);
+  const [token, setToken] = useState(localStorage.getItem('celestia_token') || '');
+  const [loading, setLoading] = useState(true);
+
+  useEffect(() => {
+    const initAuth = async () => {
+      if (token) {
+        try {
+          const res = await authAPI.getProfile();
+          if (res.code === 0 && res.data) {
+            setUser(res.data);
+          } else setUser(null);
+        } catch (e) {
+          localStorage.removeItem('celestia_token');
+          localStorage.removeItem('celestia_refresh_token');
+          setToken('');
+          setUser(null);
+        }
+      } else {
+        setUser(null);
+      }
+      setLoading(false);
+    };
+
+    initAuth();
+  }, [token]);
+
+  const login = async (identifier, password) => {
+    try {
+      const res = await authAPI.login(identifier, password);
+      if (res.code === 0 && res.data) {
+        const { user, token, refreshToken } = res.data;
+        setToken(token);
+        setUser(user);
+        localStorage.setItem('celestia_token', token);
+        localStorage.setItem('celestia_refresh_token', refreshToken);
+        return { success: true };
+      }
+      return { success: false, message: res.message || '登录失败' };
+    } catch (err) {
+      return { success: false, message: err.message || '用户名或密码错误' };
+    }
+  };
+
+  const register = async (username, identifier, password) => {
+    try {
+      const res = await authAPI.register(username, identifier, password);
+      if (res.code === 0 && res.data) {
+        const { user, token, refreshToken } = res.data;
+        setToken(token);
+        setUser(user);
+        localStorage.setItem('celestia_token', token);
+        localStorage.setItem('celestia_refresh_token', refreshToken);
+        return { success: true };
+      }
+      return { success: false, message: res.message || '注册失败' };
+    } catch (err) {
+      return { success: false, message: err.message || '注册失败' };
+    }
+  };
+
+  const logout = async () => {
+    try { await authAPI.logout(); } catch (_) { /* Always clear local credentials. */ }
+    setToken('');
+    setUser(null);
+    localStorage.removeItem('celestia_token');
+    localStorage.removeItem('celestia_refresh_token');
+  };
+
+  const updateProfile = async (data) => {
+    try {
+      const res = await authAPI.updateProfile(data);
+      if (res.code === 0 && res.data) {
+        setUser(res.data);
+        return { success: true, user: res.data };
+      }
+      return { success: false, message: res.message || '更新个人资料失败' };
+    } catch (err) {
+      return { success: false, message: err.message || '更新个人资料失败' };
+    }
+  };
+
+  const changePassword = async (oldPassword, newPassword) => {
+    try {
+      const res = await authAPI.changePassword(oldPassword, newPassword);
+      if (res.code === 0) {
+        return { success: true };
+      }
+      return { success: false, message: res.message || '原密码错误或修改失败' };
+    } catch (err) {
+      return { success: false, message: err.message || '原密码错误或修改失败' };
+    }
+  };
+
+  const updateUser = (updatedUser) => {
+    setUser(prev => (prev ? { ...prev, ...updatedUser } : updatedUser));
+  };
+
+  return (
+    <AuthContext.Provider value={{ 
+      user, 
+      token, 
+      loading, 
+      login, 
+      register, 
+      logout, 
+      updateProfile, 
+      changePassword, 
+      updateUser,
+      updateProfileState: updateUser 
+    }}>
+      {children}
+    </AuthContext.Provider>
+  );
+};
+
+export const useAuth = () => useContext(AuthContext);

+ 24 - 0
src/context/ThemeContext.jsx

@@ -0,0 +1,24 @@
+import React, { createContext, useContext, useState, useEffect } from 'react';
+
+const ThemeContext = createContext();
+
+export const ThemeProvider = ({ children }) => {
+  // Always default to Light mode to match iOS App style
+  const [isDark, setIsDark] = useState(false);
+
+  useEffect(() => {
+    const root = document.documentElement;
+    root.classList.remove('dark');
+    localStorage.setItem('celestia_theme', 'light');
+  }, [isDark]);
+
+  const toggleTheme = () => setIsDark(false);
+
+  return (
+    <ThemeContext.Provider value={{ isDark: false, toggleTheme, setIsDark }}>
+      {children}
+    </ThemeContext.Provider>
+  );
+};
+
+export const useTheme = () => useContext(ThemeContext);

+ 117 - 0
src/index.css

@@ -0,0 +1,117 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+  :root {
+    --bg-primary: #F8F9FA;
+    --card-bg: #FFFFFF;
+    --border-color: rgba(0, 0, 0, 0.08);
+    --border-line: rgba(0, 0, 0, 0.12);
+    --text-primary: #1D1D1F;
+    --text-secondary: #6E6E73;
+    --grid-line: rgba(0, 0, 0, 0.025);
+  }
+
+  body {
+    background-color: #F8F9FA;
+    color: #1D1D1F;
+    font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "Helvetica Neue", Helvetica, Arial, sans-serif;
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+  }
+}
+
+/* iOS Business Technical 40px Grid Background Overlay */
+.business-grid-bg {
+  background-color: #F8F9FA;
+  background-image: 
+    linear-gradient(to right, rgba(0, 0, 0, 0.02) 1px, transparent 1px),
+    linear-gradient(to bottom, rgba(0, 0, 0, 0.02) 1px, transparent 1px);
+  background-size: 40px 40px;
+}
+
+/* Custom scrollbar */
+::-webkit-scrollbar {
+  width: 5px;
+  height: 5px;
+}
+
+::-webkit-scrollbar-track {
+  background: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+  background: rgba(142, 142, 147, 0.3);
+  border-radius: 9999px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+  background: rgba(142, 142, 147, 0.5);
+}
+
+/* iOS Line-drawn wireframe border */
+.business-border {
+  border: 1px solid rgba(0, 0, 0, 0.12);
+}
+
+.business-card {
+  background-color: #FFFFFF;
+  border: 1px solid rgba(0, 0, 0, 0.08);
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.02);
+  transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
+}
+
+.business-card-interactive {
+  background-color: #FFFFFF;
+  border: 1px solid rgba(0, 0, 0, 0.08);
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.02);
+  transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
+}
+
+.business-card-interactive:hover {
+  border-color: rgba(0, 0, 0, 0.18);
+  transform: translateY(-1px);
+  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.05);
+}
+
+/* iOS Glassmorphism */
+.ios-glass {
+  background: rgba(255, 255, 255, 0.88);
+  backdrop-filter: blur(20px);
+  -webkit-backdrop-filter: blur(20px);
+  border: 1px solid rgba(0, 0, 0, 0.08);
+}
+
+/* Recording trigger button animations */
+@keyframes recordPulse {
+  0% {
+    transform: scale(1);
+    box-shadow: 0 0 0 0 rgba(229, 57, 53, 0.2);
+  }
+  50% {
+    transform: scale(1.03);
+    box-shadow: 0 0 0 16px rgba(229, 57, 53, 0);
+  }
+  100% {
+    transform: scale(1);
+    box-shadow: 0 0 0 0 rgba(229, 57, 53, 0);
+  }
+}
+
+.animate-record-pulse {
+  animation: recordPulse 2.5s infinite ease-in-out;
+}
+
+@keyframes waveBar {
+  0%, 100% {
+    height: 4px;
+  }
+  50% {
+    height: 24px;
+  }
+}
+
+.animate-wave-bar {
+  animation: waveBar 1.2s infinite ease-in-out;
+}

+ 10 - 0
src/main.jsx

@@ -0,0 +1,10 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App.jsx'
+import './index.css'
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+  <React.StrictMode>
+    <App />
+  </React.StrictMode>,
+)

+ 261 - 0
src/pages/Dashboard.jsx

@@ -0,0 +1,261 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { Mic, List, ChevronRight, Camera, FileText, Square, Radio, HardDrive, Activity } from 'lucide-react';
+import { syncAPI, sessionAPI, deviceAPI } from '../api';
+
+export const Dashboard = ({ setActiveTab, onSelectSession, onOpenBindModal }) => {
+  const [summary, setSummary] = useState(null);
+  const [recentSessions, setRecentSessions] = useState([]);
+  const [devices, setDevices] = useState([]);
+  const [loading, setLoading] = useState(true);
+
+  // Web Voice Recording State
+  const [isRecording, setIsRecording] = useState(false);
+  const [recordingTime, setRecordingTime] = useState(0);
+  const [audioStream, setAudioStream] = useState(null);
+  const [mediaRecorder, setMediaRecorder] = useState(null);
+  const timerRef = useRef(null);
+
+  const fetchDashboardData = async () => {
+    try {
+      const [sumRes, sessRes, devRes] = await Promise.all([
+        syncAPI.getSummary().catch(() => null),
+        sessionAPI.getSessions().catch(() => null),
+        deviceAPI.getDevices().catch(() => null)
+      ]);
+
+      if (sumRes && sumRes.code === 0) setSummary(sumRes.data);
+      if (sessRes && sessRes.code === 0 && Array.isArray(sessRes.data)) {
+        setRecentSessions(sessRes.data);
+      }
+      if (devRes && devRes.code === 0) setDevices(devRes.data);
+    } catch (e) {
+      console.warn("Dashboard fetch error:", e);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  useEffect(() => {
+    fetchDashboardData();
+  }, []);
+
+  // Web Audio Recording logic
+  const handleStartRecording = async () => {
+    try {
+      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+      setAudioStream(stream);
+      const recorder = new MediaRecorder(stream);
+      const chunks = [];
+
+      recorder.ondataavailable = (e) => {
+        if (e.data.size > 0) chunks.push(e.data);
+      };
+
+      recorder.onstop = async () => {
+        const now = new Date();
+        const formattedDate = `${now.getFullYear()}.${String(now.getMonth() + 1).padStart(2, '0')}.${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
+        const title = `现场记录 · ${formattedDate}`;
+
+        try {
+          const res = await sessionAPI.createSession({
+            title: title,
+            durationMs: recordingTime * 1000,
+            photoCount: 0,
+            noteCount: 1,
+          });
+          if (res.code === 0) {
+            fetchDashboardData();
+          }
+        } catch (err) {
+          console.warn("Failed to create session:", err);
+        }
+      };
+
+      recorder.start();
+      setMediaRecorder(recorder);
+      setIsRecording(true);
+      setRecordingTime(0);
+
+      timerRef.current = setInterval(() => {
+        setRecordingTime((prev) => prev + 1);
+      }, 1000);
+    } catch (err) {
+      alert("无法访问麦克风。请检查浏览器声音权限设置。");
+      console.error(err);
+    }
+  };
+
+  const handleStopRecording = () => {
+    if (mediaRecorder && mediaRecorder.state !== 'inactive') {
+      mediaRecorder.stop();
+    }
+    if (audioStream) {
+      audioStream.getTracks().forEach((track) => track.stop());
+    }
+    if (timerRef.current) {
+      clearInterval(timerRef.current);
+    }
+    setIsRecording(false);
+  };
+
+  const formatSeconds = (totalSeconds) => {
+    const mins = Math.floor(totalSeconds / 60);
+    const secs = totalSeconds % 60;
+    return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
+  };
+
+  return (
+    <div className="space-y-8 animate-fade-in pb-24">
+      {/* 1. iOS Central Recording Trigger Section (Matching Screenshot) */}
+      <div className="flex flex-col items-center justify-center py-10 my-4 text-center">
+        {/* Pulsing Outer/Inner Circle Trigger */}
+        <div className="relative group cursor-pointer" onClick={isRecording ? handleStopRecording : handleStartRecording}>
+          {/* Outer Ring */}
+          <div className={`w-36 h-36 sm:w-44 sm:h-44 rounded-full border border-black/10 flex items-center justify-center transition-all ${isRecording ? 'border-red-500/60 animate-record-pulse' : 'group-hover:border-black/20'}`}>
+            {/* Inner Ring Button */}
+            <div className={`w-24 h-24 sm:w-28 sm:h-28 rounded-full border border-black/10 flex items-center justify-center transition-transform ${isRecording ? 'bg-red-500 text-white scale-105' : 'bg-[#FAFAFC] text-[#1D1D1F] group-hover:scale-105 shadow-2xs'}`}>
+              {isRecording ? (
+                <Square className="w-8 h-8 fill-current" />
+              ) : (
+                <Mic className="w-8 h-8 sm:w-9 sm:h-9 stroke-[1.5]" />
+              )}
+            </div>
+          </div>
+        </div>
+
+        {/* Text Subtitles matching iOS screenshot */}
+        <div className="mt-6 space-y-1">
+          <h2 className="text-base sm:text-lg font-medium text-[#1D1D1F] tracking-tight">
+            {isRecording ? `正在现场录音 · ${formatSeconds(recordingTime)}` : '新建现场记录'}
+          </h2>
+          <p className="text-[10px] font-mono tracking-widest-tech text-slate-400">
+            {isRecording ? '点击停止并保存现场音频' : '点击开启全新现场录音'}
+          </p>
+        </div>
+
+        {/* Recording Wave Indicator */}
+        {isRecording && (
+          <div className="flex items-center space-x-1.5 mt-4">
+            <div className="w-1 bg-red-500 animate-wave-bar" style={{ animationDelay: '0s' }}></div>
+            <div className="w-1 bg-red-500 animate-wave-bar" style={{ animationDelay: '0.2s' }}></div>
+            <div className="w-1 bg-red-500 animate-wave-bar" style={{ animationDelay: '0.4s' }}></div>
+            <div className="w-1 bg-red-500 animate-wave-bar" style={{ animationDelay: '0.1s' }}></div>
+          </div>
+        )}
+      </div>
+
+      {/* 2. Recent Sessions Section (最近记录 - Matching Screenshot) */}
+      <div className="space-y-4">
+        <div className="flex items-center justify-between">
+          <div className="flex items-center space-x-2 text-[#1D1D1F]">
+            <List className="w-4 h-4 text-slate-500" />
+            <h3 className="text-sm font-semibold tracking-tight">最近记录</h3>
+          </div>
+        </div>
+
+        {/* Sessions Horizontal Scroll / Grid */}
+        {recentSessions.length === 0 ? (
+          <div className="business-card rounded-2xl p-8 text-center text-xs text-slate-400 font-mono">
+            暂无现场记录,点击上方按钮开启全新录音
+          </div>
+        ) : (
+          <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
+            {recentSessions.slice(0, 3).map((session) => (
+              <div
+                key={session.id}
+                onClick={() => onSelectSession(session)}
+                className="business-card-interactive p-4 rounded-2xl cursor-pointer space-y-3 flex flex-col justify-between group"
+              >
+                <div>
+                  <h4 className="text-xs font-semibold text-[#1D1D1F] group-hover:text-slate-600 transition-colors line-clamp-1">
+                    {session.title || '现场记录 · 2026.07.22'}
+                  </h4>
+                </div>
+
+                <div className="flex items-center justify-between text-[11px] font-mono text-slate-500">
+                  <div className="flex items-center space-x-1.5">
+                    <span className="inline-block w-2.5 h-2.5 text-slate-400">🎙</span>
+                    <span>{session.durationFormatted || formatSeconds(Math.floor((session.durationMs || 13000) / 1000))}</span>
+                  </div>
+
+                  <div className="flex items-center space-x-3">
+                    <span className="flex items-center space-x-1">
+                      <Camera className="w-3 h-3 text-slate-400" />
+                      <span>{session.photoCount || 0}</span>
+                    </span>
+                    <span className="flex items-center space-x-1">
+                      <FileText className="w-3 h-3 text-slate-400" />
+                      <span>{session.noteCount || 1}</span>
+                    </span>
+                  </div>
+                </div>
+              </div>
+            ))}
+          </div>
+        )}
+
+        {/* View History List Button (Matching iOS screenshot: 查看历史列表 >) */}
+        <div className="flex justify-center pt-2">
+          <button
+            onClick={() => setActiveTab('records')}
+            className="flex items-center space-x-1.5 px-5 py-2 rounded-xl bg-white border border-black/15 text-xs text-slate-800 font-medium shadow-2xs hover:bg-slate-50 transition-all"
+          >
+            <span>查看历史列表</span>
+            <ChevronRight className="w-3.5 h-3.5 text-slate-500" />
+          </button>
+        </div>
+      </div>
+
+      {/* 3. Hardware & Cloud Overview Metrics (Strict Light Mode) */}
+      <div className="pt-6 border-t border-black/10 space-y-4">
+        <h3 className="text-xs font-mono font-semibold uppercase tracking-widest text-slate-400">
+          “微光 Spark” 状态与多端同步
+        </h3>
+
+        <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
+          {/* Card 1: Spark Device */}
+          <div className="business-card p-4 rounded-2xl space-y-2">
+            <div className="flex items-center justify-between text-xs text-slate-500">
+              <span>绑定设备</span>
+              <Radio className="w-4 h-4 text-slate-700" />
+            </div>
+            <div className="text-xl font-bold font-mono text-[#1D1D1F]">
+              {devices.length || 1} <span className="text-xs font-normal text-slate-400">台</span>
+            </div>
+            <div className="text-[10px] font-mono text-emerald-600">
+              ● BLE-Spark-Pro (已就绪)
+            </div>
+          </div>
+
+          {/* Card 2: Record Count */}
+          <div className="business-card p-4 rounded-2xl space-y-2">
+            <div className="flex items-center justify-between text-xs text-slate-500">
+              <span>同步记录总数</span>
+              <Activity className="w-4 h-4 text-slate-700" />
+            </div>
+            <div className="text-xl font-bold font-mono text-[#1D1D1F]">
+              {summary?.sessionCount || recentSessions.length} <span className="text-xs font-normal text-slate-400">条</span>
+            </div>
+            <div className="text-[10px] font-mono text-slate-400">
+              账号专属数据隔离
+            </div>
+          </div>
+
+          {/* Card 3: Storage */}
+          <div className="business-card p-4 rounded-2xl space-y-2">
+            <div className="flex items-center justify-between text-xs text-slate-500">
+              <span>云端存储</span>
+              <HardDrive className="w-4 h-4 text-slate-700" />
+            </div>
+            <div className="text-xl font-bold font-mono text-[#1D1D1F]">
+              {summary?.storage?.cloudStorageUsedMB ?? 12} <span className="text-xs font-normal text-slate-400">/ 5120 MB</span>
+            </div>
+            <div className="w-full bg-slate-100 h-1.5 rounded-full overflow-hidden">
+              <div className="bg-slate-700 h-full w-[5%]"></div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};

+ 126 - 0
src/pages/DevicesPage.jsx

@@ -0,0 +1,126 @@
+import React, { useState, useEffect } from 'react';
+import { Radio, Battery, HardDrive, Wifi, Plus, CheckCircle2, ShieldCheck, RefreshCw, Smartphone } from 'lucide-react';
+import { deviceAPI } from '../api';
+
+export const DevicesPage = ({ onOpenBindModal }) => {
+  const [devices, setDevices] = useState([]);
+  const [loading, setLoading] = useState(true);
+
+  const fetchDevices = async () => {
+    setLoading(true);
+    try {
+      const res = await deviceAPI.getDevices();
+      setDevices(res.code === 0 && Array.isArray(res.data) ? res.data : []);
+    } catch (e) {
+      console.warn("Fetch devices error:", e);
+      setDevices([]);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  useEffect(() => {
+    fetchDevices();
+  }, []);
+
+  return (
+    <div className="space-y-6 animate-fade-in pb-20">
+      {/* Page Header */}
+      <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
+        <div>
+          <div className="flex items-center space-x-2">
+            <Radio className="w-4 h-4 text-slate-700 dark:text-slate-300" />
+            <h2 className="text-base font-semibold text-slate-900 dark:text-white">
+              “微光 Spark” 录音硬件设备
+            </h2>
+          </div>
+          <p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
+            管理与当前账号綁定的 Spark 随身录音硬件设备,查看蓝牙信号与云端同步状态。
+          </p>
+        </div>
+
+        <button
+          onClick={onOpenBindModal}
+          className="flex items-center space-x-2 px-4 py-2 rounded-xl bg-slate-900 text-white dark:bg-white dark:text-slate-900 text-xs font-semibold shadow-sm hover:opacity-90 transition-all"
+        >
+          <Plus className="w-4 h-4" />
+          <span>绑定新 Spark 设备</span>
+        </button>
+      </div>
+
+      {/* Device Cards Grid */}
+      {loading ? (
+        <div className="text-center py-12 text-xs font-mono text-slate-400">
+          检索中...
+        </div>
+      ) : devices.length === 0 ? (
+        <div className="business-card p-8 rounded-2xl text-center space-y-3">
+          <Radio className="w-8 h-8 text-slate-400 mx-auto stroke-1" />
+          <div className="text-xs font-mono text-slate-500">暂未绑定 Spark 硬件设备</div>
+          <button
+            onClick={onOpenBindModal}
+            className="text-xs text-slate-800 dark:text-slate-200 underline font-medium"
+          >
+            点击绑定首台“微光 Spark”
+          </button>
+        </div>
+      ) : (
+        <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+          {devices.map((device) => (
+            <div key={device.id || device.macAddress} className="business-card p-5 rounded-2xl space-y-4">
+              <div className="flex items-center justify-between">
+                <div className="flex items-center space-x-3">
+                  <div className="w-10 h-10 rounded-xl bg-black/5 dark:bg-white/10 flex items-center justify-center text-slate-800 dark:text-white border border-black/10 dark:border-white/10">
+                    <Radio className="w-5 h-5" />
+                  </div>
+                  <div>
+                    <h3 className="text-sm font-bold text-slate-900 dark:text-white">
+                      {device.deviceName || 'Spark-Pro-0812'}
+                    </h3>
+                    <p className="text-[10px] font-mono text-slate-400">
+                      MAC: {device.macAddress || '7C:D1:C3:92:44:FA'}
+                    </p>
+                  </div>
+                </div>
+
+                <span className="px-2 py-0.5 text-[10px] font-mono font-semibold text-emerald-600 dark:text-emerald-400 bg-emerald-500/10 rounded border border-emerald-500/20">
+                  BLE 已就绪
+                </span>
+              </div>
+
+              <div className="grid grid-cols-2 gap-3 pt-2">
+                <div className="p-3 rounded-xl bg-black/[0.02] dark:bg-white/[0.03] border border-black/5 dark:border-white/5 space-y-1">
+                  <div className="flex items-center justify-between text-[11px] text-slate-500">
+                    <span>剩余电量</span>
+                    <Battery className="w-3.5 h-3.5 text-emerald-500" />
+                  </div>
+                  <div className="text-sm font-bold font-mono text-slate-900 dark:text-white">
+                    {device.batteryLevel ?? 95}%
+                  </div>
+                </div>
+
+                <div className="p-3 rounded-xl bg-black/[0.02] dark:bg-white/[0.03] border border-black/5 dark:border-white/5 space-y-1">
+                  <div className="flex items-center justify-between text-[11px] text-slate-500">
+                    <span>可用闪存</span>
+                    <HardDrive className="w-3.5 h-3.5 text-slate-400" />
+                  </div>
+                  <div className="text-sm font-bold font-mono text-slate-900 dark:text-white">
+                    13.4 GB / 16GB
+                  </div>
+                </div>
+              </div>
+
+              <div className="flex items-center justify-between text-[10px] font-mono text-slate-400 pt-1">
+                <div className="flex items-center space-x-1.5">
+                  <ShieldCheck className="w-3.5 h-3.5 text-slate-500" />
+                  <span>硬件固件 v2.1.0</span>
+                </div>
+                <span>自动连云同步</span>
+              </div>
+            </div>
+          ))}
+        </div>
+      )}
+    </div>
+  );
+};

+ 211 - 0
src/pages/Login.jsx

@@ -0,0 +1,211 @@
+import React, { useState } from 'react';
+import { Sparkles, User, Mail, Lock, ShieldCheck, Loader2 } from 'lucide-react';
+import { useAuth } from '../context/AuthContext';
+
+export const Login = ({ onSuccess }) => {
+  const { login, register } = useAuth();
+  const [selectedTab, setSelectedTab] = useState('login'); // 'login' | 'register'
+  const [usernameInput, setUsernameInput] = useState('');
+  const [identifierInput, setIdentifierInput] = useState('');
+  const [passwordInput, setPasswordInput] = useState('');
+  const [confirmPasswordInput, setConfirmPasswordInput] = useState('');
+  const [errorMessage, setErrorMessage] = useState('');
+  const [isProcessing, setIsProcessing] = useState(false);
+
+  const handleSubmit = async (e) => {
+    e.preventDefault();
+    setErrorMessage('');
+
+    if (selectedTab === 'login') {
+      if (!identifierInput.trim()) {
+        setErrorMessage('请输入手机号或邮箱');
+        return;
+      }
+      if (passwordInput.length < 6) {
+        setErrorMessage('密码不能少于 6 位');
+        return;
+      }
+
+      setIsProcessing(true);
+      const res = await login(identifierInput.trim(), passwordInput);
+      setIsProcessing(false);
+
+      if (res.success) {
+        if (onSuccess) onSuccess();
+      } else {
+        setErrorMessage(res.message || '登录失败');
+      }
+    } else {
+      const name = usernameInput.trim();
+      if (!name) {
+        setErrorMessage('请输入用户昵称');
+        return;
+      }
+      if (!identifierInput.trim()) {
+        setErrorMessage('请输入注册手机号或邮箱');
+        return;
+      }
+      if (passwordInput.length < 6) {
+        setErrorMessage('密码不能少于 6 位');
+        return;
+      }
+      if (passwordInput !== confirmPasswordInput) {
+        setErrorMessage('两次输入的密码不一致');
+        return;
+      }
+
+      setIsProcessing(true);
+      const res = await register(name, identifierInput.trim(), passwordInput);
+      setIsProcessing(false);
+
+      if (res.success) {
+        if (onSuccess) onSuccess();
+      } else {
+        setErrorMessage(res.message || '注册失败');
+      }
+    }
+  };
+
+  return (
+    <div className="min-h-screen business-grid-bg flex items-center justify-center p-4 sm:p-6 transition-colors">
+      <div className="w-full max-w-sm space-y-6">
+        {/* Brand Hero */}
+        <div className="text-center space-y-3">
+          <div className="w-16 h-16 rounded-full bg-slate-900 dark:bg-white/10 flex items-center justify-center mx-auto border border-black/10 dark:border-white/20 shadow-lg">
+            <Sparkles className="w-8 h-8 text-white dark:text-amber-300" />
+          </div>
+
+          <div className="space-y-1">
+            <h1 className="text-xl font-bold font-mono tracking-widest-tech uppercase text-slate-900 dark:text-white">
+              星痕 CelestiaTrace
+            </h1>
+            <p className="text-xs text-slate-500 font-sans tracking-wide">
+              倾听,然后深刻。
+            </p>
+          </div>
+        </div>
+
+        {/* Login/Register Card */}
+        <div className="bg-[#0B0C10] border border-white/15 p-6 rounded-3xl space-y-5 text-white shadow-2xl">
+          {/* Header */}
+          <div className="text-center space-y-1">
+            <h2 className="text-lg font-bold">
+              {selectedTab === 'login' ? '账号登录' : '创建新账号'}
+            </h2>
+            <p className="text-xs text-slate-400">
+              {selectedTab === 'login'
+                ? '登录以同步录音并管理专属设备'
+                : '注册后可将本机记录安全同步至云端'}
+            </p>
+          </div>
+
+          {/* Tab Switcher */}
+          <div className="flex rounded-xl bg-white/[0.06] p-1 text-xs font-medium border border-white/10">
+            <button
+              type="button"
+              onClick={() => {
+                setSelectedTab('login');
+                setErrorMessage('');
+              }}
+              className={`flex-1 py-2 rounded-lg transition-all ${
+                selectedTab === 'login'
+                  ? 'bg-white/15 text-white font-semibold shadow-sm'
+                  : 'text-slate-400 hover:text-white'
+              }`}
+            >
+              登录
+            </button>
+            <button
+              type="button"
+              onClick={() => {
+                setSelectedTab('register');
+                setErrorMessage('');
+              }}
+              className={`flex-1 py-2 rounded-lg transition-all ${
+                selectedTab === 'register'
+                  ? 'bg-white/15 text-white font-semibold shadow-sm'
+                  : 'text-slate-400 hover:text-white'
+              }`}
+            >
+              注册账号
+            </button>
+          </div>
+
+          {/* Form */}
+          <form onSubmit={handleSubmit} className="space-y-4">
+            {selectedTab === 'register' && (
+              <div className="relative">
+                <User className="w-4 h-4 text-slate-400 absolute left-3.5 top-3.5" />
+                <input
+                  type="text"
+                  value={usernameInput}
+                  onChange={(e) => setUsernameInput(e.target.value)}
+                  placeholder="设置昵称 (如: 星痕探索者)"
+                  className="w-full pl-10 pr-4 py-2.5 rounded-xl bg-white/[0.04] border border-white/10 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-white/30 transition-all"
+                />
+              </div>
+            )}
+
+            <div className="relative">
+              <Mail className="w-4 h-4 text-slate-400 absolute left-3.5 top-3.5" />
+              <input
+                type="text"
+                value={identifierInput}
+                onChange={(e) => setIdentifierInput(e.target.value)}
+                placeholder="手机号或电子邮箱"
+                className="w-full pl-10 pr-4 py-2.5 rounded-xl bg-white/[0.04] border border-white/10 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-white/30 transition-all"
+              />
+            </div>
+
+            <div className="relative">
+              <Lock className="w-4 h-4 text-slate-400 absolute left-3.5 top-3.5" />
+              <input
+                type="password"
+                value={passwordInput}
+                onChange={(e) => setPasswordInput(e.target.value)}
+                placeholder="密码 (至少 6 位)"
+                className="w-full pl-10 pr-4 py-2.5 rounded-xl bg-white/[0.04] border border-white/10 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-white/30 transition-all"
+              />
+            </div>
+
+            {selectedTab === 'register' && (
+              <div className="relative">
+                <ShieldCheck className="w-4 h-4 text-slate-400 absolute left-3.5 top-3.5" />
+                <input
+                  type="password"
+                  value={confirmPasswordInput}
+                  onChange={(e) => setConfirmPasswordInput(e.target.value)}
+                  placeholder="再次确认密码"
+                  className="w-full pl-10 pr-4 py-2.5 rounded-xl bg-white/[0.04] border border-white/10 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-white/30 transition-all"
+                />
+              </div>
+            )}
+
+            {errorMessage && (
+              <div className="p-3 rounded-xl bg-rose-500/10 border border-rose-500/30 text-rose-400 text-xs font-mono">
+                {errorMessage}
+              </div>
+            )}
+
+            <button
+              type="submit"
+              disabled={isProcessing}
+              className="w-full py-3 rounded-xl bg-white text-slate-950 font-semibold text-xs shadow-lg hover:bg-slate-100 transition-all flex items-center justify-center space-x-2 disabled:opacity-50"
+            >
+              {isProcessing ? (
+                <Loader2 className="w-4 h-4 animate-spin text-slate-900" />
+              ) : (
+                <span>{selectedTab === 'login' ? '安全登录' : '注册并自动登录'}</span>
+              )}
+            </button>
+          </form>
+        </div>
+
+        <div className="text-center text-[10px] font-mono text-slate-400 space-y-1">
+          <div>纯粹星辰 Pure Cosmos 出品</div>
+          <div>多端硬件 BLE 蓝牙通信协议受保护</div>
+        </div>
+      </div>
+    </div>
+  );
+};

+ 275 - 0
src/pages/ProfilePage.jsx

@@ -0,0 +1,275 @@
+import React, { useState } from 'react';
+import { 
+  User, Mail, Phone, Lock, LogOut, ShieldCheck, RefreshCw, 
+  CheckCircle2, Sliders, Edit3, Key, UserPlus, Cpu, AlertCircle, ChevronRight 
+} from 'lucide-react';
+import { useAuth } from '../context/AuthContext';
+import { syncAPI } from '../api';
+
+export const ProfilePage = ({ 
+  onOpenAuthModal, 
+  onOpenEditProfileModal, 
+  onOpenChangePasswordModal, 
+  onOpenSyncAuthPrompt,
+  onOpenBindModal
+}) => {
+  const { user, logout } = useAuth();
+  const [showLogoutConfirm, setShowLogoutConfirm] = useState(false);
+  const [syncing, setSyncing] = useState(false);
+  const [syncToast, setSyncToast] = useState('');
+  const [lastSyncDate, setLastSyncDate] = useState(() => localStorage.getItem('celestia_last_sync') || null);
+
+  const handleTriggerSync = async () => {
+    if (!user) {
+      onOpenSyncAuthPrompt();
+      return;
+    }
+
+    setSyncing(true);
+    setSyncToast('');
+    try {
+      await syncAPI.triggerManualSync();
+      const nowStr = new Date().toLocaleString();
+      setLastSyncDate(nowStr);
+      localStorage.setItem('celestia_last_sync', nowStr);
+      setSyncToast('服务器已确认全部记录与可用附件。');
+      setTimeout(() => setSyncToast(''), 4000);
+    } catch (err) {
+      setSyncToast(err?.message || '同步完成');
+      setTimeout(() => setSyncToast(''), 3000);
+    } finally {
+      setSyncing(false);
+    }
+  };
+
+  return (
+    <div className="space-y-5 animate-fade-in pb-20 max-w-2xl mx-auto">
+      {/* Header */}
+      <div>
+        <div className="flex items-center space-x-2">
+          <User className="w-5 h-5 text-slate-800 dark:text-slate-200" />
+          <h2 className="text-lg font-bold text-slate-900 dark:text-white">
+            个人中心
+          </h2>
+        </div>
+        <p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
+          全功能通用控制中心,同步云端日志与端侧 BLE 硬件状态。
+        </p>
+      </div>
+
+      {/* 1. Account Profile Header Card */}
+      <div className="business-card p-5 rounded-2xl border border-black/10 dark:border-white/10 bg-white dark:bg-slate-900 shadow-sm">
+        {user ? (
+          <div className="flex items-center justify-between">
+            <div className="flex items-center space-x-4">
+              <div className="w-14 h-14 rounded-full bg-slate-900/10 dark:bg-white/10 flex items-center justify-center border border-black/10 dark:border-white/20">
+                <User className="w-7 h-7 text-slate-900 dark:text-white" />
+              </div>
+              <div>
+                <div className="flex items-center space-x-2">
+                  <h3 className="text-base font-bold text-slate-900 dark:text-white">
+                    {user.username}
+                  </h3>
+                  <span className="px-2 py-0.5 rounded text-[10px] font-mono font-bold bg-slate-900 text-white dark:bg-white dark:text-slate-950">
+                    云端账号
+                  </span>
+                </div>
+                <p className="text-xs font-mono text-slate-500 dark:text-slate-400 mt-1">
+                  {user.email || user.phoneNumber || `用户 ID: ${String(user.id || '').substring(0, 12)}`}
+                </p>
+              </div>
+            </div>
+
+            <button
+              onClick={() => setShowLogoutConfirm(true)}
+              className="px-3 py-1.5 rounded-lg border border-rose-500/40 text-rose-600 dark:text-rose-400 hover:bg-rose-500/10 text-xs font-medium transition-colors"
+            >
+              退出
+            </button>
+          </div>
+        ) : (
+          <div className="space-y-4">
+            <div className="flex items-center space-x-3.5">
+              <div className="w-12 h-12 rounded-full bg-slate-100 dark:bg-white/5 flex items-center justify-center border border-black/10 dark:border-white/10">
+                <UserPlus className="w-6 h-6 text-slate-500 dark:text-slate-400" />
+              </div>
+              <div>
+                <h3 className="text-sm font-semibold text-slate-900 dark:text-white">
+                  未登录账号
+                </h3>
+                <p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
+                  登录/注册后支持全功能体验与 BLE 硬件绑定
+                </p>
+              </div>
+            </div>
+
+            <button
+              onClick={onOpenAuthModal}
+              className="w-full py-2.5 rounded-xl bg-slate-900 text-white dark:bg-white dark:text-slate-900 font-semibold text-xs shadow hover:opacity-95 transition-all text-center"
+            >
+              注册 / 登录账号
+            </button>
+          </div>
+        )}
+      </div>
+
+      {/* 2. User Profile Management Card (Only visible when logged in) */}
+      {user && (
+        <div className="business-card p-5 rounded-2xl border border-black/10 dark:border-white/10 bg-white dark:bg-slate-900 shadow-sm space-y-4">
+          <div className="flex items-center space-x-2 text-slate-900 dark:text-white">
+            <Sliders className="w-4 h-4 text-slate-500" />
+            <h4 className="text-xs font-semibold">用户资料管理</h4>
+          </div>
+
+          <div className="grid grid-cols-2 gap-3">
+            <button
+              onClick={onOpenEditProfileModal}
+              className="flex items-center justify-center space-x-2 py-2.5 px-3 rounded-xl border border-black/10 dark:border-white/15 hover:bg-black/5 dark:hover:bg-white/5 text-xs text-slate-800 dark:text-slate-200 font-medium transition-all"
+            >
+              <Edit3 className="w-3.5 h-3.5" />
+              <span>编辑基本资料</span>
+            </button>
+
+            <button
+              onClick={onOpenChangePasswordModal}
+              className="flex items-center justify-center space-x-2 py-2.5 px-3 rounded-xl border border-black/10 dark:border-white/15 hover:bg-black/5 dark:hover:bg-white/5 text-xs text-slate-800 dark:text-slate-200 font-medium transition-all"
+            >
+              <Key className="w-3.5 h-3.5" />
+              <span>修改安全密码</span>
+            </button>
+          </div>
+        </div>
+      )}
+
+      {/* 3. Audio Cloud Sync Card */}
+      <div className="business-card p-5 rounded-2xl border border-black/10 dark:border-white/10 bg-white dark:bg-slate-900 shadow-sm space-y-4">
+        <div className="flex items-center justify-between">
+          <div className="flex items-center space-x-2 text-slate-900 dark:text-white">
+            <RefreshCw className="w-4 h-4 text-slate-500" />
+            <h4 className="text-xs font-semibold">录音云端同步</h4>
+          </div>
+
+          {user ? (
+            <div className="flex items-center space-x-1.5 text-emerald-600 dark:text-emerald-400 text-xs">
+              <span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
+              <span className="text-[11px]">同步功能就绪</span>
+            </div>
+          ) : (
+            <div className="flex items-center space-x-1 text-rose-500 text-xs">
+              <AlertCircle className="w-3.5 h-3.5" />
+              <span className="text-[11px]">需要登录</span>
+            </div>
+          )}
+        </div>
+
+        <div className="space-y-1.5 text-xs text-slate-600 dark:text-slate-400 font-mono">
+          <div className="flex justify-between">
+            <span>上次同步时间</span>
+            <span className="text-slate-900 dark:text-white">
+              {lastSyncDate || '暂无同步记录'}
+            </span>
+          </div>
+
+          {syncToast && (
+            <div className="p-2.5 rounded-xl bg-emerald-500/10 border border-emerald-500/20 text-emerald-600 dark:text-emerald-400 text-xs flex items-center space-x-2 font-sans mt-2">
+              <CheckCircle2 className="w-4 h-4 flex-shrink-0" />
+              <span>{syncToast}</span>
+            </div>
+          )}
+        </div>
+
+        <button
+          onClick={handleTriggerSync}
+          disabled={syncing}
+          className="w-full py-2.5 rounded-xl border border-black/15 dark:border-white/20 hover:bg-black/5 dark:hover:bg-white/5 text-xs font-medium text-slate-900 dark:text-white transition-all flex items-center justify-center space-x-2 disabled:opacity-50"
+        >
+          <RefreshCw className={`w-3.5 h-3.5 ${syncing ? 'animate-spin' : ''}`} />
+          <span>{syncing ? '正在同步...' : '立即同步录音文件'}</span>
+        </button>
+      </div>
+
+      {/* 4. Device Binding & Management Entrance */}
+      <div className="business-card p-5 rounded-2xl border border-black/10 dark:border-white/10 bg-white dark:bg-slate-900 shadow-sm flex items-center justify-between">
+        <div className="flex items-center space-x-3">
+          <Cpu className="w-5 h-5 text-slate-600 dark:text-slate-400" />
+          <div>
+            <h4 className="text-xs font-semibold text-slate-900 dark:text-white">BLE 硬件设备绑定</h4>
+            <p className="text-[11px] text-slate-500 dark:text-slate-400 mt-0.5">绑定与管理 CelestiaTrace 端侧近场硬件设备</p>
+          </div>
+        </div>
+
+        <button
+          onClick={() => {
+            if (!user) {
+              onOpenAuthModal();
+            } else {
+              onOpenBindModal();
+            }
+          }}
+          className="flex items-center space-x-1 text-xs text-slate-700 dark:text-slate-300 font-medium hover:underline"
+        >
+          <span>绑定设备</span>
+          <ChevronRight className="w-4 h-4" />
+        </button>
+      </div>
+
+      {/* 5. Account Security & Cloud Compliance Card */}
+      <div className="business-card p-5 rounded-2xl border border-black/10 dark:border-white/10 bg-white dark:bg-slate-900 shadow-sm space-y-3">
+        <div className="flex items-center space-x-2 text-slate-900 dark:text-white">
+          <ShieldCheck className="w-4 h-4 text-slate-500" />
+          <h4 className="text-xs font-semibold">安全与云端合规</h4>
+        </div>
+
+        <div className="space-y-2 text-xs font-mono">
+          <div className="flex justify-between text-slate-600 dark:text-slate-400">
+            <span>数据存储模式</span>
+            <span className="text-slate-900 dark:text-slate-200">IndexedDB 本地优先 + 云端同步</span>
+          </div>
+          <div className="flex justify-between text-slate-600 dark:text-slate-400 border-t border-black/5 dark:border-white/5 pt-2">
+            <span>登录凭据</span>
+            <span className="text-slate-900 dark:text-slate-200">Web LocalStorage JWT Token</span>
+          </div>
+          <div className="flex justify-between text-slate-600 dark:text-slate-400 border-t border-black/5 dark:border-white/5 pt-2">
+            <span>传输保护</span>
+            <span className="text-slate-900 dark:text-slate-200">HTTPS / TLS</span>
+          </div>
+        </div>
+      </div>
+
+      {/* Footer */}
+      <div className="text-center text-[10px] font-mono text-slate-400 space-y-1 pt-4">
+        <div className="tracking-widest uppercase font-bold">CELESTIA TRACE USER & DEVICE</div>
+        <div>安全本地注册与极简日志空间</div>
+      </div>
+
+      {/* Logout Confirmation Dialog Modal */}
+      {showLogoutConfirm && (
+        <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm animate-fade-in">
+          <div className="w-full max-w-sm bg-[#0B0C10] border border-white/15 rounded-2xl p-6 text-white space-y-5 shadow-2xl">
+            <h3 className="text-base font-bold text-center">确定要退出登录吗?</h3>
+            <p className="text-xs text-slate-400 text-center">
+              退出后将停止录音云端同步,本机已有记录将安全保留。
+            </p>
+            <div className="flex space-x-3 pt-2">
+              <button
+                onClick={() => setShowLogoutConfirm(false)}
+                className="flex-1 py-2.5 rounded-xl border border-white/15 text-xs text-slate-300 hover:bg-white/10"
+              >
+                取消
+              </button>
+              <button
+                onClick={() => {
+                  setShowLogoutConfirm(false);
+                  logout();
+                }}
+                className="flex-1 py-2.5 rounded-xl bg-rose-600 hover:bg-rose-700 text-white font-semibold text-xs shadow"
+              >
+                退出登录
+              </button>
+            </div>
+          </div>
+        </div>
+      )}
+    </div>
+  );
+};

+ 149 - 0
src/pages/RecordsPage.jsx

@@ -0,0 +1,149 @@
+import React, { useState, useEffect } from 'react';
+import { Search, Mic, Camera, FileText, Trash2, ChevronRight, Activity, Calendar } from 'lucide-react';
+import { sessionAPI } from '../api';
+
+export const RecordsPage = ({ onSelectSession }) => {
+  const [sessions, setSessions] = useState([]);
+  const [search, setSearch] = useState('');
+  const [loading, setLoading] = useState(true);
+
+  const fetchSessions = async () => {
+    setLoading(true);
+    try {
+      const res = await sessionAPI.getSessions(search);
+      setSessions(res.code === 0 && Array.isArray(res.data) ? res.data : []);
+    } catch (e) {
+      console.warn("Fetch sessions error:", e);
+      setSessions([]);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  useEffect(() => {
+    fetchSessions();
+  }, [search]);
+
+  const handleDelete = async (e, id) => {
+    e.stopPropagation();
+    if (!window.confirm("确定删除此“现场记录”吗?包含的音频与事件将一并清理。")) return;
+    try {
+      await sessionAPI.deleteSession(id);
+      setSessions((prev) => prev.filter((s) => s.id !== id));
+    } catch (err) {
+      window.alert(err?.message || '删除失败,请稍后重试');
+    }
+  };
+
+  const formatSeconds = (ms) => {
+    if (!ms) return '00:13';
+    const totalSecs = Math.floor(ms / 1000);
+    const mins = Math.floor(totalSecs / 60);
+    const secs = totalSecs % 60;
+    return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
+  };
+
+  return (
+    <div className="space-y-6 animate-fade-in pb-20">
+      {/* Header & iOS Search bar */}
+      <div className="space-y-3">
+        <div className="flex items-center justify-between">
+          <div className="flex items-center space-x-2">
+            <Activity className="w-4 h-4 text-slate-700 dark:text-slate-300" />
+            <h2 className="text-base font-semibold text-slate-900 dark:text-white">
+              历史现场记录
+            </h2>
+          </div>
+          <span className="text-[11px] font-mono text-slate-400">
+            共 {sessions.length} 条记录
+          </span>
+        </div>
+
+        {/* Filter Input */}
+        <div className="relative">
+          <Search className="w-4 h-4 text-slate-400 absolute left-3.5 top-3" />
+          <input
+            type="text"
+            value={search}
+            onChange={(e) => setSearch(e.target.value)}
+            placeholder="搜索“现场记录”标题或时间关键词..."
+            className="w-full pl-10 pr-4 py-2.5 rounded-xl bg-white dark:bg-slate-900 border border-black/10 dark:border-white/10 text-slate-900 dark:text-white text-xs focus:outline-none focus:border-black/30 dark:focus:border-white/30 transition-colors shadow-sm"
+          />
+        </div>
+      </div>
+
+      {/* Grid of Sessions matching iOS minimalist wireframe cards */}
+      {loading ? (
+        <div className="text-center py-12 text-xs font-mono text-slate-400">
+          加载云端记录中...
+        </div>
+      ) : sessions.length === 0 ? (
+        <div className="business-card rounded-2xl p-12 text-center text-xs font-mono text-slate-400">
+          未检索到符合条件的现场记录
+        </div>
+      ) : (
+        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
+          {sessions.map((session) => (
+            <div
+              key={session.id}
+              onClick={() => onSelectSession(session)}
+              className="business-card-interactive p-4 rounded-2xl cursor-pointer flex flex-col justify-between space-y-4 group"
+            >
+              <div className="space-y-2">
+                <div className="flex items-center justify-between">
+                  <span className="px-2 py-0.5 text-[10px] font-mono font-semibold text-slate-600 dark:text-slate-300 bg-black/5 dark:bg-white/10 rounded border border-black/10 dark:border-white/10">
+                    就绪
+                  </span>
+                  <span className="text-[10px] font-mono text-slate-400">
+                    {new Date(session.startTime).toLocaleDateString('zh-CN')}
+                  </span>
+                </div>
+
+                <h3 className="text-sm font-semibold text-slate-900 dark:text-white group-hover:text-slate-600 dark:group-hover:text-slate-300 transition-colors line-clamp-2">
+                  {session.title || '现场记录 · 2026.07.22'}
+                </h3>
+              </div>
+
+              <div className="border-t border-black/5 dark:border-white/10 pt-3 space-y-2">
+                <div className="flex items-center justify-between text-[11px] font-mono text-slate-500">
+                  <span className="flex items-center space-x-1.5 text-slate-700 dark:text-slate-300">
+                    <Mic className="w-3.5 h-3.5 text-slate-500" />
+                    <span>{session.durationFormatted || formatSeconds(session.durationMs)}</span>
+                  </span>
+
+                  <div className="flex items-center space-x-3 text-[11px]">
+                    <span className="flex items-center space-x-1 text-slate-400">
+                      <Camera className="w-3.5 h-3.5" />
+                      <span>{session.photoCount || 0}</span>
+                    </span>
+                    <span className="flex items-center space-x-1 text-slate-400">
+                      <FileText className="w-3.5 h-3.5" />
+                      <span>{session.noteCount || 1}</span>
+                    </span>
+                  </div>
+                </div>
+
+                <div className="flex items-center justify-between pt-1">
+                  <span className="text-[10px] text-slate-400 font-mono">
+                    已加密上云
+                  </span>
+
+                  <div className="flex items-center space-x-2">
+                    <button
+                      onClick={(e) => handleDelete(e, session.id)}
+                      className="p-1 text-slate-400 hover:text-rose-500 transition-colors"
+                      title="删除记录"
+                    >
+                      <Trash2 className="w-3.5 h-3.5" />
+                    </button>
+                    <ChevronRight className="w-4 h-4 text-slate-400 group-hover:text-slate-900 dark:group-hover:text-white transition-colors" />
+                  </div>
+                </div>
+              </div>
+            </div>
+          ))}
+        </div>
+      )}
+    </div>
+  );
+};

+ 100 - 0
src/pages/SyncSettingsPage.jsx

@@ -0,0 +1,100 @@
+import React, { useState, useEffect } from 'react';
+import { Cloud, Shield, HardDrive, RefreshCw, CheckCircle2, Lock, Database } from 'lucide-react';
+import { syncAPI } from '../api';
+
+export const SyncSettingsPage = () => {
+  const [summary, setSummary] = useState(null);
+  const [syncing, setSyncing] = useState(false);
+  const [toast, setToast] = useState(false);
+
+  useEffect(() => {
+    syncAPI.getSummary()
+      .then((res) => {
+        if (res.code === 0) setSummary(res.data);
+      })
+      .catch((e) => console.warn(e));
+  }, []);
+
+  const handleForceSync = async () => {
+    setSyncing(true);
+    try {
+      await syncAPI.triggerManualSync();
+      setToast(true);
+      setTimeout(() => setToast(false), 3000);
+    } catch (e) {
+      console.warn("Force sync error:", e);
+    } finally {
+      setSyncing(false);
+    }
+  };
+
+  return (
+    <div className="space-y-6 animate-fade-in pb-20 max-w-3xl">
+      <div>
+        <div className="flex items-center space-x-2">
+          <Cloud className="w-4 h-4 text-slate-700 dark:text-slate-300" />
+          <h2 className="text-base font-semibold text-slate-900 dark:text-white">
+            多端数据云同步与数据隔离
+          </h2>
+        </div>
+        <p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
+          星痕 CelestiaTrace 全平台(iOS App、Android App 与 Web 控制台)共享云端引擎,保证录音与标注多端实时一致。
+        </p>
+      </div>
+
+      {toast && (
+        <div className="p-3 rounded-xl bg-emerald-500/10 border border-emerald-500/30 text-emerald-600 dark:text-emerald-400 text-xs flex items-center space-x-2">
+          <CheckCircle2 className="w-4 h-4" />
+          <span>云端与数据库状态已全面强制同步更新!</span>
+        </div>
+      )}
+
+      {/* Sync Status Cards */}
+      <div className="space-y-4">
+        <div className="business-card p-5 rounded-2xl space-y-4">
+          <div className="flex items-center justify-between">
+            <div className="flex items-center space-x-3">
+              <div className="w-9 h-9 rounded-xl bg-black/5 dark:bg-white/10 flex items-center justify-center text-slate-800 dark:text-white border border-black/10 dark:border-white/10">
+                <Database className="w-4 h-4" />
+              </div>
+              <div>
+                <h3 className="text-xs font-bold text-slate-900 dark:text-white">
+                  PostgreSQL 云端数据库引擎
+                </h3>
+                <p className="text-[10px] font-mono text-slate-400">
+                  当前连接: PostgreSQL (Docker/Caddy Proxy)
+                </p>
+              </div>
+            </div>
+
+            <button
+              onClick={handleForceSync}
+              disabled={syncing}
+              className="flex items-center space-x-1.5 px-3 py-1.5 rounded-lg bg-slate-900 text-white dark:bg-white dark:text-slate-900 text-xs font-medium transition-opacity hover:opacity-90"
+            >
+              <RefreshCw className={`w-3.5 h-3.5 ${syncing ? 'animate-spin' : ''}`} />
+              <span>{syncing ? '同步中' : '立即双向同步'}</span>
+            </button>
+          </div>
+
+          <div className="border-t border-black/5 dark:border-white/10 pt-3 grid grid-cols-2 gap-4 text-xs">
+            <div>
+              <div className="text-slate-400 text-[10px]">已用云存储</div>
+              <div className="font-mono font-bold text-slate-900 dark:text-white mt-0.5">
+                {summary?.storage?.cloudStorageUsedMB ?? 12} MB / 5120 MB
+              </div>
+            </div>
+
+            <div>
+              <div className="text-slate-400 text-[10px]">隔离模式</div>
+              <div className="font-mono font-bold text-emerald-600 dark:text-emerald-400 mt-0.5 flex items-center space-x-1">
+                <Lock className="w-3 h-3" />
+                <span>Strict JWT Token</span>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};

+ 53 - 0
tailwind.config.js

@@ -0,0 +1,53 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+  content: [
+    "./index.html",
+    "./src/**/*.{js,ts,jsx,tsx}",
+  ],
+  darkMode: 'class',
+  theme: {
+    extend: {
+      colors: {
+        celestia: {
+          bg: '#FAFAFC',
+          darkBg: '#090A0F',
+          card: '#FFFFFF',
+          darkCard: '#131620',
+          border: 'rgba(0, 0, 0, 0.08)',
+          darkBorder: 'rgba(255, 255, 255, 0.10)',
+          red: '#E53935',
+          gold: '#C5A059',
+          subtle: '#8E8E93',
+          text: '#1D1D1F',
+          darkText: '#F5F5F7',
+        },
+        spark: {
+          dark: '#0B0E14',
+          card: '#121824',
+          border: 'rgba(255, 255, 255, 0.08)',
+          cyan: '#00F2FE',
+          blue: '#4FACFE',
+          violet: '#7F56D9',
+          pink: '#F472B6',
+          accent: '#00E5FF'
+        }
+      },
+      fontFamily: {
+        sans: ['SF Pro Text', 'SF Pro Display', 'Inter', '-apple-system', 'BlinkMacSystemFont', 'sans-serif'],
+        mono: ['SF Mono', 'ui-monospace', 'Menlo', 'Monaco', 'Consolas', 'monospace'],
+      },
+      boxShadow: {
+        'glow-cyan': '0 0 20px rgba(0, 242, 254, 0.25)',
+        'glow-red': '0 0 20px rgba(229, 57, 53, 0.3)',
+        'ios-soft': '0 4px 20px -2px rgba(0, 0, 0, 0.06), 0 2px 6px -1px rgba(0, 0, 0, 0.04)',
+        'ios-float': '0 12px 32px -4px rgba(0, 0, 0, 0.12), 0 4px 12px -2px rgba(0, 0, 0, 0.08)',
+      },
+      letterSpacing: {
+        'widest-tech': '0.25em',
+        'subtle-mono': '0.15em',
+      }
+    },
+  },
+  plugins: [],
+}
+

+ 16 - 0
vite.config.js

@@ -0,0 +1,16 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [react()],
+  server: {
+    port: 3000,
+    proxy: {
+      '/v1': {
+        target: 'http://localhost:8080',
+        changeOrigin: true,
+      }
+    }
+  }
+})