diff --git a/.github/workflows/cd-deploy-docs.yml b/.github/workflows/cd-deploy-docs.yml
index bf8a1e8..d2f7ae1 100644
--- a/.github/workflows/cd-deploy-docs.yml
+++ b/.github/workflows/cd-deploy-docs.yml
@@ -4,7 +4,7 @@ on:
workflow_dispatch:
inputs:
version:
- description: 'Version (e.g. v1.0, v1.1)'
+ description: 'Version (e.g. v1.0, v1.1, v2.0.0-beta.1)'
required: true
concurrency:
@@ -49,7 +49,7 @@ jobs:
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/zip" \
--data-binary @$GITHUB_WORKSPACE/docs.zip \
- "${URL}/api/products/shelf/versions/${VER}"
+ "${URL}/_api/products/shelf/versions/${VER}"
- name: Deployment summary
run: |
diff --git a/.github/workflows/cd-deploy-production.yml b/.github/workflows/cd-deploy-production.yml
index 74e0569..d5ce22a 100644
--- a/.github/workflows/cd-deploy-production.yml
+++ b/.github/workflows/cd-deploy-production.yml
@@ -29,8 +29,8 @@ jobs:
exit 1
fi
- if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
- echo "::error ::Invalid release version format. Expected X.Y.Z (e.g., 1.2.3), got: $VERSION"
+ if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-.+)?$ ]]; then
+ echo "::error ::Invalid release version format. Expected X.Y.Z or X.Y.Z-label (e.g., 1.2.3, 1.2.3-beta.1), got: $VERSION"
exit 1
fi
@@ -69,6 +69,15 @@ jobs:
--password ${{ secrets.PERSONAL_PACKAGES_TOKEN }} \
--store-password-in-clear-text
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+
+ - name: Build Admin UI
+ working-directory: ./src/Cocoar.Shelf.Client
+ run: npm ci && npm run build
+
- name: Restore dependencies
run: dotnet restore Cocoar.Shelf.slnx
working-directory: ./src
@@ -196,9 +205,15 @@ jobs:
env:
VERSION: ${{ needs.validate-version.outputs.version }}
run: |
- MAJOR=$(echo "$VERSION" | cut -d. -f1)
- MINOR=$(echo "$VERSION" | cut -d. -f2)
- echo "value=v${MAJOR}.${MINOR}" >> $GITHUB_OUTPUT
+ if [[ "$VERSION" == *-* ]]; then
+ # Pre-release: use full version (e.g. v6.0.0-beta.1)
+ echo "value=v${VERSION}" >> $GITHUB_OUTPUT
+ else
+ # Stable: use major.minor (e.g. v5.2)
+ MAJOR=$(echo "$VERSION" | cut -d. -f1)
+ MINOR=$(echo "$VERSION" | cut -d. -f2)
+ echo "value=v${MAJOR}.${MINOR}" >> $GITHUB_OUTPUT
+ fi
- name: Upload to Shelf
env:
@@ -211,7 +226,7 @@ jobs:
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/zip" \
--data-binary @$GITHUB_WORKSPACE/docs.zip \
- "${URL}/api/products/shelf/versions/${VER}"
+ "${URL}/_api/products/shelf/versions/${VER}"
- name: Docs deployment summary
run: |
diff --git a/.github/workflows/cd-deploy-staging.yml b/.github/workflows/cd-deploy-staging.yml
index b3874b9..010ec8a 100644
--- a/.github/workflows/cd-deploy-staging.yml
+++ b/.github/workflows/cd-deploy-staging.yml
@@ -75,6 +75,15 @@ jobs:
--password ${{ secrets.PERSONAL_PACKAGES_TOKEN }} \
--store-password-in-clear-text
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+
+ - name: Build Admin UI
+ working-directory: ./src/Cocoar.Shelf.Client
+ run: npm ci && npm run build
+
- name: Restore dependencies
run: dotnet restore Cocoar.Shelf.slnx
working-directory: ./src
diff --git a/.github/workflows/ci-develop.yml b/.github/workflows/ci-develop.yml
index f8fc836..e7e12ae 100644
--- a/.github/workflows/ci-develop.yml
+++ b/.github/workflows/ci-develop.yml
@@ -48,6 +48,15 @@ jobs:
- name: Log version
run: echo "Building version ${{ steps.gv.outputs.SemVer }}"
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+
+ - name: Build Admin UI
+ working-directory: ./src/Cocoar.Shelf.Client
+ run: npm ci && npm run build
+
- name: Restore dependencies
run: dotnet restore Cocoar.Shelf.slnx
working-directory: ./src
diff --git a/.github/workflows/ci-pr-validation.yml b/.github/workflows/ci-pr-validation.yml
index 381a4ac..7cdce15 100644
--- a/.github/workflows/ci-pr-validation.yml
+++ b/.github/workflows/ci-pr-validation.yml
@@ -48,6 +48,15 @@ jobs:
- name: Log version
run: echo "Building version ${{ steps.gv.outputs.SemVer }}"
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+
+ - name: Build Admin UI
+ working-directory: ./src/Cocoar.Shelf.Client
+ run: npm ci && npm run build
+
- name: Restore dependencies
run: dotnet restore Cocoar.Shelf.slnx
working-directory: ./src
diff --git a/.gitignore b/.gitignore
index f5cf5ed..4b358f9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -57,6 +57,13 @@ nul
local/
.local/
+# SPA build output (generated by Vite)
+src/Cocoar.Shelf/wwwroot/assets/
+src/Cocoar.Shelf/wwwroot/index.html
+
+# Client dependencies
+src/Cocoar.Shelf.Client/node_modules/
+
# VitePress
website/.vitepress/cache/
website/.vitepress/dist/
diff --git a/Dockerfile b/Dockerfile
index c3df8c2..2810563 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,3 +1,10 @@
+FROM node:22-alpine AS client-build
+WORKDIR /client
+COPY src/Cocoar.Shelf.Client/package.json src/Cocoar.Shelf.Client/package-lock.json ./
+RUN npm ci
+COPY src/Cocoar.Shelf.Client/ .
+RUN npx vite build --outDir /client/dist
+
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
@@ -5,6 +12,7 @@ COPY src/Cocoar.Shelf/Cocoar.Shelf.csproj Cocoar.Shelf/
RUN dotnet restore Cocoar.Shelf/Cocoar.Shelf.csproj
COPY src/ .
+COPY --from=client-build /client/dist/ Cocoar.Shelf/wwwroot/
RUN dotnet publish Cocoar.Shelf/Cocoar.Shelf.csproj -c Release -o /app/publish
FROM mcr.microsoft.com/dotnet/aspnet:10.0
diff --git a/local-config/products/configuration.json b/local-config/products/configuration.json
new file mode 100644
index 0000000..873bec9
--- /dev/null
+++ b/local-config/products/configuration.json
@@ -0,0 +1,6 @@
+{
+ "name": "configuration",
+ "displayName": "Cocoar.Configuration",
+ "description": "Reactive configuration for .NET",
+ "source": "upload"
+}
diff --git a/src/Cocoar.Shelf.Client/index.html b/src/Cocoar.Shelf.Client/index.html
new file mode 100644
index 0000000..31c8beb
--- /dev/null
+++ b/src/Cocoar.Shelf.Client/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+ Shelf
+
+
+
+
+
+ Are you an LLM? View /llms.txt for a machine-readable index of all documentation products and links to their full LLM documentation.
+
+
+
diff --git a/src/Cocoar.Shelf.Client/package-lock.json b/src/Cocoar.Shelf.Client/package-lock.json
new file mode 100644
index 0000000..b96019b
--- /dev/null
+++ b/src/Cocoar.Shelf.Client/package-lock.json
@@ -0,0 +1,2364 @@
+{
+ "name": "@cocoar/shelf-admin",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "@cocoar/shelf-admin",
+ "version": "0.0.0",
+ "dependencies": {
+ "@cocoar/vue-ui": "0.1.0-beta.25",
+ "overlayscrollbars": "^2.14.0",
+ "pinia": "^2.3.1",
+ "vue": "^3.5.28",
+ "vue-router": "^4.5.0"
+ },
+ "devDependencies": {
+ "@tailwindcss/postcss": "^4.2.0",
+ "@vitejs/plugin-vue": "^6.0.0",
+ "@vue/tsconfig": "^0.7.0",
+ "lucide-static": "^0.577.0",
+ "tailwindcss": "^4.2.0",
+ "typescript": "~5.9.0",
+ "vite": "^7.3.0",
+ "vue-tsc": "^2.2.0"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@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/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@cocoar/vue-fragment-parser": {
+ "version": "0.1.0-beta.25",
+ "resolved": "https://registry.npmjs.org/@cocoar/vue-fragment-parser/-/vue-fragment-parser-0.1.0-beta.25.tgz",
+ "integrity": "sha512-wy1DMw+XiFTbls5KSxdbH0E2bNHHqPWbsCfIdgDLdQDFZxGEwxuPSJQZkrc89s2Jf/+ih2ETWTGG15RTkXx0aA==",
+ "dependencies": {
+ "path-to-regexp": "^8.3.0"
+ }
+ },
+ "node_modules/@cocoar/vue-localization": {
+ "version": "0.1.0-beta.25",
+ "resolved": "https://registry.npmjs.org/@cocoar/vue-localization/-/vue-localization-0.1.0-beta.25.tgz",
+ "integrity": "sha512-OIvrRN+OE3i9Jgu7PcTXp62c/GBuqkJWR0rvBCIT6v8R56JdVIeDoqPJtG7RS0rbXKDoD9TqA0R0BCww5uwQbA==",
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
+ "node_modules/@cocoar/vue-ui": {
+ "version": "0.1.0-beta.25",
+ "resolved": "https://registry.npmjs.org/@cocoar/vue-ui/-/vue-ui-0.1.0-beta.25.tgz",
+ "integrity": "sha512-1nKV6JdnpZTDJ2cHbj5QMQxsmfarF0bvjQiOmOtKTw68ZK7lzldGW9bu+nh2Tc17GqqENlLqT1UjTMaHM3IMPQ==",
+ "dependencies": {
+ "@cocoar/vue-fragment-parser": "0.1.0-beta.25",
+ "@cocoar/vue-localization": "0.1.0-beta.25",
+ "@js-temporal/polyfill": "^0.5.1",
+ "@maskito/core": "^5.1.1",
+ "@maskito/kit": "^5.1.1",
+ "@maskito/vue": "^5.1.1",
+ "prismjs": "^1.30.0"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
+ "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz",
+ "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz",
+ "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz",
+ "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz",
+ "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz",
+ "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz",
+ "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz",
+ "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz",
+ "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz",
+ "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz",
+ "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz",
+ "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz",
+ "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz",
+ "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz",
+ "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz",
+ "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz",
+ "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz",
+ "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz",
+ "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz",
+ "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz",
+ "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz",
+ "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz",
+ "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz",
+ "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz",
+ "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz",
+ "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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/@js-temporal/polyfill": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/@js-temporal/polyfill/-/polyfill-0.5.1.tgz",
+ "integrity": "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==",
+ "license": "ISC",
+ "dependencies": {
+ "jsbi": "^4.3.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@maskito/core": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@maskito/core/-/core-5.1.2.tgz",
+ "integrity": "sha512-eoeQ41uDu9AuhFQDzAPTNTr5VM+hMpRsrJjtHzCH3FM7u+/mOGLgtEeGE1+5Up5UCtY7h/N1hPaZ/qT5mcNWXQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@maskito/kit": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@maskito/kit/-/kit-5.1.2.tgz",
+ "integrity": "sha512-inVxaa36dLQp1NQ/a5dM791qgDZUulPDs299pS6KNXKN7wrisybSIoRVrpjoZt/QIe2TMtku313sBgtf2LhFAQ==",
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "@maskito/core": "^5.1.2"
+ }
+ },
+ "node_modules/@maskito/vue": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@maskito/vue/-/vue-5.1.2.tgz",
+ "integrity": "sha512-HDkmxeIMWb+Nt/3duDQ+HvndILmA1sBhZT4hc5T+ClbP6k32txStvtAB20NZd/lYoa4J7Nvn666MH5wqJY7bdg==",
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "@maskito/core": "^5.1.2",
+ "vue": ">=3.0.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz",
+ "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.59.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.1.tgz",
+ "integrity": "sha512-xB0b51TB7IfDEzAojXahmr+gfA00uYVInJGgNNkeQG6RPnCPGr7udsylFLTubuIUSRE6FkcI1NElyRt83PP5oQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.59.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.1.tgz",
+ "integrity": "sha512-XOjPId0qwSDKHaIsdzHJtKCxX0+nH8MhBwvrNsT7tVyKmdTx1jJ4XzN5RZXCdTzMpufLb+B8llTC0D8uCrLhcw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.59.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.1.tgz",
+ "integrity": "sha512-vQuRd28p0gQpPrS6kppd8IrWmFo42U8Pz1XLRjSZXq5zCqyMDYFABT7/sywL11mO1EL10Qhh7MVPEwkG8GiBeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.59.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.1.tgz",
+ "integrity": "sha512-x6VG6U29+Ivlnajrg1IHdzXeAwSoEHBFVO+CtC9Brugx6de712CUJobRUxsIA0KYrQvCmzNrMPFTT1A4CCqNTg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.59.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.1.tgz",
+ "integrity": "sha512-Sgi0Uo6t1YCHJMNO3Y8+bm+SvOanUGkoZKn/VJPwYUe2kp31X5KnXmzKd/NjW8iA3gFcfNZ64zh14uOGrIllCQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.59.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.1.tgz",
+ "integrity": "sha512-AM4xnwEZwukdhk7laMWfzWu9JGSVnJd+Fowt6Fd7QW1nrf3h0Hp7Qx5881M4aqrUlKBCybOxz0jofvIIfl7C5g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.59.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.1.tgz",
+ "integrity": "sha512-KUizqxpwaR2AZdAUsMWfL/C94pUu7TKpoPd88c8yFVixJ+l9hejkrwoK5Zj3wiNh65UeyryKnJyxL1b7yNqFQA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.59.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.1.tgz",
+ "integrity": "sha512-MZoQ/am77ckJtZGFAtPucgUuJWiop3m2R3lw7tC0QCcbfl4DRhQUBUkHWCkcrT3pqy5Mzv5QQgY6Dmlba6iTWg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.59.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.1.tgz",
+ "integrity": "sha512-Sez95TP6xGjkWB1608EfhCX1gdGrO5wzyN99VqzRtC17x/1bhw5VU1V0GfKUwbW/Xr1J8mSasoFoJa6Y7aGGSA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.59.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.1.tgz",
+ "integrity": "sha512-9Cs2Seq98LWNOJzR89EGTZoiP8EkZ9UbQhBlDgfAkM6asVna1xJ04W2CLYWDN/RpUgOjtQvcv8wQVi1t5oQazA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.59.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.1.tgz",
+ "integrity": "sha512-n9yqttftgFy7IrNEnHy1bOp6B4OSe8mJDiPkT7EqlM9FnKOwUMnCK62ixW0Kd9Clw0/wgvh8+SqaDXMFvw3KqQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.59.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.1.tgz",
+ "integrity": "sha512-SfpNXDzVTqs/riak4xXcLpq5gIQWsqGWMhN1AGRQKB4qGSs4r0sEs3ervXPcE1O9RsQ5bm8Muz6zmQpQnPss1g==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.59.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.1.tgz",
+ "integrity": "sha512-LjaChED0wQnjKZU+tsmGbN+9nN1XhaWUkAlSbTdhpEseCS4a15f/Q8xC2BN4GDKRzhhLZpYtJBZr2NZhR0jvNw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.59.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.1.tgz",
+ "integrity": "sha512-ojW7iTJSIs4pwB2xV6QXGwNyDctvXOivYllttuPbXguuKDX5vwpqYJsHc6D2LZzjDGHML414Tuj3LvVPe1CT1A==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.59.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.1.tgz",
+ "integrity": "sha512-FP+Q6WTcxxvsr0wQczhSE+tOZvFPV8A/mUE6mhZYFW9/eea/y/XqAgRoLLMuE9Cz0hfX5bi7p116IWoB+P237A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.59.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.1.tgz",
+ "integrity": "sha512-L1uD9b/Ig8Z+rn1KttCJjwhN1FgjRMBKsPaBsDKkfUl7GfFq71pU4vWCnpOsGljycFEbkHWARZLf4lMYg3WOLw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.59.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.1.tgz",
+ "integrity": "sha512-EZc9NGTk/oSUzzOD4nYY4gIjteo2M3CiozX6t1IXGCOdgxJTlVu/7EdPeiqeHPSIrxkLhavqpBAUCfvC6vBOug==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.59.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.1.tgz",
+ "integrity": "sha512-NQ9KyU1Anuy59L8+HHOKM++CoUxrQWrZWXRik4BJFm+7i5NP6q/SW43xIBr80zzt+PDBJ7LeNmloQGfa0JGk0w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.59.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.1.tgz",
+ "integrity": "sha512-GZkLk2t6naywsveSFBsEb0PLU+JC9ggVjbndsbG20VPhar6D1gkMfCx4NfP9owpovBXTN+eRdqGSkDGIxPHhmQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.59.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.1.tgz",
+ "integrity": "sha512-1hjG9Jpl2KDOetr64iQd8AZAEjkDUUK5RbDkYWsViYLC1op1oNzdjMJeFiofcGhqbNTaY2kfgqowE7DILifsrA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.59.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.1.tgz",
+ "integrity": "sha512-ARoKfflk0SiiYm3r1fmF73K/yB+PThmOwfWCk1sr7x/k9dc3uGLWuEE9if+Pw21el8MSpp3TMnG5vLNsJ/MMGQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.59.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.1.tgz",
+ "integrity": "sha512-oOST61G6VM45Mz2vdzWMr1s2slI7y9LqxEV5fCoWi2MDONmMvgsJVHSXxce/I2xOSZPTZ47nDPOl1tkwKWSHcw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.59.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.1.tgz",
+ "integrity": "sha512-x5WgLi5dWpRz7WclKBGEF15LcWTh0ewrHM6Cq4A+WUbkysUMZNeqt05bwPonOQ3ihPS/WMhAZV5zB1DfnI4Sxg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.59.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.1.tgz",
+ "integrity": "sha512-wS+zHAJRVP5zOL0e+a3V3E/NTEwM2HEvvNKoDy5Xcfs0o8lljxn+EAFPkUsxihBdmDq1JWzXmmB9cbssCPdxxw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.59.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.1.tgz",
+ "integrity": "sha512-rhHyrMeLpErT/C7BxcEsU4COHQUzHyrPYW5tOZUeUhziNtRuYxmDWvqQqzpuUt8xpOgmbKa1btGXfnA/ANVO+g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz",
+ "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "enhanced-resolve": "^5.19.0",
+ "jiti": "^2.6.1",
+ "lightningcss": "1.32.0",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.2.2"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz",
+ "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.2.2",
+ "@tailwindcss/oxide-darwin-arm64": "4.2.2",
+ "@tailwindcss/oxide-darwin-x64": "4.2.2",
+ "@tailwindcss/oxide-freebsd-x64": "4.2.2",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.2.2",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.2.2",
+ "@tailwindcss/oxide-linux-x64-musl": "4.2.2",
+ "@tailwindcss/oxide-wasm32-wasi": "4.2.2",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.2.2"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz",
+ "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz",
+ "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz",
+ "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz",
+ "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz",
+ "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz",
+ "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz",
+ "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz",
+ "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz",
+ "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz",
+ "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==",
+ "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.8.1",
+ "@emnapi/runtime": "^1.8.1",
+ "@emnapi/wasi-threads": "^1.1.0",
+ "@napi-rs/wasm-runtime": "^1.1.1",
+ "@tybys/wasm-util": "^0.10.1",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz",
+ "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz",
+ "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/postcss": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.2.tgz",
+ "integrity": "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "@tailwindcss/node": "4.2.2",
+ "@tailwindcss/oxide": "4.2.2",
+ "postcss": "^8.5.6",
+ "tailwindcss": "4.2.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vitejs/plugin-vue": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.5.tgz",
+ "integrity": "sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "1.0.0-rc.2"
+ },
+ "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.15",
+ "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz",
+ "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/source-map": "2.4.15"
+ }
+ },
+ "node_modules/@volar/source-map": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz",
+ "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@volar/typescript": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz",
+ "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.15",
+ "path-browserify": "^1.0.1",
+ "vscode-uri": "^3.0.8"
+ }
+ },
+ "node_modules/@vue/compiler-core": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz",
+ "integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@vue/shared": "3.5.30",
+ "entities": "^7.0.1",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz",
+ "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.30",
+ "@vue/shared": "3.5.30"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz",
+ "integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@vue/compiler-core": "3.5.30",
+ "@vue/compiler-dom": "3.5.30",
+ "@vue/compiler-ssr": "3.5.30",
+ "@vue/shared": "3.5.30",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.21",
+ "postcss": "^8.5.8",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz",
+ "integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.30",
+ "@vue/shared": "3.5.30"
+ }
+ },
+ "node_modules/@vue/compiler-vue2": {
+ "version": "2.7.16",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
+ "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "de-indent": "^1.0.2",
+ "he": "^1.2.0"
+ }
+ },
+ "node_modules/@vue/devtools-api": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/language-core": {
+ "version": "2.2.12",
+ "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz",
+ "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.15",
+ "@vue/compiler-dom": "^3.5.0",
+ "@vue/compiler-vue2": "^2.7.16",
+ "@vue/shared": "^3.5.0",
+ "alien-signals": "^1.0.3",
+ "minimatch": "^9.0.3",
+ "muggle-string": "^0.4.1",
+ "path-browserify": "^1.0.1"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz",
+ "integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/shared": "3.5.30"
+ }
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz",
+ "integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.30",
+ "@vue/shared": "3.5.30"
+ }
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz",
+ "integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.30",
+ "@vue/runtime-core": "3.5.30",
+ "@vue/shared": "3.5.30",
+ "csstype": "^3.2.3"
+ }
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz",
+ "integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.30",
+ "@vue/shared": "3.5.30"
+ },
+ "peerDependencies": {
+ "vue": "3.5.30"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.30.tgz",
+ "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/tsconfig": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.7.0.tgz",
+ "integrity": "sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "typescript": "5.x",
+ "vue": "^3.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ },
+ "vue": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/alien-signals": {
+ "version": "1.0.13",
+ "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
+ "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/de-indent": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
+ "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/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.20.1",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz",
+ "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/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/esbuild": {
+ "version": "0.27.4",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
+ "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.4",
+ "@esbuild/android-arm": "0.27.4",
+ "@esbuild/android-arm64": "0.27.4",
+ "@esbuild/android-x64": "0.27.4",
+ "@esbuild/darwin-arm64": "0.27.4",
+ "@esbuild/darwin-x64": "0.27.4",
+ "@esbuild/freebsd-arm64": "0.27.4",
+ "@esbuild/freebsd-x64": "0.27.4",
+ "@esbuild/linux-arm": "0.27.4",
+ "@esbuild/linux-arm64": "0.27.4",
+ "@esbuild/linux-ia32": "0.27.4",
+ "@esbuild/linux-loong64": "0.27.4",
+ "@esbuild/linux-mips64el": "0.27.4",
+ "@esbuild/linux-ppc64": "0.27.4",
+ "@esbuild/linux-riscv64": "0.27.4",
+ "@esbuild/linux-s390x": "0.27.4",
+ "@esbuild/linux-x64": "0.27.4",
+ "@esbuild/netbsd-arm64": "0.27.4",
+ "@esbuild/netbsd-x64": "0.27.4",
+ "@esbuild/openbsd-arm64": "0.27.4",
+ "@esbuild/openbsd-x64": "0.27.4",
+ "@esbuild/openharmony-arm64": "0.27.4",
+ "@esbuild/sunos-x64": "0.27.4",
+ "@esbuild/win32-arm64": "0.27.4",
+ "@esbuild/win32-ia32": "0.27.4",
+ "@esbuild/win32-x64": "0.27.4"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
+ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/jsbi": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.2.tgz",
+ "integrity": "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "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-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "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-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "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-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/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,
+ "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.npmjs.org/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.npmjs.org/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-static": {
+ "version": "0.577.0",
+ "resolved": "https://registry.npmjs.org/lucide-static/-/lucide-static-0.577.0.tgz",
+ "integrity": "sha512-hx39J5Tq4JWF2ALY+5YRg+SxQLpeAmLJDXNcqiBJH/UuVwp43it9fyki/onZO7AVFgG5ZbB+fWwZR9mwGHE2XQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/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/minimatch": {
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/muggle-string": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
+ "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "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/overlayscrollbars": {
+ "version": "2.14.0",
+ "resolved": "https://registry.npmjs.org/overlayscrollbars/-/overlayscrollbars-2.14.0.tgz",
+ "integrity": "sha512-RjV0pqc79kYhQLC3vTcLRb5GLpI1n6qh0Oua3g+bGH4EgNOJHVBGP7u0zZtxoAa0dkHlAqTTSYRb9MMmxNLjig==",
+ "license": "MIT"
+ },
+ "node_modules/path-browserify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/path-to-regexp": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
+ "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pinia": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz",
+ "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-api": "^6.6.3",
+ "vue-demi": "^0.14.10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.4.4",
+ "vue": "^2.7.0 || ^3.5.11"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.8",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
+ "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+ "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.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prismjs": {
+ "version": "1.30.0",
+ "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
+ "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.59.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.1.tgz",
+ "integrity": "sha512-iZKH8BeoCwTCBTZBZWQQMreekd4mdomwdjIQ40GC1oZm6o+8PnNMIxFOiCsGMWeS8iDJ7KZcl7KwmKk/0HOQpA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.59.1",
+ "@rollup/rollup-android-arm64": "4.59.1",
+ "@rollup/rollup-darwin-arm64": "4.59.1",
+ "@rollup/rollup-darwin-x64": "4.59.1",
+ "@rollup/rollup-freebsd-arm64": "4.59.1",
+ "@rollup/rollup-freebsd-x64": "4.59.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.59.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.59.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.59.1",
+ "@rollup/rollup-linux-arm64-musl": "4.59.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.59.1",
+ "@rollup/rollup-linux-loong64-musl": "4.59.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.59.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.59.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.59.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.59.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.59.1",
+ "@rollup/rollup-linux-x64-gnu": "4.59.1",
+ "@rollup/rollup-linux-x64-musl": "4.59.1",
+ "@rollup/rollup-openbsd-x64": "4.59.1",
+ "@rollup/rollup-openharmony-arm64": "4.59.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.59.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.59.1",
+ "@rollup/rollup-win32-x64-gnu": "4.59.1",
+ "@rollup/rollup-win32-x64-msvc": "4.59.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/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/tailwindcss": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz",
+ "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
+ "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
+ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.27.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "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",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.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
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "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.npmjs.org/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.30",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz",
+ "integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.30",
+ "@vue/compiler-sfc": "3.5.30",
+ "@vue/runtime-dom": "3.5.30",
+ "@vue/server-renderer": "3.5.30",
+ "@vue/shared": "3.5.30"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-demi": {
+ "version": "0.14.10",
+ "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "vue-demi-fix": "bin/vue-demi-fix.js",
+ "vue-demi-switch": "bin/vue-demi-switch.js"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "@vue/composition-api": "^1.0.0-rc.1",
+ "vue": "^3.0.0-0 || ^2.6.0"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-router": {
+ "version": "4.6.4",
+ "resolved": "https://registry.npmjs.org/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": "2.2.12",
+ "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz",
+ "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/typescript": "2.4.15",
+ "@vue/language-core": "2.2.12"
+ },
+ "bin": {
+ "vue-tsc": "bin/vue-tsc.js"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.0.0"
+ }
+ }
+ }
+}
diff --git a/src/Cocoar.Shelf.Client/package.json b/src/Cocoar.Shelf.Client/package.json
new file mode 100644
index 0000000..fd355fb
--- /dev/null
+++ b/src/Cocoar.Shelf.Client/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "@cocoar/shelf-admin",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vue-tsc --noEmit && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@cocoar/vue-ui": "0.1.0-beta.25",
+ "overlayscrollbars": "^2.14.0",
+ "pinia": "^2.3.1",
+ "vue": "^3.5.28",
+ "vue-router": "^4.5.0"
+ },
+ "devDependencies": {
+ "@tailwindcss/postcss": "^4.2.0",
+ "@vitejs/plugin-vue": "^6.0.0",
+ "@vue/tsconfig": "^0.7.0",
+ "lucide-static": "^0.577.0",
+ "tailwindcss": "^4.2.0",
+ "typescript": "~5.9.0",
+ "vite": "^7.3.0",
+ "vue-tsc": "^2.2.0"
+ }
+}
diff --git a/src/Cocoar.Shelf.Client/src/App.vue b/src/Cocoar.Shelf.Client/src/App.vue
new file mode 100644
index 0000000..68eef6f
--- /dev/null
+++ b/src/Cocoar.Shelf.Client/src/App.vue
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/src/Cocoar.Shelf.Client/src/cocoar-modules.d.ts b/src/Cocoar.Shelf.Client/src/cocoar-modules.d.ts
new file mode 100644
index 0000000..5bf0a15
--- /dev/null
+++ b/src/Cocoar.Shelf.Client/src/cocoar-modules.d.ts
@@ -0,0 +1,29 @@
+declare module '@cocoar/vue-ui' {
+ import type { Plugin, Component } from 'vue';
+
+ export const CoarIconPlugin: Plugin;
+ export const CoarOverlayPlugin: Plugin;
+ export const CORE_ICONS: unknown;
+
+ export class CoarHttpIconSource {
+ constructor(resolver: (name: string) => string);
+ }
+
+ export const CoarButton: Component;
+ export const CoarCard: Component;
+ export const CoarCheckbox: Component;
+ export const CoarIcon: Component;
+ export const CoarNote: Component;
+ export const CoarOverlayHost: Component;
+ export const CoarSelect: Component;
+ export const CoarSpinner: Component;
+ export const CoarTable: Component;
+ export const CoarTag: Component;
+ export const CoarTextInput: Component;
+}
+
+declare module '@cocoar/vue-ui/styles' {}
+
+declare interface Window {
+ __SHELF_OPTIONS__: { pathBase: string };
+}
diff --git a/src/Cocoar.Shelf.Client/src/composables/useUI.ts b/src/Cocoar.Shelf.Client/src/composables/useUI.ts
new file mode 100644
index 0000000..6bf40ac
--- /dev/null
+++ b/src/Cocoar.Shelf.Client/src/composables/useUI.ts
@@ -0,0 +1,86 @@
+import { reactive } from 'vue';
+
+export interface UIButton {
+ text?: string;
+ disabled?: boolean;
+ loading?: boolean;
+ visible?: boolean;
+ onClick?: () => void;
+}
+
+export interface UIHeader {
+ show: boolean;
+ title?: string;
+ subTitle?: string;
+ icon?: string;
+}
+
+export interface UIContent {
+ scrollable: boolean;
+ showLoadingBar: boolean;
+ container: boolean;
+ padding: boolean;
+}
+
+export interface UIFooter {
+ show: boolean;
+ button1: UIButton;
+ button2: UIButton;
+ button3: UIButton;
+}
+
+export interface UIContext {
+ header: UIHeader;
+ content: UIContent;
+ footer: UIFooter;
+}
+
+function createDefaults(): UIContext {
+ return {
+ header: {
+ show: true,
+ title: undefined,
+ subTitle: undefined,
+ icon: undefined,
+ },
+ content: {
+ scrollable: true,
+ showLoadingBar: false,
+ container: true,
+ padding: true,
+ },
+ footer: {
+ show: false,
+ button1: { visible: false, disabled: false, loading: false },
+ button2: { visible: false, disabled: false, loading: false },
+ button3: { visible: false, disabled: false, loading: false },
+ },
+ };
+}
+
+const state = reactive(createDefaults());
+
+export function useUI() {
+ function set(fn: (ctx: UIContext) => void) {
+ const defaults = createDefaults();
+ Object.assign(state.header, defaults.header);
+ Object.assign(state.content, defaults.content);
+ Object.assign(state.footer.button1, defaults.footer.button1);
+ Object.assign(state.footer.button2, defaults.footer.button2);
+ Object.assign(state.footer.button3, defaults.footer.button3);
+ state.footer.show = defaults.footer.show;
+ fn(state);
+ }
+
+ function reset() {
+ const defaults = createDefaults();
+ Object.assign(state.header, defaults.header);
+ Object.assign(state.content, defaults.content);
+ Object.assign(state.footer.button1, defaults.footer.button1);
+ Object.assign(state.footer.button2, defaults.footer.button2);
+ Object.assign(state.footer.button3, defaults.footer.button3);
+ state.footer.show = defaults.footer.show;
+ }
+
+ return { state, set, reset };
+}
diff --git a/src/Cocoar.Shelf.Client/src/core/api/http.ts b/src/Cocoar.Shelf.Client/src/core/api/http.ts
new file mode 100644
index 0000000..463b662
--- /dev/null
+++ b/src/Cocoar.Shelf.Client/src/core/api/http.ts
@@ -0,0 +1,61 @@
+import { useAuthStore } from '@/stores/auth.store';
+import { router } from '@/router';
+
+export class ApiError extends Error {
+ constructor(public readonly status: number, public readonly body: unknown) {
+ const message = (body as { error?: string })?.error ?? `HTTP ${status}`;
+ super(message);
+ this.name = 'ApiError';
+ }
+}
+
+async function request(path: string, init: RequestInit = {}): Promise {
+ const headers: Record = {
+ ...(init.headers as Record),
+ };
+
+ if (init.body && typeof init.body === 'string') {
+ headers['Content-Type'] = 'application/json';
+ }
+
+ const response = await fetch(`/_api${path}`, {
+ ...init,
+ headers,
+ credentials: 'include',
+ });
+
+ if (!response.ok) {
+ if (response.status === 401) {
+ const auth = useAuthStore();
+ auth.isAuthenticated = false;
+ auth.userName = null;
+ router.push('/login');
+ }
+ const contentType = response.headers.get('content-type') ?? '';
+ const errData = contentType.includes('application/json')
+ ? await response.json().catch(() => null)
+ : await response.text().catch(() => null);
+ throw new ApiError(response.status, errData);
+ }
+
+ if (response.status === 204 || response.headers.get('content-length') === '0') {
+ return undefined as T;
+ }
+
+ return await response.json() as T;
+}
+
+export const http = {
+ get: (path: string) => request(path, { method: 'GET' }),
+ post: (path: string, body?: unknown) =>
+ request(path, { method: 'POST', body: body !== undefined ? JSON.stringify(body) : undefined }),
+ put: (path: string, body?: unknown) =>
+ request(path, { method: 'PUT', body: body !== undefined ? JSON.stringify(body) : undefined }),
+ delete: (path: string) => request(path, { method: 'DELETE' }),
+ upload: (path: string, file: File | Blob) =>
+ request(path, {
+ method: 'POST',
+ body: file,
+ headers: { 'Content-Type': 'application/zip' },
+ }),
+};
diff --git a/src/Cocoar.Shelf.Client/src/core/api/shelf-api.ts b/src/Cocoar.Shelf.Client/src/core/api/shelf-api.ts
new file mode 100644
index 0000000..43a084c
--- /dev/null
+++ b/src/Cocoar.Shelf.Client/src/core/api/shelf-api.ts
@@ -0,0 +1,17 @@
+import { http } from './http';
+import type { Product, ProductVersions, CreateProductRequest, UpdateProductRequest } from '../models/shelf.models';
+
+export const shelfApi = {
+ getProducts: () => http.get('/products'),
+ getProduct: (name: string) => http.get(`/products/${name}`),
+ getVersions: (product: string) => http.get(`/products/${product}/versions`),
+ createProduct: (req: CreateProductRequest) => http.post('/products', req),
+ updateProduct: (name: string, req: UpdateProductRequest) => http.put(`/products/${name}`, req),
+ deleteProduct: (name: string, deleteData = false) =>
+ http.delete(`/products/${name}${deleteData ? '?deleteData=true' : ''}`),
+
+ deleteVersion: (product: string, version: string) =>
+ http.delete(`/products/${product}/versions/${version}`),
+ uploadVersion: (product: string, version: string, file: File | Blob) =>
+ http.upload(`/products/${product}/versions/${version}`, file),
+};
diff --git a/src/Cocoar.Shelf.Client/src/core/models/shelf.models.ts b/src/Cocoar.Shelf.Client/src/core/models/shelf.models.ts
new file mode 100644
index 0000000..a9c3041
--- /dev/null
+++ b/src/Cocoar.Shelf.Client/src/core/models/shelf.models.ts
@@ -0,0 +1,36 @@
+export interface Product {
+ name: string;
+ displayName: string | null;
+ description: string | null;
+ source: string;
+ visibility: string;
+ tags: string[];
+ showWhenEmpty: boolean;
+ latest: string | null;
+ versions: string[];
+}
+
+export interface ProductVersions {
+ name: string;
+ latest: string | null;
+ versions: string[];
+}
+
+export interface CreateProductRequest {
+ name: string;
+ displayName?: string;
+ description?: string;
+ source?: string;
+ visibility?: string;
+ tags?: string[];
+ showWhenEmpty?: boolean;
+}
+
+export interface UpdateProductRequest {
+ displayName?: string;
+ description?: string;
+ source?: string;
+ visibility?: string;
+ tags?: string[];
+ showWhenEmpty?: boolean;
+}
diff --git a/src/Cocoar.Shelf.Client/src/layouts/AdminLayout.vue b/src/Cocoar.Shelf.Client/src/layouts/AdminLayout.vue
new file mode 100644
index 0000000..46ee765
--- /dev/null
+++ b/src/Cocoar.Shelf.Client/src/layouts/AdminLayout.vue
@@ -0,0 +1,246 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Cocoar.Shelf.Client/src/main.ts b/src/Cocoar.Shelf.Client/src/main.ts
new file mode 100644
index 0000000..8e4f882
--- /dev/null
+++ b/src/Cocoar.Shelf.Client/src/main.ts
@@ -0,0 +1,23 @@
+import { createApp } from 'vue';
+import { createPinia } from 'pinia';
+import { CoarIconPlugin, CoarOverlayPlugin, CoarHttpIconSource, CORE_ICONS } from '@cocoar/vue-ui';
+import App from './App.vue';
+import { router } from './router';
+import '@cocoar/vue-ui/styles';
+import './styles.css';
+
+const app = createApp(App);
+app.use(createPinia());
+app.use(router);
+app.use(CoarIconPlugin, {
+ sources: [
+ CORE_ICONS,
+ {
+ key: 'lucide',
+ source: new CoarHttpIconSource((name) => `/icons/lucide/${name}.svg`),
+ },
+ ],
+ defaultSource: 'lucide',
+});
+app.use(CoarOverlayPlugin);
+app.mount('#app');
diff --git a/src/Cocoar.Shelf.Client/src/router/index.ts b/src/Cocoar.Shelf.Client/src/router/index.ts
new file mode 100644
index 0000000..ddc0473
--- /dev/null
+++ b/src/Cocoar.Shelf.Client/src/router/index.ts
@@ -0,0 +1,50 @@
+import { createRouter, createWebHistory } from 'vue-router';
+import { useAuthStore } from '@/stores/auth.store';
+
+const pathBase = (window.__SHELF_OPTIONS__?.pathBase ?? '').replace(/\/$/, '');
+
+export const router = createRouter({
+ history: createWebHistory(pathBase + '/'),
+ routes: [
+ {
+ path: '/',
+ component: () => import('@/views/LandingView.vue'),
+ meta: { public: true },
+ },
+ {
+ path: '/login',
+ component: () => import('@/views/LoginView.vue'),
+ meta: { public: true },
+ },
+ {
+ path: '/admin',
+ component: () => import('@/layouts/AdminLayout.vue'),
+ children: [
+ { path: '', component: () => import('@/views/DashboardView.vue') },
+ { path: 'products', component: () => import('@/views/products/ProductListView.vue') },
+ { path: 'products/create', component: () => import('@/views/products/ProductFormView.vue') },
+ { path: 'products/:name', component: () => import('@/views/products/ProductDetailView.vue') },
+ { path: 'products/:name/edit', component: () => import('@/views/products/ProductFormView.vue') },
+ ],
+ },
+ ],
+});
+
+let sessionChecked = false;
+
+router.beforeEach(async (to) => {
+ const auth = useAuthStore();
+
+ // Check session once on first navigation (handles page refresh)
+ if (!sessionChecked) {
+ sessionChecked = true;
+ await auth.checkSession();
+ }
+
+ if (!to.meta.public && !auth.isAuthenticated) {
+ return '/login';
+ }
+ if (to.path === '/login' && auth.isAuthenticated) {
+ return '/admin';
+ }
+});
diff --git a/src/Cocoar.Shelf.Client/src/stores/auth.store.ts b/src/Cocoar.Shelf.Client/src/stores/auth.store.ts
new file mode 100644
index 0000000..1414cfd
--- /dev/null
+++ b/src/Cocoar.Shelf.Client/src/stores/auth.store.ts
@@ -0,0 +1,61 @@
+import { defineStore } from 'pinia';
+import { ref, computed } from 'vue';
+
+export const useAuthStore = defineStore('auth', () => {
+ const isAuthenticated = ref(false);
+ const userName = ref(null);
+
+ async function checkSession(): Promise {
+ try {
+ const response = await fetch('/_api/auth/me', { credentials: 'include' });
+ if (response.ok) {
+ const data = await response.json();
+ isAuthenticated.value = data.authenticated;
+ userName.value = data.name;
+ return true;
+ }
+ isAuthenticated.value = false;
+ userName.value = null;
+ return false;
+ } catch {
+ isAuthenticated.value = false;
+ userName.value = null;
+ return false;
+ }
+ }
+
+ async function login(apiKey: string): Promise {
+ try {
+ const response = await fetch('/_api/auth/login', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ body: JSON.stringify({ apiKey }),
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ isAuthenticated.value = true;
+ userName.value = data.name;
+ return true;
+ }
+ return false;
+ } catch {
+ return false;
+ }
+ }
+
+ async function logout() {
+ try {
+ await fetch('/_api/auth/logout', {
+ method: 'POST',
+ credentials: 'include',
+ });
+ } finally {
+ isAuthenticated.value = false;
+ userName.value = null;
+ }
+ }
+
+ return { isAuthenticated, userName, checkSession, login, logout };
+});
diff --git a/src/Cocoar.Shelf.Client/src/stores/preferences.store.ts b/src/Cocoar.Shelf.Client/src/stores/preferences.store.ts
new file mode 100644
index 0000000..1e0b6f9
--- /dev/null
+++ b/src/Cocoar.Shelf.Client/src/stores/preferences.store.ts
@@ -0,0 +1,49 @@
+import { defineStore } from 'pinia';
+import { ref, watch } from 'vue';
+
+const STORAGE_KEY = 'shelf:preferences';
+
+interface Preferences {
+ showPreview: boolean;
+ selectedTags: string[];
+}
+
+function load(): Preferences {
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY);
+ if (raw) return { ...defaults(), ...JSON.parse(raw) };
+ } catch { /* ignore corrupt data */ }
+ return defaults();
+}
+
+function defaults(): Preferences {
+ return { showPreview: false, selectedTags: [] };
+}
+
+export const usePreferencesStore = defineStore('preferences', () => {
+ const saved = load();
+ const showPreview = ref(saved.showPreview);
+ const selectedTags = ref(saved.selectedTags);
+
+ function persist() {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify({
+ showPreview: showPreview.value,
+ selectedTags: selectedTags.value,
+ }));
+ }
+
+ watch(showPreview, persist);
+ watch(selectedTags, persist, { deep: true });
+
+ function toggleTag(tag: string) {
+ const idx = selectedTags.value.indexOf(tag);
+ if (idx === -1) selectedTags.value.push(tag);
+ else selectedTags.value.splice(idx, 1);
+ }
+
+ function clearTags() {
+ selectedTags.value = [];
+ }
+
+ return { showPreview, selectedTags, toggleTag, clearTags };
+});
diff --git a/src/Cocoar.Shelf.Client/src/styles.css b/src/Cocoar.Shelf.Client/src/styles.css
new file mode 100644
index 0000000..a2e723d
--- /dev/null
+++ b/src/Cocoar.Shelf.Client/src/styles.css
@@ -0,0 +1,10 @@
+@import "tailwindcss";
+
+html, body, #app {
+ height: 100%;
+ margin: 0;
+}
+
+body {
+ background-color: var(--coar-background-neutral-secondary);
+}
diff --git a/src/Cocoar.Shelf.Client/src/views/DashboardView.vue b/src/Cocoar.Shelf.Client/src/views/DashboardView.vue
new file mode 100644
index 0000000..e82898b
--- /dev/null
+++ b/src/Cocoar.Shelf.Client/src/views/DashboardView.vue
@@ -0,0 +1,153 @@
+
+
+
+
+
{{ products.length }}
+
Products
+
+
+
{{ totalVersions }}
+
Versions
+
+
+
+
+
Products
+
+
+ {{ product.displayName || product.name }}
+
+ {{ product.latest }}
+ {{ product.versions.length }} version{{ product.versions.length !== 1 ? 's' : '' }}
+
+
+
+
+
+
{{ error }}
+
+
+
+
+
+
diff --git a/src/Cocoar.Shelf.Client/src/views/LandingView.vue b/src/Cocoar.Shelf.Client/src/views/LandingView.vue
new file mode 100644
index 0000000..1356db9
--- /dev/null
+++ b/src/Cocoar.Shelf.Client/src/views/LandingView.vue
@@ -0,0 +1,404 @@
+
+
+
+
+
+
+
+
+
+ No documentation available yet.
+
+
+
+
+
+
+
diff --git a/src/Cocoar.Shelf.Client/src/views/LoginView.vue b/src/Cocoar.Shelf.Client/src/views/LoginView.vue
new file mode 100644
index 0000000..d7200a6
--- /dev/null
+++ b/src/Cocoar.Shelf.Client/src/views/LoginView.vue
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
diff --git a/src/Cocoar.Shelf.Client/src/views/products/ProductDetailView.vue b/src/Cocoar.Shelf.Client/src/views/products/ProductDetailView.vue
new file mode 100644
index 0000000..15f6438
--- /dev/null
+++ b/src/Cocoar.Shelf.Client/src/views/products/ProductDetailView.vue
@@ -0,0 +1,277 @@
+
+
+
{{ error }}
+
{{ successMessage }}
+
+
+
+
+
+ Name
+ {{ product.name }}
+
+
+ Display Name
+ {{ product.displayName || '—' }}
+
+
+ Description
+ {{ product.description || '—' }}
+
+
+ Source
+ {{ product.source }}
+
+
+ Visibility
+
+
+ {{ product.visibility }}
+
+
+
+
+ Latest Version
+
+ {{ product.latest }}
+ —
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Upload
+
+
+
+
+
+
+
+
+
+ | Version |
+ Status |
+ |
+
+
+
+
+ |
+ {{ v }}
+ |
+
+ latest
+ |
+
+
+ Delete
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Cocoar.Shelf.Client/src/views/products/ProductFormView.vue b/src/Cocoar.Shelf.Client/src/views/products/ProductFormView.vue
new file mode 100644
index 0000000..09a129a
--- /dev/null
+++ b/src/Cocoar.Shelf.Client/src/views/products/ProductFormView.vue
@@ -0,0 +1,298 @@
+
+
+
{{ error }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Cocoar.Shelf.Client/src/views/products/ProductListView.vue b/src/Cocoar.Shelf.Client/src/views/products/ProductListView.vue
new file mode 100644
index 0000000..6c2c32e
--- /dev/null
+++ b/src/Cocoar.Shelf.Client/src/views/products/ProductListView.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
{{ error }}
+
+
+
+
+ | Name |
+ Display Name |
+ Visibility |
+ Latest |
+ Versions |
+
+
+
+
+ | {{ p.name }} |
+ {{ p.displayName || '—' }} |
+
+
+ {{ p.visibility }}
+
+ |
+
+ {{ p.latest }}
+ —
+ |
+ {{ p.versions.length }} |
+
+
+
+
+
+ No products registered yet.
+
+
+
+
+
+
+
diff --git a/src/Cocoar.Shelf.Client/tsconfig.json b/src/Cocoar.Shelf.Client/tsconfig.json
new file mode 100644
index 0000000..aa7faa7
--- /dev/null
+++ b/src/Cocoar.Shelf.Client/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "extends": "@vue/tsconfig/tsconfig.dom.json",
+ "include": ["src/**/*"],
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ },
+ "strict": true,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false
+ }
+}
diff --git a/src/Cocoar.Shelf.Client/vite.config.ts b/src/Cocoar.Shelf.Client/vite.config.ts
new file mode 100644
index 0000000..0bb22fb
--- /dev/null
+++ b/src/Cocoar.Shelf.Client/vite.config.ts
@@ -0,0 +1,31 @@
+import { defineConfig } from 'vite';
+import vue from '@vitejs/plugin-vue';
+import tailwindcss from '@tailwindcss/postcss';
+
+export default defineConfig({
+ base: '/',
+ plugins: [vue()],
+ css: {
+ postcss: {
+ plugins: [tailwindcss()],
+ },
+ },
+ build: {
+ outDir: '../Cocoar.Shelf/wwwroot',
+ emptyOutDir: true,
+ },
+ server: {
+ port: 5173,
+ proxy: {
+ '/_api': {
+ target: 'http://localhost:5200',
+ changeOrigin: true,
+ },
+ },
+ },
+ resolve: {
+ alias: {
+ '@': '/src',
+ },
+ },
+});
diff --git a/src/Cocoar.Shelf/Cocoar.Shelf.csproj b/src/Cocoar.Shelf/Cocoar.Shelf.csproj
index 97a625f..83366c4 100644
--- a/src/Cocoar.Shelf/Cocoar.Shelf.csproj
+++ b/src/Cocoar.Shelf/Cocoar.Shelf.csproj
@@ -5,7 +5,13 @@
+
+
+
+
+
+
diff --git a/src/Cocoar.Shelf/Endpoints/ApiEndpoints.cs b/src/Cocoar.Shelf/Endpoints/ApiEndpoints.cs
index 153c375..ca02adc 100644
--- a/src/Cocoar.Shelf/Endpoints/ApiEndpoints.cs
+++ b/src/Cocoar.Shelf/Endpoints/ApiEndpoints.cs
@@ -1,119 +1,491 @@
-using System.Text.RegularExpressions;
-using Cocoar.Shelf.Services;
-using Microsoft.AspNetCore.Http.Features;
-using Microsoft.Extensions.Options;
-
-namespace Cocoar.Shelf.Endpoints;
-
-public static class ApiEndpoints
-{
- public static WebApplication MapApiEndpoints(this WebApplication app)
- {
- var api = app.MapGroup("/api");
-
- api.MapGet("/products", GetProducts);
- api.MapGet("/products/{product}/versions", GetVersions);
- api.MapPost("/products/{product}/versions/{version}", UploadVersion)
- .AddEndpointFilter();
-
- return app;
- }
-
- private static IResult GetProducts(IProductConfigService configService, IManifestService manifestService)
- {
- var products = configService.GetAll().Select(config =>
- {
- var manifest = manifestService.GetManifest(config.Name);
- return new
- {
- config.Name,
- config.DisplayName,
- config.Description,
- config.Source,
- Latest = manifest?.Latest,
- Versions = manifest?.Versions ?? (IReadOnlyList)[]
- };
- });
-
- return Results.Ok(products);
- }
-
- private static IResult GetVersions(
- string product,
- IProductConfigService configService,
- IManifestService manifestService)
- {
- var config = configService.GetConfig(product);
- if (config == null)
- return Results.Json(new { error = $"Product '{product}' is not registered" }, statusCode: 404);
-
- var manifest = manifestService.GetManifest(product);
-
- return Results.Ok(new
- {
- Name = product,
- Latest = manifest?.Latest,
- Versions = manifest?.Versions ?? (IReadOnlyList)[]
- });
- }
-
- private static async Task UploadVersion(
- string product,
- string version,
- HttpContext httpContext,
- IProductConfigService configService,
- IUploadService uploadService,
- IOptions options,
- CancellationToken ct)
- {
- var opts = options.Value;
-
- // Increase request body size limit for this endpoint
- var maxSizeFeature = httpContext.Features.Get();
- if (maxSizeFeature is { IsReadOnly: false })
- maxSizeFeature.MaxRequestBodySize = opts.MaxUploadSizeBytes;
-
- // Check product is registered
- var config = configService.GetConfig(product);
- if (config == null)
- return Results.Json(new { error = $"Product '{product}' is not registered" }, statusCode: 404);
-
- // Validate version format
- if (!Regex.IsMatch(version, opts.VersionPattern))
- return Results.Json(new { error = $"Invalid version format: '{version}'" }, statusCode: 400);
-
- // Check Content-Length if present
- if (httpContext.Request.ContentLength > opts.MaxUploadSizeBytes)
- return Results.Json(new { error = "Upload exceeds maximum allowed size" }, statusCode: 413);
-
- // Read body with size limit
- using var ms = new MemoryStream();
- var buffer = new byte[81920];
- long totalRead = 0;
- int bytesRead;
-
- while ((bytesRead = await httpContext.Request.Body.ReadAsync(buffer, ct)) > 0)
- {
- totalRead += bytesRead;
- if (totalRead > opts.MaxUploadSizeBytes)
- return Results.Json(new { error = "Upload exceeds maximum allowed size" }, statusCode: 413);
- ms.Write(buffer, 0, bytesRead);
- }
-
- ms.Position = 0;
-
- var result = await uploadService.UploadVersionAsync(product, version, ms, ct);
-
- return result.Status switch
- {
- UploadStatus.Success => Results.Created($"{httpContext.Request.PathBase}/api/products/{product}/versions/{version}", null),
- UploadStatus.MissingIndexHtml => Results.Json(
- new { error = result.Error ?? "Archive must contain an index.html at the root" }, statusCode: 400),
- UploadStatus.VersionConflict => Results.Json(
- new { error = result.Error ?? "Upload for this version is already in progress" }, statusCode: 409),
- UploadStatus.InvalidArchive => Results.Json(
- new { error = result.Error ?? "Invalid ZIP archive" }, statusCode: 400),
- _ => Results.Json(new { error = "Internal error" }, statusCode: 500)
- };
- }
-}
+using System.Text.RegularExpressions;
+using Cocoar.Shelf.Models;
+using Cocoar.Shelf.Services;
+using Microsoft.AspNetCore.Http.Features;
+
+namespace Cocoar.Shelf.Endpoints;
+
+public static partial class ApiEndpoints
+{
+ private static readonly Regex ProductNameRegex = new("^[a-z0-9][a-z0-9-]*$", RegexOptions.Compiled);
+ private static readonly HashSet ReservedNames = new(StringComparer.OrdinalIgnoreCase) { "admin", "api" };
+
+ public static WebApplication MapApiEndpoints(this WebApplication app)
+ {
+ var api = app.MapGroup("/_api");
+
+ // Auth endpoints (cookie-based)
+ api.MapAuthEndpoints();
+
+ // Public read endpoints
+ api.MapGet("/products", GetProducts);
+ api.MapGet("/products/{product}", GetProduct);
+ api.MapGet("/products/{product}/versions", GetVersions);
+ api.MapGet("/shelf-config", GetShelfConfig);
+
+ // Protected write endpoints (cookie or Bearer API key)
+ api.MapPost("/products", CreateProduct)
+ .AddEndpointFilter();
+ api.MapPut("/products/{product}", UpdateProduct)
+ .AddEndpointFilter();
+ api.MapDelete("/products/{product}", DeleteProduct)
+ .AddEndpointFilter();
+ api.MapPost("/products/{product}/versions/{version}", UploadVersion)
+ .AddEndpointFilter();
+ api.MapDelete("/products/{product}/versions/{version}", DeleteVersion)
+ .AddEndpointFilter();
+
+ return app;
+ }
+
+ private static IResult GetProducts(
+ IProductConfigService configService,
+ IManifestService manifestService,
+ ILoggerFactory loggerFactory)
+ {
+ var logger = loggerFactory.CreateLogger("Cocoar.Shelf.Api");
+ try
+ {
+ var products = configService.GetAll().Select(config =>
+ {
+ var manifest = manifestService.GetManifest(config.Name);
+ return new
+ {
+ config.Name,
+ config.DisplayName,
+ config.Description,
+ config.Source,
+ config.Visibility,
+ config.Tags,
+ config.ShowWhenEmpty,
+ Latest = manifest?.Latest,
+ Versions = manifest?.Versions ?? (IReadOnlyList)[]
+ };
+ });
+
+ return Results.Ok(products);
+ }
+ catch (Exception ex)
+ {
+ LogListProductsFailed(logger, ex);
+ return Results.Json(new { error = "Failed to list products" }, statusCode: 500);
+ }
+ }
+
+ private static IResult GetVersions(
+ string product,
+ IProductConfigService configService,
+ IManifestService manifestService,
+ ILoggerFactory loggerFactory)
+ {
+ var logger = loggerFactory.CreateLogger("Cocoar.Shelf.Api");
+ try
+ {
+ var config = configService.GetConfig(product);
+ if (config == null)
+ {
+ LogProductNotRegistered(logger, product);
+ return Results.Json(new { error = $"Product '{product}' is not registered" }, statusCode: 404);
+ }
+
+ var manifest = manifestService.GetManifest(product);
+
+ return Results.Ok(new
+ {
+ Name = product,
+ Latest = manifest?.Latest,
+ Versions = manifest?.Versions ?? (IReadOnlyList)[]
+ });
+ }
+ catch (Exception ex)
+ {
+ LogGetVersionsFailed(logger, product, ex);
+ return Results.Json(new { error = $"Failed to get versions for product '{product}'" }, statusCode: 500);
+ }
+ }
+
+ private static IResult GetProduct(
+ string product,
+ IProductConfigService configService,
+ IManifestService manifestService,
+ ILoggerFactory loggerFactory)
+ {
+ var logger = loggerFactory.CreateLogger("Cocoar.Shelf.Api");
+ try
+ {
+ var config = configService.GetConfig(product);
+ if (config == null)
+ {
+ LogProductNotRegistered(logger, product);
+ return Results.Json(new { error = $"Product '{product}' is not registered" }, statusCode: 404);
+ }
+
+ var manifest = manifestService.GetManifest(product);
+ return Results.Ok(new
+ {
+ config.Name,
+ config.DisplayName,
+ config.Description,
+ config.Source,
+ config.Visibility,
+ config.Tags,
+ config.ShowWhenEmpty,
+ Latest = manifest?.Latest,
+ Versions = manifest?.Versions ?? (IReadOnlyList)[]
+ });
+ }
+ catch (Exception ex)
+ {
+ LogGetProductFailed(logger, product, ex);
+ return Results.Json(new { error = $"Failed to get product '{product}'" }, statusCode: 500);
+ }
+ }
+
+ private static IResult GetShelfConfig(ShelfOptions options) =>
+ Results.Ok(new { pathBase = options.PathBase });
+
+ private static async Task UploadVersion(
+ string product,
+ string version,
+ HttpContext httpContext,
+ IProductConfigService configService,
+ IUploadService uploadService,
+ ShelfOptions options,
+ ILoggerFactory loggerFactory,
+ CancellationToken ct)
+ {
+ var logger = loggerFactory.CreateLogger("Cocoar.Shelf.Api");
+ try
+ {
+ return await UploadVersionCore(product, version, httpContext, configService, uploadService, options, logger, ct);
+ }
+ catch (BadHttpRequestException ex)
+ {
+ LogUploadBadRequest(logger, product, version, ex);
+ return Results.Json(new { error = $"Bad request: {ex.Message}" }, statusCode: 400);
+ }
+ catch (OperationCanceledException)
+ {
+ LogUploadCancelled(logger, product, version);
+ return Results.Json(new { error = "Upload cancelled" }, statusCode: 499);
+ }
+ catch (Exception ex)
+ {
+ LogUploadFailed(logger, product, version, ex);
+ return Results.Json(new { error = $"Upload failed: {ex.Message}" }, statusCode: 500);
+ }
+ }
+
+ private static async Task UploadVersionCore(
+ string product,
+ string version,
+ HttpContext httpContext,
+ IProductConfigService configService,
+ IUploadService uploadService,
+ ShelfOptions opts,
+ ILogger logger,
+ CancellationToken ct)
+ {
+ // Increase request body size limit for this endpoint
+ var maxSizeFeature = httpContext.Features.Get();
+ if (maxSizeFeature is { IsReadOnly: false })
+ maxSizeFeature.MaxRequestBodySize = opts.MaxUploadSizeBytes;
+
+ // Check product is registered
+ var config = configService.GetConfig(product);
+ if (config == null)
+ {
+ LogUploadProductNotRegistered(logger, product);
+ return Results.Json(new { error = $"Product '{product}' is not registered" }, statusCode: 404);
+ }
+
+ // Validate version format
+ if (string.IsNullOrWhiteSpace(version))
+ {
+ LogUploadEmptyVersion(logger, product);
+ return Results.Json(new { error = "Version must not be empty" }, statusCode: 400);
+ }
+
+ if (!Regex.IsMatch(version, opts.VersionPattern))
+ {
+ LogUploadInvalidVersion(logger, version, product, opts.VersionPattern);
+ return Results.Json(
+ new { error = $"Invalid version format: '{version}'. Must match pattern: {opts.VersionPattern}" },
+ statusCode: 400);
+ }
+
+ // Check Content-Length if present
+ if (httpContext.Request.ContentLength > opts.MaxUploadSizeBytes)
+ {
+ LogUploadTooLarge(logger, httpContext.Request.ContentLength, opts.MaxUploadSizeBytes, product, version);
+ return Results.Json(new { error = $"Upload exceeds maximum allowed size ({opts.MaxUploadSizeBytes} bytes)" }, statusCode: 413);
+ }
+
+ // Read body with size limit
+ using var ms = new MemoryStream();
+ var buffer = new byte[81920];
+ long totalRead = 0;
+ int bytesRead;
+
+ while ((bytesRead = await httpContext.Request.Body.ReadAsync(buffer, ct)) > 0)
+ {
+ totalRead += bytesRead;
+ if (totalRead > opts.MaxUploadSizeBytes)
+ {
+ LogUploadBodyTooLarge(logger, opts.MaxUploadSizeBytes, product, version);
+ return Results.Json(new { error = $"Upload exceeds maximum allowed size ({opts.MaxUploadSizeBytes} bytes)" }, statusCode: 413);
+ }
+ ms.Write(buffer, 0, bytesRead);
+ }
+
+ ms.Position = 0;
+
+ LogUploadProcessing(logger, product, version, totalRead);
+
+ var result = await uploadService.UploadVersionAsync(product, version, ms, ct);
+
+ switch (result.Status)
+ {
+ case UploadStatus.Success:
+ return Results.Created($"{httpContext.Request.PathBase}/_api/products/{product}/versions/{version}", null);
+
+ case UploadStatus.MissingIndexHtml:
+ LogUploadRejected(logger, product, version, "missing index.html");
+ return Results.Json(new { error = result.Error ?? "Archive must contain an index.html at the root" }, statusCode: 400);
+
+ case UploadStatus.VersionConflict:
+ LogUploadRejected(logger, product, version, "concurrent upload");
+ return Results.Json(new { error = result.Error ?? "Upload for this version is already in progress" }, statusCode: 409);
+
+ case UploadStatus.InvalidArchive:
+ LogUploadRejected(logger, product, version, result.Error ?? "invalid archive");
+ return Results.Json(new { error = result.Error ?? "Invalid ZIP archive" }, statusCode: 400);
+
+ default:
+ LogUploadUnexpectedStatus(logger, product, version, result.Status);
+ return Results.Json(new { error = result.Error ?? "Internal error" }, statusCode: 500);
+ }
+ }
+
+ private static async Task CreateProduct(
+ CreateProductRequest request,
+ IProductConfigService configService,
+ ILoggerFactory loggerFactory)
+ {
+ var logger = loggerFactory.CreateLogger("Cocoar.Shelf.Api");
+ try
+ {
+ if (string.IsNullOrWhiteSpace(request.Name))
+ return Results.Json(new { error = "Product name is required" }, statusCode: 400);
+
+ if (!ProductNameRegex.IsMatch(request.Name))
+ return Results.Json(new { error = "Product name must contain only lowercase letters, numbers, and hyphens" }, statusCode: 400);
+
+ if (ReservedNames.Contains(request.Name))
+ return Results.Json(new { error = $"'{request.Name}' is a reserved name" }, statusCode: 400);
+
+ if (configService.GetConfig(request.Name) != null)
+ {
+ LogProductAlreadyExists(logger, request.Name);
+ return Results.Json(new { error = $"Product '{request.Name}' already exists" }, statusCode: 409);
+ }
+
+ var config = new ProductConfig
+ {
+ Name = request.Name,
+ DisplayName = request.DisplayName,
+ Description = request.Description,
+ Source = request.Source ?? "upload",
+ Visibility = request.Visibility ?? "public",
+ Tags = NormalizeTags(request.Tags),
+ ShowWhenEmpty = request.ShowWhenEmpty ?? false
+ };
+
+ await configService.CreateAsync(config);
+ LogProductCreated(logger, request.Name);
+ return Results.Created($"/_api/products/{request.Name}", config);
+ }
+ catch (Exception ex)
+ {
+ LogProductOperationFailed(logger, "create", request.Name, ex);
+ return Results.Json(new { error = $"Failed to create product: {ex.Message}" }, statusCode: 500);
+ }
+ }
+
+ private static async Task UpdateProduct(
+ string product,
+ UpdateProductRequest request,
+ IProductConfigService configService,
+ ILoggerFactory loggerFactory)
+ {
+ var logger = loggerFactory.CreateLogger("Cocoar.Shelf.Api");
+ try
+ {
+ var existing = configService.GetConfig(product);
+ if (existing == null)
+ {
+ LogProductNotRegistered(logger, product);
+ return Results.Json(new { error = $"Product '{product}' is not registered" }, statusCode: 404);
+ }
+
+ var config = new ProductConfig
+ {
+ Name = product,
+ DisplayName = request.DisplayName ?? existing.DisplayName,
+ Description = request.Description ?? existing.Description,
+ Source = request.Source ?? existing.Source,
+ Visibility = request.Visibility ?? existing.Visibility,
+ Tags = request.Tags != null ? NormalizeTags(request.Tags) : existing.Tags,
+ ShowWhenEmpty = request.ShowWhenEmpty ?? existing.ShowWhenEmpty
+ };
+
+ await configService.UpdateAsync(config);
+ LogProductUpdated(logger, product);
+ return Results.Ok(config);
+ }
+ catch (Exception ex)
+ {
+ LogProductOperationFailed(logger, "update", product, ex);
+ return Results.Json(new { error = $"Failed to update product: {ex.Message}" }, statusCode: 500);
+ }
+ }
+
+ private static async Task DeleteProduct(
+ string product,
+ IProductConfigService configService,
+ IUploadService uploadService,
+ ILoggerFactory loggerFactory,
+ CancellationToken ct,
+ bool deleteData = false)
+ {
+ var logger = loggerFactory.CreateLogger("Cocoar.Shelf.Api");
+ try
+ {
+ var existing = configService.GetConfig(product);
+ if (existing == null)
+ {
+ LogProductNotRegistered(logger, product);
+ return Results.Json(new { error = $"Product '{product}' is not registered" }, statusCode: 404);
+ }
+
+ await configService.DeleteAsync(product);
+
+ if (deleteData)
+ await uploadService.DeleteProductDataAsync(product, ct);
+
+ LogProductDeleted(logger, product, deleteData);
+
+ return Results.NoContent();
+ }
+ catch (Exception ex)
+ {
+ LogProductOperationFailed(logger, "delete", product, ex);
+ return Results.Json(new { error = $"Failed to delete product: {ex.Message}" }, statusCode: 500);
+ }
+ }
+
+ private static async Task DeleteVersion(
+ string product,
+ string version,
+ IProductConfigService configService,
+ IUploadService uploadService,
+ ShelfOptions options,
+ ILoggerFactory loggerFactory,
+ CancellationToken ct)
+ {
+ var logger = loggerFactory.CreateLogger("Cocoar.Shelf.Api");
+ try
+ {
+ var config = configService.GetConfig(product);
+ if (config == null)
+ {
+ LogProductNotRegistered(logger, product);
+ return Results.Json(new { error = $"Product '{product}' is not registered" }, statusCode: 404);
+ }
+
+ if (!Regex.IsMatch(version, options.VersionPattern))
+ {
+ LogUploadInvalidVersion(logger, version, product, options.VersionPattern);
+ return Results.Json(new { error = $"Invalid version format: '{version}'" }, statusCode: 400);
+ }
+
+ var deleted = await uploadService.DeleteVersionAsync(product, version, ct);
+ if (!deleted)
+ return Results.Json(new { error = $"Version '{version}' not found for product '{product}'" }, statusCode: 404);
+
+ LogVersionDeleted(logger, product, version);
+ return Results.NoContent();
+ }
+ catch (Exception ex)
+ {
+ LogProductOperationFailed(logger, "delete version", $"{product}/{version}", ex);
+ return Results.Json(new { error = $"Failed to delete version: {ex.Message}" }, statusCode: 500);
+ }
+ }
+
+ [LoggerMessage(Level = LogLevel.Error, Message = "Failed to list products")]
+ private static partial void LogListProductsFailed(ILogger logger, Exception ex);
+
+ [LoggerMessage(Level = LogLevel.Warning, Message = "Product not registered: {Product}")]
+ private static partial void LogProductNotRegistered(ILogger logger, string product);
+
+ [LoggerMessage(Level = LogLevel.Error, Message = "Failed to get versions for product {Product}")]
+ private static partial void LogGetVersionsFailed(ILogger logger, string product, Exception ex);
+
+ [LoggerMessage(Level = LogLevel.Error, Message = "Failed to get product {Product}")]
+ private static partial void LogGetProductFailed(ILogger logger, string product, Exception ex);
+
+ [LoggerMessage(Level = LogLevel.Warning, Message = "Upload bad request for {Product}/{Version}")]
+ private static partial void LogUploadBadRequest(ILogger logger, string product, string version, Exception ex);
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Upload cancelled for {Product}/{Version}")]
+ private static partial void LogUploadCancelled(ILogger logger, string product, string version);
+
+ [LoggerMessage(Level = LogLevel.Error, Message = "Upload failed for {Product}/{Version}")]
+ private static partial void LogUploadFailed(ILogger logger, string product, string version, Exception ex);
+
+ [LoggerMessage(Level = LogLevel.Warning, Message = "Upload rejected: product {Product} is not registered")]
+ private static partial void LogUploadProductNotRegistered(ILogger logger, string product);
+
+ [LoggerMessage(Level = LogLevel.Warning, Message = "Upload rejected: empty version for product {Product}")]
+ private static partial void LogUploadEmptyVersion(ILogger logger, string product);
+
+ [LoggerMessage(Level = LogLevel.Warning, Message = "Upload rejected: invalid version format {Version} for product {Product} (pattern: {Pattern})")]
+ private static partial void LogUploadInvalidVersion(ILogger logger, string version, string product, string pattern);
+
+ [LoggerMessage(Level = LogLevel.Warning, Message = "Upload rejected: content length {Length} exceeds limit {Limit} for {Product}/{Version}")]
+ private static partial void LogUploadTooLarge(ILogger logger, long? length, long limit, string product, string version);
+
+ [LoggerMessage(Level = LogLevel.Warning, Message = "Upload rejected: body exceeds limit {Limit} for {Product}/{Version}")]
+ private static partial void LogUploadBodyTooLarge(ILogger logger, long limit, string product, string version);
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Processing upload for {Product}/{Version} ({Bytes} bytes)")]
+ private static partial void LogUploadProcessing(ILogger logger, string product, string version, long bytes);
+
+ [LoggerMessage(Level = LogLevel.Warning, Message = "Upload rejected for {Product}/{Version}: {Reason}")]
+ private static partial void LogUploadRejected(ILogger logger, string product, string version, string reason);
+
+ [LoggerMessage(Level = LogLevel.Error, Message = "Upload failed with unexpected status {Status} for {Product}/{Version}")]
+ private static partial void LogUploadUnexpectedStatus(ILogger logger, string product, string version, UploadStatus status);
+
+ [LoggerMessage(Level = LogLevel.Warning, Message = "Product already exists: {Product}")]
+ private static partial void LogProductAlreadyExists(ILogger logger, string product);
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Product created: {Product}")]
+ private static partial void LogProductCreated(ILogger logger, string product);
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Product updated: {Product}")]
+ private static partial void LogProductUpdated(ILogger logger, string product);
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Product deleted: {Product} (deleteData={DeleteData})")]
+ private static partial void LogProductDeleted(ILogger logger, string product, bool deleteData);
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Version deleted: {Product}/{Version}")]
+ private static partial void LogVersionDeleted(ILogger logger, string product, string version);
+
+ [LoggerMessage(Level = LogLevel.Error, Message = "Failed to {Operation} product {Product}")]
+ private static partial void LogProductOperationFailed(ILogger logger, string operation, string product, Exception ex);
+
+ private static IReadOnlyList NormalizeTags(IReadOnlyList? tags) =>
+ tags == null ? [] : tags.Select(t => t.Trim()).Where(t => t.Length > 0).Distinct(StringComparer.OrdinalIgnoreCase).Order().ToList();
+}
diff --git a/src/Cocoar.Shelf/Endpoints/ApiKeyFilter.cs b/src/Cocoar.Shelf/Endpoints/ApiKeyFilter.cs
index 2d5a800..d6902de 100644
--- a/src/Cocoar.Shelf/Endpoints/ApiKeyFilter.cs
+++ b/src/Cocoar.Shelf/Endpoints/ApiKeyFilter.cs
@@ -1,27 +1,64 @@
-using Microsoft.Extensions.Options;
-
namespace Cocoar.Shelf.Endpoints;
-public class ApiKeyFilter : IEndpointFilter
+///
+/// Endpoint filter that accepts authentication via either:
+/// - Cookie session (from Admin UI / browser)
+/// - Bearer API key (from CI/CD pipelines)
+///
+public partial class ApiKeyFilter(ILogger logger) : IEndpointFilter
{
public async ValueTask