diff --git a/.github/demo/demo.png b/.github/demo/demo.png
new file mode 100644
index 0000000..4dbcaee
Binary files /dev/null and b/.github/demo/demo.png differ
diff --git a/.github/demo/demo1.jpg b/.github/demo/demo1.jpg
deleted file mode 100644
index 099a4e9..0000000
Binary files a/.github/demo/demo1.jpg and /dev/null differ
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 25ef135..d3d2133 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -25,6 +25,18 @@ jobs:
with:
go-version-file: "src/go.mod"
cache-dependency-path: "src/go.sum"
+
+ - name: 设置 Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version-file: web/package.json
+ cache-dependency-path: web/package-lock.json
+
+ - name: 构建前端
+ run: |
+ cd web
+ npm ci
+ npm run build
- name: 获取版本号
id: version
diff --git a/.gitignore b/.gitignore
index f26d439..d9dfbad 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,26 @@
-.idea
-.vscode
+# IDE / OS
+.idea/
+.vscode/
.DS_Store
+
+# Go build artifacts
/hubproxy*
*.exe
+build/
+
+# Frontend (web/)
+web/node_modules/
+web/dist/
+web/dist-ssr/
+web/.tmp/
+web/*.local
+web/npm-debug.log*
+web/yarn-debug.log*
+web/yarn-error.log*
+web/pnpm-debug.log*
+
+# Frontend build output embedded by Go
+src/dist/
+
+# Logs
+*.log
diff --git a/Dockerfile b/Dockerfile
index f81133b..b84d41b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,3 +1,11 @@
+FROM node:24-alpine AS frontend
+
+WORKDIR /web
+COPY web/package.json web/package-lock.json ./
+RUN npm ci
+COPY web/ .
+RUN npm run build
+
FROM golang:1.26-alpine AS builder
ARG TARGETARCH
@@ -8,6 +16,7 @@ COPY src/go.mod src/go.sum ./
RUN apk add --no-cache upx && go mod download
COPY src/ .
+COPY --from=frontend /src/dist ./dist
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -ldflags="-s -w -X main.Version=${VERSION}" -trimpath -o hubproxy . && upx -9 hubproxy
diff --git a/README.md b/README.md
index 87f892d..ae88cef 100644
--- a/README.md
+++ b/README.md
@@ -165,7 +165,7 @@ port = 5000
fileSize = 2147483648
# HTTP/2 多路复用,提升下载速度
enableH2C = false
-# 是否启用前端静态页面
+# 是否启用前端页面(Vue SPA)
enableFrontend = true
[rateLimit]
@@ -262,7 +262,7 @@ CONFIG_PATH=config.toml # 配置文件路径
SERVER_HOST=0.0.0.0 # 监听地址
SERVER_PORT=5000 # 监听端口
ENABLE_H2C=false # 是否启用 H2C
-ENABLE_FRONTEND=true # 是否启用前端静态页面
+ENABLE_FRONTEND=true # 是否启用前端页面(Vue SPA)
MAX_FILE_SIZE=2147483648 # GitHub 文件大小限制(字节)
RATE_LIMIT=500 # 每周期请求数
RATE_PERIOD_HOURS=3 # 限流周期(小时)
@@ -314,8 +314,4 @@ example.com {
## 界面预览
-
-
-## Star History
-
-[](https://www.star-history.com/?repos=sky22333%2Fhubproxy&type=date&legend=top-left)
+
diff --git a/src/handlers/search.go b/src/handlers/search.go
index f00d267..9d3a5dd 100644
--- a/src/handlers/search.go
+++ b/src/handlers/search.go
@@ -466,9 +466,9 @@ func sendErrorResponse(c *gin.Context, message string) {
c.JSON(http.StatusBadRequest, gin.H{"error": message})
}
-// RegisterSearchRoute 注册搜索相关路由
+// RegisterSearchRoute 注册搜索与标签相关 API 路由。
func RegisterSearchRoute(r *gin.Engine) {
- r.GET("/search", func(c *gin.Context) {
+ r.GET("/api/search", func(c *gin.Context) {
query := c.Query("q")
if query == "" {
sendErrorResponse(c, "搜索关键词不能为空")
@@ -486,7 +486,7 @@ func RegisterSearchRoute(r *gin.Engine) {
c.JSON(http.StatusOK, result)
})
- r.GET("/tags/:namespace/:name", func(c *gin.Context) {
+ r.GET("/api/tags/:namespace/:name", func(c *gin.Context) {
namespace := c.Param("namespace")
name := c.Param("name")
@@ -503,15 +503,9 @@ func RegisterSearchRoute(r *gin.Engine) {
return
}
- if c.Query("page") != "" || c.Query("page_size") != "" {
- c.JSON(http.StatusOK, gin.H{
- "tags": tags,
- "has_more": hasMore,
- "page": page,
- "page_size": pageSize,
- })
- } else {
- c.JSON(http.StatusOK, tags)
- }
+ c.JSON(http.StatusOK, TagPageResult{
+ Tags: tags,
+ HasMore: hasMore,
+ })
})
}
diff --git a/src/main.go b/src/main.go
index d757d59..cd542cb 100644
--- a/src/main.go
+++ b/src/main.go
@@ -4,7 +4,9 @@ import (
"embed"
"fmt"
"log"
+ "mime"
"net/http"
+ "path"
"strings"
"time"
@@ -16,7 +18,7 @@ import (
"hubproxy/utils"
)
-//go:embed public/*
+//go:embed all:dist
var staticFiles embed.FS
var (
@@ -26,18 +28,63 @@ var (
var Version = "dev"
+func init() {
+ for ext, typ := range map[string]string{
+ ".js": "application/javascript; charset=utf-8",
+ ".mjs": "application/javascript; charset=utf-8",
+ ".woff": "font/woff",
+ ".woff2": "font/woff2",
+ ".map": "application/json",
+ } {
+ _ = mime.AddExtensionType(ext, typ)
+ }
+}
+
+func contentTypeFor(filename string) string {
+ if ct := mime.TypeByExtension(path.Ext(filename)); ct != "" {
+ return ct
+ }
+ return "application/octet-stream"
+}
+
func serveEmbedFile(c *gin.Context, filename string) {
data, err := staticFiles.ReadFile(filename)
if err != nil {
c.Status(http.StatusNotFound)
return
}
+ c.Data(http.StatusOK, contentTypeFor(filename), data)
+}
- contentType := "text/html; charset=utf-8"
- if strings.HasSuffix(filename, ".ico") {
- contentType = "image/x-icon"
+func serveSPA(c *gin.Context) {
+ serveEmbedFile(c, "dist/index.html")
+}
+
+func registerFrontendRoutes(router *gin.Engine, enabled bool) {
+ if !enabled {
+ notFound := func(c *gin.Context) { c.Status(http.StatusNotFound) }
+ router.GET("/", notFound)
+ router.GET("/images", notFound)
+ router.GET("/search", notFound)
+ router.GET("/assets/*filepath", notFound)
+ router.GET("/favicon.svg", notFound)
+ return
}
- c.Data(http.StatusOK, contentType, data)
+
+ router.GET("/", serveSPA)
+ router.GET("/images", serveSPA)
+ router.GET("/search", serveSPA)
+ router.GET("/favicon.svg", func(c *gin.Context) {
+ serveEmbedFile(c, "dist/favicon.svg")
+ })
+ router.GET("/assets/*filepath", func(c *gin.Context) {
+ filepath := strings.TrimPrefix(c.Param("filepath"), "/")
+ if filepath == "" || strings.Contains(filepath, "..") {
+ c.Status(http.StatusNotFound)
+ return
+ }
+ serveEmbedFile(c, path.Join("dist/assets", filepath))
+ })
}
func buildRouter(cfg *config.AppConfig) *gin.Engine {
@@ -56,32 +103,7 @@ func buildRouter(cfg *config.AppConfig) *gin.Engine {
initHealthRoutes(router)
handlers.InitImageTarRoutes(router)
-
- if cfg.Server.EnableFrontend {
- router.GET("/", func(c *gin.Context) {
- serveEmbedFile(c, "public/index.html")
- })
- router.GET("/public/*filepath", func(c *gin.Context) {
- filepath := strings.TrimPrefix(c.Param("filepath"), "/")
- serveEmbedFile(c, "public/"+filepath)
- })
- router.GET("/images.html", func(c *gin.Context) {
- serveEmbedFile(c, "public/images.html")
- })
- router.GET("/search.html", func(c *gin.Context) {
- serveEmbedFile(c, "public/search.html")
- })
- router.GET("/favicon.ico", func(c *gin.Context) {
- serveEmbedFile(c, "public/favicon.ico")
- })
- } else {
- router.GET("/", func(c *gin.Context) { c.Status(http.StatusNotFound) })
- router.GET("/public/*filepath", func(c *gin.Context) { c.Status(http.StatusNotFound) })
- router.GET("/images.html", func(c *gin.Context) { c.Status(http.StatusNotFound) })
- router.GET("/search.html", func(c *gin.Context) { c.Status(http.StatusNotFound) })
- router.GET("/favicon.ico", func(c *gin.Context) { c.Status(http.StatusNotFound) })
- }
-
+ registerFrontendRoutes(router, cfg.Server.EnableFrontend)
handlers.RegisterSearchRoute(router)
router.Any("/token", handlers.ProxyDockerAuthGin)
diff --git a/src/main_test.go b/src/main_test.go
index 378a19c..be5952f 100644
--- a/src/main_test.go
+++ b/src/main_test.go
@@ -71,7 +71,7 @@ func TestFrontendDisabledRoutesReturnNotFound(t *testing.T) {
enableFrontend = false
`)
- for _, path := range []string{"/", "/images.html", "/search.html", "/favicon.ico"} {
+ for _, path := range []string{"/", "/images", "/search", "/favicon.svg"} {
w := performRequest(router, http.MethodGet, path, "")
if w.Code != http.StatusNotFound {
t.Fatalf("%s status = %d, want 404", path, w.Code)
@@ -157,10 +157,13 @@ func TestDockerV2PingAndInvalidPath(t *testing.T) {
}
}
-func TestSearchRouteRejectsMissingQuery(t *testing.T) {
- router := newTestRouter(t, "")
+func TestSearchAPIRejectsMissingQuery(t *testing.T) {
+ router := newTestRouter(t, `
+[server]
+enableFrontend = false
+`)
- w := performRequest(router, http.MethodGet, "/search", "")
+ w := performRequest(router, http.MethodGet, "/api/search", "")
if w.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want 400; body=%s", w.Code, w.Body.String())
}
@@ -173,3 +176,21 @@ func TestSearchRouteRejectsMissingQuery(t *testing.T) {
t.Fatalf("missing error response: %#v", got)
}
}
+
+func TestSearchServesSPAWhenFrontendEnabled(t *testing.T) {
+ router := newTestRouter(t, `
+[server]
+enableFrontend = true
+`)
+
+ w := performRequest(router, http.MethodGet, "/search?q=nginx", "")
+ if w.Code != http.StatusOK {
+ t.Fatalf("status = %d, want 200; body=%s", w.Code, w.Body.String())
+ }
+ if !strings.Contains(w.Header().Get("Content-Type"), "text/html") {
+ t.Fatalf("content-type = %q, want text/html", w.Header().Get("Content-Type"))
+ }
+ if !strings.Contains(w.Body.String(), `
`) {
+ t.Fatalf("SPA shell missing: %s", w.Body.String())
+ }
+}
diff --git a/src/public/favicon.ico b/src/public/favicon.ico
deleted file mode 100644
index 1ead7d6..0000000
Binary files a/src/public/favicon.ico and /dev/null differ
diff --git a/src/public/images.html b/src/public/images.html
deleted file mode 100644
index 645f2a1..0000000
--- a/src/public/images.html
+++ /dev/null
@@ -1,961 +0,0 @@
-
-
-
-
-
-
-
-
-
Docker离线镜像下载
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/public/index.html b/src/public/index.html
deleted file mode 100644
index beec1e9..0000000
--- a/src/public/index.html
+++ /dev/null
@@ -1,832 +0,0 @@
-
-
-
-
-
-
-
-
-
Github、Docker加速
-
-
-
-
-
-
-
-
-
-
-
GitHub 文件加速
-
- 快速下载GitHub上的文件和仓库,解决国内访问GitHub速度慢的问题,支持Docker镜像加速和Hugging Face仓库。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Docker 官方镜像:
- docker pull /nginx
-
- Docker 镜像:
- docker pull /user/image
-
- ghcr.io 镜像:
- docker pull /ghcr.io/user/image
-
- Quay.io 镜像:
- docker pull /quay.io/org/image
-
- Kubernetes 镜像:
- docker pull /registry.k8s.io/pause:3.8
-
-
-
-
-
- 链接已复制到剪贴板
-
-
-
-
-
-
-
-
diff --git a/src/public/search.html b/src/public/search.html
deleted file mode 100644
index 7bf6d6a..0000000
--- a/src/public/search.html
+++ /dev/null
@@ -1,1501 +0,0 @@
-
-
-
-
-
-
-
-
-
Docker镜像搜索
-
-
-
-
-
-
-
-
-
Docker镜像搜索
-
-
-
-
-
-
← 返回搜索结果
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/utils/ratelimiter.go b/src/utils/ratelimiter.go
index 3512cc9..4741e40 100644
--- a/src/utils/ratelimiter.go
+++ b/src/utils/ratelimiter.go
@@ -212,8 +212,9 @@ func (i *IPRateLimiter) GetLimiter(ip string) (*rate.Limiter, bool) {
func RateLimitMiddleware(limiter *IPRateLimiter) gin.HandlerFunc {
return func(c *gin.Context) {
path := c.Request.URL.Path
- if path == "/" || path == "/favicon.ico" || path == "/images.html" || path == "/search.html" ||
- strings.HasPrefix(path, "/public/") {
+ if path == "/" || path == "/images" || path == "/search" ||
+ path == "/favicon.svg" ||
+ strings.HasPrefix(path, "/assets/") {
c.Next()
return
}
diff --git a/web/.gitignore b/web/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/web/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/web/README.md b/web/README.md
new file mode 100644
index 0000000..fd3547d
--- /dev/null
+++ b/web/README.md
@@ -0,0 +1,9 @@
+# HubProxy Web
+
+Vue 3 + Vite + Tailwind 前端。
+
+```bash
+npm install
+npm run dev # 代理到 :5000
+npm run build # 输出到 ../src/dist
+```
diff --git a/web/index.html b/web/index.html
new file mode 100644
index 0000000..e1ede59
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
HubProxy
+
+
+
+
+
+
diff --git a/web/package-lock.json b/web/package-lock.json
new file mode 100644
index 0000000..e2282fa
--- /dev/null
+++ b/web/package-lock.json
@@ -0,0 +1,1723 @@
+{
+ "name": "web",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "web",
+ "version": "0.0.0",
+ "dependencies": {
+ "@fontsource-variable/manrope": "^5.2.8",
+ "@fontsource/syne": "^5.2.7",
+ "clsx": "^2.1.1",
+ "lucide-vue-next": "^0.577.0",
+ "tailwind-merge": "^3.6.0",
+ "vue": "^3.5.39",
+ "vue-router": "^4.6.4"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.3.2",
+ "@types/node": "^24.13.2",
+ "@vitejs/plugin-vue": "^6.0.7",
+ "@vue/tsconfig": "^0.9.1",
+ "tailwindcss": "^4.3.2",
+ "typescript": "~6.0.2",
+ "vite": "^8.1.1",
+ "vue-tsc": "^3.3.5"
+ }
+ },
+ "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==",
+ "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==",
+ "license": "MIT",
+ "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==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.7"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.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==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmmirror.com/@emnapi/core/-/core-1.11.1.tgz",
+ "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.2",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.11.1.tgz",
+ "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
+ "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@fontsource-variable/manrope": {
+ "version": "5.2.8",
+ "resolved": "https://registry.npmmirror.com/@fontsource-variable/manrope/-/manrope-5.2.8.tgz",
+ "integrity": "sha512-nc9lOuCRz73UHnovDE2bwXUdghE2SEOc7Aii0qGe3CLyE03W1a7VnY5Z6euRiapiKbCkGS+eXbY3s/kvWeGeSw==",
+ "license": "OFL-1.1",
+ "funding": {
+ "url": "https://github.com/sponsors/ayuhito"
+ }
+ },
+ "node_modules/@fontsource/syne": {
+ "version": "5.2.7",
+ "resolved": "https://registry.npmmirror.com/@fontsource/syne/-/syne-5.2.7.tgz",
+ "integrity": "sha512-OgWgLPSbieiSfdFXgMs0l/rspIFzVoDZxw8wKj15n39epRqO5+Uv36c0MXNDcw2GuK/TIBgpC9G8VAgs+bVm7A==",
+ "license": "OFL-1.1",
+ "funding": {
+ "url": "https://github.com/sponsors/ayuhito"
+ }
+ },
+ "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==",
+ "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/@napi-rs/wasm-runtime": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
+ "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.3"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.139.0",
+ "resolved": "https://registry.npmmirror.com/@oxc-project/types/-/types-0.139.0.tgz",
+ "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
+ "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz",
+ "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz",
+ "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz",
+ "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz",
+ "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz",
+ "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz",
+ "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz",
+ "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz",
+ "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz",
+ "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz",
+ "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz",
+ "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz",
+ "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.11.1",
+ "@emnapi/runtime": "1.11.1",
+ "@napi-rs/wasm-runtime": "^1.1.6"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",
+ "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz",
+ "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmmirror.com/@tailwindcss/node/-/node-4.3.2.tgz",
+ "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "enhanced-resolve": "5.21.6",
+ "jiti": "^2.7.0",
+ "lightningcss": "1.32.0",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.3.2"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide/-/oxide-4.3.2.tgz",
+ "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.3.2",
+ "@tailwindcss/oxide-darwin-arm64": "4.3.2",
+ "@tailwindcss/oxide-darwin-x64": "4.3.2",
+ "@tailwindcss/oxide-freebsd-x64": "4.3.2",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.3.2",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.3.2",
+ "@tailwindcss/oxide-linux-x64-musl": "4.3.2",
+ "@tailwindcss/oxide-wasm32-wasi": "4.3.2",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.3.2"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz",
+ "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz",
+ "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz",
+ "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz",
+ "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz",
+ "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz",
+ "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz",
+ "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz",
+ "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz",
+ "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz",
+ "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.11.1",
+ "@emnapi/runtime": "^1.11.1",
+ "@emnapi/wasi-threads": "^1.2.2",
+ "@napi-rs/wasm-runtime": "^1.1.4",
+ "@tybys/wasm-util": "^0.10.2",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz",
+ "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz",
+ "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/vite": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmmirror.com/@tailwindcss/vite/-/vite-4.3.2.tgz",
+ "integrity": "sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tailwindcss/node": "4.3.2",
+ "@tailwindcss/oxide": "4.3.2",
+ "tailwindcss": "4.3.2"
+ },
+ "peerDependencies": {
+ "vite": "^5.2.0 || ^6 || ^7 || ^8"
+ }
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.3",
+ "resolved": "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
+ "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "24.13.3",
+ "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.13.3.tgz",
+ "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.18.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-vue": {
+ "version": "6.0.7",
+ "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz",
+ "integrity": "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
+ "vue": "^3.2.25"
+ }
+ },
+ "node_modules/@volar/language-core": {
+ "version": "2.4.28",
+ "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.28.tgz",
+ "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/source-map": "2.4.28"
+ }
+ },
+ "node_modules/@volar/source-map": {
+ "version": "2.4.28",
+ "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.28.tgz",
+ "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@volar/typescript": {
+ "version": "2.4.28",
+ "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.28.tgz",
+ "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.28",
+ "path-browserify": "^1.0.1",
+ "vscode-uri": "^3.0.8"
+ }
+ },
+ "node_modules/@vue/compiler-core": {
+ "version": "3.5.39",
+ "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.39.tgz",
+ "integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.7",
+ "@vue/shared": "3.5.39",
+ "entities": "^7.0.1",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.39",
+ "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz",
+ "integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.39",
+ "@vue/shared": "3.5.39"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.39",
+ "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz",
+ "integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.7",
+ "@vue/compiler-core": "3.5.39",
+ "@vue/compiler-dom": "3.5.39",
+ "@vue/compiler-ssr": "3.5.39",
+ "@vue/shared": "3.5.39",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.21",
+ "postcss": "^8.5.15",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.5.39",
+ "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz",
+ "integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.39",
+ "@vue/shared": "3.5.39"
+ }
+ },
+ "node_modules/@vue/devtools-api": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/language-core": {
+ "version": "3.3.7",
+ "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-3.3.7.tgz",
+ "integrity": "sha512-LzmkKinXAMMoh8Jfi/jMUSDUjuPdv8mynH5WJGKfXyZtDw3hQ6GBaoI6Bcnl/Xqlu32q/0Z6i/trp4VXykzyLw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.28",
+ "@vue/compiler-dom": "^3.5.0",
+ "@vue/shared": "^3.5.0",
+ "alien-signals": "^3.2.1",
+ "muggle-string": "^0.4.1",
+ "path-browserify": "^1.0.1",
+ "picomatch": "^4.0.4"
+ }
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.5.39",
+ "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.39.tgz",
+ "integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/shared": "3.5.39"
+ }
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.5.39",
+ "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.39.tgz",
+ "integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.39",
+ "@vue/shared": "3.5.39"
+ }
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.39",
+ "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz",
+ "integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.39",
+ "@vue/runtime-core": "3.5.39",
+ "@vue/shared": "3.5.39",
+ "csstype": "^3.2.3"
+ }
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.39",
+ "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.39.tgz",
+ "integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.39",
+ "@vue/shared": "3.5.39"
+ },
+ "peerDependencies": {
+ "vue": "3.5.39"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.39",
+ "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.39.tgz",
+ "integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/tsconfig": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmmirror.com/@vue/tsconfig/-/tsconfig-0.9.1.tgz",
+ "integrity": "sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "typescript": ">= 5.8",
+ "vue": "^3.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ },
+ "vue": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/alien-signals": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-3.2.1.tgz",
+ "integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "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==",
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.21.6",
+ "resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz",
+ "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT"
+ },
+ "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/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/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/jiti": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lucide-vue-next": {
+ "version": "0.577.0",
+ "resolved": "https://registry.npmmirror.com/lucide-vue-next/-/lucide-vue-next-0.577.0.tgz",
+ "integrity": "sha512-py05bAfv9SHVJqscbiOnjcnLlEmOffA58a+7XhZuFxrs6txe1E8VoR1ngWGTYO+9aVKABAz8l3ee3PqiQN9QPA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "vue": ">=3.0.1"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/muggle-string": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz",
+ "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.15",
+ "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.15.tgz",
+ "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
+ "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/path-browserify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz",
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+ "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==",
+ "license": "ISC"
+ },
+ "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/postcss": {
+ "version": "8.5.16",
+ "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.16.tgz",
+ "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
+ "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.12",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmmirror.com/rolldown/-/rolldown-1.1.5.tgz",
+ "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.139.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.1.5",
+ "@rolldown/binding-darwin-arm64": "1.1.5",
+ "@rolldown/binding-darwin-x64": "1.1.5",
+ "@rolldown/binding-freebsd-x64": "1.1.5",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.1.5",
+ "@rolldown/binding-linux-arm64-gnu": "1.1.5",
+ "@rolldown/binding-linux-arm64-musl": "1.1.5",
+ "@rolldown/binding-linux-ppc64-gnu": "1.1.5",
+ "@rolldown/binding-linux-s390x-gnu": "1.1.5",
+ "@rolldown/binding-linux-x64-gnu": "1.1.5",
+ "@rolldown/binding-linux-x64-musl": "1.1.5",
+ "@rolldown/binding-openharmony-arm64": "1.1.5",
+ "@rolldown/binding-wasm32-wasi": "1.1.5",
+ "@rolldown/binding-win32-arm64-msvc": "1.1.5",
+ "@rolldown/binding-win32-x64-msvc": "1.1.5"
+ }
+ },
+ "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==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tailwind-merge": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-3.6.0.tgz",
+ "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.3.2.tgz",
+ "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmmirror.com/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "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/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/typescript": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmmirror.com/typescript/-/typescript-6.0.3.tgz",
+ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.18.2",
+ "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.18.2.tgz",
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "8.1.4",
+ "resolved": "https://registry.npmmirror.com/vite/-/vite-8.1.4.tgz",
+ "integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.5",
+ "postcss": "^8.5.16",
+ "rolldown": "~1.1.4",
+ "tinyglobby": "^0.2.17"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.3.0",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vscode-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz",
+ "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vue": {
+ "version": "3.5.39",
+ "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.39.tgz",
+ "integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.39",
+ "@vue/compiler-sfc": "3.5.39",
+ "@vue/runtime-dom": "3.5.39",
+ "@vue/server-renderer": "3.5.39",
+ "@vue/shared": "3.5.39"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-router": {
+ "version": "4.6.4",
+ "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz",
+ "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-api": "^6.6.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
+ "node_modules/vue-tsc": {
+ "version": "3.3.7",
+ "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-3.3.7.tgz",
+ "integrity": "sha512-+C+rgD49wAQ5bUTl2sp5a8Bzg4YoldMNXM+g7CFe604MYcQ8PrZPMQhIjJSzKXtPBCa+C5ayMipqjbA7splekQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/typescript": "2.4.28",
+ "@vue/language-core": "3.3.7"
+ },
+ "bin": {
+ "vue-tsc": "bin/vue-tsc.js"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.0.0"
+ }
+ }
+ }
+}
diff --git a/web/package.json b/web/package.json
new file mode 100644
index 0000000..243ac2a
--- /dev/null
+++ b/web/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "web",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "packageManager": "npm@11.12.1",
+ "engines": {
+ "node": ">=24"
+ },
+ "scripts": {
+ "dev": "vite",
+ "build": "vue-tsc -b && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@fontsource-variable/manrope": "^5.2.8",
+ "@fontsource/syne": "^5.2.7",
+ "clsx": "^2.1.1",
+ "lucide-vue-next": "^0.577.0",
+ "tailwind-merge": "^3.6.0",
+ "vue": "^3.5.39",
+ "vue-router": "^4.6.4"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.3.2",
+ "@types/node": "^24.13.2",
+ "@vitejs/plugin-vue": "^6.0.7",
+ "@vue/tsconfig": "^0.9.1",
+ "tailwindcss": "^4.3.2",
+ "typescript": "~6.0.2",
+ "vite": "^8.1.1",
+ "vue-tsc": "^3.3.5"
+ }
+}
diff --git a/web/public/favicon.svg b/web/public/favicon.svg
new file mode 100644
index 0000000..6893eb1
--- /dev/null
+++ b/web/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/public/icons.svg b/web/public/icons.svg
new file mode 100644
index 0000000..e952219
--- /dev/null
+++ b/web/public/icons.svg
@@ -0,0 +1,24 @@
+
diff --git a/web/src/App.vue b/web/src/App.vue
new file mode 100644
index 0000000..bcd4d75
--- /dev/null
+++ b/web/src/App.vue
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/api.ts b/web/src/api.ts
new file mode 100644
index 0000000..e508a80
--- /dev/null
+++ b/web/src/api.ts
@@ -0,0 +1,137 @@
+class ApiError extends Error {
+ status: number
+
+ constructor(message: string, status: number) {
+ super(message)
+ this.name = 'ApiError'
+ this.status = status
+ }
+}
+
+async function parseError(res: Response): Promise
{
+ const contentType = res.headers.get('Content-Type') || ''
+ if (contentType.includes('application/json')) {
+ try {
+ const data = (await res.json()) as { error?: string; message?: string }
+ return data.error || data.message || `请求失败 (${res.status})`
+ } catch {
+ return `请求失败 (${res.status})`
+ }
+ }
+ try {
+ const text = await res.text()
+ return text || `请求失败 (${res.status})`
+ } catch {
+ return `请求失败 (${res.status})`
+ }
+}
+
+async function getJSON(url: string, init?: RequestInit): Promise {
+ const res = await fetch(url, {
+ ...init,
+ headers: {
+ Accept: 'application/json',
+ ...(init?.headers || {}),
+ },
+ cache: 'no-store',
+ })
+ if (!res.ok) throw new ApiError(await parseError(res), res.status)
+ return (await res.json()) as T
+}
+
+export interface PrepareDownloadResponse {
+ download_url: string
+}
+
+export interface ImageInfoResponse {
+ success: boolean
+}
+
+export interface Repository {
+ repo_name?: string
+ short_description?: string
+ is_official?: boolean
+ star_count?: number
+ pull_count?: number
+ namespace?: string
+}
+
+export interface SearchResponse {
+ count: number
+ results: Repository[]
+}
+
+export interface TagInfo {
+ name: string
+ last_updated?: string
+ full_size?: number
+ images?: Array<{
+ architecture?: string
+ os?: string
+ variant?: string
+ size?: number
+ }>
+}
+
+export interface TagPageResult {
+ tags: TagInfo[]
+ has_more: boolean
+}
+
+export function prepareSingleDownload(params: {
+ image: string
+ platform?: string
+ compressed: boolean
+}) {
+ const q = new URLSearchParams()
+ q.set('image', params.image)
+ q.set('mode', 'prepare')
+ q.set('compressed', String(params.compressed))
+ if (params.platform?.trim()) q.set('platform', params.platform.trim())
+ return getJSON(`/api/image/download?${q}`)
+}
+
+export function fetchImageInfo(image: string) {
+ const q = new URLSearchParams({ image })
+ return getJSON(`/api/image/info?${q}`)
+}
+
+export function prepareBatchDownload(body: {
+ images: string[]
+ platform?: string
+ useCompressedLayers: boolean
+}) {
+ return getJSON('/api/image/batch?mode=prepare', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body),
+ })
+}
+
+export function searchImages(q: string, page: number, pageSize = 25) {
+ const params = new URLSearchParams({
+ q,
+ page: String(page),
+ page_size: String(pageSize),
+ })
+ return getJSON(`/api/search?${params}`)
+}
+
+export function fetchTags(namespace: string, name: string, page: number, pageSize = 100) {
+ const params = new URLSearchParams({
+ page: String(page),
+ page_size: String(pageSize),
+ })
+ return getJSON(
+ `/api/tags/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}?${params}`,
+ )
+}
+
+export function triggerDownload(url: string) {
+ const link = document.createElement('a')
+ link.href = url
+ link.style.display = 'none'
+ document.body.appendChild(link)
+ link.click()
+ document.body.removeChild(link)
+}
diff --git a/web/src/components/AppShell.vue b/web/src/components/AppShell.vue
new file mode 100644
index 0000000..552ee8f
--- /dev/null
+++ b/web/src/components/AppShell.vue
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+ HubProxy
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ link.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/components/PageHero.vue b/web/src/components/PageHero.vue
new file mode 100644
index 0000000..b6b3b38
--- /dev/null
+++ b/web/src/components/PageHero.vue
@@ -0,0 +1,19 @@
+
+
+
+
+ {{ eyebrow }}
+ {{ title }}
+
+ {{ subtitle }}
+
+
+
+
diff --git a/web/src/components/ThemeToggle.vue b/web/src/components/ThemeToggle.vue
new file mode 100644
index 0000000..e0c8755
--- /dev/null
+++ b/web/src/components/ThemeToggle.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
diff --git a/web/src/components/ui/Button.vue b/web/src/components/ui/Button.vue
new file mode 100644
index 0000000..0b51b2c
--- /dev/null
+++ b/web/src/components/ui/Button.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
diff --git a/web/src/components/ui/Input.vue b/web/src/components/ui/Input.vue
new file mode 100644
index 0000000..6b4339a
--- /dev/null
+++ b/web/src/components/ui/Input.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
diff --git a/web/src/components/ui/Switch.vue b/web/src/components/ui/Switch.vue
new file mode 100644
index 0000000..70e090e
--- /dev/null
+++ b/web/src/components/ui/Switch.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
diff --git a/web/src/components/ui/Textarea.vue b/web/src/components/ui/Textarea.vue
new file mode 100644
index 0000000..f176da2
--- /dev/null
+++ b/web/src/components/ui/Textarea.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
diff --git a/web/src/fonts/syne-600.woff2.css b/web/src/fonts/syne-600.woff2.css
new file mode 100644
index 0000000..185c169
--- /dev/null
+++ b/web/src/fonts/syne-600.woff2.css
@@ -0,0 +1,29 @@
+@font-face {
+ font-family: 'Syne';
+ font-style: normal;
+ font-display: swap;
+ font-weight: 600;
+ src: url('@fontsource/syne/files/syne-greek-600-normal.woff2') format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+
+@font-face {
+ font-family: 'Syne';
+ font-style: normal;
+ font-display: swap;
+ font-weight: 600;
+ src: url('@fontsource/syne/files/syne-latin-ext-600-normal.woff2') format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
+ U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0,
+ U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+@font-face {
+ font-family: 'Syne';
+ font-style: normal;
+ font-display: swap;
+ font-weight: 600;
+ src: url('@fontsource/syne/files/syne-latin-600-normal.woff2') format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
+ U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts
new file mode 100644
index 0000000..042d1a5
--- /dev/null
+++ b/web/src/lib/utils.ts
@@ -0,0 +1,80 @@
+import type { ClassValue } from 'clsx'
+import { clsx } from 'clsx'
+import { twMerge } from 'tailwind-merge'
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
+
+export function formatNumber(num: number): string {
+ if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(1)}B+`
+ if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(1)}M+`
+ if (num >= 1_000) return `${(num / 1_000).toFixed(1)}K+`
+ return String(num)
+}
+
+export function formatSize(bytes?: number): string {
+ if (bytes == null || bytes <= 0) return ''
+ const units = ['B', 'KB', 'MB', 'GB']
+ let size = bytes
+ let i = 0
+ while (size >= 1024 && i < units.length - 1) {
+ size /= 1024
+ i++
+ }
+ return `${size.toFixed(i === 0 ? 0 : 1)} ${units[i]}`
+}
+
+export function formatArchs(
+ images?: Array<{ architecture?: string; os?: string; variant?: string }>,
+): string[] {
+ if (!images?.length) return []
+ const seen = new Set()
+ const out: string[] = []
+ for (const img of images) {
+ const arch = img.architecture?.trim()
+ if (!arch || arch === 'unknown') continue
+ const os = img.os?.trim()
+ let label = os && os !== 'unknown' ? `${os}/${arch}` : arch
+ if (img.variant) label += `/${img.variant}`
+ if (seen.has(label)) continue
+ seen.add(label)
+ out.push(label)
+ }
+ return out
+}
+
+export function formatTimeAgo(dateString?: string): string {
+ if (!dateString) return '未知时间'
+ const date = new Date(dateString)
+ if (Number.isNaN(date.getTime())) return '未知时间'
+
+ const diffMs = Math.abs(Date.now() - date.getTime())
+ const minutes = Math.floor(diffMs / 60_000)
+ const hours = Math.floor(diffMs / 3_600_000)
+ const days = Math.floor(diffMs / 86_400_000)
+ const months = Math.floor(days / 30)
+ const years = Math.floor(days / 365)
+
+ if (minutes < 1) return '刚刚'
+ if (minutes < 60) return `${minutes}分钟前`
+ if (hours < 24) return `${hours}小时前`
+ if (days < 7) return `${days}天前`
+ if (days < 30) return `${Math.floor(days / 7)}周前`
+ if (months < 12) return `${months}个月前`
+ if (years < 1) return '近1年'
+ return `${years}年前`
+}
+
+export async function copyText(text: string): Promise {
+ try {
+ await navigator.clipboard.writeText(text)
+ return true
+ } catch {
+ return false
+ }
+}
+
+export function errorMessage(error: unknown, fallback: string): string {
+ return error instanceof Error ? error.message : fallback
+}
diff --git a/web/src/main.ts b/web/src/main.ts
new file mode 100644
index 0000000..4a5fdd1
--- /dev/null
+++ b/web/src/main.ts
@@ -0,0 +1,10 @@
+import { createApp } from 'vue'
+import './style.css'
+import App from './App.vue'
+import router from './router'
+
+if ('scrollRestoration' in history) {
+ history.scrollRestoration = 'manual'
+}
+
+createApp(App).use(router).mount('#app')
diff --git a/web/src/pages/HomePage.vue b/web/src/pages/HomePage.vue
new file mode 100644
index 0000000..df1da20
--- /dev/null
+++ b/web/src/pages/HomePage.vue
@@ -0,0 +1,178 @@
+
+
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ error }}
+
+
+
+ 加速链接已生成
+
+
+ {{ output }}
+
+
+
+
+
+
+
+
+
+
+
+
+ Docker 镜像加速
+
+
+ 在镜像名前加上本站域名,一行命令即可加速拉取
+
+
+
+
+
+
+
+
{{ item.label }}
+
+ $
+ {{ item.original }}
+
+
+ $
+ {{ item.accelerated }}
+
+
+
+
+
+
+
diff --git a/web/src/pages/ImagesPage.vue b/web/src/pages/ImagesPage.vue
new file mode 100644
index 0000000..a0c5afe
--- /dev/null
+++ b/web/src/pages/ImagesPage.vue
@@ -0,0 +1,176 @@
+
+
+
+
+
+
+
+
+
+
+ 批量下载
+
+
+
+ {{ batchError }}
+
+
+ {{ batchStatus }}
+
+
+
+
+
+
+ 压缩层
+
+
+
+
+
+
diff --git a/web/src/pages/SearchPage.vue b/web/src/pages/SearchPage.vue
new file mode 100644
index 0000000..3e5d083
--- /dev/null
+++ b/web/src/pages/SearchPage.vue
@@ -0,0 +1,400 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ searchError }}
+
+
+
+
+
+
+ 共 {{ resultCount }} 条结果
+ · 第 {{ resultsPage }} / {{ totalPages }} 页
+
+
+
+
+
+
+ 第 {{ resultsPage }} 页
+
+
+
+
+
+
+
+
+
+
+
{{ selected.fullRepoName }}
+ 官方
+
+
+ {{ selected.raw.short_description || '暂无描述' }}
+
+
+ {{ copyHint }}
+
+
+
+
+
+
+
+ 第 {{ tagsPage }} 页
+
+
+
+
+
+
{{ tagsError }}
+
+
+ 没有匹配的标签
+
+
+
+
+
{{ tag.name }}
+
+ {{ size }} ·
+ {{ formatTimeAgo(tag.last_updated) }}
+
+
+ {{ arch }}
+
+
+
+
+
+
+
+
+
diff --git a/web/src/router/index.ts b/web/src/router/index.ts
new file mode 100644
index 0000000..80170b7
--- /dev/null
+++ b/web/src/router/index.ts
@@ -0,0 +1,37 @@
+import { createRouter, createWebHistory } from 'vue-router'
+import HomePage from '@/pages/HomePage.vue'
+import ImagesPage from '@/pages/ImagesPage.vue'
+import SearchPage from '@/pages/SearchPage.vue'
+
+const router = createRouter({
+ history: createWebHistory(),
+ routes: [
+ {
+ path: '/',
+ component: HomePage,
+ meta: { title: 'GitHub 加速' },
+ },
+ {
+ path: '/images',
+ component: ImagesPage,
+ meta: { title: '离线镜像下载' },
+ },
+ {
+ path: '/search',
+ component: SearchPage,
+ meta: { title: '镜像搜索' },
+ },
+ ],
+ scrollBehavior(to, from, savedPosition) {
+ if (savedPosition) return savedPosition
+ if (to.path !== from.path) return { top: 0, left: 0 }
+ return false
+ },
+})
+
+router.afterEach((to) => {
+ const title = (to.meta.title as string) || 'HubProxy'
+ document.title = `${title} · HubProxy`
+})
+
+export default router
diff --git a/web/src/style.css b/web/src/style.css
new file mode 100644
index 0000000..e27b4c6
--- /dev/null
+++ b/web/src/style.css
@@ -0,0 +1,233 @@
+@import "tailwindcss";
+@import "@fontsource-variable/manrope";
+@import "./fonts/syne-600.woff2.css";
+
+@custom-variant dark (&:is(.dark *));
+
+@theme inline {
+ --font-sans: "Manrope Variable", "Manrope", ui-sans-serif, system-ui, sans-serif;
+ --font-display: "Syne", "Manrope Variable", ui-sans-serif, system-ui, sans-serif;
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --radius-lg: var(--radius);
+}
+
+:root {
+ --radius: 0.75rem;
+ --background: oklch(0.985 0.004 260);
+ --foreground: oklch(0.2 0.02 260);
+ --primary: oklch(0.46 0.14 264);
+ --primary-foreground: oklch(0.99 0.005 264);
+ --secondary: oklch(0.945 0.012 260);
+ --secondary-foreground: oklch(0.28 0.03 260);
+ --muted: oklch(0.955 0.008 260);
+ --muted-foreground: oklch(0.48 0.025 260);
+ --accent: oklch(0.94 0.015 260);
+ --accent-foreground: oklch(0.26 0.03 260);
+ --destructive: oklch(0.55 0.2 25);
+ --border: oklch(0.9 0.01 260);
+ --input: oklch(0.9 0.01 260);
+ --ring: oklch(0.46 0.14 264);
+}
+
+.dark {
+ --background: oklch(0.16 0.015 260);
+ --foreground: oklch(0.96 0.008 260);
+ --primary: oklch(0.72 0.11 264);
+ --primary-foreground: oklch(0.16 0.02 260);
+ --secondary: oklch(0.24 0.02 260);
+ --secondary-foreground: oklch(0.94 0.008 260);
+ --muted: oklch(0.24 0.02 260);
+ --muted-foreground: oklch(0.68 0.025 260);
+ --accent: oklch(0.26 0.025 260);
+ --accent-foreground: oklch(0.94 0.008 260);
+ --destructive: oklch(0.65 0.17 22);
+ --border: oklch(1 0 0 / 11%);
+ --input: oklch(1 0 0 / 14%);
+ --ring: oklch(0.72 0.11 264);
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ html {
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+ }
+ html::-webkit-scrollbar {
+ display: none;
+ }
+ body {
+ @apply bg-background text-foreground font-sans antialiased;
+ }
+}
+
+@layer components {
+ .shell-atmosphere {
+ position: relative;
+ isolation: isolate;
+ overflow-x: clip;
+ }
+
+ .shell-atmosphere::before {
+ content: "";
+ pointer-events: none;
+ position: fixed;
+ inset: 0;
+ z-index: -2;
+ background:
+ radial-gradient(ellipse 70% 45% at 50% -15%, oklch(0.72 0.06 264 / 0.12), transparent 60%),
+ linear-gradient(180deg, var(--background), oklch(0.975 0.006 260));
+ }
+
+ .dark .shell-atmosphere::before {
+ background:
+ radial-gradient(ellipse 65% 40% at 50% -10%, oklch(0.45 0.08 264 / 0.18), transparent 55%),
+ linear-gradient(180deg, var(--background), oklch(0.14 0.015 260));
+ }
+
+ .brand-mark {
+ @apply bg-primary/10 text-primary dark:bg-primary/15;
+ }
+
+ .eyebrow {
+ @apply mb-3 text-[11px] font-semibold tracking-[0.22em] text-primary uppercase;
+ }
+
+ .page-hero {
+ @apply relative mb-14 space-y-4 pb-12 text-center sm:mb-16 sm:pb-14;
+ }
+
+ .page-hero::after {
+ content: "";
+ position: absolute;
+ left: 50%;
+ bottom: 0;
+ width: min(8rem, 32%);
+ height: 2px;
+ transform: translateX(-50%);
+ border-radius: 999px;
+ background: var(--border);
+ }
+
+ .display-title {
+ font-family: var(--font-display);
+ @apply text-5xl font-semibold tracking-tight sm:text-6xl;
+ }
+
+ .gradient-text {
+ background: linear-gradient(135deg, var(--foreground) 30%, var(--primary) 100%);
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+ }
+
+ .feature-pill {
+ @apply inline-flex items-center gap-2 rounded-full border border-border bg-background/60 px-3.5 py-1.5 text-sm text-muted-foreground backdrop-blur-sm;
+ }
+
+ .surface-panel {
+ @apply rounded-xl border border-border/80 bg-background/50 p-5 backdrop-blur-sm;
+ }
+
+ .terminal-block {
+ @apply overflow-hidden rounded-xl border border-border/80 bg-background/50 backdrop-blur-sm;
+ }
+
+ .terminal-header {
+ @apply flex items-center gap-1.5 border-b border-border/70 px-4 py-2.5;
+ }
+
+ .terminal-dot {
+ @apply size-2 rounded-full bg-border;
+ }
+
+ .terminal-body {
+ @apply space-y-0 px-4 py-3;
+ }
+
+ .terminal-example {
+ @apply space-y-1 py-3;
+ }
+
+ .terminal-example + .terminal-example {
+ @apply border-t border-border/60;
+ }
+
+ .example-tag {
+ @apply mb-1 inline-block rounded-md bg-primary/10 px-2 py-0.5 text-[11px] font-medium text-primary;
+ }
+
+ .section-gap {
+ @apply space-y-8 border-t border-border pt-12;
+ }
+
+ .field-block {
+ @apply space-y-4;
+ }
+}
+
+.page-enter-active {
+ transition: opacity 150ms cubic-bezier(0.22, 1, 0.36, 1);
+}
+
+.page-enter-from {
+ opacity: 0;
+}
+
+.fade-enter-active,
+.fade-leave-active {
+ transition:
+ opacity 180ms cubic-bezier(0.22, 1, 0.36, 1),
+ transform 180ms cubic-bezier(0.22, 1, 0.36, 1);
+}
+
+.fade-enter-from,
+.fade-leave-to {
+ opacity: 0;
+ transform: translateY(4px);
+}
+
+.menu-enter-active,
+.menu-leave-active {
+ transition:
+ opacity 160ms cubic-bezier(0.22, 1, 0.36, 1),
+ transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
+}
+
+.menu-enter-from,
+.menu-leave-to {
+ opacity: 0;
+ transform: translateY(-4px);
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .page-enter-active,
+ .fade-enter-active,
+ .fade-leave-active,
+ .menu-enter-active,
+ .menu-leave-active {
+ transition: none !important;
+ }
+ .page-enter-from,
+ .fade-enter-from,
+ .fade-leave-to,
+ .menu-enter-from,
+ .menu-leave-to {
+ opacity: 1;
+ transform: none;
+ }
+}
diff --git a/web/tsconfig.app.json b/web/tsconfig.app.json
new file mode 100644
index 0000000..da00ac4
--- /dev/null
+++ b/web/tsconfig.app.json
@@ -0,0 +1,18 @@
+{
+ "extends": "@vue/tsconfig/tsconfig.dom.json",
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "types": ["vite/client"],
+ "allowArbitraryExtensions": true,
+ "ignoreDeprecations": "6.0",
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ },
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
+}
diff --git a/web/tsconfig.json b/web/tsconfig.json
new file mode 100644
index 0000000..fec8c8e
--- /dev/null
+++ b/web/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ],
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ }
+}
diff --git a/web/tsconfig.node.json b/web/tsconfig.node.json
new file mode 100644
index 0000000..8455dcb
--- /dev/null
+++ b/web/tsconfig.node.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023"],
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "module": "nodenext",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/web/vite.config.ts b/web/vite.config.ts
new file mode 100644
index 0000000..c760f73
--- /dev/null
+++ b/web/vite.config.ts
@@ -0,0 +1,23 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import tailwindcss from '@tailwindcss/vite'
+import path from 'node:path'
+
+export default defineConfig({
+ plugins: [vue(), tailwindcss()],
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
+ },
+ },
+ build: {
+ outDir: '../src/dist',
+ emptyOutDir: true,
+ sourcemap: false,
+ },
+ server: {
+ proxy: {
+ '/api': 'http://127.0.0.1:5000',
+ },
+ },
+})