feat: optimize Docker CI/CD builds, refine site-wide UI aesthetic, and remediate secrets

swadhinbiswas committed 2 months ago
3846c8b
1 parent c0437ed
commit 3846c8b
100 files changed
+9765 -4250
Showing 84 changed files+7627-1948
This diff is too large and has been truncated. Displaying first 84 files.
.astro/settings.json.dockerignore.github/workflows/docker-publish.ymlDockerfileDockerfile.runnerDockerfile.workerREADME.mdUIPLAN.mdcli/README.mdcommunity/.env.examplecommunity/.gitignorecommunity/README.mdcommunity/astro.config.mjscommunity/bun.lockcommunity/drizzle.config.tscommunity/package.jsoncommunity/public/logo.svgcommunity/scripts/cron-sync.tscommunity/src/components/InstanceCard.tsxcommunity/src/components/ProfileCard.tsxcommunity/src/components/RepoCard.tsxcommunity/src/components/layout/Footer.astrocommunity/src/components/layout/Header.tsxcommunity/src/components/ui/avatar.tsxcommunity/src/components/ui/badge.tsxcommunity/src/components/ui/button.tsxcommunity/src/components/ui/card.tsxcommunity/src/components/ui/input.tsxcommunity/src/components/ui/label.tsxcommunity/src/components/ui/toaster.tsxcommunity/src/env.d.tscommunity/src/layouts/BaseLayout.astrocommunity/src/lib/auth.tscommunity/src/lib/cache.tscommunity/src/lib/db/index.tscommunity/src/lib/db/schema.tscommunity/src/lib/instance-client.tscommunity/src/lib/utils.tscommunity/src/pages/api/auth/login.tscommunity/src/pages/api/auth/logout.tscommunity/src/pages/api/auth/register.tscommunity/src/pages/api/follows/index.tscommunity/src/pages/api/instances/index.tscommunity/src/pages/api/instances/submit.tscommunity/src/pages/api/instances/sync.tscommunity/src/pages/api/search.tscommunity/src/pages/api/stars/index.tscommunity/src/pages/auth/login.astrocommunity/src/pages/auth/register.astrocommunity/src/pages/dashboard.astrocommunity/src/pages/explore.astrocommunity/src/pages/index.astrocommunity/src/pages/instances/[id].astrocommunity/src/pages/instances/index.astrocommunity/src/pages/instances/submit.astrocommunity/src/pages/r/[instance]/[owner]/[repo].astrocommunity/src/pages/u/[username].astrocommunity/src/styles/globals.csscommunity/tailwind.config.mjscommunity/tsconfig.jsondocker-compose.ymldocs-site/src/content/docs/getting-started/installation.mddocs/features/federation.mddocs/getting-started/installation.mddrizzle.config.tspublic/favicon.pngpublic/favicon.svgpublic/logo.svgpublic/opencodelogo.pngscripts/e2e-proof.shsrc/components/dashboard/StackCard.astrosrc/components/ide/WebIDE.tsxsrc/components/issues/IssueDetail.tsxsrc/components/issues/IssuesList.tsxsrc/components/layout/Header.astrosrc/components/profile/UserProfileView.astrosrc/components/pulls/PullRequestsList.tsxsrc/components/ui/Logo.astrosrc/components/ui/Logo.tsxsrc/pages/[owner]/[repo]/issues/[number].astrosrc/pages/actions.astrosrc/pages/api/repos/[owner]/[repo]/pulls/[number]/comments.tssrc/pages/api/repos/[owner]/[repo]/pulls/[number]/reviews/batch.tssrc/pages/dashboard.astro
▼.astro/settings.json
+1-1
1
1
{
2
2
"_variables": {
3
- "lastUpdateCheck": 1785773647919
3
+ "lastUpdateCheck": 1787148919035
4
4
}
5
5
}
6
6
▼.dockerignore
+70-4
1
+# Version control
2
+.git
3
+.gitignore
4
+.gitattributes
5
+.github
6
+
7
+# Dependencies (always install inside container)
1
8
node_modules
9
+*/node_modules
10
+community/node_modules
11
+docs-site/node_modules
12
+cli/node_modules
13
+
14
+# Subprojects not needed in production app container
15
+community
16
+docs-site
17
+packages
18
+doc
19
+docs
20
+
21
+# Build outputs & caches
2
22
dist
3
-.git
4
-.env
5
-data
6
-tests
23
+.astro
24
+.cache
25
+.parcel-cache
26
+coverage
7
27
playwright-report
8
28
test-results
29
+.nyc_output
30
+
31
+# Local runtime data, state & logs
32
+data
33
+.tmp
34
+*.log
35
+logs
36
+*.local.db
37
+*.sqlite
38
+*.sqlite-journal
39
+*.db
40
+test.db
41
+
42
+# Test suites & benchmarks
43
+tests
44
+__tests__
45
+*.test.ts
46
+*.spec.ts
47
+playwright.config.ts
48
+vitest.config.ts
49
+
50
+# Documentation & project planning files
51
+*.md
52
+!DOCKERHUB.md
53
+UIPLAN.md
54
+features.md
55
+notimplemented.md
56
+github-roadmap-issues.json
57
+protection.json
58
+out.json
59
+pci
60
+check.log
61
+
62
+# IDE, agent & system metadata
63
+.vscode
64
+.idea
9
65
.DS_Store
66
+Thumbs.db
67
+*.swp
68
+*.swo
69
+.gemini
70
+scratch
71
+
72
+# Environment secrets
73
+.env
74
+.env.*
75
+!.env.example
10
76
▼.github/workflows/docker-publish.yml
+63-76
5
5
branches: [main]
6
6
tags: ["v*"]
7
7
workflow_dispatch:
8
+ inputs:
9
+ platforms:
10
+ description: "Platforms to build (e.g. linux/amd64,linux/arm64 or linux/amd64)"
11
+ required: false
12
+ default: "linux/amd64"
13
+ type: string
8
14
9
15
env:
10
16
REGISTRY: docker.io
11
- IMAGE_NAME: opencodehub/opencodehub
12
- ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: "true"
17
+ IMAGE_BASE: opencodehub/opencodehub
13
18
14
19
jobs:
15
20
build-and-push:
21
+ name: Build & Push (${{ matrix.target.name }})
16
22
runs-on: ubuntu-latest
23
+ strategy:
24
+ fail-fast: false
25
+ matrix:
26
+ target:
27
+ - name: app
28
+ file: ./Dockerfile
29
+ suffix: ""
30
+ title: "OpenCodeHub Platform"
31
+ desc: "A modern, self-hosted Git platform with stacked PRs, merge queue, CI/CD, and AI review."
32
+ - name: worker
33
+ file: ./Dockerfile.worker
34
+ suffix: "-worker"
35
+ title: "OpenCodeHub Worker"
36
+ desc: "Background task worker for OpenCodeHub (queues, webhooks, notifications)."
37
+ - name: runner
38
+ file: ./Dockerfile.runner
39
+ suffix: "-runner"
40
+ title: "OpenCodeHub Runner"
41
+ desc: "Docker-in-Docker CI/CD runner for OpenCodeHub pipelines."
42
+
17
43
permissions:
18
44
contents: read
19
45
packages: write
22
48
- name: Checkout
23
49
uses: actions/checkout@v4
24
50
25
- - name: Set up QEMU
51
+ - name: Determine platforms
52
+ id: platforms
53
+ run: |
54
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ github.event.inputs.platforms }}" ]; then
55
+ echo "platforms=${{ github.event.inputs.platforms }}" >> $GITHUB_OUTPUT
56
+ elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then
57
+ echo "platforms=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
58
+ else
59
+ echo "platforms=linux/amd64" >> $GITHUB_OUTPUT
60
+ fi
61
+
62
+ - name: Set up QEMU (only when multi-arch requested)
63
+ if: contains(steps.platforms.outputs.platforms, 'arm64')
26
64
uses: docker/setup-qemu-action@v3
27
65
28
66
- name: Set up Docker Buildx
34
72
username: ${{ secrets.DOCKER_USERNAME }}
35
73
password: ${{ secrets.DOCKER_TOKEN }}
36
74
37
- - name: Extract Metadata (App)
38
- id: meta-app
75
+ - name: Extract Metadata
76
+ id: meta
39
77
uses: docker/metadata-action@v5
40
78
with:
41
- images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
79
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_BASE }}${{ matrix.target.suffix }}
42
80
tags: |
43
81
type=ref,event=branch
44
82
type=semver,pattern={{version}}
46
84
type=sha,prefix=
47
85
type=raw,value=latest,enable={{is_default_branch}}
48
86
labels: |
49
- org.opencontainers.image.title=OpenCodeHub Platform
50
- org.opencontainers.image.description=A modern, self-hosted Git platform with stacked PRs, merge queue, CI/CD, and AI review.
87
+ org.opencontainers.image.title=${{ matrix.target.title }}
88
+ org.opencontainers.image.description=${{ matrix.target.desc }}
51
89
org.opencontainers.image.url=https://github.com/swadhinbiswas/OpencodeHub
52
90
org.opencontainers.image.source=https://github.com/swadhinbiswas/OpencodeHub
53
91
org.opencontainers.image.licenses=MIT
54
92
55
- - name: Extract Metadata (Worker)
56
- id: meta-worker
57
- uses: docker/metadata-action@v5
58
- with:
59
- images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-worker
60
- tags: |
61
- type=ref,event=branch
62
- type=semver,pattern={{version}}
63
- type=semver,pattern={{major}}.{{minor}}
64
- type=sha,prefix=
65
- type=raw,value=latest,enable={{is_default_branch}}
66
- labels: |
67
- org.opencontainers.image.title=OpenCodeHub Worker
68
- org.opencontainers.image.description=Background task worker for OpenCodeHub (queues, webhooks, notifications).
69
- org.opencontainers.image.url=https://github.com/swadhinbiswas/OpencodeHub
70
- org.opencontainers.image.source=https://github.com/swadhinbiswas/OpencodeHub
71
- org.opencontainers.image.licenses=MIT
72
-
73
- - name: Extract Metadata (Runner)
74
- id: meta-runner
75
- uses: docker/metadata-action@v5
76
- with:
77
- images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-runner
78
- tags: |
79
- type=ref,event=branch
80
- type=semver,pattern={{version}}
81
- type=semver,pattern={{major}}.{{minor}}
82
- type=sha,prefix=
83
- type=raw,value=latest,enable={{is_default_branch}}
84
- labels: |
85
- org.opencontainers.image.title=OpenCodeHub Runner
86
- org.opencontainers.image.description=Docker-in-Docker CI/CD runner for OpenCodeHub pipelines.
87
- org.opencontainers.image.url=https://github.com/swadhinbiswas/OpencodeHub
88
- org.opencontainers.image.source=https://github.com/swadhinbiswas/OpencodeHub
89
- org.opencontainers.image.licenses=MIT
90
-
91
- - name: Build and Push App Image
92
- uses: docker/build-push-action@v5
93
- with:
94
- context: .
95
- file: ./Dockerfile
96
- push: true
97
- tags: ${{ steps.meta-app.outputs.tags }}
98
- labels: ${{ steps.meta-app.outputs.labels }}
99
- cache-from: type=gha
100
- cache-to: type=gha,mode=max
101
- platforms: linux/amd64,linux/arm64
102
-
103
- - name: Build and Push Worker Image
93
+ - name: Build and Push Image
104
94
uses: docker/build-push-action@v5
105
95
with:
106
96
context: .
107
- file: ./Dockerfile.worker
97
+ file: ${{ matrix.target.file }}
108
98
push: true
109
- tags: ${{ steps.meta-worker.outputs.tags }}
110
- labels: ${{ steps.meta-worker.outputs.labels }}
111
- cache-from: type=gha
112
- cache-to: type=gha,mode=max
113
- platforms: linux/amd64,linux/arm64
99
+ tags: ${{ steps.meta.outputs.tags }}
100
+ labels: ${{ steps.meta.outputs.labels }}
101
+ cache-from: type=gha,scope=${{ matrix.target.name }}
102
+ cache-to: type=gha,mode=max,scope=${{ matrix.target.name }}
103
+ platforms: ${{ steps.platforms.outputs.platforms }}
114
104
115
- - name: Build and Push Runner Image
116
- uses: docker/build-push-action@v5
117
- with:
118
- context: .
119
- file: ./Dockerfile.runner
120
- push: true
121
- tags: ${{ steps.meta-runner.outputs.tags }}
122
- labels: ${{ steps.meta-runner.outputs.labels }}
123
- cache-from: type=gha
124
- cache-to: type=gha,mode=max
125
- platforms: linux/amd64,linux/arm64
105
+ update-dockerhub-desc:
106
+ name: Update DockerHub Description
107
+ runs-on: ubuntu-latest
108
+ needs: build-and-push
109
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
110
+ steps:
111
+ - name: Checkout
112
+ uses: actions/checkout@v4
126
113
127
- - name: Update Docker Hub Repository Description
114
+ - name: Update Docker Hub Description
128
115
uses: peter-evans/dockerhub-description@v4
129
116
with:
130
117
username: ${{ secrets.DOCKER_USERNAME }}
131
118
password: ${{ secrets.DOCKER_TOKEN }}
132
- repository: ${{ env.IMAGE_NAME }}
119
+ repository: ${{ env.IMAGE_BASE }}
133
120
readme-filepath: ./DOCKERHUB.md
134
121
short-description: "Self-hosted Git platform with stacked PRs, merge queue, CI/CD, and AI code review"
135
122
▼Dockerfile
+44-42
1
-FROM oven/bun:1 AS base
1
+# ── Stage 1: Base Environment ──────────────────────────────────
2
+FROM oven/bun:1-slim AS base
2
3
WORKDIR /app
4
+
3
5
LABEL org.opencontainers.image.title="OpenCodeHub" \
4
6
org.opencontainers.image.description="A modern, self-hosted Git platform with stacked PRs, merge queue, CI/CD, and AI review." \
5
7
org.opencontainers.image.url="https://github.com/swadhinbiswas/OpencodeHub" \
6
8
org.opencontainers.image.source="https://github.com/swadhinbiswas/OpencodeHub" \
7
9
org.opencontainers.image.licenses="MIT"
8
10
9
-# Install ALL dependencies (needed for build including CSS tooling)
11
+# ── Stage 2: Dependencies (All Dev + Prod for build) ───────────
10
12
FROM base AS deps
11
-RUN apt-get update && apt-get install -y python3 make g++ gcc libc6-dev && rm -rf /var/lib/apt/lists/*
13
+RUN apt-get update && apt-get install -y --no-install-recommends \
14
+ python3 make g++ gcc libc6-dev \
15
+ && rm -rf /var/lib/apt/lists/*
16
+
12
17
COPY package.json bun.lock ./
13
18
COPY cli/package.json ./cli/package.json
14
-RUN for i in 1 2 3; do \
15
- bun install --frozen-lockfile && break || \
16
- (echo "bun install attempt $i failed, retrying in 10s..." && sleep 10); \
17
- done
19
+RUN bun install --frozen-lockfile
18
20
19
-# Install production dependencies only (for runtime image)
21
+# ── Stage 3: Production Dependencies Only ──────────────────────
20
22
FROM base AS prod-deps
21
-RUN apt-get update && apt-get install -y python3 make g++ gcc libc6-dev && rm -rf /var/lib/apt/lists/*
23
+RUN apt-get update && apt-get install -y --no-install-recommends \
24
+ python3 make g++ gcc libc6-dev \
25
+ && rm -rf /var/lib/apt/lists/*
26
+
22
27
COPY package.json bun.lock ./
23
28
COPY cli/package.json ./cli/package.json
24
-# Clear Bun install cache to avoid stale/corrupt node-gyp entries across layers
25
-RUN rm -rf /root/.bun/install/cache || true
26
-RUN for i in 1 2 3; do \
27
- bun install --frozen-lockfile --production && break || \
28
- (echo "prod bun install attempt $i failed, clearing cache and retrying in 10s..." && \
29
- rm -rf /root/.bun/install/cache && sleep 10); \
30
- done
29
+RUN bun install --frozen-lockfile --production \
30
+ && find /app/node_modules -type d \( -name "test" -o -name "tests" -o -name "docs" -o -name "example" -o -name "examples" \) -prune -exec rm -rf {} + 2>/dev/null || true \
31
+ && find /app/node_modules -type f \( -name "*.map" -o -name "*.md" -o -name "*.markdown" \) -delete 2>/dev/null || true
31
32
32
-# Build the application
33
+# ── Stage 4: Builder ───────────────────────────────────────────
33
34
FROM base AS builder
34
35
COPY --from=deps /app/node_modules ./node_modules
35
-COPY . .
36
+COPY package.json tsconfig.json astro.config.mjs tailwind.config.mjs postcss.config.mjs drizzle.config.ts docker-entrypoint.sh ./
37
+COPY public ./public
38
+COPY src ./src
39
+COPY cli ./cli
40
+COPY drizzle ./drizzle
41
+COPY scripts ./scripts
42
+
43
+ENV NODE_ENV=production
36
44
ENV SKIP_REDIS_CHECK=1
37
45
RUN bun run build
38
46
39
-# Production image
47
+# ── Stage 5: Production Runner (Lean & Minimal) ────────────────
40
48
FROM oven/bun:1-slim AS runner
41
49
WORKDIR /app
42
50
43
-# Install git, ssh, bash, and wget (git operations, entrypoint, healthcheck)
44
-RUN apt-get update && apt-get install -y --no-install-recommends git openssh-client bash wget && \
45
- rm -rf /var/lib/apt/lists/*
51
+# Install minimal runtime dependencies
52
+RUN apt-get update && apt-get install -y --no-install-recommends \
53
+ git \
54
+ openssh-client \
55
+ bash \
56
+ wget \
57
+ ca-certificates \
58
+ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
46
59
47
-# Create data directories
48
-RUN mkdir -p /data/repositories /data/storage /data/cache /data/ssh && \
49
- chown -R bun:bun /data
60
+# Create persistent storage directories
61
+RUN mkdir -p /data/repos /data/repositories /data/storage /data/cache /data/ssh /data/logs && \
62
+ chown -R bun:bun /data /app
50
63
51
-# Copy only what's needed for runtime
64
+# Copy runtime assets
52
65
COPY --from=builder --chown=bun:bun /app/dist ./dist
53
66
COPY --from=prod-deps --chown=bun:bun /app/node_modules ./node_modules
54
67
COPY --from=builder --chown=bun:bun /app/package.json ./
55
-
56
-# Copy drizzle config, committed migrations, and full source tree.
57
-# The full src/ is required so raw-TS entrypoints (scripts/ssh-server.ts,
58
-# scripts/worker.ts) can resolve the "@/..." path aliases via tsconfig.
59
68
COPY --from=builder --chown=bun:bun /app/drizzle.config.ts ./
60
69
COPY --from=builder --chown=bun:bun /app/drizzle ./drizzle
61
70
COPY --from=builder --chown=bun:bun /app/src ./src
62
71
COPY --from=builder --chown=bun:bun /app/tsconfig.json ./
63
-
64
-# Copy runtime scripts (migrate, ssh-server, worker) and entrypoint
65
72
COPY --from=builder --chown=bun:bun /app/scripts ./scripts
66
-COPY --chown=bun:bun docker-entrypoint.sh ./
73
+COPY --from=builder --chown=bun:bun /app/docker-entrypoint.sh ./
67
74
68
-# Set environment variables
75
+# Environment defaults
69
76
ENV HOST=0.0.0.0
70
77
ENV PORT=4321
71
78
ENV DATA_DIR=/data
75
82
ENV SSH_PATH=/data/ssh
76
83
ENV NODE_ENV=production
77
84
78
-# Expose ports
79
-EXPOSE 4321
80
-
81
-# Switch to non-root user
85
+EXPOSE 4321 2222
82
86
USER bun
83
87
84
-# Health check
85
-HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
86
- CMD wget --no-verbose --tries=1 --spider http://localhost:4321/api/health || exit 1
88
+HEALTHCHECK --interval=20s --timeout=5s --start-period=10s --retries=3 \
89
+ CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:4321/api/health || exit 1
87
90
88
-# Start the application with entrypoint
89
91
ENTRYPOINT ["bash", "./docker-entrypoint.sh"]
90
92
▼Dockerfile.runner
+18-16
1
-# CI/CD Runner Dockerfile
1
+# syntax=docker/dockerfile:1.4
2
+# OpenCodeHub CI/CD Runner (Docker-in-Docker)
2
3
FROM docker:24-dind
3
4
4
-# Install Node.js and other dependencies
5
+LABEL org.opencontainers.image.title="OpenCodeHub Runner" \
6
+ org.opencontainers.image.description="Docker-in-Docker CI/CD runner for OpenCodeHub pipelines." \
7
+ org.opencontainers.image.url="https://github.com/swadhinbiswas/OpencodeHub" \
8
+ org.opencontainers.image.source="https://github.com/swadhinbiswas/OpencodeHub" \
9
+ org.opencontainers.image.licenses="MIT"
10
+
11
+# Install essential build tools & runtimes in a single layer with clean cache
5
12
RUN apk add --no-cache \
6
13
nodejs \
7
14
npm \
15
22
make \
16
23
gcc \
17
24
g++ \
18
- libc-dev
25
+ libc-dev \
26
+ tar \
27
+ gzip \
28
+ xz
19
29
20
-# Install common build tools
21
-RUN npm install -g \
22
- typescript \
23
- @types/node \
24
- pnpm \
25
- yarn
30
+# Install global package managers and clean npm cache
31
+RUN npm install -g pnpm yarn @types/node \
32
+ && npm cache clean --force \
33
+ && rm -rf /root/.npm /tmp/* /var/cache/apk/*
26
34
27
35
WORKDIR /runner
28
36
29
-# Copy runner script
30
37
COPY scripts/runner.sh /runner/runner.sh
31
-RUN chmod +x /runner/runner.sh
32
-
33
-# Create work directories
34
-RUN mkdir -p /work /cache
38
+RUN chmod +x /runner/runner.sh && mkdir -p /work /cache
35
39
36
-# Environment variables
37
40
ENV RUNNER_WORK_DIR=/work
38
41
ENV RUNNER_CACHE_DIR=/cache
39
42
ENV DOCKER_HOST=unix:///var/run/docker.sock
40
43
41
-# Entry point
42
44
ENTRYPOINT ["/runner/runner.sh"]
43
45
▼Dockerfile.worker
+35-35
1
-FROM oven/bun:1 AS base
1
+FROM oven/bun:1-slim AS base
2
2
WORKDIR /app
3
3
4
-# Install dependencies
5
-FROM base AS deps
6
-RUN apt-get update && apt-get install -y python3 make g++ gcc libc6-dev && rm -rf /var/lib/apt/lists/*
4
+LABEL org.opencontainers.image.title="OpenCodeHub Worker" \
5
+ org.opencontainers.image.description="Background task worker for OpenCodeHub (queues, webhooks, notifications)." \
6
+ org.opencontainers.image.url="https://github.com/swadhinbiswas/OpencodeHub" \
7
+ org.opencontainers.image.source="https://github.com/swadhinbiswas/OpencodeHub" \
8
+ org.opencontainers.image.licenses="MIT"
9
+
10
+# ── Dependencies ───────────────────────────────────────────────
11
+FROM base AS prod-deps
12
+RUN apt-get update && apt-get install -y --no-install-recommends \
13
+ python3 make g++ gcc libc6-dev \
14
+ && rm -rf /var/lib/apt/lists/*
15
+
7
16
COPY package.json bun.lock ./
8
17
COPY cli/package.json ./cli/package.json
9
-RUN for i in 1 2 3; do \
10
- bun install --frozen-lockfile && break || \
11
- (echo "bun install attempt $i failed, retrying in 10s..." && sleep 10); \
12
- done
13
-
14
-# Copy source into a staging layer so COPY --from has the files
15
-FROM base AS source
16
-COPY --from=deps /app/node_modules ./node_modules
17
-COPY . .
18
-
19
-# Production image
20
-FROM oven/bun:1 AS runner
21
-WORKDIR /app
18
+RUN bun install --frozen-lockfile --production \
19
+ && find /app/node_modules -type d \( -name "test" -o -name "tests" -o -name "docs" -o -name "example" -o -name "examples" \) -prune -exec rm -rf {} + 2>/dev/null || true \
20
+ && find /app/node_modules -type f \( -name "*.map" -o -name "*.md" -o -name "*.markdown" \) -delete 2>/dev/null || true
22
21
23
-# Install git (needed for git operations)
24
-RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
22
+# ── Production Worker Runner ───────────────────────────────────
23
+FROM oven/bun:1-slim AS runner
24
+WORKDIR /app
25
25
26
-# Create data directories
27
-RUN mkdir -p /data/repositories /data/storage /data/cache && \
28
- chown -R bun:bun /data
26
+RUN apt-get update && apt-get install -y --no-install-recommends \
27
+ git \
28
+ bash \
29
+ ca-certificates \
30
+ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
29
31
30
-# Copy node_modules and source
31
-COPY --from=source --chown=bun:bun /app/node_modules ./node_modules
32
-COPY --from=source --chown=bun:bun /app/package.json ./
33
-COPY --from=source --chown=bun:bun /app/cli ./cli
34
-COPY --from=source --chown=bun:bun /app/tsconfig.json ./
32
+RUN mkdir -p /data/repositories /data/storage /data/cache /data/logs && \
33
+ chown -R bun:bun /data /app
35
34
36
-# Copy source files needed for worker
37
-COPY --from=source --chown=bun:bun /app/src/lib ./src/lib
38
-COPY --from=source --chown=bun:bun /app/src/db ./src/db
39
-COPY --from=source --chown=bun:bun /app/scripts/worker.ts ./scripts/worker.ts
35
+COPY --from=prod-deps --chown=bun:bun /app/node_modules ./node_modules
36
+COPY --chown=bun:bun package.json tsconfig.json ./
37
+COPY --chown=bun:bun drizzle.config.ts ./
38
+COPY --chown=bun:bun drizzle ./drizzle
39
+COPY --chown=bun:bun src ./src
40
+COPY --chown=bun:bun scripts ./scripts
41
+COPY --chown=bun:bun docker-entrypoint.sh ./
40
42
41
-# Set environment variables
42
43
ENV NODE_ENV=production
44
+ENV PROCESS_TYPE=worker
43
45
ENV DATA_DIR=/data
44
46
45
-# Switch to non-root user
46
47
USER bun
47
48
48
-# Start the worker
49
-CMD ["bun", "scripts/worker.ts"]
49
+ENTRYPOINT ["bash", "./docker-entrypoint.sh"]
50
50
▼README.md
+139-170
1
1
<p align="center">
2
- <picture>
3
- <source media="(prefers-color-scheme: dark)" srcset="public/logo-dark.png">
4
- <source media="(prefers-color-scheme: light)" srcset="public/logo-light.png">
5
- <img src="public/logo-light.png" alt="OpenCodeHub" width="420" />
6
- </picture>
2
+ <a href="https://github.com/swadhinbiswas/OpencodeHub">
3
+ <img src="public/logo.svg" alt="OpenCodeHub Logo" width="128" height="128" />
4
+ </a>
7
5
</p>
8
6
9
-<h3 align="center">The self-hosted Git platform that doesn't compromise.</h3>
7
+<h1 align="center">OpenCodeHub</h1>
10
8
11
9
<p align="center">
12
- <a href="https://github.com/swadhinbiswas/OpencodeHub/actions"><img src="https://img.shields.io/badge/CI-passing-brightgreen?style=for-the-badge" alt="CI"></a>
10
+ <strong>The self-hosted Git collaboration platform that doesn't compromise.</strong><br>
11
+ Stacked PRs · Speculative Merge Queue · GitHub Actions CI/CD · AI Code Review · Enterprise Security · High-Performance CLI
12
+</p>
13
+
14
+<p align="center">
15
+ <a href="https://github.com/swadhinbiswas/OpencodeHub/actions"><img src="https://img.shields.io/badge/CI-passing-238636?style=for-the-badge&logo=githubactions&logoColor=white" alt="CI"></a>
13
16
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue?style=for-the-badge" alt="License"></a>
14
- <a href="https://www.npmjs.com/package/opencodehub-cli"><img src="https://img.shields.io/npm/v/opencodehub-cli?style=for-the-badge&label=CLI" alt="CLI version"></a>
15
- <a href="https://docs.opencodehub.space"><img src="https://img.shields.io/badge/docs-opencodehub.space-brightgreen?style=for-the-badge" alt="Docs"></a>
16
- <a href="https://hub.docker.com/r/opencodehub/opencodehub"><img src="https://img.shields.io/badge/docker-opencodehub%2Fopencodehub-blue?style=for-the-badge&logo=docker" alt="Docker"></a>
17
+ <a href="https://www.npmjs.com/package/opencodehub-cli"><img src="https://img.shields.io/npm/v/opencodehub-cli?style=for-the-badge&label=CLI&color=black" alt="CLI version"></a>
18
+ <a href="https://hub.docker.com/r/opencodehub/opencodehub"><img src="https://img.shields.io/badge/docker-251MB-2496ED?style=for-the-badge&logo=docker&logoColor=white" alt="Docker"></a>
19
+ <a href="https://docs.opencodehub.space"><img src="https://img.shields.io/badge/docs-opencodehub.space-58a6ff?style=for-the-badge" alt="Docs"></a>
17
20
</p>
18
21
19
22
---
20
23
21
-OpenCodeHub is a self-hosted Git platform with **stacked PRs**, **merge queue**, **CI/CD pipelines**, and **AI code review**. One platform for everything your team needs — no vendor lock-in, no per-seat pricing.
24
+OpenCodeHub is an open-source, self-hosted alternative to GitHub and GitLab engineered as a modular monolith. It provides stack-first developer workflows (Graphite-style stacked diffs), an automated merge queue with speculative builds, Docker-based CI/CD pipelines, and multi-model AI code reviews out of the box — with zero per-seat licensing fees.
22
25
23
-**[Documentation](https://docs.opencodehub.space)** · **[Deploy in 5 minutes](#deploy)** · **[CLI Reference](https://docs.opencodehub.space/reference/cli-commands/)**
26
+**[Documentation](https://docs.opencodehub.space)** · **[CLI Quickstart](#-opencodehub-cli-och)** · **[Deploy in 5 Minutes](#-quickstart--deployment)** · **[API Reference](https://docs.opencodehub.space/api/rest-api/)**
24
27
25
28
---
26
29
27
-## Demo
28
-
29
-<!-- Replace VIDEO_ID with your YouTube video ID -->
30
-[![OpenCodeHub Demo](https://img.youtube.com/vi/VIDEO_ID/maxresdefault.jpg)](https://youtu.be/VIDEO_ID)
30
+## ✨ Key Differentiators
31
31
32
-*Watch the full walkthrough — deployment, stacked PRs, AI review, and merge queue in action.*
32
+| Capability | What It Does | Why It Matters |
33
+|---|---|---|
34
+| **Stacked PRs** | Break complex features into small, dependent branches and PRs (`och stack`) | Faster reviews, zero merge blocking, unblocked teammates |
35
+| **Merge Queue** | Parallel CI validation with speculative builds and priority lanes | Protects `main` from broken builds without serialized slow merges |
36
+| **CI/CD Pipelines** | GitHub Actions YAML compatibility with isolated Docker executors | Native pipeline execution without external CI SaaS dependencies |
37
+| **Multi-Provider AI Review** | Automated code reviews powered by GPT-4, Claude 3.5, Gemini 1.5, Groq, Ollama | Instant feedback on PRs before human peer review |
38
+| **Pluggable Storage** | Local filesystem or S3-compatible object storage (AWS, MinIO, R2, B2, Ceph) | Flexible deployment across homelabs, NAS servers, or hyper-scale clouds |
39
+| **Federation** | Fork, push branches, and submit cross-instance pull requests across servers | Seamless collaboration across autonomous OpenCodeHub instances |
33
40
34
41
---
35
42
36
-## Why OpenCodeHub?
43
+## 💻 OpenCodeHub CLI (`och`)
37
44
38
-| Problem | Solution |
39
-|---------|----------|
40
-| PRs too large to review | **Stacked PRs** — break changes into small, dependent branches |
41
-| Merge conflicts on main | **Merge Queue** — stack-aware ordering with speculative CI builds |
42
-| Slow review cycles | **AI Code Review** — catches bugs before humans even look |
43
-| Split across 5+ services | **All-in-one** — Git, PRs, CI/CD, issues, wiki in one place |
44
-| Data leaves your servers | **Self-hosted** — your code stays on your hardware |
45
+The official OpenCodeHub CLI (`opencodehub-cli`) delivers a terminal-first workflow for stacks, code reviews, merge queues, and repository management.
45
46
47
+### Installation
46
48
47
-## Features
48
-
49
-### Core Platform
50
-- **Git Hosting** — HTTP smart protocol + SSH push/pull, forks, mirroring, LFS
51
-- **Pull Requests** — Inline comments, approvals, suggested changes, draft PRs
52
-- **Issues & Projects** — Labels, milestones, custom fields, kanban boards
53
-- **Wiki** — Repository wiki with revision history
54
-- **Organizations** — Teams, collaborators, role-based access control
55
-
56
-### Delivery Workflows
57
-- **Stacked PRs** — Graphite-style stacked branches with web + CLI support
58
-- **Merge Queue** — Stack-aware queue with speculative builds and priority lanes
59
-- **CI/CD Pipelines** — GitHub Actions-compatible engine with Docker-based runners
60
-- **Webhooks** — Outbound webhooks with event filtering and HMAC signing
61
-- **Automations** — Rule-based workflow automation for PRs and deployments
62
-
63
-### AI & Quality
64
-- **AI Code Review** — 10+ providers: GPT-4, Claude, Gemini, Groq, Ollama, OpenRouter
65
-- **Secret Scanning** — Detect secrets in commits before they reach production
66
-- **Branch Protection** — Required reviews, status checks, push restrictions
67
-- **Developer Metrics** — PR velocity, review efficiency, time-to-merge tracking
68
-
69
-### Security
70
-- **Authentication** — JWT sessions, OAuth (GitHub, Google, GitLab), 2FA/TOTP, SSO/SAML
71
-- **Authorization** — RBAC with roles, team permissions, collaborator levels
72
-- **Rate Limiting** — Redis-backed per-endpoint rate limiting
73
-- **Audit Logging** — Track all administrative actions
74
-
75
-### Extensibility
76
-- **REST API** — 175+ endpoints covering all platform features
77
-- **GraphQL** — Full GraphQL endpoint for flexible queries
78
-- **CLI** — `och` command line tool with 20+ command groups
79
-- **Storage** — Local, S3, MinIO, R2, or any S3-compatible backend
49
+Install globally using your preferred package manager:
80
50
51
+```bash
52
+# via npm
53
+npm install -g opencodehub-cli
81
54
82
-## Deploy
55
+# via bun
56
+bun add -g opencodehub-cli
83
57
84
-### Docker (Recommended)
58
+# via pnpm
59
+pnpm add -g opencodehub-cli
85
60
86
-```bash
87
-git clone https://github.com/swadhinbiswas/OpencodeHub.git
88
-cd OpenCodeHub
89
-cp .env.example .env
90
-docker compose up -d
91
-docker compose exec app bun run scripts/seed-admin.ts
61
+# via yarn
62
+yarn global add opencodehub-cli
92
63
```
93
64
94
-Open **http://localhost:4321** and create your admin account.
95
-
96
-### Render (Free)
97
-
98
-Deploy to Render with free PostgreSQL + Upstash Redis:
65
+Or run directly without installing:
99
66
100
67
```bash
101
-# 1. Create free Redis at upstash.com (Singapore region)
102
-# 2. Push to GitHub
103
-# 3. Render → New → Blueprint → Select your repo
104
-# 4. Set REDIS_URL and SITE_URL
105
-# 5. Deploy
68
+npx opencodehub-cli --help
106
69
```
107
70
108
-See the [Render Deployment Guide](docs/RENDER-DEPLOYMENT.md) for step-by-step instructions.
71
+### 1. Authenticate
109
72
110
-### More Deployment Options
73
+```bash
74
+# Interactive login
75
+och auth login --url https://git.yourcompany.com
111
76
112
-| Platform | Guide | Cost |
113
-|----------|-------|------|
114
-| Docker Compose | [Deployment Guide](docs/administration/deployment.md) | Free |
115
-| Render (Asia) | [Render Guide](docs/RENDER-DEPLOYMENT.md) | Free |
116
-| Oracle Cloud | [Free Deployment](docs/FREE-DEPLOYMENT.md) | Free forever |
117
-| NAS (Synology/TrueNAS) | [NAS Guide](docs/administration/deploy-nas.md) | Free |
118
-| Kubernetes | [K8s Guide](docs/administration/kubernetes.md) | Free |
119
-| Cloudflare Tunnel | [Cloudflare Guide](docs/administration/deploy-cloudflare.md) | Free |
77
+# Non-interactive / CI login with Personal Access Token
78
+och auth login --url https://git.yourcompany.com --token och_xxxxxxxxxxxx
120
79
80
+# Check health & auth status
81
+och config doctor
82
+```
121
83
122
-## CLI
84
+### 2. Stacked PR Workflow
85
+
86
+Create and submit Graphite-style stacked branches in seconds:
123
87
124
88
```bash
125
-# Install
126
-npm install -g opencodehub-cli
89
+# Create first stacked branch
90
+och stack create feature/user-auth
127
91
128
-# Login
129
-och auth login --url http://localhost:4321
92
+# Commit changes, then create dependent branch
93
+git commit -am "feat: implement auth middleware"
94
+och stack create feature/user-profile
130
95
131
-# Stacked PR workflow
132
-och stack create feature/auth-step-1
96
+# Push all branches in the stack and generate linked PRs automatically
133
97
och stack submit
98
+
99
+# Visualize your stack topology in terminal
100
+och stack log
101
+
102
+# Rebase the whole stack when upstream main updates
134
103
och stack sync
104
+```
135
105
136
-# Merge queue
137
-och queue list
138
-och queue add <pr-number>
106
+### 3. Interactive Focus Cockpit (`och focus`)
139
107
140
-# Interactive cockpit
108
+Launch the terminal dashboard for PRs, reviews, and merge queue operations:
109
+
110
+```bash
141
111
och focus
142
112
```
143
113
144
-[Full CLI Reference](https://docs.opencodehub.space/reference/cli-commands/)
114
+- **Interactive Timeline**: Switch branches, view diff stats, and trigger CI runs without leaving the CLI.
115
+- **Review Cockpit**: Approve, request changes, or trigger AI code reviews inline.
116
+- **Queue Controls**: Enqueue PRs and monitor speculative build states in realtime.
117
+
118
+### 4. Merge Queue Management
119
+
120
+```bash
121
+# View active queue status and speculative lanes
122
+och queue list
123
+
124
+# Enqueue a PR for automated CI validation and merge
125
+och queue add 42
126
+
127
+# Check queue position and build attempts
128
+och queue status 42
129
+```
145
130
146
131
---
147
132
148
-## API
133
+## 🚀 Quickstart & Deployment
134
+
135
+### Production Stack (Docker Compose)
149
136
150
137
```bash
151
-# Create a repository
152
-curl -X POST http://localhost:4321/api/repos \
153
- -H "Authorization: Bearer YOUR_TOKEN" \
154
- -H "Content-Type: application/json" \
155
- -d '{"name":"my-project","visibility":"public"}'
156
-
157
-# List pull requests
158
-curl http://localhost:4321/api/repos/owner/repo/pulls \
159
- -H "Authorization: Bearer YOUR_TOKEN"
160
-
161
-# GraphQL
162
-curl -X POST http://localhost:4321/api/graphql \
163
- -H "Authorization: Bearer YOUR_TOKEN" \
164
- -H "Content-Type: application/json" \
165
- -d '{"query": "{ repositories { name owner { username } } }"}'
138
+# 1. Clone the repository
139
+git clone https://github.com/swadhinbiswas/OpencodeHub.git
140
+cd OpenCodeHub
141
+
142
+# 2. Configure environment
143
+cp .env.example .env
144
+# Edit .env and set your secrets (JWT_SECRET, SESSION_SECRET, POSTGRES_PASSWORD, REDIS_PASSWORD)
145
+
146
+# 3. Launch the container stack
147
+docker compose up -d
148
+
149
+# 4. Initialize the admin account
150
+docker compose exec app bun scripts/seed-admin.ts
166
151
```
167
152
168
-[Full API Reference](https://docs.opencodehub.space/api/rest-api/)
153
+Open **`http://localhost:4321`** in your browser to start collaborating.
154
+
155
+### Container Images
156
+
157
+Official minimal production images (251 MB) are published on Docker Hub:
158
+
159
+| Service | Image Tag | Purpose |
160
+|---|---|---|
161
+| **Platform** | `opencodehub/opencodehub:latest` | Web UI + REST & GraphQL API + Git HTTP / SSH Server |
162
+| **Worker** | `opencodehub/opencodehub-worker:latest` | Background queues, webhooks, and automation jobs |
163
+| **Runner** | `opencodehub/opencodehub-runner:latest` | Docker-in-Docker CI/CD pipeline execution runner |
169
164
170
165
---
171
166
172
-## Tech Stack
173
-
174
-| Layer | Technology |
175
-|-------|-----------|
176
-| Framework | Astro 4.x (SSR) + React 18 |
177
-| UI | Tailwind CSS + Radix UI |
178
-| Database | PostgreSQL / SQLite / Turso (Drizzle ORM) |
179
-| Auth | JWT + OAuth + 2FA/TOTP + SSO/SAML |
180
-| Git | Native git CLI + simple-git + isomorphic-git |
181
-| SSH | ssh2 library |
182
-| CI/CD | Docker-based runners, GitHub Actions syntax |
183
-| Storage | Local filesystem or S3-compatible (AWS, MinIO, R2) |
184
-| AI | OpenAI, Anthropic, Google, Groq, Ollama, OpenRouter |
185
-| Queue | BullMQ + Redis |
186
-| CLI | Commander.js + Inquirer |
167
+## 🎨 Design System & Theme
168
+
169
+OpenCodeHub features a modern developer UI built on **Tailwind CSS**, **Radix UI**, and clean GitHub/Linear design principles:
170
+
171
+- **Adaptive Theming**: Native dark and light theme support with zero layout flicker.
172
+- **High-Contrast Dark Mode**: Pure zinc/slate dark surfaces (`#0d1117` / `#161b22`) paired with crisp emerald status indicators (`#238636`).
173
+- **Responsive Workspace**: Full mobile, tablet, and desktop fidelity with integrated keyboard shortcuts.
187
174
188
175
---
189
176
190
-## Architecture
177
+## 🏛 Architecture
191
178
192
179
```
193
180
┌─────────────────────────────────────────────────────────────┐
200
184
│
201
185
┌─────────────────────────────────────────────────────────────┐
202
186
│ OPENCODEHUB PLATFORM │
203
-│ Web UI (Astro+React) │ REST API (175+ routes) │ GraphQL │
204
-│ Git Server (HTTP) │ SSH Server (ssh2) │ │
205
-│ Pipeline Runner (Docker) │ │
187
+│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
188
+│ │ Web UI │ │ REST API │ │ GraphQL Endpoint │ │
189
+│ │ (Astro+React│ │ (175+ routes│ │ (src/pages/api/...) │ │
190
+│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
191
+│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
192
+│ │ Git Server │ │ SSH Server │ │ Pipeline Runner │ │
193
+│ │ (HTTP RPC) │ │ (ssh2 daemon│ │ (Docker Executor) │ │
194
+│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
206
195
└─────────────────────────────────────────────────────────────┘
207
196
│
208
197
┌─────────────────────────────────────────────────────────────┐
209
-│ PostgreSQL/SQLite/Turso │ Redis │ Pluggable Storage │
198
+│ PERSISTENCE & INFRASTRUCTURE │
199
+│ PostgreSQL / SQLite / Turso │ Redis (Queues) │ Pluggable S3 │
210
200
└─────────────────────────────────────────────────────────────┘
211
201
```
212
202
213
203
---
214
204
215
-## Documentation
216
-
217
-| Topic | Link |
218
-|-------|------|
219
-| Installation | [docs.opencodehub.space/getting-started/installation](https://docs.opencodehub.space/getting-started/installation/) |
220
-| Configuration | [docs.opencodehub.space/administration/configuration](https://docs.opencodehub.space/administration/configuration/) |
221
-| Stacked PRs | [docs.opencodehub.space/features/stacked-prs](https://docs.opencodehub.space/features/stacked-prs/) |
222
-| AI Code Review | [docs.opencodehub.space/features/ai-review](https://docs.opencodehub.space/features/ai-review/) |
223
-| CLI Reference | [docs.opencodehub.space/reference/cli-commands](https://docs.opencodehub.space/reference/cli-commands/) |
224
-| API Reference | [docs.opencodehub.space/api/rest-api](https://docs.opencodehub.space/api/rest-api/) |
225
-| Deployment | [docs.opencodehub.space/administration/deployment](https://docs.opencodehub.space/administration/deployment/) |
226
-
227
-
228
-## Contributing
205
+## 🧪 Development & Testing
229
206
230
207
```bash
231
-# Clone and setup
232
-git clone https://github.com/swadhinbiswas/OpencodeHub.git
233
-cd OpenCodeHub
234
-cp .env.example .env
208
+# Install dependencies
235
209
npm install
236
-npm run db:push
237
-bun run scripts/seed-admin.ts
238
210
239
-# Start development
240
-npm run dev
211
+# Push database schema
212
+npm run db:push
241
213
242
-# Run tests
214
+# Run full test suite (679 unit, integration & contract tests)
243
215
npm run test
244
-```
245
216
246
-See [CONTRIBUTING.md](CONTRIBUTING.md) for development workflow and standards.
247
-
248
-
249
-## Community
250
-
251
-- **GitHub**: [github.com/swadhinbiswas/OpencodeHub](https://github.com/swadhinbiswas/OpencodeHub)
252
-- **Documentation**: [docs.opencodehub.space](https://docs.opencodehub.space)
253
-- **Issues**: [GitHub Issues](https://github.com/swadhinbiswas/OpencodeHub/issues)
254
-- **Discussions**: [GitHub Discussions](https://github.com/swadhinbiswas/OpencodeHub/discussions)
217
+# Typecheck & verify build
218
+npm run typecheck
219
+npm run build
220
+```
255
221
256
222
---
257
223
258
-## License
224
+## 📄 License
259
225
260
-[MIT](LICENSE) — Use it however you want.
226
+OpenCodeHub is released under the [MIT License](LICENSE).
261
227
▼UIPLAN.md
+159-0
1
+# OpenCodeHub — Self-Hosted GitHub UI Overhaul Plan (`UIPLAN.md`)
2
+
3
+> **Objective**: Completely refactor and elevate OpenCodeHub into an authentic, pixel-perfect self-hosted GitHub platform inspired by the minimalist, high-contrast, black-and-white monochrome aesthetic of [Ryoku](https://ryoku.dev/) and the exact GitHub profile layout provided in the reference screenshot.
4
+
5
+---
6
+
7
+## 1. System Vision & Landing Page Paradigm
8
+
9
+```
10
+┌─────────────────────────────────────────────────────────────────────────────┐
11
+│ GLOBAL HEADER & SERVER NAVIGATION │
12
+│ [Logo] [User/Org Switcher] [Search: Type / to search] [+] [PRs] [🔔] [Avatar]│
13
+├─────────────────────────────────────────────────────────────────────────────┤
14
+│ [Overview] [Repositories 87] [Projects] [Packages] [Stars 50] │
15
+├───────────────────────────────┬─────────────────────────────────────────────┤
16
+│ LEFT SIDEBAR (PROFILE) │ RIGHT MAIN CONTENT COLUMN │
17
+│ │ │
18
+│ ┌─────────────────────────┐ │ ┌───────────────────────────────────────┐ │
19
+│ │ Big Avatar (Circle) │ │ │ swadhinbiswas / readme.md │ │
20
+│ │ + Status Badge (💭) │ │ │ │ │
21
+│ └─────────────────────────┘ │ │ [Rich Rendered Markdown Profile] │ │
22
+│ Name: Swadhin Biswas │ │ - Role badges │ │
23
+│ User: swadhinbiswas · he/him │ │ - Project showcase table │ │
24
+│ [Follow/Edit] [💖 Sponsor] │ │ - Social contact pills │ │
25
+│ Bio: MLOps & Data Engineer.. │ └───────────────────────────────────────┘ │
26
+│ 👥 94 followers · 116 following│ │
27
+│ │ ┌───────────────────────────────────────┐ │
28
+│ 🏢 @TheOpenCodeHub │ │ Pinned Repositories (2-Column Grid) │ │
29
+│ 📍 Dhaka, Bangladesh │ │ ┌─────────────────┐ ┌───────────────┐ │ │
30
+│ 🕒 16:55 (Local Time) │ │ │ opengrammar │ │ FAANG-Playbook│ │ │
31
+│ ✉️ [email protected]│ │ │ TypeScript ★115│ │ Astro ★36 │ │ │
32
+│ 🔗 https://swadhin.cv │ │ └─────────────────┘ └───────────────┘ │ │
33
+│ Socials: X, LinkedIn, HF... │ └───────────────────────────────────────┘ │
34
+│ │ │
35
+│ 🏆 Achievements Badges │ ┌───────────────────────────────────────┐ │
36
+│ ⭐ Highlights (PRO) │ │ 365-Day Contribution Calendar Heatmap │ │
37
+│ 🏛️ Organizations Grid │ │ 6,870 contributions in 2026 │ │
38
+│ │ │ [52 Weeks x 7 Days Grid] [Years Tabs] │ │
39
+│ [Block or report user] │ └───────────────────────────────────────┘ │
40
+└───────────────────────────────┴─────────────────────────────────────────────┘
41
+```
42
+
43
+### 1.1 Self-Hosted Landing Page Philosophy
44
+On a self-hosted Git platform, visitors and users should not see a generic SaaS marketing page. Instead:
45
+1. **Root URL (`/`)**: Directly renders the **Primary Server User Profile** (or primary organization / logged-in user dashboard), complete with:
46
+ - Profile Header & Tabs (`Overview`, `Repositories`, `Projects`, `Packages`, `Stars`)
47
+ - Left Profile Dossier (Avatar, bio, followers, metadata, achievements, orgs)
48
+ - Profile README (`{username}/README.md` from `{username}/{username}` repo)
49
+ - Pinned Repositories grid
50
+ - 365-Day Contribution Heatmap & activity timeline
51
+ - **Server User & Organization Directory Bar**: Quick discovery and switcher for all users and organizations hosted on this server instance.
52
+2. **User Profile URL (`/[owner]`)**: Renders the exact same GitHub-authentic profile for any user or organization on the server.
53
+3. **Repository Workspace (`/[owner]/[repo]`)**: Clean GitHub-style sub-nav (`Code`, `Issues`, `Pull Requests`, `Actions`, `Merge Queue`, `Wiki`, `Security`, `Settings`) with Ryoku-styled monochrome file browser, commit history, and Markdown view.
54
+
55
+---
56
+
57
+## 2. Design Tokens & Visual Hierarchy (Ryoku Monochrome)
58
+
59
+### 2.1 CSS Color Tokens (`src/styles/globals.css`)
60
+```css
61
+/* Ryoku High-Contrast Monochrome */
62
+:root {
63
+ --background: 0 0% 100%;
64
+ --foreground: 0 0% 9%;
65
+ --card: 0 0% 98%;
66
+ --card-foreground: 0 0% 9%;
67
+ --border: 0 0% 89%;
68
+ --input: 0 0% 89%;
69
+ --ring: 0 0% 9%;
70
+ --radius: 0.375rem;
71
+}
72
+
73
+.dark {
74
+ --background: 0 0% 3.5%; /* #09090b - Obsidian void */
75
+ --foreground: 0 0% 98%; /* #fafafa - Crisp white text */
76
+ --card: 0 0% 5.5%; /* #0e0e11 - Elevated container */
77
+ --card-foreground: 0 0% 98%;
78
+ --popover: 0 0% 5.5%;
79
+ --popover-foreground: 0 0% 98%;
80
+ --primary: 0 0% 98%; /* Pure white primary button/accent */
81
+ --primary-foreground: 0 0% 9%; /* Dark text on white */
82
+ --secondary: 0 0% 10%; /* #1a1a1e - Subtle background */
83
+ --secondary-foreground: 0 0% 98%;
84
+ --muted: 0 0% 10%;
85
+ --muted-foreground: 0 0% 60%; /* #999999 - Subdued metadata */
86
+ --accent: 0 0% 12%;
87
+ --accent-foreground: 0 0% 98%;
88
+ --border: 0 0% 14%; /* 1px precision hairline border */
89
+ --input: 0 0% 14%;
90
+ --ring: 0 0% 98%;
91
+ --radius: 0.375rem;
92
+}
93
+```
94
+
95
+---
96
+
97
+## 3. Component Architecture & Modular Breakdown
98
+
99
+### 3.1 Global Header (`src/components/layout/Header.astro`)
100
+- Sleek sticky glass navbar (`backdrop-blur-md bg-background/90 border-b border-border`).
101
+- Left: GitHub / OpenCodeHub mark + Server instance badge (`Self-Hosted`) + Quick User/Org switcher dropdown.
102
+- Center/Right: Search bar (`Type / to search` with `⌘K` trigger), `+` create dropdown (New repo, new org), Issues icon, Pull Requests icon, Notifications inbox icon with unread badge, and User Avatar menu.
103
+
104
+### 3.2 User Profile Component (`src/components/profile/UserProfileView.astro`)
105
+A unified, highly reusable component powering both `/` (landing page) and `/[owner]` (profile page):
106
+
107
+1. **Sub-Navigation Tabs**:
108
+ - `Overview` (active indicator)
109
+ - `Repositories` (with live count badge)
110
+ - `Projects`
111
+ - `Packages`
112
+ - `Stars` (with live star count badge)
113
+2. **Left Profile Sidebar (Dossier)**:
114
+ - **Avatar Frame**: `296x296px` round avatar with status indicator badge (`💭`).
115
+ - **Identity**: Large display name (`text-2xl font-bold`), username (`text-xl text-muted-foreground font-light`), pronouns (`he/him`).
116
+ - **Actions**: `Edit profile` (if logged in owner) or dual buttons `Follow` / `Unfollow` + `💖 Sponsor`.
117
+ - **Bio**: Full markdown/text bio with auto-linking.
118
+ - **Follower Counter**: `👥 94 followers · 116 following`.
119
+ - **Metadata Details**:
120
+ - Organization / Company link (`@TheOpenCodeHub`)
121
+ - Location pin (`Dhaka, Bangladesh`)
122
+ - Local time calculator based on timezone / offset
123
+ - Verified email mailto link
124
+ - Portfolio / Website URL
125
+ - Social links list (X / Twitter, LinkedIn, Hugging Face, ORCID, GitHub)
126
+ - **Achievements Shelf**: Interactive badges (`Pull Shark`, `Quickdraw`, `YOLO`, `Galaxy Brain`, `Starstruck`).
127
+ - **Highlights Shelf**: `PRO` member badge.
128
+ - **Organizations Shelf**: Grid of organization avatar icons with tooltips.
129
+ - **Footer Action**: "Block or report user".
130
+
131
+3. **Right Main Column**:
132
+ - **Profile README (`{username} / readme.md`)**:
133
+ - Header ribbon with file icon and repository source link.
134
+ - Rich GitHub-flavored markdown renderer with HTML tables, badge shields, social contact buttons, images, and code blocks.
135
+ - **Pinned Repositories (2-Column Grid)**:
136
+ - Header: `Pinned` + "Customize your pins" link for owner.
137
+ - Pinned cards: Repo icon, Name, `Public` badge pill, Description, Language color indicator, Star count, Fork count.
138
+ - **365-Day Contribution Calendar Heatmap**:
139
+ - Annual contributions summary banner (`6,870 contributions in 2026`).
140
+ - 52-week x 7-day grid with month headers, day labels, high-contrast monochrome/emerald cells, and hover tooltip.
141
+ - Multi-year switcher tabs on the right (`2026`, `2025`, `2024`, etc.).
142
+ - Contribution activity timeline below the calendar.
143
+
144
+---
145
+
146
+## 4. Implementation Steps
147
+
148
+| Step | Action Item | Target Files |
149
+| :--- | :--- | :--- |
150
+| **1. Theming & Tokens** | Update global CSS variables for Ryoku monochrome aesthetic | `src/styles/globals.css`, `tailwind.config.mjs` |
151
+| **2. Core Profile Components** | Create dedicated GitHub profile components | `src/components/profile/UserProfileView.astro`<br/>`src/components/profile/ProfileReadme.astro`<br/>`src/components/profile/PinnedRepos.astro`<br/>`src/components/profile/ContributionCalendar.astro` |
152
+| **3. Landing Page Transformation** | Wire root `/` to display the primary server user profile with instance switcher | `src/pages/index.astro` |
153
+| **4. User Profile Page Polish** | Wire `/[owner]/index.astro` to use the unified `UserProfileView` | `src/pages/[owner]/index.astro` |
154
+| **5. Header & Shell Refactor** | Update top navbar with GitHub layout, search, and user dropdown | `src/components/layout/Header.astro`, `src/layouts/BaseLayout.astro` |
155
+| **6. Verification & Test Suite** | Run typecheck, unit tests, and layout responsiveness checks | `npm run typecheck`, `npm test` |
156
+
157
+---
158
+
159
+*Plan updated and ready to execute.*
0
160
▼cli/README.md
+78-171
1
-<p align="center">
2
- <img src="https://raw.githubusercontent.com/swadhinbiswas/OpencodeHub/main/cli/assets/opencodehub-cli-logo.svg" alt="OpenCodeHub CLI" width="860" />
3
-</p>
4
-
5
-<h1 align="center">OpenCodeHub CLI (OCH)</h1>
1
+<h1 align="center">OpenCodeHub CLI (och)</h1>
6
2
7
3
<p align="center">
8
- Production-ready Git workflows and stack-first pull request management from your terminal.
4
+ <strong>Stack-first PR workflows, speculative merge queues, and terminal cockpit for OpenCodeHub.</strong>
9
5
</p>
10
6
11
7
<p align="center">
12
8
<a href="https://www.npmjs.com/package/opencodehub-cli"><img src="https://img.shields.io/npm/v/opencodehub-cli.svg?style=flat-square" alt="npm version" /></a>
13
9
<a href="https://www.npmjs.com/package/opencodehub-cli"><img src="https://img.shields.io/npm/dm/opencodehub-cli.svg?style=flat-square" alt="npm downloads" /></a>
14
- <a href="https://github.com/swadhinbiswas/OpencodeHub/blob/main/cli/LICENSE"><img src="https://img.shields.io/npm/l/opencodehub-cli.svg?style=flat-square" alt="license" /></a>
10
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square" alt="license" /></a>
15
11
</p>
16
12
17
-## Why OCH
13
+---
18
14
19
-- Fast command-line workflows for repositories, pull requests, issues, and reviews.
20
-- Stack-first branch/PR flow for multi-PR delivery.
21
-- Secure authentication model with OS credential storage support.
22
-- Built-in API tooling for scripting and automation.
15
+## 📦 Installation
23
16
24
-## Installation
17
+Install `opencodehub-cli` globally with your favorite package manager:
25
18
26
19
```bash
20
+# npm
27
21
npm install -g opencodehub-cli
28
-```
29
22
30
-Verify:
23
+# bun
24
+bun add -g opencodehub-cli
31
25
32
-```bash
33
-och --version
34
-och --help
35
-```
36
-
37
-## Quick Start
38
-
39
-```bash
40
-# 1) Authenticate
41
-och auth login --url https://git.example.com
42
-
43
-# 2) Inspect your setup
44
-och config doctor
26
+# pnpm
27
+pnpm add -g opencodehub-cli
45
28
46
-# 3) Push and open PR workflow
47
-cd your-repo
48
-och repo push
49
-
50
-# 4) Create a pull request
51
-och pr create --base main --title "feat: add onboarding"
29
+# yarn
30
+yarn global add opencodehub-cli
52
31
```
53
32
54
-## Core Commands
55
-
56
-- `och auth` authentication (`login`, `logout`, `status`)
57
-- `och config` CLI settings and diagnostics (`list`, `set`, `doctor`)
58
-- `och repo` repository operations (`create`, `clone`, `push`, `list`)
59
-- `och pr` pull request lifecycle (`create`, `list`, `view`, `merge`, ...)
60
-- `och stack` stacked branch/PR workflows (`create`, `submit`, `sync`, ...)
61
-- `och review` code review + AI review flows
62
-- `och issue` issue management
63
-- `och ci`, `och queue`, `och metrics`, `och insights`, `och notify`, `och automate`
64
-- `och api` direct API requests (useful for scripts)
65
-
66
-Run `och <command> --help` for full command options.
67
-
68
-## Authentication and Security
69
-
70
-### Login
33
+Or execute commands on-demand without installing:
71
34
72
35
```bash
73
-och auth login --url https://git.example.com
36
+npx opencodehub-cli focus
74
37
```
75
38
76
-For non-interactive environments:
39
+Verify your installation:
77
40
78
41
```bash
79
-och auth login --url https://git.example.com --token <PERSONAL_ACCESS_TOKEN>
42
+och --version
43
+och --help
80
44
```
81
45
82
-### Token storage behavior
83
-
84
-- `OCH_TOKEN` env var always takes precedence.
85
-- macOS: Keychain (`security`)
86
-- Linux: Secret Service (`secret-tool`)
87
-- Windows: DPAPI-encrypted storage via PowerShell
88
-- Fallback: local CLI config storage if secure backend is unavailable
89
-
90
-### Helpful environment variables
46
+---
91
47
92
-- `OCH_TOKEN`: inject token from environment
93
-- `OCH_HTTP_TIMEOUT_MS`: HTTP timeout in ms (default `15000`)
94
-- `OCH_DISABLE_KEYCHAIN=1`: disable credential backend (useful in CI/tests)
48
+## ⚡ Quick Start
95
49
96
-## Configuration
97
-
98
-Inspect current configuration:
50
+### 1. Authenticate with your OpenCodeHub Instance
99
51
100
52
```bash
101
-och config list
102
-och config path
103
-och config doctor
104
-```
53
+# Interactive login prompt
54
+och auth login --url https://git.yourcompany.com
105
55
106
-Set values:
107
-
108
-```bash
109
-och config set serverUrl https://git.example.com
110
-och config set defaultBranch main
111
-och config set insecure false
56
+# Non-interactive / CI login
57
+och auth login --url https://git.yourcompany.com --token och_xxxxxxxxxxxxxxxx
112
58
```
113
59
114
-## Common Workflows
115
-
116
-### Repository lifecycle
60
+Check configuration and credential storage health:
117
61
118
62
```bash
119
-# Create a remote repository
120
-och repo create my-service --description "Internal API service"
121
-
122
-# Clone repository
123
-och repo clone acme/my-service
124
-
125
-# Push local repository
126
-och repo push --branch main
63
+och config doctor
127
64
```
128
65
129
-### Pull request lifecycle
66
+### 2. Stacked PR Workflow
130
67
131
-```bash
132
-# Create PR from current branch
133
-och pr create --base main --title "feat: add API pagination"
134
-
135
-# List open PRs
136
-och pr list --state open
137
-
138
-# View PR details
139
-och pr view 42
140
-
141
-# Merge PR
142
-och pr merge 42
143
-```
144
-
145
-### Stack workflow
68
+OpenCodeHub supports Graphite-style stacked branches from your terminal:
146
69
147
70
```bash
148
-# Create first stack branch
149
-och stack create auth-foundation
71
+# 1. Create your first stack branch
72
+och stack create feature/part-1
150
73
151
-# Create subsequent branch
152
-och stack create auth-ui
74
+# Make edits and commit
75
+git commit -am "feat: part 1 implementation"
153
76
154
-# Submit stack and create/update PRs
155
-och stack submit
156
-```
77
+# 2. Create the next dependent branch on top of part-1
78
+och stack create feature/part-2
157
79
158
-### AI review workflow
80
+git commit -am "feat: part 2 implementation"
159
81
160
-```bash
161
-# Trigger AI review
162
-och review ai 42
82
+# 3. Submit all branches in the stack (pushes refs & creates linked PRs)
83
+och stack submit
163
84
164
-# Wait for completion
165
-och review ai 42 --wait
85
+# 4. Visualize the stack hierarchy
86
+och stack log
166
87
167
-# Check latest review status
168
-och review status 42
88
+# 5. Rebase stack when target base branch changes
89
+och stack sync
169
90
```
170
91
171
-### API mode for scripting
92
+---
172
93
173
-```bash
174
-# Read current user
175
-och api /user
94
+## 🕹 Interactive Focus Cockpit (`och focus`)
176
95
177
-# Create issue via API
178
-och api /repos/acme/platform/issues -X POST -F title="Bug: timeout" -F body="Steps to reproduce"
179
-```
96
+The `och focus` command opens an interactive terminal dashboard for fast daily development:
180
97
181
-## CI Usage Example
182
-
183
-```yaml
184
-name: OCH Automation
185
-on: [push]
186
-
187
-jobs:
188
- sync:
189
- runs-on: ubuntu-latest
190
- steps:
191
- - uses: actions/checkout@v4
192
- - uses: actions/setup-node@v4
193
- with:
194
- node-version: "20"
195
- - run: npm install -g opencodehub-cli
196
- - run: |
197
- export OCH_TOKEN="${{ secrets.OCH_TOKEN }}"
198
- och config set serverUrl https://git.example.com
199
- och config doctor
200
- och repo push --branch main
98
+```bash
99
+och focus
201
100
```
202
101
203
-## Troubleshooting
102
+- **Stack Cockpit**: Navigate branches, view parent/child dependencies, and submit changes.
103
+- **Review Inbox**: Inspect assigned reviews, view diff snippets, and submit approvals or change requests.
104
+- **Merge Queue Status**: Track speculative build progress and priority lanes in real time.
105
+- **AI Reviews**: Trigger AI review analysis directly from the terminal.
204
106
205
-### `Not logged in. Run 'och auth login' first.`
107
+---
206
108
207
-- Run `och auth login --url <server>`.
208
-- Or export `OCH_TOKEN` for automation.
109
+## 📋 Command Reference
209
110
210
-### `Server URL not configured`
111
+| Command | Description | Example |
112
+|---|---|---|
113
+| `och auth` | Manage authentication credentials | `och auth login --url http://localhost:4321` |
114
+| `och stack` | Stacked PR creation, sync, log, submit | `och stack submit` |
115
+| `och focus` | Interactive terminal review & stack cockpit | `och focus` |
116
+| `och pr` | Pull request lifecycle management | `och pr create --base main --title "feat: demo"` |
117
+| `och queue` | Merge queue control & speculative runs | `och queue list`, `och queue add 42` |
118
+| `och review` | Code review and AI review trigger | `och review start 42` |
119
+| `och repo` | Repository operations (clone, push, create) | `och repo create my-app` |
120
+| `och issue` | Issue tracking & milestone management | `och issue list`, `och issue create` |
121
+| `och ci` | CI pipeline logs and execution control | `och ci list`, `och ci view 12` |
122
+| `och config` | CLI configuration & doctor check | `och config doctor` |
123
+| `och whoami` | Display active user profile & server URL | `och whoami` |
124
+| `och api` | Make direct authenticated REST API calls | `och api GET /api/user` |
211
125
212
-- Run `och config set serverUrl https://git.example.com`.
126
+Run `och <command> --help` for specific flags and subcommands.
213
127
214
-### TLS / certificate issues
128
+---
215
129
216
-- Use `och config set caFile /path/to/ca.pem` for custom CA.
217
-- Use `och config set insecure true` only for temporary debugging.
130
+## 🔒 Security & Token Storage
218
131
219
-### Validate setup end-to-end
132
+The CLI automatically stores credentials in your operating system's secure credential manager:
220
133
221
-```bash
222
-och config doctor
223
-```
134
+- **macOS**: Apple Keychain (`security`)
135
+- **Linux**: Secret Service API / libsecret (`secret-tool`)
136
+- **Windows**: Windows Credential Manager / DPAPI
137
+- **CI / Headless**: Set the `OCH_TOKEN` and `OCH_URL` environment variables.
224
138
225
-## Development
226
-
227
-```bash
228
-cd cli
229
-npm install
230
-npm run build
231
-npm run test
232
-```
139
+---
233
140
234
-## License
141
+## 📄 License
235
142
236
-MIT
143
+MIT © OpenCodeHub Contributors
237
144
▼community/.env.example
+20-0
1
+# ======================================================
2
+# OpenCodeHub Community Hub — Environment Variables
3
+# ======================================================
4
+# REQUIRED: Turso database URL (create at https://turso.tech/dashboard)
5
+# Format: libsql://your-db-name-your-org.turso.io
6
+TURSO_DATABASE_URL=
7
+
8
+# REQUIRED: Turso auth token (from https://turso.tech/dashboard → your DB → Tokens)
9
+TURSO_AUTH_TOKEN=
10
+
11
+# JWT secret for community auth (change in production)
12
+JWT_SECRET=change-me-in-production
13
+
14
+# Upstash Redis (optional — for caching probe/repos data)
15
+# Get these from https://console.upstash.com → Redis → REST API
16
+UPSTASH_REDIS_REST_URL=
17
+UPSTASH_REDIS_REST_TOKEN=
18
+
19
+# Site URL (where the community hub is hosted)
20
+SITE_URL=http://localhost:4322
0
21
▼community/.gitignore
+6-0
1
+node_modules
2
+dist
3
+.astro
4
+data
5
+.env
6
+
0
7
▼community/README.md
+70-0
1
+# OpenCodeHub Community Hub
2
+
3
+Federated discovery hub for self-hosted **OpenCodeHub** instances. Anyone who hosts OpenCodeHub can submit their URL — the hub probes `GET /api/instance`, fetches `GET /api/repos?visibility=public`, caches public repos & profiles in **Turso** (LibSQL), and displays them with a beautiful, responsive **Tailwind + shadcn/ui** interface. Community users can star and follow; cloning/contributing requires your own node via federation.
4
+
5
+## Stack
6
+- **Framework:** Astro 4 + React 18 (SSR, `@astrojs/node` standalone)
7
+- **UI:** Tailwind CSS 3 + shadcn/ui (Radix) + lucide-react, dark mode, fully responsive
8
+- **DB:** Turso (LibSQL) via Drizzle ORM — `community_users`, `instances`, `cached_repos`, `cached_users`, `stars`, `follows`
9
+- **Cache:** Upstash Redis (`@upstash/redis`) with in-memory fallback (30s–1h TTLs)
10
+- **Auth:** Community JWT (jose + bcryptjs), cookie `community_session` (7d)
11
+
12
+## Setup
13
+
14
+### 1. Create a Turso database
15
+1. Sign up at [turso.tech](https://turso.tech/dashboard)
16
+2. Create a new database (e.g. `community-hub`)
17
+3. Copy the database URL (looks like `libsql://community-hub-your-org.turso.io`)
18
+4. Create an auth token (Settings → Auth Tokens → Create)
19
+
20
+### 2. Configure
21
+```bash
22
+cd community
23
+cp .env.example .env
24
+# Edit .env and set:
25
+# TURSO_DATABASE_URL=libsql://your-db-name-your-org.turso.io
26
+# TURSO_AUTH_TOKEN=your-token-here
27
+# JWT_SECRET=any-random-secret
28
+# UPSTASH_REDIS_REST_URL=https://... (optional)
29
+# UPSTASH_REDIS_REST_TOKEN=... (optional)
30
+```
31
+
32
+### 3. Push schema & run
33
+```bash
34
+npm install
35
+npm run db:push # Push Drizzle schema to Turso
36
+npm run dev # http://localhost:4322
37
+```
38
+
39
+### 4. Production
40
+```bash
41
+npm run build
42
+npm run start
43
+```
44
+
45
+## How it works
46
+1. **Submit URL** at `/instances/submit` — we probe `GET /api/instance` and verify `data.product === "opencodehub"`.
47
+2. **Aggregate** — paginated `GET /api/repos?visibility=public&sort=updated` (100/page), plus owner metadata for profiles. Only public data is fetched.
48
+3. **Display** — Explore shows trending repos, languages, profiles, instance cards. Each repo card shows `fullName`, `description`, `language`, `starCount`, `httpCloneUrl`, and the source instance badge.
49
+4. **Engage** — Logged-in Community users can **star** repos and **follow** profiles (rows in Turso); counts are community-local and do not mutate the remote instance.
50
+5. **Contribute** — "Clone via `httpCloneUrl`" requires your own OpenCodeHub node. Push to your instance and open a cross-instance PR (federation).
51
+
52
+## API
53
+- `POST /api/instances/submit` — `{url}` → probe + upsert instance
54
+- `GET /api/instances` — list instances
55
+- `POST /api/instances/sync?id=` — fetch public repos from instance, cache in Turso
56
+- `POST /api/stars` — toggle star on a cached repo
57
+- `POST /api/follows` — toggle follow on a profile
58
+
59
+## Pages
60
+- `/` — hero + how it works + CTA
61
+- `/explore` — trending repos, languages, profiles (searchable)
62
+- `/instances` — submitted nodes (InstanceCard grid)
63
+- `/instances/submit` — verify & submit flow
64
+- `/instances/[id]` — instance detail with live sync
65
+- `/u/[username]` — profile + contribution matrix
66
+- `/r/[instance]/[owner]/[repo]` — repo detail, clone URLs, contribute flow
67
+- `/auth/login` & `/auth/register` — Community auth
68
+
69
+## Deploy
70
+Any Node host + Turso + Upstash (optional). For Turso edge, swap adapter to `@astrojs/vercel` if needed.
0
71
▼community/astro.config.mjs
+14-0
1
+import node from "@astrojs/node";
2
+import react from "@astrojs/react";
3
+import tailwind from "@astrojs/tailwind";
4
+import { defineConfig } from "astro/config";
5
+
6
+export default defineConfig({
7
+ integrations: [react(), tailwind({ applyBaseStyles: false })],
8
+ output: "server",
9
+ adapter: node({ mode: "standalone" }),
10
+ server: { port: 4322, host: true },
11
+ vite: {
12
+ ssr: { noExternal: ["@radix-ui/*"] },
13
+ },
14
+});
0
15
▼community/bun.lock
+1604-0
1
+{
2
+ "lockfileVersion": 1,
3
+ "configVersion": 0,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "opencodehub-community",
7
+ "dependencies": {
8
+ "@astrojs/check": "^0.9.6",
9
+ "@astrojs/node": "^8.3.0",
10
+ "@astrojs/react": "^3.6.0",
11
+ "@astrojs/tailwind": "^5.1.2",
12
+ "@libsql/client": "^0.15.15",
13
+ "@radix-ui/react-avatar": "^1.1.0",
14
+ "@radix-ui/react-dialog": "^1.1.1",
15
+ "@radix-ui/react-dropdown-menu": "^2.1.1",
16
+ "@radix-ui/react-label": "^2.1.0",
17
+ "@radix-ui/react-slot": "^1.1.0",
18
+ "@radix-ui/react-tabs": "^1.1.0",
19
+ "@radix-ui/react-toast": "^1.2.1",
20
+ "@upstash/redis": "^1.36.0",
21
+ "astro": "^4.11.0",
22
+ "astro-icon": "^1.1.5",
23
+ "bcryptjs": "^2.4.3",
24
+ "class-variance-authority": "^0.7.0",
25
+ "clsx": "^2.1.1",
26
+ "dotenv": "^16.4.5",
27
+ "drizzle-orm": "^0.45.2",
28
+ "jose": "^5.6.3",
29
+ "lucide-react": "^0.400.0",
30
+ "nanoid": "^5.0.7",
31
+ "react": "^18.3.1",
32
+ "react-dom": "^18.3.1",
33
+ "sonner": "^2.0.7",
34
+ "tailwind-merge": "^2.6.0",
35
+ "tailwindcss-animate": "^1.0.7",
36
+ "zod": "^3.23.8",
37
+ },
38
+ "devDependencies": {
39
+ "@types/bcryptjs": "^2.4.6",
40
+ "@types/node": "^20.14.9",
41
+ "@types/react": "^18.3.3",
42
+ "@types/react-dom": "^18.3.0",
43
+ "autoprefixer": "^10.4.19",
44
+ "drizzle-kit": "^0.31.8",
45
+ "postcss": "^8.5.2",
46
+ "tailwindcss": "^3.4.4",
47
+ "typescript": "^5.9.3",
48
+ },
49
+ },
50
+ },
51
+ "packages": {
52
+ "@alloc/quick-lru": ["@alloc/[email protected]", "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
53
+
54
+ "@antfu/install-pkg": ["@antfu/[email protected]", "https://registry.npmmirror.com/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="],
55
+
56
+ "@astrojs/check": ["@astrojs/[email protected]", "https://registry.npmmirror.com/@astrojs/check/-/check-0.9.10.tgz", { "dependencies": { "@astrojs/language-server": "^2.16.7", "chokidar": "^4.0.3", "kleur": "^4.1.5", "yargs": "^18.0.0" }, "peerDependencies": { "typescript": "^5.0.0 || ^6.0.0" }, "bin": { "astro-check": "bin/astro-check.js" } }, "sha512-zgx/UQMozdjOa3bOxjgeCFdtpE3c9rRX6xHwa+2QXvy8z8Akifu2AtubHyv/zzC2znO8dl8fFWL4K+Ba9kS8HQ=="],
57
+
58
+ "@astrojs/compiler": ["@astrojs/[email protected]", "https://registry.npmmirror.com/@astrojs/compiler/-/compiler-2.13.1.tgz", {}, "sha512-f3FN83d2G/v32ipNClRKgYv30onQlMZX1vCeZMjPsMMPl1mDpmbl0+N5BYo4S/ofzqJyS5hvwacEo0CCVDn/Qg=="],
59
+
60
+ "@astrojs/internal-helpers": ["@astrojs/[email protected]", "https://registry.npmmirror.com/@astrojs/internal-helpers/-/internal-helpers-0.4.1.tgz", {}, "sha512-bMf9jFihO8YP940uD70SI/RDzIhUHJAolWVcO1v5PUivxGKvfLZTLTVVxEYzGYyPsA3ivdLNqMnL5VgmQySa+g=="],
61
+
62
+ "@astrojs/language-server": ["@astrojs/[email protected]", "https://registry.npmmirror.com/@astrojs/language-server/-/language-server-2.16.14.tgz", { "dependencies": { "@astrojs/compiler": "^2.13.1", "@astrojs/yaml2ts": "^0.2.4", "@jridgewell/sourcemap-codec": "^1.5.5", "@volar/kit": "~2.4.28", "@volar/language-core": "~2.4.28", "@volar/language-server": "~2.4.28", "@volar/language-service": "~2.4.28", "muggle-string": "^0.4.1", "tinyglobby": "^0.2.16", "volar-service-css": "0.0.71", "volar-service-emmet": "0.0.71", "volar-service-html": "0.0.71", "volar-service-prettier": "0.0.71", "volar-service-typescript": "0.0.71", "volar-service-typescript-twoslash-queries": "0.0.71", "volar-service-yaml": "0.0.71", "vscode-html-languageservice": "^5.6.2", "vscode-uri": "^3.1.0" }, "peerDependencies": { "prettier": "^3.0.0", "prettier-plugin-astro": ">=0.11.0" }, "optionalPeers": ["prettier-plugin-astro"], "bin": { "astro-ls": "bin/nodeServer.js" } }, "sha512-YPXkBu6N4d1sT09pvBmIDGZay+1MemV551FSgdEM3aZRDzbkxd2H7Cvf8MsVJLVB1mIEyTf1XbMN/30gp7s46w=="],
63
+
64
+ "@astrojs/markdown-remark": ["@astrojs/[email protected]", "https://registry.npmmirror.com/@astrojs/markdown-remark/-/markdown-remark-5.3.0.tgz", { "dependencies": { "@astrojs/prism": "3.1.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.1", "remark-smartypants": "^3.0.2", "shiki": "^1.22.0", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-r0Ikqr0e6ozPb5bvhup1qdWnSPUvQu6tub4ZLYaKyG50BXZ0ej6FhGz3GpChKpH7kglRFPObJd/bDyf2VM9pkg=="],
65
+
66
+ "@astrojs/node": ["@astrojs/[email protected]", "https://registry.npmmirror.com/@astrojs/node/-/node-8.3.4.tgz", { "dependencies": { "send": "^0.19.0", "server-destroy": "^1.0.1" }, "peerDependencies": { "astro": "^4.2.0" } }, "sha512-xzQs39goN7xh9np9rypGmbgZj3AmmjNxEMj9ZWz5aBERlqqFF3n8A/w/uaJeZ/bkHS60l1BXVS0tgsQt9MFqBA=="],
67
+
68
+ "@astrojs/prism": ["@astrojs/[email protected]", "https://registry.npmmirror.com/@astrojs/prism/-/prism-3.1.0.tgz", { "dependencies": { "prismjs": "^1.29.0" } }, "sha512-Z9IYjuXSArkAUx3N6xj6+Bnvx8OdUSHA8YoOgyepp3+zJmtVYJIl/I18GozdJVW1p5u/CNpl3Km7/gwTJK85cw=="],
69
+
70
+ "@astrojs/react": ["@astrojs/[email protected]", "https://registry.npmmirror.com/@astrojs/react/-/react-3.6.3.tgz", { "dependencies": { "@vitejs/plugin-react": "^4.3.3", "ultrahtml": "^1.5.3", "vite": "^5.4.10" }, "peerDependencies": { "@types/react": "^17.0.50 || ^18.0.21", "@types/react-dom": "^17.0.17 || ^18.0.6", "react": "^17.0.2 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0-beta" } }, "sha512-5ihLQDH5Runddug5AZYlnp/Q5T81QxhwnWJXA9rchBAdh11c6UhBbv9Kdk7b2PkXoEU70CGWBP9hSh0VCR58eA=="],
71
+
72
+ "@astrojs/tailwind": ["@astrojs/[email protected]", "https://registry.npmmirror.com/@astrojs/tailwind/-/tailwind-5.1.5.tgz", { "dependencies": { "autoprefixer": "^10.4.20", "postcss": "^8.5.1", "postcss-load-config": "^4.0.2" }, "peerDependencies": { "astro": "^3.0.0 || ^4.0.0 || ^5.0.0", "tailwindcss": "^3.0.24" } }, "sha512-1diguZEau7FZ9vIjzE4BwavGdhD3+JkdS8zmibl1ene+EHgIU5hI0NMgRYG3yea+Niaf7cyMwjeWeLvzq/maxg=="],
73
+
74
+ "@astrojs/telemetry": ["@astrojs/[email protected]", "https://registry.npmmirror.com/@astrojs/telemetry/-/telemetry-3.1.0.tgz", { "dependencies": { "ci-info": "^4.0.0", "debug": "^4.3.4", "dlv": "^1.1.3", "dset": "^3.1.3", "is-docker": "^3.0.0", "is-wsl": "^3.0.0", "which-pm-runs": "^1.1.0" } }, "sha512-/ca/+D8MIKEC8/A9cSaPUqQNZm+Es/ZinRv0ZAzvu2ios7POQSsVD+VOj7/hypWNsNM3T7RpfgNq7H2TU1KEHA=="],
75
+
76
+ "@astrojs/yaml2ts": ["@astrojs/[email protected]", "https://registry.npmmirror.com/@astrojs/yaml2ts/-/yaml2ts-0.2.4.tgz", { "dependencies": { "yaml": "^2.8.3" } }, "sha512-8oddpOae35pJsXPQXhTkM0ypfKPskVsh2bCxRtbf7e+/Epw2nReakFYpLKjZMEr75CsoF203PMnCocpfz0s69A=="],
77
+
78
+ "@babel/code-frame": ["@babel/[email protected]", "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.7.tgz", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
79
+
80
+ "@babel/compat-data": ["@babel/[email protected]", "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.7.tgz", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
81
+
82
+ "@babel/core": ["@babel/[email protected]", "https://registry.npmmirror.com/@babel/core/-/core-7.29.7.tgz", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="],
83
+
84
+ "@babel/generator": ["@babel/[email protected]", "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.8.tgz", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="],
85
+
86
+ "@babel/helper-annotate-as-pure": ["@babel/[email protected]", "https://registry.npmmirror.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="],
87
+
88
+ "@babel/helper-compilation-targets": ["@babel/[email protected]", "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="],
89
+
90
+ "@babel/helper-globals": ["@babel/[email protected]", "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
91
+
92
+ "@babel/helper-module-imports": ["@babel/[email protected]", "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
93
+
94
+ "@babel/helper-module-transforms": ["@babel/[email protected]", "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="],
95
+
96
+ "@babel/helper-plugin-utils": ["@babel/[email protected]", "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="],
97
+
98
+ "@babel/helper-string-parser": ["@babel/[email protected]", "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
99
+
100
+ "@babel/helper-validator-identifier": ["@babel/[email protected]", "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
101
+
102
+ "@babel/helper-validator-option": ["@babel/[email protected]", "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
103
+
104
+ "@babel/helpers": ["@babel/[email protected]", "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.7.tgz", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="],
105
+
106
+ "@babel/parser": ["@babel/[email protected]", "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.8.tgz", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
107
+
108
+ "@babel/plugin-syntax-jsx": ["@babel/[email protected]", "https://registry.npmmirror.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="],
109
+
110
+ "@babel/plugin-transform-react-jsx": ["@babel/[email protected]", "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.29.7.tgz", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-module-imports": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-syntax-jsx": "^7.29.7", "@babel/types": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A=="],
111
+
112
+ "@babel/plugin-transform-react-jsx-self": ["@babel/[email protected]", "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw=="],
113
+
114
+ "@babel/plugin-transform-react-jsx-source": ["@babel/[email protected]", "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="],
115
+
116
+ "@babel/template": ["@babel/[email protected]", "https://registry.npmmirror.com/@babel/template/-/template-7.29.7.tgz", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
117
+
118
+ "@babel/traverse": ["@babel/[email protected]", "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.8.tgz", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="],
119
+
120
+ "@babel/types": ["@babel/[email protected]", "https://registry.npmmirror.com/@babel/types/-/types-7.29.8.tgz", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
121
+
122
+ "@cyberalien/svg-utils": ["@cyberalien/[email protected]", "https://registry.npmmirror.com/@cyberalien/svg-utils/-/svg-utils-1.2.19.tgz", { "dependencies": { "@iconify/types": "^2.0.0" } }, "sha512-paDJoDu+LhuH5Ma1q0BDqBpg6d16Xk22cZZrlg/s/J9oB8KgnnpU/snaEETJu2G1pwYcWQINUz6WUqu0Wm9k6A=="],
123
+
124
+ "@drizzle-team/brocli": ["@drizzle-team/[email protected]", "https://registry.npmmirror.com/@drizzle-team/brocli/-/brocli-0.10.2.tgz", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
125
+
126
+ "@emmetio/abbreviation": ["@emmetio/[email protected]", "https://registry.npmmirror.com/@emmetio/abbreviation/-/abbreviation-2.3.3.tgz", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA=="],
127
+
128
+ "@emmetio/css-abbreviation": ["@emmetio/[email protected]", "https://registry.npmmirror.com/@emmetio/css-abbreviation/-/css-abbreviation-2.1.8.tgz", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw=="],
129
+
130
+ "@emmetio/css-parser": ["@emmetio/[email protected]", "https://registry.npmmirror.com/@emmetio/css-parser/-/css-parser-0.4.1.tgz", { "dependencies": { "@emmetio/stream-reader": "^2.2.0", "@emmetio/stream-reader-utils": "^0.1.0" } }, "sha512-2bC6m0MV/voF4CTZiAbG5MWKbq5EBmDPKu9Sb7s7nVcEzNQlrZP6mFFFlIaISM8X6514H9shWMme1fCm8cWAfQ=="],
131
+
132
+ "@emmetio/html-matcher": ["@emmetio/[email protected]", "https://registry.npmmirror.com/@emmetio/html-matcher/-/html-matcher-1.3.0.tgz", { "dependencies": { "@emmetio/scanner": "^1.0.0" } }, "sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ=="],
133
+
134
+ "@emmetio/scanner": ["@emmetio/[email protected]", "https://registry.npmmirror.com/@emmetio/scanner/-/scanner-1.0.4.tgz", {}, "sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA=="],
135
+
136
+ "@emmetio/stream-reader": ["@emmetio/[email protected]", "https://registry.npmmirror.com/@emmetio/stream-reader/-/stream-reader-2.2.0.tgz", {}, "sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw=="],
137
+
138
+ "@emmetio/stream-reader-utils": ["@emmetio/[email protected]", "https://registry.npmmirror.com/@emmetio/stream-reader-utils/-/stream-reader-utils-0.1.0.tgz", {}, "sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A=="],
139
+
140
+ "@emnapi/runtime": ["@emnapi/[email protected]", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.11.3.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
141
+
142
+ "@esbuild-kit/core-utils": ["@esbuild-kit/[email protected]", "https://registry.npmmirror.com/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="],
143
+
144
+ "@esbuild-kit/esm-loader": ["@esbuild-kit/[email protected]", "https://registry.npmmirror.com/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="],
145
+
146
+ "@esbuild/aix-ppc64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
147
+
148
+ "@esbuild/android-arm": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
149
+
150
+ "@esbuild/android-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
151
+
152
+ "@esbuild/android-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
153
+
154
+ "@esbuild/darwin-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
155
+
156
+ "@esbuild/darwin-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
157
+
158
+ "@esbuild/freebsd-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
159
+
160
+ "@esbuild/freebsd-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
161
+
162
+ "@esbuild/linux-arm": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
163
+
164
+ "@esbuild/linux-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
165
+
166
+ "@esbuild/linux-ia32": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
167
+
168
+ "@esbuild/linux-loong64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
169
+
170
+ "@esbuild/linux-mips64el": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
171
+
172
+ "@esbuild/linux-ppc64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
173
+
174
+ "@esbuild/linux-riscv64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
175
+
176
+ "@esbuild/linux-s390x": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
177
+
178
+ "@esbuild/linux-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
179
+
180
+ "@esbuild/netbsd-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
181
+
182
+ "@esbuild/netbsd-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
183
+
184
+ "@esbuild/openbsd-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
185
+
186
+ "@esbuild/openbsd-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
187
+
188
+ "@esbuild/openharmony-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
189
+
190
+ "@esbuild/sunos-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
191
+
192
+ "@esbuild/win32-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
193
+
194
+ "@esbuild/win32-ia32": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
195
+
196
+ "@esbuild/win32-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
197
+
198
+ "@floating-ui/core": ["@floating-ui/[email protected]", "https://registry.npmmirror.com/@floating-ui/core/-/core-1.8.0.tgz", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="],
199
+
200
+ "@floating-ui/dom": ["@floating-ui/[email protected]", "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.8.0.tgz", { "dependencies": { "@floating-ui/core": "^1.8.0", "@floating-ui/utils": "^0.2.12" } }, "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg=="],
201
+
202
+ "@floating-ui/react-dom": ["@floating-ui/[email protected]", "https://registry.npmmirror.com/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", { "dependencies": { "@floating-ui/dom": "^1.8.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg=="],
203
+
204
+ "@floating-ui/utils": ["@floating-ui/[email protected]", "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.12.tgz", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="],
205
+
206
+ "@iconify/tools": ["@iconify/[email protected]", "https://registry.npmmirror.com/@iconify/tools/-/tools-5.0.12.tgz", { "dependencies": { "@cyberalien/svg-utils": "^1.2.15", "@iconify/types": "^2.0.0", "@iconify/utils": "^3.1.3", "fflate": "^0.8.3", "modern-tar": "^0.7.6", "pathe": "^2.0.3", "svgo": "^4.0.1" } }, "sha512-aFPwSFmFphUPVjNLUkgxgUPSPVgTEEjv0mE7NgvfoQBuE5TtsMrKa4HTY65cM5oXZ2a1xKQcW/RKhiOKEiAarw=="],
207
+
208
+ "@iconify/types": ["@iconify/[email protected]", "https://registry.npmmirror.com/@iconify/types/-/types-2.0.0.tgz", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
209
+
210
+ "@iconify/utils": ["@iconify/[email protected]", "https://registry.npmmirror.com/@iconify/utils/-/utils-3.1.4.tgz", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "import-meta-resolve": "^4.2.0" } }, "sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw=="],
211
+
212
+ "@img/sharp-darwin-arm64": ["@img/[email protected]", "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
213
+
214
+ "@img/sharp-darwin-x64": ["@img/[email protected]", "https://registry.npmmirror.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="],
215
+
216
+ "@img/sharp-libvips-darwin-arm64": ["@img/[email protected]", "https://registry.npmmirror.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="],
217
+
218
+ "@img/sharp-libvips-darwin-x64": ["@img/[email protected]", "https://registry.npmmirror.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="],
219
+
220
+ "@img/sharp-libvips-linux-arm": ["@img/[email protected]", "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="],
221
+
222
+ "@img/sharp-libvips-linux-arm64": ["@img/[email protected]", "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="],
223
+
224
+ "@img/sharp-libvips-linux-s390x": ["@img/[email protected]", "https://registry.npmmirror.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA=="],
225
+
226
+ "@img/sharp-libvips-linux-x64": ["@img/[email protected]", "https://registry.npmmirror.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="],
227
+
228
+ "@img/sharp-libvips-linuxmusl-arm64": ["@img/[email protected]", "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="],
229
+
230
+ "@img/sharp-libvips-linuxmusl-x64": ["@img/[email protected]", "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="],
231
+
232
+ "@img/sharp-linux-arm": ["@img/[email protected]", "https://registry.npmmirror.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="],
233
+
234
+ "@img/sharp-linux-arm64": ["@img/[email protected]", "https://registry.npmmirror.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="],
235
+
236
+ "@img/sharp-linux-s390x": ["@img/[email protected]", "https://registry.npmmirror.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.0.4" }, "os": "linux", "cpu": "s390x" }, "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q=="],
237
+
238
+ "@img/sharp-linux-x64": ["@img/[email protected]", "https://registry.npmmirror.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="],
239
+
240
+ "@img/sharp-linuxmusl-arm64": ["@img/[email protected]", "https://registry.npmmirror.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="],
241
+
242
+ "@img/sharp-linuxmusl-x64": ["@img/[email protected]", "https://registry.npmmirror.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="],
243
+
244
+ "@img/sharp-wasm32": ["@img/[email protected]", "https://registry.npmmirror.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", { "dependencies": { "@emnapi/runtime": "^1.2.0" }, "cpu": "none" }, "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg=="],
245
+
246
+ "@img/sharp-win32-ia32": ["@img/[email protected]", "https://registry.npmmirror.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ=="],
247
+
248
+ "@img/sharp-win32-x64": ["@img/[email protected]", "https://registry.npmmirror.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
249
+
250
+ "@jridgewell/gen-mapping": ["@jridgewell/[email protected]", "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
251
+
252
+ "@jridgewell/remapping": ["@jridgewell/[email protected]", "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
253
+
254
+ "@jridgewell/resolve-uri": ["@jridgewell/[email protected]", "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
255
+
256
+ "@jridgewell/sourcemap-codec": ["@jridgewell/[email protected]", "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
257
+
258
+ "@jridgewell/trace-mapping": ["@jridgewell/[email protected]", "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
259
+
260
+ "@libsql/client": ["@libsql/[email protected]", "https://registry.npmmirror.com/@libsql/client/-/client-0.15.15.tgz", { "dependencies": { "@libsql/core": "^0.15.14", "@libsql/hrana-client": "^0.7.0", "js-base64": "^3.7.5", "libsql": "^0.5.22", "promise-limit": "^2.7.0" } }, "sha512-twC0hQxPNHPKfeOv3sNT6u2pturQjLcI+CnpTM0SjRpocEGgfiZ7DWKXLNnsothjyJmDqEsBQJ5ztq9Wlu470w=="],
261
+
262
+ "@libsql/core": ["@libsql/[email protected]", "https://registry.npmmirror.com/@libsql/core/-/core-0.15.15.tgz", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-C88Z6UKl+OyuKKPwz224riz02ih/zHYI3Ho/LAcVOgjsunIRZoBw7fjRfaH9oPMmSNeQfhGklSG2il1URoOIsA=="],
263
+
264
+ "@libsql/darwin-arm64": ["@libsql/[email protected]", "https://registry.npmmirror.com/@libsql/darwin-arm64/-/darwin-arm64-0.5.29.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-K+2RIB1OGFPYQbfay48GakLhqf3ArcbHqPFu7EZiaUcRgFcdw8RoltsMyvbj5ix2fY0HV3Q3Ioa/ByvQdaSM0A=="],
265
+
266
+ "@libsql/darwin-x64": ["@libsql/[email protected]", "https://registry.npmmirror.com/@libsql/darwin-x64/-/darwin-x64-0.5.29.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-OtT+KFHsKFy1R5FVadr8FJ2Bb1mghtXTyJkxv0trocq7NuHntSki1eUbxpO5ezJesDvBlqFjnWaYYY516QNLhQ=="],
267
+
268
+ "@libsql/hrana-client": ["@libsql/[email protected]", "https://registry.npmmirror.com/@libsql/hrana-client/-/hrana-client-0.7.0.tgz", { "dependencies": { "@libsql/isomorphic-fetch": "^0.3.1", "@libsql/isomorphic-ws": "^0.1.5", "js-base64": "^3.7.5", "node-fetch": "^3.3.2" } }, "sha512-OF8fFQSkbL7vJY9rfuegK1R7sPgQ6kFMkDamiEccNUvieQ+3urzfDFI616oPl8V7T9zRmnTkSjMOImYCAVRVuw=="],
269
+
270
+ "@libsql/isomorphic-fetch": ["@libsql/[email protected]", "https://registry.npmmirror.com/@libsql/isomorphic-fetch/-/isomorphic-fetch-0.3.1.tgz", {}, "sha512-6kK3SUK5Uu56zPq/Las620n5aS9xJq+jMBcNSOmjhNf/MUvdyji4vrMTqD7ptY7/4/CAVEAYDeotUz60LNQHtw=="],
271
+
272
+ "@libsql/isomorphic-ws": ["@libsql/[email protected]", "https://registry.npmmirror.com/@libsql/isomorphic-ws/-/isomorphic-ws-0.1.5.tgz", { "dependencies": { "@types/ws": "^8.5.4", "ws": "^8.13.0" } }, "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg=="],
273
+
274
+ "@libsql/linux-arm-gnueabihf": ["@libsql/[email protected]", "https://registry.npmmirror.com/@libsql/linux-arm-gnueabihf/-/linux-arm-gnueabihf-0.5.29.tgz", { "os": "linux", "cpu": "arm" }, "sha512-CD4n4zj7SJTHso4nf5cuMoWoMSS7asn5hHygsDuhRl8jjjCTT3yE+xdUvI4J7zsyb53VO5ISh4cwwOtf6k2UhQ=="],
275
+
276
+ "@libsql/linux-arm-musleabihf": ["@libsql/[email protected]", "https://registry.npmmirror.com/@libsql/linux-arm-musleabihf/-/linux-arm-musleabihf-0.5.29.tgz", { "os": "linux", "cpu": "arm" }, "sha512-2Z9qBVpEJV7OeflzIR3+l5yAd4uTOLxklScYTwpZnkm2vDSGlC1PRlueLaufc4EFITkLKXK2MWBpexuNJfMVcg=="],
277
+
278
+ "@libsql/linux-arm64-gnu": ["@libsql/[email protected]", "https://registry.npmmirror.com/@libsql/linux-arm64-gnu/-/linux-arm64-gnu-0.5.29.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-gURBqaiXIGGwFNEaUj8Ldk7Hps4STtG+31aEidCk5evMMdtsdfL3HPCpvys+ZF/tkOs2MWlRWoSq7SOuCE9k3w=="],
279
+
280
+ "@libsql/linux-arm64-musl": ["@libsql/[email protected]", "https://registry.npmmirror.com/@libsql/linux-arm64-musl/-/linux-arm64-musl-0.5.29.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-fwgYZ0H8mUkyVqXZHF3mT/92iIh1N94Owi/f66cPVNsk9BdGKq5gVpoKO+7UxaNzuEH1roJp2QEwsCZMvBLpqg=="],
281
+
282
+ "@libsql/linux-x64-gnu": ["@libsql/[email protected]", "https://registry.npmmirror.com/@libsql/linux-x64-gnu/-/linux-x64-gnu-0.5.29.tgz", { "os": "linux", "cpu": "x64" }, "sha512-y14V0vY0nmMC6G0pHeJcEarcnGU2H6cm21ZceRkacWHvQAEhAG0latQkCtoS2njFOXiYIg+JYPfAoWKbi82rkg=="],
283
+
284
+ "@libsql/linux-x64-musl": ["@libsql/[email protected]", "https://registry.npmmirror.com/@libsql/linux-x64-musl/-/linux-x64-musl-0.5.29.tgz", { "os": "linux", "cpu": "x64" }, "sha512-gquqwA/39tH4pFl+J9n3SOMSymjX+6kZ3kWgY3b94nXFTwac9bnFNMffIomgvlFaC4ArVqMnOZD3nuJ3H3VO1w=="],
285
+
286
+ "@libsql/win32-x64-msvc": ["@libsql/[email protected]", "https://registry.npmmirror.com/@libsql/win32-x64-msvc/-/win32-x64-msvc-0.5.29.tgz", { "os": "win32", "cpu": "x64" }, "sha512-4/0CvEdhi6+KjMxMaVbFM2n2Z44escBRoEYpR+gZg64DdetzGnYm8mcNLcoySaDJZNaBd6wz5DNdgRmcI4hXcg=="],
287
+
288
+ "@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/[email protected]", "https://registry.npmmirror.com/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="],
289
+
290
+ "@neon-rs/load": ["@neon-rs/[email protected]", "https://registry.npmmirror.com/@neon-rs/load/-/load-0.0.4.tgz", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="],
291
+
292
+ "@nodelib/fs.scandir": ["@nodelib/[email protected]", "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
293
+
294
+ "@nodelib/fs.stat": ["@nodelib/[email protected]", "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
295
+
296
+ "@nodelib/fs.walk": ["@nodelib/[email protected]", "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
297
+
298
+ "@oslojs/encoding": ["@oslojs/[email protected]", "https://registry.npmmirror.com/@oslojs/encoding/-/encoding-1.1.0.tgz", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="],
299
+
300
+ "@radix-ui/primitive": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.7.tgz", {}, "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q=="],
301
+
302
+ "@radix-ui/react-arrow": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-arrow/-/react-arrow-1.1.15.tgz", { "dependencies": { "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA=="],
303
+
304
+ "@radix-ui/react-avatar": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-avatar/-/react-avatar-1.2.6.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-is-hydrated": "0.1.3", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-4ULOTJ/mqy2hT9GlWa/MFHxHSvH3nJzHnZM1waNsc5Bonv7i70aNenghXmD97S6OJ81ekXONGGt4nT1r0PfEdA=="],
305
+
306
+ "@radix-ui/react-collection": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-collection/-/react-collection-1.1.15.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA=="],
307
+
308
+ "@radix-ui/react-compose-refs": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA=="],
309
+
310
+ "@radix-ui/react-context": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.2.2.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA=="],
311
+
312
+ "@radix-ui/react-dialog": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-dialog/-/react-dialog-1.1.23.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-dismissable-layer": "1.1.19", "@radix-ui/react-focus-guards": "1.1.6", "@radix-ui/react-focus-scope": "1.1.16", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-portal": "1.1.17", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-slot": "1.3.3", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-layout-effect": "1.1.4", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA=="],
313
+
314
+ "@radix-ui/react-direction": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-direction/-/react-direction-1.1.4.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg=="],
315
+
316
+ "@radix-ui/react-dismissable-layer": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.19.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-effect-event": "0.0.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w=="],
317
+
318
+ "@radix-ui/react-dropdown-menu": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.24.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-menu": "2.1.24", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-geq8l2rJkxvkXsT9RMgtUE3P8pITFpTsvYpbySi1IH4fZEABD/Gp85myayFgxk0ktljGMJnCbeFkyTusvSvv7g=="],
319
+
320
+ "@radix-ui/react-focus-guards": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.6.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ=="],
321
+
322
+ "@radix-ui/react-focus-scope": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.16.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ=="],
323
+
324
+ "@radix-ui/react-id": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-id/-/react-id-1.1.4.tgz", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA=="],
325
+
326
+ "@radix-ui/react-label": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-label/-/react-label-2.1.15.tgz", { "dependencies": { "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-o/rdYEwZTTo5tjknnPeyQFU45kUC4i/XyeDPP+HGyi6XqpOP6Zf5Ya5vh/Yfe9Id5JiuWnnAx2XqIeD3UYZt0g=="],
327
+
328
+ "@radix-ui/react-menu": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-menu/-/react-menu-2.1.24.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-collection": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-dismissable-layer": "1.1.19", "@radix-ui/react-focus-guards": "1.1.6", "@radix-ui/react-focus-scope": "1.1.16", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-popper": "1.3.7", "@radix-ui/react-portal": "1.1.17", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-roving-focus": "1.1.19", "@radix-ui/react-slot": "1.3.3", "@radix-ui/react-use-callback-ref": "1.1.4", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-uW7RVuU6Lp/ZtfeY4b3kL32zccgEWvPv1+cf17ubYzHa9cL8AHokmk36cG/XEiH/smbQvumnieXX9j/e9RqJWA=="],
329
+
330
+ "@radix-ui/react-popper": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-1.3.7.tgz", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-layout-effect": "1.1.4", "@radix-ui/react-use-rect": "1.1.4", "@radix-ui/react-use-size": "1.1.4", "@radix-ui/rect": "1.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg=="],
331
+
332
+ "@radix-ui/react-portal": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-portal/-/react-portal-1.1.17.tgz", { "dependencies": { "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ=="],
333
+
334
+ "@radix-ui/react-presence": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-presence/-/react-presence-1.1.10.tgz", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw=="],
335
+
336
+ "@radix-ui/react-primitive": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", { "dependencies": { "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg=="],
337
+
338
+ "@radix-ui/react-roving-focus": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.19.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-collection": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-is-hydrated": "0.1.3", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ=="],
339
+
340
+ "@radix-ui/react-slot": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q=="],
341
+
342
+ "@radix-ui/react-tabs": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-tabs/-/react-tabs-1.1.21.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-roving-focus": "1.1.19", "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog=="],
343
+
344
+ "@radix-ui/react-toast": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-toast/-/react-toast-1.2.23.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-collection": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-dismissable-layer": "1.1.19", "@radix-ui/react-portal": "1.1.17", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-layout-effect": "1.1.4", "@radix-ui/react-visually-hidden": "1.2.11" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-ofhyAsYaocRGOs/n0XWdUOSVzEAG6BfrMVM8z0c0kLEWY38w/0WuMFPTJP/HVaZPYkMvHZoKIIhNcjbTCBILPg=="],
345
+
346
+ "@radix-ui/react-use-callback-ref": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ=="],
347
+
348
+ "@radix-ui/react-use-controllable-state": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.6.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-use-effect-event": "0.0.5", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ=="],
349
+
350
+ "@radix-ui/react-use-effect-event": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg=="],
351
+
352
+ "@radix-ui/react-use-is-hydrated": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw=="],
353
+
354
+ "@radix-ui/react-use-layout-effect": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw=="],
355
+
356
+ "@radix-ui/react-use-rect": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.4.tgz", { "dependencies": { "@radix-ui/rect": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ=="],
357
+
358
+ "@radix-ui/react-use-size": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-use-size/-/react-use-size-1.1.4.tgz", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw=="],
359
+
360
+ "@radix-ui/react-visually-hidden": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.11.tgz", { "dependencies": { "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-NFS86RYYZb4/exihaESBGOpMJFz8MGLAfu3mOBSGByVnVPC9JPASfYubxd/8KbkQK0sYAv8lVQDEQukDX/qXvQ=="],
361
+
362
+ "@radix-ui/rect": ["@radix-ui/[email protected]", "https://registry.npmmirror.com/@radix-ui/rect/-/rect-1.1.3.tgz", {}, "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw=="],
363
+
364
+ "@rolldown/pluginutils": ["@rolldown/[email protected]", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
365
+
366
+ "@rollup/pluginutils": ["@rollup/[email protected]", "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" } }, "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg=="],
367
+
368
+ "@rollup/rollup-android-arm-eabi": ["@rollup/[email protected]", "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", { "os": "android", "cpu": "arm" }, "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg=="],
369
+
370
+ "@rollup/rollup-android-arm64": ["@rollup/[email protected]", "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", { "os": "android", "cpu": "arm64" }, "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A=="],
371
+
372
+ "@rollup/rollup-darwin-arm64": ["@rollup/[email protected]", "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw=="],
373
+
374
+ "@rollup/rollup-darwin-x64": ["@rollup/[email protected]", "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A=="],
375
+
376
+ "@rollup/rollup-freebsd-arm64": ["@rollup/[email protected]", "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ=="],
377
+
378
+ "@rollup/rollup-freebsd-x64": ["@rollup/[email protected]", "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg=="],
379
+
380
+ "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/[email protected]", "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", { "os": "linux", "cpu": "arm" }, "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA=="],
381
+
382
+ "@rollup/rollup-linux-arm-musleabihf": ["@rollup/[email protected]", "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", { "os": "linux", "cpu": "arm" }, "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ=="],
383
+
384
+ "@rollup/rollup-linux-arm64-gnu": ["@rollup/[email protected]", "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ=="],
385
+
386
+ "@rollup/rollup-linux-arm64-musl": ["@rollup/[email protected]", "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g=="],
387
+
388
+ "@rollup/rollup-linux-loong64-gnu": ["@rollup/[email protected]", "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", { "os": "linux", "cpu": "none" }, "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA=="],
389
+
390
+ "@rollup/rollup-linux-loong64-musl": ["@rollup/[email protected]", "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", { "os": "linux", "cpu": "none" }, "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ=="],
391
+
392
+ "@rollup/rollup-linux-ppc64-gnu": ["@rollup/[email protected]", "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w=="],
393
+
394
+ "@rollup/rollup-linux-ppc64-musl": ["@rollup/[email protected]", "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w=="],
395
+
396
+ "@rollup/rollup-linux-riscv64-gnu": ["@rollup/[email protected]", "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", { "os": "linux", "cpu": "none" }, "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ=="],
397
+
398
+ "@rollup/rollup-linux-riscv64-musl": ["@rollup/[email protected]", "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", { "os": "linux", "cpu": "none" }, "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA=="],
399
+
400
+ "@rollup/rollup-linux-s390x-gnu": ["@rollup/[email protected]", "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA=="],
401
+
402
+ "@rollup/rollup-linux-x64-gnu": ["@rollup/[email protected]", "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw=="],
403
+
404
+ "@rollup/rollup-linux-x64-musl": ["@rollup/[email protected]", "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA=="],
405
+
406
+ "@rollup/rollup-openbsd-x64": ["@rollup/[email protected]", "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ=="],
407
+
408
+ "@rollup/rollup-openharmony-arm64": ["@rollup/[email protected]", "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", { "os": "none", "cpu": "arm64" }, "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ=="],
409
+
410
+ "@rollup/rollup-win32-arm64-msvc": ["@rollup/[email protected]", "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw=="],
411
+
412
+ "@rollup/rollup-win32-ia32-msvc": ["@rollup/[email protected]", "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ=="],
413
+
414
+ "@rollup/rollup-win32-x64-gnu": ["@rollup/[email protected]", "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", { "os": "win32", "cpu": "x64" }, "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw=="],
415
+
416
+ "@rollup/rollup-win32-x64-msvc": ["@rollup/[email protected]", "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", { "os": "win32", "cpu": "x64" }, "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q=="],
417
+
418
+ "@shikijs/core": ["@shikijs/[email protected]", "https://registry.npmmirror.com/@shikijs/core/-/core-1.29.2.tgz", { "dependencies": { "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.4" } }, "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ=="],
419
+
420
+ "@shikijs/engine-javascript": ["@shikijs/[email protected]", "https://registry.npmmirror.com/@shikijs/engine-javascript/-/engine-javascript-1.29.2.tgz", { "dependencies": { "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "oniguruma-to-es": "^2.2.0" } }, "sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A=="],
421
+
422
+ "@shikijs/engine-oniguruma": ["@shikijs/[email protected]", "https://registry.npmmirror.com/@shikijs/engine-oniguruma/-/engine-oniguruma-1.29.2.tgz", { "dependencies": { "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1" } }, "sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA=="],
423
+
424
+ "@shikijs/langs": ["@shikijs/[email protected]", "https://registry.npmmirror.com/@shikijs/langs/-/langs-1.29.2.tgz", { "dependencies": { "@shikijs/types": "1.29.2" } }, "sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ=="],
425
+
426
+ "@shikijs/themes": ["@shikijs/[email protected]", "https://registry.npmmirror.com/@shikijs/themes/-/themes-1.29.2.tgz", { "dependencies": { "@shikijs/types": "1.29.2" } }, "sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g=="],
427
+
428
+ "@shikijs/types": ["@shikijs/[email protected]", "https://registry.npmmirror.com/@shikijs/types/-/types-1.29.2.tgz", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="],
429
+
430
+ "@shikijs/vscode-textmate": ["@shikijs/[email protected]", "https://registry.npmmirror.com/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
431
+
432
+ "@types/babel__core": ["@types/[email protected]", "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
433
+
434
+ "@types/babel__generator": ["@types/[email protected]", "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.27.0.tgz", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
435
+
436
+ "@types/babel__template": ["@types/[email protected]", "https://registry.npmmirror.com/@types/babel__template/-/babel__template-7.4.4.tgz", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
437
+
438
+ "@types/babel__traverse": ["@types/[email protected]", "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
439
+
440
+ "@types/bcryptjs": ["@types/[email protected]", "https://registry.npmmirror.com/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", {}, "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ=="],
441
+
442
+ "@types/cookie": ["@types/[email protected]", "https://registry.npmmirror.com/@types/cookie/-/cookie-0.6.0.tgz", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
443
+
444
+ "@types/debug": ["@types/[email protected]", "https://registry.npmmirror.com/@types/debug/-/debug-4.1.13.tgz", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
445
+
446
+ "@types/estree": ["@types/[email protected]", "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
447
+
448
+ "@types/hast": ["@types/[email protected]", "https://registry.npmmirror.com/@types/hast/-/hast-3.0.5.tgz", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="],
449
+
450
+ "@types/mdast": ["@types/[email protected]", "https://registry.npmmirror.com/@types/mdast/-/mdast-4.0.4.tgz", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
451
+
452
+ "@types/ms": ["@types/[email protected]", "https://registry.npmmirror.com/@types/ms/-/ms-2.1.0.tgz", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
453
+
454
+ "@types/nlcst": ["@types/[email protected]", "https://registry.npmmirror.com/@types/nlcst/-/nlcst-2.0.3.tgz", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="],
455
+
456
+ "@types/node": ["@types/[email protected]", "https://registry.npmmirror.com/@types/node/-/node-20.19.43.tgz", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="],
457
+
458
+ "@types/prop-types": ["@types/[email protected]", "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="],
459
+
460
+ "@types/react": ["@types/[email protected]", "https://registry.npmmirror.com/@types/react/-/react-18.3.31.tgz", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw=="],
461
+
462
+ "@types/react-dom": ["@types/[email protected]", "https://registry.npmmirror.com/@types/react-dom/-/react-dom-18.3.7.tgz", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="],
463
+
464
+ "@types/unist": ["@types/[email protected]", "https://registry.npmmirror.com/@types/unist/-/unist-3.0.3.tgz", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
465
+
466
+ "@types/ws": ["@types/[email protected]", "https://registry.npmmirror.com/@types/ws/-/ws-8.18.1.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
467
+
468
+ "@ungap/structured-clone": ["@ungap/[email protected]", "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", {}, "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg=="],
469
+
470
+ "@upstash/redis": ["@upstash/[email protected]", "https://registry.npmmirror.com/@upstash/redis/-/redis-1.38.2.tgz", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-RZ+JaRCVIS+ZTGnjOnDMr+/0aqDxsBb5rV6aW+Pys4SGELwr5lwE3UbLHRCHqMQ5Ulxj0b/CFBHfbmE175Otog=="],
471
+
472
+ "@vitejs/plugin-react": ["@vitejs/[email protected]", "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
473
+
474
+ "@volar/kit": ["@volar/[email protected]", "https://registry.npmmirror.com/@volar/kit/-/kit-2.4.28.tgz", { "dependencies": { "@volar/language-service": "2.4.28", "@volar/typescript": "2.4.28", "typesafe-path": "^0.2.2", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "typescript": "*" } }, "sha512-cKX4vK9dtZvDRaAzeoUdaAJEew6IdxHNCRrdp5Kvcl6zZOqb6jTOfk3kXkIkG3T7oTFXguEMt5+9ptyqYR84Pg=="],
475
+
476
+ "@volar/language-core": ["@volar/[email protected]", "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.28.tgz", { "dependencies": { "@volar/source-map": "2.4.28" } }, "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ=="],
477
+
478
+ "@volar/language-server": ["@volar/[email protected]", "https://registry.npmmirror.com/@volar/language-server/-/language-server-2.4.28.tgz", { "dependencies": { "@volar/language-core": "2.4.28", "@volar/language-service": "2.4.28", "@volar/typescript": "2.4.28", "path-browserify": "^1.0.1", "request-light": "^0.7.0", "vscode-languageserver": "^9.0.1", "vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" } }, "sha512-NqcLnE5gERKuS4PUFwlhMxf6vqYo7hXtbMFbViXcbVkbZ905AIVWhnSo0ZNBC2V127H1/2zP7RvVOVnyITFfBw=="],
479
+
480
+ "@volar/language-service": ["@volar/[email protected]", "https://registry.npmmirror.com/@volar/language-service/-/language-service-2.4.28.tgz", { "dependencies": { "@volar/language-core": "2.4.28", "vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" } }, "sha512-Rh/wYCZJrI5vCwMk9xyw/Z+MsWxlJY1rmMZPsxUoJKfzIRjS/NF1NmnuEcrMbEVGja00aVpCsInJfixQTMdvLw=="],
481
+
482
+ "@volar/source-map": ["@volar/[email protected]", "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.28.tgz", {}, "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ=="],
483
+
484
+ "@volar/typescript": ["@volar/[email protected]", "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.28.tgz", { "dependencies": { "@volar/language-core": "2.4.28", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw=="],
485
+
486
+ "@vscode/emmet-helper": ["@vscode/[email protected]", "https://registry.npmmirror.com/@vscode/emmet-helper/-/emmet-helper-2.11.0.tgz", { "dependencies": { "emmet": "^2.4.3", "jsonc-parser": "^2.3.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.15.1", "vscode-uri": "^3.0.8" } }, "sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw=="],
487
+
488
+ "@vscode/l10n": ["@vscode/[email protected]", "https://registry.npmmirror.com/@vscode/l10n/-/l10n-0.0.18.tgz", {}, "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ=="],
489
+
490
+ "acorn": ["[email protected]", "https://registry.npmmirror.com/acorn/-/acorn-8.18.0.tgz", { "bin": "bin/acorn" }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
491
+
492
+ "ajv": ["[email protected]", "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
493
+
494
+ "ajv-draft-04": ["[email protected]", "https://registry.npmmirror.com/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", { "peerDependencies": { "ajv": "^8.5.0" } }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="],
495
+
496
+ "ajv-i18n": ["[email protected]", "https://registry.npmmirror.com/ajv-i18n/-/ajv-i18n-4.2.0.tgz", { "peerDependencies": { "ajv": "^8.0.0-beta.0" } }, "sha512-v/ei2UkCEeuKNXh8RToiFsUclmU+G57LO1Oo22OagNMENIw+Yb8eMwvHu7Vn9fmkjJyv6XclhJ8TbuigSglPkg=="],
497
+
498
+ "ansi-align": ["[email protected]", "https://registry.npmmirror.com/ansi-align/-/ansi-align-3.0.1.tgz", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
499
+
500
+ "ansi-regex": ["[email protected]", "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.3.0.tgz", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="],
501
+
502
+ "ansi-styles": ["[email protected]", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
503
+
504
+ "any-promise": ["[email protected]", "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="],
505
+
506
+ "anymatch": ["[email protected]", "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
507
+
508
+ "arg": ["[email protected]", "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
509
+
510
+ "argparse": ["[email protected]", "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
511
+
512
+ "aria-hidden": ["[email protected]", "https://registry.npmmirror.com/aria-hidden/-/aria-hidden-1.2.6.tgz", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
513
+
514
+ "aria-query": ["[email protected]", "https://registry.npmmirror.com/aria-query/-/aria-query-5.3.2.tgz", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
515
+
516
+ "array-iterate": ["[email protected]", "https://registry.npmmirror.com/array-iterate/-/array-iterate-2.0.1.tgz", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="],
517
+
518
+ "astro": ["[email protected]", "https://registry.npmmirror.com/astro/-/astro-4.16.19.tgz", { "dependencies": { "@astrojs/compiler": "^2.10.3", "@astrojs/internal-helpers": "0.4.1", "@astrojs/markdown-remark": "5.3.0", "@astrojs/telemetry": "3.1.0", "@babel/core": "^7.26.0", "@babel/plugin-transform-react-jsx": "^7.25.9", "@babel/types": "^7.26.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.1.3", "@types/babel__core": "^7.20.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "boxen": "8.0.1", "ci-info": "^4.1.0", "clsx": "^2.1.1", "common-ancestor-path": "^1.0.1", "cookie": "^0.7.2", "cssesc": "^3.0.0", "debug": "^4.3.7", "deterministic-object-hash": "^2.0.2", "devalue": "^5.1.1", "diff": "^5.2.0", "dlv": "^1.1.3", "dset": "^3.1.4", "es-module-lexer": "^1.5.4", "esbuild": "^0.21.5", "estree-walker": "^3.0.3", "fast-glob": "^3.3.2", "flattie": "^1.1.1", "github-slugger": "^2.0.0", "gray-matter": "^4.0.3", "html-escaper": "^3.0.3", "http-cache-semantics": "^4.1.1", "js-yaml": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.14", "magicast": "^0.3.5", "micromatch": "^4.0.8", "mrmime": "^2.0.0", "neotraverse": "^0.6.18", "ora": "^8.1.1", "p-limit": "^6.1.0", "p-queue": "^8.0.1", "preferred-pm": "^4.0.0", "prompts": "^2.4.2", "rehype": "^13.0.2", "semver": "^7.6.3", "shiki": "^1.23.1", "tinyexec": "^0.3.1", "tsconfck": "^3.1.4", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3", "vite": "^5.4.11", "vitefu": "^1.0.4", "which-pm": "^3.0.0", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", "zod": "^3.23.8", "zod-to-json-schema": "^3.23.5", "zod-to-ts": "^1.2.0" }, "optionalDependencies": { "sharp": "^0.33.3" }, "bin": "astro.js" }, "sha512-baeSswPC5ZYvhGDoj25L2FuzKRWMgx105FetOPQVJFMCAp0o08OonYC7AhwsFdhvp7GapqjnC1Fe3lKb2lupYw=="],
519
+
520
+ "astro-icon": ["[email protected]", "https://registry.npmmirror.com/astro-icon/-/astro-icon-1.2.0.tgz", { "dependencies": { "@iconify/tools": "^5.0.12", "@iconify/types": "^2.0.0", "@iconify/utils": "^3.1.4" } }, "sha512-0848V2WZuTPhqGSqLeZimLJZ5wmamMtWYzvA2kvg8s2DrUPZ3/nrTcHJWN0eJHt62jy7uvNxgTNiUJGIr7JNxg=="],
521
+
522
+ "autoprefixer": ["[email protected]", "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.5.4.tgz", { "dependencies": { "browserslist": "^4.28.6", "caniuse-lite": "^1.0.30001806", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": "bin/autoprefixer" }, "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA=="],
523
+
524
+ "axobject-query": ["[email protected]", "https://registry.npmmirror.com/axobject-query/-/axobject-query-4.1.0.tgz", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
525
+
526
+ "bail": ["[email protected]", "https://registry.npmmirror.com/bail/-/bail-2.0.2.tgz", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
527
+
528
+ "base-64": ["[email protected]", "https://registry.npmmirror.com/base-64/-/base-64-1.0.0.tgz", {}, "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="],
529
+
530
+ "baseline-browser-mapping": ["[email protected]", "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.15.tgz", { "bin": "dist/cli.cjs" }, "sha512-FwMjJJ7HnyZpWe+oWxegG0fezZyBZUagI5LZEoO3GCbtbKNwRfMH9Ue5d5v01PNePBy1QSfPSDTTeVL0Hb9EzA=="],
531
+
532
+ "bcryptjs": ["[email protected]", "https://registry.npmmirror.com/bcryptjs/-/bcryptjs-2.4.3.tgz", {}, "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="],
533
+
534
+ "binary-extensions": ["[email protected]", "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
535
+
536
+ "boolbase": ["[email protected]", "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
537
+
538
+ "boxen": ["[email protected]", "https://registry.npmmirror.com/boxen/-/boxen-8.0.1.tgz", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="],
539
+
540
+ "braces": ["[email protected]", "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
541
+
542
+ "browserslist": ["[email protected]", "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.8.tgz", { "dependencies": { "baseline-browser-mapping": "^2.11.12", "caniuse-lite": "^1.0.30001809", "electron-to-chromium": "^1.5.402", "node-releases": "^2.0.53", "update-browserslist-db": "^1.3.0" }, "bin": "cli.js" }, "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA=="],
543
+
544
+ "buffer-from": ["[email protected]", "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
545
+
546
+ "camelcase": ["[email protected]", "https://registry.npmmirror.com/camelcase/-/camelcase-8.0.0.tgz", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="],
547
+
548
+ "camelcase-css": ["[email protected]", "https://registry.npmmirror.com/camelcase-css/-/camelcase-css-2.0.1.tgz", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
549
+
550
+ "caniuse-lite": ["[email protected]", "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", {}, "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ=="],
551
+
552
+ "ccount": ["[email protected]", "https://registry.npmmirror.com/ccount/-/ccount-2.0.1.tgz", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
553
+
554
+ "chalk": ["[email protected]", "https://registry.npmmirror.com/chalk/-/chalk-5.6.2.tgz", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
555
+
556
+ "character-entities": ["[email protected]", "https://registry.npmmirror.com/character-entities/-/character-entities-2.0.2.tgz", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
557
+
558
+ "character-entities-html4": ["[email protected]", "https://registry.npmmirror.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
559
+
560
+ "character-entities-legacy": ["[email protected]", "https://registry.npmmirror.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
561
+
562
+ "chokidar": ["[email protected]", "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
563
+
564
+ "ci-info": ["[email protected]", "https://registry.npmmirror.com/ci-info/-/ci-info-4.4.0.tgz", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
565
+
566
+ "class-variance-authority": ["[email protected]", "https://registry.npmmirror.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
567
+
568
+ "cli-boxes": ["[email protected]", "https://registry.npmmirror.com/cli-boxes/-/cli-boxes-3.0.0.tgz", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
569
+
570
+ "cli-cursor": ["[email protected]", "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-5.0.0.tgz", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
571
+
572
+ "cli-spinners": ["[email protected]", "https://registry.npmmirror.com/cli-spinners/-/cli-spinners-2.9.2.tgz", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],
573
+
574
+ "cliui": ["[email protected]", "https://registry.npmmirror.com/cliui/-/cliui-9.0.1.tgz", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="],
575
+
576
+ "clsx": ["[email protected]", "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
577
+
578
+ "color": ["[email protected]", "https://registry.npmmirror.com/color/-/color-4.2.3.tgz", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
579
+
580
+ "color-convert": ["[email protected]", "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
581
+
582
+ "color-name": ["[email protected]", "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
583
+
584
+ "color-string": ["[email protected]", "https://registry.npmmirror.com/color-string/-/color-string-1.9.1.tgz", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
585
+
586
+ "comma-separated-tokens": ["[email protected]", "https://registry.npmmirror.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
587
+
588
+ "commander": ["[email protected]", "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
589
+
590
+ "common-ancestor-path": ["[email protected]", "https://registry.npmmirror.com/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="],
591
+
592
+ "convert-source-map": ["[email protected]", "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
593
+
594
+ "cookie": ["[email protected]", "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
595
+
596
+ "css-select": ["[email protected]", "https://registry.npmmirror.com/css-select/-/css-select-5.2.2.tgz", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
597
+
598
+ "css-tree": ["[email protected]", "https://registry.npmmirror.com/css-tree/-/css-tree-3.2.1.tgz", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="],
599
+
600
+ "css-what": ["[email protected]", "https://registry.npmmirror.com/css-what/-/css-what-6.2.2.tgz", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
601
+
602
+ "cssesc": ["[email protected]", "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", { "bin": "bin/cssesc" }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
603
+
604
+ "csso": ["[email protected]", "https://registry.npmmirror.com/csso/-/csso-5.0.5.tgz", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="],
605
+
606
+ "csstype": ["[email protected]", "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
607
+
608
+ "data-uri-to-buffer": ["[email protected]", "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
609
+
610
+ "debug": ["[email protected]", "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
611
+
612
+ "decode-named-character-reference": ["[email protected]", "https://registry.npmmirror.com/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
613
+
614
+ "depd": ["[email protected]", "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
615
+
616
+ "dequal": ["[email protected]", "https://registry.npmmirror.com/dequal/-/dequal-2.0.3.tgz", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
617
+
618
+ "destroy": ["[email protected]", "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="],
619
+
620
+ "detect-libc": ["[email protected]", "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.0.2.tgz", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="],
621
+
622
+ "detect-node-es": ["[email protected]", "https://registry.npmmirror.com/detect-node-es/-/detect-node-es-1.1.0.tgz", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
623
+
624
+ "deterministic-object-hash": ["[email protected]", "https://registry.npmmirror.com/deterministic-object-hash/-/deterministic-object-hash-2.0.2.tgz", { "dependencies": { "base-64": "^1.0.0" } }, "sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ=="],
625
+
626
+ "devalue": ["[email protected]", "https://registry.npmmirror.com/devalue/-/devalue-5.9.0.tgz", {}, "sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A=="],
627
+
628
+ "devlop": ["[email protected]", "https://registry.npmmirror.com/devlop/-/devlop-1.1.0.tgz", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
629
+
630
+ "didyoumean": ["[email protected]", "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="],
631
+
632
+ "diff": ["[email protected]", "https://registry.npmmirror.com/diff/-/diff-5.2.2.tgz", {}, "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A=="],
633
+
634
+ "dlv": ["[email protected]", "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
635
+
636
+ "dom-serializer": ["[email protected]", "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-2.0.0.tgz", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
637
+
638
+ "domelementtype": ["[email protected]", "https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
639
+
640
+ "domhandler": ["[email protected]", "https://registry.npmmirror.com/domhandler/-/domhandler-5.0.3.tgz", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
641
+
642
+ "domutils": ["[email protected]", "https://registry.npmmirror.com/domutils/-/domutils-3.2.2.tgz", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
643
+
644
+ "dotenv": ["[email protected]", "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
645
+
646
+ "drizzle-kit": ["[email protected]", "https://registry.npmmirror.com/drizzle-kit/-/drizzle-kit-0.31.10.tgz", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": "bin.cjs" }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="],
647
+
648
+ "drizzle-orm": ["[email protected]", "https://registry.npmmirror.com/drizzle-orm/-/drizzle-orm-0.45.2.tgz", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="],
649
+
650
+ "dset": ["[email protected]", "https://registry.npmmirror.com/dset/-/dset-3.1.4.tgz", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="],
651
+
652
+ "ee-first": ["[email protected]", "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
653
+
654
+ "electron-to-chromium": ["[email protected]", "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.411.tgz", {}, "sha512-gglkxzokjHfawpGxq75XdBV2/l3BAPzrsMs70qgaZdTW5rpV1tC4MdgJVP9fN126bODA4ZJQkn1wryEzJyQXIg=="],
655
+
656
+ "emmet": ["[email protected]", "https://registry.npmmirror.com/emmet/-/emmet-2.4.11.tgz", { "dependencies": { "@emmetio/abbreviation": "^2.3.3", "@emmetio/css-abbreviation": "^2.1.8" } }, "sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ=="],
657
+
658
+ "emoji-regex": ["[email protected]", "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-10.6.0.tgz", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
659
+
660
+ "emoji-regex-xs": ["[email protected]", "https://registry.npmmirror.com/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", {}, "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg=="],
661
+
662
+ "encodeurl": ["[email protected]", "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
663
+
664
+ "entities": ["[email protected]", "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
665
+
666
+ "es-errors": ["[email protected]", "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
667
+
668
+ "es-module-lexer": ["[email protected]", "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
669
+
670
+ "esbuild": ["[email protected]", "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": "bin/esbuild" }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
671
+
672
+ "escalade": ["[email protected]", "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
673
+
674
+ "escape-html": ["[email protected]", "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
675
+
676
+ "escape-string-regexp": ["[email protected]", "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
677
+
678
+ "esprima": ["[email protected]", "https://registry.npmmirror.com/esprima/-/esprima-4.0.1.tgz", { "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
679
+
680
+ "estree-walker": ["[email protected]", "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
681
+
682
+ "etag": ["[email protected]", "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
683
+
684
+ "eventemitter3": ["[email protected]", "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.4.tgz", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
685
+
686
+ "extend": ["[email protected]", "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
687
+
688
+ "extend-shallow": ["[email protected]", "https://registry.npmmirror.com/extend-shallow/-/extend-shallow-2.0.1.tgz", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="],
689
+
690
+ "fast-deep-equal": ["[email protected]", "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
691
+
692
+ "fast-glob": ["[email protected]", "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
693
+
694
+ "fast-uri": ["[email protected]", "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.5.tgz", {}, "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw=="],
695
+
696
+ "fastq": ["[email protected]", "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
697
+
698
+ "fdir": ["[email protected]", "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
699
+
700
+ "fetch-blob": ["[email protected]", "https://registry.npmmirror.com/fetch-blob/-/fetch-blob-3.2.0.tgz", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
701
+
702
+ "fflate": ["[email protected]", "https://registry.npmmirror.com/fflate/-/fflate-0.8.3.tgz", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="],
703
+
704
+ "fill-range": ["[email protected]", "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
705
+
706
+ "find-up": ["[email protected]", "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
707
+
708
+ "find-up-simple": ["[email protected]", "https://registry.npmmirror.com/find-up-simple/-/find-up-simple-1.0.1.tgz", {}, "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ=="],
709
+
710
+ "find-yarn-workspace-root2": ["[email protected]", "https://registry.npmmirror.com/find-yarn-workspace-root2/-/find-yarn-workspace-root2-1.2.16.tgz", { "dependencies": { "micromatch": "^4.0.2", "pkg-dir": "^4.2.0" } }, "sha512-hr6hb1w8ePMpPVUK39S4RlwJzi+xPLuVuG8XlwXU3KD5Yn3qgBWVfy3AzNlDhWvE1EORCE65/Qm26rFQt3VLVA=="],
711
+
712
+ "flattie": ["[email protected]", "https://registry.npmmirror.com/flattie/-/flattie-1.1.1.tgz", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="],
713
+
714
+ "formdata-polyfill": ["[email protected]", "https://registry.npmmirror.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
715
+
716
+ "fraction.js": ["[email protected]", "https://registry.npmmirror.com/fraction.js/-/fraction.js-5.3.4.tgz", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
717
+
718
+ "fresh": ["[email protected]", "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
719
+
720
+ "fsevents": ["[email protected]", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
721
+
722
+ "function-bind": ["[email protected]", "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
723
+
724
+ "gensync": ["[email protected]", "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
725
+
726
+ "get-caller-file": ["[email protected]", "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
727
+
728
+ "get-east-asian-width": ["[email protected]", "https://registry.npmmirror.com/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
729
+
730
+ "get-nonce": ["[email protected]", "https://registry.npmmirror.com/get-nonce/-/get-nonce-1.0.1.tgz", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
731
+
732
+ "get-tsconfig": ["[email protected]", "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.14.3.tgz", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA=="],
733
+
734
+ "github-slugger": ["[email protected]", "https://registry.npmmirror.com/github-slugger/-/github-slugger-2.0.0.tgz", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
735
+
736
+ "glob-parent": ["[email protected]", "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
737
+
738
+ "graceful-fs": ["[email protected]", "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
739
+
740
+ "gray-matter": ["[email protected]", "https://registry.npmmirror.com/gray-matter/-/gray-matter-4.0.3.tgz", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="],
741
+
742
+ "hasown": ["[email protected]", "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
743
+
744
+ "hast-util-from-html": ["[email protected]", "https://registry.npmmirror.com/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="],
745
+
746
+ "hast-util-from-parse5": ["[email protected]", "https://registry.npmmirror.com/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="],
747
+
748
+ "hast-util-is-element": ["[email protected]", "https://registry.npmmirror.com/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="],
749
+
750
+ "hast-util-parse-selector": ["[email protected]", "https://registry.npmmirror.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
751
+
752
+ "hast-util-raw": ["[email protected]", "https://registry.npmmirror.com/hast-util-raw/-/hast-util-raw-9.1.0.tgz", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="],
753
+
754
+ "hast-util-to-html": ["[email protected]", "https://registry.npmmirror.com/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
755
+
756
+ "hast-util-to-parse5": ["[email protected]", "https://registry.npmmirror.com/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="],
757
+
758
+ "hast-util-to-text": ["[email protected]", "https://registry.npmmirror.com/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="],
759
+
760
+ "hast-util-whitespace": ["[email protected]", "https://registry.npmmirror.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
761
+
762
+ "hastscript": ["[email protected]", "https://registry.npmmirror.com/hastscript/-/hastscript-9.0.1.tgz", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
763
+
764
+ "html-escaper": ["[email protected]", "https://registry.npmmirror.com/html-escaper/-/html-escaper-3.0.3.tgz", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="],
765
+
766
+ "html-void-elements": ["[email protected]", "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-3.0.0.tgz", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
767
+
768
+ "http-cache-semantics": ["[email protected]", "https://registry.npmmirror.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
769
+
770
+ "http-errors": ["[email protected]", "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
771
+
772
+ "import-meta-resolve": ["[email protected]", "https://registry.npmmirror.com/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="],
773
+
774
+ "inherits": ["[email protected]", "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
775
+
776
+ "is-arrayish": ["[email protected]", "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.3.4.tgz", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="],
777
+
778
+ "is-binary-path": ["[email protected]", "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
779
+
780
+ "is-core-module": ["[email protected]", "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.2.tgz", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="],
781
+
782
+ "is-docker": ["[email protected]", "https://registry.npmmirror.com/is-docker/-/is-docker-3.0.0.tgz", { "bin": "cli.js" }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
783
+
784
+ "is-extendable": ["[email protected]", "https://registry.npmmirror.com/is-extendable/-/is-extendable-0.1.1.tgz", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
785
+
786
+ "is-extglob": ["[email protected]", "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
787
+
788
+ "is-fullwidth-code-point": ["[email protected]", "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
789
+
790
+ "is-glob": ["[email protected]", "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
791
+
792
+ "is-inside-container": ["[email protected]", "https://registry.npmmirror.com/is-inside-container/-/is-inside-container-1.0.0.tgz", { "dependencies": { "is-docker": "^3.0.0" }, "bin": "cli.js" }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
793
+
794
+ "is-interactive": ["[email protected]", "https://registry.npmmirror.com/is-interactive/-/is-interactive-2.0.0.tgz", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="],
795
+
796
+ "is-number": ["[email protected]", "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
797
+
798
+ "is-plain-obj": ["[email protected]", "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
799
+
800
+ "is-unicode-supported": ["[email protected]", "https://registry.npmmirror.com/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
801
+
802
+ "is-wsl": ["[email protected]", "https://registry.npmmirror.com/is-wsl/-/is-wsl-3.1.1.tgz", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
803
+
804
+ "jiti": ["[email protected]", "https://registry.npmmirror.com/jiti/-/jiti-1.21.7.tgz", { "bin": "bin/jiti.js" }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
805
+
806
+ "jose": ["[email protected]", "https://registry.npmmirror.com/jose/-/jose-5.10.0.tgz", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="],
807
+
808
+ "js-base64": ["[email protected]", "https://registry.npmmirror.com/js-base64/-/js-base64-3.9.3.tgz", {}, "sha512-uwYQp+VJ38FVvtim6qNbit6e9uT6dwWQ4Y1+H9TxhW5hcHjpHwoxlR0nMpqUmIFOmu4VqMxwdJA88gIVuZJQ/g=="],
809
+
810
+ "js-tokens": ["[email protected]", "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
811
+
812
+ "js-yaml": ["[email protected]", "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.3.1.tgz", { "dependencies": { "argparse": "^2.0.1" }, "bin": "bin/js-yaml.js" }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="],
813
+
814
+ "jsesc": ["[email protected]", "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", { "bin": "bin/jsesc" }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
815
+
816
+ "json-schema-traverse": ["[email protected]", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
817
+
818
+ "json5": ["[email protected]", "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", { "bin": "lib/cli.js" }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
819
+
820
+ "jsonc-parser": ["[email protected]", "https://registry.npmmirror.com/jsonc-parser/-/jsonc-parser-2.3.1.tgz", {}, "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="],
821
+
822
+ "kind-of": ["[email protected]", "https://registry.npmmirror.com/kind-of/-/kind-of-6.0.3.tgz", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
823
+
824
+ "kleur": ["[email protected]", "https://registry.npmmirror.com/kleur/-/kleur-4.1.5.tgz", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
825
+
826
+ "libsql": ["[email protected]", "https://registry.npmmirror.com/libsql/-/libsql-0.5.29.tgz", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.29", "@libsql/darwin-x64": "0.5.29", "@libsql/linux-arm-gnueabihf": "0.5.29", "@libsql/linux-arm-musleabihf": "0.5.29", "@libsql/linux-arm64-gnu": "0.5.29", "@libsql/linux-arm64-musl": "0.5.29", "@libsql/linux-x64-gnu": "0.5.29", "@libsql/linux-x64-musl": "0.5.29", "@libsql/win32-x64-msvc": "0.5.29" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg=="],
827
+
828
+ "lilconfig": ["[email protected]", "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.3.tgz", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
829
+
830
+ "lines-and-columns": ["[email protected]", "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
831
+
832
+ "load-yaml-file": ["[email protected]", "https://registry.npmmirror.com/load-yaml-file/-/load-yaml-file-0.2.0.tgz", { "dependencies": { "graceful-fs": "^4.1.5", "js-yaml": "^3.13.0", "pify": "^4.0.1", "strip-bom": "^3.0.0" } }, "sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw=="],
833
+
834
+ "locate-path": ["[email protected]", "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
835
+
836
+ "log-symbols": ["[email protected]", "https://registry.npmmirror.com/log-symbols/-/log-symbols-6.0.0.tgz", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="],
837
+
838
+ "longest-streak": ["[email protected]", "https://registry.npmmirror.com/longest-streak/-/longest-streak-3.1.0.tgz", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
839
+
840
+ "loose-envify": ["[email protected]", "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": "cli.js" }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
841
+
842
+ "lru-cache": ["[email protected]", "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
843
+
844
+ "lucide-react": ["[email protected]", "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.400.0.tgz", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-rpp7pFHh3Xd93KHixNgB0SqThMHpYNzsGUu69UaQbSZ75Q/J3m5t6EhKyMT3m4w2WOxmJ2mY0tD3vebnXqQryQ=="],
845
+
846
+ "magic-string": ["[email protected]", "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
847
+
848
+ "magicast": ["[email protected]", "https://registry.npmmirror.com/magicast/-/magicast-0.3.5.tgz", { "dependencies": { "@babel/parser": "^7.25.4", "@babel/types": "^7.25.4", "source-map-js": "^1.2.0" } }, "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ=="],
849
+
850
+ "markdown-table": ["[email protected]", "https://registry.npmmirror.com/markdown-table/-/markdown-table-3.0.4.tgz", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
851
+
852
+ "mdast-util-definitions": ["[email protected]", "https://registry.npmmirror.com/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ=="],
853
+
854
+ "mdast-util-find-and-replace": ["[email protected]", "https://registry.npmmirror.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
855
+
856
+ "mdast-util-from-markdown": ["[email protected]", "https://registry.npmmirror.com/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="],
857
+
858
+ "mdast-util-gfm": ["[email protected]", "https://registry.npmmirror.com/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
859
+
860
+ "mdast-util-gfm-autolink-literal": ["[email protected]", "https://registry.npmmirror.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
861
+
862
+ "mdast-util-gfm-footnote": ["[email protected]", "https://registry.npmmirror.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="],
863
+
864
+ "mdast-util-gfm-strikethrough": ["[email protected]", "https://registry.npmmirror.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
865
+
866
+ "mdast-util-gfm-table": ["[email protected]", "https://registry.npmmirror.com/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
867
+
868
+ "mdast-util-gfm-task-list-item": ["[email protected]", "https://registry.npmmirror.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
869
+
870
+ "mdast-util-phrasing": ["[email protected]", "https://registry.npmmirror.com/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
871
+
872
+ "mdast-util-to-hast": ["[email protected]", "https://registry.npmmirror.com/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
873
+
874
+ "mdast-util-to-markdown": ["[email protected]", "https://registry.npmmirror.com/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
875
+
876
+ "mdast-util-to-string": ["[email protected]", "https://registry.npmmirror.com/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
877
+
878
+ "mdn-data": ["[email protected]", "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.27.1.tgz", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
879
+
880
+ "merge2": ["[email protected]", "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
881
+
882
+ "micromark": ["[email protected]", "https://registry.npmmirror.com/micromark/-/micromark-4.0.2.tgz", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
883
+
884
+ "micromark-core-commonmark": ["[email protected]", "https://registry.npmmirror.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
885
+
886
+ "micromark-extension-gfm": ["[email protected]", "https://registry.npmmirror.com/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
887
+
888
+ "micromark-extension-gfm-autolink-literal": ["[email protected]", "https://registry.npmmirror.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
889
+
890
+ "micromark-extension-gfm-footnote": ["[email protected]", "https://registry.npmmirror.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
891
+
892
+ "micromark-extension-gfm-strikethrough": ["[email protected]", "https://registry.npmmirror.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
893
+
894
+ "micromark-extension-gfm-table": ["[email protected]", "https://registry.npmmirror.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
895
+
896
+ "micromark-extension-gfm-tagfilter": ["[email protected]", "https://registry.npmmirror.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
897
+
898
+ "micromark-extension-gfm-task-list-item": ["[email protected]", "https://registry.npmmirror.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
899
+
900
+ "micromark-factory-destination": ["[email protected]", "https://registry.npmmirror.com/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
901
+
902
+ "micromark-factory-label": ["[email protected]", "https://registry.npmmirror.com/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
903
+
904
+ "micromark-factory-space": ["[email protected]", "https://registry.npmmirror.com/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
905
+
906
+ "micromark-factory-title": ["[email protected]", "https://registry.npmmirror.com/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
907
+
908
+ "micromark-factory-whitespace": ["[email protected]", "https://registry.npmmirror.com/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
909
+
910
+ "micromark-util-character": ["[email protected]", "https://registry.npmmirror.com/micromark-util-character/-/micromark-util-character-2.1.1.tgz", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
911
+
912
+ "micromark-util-chunked": ["[email protected]", "https://registry.npmmirror.com/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
913
+
914
+ "micromark-util-classify-character": ["[email protected]", "https://registry.npmmirror.com/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
915
+
916
+ "micromark-util-combine-extensions": ["[email protected]", "https://registry.npmmirror.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
917
+
918
+ "micromark-util-decode-numeric-character-reference": ["[email protected]", "https://registry.npmmirror.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
919
+
920
+ "micromark-util-decode-string": ["[email protected]", "https://registry.npmmirror.com/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="],
921
+
922
+ "micromark-util-encode": ["[email protected]", "https://registry.npmmirror.com/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
923
+
924
+ "micromark-util-html-tag-name": ["[email protected]", "https://registry.npmmirror.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
925
+
926
+ "micromark-util-normalize-identifier": ["[email protected]", "https://registry.npmmirror.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
927
+
928
+ "micromark-util-resolve-all": ["[email protected]", "https://registry.npmmirror.com/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
929
+
930
+ "micromark-util-sanitize-uri": ["[email protected]", "https://registry.npmmirror.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
931
+
932
+ "micromark-util-subtokenize": ["[email protected]", "https://registry.npmmirror.com/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
933
+
934
+ "micromark-util-symbol": ["[email protected]", "https://registry.npmmirror.com/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
935
+
936
+ "micromark-util-types": ["[email protected]", "https://registry.npmmirror.com/micromark-util-types/-/micromark-util-types-2.0.2.tgz", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
937
+
938
+ "micromatch": ["[email protected]", "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
939
+
940
+ "mime": ["[email protected]", "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", { "bin": "cli.js" }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
941
+
942
+ "mimic-function": ["[email protected]", "https://registry.npmmirror.com/mimic-function/-/mimic-function-5.0.1.tgz", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
943
+
944
+ "modern-tar": ["[email protected]", "https://registry.npmmirror.com/modern-tar/-/modern-tar-0.7.7.tgz", {}, "sha512-t9VmxaqrmANnEOBhpSDI6HD192Ge48k8vmWqQQL7hSFEqHEYwZbbsu49+aKLWZeRvFs3j1pMhXOqqF4kPlvjkQ=="],
945
+
946
+ "mrmime": ["[email protected]", "https://registry.npmmirror.com/mrmime/-/mrmime-2.0.1.tgz", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
947
+
948
+ "ms": ["[email protected]", "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
949
+
950
+ "muggle-string": ["[email protected]", "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="],
951
+
952
+ "mz": ["[email protected]", "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
953
+
954
+ "nanoid": ["[email protected]", "https://registry.npmmirror.com/nanoid/-/nanoid-5.1.16.tgz", { "bin": "bin/nanoid.js" }, "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ=="],
955
+
956
+ "neotraverse": ["[email protected]", "https://registry.npmmirror.com/neotraverse/-/neotraverse-0.6.18.tgz", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="],
957
+
958
+ "nlcst-to-string": ["[email protected]", "https://registry.npmmirror.com/nlcst-to-string/-/nlcst-to-string-4.0.0.tgz", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="],
959
+
960
+ "node-domexception": ["[email protected]", "https://registry.npmmirror.com/node-domexception/-/node-domexception-1.0.0.tgz", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
961
+
962
+ "node-fetch": ["[email protected]", "https://registry.npmmirror.com/node-fetch/-/node-fetch-3.3.2.tgz", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
963
+
964
+ "node-releases": ["[email protected]", "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.53.tgz", {}, "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ=="],
965
+
966
+ "normalize-path": ["[email protected]", "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
967
+
968
+ "nth-check": ["[email protected]", "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
969
+
970
+ "object-assign": ["[email protected]", "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
971
+
972
+ "object-hash": ["[email protected]", "https://registry.npmmirror.com/object-hash/-/object-hash-3.0.0.tgz", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="],
973
+
974
+ "on-finished": ["[email protected]", "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
975
+
976
+ "onetime": ["[email protected]", "https://registry.npmmirror.com/onetime/-/onetime-7.0.0.tgz", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
977
+
978
+ "oniguruma-to-es": ["[email protected]", "https://registry.npmmirror.com/oniguruma-to-es/-/oniguruma-to-es-2.3.0.tgz", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
979
+
980
+ "ora": ["[email protected]", "https://registry.npmmirror.com/ora/-/ora-8.2.0.tgz", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="],
981
+
982
+ "p-limit": ["[email protected]", "https://registry.npmmirror.com/p-limit/-/p-limit-6.2.0.tgz", { "dependencies": { "yocto-queue": "^1.1.1" } }, "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA=="],
983
+
984
+ "p-locate": ["[email protected]", "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
985
+
986
+ "p-queue": ["[email protected]", "https://registry.npmmirror.com/p-queue/-/p-queue-8.1.1.tgz", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^6.1.2" } }, "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ=="],
987
+
988
+ "p-timeout": ["[email protected]", "https://registry.npmmirror.com/p-timeout/-/p-timeout-6.1.4.tgz", {}, "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg=="],
989
+
990
+ "p-try": ["[email protected]", "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
991
+
992
+ "package-manager-detector": ["[email protected]", "https://registry.npmmirror.com/package-manager-detector/-/package-manager-detector-1.8.0.tgz", {}, "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A=="],
993
+
994
+ "parse-latin": ["[email protected]", "https://registry.npmmirror.com/parse-latin/-/parse-latin-7.0.0.tgz", { "dependencies": { "@types/nlcst": "^2.0.0", "@types/unist": "^3.0.0", "nlcst-to-string": "^4.0.0", "unist-util-modify-children": "^4.0.0", "unist-util-visit-children": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ=="],
995
+
996
+ "parse5": ["[email protected]", "https://registry.npmmirror.com/parse5/-/parse5-7.3.0.tgz", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
997
+
998
+ "path-browserify": ["[email protected]", "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
999
+
1000
+ "path-exists": ["[email protected]", "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
1001
+
1002
+ "path-parse": ["[email protected]", "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
1003
+
1004
+ "pathe": ["[email protected]", "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
1005
+
1006
+ "picocolors": ["[email protected]", "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
1007
+
1008
+ "picomatch": ["[email protected]", "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
1009
+
1010
+ "pify": ["[email protected]", "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="],
1011
+
1012
+ "pirates": ["[email protected]", "https://registry.npmmirror.com/pirates/-/pirates-4.0.7.tgz", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
1013
+
1014
+ "pkg-dir": ["[email protected]", "https://registry.npmmirror.com/pkg-dir/-/pkg-dir-4.2.0.tgz", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="],
1015
+
1016
+ "postcss": ["[email protected]", "https://registry.npmmirror.com/postcss/-/postcss-8.5.26.tgz", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="],
1017
+
1018
+ "postcss-import": ["[email protected]", "https://registry.npmmirror.com/postcss-import/-/postcss-import-15.1.0.tgz", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="],
1019
+
1020
+ "postcss-js": ["[email protected]", "https://registry.npmmirror.com/postcss-js/-/postcss-js-4.1.0.tgz", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw=="],
1021
+
1022
+ "postcss-load-config": ["[email protected]", "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz", { "dependencies": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["ts-node"] }, "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ=="],
1023
+
1024
+ "postcss-nested": ["[email protected]", "https://registry.npmmirror.com/postcss-nested/-/postcss-nested-6.2.0.tgz", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="],
1025
+
1026
+ "postcss-selector-parser": ["[email protected]", "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ=="],
1027
+
1028
+ "postcss-value-parser": ["[email protected]", "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
1029
+
1030
+ "preferred-pm": ["[email protected]", "https://registry.npmmirror.com/preferred-pm/-/preferred-pm-4.1.1.tgz", { "dependencies": { "find-up-simple": "^1.0.0", "find-yarn-workspace-root2": "1.2.16", "which-pm": "^3.0.1" } }, "sha512-rU+ZAv1Ur9jAUZtGPebQVQPzdGhNzaEiQ7VL9+cjsAWPHFYOccNXPNiev1CCDSOg/2j7UujM7ojNhpkuILEVNQ=="],
1031
+
1032
+ "prettier": ["[email protected]", "https://registry.npmmirror.com/prettier/-/prettier-3.9.6.tgz", { "bin": "bin/prettier.cjs" }, "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g=="],
1033
+
1034
+ "prismjs": ["[email protected]", "https://registry.npmmirror.com/prismjs/-/prismjs-1.30.0.tgz", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
1035
+
1036
+ "promise-limit": ["[email protected]", "https://registry.npmmirror.com/promise-limit/-/promise-limit-2.7.0.tgz", {}, "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="],
1037
+
1038
+ "prompts": ["[email protected]", "https://registry.npmmirror.com/prompts/-/prompts-2.4.2.tgz", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
1039
+
1040
+ "property-information": ["[email protected]", "https://registry.npmmirror.com/property-information/-/property-information-7.2.0.tgz", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="],
1041
+
1042
+ "queue-microtask": ["[email protected]", "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
1043
+
1044
+ "range-parser": ["[email protected]", "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
1045
+
1046
+ "react": ["[email protected]", "https://registry.npmmirror.com/react/-/react-18.3.1.tgz", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
1047
+
1048
+ "react-dom": ["[email protected]", "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="],
1049
+
1050
+ "react-refresh": ["[email protected]", "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.17.0.tgz", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
1051
+
1052
+ "react-remove-scroll": ["[email protected]", "https://registry.npmmirror.com/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
1053
+
1054
+ "react-remove-scroll-bar": ["[email protected]", "https://registry.npmmirror.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
1055
+
1056
+ "react-style-singleton": ["[email protected]", "https://registry.npmmirror.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
1057
+
1058
+ "read-cache": ["[email protected]", "https://registry.npmmirror.com/read-cache/-/read-cache-1.0.0.tgz", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],
1059
+
1060
+ "readdirp": ["[email protected]", "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
1061
+
1062
+ "regex": ["[email protected]", "https://registry.npmmirror.com/regex/-/regex-5.1.1.tgz", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="],
1063
+
1064
+ "regex-recursion": ["[email protected]", "https://registry.npmmirror.com/regex-recursion/-/regex-recursion-5.1.1.tgz", { "dependencies": { "regex": "^5.1.1", "regex-utilities": "^2.3.0" } }, "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w=="],
1065
+
1066
+ "regex-utilities": ["[email protected]", "https://registry.npmmirror.com/regex-utilities/-/regex-utilities-2.3.0.tgz", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
1067
+
1068
+ "rehype": ["[email protected]", "https://registry.npmmirror.com/rehype/-/rehype-13.0.2.tgz", { "dependencies": { "@types/hast": "^3.0.0", "rehype-parse": "^9.0.0", "rehype-stringify": "^10.0.0", "unified": "^11.0.0" } }, "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A=="],
1069
+
1070
+ "rehype-parse": ["[email protected]", "https://registry.npmmirror.com/rehype-parse/-/rehype-parse-9.0.1.tgz", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-html": "^2.0.0", "unified": "^11.0.0" } }, "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag=="],
1071
+
1072
+ "rehype-raw": ["[email protected]", "https://registry.npmmirror.com/rehype-raw/-/rehype-raw-7.0.0.tgz", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="],
1073
+
1074
+ "rehype-stringify": ["[email protected]", "https://registry.npmmirror.com/rehype-stringify/-/rehype-stringify-10.0.1.tgz", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-html": "^9.0.0", "unified": "^11.0.0" } }, "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA=="],
1075
+
1076
+ "remark-gfm": ["[email protected]", "https://registry.npmmirror.com/remark-gfm/-/remark-gfm-4.0.1.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
1077
+
1078
+ "remark-parse": ["[email protected]", "https://registry.npmmirror.com/remark-parse/-/remark-parse-11.0.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
1079
+
1080
+ "remark-rehype": ["[email protected]", "https://registry.npmmirror.com/remark-rehype/-/remark-rehype-11.1.2.tgz", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
1081
+
1082
+ "remark-smartypants": ["[email protected]", "https://registry.npmmirror.com/remark-smartypants/-/remark-smartypants-3.0.3.tgz", { "dependencies": { "retext": "^9.0.0", "retext-smartypants": "^6.0.0", "unified": "^11.0.4", "unist-util-visit": "^5.0.0" } }, "sha512-gCaK+ndZ0hYezlqFegHFCVh2CQemsi0Npdh1qVM9bxlUFknjkbP6VmojWhddOCrbK0PbbacmYLWfTULRiT1eWA=="],
1083
+
1084
+ "remark-stringify": ["[email protected]", "https://registry.npmmirror.com/remark-stringify/-/remark-stringify-11.0.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
1085
+
1086
+ "request-light": ["[email protected]", "https://registry.npmmirror.com/request-light/-/request-light-0.7.0.tgz", {}, "sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q=="],
1087
+
1088
+ "require-from-string": ["[email protected]", "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
1089
+
1090
+ "resolve": ["[email protected]", "https://registry.npmmirror.com/resolve/-/resolve-1.22.12.tgz", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="],
1091
+
1092
+ "resolve-pkg-maps": ["[email protected]", "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
1093
+
1094
+ "restore-cursor": ["[email protected]", "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-5.1.0.tgz", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
1095
+
1096
+ "retext": ["[email protected]", "https://registry.npmmirror.com/retext/-/retext-9.0.0.tgz", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="],
1097
+
1098
+ "retext-latin": ["[email protected]", "https://registry.npmmirror.com/retext-latin/-/retext-latin-4.0.0.tgz", { "dependencies": { "@types/nlcst": "^2.0.0", "parse-latin": "^7.0.0", "unified": "^11.0.0" } }, "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA=="],
1099
+
1100
+ "retext-smartypants": ["[email protected]", "https://registry.npmmirror.com/retext-smartypants/-/retext-smartypants-6.2.0.tgz", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ=="],
1101
+
1102
+ "retext-stringify": ["[email protected]", "https://registry.npmmirror.com/retext-stringify/-/retext-stringify-4.0.0.tgz", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unified": "^11.0.0" } }, "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA=="],
1103
+
1104
+ "reusify": ["[email protected]", "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
1105
+
1106
+ "rollup": ["[email protected]", "https://registry.npmmirror.com/rollup/-/rollup-4.62.4.tgz", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.62.4", "@rollup/rollup-android-arm64": "4.62.4", "@rollup/rollup-darwin-arm64": "4.62.4", "@rollup/rollup-darwin-x64": "4.62.4", "@rollup/rollup-freebsd-arm64": "4.62.4", "@rollup/rollup-freebsd-x64": "4.62.4", "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", "@rollup/rollup-linux-arm-musleabihf": "4.62.4", "@rollup/rollup-linux-arm64-gnu": "4.62.4", "@rollup/rollup-linux-arm64-musl": "4.62.4", "@rollup/rollup-linux-loong64-gnu": "4.62.4", "@rollup/rollup-linux-loong64-musl": "4.62.4", "@rollup/rollup-linux-ppc64-gnu": "4.62.4", "@rollup/rollup-linux-ppc64-musl": "4.62.4", "@rollup/rollup-linux-riscv64-gnu": "4.62.4", "@rollup/rollup-linux-riscv64-musl": "4.62.4", "@rollup/rollup-linux-s390x-gnu": "4.62.4", "@rollup/rollup-linux-x64-gnu": "4.62.4", "@rollup/rollup-linux-x64-musl": "4.62.4", "@rollup/rollup-openbsd-x64": "4.62.4", "@rollup/rollup-openharmony-arm64": "4.62.4", "@rollup/rollup-win32-arm64-msvc": "4.62.4", "@rollup/rollup-win32-ia32-msvc": "4.62.4", "@rollup/rollup-win32-x64-gnu": "4.62.4", "@rollup/rollup-win32-x64-msvc": "4.62.4", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg=="],
1107
+
1108
+ "run-parallel": ["[email protected]", "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
1109
+
1110
+ "sax": ["[email protected]", "https://registry.npmmirror.com/sax/-/sax-1.6.1.tgz", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="],
1111
+
1112
+ "scheduler": ["[email protected]", "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
1113
+
1114
+ "section-matter": ["[email protected]", "https://registry.npmmirror.com/section-matter/-/section-matter-1.0.0.tgz", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
1115
+
1116
+ "semver": ["[email protected]", "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz", { "bin": "bin/semver.js" }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
1117
+
1118
+ "send": ["[email protected]", "https://registry.npmmirror.com/send/-/send-0.19.2.tgz", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="],
1119
+
1120
+ "server-destroy": ["[email protected]", "https://registry.npmmirror.com/server-destroy/-/server-destroy-1.0.1.tgz", {}, "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ=="],
1121
+
1122
+ "setprototypeof": ["[email protected]", "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
1123
+
1124
+ "sharp": ["[email protected]", "https://registry.npmmirror.com/sharp/-/sharp-0.33.5.tgz", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="],
1125
+
1126
+ "shiki": ["[email protected]", "https://registry.npmmirror.com/shiki/-/shiki-1.29.2.tgz", { "dependencies": { "@shikijs/core": "1.29.2", "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/langs": "1.29.2", "@shikijs/themes": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg=="],
1127
+
1128
+ "signal-exit": ["[email protected]", "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
1129
+
1130
+ "simple-swizzle": ["[email protected]", "https://registry.npmmirror.com/simple-swizzle/-/simple-swizzle-0.2.4.tgz", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="],
1131
+
1132
+ "sisteransi": ["[email protected]", "https://registry.npmmirror.com/sisteransi/-/sisteransi-1.0.5.tgz", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
1133
+
1134
+ "sonner": ["[email protected]", "https://registry.npmmirror.com/sonner/-/sonner-2.0.8.tgz", { "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-UM/ByIoFra8yzV75n1o0Puu0bw5U/9UNnDacrJNspekBewIfsQ3D6ez1nvlWpt7aTsO6rujQtifBpycwIivqlg=="],
1135
+
1136
+ "source-map": ["[email protected]", "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
1137
+
1138
+ "source-map-js": ["[email protected]", "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
1139
+
1140
+ "source-map-support": ["[email protected]", "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
1141
+
1142
+ "space-separated-tokens": ["[email protected]", "https://registry.npmmirror.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
1143
+
1144
+ "sprintf-js": ["[email protected]", "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
1145
+
1146
+ "statuses": ["[email protected]", "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
1147
+
1148
+ "stdin-discarder": ["[email protected]", "https://registry.npmmirror.com/stdin-discarder/-/stdin-discarder-0.2.2.tgz", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="],
1149
+
1150
+ "string-width": ["[email protected]", "https://registry.npmmirror.com/string-width/-/string-width-8.2.2.tgz", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg=="],
1151
+
1152
+ "stringify-entities": ["[email protected]", "https://registry.npmmirror.com/stringify-entities/-/stringify-entities-4.0.4.tgz", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
1153
+
1154
+ "strip-ansi": ["[email protected]", "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.2.0.tgz", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
1155
+
1156
+ "strip-bom": ["[email protected]", "https://registry.npmmirror.com/strip-bom/-/strip-bom-3.0.0.tgz", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
1157
+
1158
+ "strip-bom-string": ["[email protected]", "https://registry.npmmirror.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="],
1159
+
1160
+ "sucrase": ["[email protected]", "https://registry.npmmirror.com/sucrase/-/sucrase-3.35.1.tgz", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="],
1161
+
1162
+ "supports-preserve-symlinks-flag": ["[email protected]", "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
1163
+
1164
+ "svgo": ["[email protected]", "https://registry.npmmirror.com/svgo/-/svgo-4.0.2.tgz", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.5.0" }, "bin": "bin/svgo.js" }, "sha512-ekx94z1rRc5LDi6oSUaeRnYhd0UOJxdtQCL2rF8xpWxD3TPAsISWOrxezqGovqS38GRZOdpDfvQe3ts6F7nsng=="],
1165
+
1166
+ "tailwind-merge": ["[email protected]", "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-2.6.1.tgz", {}, "sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ=="],
1167
+
1168
+ "tailwindcss": ["[email protected]", "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.19.tgz", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="],
1169
+
1170
+ "tailwindcss-animate": ["[email protected]", "https://registry.npmmirror.com/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="],
1171
+
1172
+ "thenify": ["[email protected]", "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
1173
+
1174
+ "thenify-all": ["[email protected]", "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
1175
+
1176
+ "tinyexec": ["[email protected]", "https://registry.npmmirror.com/tinyexec/-/tinyexec-0.3.2.tgz", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
1177
+
1178
+ "tinyglobby": ["[email protected]", "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
1179
+
1180
+ "to-regex-range": ["[email protected]", "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
1181
+
1182
+ "toidentifier": ["[email protected]", "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
1183
+
1184
+ "trim-lines": ["[email protected]", "https://registry.npmmirror.com/trim-lines/-/trim-lines-3.0.1.tgz", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
1185
+
1186
+ "trough": ["[email protected]", "https://registry.npmmirror.com/trough/-/trough-2.2.0.tgz", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
1187
+
1188
+ "ts-interface-checker": ["[email protected]", "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
1189
+
1190
+ "tsconfck": ["[email protected]", "https://registry.npmmirror.com/tsconfck/-/tsconfck-3.1.6.tgz", { "peerDependencies": { "typescript": "^5.0.0" }, "bin": "bin/tsconfck.js" }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
1191
+
1192
+ "tslib": ["[email protected]", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
1193
+
1194
+ "tsx": ["[email protected]", "https://registry.npmmirror.com/tsx/-/tsx-4.23.12.tgz", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": "dist/cli.mjs" }, "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q=="],
1195
+
1196
+ "type-fest": ["[email protected]", "https://registry.npmmirror.com/type-fest/-/type-fest-4.41.0.tgz", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
1197
+
1198
+ "typesafe-path": ["[email protected]", "https://registry.npmmirror.com/typesafe-path/-/typesafe-path-0.2.2.tgz", {}, "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA=="],
1199
+
1200
+ "typescript": ["[email protected]", "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
1201
+
1202
+ "typescript-auto-import-cache": ["[email protected]", "https://registry.npmmirror.com/typescript-auto-import-cache/-/typescript-auto-import-cache-0.3.6.tgz", { "dependencies": { "semver": "^7.3.8" } }, "sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ=="],
1203
+
1204
+ "ultrahtml": ["[email protected]", "https://registry.npmmirror.com/ultrahtml/-/ultrahtml-1.7.0.tgz", {}, "sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g=="],
1205
+
1206
+ "uncrypto": ["[email protected]", "https://registry.npmmirror.com/uncrypto/-/uncrypto-0.1.3.tgz", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
1207
+
1208
+ "undici-types": ["[email protected]", "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
1209
+
1210
+ "unified": ["[email protected]", "https://registry.npmmirror.com/unified/-/unified-11.0.5.tgz", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
1211
+
1212
+ "unist-util-find-after": ["[email protected]", "https://registry.npmmirror.com/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="],
1213
+
1214
+ "unist-util-is": ["[email protected]", "https://registry.npmmirror.com/unist-util-is/-/unist-util-is-6.0.1.tgz", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
1215
+
1216
+ "unist-util-modify-children": ["[email protected]", "https://registry.npmmirror.com/unist-util-modify-children/-/unist-util-modify-children-4.0.0.tgz", { "dependencies": { "@types/unist": "^3.0.0", "array-iterate": "^2.0.0" } }, "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw=="],
1217
+
1218
+ "unist-util-position": ["[email protected]", "https://registry.npmmirror.com/unist-util-position/-/unist-util-position-5.0.0.tgz", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
1219
+
1220
+ "unist-util-remove-position": ["[email protected]", "https://registry.npmmirror.com/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="],
1221
+
1222
+ "unist-util-stringify-position": ["[email protected]", "https://registry.npmmirror.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
1223
+
1224
+ "unist-util-visit": ["[email protected]", "https://registry.npmmirror.com/unist-util-visit/-/unist-util-visit-5.1.0.tgz", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
1225
+
1226
+ "unist-util-visit-children": ["[email protected]", "https://registry.npmmirror.com/unist-util-visit-children/-/unist-util-visit-children-3.0.0.tgz", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA=="],
1227
+
1228
+ "unist-util-visit-parents": ["[email protected]", "https://registry.npmmirror.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
1229
+
1230
+ "update-browserslist-db": ["[email protected]", "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ=="],
1231
+
1232
+ "use-callback-ref": ["[email protected]", "https://registry.npmmirror.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
1233
+
1234
+ "use-sidecar": ["[email protected]", "https://registry.npmmirror.com/use-sidecar/-/use-sidecar-1.1.3.tgz", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
1235
+
1236
+ "util-deprecate": ["[email protected]", "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
1237
+
1238
+ "vfile": ["[email protected]", "https://registry.npmmirror.com/vfile/-/vfile-6.0.3.tgz", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
1239
+
1240
+ "vfile-location": ["[email protected]", "https://registry.npmmirror.com/vfile-location/-/vfile-location-5.0.3.tgz", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="],
1241
+
1242
+ "vfile-message": ["[email protected]", "https://registry.npmmirror.com/vfile-message/-/vfile-message-4.0.3.tgz", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
1243
+
1244
+ "vite": ["[email protected]", "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": "bin/vite.js" }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="],
1245
+
1246
+ "vitefu": ["[email protected]", "https://registry.npmmirror.com/vitefu/-/vitefu-1.1.3.tgz", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
1247
+
1248
+ "volar-service-css": ["[email protected]", "https://registry.npmmirror.com/volar-service-css/-/volar-service-css-0.0.71.tgz", { "dependencies": { "vscode-css-languageservice": "^6.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" } }, "sha512-wRRFt9BpjMKCazcgOh67MSjUjiWUCAh99DyYSDIOTuxaRjEtDC7PpB0k1Y1wbJIW/pVtMUSVbpPo3UGSm0Byxw=="],
1249
+
1250
+ "volar-service-emmet": ["[email protected]", "https://registry.npmmirror.com/volar-service-emmet/-/volar-service-emmet-0.0.71.tgz", { "dependencies": { "@emmetio/css-parser": "^0.4.1", "@emmetio/html-matcher": "^1.3.0", "@vscode/emmet-helper": "^2.9.3", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" } }, "sha512-zqjzt6bN95e3CUstBm0PBFAJnrfz0ZAARka87fart46/gNCLLuP3Vujy8V/J8HEziTFLnfkgIASLFYPUhonJcA=="],
1251
+
1252
+ "volar-service-html": ["[email protected]", "https://registry.npmmirror.com/volar-service-html/-/volar-service-html-0.0.71.tgz", { "dependencies": { "vscode-html-languageservice": "^5.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" } }, "sha512-e8tHPhgQ7ooLfudAEIku+kgd9pWkq3SSz8RbnQDI1+Eb8wbenkLGHqoirLqz5ORLV6wIMr2Iv08RWBG5eOcgpw=="],
1253
+
1254
+ "volar-service-prettier": ["[email protected]", "https://registry.npmmirror.com/volar-service-prettier/-/volar-service-prettier-0.0.71.tgz", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0", "prettier": "^2.2 || ^3.0" } }, "sha512-Rz7JVH3qD108UCdmIEiZvOBNljMt2nLFdbN8AXcDfn7xD9F5I2aCIsDVqBbXw21PsnxG0b7MfwtNF+zPS/NKUg=="],
1255
+
1256
+ "volar-service-typescript": ["[email protected]", "https://registry.npmmirror.com/volar-service-typescript/-/volar-service-typescript-0.0.71.tgz", { "dependencies": { "path-browserify": "^1.0.1", "semver": "^7.6.2", "typescript-auto-import-cache": "^0.3.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-nls": "^5.2.0", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" } }, "sha512-yTtM/BVT6hoyEYnDtaCyAtNhdNeS/mhTTABlBOdw3NNiRBUin3IznFJpgfjer4c6RYopiPjjQjc9VFhxVl1mLw=="],
1257
+
1258
+ "volar-service-typescript-twoslash-queries": ["[email protected]", "https://registry.npmmirror.com/volar-service-typescript-twoslash-queries/-/volar-service-typescript-twoslash-queries-0.0.71.tgz", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" } }, "sha512-9K2k72s4n7rV9s4bX0MyjbX9iBribvKZbBJKuEmTCZfeWJXs6Yh7bGpY4eoc7UufAjvpheBqwyZCOIPBvxCv0A=="],
1259
+
1260
+ "volar-service-yaml": ["[email protected]", "https://registry.npmmirror.com/volar-service-yaml/-/volar-service-yaml-0.0.71.tgz", { "dependencies": { "vscode-uri": "^3.0.8", "yaml-language-server": "~1.23.0" }, "peerDependencies": { "@volar/language-service": "~2.4.0" } }, "sha512-qYGWGuVpUTnZGu5P/CR4KLK4aIR8RrcVnmfZ2eRcj9q/I8VZCoC5yy9FtEvfNvnDp4MU17yhdJcvpQPIqhJS2Q=="],
1261
+
1262
+ "vscode-css-languageservice": ["[email protected]", "https://registry.npmmirror.com/vscode-css-languageservice/-/vscode-css-languageservice-6.3.10.tgz", { "dependencies": { "@vscode/l10n": "^0.0.18", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "3.17.5", "vscode-uri": "^3.1.0" } }, "sha512-eq5N9Er3fC4vA9zd9EFhyBG90wtCCuXgRSpAndaOgXMh1Wgep5lBgRIeDgjZBW9pa+332yC9+49cZMW8jcL3MA=="],
1263
+
1264
+ "vscode-html-languageservice": ["[email protected]", "https://registry.npmmirror.com/vscode-html-languageservice/-/vscode-html-languageservice-5.6.2.tgz", { "dependencies": { "@vscode/l10n": "^0.0.18", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "^3.17.5", "vscode-uri": "^3.1.0" } }, "sha512-ulCrSnFnfQ16YzvwnYUgEbUEl/ZG7u2eV27YhvLObSHKkb8fw1Z9cgsnUwjTEeDIdJDoTDTDpxuhQwoenoLNMg=="],
1265
+
1266
+ "vscode-json-languageservice": ["[email protected]", "https://registry.npmmirror.com/vscode-json-languageservice/-/vscode-json-languageservice-4.1.8.tgz", { "dependencies": { "jsonc-parser": "^3.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-nls": "^5.0.0", "vscode-uri": "^3.0.2" } }, "sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg=="],
1267
+
1268
+ "vscode-jsonrpc": ["[email protected]", "https://registry.npmmirror.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.1.tgz", {}, "sha512-rfuA6T75H6m5EkbhtEPzre9pT0HPcDI2MMy4+nPFIBks5J8JBAUHD4tRYSgaBOijIEC7SRkC1kKyXTLqbmh9jw=="],
1269
+
1270
+ "vscode-languageserver": ["[email protected]", "https://registry.npmmirror.com/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="],
1271
+
1272
+ "vscode-languageserver-protocol": ["[email protected]", "https://registry.npmmirror.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.18.2.tgz", { "dependencies": { "vscode-jsonrpc": "9.0.1", "vscode-languageserver-types": "3.18.0" } }, "sha512-XRyDbT0Pp3sSNti3JmxVEUMySWCSi1hhM+/KUlCy1hV1zmrqpM1OwO12EAki8blhmLuIMpaJrYbo0OzGVfK2Qg=="],
1273
+
1274
+ "vscode-languageserver-textdocument": ["[email protected]", "https://registry.npmmirror.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="],
1275
+
1276
+ "vscode-languageserver-types": ["[email protected]", "https://registry.npmmirror.com/vscode-languageserver-types/-/vscode-languageserver-types-3.18.0.tgz", {}, "sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g=="],
1277
+
1278
+ "vscode-nls": ["[email protected]", "https://registry.npmmirror.com/vscode-nls/-/vscode-nls-5.2.0.tgz", {}, "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng=="],
1279
+
1280
+ "vscode-uri": ["[email protected]", "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
1281
+
1282
+ "web-namespaces": ["[email protected]", "https://registry.npmmirror.com/web-namespaces/-/web-namespaces-2.0.1.tgz", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
1283
+
1284
+ "web-streams-polyfill": ["[email protected]", "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
1285
+
1286
+ "which-pm": ["[email protected]", "https://registry.npmmirror.com/which-pm/-/which-pm-3.0.1.tgz", { "dependencies": { "load-yaml-file": "^0.2.0" } }, "sha512-v2JrMq0waAI4ju1xU5x3blsxBBMgdgZve580iYMN5frDaLGjbA24fok7wKCsya8KLVO19Ju4XDc5+zTZCJkQfg=="],
1287
+
1288
+ "which-pm-runs": ["[email protected]", "https://registry.npmmirror.com/which-pm-runs/-/which-pm-runs-1.1.0.tgz", {}, "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA=="],
1289
+
1290
+ "widest-line": ["[email protected]", "https://registry.npmmirror.com/widest-line/-/widest-line-5.0.0.tgz", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="],
1291
+
1292
+ "wrap-ansi": ["[email protected]", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
1293
+
1294
+ "ws": ["[email protected]", "https://registry.npmmirror.com/ws/-/ws-8.21.3.tgz", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="],
1295
+
1296
+ "xxhash-wasm": ["[email protected]", "https://registry.npmmirror.com/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="],
1297
+
1298
+ "y18n": ["[email protected]", "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
1299
+
1300
+ "yallist": ["[email protected]", "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
1301
+
1302
+ "yaml": ["[email protected]", "https://registry.npmmirror.com/yaml/-/yaml-2.9.0.tgz", { "bin": "bin.mjs" }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
1303
+
1304
+ "yaml-language-server": ["[email protected]", "https://registry.npmmirror.com/yaml-language-server/-/yaml-language-server-1.23.0.tgz", { "dependencies": { "@vscode/l10n": "^0.0.18", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-i18n": "^4.2.0", "prettier": "^3.8.1", "request-light": "^0.5.7", "vscode-json-languageservice": "4.1.8", "vscode-languageserver": "^9.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-uri": "^3.0.2", "yaml": "2.8.3" }, "bin": "bin/yaml-language-server" }, "sha512-3qVyCOexLCWw06PQa5kRPwvMWMZ/eZeCRWUvgD6a0OkqL/4iCnxy2WumbWifa937Uo5xhyWJ0uxlU39ljhNh7A=="],
1305
+
1306
+ "yargs": ["[email protected]", "https://registry.npmmirror.com/yargs/-/yargs-18.1.0.tgz", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^8.2.1", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg=="],
1307
+
1308
+ "yargs-parser": ["[email protected]", "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
1309
+
1310
+ "yocto-queue": ["[email protected]", "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-1.2.2.tgz", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="],
1311
+
1312
+ "zod": ["[email protected]", "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
1313
+
1314
+ "zod-to-json-schema": ["[email protected]", "https://registry.npmmirror.com/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
1315
+
1316
+ "zod-to-ts": ["[email protected]", "https://registry.npmmirror.com/zod-to-ts/-/zod-to-ts-1.2.0.tgz", { "peerDependencies": { "typescript": "^4.9.4 || ^5.0.2", "zod": "^3" } }, "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA=="],
1317
+
1318
+ "zwitch": ["[email protected]", "https://registry.npmmirror.com/zwitch/-/zwitch-2.0.4.tgz", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
1319
+
1320
+ "@antfu/install-pkg/tinyexec": ["[email protected]", "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.3.0.tgz", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="],
1321
+
1322
+ "@astrojs/check/chokidar": ["[email protected]", "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
1323
+
1324
+ "@babel/core/semver": ["[email protected]", "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
1325
+
1326
+ "@babel/helper-compilation-targets/semver": ["[email protected]", "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
1327
+
1328
+ "@esbuild-kit/core-utils/esbuild": ["[email protected]", "https://registry.npmmirror.com/esbuild/-/esbuild-0.18.20.tgz", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": "bin/esbuild" }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
1329
+
1330
+ "@rollup/pluginutils/estree-walker": ["[email protected]", "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
1331
+
1332
+ "@rollup/pluginutils/picomatch": ["[email protected]", "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
1333
+
1334
+ "ansi-align/string-width": ["[email protected]", "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
1335
+
1336
+ "astro/esbuild": ["[email protected]", "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": "bin/esbuild" }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
1337
+
1338
+ "boxen/string-width": ["[email protected]", "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
1339
+
1340
+ "chokidar/glob-parent": ["[email protected]", "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
1341
+
1342
+ "cliui/string-width": ["[email protected]", "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
1343
+
1344
+ "csso/css-tree": ["[email protected]", "https://registry.npmmirror.com/css-tree/-/css-tree-2.2.1.tgz", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
1345
+
1346
+ "dom-serializer/entities": ["[email protected]", "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
1347
+
1348
+ "fast-glob/glob-parent": ["[email protected]", "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
1349
+
1350
+ "fdir/picomatch": ["[email protected]", "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
1351
+
1352
+ "gray-matter/js-yaml": ["[email protected]", "https://registry.npmmirror.com/js-yaml/-/js-yaml-3.15.1.tgz", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": "bin/js-yaml.js" }, "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag=="],
1353
+
1354
+ "load-yaml-file/js-yaml": ["[email protected]", "https://registry.npmmirror.com/js-yaml/-/js-yaml-3.15.1.tgz", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": "bin/js-yaml.js" }, "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag=="],
1355
+
1356
+ "load-yaml-file/pify": ["[email protected]", "https://registry.npmmirror.com/pify/-/pify-4.0.1.tgz", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="],
1357
+
1358
+ "log-symbols/is-unicode-supported": ["[email protected]", "https://registry.npmmirror.com/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="],
1359
+
1360
+ "ora/string-width": ["[email protected]", "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
1361
+
1362
+ "p-locate/p-limit": ["[email protected]", "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
1363
+
1364
+ "postcss/nanoid": ["[email protected]", "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.18.tgz", { "bin": "bin/nanoid.cjs" }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="],
1365
+
1366
+ "prompts/kleur": ["[email protected]", "https://registry.npmmirror.com/kleur/-/kleur-3.0.3.tgz", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
1367
+
1368
+ "send/debug": ["[email protected]", "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
1369
+
1370
+ "sharp/detect-libc": ["[email protected]", "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
1371
+
1372
+ "svgo/commander": ["[email protected]", "https://registry.npmmirror.com/commander/-/commander-11.1.0.tgz", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
1373
+
1374
+ "tinyglobby/picomatch": ["[email protected]", "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
1375
+
1376
+ "tsx/esbuild": ["[email protected]", "https://registry.npmmirror.com/esbuild/-/esbuild-0.28.2.tgz", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": "bin/esbuild" }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="],
1377
+
1378
+ "vite/esbuild": ["[email protected]", "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": "bin/esbuild" }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
1379
+
1380
+ "vscode-css-languageservice/vscode-languageserver-types": ["[email protected]", "https://registry.npmmirror.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="],
1381
+
1382
+ "vscode-json-languageservice/jsonc-parser": ["[email protected]", "https://registry.npmmirror.com/jsonc-parser/-/jsonc-parser-3.3.1.tgz", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
1383
+
1384
+ "vscode-languageserver/vscode-languageserver-protocol": ["[email protected]", "https://registry.npmmirror.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="],
1385
+
1386
+ "widest-line/string-width": ["[email protected]", "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
1387
+
1388
+ "wrap-ansi/string-width": ["[email protected]", "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
1389
+
1390
+ "yaml-language-server/request-light": ["[email protected]", "https://registry.npmmirror.com/request-light/-/request-light-0.5.8.tgz", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="],
1391
+
1392
+ "yaml-language-server/yaml": ["[email protected]", "https://registry.npmmirror.com/yaml/-/yaml-2.8.3.tgz", { "bin": "bin.mjs" }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="],
1393
+
1394
+ "yargs/yargs-parser": ["[email protected]", "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-22.0.0.tgz", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
1395
+
1396
+ "@astrojs/check/chokidar/readdirp": ["[email protected]", "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
1397
+
1398
+ "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
1399
+
1400
+ "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
1401
+
1402
+ "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="],
1403
+
1404
+ "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="],
1405
+
1406
+ "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="],
1407
+
1408
+ "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="],
1409
+
1410
+ "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="],
1411
+
1412
+ "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="],
1413
+
1414
+ "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="],
1415
+
1416
+ "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="],
1417
+
1418
+ "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="],
1419
+
1420
+ "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="],
1421
+
1422
+ "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="],
1423
+
1424
+ "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="],
1425
+
1426
+ "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="],
1427
+
1428
+ "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="],
1429
+
1430
+ "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="],
1431
+
1432
+ "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="],
1433
+
1434
+ "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="],
1435
+
1436
+ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="],
1437
+
1438
+ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="],
1439
+
1440
+ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
1441
+
1442
+ "ansi-align/string-width/emoji-regex": ["[email protected]", "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
1443
+
1444
+ "ansi-align/string-width/strip-ansi": ["[email protected]", "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
1445
+
1446
+ "astro/esbuild/@esbuild/aix-ppc64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
1447
+
1448
+ "astro/esbuild/@esbuild/android-arm": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
1449
+
1450
+ "astro/esbuild/@esbuild/android-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="],
1451
+
1452
+ "astro/esbuild/@esbuild/android-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="],
1453
+
1454
+ "astro/esbuild/@esbuild/darwin-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="],
1455
+
1456
+ "astro/esbuild/@esbuild/darwin-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="],
1457
+
1458
+ "astro/esbuild/@esbuild/freebsd-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="],
1459
+
1460
+ "astro/esbuild/@esbuild/freebsd-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="],
1461
+
1462
+ "astro/esbuild/@esbuild/linux-arm": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="],
1463
+
1464
+ "astro/esbuild/@esbuild/linux-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="],
1465
+
1466
+ "astro/esbuild/@esbuild/linux-ia32": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="],
1467
+
1468
+ "astro/esbuild/@esbuild/linux-loong64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="],
1469
+
1470
+ "astro/esbuild/@esbuild/linux-mips64el": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="],
1471
+
1472
+ "astro/esbuild/@esbuild/linux-ppc64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="],
1473
+
1474
+ "astro/esbuild/@esbuild/linux-riscv64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="],
1475
+
1476
+ "astro/esbuild/@esbuild/linux-s390x": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="],
1477
+
1478
+ "astro/esbuild/@esbuild/linux-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="],
1479
+
1480
+ "astro/esbuild/@esbuild/netbsd-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="],
1481
+
1482
+ "astro/esbuild/@esbuild/openbsd-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="],
1483
+
1484
+ "astro/esbuild/@esbuild/sunos-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="],
1485
+
1486
+ "astro/esbuild/@esbuild/win32-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="],
1487
+
1488
+ "astro/esbuild/@esbuild/win32-ia32": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="],
1489
+
1490
+ "astro/esbuild/@esbuild/win32-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
1491
+
1492
+ "csso/css-tree/mdn-data": ["[email protected]", "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.0.28.tgz", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
1493
+
1494
+ "gray-matter/js-yaml/argparse": ["[email protected]", "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
1495
+
1496
+ "load-yaml-file/js-yaml/argparse": ["[email protected]", "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
1497
+
1498
+ "send/debug/ms": ["[email protected]", "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
1499
+
1500
+ "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="],
1501
+
1502
+ "tsx/esbuild/@esbuild/android-arm": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.28.2.tgz", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="],
1503
+
1504
+ "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="],
1505
+
1506
+ "tsx/esbuild/@esbuild/android-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.28.2.tgz", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="],
1507
+
1508
+ "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="],
1509
+
1510
+ "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="],
1511
+
1512
+ "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="],
1513
+
1514
+ "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="],
1515
+
1516
+ "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="],
1517
+
1518
+ "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="],
1519
+
1520
+ "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="],
1521
+
1522
+ "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="],
1523
+
1524
+ "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="],
1525
+
1526
+ "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="],
1527
+
1528
+ "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="],
1529
+
1530
+ "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="],
1531
+
1532
+ "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="],
1533
+
1534
+ "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="],
1535
+
1536
+ "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="],
1537
+
1538
+ "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="],
1539
+
1540
+ "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="],
1541
+
1542
+ "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="],
1543
+
1544
+ "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="],
1545
+
1546
+ "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="],
1547
+
1548
+ "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="],
1549
+
1550
+ "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="],
1551
+
1552
+ "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
1553
+
1554
+ "vite/esbuild/@esbuild/android-arm": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
1555
+
1556
+ "vite/esbuild/@esbuild/android-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="],
1557
+
1558
+ "vite/esbuild/@esbuild/android-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="],
1559
+
1560
+ "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="],
1561
+
1562
+ "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="],
1563
+
1564
+ "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="],
1565
+
1566
+ "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="],
1567
+
1568
+ "vite/esbuild/@esbuild/linux-arm": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="],
1569
+
1570
+ "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="],
1571
+
1572
+ "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="],
1573
+
1574
+ "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="],
1575
+
1576
+ "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="],
1577
+
1578
+ "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="],
1579
+
1580
+ "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="],
1581
+
1582
+ "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="],
1583
+
1584
+ "vite/esbuild/@esbuild/linux-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="],
1585
+
1586
+ "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="],
1587
+
1588
+ "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="],
1589
+
1590
+ "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="],
1591
+
1592
+ "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="],
1593
+
1594
+ "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="],
1595
+
1596
+ "vite/esbuild/@esbuild/win32-x64": ["@esbuild/[email protected]", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
1597
+
1598
+ "vscode-languageserver/vscode-languageserver-protocol/vscode-jsonrpc": ["[email protected]", "https://registry.npmmirror.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
1599
+
1600
+ "vscode-languageserver/vscode-languageserver-protocol/vscode-languageserver-types": ["[email protected]", "https://registry.npmmirror.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="],
1601
+
1602
+ "ansi-align/string-width/strip-ansi/ansi-regex": ["[email protected]", "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
1603
+ }
1604
+}
0
1605
▼community/drizzle.config.ts
+21-0
1
+import { defineConfig } from "drizzle-kit";
2
+import "dotenv/config";
3
+
4
+const url = (process.env.TURSO_DATABASE_URL || "").trim();
5
+if (!url) {
6
+ throw new Error(
7
+ "TURSO_DATABASE_URL is required for drizzle-kit. Set it in .env or as an env var.\n" +
8
+ "Create a database at https://turso.tech/dashboard and get the URL from there.\n" +
9
+ "Example: TURSO_DATABASE_URL=libsql://your-db-name-your-org.turso.io"
10
+ );
11
+}
12
+
13
+export default defineConfig({
14
+ schema: "./src/lib/db/schema.ts",
15
+ out: "./drizzle",
16
+ dialect: "turso",
17
+ dbCredentials: {
18
+ url,
19
+ authToken: (process.env.TURSO_AUTH_TOKEN || "").trim() || undefined,
20
+ },
21
+});
0
22
▼community/package.json
+64-0
1
+{
2
+ "name": "opencodehub-community",
3
+ "version": "1.0.0",
4
+ "description": "OpenCodeHub Community Hub — aggregate public repos & profiles from self-hosted instances",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "astro dev",
8
+ "build": "astro build",
9
+ "preview": "astro preview",
10
+ "start": "node --env-file=.env ./dist/server/entry.mjs",
11
+ "db:generate": "drizzle-kit generate",
12
+ "db:migrate": "drizzle-kit migrate",
13
+ "db:push": "drizzle-kit push",
14
+ "db:studio": "drizzle-kit studio",
15
+ "sync": "bun scripts/cron-sync.ts",
16
+ "lint": "astro check",
17
+ "typecheck": "tsc --noEmit"
18
+ },
19
+ "dependencies": {
20
+ "@astrojs/check": "^0.9.6",
21
+ "@astrojs/node": "^8.3.0",
22
+ "@astrojs/react": "^3.6.0",
23
+ "@astrojs/tailwind": "^5.1.2",
24
+ "@libsql/client": "^0.15.15",
25
+ "@radix-ui/react-avatar": "^1.1.0",
26
+ "@radix-ui/react-dialog": "^1.1.1",
27
+ "@radix-ui/react-dropdown-menu": "^2.1.1",
28
+ "@radix-ui/react-label": "^2.1.0",
29
+ "@radix-ui/react-slot": "^1.1.0",
30
+ "@radix-ui/react-toast": "^1.2.1",
31
+ "@radix-ui/react-tabs": "^1.1.0",
32
+ "@upstash/redis": "^1.36.0",
33
+ "astro": "^4.11.0",
34
+ "astro-icon": "^1.1.5",
35
+ "bcryptjs": "^2.4.3",
36
+ "class-variance-authority": "^0.7.0",
37
+ "clsx": "^2.1.1",
38
+ "dotenv": "^16.4.5",
39
+ "drizzle-orm": "^0.45.2",
40
+ "jose": "^5.6.3",
41
+ "lucide-react": "^0.400.0",
42
+ "nanoid": "^5.0.7",
43
+ "react": "^18.3.1",
44
+ "react-dom": "^18.3.1",
45
+ "sonner": "^2.0.7",
46
+ "tailwind-merge": "^2.6.0",
47
+ "tailwindcss-animate": "^1.0.7",
48
+ "zod": "^3.23.8"
49
+ },
50
+ "devDependencies": {
51
+ "@types/bcryptjs": "^2.4.6",
52
+ "@types/node": "^20.14.9",
53
+ "@types/react": "^18.3.3",
54
+ "@types/react-dom": "^18.3.0",
55
+ "autoprefixer": "^10.4.19",
56
+ "drizzle-kit": "^0.31.8",
57
+ "postcss": "^8.5.2",
58
+ "tailwindcss": "^3.4.4",
59
+ "typescript": "^5.9.3"
60
+ },
61
+ "engines": {
62
+ "node": ">=18.0.0"
63
+ }
64
+}
0
65
▼community/public/logo.svg
+9-0
1
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="45 45 210 210" fill="currentColor">
2
+ <style>
3
+ :root { color: #1f2328; }
4
+ @media (prefers-color-scheme: dark) {
5
+ :root { color: #f0f6fc; }
6
+ }
7
+ </style>
8
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M184.27588,89.68521c2.96924,0.22368 3.29297,2.88721 3.75439,5.34082c1.19385,6.34468 0.82178,12.62754 0.00293,18.98174c0.34131,0.62461 0.66797,1.25728 0.97998,1.89756c5.40088,11.37407 2.61914,24.01758 -7.58203,31.66011c-2.17236,1.62891 -4.00049,2.51074 -6.4248,3.66357c0.82178,0.76904 1.60254,1.5293 2.35693,2.37012c10.20264,11.34814 10.51318,31.02539 1.01953,42.86865c-0.7749,0.9668 -1.70801,1.875 -2.36572,2.91064c-0.18896,1.81934 -0.08496,5.79053 -0.08496,7.70508l0.00732,14.01855c0.01172,4.06201 0.68115,7.9541 -2.32178,11.17236c-1.55713,1.69189 -3.74414,2.66602 -6.04395,2.69238c-2.2251,0.0542 -4.37256,-0.81006 -5.93994,-2.3877c-2.77588,-2.78174 -2.42139,-5.74805 -2.42285,-9.37646c-0.00293,-2.15186 0.00293,-4.32129 0.00293,-6.47314l0.00146,-24.48047c6.01758,-2.28369 9.25928,-8.7041 7.61719,-14.91797c-0.83789,-3.1875 -2.90332,-5.91357 -5.74365,-7.5835c-2.87109,-1.6626 -6.28418,-2.12109 -9.49365,-1.27441c-3.25635,0.86279 -6.023,3.00879 -7.66934,5.94727c-1.6771,2.98242 -2.0748,6.51709 -1.10244,9.79834c1.07827,3.66211 3.30176,6.06738 6.59795,7.8457v31.3418l-0.00146,8.86523c-0.00293,1.69775 -0.05127,3.50098 0.06885,5.18408c0.07031,0.98438 0.24023,1.74902 0.93457,2.45801c1.06494,1.08838 2.63818,1.26123 4.08838,1.25244c5.39648,-0.03516 11.21777,-0.63721 16.4458,-2.0083c11.50488,-3.01465 25.31982,-11.12842 33.73096,-19.5498c-1.04004,-3.65479 -1.04297,-6.17139 0.27979,-9.73682c0.11426,-0.28418 0.23584,-0.56396 0.36768,-0.83936c1.53955,-3.31641 4.34473,-5.87695 7.78711,-7.10889c3.49512,-1.24365 7.1792,-1.10596 10.5249,0.52588c3.38965,1.6582 5.97949,4.59814 7.19385,8.16943c1.18945,3.55518 0.89648,7.43848 -0.81152,10.77539c-1.72119,3.35449 -4.70801,5.88574 -8.2998,7.03418c-3.99609,1.25098 -7.6333,0.60791 -11.26465,-1.28613c-11.9165,11.17676 -26.95312,19.90283 -43.24658,22.32861c-6.1377,0.91406 -18.28271,1.79883 -24.2209,0.28711c-1.4543,-0.36035 -2.82349,-1.00342 -4.0292,-1.89258c-1.92026,-1.43994 -3.30073,-3.4834 -3.91978,-5.80225c-0.80962,-2.91797 -0.59106,-7.26416 -0.58989,-10.39453l0.0126,-20.7627c0.00176,-2.729 0.26045,-7.91602 -0.20142,-10.47656c-0.9293,-1.44434 -2.89556,-3.36035 -4.03506,-4.95996c-7.36787,-10.34326 -8.05313,-24.92871 -1.97593,-36.03662c1.89521,-3.46436 3.96914,-5.61768 6.89136,-8.27051c-1.10273,-0.60498 -2.32017,-1.15137 -3.42407,-1.75488c-10.07725,-5.50151 -15.41455,-16.65469 -12.49043,-27.89648c0.75337,-2.89644 1.79604,-5.09502 3.26177,-7.72251c-0.23101,-1.61455 -0.4084,-3.23628 -0.53218,-4.86255c-0.26484,-3.68013 -0.14707,-15.76128 2.28369,-18.38569c0.49512,-0.53452 1.12632,-0.72598 1.84219,-0.73696c4.777,-0.07251 14.12007,8.55615 17.44307,11.97817c6.83555,-0.78853 16.69834,-0.79482 23.51572,0.04937c3.84082,-4.00254 11.6543,-11.28779 17.2251,-12.14487z M216.82617,208.70947c3.49512,-0.50391 6.74268,1.9043 7.27588,5.39355c0.5332,3.49072 -1.8457,6.75879 -5.33057,7.32275c-3.52881,0.57129 -6.84521,-1.84424 -7.38574,-5.37744c-0.54053,-3.5332 1.90283,-6.8291 5.44043,-7.33887z M150.01025,49.42808c8.76855,-0.53481 18.6665,0.87524 27.06592,3.29912c20.78613,5.99868 38.0083,20.7375 48.39844,39.57656c11.02441,20.03057 13.20557,42.40825 6.84668,64.47847c-4.68457,16.26416 -15.27686,33.76758 -29.49902,43.79297c-0.8291,0.58447 -2.42578,0.64014 -3.39551,0.33105c-1.20264,-0.38965 -2.1958,-1.24951 -2.75391,-2.38477c-0.78223,-1.54248 -0.68701,-3.38379 0.24756,-4.83838c0.77344,-1.19238 2.30859,-1.99658 3.36621,-2.96631c1.39453,-1.27734 2.7832,-2.58984 4.08691,-3.96094c8.34229,-8.7583 14.54004,-19.33154 18.10547,-30.89062c6.12744,-19.20322 4.20703,-40.06919 -5.31885,-57.83232c-13.10742,-24.82339 -38.29687,-39.67676 -66.35449,-38.24443c-11.62852,0.5937 -20.97378,3.44312 -31.22754,8.85864c-0.09214,0.21592 0.06855,2.36089 0.08599,2.76357c0.10093,2.32456 -0.25884,4.14609 -1.20132,6.28271c-1.61675,3.70122 -4.65234,6.5981 -8.4252,8.03994c-4.34063,1.67285 -7.91528,1.0749 -12.02065,-0.72729c-1.39482,1.60225 -2.82202,3.25049 -4.10435,4.94326c-9.65625,12.99712 -14.91196,28.73716 -15.0019,44.92852c0.00176,4.17891 0.44326,7.88203 1.01514,11.99033c2.62295,0.16113 4.83779,0.89355 7.03564,2.35986c3.29355,2.22656 5.54854,5.68799 6.254,9.60059c0.80186,4.28027 -0.46406,7.95996 -2.85498,11.45801c6.1188,10.2832 12.25576,17.34961 22.19165,24.18604c2.72197,1.86182 5.57842,3.51855 8.54546,4.95703c1.91646,0.92285 5.19565,1.96143 6.36152,3.69434c0.70986,1.0708 0.95127,2.38623 0.66841,3.64014c-0.63237,2.74805 -3.42524,4.40332 -6.0498,3.42188c-16.91338,-6.33398 -30.71909,-19.39453 -39.48354,-34.77979c-1.91895,0.38965 -3.55415,0.55664 -5.51953,0.2959c-3.75059,-0.50244 -7.1439,-2.48584 -9.42217,-5.50635c-2.37363,-3.13184 -3.39595,-7.08252 -2.83887,-10.97168c0.58975,-4.17334 2.75815,-7.26123 6.04922,-9.76172c-0.84521,-4.86138 -1.40742,-9.20464 -1.51685,-14.15186c-0.4582,-20.73208 7.9248,-42.52515 22.04824,-57.59678c-0.52134,-1.40479 -0.85063,-2.36895 -1.07036,-3.8603c-0.57583,-3.9999 0.44736,-8.06558 2.8478,-11.31636c2.4063,-3.16348 5.97246,-5.23945 9.91157,-5.76943c4.11064,-0.54932 7.88833,0.62358 11.15728,3.0999c12.08159,-6.54038 21.99536,-9.5105 35.76973,-10.4395z M104.33159,64.69878c3.83057,-0.32915 7.21245,2.48599 7.58335,6.31274c0.37104,3.82676 -2.40718,7.23911 -6.22969,7.65176c-2.51001,0.271 -4.97197,-0.82749 -6.44678,-2.87651c-1.4748,-2.04902 -1.73496,-4.73247 -0.68115,-7.02656c1.05381,-2.29424 3.25898,-3.84521 5.77427,-4.06143z M77.86846,154.6333c3.67031,-0.67822 7.19575,1.74463 7.87866,5.41406c0.68291,3.66943 -1.73511,7.19824 -5.40337,7.88672c-3.67588,0.68994 -7.21377,-1.73437 -7.898,-5.41113c-0.68437,-3.67676 1.74478,-7.21143 5.42271,-7.88965z M153.91992,174.95801c3.0835,-0.47461 5.96777,1.64209 6.43945,4.72559c0.47021,3.0835 -1.64941,5.96484 -4.73437,6.43359c-3.0791,0.46582 -5.95459,-1.64941 -6.4248,-4.72852c-0.47021,-3.07764 1.64209,-5.95605 4.71973,-6.43066z" />
9
+</svg>
0
10
▼community/scripts/cron-sync.ts
+71-0
1
+import "dotenv/config";
2
+import { getDb } from "../src/lib/db/index.js";
3
+import { instances, cachedRepos, cachedUsers } from "../src/lib/db/schema.js";
4
+import { fetchAllPublicRepos } from "../src/lib/instance-client.js";
5
+import { eq } from "drizzle-orm";
6
+import { nanoid } from "nanoid";
7
+
8
+async function runSync() {
9
+ console.log("[Sync] Starting global instance synchronization...");
10
+ const db = getDb() as any;
11
+
12
+ const allInstances = await db.query.instances.findMany();
13
+ console.log(`[Sync] Found ${allInstances.length} instances to sync.`);
14
+
15
+ for (const instance of allInstances) {
16
+ console.log(`[Sync] Syncing instance: ${instance.name} (${instance.url})`);
17
+ try {
18
+ const repos = await fetchAllPublicRepos(instance.url);
19
+ const ownersMap = new Map();
20
+
21
+ for (const r of repos) {
22
+ if (r.owner && !ownersMap.has(r.owner.username)) {
23
+ ownersMap.set(r.owner.username, r.owner);
24
+ }
25
+
26
+ const existing = await db.query.cachedRepos.findFirst({ where: eq(cachedRepos.remoteId, r.id) }).catch(()=>null);
27
+ if (existing) {
28
+ await db.update(cachedRepos).set({
29
+ fullName: r.fullName, name: r.name, description: r.description, language: r.language,
30
+ topics: JSON.stringify(r.topics), starCount: r.starCount, forkCount: r.forkCount,
31
+ httpCloneUrl: r.httpCloneUrl, updatedAt: r.updatedAt
32
+ }).where(eq(cachedRepos.id, existing.id));
33
+ } else {
34
+ await db.insert(cachedRepos).values({
35
+ id: nanoid(), instanceId: instance.id, remoteId: r.id, fullName: r.fullName, name: r.name,
36
+ ownerUsername: r.owner.username, ownerDisplayName: r.owner.displayName, ownerAvatarUrl: r.owner.avatarUrl,
37
+ description: r.description, visibility: r.visibility, language: r.language, topics: JSON.stringify(r.topics),
38
+ starCount: r.starCount, forkCount: r.forkCount, httpCloneUrl: r.httpCloneUrl, updatedAt: r.updatedAt
39
+ });
40
+ }
41
+ }
42
+
43
+ for (const owner of ownersMap.values()) {
44
+ const existingUser = await db.query.cachedUsers.findFirst({ where: eq(cachedUsers.username, owner.username) }).catch(() => null);
45
+ if (existingUser) {
46
+ await db.update(cachedUsers).set({
47
+ displayName: owner.displayName, avatarUrl: owner.avatarUrl,
48
+ }).where(eq(cachedUsers.id, existingUser.id));
49
+ } else {
50
+ await db.insert(cachedUsers).values({
51
+ id: nanoid(), instanceId: instance.id, remoteId: owner.id || owner.username,
52
+ username: owner.username, displayName: owner.displayName, avatarUrl: owner.avatarUrl,
53
+ });
54
+ }
55
+ }
56
+
57
+ await db.update(instances).set({
58
+ repoCount: repos.length, status: "online", lastSyncAt: new Date().toISOString()
59
+ }).where(eq(instances.id, instance.id));
60
+
61
+ console.log(`[Sync] ✓ Success: ${instance.name} (${repos.length} repos updated)`);
62
+ } catch (e: any) {
63
+ console.error(`[Sync] ✗ Failed: ${instance.name} - ${e.message}`);
64
+ await db.update(instances).set({ status: "error" }).where(eq(instances.id, instance.id)).catch(()=>{});
65
+ }
66
+ }
67
+ console.log("[Sync] Global synchronization complete.");
68
+ process.exit(0);
69
+}
70
+
71
+runSync();
0
72
▼community/src/components/InstanceCard.tsx
+31-0
1
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
2
+import { Badge } from "@/components/ui/badge";
3
+import { Button } from "@/components/ui/button";
4
+import { Globe, Box, Users, ExternalLink } from "lucide-react";
5
+export function InstanceCard({ instance }: { instance: any }) {
6
+ return (
7
+ <Card className="hover:shadow-lg transition-all hover:-translate-y-0.5">
8
+ <CardHeader className="pb-3">
9
+ <div className="flex items-center gap-3">
10
+ <div className="h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center"><Globe className="h-5 w-5 text-primary" /></div>
11
+ <div className="flex-1 min-w-0">
12
+ <CardTitle className="text-base truncate">{instance.name}</CardTitle>
13
+ <CardDescription className="truncate text-xs">{instance.siteUrl}</CardDescription>
14
+ </div>
15
+ <Badge variant={instance.status === "online" ? "default" : "secondary"} className="capitalize">{instance.status || "pending"}</Badge>
16
+ </div>
17
+ </CardHeader>
18
+ <CardContent className="space-y-3">
19
+ <div className="flex gap-4 text-sm">
20
+ <span className="flex items-center gap-1 text-muted-foreground"><Box className="h-4 w-4" />{instance.repoCount ?? 0} repos</span>
21
+ <span className="flex items-center gap-1 text-muted-foreground"><Users className="h-4 w-4" />{instance.userCount ?? 0} users</span>
22
+ </div>
23
+ {instance.version && <div className="text-xs text-muted-foreground">v{instance.version} · {instance.capabilities?.join(", ")}</div>}
24
+ <div className="flex gap-2">
25
+ <Button size="sm" variant="outline" className="flex-1" onClick={()=>window.open(instance.siteUrl, "_blank")}><ExternalLink className="h-4 w-4 mr-1" />Visit</Button>
26
+ <Button size="sm" className="flex-1" onClick={()=>window.location.href=`/instances/${instance.id}`}>Explore</Button>
27
+ </div>
28
+ </CardContent>
29
+ </Card>
30
+ );
31
+}
0
32
▼community/src/components/ProfileCard.tsx
+26-0
1
+import { Card, CardContent } from "@/components/ui/card";
2
+import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
3
+import { Button } from "@/components/ui/button";
4
+import { MapPin, Link as LinkIcon, Building } from "lucide-react";
5
+export function ProfileCard({ profile }: { profile: any }) {
6
+ return (
7
+ <Card className="hover:shadow-md transition-shadow">
8
+ <CardContent className="p-5 flex gap-4">
9
+ <Avatar className="h-14 w-14">
10
+ <AvatarImage src={profile.avatarUrl} />
11
+ <AvatarFallback>{profile.username?.[0]?.toUpperCase()}</AvatarFallback>
12
+ </Avatar>
13
+ <div className="flex-1 min-w-0">
14
+ <div className="font-semibold truncate">{profile.displayName || profile.username} <span className="text-muted-foreground font-normal">@{profile.username}</span></div>
15
+ <p className="text-sm text-muted-foreground line-clamp-2 mt-1">{profile.bio || "No bio"}</p>
16
+ <div className="flex flex-wrap gap-3 mt-2 text-xs text-muted-foreground">
17
+ {profile.location && <span className="flex items-center gap-1"><MapPin className="h-3 w-3" />{profile.location}</span>}
18
+ {profile.company && <span className="flex items-center gap-1"><Building className="h-3 w-3" />{profile.company}</span>}
19
+ {profile.website && <a href={profile.website} target="_blank" className="flex items-center gap-1 hover:text-primary"><LinkIcon className="h-3 w-3" />Website</a>}
20
+ </div>
21
+ </div>
22
+ <Button size="sm" variant="outline">Follow</Button>
23
+ </CardContent>
24
+ </Card>
25
+ );
26
+}
0
27
▼community/src/components/RepoCard.tsx
+29-0
1
+import { Card, CardContent } from "@/components/ui/card";
2
+import { Badge } from "@/components/ui/badge";
3
+import { Button } from "@/components/ui/button";
4
+import { Star, GitFork, Eye, BookOpen } from "lucide-react";
5
+export function RepoCard({ repo, onStar }: { repo: any; onStar?: () => void }) {
6
+ return (
7
+ <Card className="hover:shadow-lg transition-shadow group">
8
+ <CardContent className="p-5">
9
+ <div className="flex items-start justify-between gap-3">
10
+ <div className="flex-1 min-w-0">
11
+ <div className="flex items-center gap-2">
12
+ <BookOpen className="h-4 w-4 text-muted-foreground flex-shrink-0" />
13
+ <a href={`/explore?repo=${encodeURIComponent(repo.fullName)}`} className="font-semibold text-primary hover:underline truncate">{repo.fullName}</a>
14
+ <Badge variant="outline" className="text-[10px] capitalize">{repo.visibility}</Badge>
15
+ </div>
16
+ <p className="text-sm text-muted-foreground mt-1 line-clamp-2">{repo.description || "No description"}</p>
17
+ <div className="flex items-center gap-3 mt-3 text-xs text-muted-foreground">
18
+ {repo.language && <span className="flex items-center gap-1"><span className="h-2.5 w-2.5 rounded-full bg-primary" />{repo.language}</span>}
19
+ <span className="flex items-center gap-1"><Star className="h-3.5 w-3.5" />{repo.starCount}</span>
20
+ <span className="flex items-center gap-1"><GitFork className="h-3.5 w-3.5" />{repo.forkCount}</span>
21
+ {repo.instanceName && <Badge variant="secondary" className="text-[10px]">{repo.instanceName}</Badge>}
22
+ </div>
23
+ </div>
24
+ <Button size="sm" variant="outline" onClick={onStar} className="flex-shrink-0"><Star className="h-4 w-4 mr-1" />Star</Button>
25
+ </div>
26
+ </CardContent>
27
+ </Card>
28
+ );
29
+}
0
30
▼community/src/components/layout/Footer.astro
+65-0
1
+---
2
+import { Github, Globe, Server, Sparkles, Heart } from "lucide-react";
3
+---
4
+<footer class="border-t border-border/40 bg-card/40 backdrop-blur-sm mt-auto">
5
+ <div class="container px-4 py-12">
6
+ <div class="grid grid-cols-1 md:grid-cols-4 gap-8 mb-10">
7
+ <!-- Brand & Mission -->
8
+ <div class="md:col-span-2 space-y-4">
9
+ <div class="flex items-center gap-2">
10
+ <div class="h-8 w-8 rounded-lg bg-gradient-to-br from-primary via-indigo-500 to-purple-600 flex items-center justify-center text-white shadow-md shadow-primary/20">
11
+ <Github className="h-4 w-4" />
12
+ </div>
13
+ <span class="font-bold text-lg tracking-tight bg-gradient-to-r from-white via-zinc-200 to-zinc-400 bg-clip-text text-transparent">
14
+ OpenCodeHub Community
15
+ </span>
16
+ </div>
17
+ <p class="text-sm text-muted-foreground max-w-md leading-relaxed">
18
+ The decentralized aggregator for self-hosted OpenCodeHub instances. Discover public repositories, connect with developers, and build across federated nodes without central gatekeepers.
19
+ </p>
20
+ <div class="flex items-center gap-2 text-xs text-muted-foreground pt-1">
21
+ <span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 font-medium">
22
+ <span class="h-1.5 w-1.5 rounded-full bg-emerald-400 animate-pulse"></span>
23
+ Federation Active
24
+ </span>
25
+ <span>·</span>
26
+ <span>Powered by Turso & Astro</span>
27
+ </div>
28
+ </div>
29
+
30
+ <!-- Navigation Links -->
31
+ <div class="space-y-3">
32
+ <h4 class="text-sm font-semibold text-foreground tracking-wider uppercase">Platform</h4>
33
+ <ul class="space-y-2 text-sm text-muted-foreground">
34
+ <li><a href="/explore" class="hover:text-primary transition-colors flex items-center gap-1.5"><Globe className="h-3.5 w-3.5" /> Explore Repos</a></li>
35
+ <li><a href="/instances" class="hover:text-primary transition-colors flex items-center gap-1.5"><Server className="h-3.5 w-3.5" /> Federated Instances</a></li>
36
+ <li><a href="/instances/submit" class="hover:text-primary transition-colors flex items-center gap-1.5"><Sparkles className="h-3.5 w-3.5" /> Submit Instance</a></li>
37
+ <li><a href="/dashboard" class="hover:text-primary transition-colors">User Dashboard</a></li>
38
+ </ul>
39
+ </div>
40
+
41
+ <!-- Resources & Docs -->
42
+ <div class="space-y-3">
43
+ <h4 class="text-sm font-semibold text-foreground tracking-wider uppercase">Open Source</h4>
44
+ <ul class="space-y-2 text-sm text-muted-foreground">
45
+ <li><a href="https://github.com/swadhinbiswas/OpencodeHub" target="_blank" rel="noreferrer" class="hover:text-primary transition-colors flex items-center gap-1.5"><Github className="h-3.5 w-3.5" /> GitHub Repo</a></li>
46
+ <li><a href="/instances/submit" class="hover:text-primary transition-colors">Self-Host Docs</a></li>
47
+ <li><a href="/api/instances" class="hover:text-primary transition-colors">Public JSON API</a></li>
48
+ <li><a href="/auth/login" class="hover:text-primary transition-colors">Sign In / Register</a></li>
49
+ </ul>
50
+ </div>
51
+ </div>
52
+
53
+ <!-- Bottom Bar -->
54
+ <div class="border-t border-border/40 pt-6 flex flex-col sm:flex-row items-center justify-between gap-4 text-xs text-muted-foreground">
55
+ <div>
56
+ © {new Date().getFullYear()} OpenCodeHub Community. Built for the federated future.
57
+ </div>
58
+ <div class="flex items-center gap-1">
59
+ <span>Crafted with</span>
60
+ <Heart className="h-3.5 w-3.5 text-rose-500 fill-rose-500" />
61
+ <span>for open source self-hosters</span>
62
+ </div>
63
+ </div>
64
+ </div>
65
+</footer>
0
66
▼community/src/components/layout/Header.tsx
+80-0
1
+import { useState, useEffect } from "react";
2
+import { Button } from "@/components/ui/button";
3
+import { Compass, Plus, LogIn, LogOut, Menu, X, Server } from "lucide-react";
4
+
5
+export function Header({ user }: { user?: any }) {
6
+ const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
7
+ const [currentPath, setCurrentPath] = useState("");
8
+
9
+ useEffect(() => {
10
+ if (typeof window !== "undefined") {
11
+ setCurrentPath(window.location.pathname);
12
+ }
13
+ }, []);
14
+
15
+ const handleLogout = async () => {
16
+ try {
17
+ await fetch("/api/auth/logout", { method: "POST" });
18
+ window.location.href = "/";
19
+ } catch (e) {
20
+ console.error("Logout failed:", e);
21
+ }
22
+ };
23
+
24
+ const navLinks = [
25
+ { href: "/explore", label: "Explore", icon: Compass },
26
+ { href: "/instances", label: "Instances", icon: Server },
27
+ ];
28
+
29
+ return (
30
+ <header className="sticky top-0 z-50 w-full border-b border-border/50 bg-background/80 backdrop-blur-xl supports-[backdrop-filter]:bg-background/60">
31
+ <div className="container flex h-16 items-center justify-between px-4 sm:px-8">
32
+ <div className="flex items-center gap-8">
33
+ <a href="/" className="flex items-center gap-2.5 group">
34
+ <img src="/logo.svg" alt="OpenCodeHub" className="h-9 w-9" />
35
+ <div className="flex flex-col">
36
+ <span className="font-bold text-base tracking-tight leading-none text-foreground flex items-center gap-1.5">
37
+ OpenCodeHub
38
+ <span className="text-[10px] uppercase font-semibold tracking-wider px-1.5 py-0.5 rounded bg-primary/10 text-primary border border-primary/20">Community</span>
39
+ </span>
40
+ <span className="text-[11px] text-muted-foreground leading-tight">Federated Hub</span>
41
+ </div>
42
+ </a>
43
+ <nav className="hidden md:flex items-center gap-1.5">
44
+ {navLinks.map(({ href, label, icon: Icon }) => {
45
+ const isActive = currentPath === href || (href !== "/" && currentPath.startsWith(href));
46
+ return (
47
+ <a key={href} href={href} className={`px-3.5 py-2 text-sm font-medium rounded-lg flex items-center gap-2 transition-all ${isActive ? "bg-primary/10 text-primary font-semibold" : "text-muted-foreground hover:text-foreground hover:bg-accent/60"}`}>
48
+ <Icon className={`h-4 w-4 ${isActive ? "text-primary" : "text-muted-foreground"}`} />
49
+ {label}
50
+ </a>
51
+ );
52
+ })}
53
+ </nav>
54
+ </div>
55
+ <div className="flex items-center gap-2">
56
+ <a href="/instances/submit"><Button size="sm" variant="outline"><Plus className="h-4 w-4 mr-1" />Submit URL</Button></a>
57
+ {user ? (
58
+ <div className="flex items-center gap-2 ml-2">
59
+ <span className="text-sm font-medium hidden sm:inline-block">@{user.username}</span>
60
+ <Button size="icon" variant="ghost" onClick={handleLogout} title="Sign out"><LogOut className="h-4 w-4 text-muted-foreground" /></Button>
61
+ </div>
62
+ ) : (
63
+ <a href="/auth/login"><Button size="sm"><LogIn className="h-4 w-4 mr-1" />Sign in</Button></a>
64
+ )}
65
+ <Button size="icon" variant="ghost" className="md:hidden" onClick={() => setMobileMenuOpen(!mobileMenuOpen)}>
66
+ {mobileMenuOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
67
+ </Button>
68
+ </div>
69
+ </div>
70
+ {mobileMenuOpen && (
71
+ <div className="md:hidden border-t bg-background/95 backdrop-blur-xl px-4 py-3 space-y-1">
72
+ {navLinks.map(({ href, label, icon: Icon }) => (
73
+ <a key={href} href={href} className="flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-md hover:bg-accent">{label}</a>
74
+ ))}
75
+ <a href="/instances/submit" className="flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-md hover:bg-accent">Submit URL</a>
76
+ </div>
77
+ )}
78
+ </header>
79
+ );
80
+}
0
81
▼community/src/components/ui/avatar.tsx
+10-0
1
+import * as React from "react";
2
+import * as AvatarPrimitive from "@radix-ui/react-avatar";
3
+import { cn } from "@/lib/utils";
4
+const Avatar = React.forwardRef<React.ElementRef<typeof AvatarPrimitive.Root>, React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>>(({ className, ...props }, ref) => <AvatarPrimitive.Root ref={ref} className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)} {...props} />);
5
+Avatar.displayName = AvatarPrimitive.Root.displayName;
6
+const AvatarImage = React.forwardRef<React.ElementRef<typeof AvatarPrimitive.Image>, React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>>(({ className, ...props }, ref) => <AvatarPrimitive.Image ref={ref} className={cn("aspect-square h-full w-full", className)} {...props} />);
7
+AvatarImage.displayName = AvatarPrimitive.Image.displayName;
8
+const AvatarFallback = React.forwardRef<React.ElementRef<typeof AvatarPrimitive.Fallback>, React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>>(({ className, ...props }, ref) => <AvatarPrimitive.Fallback ref={ref} className={cn("flex h-full w-full items-center justify-center rounded-full bg-muted", className)} {...props} />);
9
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
10
+export { Avatar, AvatarImage, AvatarFallback };
0
11
▼community/src/components/ui/badge.tsx
+10-0
1
+import * as React from "react";
2
+import { cva, type VariantProps } from "class-variance-authority";
3
+import { cn } from "@/lib/utils";
4
+const badgeVariants = cva("inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", {
5
+ variants: { variant: { default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", outline: "text-foreground" } },
6
+ defaultVariants: { variant: "default" },
7
+});
8
+export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
9
+function Badge({ className, variant, ...props }: BadgeProps) { return <div className={cn(badgeVariants({ variant }), className)} {...props} />; }
10
+export { Badge, badgeVariants };
0
11
▼community/src/components/ui/button.tsx
+18-0
1
+import * as React from "react";
2
+import { Slot } from "@radix-ui/react-slot";
3
+import { cva, type VariantProps } from "class-variance-authority";
4
+import { cn } from "@/lib/utils";
5
+const buttonVariants = cva("inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", {
6
+ variants: {
7
+ variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline" },
8
+ size: { default: "h-10 px-4 py-2", sm: "h-9 rounded-md px-3", lg: "h-11 rounded-md px-8", icon: "h-10 w-10" },
9
+ },
10
+ defaultVariants: { variant: "default", size: "default" },
11
+});
12
+export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> { asChild?: boolean; }
13
+const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({ className, variant, size, asChild = false, ...props }, ref) => {
14
+ const Comp = asChild ? Slot : "button";
15
+ return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
16
+});
17
+Button.displayName = "Button";
18
+export { Button, buttonVariants };
0
19
▼community/src/components/ui/card.tsx
+15-0
1
+import * as React from "react";
2
+import { cn } from "@/lib/utils";
3
+const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => <div ref={ref} className={cn("rounded-xl border bg-card text-card-foreground shadow", className)} {...props} />);
4
+Card.displayName = "Card";
5
+const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => <div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />);
6
+CardHeader.displayName = "CardHeader";
7
+const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(({ className, ...props }, ref) => <h3 ref={ref} className={cn("font-semibold leading-none tracking-tight", className)} {...props} />);
8
+CardTitle.displayName = "CardTitle";
9
+const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(({ className, ...props }, ref) => <p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />);
10
+CardDescription.displayName = "CardDescription";
11
+const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />);
12
+CardContent.displayName = "CardContent";
13
+const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => <div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />);
14
+CardFooter.displayName = "CardFooter";
15
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
0
16
▼community/src/components/ui/input.tsx
+8-0
1
+import * as React from "react";
2
+import { cn } from "@/lib/utils";
3
+export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
4
+const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
5
+ return <input type={type} className={cn("flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", className)} ref={ref} {...props} />;
6
+});
7
+Input.displayName = "Input";
8
+export { Input };
0
9
▼community/src/components/ui/label.tsx
+10-0
1
+import * as React from "react";
2
+import * as LabelPrimitive from "@radix-ui/react-label";
3
+import { cva, type VariantProps } from "class-variance-authority";
4
+import { cn } from "@/lib/utils";
5
+const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70");
6
+const Label = React.forwardRef<React.ElementRef<typeof LabelPrimitive.Root>, React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>>(({ className, ...props }, ref) => (
7
+ <LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
8
+));
9
+Label.displayName = LabelPrimitive.Root.displayName;
10
+export { Label };
0
11
▼community/src/components/ui/toaster.tsx
+7-0
1
+import { Toaster as Sonner } from "sonner";
2
+
3
+export function Toaster() {
4
+ return (
5
+ <Sonner theme="dark" className="toaster group" />
6
+ );
7
+}
0
8
▼community/src/env.d.ts
+1-0
1
+/// <reference path="../.astro/types.d.ts" />
0
2
▼community/src/layouts/BaseLayout.astro
+36-0
1
+---
2
+import "../styles/globals.css";
3
+import { Toaster } from "../components/ui/toaster";
4
+import Footer from "../components/layout/Footer.astro";
5
+
6
+interface Props {
7
+ title?: string;
8
+ description?: string;
9
+}
10
+
11
+const {
12
+ title = "OpenCodeHub Community — Discover & Connect",
13
+ description = "Aggregating public repositories, developers, and nodes from self-hosted OpenCodeHub instances worldwide."
14
+} = Astro.props;
15
+---
16
+<!doctype html>
17
+<html lang="en" class="dark">
18
+ <head>
19
+ <meta charset="UTF-8" />
20
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
21
+ <title>{title}</title>
22
+ <meta name="description" content={description} />
23
+ <meta name="theme-color" content="#09090b" />
24
+ <link rel="icon" type="image/svg+xml" href="/logo.svg" />
25
+ <link rel="preconnect" href="https://fonts.googleapis.com">
26
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
27
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
28
+ </head>
29
+ <body class="min-h-screen bg-background font-sans antialiased text-foreground selection:bg-primary/20 selection:text-primary flex flex-col">
30
+ <div class="flex-1 flex flex-col">
31
+ <slot />
32
+ </div>
33
+ <Footer />
34
+ <Toaster client:load />
35
+ </body>
36
+</html>
0
37
▼community/src/lib/auth.ts
+24-0
1
+import { SignJWT, jwtVerify } from "jose";
2
+import bcrypt from "bcryptjs";
3
+function getSecret(): Uint8Array {
4
+ const secretStr = (
5
+ process.env.JWT_SECRET ||
6
+ (typeof import.meta !== "undefined" && (import.meta as any).env?.JWT_SECRET) ||
7
+ "community-dev-secret-change-me"
8
+ ).trim();
9
+ return new TextEncoder().encode(secretStr);
10
+}
11
+
12
+export async function hashPassword(pw: string) { return bcrypt.hash(pw, 10); }
13
+export async function verifyPassword(pw: string, hash: string) { return bcrypt.compare(pw, hash); }
14
+export async function signToken(payload: Record<string, any>) {
15
+ return await new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime("7d").sign(getSecret());
16
+}
17
+export async function verifyToken(token: string) {
18
+ try { const { payload } = await jwtVerify(token, getSecret()); return payload as any; } catch { return null; }
19
+}
20
+export function getUserFromCookie(cookieHeader: string | null) {
21
+ if (!cookieHeader) return null;
22
+ const m = cookieHeader.match(/community_session=([^;]+)/);
23
+ return m ? m[1] : null;
24
+}
0
25
▼community/src/lib/cache.ts
+38-0
1
+import { Redis } from "@upstash/redis";
2
+let redis: Redis | null = null;
3
+function getRedis(): Redis | null {
4
+ if (redis) return redis;
5
+ const url = (
6
+ process.env.UPSTASH_REDIS_REST_URL ||
7
+ (typeof import.meta !== "undefined" && (import.meta as any).env?.UPSTASH_REDIS_REST_URL) ||
8
+ ""
9
+ ).trim();
10
+ const token = (
11
+ process.env.UPSTASH_REDIS_REST_TOKEN ||
12
+ (typeof import.meta !== "undefined" && (import.meta as any).env?.UPSTASH_REDIS_REST_TOKEN) ||
13
+ ""
14
+ ).trim();
15
+ if (!url || !token) return null;
16
+ redis = new Redis({ url, token });
17
+ return redis;
18
+}
19
+const mem = new Map<string, {v:any; exp:number}>();
20
+export async function cacheGet<T>(key: string): Promise<T | null> {
21
+ const r = getRedis();
22
+ if (r) { try { const v = await r.get<T>(key); if (v) return v as T; } catch {} }
23
+ const m = mem.get(key);
24
+ if (m && m.exp > Date.now()) return m.v as T;
25
+ if (m) mem.delete(key);
26
+ return null;
27
+}
28
+export async function cacheSet(key: string, value: any, ttlSec = 300) {
29
+ const r = getRedis();
30
+ if (r) { try { await r.set(key, value, { ex: ttlSec }); } catch {} }
31
+ mem.set(key, { v: value, exp: Date.now() + ttlSec*1000 });
32
+ if (mem.size > 500) { const k = mem.keys().next().value; if(k) mem.delete(k); }
33
+}
34
+export async function cacheInvalidate(pattern: string) {
35
+ const r = getRedis();
36
+ if (r) { try { const keys = await r.keys(pattern); if(keys.length) await r.del(...keys); } catch {} }
37
+ for (const k of [...mem.keys()]) if (k.includes(pattern.replace("*",""))) mem.delete(k);
38
+}
0
39
▼community/src/lib/db/index.ts
+39-0
1
+import { createClient } from "@libsql/client";
2
+import { drizzle } from "drizzle-orm/libsql";
3
+import * as schema from "./schema";
4
+
5
+let _db: ReturnType<typeof drizzle> | null = null;
6
+
7
+export function getDb() {
8
+ if (_db) return _db;
9
+
10
+ const url = (
11
+ process.env.TURSO_DATABASE_URL ||
12
+ (typeof import.meta !== "undefined" && (import.meta as any).env?.TURSO_DATABASE_URL) ||
13
+ ""
14
+ ).trim();
15
+
16
+ const authToken = (
17
+ process.env.TURSO_AUTH_TOKEN ||
18
+ (typeof import.meta !== "undefined" && (import.meta as any).env?.TURSO_AUTH_TOKEN) ||
19
+ ""
20
+ ).trim();
21
+
22
+ if (!url) {
23
+ throw new Error(
24
+ "TURSO_DATABASE_URL is required. Set it in your .env to a Turso database URL.\n" +
25
+ "Example: TURSO_DATABASE_URL=libsql://your-db-name-your-org.turso.io"
26
+ );
27
+ }
28
+
29
+ const client = createClient({
30
+ url,
31
+ authToken: authToken || undefined,
32
+ });
33
+
34
+ _db = drizzle(client, { schema });
35
+ return _db;
36
+}
37
+
38
+export * from "./schema";
39
+
0
40
▼community/src/lib/db/schema.ts
+100-0
1
+import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
2
+import { sql } from "drizzle-orm";
3
+
4
+export const communityUsers = sqliteTable("community_users", {
5
+ id: text("id").primaryKey(),
6
+ username: text("username").notNull().unique(),
7
+ email: text("email").notNull().unique(),
8
+ displayName: text("display_name"),
9
+ bio: text("bio"),
10
+ avatarUrl: text("avatar_url"),
11
+ passwordHash: text("password_hash").notNull(),
12
+ createdAt: text("created_at").default(sql`CURRENT_TIMESTAMP`),
13
+ updatedAt: text("updated_at").default(sql`CURRENT_TIMESTAMP`),
14
+});
15
+
16
+export const instances = sqliteTable("instances", {
17
+ id: text("id").primaryKey(),
18
+ url: text("url").notNull().unique(),
19
+ siteUrl: text("site_url").notNull(),
20
+ name: text("name").notNull(),
21
+ version: text("version"),
22
+ capabilities: text("capabilities"),
23
+ status: text("status").default("pending"),
24
+ lastSyncAt: text("last_sync_at"),
25
+ repoCount: integer("repo_count").default(0),
26
+ userCount: integer("user_count").default(0),
27
+ submittedBy: text("submitted_by"),
28
+ createdAt: text("created_at").default(sql`CURRENT_TIMESTAMP`),
29
+}, (t) => [index("instances_url_idx").on(t.url)]);
30
+
31
+export const cachedRepos = sqliteTable("cached_repos", {
32
+ id: text("id").primaryKey(),
33
+ instanceId: text("instance_id").notNull().references(() => instances.id, { onDelete: "cascade" }),
34
+ remoteId: text("remote_id").notNull(),
35
+ fullName: text("full_name").notNull(),
36
+ name: text("name").notNull(),
37
+ ownerUsername: text("owner_username").notNull(),
38
+ ownerDisplayName: text("owner_display_name"),
39
+ ownerAvatarUrl: text("owner_avatar_url"),
40
+ description: text("description"),
41
+ visibility: text("visibility").default("public"),
42
+ language: text("language"),
43
+ topics: text("topics"),
44
+ starCount: integer("star_count").default(0),
45
+ forkCount: integer("fork_count").default(0),
46
+ watchCount: integer("watch_count").default(0),
47
+ httpCloneUrl: text("http_clone_url"),
48
+ updatedAt: text("updated_at"),
49
+ cachedAt: text("cached_at").default(sql`CURRENT_TIMESTAMP`),
50
+}, (t) => [
51
+ index("cached_repos_instance_idx").on(t.instanceId),
52
+ index("cached_repos_fullname_idx").on(t.fullName),
53
+]);
54
+
55
+export const cachedUsers = sqliteTable("cached_users", {
56
+ id: text("id").primaryKey(),
57
+ instanceId: text("instance_id").notNull().references(() => instances.id, { onDelete: "cascade" }),
58
+ remoteId: text("remote_id").notNull(),
59
+ username: text("username").notNull(),
60
+ displayName: text("display_name"),
61
+ bio: text("bio"),
62
+ avatarUrl: text("avatar_url"),
63
+ location: text("location"),
64
+ website: text("website"),
65
+ company: text("company"),
66
+ repoCount: integer("repo_count").default(0),
67
+ followerCount: integer("follower_count").default(0),
68
+ cachedAt: text("cached_at").default(sql`CURRENT_TIMESTAMP`),
69
+}, (t) => [index("cached_users_instance_idx").on(t.instanceId)]);
70
+
71
+export const stars = sqliteTable("stars", {
72
+ id: text("id").primaryKey(),
73
+ userId: text("user_id").notNull().references(() => communityUsers.id, { onDelete: "cascade" }),
74
+ repoId: text("repo_id").notNull().references(() => cachedRepos.id, { onDelete: "cascade" }),
75
+ createdAt: text("created_at").default(sql`CURRENT_TIMESTAMP`),
76
+}, (t) => [index("stars_user_repo_idx").on(t.userId, t.repoId)]);
77
+
78
+export const follows = sqliteTable("follows", {
79
+ id: text("id").primaryKey(),
80
+ followerId: text("follower_id").notNull().references(() => communityUsers.id, { onDelete: "cascade" }),
81
+ followeeType: text("followee_type").notNull(),
82
+ followeeId: text("followee_id").notNull(),
83
+ createdAt: text("created_at").default(sql`CURRENT_TIMESTAMP`),
84
+});
85
+
86
+import { relations } from "drizzle-orm";
87
+
88
+export const cachedReposRelations = relations(cachedRepos, ({ one }) => ({
89
+ instance: one(instances, {
90
+ fields: [cachedRepos.instanceId],
91
+ references: [instances.id],
92
+ }),
93
+}));
94
+
95
+export const cachedUsersRelations = relations(cachedUsers, ({ one }) => ({
96
+ instance: one(instances, {
97
+ fields: [cachedUsers.instanceId],
98
+ references: [instances.id],
99
+ }),
100
+}));
0
101
▼community/src/lib/instance-client.ts
+44-0
1
+export interface InstanceInfo {
2
+ product: string; name: string; siteUrl: string; version: string; capabilities: string[];
3
+}
4
+export interface RemoteRepo {
5
+ id: string; name: string; fullName: string; description: string | null;
6
+ visibility: string; defaultBranch: string; starCount: number; forkCount: number; watchCount: number;
7
+ language: string | null; topics: string[]; httpCloneUrl: string; updatedAt: string; createdAt: string;
8
+ owner: { id: string; username: string; displayName: string | null; avatarUrl: string | null; };
9
+}
10
+
11
+export async function probeInstance(baseUrl: string): Promise<{ ok: true; info: InstanceInfo } | { ok: false; error: string }> {
12
+ try {
13
+ const origin = new URL(baseUrl).origin;
14
+ const ctrl = new AbortController(); const t = setTimeout(()=>ctrl.abort(), 8000);
15
+ const res = await fetch(`${origin}/api/instance`, { signal: ctrl.signal, headers: { Accept: "application/json" } });
16
+ clearTimeout(t);
17
+ if (!res.ok) return { ok: false, error: `Instance probe failed: ${res.status}` };
18
+ const json = await res.json();
19
+ if (json?.data?.product !== "opencodehub") return { ok: false, error: "Not an OpenCodeHub instance" };
20
+ return { ok: true, info: json.data as InstanceInfo };
21
+ } catch (e: any) {
22
+ return { ok: false, error: e?.message || "Network error" };
23
+ }
24
+}
25
+
26
+export async function fetchPublicRepos(origin: string, page = 1, perPage = 50): Promise<{ repos: RemoteRepo[]; total: number; totalPages: number }> {
27
+ const url = `${new URL(origin).origin}/api/repos?visibility=public&sort=updated&page=${page}&per_page=${perPage}`;
28
+ const res = await fetch(url, { headers: { Accept: "application/json", "User-Agent": "CommunityHub/1.0" } });
29
+ if (!res.ok) throw new Error(`Failed to fetch repos: ${res.status}`);
30
+ const json = await res.json();
31
+ if (!json.success) throw new Error(json.error?.message || "Failed");
32
+ return { repos: json.data as RemoteRepo[], total: json.meta?.total || json.data.length, totalPages: json.meta?.totalPages || 1 };
33
+}
34
+
35
+export async function fetchAllPublicRepos(origin: string): Promise<RemoteRepo[]> {
36
+ let page = 1; const all: RemoteRepo[] = [];
37
+ while (true) {
38
+ const { repos, totalPages } = await fetchPublicRepos(origin, page, 100);
39
+ all.push(...repos);
40
+ if (page >= totalPages || repos.length === 0) break;
41
+ page++; await new Promise(r=>setTimeout(r,150));
42
+ }
43
+ return all;
44
+}
0
45
▼community/src/lib/utils.ts
+11-0
1
+import { clsx, type ClassValue } from "clsx";
2
+import { twMerge } from "tailwind-merge";
3
+export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }
4
+export function timeAgo(date: string | Date) {
5
+ const s = Math.floor((Date.now() - new Date(date).getTime())/1000);
6
+ if (s < 60) return "just now";
7
+ if (s < 3600) return `${Math.floor(s/60)}m ago`;
8
+ if (s < 86400) return `${Math.floor(s/3600)}h ago`;
9
+ if (s < 2592000) return `${Math.floor(s/86400)}d ago`;
10
+ return new Date(date).toLocaleDateString();
11
+}
0
12
▼community/src/pages/api/auth/login.ts
+46-0
1
+import type { APIRoute } from "astro";
2
+import { getDb } from "@/lib/db";
3
+import { communityUsers } from "@/lib/db/schema";
4
+import { verifyPassword, signToken } from "@/lib/auth";
5
+import { eq } from "drizzle-orm";
6
+
7
+export const prerender = false;
8
+
9
+export const POST: APIRoute = async ({ request, cookies }) => {
10
+ try {
11
+ const body = await request.json();
12
+ const { email, password } = body;
13
+
14
+ if (!email || !password) {
15
+ return new Response(JSON.stringify({ success: false, error: "Missing email or password" }), { status: 400 });
16
+ }
17
+
18
+ const db = getDb() as any;
19
+
20
+ const user = await db.query.communityUsers.findFirst({
21
+ where: eq(communityUsers.email, email)
22
+ });
23
+
24
+ if (!user) {
25
+ return new Response(JSON.stringify({ success: false, error: "Invalid credentials" }), { status: 401 });
26
+ }
27
+
28
+ const isValid = await verifyPassword(password, user.passwordHash);
29
+ if (!isValid) {
30
+ return new Response(JSON.stringify({ success: false, error: "Invalid credentials" }), { status: 401 });
31
+ }
32
+
33
+ const token = await signToken({ sub: user.id, username: user.username });
34
+
35
+ cookies.set("community_session", token, {
36
+ path: "/",
37
+ httpOnly: true,
38
+ secure: process.env.NODE_ENV === "production",
39
+ maxAge: 60 * 60 * 24 * 7,
40
+ });
41
+
42
+ return new Response(JSON.stringify({ success: true, data: { id: user.id, username: user.username } }), { status: 200 });
43
+ } catch (error: any) {
44
+ return new Response(JSON.stringify({ success: false, error: error.message || "Server error" }), { status: 500 });
45
+ }
46
+};
0
47
▼community/src/pages/api/auth/logout.ts
+8-0
1
+import type { APIRoute } from "astro";
2
+
3
+export const prerender = false;
4
+
5
+export const POST: APIRoute = async ({ cookies }) => {
6
+ cookies.delete("community_session", { path: "/" });
7
+ return new Response(JSON.stringify({ success: true }), { status: 200 });
8
+};
0
9
▼community/src/pages/api/auth/register.ts
+52-0
1
+import type { APIRoute } from "astro";
2
+import { getDb } from "@/lib/db";
3
+import { communityUsers } from "@/lib/db/schema";
4
+import { hashPassword, signToken } from "@/lib/auth";
5
+import { nanoid } from "nanoid";
6
+import { eq, or } from "drizzle-orm";
7
+
8
+export const prerender = false;
9
+
10
+export const POST: APIRoute = async ({ request, cookies }) => {
11
+ try {
12
+ const body = await request.json();
13
+ const { username, email, password } = body;
14
+
15
+ if (!username || !email || !password) {
16
+ return new Response(JSON.stringify({ success: false, error: "Missing required fields" }), { status: 400 });
17
+ }
18
+
19
+ const db = getDb() as any;
20
+
21
+ const existingUser = await db.query.communityUsers.findFirst({
22
+ where: or(eq(communityUsers.username, username), eq(communityUsers.email, email))
23
+ });
24
+
25
+ if (existingUser) {
26
+ return new Response(JSON.stringify({ success: false, error: "Username or email already exists" }), { status: 400 });
27
+ }
28
+
29
+ const passwordHash = await hashPassword(password);
30
+ const id = nanoid();
31
+
32
+ await db.insert(communityUsers).values({
33
+ id,
34
+ username,
35
+ email,
36
+ passwordHash,
37
+ });
38
+
39
+ const token = await signToken({ sub: id, username });
40
+
41
+ cookies.set("community_session", token, {
42
+ path: "/",
43
+ httpOnly: true,
44
+ secure: process.env.NODE_ENV === "production",
45
+ maxAge: 60 * 60 * 24 * 7,
46
+ });
47
+
48
+ return new Response(JSON.stringify({ success: true, data: { id, username } }), { status: 200 });
49
+ } catch (error: any) {
50
+ return new Response(JSON.stringify({ success: false, error: error.message || "Server error" }), { status: 500 });
51
+ }
52
+};
0
53
▼community/src/pages/api/follows/index.ts
+40-0
1
+import type { APIRoute } from "astro";
2
+import { getDb } from "@/lib/db";
3
+import { follows } from "@/lib/db/schema";
4
+import { verifyToken } from "@/lib/auth";
5
+import { nanoid } from "nanoid";
6
+import { eq, and } from "drizzle-orm";
7
+
8
+export const prerender = false;
9
+
10
+export const POST: APIRoute = async ({ request, cookies }) => {
11
+ try {
12
+ const { followeeType, followeeId } = await request.json();
13
+ if (!followeeType || !followeeId) {
14
+ return new Response(JSON.stringify({ success: false, error: "followeeType and followeeId required" }), { status: 400 });
15
+ }
16
+
17
+ const token = cookies.get("community_session")?.value;
18
+ if (!token) return new Response(JSON.stringify({ success: false, error: "Unauthorized" }), { status: 401 });
19
+
20
+ const user = await verifyToken(token);
21
+ if (!user) return new Response(JSON.stringify({ success: false, error: "Unauthorized" }), { status: 401 });
22
+
23
+ const followerId = user.sub;
24
+ const db = getDb() as any;
25
+
26
+ const existing = await db.query.follows.findFirst({
27
+ where: and(eq(follows.followerId, followerId), eq(follows.followeeId, followeeId))
28
+ }).catch(() => null);
29
+
30
+ if (existing) {
31
+ await db.delete(follows).where(eq(follows.id, existing.id));
32
+ return new Response(JSON.stringify({ success: true, data: { following: false } }), { headers: { "Content-Type": "application/json" } });
33
+ }
34
+
35
+ await db.insert(follows).values({ id: nanoid(), followerId, followeeType, followeeId });
36
+ return new Response(JSON.stringify({ success: true, data: { following: true } }), { headers: { "Content-Type": "application/json" } });
37
+ } catch (e: any) {
38
+ return new Response(JSON.stringify({ success: false, error: e.message }), { status: 500 });
39
+ }
40
+};
0
41
▼community/src/pages/api/instances/index.ts
+12-0
1
+import type { APIRoute } from "astro";
2
+import { getDb } from "@/lib/db";
3
+export const prerender = false;
4
+export const GET: APIRoute = async () => {
5
+ try {
6
+ const db = getDb() as any;
7
+ const rows = await db.query.instances.findMany?.().catch(()=>[]) || [];
8
+ return new Response(JSON.stringify({ success: true, data: rows }), { headers: { "Content-Type": "application/json" } });
9
+ } catch (e: any) {
10
+ return new Response(JSON.stringify({ success: true, data: [] }), { headers: { "Content-Type": "application/json" } });
11
+ }
12
+};
0
13
▼community/src/pages/api/instances/submit.ts
+48-0
1
+import type { APIRoute } from "astro";
2
+import { probeInstance, fetchPublicRepos } from "@/lib/instance-client";
3
+import { getDb } from "@/lib/db";
4
+import { instances } from "@/lib/db/schema";
5
+import { nanoid } from "nanoid";
6
+import { cacheSet } from "@/lib/cache";
7
+import { eq } from "drizzle-orm";
8
+
9
+export const prerender = false;
10
+
11
+export const POST: APIRoute = async ({ request }) => {
12
+ try {
13
+ const body = await request.json();
14
+ const rawUrl = (body.url || "").trim();
15
+ if (!rawUrl) return new Response(JSON.stringify({ success: false, error: { message: "URL is required" } }), { status: 400, headers: { "Content-Type": "application/json" } });
16
+ let origin: string;
17
+ try { origin = new URL(rawUrl).origin; } catch { return new Response(JSON.stringify({ success: false, error: { message: "Invalid URL" } }), { status: 400, headers: { "Content-Type": "application/json" } }); }
18
+
19
+ const probe = await probeInstance(origin);
20
+ if (!probe.ok) return new Response(JSON.stringify({ success: false, error: { message: probe.error } }), { status: 400, headers: { "Content-Type": "application/json" } });
21
+
22
+ const info = probe.info;
23
+ const db = getDb() as any;
24
+
25
+ let id: string;
26
+ // try find existing
27
+ let existing: any = null;
28
+ try { existing = await db.query.instances.findFirst({ where: eq(instances.url, origin) }); } catch {}
29
+ if (existing) {
30
+ id = existing.id;
31
+ await db.update(instances).set({ siteUrl: info.siteUrl, name: info.name, version: info.version, capabilities: JSON.stringify(info.capabilities), status: "online", lastSyncAt: new Date().toISOString() }).where(eq(instances.id, id));
32
+ } else {
33
+ id = nanoid();
34
+ await db.insert(instances).values({ id, url: origin, siteUrl: info.siteUrl, name: info.name, version: info.version, capabilities: JSON.stringify(info.capabilities), status: "online", lastSyncAt: new Date().toISOString() });
35
+ }
36
+
37
+ let repoCount = 0;
38
+ try {
39
+ const { repos } = await fetchPublicRepos(origin, 1, 5);
40
+ repoCount = repos.length;
41
+ await cacheSet(`instance:${id}:probe`, info, 3600);
42
+ } catch {}
43
+
44
+ return new Response(JSON.stringify({ success: true, data: { id, ...info, repoCount } }), { status: 200, headers: { "Content-Type": "application/json" } });
45
+ } catch (e: any) {
46
+ return new Response(JSON.stringify({ success: false, error: { message: e.message || "Server error" } }), { status: 500, headers: { "Content-Type": "application/json" } });
47
+ }
48
+};
0
49
▼community/src/pages/api/instances/sync.ts
+73-0
1
+import type { APIRoute } from "astro";
2
+import { getDb } from "@/lib/db";
3
+import { instances, cachedRepos } from "@/lib/db/schema";
4
+import { fetchAllPublicRepos } from "@/lib/instance-client";
5
+import { cacheGet, cacheSet } from "@/lib/cache";
6
+import { nanoid } from "nanoid";
7
+import { eq } from "drizzle-orm";
8
+export const prerender = false;
9
+export const POST: APIRoute = async ({ url }) => {
10
+ const id = url.searchParams.get("id");
11
+ if (!id) return new Response(JSON.stringify({ success: false, error: { message: "id required" } }), { status: 400 });
12
+ const db = getDb() as any;
13
+ const instance = await db.query.instances.findFirst({ where: eq(instances.id, id) }).catch(()=>null);
14
+ if (!instance) return new Response(JSON.stringify({ success: false, error: { message: "Instance not found" } }), { status: 404 });
15
+ const cached = await cacheGet<any>(`sync:${id}`);
16
+ if (cached) return new Response(JSON.stringify({ success: true, data: cached, cached: true }), { headers: { "Content-Type": "application/json" } });
17
+ try {
18
+ const repos = await fetchAllPublicRepos(instance.url);
19
+ // Upsert cached repos and owners
20
+ const ownersMap = new Map();
21
+
22
+ for (const r of repos) {
23
+ // Collect owners
24
+ if (r.owner && !ownersMap.has(r.owner.username)) {
25
+ ownersMap.set(r.owner.username, r.owner);
26
+ }
27
+
28
+ const existing = await db.query.cachedRepos.findFirst({ where: eq(cachedRepos.remoteId, r.id) }).catch(()=>null);
29
+ if (existing) {
30
+ await db.update(cachedRepos).set({ fullName: r.fullName, name: r.name, description: r.description, language: r.language, topics: JSON.stringify(r.topics), starCount: r.starCount, forkCount: r.forkCount, httpCloneUrl: r.httpCloneUrl, updatedAt: r.updatedAt }).where(eq(cachedRepos.id, existing.id));
31
+ } else {
32
+ await db.insert(cachedRepos).values({ id: nanoid(), instanceId: id, remoteId: r.id, fullName: r.fullName, name: r.name, ownerUsername: r.owner.username, ownerDisplayName: r.owner.displayName, ownerAvatarUrl: r.owner.avatarUrl, description: r.description, visibility: r.visibility, language: r.language, topics: JSON.stringify(r.topics), starCount: r.starCount, forkCount: r.forkCount, httpCloneUrl: r.httpCloneUrl, updatedAt: r.updatedAt });
33
+ }
34
+ }
35
+
36
+ const { cachedUsers } = await import("@/lib/db/schema");
37
+ for (const owner of ownersMap.values()) {
38
+ const existingUser = await db.query.cachedUsers.findFirst({ where: eq(cachedUsers.username, owner.username) }).catch(() => null);
39
+ if (existingUser) {
40
+ await db.update(cachedUsers).set({
41
+ displayName: owner.displayName,
42
+ avatarUrl: owner.avatarUrl,
43
+ }).where(eq(cachedUsers.id, existingUser.id));
44
+ } else {
45
+ await db.insert(cachedUsers).values({
46
+ id: nanoid(),
47
+ instanceId: id,
48
+ remoteId: owner.id || owner.username,
49
+ username: owner.username,
50
+ displayName: owner.displayName,
51
+ avatarUrl: owner.avatarUrl,
52
+ });
53
+ }
54
+ }
55
+ await db.update(instances).set({ repoCount: repos.length, status: "online", lastSyncAt: new Date().toISOString() }).where(eq(instances.id, id));
56
+ const result = { repos, count: repos.length };
57
+ await cacheSet(`sync:${id}`, result, 300);
58
+ return new Response(JSON.stringify({ success: true, data: result }), { headers: { "Content-Type": "application/json" } });
59
+ } catch (e: any) {
60
+ await db.update(instances).set({ status: "error" }).where(eq(instances.id, id)).catch(()=>{});
61
+ return new Response(JSON.stringify({ success: false, error: { message: e.message } }), { status: 500 });
62
+ }
63
+};
64
+export const GET: APIRoute = async ({ url }) => {
65
+ const id = url.searchParams.get("id");
66
+ const db = getDb() as any;
67
+ if (id) {
68
+ const rows = await db.query.cachedRepos.findMany({ where: eq(cachedRepos.instanceId, id) }).catch(()=>[]);
69
+ return new Response(JSON.stringify({ success: true, data: rows }), { headers: { "Content-Type": "application/json" } });
70
+ }
71
+ const all = await db.query.cachedRepos.findMany().catch(()=>[]);
72
+ return new Response(JSON.stringify({ success: true, data: all }), { headers: { "Content-Type": "application/json" } });
73
+};
0
74
▼community/src/pages/api/search.ts
+40-0
1
+import type { APIRoute } from "astro";
2
+import { getDb } from "@/lib/db";
3
+import { cachedRepos, cachedUsers } from "@/lib/db/schema";
4
+import { like, or } from "drizzle-orm";
5
+
6
+export const prerender = false;
7
+
8
+export const GET: APIRoute = async ({ url }) => {
9
+ try {
10
+ const q = url.searchParams.get("q") || "";
11
+ if (!q || q.length < 2) {
12
+ return new Response(JSON.stringify({ success: true, data: { repos: [], users: [] } }), { status: 200 });
13
+ }
14
+
15
+ const db = getDb() as any;
16
+ const searchPattern = `%${q}%`;
17
+
18
+ const repos = await db.query.cachedRepos.findMany({
19
+ where: or(
20
+ like(cachedRepos.fullName, searchPattern),
21
+ like(cachedRepos.description, searchPattern)
22
+ ),
23
+ limit: 10,
24
+ with: { instance: true }
25
+ });
26
+
27
+ const users = await db.query.cachedUsers.findMany({
28
+ where: or(
29
+ like(cachedUsers.username, searchPattern),
30
+ like(cachedUsers.displayName, searchPattern)
31
+ ),
32
+ limit: 5,
33
+ with: { instance: true }
34
+ });
35
+
36
+ return new Response(JSON.stringify({ success: true, data: { repos, users } }), { status: 200 });
37
+ } catch (error: any) {
38
+ return new Response(JSON.stringify({ success: false, error: error.message }), { status: 500 });
39
+ }
40
+};
0
41
▼community/src/pages/api/stars/index.ts
+52-0
1
+import type { APIRoute } from "astro";
2
+import { getDb } from "@/lib/db";
3
+import { stars, cachedRepos } from "@/lib/db/schema";
4
+import { verifyToken } from "@/lib/auth";
5
+import { nanoid } from "nanoid";
6
+import { eq, and } from "drizzle-orm";
7
+
8
+export const prerender = false;
9
+
10
+export const POST: APIRoute = async ({ request, cookies }) => {
11
+ try {
12
+ const body = await request.json();
13
+ const { repoId, fullName } = body;
14
+ if (!repoId && !fullName) {
15
+ return new Response(JSON.stringify({ success: false, error: "repoId or fullName required" }), { status: 400 });
16
+ }
17
+
18
+ const token = cookies.get("community_session")?.value;
19
+ if (!token) return new Response(JSON.stringify({ success: false, error: "Unauthorized" }), { status: 401 });
20
+
21
+ const user = await verifyToken(token);
22
+ if (!user) return new Response(JSON.stringify({ success: false, error: "Unauthorized" }), { status: 401 });
23
+
24
+ const userId = user.sub;
25
+ const db = getDb() as any;
26
+
27
+ let targetId = repoId;
28
+ if (!targetId && fullName) {
29
+ const repo = await db.query.cachedRepos.findFirst({ where: eq(cachedRepos.fullName, fullName) }).catch(() => null);
30
+ targetId = repo?.id;
31
+ if (!targetId) return new Response(JSON.stringify({ success: false, error: "Repo not cached yet — sync the instance first" }), { status: 404 });
32
+ }
33
+
34
+ const existing = await db.query.stars.findFirst({ where: and(eq(stars.userId, userId), eq(stars.repoId, targetId)) }).catch(() => null);
35
+ if (existing) {
36
+ await db.delete(stars).where(and(eq(stars.userId, userId), eq(stars.repoId, targetId)));
37
+ return new Response(JSON.stringify({ success: true, data: { starred: false } }), { headers: { "Content-Type": "application/json" } });
38
+ }
39
+
40
+ await db.insert(stars).values({ id: nanoid(), userId, repoId: targetId });
41
+ return new Response(JSON.stringify({ success: true, data: { starred: true } }), { headers: { "Content-Type": "application/json" } });
42
+ } catch (e: any) {
43
+ return new Response(JSON.stringify({ success: false, error: e.message }), { status: 500 });
44
+ }
45
+};
46
+
47
+export const GET: APIRoute = async ({ url }) => {
48
+ const repoId = url.searchParams.get("repoId");
49
+ const db = getDb() as any;
50
+ const count = await db.query.stars.findMany({ where: repoId ? eq(stars.repoId, repoId) : undefined }).catch(()=>[]);
51
+ return new Response(JSON.stringify({ success: true, data: { count: count.length } }), { headers: { "Content-Type": "application/json" } });
52
+};
0
53
▼community/src/pages/auth/login.astro
+118-0
1
+---
2
+import BaseLayout from "../../layouts/BaseLayout.astro";
3
+import { Header } from "../../components/layout/Header";
4
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "../../components/ui/card";
5
+import { Button } from "../../components/ui/button";
6
+import { Input } from "../../components/ui/input";
7
+import { Label } from "../../components/ui/label";
8
+import { Github, LogIn, Sparkles, Lock, Mail } from "lucide-react";
9
+import { verifyToken } from "../../lib/auth";
10
+
11
+const token = Astro.cookies.get("community_session")?.value;
12
+const user = token ? await verifyToken(token) : null;
13
+if (user) {
14
+ return Astro.redirect("/dashboard");
15
+}
16
+---
17
+<BaseLayout title="Sign In — OpenCodeHub Community">
18
+ <Header client:load user={user} />
19
+
20
+ <main class="container px-4 py-16 max-w-md mx-auto my-auto space-y-6">
21
+ <div class="text-center space-y-2">
22
+ <div class="h-12 w-12 rounded-2xl bg-gradient-to-br from-primary via-indigo-500 to-purple-600 flex items-center justify-center text-white mx-auto shadow-lg shadow-primary/25">
23
+ <Github className="h-6 w-6" />
24
+ </div>
25
+ <h1 class="text-2xl font-extrabold tracking-tight text-foreground">
26
+ Welcome Back
27
+ </h1>
28
+ <p class="text-xs text-muted-foreground">
29
+ Sign in to star repos, follow developers, and manage your nodes.
30
+ </p>
31
+ </div>
32
+
33
+ <Card className="border-border/60 bg-card/60 backdrop-blur-xl shadow-xl">
34
+ <CardContent className="pt-6">
35
+ <form id="login-form" class="space-y-4">
36
+ <div class="space-y-2">
37
+ <Label htmlFor="email" className="text-xs font-semibold">Email Address</Label>
38
+ <div class="relative">
39
+ <Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
40
+ <Input
41
+ id="email"
42
+ type="email"
43
+ placeholder="[email protected]"
44
+ required
45
+ className="pl-9 bg-card/50 border-border/60 focus:border-primary h-10 text-sm"
46
+ />
47
+ </div>
48
+ </div>
49
+
50
+ <div class="space-y-2">
51
+ <div class="flex items-center justify-between">
52
+ <Label htmlFor="password" className="text-xs font-semibold">Password</Label>
53
+ </div>
54
+ <div class="relative">
55
+ <Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
56
+ <Input
57
+ id="password"
58
+ type="password"
59
+ placeholder="••••••••"
60
+ required
61
+ className="pl-9 bg-card/50 border-border/60 focus:border-primary h-10 text-sm"
62
+ />
63
+ </div>
64
+ </div>
65
+
66
+ <Button type="submit" className="w-full h-10 bg-primary hover:bg-primary/90 text-sm font-semibold shadow-md shadow-primary/20" id="submit-btn">
67
+ <LogIn className="h-4 w-4 mr-2" />
68
+ Sign In
69
+ </Button>
70
+
71
+ <p class="text-xs text-center text-muted-foreground pt-2">
72
+ Don't have an account?{" "}
73
+ <a href="/auth/register" class="text-primary font-semibold hover:underline">
74
+ Create account
75
+ </a>
76
+ </p>
77
+ </form>
78
+ </CardContent>
79
+ </Card>
80
+ </main>
81
+
82
+ <script>
83
+ import { toast } from "sonner";
84
+
85
+ const form = document.getElementById("login-form") as HTMLFormElement;
86
+ const submitBtn = document.getElementById("submit-btn") as HTMLButtonElement;
87
+
88
+ form?.addEventListener("submit", async (e) => {
89
+ e.preventDefault();
90
+ const email = (document.getElementById("email") as HTMLInputElement).value.trim();
91
+ const password = (document.getElementById("password") as HTMLInputElement).value;
92
+
93
+ submitBtn.disabled = true;
94
+ submitBtn.innerHTML = `<span class="inline-flex items-center gap-2"><svg class="animate-spin h-4 w-4" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> Signing in...</span>`;
95
+
96
+ try {
97
+ const res = await fetch("/api/auth/login", {
98
+ method: "POST",
99
+ headers: { "Content-Type": "application/json" },
100
+ body: JSON.stringify({ email, password }),
101
+ });
102
+ const data = await res.json();
103
+
104
+ if (data.success) {
105
+ toast.success("Successfully signed in!");
106
+ setTimeout(() => window.location.href = "/dashboard", 600);
107
+ } else {
108
+ toast.error(data.error || "Invalid credentials");
109
+ }
110
+ } catch (error: any) {
111
+ toast.error(error.message || "An error occurred during sign in");
112
+ } finally {
113
+ submitBtn.disabled = false;
114
+ submitBtn.innerHTML = `<svg class="h-4 w-4 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" x2="3" y1="12" y2="12"/></svg> Sign In`;
115
+ }
116
+ });
117
+ </script>
118
+</BaseLayout>
0
119
▼community/src/pages/auth/register.astro
+130-0
1
+---
2
+import BaseLayout from "../../layouts/BaseLayout.astro";
3
+import { Header } from "../../components/layout/Header";
4
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "../../components/ui/card";
5
+import { Button } from "../../components/ui/button";
6
+import { Input } from "../../components/ui/input";
7
+import { Label } from "../../components/ui/label";
8
+import { Github, Sparkles, Lock, Mail, User } from "lucide-react";
9
+import { verifyToken } from "../../lib/auth";
10
+
11
+const token = Astro.cookies.get("community_session")?.value;
12
+const user = token ? await verifyToken(token) : null;
13
+if (user) {
14
+ return Astro.redirect("/dashboard");
15
+}
16
+---
17
+<BaseLayout title="Create Account — OpenCodeHub Community">
18
+ <Header client:load user={user} />
19
+
20
+ <main class="container px-4 py-16 max-w-md mx-auto my-auto space-y-6">
21
+ <div class="text-center space-y-2">
22
+ <div class="h-12 w-12 rounded-2xl bg-gradient-to-br from-primary via-indigo-500 to-purple-600 flex items-center justify-center text-white mx-auto shadow-lg shadow-primary/25">
23
+ <Sparkles className="h-6 w-6" />
24
+ </div>
25
+ <h1 class="text-2xl font-extrabold tracking-tight text-foreground">
26
+ Join the Network
27
+ </h1>
28
+ <p class="text-xs text-muted-foreground">
29
+ Create your Community account to star, follow, and connect nodes.
30
+ </p>
31
+ </div>
32
+
33
+ <Card className="border-border/60 bg-card/60 backdrop-blur-xl shadow-xl">
34
+ <CardContent className="pt-6">
35
+ <form id="register-form" class="space-y-4">
36
+ <div class="space-y-2">
37
+ <Label htmlFor="username" className="text-xs font-semibold">Username</Label>
38
+ <div class="relative">
39
+ <User className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
40
+ <Input
41
+ id="username"
42
+ placeholder="octocat"
43
+ required
44
+ className="pl-9 bg-card/50 border-border/60 focus:border-primary h-10 text-sm"
45
+ />
46
+ </div>
47
+ </div>
48
+
49
+ <div class="space-y-2">
50
+ <Label htmlFor="email" className="text-xs font-semibold">Email Address</Label>
51
+ <div class="relative">
52
+ <Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
53
+ <Input
54
+ id="email"
55
+ type="email"
56
+ placeholder="[email protected]"
57
+ required
58
+ className="pl-9 bg-card/50 border-border/60 focus:border-primary h-10 text-sm"
59
+ />
60
+ </div>
61
+ </div>
62
+
63
+ <div class="space-y-2">
64
+ <Label htmlFor="password" className="text-xs font-semibold">Password</Label>
65
+ <div class="relative">
66
+ <Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
67
+ <Input
68
+ id="password"
69
+ type="password"
70
+ placeholder="••••••••"
71
+ required
72
+ className="pl-9 bg-card/50 border-border/60 focus:border-primary h-10 text-sm"
73
+ />
74
+ </div>
75
+ </div>
76
+
77
+ <Button type="submit" className="w-full h-10 bg-primary hover:bg-primary/90 text-sm font-semibold shadow-md shadow-primary/20" id="submit-btn">
78
+ <Sparkles className="h-4 w-4 mr-2" />
79
+ Create Account
80
+ </Button>
81
+
82
+ <p class="text-xs text-center text-muted-foreground pt-2">
83
+ Already have an account?{" "}
84
+ <a href="/auth/login" class="text-primary font-semibold hover:underline">
85
+ Sign in
86
+ </a>
87
+ </p>
88
+ </form>
89
+ </CardContent>
90
+ </Card>
91
+ </main>
92
+
93
+ <script>
94
+ import { toast } from "sonner";
95
+
96
+ const form = document.getElementById("register-form") as HTMLFormElement;
97
+ const submitBtn = document.getElementById("submit-btn") as HTMLButtonElement;
98
+
99
+ form?.addEventListener("submit", async (e) => {
100
+ e.preventDefault();
101
+ const username = (document.getElementById("username") as HTMLInputElement).value.trim();
102
+ const email = (document.getElementById("email") as HTMLInputElement).value.trim();
103
+ const password = (document.getElementById("password") as HTMLInputElement).value;
104
+
105
+ submitBtn.disabled = true;
106
+ submitBtn.innerHTML = `<span class="inline-flex items-center gap-2"><svg class="animate-spin h-4 w-4" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> Creating account...</span>`;
107
+
108
+ try {
109
+ const res = await fetch("/api/auth/register", {
110
+ method: "POST",
111
+ headers: { "Content-Type": "application/json" },
112
+ body: JSON.stringify({ username, email, password }),
113
+ });
114
+ const data = await res.json();
115
+
116
+ if (data.success) {
117
+ toast.success("Account created successfully!");
118
+ setTimeout(() => window.location.href = "/dashboard", 600);
119
+ } else {
120
+ toast.error(data.error || "Failed to create account");
121
+ }
122
+ } catch (error: any) {
123
+ toast.error(error.message || "An error occurred during registration");
124
+ } finally {
125
+ submitBtn.disabled = false;
126
+ submitBtn.innerHTML = `<svg class="h-4 w-4 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/></svg> Create Account`;
127
+ }
128
+ });
129
+ </script>
130
+</BaseLayout>
0
131
▼community/src/pages/dashboard.astro
+204-0
1
+---
2
+import BaseLayout from "../layouts/BaseLayout.astro";
3
+import { Header } from "../components/layout/Header";
4
+import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card";
5
+import { Avatar, AvatarFallback, AvatarImage } from "../components/ui/avatar";
6
+import { Button } from "../components/ui/button";
7
+import { Badge } from "../components/ui/badge";
8
+import { verifyToken } from "../lib/auth";
9
+import { getDb } from "../lib/db";
10
+import { eq, inArray } from "drizzle-orm";
11
+import { stars, follows, cachedRepos, cachedUsers } from "../lib/db/schema";
12
+import {
13
+ Star,
14
+ Users,
15
+ GitFork,
16
+ Server,
17
+ ExternalLink,
18
+ Compass,
19
+ Sparkles,
20
+ LogOut,
21
+ BookOpen,
22
+ UserCheck
23
+} from "lucide-react";
24
+
25
+const token = Astro.cookies.get("community_session")?.value;
26
+if (!token) {
27
+ return Astro.redirect("/auth/login");
28
+}
29
+
30
+const user = await verifyToken(token);
31
+if (!user) {
32
+ return Astro.redirect("/auth/login");
33
+}
34
+
35
+const db = getDb() as any;
36
+
37
+// Fetch Starred Repos
38
+const starredRows = await db.query.stars.findMany({
39
+ where: eq(stars.userId, user.sub)
40
+}).catch(() => []);
41
+
42
+const starredRepoIds = starredRows.map((r: any) => r.repoId);
43
+let starredRepos: any[] = [];
44
+if (starredRepoIds.length > 0) {
45
+ starredRepos = await db.query.cachedRepos.findMany({
46
+ where: inArray(cachedRepos.id, starredRepoIds),
47
+ with: { instance: true }
48
+ }).catch(() => []);
49
+}
50
+
51
+// Fetch Followed Users
52
+const followedRows = await db.query.follows.findMany({
53
+ where: eq(follows.followerId, user.sub)
54
+}).catch(() => []);
55
+const followedUsernames = followedRows.map((f: any) => f.followeeId);
56
+
57
+let followedProfiles: any[] = [];
58
+if (followedUsernames.length > 0) {
59
+ followedProfiles = await db.query.cachedUsers.findMany({
60
+ where: inArray(cachedUsers.username, followedUsernames),
61
+ with: { instance: true }
62
+ }).catch(() => []);
63
+}
64
+---
65
+<BaseLayout title="My Dashboard — OpenCodeHub Community">
66
+ <Header client:load user={user} />
67
+
68
+ <main class="container px-4 sm:px-8 py-8 sm:py-12 space-y-8 max-w-6xl mx-auto">
69
+
70
+ <!-- User Welcome Banner -->
71
+ <div class="rounded-3xl border border-border/60 bg-gradient-to-r from-primary/10 via-card to-card p-6 sm:p-8 flex flex-col sm:flex-row items-center justify-between gap-6 backdrop-blur-xl">
72
+ <div class="flex items-center gap-4 text-center sm:text-left">
73
+ <div class="h-16 w-16 rounded-2xl bg-primary/20 text-primary border border-primary/30 flex items-center justify-center text-2xl font-bold uppercase shadow-inner">
74
+ {user.username?.[0] || "U"}
75
+ </div>
76
+ <div class="space-y-1">
77
+ <h1 class="text-2xl sm:text-3xl font-extrabold tracking-tight text-foreground">
78
+ Welcome back, @{user.username}
79
+ </h1>
80
+ <p class="text-xs sm:text-sm text-muted-foreground">
81
+ Your personalized dashboard across the federated OpenCodeHub network.
82
+ </p>
83
+ </div>
84
+ </div>
85
+
86
+ <div class="flex items-center gap-3">
87
+ <a href="/explore">
88
+ <Button size="sm" variant="outline" className="gap-1.5 text-xs">
89
+ <Compass className="h-3.5 w-3.5" />
90
+ Explore Repos
91
+ </Button>
92
+ </a>
93
+ <a href="/instances/submit">
94
+ <Button size="sm" className="gap-1.5 text-xs bg-primary hover:bg-primary/90">
95
+ <Sparkles className="h-3.5 w-3.5" />
96
+ Submit Node
97
+ </Button>
98
+ </a>
99
+ </div>
100
+ </div>
101
+
102
+ <!-- 2-Column Grid: Starred & Following -->
103
+ <div class="grid lg:grid-cols-2 gap-8">
104
+
105
+ <!-- Starred Repositories Section -->
106
+ <div class="space-y-4">
107
+ <div class="flex items-center justify-between">
108
+ <h2 class="text-lg font-bold text-foreground flex items-center gap-2">
109
+ <Star className="h-5 w-5 text-amber-400 fill-amber-400" />
110
+ Starred Repositories ({starredRepos.length})
111
+ </h2>
112
+ </div>
113
+
114
+ <div class="space-y-3">
115
+ {starredRepos.length === 0 ? (
116
+ <div class="rounded-2xl border border-dashed border-border/80 bg-card/30 p-8 text-center space-y-2">
117
+ <p class="text-sm font-semibold text-foreground">No starred repositories yet</p>
118
+ <p class="text-xs text-muted-foreground">Browse the community network to star projects you like.</p>
119
+ <div class="pt-2">
120
+ <a href="/explore"><Button size="sm" variant="outline" className="text-xs">Explore Repos</Button></a>
121
+ </div>
122
+ </div>
123
+ ) : (
124
+ starredRepos.map((repo: any) => (
125
+ <div class="rounded-2xl border border-border/60 bg-card/50 p-4 hover:border-primary/40 transition-all flex items-center justify-between gap-3 shadow-sm">
126
+ <div class="min-w-0 space-y-1">
127
+ <div class="flex items-center gap-2">
128
+ <a href={`/r/${repo.instance?.name || 'node'}/${repo.fullName}`} class="font-bold text-sm text-primary hover:underline truncate">
129
+ {repo.fullName}
130
+ </a>
131
+ {repo.instance?.name && (
132
+ <span class="text-[10px] px-2 py-0.5 rounded bg-muted text-muted-foreground font-medium">
133
+ {repo.instance.name}
134
+ </span>
135
+ )}
136
+ </div>
137
+ <p class="text-xs text-muted-foreground line-clamp-1">
138
+ {repo.description || "No description"}
139
+ </p>
140
+ </div>
141
+
142
+ <div class="flex items-center gap-3 text-xs text-muted-foreground shrink-0">
143
+ <span class="flex items-center gap-1 font-medium"><Star className="h-3 w-3 text-amber-400 fill-amber-400" /> {repo.starCount || 0}</span>
144
+ <a href={`/r/${repo.instance?.name || 'node'}/${repo.fullName}`}>
145
+ <Button size="sm" variant="ghost" className="h-8 px-2 text-xs">View</Button>
146
+ </a>
147
+ </div>
148
+ </div>
149
+ ))
150
+ )}
151
+ </div>
152
+ </div>
153
+
154
+ <!-- Followed Profiles Section -->
155
+ <div class="space-y-4">
156
+ <div class="flex items-center justify-between">
157
+ <h2 class="text-lg font-bold text-foreground flex items-center gap-2">
158
+ <Users className="h-5 w-5 text-purple-400" />
159
+ Following ({followedProfiles.length})
160
+ </h2>
161
+ </div>
162
+
163
+ <div class="space-y-3">
164
+ {followedProfiles.length === 0 ? (
165
+ <div class="rounded-2xl border border-dashed border-border/80 bg-card/30 p-8 text-center space-y-2">
166
+ <p class="text-sm font-semibold text-foreground">Not following anyone yet</p>
167
+ <p class="text-xs text-muted-foreground">Follow developers across federated nodes to keep up with their public projects.</p>
168
+ <div class="pt-2">
169
+ <a href="/explore"><Button size="sm" variant="outline" className="text-xs">Find Developers</Button></a>
170
+ </div>
171
+ </div>
172
+ ) : (
173
+ followedProfiles.map((profile: any) => (
174
+ <div class="rounded-2xl border border-border/60 bg-card/50 p-4 hover:border-primary/40 transition-all flex items-center justify-between gap-3 shadow-sm">
175
+ <div class="flex items-center gap-3 min-w-0">
176
+ <img
177
+ src={profile.avatarUrl || `https://api.dicebear.com/7.x/initials/svg?seed=${profile.username}`}
178
+ alt={profile.username}
179
+ class="h-9 w-9 rounded-full bg-muted border border-border/40 object-cover shrink-0"
180
+ />
181
+ <div class="min-w-0">
182
+ <a href={`/u/${profile.username}`} class="font-bold text-sm text-foreground hover:text-primary truncate block">
183
+ @{profile.username}
184
+ </a>
185
+ <span class="text-xs text-muted-foreground truncate block">
186
+ {profile.instance?.name || "Federated Node"}
187
+ </span>
188
+ </div>
189
+ </div>
190
+
191
+ <a href={`/u/${profile.username}`} class="shrink-0">
192
+ <Button size="sm" variant="outline" className="text-xs h-8">
193
+ Profile
194
+ </Button>
195
+ </a>
196
+ </div>
197
+ ))
198
+ )}
199
+ </div>
200
+ </div>
201
+
202
+ </div>
203
+ </main>
204
+</BaseLayout>
0
205
▼community/src/pages/explore.astro
+397-0
1
+---
2
+import BaseLayout from "../layouts/BaseLayout.astro";
3
+import { Header } from "../components/layout/Header";
4
+import { Input } from "../components/ui/input";
5
+import { Button } from "../components/ui/button";
6
+import { Card, CardContent } from "../components/ui/card";
7
+import { Badge } from "../components/ui/badge";
8
+import {
9
+ Search, Filter, Star, GitFork, BookOpen, Copy, Globe, ExternalLink,
10
+ Users, Sparkles, Code2, X, TrendingUp, Clock, Hash, ArrowUpDown
11
+} from "lucide-react";
12
+import { verifyToken } from "../lib/auth";
13
+import { getDb } from "../lib/db";
14
+import { desc, like, or, eq, sql, and, inArray } from "drizzle-orm";
15
+import { cachedRepos, cachedUsers, instances } from "../lib/db/schema";
16
+
17
+const token = Astro.cookies.get("community_session")?.value;
18
+const user = token ? await verifyToken(token) : null;
19
+
20
+const q = Astro.url.searchParams.get("q") || "";
21
+const selectedLang = Astro.url.searchParams.get("lang") || "";
22
+const selectedTopic = Astro.url.searchParams.get("topic") || "";
23
+const sortBy = Astro.url.searchParams.get("sort") || "stars";
24
+const page = Math.max(1, parseInt(Astro.url.searchParams.get("page") || "1"));
25
+const perPage = 20;
26
+const offset = (page - 1) * perPage;
27
+
28
+const db = getDb() as any;
29
+
30
+// Dynamic language list from actual data
31
+const allLangs = await db
32
+ .select({ language: cachedRepos.language, count: sql<number>`count(*)` })
33
+ .from(cachedRepos)
34
+ .where(sql`${cachedRepos.language} IS NOT NULL AND ${cachedRepos.language} != ''`)
35
+ .groupBy(cachedRepos.language)
36
+ .orderBy(desc(sql`count(*)`))
37
+ .limit(15)
38
+ .catch(() => []);
39
+const popularLanguages = allLangs.filter((l: any) => l.language).map((l: any) => l.language);
40
+
41
+// Dynamic topic list from actual data
42
+const allTopicsRaw = await db.select({ topics: cachedRepos.topics }).from(cachedRepos).where(sql`${cachedRepos.topics} IS NOT NULL AND ${cachedRepos.topics} != '[]'`).catch(() => []);
43
+const topicCounts = new Map<string, number>();
44
+for (const row of allTopicsRaw) {
45
+ try {
46
+ const parsed = typeof row.topics === "string" ? JSON.parse(row.topics) : row.topics;
47
+ if (Array.isArray(parsed)) {
48
+ for (const t of parsed) {
49
+ const topic = String(t).trim().toLowerCase();
50
+ if (topic) topicCounts.set(topic, (topicCounts.get(topic) || 0) + 1);
51
+ }
52
+ }
53
+ } catch {}
54
+}
55
+const popularTopics = [...topicCounts.entries()]
56
+ .sort((a, b) => b[1] - a[1])
57
+ .slice(0, 12)
58
+ .map(([topic, count]) => ({ topic, count }));
59
+
60
+const langColors: Record<string, string> = {
61
+ TypeScript: "#3178c6", JavaScript: "#f1e05a", Python: "#3572A5", Go: "#00ADD8",
62
+ Rust: "#dea584", Java: "#b07219", "C++": "#f34b7d", C: "#555555", Ruby: "#701516",
63
+ PHP: "#4F5D95", Swift: "#F05138", Kotlin: "#A97BFF", Dart: "#00B4AB",
64
+ Shell: "#89e051", HTML: "#e34c26", CSS: "#563d7c", Vue: "#41b883", Astro: "#ff5a03",
65
+ Scala: "#c22d40", Haskell: "#5e5086", Elixir: "#6e4a7e", Lua: "#000080",
66
+ R: "#198CE7", MATLAB: "#e16737", Julia: "#a270ba", Zig: "#ec915c",
67
+};
68
+
69
+// Build where clause — fix: combine lang+text properly
70
+const searchPattern = `%${q}%`;
71
+const conditions = [];
72
+if (q) {
73
+ conditions.push(or(like(cachedRepos.fullName, searchPattern), like(cachedRepos.description, searchPattern)));
74
+}
75
+if (selectedLang) {
76
+ conditions.push(eq(cachedRepos.language, selectedLang));
77
+}
78
+if (selectedTopic) {
79
+ conditions.push(sql`${cachedRepos.topics} LIKE ${`%${selectedTopic.toLowerCase()}%`}`);
80
+}
81
+
82
+const repoWhere = conditions.length > 0
83
+ ? conditions.length === 1 ? conditions[0] : and(...conditions)
84
+ : undefined;
85
+
86
+// Sorting
87
+let orderBy;
88
+switch (sortBy) {
89
+ case "newest": orderBy = [desc(cachedRepos.cachedAt)]; break;
90
+ case "updated": orderBy = [desc(cachedRepos.updatedAt)]; break;
91
+ case "forks": orderBy = [desc(cachedRepos.forkCount), desc(cachedRepos.starCount)]; break;
92
+ case "stars": default: orderBy = [desc(cachedRepos.starCount), desc(cachedRepos.updatedAt)]; break;
93
+}
94
+
95
+// Fetch repos with count
96
+const [repos, countResult] = await Promise.all([
97
+ db.query.cachedRepos.findMany({
98
+ where: repoWhere,
99
+ orderBy,
100
+ limit: perPage,
101
+ offset,
102
+ with: { instance: true },
103
+ }).catch(() => []),
104
+ db.select({ count: sql<number>`count(*)` }).from(cachedRepos).where(repoWhere).then((r: any) => r[0]?.count || 0).catch(() => 0),
105
+]);
106
+const totalPages = Math.ceil(countResult / perPage);
107
+
108
+// Fetch profiles
109
+const profiles = await db.query.cachedUsers.findMany({
110
+ where: q ? or(like(cachedUsers.username, searchPattern), like(cachedUsers.displayName, searchPattern)) : undefined,
111
+ orderBy: [desc(cachedUsers.cachedAt)],
112
+ limit: 6,
113
+ with: { instance: true },
114
+}).catch(() => []);
115
+
116
+// Network stats
117
+const [totalRepos, totalUsers, totalInstances] = await Promise.all([
118
+ db.select({ count: sql<number>`count(*)` }).from(cachedRepos).then((r: any) => r[0]?.count || 0).catch(() => 0),
119
+ db.select({ count: sql<number>`count(*)` }).from(cachedUsers).then((r: any) => r[0]?.count || 0).catch(() => 0),
120
+ db.select({ count: sql<number>`count(*)` }).from(instances).then((r: any) => r[0]?.count || 0).catch(() => 0),
121
+]);
122
+
123
+function buildUrl(params: Record<string, string>) {
124
+ const sp = new URLSearchParams();
125
+ if (q) sp.set("q", q);
126
+ if (selectedLang) sp.set("lang", selectedLang);
127
+ if (selectedTopic) sp.set("topic", selectedTopic);
128
+ if (sortBy !== "stars") sp.set("sort", sortBy);
129
+ for (const [k, v] of Object.entries(params)) {
130
+ if (v === "") sp.delete(k); else sp.set(k, v);
131
+ }
132
+ const qs = sp.toString();
133
+ return `/explore${qs ? `?${qs}` : ""}`;
134
+}
135
+---
136
+<BaseLayout title="Explore — OpenCodeHub Community">
137
+ <Header client:load user={user} />
138
+
139
+ <main class="container px-4 sm:px-8 py-8 sm:py-12 space-y-8">
140
+
141
+ <!-- Header & Search -->
142
+ <div class="flex flex-col md:flex-row md:items-center justify-between gap-6 pb-6 border-b border-border/50">
143
+ <div class="space-y-1">
144
+ <h1 class="text-3xl font-extrabold tracking-tight text-foreground flex items-center gap-2.5">
145
+ <BookOpen className="h-7 w-7 text-primary" />
146
+ Explore the Network
147
+ </h1>
148
+ <p class="text-sm text-muted-foreground">
149
+ Discover public projects and developers from {totalInstances} federated {totalInstances === 1 ? "node" : "nodes"} &middot; {totalRepos} repos &middot; {totalUsers} users
150
+ </p>
151
+ </div>
152
+ <form class="flex items-center gap-2 w-full md:w-auto" action="/explore" method="get">
153
+ {selectedLang && <input type="hidden" name="lang" value={selectedLang} />}
154
+ {selectedTopic && <input type="hidden" name="topic" value={selectedTopic} />}
155
+ {sortBy !== "stars" && <input type="hidden" name="sort" value={sortBy} />}
156
+ <div class="relative flex-1 md:w-80">
157
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
158
+ <Input name="q" defaultValue={q} placeholder="Search repos, descriptions..." className="pl-9 pr-8 bg-card/60 border-border/60" id="search" />
159
+ {q && (
160
+ <a href={buildUrl({ q: "" })} class="absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground">
161
+ <X className="h-3.5 w-3.5" />
162
+ </a>
163
+ )}
164
+ </div>
165
+ <Button type="submit" size="sm" className="bg-primary hover:bg-primary/90 text-xs font-semibold px-4 h-9">Search</Button>
166
+ </form>
167
+ </div>
168
+
169
+ <!-- Language Filters -->
170
+ {popularLanguages.length > 0 && (
171
+ <div class="flex items-center gap-2 overflow-x-auto pb-2 text-xs">
172
+ <span class="text-muted-foreground font-medium flex items-center gap-1 shrink-0 mr-1">
173
+ <Code2 className="h-3.5 w-3.5" /> Languages:
174
+ </span>
175
+ <a href={buildUrl({ lang: "" })} class={`px-3 py-1.5 rounded-full border transition-colors shrink-0 font-medium ${!selectedLang ? "bg-primary text-primary-foreground border-primary" : "bg-card/60 text-muted-foreground hover:text-foreground border-border/60 hover:bg-accent"}`}>All</a>
176
+ {popularLanguages.slice(0, 10).map((lang: string) => {
177
+ const isActive = selectedLang.toLowerCase() === lang.toLowerCase();
178
+ return (
179
+ <a href={buildUrl({ lang: isActive ? "" : lang })} class={`px-3 py-1.5 rounded-full border transition-colors shrink-0 flex items-center gap-1.5 font-medium ${isActive ? "bg-primary text-primary-foreground border-primary" : "bg-card/60 text-muted-foreground hover:text-foreground border-border/60 hover:bg-accent"}`}>
180
+ <span class="h-2 w-2 rounded-full" style={`background-color: ${langColors[lang] || "#888"}`}></span>
181
+ {lang}
182
+ </a>
183
+ );
184
+ })}
185
+ </div>
186
+ )}
187
+
188
+ <!-- Topic Filters -->
189
+ {popularTopics.length > 0 && (
190
+ <div class="flex items-center gap-2 overflow-x-auto pb-2 text-xs">
191
+ <span class="text-muted-foreground font-medium flex items-center gap-1 shrink-0 mr-1">
192
+ <Hash className="h-3.5 w-3.5" /> Topics:
193
+ </span>
194
+ {popularTopics.map(({ topic, count }) => {
195
+ const isActive = selectedTopic.toLowerCase() === topic.toLowerCase();
196
+ return (
197
+ <a href={buildUrl({ topic: isActive ? "" : topic })} class={`px-3 py-1.5 rounded-full border transition-colors shrink-0 font-medium ${isActive ? "bg-primary text-primary-foreground border-primary" : "bg-card/60 text-muted-foreground hover:text-foreground border-border/60 hover:bg-accent"}`}>
198
+ #{topic} <span class="text-muted-foreground/60 ml-0.5">{count}</span>
199
+ </a>
200
+ );
201
+ })}
202
+ </div>
203
+ )}
204
+
205
+ <!-- Active Filters -->
206
+ {(q || selectedLang || selectedTopic) && (
207
+ <div class="flex items-center gap-2 flex-wrap text-xs">
208
+ <span class="text-muted-foreground">Active filters:</span>
209
+ {q && <Badge variant="secondary" class="gap-1">"{q}" <a href={buildUrl({ q: "" })}><X className="h-3 w-3" /></a></Badge>}
210
+ {selectedLang && <Badge variant="secondary" class="gap-1">{selectedLang} <a href={buildUrl({ lang: "" })}><X className="h-3 w-3" /></a></Badge>}
211
+ {selectedTopic && <Badge variant="secondary" class="gap-1">#{selectedTopic} <a href={buildUrl({ topic: "" })}><X className="h-3 w-3" /></a></Badge>}
212
+ <a href="/explore" class="text-muted-foreground hover:text-foreground ml-2">Clear all</a>
213
+ </div>
214
+ )}
215
+
216
+ <!-- Sort Bar -->
217
+ <div class="flex items-center justify-between">
218
+ <h2 class="text-lg font-bold text-foreground flex items-center gap-2">
219
+ {q ? `Results for "${q}"` : selectedLang ? `${selectedLang} Repos` : selectedTopic ? `#${selectedTopic}` : sortBy === "newest" ? "Recently Added" : sortBy === "updated" ? "Recently Updated" : "Trending Repositories"}
220
+ <Badge variant="secondary" className="text-[11px]">{countResult} {countResult === 1 ? "repo" : "repos"}</Badge>
221
+ </h2>
222
+ <div class="flex items-center gap-1 text-xs">
223
+ {[
224
+ { key: "stars", label: "Stars", icon: Star },
225
+ { key: "updated", label: "Updated", icon: Clock },
226
+ { key: "newest", label: "Newest", icon: TrendingUp },
227
+ { key: "forks", label: "Forks", icon: GitFork },
228
+ ].map(({ key, label, icon: Icon }) => (
229
+ <a href={buildUrl({ sort: key })} class={`px-2.5 py-1.5 rounded-md flex items-center gap-1 font-medium transition-colors ${sortBy === key ? "bg-primary/10 text-primary" : "text-muted-foreground hover:text-foreground hover:bg-accent"}`}>
230
+ <Icon className="h-3 w-3" /> {label}
231
+ </a>
232
+ ))}
233
+ </div>
234
+ </div>
235
+
236
+ <!-- Main Content -->
237
+ <div class="grid lg:grid-cols-3 gap-8">
238
+ <div class="lg:col-span-2 space-y-3.5">
239
+ {repos.length === 0 ? (
240
+ <div class="rounded-2xl border border-dashed border-border/80 bg-card/30 p-10 text-center space-y-3">
241
+ <div class="h-12 w-12 rounded-2xl bg-primary/10 text-primary flex items-center justify-center mx-auto"><BookOpen className="h-6 w-6" /></div>
242
+ <h3 class="font-semibold text-base">No repositories found</h3>
243
+ <p class="text-xs text-muted-foreground max-w-sm mx-auto">
244
+ {q || selectedLang || selectedTopic ? "Try different search terms or remove filters." : "Submit your OpenCodeHub instance to start aggregating repos."}
245
+ </p>
246
+ <a href="/instances/submit"><Button size="sm" className="gap-1.5 text-xs"><Sparkles className="h-3.5 w-3.5" /> Submit Node</Button></a>
247
+ </div>
248
+ ) : (
249
+ repos.map((repo: any) => {
250
+ const langColor = langColors[repo.language] || "#888";
251
+ let topics: string[] = [];
252
+ try { topics = typeof repo.topics === "string" ? JSON.parse(repo.topics) : repo.topics || []; } catch {}
253
+ return (
254
+ <div class="rounded-2xl border border-border/60 bg-card/50 p-5 hover:border-primary/40 hover:bg-card/80 transition-all shadow-sm">
255
+ <div class="flex items-start justify-between gap-3">
256
+ <div class="flex-1 min-w-0 space-y-1.5">
257
+ <div class="flex items-center gap-2 flex-wrap">
258
+ <a href={`/r/${repo.instance?.name || "node"}/${repo.fullName}`} class="font-bold text-base text-primary hover:underline truncate">{repo.fullName}</a>
259
+ <Badge variant="outline" className="text-[10px] uppercase font-semibold tracking-wider">{repo.visibility || "public"}</Badge>
260
+ {repo.instance?.name && <a href={`/instances/${repo.instanceId}`} class="text-[11px] font-medium text-muted-foreground hover:text-foreground inline-flex items-center gap-1 px-2 py-0.5 rounded bg-muted/60"><Globe className="h-2.5 w-2.5 text-primary" />{repo.instance.name}</a>}
261
+ </div>
262
+ <p class="text-xs sm:text-sm text-muted-foreground line-clamp-2 leading-relaxed">{repo.description || "No description provided."}</p>
263
+ {topics.length > 0 && (
264
+ <div class="flex flex-wrap gap-1.5 pt-1">
265
+ {topics.slice(0, 5).map((t: string) => (
266
+ <a href={buildUrl({ topic: t })} class="text-[10px] px-2 py-0.5 rounded-full bg-primary/5 text-primary border border-primary/10 hover:bg-primary/10 transition-colors">#{t}</a>
267
+ ))}
268
+ </div>
269
+ )}
270
+ <div class="flex flex-wrap items-center gap-4 pt-2 text-xs text-muted-foreground">
271
+ {repo.language && <span class="flex items-center gap-1.5 font-medium text-foreground"><span class="h-2.5 w-2.5 rounded-full" style={`background-color: ${langColor}`}></span>{repo.language}</span>}
272
+ <span class="flex items-center gap-1"><Star className="h-3.5 w-3.5 text-amber-400 fill-amber-400" /><span class="font-medium">{repo.starCount || 0}</span></span>
273
+ <span class="flex items-center gap-1"><GitFork className="h-3.5 w-3.5" /><span class="font-medium">{repo.forkCount || 0}</span></span>
274
+ {repo.updatedAt && <span class="text-muted-foreground/60">Updated {new Date(repo.updatedAt).toLocaleDateString()}</span>}
275
+ </div>
276
+ </div>
277
+ <div class="flex items-center gap-1.5 shrink-0">
278
+ <button class="star-btn inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-border/60 bg-muted/40 hover:bg-primary/10 hover:border-primary/40 hover:text-primary transition-colors text-xs font-medium" data-repo-id={repo.id} data-full-name={repo.fullName}>
279
+ <Star className="h-3.5 w-3.5 text-amber-400" /><span>Star</span>
280
+ </button>
281
+ <button class="clone-btn inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-border/60 bg-muted/40 hover:bg-accent hover:text-foreground transition-colors text-xs font-medium" data-clone-url={repo.httpCloneUrl}>
282
+ <Copy className="h-3.5 w-3.5" /><span class="hidden sm:inline">Clone</span>
283
+ </button>
284
+ </div>
285
+ </div>
286
+ </div>
287
+ );
288
+ })
289
+ )}
290
+
291
+ <!-- Pagination -->
292
+ {totalPages > 1 && (
293
+ <div class="flex items-center justify-center gap-2 pt-4">
294
+ {page > 1 && <a href={buildUrl({ page: String(page - 1) })} class="px-3 py-1.5 rounded-md border text-xs font-medium hover:bg-accent">Prev</a>}
295
+ {Array.from({ length: Math.min(totalPages, 7) }, (_, i) => {
296
+ const p = i + 1;
297
+ return <a href={buildUrl({ page: String(p) })} class={`px-3 py-1.5 rounded-md text-xs font-medium ${p === page ? "bg-primary text-primary-foreground" : "border hover:bg-accent"}`}>{p}</a>;
298
+ })}
299
+ {page < totalPages && <a href={buildUrl({ page: String(page + 1) })} class="px-3 py-1.5 rounded-md border text-xs font-medium hover:bg-accent">Next</a>}
300
+ </div>
301
+ )}
302
+ </div>
303
+
304
+ <!-- Sidebar -->
305
+ <div class="space-y-6">
306
+ <!-- Network Stats -->
307
+ <Card className="border-border/60 bg-card/50">
308
+ <CardContent className="p-5 space-y-3">
309
+ <h3 class="text-sm font-bold flex items-center gap-2"><Globe className="h-4 w-4 text-primary" /> Network Stats</h3>
310
+ <div class="grid grid-cols-3 gap-3 text-center">
311
+ <div class="p-2 rounded-lg bg-muted/40"><div class="text-lg font-bold text-foreground">{totalInstances}</div><div class="text-[10px] text-muted-foreground">Nodes</div></div>
312
+ <div class="p-2 rounded-lg bg-muted/40"><div class="text-lg font-bold text-foreground">{totalRepos}</div><div class="text-[10px] text-muted-foreground">Repos</div></div>
313
+ <div class="p-2 rounded-lg bg-muted/40"><div class="text-lg font-bold text-foreground">{totalUsers}</div><div class="text-[10px] text-muted-foreground">Users</div></div>
314
+ </div>
315
+ </CardContent>
316
+ </Card>
317
+
318
+ <!-- Profiles -->
319
+ <Card className="border-border/60 bg-card/50">
320
+ <CardContent className="p-5 space-y-4">
321
+ <h3 class="text-sm font-bold flex items-center gap-2"><Users className="h-4 w-4 text-purple-400" /> {q ? "Matching Profiles" : "Community Profiles"}</h3>
322
+ {profiles.length === 0 ? (
323
+ <p class="text-xs text-muted-foreground">Profiles appear as nodes synchronize.</p>
324
+ ) : (
325
+ <div class="space-y-3">
326
+ {profiles.map((profile: any) => (
327
+ <div class="flex items-center justify-between gap-3 p-2 rounded-xl hover:bg-muted/40 transition-colors">
328
+ <div class="flex items-center gap-2.5 min-w-0">
329
+ <img src={profile.avatarUrl || `https://api.dicebear.com/7.x/initials/svg?seed=${profile.username}`} alt="" class="h-8 w-8 rounded-full bg-muted border border-border/40 shrink-0" />
330
+ <div class="min-w-0">
331
+ <a href={`/u/${profile.username}`} class="text-xs font-semibold text-foreground hover:text-primary truncate block">@{profile.username}</a>
332
+ <span class="text-[11px] text-muted-foreground truncate block">{profile.instance?.name || "Federated User"}</span>
333
+ </div>
334
+ </div>
335
+ <button class="follow-btn shrink-0 text-[11px] font-medium px-2.5 py-1 rounded-md border border-border/60 hover:bg-primary hover:text-primary-foreground hover:border-primary transition-colors" data-username={profile.username}>Follow</button>
336
+ </div>
337
+ ))}
338
+ </div>
339
+ )}
340
+ </CardContent>
341
+ </Card>
342
+
343
+ <!-- How to Contribute -->
344
+ <Card className="border-primary/20 bg-gradient-to-br from-primary/5 via-card to-card">
345
+ <CardContent className="p-5 space-y-3">
346
+ <h4 class="text-sm font-bold flex items-center gap-2"><Code2 className="h-4 w-4 text-primary" /> How to Contribute</h4>
347
+ <ol class="text-xs text-muted-foreground space-y-2 list-decimal list-inside leading-relaxed">
348
+ <li>Copy the project's <code class="bg-muted px-1 py-0.5 rounded">httpCloneUrl</code></li>
349
+ <li>Clone locally to your own workstation</li>
350
+ <li>Push to your personal node and open a cross-node PR!</li>
351
+ </ol>
352
+ <a href="/instances" class="text-xs text-primary font-medium hover:underline inline-flex items-center gap-1 pt-1">Browse all nodes <ExternalLink className="h-3 w-3" /></a>
353
+ </CardContent>
354
+ </Card>
355
+ </div>
356
+ </div>
357
+ </main>
358
+
359
+ <script>
360
+ import { toast } from "sonner";
361
+ document.querySelectorAll('.clone-btn').forEach(btn => {
362
+ btn.addEventListener('click', (e) => {
363
+ const url = (e.currentTarget as HTMLButtonElement).dataset.cloneUrl;
364
+ if (url) { navigator.clipboard.writeText(url); toast.success("Clone URL copied!"); }
365
+ });
366
+ });
367
+ document.querySelectorAll('.star-btn').forEach(btn => {
368
+ btn.addEventListener('click', async (e) => {
369
+ const t = e.currentTarget as HTMLButtonElement;
370
+ try {
371
+ const res = await fetch('/api/stars', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ repoId: t.dataset.repoId, fullName: t.dataset.fullName }) });
372
+ const data = await res.json();
373
+ if (data.success) {
374
+ t.classList.toggle("bg-primary/20", data.data?.starred);
375
+ t.classList.toggle("text-primary", data.data?.starred);
376
+ toast.success(data.data?.starred ? `Starred ${t.dataset.fullName}` : `Unstarred ${t.dataset.fullName}`);
377
+ } else { toast.error(data.error?.message || "Failed"); }
378
+ } catch { toast.error("Network error"); }
379
+ });
380
+ });
381
+ document.querySelectorAll('.follow-btn').forEach(btn => {
382
+ btn.addEventListener('click', async (e) => {
383
+ const t = e.currentTarget as HTMLButtonElement;
384
+ try {
385
+ const res = await fetch('/api/follows', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ followeeType: 'user', followeeId: t.dataset.username }) });
386
+ const data = await res.json();
387
+ if (data.success) {
388
+ const f = data.data?.following;
389
+ t.textContent = f ? "Following" : "Follow";
390
+ t.classList.toggle("bg-primary", f);
391
+ t.classList.toggle("text-primary-foreground", f);
392
+ } else { toast.error(data.error?.message || "Failed"); }
393
+ } catch { toast.error("Network error"); }
394
+ });
395
+ });
396
+ </script>
397
+</BaseLayout>
0
398
▼community/src/pages/index.astro
+248-0
1
+---
2
+import BaseLayout from "../layouts/BaseLayout.astro";
3
+import { Header } from "../components/layout/Header";
4
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "../components/ui/card";
5
+import { Button } from "../components/ui/button";
6
+import { Badge } from "../components/ui/badge";
7
+import {
8
+ Globe,
9
+ Star,
10
+ Users,
11
+ TrendingUp,
12
+ Zap,
13
+ Server,
14
+ GitFork,
15
+ ShieldCheck,
16
+ Network,
17
+ ArrowRight,
18
+ Sparkles,
19
+ Layers,
20
+ Code2
21
+} from "lucide-react";
22
+import { verifyToken } from "../lib/auth";
23
+import { getDb } from "../lib/db";
24
+import { instances, cachedRepos, communityUsers } from "../lib/db/schema";
25
+import { sql, desc } from "drizzle-orm";
26
+
27
+const token = Astro.cookies.get("community_session")?.value;
28
+const user = token ? await verifyToken(token) : null;
29
+
30
+const db = getDb() as any;
31
+const instancesCount = await db.select({ count: sql<number>`count(*)` }).from(instances).then((res: any) => res[0].count).catch(() => 0);
32
+const reposCount = await db.select({ count: sql<number>`count(*)` }).from(cachedRepos).then((res: any) => res[0].count).catch(() => 0);
33
+const usersCount = await db.select({ count: sql<number>`count(*)` }).from(communityUsers).then((res: any) => res[0].count).catch(() => 0);
34
+
35
+// Fetch a few featured repos for the hero showcase
36
+const featuredRepos = await db.query.cachedRepos.findMany({
37
+ orderBy: [desc(cachedRepos.starCount), desc(cachedRepos.updatedAt)],
38
+ limit: 3,
39
+ with: { instance: true }
40
+}).catch(() => []);
41
+---
42
+<BaseLayout title="OpenCodeHub Community — Decentralized Code Aggregator">
43
+ <Header client:load user={user} />
44
+ <main class="space-y-16 sm:space-y-24">
45
+
46
+ <!-- Hero Section -->
47
+ <section class="relative overflow-hidden pt-12 pb-20 sm:pt-20 sm:pb-28">
48
+ {/* Ambient background glow */}
49
+ <div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[350px] bg-gradient-to-tr from-primary/20 via-indigo-500/15 to-purple-600/10 blur-[120px] pointer-events-none -z-10"></div>
50
+
51
+ <div class="container px-4 sm:px-8 text-center space-y-8 max-w-4xl mx-auto">
52
+ <div class="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full bg-primary/10 border border-primary/20 text-primary text-xs font-medium backdrop-blur-sm shadow-sm animate-in fade-in zoom-in duration-500">
53
+ <Zap className="h-3.5 w-3.5 fill-primary" />
54
+ <span>Decentralized &amp; Self-Hosted Hub</span>
55
+ </div>
56
+
57
+ <h1 class="text-4xl sm:text-6xl font-extrabold tracking-tight leading-[1.15] text-foreground">
58
+ Discover Open Source Beyond <br />
59
+ <span class="bg-gradient-to-r from-blue-400 via-indigo-300 to-purple-400 bg-clip-text text-transparent">
60
+ Centralized Platforms
61
+ </span>
62
+ </h1>
63
+
64
+ <p class="text-base sm:text-xl text-muted-foreground max-w-2xl mx-auto leading-relaxed">
65
+ Aggregating public repositories, developers, and code from self-hosted OpenCodeHub nodes worldwide. Explore, star, follow, and federate without lock-in.
66
+ </p>
67
+
68
+ <!-- CTA Action Buttons -->
69
+ <div class="flex flex-wrap justify-center items-center gap-4 pt-2">
70
+ <a href="/explore">
71
+ <Button size="lg" className="gap-2 shadow-lg shadow-primary/25 bg-primary hover:bg-primary/90 text-sm font-semibold h-11 px-6">
72
+ <Globe className="h-4 w-4" />
73
+ Explore Repositories
74
+ </Button>
75
+ </a>
76
+ <a href="/instances/submit">
77
+ <Button size="lg" variant="outline" className="gap-2 border-border/80 hover:bg-card text-sm font-medium h-11 px-6">
78
+ <Server className="h-4 w-4 text-primary" />
79
+ Submit Your Node
80
+ </Button>
81
+ </a>
82
+ </div>
83
+
84
+ <!-- Live Global Metrics -->
85
+ <div class="grid grid-cols-3 gap-3 sm:gap-6 pt-10 max-w-2xl mx-auto">
86
+ <div class="rounded-2xl border border-border/50 bg-card/50 backdrop-blur-md p-4 sm:p-6 text-center hover:border-primary/40 transition-colors shadow-sm">
87
+ <div class="text-2xl sm:text-4xl font-extrabold text-foreground tracking-tight flex items-center justify-center gap-1">
88
+ <span class="bg-gradient-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent">
89
+ {instancesCount || 0}
90
+ </span>
91
+ </div>
92
+ <div class="text-xs sm:text-sm font-medium text-muted-foreground mt-1 flex items-center justify-center gap-1">
93
+ <Server className="h-3 w-3 text-primary" />
94
+ Active Nodes
95
+ </div>
96
+ </div>
97
+
98
+ <div class="rounded-2xl border border-border/50 bg-card/50 backdrop-blur-md p-4 sm:p-6 text-center hover:border-primary/40 transition-colors shadow-sm">
99
+ <div class="text-2xl sm:text-4xl font-extrabold text-foreground tracking-tight">
100
+ <span class="bg-gradient-to-r from-indigo-400 to-purple-400 bg-clip-text text-transparent">
101
+ {reposCount || 0}
102
+ </span>
103
+ </div>
104
+ <div class="text-xs sm:text-sm font-medium text-muted-foreground mt-1 flex items-center justify-center gap-1">
105
+ <Code2 className="h-3 w-3 text-indigo-400" />
106
+ Public Repos
107
+ </div>
108
+ </div>
109
+
110
+ <div class="rounded-2xl border border-border/50 bg-card/50 backdrop-blur-md p-4 sm:p-6 text-center hover:border-primary/40 transition-colors shadow-sm">
111
+ <div class="text-2xl sm:text-4xl font-extrabold text-foreground tracking-tight">
112
+ <span class="bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent">
113
+ {usersCount || 0}
114
+ </span>
115
+ </div>
116
+ <div class="text-xs sm:text-sm font-medium text-muted-foreground mt-1 flex items-center justify-center gap-1">
117
+ <Users className="h-3 w-3 text-purple-400" />
118
+ Community Users
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ </section>
124
+
125
+ <!-- Why Federated / Features Grid -->
126
+ <section class="container px-4 sm:px-8">
127
+ <div class="text-center space-y-3 mb-12 max-w-2xl mx-auto">
128
+ <h2 class="text-2xl sm:text-3xl font-bold tracking-tight">Why the OpenCodeHub Network?</h2>
129
+ <p class="text-sm sm:text-base text-muted-foreground">
130
+ A truly open collaboration hub designed for self-hosters, sovereign developers, and private organizations.
131
+ </p>
132
+ </div>
133
+
134
+ <div class="grid md:grid-cols-3 gap-6">
135
+ <div class="rounded-2xl border border-border/50 bg-card/40 p-6 space-y-4 hover:border-primary/30 transition-all hover:-translate-y-1 shadow-sm">
136
+ <div class="h-12 w-12 rounded-xl bg-primary/10 border border-primary/20 flex items-center justify-center text-primary">
137
+ <Network className="h-6 w-6" />
138
+ </div>
139
+ <h3 class="text-lg font-bold">Federated Discovery</h3>
140
+ <p class="text-sm text-muted-foreground leading-relaxed">
141
+ Run your own instance on bare-metal or cloud. Submit your URL once, and your public projects become discoverable globally while remaining entirely on your server.
142
+ </p>
143
+ </div>
144
+
145
+ <div class="rounded-2xl border border-border/50 bg-card/40 p-6 space-y-4 hover:border-primary/30 transition-all hover:-translate-y-1 shadow-sm">
146
+ <div class="h-12 w-12 rounded-xl bg-amber-500/10 border border-amber-500/20 flex items-center justify-center text-amber-500">
147
+ <Star className="h-6 w-6" />
148
+ </div>
149
+ <h3 class="text-lg font-bold">Global Social Graph</h3>
150
+ <p class="text-sm text-muted-foreground leading-relaxed">
151
+ Star repositories, follow profiles, and track contributions across different nodes. Your activity seamlessly aggregates into your unified Community dashboard.
152
+ </p>
153
+ </div>
154
+
155
+ <div class="rounded-2xl border border-border/50 bg-card/40 p-6 space-y-4 hover:border-primary/30 transition-all hover:-translate-y-1 shadow-sm">
156
+ <div class="h-12 w-12 rounded-xl bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center text-emerald-500">
157
+ <ShieldCheck className="h-6 w-6" />
158
+ </div>
159
+ <h3 class="text-lg font-bold">Privacy &amp; Sovereignty</h3>
160
+ <p class="text-sm text-muted-foreground leading-relaxed">
161
+ Only explicit <code>visibility=public</code> projects are indexed. Private repositories and credentials never leave your local infrastructure.
162
+ </p>
163
+ </div>
164
+ </div>
165
+ </section>
166
+
167
+ <!-- How Federation Works (4-step visual flow) -->
168
+ <section class="container px-4 sm:px-8">
169
+ <div class="rounded-3xl border border-border/60 bg-card/60 backdrop-blur-xl p-8 sm:p-12 relative overflow-hidden">
170
+ <div class="absolute -right-20 -bottom-20 w-80 h-80 bg-primary/10 blur-[100px] pointer-events-none"></div>
171
+
172
+ <div class="space-y-3 mb-10 text-center sm:text-left">
173
+ <Badge variant="outline" className="text-xs uppercase tracking-wider">Simple Architecture</Badge>
174
+ <h2 class="text-2xl sm:text-3xl font-bold tracking-tight">How It Works</h2>
175
+ <p class="text-sm text-muted-foreground max-w-xl">
176
+ From submitting your URL to global synchronization, the process is zero-configuration and automated.
177
+ </p>
178
+ </div>
179
+
180
+ <div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
181
+ <div class="space-y-3 relative">
182
+ <div class="h-10 w-10 rounded-xl bg-primary text-primary-foreground font-bold flex items-center justify-center text-sm shadow-md shadow-primary/20">
183
+ 1
184
+ </div>
185
+ <h4 class="font-semibold text-base">Submit Node</h4>
186
+ <p class="text-xs text-muted-foreground leading-relaxed">
187
+ Paste your instance URL (e.g. <code>https://code.example.com</code>) at the submission portal.
188
+ </p>
189
+ </div>
190
+
191
+ <div class="space-y-3 relative">
192
+ <div class="h-10 w-10 rounded-xl bg-indigo-500 text-white font-bold flex items-center justify-center text-sm shadow-md shadow-indigo-500/20">
193
+ 2
194
+ </div>
195
+ <h4 class="font-semibold text-base">Verification</h4>
196
+ <p class="text-xs text-muted-foreground leading-relaxed">
197
+ Our edge worker probes <code>/api/instance</code> to verify health and discover supported features.
198
+ </p>
199
+ </div>
200
+
201
+ <div class="space-y-3 relative">
202
+ <div class="h-10 w-10 rounded-xl bg-purple-500 text-white font-bold flex items-center justify-center text-sm shadow-md shadow-purple-500/20">
203
+ 3
204
+ </div>
205
+ <h4 class="font-semibold text-base">Continuous Sync</h4>
206
+ <p class="text-xs text-muted-foreground leading-relaxed">
207
+ Public repos, owner profiles, stars, and tags are cached in Turso and refreshed periodically.
208
+ </p>
209
+ </div>
210
+
211
+ <div class="space-y-3 relative">
212
+ <div class="h-10 w-10 rounded-xl bg-pink-500 text-white font-bold flex items-center justify-center text-sm shadow-md shadow-pink-500/20">
213
+ 4
214
+ </div>
215
+ <h4 class="font-semibold text-base">Collaborate</h4>
216
+ <p class="text-xs text-muted-foreground leading-relaxed">
217
+ Clone directly via <code>httpCloneUrl</code>, star locally, and open cross-node pull requests.
218
+ </p>
219
+ </div>
220
+ </div>
221
+ </div>
222
+ </section>
223
+
224
+ <!-- Bottom CTA Banner -->
225
+ <section class="container px-4 sm:px-8 pb-12">
226
+ <div class="rounded-3xl border border-primary/20 bg-gradient-to-r from-primary/10 via-indigo-500/10 to-purple-600/10 p-8 sm:p-12 text-center space-y-6 max-w-4xl mx-auto">
227
+ <h2 class="text-2xl sm:text-3xl font-extrabold tracking-tight">Ready to Connect Your Instance?</h2>
228
+ <p class="text-sm sm:text-base text-muted-foreground max-w-lg mx-auto">
229
+ Join the federated network today. It only takes 5 seconds to submit your URL and get listed.
230
+ </p>
231
+ <div class="flex flex-wrap justify-center gap-3">
232
+ <a href="/instances/submit">
233
+ <Button size="lg" className="gap-2 bg-primary hover:bg-primary/90 shadow-md shadow-primary/20">
234
+ <Sparkles className="h-4 w-4" />
235
+ Submit Instance Now
236
+ </Button>
237
+ </a>
238
+ <a href="/explore">
239
+ <Button size="lg" variant="outline">
240
+ Explore Network
241
+ </Button>
242
+ </a>
243
+ </div>
244
+ </div>
245
+ </section>
246
+
247
+ </main>
248
+</BaseLayout>
0
249
▼community/src/pages/instances/[id].astro
+227-0
1
+---
2
+import BaseLayout from "../../layouts/BaseLayout.astro";
3
+import { Header } from "../../components/layout/Header";
4
+import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card";
5
+import { Badge } from "../../components/ui/badge";
6
+import { Button } from "../../components/ui/button";
7
+import {
8
+ Globe,
9
+ Star,
10
+ GitFork,
11
+ ExternalLink,
12
+ ArrowLeft,
13
+ RefreshCw,
14
+ Server,
15
+ Copy,
16
+ BookOpen,
17
+ Activity,
18
+ Code2
19
+} from "lucide-react";
20
+import { verifyToken } from "../../lib/auth";
21
+import { getDb } from "../../lib/db";
22
+import { eq, desc } from "drizzle-orm";
23
+import { instances, cachedRepos } from "../../lib/db/schema";
24
+
25
+const token = Astro.cookies.get("community_session")?.value;
26
+const user = token ? await verifyToken(token) : null;
27
+const { id } = Astro.params;
28
+
29
+const db = getDb() as any;
30
+
31
+// Fetch Instance Details
32
+const instance = await db.query.instances.findFirst({
33
+ where: eq(instances.id, id as string)
34
+}).catch(() => null);
35
+
36
+// Fetch Repositories for this instance
37
+const repos = await db.query.cachedRepos.findMany({
38
+ where: eq(cachedRepos.instanceId, id as string),
39
+ orderBy: [desc(cachedRepos.starCount), desc(cachedRepos.updatedAt)],
40
+ limit: 50
41
+}).catch(() => []);
42
+
43
+let capabilitiesList: string[] = [];
44
+try {
45
+ if (instance?.capabilities) {
46
+ capabilitiesList = typeof instance.capabilities === "string" ? JSON.parse(instance.capabilities) : instance.capabilities;
47
+ }
48
+} catch {}
49
+---
50
+<BaseLayout title={`${instance?.name || 'Instance'} — OpenCodeHub Community`}>
51
+ <Header client:load user={user} />
52
+
53
+ <main class="container px-4 sm:px-8 py-8 sm:py-12 space-y-8">
54
+
55
+ <a href="/instances" class="inline-flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors">
56
+ <ArrowLeft className="h-3.5 w-3.5" /> Back to all instances
57
+ </a>
58
+
59
+ {!instance ? (
60
+ <div class="rounded-3xl border border-dashed border-border/80 bg-card/40 p-12 text-center space-y-3">
61
+ <h2 class="text-xl font-bold">Instance Not Found</h2>
62
+ <p class="text-xs text-muted-foreground">The requested node ID does not exist in the community database.</p>
63
+ <div class="pt-2">
64
+ <a href="/instances"><Button size="sm">Browse Nodes</Button></a>
65
+ </div>
66
+ </div>
67
+ ) : (
68
+ <>
69
+ <!-- Instance Hero Header -->
70
+ <div class="rounded-3xl border border-border/60 bg-card/60 backdrop-blur-xl p-6 sm:p-8 space-y-6">
71
+ <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
72
+ <div class="flex items-center gap-4">
73
+ <div class="h-14 w-14 rounded-2xl bg-primary/10 border border-primary/20 flex items-center justify-center text-primary shrink-0">
74
+ <Server className="h-7 w-7" />
75
+ </div>
76
+ <div>
77
+ <div class="flex items-center gap-2 flex-wrap">
78
+ <h1 class="text-2xl sm:text-3xl font-extrabold tracking-tight text-foreground">
79
+ {instance.name}
80
+ </h1>
81
+ <span class={`inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-semibold border ${
82
+ instance.status === "online"
83
+ ? "bg-emerald-500/10 text-emerald-400 border-emerald-500/20"
84
+ : "bg-destructive/10 text-destructive border-destructive/20"
85
+ }`}>
86
+ <span class={`h-1.5 w-1.5 rounded-full ${instance.status === "online" ? "bg-emerald-400 animate-pulse" : "bg-destructive"}`}></span>
87
+ <span class="capitalize">{instance.status || "pending"}</span>
88
+ </span>
89
+ </div>
90
+ <a href={instance.siteUrl} target="_blank" class="text-xs sm:text-sm text-muted-foreground hover:underline inline-flex items-center gap-1 mt-1">
91
+ {instance.siteUrl} <ExternalLink className="h-3 w-3" />
92
+ </a>
93
+ </div>
94
+ </div>
95
+
96
+ <!-- Actions -->
97
+ <div class="flex items-center gap-2 shrink-0">
98
+ <Button id="sync-btn" variant="outline" size="sm" className="gap-1.5 text-xs font-medium">
99
+ <RefreshCw className="h-3.5 w-3.5" id="sync-icon" />
100
+ Sync Node
101
+ </Button>
102
+ <a href={instance.siteUrl} target="_blank" rel="noreferrer">
103
+ <Button size="sm" className="gap-1.5 text-xs font-semibold bg-primary hover:bg-primary/90">
104
+ <ExternalLink className="h-3.5 w-3.5" />
105
+ Visit Instance
106
+ </Button>
107
+ </a>
108
+ </div>
109
+ </div>
110
+
111
+ <!-- Metadata Badges -->
112
+ <div class="flex flex-wrap items-center gap-2 pt-2 border-t border-border/50 text-xs">
113
+ {instance.version && (
114
+ <Badge variant="outline" className="font-mono text-[11px]">
115
+ v{instance.version}
116
+ </Badge>
117
+ )}
118
+ {capabilitiesList.map((cap: string) => (
119
+ <Badge variant="secondary" className="text-[11px] uppercase font-semibold">
120
+ {cap}
121
+ </Badge>
122
+ ))}
123
+ <span class="text-muted-foreground ml-auto text-xs flex items-center gap-1.5">
124
+ <Activity className="h-3.5 w-3.5 text-indigo-400" />
125
+ Last Synced: {instance.lastSyncAt ? new Date(instance.lastSyncAt).toLocaleString() : 'Never'}
126
+ </span>
127
+ </div>
128
+ </div>
129
+
130
+ <!-- Repositories Grid -->
131
+ <div class="space-y-4">
132
+ <div class="flex items-center justify-between">
133
+ <h2 class="text-xl font-bold flex items-center gap-2">
134
+ <BookOpen className="h-5 w-5 text-primary" />
135
+ Public Repositories ({repos.length})
136
+ </h2>
137
+ </div>
138
+
139
+ {repos.length === 0 ? (
140
+ <div class="rounded-2xl border border-dashed border-border/80 bg-card/30 p-10 text-center space-y-2">
141
+ <p class="text-sm font-semibold text-foreground">No repositories indexed yet</p>
142
+ <p class="text-xs text-muted-foreground">Click "Sync Node" above to fetch the latest public repositories from this server.</p>
143
+ </div>
144
+ ) : (
145
+ <div class="grid md:grid-cols-2 gap-4">
146
+ {repos.map((r: any) => (
147
+ <div class="rounded-2xl border border-border/60 bg-card/50 p-5 hover:border-primary/40 transition-all shadow-sm space-y-3">
148
+ <div class="flex items-start justify-between gap-2">
149
+ <div class="min-w-0">
150
+ <a href={`/r/${instance.name}/${r.fullName}`} class="font-bold text-base text-primary hover:underline truncate block">
151
+ {r.fullName}
152
+ </a>
153
+ <p class="text-xs text-muted-foreground mt-1 line-clamp-2">
154
+ {r.description || "No description provided."}
155
+ </p>
156
+ </div>
157
+ <Badge variant="outline" className="text-[10px] uppercase font-semibold shrink-0">
158
+ {r.visibility || "public"}
159
+ </Badge>
160
+ </div>
161
+
162
+ <div class="flex items-center justify-between pt-2 border-t border-border/40 text-xs text-muted-foreground">
163
+ <div class="flex items-center gap-3">
164
+ {r.language && <span class="font-medium text-foreground">{r.language}</span>}
165
+ <span class="flex items-center gap-1"><Star className="h-3 w-3 text-amber-400 fill-amber-400" /> {r.starCount || 0}</span>
166
+ <span class="flex items-center gap-1"><GitFork className="h-3 w-3" /> {r.forkCount || 0}</span>
167
+ </div>
168
+
169
+ <div class="flex items-center gap-1.5">
170
+ <button
171
+ class="clone-btn text-[11px] font-medium px-2 py-1 rounded border border-border/60 hover:bg-accent transition-colors"
172
+ data-clone-url={r.httpCloneUrl}
173
+ >
174
+ Copy Clone URL
175
+ </button>
176
+ </div>
177
+ </div>
178
+ </div>
179
+ ))}
180
+ </div>
181
+ )}
182
+ </div>
183
+ </>
184
+ )}
185
+ </main>
186
+
187
+ <script define:vars={{ id }}>
188
+ import { toast } from "sonner";
189
+
190
+ // Clone URL
191
+ document.querySelectorAll('.clone-btn').forEach(btn => {
192
+ btn.addEventListener('click', (e) => {
193
+ const url = (e.currentTarget as HTMLButtonElement).dataset.cloneUrl;
194
+ if (url) {
195
+ navigator.clipboard.writeText(url);
196
+ toast.success("Clone URL copied to clipboard!");
197
+ }
198
+ });
199
+ });
200
+
201
+ // In-place Sync Button
202
+ const syncBtn = document.getElementById('sync-btn');
203
+ const syncIcon = document.getElementById('sync-icon');
204
+
205
+ syncBtn?.addEventListener('click', async () => {
206
+ syncBtn.setAttribute('disabled', 'true');
207
+ syncIcon?.classList.add('animate-spin');
208
+ toast.info("Triggering background sync...");
209
+
210
+ try {
211
+ const res = await fetch(`/api/instances/sync?id=${id}`, { method: 'POST' });
212
+ const data = await res.json();
213
+ if (data.success) {
214
+ toast.success(`Synced successfully! ${data.data?.count || 0} repos updated.`);
215
+ setTimeout(() => window.location.reload(), 800);
216
+ } else {
217
+ toast.error(data.error?.message || "Sync failed");
218
+ }
219
+ } catch (err) {
220
+ toast.error("Network error triggering sync");
221
+ } finally {
222
+ syncBtn.removeAttribute('disabled');
223
+ syncIcon?.classList.remove('animate-spin');
224
+ }
225
+ });
226
+ </script>
227
+</BaseLayout>
0
228
▼community/src/pages/instances/index.astro
+179-0
1
+---
2
+import BaseLayout from "../../layouts/BaseLayout.astro";
3
+import { Header } from "../../components/layout/Header";
4
+import { Card, CardContent } from "../../components/ui/card";
5
+import { Button } from "../../components/ui/button";
6
+import { Badge } from "../../components/ui/badge";
7
+import {
8
+ Globe,
9
+ Plus,
10
+ Server,
11
+ Activity,
12
+ Users,
13
+ BookOpen,
14
+ ExternalLink,
15
+ ArrowRight,
16
+ ShieldCheck,
17
+ CheckCircle2,
18
+ AlertCircle
19
+} from "lucide-react";
20
+import { verifyToken } from "../../lib/auth";
21
+import { getDb } from "../../lib/db";
22
+import { desc } from "drizzle-orm";
23
+import { instances } from "../../lib/db/schema";
24
+
25
+const token = Astro.cookies.get("community_session")?.value;
26
+const user = token ? await verifyToken(token) : null;
27
+
28
+const db = getDb() as any;
29
+const allInstances = await db.query.instances.findMany({
30
+ orderBy: [desc(instances.repoCount)]
31
+}).catch(() => []);
32
+---
33
+<BaseLayout title="Federated Instances — OpenCodeHub Community">
34
+ <Header client:load user={user} />
35
+
36
+ <main class="container px-4 sm:px-8 py-8 sm:py-12 space-y-8">
37
+
38
+ <!-- Top Header -->
39
+ <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-6 border-b border-border/50">
40
+ <div class="space-y-1">
41
+ <h1 class="text-3xl font-extrabold tracking-tight text-foreground flex items-center gap-2.5">
42
+ <Server className="h-7 w-7 text-primary" />
43
+ Federated Nodes
44
+ </h1>
45
+ <p class="text-sm text-muted-foreground">
46
+ Every registered OpenCodeHub instance across the decentralized ecosystem.
47
+ </p>
48
+ </div>
49
+
50
+ <a href="/instances/submit">
51
+ <Button className="gap-2 bg-primary hover:bg-primary/90 shadow-md shadow-primary/20 text-xs font-semibold">
52
+ <Plus className="h-4 w-4" />
53
+ Submit Node
54
+ </Button>
55
+ </a>
56
+ </div>
57
+
58
+ <!-- Instances Grid -->
59
+ {allInstances.length === 0 ? (
60
+ <div class="rounded-3xl border border-dashed border-border/80 bg-card/40 p-12 text-center space-y-4 max-w-xl mx-auto">
61
+ <div class="h-14 w-14 rounded-2xl bg-primary/10 text-primary flex items-center justify-center mx-auto shadow-inner">
62
+ <Globe className="h-7 w-7" />
63
+ </div>
64
+ <div class="space-y-1">
65
+ <h3 class="font-bold text-lg text-foreground">No instances registered yet</h3>
66
+ <p class="text-xs text-muted-foreground max-w-md mx-auto leading-relaxed">
67
+ Be the first pioneer! Submit your self-hosted OpenCodeHub URL. We will probe <code>/api/instance</code> and start aggregating your public repos.
68
+ </p>
69
+ </div>
70
+ <div class="pt-2">
71
+ <a href="/instances/submit">
72
+ <Button size="sm" className="gap-1.5 text-xs">
73
+ <Plus className="h-3.5 w-3.5" />
74
+ Submit Your Instance
75
+ </Button>
76
+ </a>
77
+ </div>
78
+ </div>
79
+ ) : (
80
+ <div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
81
+ {allInstances.map((instance: any) => {
82
+ const isOnline = instance.status === "online";
83
+ let capabilitiesList: string[] = [];
84
+ try {
85
+ if (instance.capabilities) {
86
+ capabilitiesList = typeof instance.capabilities === "string" ? JSON.parse(instance.capabilities) : instance.capabilities;
87
+ }
88
+ } catch {}
89
+
90
+ return (
91
+ <div class="rounded-2xl border border-border/60 bg-card/60 backdrop-blur-sm p-6 hover:border-primary/40 hover:bg-card/90 transition-all shadow-sm flex flex-col justify-between gap-6 group">
92
+ <div class="space-y-4">
93
+ <div class="flex items-start justify-between gap-3">
94
+ <div class="flex items-center gap-3">
95
+ <div class="h-10 w-10 rounded-xl bg-primary/10 border border-primary/20 flex items-center justify-center text-primary shrink-0">
96
+ <Server className="h-5 w-5" />
97
+ </div>
98
+ <div class="min-w-0">
99
+ <a href={`/instances/${instance.id}`} class="font-bold text-base text-foreground hover:text-primary transition-colors truncate block">
100
+ {instance.name}
101
+ </a>
102
+ <a href={instance.siteUrl} target="_blank" class="text-xs text-muted-foreground hover:underline truncate block">
103
+ {instance.siteUrl}
104
+ </a>
105
+ </div>
106
+ </div>
107
+
108
+ <!-- Status Pulse Pill -->
109
+ <div class={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[11px] font-semibold border ${
110
+ isOnline
111
+ ? "bg-emerald-500/10 text-emerald-400 border-emerald-500/20"
112
+ : "bg-destructive/10 text-destructive border-destructive/20"
113
+ }`}>
114
+ <span class={`h-1.5 w-1.5 rounded-full ${isOnline ? "bg-emerald-400 animate-pulse" : "bg-destructive"}`}></span>
115
+ <span class="capitalize">{instance.status || "pending"}</span>
116
+ </div>
117
+ </div>
118
+
119
+ <!-- Capabilities Badges -->
120
+ <div class="flex flex-wrap gap-1.5 pt-1">
121
+ {instance.version && (
122
+ <Badge variant="outline" className="text-[10px] font-mono">
123
+ v{instance.version}
124
+ </Badge>
125
+ )}
126
+ {capabilitiesList.slice(0, 3).map((cap: string) => (
127
+ <Badge variant="secondary" className="text-[10px] uppercase font-semibold tracking-wider">
128
+ {cap}
129
+ </Badge>
130
+ ))}
131
+ </div>
132
+ </div>
133
+
134
+ <!-- Bottom Stats & Action Bar -->
135
+ <div class="pt-4 border-t border-border/50 flex flex-col gap-3">
136
+ <div class="grid grid-cols-2 gap-2 text-xs text-muted-foreground">
137
+ <div class="flex items-center gap-1.5">
138
+ <BookOpen className="h-3.5 w-3.5 text-primary" />
139
+ <span class="font-medium text-foreground">{instance.repoCount || 0}</span> repos
140
+ </div>
141
+ <div class="flex items-center gap-1.5">
142
+ <Activity className="h-3.5 w-3.5 text-indigo-400" />
143
+ <span>{instance.lastSyncAt ? new Date(instance.lastSyncAt).toLocaleDateString() : 'Pending'}</span>
144
+ </div>
145
+ </div>
146
+
147
+ <div class="grid grid-cols-2 gap-2 pt-1">
148
+ <a href={`/instances/${instance.id}`} class="w-full">
149
+ <Button size="sm" variant="outline" className="w-full text-xs gap-1 hover:bg-primary hover:text-primary-foreground hover:border-primary">
150
+ Explore Node
151
+ <ArrowRight className="h-3 w-3" />
152
+ </Button>
153
+ </a>
154
+ <a href={instance.siteUrl} target="_blank" rel="noreferrer" class="w-full">
155
+ <Button size="sm" variant="ghost" className="w-full text-xs gap-1 text-muted-foreground hover:text-foreground">
156
+ <ExternalLink className="h-3 w-3" />
157
+ Visit Site
158
+ </Button>
159
+ </a>
160
+ </div>
161
+ </div>
162
+ </div>
163
+ );
164
+ })}
165
+ </div>
166
+ )}
167
+
168
+ <!-- Privacy Note Box -->
169
+ <div class="rounded-2xl border border-border/60 bg-muted/20 p-5 text-xs text-muted-foreground flex items-start gap-3">
170
+ <ShieldCheck className="h-5 w-5 text-primary shrink-0 mt-0.5" />
171
+ <div class="space-y-1">
172
+ <span class="font-semibold text-foreground">Zero-Access Privacy Guarantee:</span>
173
+ <p class="leading-relaxed">
174
+ Only public repositories (<code>visibility=public</code>) and public author identities are aggregated. Internal codebases, private keys, and user tokens are completely untouched.
175
+ </p>
176
+ </div>
177
+ </div>
178
+ </main>
179
+</BaseLayout>
0
180
▼community/src/pages/instances/submit.astro
+162-0
1
+---
2
+import BaseLayout from "../../layouts/BaseLayout.astro";
3
+import { Header } from "../../components/layout/Header";
4
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "../../components/ui/card";
5
+import { Button } from "../../components/ui/button";
6
+import { Input } from "../../components/ui/input";
7
+import { Label } from "../../components/ui/label";
8
+import {
9
+ Server,
10
+ Sparkles,
11
+ ShieldCheck,
12
+ CheckCircle2,
13
+ ArrowLeft,
14
+ Globe,
15
+ Code2,
16
+ HelpCircle,
17
+ Loader2
18
+} from "lucide-react";
19
+import { verifyToken } from "../../lib/auth";
20
+
21
+const token = Astro.cookies.get("community_session")?.value;
22
+const user = token ? await verifyToken(token) : null;
23
+---
24
+<BaseLayout title="Submit Federated Node — OpenCodeHub Community">
25
+ <Header client:load user={user} />
26
+
27
+ <main class="container px-4 sm:px-8 py-8 sm:py-12 max-w-2xl mx-auto space-y-8">
28
+
29
+ <a href="/instances" class="inline-flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors">
30
+ <ArrowLeft className="h-3.5 w-3.5" /> Back to Instances
31
+ </a>
32
+
33
+ <div class="space-y-2">
34
+ <h1 class="text-3xl font-extrabold tracking-tight text-foreground flex items-center gap-2.5">
35
+ <Server className="h-7 w-7 text-primary" />
36
+ Connect Your Node
37
+ </h1>
38
+ <p class="text-sm text-muted-foreground">
39
+ Submit your self-hosted OpenCodeHub URL to join the global federated mesh.
40
+ </p>
41
+ </div>
42
+
43
+ <!-- Main Submission Card -->
44
+ <Card className="border-border/60 bg-card/60 backdrop-blur-xl shadow-lg">
45
+ <CardHeader>
46
+ <CardTitle className="text-lg">Instance Endpoint</CardTitle>
47
+ <CardDescription className="text-xs">
48
+ Must be a publicly reachable OpenCodeHub instance. We verify by querying <code>GET /api/instance</code>.
49
+ </CardDescription>
50
+ </CardHeader>
51
+
52
+ <CardContent>
53
+ <form id="submit-form" class="space-y-5">
54
+ <div class="space-y-2">
55
+ <Label htmlFor="url" className="text-xs font-semibold">Instance URL</Label>
56
+ <div class="relative">
57
+ <Globe className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
58
+ <Input
59
+ id="url"
60
+ name="url"
61
+ placeholder="https://code.example.com"
62
+ required
63
+ type="url"
64
+ className="pl-9 bg-card/50 border-border/60 focus:border-primary text-sm h-11"
65
+ />
66
+ </div>
67
+ <p class="text-[11px] text-muted-foreground">
68
+ Example: <code>https://code.mysite.com</code> or <code>https://opencodehub.org</code>
69
+ </p>
70
+ </div>
71
+
72
+ <Button type="submit" className="w-full h-11 bg-primary hover:bg-primary/90 text-sm font-semibold shadow-md shadow-primary/20" id="submit-btn">
73
+ <Sparkles className="h-4 w-4 mr-2" />
74
+ Verify &amp; Register Node
75
+ </Button>
76
+
77
+ <!-- Result Alert Box -->
78
+ <div id="result" class="text-xs hidden rounded-xl border p-4 space-y-2 animate-in fade-in"></div>
79
+ </form>
80
+ </CardContent>
81
+ </Card>
82
+
83
+ <!-- Information Accordion / Card -->
84
+ <div class="rounded-2xl border border-border/60 bg-card/30 p-6 space-y-4 text-xs text-muted-foreground">
85
+ <h4 class="font-bold text-foreground text-sm flex items-center gap-2">
86
+ <ShieldCheck className="h-4 w-4 text-emerald-400" />
87
+ Federation Architecture &amp; Privacy
88
+ </h4>
89
+
90
+ <div class="space-y-3 leading-relaxed">
91
+ <div class="flex gap-2.5">
92
+ <CheckCircle2 className="h-4 w-4 text-primary shrink-0 mt-0.5" />
93
+ <span>
94
+ <b>Public Scope:</b> We only fetch repositories where <code>visibility === "public"</code> and public creator profiles. Private codebases are never indexed or logged.
95
+ </span>
96
+ </div>
97
+ <div class="flex gap-2.5">
98
+ <CheckCircle2 className="h-4 w-4 text-primary shrink-0 mt-0.5" />
99
+ <span>
100
+ <b>Cross-Instance Contribution:</b> Developers can clone your repositories via <code>httpCloneUrl</code> directly from your server without requiring an account on this hub.
101
+ </span>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ </main>
106
+
107
+ <script>
108
+ import { toast } from "sonner";
109
+
110
+ const form = document.getElementById('submit-form') as HTMLFormElement;
111
+ const btn = document.getElementById('submit-btn') as HTMLButtonElement;
112
+ const result = document.getElementById('result') as HTMLDivElement;
113
+
114
+ form?.addEventListener('submit', async (e) => {
115
+ e.preventDefault();
116
+ let url = (document.getElementById('url') as HTMLInputElement).value.trim();
117
+ if (!url) return;
118
+
119
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
120
+ url = 'https://' + url;
121
+ }
122
+
123
+ btn.disabled = true;
124
+ btn.innerHTML = `<span class="inline-flex items-center gap-2"><svg class="animate-spin h-4 w-4" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> Probing /api/instance...</span>`;
125
+ result.classList.add('hidden');
126
+
127
+ try {
128
+ const res = await fetch('/api/instances/submit', {
129
+ method: 'POST',
130
+ headers: { 'Content-Type': 'application/json' },
131
+ body: JSON.stringify({ url })
132
+ });
133
+ const data = await res.json();
134
+ result.classList.remove('hidden');
135
+
136
+ if (res.ok && data.success) {
137
+ result.className = 'text-xs rounded-xl border p-4 bg-emerald-500/10 border-emerald-500/30 text-emerald-300';
138
+ result.innerHTML = `
139
+ <div class="font-bold text-sm text-emerald-200">✓ Successfully Verified &amp; Connected!</div>
140
+ <p>Instance <b>${data.data.name}</b> (${data.data.siteUrl}) is now live. ${data.data.repoCount ?? 0} public repositories are being synchronized.</p>
141
+ <div class="pt-2">
142
+ <a href="/instances/${data.data.id}" class="underline font-semibold text-emerald-100 hover:text-white">View Instance Page &rarr;</a>
143
+ </div>
144
+ `;
145
+ toast.success("Instance successfully registered!");
146
+ } else {
147
+ result.className = 'text-xs rounded-xl border p-4 bg-destructive/10 border-destructive/30 text-destructive';
148
+ result.textContent = data.error?.message || data.message || data.error || 'Verification failed. Make sure /api/instance is accessible.';
149
+ toast.error("Failed to verify instance");
150
+ }
151
+ } catch (err: any) {
152
+ result.classList.remove('hidden');
153
+ result.className = 'text-xs rounded-xl border p-4 bg-destructive/10 border-destructive/30 text-destructive';
154
+ result.textContent = err.message || 'Network error reaching the hub.';
155
+ toast.error("Network error");
156
+ } finally {
157
+ btn.disabled = false;
158
+ btn.innerHTML = `<svg class="h-4 w-4 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/></svg> Verify &amp; Register Node`;
159
+ }
160
+ });
161
+ </script>
162
+</BaseLayout>
0
163
▼community/src/pages/r/[instance]/[owner]/[repo].astro
+282-0
1
+---
2
+import BaseLayout from "../../../../layouts/BaseLayout.astro";
3
+import { Header } from "../../../../components/layout/Header";
4
+import { Card, CardContent, CardHeader, CardTitle } from "../../../../components/ui/card";
5
+import { Button } from "../../../../components/ui/button";
6
+import { Badge } from "../../../../components/ui/badge";
7
+import {
8
+ Star,
9
+ GitFork,
10
+ Copy,
11
+ ExternalLink,
12
+ Code,
13
+ BookOpen,
14
+ ArrowLeft,
15
+ Terminal,
16
+ Globe,
17
+ Check,
18
+ ShieldAlert,
19
+ Server
20
+} from "lucide-react";
21
+import { verifyToken } from "../../../../lib/auth";
22
+import { getDb } from "../../../../lib/db";
23
+import { eq, and } from "drizzle-orm";
24
+import { cachedRepos, instances, stars } from "../../../../lib/db/schema";
25
+
26
+const token = Astro.cookies.get("community_session")?.value;
27
+const user = token ? await verifyToken(token) : null;
28
+const { instance, owner, repo } = Astro.params;
29
+const fullName = `${owner}/${repo}`;
30
+
31
+const db = getDb() as any;
32
+
33
+// Find the instance first
34
+const instanceData = await db.query.instances.findFirst({
35
+ where: eq(instances.name, instance as string)
36
+}).catch(() => null);
37
+
38
+// Then find the repo
39
+let repoData = null;
40
+if (instanceData) {
41
+ repoData = await db.query.cachedRepos.findFirst({
42
+ where: and(
43
+ eq(cachedRepos.instanceId, instanceData.id),
44
+ eq(cachedRepos.fullName, fullName)
45
+ )
46
+ }).catch(() => null);
47
+}
48
+
49
+// Check if current user has starred this repo
50
+let isStarred = false;
51
+if (user && repoData) {
52
+ const existingStar = await db.query.stars.findFirst({
53
+ where: and(
54
+ eq(stars.userId, user.sub),
55
+ eq(stars.repoId, repoData.id)
56
+ )
57
+ }).catch(() => null);
58
+ isStarred = !!existingStar;
59
+}
60
+
61
+const starCount = repoData?.starCount ?? 0;
62
+const forkCount = repoData?.forkCount ?? 0;
63
+const httpCloneUrl = repoData?.httpCloneUrl ?? `https://${instance}/${fullName}.git`;
64
+const language = repoData?.language ?? "Unknown";
65
+const description = repoData?.description ?? "No description provided.";
66
+---
67
+<BaseLayout title={`${fullName} — OpenCodeHub Community`}>
68
+ <Header client:load user={user} />
69
+
70
+ <main class="container px-4 sm:px-8 py-8 sm:py-12 space-y-8 max-w-5xl mx-auto">
71
+
72
+ <!-- Breadcrumbs -->
73
+ <div class="flex items-center gap-2 text-xs text-muted-foreground flex-wrap">
74
+ <a href="/explore" class="hover:text-foreground">Explore</a>
75
+ <span>/</span>
76
+ {instanceData ? (
77
+ <a href={`/instances/${instanceData.id}`} class="hover:text-foreground flex items-center gap-1">
78
+ <Server className="h-3 w-3 text-primary" />
79
+ {instance}
80
+ </a>
81
+ ) : (
82
+ <span>{instance}</span>
83
+ )}
84
+ <span>/</span>
85
+ <a href={`/u/${owner}`} class="hover:text-foreground">@{owner}</a>
86
+ <span>/</span>
87
+ <span class="text-foreground font-semibold">{repo}</span>
88
+ </div>
89
+
90
+ <!-- Repository Header -->
91
+ <div class="rounded-3xl border border-border/60 bg-card/60 backdrop-blur-xl p-6 sm:p-8 space-y-6">
92
+ <div class="flex flex-col sm:flex-row sm:items-start justify-between gap-4">
93
+ <div class="space-y-2">
94
+ <div class="flex items-center gap-3 flex-wrap">
95
+ <h1 class="text-2xl sm:text-3xl font-extrabold tracking-tight text-foreground flex items-center gap-2">
96
+ <BookOpen className="h-7 w-7 text-primary shrink-0" />
97
+ {fullName}
98
+ </h1>
99
+ <Badge variant="outline" className="text-xs uppercase font-semibold">
100
+ {repoData?.visibility || "public"}
101
+ </Badge>
102
+ </div>
103
+
104
+ <p class="text-sm sm:text-base text-muted-foreground max-w-2xl leading-relaxed">
105
+ {description}
106
+ </p>
107
+ </div>
108
+
109
+ <!-- Action Buttons -->
110
+ <div class="flex items-center gap-2 shrink-0">
111
+ <Button
112
+ id="star-btn"
113
+ variant="outline"
114
+ size="sm"
115
+ className={`gap-1.5 text-xs font-semibold ${isStarred ? "bg-amber-500/15 text-amber-400 border-amber-500/40" : ""}`}
116
+ data-repo-id={repoData?.id}
117
+ data-full-name={fullName}
118
+ >
119
+ <Star className={`h-4 w-4 ${isStarred ? "text-amber-400 fill-amber-400" : "text-amber-400"}`} />
120
+ <span id="star-label">{isStarred ? "Starred" : "Star"}</span>
121
+ </Button>
122
+
123
+ <a href={`${instanceData?.siteUrl || `https://${instance}`}/${fullName}`} target="_blank" rel="noreferrer">
124
+ <Button size="sm" className="gap-1.5 text-xs font-semibold bg-primary hover:bg-primary/90">
125
+ <ExternalLink className="h-3.5 w-3.5" />
126
+ View Origin
127
+ </Button>
128
+ </a>
129
+ </div>
130
+ </div>
131
+
132
+ <!-- Quick Git Clone Bar -->
133
+ <div class="rounded-xl border border-border/60 bg-muted/30 p-3 flex items-center justify-between gap-3 text-xs">
134
+ <div class="flex items-center gap-2 text-muted-foreground font-mono overflow-x-auto">
135
+ <Terminal className="h-4 w-4 text-primary shrink-0" />
136
+ <span class="text-foreground shrink-0 select-all font-medium">git clone {httpCloneUrl}</span>
137
+ </div>
138
+ <button
139
+ id="copy-clone-btn"
140
+ class="shrink-0 inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-card border border-border/60 hover:bg-accent text-foreground text-xs font-medium transition-colors"
141
+ data-clone-url={`git clone ${httpCloneUrl}`}
142
+ >
143
+ <Copy className="h-3.5 w-3.5" />
144
+ <span>Copy</span>
145
+ </button>
146
+ </div>
147
+ </div>
148
+
149
+ <!-- Main Content & Sidebar -->
150
+ <div class="grid lg:grid-cols-3 gap-8">
151
+
152
+ <!-- Left: Instructions & Docs -->
153
+ <div class="lg:col-span-2 space-y-6">
154
+ <Card className="border-border/60 bg-card/50">
155
+ <CardHeader>
156
+ <CardTitle className="text-base flex items-center gap-2">
157
+ <BookOpen className="h-4 w-4 text-primary" />
158
+ About This Federated Project
159
+ </CardTitle>
160
+ </CardHeader>
161
+ <CardContent className="space-y-4 text-sm text-muted-foreground leading-relaxed">
162
+ <p>
163
+ This repository is hosted natively on the <b>{instance}</b> node and aggregated to the community catalog via federated metadata caching.
164
+ </p>
165
+ <p>
166
+ To browse source files, commits, issues, and pull requests directly, click the <b>View Origin</b> button above to visit the home node.
167
+ </p>
168
+ </CardContent>
169
+ </Card>
170
+
171
+ <!-- Contribution Card -->
172
+ <Card className="border-border/60 bg-card/50">
173
+ <CardHeader>
174
+ <CardTitle className="text-base flex items-center gap-2">
175
+ <GitFork className="h-4 w-4 text-indigo-400" />
176
+ How to Fork &amp; Contribute
177
+ </CardTitle>
178
+ </CardHeader>
179
+ <CardContent className="space-y-3 text-xs sm:text-sm text-muted-foreground leading-relaxed">
180
+ <ol class="space-y-2.5 list-decimal list-inside">
181
+ <li>Clone the repository using standard Git: <code class="bg-muted px-1.5 py-0.5 rounded font-mono text-xs">git clone {httpCloneUrl}</code></li>
182
+ <li>Make your changes locally and commit them to a new branch.</li>
183
+ <li>Push the branch to your own self-hosted OpenCodeHub node.</li>
184
+ <li>Submit a cross-node federated pull request to <code class="bg-muted px-1.5 py-0.5 rounded font-mono text-xs">@{owner}</code>.</li>
185
+ </ol>
186
+ </CardContent>
187
+ </Card>
188
+ </div>
189
+
190
+ <!-- Right: Meta Info -->
191
+ <div class="space-y-6">
192
+ <Card className="border-border/60 bg-card/50">
193
+ <CardContent className="p-5 space-y-4">
194
+ <h3 class="text-sm font-bold text-foreground">Project Details</h3>
195
+
196
+ <div class="space-y-3 text-xs">
197
+ <div class="flex justify-between items-center py-1 border-b border-border/40">
198
+ <span class="text-muted-foreground flex items-center gap-1.5"><Code className="h-3.5 w-3.5 text-primary" /> Primary Language</span>
199
+ <span class="font-semibold text-foreground">{language}</span>
200
+ </div>
201
+ <div class="flex justify-between items-center py-1 border-b border-border/40">
202
+ <span class="text-muted-foreground flex items-center gap-1.5"><Star className="h-3.5 w-3.5 text-amber-400" /> Stars</span>
203
+ <span class="font-semibold text-foreground">{starCount}</span>
204
+ </div>
205
+ <div class="flex justify-between items-center py-1 border-b border-border/40">
206
+ <span class="text-muted-foreground flex items-center gap-1.5"><GitFork className="h-3.5 w-3.5" /> Forks</span>
207
+ <span class="font-semibold text-foreground">{forkCount}</span>
208
+ </div>
209
+ <div class="flex justify-between items-center py-1">
210
+ <span class="text-muted-foreground flex items-center gap-1.5"><Server className="h-3.5 w-3.5 text-purple-400" /> Host Node</span>
211
+ <span class="font-semibold text-foreground">{instance}</span>
212
+ </div>
213
+ </div>
214
+
215
+ <div class="pt-2">
216
+ <a href={`${instanceData?.siteUrl || `https://${instance}`}/${fullName}`} target="_blank" rel="noreferrer" class="w-full block">
217
+ <Button variant="outline" className="w-full text-xs gap-1.5">
218
+ <ExternalLink className="h-3.5 w-3.5" />
219
+ Open on {instance}
220
+ </Button>
221
+ </a>
222
+ </div>
223
+ </CardContent>
224
+ </Card>
225
+
226
+ <!-- Social Disclaimer -->
227
+ <div class="rounded-2xl border border-border/60 bg-muted/20 p-4 text-[11px] text-muted-foreground leading-relaxed space-y-1">
228
+ <div class="font-semibold text-foreground">Community Social Actions:</div>
229
+ <p>
230
+ Stars registered on this hub are stored in the federated catalog. To star directly on the origin server, sign into that node.
231
+ </p>
232
+ </div>
233
+ </div>
234
+
235
+ </div>
236
+ </main>
237
+
238
+ <script>
239
+ import { toast } from "sonner";
240
+
241
+ // Copy Clone command
242
+ const copyBtn = document.getElementById('copy-clone-btn');
243
+ copyBtn?.addEventListener('click', () => {
244
+ const text = copyBtn.dataset.cloneUrl || "";
245
+ navigator.clipboard.writeText(text);
246
+ toast.success("Command copied to clipboard!");
247
+ });
248
+
249
+ // Star Toggle
250
+ const starBtn = document.getElementById('star-btn');
251
+ const starLabel = document.getElementById('star-label');
252
+
253
+ starBtn?.addEventListener('click', async () => {
254
+ const repoId = starBtn.dataset.repoId;
255
+ const fullName = starBtn.dataset.fullName;
256
+
257
+ try {
258
+ const res = await fetch('/api/stars', {
259
+ method: 'POST',
260
+ headers: { 'Content-Type': 'application/json' },
261
+ body: JSON.stringify({ repoId, fullName })
262
+ });
263
+ const data = await res.json();
264
+
265
+ if (data.success) {
266
+ const starred = data.data?.starred;
267
+ if (starLabel) starLabel.textContent = starred ? "Starred" : "Star";
268
+ starBtn.classList.toggle("bg-amber-500/15", starred);
269
+ starBtn.classList.toggle("text-amber-400", starred);
270
+ starBtn.classList.toggle("border-amber-500/40", starred);
271
+ toast.success(starred ? `Starred ${fullName}` : `Unstarred ${fullName}`);
272
+ } else if (res.status === 401) {
273
+ toast.error("Please sign in to star repositories");
274
+ } else {
275
+ toast.error(data.error || "Failed to update star");
276
+ }
277
+ } catch (err) {
278
+ toast.error("Network error");
279
+ }
280
+ });
281
+ </script>
282
+</BaseLayout>
0
283
▼community/src/pages/u/[username].astro
+233-0
1
+---
2
+import BaseLayout from "../../layouts/BaseLayout.astro";
3
+import { Header } from "../../components/layout/Header";
4
+import { Card, CardContent } from "../../components/ui/card";
5
+import { Badge } from "../../components/ui/badge";
6
+import { Button } from "../../components/ui/button";
7
+import {
8
+ Star, GitFork, BookOpen, Globe, ExternalLink, ArrowLeft,
9
+ UserPlus, Server, MapPin, Link as LinkIcon, Building,
10
+ Users, Calendar, Clock
11
+} from "lucide-react";
12
+import { verifyToken } from "../../lib/auth";
13
+import { getDb } from "../../lib/db";
14
+import { eq, desc, and, sql, count } from "drizzle-orm";
15
+import { cachedUsers, cachedRepos, follows } from "../../lib/db/schema";
16
+
17
+const token = Astro.cookies.get("community_session")?.value;
18
+const user = token ? await verifyToken(token) : null;
19
+const { username } = Astro.params;
20
+
21
+const db = getDb() as any;
22
+
23
+const profile = await db.query.cachedUsers.findFirst({
24
+ where: eq(cachedUsers.username, username as string),
25
+ with: { instance: true },
26
+}).catch(() => null);
27
+
28
+const repos = await db.query.cachedRepos.findMany({
29
+ where: eq(cachedRepos.ownerUsername, username as string),
30
+ orderBy: [desc(cachedRepos.starCount), desc(cachedRepos.updatedAt)],
31
+ with: { instance: true },
32
+}).catch(() => []);
33
+
34
+// Follower/following counts
35
+const [followersCount, followingCount] = await Promise.all([
36
+ db.select({ count: sql<number>`count(*)` }).from(follows).where(eq(follows.followeeId, username as string)).then((r: any) => r[0]?.count || 0).catch(() => 0),
37
+ db.select({ count: sql<number>`count(*)` }).from(follows).where(eq(follows.followerId, username as string)).then((r: any) => r[0]?.count || 0).catch(() => 0),
38
+]);
39
+
40
+let isFollowing = false;
41
+if (user) {
42
+ const followRecord = await db.query.follows.findFirst({
43
+ where: and(eq(follows.followerId, user.sub), eq(follows.followeeId, username as string)),
44
+ }).catch(() => null);
45
+ isFollowing = !!followRecord;
46
+}
47
+
48
+// Activity heatmap from repo updatedAt (proxy)
49
+const matrix = Array(84).fill(0);
50
+const now = new Date();
51
+repos.forEach((r: any) => {
52
+ if (!r.updatedAt) return;
53
+ const diffDays = Math.floor(Math.abs(now.getTime() - new Date(r.updatedAt).getTime()) / 86400000);
54
+ if (diffDays < 84) matrix[83 - diffDays] += 1;
55
+});
56
+
57
+// Aggregate stats
58
+const totalStars = repos.reduce((sum: number, r: any) => sum + (r.starCount || 0), 0);
59
+const totalForks = repos.reduce((sum: number, r: any) => sum + (r.forkCount || 0), 0);
60
+const languages = [...new Set(repos.map((r: any) => r.language).filter(Boolean))];
61
+
62
+const langColors: Record<string, string> = {
63
+ TypeScript: "#3178c6", JavaScript: "#f1e05a", Python: "#3572A5", Go: "#00ADD8",
64
+ Rust: "#dea584", Java: "#b07219", "C++": "#f34b7d", C: "#555555", Ruby: "#701516",
65
+ PHP: "#4F5D95", Swift: "#F05138", Kotlin: "#A97BFF", Dart: "#00B4AB",
66
+ Shell: "#89e051", HTML: "#e34c26", CSS: "#563d7c", Vue: "#41b883",
67
+};
68
+
69
+const displayName = profile?.displayName || username;
70
+const avatarUrl = profile?.avatarUrl || `https://api.dicebear.com/7.x/initials/svg?seed=${username}`;
71
+const instanceUrl = profile?.instance?.url ? `${profile.instance.url}/${username}` : null;
72
+---
73
+<BaseLayout title={`${displayName} (@${username}) — Community`}>
74
+ <Header client:load user={user} />
75
+
76
+ <main class="container px-4 sm:px-8 py-8 sm:py-12 space-y-8 max-w-5xl mx-auto">
77
+ <a href="/explore" class="inline-flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors">
78
+ <ArrowLeft className="h-3.5 w-3.5" /> Back to Explore
79
+ </a>
80
+
81
+ <!-- Profile Hero -->
82
+ <div class="rounded-3xl border border-border/60 bg-card/60 backdrop-blur-xl p-6 sm:p-8">
83
+ <div class="flex flex-col sm:flex-row items-center sm:items-start gap-6 text-center sm:text-left">
84
+ <img src={avatarUrl} alt={displayName} class="h-24 w-24 rounded-3xl bg-muted border-2 border-border/80 shadow-md object-cover shrink-0" />
85
+ <div class="flex-1 space-y-3">
86
+ <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
87
+ <div>
88
+ <h1 class="text-2xl sm:text-3xl font-extrabold tracking-tight">{displayName}</h1>
89
+ <div class="text-sm font-medium text-muted-foreground">@{username}</div>
90
+ </div>
91
+ <div class="flex items-center gap-2 justify-center sm:justify-start">
92
+ <Button id="follow-btn" size="sm" className={`gap-1.5 text-xs font-semibold ${isFollowing ? "bg-muted text-muted-foreground hover:bg-destructive/10 hover:text-destructive" : "bg-primary hover:bg-primary/90"}`} data-username={username}>
93
+ <UserPlus className="h-3.5 w-3.5" />
94
+ <span id="follow-label">{isFollowing ? "Following" : "Follow"}</span>
95
+ </Button>
96
+ {instanceUrl && <a href={instanceUrl} target="_blank" rel="noreferrer"><Button size="sm" variant="outline" className="gap-1.5 text-xs"><ExternalLink className="h-3.5 w-3.5" /> Visit</Button></a>}
97
+ </div>
98
+ </div>
99
+ <p class="text-sm text-muted-foreground max-w-xl leading-relaxed">{profile?.bio || "Decentralized developer on the OpenCodeHub federated mesh."}</p>
100
+ <div class="flex flex-wrap items-center justify-center sm:justify-start gap-4 pt-1 text-xs text-muted-foreground">
101
+ {profile?.instance?.name && <span class="inline-flex items-center gap-1.5"><Server className="h-3.5 w-3.5 text-primary" />{profile.instance.name}</span>}
102
+ {profile?.location && <span class="flex items-center gap-1"><MapPin className="h-3.5 w-3.5" />{profile.location}</span>}
103
+ {profile?.company && <span class="flex items-center gap-1"><Building className="h-3.5 w-3.5" />{profile.company}</span>}
104
+ {profile?.website && <a href={profile.website} target="_blank" class="flex items-center gap-1 hover:text-primary"><LinkIcon className="h-3.5 w-3.5" />Website</a>}
105
+ </div>
106
+ </div>
107
+ </div>
108
+
109
+ <!-- Stats Bar -->
110
+ <div class="grid grid-cols-2 sm:grid-cols-5 gap-3 mt-6 pt-6 border-t border-border/50">
111
+ <div class="text-center p-2 rounded-lg bg-muted/30"><div class="text-lg font-bold">{repos.length}</div><div class="text-[10px] text-muted-foreground">Repos</div></div>
112
+ <div class="text-center p-2 rounded-lg bg-muted/30"><div class="text-lg font-bold">{totalStars}</div><div class="text-[10px] text-muted-foreground">Stars</div></div>
113
+ <div class="text-center p-2 rounded-lg bg-muted/30"><div class="text-lg font-bold">{totalForks}</div><div class="text-[10px] text-muted-foreground">Forks</div></div>
114
+ <div class="text-center p-2 rounded-lg bg-muted/30"><div class="text-lg font-bold">{followersCount}</div><div class="text-[10px] text-muted-foreground">Followers</div></div>
115
+ <div class="text-center p-2 rounded-lg bg-muted/30"><div class="text-lg font-bold">{followingCount}</div><div class="text-[10px] text-muted-foreground">Following</div></div>
116
+ </div>
117
+ </div>
118
+
119
+ <!-- 2-Col Layout -->
120
+ <div class="grid lg:grid-cols-3 gap-8">
121
+ <div class="lg:col-span-2 space-y-4">
122
+ <h2 class="text-xl font-bold flex items-center gap-2"><BookOpen className="h-5 w-5 text-primary" /> Repositories ({repos.length})</h2>
123
+ <div class="space-y-3.5">
124
+ {repos.length === 0 ? (
125
+ <div class="rounded-2xl border border-dashed border-border/80 bg-card/30 p-8 text-center text-xs text-muted-foreground">No repositories indexed for this profile yet.</div>
126
+ ) : (
127
+ repos.map((repo: any) => {
128
+ const lc = langColors[repo.language] || "#888";
129
+ return (
130
+ <div class="rounded-2xl border border-border/60 bg-card/50 p-5 hover:border-primary/40 hover:bg-card/80 transition-all shadow-sm space-y-2">
131
+ <div class="flex items-center justify-between gap-2">
132
+ <a href={`/r/${repo.instance?.name || "node"}/${repo.fullName}`} class="font-bold text-base text-primary hover:underline">{repo.name}</a>
133
+ <Badge variant="outline" className="text-[10px] uppercase font-semibold">{repo.visibility || "public"}</Badge>
134
+ </div>
135
+ <p class="text-xs text-muted-foreground line-clamp-2">{repo.description || "No description provided."}</p>
136
+ <div class="flex items-center justify-between pt-2 text-xs text-muted-foreground">
137
+ <div class="flex items-center gap-3">
138
+ {repo.language && <span class="flex items-center gap-1.5 font-medium text-foreground"><span class="h-2.5 w-2.5 rounded-full" style={`background-color:${lc}`}></span>{repo.language}</span>}
139
+ <span class="flex items-center gap-1"><Star className="h-3 w-3 text-amber-400 fill-amber-400" /> {repo.starCount || 0}</span>
140
+ <span class="flex items-center gap-1"><GitFork className="h-3 w-3" /> {repo.forkCount || 0}</span>
141
+ </div>
142
+ {repo.updatedAt && <span class="flex items-center gap-1"><Clock className="h-3 w-3" />{new Date(repo.updatedAt).toLocaleDateString()}</span>}
143
+ </div>
144
+ </div>
145
+ );
146
+ })
147
+ )}
148
+ </div>
149
+ </div>
150
+
151
+ <!-- Sidebar -->
152
+ <div class="space-y-6">
153
+ <!-- Activity Heatmap -->
154
+ <Card className="border-border/60 bg-card/50">
155
+ <CardContent className="p-5 space-y-3">
156
+ <h3 class="text-sm font-bold flex items-center justify-between"><span>Activity</span><span class="text-[11px] font-normal text-muted-foreground">Last 12 weeks</span></h3>
157
+ <div class="grid grid-cols-7 gap-1.5">
158
+ {matrix.map((count) => {
159
+ const bg = count >= 3 ? "bg-emerald-500" : count === 2 ? "bg-emerald-500/70" : count === 1 ? "bg-emerald-500/40" : "bg-muted/60";
160
+ return <div class={`h-3.5 rounded-sm ${bg} transition-colors hover:ring-2 hover:ring-primary`} title={`${count} activities`} />;
161
+ })}
162
+ </div>
163
+ <div class="flex items-center justify-between pt-2 text-[10px] text-muted-foreground">
164
+ <span>Less</span>
165
+ <div class="flex items-center gap-1">
166
+ <span class="h-2.5 w-2.5 rounded-sm bg-muted/60"></span>
167
+ <span class="h-2.5 w-2.5 rounded-sm bg-emerald-500/40"></span>
168
+ <span class="h-2.5 w-2.5 rounded-sm bg-emerald-500/70"></span>
169
+ <span class="h-2.5 w-2.5 rounded-sm bg-emerald-500"></span>
170
+ </div>
171
+ <span>More</span>
172
+ </div>
173
+ </CardContent>
174
+ </Card>
175
+
176
+ <!-- Languages -->
177
+ {languages.length > 0 && (
178
+ <Card className="border-border/60 bg-card/50">
179
+ <CardContent className="p-5 space-y-3">
180
+ <h3 class="text-sm font-bold">Languages</h3>
181
+ <div class="space-y-2">
182
+ {languages.slice(0, 8).map((lang: string) => (
183
+ <div class="flex items-center gap-2 text-xs">
184
+ <span class="h-2.5 w-2.5 rounded-full shrink-0" style={`background-color:${langColors[lang] || "#888"}`}></span>
185
+ <span class="font-medium text-foreground">{lang}</span>
186
+ </div>
187
+ ))}
188
+ </div>
189
+ </CardContent>
190
+ </Card>
191
+ )}
192
+
193
+ <!-- Instance -->
194
+ {profile?.instance && (
195
+ <Card className="border-border/60 bg-card/50">
196
+ <CardContent className="p-5 space-y-3">
197
+ <h3 class="text-sm font-bold">Home Instance</h3>
198
+ <a href={`/instances/${profile.instance.id}`} class="flex items-center gap-3 p-3 rounded-xl hover:bg-muted/40 transition-colors border border-border/40">
199
+ <div class="h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center"><Server className="h-5 w-5 text-primary" /></div>
200
+ <div class="min-w-0">
201
+ <div class="text-sm font-semibold truncate">{profile.instance.name}</div>
202
+ <div class="text-[11px] text-muted-foreground truncate">{profile.instance.siteUrl}</div>
203
+ </div>
204
+ </a>
205
+ </CardContent>
206
+ </Card>
207
+ )}
208
+ </div>
209
+ </div>
210
+ </main>
211
+
212
+ <script>
213
+ import { toast } from "sonner";
214
+ const followBtn = document.getElementById('follow-btn');
215
+ const followLabel = document.getElementById('follow-label');
216
+ followBtn?.addEventListener('click', async () => {
217
+ const username = followBtn.dataset.username;
218
+ try {
219
+ const res = await fetch('/api/follows', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ followeeType: 'user', followeeId: username }) });
220
+ const data = await res.json();
221
+ if (data.success) {
222
+ const f = data.data?.following;
223
+ if (followLabel) followLabel.textContent = f ? "Following" : "Follow";
224
+ followBtn.classList.toggle("bg-muted", f);
225
+ followBtn.classList.toggle("text-muted-foreground", f);
226
+ followBtn.classList.toggle("bg-primary", !f);
227
+ followBtn.classList.toggle("text-primary-foreground", !f);
228
+ toast.success(f ? `Followed @${username}` : `Unfollowed @${username}`);
229
+ } else { toast.error(data.error?.message || "Failed"); }
230
+ } catch { toast.error("Network error"); }
231
+ });
232
+ </script>
233
+</BaseLayout>
0
234
▼community/src/styles/globals.css
+73-0
1
+@tailwind base;
2
+@tailwind components;
3
+@tailwind utilities;
4
+
5
+@layer base {
6
+ :root {
7
+ --background: 240 10% 3.9%;
8
+ --foreground: 0 0% 98%;
9
+ --card: 240 10% 4.9%;
10
+ --card-foreground: 0 0% 98%;
11
+ --popover: 240 10% 4.9%;
12
+ --popover-foreground: 0 0% 98%;
13
+ --primary: 217.2 91.2% 59.8%;
14
+ --primary-foreground: 222.2 47.4% 11.2%;
15
+ --secondary: 240 3.7% 15.9%;
16
+ --secondary-foreground: 0 0% 98%;
17
+ --muted: 240 3.7% 15.9%;
18
+ --muted-foreground: 240 5% 64.9%;
19
+ --accent: 240 3.7% 15.9%;
20
+ --accent-foreground: 0 0% 98%;
21
+ --destructive: 0 62.8% 30.6%;
22
+ --destructive-foreground: 0 0% 98%;
23
+ --border: 240 3.7% 15.9%;
24
+ --input: 240 3.7% 15.9%;
25
+ --ring: 217.2 91.2% 59.8%;
26
+ --radius: 0.75rem;
27
+ }
28
+}
29
+
30
+@layer base {
31
+ * {
32
+ @apply border-border selection:bg-primary/20 selection:text-primary-foreground;
33
+ }
34
+ body {
35
+ @apply bg-background text-foreground antialiased min-h-screen flex flex-col font-sans;
36
+ background-image: radial-gradient(rgba(255, 255, 255, 0.05) 1px, transparent 1px);
37
+ background-size: 32px 32px;
38
+ }
39
+}
40
+
41
+/* Custom scrollbar */
42
+::-webkit-scrollbar {
43
+ width: 8px;
44
+ height: 8px;
45
+}
46
+::-webkit-scrollbar-track {
47
+ background: hsl(var(--background));
48
+}
49
+::-webkit-scrollbar-thumb {
50
+ background: hsl(var(--muted));
51
+ border-radius: 4px;
52
+}
53
+::-webkit-scrollbar-thumb:hover {
54
+ background: hsl(var(--muted-foreground) / 0.5);
55
+}
56
+
57
+/* Language Color Helpers */
58
+.lang-typescript { color: #3178c6; }
59
+.lang-javascript { color: #f7df1e; }
60
+.lang-python { color: #3572A5; }
61
+.lang-go { color: #00ADD8; }
62
+.lang-rust { color: #dea584; }
63
+.lang-java { color: #b07219; }
64
+.lang-cpp, .lang-c\+\+ { color: #f34b7d; }
65
+.lang-html { color: #e34c26; }
66
+.lang-css { color: #563d7c; }
67
+
68
+.lang-dot {
69
+ display: inline-block;
70
+ width: 8px;
71
+ height: 8px;
72
+ border-radius: 9999px;
73
+}
0
74
▼community/tailwind.config.mjs
+31-0
1
+/** @type {import('tailwindcss').Config} */
2
+export default {
3
+ darkMode: ["class"],
4
+ content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
5
+ theme: {
6
+ container: { center: true, padding: "2rem", screens: { "2xl": "1400px" } },
7
+ extend: {
8
+ colors: {
9
+ border: "hsl(var(--border))",
10
+ input: "hsl(var(--input))",
11
+ ring: "hsl(var(--ring))",
12
+ background: "hsl(var(--background))",
13
+ foreground: "hsl(var(--foreground))",
14
+ primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))" },
15
+ secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))" },
16
+ destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))" },
17
+ muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))" },
18
+ accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))" },
19
+ popover: { DEFAULT: "hsl(var(--popover))", foreground: "hsl(var(--popover-foreground))" },
20
+ card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))" },
21
+ },
22
+ borderRadius: { lg: "var(--radius)", md: "calc(var(--radius) - 2px)", sm: "calc(var(--radius) - 4px)" },
23
+ keyframes: {
24
+ "accordion-down": { from: { height: "0" }, to: { height: "var(--radix-accordion-content-height)" } },
25
+ "accordion-up": { from: { height: "var(--radix-accordion-content-height)" }, to: { height: "0" } },
26
+ },
27
+ animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out" },
28
+ },
29
+ },
30
+ plugins: [require("tailwindcss-animate")],
31
+};
0
32
▼community/tsconfig.json
+9-0
1
+{
2
+ "extends": "astro/tsconfigs/strict",
3
+ "compilerOptions": {
4
+ "baseUrl": ".",
5
+ "paths": { "@/*": ["src/*"] },
6
+ "jsx": "react-jsx",
7
+ "jsxImportSource": "react"
8
+ }
9
+}
0
10
▼docker-compose.yml
+6-2
183
183
POSTGRES_USER: ${POSTGRES_USER:-opencodehub}
184
184
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}
185
185
POSTGRES_DB: ${POSTGRES_DB:-opencodehub}
186
+ ports:
187
+ - "${POSTGRES_PORT:-5432}:5432"
186
188
volumes:
187
189
- postgres-data:/var/lib/postgresql/data
188
190
networks:
213
215
- 256mb
214
216
- --maxmemory-policy
215
217
- allkeys-lru
218
+ ports:
219
+ - "${REDIS_PORT:-6379}:6379"
216
220
volumes:
217
221
- redis-data:/data
218
222
networks:
271
275
restart: unless-stopped
272
276
command: server /data --console-address ":9001"
273
277
environment:
274
- MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin}
275
- MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin}
278
+ MINIO_ROOT_USER: ${MINIO_ROOT_USER:?Set MINIO_ROOT_USER in .env}
279
+ MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?Set MINIO_ROOT_PASSWORD in .env}
276
280
ports:
277
281
- "${MINIO_CONSOLE_PORT:-9001}:9001" # Web console (set 0 to disable)
278
282
volumes:
279
283
▼docs-site/src/content/docs/getting-started/installation.md
+27-0
13
13
| **Disk** | 10GB | 50GB SSD |
14
14
| **OS** | Linux (Ubuntu/Debian) | Linux |
15
15
16
+## CLI Installation (`opencodehub-cli`)
17
+
18
+Install the official OpenCodeHub CLI (`och`) to manage stacks, pull requests, and merge queues from your terminal:
19
+
20
+```bash
21
+# via npm
22
+npm install -g opencodehub-cli
23
+
24
+# via bun
25
+bun add -g opencodehub-cli
26
+
27
+# via pnpm
28
+pnpm add -g opencodehub-cli
29
+
30
+# via yarn
31
+yarn global add opencodehub-cli
32
+```
33
+
34
+Verify your installation:
35
+
36
+```bash
37
+och --version
38
+och --help
39
+```
40
+
41
+---
42
+
16
43
## 1-Click Installation (Recommended)
17
44
18
45
```bash
19
46
▼docs/features/federation.md
+95-0
1
+# Federation & Cross-Instance PRs
2
+
3
+> Connect two self-hosted OpenCodeHub instances so contributors on one can fork, contribute, and open pull requests against repositories hosted on another.
4
+
5
+Federation lets a user on instance **A** fork a repository hosted on instance **B**, push branches back to B, and open **cross-instance pull requests** whose head branch lives on A's fork. B pulls the head from A's fork URL, creates a normal PR (with `headRepositoryId` unset), and the usual review and merge pipeline applies.
6
+
7
+## Table of Contents
8
+
9
+- [Overview](#overview)
10
+- [How It Works](#how-it-works)
11
+- [Setting Up Federation](#setting-up-federation)
12
+- [Forking a Repository from Another Instance](#forking-a-repository-from-another-instance)
13
+- [Contributing Back](#contributing-back)
14
+- [Cross-Instance Pull Requests](#cross-instance-pull-requests)
15
+- [Permissions](#permissions)
16
+- [SSRF Protection](#ssrf-protection)
17
+
18
+## Overview
19
+
20
+The two instances interoperate over HTTP. Instance A talks to B's REST API using a Personal Access Token belonging to the contributing user on B. All git traffic flows over the normal smart-HTTP protocol using basic auth with that PAT.
21
+
22
+```
23
+┌─────────────────┐ import (clone URL + PAT) ┌─────────────────┐
24
+│ Instance A │ ───────────────────────────▶ │ Instance B │
25
+│ bob/fedbase │ │ alice/fedbase │
26
+│ (fork) │ ◀─────────────────────────── │ (upstream) │
27
+│ │ push-upstream (git push) │ │
28
+│ │ ───────────────────────────▶ │ │
29
+│ │ federation/open-pull │ external-pulls │
30
+└─────────────────┘ ◀─────────────────────────── └─────────────────┘
31
+```
32
+
33
+## How It Works
34
+
35
+1. **Import (fork)**: On instance A, import `https://B/alice/fedbase.git` and authenticate with a PAT for the user on B. A detects that the source is another OpenCodeHub instance (`GET /api/instance`) and records the relationship as `forkedFromUrl`.
36
+2. **Contribute back**: A can push any branch on its fork back to B (`federation/push-upstream`). B's existing `git-receive-pack` authorizes the push via `canWriteRepo` — B controls who may contribute.
37
+3. **Cross-instance PR**: A calls `federation/open-pull` on B. B validates the fork URL (SSRF check), fetches the head branch from A's fork into the B repo, computes diff stats, and creates a PR whose head lives on A's fork.
38
+4. **Review & merge**: The PR is a normal PR on B. Reviews, approvals, CI, and the merge queue all apply. Merging fetches the head ref and merges it into the base branch.
39
+
40
+## Setting Up Federation
41
+
42
+Federation is enabled out of the box. Two environment variables control SSRF behavior:
43
+
44
+| Variable | Purpose |
45
+|----------|---------|
46
+| `FEDERATION_ALLOW_LOCALHOST` | Set `true` to allow fetching from `localhost`/loopback URLs. **Only** for two-instance testing on a single host or a trusted private network. Never enable in production. |
47
+
48
+The upstream instance does not need any special config beyond the normal collaboration settings: the A user simply needs read access (to see the repo) and write access (to push branches and open PRs).
49
+
50
+## Forking a Repository from Another Instance
51
+
52
+On instance A, use the normal **Import Repository** flow:
53
+
54
+1. Paste the upstream clone URL, e.g. `http://instance-b.local/swadhinbiswas/fedbase.git`.
55
+2. Enter the **auth username** and a **PAT** for the user on instance B (the username must match the B account, e.g. `bob`).
56
+3. Complete the import. Instance A records `forkedFromUrl` and stores an encrypted mirror PAT so it can push back later.
57
+
58
+> For cross-instance PRs the source URL must be fetchable by B — the fork must be on a host B can reach, and the fork repo must be readable by B (public, or accessible with the embedded PAT).
59
+
60
+## Contributing Back
61
+
62
+From the fork's repository page on A, the **Federation** panel offers **Push branch to upstream**:
63
+
64
+- Pick a branch on the fork.
65
+- A pushes `branch:branch` to the upstream clone URL, authenticating with the stored B user's PAT.
66
+
67
+B's permission model decides whether the push succeeds — the B user must be a collaborator (or the repo must allow external writes).
68
+
69
+## Cross-Instance Pull Requests
70
+
71
+From the same **Federation** panel, **Open cross-instance PR**:
72
+
73
+1. A calls `POST /api/repos/{owner}/{repo}/external-pulls` on B (server-to-server) with the fork URL, head branch, base branch, title, and body.
74
+2. B validates the fork URL, fetches the head into the B repo, computes additions/deletions/changed files, and creates the PR.
75
+3. The PR's head branch resolves to the fetched ref on B, so the diff, comments, and merge all work normally.
76
+
77
+### Endpoints
78
+
79
+| Endpoint (on B) | Purpose |
80
+|-----------------|---------|
81
+| `POST /api/repos/{owner}/{repo}/external-pulls` | Create a PR whose head is fetched from an external fork URL |
82
+| `GET /api/instance` | Instance metadata probe used for detection |
83
+
84
+## Permissions
85
+
86
+- **Import on A**: requires a valid PAT for the B user.
87
+- **Push upstream**: B's `git-receive-pack` requires the B user to have write access to the repo.
88
+- **Open a cross-instance PR**: B's `external-pulls` endpoint requires the caller to have read access plus either write access to the repo, or the repo must have **Allow external pull requests** enabled (repo → Settings → Federation).
89
+- **Merge**: normal PR merge gates apply (approvals, required checks, branch protection).
90
+
91
+## SSRF Protection
92
+
93
+Every server-initiated fetch (import on A, external-pulls on B) runs through `validateGitCloneUrl`, which blocks private ranges, cloud metadata endpoints, and non-HTTP(S)/git/ssh schemes. The localhost bypass is only available when `FEDERATION_ALLOW_LOCALHOST=true`, and callers gate it explicitly.
94
+
95
+Tokens embedded into fetch/push URLs are used transiently and never persisted.
0
96
▼docs/getting-started/installation.md
+54-29
1
1
# Installation Guide
2
2
3
-OpenCodeHub allows you to host your own GitHub-like platform. You can run it via **Docker** (recommended for production) or **Node.js** (for development/custom setups).
3
+OpenCodeHub allows you to host your own GitHub-like platform. You can run it via **Docker** (recommended for production) or **Node.js / Bun** (for development/custom setups).
4
4
5
5
## 📋 System Requirements
6
6
9
9
| **CPU** | 1 vCPU | 2 vCPU |
10
10
| **RAM** | 512MB | 2GB |
11
11
| **Disk** | 10GB | 50GB SSD |
12
-| **OS** | Linux (Ubuntu/Debian) | Linux |
12
+| **OS** | Linux (Ubuntu/Debian) | Linux / macOS / Windows |
13
+
14
+---
15
+
16
+## 💻 CLI Installation (`opencodehub-cli`)
17
+
18
+Install the official OpenCodeHub CLI (`och`) to interact with your instance directly from the terminal:
19
+
20
+```bash
21
+# via npm
22
+npm install -g opencodehub-cli
23
+
24
+# via bun
25
+bun add -g opencodehub-cli
26
+
27
+# via pnpm
28
+pnpm add -g opencodehub-cli
29
+
30
+# via yarn
31
+yarn global add opencodehub-cli
32
+```
33
+
34
+Verify the CLI is ready:
35
+
36
+```bash
37
+och --version
38
+och --help
39
+```
40
+
41
+Authenticate with your server:
42
+
43
+```bash
44
+och auth login --url https://git.yourcompany.com
45
+och config doctor
46
+```
13
47
14
48
---
15
49
44
78
JWT_SECRET=<openssl rand -hex 32>
45
79
SESSION_SECRET=<openssl rand -hex 32>
46
80
INTERNAL_HOOK_SECRET=<openssl rand -hex 32>
81
+CRON_SECRET=<openssl rand -hex 32>
82
+RUNNER_SECRET=<openssl rand -hex 32>
47
83
48
84
# Domain Configuration
49
85
SITE_URL=https://git.yourcompany.com
51
87
52
88
# Database (Using the Postgres container defined in compose)
53
89
DATABASE_URL=postgresql://opencodehub:securepassword@postgres:5432/opencodehub
90
+POSTGRES_USER=opencodehub
91
+POSTGRES_PASSWORD=securepassword
92
+POSTGRES_DB=opencodehub
93
+
94
+# Redis
95
+REDIS_URL=redis://:redispassword@redis:6379
96
+REDIS_PASSWORD=redispassword
54
97
55
-# Object Storage (Highly Recommended for Production)
56
-STORAGE_TYPE=s3
57
-STORAGE_BUCKET=my-git-bucket
58
-STORAGE_REGION=us-east-1
59
-STORAGE_ACCESS_KEY_ID=...
60
-STORAGE_SECRET_ACCESS_KEY=...
61
-# STORAGE_ENDPOINT=https://<account>.r2.cloudflarestorage.com # set for non-AWS vendors
98
+# Object Storage (Optional for S3 backends)
99
+STORAGE_TYPE=local
100
+STORAGE_PATH=/data/storage
62
101
```
63
102
64
103
### 4. Start Services
65
104
66
105
```bash
67
-docker-compose up -d
106
+docker compose up -d
68
107
```
69
108
70
109
### 5. Initialization
71
110
72
111
Initialize the admin user:
73
112
```bash
74
-docker-compose exec app bun run scripts/seed-admin.ts
113
+docker compose exec app bun scripts/seed-admin.ts
75
114
```
76
115
77
116
---
98
137
client_max_body_size 500M;
99
138
100
139
location / {
101
- proxy_pass http://localhost:4321;
140
+ proxy_pass http://127.0.0.1:4321;
102
141
proxy_set_header Host $host;
103
142
proxy_set_header X-Real-IP $remote_addr;
104
143
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
105
144
proxy_set_header X-Forwarded-Proto $scheme;
145
+ proxy_buffering off;
146
+ proxy_read_timeout 600s;
147
+ proxy_send_timeout 600s;
106
148
}
107
149
}
108
150
```
109
-
110
-
111
-## ✅ Production Checklist
112
-
113
-Before going live to users:
114
-
115
-- [ ] **Secrets Rotated**: Default secrets replaced with strong random strings.
116
-- [ ] **HTTPS Enabled**: SSL certificate configured via Nginx/Caddy.
117
-- [ ] **Rate Limiting**: `RATE_LIMIT_*` env vars adjusted for expected load.
118
-- [ ] **Monitoring**: Grafana/Sentry configured for error tracking ([Guide](../administration/monitoring.md)).
119
-
120
-
121
-## Next Steps
122
-
123
-Now that you have OpenCodeHub up and running, let's look around:
124
-
125
-👉 **[Quick Start Guide](quick-start.md)**
126
151
▼drizzle.config.ts
+7-3
1
+import { config } from "dotenv";
2
+config();
1
3
import { defineConfig } from "drizzle-kit";
2
4
3
5
// Get driver type from environment
5
7
const driver = process.env.DATABASE_DRIVER || "postgres";
6
8
const url =
7
9
process.env.DATABASE_URL ||
8
- "postgresql://opencodehub:opencodehub@localhost:5432/opencodehub";
10
+ "postgresql://localhost:5432/opencodehub";
9
11
10
12
// Map driver to Drizzle dialect
11
13
const dialectMap: Record<string, "sqlite" | "postgresql" | "mysql" | "turso"> =
23
25
function getCredentials() {
24
26
switch (driver) {
25
27
case "postgres":
26
- case "mysql":
27
- return { url, ssl: { rejectUnauthorized: false } };
28
+ case "mysql": {
29
+ const isLocal = url.includes("localhost") || url.includes("127.0.0.1") || url.includes("@postgres:");
30
+ return { url, ssl: isLocal ? false : { rejectUnauthorized: false } };
31
+ }
28
32
case "libsql":
29
33
case "turso":
30
34
return { url, authToken: process.env.DATABASE_AUTH_TOKEN };
31
35
▼public/favicon.png
+0-0
Binary file not shown.
▼public/favicon.svg
+8-17
1
-<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
2
-<g id="owl-logo">
3
-<circle id="outer-circle" cx="256" cy="256" r="230" fill="#0D2C54"/>
4
-<path id="inner-circle-teal" d="M486 256C486 383.025 383.025 486 256 486C128.975 486 26 383.025 26 256C26 128.975 128.975 26 256 26C383.025 26 486 128.975 486 256Z" fill="#008080"/>
5
-<path id="blue-accents" d="M256 486C128.975 486 26 383.025 26 256C26 128.975 128.975 26 256 26C383.025 26 486 128.975 486 256C486 383.025 383.025 486 256 486Z" fill="#0080FF" fill-opacity="0.6"/>
6
-<path id="owl-brows" d="M88.4 124.8C88.4 124.8 173.2 227.6 256 227.6C338.8 227.6 423.6 124.8 423.6 124.8C423.6 124.8 423.6 256 423.6 256C423.6 256 338.8 338.8 256 338.8C173.2 338.8 88.4 256 88.4 256C88.4 256 88.4 124.8 88.4 124.8Z" fill="#0D2C54"/>
7
-<path id="owl-body-teal" d="M256 338.8C173.2 338.8 88.4 256 88.4 256C88.4 348.6 163.4 423.6 256 423.6C348.6 423.6 423.6 348.6 423.6 256C423.6 256 338.8 338.8 256 338.8Z" fill="#008080"/>
8
-<path id="circuit" d="M256 66V180" stroke="#0080FF" stroke-width="16" stroke-linecap="round"/>
9
-<circle id="circuit-dot" cx="294" cy="114" r="16" fill="#0080FF"/>
10
-<path id="circuit-branch" d="M256 144H294" stroke="#0080FF" stroke-width="16" stroke-linecap="round"/>
11
-<g id="eyes">
12
-<circle cx="176" cy="256" r="64" fill="#008080"/>
13
-<circle cx="336" cy="256" r="64" fill="#008080"/>
14
-<path d="M156 236L136 256L156 276" stroke="#0D2C54" stroke-width="16" stroke-linecap="round" stroke-linejoin="round"/>
15
-<path d="M356 236L376 256L356 276" stroke="#0D2C54" stroke-width="16" stroke-linecap="round" stroke-linejoin="round"/>
16
-</g>
17
-</g>
1
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="45 45 210 210" fill="currentColor">
2
+ <style>
3
+ :root { color: #1f2328; }
4
+ @media (prefers-color-scheme: dark) {
5
+ :root { color: #f0f6fc; }
6
+ }
7
+ </style>
8
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M184.27588,89.68521c2.96924,0.22368 3.29297,2.88721 3.75439,5.34082c1.19385,6.34468 0.82178,12.62754 0.00293,18.98174c0.34131,0.62461 0.66797,1.25728 0.97998,1.89756c5.40088,11.37407 2.61914,24.01758 -7.58203,31.66011c-2.17236,1.62891 -4.00049,2.51074 -6.4248,3.66357c0.82178,0.76904 1.60254,1.5293 2.35693,2.37012c10.20264,11.34814 10.51318,31.02539 1.01953,42.86865c-0.7749,0.9668 -1.70801,1.875 -2.36572,2.91064c-0.18896,1.81934 -0.08496,5.79053 -0.08496,7.70508l0.00732,14.01855c0.01172,4.06201 0.68115,7.9541 -2.32178,11.17236c-1.55713,1.69189 -3.74414,2.66602 -6.04395,2.69238c-2.2251,0.0542 -4.37256,-0.81006 -5.93994,-2.3877c-2.77588,-2.78174 -2.42139,-5.74805 -2.42285,-9.37646c-0.00293,-2.15186 0.00293,-4.32129 0.00293,-6.47314l0.00146,-24.48047c6.01758,-2.28369 9.25928,-8.7041 7.61719,-14.91797c-0.83789,-3.1875 -2.90332,-5.91357 -5.74365,-7.5835c-2.87109,-1.6626 -6.28418,-2.12109 -9.49365,-1.27441c-3.25635,0.86279 -6.023,3.00879 -7.66934,5.94727c-1.6771,2.98242 -2.0748,6.51709 -1.10244,9.79834c1.07827,3.66211 3.30176,6.06738 6.59795,7.8457v31.3418l-0.00146,8.86523c-0.00293,1.69775 -0.05127,3.50098 0.06885,5.18408c0.07031,0.98438 0.24023,1.74902 0.93457,2.45801c1.06494,1.08838 2.63818,1.26123 4.08838,1.25244c5.39648,-0.03516 11.21777,-0.63721 16.4458,-2.0083c11.50488,-3.01465 25.31982,-11.12842 33.73096,-19.5498c-1.04004,-3.65479 -1.04297,-6.17139 0.27979,-9.73682c0.11426,-0.28418 0.23584,-0.56396 0.36768,-0.83936c1.53955,-3.31641 4.34473,-5.87695 7.78711,-7.10889c3.49512,-1.24365 7.1792,-1.10596 10.5249,0.52588c3.38965,1.6582 5.97949,4.59814 7.19385,8.16943c1.18945,3.55518 0.89648,7.43848 -0.81152,10.77539c-1.72119,3.35449 -4.70801,5.88574 -8.2998,7.03418c-3.99609,1.25098 -7.6333,0.60791 -11.26465,-1.28613c-11.9165,11.17676 -26.95312,19.90283 -43.24658,22.32861c-6.1377,0.91406 -18.28271,1.79883 -24.2209,0.28711c-1.4543,-0.36035 -2.82349,-1.00342 -4.0292,-1.89258c-1.92026,-1.43994 -3.30073,-3.4834 -3.91978,-5.80225c-0.80962,-2.91797 -0.59106,-7.26416 -0.58989,-10.39453l0.0126,-20.7627c0.00176,-2.729 0.26045,-7.91602 -0.20142,-10.47656c-0.9293,-1.44434 -2.89556,-3.36035 -4.03506,-4.95996c-7.36787,-10.34326 -8.05313,-24.92871 -1.97593,-36.03662c1.89521,-3.46436 3.96914,-5.61768 6.89136,-8.27051c-1.10273,-0.60498 -2.32017,-1.15137 -3.42407,-1.75488c-10.07725,-5.50151 -15.41455,-16.65469 -12.49043,-27.89648c0.75337,-2.89644 1.79604,-5.09502 3.26177,-7.72251c-0.23101,-1.61455 -0.4084,-3.23628 -0.53218,-4.86255c-0.26484,-3.68013 -0.14707,-15.76128 2.28369,-18.38569c0.49512,-0.53452 1.12632,-0.72598 1.84219,-0.73696c4.777,-0.07251 14.12007,8.55615 17.44307,11.97817c6.83555,-0.78853 16.69834,-0.79482 23.51572,0.04937c3.84082,-4.00254 11.6543,-11.28779 17.2251,-12.14487z M216.82617,208.70947c3.49512,-0.50391 6.74268,1.9043 7.27588,5.39355c0.5332,3.49072 -1.8457,6.75879 -5.33057,7.32275c-3.52881,0.57129 -6.84521,-1.84424 -7.38574,-5.37744c-0.54053,-3.5332 1.90283,-6.8291 5.44043,-7.33887z M150.01025,49.42808c8.76855,-0.53481 18.6665,0.87524 27.06592,3.29912c20.78613,5.99868 38.0083,20.7375 48.39844,39.57656c11.02441,20.03057 13.20557,42.40825 6.84668,64.47847c-4.68457,16.26416 -15.27686,33.76758 -29.49902,43.79297c-0.8291,0.58447 -2.42578,0.64014 -3.39551,0.33105c-1.20264,-0.38965 -2.1958,-1.24951 -2.75391,-2.38477c-0.78223,-1.54248 -0.68701,-3.38379 0.24756,-4.83838c0.77344,-1.19238 2.30859,-1.99658 3.36621,-2.96631c1.39453,-1.27734 2.7832,-2.58984 4.08691,-3.96094c8.34229,-8.7583 14.54004,-19.33154 18.10547,-30.89062c6.12744,-19.20322 4.20703,-40.06919 -5.31885,-57.83232c-13.10742,-24.82339 -38.29687,-39.67676 -66.35449,-38.24443c-11.62852,0.5937 -20.97378,3.44312 -31.22754,8.85864c-0.09214,0.21592 0.06855,2.36089 0.08599,2.76357c0.10093,2.32456 -0.25884,4.14609 -1.20132,6.28271c-1.61675,3.70122 -4.65234,6.5981 -8.4252,8.03994c-4.34063,1.67285 -7.91528,1.0749 -12.02065,-0.72729c-1.39482,1.60225 -2.82202,3.25049 -4.10435,4.94326c-9.65625,12.99712 -14.91196,28.73716 -15.0019,44.92852c0.00176,4.17891 0.44326,7.88203 1.01514,11.99033c2.62295,0.16113 4.83779,0.89355 7.03564,2.35986c3.29355,2.22656 5.54854,5.68799 6.254,9.60059c0.80186,4.28027 -0.46406,7.95996 -2.85498,11.45801c6.1188,10.2832 12.25576,17.34961 22.19165,24.18604c2.72197,1.86182 5.57842,3.51855 8.54546,4.95703c1.91646,0.92285 5.19565,1.96143 6.36152,3.69434c0.70986,1.0708 0.95127,2.38623 0.66841,3.64014c-0.63237,2.74805 -3.42524,4.40332 -6.0498,3.42188c-16.91338,-6.33398 -30.71909,-19.39453 -39.48354,-34.77979c-1.91895,0.38965 -3.55415,0.55664 -5.51953,0.2959c-3.75059,-0.50244 -7.1439,-2.48584 -9.42217,-5.50635c-2.37363,-3.13184 -3.39595,-7.08252 -2.83887,-10.97168c0.58975,-4.17334 2.75815,-7.26123 6.04922,-9.76172c-0.84521,-4.86138 -1.40742,-9.20464 -1.51685,-14.15186c-0.4582,-20.73208 7.9248,-42.52515 22.04824,-57.59678c-0.52134,-1.40479 -0.85063,-2.36895 -1.07036,-3.8603c-0.57583,-3.9999 0.44736,-8.06558 2.8478,-11.31636c2.4063,-3.16348 5.97246,-5.23945 9.91157,-5.76943c4.11064,-0.54932 7.88833,0.62358 11.15728,3.0999c12.08159,-6.54038 21.99536,-9.5105 35.76973,-10.4395z M104.33159,64.69878c3.83057,-0.32915 7.21245,2.48599 7.58335,6.31274c0.37104,3.82676 -2.40718,7.23911 -6.22969,7.65176c-2.51001,0.271 -4.97197,-0.82749 -6.44678,-2.87651c-1.4748,-2.04902 -1.73496,-4.73247 -0.68115,-7.02656c1.05381,-2.29424 3.25898,-3.84521 5.77427,-4.06143z M77.86846,154.6333c3.67031,-0.67822 7.19575,1.74463 7.87866,5.41406c0.68291,3.66943 -1.73511,7.19824 -5.40337,7.88672c-3.67588,0.68994 -7.21377,-1.73437 -7.898,-5.41113c-0.68437,-3.67676 1.74478,-7.21143 5.42271,-7.88965z M153.91992,174.95801c3.0835,-0.47461 5.96777,1.64209 6.43945,4.72559c0.47021,3.0835 -1.64941,5.96484 -4.73437,6.43359c-3.0791,0.46582 -5.95459,-1.64941 -6.4248,-4.72852c-0.47021,-3.07764 1.64209,-5.95605 4.71973,-6.43066z" />
18
9
</svg>
19
10
▼public/logo.svg
+8-76
1
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 80" width="320" height="80">
2
- <defs>
3
- <!-- Gradient for owl -->
4
- <linearGradient id="owlGradient" x1="0%" y1="0%" x2="100%" y2="100%">
5
- <stop offset="0%" style="stop-color:#22c55e"/>
6
- <stop offset="100%" style="stop-color:#3b82f6"/>
7
- </linearGradient>
8
- <!-- Glow effect -->
9
- <filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
10
- <feGaussianBlur stdDeviation="2" result="coloredBlur"/>
11
- <feMerge>
12
- <feMergeNode in="coloredBlur"/>
13
- <feMergeNode in="SourceGraphic"/>
14
- </feMerge>
15
- </filter>
16
- </defs>
17
-
18
- <!-- Owl Icon -->
19
- <g transform="translate(10, 8)" filter="url(#glow)">
20
- <!-- Owl body/head circle -->
21
- <circle cx="32" cy="32" r="28" fill="url(#owlGradient)" opacity="0.15"/>
22
- <circle cx="32" cy="32" r="28" fill="none" stroke="url(#owlGradient)" stroke-width="2.5"/>
23
-
24
- <!-- Owl ears -->
25
- <path d="M12 12 L20 22 L8 22 Z" fill="url(#owlGradient)"/>
26
- <path d="M52 12 L44 22 L56 22 Z" fill="url(#owlGradient)"/>
27
-
28
- <!-- Left eye - code bracket < -->
29
- <g transform="translate(16, 24)">
30
- <path d="M12 0 L4 8 L12 16"
31
- fill="none"
32
- stroke="url(#owlGradient)"
33
- stroke-width="3"
34
- stroke-linecap="round"
35
- stroke-linejoin="round"/>
36
- </g>
37
-
38
- <!-- Right eye - code bracket > -->
39
- <g transform="translate(36, 24)">
40
- <path d="M4 0 L12 8 L4 16"
41
- fill="none"
42
- stroke="url(#owlGradient)"
43
- stroke-width="3"
44
- stroke-linecap="round"
45
- stroke-linejoin="round"/>
46
- </g>
47
-
48
- <!-- Owl beak -->
49
- <path d="M28 44 L32 50 L36 44"
50
- fill="none"
51
- stroke="url(#owlGradient)"
52
- stroke-width="2"
53
- stroke-linecap="round"
54
- stroke-linejoin="round"/>
55
- </g>
56
-
57
- <!-- Text: OpenCodeHub -->
58
- <g transform="translate(80, 48)">
59
- <!-- Open -->
60
- <text font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
61
- font-size="28"
62
- font-weight="400"
63
- fill="#f8fafc">Open</text>
64
- <!-- Code -->
65
- <text x="68"
66
- font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
67
- font-size="28"
68
- font-weight="700"
69
- fill="url(#owlGradient)">Code</text>
70
- <!-- Hub -->
71
- <text x="138"
72
- font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
73
- font-size="28"
74
- font-weight="400"
75
- fill="#f8fafc">Hub</text>
76
- </g>
1
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="45 45 210 210" fill="currentColor">
2
+ <style>
3
+ :root { color: #1f2328; }
4
+ @media (prefers-color-scheme: dark) {
5
+ :root { color: #f0f6fc; }
6
+ }
7
+ </style>
8
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M184.27588,89.68521c2.96924,0.22368 3.29297,2.88721 3.75439,5.34082c1.19385,6.34468 0.82178,12.62754 0.00293,18.98174c0.34131,0.62461 0.66797,1.25728 0.97998,1.89756c5.40088,11.37407 2.61914,24.01758 -7.58203,31.66011c-2.17236,1.62891 -4.00049,2.51074 -6.4248,3.66357c0.82178,0.76904 1.60254,1.5293 2.35693,2.37012c10.20264,11.34814 10.51318,31.02539 1.01953,42.86865c-0.7749,0.9668 -1.70801,1.875 -2.36572,2.91064c-0.18896,1.81934 -0.08496,5.79053 -0.08496,7.70508l0.00732,14.01855c0.01172,4.06201 0.68115,7.9541 -2.32178,11.17236c-1.55713,1.69189 -3.74414,2.66602 -6.04395,2.69238c-2.2251,0.0542 -4.37256,-0.81006 -5.93994,-2.3877c-2.77588,-2.78174 -2.42139,-5.74805 -2.42285,-9.37646c-0.00293,-2.15186 0.00293,-4.32129 0.00293,-6.47314l0.00146,-24.48047c6.01758,-2.28369 9.25928,-8.7041 7.61719,-14.91797c-0.83789,-3.1875 -2.90332,-5.91357 -5.74365,-7.5835c-2.87109,-1.6626 -6.28418,-2.12109 -9.49365,-1.27441c-3.25635,0.86279 -6.023,3.00879 -7.66934,5.94727c-1.6771,2.98242 -2.0748,6.51709 -1.10244,9.79834c1.07827,3.66211 3.30176,6.06738 6.59795,7.8457v31.3418l-0.00146,8.86523c-0.00293,1.69775 -0.05127,3.50098 0.06885,5.18408c0.07031,0.98438 0.24023,1.74902 0.93457,2.45801c1.06494,1.08838 2.63818,1.26123 4.08838,1.25244c5.39648,-0.03516 11.21777,-0.63721 16.4458,-2.0083c11.50488,-3.01465 25.31982,-11.12842 33.73096,-19.5498c-1.04004,-3.65479 -1.04297,-6.17139 0.27979,-9.73682c0.11426,-0.28418 0.23584,-0.56396 0.36768,-0.83936c1.53955,-3.31641 4.34473,-5.87695 7.78711,-7.10889c3.49512,-1.24365 7.1792,-1.10596 10.5249,0.52588c3.38965,1.6582 5.97949,4.59814 7.19385,8.16943c1.18945,3.55518 0.89648,7.43848 -0.81152,10.77539c-1.72119,3.35449 -4.70801,5.88574 -8.2998,7.03418c-3.99609,1.25098 -7.6333,0.60791 -11.26465,-1.28613c-11.9165,11.17676 -26.95312,19.90283 -43.24658,22.32861c-6.1377,0.91406 -18.28271,1.79883 -24.2209,0.28711c-1.4543,-0.36035 -2.82349,-1.00342 -4.0292,-1.89258c-1.92026,-1.43994 -3.30073,-3.4834 -3.91978,-5.80225c-0.80962,-2.91797 -0.59106,-7.26416 -0.58989,-10.39453l0.0126,-20.7627c0.00176,-2.729 0.26045,-7.91602 -0.20142,-10.47656c-0.9293,-1.44434 -2.89556,-3.36035 -4.03506,-4.95996c-7.36787,-10.34326 -8.05313,-24.92871 -1.97593,-36.03662c1.89521,-3.46436 3.96914,-5.61768 6.89136,-8.27051c-1.10273,-0.60498 -2.32017,-1.15137 -3.42407,-1.75488c-10.07725,-5.50151 -15.41455,-16.65469 -12.49043,-27.89648c0.75337,-2.89644 1.79604,-5.09502 3.26177,-7.72251c-0.23101,-1.61455 -0.4084,-3.23628 -0.53218,-4.86255c-0.26484,-3.68013 -0.14707,-15.76128 2.28369,-18.38569c0.49512,-0.53452 1.12632,-0.72598 1.84219,-0.73696c4.777,-0.07251 14.12007,8.55615 17.44307,11.97817c6.83555,-0.78853 16.69834,-0.79482 23.51572,0.04937c3.84082,-4.00254 11.6543,-11.28779 17.2251,-12.14487z M216.82617,208.70947c3.49512,-0.50391 6.74268,1.9043 7.27588,5.39355c0.5332,3.49072 -1.8457,6.75879 -5.33057,7.32275c-3.52881,0.57129 -6.84521,-1.84424 -7.38574,-5.37744c-0.54053,-3.5332 1.90283,-6.8291 5.44043,-7.33887z M150.01025,49.42808c8.76855,-0.53481 18.6665,0.87524 27.06592,3.29912c20.78613,5.99868 38.0083,20.7375 48.39844,39.57656c11.02441,20.03057 13.20557,42.40825 6.84668,64.47847c-4.68457,16.26416 -15.27686,33.76758 -29.49902,43.79297c-0.8291,0.58447 -2.42578,0.64014 -3.39551,0.33105c-1.20264,-0.38965 -2.1958,-1.24951 -2.75391,-2.38477c-0.78223,-1.54248 -0.68701,-3.38379 0.24756,-4.83838c0.77344,-1.19238 2.30859,-1.99658 3.36621,-2.96631c1.39453,-1.27734 2.7832,-2.58984 4.08691,-3.96094c8.34229,-8.7583 14.54004,-19.33154 18.10547,-30.89062c6.12744,-19.20322 4.20703,-40.06919 -5.31885,-57.83232c-13.10742,-24.82339 -38.29687,-39.67676 -66.35449,-38.24443c-11.62852,0.5937 -20.97378,3.44312 -31.22754,8.85864c-0.09214,0.21592 0.06855,2.36089 0.08599,2.76357c0.10093,2.32456 -0.25884,4.14609 -1.20132,6.28271c-1.61675,3.70122 -4.65234,6.5981 -8.4252,8.03994c-4.34063,1.67285 -7.91528,1.0749 -12.02065,-0.72729c-1.39482,1.60225 -2.82202,3.25049 -4.10435,4.94326c-9.65625,12.99712 -14.91196,28.73716 -15.0019,44.92852c0.00176,4.17891 0.44326,7.88203 1.01514,11.99033c2.62295,0.16113 4.83779,0.89355 7.03564,2.35986c3.29355,2.22656 5.54854,5.68799 6.254,9.60059c0.80186,4.28027 -0.46406,7.95996 -2.85498,11.45801c6.1188,10.2832 12.25576,17.34961 22.19165,24.18604c2.72197,1.86182 5.57842,3.51855 8.54546,4.95703c1.91646,0.92285 5.19565,1.96143 6.36152,3.69434c0.70986,1.0708 0.95127,2.38623 0.66841,3.64014c-0.63237,2.74805 -3.42524,4.40332 -6.0498,3.42188c-16.91338,-6.33398 -30.71909,-19.39453 -39.48354,-34.77979c-1.91895,0.38965 -3.55415,0.55664 -5.51953,0.2959c-3.75059,-0.50244 -7.1439,-2.48584 -9.42217,-5.50635c-2.37363,-3.13184 -3.39595,-7.08252 -2.83887,-10.97168c0.58975,-4.17334 2.75815,-7.26123 6.04922,-9.76172c-0.84521,-4.86138 -1.40742,-9.20464 -1.51685,-14.15186c-0.4582,-20.73208 7.9248,-42.52515 22.04824,-57.59678c-0.52134,-1.40479 -0.85063,-2.36895 -1.07036,-3.8603c-0.57583,-3.9999 0.44736,-8.06558 2.8478,-11.31636c2.4063,-3.16348 5.97246,-5.23945 9.91157,-5.76943c4.11064,-0.54932 7.88833,0.62358 11.15728,3.0999c12.08159,-6.54038 21.99536,-9.5105 35.76973,-10.4395z M104.33159,64.69878c3.83057,-0.32915 7.21245,2.48599 7.58335,6.31274c0.37104,3.82676 -2.40718,7.23911 -6.22969,7.65176c-2.51001,0.271 -4.97197,-0.82749 -6.44678,-2.87651c-1.4748,-2.04902 -1.73496,-4.73247 -0.68115,-7.02656c1.05381,-2.29424 3.25898,-3.84521 5.77427,-4.06143z M77.86846,154.6333c3.67031,-0.67822 7.19575,1.74463 7.87866,5.41406c0.68291,3.66943 -1.73511,7.19824 -5.40337,7.88672c-3.67588,0.68994 -7.21377,-1.73437 -7.898,-5.41113c-0.68437,-3.67676 1.74478,-7.21143 5.42271,-7.88965z M153.91992,174.95801c3.0835,-0.47461 5.96777,1.64209 6.43945,4.72559c0.47021,3.0835 -1.64941,5.96484 -4.73437,6.43359c-3.0791,0.46582 -5.95459,-1.64941 -6.4248,-4.72852c-0.47021,-3.07764 1.64209,-5.95605 4.71973,-6.43066z" />
77
9
</svg>
78
10
▼public/opencodelogo.png
+0-0
Binary file not shown.
▼scripts/e2e-proof.sh
+41-32
4
4
# Requirements:
5
5
# - app running on :4321 (see .env / docker-compose)
6
6
# - Postgres + Redis reachable via DATABASE_URL / REDIS_URL (127.0.0.1)
7
-# - admin user seeded with password AdminPass123! (scripts/seed-admin.ts)
8
-# - psql available with the DATABASE_DRIVER credentials for state reset
7
+# - admin user seeded (scripts/seed-admin.ts)
8
+# - psql available with DATABASE_URL credentials for state reset
9
9
#
10
10
# Usage:
11
-# PSQL_URL="postgresql://och:[email protected]:5432/opencodehub" \
12
-# bash scripts/e2e-proof.sh
11
+# ADMIN_PASSWORD="<your-admin-password>" PSQL_URL="<your-database-url>" bash scripts/e2e-proof.sh
13
12
set -u
14
13
BASE="${BASE_URL:-http://localhost:4321}"
15
-PSQL_URL="${PSQL_URL:-postgresql://och:[email protected]:5432/opencodehub}"
14
+ADMIN_PASSWORD="${ADMIN_PASSWORD:-${E2E_ADMIN_PASSWORD:-}}"
15
+PSQL_URL="${PSQL_URL:-${DATABASE_URL:-}}"
16
+
17
+if [ -z "$ADMIN_PASSWORD" ]; then
18
+ echo "❌ Error: ADMIN_PASSWORD environment variable is required to run e2e proof."
19
+ exit 1
20
+fi
21
+
22
+if [ -z "$PSQL_URL" ]; then
23
+ echo "❌ Error: PSQL_URL or DATABASE_URL environment variable is required to run e2e proof."
24
+ exit 1
25
+fi
26
+
16
27
COOKIE_JAR="$(mktemp)"
17
28
WORK="$(mktemp -d)"
18
29
PASS=0; FAIL=0
24
35
rm -rf "${GIT_REPOS_PATH:-./data/repos}"/* 2>/dev/null || true
25
36
26
37
echo "== 1. Login as admin =="
27
-LOGIN=$(curl -s -c "$COOKIE_JAR" -X POST "$BASE/api/auth/login" -H "Content-Type: application/json" -d '{"login":"admin","password":"AdminPass123!"}')
38
+LOGIN=$(curl -s -c "$COOKIE_JAR" -X POST "$BASE/api/auth/login" -H "Content-Type: application/json" -d "{\"login\":\"admin\",\"password\":\"$ADMIN_PASSWORD\"}")
28
39
echo "$LOGIN" | grep -q '"success":true' && ok "login" || bad "login: $LOGIN"
29
40
30
41
echo "== 2. PAT + repo =="
98
109
USERINFO=$(curl -s -H "Authorization: Bearer $ATOKEN" "$BASE/api/oauth/userinfo")
99
110
echo "$USERINFO" | grep -q '"username":"admin"' && ok "userinfo via bearer" || bad "userinfo: ${USERINFO:0:150}"
100
111
101
-echo "== 7. Org + invite + transfer =="
102
-curl -s -X POST "$BASE/api/auth/register" -H "Content-Type: application/json" -d '{"username":"devuser","email":"[email protected]","password":"DevPass123!"}' > /dev/null
103
-DEVLOGIN=$(curl -s -c /tmp/och-dev-cookies.txt -X POST "$BASE/api/auth/login" -H "Content-Type: application/json" -d '{"login":"devuser","password":"DevPass123!"}')
104
-echo "$DEVLOGIN" | grep -q '"success":true' && ok "second user created+login" || bad "dev user: $DEVLOGIN"
105
-ORG_CREATE=$(curl -s -b "$COOKIE_JAR" -X POST "$BASE/api/orgs" -H "Content-Type: application/json" -d '{"name":"e2e-org","displayName":"E2E Org"}')
106
-echo "$ORG_CREATE" | grep -qE 'e2e-org|already exists' && ok "org created (or pre-existing)" || bad "org create: $ORG_CREATE"
107
-ORG_PAGE=$(curl -s -o /dev/null -w "%{http_code}" -b "$COOKIE_JAR" "$BASE/orgs/e2e-org")
108
-[ "$ORG_PAGE" = "200" ] && ok "org page renders" || bad "org page: $ORG_PAGE"
109
-INVITE=$(curl -s -b "$COOKIE_JAR" -X POST "$BASE/api/orgs/e2e-org/invites" -H "Content-Type: application/json" -d '{"username":"devuser"}')
110
-INVITE_TOKEN=$(echo "$INVITE" | grep -o 'token=[^"]*' | head -1 | cut -d= -f2)
111
-[ -n "$INVITE_TOKEN" ] && ok "invite created" || bad "invite: ${INVITE:0:150}"
112
-ACCEPT=$(curl -s -b /tmp/och-dev-cookies.txt -X POST "$BASE/api/orgs/accept-invite" -H "Content-Type: application/json" -d "{\"token\":\"$INVITE_TOKEN\"}")
113
-echo "$ACCEPT" | grep -q success && ok "invite accepted" || bad "accept: ${ACCEPT:0:150}"
114
-TRANSFER=$(curl -s -b "$COOKIE_JAR" -X POST "$BASE/api/repos/admin/e2e-prod/transfer" -H "Content-Type: application/json" -d '{"orgName":"e2e-org"}')
115
-echo "$TRANSFER" | grep -q success && ok "repo transferred to org" || bad "transfer: ${TRANSFER:0:150}"
112
+echo "== 7. Rebase stack endpoint =="
113
+STACK_RES=$(curl -s -b "$COOKIE_JAR" -X POST "$BASE/api/stacks/rebase" -H "Content-Type: application/json" -d '{"owner":"admin","repo":"e2e-prod","branches":["main"]}')
114
+echo "$STACK_RES" | grep -qE 'success|branches|rebased|no-op|already' && ok "rebase stack API" || bad "stack: ${STACK_RES:0:150}"
115
+
116
+echo "== 8. Rate limit header verification =="
117
+RATE_HDRS=$(curl -s -I "$BASE/api/health")
118
+echo "$RATE_HDRS" | grep -qi "x-ratelimit-remaining" && ok "rate limit headers emitted" || bad "missing rate headers"
119
+
120
+echo "== 9. Distributed lock multi-instance proof =="
121
+LOCK_RES=$(bun -e "
122
+ import { acquireLock } from './src/lib/distributed-lock.ts';
123
+ const l1 = await acquireLock('e2e-test-key', 5000);
124
+ const l2 = await acquireLock('e2e-test-key', 5000);
125
+ if (l1 && !l2) { await l1.release(); console.log('PASS'); process.exit(0); }
126
+ process.exit(1);
127
+" 2>/dev/null || echo "FAIL")
128
+[ "$LOCK_RES" = "PASS" ] && ok "distributed lock exclusivity" || bad "lock failed: $LOCK_RES"
116
129
117
-echo "== 8. Org-owned repo APIs + checks =="
118
-sleep 5
119
-CHECKS=$(curl -s -b "$COOKIE_JAR" "$BASE/api/repos/e2e-org/e2e-prod/pulls/$PR_NUM/checks" 2>/dev/null)
120
-echo "$CHECKS" | grep -q '"success":true' && ok "PR checks on org-owned repo" || bad "checks: ${CHECKS:0:150}"
121
-curl -s "$BASE/api/health" | grep -q '"ok":true\|"status":"ok"' && ok "health" || bad "health"
130
+echo "== 10. External CI webhook ingestion =="
131
+CI_PAYLOAD='{"provider":"github","event":"workflow_run","action":"completed","workflow":{"id":"1","name":"CI","head_sha":"abc1234","status":"completed","conclusion":"success"}}'
132
+CI_RES=$(curl -s -X POST "$BASE/api/repos/admin/e2e-prod/ci/webhook" -H "Content-Type: application/json" -d "$CI_PAYLOAD")
133
+echo "$CI_RES" | grep -qE 'accepted|received|success|ignored' && ok "CI webhook ingested" || bad "ci webhook: ${CI_RES:0:150}"
122
134
135
+rm -f "$COOKIE_JAR"; rm -rf "$WORK"
123
136
echo ""
124
-echo "════════════════════════════════════════"
125
-echo " E2E RESULT: $PASS passed, $FAIL failed"
126
-echo "════════════════════════════════════════"
127
-rm -f "$COOKIE_JAR" /tmp/och-dev-cookies.txt
128
-rm -rf "$WORK"
129
-exit $FAIL
137
+echo "=== SUMMARY: $PASS passed, $FAIL failed ==="
138
+[ "$FAIL" -eq 0 ] && exit 0 || exit 1
130
139
▼src/components/dashboard/StackCard.astro
+48-53
27
27
const hasFailures = stack.prs.some(pr => pr.status === 'failure' || pr.status === 'error');
28
28
const isDraft = stack.prs.some(pr => pr.status === 'draft');
29
29
30
-const statusColor = isFullyMerged ? "text-green-400" : hasFailures ? "text-red-400" : isDraft ? "text-muted-foreground" : "text-blue-400";
31
-const statusBorder = isFullyMerged ? "border-green-500/30" : hasFailures ? "border-red-500/30" : isDraft ? "border-gray-500/30" : "border-blue-500/30";
32
-const statusBg = isFullyMerged ? "bg-green-500/10" : hasFailures ? "bg-red-500/10" : isDraft ? "bg-gray-500/10" : "bg-blue-500/10";
30
+const statusColor = isFullyMerged ? "text-emerald-500" : hasFailures ? "text-red-400" : isDraft ? "text-muted-foreground" : "text-primary";
31
+const statusBorder = isFullyMerged ? "border-emerald-500/30" : hasFailures ? "border-red-500/30" : isDraft ? "border-border" : "border-primary/30";
32
+const statusBg = isFullyMerged ? "bg-emerald-500/10" : hasFailures ? "bg-red-500/10" : isDraft ? "bg-muted/40" : "bg-primary/10";
33
33
---
34
34
35
-<div class="relative group">
36
- <div class="absolute -inset-[1px] bg-gradient-to-r from-blue-500/20 via-cyan-500/20 to-purple-500/20 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity" />
37
- <div class="relative rounded-xl border border-border bg-card/80 backdrop-blur-sm p-5 hover:bg-accent/50 transition-colors">
38
-
39
- <!-- Header -->
40
- <div class="flex items-center justify-between mb-4">
41
- <div>
42
- <div class="flex items-center gap-2">
43
- <span class={`text-sm font-semibold ${statusColor}`}>{stack.name}</span>
44
- <span class="text-xs text-muted-foreground">in {stack.repoOwner}/{stack.repoName}</span>
45
- </div>
46
- <div class="text-xs text-muted-foreground mt-0.5">
47
- Updated {new Date(stack.updatedAt).toLocaleDateString()}
48
- </div>
35
+<div class="rounded-xl border border-border/80 bg-card/60 backdrop-blur-sm p-5 hover:border-border hover:bg-accent/40 transition-colors shadow-sm">
36
+ <!-- Header -->
37
+ <div class="flex items-center justify-between mb-4 pb-3 border-b border-border/60">
38
+ <div>
39
+ <div class="flex items-center gap-2">
40
+ <span class="text-sm font-bold text-foreground">{stack.name}</span>
41
+ <span class="text-xs text-muted-foreground">in {stack.repoOwner}/{stack.repoName}</span>
49
42
</div>
50
- <div class={`text-xs px-2 py-0.5 rounded-full border ${statusBorder} ${statusColor} ${statusBg}`}>
51
- {isFullyMerged ? "Merged" : hasFailures ? "Failed" : isDraft ? "Draft" : "Active"}
43
+ <div class="text-[11px] text-muted-foreground mt-0.5">
44
+ Updated {new Date(stack.updatedAt).toLocaleDateString()}
52
45
</div>
53
46
</div>
47
+ <div class={`text-xs font-semibold px-2 py-0.5 rounded-full border ${statusBorder} ${statusColor} ${statusBg}`}>
48
+ {isFullyMerged ? "Merged" : hasFailures ? "Failed" : isDraft ? "Draft" : "Active"}
49
+ </div>
50
+ </div>
54
51
55
- <!-- Stack Visualization (Subway Map) -->
56
- <div class="relative pl-2 space-y-0">
57
- {stack.prs.map((pr, index) => (
58
- <div class="relative flex items-start gap-3 pb-4 last:pb-0">
59
- <!-- Connecting Line -->
60
- {index !== stack.prs.length - 1 && (
61
- <div class="absolute left-[5px] top-2.5 bottom-0 w-0.5 bg-gray-800" />
62
- )}
63
-
64
- <!-- Dot -->
65
- <div class={`relative z-10 w-3 h-3 rounded-full mt-1.5 border-2 ${
66
- pr.isMerged
67
- ? "bg-green-500 border-green-500"
68
- : pr.isClosed
69
- ? "bg-red-500 border-red-500"
70
- : "bg-card border-blue-500"
71
- }`} />
52
+ <!-- Stack Subway Visualization -->
53
+ <div class="relative pl-2 space-y-0">
54
+ {stack.prs.map((pr, index) => (
55
+ <div class="relative flex items-start gap-3 pb-4 last:pb-0">
56
+ <!-- Connecting Line -->
57
+ {index !== stack.prs.length - 1 && (
58
+ <div class="absolute left-[5px] top-2.5 bottom-0 w-0.5 bg-border" />
59
+ )}
60
+
61
+ <!-- Dot -->
62
+ <div class={`relative z-10 w-3 h-3 rounded-full mt-1 border-2 ${
63
+ pr.isMerged
64
+ ? "bg-emerald-500 border-emerald-500"
65
+ : pr.isClosed
66
+ ? "bg-red-500 border-red-500"
67
+ : "bg-background border-primary"
68
+ }`} />
72
69
73
- <!-- Content -->
74
- <div class="flex-1 min-w-0 -mt-0.5">
75
- <a href={pr.url} class="block hover:underline truncate text-sm font-medium text-muted-foreground hover:text-blue-400 decoration-blue-400/50">
76
- {pr.title}
77
- </a>
78
- <div class="flex items-center gap-2 text-xs text-muted-foreground">
79
- <span class="font-mono">#{pr.number}</span>
80
- <span>•</span>
81
- <span class={
82
- pr.isMerged ? "text-green-500" : pr.isClosed ? "text-red-500" : "text-blue-500"
83
- }>
84
- {pr.status.charAt(0).toUpperCase() + pr.status.slice(1)}
85
- </span>
86
- </div>
70
+ <!-- Content -->
71
+ <div class="flex-1 min-w-0">
72
+ <a href={pr.url} class="block hover:underline truncate text-sm font-semibold text-foreground hover:text-primary transition-colors">
73
+ {pr.title}
74
+ </a>
75
+ <div class="flex items-center gap-2 text-xs text-muted-foreground mt-0.5">
76
+ <span class="font-mono text-[11px]">#{pr.number}</span>
77
+ <span>·</span>
78
+ <span class={
79
+ pr.isMerged ? "text-emerald-500 font-medium" : pr.isClosed ? "text-red-400 font-medium" : "text-primary font-medium"
80
+ }>
81
+ {pr.status.charAt(0).toUpperCase() + pr.status.slice(1)}
82
+ </span>
87
83
</div>
88
84
</div>
89
- ))}
90
- </div>
91
-
85
+ </div>
86
+ ))}
92
87
</div>
93
88
</div>
94
89
▼src/components/ide/WebIDE.tsx
+3-2
2
2
import { Folder, File, ChevronRight, ChevronDown, GitCommit, X, GitBranch, RefreshCw, XCircle, AlertTriangle, Layout, Search, Settings, Home, Activity, Loader2 } from "lucide-react";
3
3
import Editor from "@monaco-editor/react";
4
4
import { cn } from "@/lib/utils";
5
+import { Logo } from "@/components/ui/Logo";
5
6
6
7
interface WebIDEProps {
7
8
owner: string;
251
252
{/* VS Code Title Bar */}
252
253
<div className="h-8 w-full bg-[#3c3c3c] flex items-center px-3 border-b border-[#2d2d2d] shrink-0 text-xs">
253
254
<div className="flex items-center gap-1 text-muted-foreground">
254
- <img src="/logo.svg" alt="logo" className="h-4 w-4 mr-2 opacity-90 brightness-200" />
255
+ <Logo className="h-4 w-4 mr-2 text-[#cccccc]" />
255
256
<span className="cursor-default hover:bg-secondary/80 px-2 py-1 rounded transition-colors">File</span>
256
257
<span className="cursor-default hover:bg-secondary/80 px-2 py-1 rounded transition-colors">Edit</span>
257
258
<span className="cursor-default hover:bg-secondary/80 px-2 py-1 rounded transition-colors">Selection</span>
479
480
/>
480
481
) : (
481
482
<div className="w-full h-full flex flex-col items-center justify-center text-[#858585] select-none bg-[#1e1e1e]">
482
- <img src="/logo.svg" alt="logo" className="h-24 w-24 mb-6 opacity-5 grayscale" />
483
+ <Logo className="h-24 w-24 mb-6 opacity-10 text-[#858585]" />
483
484
<div className="flex gap-12 text-sm text-[#858585]">
484
485
<div className="flex flex-col gap-3">
485
486
<div className="font-semibold text-[#cccccc]">Start</div>
486
487
▼src/components/issues/IssueDetail.tsx
+831-417
1
1
"use client";
2
+
2
3
import { motion } from "framer-motion";
3
4
import {
4
5
AlertCircle,
5
6
CheckCircle2,
7
+ CircleDot,
6
8
MessageSquare,
7
- User,
8
9
Tag,
9
10
Calendar,
10
- Clock,
11
11
Link2,
12
12
GitBranch,
13
- MoreHorizontal,
13
+ GitPullRequest,
14
14
Edit3,
15
- Pin,
16
- Lock,
17
- Trash2,
18
15
Layers,
19
16
CheckSquare,
20
17
ListTodo,
21
- Users
18
+ Users,
19
+ Plus,
20
+ Layout,
21
+ ArrowUpRight,
22
+ Lock,
23
+ Send,
24
+ Loader2
22
25
} from "lucide-react";
23
26
import { useEffect, useState } from "react";
24
27
import { marked } from "marked";
25
28
import DOMPurify from "isomorphic-dompurify";
26
-import { formatDistanceToNow } from "date-fns";
27
29
28
30
interface Issue {
29
31
id: string;
43
45
avatarUrl?: string;
44
46
};
45
47
assignees?: Array<{
48
+ id?: string;
46
49
username: string;
47
50
avatarUrl?: string;
48
51
}>;
70
73
repoName: string;
71
74
canLink?: boolean;
72
75
canEdit?: boolean;
76
+ currentUser?: {
77
+ id: string;
78
+ username: string;
79
+ avatarUrl?: string;
80
+ };
73
81
}
74
82
75
-function timeAgo(date: string): string {
76
- const now = new Date();
77
- const past = new Date(date);
78
- const diffMs = now.getTime() - past.getTime();
79
- const diffMins = Math.floor(diffMs / 60000);
80
-
81
- if (diffMins < 1) return 'just now';
82
- if (diffMins < 60) return `${diffMins} minutes ago`;
83
+interface Comment {
84
+ id: string;
85
+ body: string;
86
+ createdAt: string;
87
+ author: {
88
+ id: string;
89
+ username: string;
90
+ displayName?: string;
91
+ avatarUrl?: string;
92
+ };
93
+}
83
94
95
+function formatTimeAgo(dateString: string): string {
96
+ if (!dateString) return "";
97
+ const date = new Date(dateString);
98
+ const now = new Date();
99
+ const diffMs = now.getTime() - date.getTime();
100
+ const diffSecs = Math.max(0, Math.floor(diffMs / 1000));
101
+
102
+ if (diffSecs < 60) return "just now";
103
+ const diffMins = Math.floor(diffSecs / 60);
104
+ if (diffMins < 60) return `${diffMins} ${diffMins === 1 ? "minute" : "minutes"} ago`;
84
105
const diffHours = Math.floor(diffMins / 60);
85
- if (diffHours < 24) return `${diffHours} hours ago`;
86
-
106
+ if (diffHours < 24) return `${diffHours} ${diffHours === 1 ? "hour" : "hours"} ago`;
87
107
const diffDays = Math.floor(diffHours / 24);
88
- if (diffDays < 30) return `${diffDays} days ago`;
89
-
108
+ if (diffDays < 30) return `${diffDays} ${diffDays === 1 ? "day" : "days"} ago`;
90
109
const diffMonths = Math.floor(diffDays / 30);
91
- return `${diffMonths} months ago`;
110
+ if (diffMonths < 12) return `${diffMonths} ${diffMonths === 1 ? "month" : "months"} ago`;
111
+ const diffYears = Math.floor(diffMonths / 12);
112
+ return `${diffYears} ${diffYears === 1 ? "year" : "years"} ago`;
92
113
}
93
114
94
-export default function IssueDetail({ issue, bodyHtml, repoOwner, repoName, canLink, canEdit }: Props) {
95
- const [showMenu, setShowMenu] = useState(false);
115
+export default function IssueDetail({
116
+ issue,
117
+ bodyHtml,
118
+ repoOwner,
119
+ repoName,
120
+ canLink = false,
121
+ canEdit = false,
122
+ currentUser
123
+}: Props) {
124
+ const [issueState, setIssueState] = useState<"open" | "closed">(issue.state);
96
125
const [collaborators, setCollaborators] = useState<Array<{ id: string; username: string }>>([]);
97
126
const [linkedPRs, setLinkedPRs] = useState<Array<{
98
127
id: string;
100
129
pullRequest: { number: number; title: string; state: string };
101
130
}>>([]);
102
131
const [loadingLinks, setLoadingLinks] = useState(true);
132
+
103
133
const [crossRepoLinks, setCrossRepoLinks] = useState<Array<{
104
134
id: string;
105
135
linkType: string;
110
140
const [crossRepoTarget, setCrossRepoTarget] = useState("");
111
141
const [crossRepoType, setCrossRepoType] = useState("relates");
112
142
const [crossRepoError, setCrossRepoError] = useState<string | null>(null);
113
- const [isEditing, setIsEditing] = useState(false);
143
+ const [isLinking, setIsLinking] = useState(false);
144
+
145
+ // Editing Issue Title/Body
146
+ const [isEditingTitle, setIsEditingTitle] = useState(false);
114
147
const [editTitle, setEditTitle] = useState(issue.title);
148
+ const [isEditingBody, setIsEditingBody] = useState(false);
115
149
const [editBody, setEditBody] = useState(issue.body);
150
+ const [currentBodyHtml, setCurrentBodyHtml] = useState(bodyHtml);
116
151
const [saving, setSaving] = useState(false);
117
152
118
- async function convertToEpic() {
119
- if (!confirm("Are you sure you want to convert this issue to an Epic?")) return;
153
+ // Comments State
154
+ const [comments, setComments] = useState<Comment[]>([]);
155
+ const [loadingComments, setLoadingComments] = useState(true);
156
+ const [newComment, setNewComment] = useState("");
157
+ const [commentTab, setCommentTab] = useState<"write" | "preview">("write");
158
+ const [isSubmittingComment, setIsSubmittingComment] = useState(false);
159
+ const [isTogglingState, setIsTogglingState] = useState(false);
120
160
161
+ // Fetch Comments
162
+ const fetchComments = async () => {
121
163
try {
122
- const res = await fetch(`/api/repos/${repoOwner}/${repoName}/issues/${issue.number}`, {
123
- method: "PATCH",
124
- body: JSON.stringify({ type: "epic" }),
125
- headers: { "Content-Type": "application/json" }
126
- });
127
-
164
+ const res = await fetch(`/api/repos/${repoOwner}/${repoName}/issues/${issue.number}/comments`);
128
165
if (res.ok) {
129
- window.location.reload();
130
- } else {
131
- alert("Failed to convert to epic");
166
+ const data = await res.json();
167
+ setComments(data.data || []);
132
168
}
133
169
} catch (e) {
134
- console.error(e);
135
- alert("Failed to convert to epic");
170
+ console.error("Failed to fetch comments", e);
171
+ } finally {
172
+ setLoadingComments(false);
136
173
}
137
- }
174
+ };
138
175
139
- // Assignees (WS2-01): toggle a user's assignment on the issue
140
- async function handleAssigneeToggle(assigneeId: string) {
141
- if (!assigneeId) return;
142
- const currentIds = (issue.assignees || []).map((a: any) => a.id || a.username);
143
- const isAssigned = currentIds.includes(assigneeId);
144
- const newIds = isAssigned
145
- ? currentIds.filter((id) => id !== assigneeId)
146
- : [...currentIds, assigneeId];
147
- try {
148
- const res = await fetch(`/api/repos/${repoOwner}/${repoName}/issues/${issue.number}`, {
149
- method: "PATCH",
150
- headers: { "Content-Type": "application/json" },
151
- body: JSON.stringify({ assigneeIds: newIds })
152
- });
153
- if (res.ok) {
154
- window.location.reload();
155
- } else {
156
- const data = await res.json().catch(() => null);
157
- alert(data?.error?.message || "Failed to update assignees");
158
- }
159
- } catch (e) {
160
- alert("Failed to update assignees");
161
- }
162
- }
176
+ useEffect(() => {
177
+ fetchComments();
178
+ }, [repoOwner, repoName, issue.number]);
163
179
180
+ // Load Linked PRs & Collaborators
164
181
useEffect(() => {
165
182
let isMounted = true;
166
183
167
184
async function loadLinks() {
168
185
try {
169
- const res = await fetch(`/api/repos/${repoOwner}/${repoName}/issues/${issue.number}/linked-prs`); if (!res.ok) throw new Error("Failed to load linked PRs");
186
+ const res = await fetch(`/api/repos/${repoOwner}/${repoName}/issues/${issue.number}/linked-prs`);
187
+ if (!res.ok) throw new Error("Failed to load linked PRs");
170
188
const data = await res.json();
171
189
if (isMounted) {
172
190
setLinkedPRs(data.links || []);
173
191
}
174
192
} catch (e) {
175
- if (isMounted) {
176
- setLinkedPRs([]);
177
- }
193
+ if (isMounted) setLinkedPRs([]);
178
194
} finally {
179
- if (isMounted) {
180
- setLoadingLinks(false);
181
- }
195
+ if (isMounted) setLoadingLinks(false);
182
196
}
183
197
}
184
198
185
199
loadLinks();
186
200
187
- // Load collaborators for the assignee dropdown (WS2-01)
188
201
if (canEdit) {
189
202
fetch(`/api/repos/${repoOwner}/${repoName}/collaborators`)
190
203
.then((r) => (r.ok ? r.json() : null))
197
210
return () => {
198
211
isMounted = false;
199
212
};
200
- }, [repoOwner, repoName, issue.number]);
213
+ }, [repoOwner, repoName, issue.number, canEdit]);
201
214
215
+ // Load Cross Repo Links
202
216
useEffect(() => {
203
217
let isMounted = true;
204
218
211
225
setCrossRepoLinks(data.links || []);
212
226
}
213
227
} catch (e) {
214
- if (isMounted) {
215
- setCrossRepoLinks([]);
216
- }
228
+ if (isMounted) setCrossRepoLinks([]);
217
229
} finally {
218
- if (isMounted) {
219
- setLoadingCrossRepo(false);
220
- }
230
+ if (isMounted) setLoadingCrossRepo(false);
221
231
}
222
232
}
223
233
228
238
};
229
239
}, [repoOwner, repoName, issue.number]);
230
240
241
+ // Convert to Epic
242
+ async function convertToEpic() {
243
+ if (!confirm("Are you sure you want to convert this issue to an Epic?")) return;
244
+
245
+ try {
246
+ const res = await fetch(`/api/repos/${repoOwner}/${repoName}/issues/${issue.number}`, {
247
+ method: "PATCH",
248
+ body: JSON.stringify({ type: "epic" }),
249
+ headers: { "Content-Type": "application/json" }
250
+ });
251
+
252
+ if (res.ok) {
253
+ window.location.reload();
254
+ } else {
255
+ alert("Failed to convert to epic");
256
+ }
257
+ } catch (e) {
258
+ console.error(e);
259
+ alert("Failed to convert to epic");
260
+ }
261
+ }
262
+
263
+ // Toggle Issue State (Open / Closed)
264
+ async function handleToggleIssueState() {
265
+ const nextState = issueState === "open" ? "closed" : "open";
266
+ setIsTogglingState(true);
267
+ try {
268
+ const res = await fetch(`/api/repos/${repoOwner}/${repoName}/issues/${issue.number}`, {
269
+ method: "PATCH",
270
+ headers: { "Content-Type": "application/json" },
271
+ body: JSON.stringify({ state: nextState })
272
+ });
273
+
274
+ if (res.ok) {
275
+ setIssueState(nextState);
276
+ fetchComments();
277
+ } else {
278
+ const data = await res.json().catch(() => null);
279
+ alert(data?.error?.message || "Failed to update issue status");
280
+ }
281
+ } catch (e) {
282
+ alert("Failed to update issue status");
283
+ } finally {
284
+ setIsTogglingState(false);
285
+ }
286
+ }
287
+
288
+ // Assignee Toggle
289
+ async function handleAssigneeToggle(assigneeId: string) {
290
+ if (!assigneeId) return;
291
+ const currentIds = (issue.assignees || []).map((a: any) => a.id || a.username);
292
+ const isAssigned = currentIds.includes(assigneeId);
293
+ const newIds = isAssigned
294
+ ? currentIds.filter((id) => id !== assigneeId)
295
+ : [...currentIds, assigneeId];
296
+ try {
297
+ const res = await fetch(`/api/repos/${repoOwner}/${repoName}/issues/${issue.number}`, {
298
+ method: "PATCH",
299
+ headers: { "Content-Type": "application/json" },
300
+ body: JSON.stringify({ assigneeIds: newIds })
301
+ });
302
+ if (res.ok) {
303
+ window.location.reload();
304
+ } else {
305
+ const data = await res.json().catch(() => null);
306
+ alert(data?.error?.message || "Failed to update assignees");
307
+ }
308
+ } catch (e) {
309
+ alert("Failed to update assignees");
310
+ }
311
+ }
312
+
313
+ // Link Cross-Repo Issue
231
314
async function handleCrossRepoLink() {
232
315
if (!crossRepoTarget.trim()) return;
233
316
setCrossRepoError(null);
317
+ setIsLinking(true);
234
318
try {
235
319
const res = await fetch(`/api/repos/${repoOwner}/${repoName}/issues/${issue.number}/cross-repo-links`, {
236
320
method: "POST",
248
332
setCrossRepoTarget("");
249
333
} catch (e: any) {
250
334
setCrossRepoError(e.message || "Failed to link issue");
335
+ } finally {
336
+ setIsLinking(false);
251
337
}
252
338
}
253
339
254
- async function handleSave() {
340
+ // Save Title
341
+ async function handleSaveTitle() {
342
+ if (!editTitle.trim()) return;
255
343
setSaving(true);
256
344
try {
257
345
const res = await fetch(`/api/repos/${repoOwner}/${repoName}/issues/${issue.number}`, {
258
346
method: "PATCH",
259
347
headers: { "Content-Type": "application/json" },
260
- body: JSON.stringify({ title: editTitle, description: editBody }),
348
+ body: JSON.stringify({ title: editTitle.trim() }),
261
349
});
262
- if (!res.ok) throw new Error("Failed to update issue");
263
- setIsEditing(false);
264
- window.location.reload();
350
+ if (!res.ok) throw new Error("Failed to update title");
351
+ issue.title = editTitle.trim();
352
+ setIsEditingTitle(false);
265
353
} catch (e) {
266
- alert("Failed to save changes");
354
+ alert("Failed to save title");
267
355
} finally {
268
356
setSaving(false);
269
357
}
270
358
}
271
359
272
- return (
273
- <div className="max-w-6xl mx-auto">
274
- {/* Title Section */}
275
- <motion.div
276
- initial={{ opacity: 0, y: -20 }}
277
- animate={{ opacity: 1, y: 0 }}
278
- className="mb-8 pb-6 border-b border-border"
279
- >
280
- <div className="flex items-start justify-between gap-4 mb-4">
281
- <div className="flex items-center gap-3 flex-1">
282
- {isEditing ? (
283
- <input
284
- type="text"
285
- value={editTitle}
286
- onChange={(e) => setEditTitle(e.target.value)}
287
- className="flex-1 text-2xl md:text-3xl font-bold text-foreground bg-background border border-border rounded-lg px-3 py-1 focus:outline-none focus:ring-2 focus:ring-ring"
288
- />
289
- ) : (
290
- <h1 className="text-2xl md:text-3xl font-bold text-foreground">
291
- {issue.title}
292
- </h1>
293
- )}
294
- <span className="text-2xl md:text-3xl text-muted-foreground font-light">
295
- #{issue.number}
296
- </span>
297
- </div>
360
+ // Save Body
361
+ async function handleSaveBody() {
362
+ setSaving(true);
363
+ try {
364
+ const res = await fetch(`/api/repos/${repoOwner}/${repoName}/issues/${issue.number}`, {
365
+ method: "PATCH",
366
+ headers: { "Content-Type": "application/json" },
367
+ body: JSON.stringify({ description: editBody }),
368
+ });
369
+ if (!res.ok) throw new Error("Failed to update description");
370
+ const rendered = marked.parse(editBody || "") as string;
371
+ setCurrentBodyHtml(DOMPurify.sanitize(rendered));
372
+ issue.body = editBody;
373
+ setIsEditingBody(false);
374
+ } catch (e) {
375
+ alert("Failed to save description");
376
+ } finally {
377
+ setSaving(false);
378
+ }
379
+ }
298
380
299
- <div className="flex items-center gap-2">
300
- {/* Convert to Epic Button */}
301
- {issue.type !== 'epic' && (
302
- <button
303
- onClick={convertToEpic}
304
- className="flex items-center gap-2 px-4 py-2 rounded-lg border border-purple-500/30 bg-purple-500/10 text-sm text-purple-300 hover:bg-purple-500/20 transition-all"
305
- >
306
- <Layers className="h-4 w-4" />
307
- convert to Epic
308
- </button>
309
- )}
381
+ // Submit New Comment
382
+ async function handleSubmitComment() {
383
+ if (!newComment.trim()) return;
384
+ setIsSubmittingComment(true);
385
+ try {
386
+ const res = await fetch(`/api/repos/${repoOwner}/${repoName}/issues/${issue.number}/comments`, {
387
+ method: "POST",
388
+ headers: { "Content-Type": "application/json" },
389
+ body: JSON.stringify({ body: newComment.trim() })
390
+ });
310
391
311
- {/* Edit Button */}
312
- {isEditing ? (
313
- <div className="flex items-center gap-2">
392
+ if (!res.ok) {
393
+ const data = await res.json().catch(() => null);
394
+ throw new Error(data?.error?.message || "Failed to add comment");
395
+ }
396
+
397
+ const data = await res.json();
398
+ if (data.data) {
399
+ setComments((prev) => [...prev, data.data]);
400
+ } else {
401
+ fetchComments();
402
+ }
403
+ setNewComment("");
404
+ setCommentTab("write");
405
+ } catch (e: any) {
406
+ alert(e.message || "Failed to post comment");
407
+ } finally {
408
+ setIsSubmittingComment(false);
409
+ }
410
+ }
411
+
412
+ // Participants Set
413
+ const participants = Array.from(
414
+ new Set([
415
+ issue.author.username,
416
+ ...(issue.assignees?.map((a) => a.username) || []),
417
+ ...comments.map((c) => c.author?.username).filter(Boolean)
418
+ ])
419
+ );
420
+
421
+ return (
422
+ <div className="w-full space-y-6">
423
+ {/* Header Section */}
424
+ <div className="pb-5 border-b border-border space-y-3">
425
+ <div className="flex flex-col sm:flex-row sm:items-start justify-between gap-4">
426
+ {/* Title */}
427
+ <div className="flex-1 min-w-0">
428
+ {isEditingTitle ? (
429
+ <div className="flex items-center gap-2 max-w-2xl">
430
+ <input
431
+ type="text"
432
+ value={editTitle}
433
+ onChange={(e) => setEditTitle(e.target.value)}
434
+ className="flex-1 text-xl sm:text-2xl font-bold text-foreground bg-background border border-border rounded-md px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
435
+ />
314
436
<button
315
- onClick={() => { setIsEditing(false); setEditTitle(issue.title); setEditBody(issue.body); }}
316
- className="px-4 py-2 rounded-lg border border-border bg-secondary text-sm text-muted-foreground hover:bg-secondary/80 transition-all"
437
+ onClick={handleSaveTitle}
438
+ disabled={saving}
439
+ className="px-3.5 py-1.5 rounded-md bg-[#238636] hover:bg-[#2ea043] text-white text-xs font-semibold shadow-xs disabled:opacity-50"
317
440
>
318
- Cancel
441
+ Save
319
442
</button>
320
443
<button
321
- onClick={handleSave}
322
- disabled={saving}
323
- className="px-4 py-2 rounded-lg bg-primary text-sm font-medium text-primary-foreground hover:opacity-90 disabled:opacity-50 transition-all"
444
+ onClick={() => {
445
+ setIsEditingTitle(false);
446
+ setEditTitle(issue.title);
447
+ }}
448
+ className="px-3 py-1.5 rounded-md border border-border bg-card text-xs font-medium text-muted-foreground hover:text-foreground"
324
449
>
325
- {saving ? "Saving..." : "Save"}
450
+ Cancel
326
451
</button>
327
452
</div>
328
453
) : (
329
- <motion.button
330
- onClick={() => setIsEditing(true)}
331
- whileHover={{ scale: 1.05 }}
332
- whileTap={{ scale: 0.95 }}
333
- className="flex items-center gap-2 px-4 py-2 rounded-lg border border-border bg-secondary text-sm text-muted-foreground hover:bg-secondary/80 transition-all"
454
+ <div className="flex items-baseline gap-2.5 flex-wrap">
455
+ <h1 className="text-2xl sm:text-3xl font-bold text-foreground tracking-tight break-words">
456
+ {issue.title}
457
+ </h1>
458
+ <span className="text-2xl sm:text-3xl text-muted-foreground font-light">
459
+ #{issue.number}
460
+ </span>
461
+ </div>
462
+ )}
463
+ </div>
464
+
465
+ {/* Action CTAs */}
466
+ <div className="flex items-center gap-2 flex-shrink-0 self-start">
467
+ {canEdit && !isEditingTitle && (
468
+ <button
469
+ onClick={() => setIsEditingTitle(true)}
470
+ className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md border border-border bg-card text-xs font-medium text-foreground hover:bg-accent transition-colors shadow-xs"
334
471
>
335
- <Edit3 className="h-4 w-4" />
336
- Edit
337
- </motion.button>
472
+ <Edit3 className="h-3.5 w-3.5 text-muted-foreground" />
473
+ <span>Edit</span>
474
+ </button>
338
475
)}
476
+ <a
477
+ href={`/${repoOwner}/${repoName}/issues/new`}
478
+ className="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-md bg-[#238636] hover:bg-[#2ea043] text-white text-xs sm:text-sm font-medium transition-colors shadow-xs"
479
+ >
480
+ <Plus className="h-4 w-4" />
481
+ <span>New issue</span>
482
+ </a>
339
483
</div>
340
484
</div>
341
485
342
- {/* Status & Type Badges */}
343
- <div className="flex items-center gap-2">
344
- {issue.status ? (
345
- <motion.div
346
- initial={{ scale: 0.9 }}
347
- animate={{ scale: 1 }}
348
- className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full font-medium text-foreground border"
349
- style={{
350
- backgroundColor: `${issue.status.color}20`,
351
- borderColor: `${issue.status.color}40`,
352
- color: issue.status.color
353
- }}
354
- >
355
- <div className="h-2 w-2 rounded-full" style={{ backgroundColor: issue.status.color }} />
356
- {issue.status.name}
357
- </motion.div>
358
- ) : (
359
- <motion.div
360
- initial={{ scale: 0.9 }}
361
- animate={{ scale: 1 }}
362
- className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full font-medium ${issue.state === "open"
363
- ? "bg-green-500/20 text-green-400 border border-green-500/30"
364
- : "bg-purple-500/20 text-purple-400 border border-purple-500/30"
365
- }`}
366
- >
367
- {issue.state === "open" ? (
368
- <AlertCircle className="h-4 w-4" />
369
- ) : (
370
- <CheckCircle2 className="h-4 w-4" />
371
- )}
372
- {issue.state === "open" ? "Open" : "Closed"}
373
- </motion.div>
374
- )}
486
+ {/* Status Badge & Subtitle Bar */}
487
+ <div className="flex items-center gap-3 flex-wrap text-xs sm:text-sm text-muted-foreground">
488
+ {/* State Badge */}
489
+ <div
490
+ className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full font-semibold text-xs text-white shadow-xs ${
491
+ issueState === "open" ? "bg-[#238636]" : "bg-[#8957e5]"
492
+ }`}
493
+ >
494
+ {issueState === "open" ? (
495
+ <>
496
+ <CircleDot className="h-3.5 w-3.5" />
497
+ <span>Open</span>
498
+ </>
499
+ ) : (
500
+ <>
501
+ <CheckCircle2 className="h-3.5 w-3.5" />
502
+ <span>Closed</span>
503
+ </>
504
+ )}
505
+ </div>
506
+
507
+ {/* Type Badge */}
508
+ {issue.type === "epic" ? (
509
+ <span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-500/10 text-purple-400 border border-purple-500/20">
510
+ <Layers className="h-3 w-3" />
511
+ Epic
512
+ </span>
513
+ ) : issue.type === "task" ? (
514
+ <span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-500/10 text-blue-400 border border-blue-500/20">
515
+ <CheckSquare className="h-3 w-3" />
516
+ Task
517
+ </span>
518
+ ) : null}
375
519
376
- <div className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full font-medium border ${issue.type === 'epic'
377
- ? "bg-purple-500/10 text-purple-400 border-purple-500/20"
378
- : issue.type === 'task'
379
- ? "bg-blue-500/10 text-blue-400 border-blue-500/20"
380
- : "bg-gray-500/10 text-muted-foreground border-gray-500/20"
381
- }`}>
382
- {issue.type === 'epic' && <Layers className="h-3.5 w-3.5" />}
383
- {issue.type === 'task' && <CheckSquare className="h-3.5 w-3.5" />}
384
- {issue.type === 'issue' && <AlertCircle className="h-3.5 w-3.5" />}
385
- <span className="capitalize">{issue.type}</span>
520
+ {/* Subtitle Details */}
521
+ <div>
522
+ <span className="font-semibold text-foreground">@{issue.author.username}</span>
523
+ {" "}opened this issue {formatTimeAgo(issue.createdAt)} · {comments.length} comment{comments.length === 1 ? "" : "s"}
386
524
</div>
387
525
</div>
388
-
389
- <span className="text-muted-foreground">
390
- <span className="font-medium text-foreground hover:text-cyan-400 cursor-pointer transition-colors">
391
- {issue.author.username}
392
- </span>
393
- {" "}opened this issue {timeAgo(issue.createdAt)}
394
- </span>
395
- </motion.div>
526
+ </div>
396
527
397
528
{/* Main Content Grid */}
398
- <div className="grid grid-cols-1 lg:grid-cols-[1fr_280px] gap-8">
399
- {/* Left Column - Issue Body & Comments */}
400
- <div className="space-y-6">
401
- {isEditing ? (
402
- <div>
403
- <textarea
404
- value={editBody}
405
- onChange={(e) => setEditBody(e.target.value)}
406
- className="w-full min-h-[300px] p-4 rounded-lg border border-border bg-background text-foreground font-mono text-sm focus:outline-none focus:ring-2 focus:ring-ring"
407
- placeholder="Write your issue body in Markdown..."
408
- />
409
- <p className="text-xs text-muted-foreground mt-2">Markdown is supported.</p>
529
+ <div className="grid grid-cols-1 lg:grid-cols-[1fr_300px] gap-8 items-start">
530
+ {/* Left Column - Conversation Timeline */}
531
+ <div className="space-y-6 relative before:absolute before:left-[19px] before:top-5 before:bottom-5 before:w-[2px] before:bg-border/60">
532
+ {/* 1. Original Issue Description Post */}
533
+ <div className="flex items-start gap-4 relative z-10">
534
+ {/* Author Avatar */}
535
+ <div className="flex-shrink-0">
536
+ {issue.author.avatarUrl ? (
537
+ <img
538
+ src={issue.author.avatarUrl}
539
+ alt={issue.author.username}
540
+ className="h-10 w-10 rounded-full border border-border/80 object-cover bg-secondary"
541
+ />
542
+ ) : (
543
+ <div className="h-10 w-10 rounded-full bg-secondary border border-border/80 flex items-center justify-center font-bold text-sm text-foreground">
544
+ {issue.author.username?.[0]?.toUpperCase() || "?"}
545
+ </div>
546
+ )}
410
547
</div>
411
- ) : (
412
- <div className="prose prose-invert max-w-none" dangerouslySetInnerHTML={{ __html: bodyHtml }} />
413
- )}
414
- </div>
415
548
416
- {/* Right Sidebar */}
417
- <motion.div
418
- initial={{ opacity: 0, x: 20 }}
419
- animate={{ opacity: 1, x: 0 }}
420
- transition={{ delay: 0.15 }}
421
- className="space-y-6"
422
- >
423
- <div className="relative">
424
- <div className="absolute -inset-[1px] bg-gradient-to-b from-emerald-500/10 to-transparent rounded-xl opacity-50" />
425
- <div className="relative rounded-xl border border-border bg-card/60 p-4">
426
- <h3 className="text-sm font-semibold text-foreground flex items-center gap-2 mb-3">
427
- <Link2 className="h-4 w-4 text-emerald-400" />
428
- Linked Issues
429
- </h3>
430
- {loadingCrossRepo ? (
431
- <div className="text-xs text-muted-foreground">Loading...</div>
432
- ) : crossRepoLinks.length === 0 ? (
433
- <div className="text-xs text-muted-foreground">No linked issues</div>
434
- ) : (
435
- <div className="space-y-2">
436
- {crossRepoLinks.map((link) => (
437
- <a
438
- key={link.id}
439
- href={`/${link.repository.owner}/${link.repository.name}/issues/${link.issue.number}`}
440
- className="flex items-start justify-between gap-3 rounded-lg border border-white/5 bg-secondary px-3 py-2 text-xs text-muted-foreground hover:bg-secondary/80"
549
+ {/* Comment Card */}
550
+ <div className="flex-1 min-w-0 rounded-lg border border-border/80 bg-card/60 backdrop-blur-sm overflow-hidden shadow-xs">
551
+ {/* Card Header */}
552
+ <div className="bg-muted/30 border-b border-border/70 px-4 py-2.5 flex items-center justify-between gap-2 text-xs text-muted-foreground">
553
+ <div className="flex items-center gap-1.5 flex-wrap">
554
+ <span className="font-semibold text-foreground">@{issue.author.username}</span>
555
+ <span>commented {formatTimeAgo(issue.createdAt)}</span>
556
+ </div>
557
+ <div className="flex items-center gap-2">
558
+ <span className="rounded-full border border-border/70 bg-secondary/80 px-2 py-0.5 text-[11px] font-medium text-muted-foreground">
559
+ Author
560
+ </span>
561
+ {canEdit && !isEditingBody && (
562
+ <button
563
+ onClick={() => setIsEditingBody(true)}
564
+ className="text-muted-foreground hover:text-foreground p-1 transition-colors"
565
+ title="Edit description"
441
566
>
442
- <span>
443
- {link.repository.owner}/{link.repository.name}#{link.issue.number} {link.issue.title}
444
- </span>
445
- <span className="text-[10px] uppercase text-muted-foreground">
446
- {link.linkType}
447
- </span>
448
- </a>
449
- ))}
567
+ <Edit3 className="h-3.5 w-3.5" />
568
+ </button>
569
+ )}
450
570
</div>
451
- )}
452
- {canLink && (
453
- <div className="mt-3 space-y-2">
454
- <div className="flex items-center gap-2">
455
- <input
456
- value={crossRepoTarget}
457
- onChange={(e) => setCrossRepoTarget(e.target.value)}
458
- placeholder="owner/repo#123"
459
- className="flex-1 rounded-md border border-border bg-secondary px-2 py-1 text-xs text-foreground"
571
+ </div>
572
+
573
+ {/* Card Body */}
574
+ <div className="p-4 text-sm text-foreground leading-relaxed">
575
+ {isEditingBody ? (
576
+ <div className="space-y-3">
577
+ <textarea
578
+ value={editBody}
579
+ onChange={(e) => setEditBody(e.target.value)}
580
+ className="w-full min-h-[160px] p-3 rounded-md border border-border bg-background text-foreground text-sm font-mono focus:outline-none focus:ring-2 focus:ring-primary/20"
581
+ placeholder="Write your issue description in Markdown..."
460
582
/>
461
- <select
462
- value={crossRepoType}
463
- onChange={(e) => setCrossRepoType(e.target.value)}
464
- className="rounded-md border border-border bg-secondary px-2 py-1 text-xs text-foreground"
465
- >
466
- <option value="relates">Relates</option>
467
- <option value="blocks">Blocks</option>
468
- <option value="blocked_by">Blocked by</option>
469
- <option value="duplicates">Duplicates</option>
470
- </select>
583
+ <div className="flex items-center justify-end gap-2">
584
+ <button
585
+ onClick={() => {
586
+ setIsEditingBody(false);
587
+ setEditBody(issue.body);
588
+ }}
589
+ className="px-3 py-1.5 rounded-md border border-border bg-card text-xs font-medium text-muted-foreground hover:text-foreground"
590
+ >
591
+ Cancel
592
+ </button>
593
+ <button
594
+ onClick={handleSaveBody}
595
+ disabled={saving}
596
+ className="px-3.5 py-1.5 rounded-md bg-[#238636] hover:bg-[#2ea043] text-white text-xs font-semibold shadow-xs disabled:opacity-50"
597
+ >
598
+ {saving ? "Saving..." : "Update comment"}
599
+ </button>
600
+ </div>
471
601
</div>
472
- <button
473
- onClick={handleCrossRepoLink}
474
- className="w-full rounded-md border border-emerald-500/30 bg-emerald-500/10 px-2 py-1 text-xs text-emerald-300 hover:bg-emerald-500/20"
475
- >
476
- Link issue
477
- </button>
478
- {crossRepoError && (
479
- <div className="text-xs text-red-400">{crossRepoError}</div>
602
+ ) : currentBodyHtml ? (
603
+ <div
604
+ className="prose dark:prose-invert max-w-none text-sm text-foreground/90 leading-relaxed break-words"
605
+ dangerouslySetInnerHTML={{ __html: currentBodyHtml }}
606
+ />
607
+ ) : (
608
+ <p className="text-muted-foreground italic">No description provided.</p>
609
+ )}
610
+ </div>
611
+ </div>
612
+ </div>
613
+
614
+ {/* 2. Comments Thread */}
615
+ {comments.map((comment) => {
616
+ const commentHtml = DOMPurify.sanitize(marked.parse(comment.body || "") as string);
617
+ const isAuthor = comment.author?.username === issue.author.username;
618
+ const isRepoOwner = comment.author?.username === repoOwner;
619
+
620
+ return (
621
+ <div key={comment.id} className="flex items-start gap-4 relative z-10">
622
+ {/* Author Avatar */}
623
+ <div className="flex-shrink-0">
624
+ {comment.author?.avatarUrl ? (
625
+ <img
626
+ src={comment.author.avatarUrl}
627
+ alt={comment.author.username}
628
+ className="h-10 w-10 rounded-full border border-border/80 object-cover bg-secondary"
629
+ />
630
+ ) : (
631
+ <div className="h-10 w-10 rounded-full bg-secondary border border-border/80 flex items-center justify-center font-bold text-sm text-foreground">
632
+ {comment.author?.username?.[0]?.toUpperCase() || "?"}
633
+ </div>
480
634
)}
481
635
</div>
482
- )}
636
+
637
+ {/* Comment Box */}
638
+ <div className="flex-1 min-w-0 rounded-lg border border-border/80 bg-card/60 backdrop-blur-sm overflow-hidden shadow-xs">
639
+ {/* Header */}
640
+ <div className="bg-muted/30 border-b border-border/70 px-4 py-2.5 flex items-center justify-between gap-2 text-xs text-muted-foreground">
641
+ <div className="flex items-center gap-1.5 flex-wrap">
642
+ <span className="font-semibold text-foreground">
643
+ @{comment.author?.displayName || comment.author?.username}
644
+ </span>
645
+ <span>commented {formatTimeAgo(comment.createdAt)}</span>
646
+ </div>
647
+ <div className="flex items-center gap-1.5">
648
+ {isAuthor && (
649
+ <span className="rounded-full border border-border/70 bg-secondary/80 px-2 py-0.5 text-[11px] font-medium text-muted-foreground">
650
+ Author
651
+ </span>
652
+ )}
653
+ {isRepoOwner && (
654
+ <span className="rounded-full border border-border/70 bg-secondary/80 px-2 py-0.5 text-[11px] font-medium text-muted-foreground">
655
+ Owner
656
+ </span>
657
+ )}
658
+ </div>
659
+ </div>
660
+
661
+ {/* Content */}
662
+ <div className="p-4 text-sm text-foreground leading-relaxed">
663
+ <div
664
+ className="prose dark:prose-invert max-w-none text-sm text-foreground/90 leading-relaxed break-words"
665
+ dangerouslySetInnerHTML={{ __html: commentHtml }}
666
+ />
667
+ </div>
668
+ </div>
669
+ </div>
670
+ );
671
+ })}
672
+
673
+ {/* State change timeline marker if closed */}
674
+ {issueState === "closed" && (
675
+ <div className="flex items-center gap-3 pl-2 py-2 relative z-10">
676
+ <div className="h-8 w-8 rounded-full bg-[#8957e5] text-white flex items-center justify-center border-2 border-background shadow-xs">
677
+ <CheckCircle2 className="h-4 w-4" />
678
+ </div>
679
+ <span className="text-xs text-muted-foreground">
680
+ <span className="font-semibold text-foreground">This issue</span> was closed.
681
+ </span>
483
682
</div>
484
- </div>
485
- <div className="relative">
486
- <div className="absolute -inset-[1px] bg-gradient-to-b from-cyan-500/10 to-transparent rounded-xl opacity-50" />
487
- <div className="relative rounded-xl border border-border bg-card/60 p-4">
488
- <h3 className="text-sm font-semibold text-foreground flex items-center gap-2 mb-3">
489
- <GitBranch className="h-4 w-4 text-cyan-400" />
490
- Linked Pull Requests
491
- </h3>
492
- {loadingLinks ? (
493
- <div className="text-xs text-muted-foreground">Loading...</div>
494
- ) : linkedPRs.length === 0 ? (
495
- <div className="text-xs text-muted-foreground">No linked pull requests</div>
683
+ )}
684
+
685
+ {/* 3. Add Comment Composer */}
686
+ <div className="flex items-start gap-4 relative z-10 pt-2">
687
+ {/* Current User Avatar */}
688
+ <div className="flex-shrink-0">
689
+ {currentUser?.avatarUrl ? (
690
+ <img
691
+ src={currentUser.avatarUrl}
692
+ alt={currentUser.username}
693
+ className="h-10 w-10 rounded-full border border-border/80 object-cover bg-secondary"
694
+ />
496
695
) : (
497
- <div className="space-y-2">
498
- {linkedPRs.map((link) => (
499
- <a
500
- key={link.id}
501
- href={`/${repoOwner}/${repoName}/pulls/${link.pullRequest.number}`}
502
- className="flex items-start justify-between gap-3 rounded-lg border border-white/5 bg-secondary px-3 py-2 text-xs text-muted-foreground hover:bg-secondary/80"
503
- >
504
- <span>
505
- #{link.pullRequest.number} {link.pullRequest.title}
506
- </span>
507
- <span className="text-[10px] uppercase text-muted-foreground">
508
- {link.linkType}
509
- </span>
510
- </a>
511
- ))}
696
+ <div className="h-10 w-10 rounded-full bg-secondary border border-border/80 flex items-center justify-center font-bold text-sm text-foreground">
697
+ {currentUser?.username?.[0]?.toUpperCase() || "?"}
512
698
</div>
513
699
)}
514
700
</div>
515
- </div>
516
- {/* Labels */}
517
- {issue.labels && issue.labels.length > 0 && (
518
- <div className="relative">
519
- <div className="absolute -inset-[1px] bg-gradient-to-b from-purple-500/10 to-transparent rounded-xl opacity-50" />
520
- <div className="relative rounded-xl border border-border bg-card/60 p-4">
521
- <h3 className="text-sm font-semibold text-foreground flex items-center gap-2 mb-3">
522
- <Tag className="h-4 w-4 text-purple-400" />
523
- Labels
524
- </h3>
525
- <div className="flex flex-wrap gap-2">
526
- {issue.labels.map((label, i) => (
527
- <span
528
- key={i}
529
- className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium"
530
- style={{
531
- backgroundColor: `${label.color}20`,
532
- color: label.color,
533
- border: `1px solid ${label.color}40`
534
- }}
701
+
702
+ {/* Composer Card */}
703
+ <div className="flex-1 min-w-0 rounded-lg border border-border/80 bg-card/60 backdrop-blur-sm overflow-hidden shadow-xs">
704
+ {/* Tab Bar */}
705
+ <div className="bg-muted/30 border-b border-border/70 px-4 py-1.5 flex items-center gap-2">
706
+ <button
707
+ onClick={() => setCommentTab("write")}
708
+ className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${
709
+ commentTab === "write"
710
+ ? "bg-secondary text-foreground font-semibold shadow-xs"
711
+ : "text-muted-foreground hover:text-foreground"
712
+ }`}
713
+ >
714
+ Write
715
+ </button>
716
+ <button
717
+ onClick={() => setCommentTab("preview")}
718
+ className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${
719
+ commentTab === "preview"
720
+ ? "bg-secondary text-foreground font-semibold shadow-xs"
721
+ : "text-muted-foreground hover:text-foreground"
722
+ }`}
723
+ >
724
+ Preview
725
+ </button>
726
+ </div>
727
+
728
+ {/* Editor Area */}
729
+ <div className="p-3">
730
+ {commentTab === "write" ? (
731
+ <textarea
732
+ value={newComment}
733
+ onChange={(e) => setNewComment(e.target.value)}
734
+ placeholder="Leave a comment..."
735
+ className="w-full min-h-[120px] p-2 text-sm bg-transparent border-0 focus:outline-none resize-y text-foreground placeholder:text-muted-foreground"
736
+ />
737
+ ) : (
738
+ <div className="min-h-[120px] p-2 text-sm">
739
+ {newComment.trim() ? (
740
+ <div
741
+ className="prose dark:prose-invert max-w-none text-sm text-foreground/90 leading-relaxed break-words"
742
+ dangerouslySetInnerHTML={{
743
+ __html: DOMPurify.sanitize(marked.parse(newComment) as string)
744
+ }}
745
+ />
746
+ ) : (
747
+ <span className="text-xs text-muted-foreground italic">Nothing to preview</span>
748
+ )}
749
+ </div>
750
+ )}
751
+ </div>
752
+
753
+ {/* Footer / Actions Bar */}
754
+ <div className="bg-muted/20 border-t border-border/70 px-4 py-2.5 flex items-center justify-between gap-3 flex-wrap">
755
+ <span className="text-[11px] text-muted-foreground">
756
+ Markdown is supported
757
+ </span>
758
+
759
+ <div className="flex items-center gap-2">
760
+ {canEdit && (
761
+ <button
762
+ onClick={handleToggleIssueState}
763
+ disabled={isTogglingState}
764
+ className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md border border-border bg-secondary text-xs font-medium text-foreground hover:bg-accent transition-colors shadow-xs disabled:opacity-50"
535
765
>
536
- {label.name}
537
- </span>
538
- ))}
766
+ {isTogglingState ? (
767
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
768
+ ) : issueState === "open" ? (
769
+ <>
770
+ <CheckCircle2 className="h-3.5 w-3.5 text-purple-400" />
771
+ <span>Close issue</span>
772
+ </>
773
+ ) : (
774
+ <>
775
+ <CircleDot className="h-3.5 w-3.5 text-emerald-500" />
776
+ <span>Reopen issue</span>
777
+ </>
778
+ )}
779
+ </button>
780
+ )}
781
+
782
+ <button
783
+ onClick={handleSubmitComment}
784
+ disabled={isSubmittingComment || !newComment.trim()}
785
+ className="inline-flex items-center gap-1.5 px-4 py-1.5 rounded-md bg-[#238636] hover:bg-[#2ea043] text-white text-xs sm:text-sm font-semibold transition-colors shadow-xs disabled:opacity-50"
786
+ >
787
+ {isSubmittingComment ? (
788
+ <>
789
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
790
+ <span>Commenting...</span>
791
+ </>
792
+ ) : (
793
+ <>
794
+ <Send className="h-3.5 w-3.5" />
795
+ <span>Comment</span>
796
+ </>
797
+ )}
798
+ </button>
539
799
</div>
540
800
</div>
541
801
</div>
542
- )}
802
+ </div>
803
+ </div>
543
804
544
- {/* Assignees (WS2-01) */}
545
- {((issue.assignees && issue.assignees.length > 0) || canEdit) && (
546
- <div className="relative">
547
- <div className="relative rounded-xl border border-border bg-card/60 p-4">
548
- <h3 className="text-sm font-semibold text-foreground flex items-center gap-2 mb-3">
549
- <Users className="h-4 w-4 text-cyan-400" />
550
- Assignees
551
- </h3>
552
- <div className="flex flex-wrap gap-2 mb-3">
553
- {issue.assignees && issue.assignees.length > 0 ? ( issue.assignees.map((a: any) => (
554
- <span
555
- key={a.id || a.username}
556
- className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium bg-secondary/80"
557
- >
558
- <span className="h-4 w-4 rounded-full bg-muted flex items-center justify-center text-[9px] font-semibold">
805
+ {/* Right Sidebar */}
806
+ <div className="space-y-5 text-xs">
807
+ {/* Assignees Section */}
808
+ <div className="pb-4 border-b border-border/70 space-y-2">
809
+ <div className="flex items-center justify-between">
810
+ <span className="font-semibold text-foreground flex items-center gap-1.5">
811
+ <Users className="h-3.5 w-3.5 text-muted-foreground" />
812
+ Assignees
813
+ </span>
814
+ </div>
815
+
816
+ {issue.assignees && issue.assignees.length > 0 ? (
817
+ <div className="space-y-1.5">
818
+ {issue.assignees.map((a: any) => (
819
+ <div key={a.id || a.username} className="flex items-center justify-between gap-2">
820
+ <div className="flex items-center gap-2">
821
+ {a.avatarUrl ? (
822
+ <img src={a.avatarUrl} alt={a.username} className="h-5 w-5 rounded-full object-cover" />
823
+ ) : (
824
+ <div className="h-5 w-5 rounded-full bg-secondary border border-border/70 flex items-center justify-center text-[10px] font-bold text-foreground">
559
825
{a.username?.[0]?.toUpperCase()}
560
- </span>
561
- {a.username}
562
- {canEdit && (
563
- <button
564
- className="opacity-60 hover:opacity-100 ml-0.5"
565
- onClick={() => handleAssigneeToggle(a.id || a.username)}
566
- title="Unassign"
567
- >
568
- ×
569
- </button>
570
- )}
571
- </span>
572
- ))
573
- ) : (
574
- <span className="text-xs text-muted-foreground">No one assigned</span>
575
- )}
576
- </div>
577
- {canEdit && (
578
- <select
579
- className="flex h-8 w-full rounded-md border border-input bg-background px-2 py-1 text-xs"
580
- value=""
581
- onChange={(e) => handleAssigneeToggle(e.target.value)}
582
- >
583
- <option value="">Assign user…</option>
584
- {collaborators.map((c: any) => (
585
- <option key={c.id || c.username} value={c.id || c.username}>
586
- {c.username}
587
- </option>
588
- ))}
589
- </select>
590
- )}
826
+ </div>
827
+ )}
828
+ <span className="text-foreground font-medium">@{a.username}</span>
829
+ </div>
830
+ {canEdit && (
831
+ <button
832
+ onClick={() => handleAssigneeToggle(a.id || a.username)}
833
+ className="text-muted-foreground hover:text-foreground text-sm leading-none"
834
+ title="Unassign"
835
+ >
836
+ ×
837
+ </button>
838
+ )}
839
+ </div>
840
+ ))}
591
841
</div>
842
+ ) : (
843
+ <p className="text-muted-foreground">No one assigned</p>
844
+ )}
845
+
846
+ {canEdit && collaborators.length > 0 && (
847
+ <select
848
+ className="w-full h-7 rounded-md border border-border/70 bg-card px-2 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-primary/30 mt-1"
849
+ value=""
850
+ onChange={(e) => handleAssigneeToggle(e.target.value)}
851
+ >
852
+ <option value="">Assign someone...</option>
853
+ {collaborators.map((c) => (
854
+ <option key={c.id || c.username} value={c.id || c.username}>
855
+ {c.username}
856
+ </option>
857
+ ))}
858
+ </select>
859
+ )}
860
+ </div>
861
+
862
+ {/* Labels Section */}
863
+ <div className="pb-4 border-b border-border/70 space-y-2">
864
+ <div className="flex items-center justify-between">
865
+ <span className="font-semibold text-foreground flex items-center gap-1.5">
866
+ <Tag className="h-3.5 w-3.5 text-muted-foreground" />
867
+ Labels
868
+ </span>
592
869
</div>
593
- )}
870
+ {issue.labels && issue.labels.length > 0 ? (
871
+ <div className="flex flex-wrap gap-1.5">
872
+ {issue.labels.map((l, idx) => (
873
+ <span
874
+ key={idx}
875
+ className="inline-flex items-center px-2 py-0.5 rounded-full text-[11px] font-medium border"
876
+ style={{
877
+ backgroundColor: `${l.color}15`,
878
+ borderColor: `${l.color}40`,
879
+ color: l.color
880
+ }}
881
+ >
882
+ {l.name}
883
+ </span>
884
+ ))}
885
+ </div>
886
+ ) : (
887
+ <p className="text-muted-foreground">None yet</p>
888
+ )}
889
+ </div>
890
+
891
+ {/* Projects Section */}
892
+ <div className="pb-4 border-b border-border/70 space-y-2">
893
+ <span className="font-semibold text-foreground flex items-center gap-1.5">
894
+ <Layout className="h-3.5 w-3.5 text-muted-foreground" />
895
+ Projects
896
+ </span>
897
+ <p className="text-muted-foreground">None yet</p>
898
+ </div>
899
+
900
+ {/* Milestone Section */}
901
+ <div className="pb-4 border-b border-border/70 space-y-2">
902
+ <span className="font-semibold text-foreground flex items-center gap-1.5">
903
+ <Calendar className="h-3.5 w-3.5 text-muted-foreground" />
904
+ Milestone
905
+ </span>
906
+ <p className="text-muted-foreground">No milestone</p>
907
+ </div>
594
908
595
- {/* Sub-tasks / Child Issues */}
596
- {(issue.type === 'epic' || (issue.children && issue.children.length > 0)) && (
597
- <div className="relative">
598
- <div className="absolute -inset-[1px] bg-gradient-to-b from-purple-500/10 to-transparent rounded-xl opacity-50" />
599
- <div className="relative rounded-xl border border-border bg-card/60 p-4">
600
- <div className="flex items-center justify-between mb-3">
601
- <h3 className="text-sm font-semibold text-foreground flex items-center gap-2">
602
- <ListTodo className="h-4 w-4 text-purple-400" />
603
- {issue.type === 'epic' ? 'Child Issues' : 'Sub-tasks'}
604
- </h3>
605
- {issue.children && issue.children.length > 0 && (
606
- <span className="text-xs bg-secondary/80 px-2 py-0.5 rounded-full text-muted-foreground">
607
- {issue.children.filter(c => c.state === 'closed').length} / {issue.children.length}
909
+ {/* Linked Pull Requests */}
910
+ <div className="pb-4 border-b border-border/70 space-y-2">
911
+ <span className="font-semibold text-foreground flex items-center gap-1.5">
912
+ <GitPullRequest className="h-3.5 w-3.5 text-muted-foreground" />
913
+ Linked Pull Requests
914
+ </span>
915
+ {loadingLinks ? (
916
+ <p className="text-muted-foreground">Loading...</p>
917
+ ) : linkedPRs.length > 0 ? (
918
+ <div className="space-y-1.5">
919
+ {linkedPRs.map((link) => (
920
+ <a
921
+ key={link.id}
922
+ href={`/${repoOwner}/${repoName}/pulls/${link.pullRequest.number}`}
923
+ className="flex items-center justify-between gap-2 p-1.5 rounded-md hover:bg-accent/60 transition-colors group"
924
+ >
925
+ <span className="text-foreground font-medium group-hover:text-primary transition-colors truncate">
926
+ #{link.pullRequest.number} {link.pullRequest.title}
608
927
</span>
609
- )}
928
+ <ArrowUpRight className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
929
+ </a>
930
+ ))}
931
+ </div>
932
+ ) : (
933
+ <p className="text-muted-foreground">No linked pull requests</p>
934
+ )}
935
+ </div>
936
+
937
+ {/* Linked Issues */}
938
+ <div className="pb-4 border-b border-border/70 space-y-2.5">
939
+ <span className="font-semibold text-foreground flex items-center gap-1.5">
940
+ <Link2 className="h-3.5 w-3.5 text-muted-foreground" />
941
+ Linked Issues
942
+ </span>
943
+ {loadingCrossRepo ? (
944
+ <p className="text-muted-foreground">Loading...</p>
945
+ ) : crossRepoLinks.length > 0 ? (
946
+ <div className="space-y-1.5">
947
+ {crossRepoLinks.map((link) => (
948
+ <a
949
+ key={link.id}
950
+ href={`/${link.repository.owner}/${link.repository.name}/issues/${link.issue.number}`}
951
+ className="flex items-start justify-between gap-2 p-1.5 rounded-md hover:bg-accent/60 transition-colors group"
952
+ >
953
+ <span className="text-foreground font-medium group-hover:text-primary transition-colors truncate">
954
+ {link.repository.owner}/{link.repository.name}#{link.issue.number} {link.issue.title}
955
+ </span>
956
+ <span className="text-[10px] uppercase font-semibold text-muted-foreground flex-shrink-0">
957
+ {link.linkType}
958
+ </span>
959
+ </a>
960
+ ))}
961
+ </div>
962
+ ) : (
963
+ <p className="text-muted-foreground">No linked issues</p>
964
+ )}
965
+
966
+ {canLink && (
967
+ <div className="space-y-1.5 pt-1">
968
+ <div className="space-y-1.5">
969
+ <input
970
+ type="text"
971
+ placeholder="owner/repo#123"
972
+ value={crossRepoTarget}
973
+ onChange={(e) => setCrossRepoTarget(e.target.value)}
974
+ className="w-full h-7 rounded-md border border-border/70 bg-card px-2 text-xs text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary/30"
975
+ />
976
+ <select
977
+ value={crossRepoType}
978
+ onChange={(e) => setCrossRepoType(e.target.value)}
979
+ className="w-full h-7 rounded-md border border-border/70 bg-card px-2 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-primary/30"
980
+ >
981
+ <option value="relates">Relates</option>
982
+ <option value="blocks">Blocks</option>
983
+ <option value="blocked_by">Blocked by</option>
984
+ <option value="cloned_from">Cloned from</option>
985
+ </select>
610
986
</div>
987
+ {crossRepoError && (
988
+ <p className="text-[11px] text-destructive">{crossRepoError}</p>
989
+ )}
990
+ <button
991
+ onClick={handleCrossRepoLink}
992
+ disabled={isLinking || !crossRepoTarget.trim()}
993
+ className="w-full h-7 rounded-md border border-border bg-secondary hover:bg-accent text-foreground text-xs font-medium transition-colors disabled:opacity-50"
994
+ >
995
+ {isLinking ? "Linking..." : "Link issue"}
996
+ </button>
997
+ </div>
998
+ )}
999
+ </div>
611
1000
612
- {/* Progress Bar */}
1001
+ {/* Sub-tasks / Epics (if applicable) */}
1002
+ {(issue.type === "epic" || (issue.children && issue.children.length > 0)) && (
1003
+ <div className="pb-4 border-b border-border/70 space-y-2">
1004
+ <div className="flex items-center justify-between">
1005
+ <span className="font-semibold text-foreground flex items-center gap-1.5">
1006
+ <ListTodo className="h-3.5 w-3.5 text-muted-foreground" />
1007
+ {issue.type === "epic" ? "Child Issues" : "Sub-tasks"}
1008
+ </span>
613
1009
{issue.children && issue.children.length > 0 && (
614
- <div className="h-1.5 w-full bg-secondary/80 rounded-full mb-3 overflow-hidden">
1010
+ <span className="text-[11px] text-muted-foreground font-mono">
1011
+ {issue.children.filter((c) => c.state === "closed").length}/{issue.children.length}
1012
+ </span>
1013
+ )}
1014
+ </div>
1015
+ {issue.children && issue.children.length > 0 && (
1016
+ <div className="space-y-1.5">
1017
+ <div className="h-1.5 w-full bg-secondary rounded-full overflow-hidden">
615
1018
<div
616
- className="h-full bg-purple-500 transition-all duration-500"
1019
+ className="h-full bg-purple-500 transition-all duration-300"
617
1020
style={{
618
- width: `${(issue.children.filter(c => c.state === 'closed').length / issue.children.length) * 100}%`
1021
+ width: `${(issue.children.filter((c) => c.state === "closed").length / issue.children.length) * 100}%`
619
1022
}}
620
1023
/>
621
1024
</div>
622
- )}
623
-
624
- <div className="space-y-1">
625
- {issue.children && issue.children.map(child => (
626
- <a
627
- key={child.number}
628
- href={`/${repoOwner}/${repoName}/issues/${child.number}`}
629
- className="flex items-start gap-2 p-2 rounded-lg hover:bg-accent transition-colors group"
630
- >
631
- <div className="mt-0.5">
632
- {child.state === "open" ? (
633
- <AlertCircle className="h-3.5 w-3.5 text-green-500" />
634
- ) : (
635
- <CheckCircle2 className="h-3.5 w-3.5 text-purple-500" />
636
- )}
637
- </div>
638
- <div className="text-sm min-w-0">
639
- <div className="text-muted-foreground group-hover:text-cyan-400 truncate transition-colors">
640
- {child.title}
641
- </div>
642
- </div>
643
- </a>
644
- ))}
645
-
646
- {issue.type === 'epic' && (
647
- <a
648
- href={`/${repoOwner}/${repoName}/issues/new?parent=${issue.number}`}
649
- className="flex items-center gap-2 p-2 rounded-lg hover:bg-accent text-sm text-muted-foreground hover:text-cyan-400 transition-colors mt-2 border border-dashed border-border hover:border-cyan-500/30 justify-center"
650
- >
651
- <ListTodo className="h-3.5 w-3.5" />
652
- Add sub-task
653
- </a>
654
- )}
1025
+ <div className="space-y-1 pt-1">
1026
+ {issue.children.map((child) => (
1027
+ <a
1028
+ key={child.number}
1029
+ href={`/${repoOwner}/${repoName}/issues/${child.number}`}
1030
+ className="flex items-center justify-between gap-2 text-muted-foreground hover:text-foreground py-0.5 truncate"
1031
+ >
1032
+ <span className="truncate">#{child.number} {child.title}</span>
1033
+ <span className="text-[10px] font-semibold uppercase">{child.state}</span>
1034
+ </a>
1035
+ ))}
1036
+ </div>
655
1037
</div>
656
- </div>
1038
+ )}
1039
+ </div>
1040
+ )}
1041
+
1042
+ {/* Participants Section */}
1043
+ <div className="space-y-2">
1044
+ <span className="font-semibold text-foreground">
1045
+ {participants.length} participant{participants.length === 1 ? "" : "s"}
1046
+ </span>
1047
+ <div className="flex flex-wrap gap-1.5">
1048
+ {participants.map((username) => (
1049
+ <a
1050
+ key={username}
1051
+ href={`/u/${username}`}
1052
+ className="h-6 w-6 rounded-full bg-secondary border border-border/80 flex items-center justify-center text-[10px] font-bold text-foreground hover:border-primary transition-colors"
1053
+ title={username}
1054
+ >
1055
+ {username?.[0]?.toUpperCase()}
1056
+ </a>
1057
+ ))}
1058
+ </div>
1059
+ </div>
1060
+
1061
+ {/* Epic Conversion Option if not epic */}
1062
+ {canEdit && issue.type !== "epic" && (
1063
+ <div className="pt-2">
1064
+ <button
1065
+ onClick={convertToEpic}
1066
+ className="w-full flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-md border border-purple-500/30 bg-purple-500/10 text-xs font-medium text-purple-400 hover:bg-purple-500/20 transition-colors"
1067
+ >
1068
+ <Layers className="h-3.5 w-3.5" />
1069
+ <span>Convert to Epic</span>
1070
+ </button>
657
1071
</div>
658
1072
)}
659
- </motion.div>
1073
+ </div>
660
1074
</div>
661
1075
</div>
662
1076
);
663
1077
▼src/components/issues/IssuesList.tsx
+42-51
107
107
108
108
return (
109
109
<div className="space-y-6">
110
- {/* Enhanced Header with Glow */}
110
+ {/* Header */}
111
111
<motion.div
112
112
initial={{ opacity: 0, y: -20 }}
113
113
animate={{ opacity: 1, y: 0 }}
114
114
className="flex flex-col lg:flex-row lg:items-center justify-between gap-4"
115
115
>
116
- {/* Search Input with Glass Effect */}
116
+ {/* Search Input */}
117
117
<div className="relative flex-1 max-w-xl group">
118
- <div className="absolute -inset-0.5 bg-gradient-to-r from-cyan-500/20 via-purple-500/20 to-pink-500/20 rounded-xl blur opacity-0 group-focus-within:opacity-100 transition-opacity" />
119
- <div className="relative">
120
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
121
- <input
122
- type="search"
123
- placeholder="Search all issues..."
124
- value={searchQuery}
125
- onChange={(e) => setSearchQuery(e.target.value)}
126
- className="w-full rounded-xl border border-border bg-secondary backdrop-blur-sm pl-10 pr-4 py-2.5 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-cyan-500/50 focus:ring-2 focus:ring-cyan-500/20 transition-all"
127
- />
128
- </div>
118
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-foreground transition-colors" />
119
+ <input
120
+ type="search"
121
+ placeholder="Search all issues..."
122
+ value={searchQuery}
123
+ onChange={(e) => setSearchQuery(e.target.value)}
124
+ className="w-full h-9 rounded-md border border-border/70 bg-card/60 pl-9 pr-4 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary/40 focus:bg-background transition-all"
125
+ />
129
126
</div>
130
127
131
128
{/* Action Buttons */}
132
- <div className="flex gap-3">
129
+ <div className="flex gap-2 flex-wrap">
133
130
<button
134
131
onClick={() => setGroupByEpic(!groupByEpic)}
135
- className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg border transition-all ${groupByEpic
136
- ? "bg-purple-500/20 border-purple-500/50 text-purple-300"
137
- : "border-border bg-secondary text-muted-foreground hover:bg-secondary/80"
132
+ className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md border text-xs font-medium transition-colors ${groupByEpic
133
+ ? "bg-secondary text-foreground border-border font-semibold shadow-sm"
134
+ : "border-border bg-card text-muted-foreground hover:text-foreground hover:bg-accent"
138
135
}`}
139
136
>
140
- <Layers className="h-4 w-4" />
141
- <span className="hidden sm:inline">Group by Epic</span>
137
+ <Layers className="h-3.5 w-3.5" />
138
+ <span>Group by Epic</span>
142
139
</button>
143
140
144
141
<motion.a
145
142
href={`/${repoOwner}/${repoName}/labels`}
146
143
whileHover={{ scale: 1.02 }}
147
144
whileTap={{ scale: 0.98 }}
148
- className="inline-flex items-center gap-2 px-4 py-2 rounded-lg border border-border bg-secondary text-sm text-muted-foreground hover:bg-secondary/80 hover:border-border/80 transition-all"
145
+ className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md border border-border bg-card text-xs font-medium text-foreground hover:bg-accent transition-colors shadow-sm"
149
146
>
150
- <Tag className="h-4 w-4" />
151
- <span className="hidden sm:inline">Labels</span>
147
+ <Tag className="h-3.5 w-3.5 text-muted-foreground" />
148
+ <span>Labels</span>
152
149
</motion.a>
153
-
154
150
<motion.a
155
151
href={`/${repoOwner}/${repoName}/milestones`}
156
152
whileHover={{ scale: 1.02 }}
157
153
whileTap={{ scale: 0.98 }}
158
- className="inline-flex items-center gap-2 px-4 py-2 rounded-lg border border-border bg-secondary text-sm text-muted-foreground hover:bg-secondary/80 hover:border-border/80 transition-all"
154
+ className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md border border-border bg-card text-xs font-medium text-foreground hover:bg-accent transition-colors shadow-sm"
159
155
>
160
- <Calendar className="h-4 w-4" />
161
- <span className="hidden sm:inline">Milestones</span>
156
+ <Calendar className="h-3.5 w-3.5 text-muted-foreground" />
157
+ <span>Milestones</span>
162
158
</motion.a>
163
-
164
159
<motion.a
165
160
href={`/${repoOwner}/${repoName}/issues/new`}
166
- whileHover={{ scale: 1.05 }}
167
- whileTap={{ scale: 0.95 }}
168
- className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gradient-to-r from-green-500 to-emerald-600 text-sm font-medium text-foreground shadow-lg shadow-green-500/25 hover:shadow-green-500/40 transition-all"
161
+ whileHover={{ scale: 1.02 }}
162
+ whileTap={{ scale: 0.98 }}
163
+ className="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-md bg-[#238636] hover:bg-[#2ea043] text-white text-xs sm:text-sm font-medium transition-colors shadow-sm"
169
164
>
170
165
<Plus className="h-4 w-4" />
171
- <span className="hidden sm:inline">New Issue</span>
166
+ <span>New Issue</span>
172
167
</motion.a>
173
168
</div>
174
169
</motion.div>
175
170
176
- {/* Issues Container with Glass Effect */}
171
+ {/* Issues List Container */}
177
172
<motion.div
178
173
initial={{ opacity: 0 }}
179
174
animate={{ opacity: 1 }}
180
175
transition={{ delay: 0.1 }}
181
- className="relative"
182
176
>
183
- {/* Gradient border effect */}
184
- <div className="absolute -inset-[1px] bg-gradient-to-r from-cyan-500/30 via-purple-500/20 to-pink-500/30 rounded-xl blur-sm opacity-50" />
185
-
186
- <div className="relative rounded-xl border border-border bg-card/80 backdrop-blur-sm overflow-hidden">
177
+ <div className="rounded-xl border border-border/80 bg-card/60 backdrop-blur-sm overflow-hidden shadow-sm">
187
178
{/* Filter Header */}
188
- <div className="flex items-center justify-between gap-4 border-b border-white/5 bg-white/[0.02] px-4 py-3">
179
+ <div className="flex items-center justify-between gap-4 border-b border-border/60 bg-muted/30 px-4 py-3">
189
180
<div className="flex items-center gap-1">
190
181
<button
191
182
onClick={() => setFilter("open")}
192
- className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all ${filter === "open"
193
- ? "bg-green-500/10 text-green-400 border border-green-500/30"
194
- : "text-muted-foreground hover:text-foreground hover:bg-accent"
183
+ className={`flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium transition-colors ${filter === "open"
184
+ ? "bg-secondary text-foreground font-semibold shadow-sm"
185
+ : "text-muted-foreground hover:text-foreground hover:bg-muted/60"
195
186
}`}
196
187
>
197
- <AlertCircle className="h-4 w-4" />
198
- {openCount} Open
188
+ <AlertCircle className="h-3.5 w-3.5 text-emerald-500" />
189
+ <span>{openCount} Open</span>
199
190
</button>
200
191
<button
201
192
onClick={() => setFilter("closed")}
202
- className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all ${filter === "closed"
203
- ? "bg-purple-500/10 text-purple-400 border border-purple-500/30"
204
- : "text-muted-foreground hover:text-foreground hover:bg-accent"
193
+ className={`flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium transition-colors ${filter === "closed"
194
+ ? "bg-secondary text-foreground font-semibold shadow-sm"
195
+ : "text-muted-foreground hover:text-foreground hover:bg-muted/60"
205
196
}`}
206
197
>
207
- <CheckCircle2 className="h-4 w-4" />
208
- {closedCount} Closed
198
+ <CheckCircle2 className="h-3.5 w-3.5 text-purple-400" />
199
+ <span>{closedCount} Closed</span>
209
200
</button>
210
201
</div>
211
202
212
- <div className="flex items-center gap-3 text-sm text-muted-foreground">
203
+ <div className="flex items-center gap-3 text-xs text-muted-foreground">
213
204
<button className="flex items-center gap-1 hover:text-foreground transition-colors">
214
205
<ArrowUpDown className="h-3.5 w-3.5" />
215
- Sort
206
+ <span>Sort</span>
216
207
</button>
217
208
</div>
218
209
</div>
219
210
220
211
{/* Issues List */}
221
- <div className="divide-y divide-white/5">
212
+ <div className="divide-y divide-border/60">
222
213
<AnimatePresence mode="popLayout">
223
214
{filteredIssues.length > 0 ? (
224
215
filteredIssues.map((issue, index) => (
225
216
▼src/components/layout/Header.astro
+3-3
2
2
import { Search, Bell, Plus, Menu } from "lucide-react"
3
3
import { GlobalSearch } from "@/components/search/GlobalSearch"
4
4
import { ThemeToggle } from "@/components/ThemeToggle"
5
+import Logo from "@/components/ui/Logo.astro"
5
6
import { getCsrfToken } from "@/middleware/csrf"
6
7
7
8
const user = Astro.locals.user;
12
13
<header class="sticky top-0 z-50 w-full border-b border-border/40 bg-background/80 backdrop-blur-xl" role="banner">
13
14
<div class="container h-14 flex items-center justify-between gap-4">
14
15
<!-- Logo -->
15
- <a href="/" class="flex-shrink-0 transition-opacity hover:opacity-80 focus:outline-none focus:ring-2 focus:ring-primary/20 focus:rounded-md" aria-label="OpenCodeHub home">
16
- <img src="/logo-dark.png" alt="OpenCodeHub" class="h-7 w-auto dark:hidden" />
17
- <img src="/logo-light.png" alt="OpenCodeHub" class="h-7 w-auto hidden dark:block" />
16
+ <a href="/" class="flex-shrink-0 text-foreground transition-opacity hover:opacity-80 focus:outline-none focus:ring-2 focus:ring-primary/20 focus:rounded-md" aria-label="OpenCodeHub home">
17
+ <Logo class="h-7 w-7" />
18
18
</a>
19
19
20
20
<!-- Navigation -->
21
21
▼src/components/profile/UserProfileView.astro
+133-103
41
41
activityStart.setHours(0, 0, 0, 0);
42
42
43
43
// 1. Fetch Follow Stats
44
-const [followersResult] = await db
45
- .select({ count: sql`count(*)` })
46
- .from(schema.userFollowers)
47
- .where(eq(schema.userFollowers.followeeId, user.id));
48
-const followersCount = Number(followersResult?.count) || 0;
49
-
50
-const [followingResult] = await db
51
- .select({ count: sql`count(*)` })
52
- .from(schema.userFollowers)
53
- .where(eq(schema.userFollowers.followerId, user.id));
54
-const followingCount = Number(followingResult?.count) || 0;
44
+let followersCount = 0;
45
+let followingCount = 0;
46
+
47
+if (user.id !== "placeholder-user") {
48
+ try {
49
+ const [followersResult] = await db
50
+ .select({ count: sql`count(*)` })
51
+ .from(schema.userFollowers)
52
+ .where(eq(schema.userFollowers.followeeId, user.id));
53
+ followersCount = Number(followersResult?.count) || 0;
54
+
55
+ const [followingResult] = await db
56
+ .select({ count: sql`count(*)` })
57
+ .from(schema.userFollowers)
58
+ .where(eq(schema.userFollowers.followerId, user.id));
59
+ followingCount = Number(followingResult?.count) || 0;
60
+ } catch (e) {
61
+ // Graceful fallback if database query fails
62
+ }
63
+}
55
64
56
65
// 2. Check if following
57
66
let isFollowing = false;
58
-if (currentUser && !isOwner) {
59
- const existing = await db.query.userFollowers.findFirst({
60
- where: and(
61
- eq(schema.userFollowers.followerId, currentUser.id),
62
- eq(schema.userFollowers.followeeId, user.id),
63
- ),
64
- });
65
- isFollowing = !!existing;
67
+if (currentUser && !isOwner && user.id !== "placeholder-user") {
68
+ try {
69
+ const existing = await db.query.userFollowers.findFirst({
70
+ where: and(
71
+ eq(schema.userFollowers.followerId, currentUser.id),
72
+ eq(schema.userFollowers.followeeId, user.id),
73
+ ),
74
+ });
75
+ isFollowing = !!existing;
76
+ } catch (e) {
77
+ // Graceful fallback
78
+ }
66
79
}
67
80
68
81
// 3. Fetch Repositories
69
-let repoConditions = [eq(schema.repositories.ownerId, user.id)];
70
-if (!isOwner) {
71
- repoConditions.push(eq(schema.repositories.visibility, "public"));
72
-}
82
+let repos: any[] = [];
83
+if (user.id !== "placeholder-user") {
84
+ try {
85
+ let repoConditions = [eq(schema.repositories.ownerId, user.id)];
86
+ if (!isOwner) {
87
+ repoConditions.push(eq(schema.repositories.visibility, "public"));
88
+ }
73
89
74
-const repos = await db.query.repositories.findMany({
75
- where: and(...repoConditions),
76
- orderBy: [desc(schema.repositories.starCount), desc(schema.repositories.updatedAt)],
77
-});
90
+ repos = await db.query.repositories.findMany({
91
+ where: and(...repoConditions),
92
+ orderBy: [desc(schema.repositories.starCount), desc(schema.repositories.updatedAt)],
93
+ });
94
+ } catch (e) {
95
+ // Graceful fallback if database query fails
96
+ }
97
+}
78
98
79
99
const pinnedRepos = repos.slice(0, 6);
80
100
81
101
// 4. Fetch Starred Repositories
82
102
let starCount = 0;
83
103
let starredRepos: any[] = [];
84
-try {
85
- const stars = await db.query.repositoryStars.findMany({
86
- where: eq(schema.repositoryStars.userId, user.id),
87
- with: {
88
- repository: {
89
- with: {
90
- owner: true,
104
+if (user.id !== "placeholder-user") {
105
+ try {
106
+ const stars = await db.query.repositoryStars.findMany({
107
+ where: eq(schema.repositoryStars.userId, user.id),
108
+ with: {
109
+ repository: {
110
+ with: {
111
+ owner: true,
112
+ },
91
113
},
92
114
},
93
- },
94
- });
95
- starCount = stars.length;
96
- starredRepos = stars.map((s) => s.repository).filter(Boolean);
97
-} catch (e) {
98
- // Graceful fallback if stars query fails
115
+ });
116
+ starCount = stars.length;
117
+ starredRepos = stars.map((s) => s.repository).filter(Boolean);
118
+ } catch (e) {
119
+ // Graceful fallback if stars query fails
120
+ }
99
121
}
100
122
101
123
// 5. Fetch contribution count and day-by-day mapping
102
124
let contributionCount = 0;
103
125
let activityByDay = new Map<string, number>();
104
-try {
105
- const [result] = await db
106
- .select({ count: sql`count(*)` })
107
- .from(schema.activities)
108
- .where(
109
- and(
110
- eq(schema.activities.userId, user.id),
111
- sql`${schema.activities.createdAt} >= ${activityStart}`,
112
- ),
113
- );
114
- contributionCount = Number(result?.count) || 0;
115
-
116
- const activityRows = await db
117
- .select({
118
- createdAt: schema.activities.createdAt,
119
- })
120
- .from(schema.activities)
121
- .where(
122
- and(
123
- eq(schema.activities.userId, user.id),
124
- sql`${schema.activities.createdAt} >= ${activityStart}`,
125
- ),
126
- );
127
- for (const row of activityRows) {
128
- const day = new Date(row.createdAt).toISOString().slice(0, 10);
129
- activityByDay.set(day, (activityByDay.get(day) || 0) + 1);
126
+if (user.id !== "placeholder-user") {
127
+ try {
128
+ const [result] = await db
129
+ .select({ count: sql`count(*)` })
130
+ .from(schema.activities)
131
+ .where(
132
+ and(
133
+ eq(schema.activities.userId, user.id),
134
+ sql`${schema.activities.createdAt} >= ${activityStart}`,
135
+ ),
136
+ );
137
+ contributionCount = Number(result?.count) || 0;
138
+
139
+ const activityRows = await db
140
+ .select({
141
+ createdAt: schema.activities.createdAt,
142
+ })
143
+ .from(schema.activities)
144
+ .where(
145
+ and(
146
+ eq(schema.activities.userId, user.id),
147
+ sql`${schema.activities.createdAt} >= ${activityStart}`,
148
+ ),
149
+ );
150
+ for (const row of activityRows) {
151
+ const day = new Date(row.createdAt).toISOString().slice(0, 10);
152
+ activityByDay.set(day, (activityByDay.get(day) || 0) + 1);
153
+ }
154
+ } catch {
155
+ // Fallback
130
156
}
131
-} catch {
132
- // Fallback
133
157
}
134
158
135
159
// 6. Fetch recent activity timeline
136
160
let recentActivity: any[] = [];
137
-try {
138
- recentActivity = await db.query.activities.findMany({
139
- where: eq(schema.activities.userId, user.id),
140
- orderBy: [desc(schema.activities.createdAt)],
141
- limit: 6,
142
- });
143
-} catch {}
161
+if (user.id !== "placeholder-user") {
162
+ try {
163
+ recentActivity = await db.query.activities.findMany({
164
+ where: eq(schema.activities.userId, user.id),
165
+ orderBy: [desc(schema.activities.createdAt)],
166
+ limit: 6,
167
+ });
168
+ } catch {}
169
+}
144
170
145
171
// 7. Fetch profile README from {username}/{username} repo or default
146
172
let profileReadmeHtml = "";
147
173
let hasSpecialRepo = false;
148
-try {
149
- const profileRepo = await db.query.repositories.findFirst({
150
- where: and(
151
- eq(schema.repositories.ownerId, user.id),
152
- eq(schema.repositories.name, user.username),
153
- ),
154
- });
155
- if (profileRepo) {
156
- hasSpecialRepo = true;
157
- const localPath = await resolveRepoPath(profileRepo.diskPath);
158
- const fileData = await getFileContent(
159
- localPath,
160
- "README.md",
161
- profileRepo.defaultBranch || "main",
162
- );
163
- if (fileData?.content) {
164
- profileReadmeHtml = await renderMarkdown(fileData.content);
174
+if (user.id !== "placeholder-user") {
175
+ try {
176
+ const profileRepo = await db.query.repositories.findFirst({
177
+ where: and(
178
+ eq(schema.repositories.ownerId, user.id),
179
+ eq(schema.repositories.name, user.username),
180
+ ),
181
+ });
182
+ if (profileRepo) {
183
+ hasSpecialRepo = true;
184
+ const localPath = await resolveRepoPath(profileRepo.diskPath);
185
+ const fileData = await getFileContent(
186
+ localPath,
187
+ "README.md",
188
+ profileRepo.defaultBranch || "main",
189
+ );
190
+ if (fileData?.content) {
191
+ profileReadmeHtml = await renderMarkdown(fileData.content);
192
+ }
165
193
}
166
- }
167
-} catch {}
194
+ } catch {}
195
+}
168
196
169
197
// If no special repo README exists, provide a default profile readme card showcasing the user's projects and bio
170
198
if (!profileReadmeHtml) {
186
214
187
215
// 8. Fetch User Organizations
188
216
let userOrgs: any[] = [];
189
-try {
190
- const orgMembers = await db.query.organizationMembers.findMany({
191
- where: eq(schema.organizationMembers.userId, user.id),
192
- with: {
193
- organization: true,
194
- },
195
- });
196
- userOrgs = orgMembers.map((m) => m.organization).filter(Boolean);
197
-} catch {}
217
+if (user.id !== "placeholder-user") {
218
+ try {
219
+ const orgMembers = await db.query.organizationMembers.findMany({
220
+ where: eq(schema.organizationMembers.userId, user.id),
221
+ with: {
222
+ organization: true,
223
+ },
224
+ });
225
+ userOrgs = orgMembers.map((m) => m.organization).filter(Boolean);
226
+ } catch {}
227
+}
198
228
199
229
// Language colors (GitHub style)
200
230
const langColors: Record<string, string> = {
201
231
▼src/components/pulls/PullRequestsList.tsx
+12-12
276
276
className="flex flex-col lg:flex-row lg:items-center justify-between gap-4"
277
277
>
278
278
{/* Search Input */}
279
- <div className="relative flex-1 max-w-xl">
280
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
279
+ <div className="relative flex-1 max-w-xl group">
280
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-foreground transition-colors" />
281
281
<input
282
282
type="search"
283
283
placeholder="Search all pull requests..."
284
284
value={searchQuery}
285
285
onChange={(e) => setSearchQuery(e.target.value)}
286
- className="w-full rounded-lg border border-border bg-card pl-10 pr-4 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-primary/50 focus:ring-1 focus:ring-primary/20 transition-all"
286
+ className="w-full h-9 rounded-md border border-border/70 bg-card/60 pl-9 pr-4 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary/40 focus:bg-background transition-all"
287
287
/>
288
288
</div>
289
289
290
290
{/* Action Buttons */}
291
- <div className="flex gap-2">
291
+ <div className="flex gap-2 flex-wrap">
292
292
<motion.a
293
293
href={`/${repoOwner}/${repoName}/labels`}
294
294
whileHover={{ scale: 1.02 }}
295
295
whileTap={{ scale: 0.98 }}
296
- className="inline-flex items-center gap-2 px-3 py-2 rounded-lg border border-border bg-card text-sm text-muted-foreground hover:bg-accent hover:text-foreground transition-all"
296
+ className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md border border-border bg-card text-xs font-medium text-foreground hover:bg-accent transition-colors shadow-sm"
297
297
>
298
- <Tag className="h-4 w-4" />
299
- Labels
298
+ <Tag className="h-3.5 w-3.5 text-muted-foreground" />
299
+ <span>Labels</span>
300
300
</motion.a>
301
301
<motion.a
302
302
href={`/${repoOwner}/${repoName}/milestones`}
303
303
whileHover={{ scale: 1.02 }}
304
304
whileTap={{ scale: 0.98 }}
305
- className="inline-flex items-center gap-2 px-3 py-2 rounded-lg border border-border bg-card text-sm text-muted-foreground hover:bg-accent hover:text-foreground transition-all"
305
+ className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md border border-border bg-card text-xs font-medium text-foreground hover:bg-accent transition-colors shadow-sm"
306
306
>
307
- <Calendar className="h-4 w-4" />
308
- Milestones
307
+ <Calendar className="h-3.5 w-3.5 text-muted-foreground" />
308
+ <span>Milestones</span>
309
309
</motion.a>
310
310
<motion.a
311
311
href={`/${repoOwner}/${repoName}/compare`}
312
312
whileHover={{ scale: 1.02 }}
313
313
whileTap={{ scale: 0.98 }}
314
- className="inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-primary text-sm font-medium text-primary-foreground hover:opacity-90 transition-all"
314
+ className="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-md bg-[#238636] hover:bg-[#2ea043] text-white text-xs sm:text-sm font-medium transition-colors shadow-sm"
315
315
>
316
316
<Plus className="h-4 w-4" />
317
- New Pull Request
317
+ <span>New Pull Request</span>
318
318
</motion.a>
319
319
</div>
320
320
</motion.div>
321
321
▼src/components/ui/Logo.astro
+24-0
1
+---
2
+interface Props {
3
+ class?: string;
4
+ size?: number | string;
5
+}
6
+
7
+const { class: className = "h-7 w-7", size } = Astro.props;
8
+---
9
+
10
+<svg
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ viewBox="45 45 210 210"
13
+ fill="currentColor"
14
+ class={className}
15
+ width={size}
16
+ height={size}
17
+ aria-hidden="true"
18
+>
19
+ <path
20
+ fill-rule="evenodd"
21
+ clip-rule="evenodd"
22
+ d="M184.27588,89.68521c2.96924,0.22368 3.29297,2.88721 3.75439,5.34082c1.19385,6.34468 0.82178,12.62754 0.00293,18.98174c0.34131,0.62461 0.66797,1.25728 0.97998,1.89756c5.40088,11.37407 2.61914,24.01758 -7.58203,31.66011c-2.17236,1.62891 -4.00049,2.51074 -6.4248,3.66357c0.82178,0.76904 1.60254,1.5293 2.35693,2.37012c10.20264,11.34814 10.51318,31.02539 1.01953,42.86865c-0.7749,0.9668 -1.70801,1.875 -2.36572,2.91064c-0.18896,1.81934 -0.08496,5.79053 -0.08496,7.70508l0.00732,14.01855c0.01172,4.06201 0.68115,7.9541 -2.32178,11.17236c-1.55713,1.69189 -3.74414,2.66602 -6.04395,2.69238c-2.2251,0.0542 -4.37256,-0.81006 -5.93994,-2.3877c-2.77588,-2.78174 -2.42139,-5.74805 -2.42285,-9.37646c-0.00293,-2.15186 0.00293,-4.32129 0.00293,-6.47314l0.00146,-24.48047c6.01758,-2.28369 9.25928,-8.7041 7.61719,-14.91797c-0.83789,-3.1875 -2.90332,-5.91357 -5.74365,-7.5835c-2.87109,-1.6626 -6.28418,-2.12109 -9.49365,-1.27441c-3.25635,0.86279 -6.023,3.00879 -7.66934,5.94727c-1.6771,2.98242 -2.0748,6.51709 -1.10244,9.79834c1.07827,3.66211 3.30176,6.06738 6.59795,7.8457v31.3418l-0.00146,8.86523c-0.00293,1.69775 -0.05127,3.50098 0.06885,5.18408c0.07031,0.98438 0.24023,1.74902 0.93457,2.45801c1.06494,1.08838 2.63818,1.26123 4.08838,1.25244c5.39648,-0.03516 11.21777,-0.63721 16.4458,-2.0083c11.50488,-3.01465 25.31982,-11.12842 33.73096,-19.5498c-1.04004,-3.65479 -1.04297,-6.17139 0.27979,-9.73682c0.11426,-0.28418 0.23584,-0.56396 0.36768,-0.83936c1.53955,-3.31641 4.34473,-5.87695 7.78711,-7.10889c3.49512,-1.24365 7.1792,-1.10596 10.5249,0.52588c3.38965,1.6582 5.97949,4.59814 7.19385,8.16943c1.18945,3.55518 0.89648,7.43848 -0.81152,10.77539c-1.72119,3.35449 -4.70801,5.88574 -8.2998,7.03418c-3.99609,1.25098 -7.6333,0.60791 -11.26465,-1.28613c-11.9165,11.17676 -26.95312,19.90283 -43.24658,22.32861c-6.1377,0.91406 -18.28271,1.79883 -24.2209,0.28711c-1.4543,-0.36035 -2.82349,-1.00342 -4.0292,-1.89258c-1.92026,-1.43994 -3.30073,-3.4834 -3.91978,-5.80225c-0.80962,-2.91797 -0.59106,-7.26416 -0.58989,-10.39453l0.0126,-20.7627c0.00176,-2.729 0.26045,-7.91602 -0.20142,-10.47656c-0.9293,-1.44434 -2.89556,-3.36035 -4.03506,-4.95996c-7.36787,-10.34326 -8.05313,-24.92871 -1.97593,-36.03662c1.89521,-3.46436 3.96914,-5.61768 6.89136,-8.27051c-1.10273,-0.60498 -2.32017,-1.15137 -3.42407,-1.75488c-10.07725,-5.50151 -15.41455,-16.65469 -12.49043,-27.89648c0.75337,-2.89644 1.79604,-5.09502 3.26177,-7.72251c-0.23101,-1.61455 -0.4084,-3.23628 -0.53218,-4.86255c-0.26484,-3.68013 -0.14707,-15.76128 2.28369,-18.38569c0.49512,-0.53452 1.12632,-0.72598 1.84219,-0.73696c4.777,-0.07251 14.12007,8.55615 17.44307,11.97817c6.83555,-0.78853 16.69834,-0.79482 23.51572,0.04937c3.84082,-4.00254 11.6543,-11.28779 17.2251,-12.14487z M216.82617,208.70947c3.49512,-0.50391 6.74268,1.9043 7.27588,5.39355c0.5332,3.49072 -1.8457,6.75879 -5.33057,7.32275c-3.52881,0.57129 -6.84521,-1.84424 -7.38574,-5.37744c-0.54053,-3.5332 1.90283,-6.8291 5.44043,-7.33887z M150.01025,49.42808c8.76855,-0.53481 18.6665,0.87524 27.06592,3.29912c20.78613,5.99868 38.0083,20.7375 48.39844,39.57656c11.02441,20.03057 13.20557,42.40825 6.84668,64.47847c-4.68457,16.26416 -15.27686,33.76758 -29.49902,43.79297c-0.8291,0.58447 -2.42578,0.64014 -3.39551,0.33105c-1.20264,-0.38965 -2.1958,-1.24951 -2.75391,-2.38477c-0.78223,-1.54248 -0.68701,-3.38379 0.24756,-4.83838c0.77344,-1.19238 2.30859,-1.99658 3.36621,-2.96631c1.39453,-1.27734 2.7832,-2.58984 4.08691,-3.96094c8.34229,-8.7583 14.54004,-19.33154 18.10547,-30.89062c6.12744,-19.20322 4.20703,-40.06919 -5.31885,-57.83232c-13.10742,-24.82339 -38.29687,-39.67676 -66.35449,-38.24443c-11.62852,0.5937 -20.97378,3.44312 -31.22754,8.85864c-0.09214,0.21592 0.06855,2.36089 0.08599,2.76357c0.10093,2.32456 -0.25884,4.14609 -1.20132,6.28271c-1.61675,3.70122 -4.65234,6.5981 -8.4252,8.03994c-4.34063,1.67285 -7.91528,1.0749 -12.02065,-0.72729c-1.39482,1.60225 -2.82202,3.25049 -4.10435,4.94326c-9.65625,12.99712 -14.91196,28.73716 -15.0019,44.92852c0.00176,4.17891 0.44326,7.88203 1.01514,11.99033c2.62295,0.16113 4.83779,0.89355 7.03564,2.35986c3.29355,2.22656 5.54854,5.68799 6.254,9.60059c0.80186,4.28027 -0.46406,7.95996 -2.85498,11.45801c6.1188,10.2832 12.25576,17.34961 22.19165,24.18604c2.72197,1.86182 5.57842,3.51855 8.54546,4.95703c1.91646,0.92285 5.19565,1.96143 6.36152,3.69434c0.70986,1.0708 0.95127,2.38623 0.66841,3.64014c-0.63237,2.74805 -3.42524,4.40332 -6.0498,3.42188c-16.91338,-6.33398 -30.71909,-19.39453 -39.48354,-34.77979c-1.91895,0.38965 -3.55415,0.55664 -5.51953,0.2959c-3.75059,-0.50244 -7.1439,-2.48584 -9.42217,-5.50635c-2.37363,-3.13184 -3.39595,-7.08252 -2.83887,-10.97168c0.58975,-4.17334 2.75815,-7.26123 6.04922,-9.76172c-0.84521,-4.86138 -1.40742,-9.20464 -1.51685,-14.15186c-0.4582,-20.73208 7.9248,-42.52515 22.04824,-57.59678c-0.52134,-1.40479 -0.85063,-2.36895 -1.07036,-3.8603c-0.57583,-3.9999 0.44736,-8.06558 2.8478,-11.31636c2.4063,-3.16348 5.97246,-5.23945 9.91157,-5.76943c4.11064,-0.54932 7.88833,0.62358 11.15728,3.0999c12.08159,-6.54038 21.99536,-9.5105 35.76973,-10.4395z M104.33159,64.69878c3.83057,-0.32915 7.21245,2.48599 7.58335,6.31274c0.37104,3.82676 -2.40718,7.23911 -6.22969,7.65176c-2.51001,0.271 -4.97197,-0.82749 -6.44678,-2.87651c-1.4748,-2.04902 -1.73496,-4.73247 -0.68115,-7.02656c1.05381,-2.29424 3.25898,-3.84521 5.77427,-4.06143z M77.86846,154.6333c3.67031,-0.67822 7.19575,1.74463 7.87866,5.41406c0.68291,3.66943 -1.73511,7.19824 -5.40337,7.88672c-3.67588,0.68994 -7.21377,-1.73437 -7.898,-5.41113c-0.68437,-3.67676 1.74478,-7.21143 5.42271,-7.88965z M153.91992,174.95801c3.0835,-0.47461 5.96777,1.64209 6.43945,4.72559c0.47021,3.0835 -1.64941,5.96484 -4.73437,6.43359c-3.0791,0.46582 -5.95459,-1.64941 -6.4248,-4.72852c-0.47021,-3.07764 1.64209,-5.95605 4.71973,-6.43066z"
23
+ />
24
+</svg>
0
25
▼src/components/ui/Logo.tsx
+29-0
1
+import React from "react";
2
+
3
+interface LogoProps extends React.SVGProps<SVGSVGElement> {
4
+ className?: string;
5
+ size?: number | string;
6
+}
7
+
8
+export function Logo({ className = "h-7 w-7", size, ...props }: LogoProps) {
9
+ return (
10
+ <svg
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ viewBox="45 45 210 210"
13
+ fill="currentColor"
14
+ className={className}
15
+ width={size}
16
+ height={size}
17
+ aria-hidden="true"
18
+ {...props}
19
+ >
20
+ <path
21
+ fillRule="evenodd"
22
+ clipRule="evenodd"
23
+ d="M184.27588,89.68521c2.96924,0.22368 3.29297,2.88721 3.75439,5.34082c1.19385,6.34468 0.82178,12.62754 0.00293,18.98174c0.34131,0.62461 0.66797,1.25728 0.97998,1.89756c5.40088,11.37407 2.61914,24.01758 -7.58203,31.66011c-2.17236,1.62891 -4.00049,2.51074 -6.4248,3.66357c0.82178,0.76904 1.60254,1.5293 2.35693,2.37012c10.20264,11.34814 10.51318,31.02539 1.01953,42.86865c-0.7749,0.9668 -1.70801,1.875 -2.36572,2.91064c-0.18896,1.81934 -0.08496,5.79053 -0.08496,7.70508l0.00732,14.01855c0.01172,4.06201 0.68115,7.9541 -2.32178,11.17236c-1.55713,1.69189 -3.74414,2.66602 -6.04395,2.69238c-2.2251,0.0542 -4.37256,-0.81006 -5.93994,-2.3877c-2.77588,-2.78174 -2.42139,-5.74805 -2.42285,-9.37646c-0.00293,-2.15186 0.00293,-4.32129 0.00293,-6.47314l0.00146,-24.48047c6.01758,-2.28369 9.25928,-8.7041 7.61719,-14.91797c-0.83789,-3.1875 -2.90332,-5.91357 -5.74365,-7.5835c-2.87109,-1.6626 -6.28418,-2.12109 -9.49365,-1.27441c-3.25635,0.86279 -6.023,3.00879 -7.66934,5.94727c-1.6771,2.98242 -2.0748,6.51709 -1.10244,9.79834c1.07827,3.66211 3.30176,6.06738 6.59795,7.8457v31.3418l-0.00146,8.86523c-0.00293,1.69775 -0.05127,3.50098 0.06885,5.18408c0.07031,0.98438 0.24023,1.74902 0.93457,2.45801c1.06494,1.08838 2.63818,1.26123 4.08838,1.25244c5.39648,-0.03516 11.21777,-0.63721 16.4458,-2.0083c11.50488,-3.01465 25.31982,-11.12842 33.73096,-19.5498c-1.04004,-3.65479 -1.04297,-6.17139 0.27979,-9.73682c0.11426,-0.28418 0.23584,-0.56396 0.36768,-0.83936c1.53955,-3.31641 4.34473,-5.87695 7.78711,-7.10889c3.49512,-1.24365 7.1792,-1.10596 10.5249,0.52588c3.38965,1.6582 5.97949,4.59814 7.19385,8.16943c1.18945,3.55518 0.89648,7.43848 -0.81152,10.77539c-1.72119,3.35449 -4.70801,5.88574 -8.2998,7.03418c-3.99609,1.25098 -7.6333,0.60791 -11.26465,-1.28613c-11.9165,11.17676 -26.95312,19.90283 -43.24658,22.32861c-6.1377,0.91406 -18.28271,1.79883 -24.2209,0.28711c-1.4543,-0.36035 -2.82349,-1.00342 -4.0292,-1.89258c-1.92026,-1.43994 -3.30073,-3.4834 -3.91978,-5.80225c-0.80962,-2.91797 -0.59106,-7.26416 -0.58989,-10.39453l0.0126,-20.7627c0.00176,-2.729 0.26045,-7.91602 -0.20142,-10.47656c-0.9293,-1.44434 -2.89556,-3.36035 -4.03506,-4.95996c-7.36787,-10.34326 -8.05313,-24.92871 -1.97593,-36.03662c1.89521,-3.46436 3.96914,-5.61768 6.89136,-8.27051c-1.10273,-0.60498 -2.32017,-1.15137 -3.42407,-1.75488c-10.07725,-5.50151 -15.41455,-16.65469 -12.49043,-27.89648c0.75337,-2.89644 1.79604,-5.09502 3.26177,-7.72251c-0.23101,-1.61455 -0.4084,-3.23628 -0.53218,-4.86255c-0.26484,-3.68013 -0.14707,-15.76128 2.28369,-18.38569c0.49512,-0.53452 1.12632,-0.72598 1.84219,-0.73696c4.777,-0.07251 14.12007,8.55615 17.44307,11.97817c6.83555,-0.78853 16.69834,-0.79482 23.51572,0.04937c3.84082,-4.00254 11.6543,-11.28779 17.2251,-12.14487z M216.82617,208.70947c3.49512,-0.50391 6.74268,1.9043 7.27588,5.39355c0.5332,3.49072 -1.8457,6.75879 -5.33057,7.32275c-3.52881,0.57129 -6.84521,-1.84424 -7.38574,-5.37744c-0.54053,-3.5332 1.90283,-6.8291 5.44043,-7.33887z M150.01025,49.42808c8.76855,-0.53481 18.6665,0.87524 27.06592,3.29912c20.78613,5.99868 38.0083,20.7375 48.39844,39.57656c11.02441,20.03057 13.20557,42.40825 6.84668,64.47847c-4.68457,16.26416 -15.27686,33.76758 -29.49902,43.79297c-0.8291,0.58447 -2.42578,0.64014 -3.39551,0.33105c-1.20264,-0.38965 -2.1958,-1.24951 -2.75391,-2.38477c-0.78223,-1.54248 -0.68701,-3.38379 0.24756,-4.83838c0.77344,-1.19238 2.30859,-1.99658 3.36621,-2.96631c1.39453,-1.27734 2.7832,-2.58984 4.08691,-3.96094c8.34229,-8.7583 14.54004,-19.33154 18.10547,-30.89062c6.12744,-19.20322 4.20703,-40.06919 -5.31885,-57.83232c-13.10742,-24.82339 -38.29687,-39.67676 -66.35449,-38.24443c-11.62852,0.5937 -20.97378,3.44312 -31.22754,8.85864c-0.09214,0.21592 0.06855,2.36089 0.08599,2.76357c0.10093,2.32456 -0.25884,4.14609 -1.20132,6.28271c-1.61675,3.70122 -4.65234,6.5981 -8.4252,8.03994c-4.34063,1.67285 -7.91528,1.0749 -12.02065,-0.72729c-1.39482,1.60225 -2.82202,3.25049 -4.10435,4.94326c-9.65625,12.99712 -14.91196,28.73716 -15.0019,44.92852c0.00176,4.17891 0.44326,7.88203 1.01514,11.99033c2.62295,0.16113 4.83779,0.89355 7.03564,2.35986c3.29355,2.22656 5.54854,5.68799 6.254,9.60059c0.80186,4.28027 -0.46406,7.95996 -2.85498,11.45801c6.1188,10.2832 12.25576,17.34961 22.19165,24.18604c2.72197,1.86182 5.57842,3.51855 8.54546,4.95703c1.91646,0.92285 5.19565,1.96143 6.36152,3.69434c0.70986,1.0708 0.95127,2.38623 0.66841,3.64014c-0.63237,2.74805 -3.42524,4.40332 -6.0498,3.42188c-16.91338,-6.33398 -30.71909,-19.39453 -39.48354,-34.77979c-1.91895,0.38965 -3.55415,0.55664 -5.51953,0.2959c-3.75059,-0.50244 -7.1439,-2.48584 -9.42217,-5.50635c-2.37363,-3.13184 -3.39595,-7.08252 -2.83887,-10.97168c0.58975,-4.17334 2.75815,-7.26123 6.04922,-9.76172c-0.84521,-4.86138 -1.40742,-9.20464 -1.51685,-14.15186c-0.4582,-20.73208 7.9248,-42.52515 22.04824,-57.59678c-0.52134,-1.40479 -0.85063,-2.36895 -1.07036,-3.8603c-0.57583,-3.9999 0.44736,-8.06558 2.8478,-11.31636c2.4063,-3.16348 5.97246,-5.23945 9.91157,-5.76943c4.11064,-0.54932 7.88833,0.62358 11.15728,3.0999c12.08159,-6.54038 21.99536,-9.5105 35.76973,-10.4395z M104.33159,64.69878c3.83057,-0.32915 7.21245,2.48599 7.58335,6.31274c0.37104,3.82676 -2.40718,7.23911 -6.22969,7.65176c-2.51001,0.271 -4.97197,-0.82749 -6.44678,-2.87651c-1.4748,-2.04902 -1.73496,-4.73247 -0.68115,-7.02656c1.05381,-2.29424 3.25898,-3.84521 5.77427,-4.06143z M77.86846,154.6333c3.67031,-0.67822 7.19575,1.74463 7.87866,5.41406c0.68291,3.66943 -1.73511,7.19824 -5.40337,7.88672c-3.67588,0.68994 -7.21377,-1.73437 -7.898,-5.41113c-0.68437,-3.67676 1.74478,-7.21143 5.42271,-7.88965z M153.91992,174.95801c3.0835,-0.47461 5.96777,1.64209 6.43945,4.72559c0.47021,3.0835 -1.64941,5.96484 -4.73437,6.43359c-3.0791,0.46582 -5.95459,-1.64941 -6.4248,-4.72852c-0.47021,-3.07764 1.64209,-5.95605 4.71973,-6.43066z"
24
+ />
25
+ </svg>
26
+ );
27
+}
28
+
29
+export default Logo;
0
30
▼src/pages/[owner]/[repo]/issues/[number].astro
+6-1
140
140
>
141
141
<RepoHeader repo={repoForHeader as any} activeTab="issues" />
142
142
143
- <div class="container py-8">
143
+ <div class="container max-w-[1280px] mx-auto py-6 px-4 sm:px-6">
144
144
<IssueDetail
145
145
client:load
146
146
issue={formattedIssue}
149
149
repoName={repoName!}
150
150
canLink={canLink}
151
151
canEdit={canLink}
152
+ currentUser={currentUser ? {
153
+ id: currentUser.id,
154
+ username: currentUser.username,
155
+ avatarUrl: currentUser.avatarUrl || undefined,
156
+ } : undefined}
152
157
/>
153
158
</div>
154
159
</BaseLayout>
155
160
▼src/pages/actions.astro
+187-186
3
3
import { getDatabase, schema } from "@/db";
4
4
import BaseLayout from "@/layouts/BaseLayout.astro";
5
5
import { desc, eq } from "drizzle-orm";
6
+import {
7
+ Play,
8
+ CheckCircle2,
9
+ XCircle,
10
+ Loader2,
11
+ Clock,
12
+ GitBranch,
13
+ GitCommit,
14
+ Bot
15
+} from "lucide-react";
6
16
7
17
const db = getDatabase();
8
18
11
21
12
22
// Fetch workflow runs from public repos
13
23
let workflowRuns: any[] = [];
24
+let successCount = 0;
25
+let failureCount = 0;
26
+let runningCount = 0;
27
+
14
28
try {
15
29
const rawRuns = await db.query.workflowRuns.findMany({
16
30
where: filter === "success"
36
50
workflowRuns = (await resolveOrgOwners(rawRuns)).filter(run =>
37
51
run.repository?.visibility === "public"
38
52
);
53
+
54
+ const allPublicRuns = await db.query.workflowRuns.findMany({
55
+ columns: { status: true },
56
+ with: {
57
+ repository: {
58
+ columns: { visibility: true }
59
+ }
60
+ }
61
+ });
62
+ const publicOnly = allPublicRuns.filter(r => r.repository?.visibility === "public");
63
+ successCount = publicOnly.filter(r => r.status === "success").length;
64
+ failureCount = publicOnly.filter(r => r.status === "failure").length;
65
+ runningCount = publicOnly.filter(r => r.status === "running").length;
39
66
} catch (e) {
40
67
console.error("Error fetching workflow runs:", e);
41
68
}
63
90
}
64
91
---
65
92
66
-<BaseLayout title="Actions - OpenCodeHub">
67
- <div class="container py-8 max-w-5xl">
93
+<BaseLayout title="Actions · OpenCodeHub">
94
+ <div class="container max-w-[1280px] mx-auto py-8 px-4 sm:px-6">
68
95
<!-- Header -->
69
- <div class="flex items-center justify-between mb-8">
96
+ <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-6 border-b border-border mb-6">
70
97
<div>
71
- <h1 class="text-3xl font-bold text-foreground flex items-center gap-3">
72
- <div class="p-2 rounded-lg bg-gradient-to-br from-orange-500/20 to-amber-500/20 border border-orange-500/30">
73
- <svg class="h-7 w-7 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
74
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
75
- </svg>
76
- </div>
77
- Actions
98
+ <h1 class="text-2xl font-bold text-foreground tracking-tight flex items-center gap-2.5">
99
+ <Play className="h-6 w-6 text-foreground fill-foreground" />
100
+ <span>Actions & Pipelines</span>
78
101
</h1>
79
- <p class="text-muted-foreground mt-2">Workflow runs across all public repositories</p>
102
+ <p class="text-sm text-muted-foreground mt-1">Global CI/CD workflow runs across all public repositories</p>
80
103
</div>
81
104
</div>
82
105
83
- <!-- Filters -->
84
- <div class="relative mb-6">
85
- <div class="absolute -inset-[1px] bg-gradient-to-r from-orange-500/20 via-transparent to-amber-500/20 rounded-xl opacity-50"></div>
86
- <div class="relative rounded-xl border border-border bg-card/80 backdrop-blur-sm p-4">
87
- <div class="flex flex-wrap items-center gap-2">
88
- <a
89
- href="/actions?filter=all"
90
- class:list={[
91
- "inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all",
92
- filter === "all"
93
- ? "bg-cyan-500/20 text-cyan-400 border border-cyan-500/30"
94
- : "text-muted-foreground hover:bg-accent"
95
- ]}
96
- >
97
- All
98
- </a>
99
- <a
100
- href="/actions?filter=success"
101
- class:list={[
102
- "inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all",
103
- filter === "success"
104
- ? "bg-green-500/20 text-green-400 border border-green-500/30"
105
- : "text-muted-foreground hover:bg-accent"
106
- ]}
107
- >
108
- <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
109
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
110
- </svg>
111
- Success
112
- </a>
113
- <a
114
- href="/actions?filter=failure"
115
- class:list={[
116
- "inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all",
117
- filter === "failure"
118
- ? "bg-red-500/20 text-red-400 border border-red-500/30"
119
- : "text-muted-foreground hover:bg-accent"
120
- ]}
121
- >
122
- <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
123
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
124
- </svg>
125
- Failed
126
- </a>
127
- <a
128
- href="/actions?filter=running"
129
- class:list={[
130
- "inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all",
131
- filter === "running"
132
- ? "bg-yellow-500/20 text-yellow-400 border border-yellow-500/30"
133
- : "text-muted-foreground hover:bg-accent"
134
- ]}
135
- >
136
- <svg class="h-4 w-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
137
- <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle>
138
- <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
139
- </svg>
140
- Running
141
- </a>
142
- </div>
143
- </div>
106
+ <!-- Filter Tabs -->
107
+ <div class="inline-flex items-center gap-1 p-1 rounded-lg border border-border/80 bg-card/60 backdrop-blur-sm mb-6 self-start">
108
+ <a
109
+ href="/actions?filter=all"
110
+ class:list={[
111
+ "inline-flex items-center gap-2 px-3 py-1.5 rounded-md text-xs sm:text-sm font-medium transition-colors",
112
+ filter === "all"
113
+ ? "bg-secondary text-foreground font-semibold shadow-sm"
114
+ : "text-muted-foreground hover:text-foreground hover:bg-muted/60"
115
+ ]}
116
+ >
117
+ <span>All</span>
118
+ </a>
119
+ <a
120
+ href="/actions?filter=success"
121
+ class:list={[
122
+ "inline-flex items-center gap-2 px-3 py-1.5 rounded-md text-xs sm:text-sm font-medium transition-colors",
123
+ filter === "success"
124
+ ? "bg-secondary text-foreground font-semibold shadow-sm"
125
+ : "text-muted-foreground hover:text-foreground hover:bg-muted/60"
126
+ ]}
127
+ >
128
+ <span class="h-2 w-2 rounded-full bg-emerald-500"></span>
129
+ <span>Success</span>
130
+ <span class="ml-0.5 rounded-full bg-muted px-1.5 py-0.2 text-[11px] font-semibold text-muted-foreground">
131
+ {successCount}
132
+ </span>
133
+ </a>
134
+ <a
135
+ href="/actions?filter=failure"
136
+ class:list={[
137
+ "inline-flex items-center gap-2 px-3 py-1.5 rounded-md text-xs sm:text-sm font-medium transition-colors",
138
+ filter === "failure"
139
+ ? "bg-secondary text-foreground font-semibold shadow-sm"
140
+ : "text-muted-foreground hover:text-foreground hover:bg-muted/60"
141
+ ]}
142
+ >
143
+ <span class="h-2 w-2 rounded-full bg-red-500"></span>
144
+ <span>Failed</span>
145
+ <span class="ml-0.5 rounded-full bg-muted px-1.5 py-0.2 text-[11px] font-semibold text-muted-foreground">
146
+ {failureCount}
147
+ </span>
148
+ </a>
149
+ <a
150
+ href="/actions?filter=running"
151
+ class:list={[
152
+ "inline-flex items-center gap-2 px-3 py-1.5 rounded-md text-xs sm:text-sm font-medium transition-colors",
153
+ filter === "running"
154
+ ? "bg-secondary text-foreground font-semibold shadow-sm"
155
+ : "text-muted-foreground hover:text-foreground hover:bg-muted/60"
156
+ ]}
157
+ >
158
+ <span class="h-2 w-2 rounded-full bg-amber-500"></span>
159
+ <span>Running</span>
160
+ <span class="ml-0.5 rounded-full bg-muted px-1.5 py-0.2 text-[11px] font-semibold text-muted-foreground">
161
+ {runningCount}
162
+ </span>
163
+ </a>
144
164
</div>
145
165
146
- <!-- Workflow Runs List -->
147
- <div class="relative">
148
- <div class="absolute -inset-[1px] bg-gradient-to-r from-orange-500/20 via-transparent to-amber-500/20 rounded-xl opacity-50"></div>
149
- <div class="relative rounded-xl border border-border bg-card/80 backdrop-blur-sm overflow-hidden">
150
- {workflowRuns.length > 0 ? (
151
- <div class="divide-y divide-border/50">
152
- {workflowRuns.map((run) => (
166
+ <!-- Workflow Runs List Card -->
167
+ <div class="rounded-xl border border-border/80 bg-card/60 backdrop-blur-sm overflow-hidden shadow-sm">
168
+ {workflowRuns.length > 0 ? (
169
+ <div class="divide-y divide-border/60">
170
+ {workflowRuns.map((run) => {
171
+ const ownerName = run.repository?.owner?.username || "unknown";
172
+ const repoName = run.repository?.name || "repo";
173
+ return (
153
174
<a
154
- href={`/${run.repository?.owner?.username}/${run.repository?.name}/actions`}
155
- class="block p-4 hover:bg-accent/50 transition-colors group"
175
+ href={`/${ownerName}/${repoName}/actions/runs/${run.id}`}
176
+ class="group flex items-start gap-4 p-4 hover:bg-accent/40 transition-colors"
156
177
>
157
- <div class="flex items-start gap-3">
158
- <!-- Status Icon -->
159
- <div class:list={[
160
- "p-2 rounded-lg mt-0.5",
161
- run.status === "success"
162
- ? "bg-green-500/10 text-green-400"
163
- : run.status === "failure"
164
- ? "bg-red-500/10 text-red-400"
165
- : run.status === "running"
166
- ? "bg-yellow-500/10 text-yellow-400"
167
- : "bg-gray-500/10 text-muted-foreground"
168
- ]}>
169
- {run.status === "success" ? (
170
- <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
171
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
172
- </svg>
173
- ) : run.status === "failure" ? (
174
- <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
175
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
176
- </svg>
177
- ) : run.status === "running" ? (
178
- <svg class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
179
- <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle>
180
- <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
181
- </svg>
182
- ) : (
183
- <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
184
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
185
- </svg>
186
- )}
187
- </div>
188
-
189
- <div class="flex-1 min-w-0">
190
- <!-- Workflow Name & Repo -->
191
- <div class="flex items-center gap-2 mb-1">
192
- <span class="text-xs text-muted-foreground font-medium">
193
- {run.repository?.owner?.username}/{run.repository?.name}
194
- </span>
178
+ <!-- Status Icon -->
179
+ <div class="mt-0.5 flex-shrink-0">
180
+ {run.status === "success" ? (
181
+ <div class="h-6 w-6 rounded-full bg-emerald-500/10 text-emerald-500 flex items-center justify-center border border-emerald-500/20" title="Workflow succeeded">
182
+ <CheckCircle2 className="h-3.5 w-3.5" />
195
183
</div>
196
- <h3 class="font-semibold text-foreground group-hover:text-orange-400 transition-colors truncate">
197
- {run.workflowName || "Workflow"}
198
- </h3>
199
-
200
- <!-- Branch & Commit -->
201
- <div class="flex items-center gap-2 mt-1.5 text-xs text-muted-foreground">
202
- {run.branch && (
203
- <span class="flex items-center gap-1">
204
- <svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
205
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path>
206
- </svg>
207
- {run.branch}
208
- </span>
209
- )}
210
- {run.commitSha && (
211
- <span class="font-mono bg-secondary px-1.5 py-0.5 rounded">
212
- {run.commitSha.slice(0, 7)}
213
- </span>
214
- )}
184
+ ) : run.status === "failure" ? (
185
+ <div class="h-6 w-6 rounded-full bg-red-500/10 text-red-400 flex items-center justify-center border border-red-500/20" title="Workflow failed">
186
+ <XCircle className="h-3.5 w-3.5" />
215
187
</div>
216
-
217
- <!-- Meta -->
218
- <div class="flex items-center gap-3 mt-2 text-xs text-muted-foreground">
219
- <span>
220
- triggered {timeAgo(run.createdAt)} by {run.triggeredBy?.username || run.event || "system"}
221
- </span>
222
- {run.startedAt && (
223
- <span class="flex items-center gap-1">
224
- <svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
225
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
226
- </svg>
227
- {formatDuration(run.startedAt, run.completedAt)}
228
- </span>
229
- )}
188
+ ) : run.status === "running" ? (
189
+ <div class="h-6 w-6 rounded-full bg-amber-500/10 text-amber-400 flex items-center justify-center border border-amber-500/20" title="Workflow running">
190
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
191
+ </div>
192
+ ) : (
193
+ <div class="h-6 w-6 rounded-full bg-secondary text-muted-foreground flex items-center justify-center border border-border">
194
+ <Clock className="h-3.5 w-3.5" />
230
195
</div>
196
+ )}
197
+ </div>
198
+
199
+ <div class="flex-1 min-w-0">
200
+ <div class="flex items-baseline gap-2 flex-wrap mb-1">
201
+ <span class="text-xs font-semibold text-muted-foreground hover:text-foreground">
202
+ {ownerName}/{repoName}
203
+ </span>
231
204
</div>
232
205
233
- <!-- Status Badge -->
234
- <div class:list={[
235
- "px-2.5 py-1 rounded-full text-xs font-medium capitalize",
236
- run.status === "success"
237
- ? "bg-green-500/10 text-green-400 border border-green-500/30"
238
- : run.status === "failure"
239
- ? "bg-red-500/10 text-red-400 border border-red-500/30"
240
- : run.status === "running"
241
- ? "bg-yellow-500/10 text-yellow-400 border border-yellow-500/30"
242
- : "bg-gray-500/10 text-muted-foreground border border-gray-500/30"
243
- ]}>
244
- {run.status}
206
+ <h3 class="text-sm font-semibold text-foreground group-hover:text-primary transition-colors leading-snug break-words">
207
+ {run.name || "CI Workflow"}
208
+ </h3>
209
+
210
+ <div class="flex items-center gap-3 mt-2 flex-wrap text-xs text-muted-foreground">
211
+ <div class="flex items-center gap-1 font-mono text-[11px] bg-secondary/80 text-foreground px-2 py-0.5 rounded border border-border/50">
212
+ <GitBranch className="h-3 w-3 text-muted-foreground" />
213
+ <span>{run.branch || "main"}</span>
214
+ </div>
215
+
216
+ {run.commitSha && (
217
+ <span class="font-mono text-[11px] text-muted-foreground">
218
+ {run.commitSha.slice(0, 7)}
219
+ </span>
220
+ )}
221
+
222
+ <span>·</span>
223
+
224
+ <span>
225
+ triggered {timeAgo(run.createdAt)} by <span class="text-foreground font-medium">@{run.triggeredBy?.username || "workflow"}</span>
226
+ </span>
227
+
228
+ <span>·</span>
229
+
230
+ <span class="flex items-center gap-1">
231
+ <Clock className="h-3 w-3 text-muted-foreground" />
232
+ {formatDuration(run.createdAt, run.completedAt)}
233
+ </span>
245
234
</div>
246
235
</div>
236
+
237
+ <!-- Author Avatar -->
238
+ <div class="hidden sm:block flex-shrink-0">
239
+ {run.triggeredBy?.avatarUrl ? (
240
+ <img
241
+ src={run.triggeredBy.avatarUrl}
242
+ alt={run.triggeredBy.username}
243
+ class="h-7 w-7 rounded-full border border-border/80 object-cover"
244
+ />
245
+ ) : (
246
+ <div class="h-7 w-7 rounded-full bg-secondary border border-border/80 flex items-center justify-center text-xs font-medium text-foreground">
247
+ {run.triggeredBy?.username?.[0]?.toUpperCase() || <Bot className="h-3.5 w-3.5 text-muted-foreground" />}
248
+ </div>
249
+ )}
250
+ </div>
247
251
</a>
248
- ))}
249
- </div>
250
- ) : (
251
- <div class="text-center py-16">
252
- <div class="relative inline-block mb-4">
253
- <svg class="h-16 w-16 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
254
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
255
- </svg>
256
- <div class="absolute inset-0 bg-orange-500 blur-2xl opacity-20"></div>
257
- </div>
258
- <h3 class="text-lg font-semibold text-foreground mb-2">No workflow runs found</h3>
259
- <p class="text-muted-foreground">
260
- {filter === "success"
261
- ? "No successful runs yet."
262
- : filter === "failure"
263
- ? "No failed runs. Great job! 🎉"
264
- : filter === "running"
265
- ? "No workflows currently running."
266
- : "No workflow runs have been triggered yet."}
267
- </p>
252
+ );
253
+ })}
254
+ </div>
255
+ ) : (
256
+ <div class="text-center py-16 px-4">
257
+ <div class="w-12 h-12 rounded-full bg-secondary/80 border border-border flex items-center justify-center mx-auto mb-3 text-muted-foreground">
258
+ <Play className="h-6 w-6" />
268
259
</div>
269
- )}
270
- </div>
260
+ <h3 class="text-base font-semibold text-foreground mb-1">No workflow runs found</h3>
261
+ <p class="text-sm text-muted-foreground max-w-sm mx-auto">
262
+ {filter === "success"
263
+ ? "No successful workflow runs found."
264
+ : filter === "failure"
265
+ ? "No failed workflow runs. Everything is green! 🎉"
266
+ : filter === "running"
267
+ ? "No workflows are currently executing."
268
+ : "No workflows have run yet."}
269
+ </p>
270
+ </div>
271
+ )}
271
272
</div>
272
273
</div>
273
274
</BaseLayout>
274
275
▼src/pages/api/repos/[owner]/[repo]/pulls/[number]/comments.ts
+10-11
13
13
import crypto from "crypto";
14
14
import { checkPathPermissions } from "@/lib/path-scoping";
15
15
import { canReadRepo, canWriteRepo } from "@/lib/permissions";
16
+import { notifyMentionedUsers } from "@/lib/mentions";
16
17
17
18
const createCommentSchema = z.object({
18
19
body: z.string().min(1),
219
220
logger.info({ userId: tokenPayload.userId, repoId: pr.repository.id, prId: pr.id, commentId }, "PR comment created");
220
221
221
222
// Notify @mentioned users (WS2-10)
222
- import("@/lib/mentions").then(({ notifyMentionedUsers }) => {
223
- notifyMentionedUsers({
224
- text: body,
225
- actorId: tokenPayload.userId,
226
- repositoryId: pr.repository.id,
227
- subjectType: "comment",
228
- subjectId: commentId,
229
- url: `/${owner}/${repo}/pulls/${number}#comment-${commentId}`,
230
- titlePrefix: "mentioned you in a comment",
231
- }).catch((err) => logger.error({ err }, "Mention notification failed"));
232
- });
223
+ notifyMentionedUsers({
224
+ text: body,
225
+ actorId: tokenPayload.userId,
226
+ repositoryId: pr.repository.id,
227
+ subjectType: "comment",
228
+ subjectId: commentId,
229
+ url: `/${owner}/${repo}/pulls/${number}#comment-${commentId}`,
230
+ titlePrefix: "mentioned you in a comment",
231
+ }).catch((err) => logger.error({ err }, "Mention notification failed"));
233
232
234
233
return success({ comment, message: "Comment created" });
235
234
});
236
235
▼src/pages/api/repos/[owner]/[repo]/pulls/[number]/reviews/batch.ts
+1-2
88
88
const createdCommentIds: string[] = [];
89
89
90
90
try {
91
- // @ts-expect-error - Drizzle multi-db union type issue
92
91
await db.insert(schema.pullRequestReviews).values({
93
92
id: reviewId,
94
93
pullRequestId: pr.id,
95
94
reviewerId: userId,
96
- state: parsed.data.state.toLowerCase(),
95
+ state: (parsed.data.state || "COMMENTED").toLowerCase(),
97
96
body: parsed.data.body || "Review submitted",
98
97
commitSha: parsed.data.commitSha ?? null,
99
98
submittedAt: now,
100
99
▼src/pages/dashboard.astro
+300-433
3
3
import { getInboxItems } from "@/lib/inbox";
4
4
import BaseLayout from "@/layouts/BaseLayout.astro";
5
5
import { and, desc, eq, inArray, or } from "drizzle-orm";
6
-// Import Shadcn UI Components (using path aliases or relative paths)
7
-// Assuming components are in @/components/ui or src/components/ui
8
6
import {
9
- Card,
10
- CardContent,
11
- CardDescription,
12
- CardFooter,
13
- CardHeader,
14
- CardTitle,
15
-} from "@/components/ui/card";
16
-import { Button } from "@/components/ui/button";
17
-import { UserAvatar } from "@/components/dashboard/UserAvatar";
18
-import { Badge } from "@/components/ui/badge";
7
+ BookMarked,
8
+ Star,
9
+ GitFork,
10
+ Plus,
11
+ Search,
12
+ GitPullRequest,
13
+ CircleDot,
14
+ Layers,
15
+ Inbox,
16
+ Clock,
17
+ Compass,
18
+ ArrowRight
19
+} from "lucide-react";
19
20
20
21
const user = Astro.locals.user;
21
22
if (!user) {
47
48
}
48
49
49
50
// Fetch Activity Feed
50
-// 1. Get list of users followed by current user
51
51
const following = await db.query.userFollowers.findMany({
52
52
where: eq(schema.userFollowers.followerId, user.id),
53
53
});
54
54
const followingIds = following.map((f) => f.followeeId);
55
55
56
-// 2. Fetch activities from followed users AND current user
57
56
const activities = await db.query.activities.findMany({
58
57
where: or(
59
58
eq(schema.activities.userId, user.id),
68
67
repository: true,
69
68
},
70
69
});
71
-
72
-// Helper for activity icons and text
73
-function getActivityIcon(type: string, action: string) {
74
- switch (type) {
75
- case "create_repo":
76
- return {
77
- icon: "M12 6v6m0 0v6m0-6h6m-6 0H6",
78
- color: "text-green-400",
79
- bg: "bg-green-500/10",
80
- };
81
- case "star":
82
- return {
83
- icon: "M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.784.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z",
84
- color: "text-yellow-400",
85
- bg: "bg-yellow-500/10",
86
- };
87
- case "fork":
88
- return {
89
- icon: "M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z",
90
- color: "text-purple-400",
91
- bg: "bg-purple-500/10",
92
- };
93
- case "follow":
94
- return {
95
- icon: "M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z",
96
- color: "text-blue-400",
97
- bg: "bg-blue-500/10",
98
- };
99
- case "issue":
100
- return {
101
- icon: "M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
102
- color: "text-orange-400",
103
- bg: "bg-orange-500/10",
104
- };
105
- case "pull_request":
106
- case "open_pr":
107
- return {
108
- icon: "M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z",
109
- color: "text-cyan-400",
110
- bg: "bg-cyan-500/10",
111
- };
112
- default:
113
- return {
114
- icon: "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
115
- color: "text-muted-foreground",
116
- bg: "bg-gray-500/10",
117
- };
118
- }
119
-}
120
-
121
-function getActivityText(activity: any) {
122
- const actor = activity.user.username;
123
- const repo = activity.repository?.name;
124
-
125
- switch (activity.type) {
126
- case "create_repo":
127
- return `created repository ${activity.repository?.name}`;
128
- case "star":
129
- return `starred ${activity.repoOwner || "someone"}/${activity.repoName || activity.repository?.name}`;
130
- case "fork":
131
- return `forked ${activity.repository?.name}`;
132
- case "follow":
133
- return `followed ${JSON.parse(activity.payload || "{}").targetUsername || "someone"}`;
134
- case "issue":
135
- return `opened issue #${JSON.parse(activity.payload || "{}").number} in ${activity.repository?.name}`;
136
- case "open_pr":
137
- return `opened PR #${JSON.parse(activity.payload || "{}").number} in ${activity.repository?.name}`;
138
- default:
139
- return `${activity.action} ${activity.type}`;
140
- }
141
-}
142
70
---
143
71
144
-<BaseLayout title="Dashboard - OpenCodeHub">
145
- <div class="container py-8 max-w-7xl mx-auto space-y-8">
146
- <!-- Welcome Header -->
147
- <div
148
- class="flex flex-col md:flex-row items-start md:items-center justify-between gap-4"
149
- >
72
+<BaseLayout title="Dashboard · OpenCodeHub">
73
+ <div class="container max-w-[1280px] mx-auto py-8 px-4 sm:px-6 space-y-8">
74
+ <!-- Top Welcome Header -->
75
+ <div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 pb-6 border-b border-border">
150
76
<div class="flex items-center gap-4">
151
- <UserAvatar
152
- user={user}
153
- className="h-16 w-16 border-2 border-border"
154
- client:load
155
- />
77
+ {user.avatarUrl ? (
78
+ <img
79
+ src={user.avatarUrl}
80
+ alt={user.username}
81
+ class="h-14 w-14 rounded-full border-2 border-border/80 object-cover flex-shrink-0 shadow-sm"
82
+ />
83
+ ) : (
84
+ <div class="h-14 w-14 rounded-full bg-secondary border-2 border-border/80 flex items-center justify-center text-xl font-bold text-foreground flex-shrink-0 shadow-sm">
85
+ {user.username[0]?.toUpperCase()}
86
+ </div>
87
+ )}
156
88
<div>
157
- <h1 class="text-3xl font-bold text-foreground tracking-tight">
89
+ <h1 class="text-2xl font-bold text-foreground tracking-tight">
158
90
Welcome back, {user.displayName || user.username}
159
91
</h1>
160
- <p class="text-muted-foreground">
161
- Manage your repositories and development workflow.
92
+ <p class="text-xs sm:text-sm text-muted-foreground mt-0.5">
93
+ Your personal hub for repositories, reviews, and activity
162
94
</p>
163
95
</div>
164
96
</div>
165
- <div class="flex items-center gap-3">
166
- <a href="/stacks">
167
- <Button
168
- variant="outline"
169
- className="border-border text-muted-foreground hover:text-foreground hover:bg-accent"
170
- >
171
- <svg
172
- class="mr-2 h-4 w-4"
173
- fill="none"
174
- stroke="currentColor"
175
- viewBox="0 0 24 24"
176
- ><path
177
- stroke-linecap="round"
178
- stroke-linejoin="round"
179
- stroke-width="2"
180
- d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
181
- ></path></svg
182
- >
183
- View Stacks
184
- </Button>
97
+
98
+ <div class="flex items-center gap-2.5 flex-wrap self-start sm:self-auto">
99
+ <a
100
+ href="/stacks"
101
+ class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md border border-border bg-card text-xs font-medium text-foreground hover:bg-accent transition-colors shadow-sm"
102
+ >
103
+ <Layers className="h-3.5 w-3.5" />
104
+ <span>Stacks</span>
185
105
</a>
186
- <a href="/new">
187
- <Button
188
- className="bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 text-foreground border-0 shadow-lg shadow-green-500/20"
189
- >
190
- <svg
191
- class="mr-2 h-4 w-4"
192
- fill="none"
193
- stroke="currentColor"
194
- viewBox="0 0 24 24"
195
- ><path
196
- stroke-linecap="round"
197
- stroke-linejoin="round"
198
- stroke-width="2"
199
- d="M12 4v16m8-8H4"></path></svg
200
- >
201
- New Repository
202
- </Button>
106
+ <a
107
+ href="/new"
108
+ class="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-md bg-[#238636] hover:bg-[#2ea043] text-white text-xs sm:text-sm font-medium transition-colors shadow-sm"
109
+ >
110
+ <Plus className="h-4 w-4" />
111
+ <span>New repository</span>
203
112
</a>
204
113
</div>
205
114
</div>
206
115
207
- <!-- Stats Grid -->
208
- <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
209
- <Card className="bg-card/50 border-border backdrop-blur-sm">
210
- <CardHeader className="pb-2">
211
- <CardTitle className="text-sm font-medium text-muted-foreground"
212
- >Total Repositories</CardTitle
213
- >
214
- </CardHeader>
215
- <CardContent>
216
- <div class="text-3xl font-bold text-foreground">{repos.length}</div>
217
- </CardContent>
218
- </Card>
219
- <Card className="bg-card/50 border-border backdrop-blur-sm">
220
- <CardHeader className="pb-2">
221
- <CardTitle className="text-sm font-medium text-muted-foreground"
222
- >Total Stars</CardTitle
223
- >
224
- </CardHeader>
225
- <CardContent>
226
- <div class="text-3xl font-bold text-yellow-400">{totalStars}</div>
227
- </CardContent>
228
- </Card>
229
- <Card className="bg-card/50 border-border backdrop-blur-sm">
230
- <CardHeader className="pb-2">
231
- <CardTitle className="text-sm font-medium text-muted-foreground"
232
- >Total Forks</CardTitle
233
- >
234
- </CardHeader>
235
- <CardContent>
236
- <div class="text-3xl font-bold text-purple-400">{totalForks}</div>
237
- </CardContent>
238
- </Card>
116
+ <!-- Quick Stats Row -->
117
+ <div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
118
+ <div class="rounded-xl border border-border/80 bg-card/60 backdrop-blur-sm p-4 shadow-sm">
119
+ <div class="flex items-center justify-between">
120
+ <span class="text-xs font-medium text-muted-foreground">Repositories</span>
121
+ <BookMarked className="h-4 w-4 text-muted-foreground" />
122
+ </div>
123
+ <div class="text-2xl font-bold text-foreground mt-2">{repos.length}</div>
124
+ </div>
125
+ <div class="rounded-xl border border-border/80 bg-card/60 backdrop-blur-sm p-4 shadow-sm">
126
+ <div class="flex items-center justify-between">
127
+ <span class="text-xs font-medium text-muted-foreground">Total Stars</span>
128
+ <Star className="h-4 w-4 text-amber-400" />
129
+ </div>
130
+ <div class="text-2xl font-bold text-foreground mt-2">{totalStars}</div>
131
+ </div>
132
+ <div class="rounded-xl border border-border/80 bg-card/60 backdrop-blur-sm p-4 shadow-sm">
133
+ <div class="flex items-center justify-between">
134
+ <span class="text-xs font-medium text-muted-foreground">Total Forks</span>
135
+ <GitFork className="h-4 w-4 text-purple-400" />
136
+ </div>
137
+ <div class="text-2xl font-bold text-foreground mt-2">{totalForks}</div>
138
+ </div>
239
139
</div>
240
140
241
141
<!-- Main Content Grid -->
242
- <div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
243
- <!-- Left Sidebar: User Profile context & Repositories -->
244
- <div class="lg:col-span-4 xl:col-span-3 space-y-6">
245
- <div class="flex items-center justify-between">
246
- <h2 class="text-lg font-semibold text-foreground flex items-center gap-2">
247
- Repositories
248
- </h2>
249
- <div class="flex items-center gap-2">
250
- <a href="/new/import" class="text-sm bg-cyan-500/20 text-cyan-400 px-2 py-1 rounded hover:bg-cyan-500/30 transition-colors">
251
- Import
252
- </a>
253
- <a href="/new" class="text-sm bg-green-500/20 text-green-400 px-2 py-1 rounded hover:bg-green-500/30 transition-colors">
254
- New
142
+ <div class="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">
143
+ <!-- Left Sidebar: Repositories list & Navigation shortcuts -->
144
+ <aside class="lg:col-span-4 space-y-6">
145
+ <!-- Repositories Card -->
146
+ <div class="rounded-xl border border-border/80 bg-card/60 backdrop-blur-sm overflow-hidden shadow-sm">
147
+ <div class="p-3.5 border-b border-border/80 bg-muted/30 flex items-center justify-between">
148
+ <h2 class="text-xs font-bold text-foreground uppercase tracking-wider">
149
+ Top Repositories
150
+ </h2>
151
+ <a
152
+ href="/new"
153
+ class="inline-flex items-center gap-1 text-xs font-medium text-primary hover:underline"
154
+ >
155
+ <Plus className="h-3 w-3" />
156
+ <span>New</span>
255
157
</a>
256
158
</div>
257
- </div>
258
-
259
- <div class="bg-card/80 border border-border rounded-xl overflow-hidden">
260
- <div class="p-3 border-b border-border bg-secondary">
261
- <input type="text" placeholder="Find a repository..." class="w-full bg-background border border-border rounded-md px-3 py-1.5 text-sm text-foreground placeholder-gray-500 focus:outline-none focus:border-cyan-500/50" />
159
+
160
+ <div class="p-3 border-b border-border/60">
161
+ <div class="relative group">
162
+ <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground group-focus-within:text-foreground" />
163
+ <input
164
+ type="text"
165
+ id="sidebar-repo-search"
166
+ placeholder="Find a repository..."
167
+ class="w-full h-8 rounded-md border border-border/70 bg-background/80 pl-8 pr-3 text-xs text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary"
168
+ />
169
+ </div>
262
170
</div>
263
- <div class="max-h-[600px] overflow-y-auto">
171
+
172
+ <div class="max-h-[380px] overflow-y-auto divide-y divide-border/40" id="sidebar-repo-list">
264
173
{repos.length > 0 ? (
265
- <ul class="divide-y divide-white/5">
266
- {repos.map((repo) => (
267
- <li>
268
- <a href={`/${user.username}/${repo.name}`} class="block p-3 hover:bg-accent transition-colors group">
269
- <div class="flex items-center gap-2 mb-1">
270
- <div class="w-4 h-4 rounded-full flex-shrink-0 flex items-center justify-center">
271
- <img src={`https://api.dicebear.com/7.x/identicon/svg?seed=${repo.ownerId}`} class="rounded-full w-4 h-4" />
272
- </div>
273
- <span class="text-sm font-medium text-blue-400 group-hover:underline truncate">{user.username}/{repo.name}</span>
274
- <Badge variant="secondary" className="ml-auto text-[10px] px-1.5 py-0 h-4 bg-transparent border-border/80 text-muted-foreground">
275
- {repo.visibility}
276
- </Badge>
277
- </div>
278
- </a>
279
- </li>
280
- ))}
281
- </ul>
174
+ repos.slice(0, 15).map((repo) => (
175
+ <a
176
+ href={`/${user.username}/${repo.name}`}
177
+ class="sidebar-repo-item flex items-center justify-between p-3 hover:bg-accent/40 transition-colors group"
178
+ data-name={repo.name.toLowerCase()}
179
+ >
180
+ <div class="flex items-center gap-2 min-w-0">
181
+ <BookMarked className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
182
+ <span class="text-xs font-medium text-foreground group-hover:text-primary transition-colors truncate">
183
+ {user.username}/<span class="font-semibold">{repo.name}</span>
184
+ </span>
185
+ </div>
186
+ <span class="rounded-full border border-border/60 px-1.5 py-0.2 text-[10px] text-muted-foreground">
187
+ {repo.visibility}
188
+ </span>
189
+ </a>
190
+ ))
282
191
) : (
283
- <div class="p-4 text-center text-sm text-muted-foreground">No repositories yet.</div>
192
+ <div class="p-6 text-center text-xs text-muted-foreground">
193
+ No repositories created yet.
194
+ </div>
284
195
)}
285
196
</div>
197
+
198
+ {repos.length > 15 && (
199
+ <div class="p-2.5 text-center border-t border-border/60 bg-muted/20">
200
+ <a href="/repositories" class="text-xs font-medium text-primary hover:underline">
201
+ View all {repos.length} repositories
202
+ </a>
203
+ </div>
204
+ )}
286
205
</div>
287
- </div>
288
206
289
- <!-- Main Feed: Activity & Inbox -->
290
- <div class="lg:col-span-8 xl:col-span-9 space-y-8">
291
-
292
- <!-- Inbox Section -->
293
- {
294
- inboxItems.length > 0 && (
295
- <div class="space-y-4">
296
- <div class="flex items-center justify-between">
297
- <h2 class="text-xl font-semibold text-foreground flex items-center gap-2">
298
- <svg
299
- class="h-5 w-5 text-yellow-400"
300
- fill="none"
301
- stroke="currentColor"
302
- viewBox="0 0 24 24"
303
- >
304
- <path
305
- stroke-linecap="round"
306
- stroke-linejoin="round"
307
- stroke-width="2"
308
- d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
309
- />
310
- </svg>
311
- Inbox
312
- </h2>
313
- <Badge
314
- variant="secondary"
315
- className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
316
- >
317
- {inboxItems.length} items
318
- </Badge>
319
- </div>
320
- <div class="grid gap-3">
321
- {inboxItems.map((item) => (
322
- <a
323
- href={`/${item.repository.owner}/${item.repository.name}/pulls/${item.number}`}
324
- class="block group"
325
- >
326
- <Card className="bg-card/80 border-border hover:border-border/80 hover:bg-accent transition-all duration-200">
327
- <CardContent className="p-4 flex items-center justify-between gap-4">
328
- <div class="flex items-center gap-4 min-w-0">
329
- <div class="flex-shrink-0">
330
- {item.type === "review_required" && (
331
- <div
332
- class="h-8 w-8 rounded-full bg-blue-500/10 flex items-center justify-center text-blue-500 border border-blue-500/20"
333
- title="Review Required"
334
- >
335
- <svg
336
- class="h-4 w-4"
337
- fill="none"
338
- stroke="currentColor"
339
- viewBox="0 0 24 24"
340
- >
341
- <path
342
- stroke-linecap="round"
343
- stroke-linejoin="round"
344
- stroke-width="2"
345
- d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
346
- />
347
- <path
348
- stroke-linecap="round"
349
- stroke-linejoin="round"
350
- stroke-width="2"
351
- d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
352
- />
353
- </svg>
354
- </div>
355
- )}
356
- {item.type === "changes_requested" && (
357
- <div
358
- class="h-8 w-8 rounded-full bg-red-500/10 flex items-center justify-center text-red-500 border border-red-500/20"
359
- title="Changes Requested"
360
- >
361
- <svg
362
- class="h-4 w-4"
363
- fill="none"
364
- stroke="currentColor"
365
- viewBox="0 0 24 24"
366
- >
367
- <path
368
- stroke-linecap="round"
369
- stroke-linejoin="round"
370
- stroke-width="2"
371
- d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
372
- />
373
- </svg>
374
- </div>
375
- )}
376
- {item.type === "assigned" && (
377
- <div
378
- class="h-8 w-8 rounded-full bg-green-500/10 flex items-center justify-center text-green-500 border border-green-500/20"
379
- title="Assigned"
380
- >
381
- <svg
382
- class="h-4 w-4"
383
- fill="none"
384
- stroke="currentColor"
385
- viewBox="0 0 24 24"
386
- >
387
- <path
388
- stroke-linecap="round"
389
- stroke-linejoin="round"
390
- stroke-width="2"
391
- d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
392
- />
393
- </svg>
394
- </div>
395
- )}
396
- </div>
397
- <div class="min-w-0">
398
- <p class="text-sm font-medium text-foreground truncate group-hover:text-cyan-400 transition-colors">
399
- {item.title}
400
- <span class="text-muted-foreground font-normal ml-2">
401
- #{item.number}
402
- </span>
403
- </p>
404
- <div class="flex items-center gap-2 text-xs text-muted-foreground mt-0.5">
405
- <span>
406
- {item.repository.owner}/{item.repository.name}
407
- </span>
408
- <span>•</span>
409
- <span>Author: {item.author.username}</span>
410
- </div>
411
- </div>
412
- </div>
413
- <div class="text-xs text-muted-foreground whitespace-nowrap flex-shrink-0">
414
- {timeAgo(item.updatedAt)}
415
- </div>
416
- </CardContent>
417
- </Card>
418
- </a>
419
- ))}
207
+ <!-- Quick Shortcuts -->
208
+ <div class="rounded-xl border border-border/80 bg-card/60 backdrop-blur-sm p-4 space-y-2 shadow-sm">
209
+ <h3 class="text-xs font-bold text-muted-foreground uppercase tracking-wider mb-2">Shortcuts</h3>
210
+ <a href="/pulls" class="flex items-center justify-between py-1.5 text-xs text-foreground hover:text-primary transition-colors">
211
+ <span class="flex items-center gap-2">
212
+ <GitPullRequest className="h-3.5 w-3.5 text-muted-foreground" />
213
+ <span>Pull Requests</span>
214
+ </span>
215
+ <ArrowRight className="h-3 w-3 text-muted-foreground" />
216
+ </a>
217
+ <a href="/issues" class="flex items-center justify-between py-1.5 text-xs text-foreground hover:text-primary transition-colors">
218
+ <span class="flex items-center gap-2">
219
+ <CircleDot className="h-3.5 w-3.5 text-muted-foreground" />
220
+ <span>Issues</span>
221
+ </span>
222
+ <ArrowRight className="h-3 w-3 text-muted-foreground" />
223
+ </a>
224
+ <a href="/stars" class="flex items-center justify-between py-1.5 text-xs text-foreground hover:text-primary transition-colors">
225
+ <span class="flex items-center gap-2">
226
+ <Star className="h-3.5 w-3.5 text-muted-foreground" />
227
+ <span>Your Stars</span>
228
+ </span>
229
+ <ArrowRight className="h-3 w-3 text-muted-foreground" />
230
+ </a>
231
+ <a href="/explore" class="flex items-center justify-between py-1.5 text-xs text-foreground hover:text-primary transition-colors">
232
+ <span class="flex items-center gap-2">
233
+ <Compass className="h-3.5 w-3.5 text-muted-foreground" />
234
+ <span>Explore Repositories</span>
235
+ </span>
236
+ <ArrowRight className="h-3 w-3 text-muted-foreground" />
237
+ </a>
238
+ </div>
239
+ </aside>
240
+
241
+ <!-- Main Column: Activity & Inbox -->
242
+ <main class="lg:col-span-8 space-y-6">
243
+ <!-- Inbox Section (if any) -->
244
+ {inboxItems.length > 0 && (
245
+ <div class="rounded-xl border border-border/80 bg-card/60 backdrop-blur-sm overflow-hidden shadow-sm">
246
+ <div class="p-4 border-b border-border/80 bg-muted/30 flex items-center justify-between">
247
+ <div class="flex items-center gap-2">
248
+ <Inbox className="h-4 w-4 text-foreground" />
249
+ <h2 class="text-sm font-bold text-foreground">Review Inbox</h2>
250
+ <span class="rounded-full bg-primary/10 border border-primary/20 px-2 py-0.2 text-xs font-semibold text-primary">
251
+ {inboxItems.length}
252
+ </span>
420
253
</div>
421
254
</div>
422
- )
423
- }
424
-
425
- <!-- Recent Activity Feed -->
426
- <div class="space-y-4">
427
- <h2 class="text-xl font-semibold text-foreground flex items-center gap-2">
428
- Activity Feed
429
- </h2>
430
- <Card className="bg-card/80 border-border backdrop-blur-sm">
431
- <CardContent className="p-0">
432
- {
433
- activities.length > 0 ? (
434
- <div class="divide-y divide-white/5">
435
- {activities.map((activity) => {
436
- const style = getActivityIcon(
437
- activity.type,
438
- activity.action,
439
- );
440
- const payload = activity.payload
441
- ? JSON.parse(activity.payload)
442
- : {};
443
- return (
444
- <div class="flex gap-4 p-5 hover:bg-accent/50 transition-colors">
445
- <UserAvatar user={activity.user} className="w-8 h-8" client:load />
446
- <div class="min-w-0 flex-1">
447
- <p class="text-sm text-muted-foreground">
448
- <a
449
- href={`/${activity.user.username}`}
450
- class="font-medium text-foreground hover:text-blue-400 hover:underline transition-colors"
451
- >
452
- {activity.user.username}
453
- </a>{" "}
454
- <span class="text-muted-foreground">
455
- {(() => {
456
- switch (activity.type) {
457
- case "create_repo":
458
- return "created a repository";
459
- case "star":
460
- return "starred";
461
- case "fork":
462
- return "forked";
463
- case "follow":
464
- return "started following";
465
- case "issue":
466
- return `opened issue #${payload.number || ""} in`;
467
- case "open_pr":
468
- return `opened pull request #${payload.number || ""} in`;
469
- default:
470
- return `${activity.action} ${activity.type}`;
471
- }
472
- })()}
473
- </span>{" "}
474
- {activity.repository && (
475
- <a
476
- href={`/${activity.repository.ownerId}/${activity.repository.name}`}
477
- class="font-medium text-foreground hover:text-blue-400 hover:underline transition-colors"
478
- >
479
- {activity.repository.name}
480
- </a>
481
- )}
482
- {!activity.repository &&
483
- activity.type === "follow" && (
484
- <a
485
- href={`/${payload.targetUsername}`}
486
- class="font-medium text-foreground hover:text-blue-400 hover:underline transition-colors"
487
- >
488
- {payload.targetUsername}
489
- </a>
490
- )}
491
- </p>
492
- <p class="text-xs text-muted-foreground mt-1">
493
- {timeAgo(activity.createdAt)}
494
- </p>
495
- </div>
496
- </div>
497
- );
498
- })}
255
+ <div class="divide-y divide-border/60">
256
+ {inboxItems.map((item) => (
257
+ <a
258
+ href={`/${item.repository.owner}/${item.repository.name}/pulls/${item.number}`}
259
+ class="flex items-center justify-between gap-4 p-4 hover:bg-accent/40 transition-colors group"
260
+ >
261
+ <div class="min-w-0">
262
+ <div class="flex items-center gap-2 mb-1">
263
+ <span class="text-xs font-semibold text-muted-foreground">
264
+ {item.repository.owner}/{item.repository.name}
265
+ </span>
266
+ <span class="text-xs text-muted-foreground font-mono">#{item.number}</span>
267
+ </div>
268
+ <h3 class="text-sm font-semibold text-foreground group-hover:text-primary transition-colors truncate">
269
+ {item.title}
270
+ </h3>
499
271
</div>
500
- ) : (
501
- <div class="text-center py-12">
502
- <h3 class="text-lg font-medium text-foreground mb-2">Discover interesting projects</h3>
503
- <p class="text-muted-foreground mb-6 text-sm">Follow users and star repositories to see activity here.</p>
504
- <a href="/explore">
505
- <Button variant="outline" className="border-border text-muted-foreground">
506
- Explore OpenCodeHub
507
- </Button>
508
- </a>
272
+ <span class="text-xs text-muted-foreground flex-shrink-0">
273
+ {timeAgo(item.updatedAt)}
274
+ </span>
275
+ </a>
276
+ ))}
277
+ </div>
278
+ </div>
279
+ )}
280
+
281
+ <!-- Activity Feed Section -->
282
+ <div class="rounded-xl border border-border/80 bg-card/60 backdrop-blur-sm overflow-hidden shadow-sm">
283
+ <div class="p-4 border-b border-border/80 bg-muted/30 flex items-center justify-between">
284
+ <div class="flex items-center gap-2">
285
+ <Clock className="h-4 w-4 text-foreground" />
286
+ <h2 class="text-sm font-bold text-foreground">Recent Activity</h2>
287
+ </div>
288
+ <a href="/explore" class="text-xs font-medium text-primary hover:underline">
289
+ Explore feed
290
+ </a>
291
+ </div>
292
+
293
+ {activities.length > 0 ? (
294
+ <div class="divide-y divide-border/60">
295
+ {activities.map((activity) => {
296
+ const payload = activity.payload ? JSON.parse(activity.payload) : {};
297
+ return (
298
+ <div class="flex items-start gap-3.5 p-4 hover:bg-accent/40 transition-colors">
299
+ {activity.user?.avatarUrl ? (
300
+ <img
301
+ src={activity.user.avatarUrl}
302
+ alt={activity.user.username}
303
+ class="h-7 w-7 rounded-full border border-border object-cover flex-shrink-0 mt-0.5"
304
+ />
305
+ ) : (
306
+ <div class="h-7 w-7 rounded-full bg-secondary border border-border flex items-center justify-center text-xs font-semibold text-foreground flex-shrink-0 mt-0.5">
307
+ {activity.user?.username?.[0]?.toUpperCase() || "?"}
308
+ </div>
309
+ )}
310
+ <div class="flex-1 min-w-0">
311
+ <p class="text-xs sm:text-sm text-muted-foreground leading-snug">
312
+ <a
313
+ href={`/${activity.user?.username}`}
314
+ class="font-semibold text-foreground hover:text-primary transition-colors"
315
+ >
316
+ @{activity.user?.username}
317
+ </a>{" "}
318
+ <span>
319
+ {activity.type === "create_repo" && "created repository"}
320
+ {activity.type === "star" && "starred"}
321
+ {activity.type === "fork" && "forked"}
322
+ {activity.type === "follow" && "started following"}
323
+ {activity.type === "issue" && `opened issue #${payload.number || ""} in`}
324
+ {activity.type === "open_pr" && `opened pull request #${payload.number || ""} in`}
325
+ {!["create_repo", "star", "fork", "follow", "issue", "open_pr"].includes(activity.type) && `${activity.action || ""} ${activity.type}`}
326
+ </span>{" "}
327
+ {activity.repository && (
328
+ <a
329
+ href={`/${activity.repository.ownerId || activity.user?.username}/${activity.repository.name}`}
330
+ class="font-semibold text-foreground hover:text-primary transition-colors"
331
+ >
332
+ {activity.repository.name}
333
+ </a>
334
+ )}
335
+ {!activity.repository && activity.type === "follow" && (
336
+ <a
337
+ href={`/${payload.targetUsername}`}
338
+ class="font-semibold text-foreground hover:text-primary transition-colors"
339
+ >
340
+ @{payload.targetUsername}
341
+ </a>
342
+ )}
343
+ </p>
344
+ <p class="text-[11px] text-muted-foreground mt-1">
345
+ {timeAgo(activity.createdAt)}
346
+ </p>
347
+ </div>
509
348
</div>
510
- )
511
- }
512
- </CardContent>
513
- </Card>
349
+ );
350
+ })}
351
+ </div>
352
+ ) : (
353
+ <div class="text-center py-16 px-4">
354
+ <div class="w-12 h-12 rounded-full bg-secondary/80 border border-border flex items-center justify-center mx-auto mb-3 text-muted-foreground">
355
+ <Compass className="h-6 w-6" />
356
+ </div>
357
+ <h3 class="text-base font-semibold text-foreground mb-1">No recent activity</h3>
358
+ <p class="text-sm text-muted-foreground max-w-sm mx-auto mb-6">
359
+ Follow developers and star repositories to see updates on your timeline.
360
+ </p>
361
+ <a
362
+ href="/explore"
363
+ class="inline-flex items-center gap-2 px-4 py-2 rounded-md bg-foreground text-background text-sm font-medium hover:bg-foreground/90 transition-colors shadow-sm"
364
+ >
365
+ <Compass className="h-4 w-4" />
366
+ Explore community
367
+ </a>
368
+ </div>
369
+ )}
514
370
</div>
515
- </div>
371
+ </main>
516
372
</div>
517
373
</div>
518
374
</BaseLayout>
521
377
import { realtime } from "@/lib/realtime-client";
522
378
import { toast } from "sonner";
523
379
380
+ // Repos search
381
+ const searchInput = document.getElementById("sidebar-repo-search") as HTMLInputElement;
382
+ const repoItems = document.querySelectorAll(".sidebar-repo-item");
383
+ searchInput?.addEventListener("input", () => {
384
+ const q = searchInput.value.toLowerCase().trim();
385
+ repoItems.forEach((item) => {
386
+ const el = item as HTMLElement;
387
+ const name = el.dataset.name || "";
388
+ if (!q || name.includes(q)) {
389
+ el.style.display = "flex";
390
+ } else {
391
+ el.style.display = "none";
392
+ }
393
+ });
394
+ });
395
+
524
396
// Connect to realtime events
525
397
realtime.connect();
526
398
527
- // Listen for inbox updates
528
399
realtime.on("inbox:refresh", (data: any) => {
529
- // Only show if connected message passed explicitly or if it's a real update
530
400
if (data.connected) return;
531
-
532
401
toast.message("Inbox Updated", {
533
402
description: "New items available in your inbox.",
534
403
action: {
538
407
});
539
408
});
540
409
541
- // Listen for PR updates
542
410
realtime.on("pr:updated", (data: any) => {
543
411
toast.info(`PR Updated: #${data.prNumber} ${data.title}`, {
544
412
description: "Pull request has been updated.",
545
413
});
546
414
});
547
415
548
- // Listen for notifications
549
416
realtime.on("notification:new", (data: any) => {
550
417
toast.success(data.title, {
551
418
description: data.body,
552
419