diff --git a/.prettierignore b/.prettierignore index 02eb652a..6a067646 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,5 @@ _bmad/ _bmad-output/ .claude/ -CHANGELOG.md \ No newline at end of file +CHANGELOG.md +web/ \ No newline at end of file diff --git a/README.md b/README.md index eb7fb02c..a991d702 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,18 @@ A VS Code extension that acts as a real-time GPS for BMAD V6 projects. It monitors workflow artifacts, tracks sprint progress, and recommends next actions — all without leaving the editor. +## Repository structure + +This repository hosts two BMAD UI surfaces, intentionally side-by-side +without a workspace orchestrator: + +- **`src/`** — The VS Code extension documented below (BMAD Dashboard inside the IDE). +- **`web/`** — A standalone Next.js dashboard (`MyBMAD`) for visualizing BMAD projects from GitHub repositories or local folders, in any browser. See [`web/README.md`](./web/README.md) for its own setup and feature list. + +Each subproject keeps its own `package.json`, lockfile, configs, and tests. Running `pnpm install` at the root installs **only** the extension. To work on the web dashboard, `cd web && pnpm install` separately. Tests, type-check, build, and lint run independently per subproject. + +Both subprojects are MIT-licensed. + ## Features ### Sidebar Dashboard diff --git a/eslint.config.mjs b/eslint.config.mjs index 601707f5..9df4699b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -191,6 +191,6 @@ export default defineConfig( // IGNORES // ============================================================================ { - ignores: ['out/', 'node_modules/', '.vscode-test/', 'coverage/', '*.js', '*.mjs'], + ignores: ['out/', 'node_modules/', '.vscode-test/', 'coverage/', '*.js', '*.mjs', 'web/**'], } ); diff --git a/web/.dockerignore b/web/.dockerignore new file mode 100644 index 00000000..42e7f119 --- /dev/null +++ b/web/.dockerignore @@ -0,0 +1,17 @@ +node_modules +.next +.git + +# Fichiers d'environnement — ne pas copier les secrets dans l'image +.env +.env.* + +# Artefacts de projet non necessaires dans l'image +_bmad-output +_bmad +docs + +# Fichiers markdown — non necessaires dans l'image de production +# Seul README.md est conserve +*.md +!README.md diff --git a/web/.env.example b/web/.env.example new file mode 100644 index 00000000..633d7701 --- /dev/null +++ b/web/.env.example @@ -0,0 +1,99 @@ +# ============================================================================= +# MyBMAD Dashboard — Environment Variables +# ============================================================================= +# Copy this file to .env and fill in your values: +# cp .env.example .env +# +# Or use the setup script to auto-generate secrets: +# bash scripts/setup.sh +# +# Never commit .env or .env.local to version control. +# ============================================================================= + +# --- Database --- +# PostgreSQL connection string. +# If using the included docker-compose.yml, the default below works out of the box. +# Format: postgresql://:@:/ +DATABASE_URL=postgresql://bmad:bmad_dev_password@localhost:5433/bmad_dashboard + +# --- Authentication (Better Auth) --- +# Random secret used to sign session tokens. Must be a long random string. +# Generate with: openssl rand -base64 32 +BETTER_AUTH_SECRET= + +# Base URL where the app is running. +# For local development, use http://localhost:3000 +BETTER_AUTH_URL=http://localhost:3000 + +# --- Registration Control --- +# Allow new users to sign up via email/password (default: false). +# For first deployment: set to true, create your account, then disable. +# Or use: pnpm db:create-admin --email admin@example.com --password secret --name Admin +# ALLOW_REGISTRATION=true + +# --- Local Filesystem (self-hosted only) --- +# Enable local folder imports. Only works when the Next.js server runs on the +# same machine as the user's files (self-hosted deployment). +# ENABLE_LOCAL_FS=true + +# --- GitHub OAuth App (optional — omit to disable GitHub login) --- +# Required for "Login with GitHub". Create an OAuth App at: +# https://github.com/settings/developers → "New OAuth App" +# +# Application name: MyBMAD (or anything you like) +# Homepage URL: http://localhost:3000 +# Authorization callback: http://localhost:3000/api/auth/callback/github +# +# After creating the app, copy the Client ID and generate a Client Secret. +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= + +# --- GitHub Personal Access Token (optional) --- +# Without a PAT, GitHub API allows 60 requests/hour. +# With a PAT, you get 5,000 requests/hour — highly recommended. +# +# Create one at: https://github.com/settings/tokens → "Generate new token (classic)" +# Required scopes: +# - "public_repo" for public repositories only +# - "repo" for private repositories +GITHUB_PAT= + +# --- Cache Revalidation --- +# Random secret to protect the /api/revalidate endpoint. +# Generate with: openssl rand -hex 32 +REVALIDATE_SECRET= + +# --- Session (optional, defaults shown) --- +# SESSION_EXPIRES_IN=604800 # 7 days in seconds +# SESSION_UPDATE_AGE=86400 # 1 day in seconds + +# ============================================================================= +# PRODUCTION DEPLOYMENT (docker/docker-compose.prod.yml) +# ============================================================================= +# In production, TWO environment files are used: +# +# .env — Docker Compose interpolation (parse time) +# Variables used by Docker Compose: DOMAIN, ACME_EMAIL, POSTGRES_* +# +# .env.local — Runtime variables injected into the Next.js container +# Contains: BETTER_AUTH_SECRET, BETTER_AUTH_URL, GITHUB_CLIENT_ID, +# GITHUB_CLIENT_SECRET, DATABASE_URL (production) +# (Copy the top variables from this file into .env.local with production values) +# ============================================================================= + +# Domain for Traefik routing and Let's Encrypt certificates +# DOMAIN=yourdomain.example.com + +# Email for Let's Encrypt certificate notifications +# ACME_EMAIL=admin@example.com + +# PostgreSQL credentials (used by both the postgres service AND DATABASE_URL) +# POSTGRES_DB=bmad_dashboard +# POSTGRES_USER=bmad +# POSTGRES_PASSWORD=change_me_in_production + +# Production DATABASE_URL (uses internal Docker network, port 5432) +# DATABASE_URL=postgresql://bmad:change_me_in_production@postgres:5432/bmad_dashboard + +# BETTER_AUTH_URL must match your production domain +# BETTER_AUTH_URL=https://yourdomain.example.com diff --git a/web/.github/ISSUE_TEMPLATE/bug_report.md b/web/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..2bff0f4d --- /dev/null +++ b/web/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,27 @@ +--- +name: Bug Report +about: Create a report to help us improve +title: "[BUG] " +labels: bug +assignees: '' +--- + +## Description +A clear description of the bug. + +## Steps to Reproduce +1. Go to '...' +2. Click on '...' +3. See error + +## Expected Behavior +What you expected to happen. + +## Actual Behavior +What actually happened. + +## Environment +- OS: [e.g. macOS 14] +- Browser: [e.g. Chrome 120] +- Docker version: [if applicable] +- Node version: [e.g. 20.x] diff --git a/web/.github/ISSUE_TEMPLATE/config.yml b/web/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..cf8afa85 --- /dev/null +++ b/web/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Security Vulnerability + url: https://github.com/DevHDI/my-bmad/security/advisories/new + about: Please report security vulnerabilities via GitHub Security Advisories diff --git a/web/.github/ISSUE_TEMPLATE/feature_request.md b/web/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..fff59a64 --- /dev/null +++ b/web/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature Request +about: Suggest an idea for this project +title: "[FEAT] " +labels: enhancement +assignees: '' +--- + +## Problem +A clear description of the problem or limitation. + +## Proposed Solution +Describe the solution you'd like. + +## Alternatives +Other solutions you've considered. + +## Additional Context +Any other context or screenshots. diff --git a/web/.github/PULL_REQUEST_TEMPLATE.md b/web/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..4588218d --- /dev/null +++ b/web/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,31 @@ +## Summary + +Briefly describe what this PR does and why. + +Closes # + +## Type of Change + +- [ ] Bug fix +- [ ] New feature +- [ ] Refactoring (no behavior change) +- [ ] Documentation update +- [ ] Dependency update + +## Changes Made + +- +- + +## Testing + +Describe how you tested this change: + +- [ ] `pnpm lint` passes +- [ ] `pnpm test` passes +- [ ] `pnpm build` succeeds +- [ ] Tested manually in browser + +## Screenshots (if UI change) + + diff --git a/web/.github/workflows/ci.yml b/web/.github/workflows/ci.yml new file mode 100644 index 00000000..fc9f20ce --- /dev/null +++ b/web/.github/workflows/ci.yml @@ -0,0 +1,77 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + lint-and-test: + name: Lint & Test + runs-on: ubuntu-latest + env: + DATABASE_URL: postgresql://placeholder:placeholder@localhost:5432/placeholder + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Generate Prisma client + run: pnpm db:generate + + - name: Lint + run: pnpm lint + + - name: Run tests + run: pnpm test + + build: + name: Build + runs-on: ubuntu-latest + needs: lint-and-test + env: + DATABASE_URL: postgresql://placeholder:placeholder@localhost:5432/placeholder + BETTER_AUTH_SECRET: ci-placeholder-secret-do-not-use + BETTER_AUTH_URL: http://localhost:3000 + GITHUB_CLIENT_ID: placeholder + GITHUB_CLIENT_SECRET: placeholder + REVALIDATE_SECRET: placeholder + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Generate Prisma client + run: pnpm db:generate + + - name: Build + run: pnpm build diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 00000000..0aaf296c --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,61 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* +!.env.example + +# Claude Code / MCP configuration (IDE-specific, not needed for contributors) +.mcp.json +/.claude/ +/.agents/ +skills-lock.json + +# playwright mcp screenshots +.playwright-mcp/ + +# BMAD methodology system and planning artifacts (local use only) +/_bmad/ +/_bmad-output/ + +# runtime data +/data/ + +# prisma generated client +/src/generated/ + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/web/.vscode/settings.json b/web/.vscode/settings.json new file mode 100644 index 00000000..8ef924f8 --- /dev/null +++ b/web/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "css.customData": [], + "css.lint.unknownAtRules": "ignore" +} diff --git a/web/CHANGELOG.md b/web/CHANGELOG.md new file mode 100644 index 00000000..6f9ab904 --- /dev/null +++ b/web/CHANGELOG.md @@ -0,0 +1,38 @@ +# Changelog + +All notable changes to MyBMAD Dashboard are documented here. + +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +--- + +## [Unreleased] + +### Added +- Open-source release preparation (LICENSE, CONTRIBUTING, SECURITY, README) + +--- + +## [0.1.0] — 2026-02-18 + +Initial public release. + +### Added +- GitHub OAuth authentication via Better Auth +- Import GitHub repositories structured with the BMAD methodology +- Dashboard overview with global stats across all projects +- Per-project views: epics timeline, stories table, docs browser, sprint status +- Velocity metrics and progress tracking +- Markdown/YAML/JSON document viewer with syntax highlighting +- Admin panel for user and role management +- Docker + Traefik production deployment with automatic TLS +- Health check API endpoint (`/api/health`) +- Cache revalidation webhook (`/api/revalidate`) +- Dark/light theme support +- Graceful error handling for parse errors in BMAD files + +--- + +[Unreleased]: https://github.com/DevHDI/my-bmad/compare/v0.1.0...HEAD +[0.1.0]: https://github.com/DevHDI/my-bmad/releases/tag/v0.1.0 diff --git a/web/CLAUDE.md b/web/CLAUDE.md new file mode 100644 index 00000000..0ab08bc1 --- /dev/null +++ b/web/CLAUDE.md @@ -0,0 +1,55 @@ +# MyBMAD Dashboard + +## Tailwind CSS + +- Uses Tailwind CSS v4 with `@theme inline` in `src/app/globals.css` (no tailwind.config file) +- **Never use arbitrary bracket values** when a canonical Tailwind class exists: + - Spacing/sizing: divide px by 4 (e.g. `160px` → `w-40`, `15px` → `mt-3.75`, `2px` → `h-0.5`) + - Ring width: `ring-3` not `ring-[3px]` + - Percentages: fraction notation (`top-1/2` not `top-[50%]`) + - Rem: convert to spacing scale (`8rem` = 128px / 4 = `min-w-32`) +- Arbitrary values are OK only for: calc expressions, non-standard values, CSS variable references +- Repeated hex colors should be added as theme tokens in `globals.css` rather than as inline arbitrary values +- shadcn UI components live in `src/components/ui/` and are project-owned — they can and should follow these conventions + +## Error Handling Pattern + +All server actions return `ActionResult`: + +```typescript +type ActionResult = { success: true; data: T } | { success: false; error: string; code?: string } +``` + +Always use `sanitizeError()` from `@/lib/errors` to sanitize error messages before returning them to clients. Never expose `error.message` directly. + +```typescript +import { sanitizeError } from "@/lib/errors"; +// ... +} catch (error) { + return { success: false, error: sanitizeError(error, "DB_ERROR"), code: "DB_ERROR" }; +} +``` + +## Server Actions Conventions + +- Always validate input with Zod at the top of the action +- Use `requireAdmin()` from `@/lib/db/helpers` for admin-only actions +- Call `revalidatePath()` or `revalidateTag()` after mutations +- Return `ActionResult` shape consistently +- Actions live in `src/actions/` + +## Database Migrations + +- **Development:** `pnpm prisma migrate dev --name ` +- **Production:** `pnpm prisma migrate deploy` (never use `migrate dev` in production) +- **After schema changes:** `pnpm prisma generate` to regenerate the client + +## Testing + +- **Framework:** Vitest +- **Run tests:** `pnpm test` +- **Watch mode:** `pnpm test:watch` +- **Test locations:** + - BMAD parsers: `src/lib/bmad/__tests__/` + - Middleware: `src/middleware.test.ts` + - Utilities: `src/lib/__tests__/` diff --git a/web/CODE_OF_CONDUCT.md b/web/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..529cf23c --- /dev/null +++ b/web/CODE_OF_CONDUCT.md @@ -0,0 +1,41 @@ +# Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes +- Focusing on what is best not just for us as individuals, but for the overall community + +Examples of unacceptable behavior: + +- The use of sexualized language or imagery, and sexual attention or advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at the contact listed in the repository. All complaints will be reviewed and investigated promptly and fairly. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1. diff --git a/web/CONTRIBUTING.md b/web/CONTRIBUTING.md new file mode 100644 index 00000000..7819d1cb --- /dev/null +++ b/web/CONTRIBUTING.md @@ -0,0 +1,169 @@ +# Contributing to MyBMAD Dashboard + +Thank you for your interest in contributing! This document explains how to get started. + +--- + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [How to Report a Bug](#how-to-report-a-bug) +- [How to Request a Feature](#how-to-request-a-feature) +- [Development Setup](#development-setup) +- [Submitting a Pull Request](#submitting-a-pull-request) +- [Coding Conventions](#coding-conventions) +- [Commit Messages](#commit-messages) + +--- + +## Code of Conduct + +This project follows a [Code of Conduct](./CODE_OF_CONDUCT.md). By participating, you agree to uphold it. + +--- + +## How to Report a Bug + +1. Check the [existing issues](https://github.com/DevHDI/my-bmad/issues) to avoid duplicates +2. Open a new issue using the **Bug Report** template +3. Include: steps to reproduce, expected behavior, actual behavior, environment details + +--- + +## How to Request a Feature + +1. Check if it has been requested already +2. Open an issue using the **Feature Request** template +3. Describe the use case and why it would be valuable + +--- + +## Development Setup + +### Requirements + +- Node.js 20+ +- pnpm 9+ +- PostgreSQL 15+ (or Docker) + +### Steps + +```bash +# 1. Fork and clone +git clone https://github.com/YOUR_USERNAME/my-bmad.git +cd my-bmad + +# 2. Install dependencies +pnpm install + +# 3. Configure environment +cp .env.example .env +# Edit .env with your values + +# 4. Start database (if using Docker) +docker compose up -d postgres + +# 5. Run migrations +pnpm db:migrate + +# 6. Start dev server +pnpm dev +``` + +--- + +## Submitting a Pull Request + +1. **Fork** the repository and create a branch from `main`: + ```bash + git checkout -b feat/your-feature-name + ``` + +2. **Make your changes** — keep them focused on a single concern + +3. **Run checks** before submitting: + ```bash + pnpm lint + pnpm test + pnpm build + ``` + +4. **Write a clear commit message** (see below) + +5. **Open a Pull Request** against `main` with: + - A clear title and description + - Reference to any related issue (`Closes #123`) + - Screenshots if the change affects the UI + +### PR Guidelines + +- Keep PRs small and focused — one feature or fix per PR +- Add tests for new functionality where applicable +- Do not change unrelated code in the same PR +- Update documentation if your change affects behavior + +--- + +## Coding Conventions + +### TypeScript + +- Use TypeScript strictly — no `any` unless absolutely necessary +- Prefer explicit types over inference for function signatures + +### Tailwind CSS + +This project uses **Tailwind CSS v4** with `@theme inline` in `src/app/globals.css`. + +- **Never use arbitrary bracket values** when a canonical Tailwind class exists: + - Spacing: `160px` → `w-40`, `15px` → `mt-3.75` + - Ring width: `ring-3` not `ring-[3px]` + - Percentages: `top-1/2` not `top-[50%]` +- Arbitrary values are OK for: `calc()` expressions, CSS variable references, non-standard values +- Repeated hex colors should be added as theme tokens in `globals.css` + +### React / Next.js + +- Use Server Components by default — add `"use client"` only when needed +- Use Server Actions for mutations (see `src/actions/`) +- Keep components small and focused + +### File Organization + +``` +src/ +├── app/ # Pages and API routes only +├── components/ # Reusable UI components +├── lib/ # Business logic, utilities, external integrations +└── actions/ # Next.js Server Actions +``` + +--- + +## Commit Messages + +Follow [Conventional Commits](https://www.conventionalcommits.org/): + +``` +type(scope): short description + +Body (optional): explain WHY not WHAT + +Closes #issue +``` + +**Types:** `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore` + +**Examples:** +``` +feat(epics): add timeline view with drag-to-collapse +fix(github): handle 403 on private repo access +docs: update deployment guide for Traefik v3 +chore(deps): upgrade Next.js to 16.2 +``` + +--- + +## License + +By contributing, you agree that your contributions will be licensed under the project's **MIT** license. diff --git a/web/Dockerfile b/web/Dockerfile new file mode 100644 index 00000000..8ca4544e --- /dev/null +++ b/web/Dockerfile @@ -0,0 +1,60 @@ +# Dockerfile — my-bmad +# Note : reste a la racine (et non docker/Dockerfile) pour simplifier le build context +FROM node:20-alpine AS base + +# Install pnpm +RUN corepack enable && corepack prepare pnpm@10.28.1 --activate + +# --- Dependencies --- +FROM base AS deps +WORKDIR /app +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +COPY prisma ./prisma +RUN pnpm install --frozen-lockfile + +# --- Build --- +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +# Valeurs factices pour le build uniquement (disparaissent apres le build) +# Evite les warnings Better Auth et Prisma pendant next build +ARG DATABASE_URL="postgresql://placeholder:placeholder@localhost:5432/placeholder" +ARG BETTER_AUTH_SECRET="build-time-placeholder-secret-not-used-at-runtime" +ARG BETTER_AUTH_URL="http://localhost:3000" +RUN pnpm db:generate +RUN pnpm build + +# --- Runner --- +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +# Copie explicite du client Prisma genere (le tracing standalone peut manquer les chemins custom) +COPY --from=builder --chown=nextjs:nodejs /app/src/generated ./src/generated +# Prisma schema + migrations for runtime migrate deploy +# Note: prisma.config.ts is NOT copied — it imports dotenv which isn't needed in production +# (env vars are injected by Docker). Without it, Prisma uses default schema discovery. +COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma +# Prisma CLI for runtime migrations — installed globally to avoid pnpm symlink issues +RUN npm install -g prisma@6.19.2 + +USER nextjs + +EXPOSE 3000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/api/health || exit 1 + +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +CMD ["sh", "-c", "prisma migrate deploy || echo 'Warning: migration skipped'; node server.js"] diff --git a/web/LICENSE b/web/LICENSE new file mode 100644 index 00000000..d3ced6fe --- /dev/null +++ b/web/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Hichem (DevHDI) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/web/README.md b/web/README.md new file mode 100644 index 00000000..b837cd5c --- /dev/null +++ b/web/README.md @@ -0,0 +1,208 @@ +# MyBMAD Dashboard + +A web dashboard - https://mybmad.hichem.cloud/ - to visualize and track [BMAD (Breakthrough Method of Agile AI-Driven Development)](https://github.com/bmad-method/bmad-method) projects from your GitHub repositories — or directly from local folders. + +> **License:** MIT — see [LICENSE](./LICENSE) for details. + +--- + +## Table of Contents + +- [What is MyBMAD Dashboard?](#what-is-mybmad-dashboard) +- [Tech Stack](#tech-stack) +- [Quick Start](#quick-start) +- [Project Structure](#project-structure) +- [Available Scripts](#available-scripts) +- [Production Deployment](#production-deployment-docker) +- [Documentation](#documentation) +- [Contributing](#contributing) +- [License](#license) + +--- + +## What is MyBMAD Dashboard? + +MyBMAD Dashboard connects to your GitHub repositories (or local folders), reads the BMAD project structure (epics, stories, sprint status, docs), and displays everything in a clean, real-time dashboard. It is designed for solo developers and small teams who use the BMAD methodology with AI coding agents. + +**Key features:** +- Import any GitHub repository that follows the BMAD structure +- **Import local folders** — no GitHub needed for self-hosted setups ([learn more](./docs/LOCAL_FOLDER.md)) +- Visualize epic progress and story status at a glance +- Support for chunked epics (individual files in `epics/` directory) as well as a single `epics.md` +- Browse BMAD docs and planning artifacts directly in the app +- Track sprint status and velocity metrics +- Repo settings modal to switch branches directly from the UI +- Email/password authentication and optional GitHub OAuth login +- Multi-user support with role management (admin / user) +- Self-hostable with Docker and automatic TLS via Traefik + +--- + +Screenshot 1 +Screenshot 2 +Screenshot 3 +Screenshot 4 +Screenshot 5 +Screenshot 6 +Screenshot 7 +Screenshot 8 + + +## Tech Stack + +| Layer | Technology | +|-------|-----------| +| Framework | [Next.js 16](https://nextjs.org) (App Router) | +| Language | TypeScript | +| Database | PostgreSQL + [Prisma](https://prisma.io) | +| Auth | [Better Auth](https://better-auth.com) (email/password + GitHub OAuth) | +| GitHub API | [@octokit/rest](https://github.com/octokit/rest.js) | +| UI | React 19 + Tailwind CSS v4 + shadcn/ui | +| Deployment | Docker + Traefik (automatic TLS) | +| Tests | [Vitest](https://vitest.dev) | + +--- + +## Quick Start + +> Full guide with all environment variables explained: **[docs/GETTING_STARTED.md](./docs/GETTING_STARTED.md)** + +```bash +git clone https://github.com/DevHDI/my-bmad.git +cd my-bmad +pnpm install + +# Auto-generate .env with secrets +bash scripts/setup.sh + +# Start PostgreSQL +docker compose up -d + +# Run migrations & create admin +pnpm db:migrate +pnpm db:create-admin --email you@example.com --password your_password --name Admin + +# Start dev server +pnpm dev +``` + +Open [http://localhost:3002](http://localhost:3002) and log in. + +--- + +## Project Structure + +``` +my-bmad/ +├── src/ +│ ├── app/ # Next.js App Router pages and API routes +│ │ ├── (dashboard)/ # Authenticated dashboard pages +│ │ │ ├── page.tsx # Home — projects overview +│ │ │ ├── repo/ # Per-repository views (epics, stories, docs) +│ │ │ ├── admin/ # Admin panel (user management) +│ │ │ └── profile/ # User profile +│ │ ├── api/ +│ │ │ ├── auth/ # Better Auth handler +│ │ │ ├── health/ # Health check endpoint +│ │ │ └── revalidate/ # Cache revalidation webhook +│ │ └── login/ # Login page +│ ├── components/ # React components +│ │ ├── dashboard/ # Dashboard-specific components +│ │ ├── docs/ # Markdown/doc viewer components +│ │ ├── epics/ # Epics & stories components +│ │ ├── layout/ # Sidebar, header, nav +│ │ └── ui/ # shadcn/ui base components +│ ├── lib/ +│ │ ├── auth/ # Better Auth configuration +│ │ ├── bmad/ # BMAD parser (reads repo structure) +│ │ ├── db/ # Prisma client and helpers +│ │ └── github/ # Octokit client with retry/throttle +│ └── actions/ # Next.js Server Actions +├── prisma/ +│ ├── schema.prisma # Database schema +│ └── migrations/ # Migration history +├── docker/ +│ ├── docker-compose.prod.yml # Production Docker Compose +│ └── DEPLOY.md # Production deployment guide +├── docs/ # Documentation +├── _bmad/ # BMAD methodology system (used to build this app) +└── _bmad-output/ # Planning artifacts generated during development +``` + +--- + +## Available Scripts + +```bash +pnpm dev # Start development server (port 3002) +pnpm build # Build for production +pnpm start # Start production server +pnpm lint # Run ESLint +pnpm test # Run tests (Vitest) +pnpm test:watch # Run tests in watch mode +pnpm db:generate # Generate Prisma client +pnpm db:migrate # Run database migrations (dev) +pnpm db:push # Push schema changes without migration +pnpm db:studio # Open Prisma Studio (database GUI) +pnpm db:create-admin # Create an admin user from the CLI +``` + +--- + +## Production Deployment (Docker) + +See the full guide in [`docker/DEPLOY.md`](./docker/DEPLOY.md). + +**Quick summary:** + +1. Clone the repo on your VPS +2. Create `.env` and `.env.local` from `.env.example` +3. Create the external Docker network: `docker network create web` +4. Launch: `docker compose --env-file .env -f docker/docker-compose.prod.yml up -d` +5. Run migrations: `docker compose ... exec my-bmad npx prisma migrate deploy` + +The stack includes: +- **Next.js** application container +- **PostgreSQL** database +- **Traefik** reverse proxy with automatic Let's Encrypt TLS + +--- + +## Documentation + +| Document | Description | +|----------|-------------| +| [Getting Started](./docs/GETTING_STARTED.md) | Full local setup guide with all environment variables | +| [Local Folder Import](./docs/LOCAL_FOLDER.md) | Import BMAD projects from the filesystem without GitHub | +| [API Endpoints](./docs/API.md) | REST API reference (revalidation, health check) | +| [Production Deployment](./docker/DEPLOY.md) | Docker + Traefik deployment guide | +| [Contributing](./CONTRIBUTING.md) | How to contribute to the project | +| [Security](./SECURITY.md) | Vulnerability reporting policy | + +--- + +## Contributing + +Contributions are welcome! Please read [CONTRIBUTING.md](./CONTRIBUTING.md) before submitting a pull request. + +--- + +## Security + +Found a vulnerability? Please read our [SECURITY.md](./SECURITY.md) and report it responsibly. + +--- + +## License + +This project is licensed under the **MIT License**. + +You are free to use, modify, and distribute this software, including for +commercial purposes, as long as the original copyright notice is preserved. +See [LICENSE](./LICENSE) for the full license text. + +--- + +## Acknowledgements + +Built with the [BMAD Method](https://github.com/bmad-method/bmad-method) — an AI-driven agile development methodology. diff --git a/web/SECURITY.md b/web/SECURITY.md new file mode 100644 index 00000000..e067f90e --- /dev/null +++ b/web/SECURITY.md @@ -0,0 +1,97 @@ +# Security Policy + +## Supported Versions + +As this project is in early development (`0.x`), only the latest version on the `main` branch receives security fixes. + +| Version | Supported | +|---------|-----------| +| `main` (latest) | Yes | +| Older releases | No | + +--- + +## Reporting a Vulnerability + +**Please do not report security vulnerabilities through public GitHub Issues.** + +If you discover a security vulnerability, please report it responsibly: + +### Option 1 — GitHub Security Advisories (Preferred) + +Use [GitHub's private vulnerability reporting](https://github.com/DevHDI/my-bmad/security/advisories/new) to report the issue confidentially. + +### Option 2 — Direct Contact + +Reach out through the repository's contact channels listed on the GitHub profile. + +--- + +## What to Include in Your Report + +Please provide as much of the following as possible: + +- A clear description of the vulnerability +- Steps to reproduce the issue +- The potential impact (what an attacker could do) +- Affected versions or components +- Any suggested fix (optional) + +--- + +## Response Process + +1. We will acknowledge receipt within **72 hours** +2. We will investigate and provide an initial assessment within **7 days** +3. We will work on a fix and coordinate disclosure with you +4. We will credit you in the release notes (unless you prefer to remain anonymous) + +--- + +## Scope + +### In Scope + +- Authentication bypass or session vulnerabilities +- SQL injection or database access issues +- GitHub token/secret exposure +- Authorization issues (accessing other users' data) +- Server-Side Request Forgery (SSRF) +- Cross-Site Scripting (XSS) in rendered content + +### Out of Scope + +- Vulnerabilities in third-party dependencies (report directly to maintainers) +- Self-inflicted issues from misconfigured deployments +- Social engineering attacks +- Rate limiting or denial of service in development environments + +--- + +## Security Best Practices for Deployment + +When self-hosting MyBMAD Dashboard: + +- **Never commit** `.env` or `.env.local` files to version control +- Use strong, randomly generated secrets for `BETTER_AUTH_SECRET` and `REVALIDATE_SECRET` +- Use environment-specific GitHub OAuth Apps (separate dev/prod apps) +- Keep your Docker images and dependencies up to date +- Restrict database access to the application container only +- Enable HTTPS (handled automatically by the Traefik setup in `docker-compose.prod.yml`) + +--- + +## Known Risks and Mitigations + +### OAuth Token Encryption at Rest + +**Status:** Not yet implemented + +**Risk:** GitHub OAuth tokens stored in the `account` table are not encrypted at rest. If an attacker gains read access to the database, they could use these tokens to access users' GitHub repositories. + +**Current mitigations:** +- Database access is restricted to the application container only (Docker network isolation) +- The application enforces session-based authentication before any token usage +- PostgreSQL connections use password authentication + +**Planned mitigation:** Implement AES-256-GCM encryption for OAuth tokens using `BETTER_AUTH_SECRET` as the key material. This requires intercepting Better Auth's account creation and token refresh flows via hooks or database triggers. diff --git a/web/components.json b/web/components.json new file mode 100644 index 00000000..0391d777 --- /dev/null +++ b/web/components.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": { + "@magicui": "https://magicui.design/r/{name}", + "@animate-ui": "https://animate-ui.com/r/{name}.json", + "@reui": "https://reui.io/r/{style}/{name}.json" + } +} diff --git a/web/docker-compose.yml b/web/docker-compose.yml new file mode 100644 index 00000000..960f1246 --- /dev/null +++ b/web/docker-compose.yml @@ -0,0 +1,28 @@ +# Development Docker Compose — PostgreSQL only +# For production with Traefik and HTTPS, use: docker compose -f docker/docker-compose.prod.yml up -d + +services: + postgres: + image: postgres:17.4-alpine + restart: unless-stopped + environment: + POSTGRES_DB: bmad_dashboard + POSTGRES_USER: bmad + POSTGRES_PASSWORD: bmad_dev_password + ports: + # DEV ONLY: exposed on host for Prisma Studio/CLI. In production, remove this + # section and let services communicate over the internal Docker network only. + - "5433:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U bmad -d bmad_dashboard"] + interval: 5s + timeout: 2s + retries: 20 + +# NOTE: In development, run the app with `pnpm dev` (hot reload, fast refresh). +# This file only provides PostgreSQL. For production, see docker/docker-compose.prod.yml. + +volumes: + postgres_data: diff --git a/web/docker/DEPLOY.md b/web/docker/DEPLOY.md new file mode 100644 index 00000000..3e243ef5 --- /dev/null +++ b/web/docker/DEPLOY.md @@ -0,0 +1,113 @@ +# Production Deployment Guide — my-bmad + +## Prerequisites + +- Docker and Docker Compose installed on the VPS +- A domain name pointing to the server's IP (DNS A record) +- Ports 80 and 443 open in the firewall + +> **IMPORTANT**: Both `.env` and `.env.local` files at the project root are **mandatory**. The Docker stack refuses to start if either one is missing. Docker Compose will fail with an error if the required variables (`DOMAIN`, `ACME_EMAIL`, `POSTGRES_*`) are not defined in `.env`. + +## 1. Prepare the environment files + +Two files are required at the project root: + +### `.env` — Docker Compose variables (parse time) + +```bash +# Domain for Traefik and Let's Encrypt +DOMAIN=mybmad.example.com +ACME_EMAIL=admin@example.com + +# PostgreSQL credentials +POSTGRES_DB=bmad_dashboard +POSTGRES_USER=bmad +POSTGRES_PASSWORD=a_strong_password_here +``` + +### `.env.local` — Next.js container runtime variables + +```bash +# Better Auth +BETTER_AUTH_SECRET=generate_with_openssl_rand_base64_32 +BETTER_AUTH_URL=https://mybmad.example.com + +# GitHub OAuth (create an app at https://github.com/settings/developers) +# Callback URL: https://mybmad.example.com/api/auth/callback/github +GITHUB_CLIENT_ID=your_client_id +GITHUB_CLIENT_SECRET=your_client_secret +``` + +## 2. Create the external Docker network + +```bash +docker network create web +``` + +This network is shared between Traefik and the application. On a multi-stack VPS, each stack can join this same network to be exposed through Traefik. + +## 3. Start the stack + +```bash +docker compose --env-file .env -f docker/docker-compose.prod.yml up -d +``` + +> **Note:** `--env-file .env` is explicit for clarity. Docker Compose loads `.env` implicitly from the working directory, but specifying it avoids any ambiguity. + +> **DATABASE_URL**: This variable is automatically built by Docker Compose in `docker-compose.prod.yml` from the `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB` variables. Do not define it in `.env.local` — this would create a conflict with the value injected by `environment:` in the compose file. + +## 3b. Run database migrations + +After the first deployment or after a Prisma schema update: + +```bash +docker compose --env-file .env -f docker/docker-compose.prod.yml exec my-bmad npx prisma migrate deploy +``` + +This command applies pending migrations to the production database. It must be run: +- **First deployment**: to create the initial tables +- **Updates**: whenever the Prisma schema changes (new field, new table, etc.) + +> **Important**: `prisma migrate deploy` is NOT destructive — it only applies pending migrations, never a reset. + +## 4. Verify the deployment + +```bash +# Check that all 3 services are running +docker compose --env-file .env -f docker/docker-compose.prod.yml ps + +# Check the health endpoint +curl https://mybmad.example.com/api/health + +# Check Traefik logs +docker compose --env-file .env -f docker/docker-compose.prod.yml logs traefik + +# Check application logs +docker compose --env-file .env -f docker/docker-compose.prod.yml logs my-bmad +``` + +## 5. Let's Encrypt Certificates + +Certificates are automatically obtained and renewed by Traefik via the TLS challenge. They are persisted in the `letsencrypt` Docker volume. + +- **First launch**: the certificate is obtained automatically (may take 1-2 min) +- **Renewal**: automatic before expiration (Traefik manages the full cycle) +- **Persistence**: certificates survive restarts thanks to the `letsencrypt` volume + +## 6. Updates + +```bash +# Rebuild and restart the application +docker compose --env-file .env -f docker/docker-compose.prod.yml up -d --build my-bmad + +# Apply migrations if the schema has changed +docker compose --env-file .env -f docker/docker-compose.prod.yml exec my-bmad npx prisma migrate deploy +``` + +## 7. Shutdown + +```bash +docker compose --env-file .env -f docker/docker-compose.prod.yml down +``` + +PostgreSQL data and Let's Encrypt certificates are preserved in Docker volumes. diff --git a/web/docker/docker-compose.prod.yml b/web/docker/docker-compose.prod.yml new file mode 100644 index 00000000..37aed6a2 --- /dev/null +++ b/web/docker/docker-compose.prod.yml @@ -0,0 +1,93 @@ +# Production Docker Compose — my-bmad +# Usage: docker compose -f docker/docker-compose.prod.yml up -d +# +# TWO environment files are required: +# +# 1. .env (project root) — Docker Compose interpolation (parse time) +# Contains: DOMAIN, ACME_EMAIL, POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD +# These variables are substituted by Docker Compose in this file (${VAR} syntax) +# +# 2. .env.local (project root) — Runtime variables for the Next.js container +# Contains: BETTER_AUTH_SECRET, BETTER_AUTH_URL, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET +# These variables are injected into the my-bmad container via env_file + +services: + # Reverse proxy with automatic HTTPS via Let's Encrypt + traefik: + image: traefik:v3.3 + restart: unless-stopped + command: + - --api.dashboard=false + - --providers.docker=true + - --providers.docker.exposedByDefault=false + - --entrypoints.web.address=:80 + - --entrypoints.websecure.address=:443 + - --entrypoints.web.http.redirections.entrypoint.to=websecure + - --entrypoints.web.http.redirections.entrypoint.scheme=https + - --certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL:?ACME_EMAIL must be set in .env} + - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json + - --certificatesresolvers.letsencrypt.acme.tlschallenge=true + ports: + - "80:80" + - "443:443" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - letsencrypt:/letsencrypt + networks: + - web + + # Next.js application + my-bmad: + build: + context: .. + dockerfile: Dockerfile + restart: unless-stopped + env_file: + - ../.env.local + environment: + - DATABASE_URL=postgresql://${POSTGRES_USER:?POSTGRES_USER must be set in .env}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in .env}@postgres:5432/${POSTGRES_DB:?POSTGRES_DB must be set in .env} + labels: + - traefik.enable=true + - traefik.docker.network=web + - traefik.http.routers.bmad.rule=Host(`${DOMAIN:?DOMAIN must be set in .env}`) + - traefik.http.routers.bmad.entrypoints=websecure + - traefik.http.routers.bmad.tls.certresolver=letsencrypt + - traefik.http.services.bmad.loadbalancer.server.port=3000 + depends_on: + postgres: + condition: service_healthy + networks: + - web + - internal + + # PostgreSQL database (internal communication only) + postgres: + image: postgres:17.4-alpine + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB:?POSTGRES_DB must be set in .env} + POSTGRES_USER: ${POSTGRES_USER:?POSTGRES_USER must be set in .env} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in .env} + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 5s + timeout: 2s + retries: 20 + networks: + - internal + +# web network: Traefik + App (proxy communication) +# internal network: App + PostgreSQL (database isolation) +# Prerequisite: create the web network before first launch: +# docker network create web +networks: + web: + external: true + internal: + internal: true + +volumes: + postgres_data: + letsencrypt: diff --git a/web/docs/API.md b/web/docs/API.md new file mode 100644 index 00000000..9298da22 --- /dev/null +++ b/web/docs/API.md @@ -0,0 +1,34 @@ +# API Endpoints + +## POST `/api/revalidate` + +Triggers cache revalidation for a specific tag. Requires authentication via the `x-revalidate-secret` header. + +**Headers:** +- `x-revalidate-secret` (required): Must match the `REVALIDATE_SECRET` environment variable + +**Body (JSON):** +```json +{ "tag": "repo-owner-name" } +``` + +**Example:** +```bash +curl -X POST https://mybmad.example.com/api/revalidate \ + -H "Content-Type: application/json" \ + -H "x-revalidate-secret: your_secret_here" \ + -d '{"tag": "repo-owner-name"}' +``` + +**Responses:** +- `200 OK` — `{ "revalidated": true, "now": }` +- `401 Unauthorized` — Invalid or missing secret +- `503 Service Unavailable` — Revalidation is disabled (no secret configured) + +## GET `/api/health` + +Health check endpoint for monitoring and load balancers. + +**Responses:** +- `200 OK` — `{ "status": "ok" }` +- `503 Service Unavailable` — `{ "status": "error" }` diff --git a/web/docs/GETTING_STARTED.md b/web/docs/GETTING_STARTED.md new file mode 100644 index 00000000..e715c876 --- /dev/null +++ b/web/docs/GETTING_STARTED.md @@ -0,0 +1,163 @@ +# Getting Started + +Step-by-step guide to run MyBMAD Dashboard locally. + +## Prerequisites + +- **Node.js** 20+ +- **pnpm** 9+ +- **Docker** (for PostgreSQL, or use your own PostgreSQL 15+ instance) +- A **GitHub OAuth App** — optional, only needed for GitHub login ([create one here](https://github.com/settings/developers)) +- A **GitHub Personal Access Token** — optional but recommended for rate limits when importing GitHub repos + +## 1. Clone the repository + +```bash +git clone https://github.com/DevHDI/my-bmad.git +cd my-bmad +``` + +## 2. Install dependencies + +```bash +pnpm install +``` + +## 3. Set up environment variables + +**Quick method** — auto-generates secrets for you: + +```bash +bash scripts/setup.sh +``` + +**Manual method** — copy the template and edit by hand: + +```bash +cp .env.example .env +``` + +Then fill in each variable as described below. + +### Database (`DATABASE_URL`) + +If you use the included Docker Compose (step 4), the default value works out of the box: + +``` +DATABASE_URL=postgresql://bmad:bmad_dev_password@localhost:5433/bmad_dashboard +``` + +The format is `postgresql://:@:/`. Adjust if you use your own PostgreSQL instance. + +### Auth secret (`BETTER_AUTH_SECRET`) + +A random string used to sign session tokens. Generate one with: + +```bash +openssl rand -base64 32 +``` + +Paste the output as the value. Any long random string works. + +### App URL (`BETTER_AUTH_URL`) + +The base URL where your app runs. For local development: + +``` +BETTER_AUTH_URL=http://localhost:3002 +``` + +In production, set this to your real domain (e.g. `https://mybmad.example.com`). + +### Revalidation secret (`REVALIDATE_SECRET`) + +A random string to protect the cache revalidation API endpoint. Generate one with: + +```bash +openssl rand -hex 32 +``` + +### GitHub OAuth App (`GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET`) + +Required for "Login with GitHub". Follow these steps: + +1. Go to [github.com/settings/developers](https://github.com/settings/developers) +2. Click **New OAuth App** +3. Fill in the form: + - **Application name:** `MyBMAD` (or anything you like) + - **Homepage URL:** `http://localhost:3002` + - **Authorization callback URL:** `http://localhost:3002/api/auth/callback/github` +4. Click **Register application** +5. Copy the **Client ID** into `GITHUB_CLIENT_ID` +6. Click **Generate a new client secret**, copy it into `GITHUB_CLIENT_SECRET` + +### GitHub Personal Access Token (`GITHUB_PAT`) — optional + +Without a PAT, the GitHub API allows only 60 requests/hour. With one, you get 5,000/hour. Recommended if you import multiple repositories. + +1. Go to [github.com/settings/tokens](https://github.com/settings/tokens) +2. Click **Generate new token (classic)** +3. Select scopes: + - `public_repo` — for public repositories only + - `repo` — if you also need private repositories +4. Copy the token into `GITHUB_PAT` + +## 4. Start PostgreSQL with Docker Compose (optional) + +If you don't have a local PostgreSQL instance: + +```bash +docker compose up -d +``` + +This starts a PostgreSQL instance on port `5433` (to avoid conflicts with a local PostgreSQL on 5432). + +## 5. Run database migrations + +```bash +pnpm db:migrate +``` + +## 6. Create your first account + +**Option A** — enable registration in `.env`, then sign up from the web UI: + +``` +ALLOW_REGISTRATION=true +``` + +After creating your account, you can set it back to `false`. + +**Option B** — create an admin directly from the command line: + +```bash +pnpm db:create-admin --email admin@example.com --password your_password --name Admin +``` + +## 7. Start the development server + +```bash +pnpm dev +``` + +Open [http://localhost:3002](http://localhost:3002) — log in and start importing repositories. + +> **Note:** The dev server runs on port **3002** (configured in `package.json`). If you set up GitHub OAuth, make sure the callback URL matches this port. + +--- + +## Environment Variables Reference + +| Variable | Required | Default / How to generate | Description | +|---|---|---|---| +| `DATABASE_URL` | Yes | `postgresql://bmad:bmad_dev_password@localhost:5433/bmad_dashboard` | PostgreSQL connection string. Works out of the box with the included Docker Compose. | +| `BETTER_AUTH_SECRET` | Yes | `openssl rand -base64 32` | Random string to sign session tokens. | +| `BETTER_AUTH_URL` | Yes | `http://localhost:3002` | Base URL where the app is running. | +| `REVALIDATE_SECRET` | Yes | `openssl rand -hex 32` | Secret to protect the cache revalidation endpoint. | +| `GITHUB_CLIENT_ID` | No | — | GitHub OAuth App Client ID. Only needed for "Login with GitHub". | +| `GITHUB_CLIENT_SECRET` | No | — | GitHub OAuth App Client Secret. | +| `GITHUB_PAT` | No | — | Personal Access Token for higher GitHub API rate limits (60 → 5,000 req/h). | +| `ALLOW_REGISTRATION` | No | `false` | Set to `true` to allow new users to sign up via email/password. | +| `ENABLE_LOCAL_FS` | No | `false` | Set to `true` to enable [local folder imports](./LOCAL_FOLDER.md). | + +> The `scripts/setup.sh` script auto-generates `BETTER_AUTH_SECRET` and `REVALIDATE_SECRET` for you. diff --git a/web/docs/LOCAL_FOLDER.md b/web/docs/LOCAL_FOLDER.md new file mode 100644 index 00000000..840d0287 --- /dev/null +++ b/web/docs/LOCAL_FOLDER.md @@ -0,0 +1,38 @@ +# Local Folder Import + +When self-hosting MyBMAD on the same machine where your BMAD projects live, you can import them directly from the filesystem — no GitHub needed. + +## Enabling + +Set the following in your `.env`: + +``` +ENABLE_LOCAL_FS=true +``` + +Restart the dev server after changing this value. + +## How it works + +1. Click **"Add a project"** in the dashboard +2. A **"Local Folder"** tab appears alongside the GitHub tab +3. Enter the **absolute path** to your project folder (e.g. `/home/user/my-project`) +4. The system validates that the folder contains a `_bmad/` or `_bmad-output/` directory +5. The project is imported and appears in your dashboard just like a GitHub repo + +Once imported, you can browse epics, stories, and docs exactly as you would with a GitHub project. Use the **Refresh** action to re-scan the folder when files change. + +## Security + +The local provider includes multiple safety guards: +- **Path traversal protection** — rejects `..`, null bytes, and special characters +- **No symlink access** — symbolic links are skipped at every level +- **File size limit** — 10 MB per file (prevents memory exhaustion) +- **File count limit** — 10,000 files max per project +- **Depth limit** — 20 directory levels max + +## Limitations + +- Only works when the Next.js server runs on the **same machine** as the project files +- No branch/version support — local folders are a live snapshot +- If you move or rename the folder, you need to re-import it diff --git a/web/docs/screen1.png b/web/docs/screen1.png new file mode 100644 index 00000000..6118d4b1 Binary files /dev/null and b/web/docs/screen1.png differ diff --git a/web/docs/screen2.png b/web/docs/screen2.png new file mode 100644 index 00000000..2dfef9ba Binary files /dev/null and b/web/docs/screen2.png differ diff --git a/web/docs/screen3.png b/web/docs/screen3.png new file mode 100644 index 00000000..9c6810b2 Binary files /dev/null and b/web/docs/screen3.png differ diff --git a/web/docs/screen4.png b/web/docs/screen4.png new file mode 100644 index 00000000..00fac8cf Binary files /dev/null and b/web/docs/screen4.png differ diff --git a/web/docs/screen5.png b/web/docs/screen5.png new file mode 100644 index 00000000..97274368 Binary files /dev/null and b/web/docs/screen5.png differ diff --git a/web/docs/screen6.png b/web/docs/screen6.png new file mode 100644 index 00000000..3cd8f61d Binary files /dev/null and b/web/docs/screen6.png differ diff --git a/web/docs/screen7.png b/web/docs/screen7.png new file mode 100644 index 00000000..c8e93f2c Binary files /dev/null and b/web/docs/screen7.png differ diff --git a/web/docs/screen8.png b/web/docs/screen8.png new file mode 100644 index 00000000..0f16a244 Binary files /dev/null and b/web/docs/screen8.png differ diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs new file mode 100644 index 00000000..3d0f7684 --- /dev/null +++ b/web/eslint.config.mjs @@ -0,0 +1,21 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + // Override default ignores of eslint-config-next. + globalIgnores([ + // Default ignores of eslint-config-next: + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + // Third-party vendored components + "src/components/animate-ui/**", + "src/components/reui/**", + ]), +]); + +export default eslintConfig; diff --git a/web/next.config.ts b/web/next.config.ts new file mode 100644 index 00000000..753c0fdd --- /dev/null +++ b/web/next.config.ts @@ -0,0 +1,28 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + output: "standalone", + images: { + unoptimized: true, + remotePatterns: [ + { + protocol: "https", + hostname: "avatars.githubusercontent.com", + }, + ], + }, + async headers() { + return [ + { + source: "/(.*)", + headers: [ + { key: "X-Frame-Options", value: "DENY" }, + { key: "X-Content-Type-Options", value: "nosniff" }, + { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, + ], + }, + ]; + }, +}; + +export default nextConfig; diff --git a/web/package.json b/web/package.json new file mode 100644 index 00000000..30a43c34 --- /dev/null +++ b/web/package.json @@ -0,0 +1,97 @@ +{ + "name": "my-bmad", + "version": "0.1.0", + "description": "A web dashboard to visualize and track BMAD (Breakthrough Method of Agile AI-Driven Development) projects from GitHub repositories.", + "license": "MIT", + "author": "Hichem (DevHDI)", + "homepage": "https://github.com/DevHDI/my-bmad", + "repository": { + "type": "git", + "url": "https://github.com/DevHDI/my-bmad.git" + }, + "bugs": { + "url": "https://github.com/DevHDI/my-bmad/issues" + }, + "keywords": [ + "bmad", + "dashboard", + "nextjs", + "github", + "agile", + "project-management", + "prisma", + "better-auth" + ], + "scripts": { + "dev": "next dev -p 3002", + "build": "next build", + "start": "next start", + "lint": "eslint", + "db:generate": "prisma generate", + "db:migrate": "prisma migrate dev", + "db:push": "prisma db push", + "db:studio": "prisma studio", + "db:create-admin": "tsx scripts/create-admin.ts", + "postinstall": "prisma generate", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@octokit/plugin-retry": "^8.0.3", + "@octokit/plugin-throttling": "^11.0.3", + "@octokit/rest": "^22.0.1", + "@prisma/client": "^6.19.2", + "@tailwindcss/typography": "^0.5.19", + "@tanstack/react-table": "^8.21.3", + "@tanstack/react-virtual": "^3.13.23", + "better-auth": "^1.4.18", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "gray-matter": "^4.0.3", + "highlight.js": "^11.11.1", + "js-yaml": "^4.1.1", + "lucide-react": "^0.563.0", + "motion": "^12.34.0", + "next": "16.1.6", + "next-themes": "^0.4.6", + "radix-ui": "^1.4.3", + "react": "19.2.3", + "react-dom": "19.2.3", + "react-markdown": "^10.1.0", + "react-use-measure": "^2.1.7", + "rehype-autolink-headings": "^7.1.0", + "rehype-highlight": "^7.0.2", + "rehype-sanitize": "^6.0.0", + "rehype-slug": "^6.0.0", + "remark-gfm": "^4.0.1", + "tailwind-merge": "^3.4.0", + "zod": "^4.3.6" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "@prisma/client", + "prisma" + ] + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/js-yaml": "^4.0.9", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "dotenv": "^17.3.1", + "eslint": "^9", + "eslint-config-next": "16.1.6", + "prisma": "^6.19.2", + "shadcn": "^3.8.4", + "tailwindcss": "^4", + "tsx": "^4.21.0", + "tw-animate-css": "^1.4.0", + "typescript": "^5", + "vitest": "^4.0.18" + } +} diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml new file mode 100644 index 00000000..4a2c754a --- /dev/null +++ b/web/pnpm-lock.yaml @@ -0,0 +1,10429 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@dnd-kit/core': + specifier: ^6.3.1 + version: 6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@dnd-kit/modifiers': + specifier: ^9.0.0 + version: 9.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + '@dnd-kit/sortable': + specifier: ^10.0.0 + version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + '@dnd-kit/utilities': + specifier: ^3.2.2 + version: 3.2.2(react@19.2.3) + '@octokit/plugin-retry': + specifier: ^8.0.3 + version: 8.0.3(@octokit/core@7.0.6) + '@octokit/plugin-throttling': + specifier: ^11.0.3 + version: 11.0.3(@octokit/core@7.0.6) + '@octokit/rest': + specifier: ^22.0.1 + version: 22.0.1 + '@prisma/client': + specifier: ^6.19.2 + version: 6.19.2(prisma@6.19.2(typescript@5.9.3))(typescript@5.9.3) + '@tailwindcss/typography': + specifier: ^0.5.19 + version: 0.5.19(tailwindcss@4.1.18) + '@tanstack/react-table': + specifier: ^8.21.3 + version: 8.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/react-virtual': + specifier: ^3.13.23 + version: 3.13.23(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + better-auth: + specifier: ^1.4.18 + version: 1.4.18(@prisma/client@6.19.2(prisma@6.19.2(typescript@5.9.3))(typescript@5.9.3))(next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(prisma@6.19.2(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@4.0.18(@types/node@20.19.33)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@20.19.33)(typescript@5.9.3))(tsx@4.21.0)) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + gray-matter: + specifier: ^4.0.3 + version: 4.0.3 + highlight.js: + specifier: ^11.11.1 + version: 11.11.1 + js-yaml: + specifier: ^4.1.1 + version: 4.1.1 + lucide-react: + specifier: ^0.563.0 + version: 0.563.0(react@19.2.3) + motion: + specifier: ^12.34.0 + version: 12.34.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: + specifier: 16.1.6 + version: 16.1.6(@babel/core@7.29.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next-themes: + specifier: ^0.4.6 + version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + radix-ui: + specifier: ^1.4.3 + version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: + specifier: 19.2.3 + version: 19.2.3 + react-dom: + specifier: 19.2.3 + version: 19.2.3(react@19.2.3) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@19.2.13)(react@19.2.3) + react-use-measure: + specifier: ^2.1.7 + version: 2.1.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rehype-autolink-headings: + specifier: ^7.1.0 + version: 7.1.0 + rehype-highlight: + specifier: ^7.0.2 + version: 7.0.2 + rehype-sanitize: + specifier: ^6.0.0 + version: 6.0.0 + rehype-slug: + specifier: ^6.0.0 + version: 6.0.0 + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 + tailwind-merge: + specifier: ^3.4.0 + version: 3.4.0 + zod: + specifier: ^4.3.6 + version: 4.3.6 + devDependencies: + '@tailwindcss/postcss': + specifier: ^4 + version: 4.1.18 + '@types/js-yaml': + specifier: ^4.0.9 + version: 4.0.9 + '@types/node': + specifier: ^20 + version: 20.19.33 + '@types/react': + specifier: ^19 + version: 19.2.13 + '@types/react-dom': + specifier: ^19 + version: 19.2.3(@types/react@19.2.13) + dotenv: + specifier: ^17.3.1 + version: 17.3.1 + eslint: + specifier: ^9 + version: 9.39.2(jiti@2.6.1) + eslint-config-next: + specifier: 16.1.6 + version: 16.1.6(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + prisma: + specifier: ^6.19.2 + version: 6.19.2(typescript@5.9.3) + shadcn: + specifier: ^3.8.4 + version: 3.8.4(@types/node@20.19.33)(typescript@5.9.3) + tailwindcss: + specifier: ^4 + version: 4.1.18 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + tw-animate-css: + specifier: ^1.4.0 + version: 1.4.0 + typescript: + specifier: ^5 + version: 5.9.3 + vitest: + specifier: ^4.0.18 + version: 4.0.18(@types/node@20.19.33)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@20.19.33)(typescript@5.9.3))(tsx@4.21.0) + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@antfu/ni@25.0.0': + resolution: {integrity: sha512-9q/yCljni37pkMr4sPrI3G4jqdIk074+iukc5aFJl7kmDCCsiJrbZ6zKxnES1Gwg+i9RcDZwvktl23puGslmvA==} + hasBin: true + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.28.6': + resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.28.5': + resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.28.6': + resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.28.6': + resolution: {integrity: sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.6': + resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-typescript@7.28.5': + resolution: {integrity: sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@better-auth/core@1.4.18': + resolution: {integrity: sha512-q+awYgC7nkLEBdx2sW0iJjkzgSHlIxGnOpsN1r/O1+a4m7osJNHtfK2mKJSL1I+GfNyIlxJF8WvD/NLuYMpmcg==} + peerDependencies: + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + better-call: 1.1.8 + jose: ^6.1.0 + kysely: ^0.28.5 + nanostores: ^1.0.1 + + '@better-auth/telemetry@1.4.18': + resolution: {integrity: sha512-e5rDF8S4j3Um/0LIVATL2in9dL4lfO2fr2v1Wio4qTMRbfxqnUDTa+6SZtwdeJrbc4O+a3c+IyIpjG9Q/6GpfQ==} + peerDependencies: + '@better-auth/core': 1.4.18 + + '@better-auth/utils@0.3.0': + resolution: {integrity: sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==} + + '@better-fetch/fetch@1.1.21': + resolution: {integrity: sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==} + + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/modifiers@9.0.0': + resolution: {integrity: sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/sortable@10.0.0': + resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + + '@dotenvx/dotenvx@1.52.0': + resolution: {integrity: sha512-CaQcc8JvtzQhUSm9877b6V4Tb7HCotkcyud9X2YwdqtQKwgljkMRwU96fVYKnzN3V0Hj74oP7Es+vZ0mS+Aa1w==} + hasBin: true + + '@ecies/ciphers@0.2.5': + resolution: {integrity: sha512-GalEZH4JgOMHYYcYmVqnFirFsjZHeoGMDt9IxEnM9F7GRUUyUksJ7Ou53L83WHJq3RWKD3AcBpo0iQh0oMpf8A==} + engines: {bun: '>=1', deno: '>=2', node: '>=16'} + peerDependencies: + '@noble/ciphers': ^1.0.0 + + '@emnapi/core@1.8.1': + resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} + + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.2': + resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@floating-ui/core@1.7.4': + resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} + + '@floating-ui/dom@1.7.5': + resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==} + + '@floating-ui/react-dom@2.1.7': + resolution: {integrity: sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@hono/node-server@1.19.9': + resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.1': + resolution: {integrity: sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==} + engines: {node: 20 || >=22} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@modelcontextprotocol/sdk@1.26.0': + resolution: {integrity: sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + + '@mswjs/interceptors@0.41.2': + resolution: {integrity: sha512-7G0Uf0yK3f2bjElBLGHIQzgRgMESczOMyYVasq1XK8P5HaXtlW4eQhz9MBL+TQILZLaruq+ClGId+hH0w4jvWw==} + engines: {node: '>=18'} + + '@napi-rs/wasm-runtime@0.2.12': + resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + + '@next/env@16.1.6': + resolution: {integrity: sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==} + + '@next/eslint-plugin-next@16.1.6': + resolution: {integrity: sha512-/Qq3PTagA6+nYVfryAtQ7/9FEr/6YVyvOtl6rZnGsbReGLf0jZU6gkpr1FuChAQpvV46a78p4cmHOVP8mbfSMQ==} + + '@next/swc-darwin-arm64@16.1.6': + resolution: {integrity: sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@16.1.6': + resolution: {integrity: sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@16.1.6': + resolution: {integrity: sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@16.1.6': + resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@16.1.6': + resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@16.1.6': + resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@16.1.6': + resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@16.1.6': + resolution: {integrity: sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@noble/ciphers@1.3.0': + resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/ciphers@2.1.1': + resolution: {integrity: sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==} + engines: {node: '>= 20.19.0'} + + '@noble/curves@1.9.7': + resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@2.0.1': + resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} + engines: {node: '>= 20.19.0'} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + + '@octokit/auth-token@6.0.0': + resolution: {integrity: sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==} + engines: {node: '>= 20'} + + '@octokit/core@7.0.6': + resolution: {integrity: sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==} + engines: {node: '>= 20'} + + '@octokit/endpoint@11.0.2': + resolution: {integrity: sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ==} + engines: {node: '>= 20'} + + '@octokit/graphql@9.0.3': + resolution: {integrity: sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==} + engines: {node: '>= 20'} + + '@octokit/openapi-types@27.0.0': + resolution: {integrity: sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==} + + '@octokit/plugin-paginate-rest@14.0.0': + resolution: {integrity: sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-request-log@6.0.0': + resolution: {integrity: sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-rest-endpoint-methods@17.0.0': + resolution: {integrity: sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-retry@8.0.3': + resolution: {integrity: sha512-vKGx1i3MC0za53IzYBSBXcrhmd+daQDzuZfYDd52X5S0M2otf3kVZTVP8bLA3EkU0lTvd1WEC2OlNNa4G+dohA==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': '>=7' + + '@octokit/plugin-throttling@11.0.3': + resolution: {integrity: sha512-34eE0RkFCKycLl2D2kq7W+LovheM/ex3AwZCYN8udpi6bxsyjZidb2McXs69hZhLmJlDqTSP8cH+jSRpiaijBg==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': ^7.0.0 + + '@octokit/request-error@7.1.0': + resolution: {integrity: sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==} + engines: {node: '>= 20'} + + '@octokit/request@10.0.7': + resolution: {integrity: sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA==} + engines: {node: '>= 20'} + + '@octokit/rest@22.0.1': + resolution: {integrity: sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==} + engines: {node: '>= 20'} + + '@octokit/types@16.0.0': + resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==} + + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + + '@prisma/client@6.19.2': + resolution: {integrity: sha512-gR2EMvfK/aTxsuooaDA32D8v+us/8AAet+C3J1cc04SW35FPdZYgLF+iN4NDLUgAaUGTKdAB0CYenu1TAgGdMg==} + engines: {node: '>=18.18'} + peerDependencies: + prisma: '*' + typescript: '>=5.1.0' + peerDependenciesMeta: + prisma: + optional: true + typescript: + optional: true + + '@prisma/config@6.19.2': + resolution: {integrity: sha512-kadBGDl+aUswv/zZMk9Mx0C8UZs1kjao8H9/JpI4Wh4SHZaM7zkTwiKn/iFLfRg+XtOAo/Z/c6pAYhijKl0nzQ==} + + '@prisma/debug@6.19.2': + resolution: {integrity: sha512-lFnEZsLdFLmEVCVNdskLDCL8Uup41GDfU0LUfquw+ercJC8ODTuL0WNKgOKmYxCJVvFwf0OuZBzW99DuWmoH2A==} + + '@prisma/engines-version@7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7': + resolution: {integrity: sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==} + + '@prisma/engines@6.19.2': + resolution: {integrity: sha512-TTkJ8r+uk/uqczX40wb+ODG0E0icVsMgwCTyTHXehaEfb0uo80M9g1aW1tEJrxmFHeOZFXdI2sTA1j1AgcHi4A==} + + '@prisma/fetch-engine@6.19.2': + resolution: {integrity: sha512-h4Ff4Pho+SR1S8XerMCC12X//oY2bG3Iug/fUnudfcXEUnIeRiBdXHFdGlGOgQ3HqKgosTEhkZMvGM9tWtYC+Q==} + + '@prisma/get-platform@6.19.2': + resolution: {integrity: sha512-PGLr06JUSTqIvztJtAzIxOwtWKtJm5WwOG6xpsgD37Rc84FpfUBGLKz65YpJBGtkRQGXTYEFie7pYALocC3MtA==} + + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-accessible-icon@1.1.7': + resolution: {integrity: sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-accordion@1.2.12': + resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-alert-dialog@1.1.15': + resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-aspect-ratio@1.1.7': + resolution: {integrity: sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-avatar@1.1.10': + resolution: {integrity: sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collapsible@1.1.12': + resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context-menu@2.2.16': + resolution: {integrity: sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.16': + resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-form@0.1.8': + resolution: {integrity: sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-hover-card@1.1.15': + resolution: {integrity: sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.7': + resolution: {integrity: sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menu@2.1.16': + resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menubar@1.1.16': + resolution: {integrity: sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-navigation-menu@1.2.14': + resolution: {integrity: sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-one-time-password-field@0.1.8': + resolution: {integrity: sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-password-toggle-field@0.1.3': + resolution: {integrity: sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-progress@1.1.7': + resolution: {integrity: sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-radio-group@1.3.8': + resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-scroll-area@1.2.10': + resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.2.6': + resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.7': + resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slider@1.3.6': + resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toast@1.2.15': + resolution: {integrity: sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle-group@1.1.11': + resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle@1.1.10': + resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toolbar@1.1.11': + resolution: {integrity: sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.2.8': + resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-is-hydrated@0.1.0': + resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + + '@rollup/rollup-android-arm-eabi@4.57.1': + resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.57.1': + resolution: {integrity: sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.57.1': + resolution: {integrity: sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.57.1': + resolution: {integrity: sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.57.1': + resolution: {integrity: sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.57.1': + resolution: {integrity: sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.57.1': + resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.57.1': + resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.57.1': + resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.57.1': + resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.57.1': + resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.57.1': + resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.57.1': + resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.57.1': + resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.57.1': + resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.57.1': + resolution: {integrity: sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.57.1': + resolution: {integrity: sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.57.1': + resolution: {integrity: sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.57.1': + resolution: {integrity: sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.57.1': + resolution: {integrity: sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==} + cpu: [x64] + os: [win32] + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@tailwindcss/node@4.1.18': + resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} + + '@tailwindcss/oxide-android-arm64@4.1.18': + resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.18': + resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.18': + resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.18': + resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.18': + resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} + engines: {node: '>= 10'} + + '@tailwindcss/postcss@4.1.18': + resolution: {integrity: sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==} + + '@tailwindcss/typography@0.5.19': + resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + + '@tanstack/react-table@8.21.3': + resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + '@tanstack/react-virtual@3.13.23': + resolution: {integrity: sha512-XnMRnHQ23piOVj2bzJqHrRrLg4r+F86fuBcwteKfbIjJrtGxb4z7tIvPVAe4B+4UVwo9G4Giuz5fmapcrnZ0OQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/table-core@8.21.3': + resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} + engines: {node: '>=12'} + + '@tanstack/virtual-core@3.13.23': + resolution: {integrity: sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==} + + '@ts-morph/common@0.27.0': + resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/js-yaml@4.0.9': + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@20.19.33': + resolution: {integrity: sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.13': + resolution: {integrity: sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==} + + '@types/statuses@2.0.6': + resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/validate-npm-package-name@4.0.2': + resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==} + + '@typescript-eslint/eslint-plugin@8.55.0': + resolution: {integrity: sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.55.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.55.0': + resolution: {integrity: sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.55.0': + resolution: {integrity: sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.55.0': + resolution: {integrity: sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.55.0': + resolution: {integrity: sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.55.0': + resolution: {integrity: sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.55.0': + resolution: {integrity: sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.55.0': + resolution: {integrity: sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.55.0': + resolution: {integrity: sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.55.0': + resolution: {integrity: sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.11.1': + resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.11.1': + resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} + cpu: [x64] + os: [win32] + + '@vitest/expect@4.0.18': + resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + + '@vitest/mocker@4.0.18': + resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.0.18': + resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + + '@vitest/runner@4.0.18': + resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + + '@vitest/snapshot@4.0.18': + resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + + '@vitest/spy@4.0.18': + resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + + '@vitest/utils@4.0.18': + resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-types-flow@0.0.8: + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + + ast-types@0.16.1: + resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} + engines: {node: '>=4'} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axe-core@4.11.1: + resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==} + engines: {node: '>=4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + baseline-browser-mapping@2.9.19: + resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} + hasBin: true + + before-after-hook@4.0.0: + resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} + + better-auth@1.4.18: + resolution: {integrity: sha512-bnyifLWBPcYVltH3RhS7CM62MoelEqC6Q+GnZwfiDWNfepXoQZBjEvn4urcERC7NTKgKq5zNBM8rvPvRBa6xcg==} + peerDependencies: + '@lynx-js/react': '*' + '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 + '@sveltejs/kit': ^2.0.0 + '@tanstack/react-start': ^1.0.0 + '@tanstack/solid-start': ^1.0.0 + better-sqlite3: ^12.0.0 + drizzle-kit: '>=0.31.4' + drizzle-orm: '>=0.41.0' + mongodb: ^6.0.0 || ^7.0.0 + mysql2: ^3.0.0 + next: ^14.0.0 || ^15.0.0 || ^16.0.0 + pg: ^8.0.0 + prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + solid-js: ^1.0.0 + svelte: ^4.0.0 || ^5.0.0 + vitest: ^2.0.0 || ^3.0.0 || ^4.0.0 + vue: ^3.0.0 + peerDependenciesMeta: + '@lynx-js/react': + optional: true + '@prisma/client': + optional: true + '@sveltejs/kit': + optional: true + '@tanstack/react-start': + optional: true + '@tanstack/solid-start': + optional: true + better-sqlite3: + optional: true + drizzle-kit: + optional: true + drizzle-orm: + optional: true + mongodb: + optional: true + mysql2: + optional: true + next: + optional: true + pg: + optional: true + prisma: + optional: true + react: + optional: true + react-dom: + optional: true + solid-js: + optional: true + svelte: + optional: true + vitest: + optional: true + vue: + optional: true + + better-call@1.1.8: + resolution: {integrity: sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw==} + peerDependencies: + zod: ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + + bottleneck@2.19.5: + resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + c12@3.1.0: + resolution: {integrity: sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==} + peerDependencies: + magicast: ^0.3.5 + peerDependenciesMeta: + magicast: + optional: true + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001769: + resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + + citty@0.2.1: + resolution: {integrity: sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + code-block-writer@13.0.3: + resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + + dedent@1.7.1: + resolution: {integrity: sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge-ts@7.1.5: + resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} + engines: {node: '>=16.0.0'} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + diff@8.0.3: + resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} + engines: {node: '>=0.3.1'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dotenv@17.3.1: + resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eciesjs@0.4.17: + resolution: {integrity: sha512-TOOURki4G7sD1wDCjj7NfLaXZZ49dFOeEb5y39IXpb8p0hRzVvfvzZHOi5JcT+PpyAbi/Y+lxPb8eTag2WYH8w==} + engines: {bun: '>=1', deno: '>=2', node: '>=16'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + effect@3.18.4: + resolution: {integrity: sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==} + + electron-to-chromium@1.5.286: + resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + enhanced-resolve@5.19.0: + resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} + engines: {node: '>=10.13.0'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-abstract@1.24.1: + resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.2.2: + resolution: {integrity: sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + eslint-config-next@16.1.6: + resolution: {integrity: sha512-vKq40io2B0XtkkNDYyleATwblNt8xuh3FWp8SpSz3pt7P01OkBFlKsJZ2mWt5WsCySlDQLckb1zMY9yE9Qy0LA==} + peerDependencies: + eslint: '>=9.0.0' + typescript: '>=3.3.1' + peerDependenciesMeta: + typescript: + optional: true + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-import-resolver-typescript@3.10.1: + resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jsx-a11y@6.10.2: + resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + + eslint-plugin-react-hooks@7.0.1: + resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.39.2: + resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + execa@9.6.1: + resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} + engines: {node: ^18.19.0 || >=20.5.0} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + express-rate-limit@8.2.1: + resolution: {integrity: sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-check@3.23.2: + resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} + engines: {node: '>=8.0.0'} + + fast-content-type-parse@3.0.0: + resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + framer-motion@12.34.0: + resolution: {integrity: sha512-+/H49owhzkzQyxtn7nZeF4kdH++I2FWrESQ184Zbcw5cEqNHYkE5yxWxcTLSj5lNx3NWdbIRy5FHqUvetD8FWg==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-extra@11.3.3: + resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==} + engines: {node: '>=14.14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + fuzzysort@3.1.0: + resolution: {integrity: sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==} + + fzf@0.5.2: + resolution: {integrity: sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.4.0: + resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-own-enumerable-keys@1.0.0: + resolution: {integrity: sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==} + engines: {node: '>=14.16'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.13.6: + resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + + giget@2.0.0: + resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} + hasBin: true + + github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.4.0: + resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphql@16.12.0: + resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + + gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hast-util-heading-rank@3.0.0: + resolution: {integrity: sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==} + + hast-util-is-element@3.0.0: + resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} + + hast-util-sanitize@5.0.2: + resolution: {integrity: sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==} + + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-to-string@3.0.1: + resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==} + + hast-util-to-text@4.0.2: + resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + highlight.js@11.11.1: + resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} + engines: {node: '>=12.0.0'} + + hono@4.11.9: + resolution: {integrity: sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==} + engines: {node: '>=16.9.0'} + + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + ip-address@10.0.1: + resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} + engines: {node: '>=20'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-obj@3.0.0: + resolution: {integrity: sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==} + engines: {node: '>=12'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-regexp@3.1.0: + resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==} + engines: {node: '>=12'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isexe@3.1.5: + resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} + engines: {node: '>=18'} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + jose@6.1.3: + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + kysely@0.28.11: + resolution: {integrity: sha512-zpGIFg0HuoC893rIjYX1BETkVWdDnzTzF5e0kWXJFg5lE0k1/LfNWBejrcnOFu8Q2Rfq/hTDTU7XLUM8QOrpzg==} + engines: {node: '>=20.0.0'} + + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + log-symbols@6.0.0: + resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} + engines: {node: '>=18'} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lowlight@3.3.0: + resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@0.563.0: + resolution: {integrity: sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + minimatch@10.1.2: + resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==} + engines: {node: 20 || >=22} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + motion-dom@12.34.0: + resolution: {integrity: sha512-Lql3NuEcScRDxTAO6GgUsRHBZOWI/3fnMlkMcH5NftzcN37zJta+bpbMAV9px4Nj057TuvRooMK7QrzMCgtz6Q==} + + motion-utils@12.29.2: + resolution: {integrity: sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==} + + motion@12.34.0: + resolution: {integrity: sha512-01Sfa/zgsD/di8zA/uFW5Eb7/SPXoGyUfy+uMRMW5Spa8j0z/UbfQewAYvPMYFCXRlyD6e5aLHh76TxeeJD+RA==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + msw@2.12.10: + resolution: {integrity: sha512-G3VUymSE0/iegFnuipujpwyTM2GuZAKXNeerUSrG2+Eg391wW63xFs5ixWsK9MWzr1AGoSkYGmyAzNgbR3+urw==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanostores@1.1.0: + resolution: {integrity: sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA==} + engines: {node: ^20.0.0 || >=22.0.0} + + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + next-themes@0.4.6: + resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} + peerDependencies: + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + + next@16.1.6: + resolution: {integrity: sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==} + engines: {node: '>=20.9.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + + nypm@0.6.5: + resolution: {integrity: sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==} + engines: {node: '>=18'} + hasBin: true + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object-treeify@1.1.33: + resolution: {integrity: sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==} + engines: {node: '>= 10'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + open@11.0.0: + resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} + engines: {node: '>=20'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@8.2.0: + resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} + engines: {node: '>=18'} + + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + powershell-utils@0.1.0: + resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} + engines: {node: '>=20'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + + prisma@6.19.2: + resolution: {integrity: sha512-XTKeKxtQElcq3U9/jHyxSPgiRgeYDKxWTPOf6NkXA0dNj5j40MfEsZkMbyNpwDWCUv7YBFUl7I2VK/6ALbmhEg==} + engines: {node: '>=18.18'} + hasBin: true + peerDependencies: + typescript: '>=5.1.0' + peerDependenciesMeta: + typescript: + optional: true + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + qs@6.14.1: + resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} + engines: {node: '>=0.6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + radix-ui@1.4.3: + resolution: {integrity: sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + rc9@2.1.2: + resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + + react-dom@19.2.3: + resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} + peerDependencies: + react: ^19.2.3 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-use-measure@2.1.7: + resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==} + peerDependencies: + react: '>=16.13' + react-dom: '>=16.13' + peerDependenciesMeta: + react-dom: + optional: true + + react@19.2.3: + resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} + engines: {node: '>=0.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + recast@0.23.11: + resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} + engines: {node: '>= 4'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + rehype-autolink-headings@7.1.0: + resolution: {integrity: sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==} + + rehype-highlight@7.0.2: + resolution: {integrity: sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==} + + rehype-sanitize@6.0.0: + resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==} + + rehype-slug@6.0.0: + resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + resolve@2.0.0-next.5: + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + hasBin: true + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + rettime@0.10.1: + resolution: {integrity: sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw==} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.57.1: + resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rou3@0.7.12: + resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==} + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shadcn@3.8.4: + resolution: {integrity: sha512-pSad/m1+PGzB0aLsRBV0EkyGg9al1nJqYUuucg6d8v8xZspPZ5/ehGNEp5M4b1KQYqdO5/gGPbkhVbgmXqG9Pw==} + hasBin: true + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stable-hash@0.0.5: + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + stdin-discarder@0.2.2: + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} + engines: {node: '>=18'} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + stringify-object@5.0.0: + resolution: {integrity: sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==} + engines: {node: '>=14.16'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + + strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + + tailwind-merge@3.4.0: + resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==} + + tailwindcss@4.1.18: + resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.0.3: + resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + engines: {node: '>=14.0.0'} + + tldts-core@7.0.23: + resolution: {integrity: sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==} + + tldts@7.0.23: + resolution: {integrity: sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==} + hasBin: true + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-morph@26.0.0: + resolution: {integrity: sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==} + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@5.4.4: + resolution: {integrity: sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==} + engines: {node: '>=20'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typescript-eslint@8.55.0: + resolution: {integrity: sha512-HE4wj+r5lmDVS9gdaN0/+iqNvPZwGfnJ5lZuz7s5vLlg9ODw0bIiiETaios9LvFI1U94/VBXGm3CB2Y5cNFMpw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-find-after@5.0.0: + resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + universal-user-agent@7.0.3: + resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unrs-resolver@1.11.1: + resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + + until-async@3.0.2: + resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + validate-npm-package-name@7.0.2: + resolution: {integrity: sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==} + engines: {node: ^20.17.0 || >=22.9.0} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.0.18: + resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.0.18 + '@vitest/browser-preview': 4.0.18 + '@vitest/browser-webdriverio': 4.0.18 + '@vitest/ui': 4.0.18 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + wsl-utils@0.3.1: + resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} + engines: {node: '>=20'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@antfu/ni@25.0.0': + dependencies: + ansis: 4.2.0 + fzf: 0.5.2 + package-manager-detector: 1.6.0 + tinyexec: 1.0.2 + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.29.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.28.5': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/preset-typescript@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)': + dependencies: + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + '@standard-schema/spec': 1.1.0 + better-call: 1.1.8(zod@4.3.6) + jose: 6.1.3 + kysely: 0.28.11 + nanostores: 1.1.0 + zod: 4.3.6 + + '@better-auth/telemetry@1.4.18(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))': + dependencies: + '@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + + '@better-auth/utils@0.3.0': {} + + '@better-fetch/fetch@1.1.21': {} + + '@dnd-kit/accessibility@3.1.1(react@19.2.3)': + dependencies: + react: 19.2.3 + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@19.2.3) + '@dnd-kit/utilities': 3.2.2(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + tslib: 2.8.1 + + '@dnd-kit/modifiers@9.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@dnd-kit/utilities': 3.2.2(react@19.2.3) + react: 19.2.3 + tslib: 2.8.1 + + '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@dnd-kit/utilities': 3.2.2(react@19.2.3) + react: 19.2.3 + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@19.2.3)': + dependencies: + react: 19.2.3 + tslib: 2.8.1 + + '@dotenvx/dotenvx@1.52.0': + dependencies: + commander: 11.1.0 + dotenv: 17.3.1 + eciesjs: 0.4.17 + execa: 5.1.1 + fdir: 6.5.0(picomatch@4.0.3) + ignore: 5.3.2 + object-treeify: 1.1.33 + picomatch: 4.0.3 + which: 4.0.0 + + '@ecies/ciphers@0.2.5(@noble/ciphers@1.3.0)': + dependencies: + '@noble/ciphers': 1.3.0 + + '@emnapi/core@1.8.1': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.8.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))': + dependencies: + eslint: 9.39.2(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.1': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.3': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.2': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@floating-ui/core@1.7.4': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.5': + dependencies: + '@floating-ui/core': 1.7.4 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/react-dom@2.1.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@floating-ui/dom': 1.7.5 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + '@floating-ui/utils@0.2.10': {} + + '@hono/node-server@1.19.9(hono@4.11.9)': + dependencies: + hono: 4.11.9 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@img/colour@1.0.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.8.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@inquirer/ansi@1.0.2': {} + + '@inquirer/confirm@5.1.21(@types/node@20.19.33)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@20.19.33) + '@inquirer/type': 3.0.10(@types/node@20.19.33) + optionalDependencies: + '@types/node': 20.19.33 + + '@inquirer/core@10.3.2(@types/node@20.19.33)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@20.19.33) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 20.19.33 + + '@inquirer/figures@1.0.15': {} + + '@inquirer/type@3.0.10(@types/node@20.19.33)': + optionalDependencies: + '@types/node': 20.19.33 + + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.1': + dependencies: + '@isaacs/balanced-match': 4.0.1 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@modelcontextprotocol/sdk@1.26.0(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.9(hono@4.11.9) + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.2.1(express@5.2.1) + hono: 4.11.9 + jose: 6.1.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.1(zod@3.25.76) + transitivePeerDependencies: + - supports-color + + '@mswjs/interceptors@0.41.2': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + + '@napi-rs/wasm-runtime@0.2.12': + dependencies: + '@emnapi/core': 1.8.1 + '@emnapi/runtime': 1.8.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@next/env@16.1.6': {} + + '@next/eslint-plugin-next@16.1.6': + dependencies: + fast-glob: 3.3.1 + + '@next/swc-darwin-arm64@16.1.6': + optional: true + + '@next/swc-darwin-x64@16.1.6': + optional: true + + '@next/swc-linux-arm64-gnu@16.1.6': + optional: true + + '@next/swc-linux-arm64-musl@16.1.6': + optional: true + + '@next/swc-linux-x64-gnu@16.1.6': + optional: true + + '@next/swc-linux-x64-musl@16.1.6': + optional: true + + '@next/swc-win32-arm64-msvc@16.1.6': + optional: true + + '@next/swc-win32-x64-msvc@16.1.6': + optional: true + + '@noble/ciphers@1.3.0': {} + + '@noble/ciphers@2.1.1': {} + + '@noble/curves@1.9.7': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/hashes@1.8.0': {} + + '@noble/hashes@2.0.1': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@nolyfill/is-core-module@1.0.39': {} + + '@octokit/auth-token@6.0.0': {} + + '@octokit/core@7.0.6': + dependencies: + '@octokit/auth-token': 6.0.0 + '@octokit/graphql': 9.0.3 + '@octokit/request': 10.0.7 + '@octokit/request-error': 7.1.0 + '@octokit/types': 16.0.0 + before-after-hook: 4.0.0 + universal-user-agent: 7.0.3 + + '@octokit/endpoint@11.0.2': + dependencies: + '@octokit/types': 16.0.0 + universal-user-agent: 7.0.3 + + '@octokit/graphql@9.0.3': + dependencies: + '@octokit/request': 10.0.7 + '@octokit/types': 16.0.0 + universal-user-agent: 7.0.3 + + '@octokit/openapi-types@27.0.0': {} + + '@octokit/plugin-paginate-rest@14.0.0(@octokit/core@7.0.6)': + dependencies: + '@octokit/core': 7.0.6 + '@octokit/types': 16.0.0 + + '@octokit/plugin-request-log@6.0.0(@octokit/core@7.0.6)': + dependencies: + '@octokit/core': 7.0.6 + + '@octokit/plugin-rest-endpoint-methods@17.0.0(@octokit/core@7.0.6)': + dependencies: + '@octokit/core': 7.0.6 + '@octokit/types': 16.0.0 + + '@octokit/plugin-retry@8.0.3(@octokit/core@7.0.6)': + dependencies: + '@octokit/core': 7.0.6 + '@octokit/request-error': 7.1.0 + '@octokit/types': 16.0.0 + bottleneck: 2.19.5 + + '@octokit/plugin-throttling@11.0.3(@octokit/core@7.0.6)': + dependencies: + '@octokit/core': 7.0.6 + '@octokit/types': 16.0.0 + bottleneck: 2.19.5 + + '@octokit/request-error@7.1.0': + dependencies: + '@octokit/types': 16.0.0 + + '@octokit/request@10.0.7': + dependencies: + '@octokit/endpoint': 11.0.2 + '@octokit/request-error': 7.1.0 + '@octokit/types': 16.0.0 + fast-content-type-parse: 3.0.0 + universal-user-agent: 7.0.3 + + '@octokit/rest@22.0.1': + dependencies: + '@octokit/core': 7.0.6 + '@octokit/plugin-paginate-rest': 14.0.0(@octokit/core@7.0.6) + '@octokit/plugin-request-log': 6.0.0(@octokit/core@7.0.6) + '@octokit/plugin-rest-endpoint-methods': 17.0.0(@octokit/core@7.0.6) + + '@octokit/types@16.0.0': + dependencies: + '@octokit/openapi-types': 27.0.0 + + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + + '@prisma/client@6.19.2(prisma@6.19.2(typescript@5.9.3))(typescript@5.9.3)': + optionalDependencies: + prisma: 6.19.2(typescript@5.9.3) + typescript: 5.9.3 + + '@prisma/config@6.19.2': + dependencies: + c12: 3.1.0 + deepmerge-ts: 7.1.5 + effect: 3.18.4 + empathic: 2.0.0 + transitivePeerDependencies: + - magicast + + '@prisma/debug@6.19.2': {} + + '@prisma/engines-version@7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7': {} + + '@prisma/engines@6.19.2': + dependencies: + '@prisma/debug': 6.19.2 + '@prisma/engines-version': 7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7 + '@prisma/fetch-engine': 6.19.2 + '@prisma/get-platform': 6.19.2 + + '@prisma/fetch-engine@6.19.2': + dependencies: + '@prisma/debug': 6.19.2 + '@prisma/engines-version': 7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7 + '@prisma/get-platform': 6.19.2 + + '@prisma/get-platform@6.19.2': + dependencies: + '@prisma/debug': 6.19.2 + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-accessible-icon@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-accordion@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-aspect-ratio@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-avatar@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.13)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-context-menu@2.2.16(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-context@1.1.2(@types/react@19.2.13)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.3) + aria-hidden: 1.2.6 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-remove-scroll: 2.7.2(@types/react@19.2.13)(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-direction@1.1.1(@types/react@19.2.13)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.13)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-form@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-hover-card@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-id@1.1.1(@types/react@19.2.13)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-label@2.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.13)(react@19.2.3) + aria-hidden: 1.2.6 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-remove-scroll: 2.7.2(@types/react@19.2.13)(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-menubar@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-one-time-password-field@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-password-toggle-field@0.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.3) + aria-hidden: 1.2.6 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-remove-scroll: 2.7.2(@types/react@19.2.13)(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@floating-ui/react-dom': 2.1.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/rect': 1.1.1 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-progress@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + aria-hidden: 1.2.6 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-remove-scroll: 2.7.2(@types/react@19.2.13)(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-separator@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-slider@1.3.6(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-slot@1.2.3(@types/react@19.2.13)(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-toast@1.2.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-toolbar@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.13)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.13)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.13)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.13)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.2.13)(react@19.2.3)': + dependencies: + react: 19.2.3 + use-sync-external-store: 1.6.0(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.13)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.13)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.13)(react@19.2.3)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.13)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.13 + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + '@radix-ui/rect@1.1.1': {} + + '@rollup/rollup-android-arm-eabi@4.57.1': + optional: true + + '@rollup/rollup-android-arm64@4.57.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.57.1': + optional: true + + '@rollup/rollup-darwin-x64@4.57.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.57.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.57.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.57.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.57.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.57.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.57.1': + optional: true + + '@rtsao/scc@1.1.0': {} + + '@sec-ant/readable-stream@0.4.1': {} + + '@sindresorhus/merge-streams@4.0.0': {} + + '@standard-schema/spec@1.1.0': {} + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/node@4.1.18': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.19.0 + jiti: 2.6.1 + lightningcss: 1.30.2 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.1.18 + + '@tailwindcss/oxide-android-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide@4.1.18': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-x64': 4.1.18 + '@tailwindcss/oxide-freebsd-x64': 4.1.18 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-x64-musl': 4.1.18 + '@tailwindcss/oxide-wasm32-wasi': 4.1.18 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 + + '@tailwindcss/postcss@4.1.18': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.1.18 + '@tailwindcss/oxide': 4.1.18 + postcss: 8.5.6 + tailwindcss: 4.1.18 + + '@tailwindcss/typography@0.5.19(tailwindcss@4.1.18)': + dependencies: + postcss-selector-parser: 6.0.10 + tailwindcss: 4.1.18 + + '@tanstack/react-table@8.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@tanstack/table-core': 8.21.3 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + '@tanstack/react-virtual@3.13.23(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@tanstack/virtual-core': 3.13.23 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + '@tanstack/table-core@8.21.3': {} + + '@tanstack/virtual-core@3.13.23': {} + + '@ts-morph/common@0.27.0': + dependencies: + fast-glob: 3.3.3 + minimatch: 10.1.2 + path-browserify: 1.0.1 + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/deep-eql@4.0.2': {} + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + + '@types/estree@1.0.8': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/js-yaml@4.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + + '@types/node@20.19.33': + dependencies: + undici-types: 6.21.0 + + '@types/react-dom@19.2.3(@types/react@19.2.13)': + dependencies: + '@types/react': 19.2.13 + + '@types/react@19.2.13': + dependencies: + csstype: 3.2.3 + + '@types/statuses@2.0.6': {} + + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + + '@types/validate-npm-package-name@4.0.2': {} + + '@typescript-eslint/eslint-plugin@8.55.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.55.0 + '@typescript-eslint/type-utils': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.55.0 + eslint: 9.39.2(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.55.0 + '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.55.0 + debug: 4.4.3 + eslint: 9.39.2(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.55.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.9.3) + '@typescript-eslint/types': 8.55.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.55.0': + dependencies: + '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/visitor-keys': 8.55.0 + + '@typescript-eslint/tsconfig-utils@8.55.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.2(jiti@2.6.1) + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.55.0': {} + + '@typescript-eslint/typescript-estree@8.55.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.55.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.9.3) + '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/visitor-keys': 8.55.0 + debug: 4.4.3 + minimatch: 9.0.5 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.55.0 + '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.55.0': + dependencies: + '@typescript-eslint/types': 8.55.0 + eslint-visitor-keys: 4.2.1 + + '@ungap/structured-clone@1.3.0': {} + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + optional: true + + '@unrs/resolver-binding-android-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + dependencies: + '@napi-rs/wasm-runtime': 0.2.12 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + optional: true + + '@vitest/expect@4.0.18': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + chai: 6.2.2 + tinyrainbow: 3.0.3 + + '@vitest/mocker@4.0.18(msw@2.12.10(@types/node@20.19.33)(typescript@5.9.3))(vite@7.3.1(@types/node@20.19.33)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))': + dependencies: + '@vitest/spy': 4.0.18 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + msw: 2.12.10(@types/node@20.19.33)(typescript@5.9.3) + vite: 7.3.1(@types/node@20.19.33)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + + '@vitest/pretty-format@4.0.18': + dependencies: + tinyrainbow: 3.0.3 + + '@vitest/runner@4.0.18': + dependencies: + '@vitest/utils': 4.0.18 + pathe: 2.0.3 + + '@vitest/snapshot@4.0.18': + dependencies: + '@vitest/pretty-format': 4.0.18 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.0.18': {} + + '@vitest/utils@4.0.18': + dependencies: + '@vitest/pretty-format': 4.0.18 + tinyrainbow: 3.0.3 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + agent-base@7.1.4: {} + + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansis@4.2.0: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + assertion-error@2.0.1: {} + + ast-types-flow@0.0.8: {} + + ast-types@0.16.1: + dependencies: + tslib: 2.8.1 + + async-function@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axe-core@4.11.1: {} + + axobject-query@4.1.0: {} + + bail@2.0.2: {} + + balanced-match@1.0.2: {} + + baseline-browser-mapping@2.9.19: {} + + before-after-hook@4.0.0: {} + + better-auth@1.4.18(@prisma/client@6.19.2(prisma@6.19.2(typescript@5.9.3))(typescript@5.9.3))(next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(prisma@6.19.2(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@4.0.18(@types/node@20.19.33)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@20.19.33)(typescript@5.9.3))(tsx@4.21.0)): + dependencies: + '@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) + '@better-auth/telemetry': 1.4.18(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)) + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + '@noble/ciphers': 2.1.1 + '@noble/hashes': 2.0.1 + better-call: 1.1.8(zod@4.3.6) + defu: 6.1.4 + jose: 6.1.3 + kysely: 0.28.11 + nanostores: 1.1.0 + zod: 4.3.6 + optionalDependencies: + '@prisma/client': 6.19.2(prisma@6.19.2(typescript@5.9.3))(typescript@5.9.3) + next: 16.1.6(@babel/core@7.29.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + prisma: 6.19.2(typescript@5.9.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + vitest: 4.0.18(@types/node@20.19.33)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@20.19.33)(typescript@5.9.3))(tsx@4.21.0) + + better-call@1.1.8(zod@4.3.6): + dependencies: + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + rou3: 0.7.12 + set-cookie-parser: 2.7.2 + optionalDependencies: + zod: 4.3.6 + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.14.1 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + bottleneck@2.19.5: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.19 + caniuse-lite: 1.0.30001769 + electron-to-chromium: 1.5.286 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + bytes@3.1.2: {} + + c12@3.1.0: + dependencies: + chokidar: 4.0.3 + confbox: 0.2.4 + defu: 6.1.4 + dotenv: 16.6.1 + exsolve: 1.0.8 + giget: 2.0.0 + jiti: 2.6.1 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 1.0.0 + pkg-types: 2.3.0 + rc9: 2.1.2 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001769: {} + + ccount@2.0.1: {} + + chai@6.2.2: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + citty@0.1.6: + dependencies: + consola: 3.4.2 + + citty@0.2.1: {} + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-spinners@2.9.2: {} + + cli-width@4.1.0: {} + + client-only@0.0.1: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clsx@2.1.1: {} + + code-block-writer@13.0.3: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + comma-separated-tokens@2.0.3: {} + + commander@11.1.0: {} + + commander@14.0.3: {} + + concat-map@0.0.1: {} + + confbox@0.2.4: {} + + consola@3.4.2: {} + + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cookie@1.1.1: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cosmiconfig@9.0.0(typescript@5.9.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.9.3 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + damerau-levenshtein@1.0.8: {} + + data-uri-to-buffer@4.0.1: {} + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + + dedent@1.7.1: {} + + deep-is@0.1.4: {} + + deepmerge-ts@7.1.5: {} + + deepmerge@4.3.1: {} + + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-lazy-prop@3.0.0: {} + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + defu@6.1.4: {} + + depd@2.0.0: {} + + dequal@2.0.3: {} + + destr@2.0.5: {} + + detect-libc@2.1.2: {} + + detect-node-es@1.1.0: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + diff@8.0.3: {} + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + dotenv@16.6.1: {} + + dotenv@17.3.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eciesjs@0.4.17: + dependencies: + '@ecies/ciphers': 0.2.5(@noble/ciphers@1.3.0) + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + + ee-first@1.1.1: {} + + effect@3.18.4: + dependencies: + '@standard-schema/spec': 1.1.0 + fast-check: 3.23.2 + + electron-to-chromium@1.5.286: {} + + emoji-regex@10.6.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + empathic@2.0.0: {} + + encodeurl@2.0.0: {} + + enhanced-resolve@5.19.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + env-paths@2.2.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-abstract@1.24.1: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.20 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.2.2: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + safe-array-concat: 1.1.3 + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + eslint-config-next@16.1.6(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@next/eslint-plugin-next': 16.1.6 + eslint: 9.39.2(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1)) + globals: 16.4.0 + typescript-eslint: 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3 + eslint: 9.39.2(jiti@2.6.1) + get-tsconfig: 4.13.6 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.15 + unrs-resolver: 1.11.1 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.39.2(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.2(jiti@2.6.1)): + dependencies: + aria-query: 5.3.2 + array-includes: 3.1.9 + array.prototype.flatmap: 1.3.3 + ast-types-flow: 0.0.8 + axe-core: 4.11.1 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 9.39.2(jiti@2.6.1) + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 + + eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@2.6.1)): + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.0 + eslint: 9.39.2(jiti@2.6.1) + hermes-parser: 0.25.1 + zod: 4.3.6 + zod-validation-error: 4.0.2(zod@4.3.6) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@2.6.1)): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.2.2 + eslint: 9.39.2(jiti@2.6.1) + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.39.2(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.3 + '@eslint/js': 9.39.2 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esprima@4.0.1: {} + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-util-is-identifier-name@3.0.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + etag@1.8.1: {} + + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + execa@9.6.1: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.2 + + expect-type@1.3.0: {} + + express-rate-limit@8.2.1(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.0.1 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.1 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + exsolve@1.0.8: {} + + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + + extend@3.0.2: {} + + fast-check@3.23.2: + dependencies: + pure-rand: 6.1.0 + + fast-content-type-parse@3.0.0: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.1: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.1.0: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + forwarded@0.2.0: {} + + framer-motion@12.34.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + motion-dom: 12.34.0 + motion-utils: 12.29.2 + tslib: 2.8.1 + optionalDependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + fresh@2.0.0: {} + + fs-extra@11.3.3: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + fuzzysort@3.1.0: {} + + fzf@0.5.2: {} + + generator-function@2.0.1: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.4.0: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-nonce@1.0.1: {} + + get-own-enumerable-keys@1.0.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@6.0.1: {} + + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.13.6: + dependencies: + resolve-pkg-maps: 1.0.0 + + giget@2.0.0: + dependencies: + citty: 0.1.6 + consola: 3.4.2 + defu: 6.1.4 + node-fetch-native: 1.6.7 + nypm: 0.6.5 + pathe: 2.0.3 + + github-slugger@2.0.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@16.4.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphql@16.12.0: {} + + gray-matter@4.0.3: + dependencies: + js-yaml: 3.14.2 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hast-util-heading-rank@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-is-element@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-sanitize@5.0.2: + dependencies: + '@types/hast': 3.0.4 + '@ungap/structured-clone': 1.3.0 + unist-util-position: 5.0.0 + + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-to-string@3.0.1: + dependencies: + '@types/hast': 3.0.4 + + hast-util-to-text@4.0.2: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + hast-util-is-element: 3.0.0 + unist-util-find-after: 5.0.0 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + headers-polyfill@4.0.3: {} + + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + highlight.js@11.11.1: {} + + hono@4.11.9: {} + + html-url-attributes@3.0.1: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + human-signals@2.1.0: {} + + human-signals@8.0.1: {} + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inherits@2.0.4: {} + + inline-style-parser@0.2.7: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + ip-address@10.0.1: {} + + ipaddr.js@1.9.1: {} + + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-arrayish@0.2.1: {} + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-bun-module@2.0.0: + dependencies: + semver: 7.7.4 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-decimal@2.0.1: {} + + is-docker@3.0.0: {} + + is-extendable@0.1.1: {} + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-fullwidth-code-point@3.0.0: {} + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-hexadecimal@2.0.1: {} + + is-in-ssh@1.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-interactive@2.0.0: {} + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-node-process@1.2.0: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-obj@3.0.0: {} + + is-plain-obj@4.1.0: {} + + is-promise@4.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-regexp@3.1.0: {} + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-stream@2.0.1: {} + + is-stream@4.0.1: {} + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.20 + + is-unicode-supported@1.3.0: {} + + is-unicode-supported@2.1.0: {} + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + isexe@3.1.5: {} + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + jiti@2.6.1: {} + + jose@6.1.3: {} + + js-tokens@4.0.0: {} + + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kind-of@6.0.3: {} + + kleur@3.0.3: {} + + kleur@4.1.5: {} + + kysely@0.28.11: {} + + language-subtag-registry@0.3.23: {} + + language-tags@1.0.9: + dependencies: + language-subtag-registry: 0.3.23 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + + lines-and-columns@1.2.4: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + log-symbols@6.0.0: + dependencies: + chalk: 5.6.2 + is-unicode-supported: 1.3.0 + + longest-streak@3.1.0: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lowlight@3.3.0: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + highlight.js: 11.11.1 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@0.563.0(react@19.2.3): + dependencies: + react: 19.2.3 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + markdown-table@3.0.4: {} + + math-intrinsics@1.1.0: {} + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.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.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mimic-fn@2.1.0: {} + + mimic-function@5.0.1: {} + + minimatch@10.1.2: + dependencies: + '@isaacs/brace-expansion': 5.0.1 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + motion-dom@12.34.0: + dependencies: + motion-utils: 12.29.2 + + motion-utils@12.29.2: {} + + motion@12.34.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + framer-motion: 12.34.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + tslib: 2.8.1 + optionalDependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + ms@2.1.3: {} + + msw@2.12.10(@types/node@20.19.33)(typescript@5.9.3): + dependencies: + '@inquirer/confirm': 5.1.21(@types/node@20.19.33) + '@mswjs/interceptors': 0.41.2 + '@open-draft/deferred-promise': 2.2.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.12.0 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.10.1 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.0 + type-fest: 5.4.4 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/node' + + mute-stream@2.0.0: {} + + nanoid@3.3.11: {} + + nanostores@1.1.0: {} + + napi-postinstall@0.3.4: {} + + natural-compare@1.4.0: {} + + negotiator@1.0.0: {} + + next-themes@0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@next/env': 16.1.6 + '@swc/helpers': 0.5.15 + baseline-browser-mapping: 2.9.19 + caniuse-lite: 1.0.30001769 + postcss: 8.4.31 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.3) + optionalDependencies: + '@next/swc-darwin-arm64': 16.1.6 + '@next/swc-darwin-x64': 16.1.6 + '@next/swc-linux-arm64-gnu': 16.1.6 + '@next/swc-linux-arm64-musl': 16.1.6 + '@next/swc-linux-x64-gnu': 16.1.6 + '@next/swc-linux-x64-musl': 16.1.6 + '@next/swc-win32-arm64-msvc': 16.1.6 + '@next/swc-win32-x64-msvc': 16.1.6 + sharp: 0.34.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + node-domexception@1.0.0: {} + + node-fetch-native@1.6.7: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + node-releases@2.0.27: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + + nypm@0.6.5: + dependencies: + citty: 0.2.1 + pathe: 2.0.3 + tinyexec: 1.0.2 + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object-treeify@1.1.33: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + obug@2.1.1: {} + + ohash@2.0.11: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + open@11.0.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 + is-inside-container: 1.0.0 + powershell-utils: 0.1.0 + wsl-utils: 0.3.1 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@8.2.0: + dependencies: + chalk: 5.6.2 + cli-cursor: 5.0.0 + cli-spinners: 2.9.2 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 6.0.0 + stdin-discarder: 0.2.2 + string-width: 7.2.0 + strip-ansi: 7.1.2 + + outvariant@1.4.3: {} + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + package-manager-detector@1.6.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.0 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse-ms@4.0.0: {} + + parseurl@1.3.3: {} + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-parse@1.0.7: {} + + path-to-regexp@6.3.0: {} + + path-to-regexp@8.3.0: {} + + pathe@2.0.3: {} + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pkce-challenge@5.0.1: {} + + pkg-types@2.3.0: + dependencies: + confbox: 0.2.4 + exsolve: 1.0.8 + pathe: 2.0.3 + + possible-typed-array-names@1.1.0: {} + + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.4.31: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + powershell-utils@0.1.0: {} + + prelude-ls@1.2.1: {} + + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + + prisma@6.19.2(typescript@5.9.3): + dependencies: + '@prisma/config': 6.19.2 + '@prisma/engines': 6.19.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - magicast + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + property-information@7.1.0: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + punycode@2.3.1: {} + + pure-rand@6.1.0: {} + + qs@6.14.1: + dependencies: + side-channel: 1.1.0 + + queue-microtask@1.2.3: {} + + radix-ui@1.4.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-accessible-icon': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-alert-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-aspect-ratio': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-avatar': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-checkbox': 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-context-menu': 2.2.16(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-form': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-hover-card': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-menubar': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-navigation-menu': 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-one-time-password-field': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-password-toggle-field': 0.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-progress': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-radio-group': 1.3.8(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-scroll-area': 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-select': 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slider': 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-switch': 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-toast': 1.2.15(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-toolbar': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.13)(react@19.2.3) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + '@types/react-dom': 19.2.3(@types/react@19.2.13) + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + rc9@2.1.2: + dependencies: + defu: 6.1.4 + destr: 2.0.5 + + react-dom@19.2.3(react@19.2.3): + dependencies: + react: 19.2.3 + scheduler: 0.27.0 + + react-is@16.13.1: {} + + react-markdown@10.1.0(@types/react@19.2.13)(react@19.2.3): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 19.2.13 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.1 + react: 19.2.3 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + react-remove-scroll-bar@2.3.8(@types/react@19.2.13)(react@19.2.3): + dependencies: + react: 19.2.3 + react-style-singleton: 2.2.3(@types/react@19.2.13)(react@19.2.3) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.13 + + react-remove-scroll@2.7.2(@types/react@19.2.13)(react@19.2.3): + dependencies: + react: 19.2.3 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.13)(react@19.2.3) + react-style-singleton: 2.2.3(@types/react@19.2.13)(react@19.2.3) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.13)(react@19.2.3) + use-sidecar: 1.1.3(@types/react@19.2.13)(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.13 + + react-style-singleton@2.2.3(@types/react@19.2.13)(react@19.2.3): + dependencies: + get-nonce: 1.0.1 + react: 19.2.3 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.13 + + react-use-measure@2.1.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + optionalDependencies: + react-dom: 19.2.3(react@19.2.3) + + react@19.2.3: {} + + readdirp@4.1.2: {} + + recast@0.23.11: + dependencies: + ast-types: 0.16.1 + esprima: 4.0.1 + source-map: 0.6.1 + tiny-invariant: 1.3.3 + tslib: 2.8.1 + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + rehype-autolink-headings@7.1.0: + dependencies: + '@types/hast': 3.0.4 + '@ungap/structured-clone': 1.3.0 + hast-util-heading-rank: 3.0.0 + hast-util-is-element: 3.0.0 + unified: 11.0.5 + unist-util-visit: 5.1.0 + + rehype-highlight@7.0.2: + dependencies: + '@types/hast': 3.0.4 + hast-util-to-text: 4.0.2 + lowlight: 3.3.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + rehype-sanitize@6.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-sanitize: 5.0.2 + + rehype-slug@6.0.0: + dependencies: + '@types/hast': 3.0.4 + github-slugger: 2.0.0 + hast-util-heading-rank: 3.0.0 + hast-util-to-string: 3.0.1 + unist-util-visit: 5.1.0 + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resolve@2.0.0-next.5: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + rettime@0.10.1: {} + + reusify@1.1.0: {} + + rollup@4.57.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.57.1 + '@rollup/rollup-android-arm64': 4.57.1 + '@rollup/rollup-darwin-arm64': 4.57.1 + '@rollup/rollup-darwin-x64': 4.57.1 + '@rollup/rollup-freebsd-arm64': 4.57.1 + '@rollup/rollup-freebsd-x64': 4.57.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.57.1 + '@rollup/rollup-linux-arm-musleabihf': 4.57.1 + '@rollup/rollup-linux-arm64-gnu': 4.57.1 + '@rollup/rollup-linux-arm64-musl': 4.57.1 + '@rollup/rollup-linux-loong64-gnu': 4.57.1 + '@rollup/rollup-linux-loong64-musl': 4.57.1 + '@rollup/rollup-linux-ppc64-gnu': 4.57.1 + '@rollup/rollup-linux-ppc64-musl': 4.57.1 + '@rollup/rollup-linux-riscv64-gnu': 4.57.1 + '@rollup/rollup-linux-riscv64-musl': 4.57.1 + '@rollup/rollup-linux-s390x-gnu': 4.57.1 + '@rollup/rollup-linux-x64-gnu': 4.57.1 + '@rollup/rollup-linux-x64-musl': 4.57.1 + '@rollup/rollup-openbsd-x64': 4.57.1 + '@rollup/rollup-openharmony-arm64': 4.57.1 + '@rollup/rollup-win32-arm64-msvc': 4.57.1 + '@rollup/rollup-win32-ia32-msvc': 4.57.1 + '@rollup/rollup-win32-x64-gnu': 4.57.1 + '@rollup/rollup-win32-x64-msvc': 4.57.1 + fsevents: 2.3.3 + + rou3@0.7.12: {} + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + + run-applescript@7.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safer-buffer@2.1.2: {} + + scheduler@0.27.0: {} + + section-matter@1.0.0: + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + + semver@6.3.1: {} + + semver@7.7.4: {} + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + set-cookie-parser@2.7.2: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + setprototypeof@1.2.0: {} + + shadcn@3.8.4(@types/node@20.19.33)(typescript@5.9.3): + dependencies: + '@antfu/ni': 25.0.0 + '@babel/core': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) + '@dotenvx/dotenvx': 1.52.0 + '@modelcontextprotocol/sdk': 1.26.0(zod@3.25.76) + '@types/validate-npm-package-name': 4.0.2 + browserslist: 4.28.1 + commander: 14.0.3 + cosmiconfig: 9.0.0(typescript@5.9.3) + dedent: 1.7.1 + deepmerge: 4.3.1 + diff: 8.0.3 + execa: 9.6.1 + fast-glob: 3.3.3 + fs-extra: 11.3.3 + fuzzysort: 3.1.0 + https-proxy-agent: 7.0.6 + kleur: 4.1.5 + msw: 2.12.10(@types/node@20.19.33)(typescript@5.9.3) + node-fetch: 3.3.2 + open: 11.0.0 + ora: 8.2.0 + postcss: 8.5.6 + postcss-selector-parser: 7.1.1 + prompts: 2.4.2 + recast: 0.23.11 + stringify-object: 5.0.0 + tailwind-merge: 3.4.0 + ts-morph: 26.0.0 + tsconfig-paths: 4.2.0 + validate-npm-package-name: 7.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.1(zod@3.25.76) + transitivePeerDependencies: + - '@cfworker/json-schema' + - '@types/node' + - babel-plugin-macros + - supports-color + - typescript + + sharp@0.34.5: + dependencies: + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.4 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + sisteransi@1.0.5: {} + + source-map-js@1.2.1: {} + + source-map@0.6.1: {} + + space-separated-tokens@2.0.2: {} + + sprintf-js@1.0.3: {} + + stable-hash@0.0.5: {} + + stackback@0.0.2: {} + + statuses@2.0.2: {} + + std-env@3.10.0: {} + + stdin-discarder@0.2.2: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + strict-event-emitter@0.5.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.4.0 + strip-ansi: 7.1.2 + + string.prototype.includes@2.0.1: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.1 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + stringify-object@5.0.0: + dependencies: + get-own-enumerable-keys: 1.0.0 + is-obj: 3.0.0 + is-regexp: 3.1.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + strip-bom-string@1.0.0: {} + + strip-bom@3.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-final-newline@4.0.0: {} + + strip-json-comments@3.1.1: {} + + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + + styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.3): + dependencies: + client-only: 0.0.1 + react: 19.2.3 + optionalDependencies: + '@babel/core': 7.29.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tagged-tag@1.0.0: {} + + tailwind-merge@3.4.0: {} + + tailwindcss@4.1.18: {} + + tapable@2.3.0: {} + + tiny-invariant@1.3.3: {} + + tinybench@2.9.0: {} + + tinyexec@1.0.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinyrainbow@3.0.3: {} + + tldts-core@7.0.23: {} + + tldts@7.0.23: + dependencies: + tldts-core: 7.0.23 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + tough-cookie@6.0.0: + dependencies: + tldts: 7.0.23 + + trim-lines@3.0.1: {} + + trough@2.2.0: {} + + ts-api-utils@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-morph@26.0.0: + dependencies: + '@ts-morph/common': 0.27.0 + code-block-writer: 13.0.3 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + tsx@4.21.0: + dependencies: + esbuild: 0.27.3 + get-tsconfig: 4.13.6 + optionalDependencies: + fsevents: 2.3.3 + + tw-animate-css@1.4.0: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@5.4.4: + dependencies: + tagged-tag: 1.0.0 + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript-eslint@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.55.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@6.21.0: {} + + unicorn-magic@0.3.0: {} + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-find-after@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + universal-user-agent@7.0.3: {} + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + unrs-resolver@1.11.1: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.11.1 + '@unrs/resolver-binding-android-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-x64': 1.11.1 + '@unrs/resolver-binding-freebsd-x64': 1.11.1 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 + '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-musl': 1.11.1 + '@unrs/resolver-binding-wasm32-wasi': 1.11.1 + '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 + '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 + '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + + until-async@3.0.2: {} + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-callback-ref@1.3.3(@types/react@19.2.13)(react@19.2.3): + dependencies: + react: 19.2.3 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.13 + + use-sidecar@1.1.3(@types/react@19.2.13)(react@19.2.3): + dependencies: + detect-node-es: 1.1.0 + react: 19.2.3 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.13 + + use-sync-external-store@1.6.0(react@19.2.3): + dependencies: + react: 19.2.3 + + util-deprecate@1.0.2: {} + + validate-npm-package-name@7.0.2: {} + + vary@1.1.2: {} + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite@7.3.1(@types/node@20.19.33)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0): + dependencies: + esbuild: 0.27.3 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.57.1 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 20.19.33 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + tsx: 4.21.0 + + vitest@4.0.18(@types/node@20.19.33)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.10(@types/node@20.19.33)(typescript@5.9.3))(tsx@4.21.0): + dependencies: + '@vitest/expect': 4.0.18 + '@vitest/mocker': 4.0.18(msw@2.12.10(@types/node@20.19.33)(typescript@5.9.3))(vite@7.3.1(@types/node@20.19.33)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + '@vitest/pretty-format': 4.0.18 + '@vitest/runner': 4.0.18 + '@vitest/snapshot': 4.0.18 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + es-module-lexer: 1.7.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 7.3.1(@types/node@20.19.33)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.19.33 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + + web-streams-polyfill@3.3.3: {} + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.20 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.20: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + which@4.0.0: + dependencies: + isexe: 3.1.5 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + wsl-utils@0.3.1: + dependencies: + is-wsl: 3.1.0 + powershell-utils: 0.1.0 + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + yoctocolors-cjs@2.1.3: {} + + yoctocolors@2.1.2: {} + + zod-to-json-schema@3.25.1(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod-validation-error@4.0.2(zod@4.3.6): + dependencies: + zod: 4.3.6 + + zod@3.25.76: {} + + zod@4.3.6: {} + + zwitch@2.0.4: {} diff --git a/web/pnpm-workspace.yaml b/web/pnpm-workspace.yaml new file mode 100644 index 00000000..581a9d5b --- /dev/null +++ b/web/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +ignoredBuiltDependencies: + - sharp + - unrs-resolver diff --git a/web/postcss.config.mjs b/web/postcss.config.mjs new file mode 100644 index 00000000..61e36849 --- /dev/null +++ b/web/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/web/prisma.config.ts b/web/prisma.config.ts new file mode 100644 index 00000000..f41957e7 --- /dev/null +++ b/web/prisma.config.ts @@ -0,0 +1,27 @@ +import "dotenv/config"; +import { defineConfig } from "prisma/config"; + +const PLACEHOLDER_DATABASE_URL = + "postgresql://placeholder:placeholder@localhost:5432/placeholder"; +const isGenerateCommand = process.argv.includes("generate"); +const databaseUrl = + process.env.DATABASE_URL ?? + (isGenerateCommand ? PLACEHOLDER_DATABASE_URL : undefined); + +if (!databaseUrl) { + throw new Error( + "DATABASE_URL is required for Prisma commands other than `prisma generate`.", + ); +} + +export default defineConfig({ + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + }, + engine: "classic", + // Fresh installs can run `prisma generate` before a local `.env` exists. + datasource: { + url: databaseUrl, + }, +}); diff --git a/web/prisma/migrations/20260214104350_init/migration.sql b/web/prisma/migrations/20260214104350_init/migration.sql new file mode 100644 index 00000000..f72cd3e7 --- /dev/null +++ b/web/prisma/migrations/20260214104350_init/migration.sql @@ -0,0 +1,101 @@ +-- CreateTable +CREATE TABLE "users" ( + "id" TEXT NOT NULL, + "name" TEXT, + "email" TEXT NOT NULL, + "emailVerified" BOOLEAN NOT NULL DEFAULT false, + "image" TEXT, + "role" TEXT NOT NULL DEFAULT 'user', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "users_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "sessions" ( + "id" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "ipAddress" TEXT, + "userAgent" TEXT, + "userId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "sessions_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "accounts" ( + "id" TEXT NOT NULL, + "accountId" TEXT NOT NULL, + "providerId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "accessToken" TEXT, + "refreshToken" TEXT, + "accessTokenExpiresAt" TIMESTAMP(3), + "refreshTokenExpiresAt" TIMESTAMP(3), + "scope" TEXT, + "idToken" TEXT, + "password" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "accounts_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "verifications" ( + "id" TEXT NOT NULL, + "identifier" TEXT NOT NULL, + "value" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "verifications_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "repos" ( + "id" TEXT NOT NULL, + "owner" TEXT NOT NULL, + "name" TEXT NOT NULL, + "branch" TEXT NOT NULL DEFAULT 'main', + "displayName" TEXT NOT NULL, + "description" TEXT, + "lastSyncedAt" TIMESTAMP(3), + "userId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "repos_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "sessions_token_key" ON "sessions"("token"); + +-- CreateIndex +CREATE INDEX "sessions_userId_idx" ON "sessions"("userId"); + +-- CreateIndex +CREATE INDEX "accounts_userId_idx" ON "accounts"("userId"); + +-- CreateIndex +CREATE INDEX "repos_userId_idx" ON "repos"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "repos_userId_owner_name_key" ON "repos"("userId", "owner", "name"); + +-- AddForeignKey +ALTER TABLE "sessions" ADD CONSTRAINT "sessions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "accounts" ADD CONSTRAINT "accounts_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "repos" ADD CONSTRAINT "repos_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/web/prisma/migrations/20260218171832_add_total_files_to_repo/migration.sql b/web/prisma/migrations/20260218171832_add_total_files_to_repo/migration.sql new file mode 100644 index 00000000..64eb3502 --- /dev/null +++ b/web/prisma/migrations/20260218171832_add_total_files_to_repo/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "repos" ADD COLUMN "totalFiles" INTEGER; diff --git a/web/prisma/migrations/20260401204440_add_source_type_and_local_path/migration.sql b/web/prisma/migrations/20260401204440_add_source_type_and_local_path/migration.sql new file mode 100644 index 00000000..dbeb40fb --- /dev/null +++ b/web/prisma/migrations/20260401204440_add_source_type_and_local_path/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "repos" ADD COLUMN "localPath" TEXT, +ADD COLUMN "sourceType" TEXT NOT NULL DEFAULT 'github'; diff --git a/web/prisma/migrations/migration_lock.toml b/web/prisma/migrations/migration_lock.toml new file mode 100644 index 00000000..044d57cd --- /dev/null +++ b/web/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/web/prisma/schema.prisma b/web/prisma/schema.prisma new file mode 100644 index 00000000..6a6e4152 --- /dev/null +++ b/web/prisma/schema.prisma @@ -0,0 +1,93 @@ +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +generator client { + provider = "prisma-client" + output = "../src/generated/prisma" +} + +model User { + id String @id @default(cuid()) + name String? + email String @unique + emailVerified Boolean @default(false) + image String? + role String @default("user") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + sessions Session[] + accounts Account[] + repos Repo[] + + @@map("users") +} + +model Session { + id String @id @default(cuid()) + token String @unique + expiresAt DateTime + ipAddress String? + userAgent String? + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([userId]) + @@map("sessions") +} + +model Account { + id String @id @default(cuid()) + accountId String + providerId String + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + accessToken String? + refreshToken String? + accessTokenExpiresAt DateTime? + refreshTokenExpiresAt DateTime? + scope String? + idToken String? + password String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([userId]) + @@map("accounts") +} + +model Verification { + id String @id @default(cuid()) + identifier String + value String + expiresAt DateTime + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("verifications") +} + +model Repo { + id String @id @default(cuid()) + owner String + name String + branch String @default("main") + displayName String + description String? + sourceType String @default("github") + localPath String? + totalFiles Int? + lastSyncedAt DateTime? + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([userId, owner, name]) + @@index([userId]) + @@map("repos") +} diff --git a/web/public/file.svg b/web/public/file.svg new file mode 100644 index 00000000..004145cd --- /dev/null +++ b/web/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/globe.svg b/web/public/globe.svg new file mode 100644 index 00000000..567f17b0 --- /dev/null +++ b/web/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/logo_mybmad.png b/web/public/logo_mybmad.png new file mode 100644 index 00000000..9ccefe05 Binary files /dev/null and b/web/public/logo_mybmad.png differ diff --git a/web/public/next.svg b/web/public/next.svg new file mode 100644 index 00000000..5174b28c --- /dev/null +++ b/web/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/vercel.svg b/web/public/vercel.svg new file mode 100644 index 00000000..77053960 --- /dev/null +++ b/web/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/window.svg b/web/public/window.svg new file mode 100644 index 00000000..b2b2a44f --- /dev/null +++ b/web/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/scripts/create-admin.ts b/web/scripts/create-admin.ts new file mode 100644 index 00000000..ef68ffcd --- /dev/null +++ b/web/scripts/create-admin.ts @@ -0,0 +1,87 @@ +/** + * Create an admin user via email/password for first deployment. + * + * Usage: + * pnpm db:create-admin --email admin@example.com --password secret --name Admin + * + * This script uses better-auth's internal sign-up flow so the password is + * hashed with the same algorithm (scrypt by default). It temporarily forces + * ALLOW_REGISTRATION=true for the duration of the script. + */ + +import "dotenv/config"; + +// Force registration on for this script +process.env.ALLOW_REGISTRATION = "true"; + +import { prisma } from "../src/lib/db/client"; +import { auth } from "../src/lib/auth/auth"; + +function parseArgs(): { email: string; password: string; name: string } { + const args = process.argv.slice(2); + let email = ""; + let password = ""; + let name = ""; + + for (let i = 0; i < args.length; i++) { + if (args[i] === "--email" && args[i + 1]) email = args[++i]; + else if (args[i] === "--password" && args[i + 1]) password = args[++i]; + else if (args[i] === "--name" && args[i + 1]) name = args[++i]; + } + + if (!email || !password || !name) { + console.error( + "Usage: pnpm db:create-admin --email --password --name " + ); + process.exit(1); + } + + if (password.length < 8) { + console.error("Password must be at least 8 characters."); + process.exit(1); + } + + return { email, password, name }; +} + +async function main() { + const { email, password, name } = parseArgs(); + + console.log(`Creating admin user: ${email}...`); + + // Check if user already exists + const existing = await prisma.user.findUnique({ where: { email } }); + if (existing) { + console.error(`User with email ${email} already exists.`); + process.exit(1); + } + + // Use better-auth's sign-up API to create the user with properly hashed password + const result = await auth.api.signUpEmail({ + body: { email, password, name }, + headers: new Headers(), + }); + + if (!result?.user?.id) { + console.error("Failed to create user via better-auth.", result); + process.exit(1); + } + + // Promote to admin + await prisma.user.update({ + where: { id: result.user.id }, + data: { role: "admin" }, + }); + + console.log(`Admin user created successfully!`); + console.log(` Name: ${name}`); + console.log(` Email: ${email}`); + console.log(` Role: admin`); + + await prisma.$disconnect(); +} + +main().catch((err) => { + console.error("Fatal error:", err); + process.exit(1); +}); diff --git a/web/scripts/setup.sh b/web/scripts/setup.sh new file mode 100755 index 00000000..f0124048 --- /dev/null +++ b/web/scripts/setup.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +# ============================================================================= +# MyBMAD Dashboard — Quick Setup Script +# ============================================================================= +# Creates a .env file with auto-generated secrets and sensible defaults. +# You only need to fill in the GitHub OAuth credentials manually afterward. +# +# Usage: bash scripts/setup.sh +# ============================================================================= + +set -euo pipefail + +ENV_FILE=".env" +ENV_EXAMPLE=".env.example" + +# Colors (disabled if not a terminal) +if [ -t 1 ]; then + GREEN='\033[0;32m' + YELLOW='\033[1;33m' + CYAN='\033[0;36m' + BOLD='\033[1m' + NC='\033[0m' +else + GREEN='' YELLOW='' CYAN='' BOLD='' NC='' +fi + +echo -e "${BOLD}MyBMAD Dashboard — Environment Setup${NC}" +echo "" + +# ------------------------------------------------------------------ +# 1. Check if .env already exists +# ------------------------------------------------------------------ +if [ -f "$ENV_FILE" ]; then + echo -e "${YELLOW}Warning:${NC} $ENV_FILE already exists." + read -rp "Overwrite it? (y/N) " answer + if [[ ! "$answer" =~ ^[Yy]$ ]]; then + echo "Aborted. Your existing $ENV_FILE was not modified." + exit 0 + fi + echo "" +fi + +# ------------------------------------------------------------------ +# 2. Check .env.example exists +# ------------------------------------------------------------------ +if [ ! -f "$ENV_EXAMPLE" ]; then + echo "Error: $ENV_EXAMPLE not found. Run this script from the project root." + exit 1 +fi + +# ------------------------------------------------------------------ +# 3. Generate secrets +# ------------------------------------------------------------------ +echo -e "${CYAN}Generating secrets...${NC}" + +GENERATED_AUTH_SECRET=$(openssl rand -base64 32) +GENERATED_REVALIDATE_SECRET=$(openssl rand -hex 32) + +echo " BETTER_AUTH_SECRET = (generated)" +echo " REVALIDATE_SECRET = (generated)" +echo "" + +# ------------------------------------------------------------------ +# 4. Create .env from template +# ------------------------------------------------------------------ +cp "$ENV_EXAMPLE" "$ENV_FILE" + +# Platform-compatible sed (macOS vs Linux) +if [[ "$OSTYPE" == "darwin"* ]]; then + SED_INPLACE=(sed -i '') +else + SED_INPLACE=(sed -i) +fi + +"${SED_INPLACE[@]}" "s|^BETTER_AUTH_SECRET=.*|BETTER_AUTH_SECRET=${GENERATED_AUTH_SECRET}|" "$ENV_FILE" +"${SED_INPLACE[@]}" "s|^REVALIDATE_SECRET=.*|REVALIDATE_SECRET=${GENERATED_REVALIDATE_SECRET}|" "$ENV_FILE" +"${SED_INPLACE[@]}" "s|^BETTER_AUTH_URL=.*|BETTER_AUTH_URL=http://localhost:3000|" "$ENV_FILE" +"${SED_INPLACE[@]}" "s|^DATABASE_URL=.*|DATABASE_URL=postgresql://bmad:bmad_dev_password@localhost:5433/bmad_dashboard|" "$ENV_FILE" + +echo -e "${GREEN}Created $ENV_FILE with auto-generated secrets.${NC}" +echo "" + +# ------------------------------------------------------------------ +# 5. Print remaining manual steps +# ------------------------------------------------------------------ +echo -e "${BOLD}Remaining manual steps:${NC}" +echo "" +echo -e " ${CYAN}1.${NC} Create a GitHub OAuth App:" +echo " https://github.com/settings/developers → New OAuth App" +echo "" +echo " Application name: MyBMAD (or anything)" +echo " Homepage URL: http://localhost:3000" +echo " Authorization callback: http://localhost:3000/api/auth/callback/github" +echo "" +echo -e " ${CYAN}2.${NC} Copy the Client ID and Client Secret into ${BOLD}.env${NC}:" +echo " GITHUB_CLIENT_ID=" +echo " GITHUB_CLIENT_SECRET=" +echo "" +echo -e " ${CYAN}3.${NC} (Optional) Create a GitHub PAT for higher rate limits:" +echo " https://github.com/settings/tokens → Generate new token (classic)" +echo " Required scope: public_repo (or repo for private repos)" +echo "" +echo -e " ${CYAN}4.${NC} Start the database and run migrations:" +echo " docker compose up -d postgres" +echo " pnpm db:migrate" +echo "" +echo -e " ${CYAN}5.${NC} Start the dev server:" +echo " pnpm dev" +echo "" +echo -e "${GREEN}Done!${NC} Open http://localhost:3000 after completing the steps above." diff --git a/web/src/actions/admin-actions.ts b/web/src/actions/admin-actions.ts new file mode 100644 index 00000000..ef46027e --- /dev/null +++ b/web/src/actions/admin-actions.ts @@ -0,0 +1,185 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { requireAdmin } from "@/lib/db/helpers"; +import { prisma } from "@/lib/db/client"; +import { z } from "zod"; +import type { ActionResult } from "@/lib/types"; +import { sanitizeError } from "@/lib/errors"; + +// --- Types --- + +export interface AdminUser { + id: string; + name: string | null; + email: string; + image: string | null; + role: string; + createdAt: Date; + _count: { repos: number }; +} + +export interface UsageMetrics { + totalUsers: number; + totalRepos: number; + recentUsers: number; + activeUsersLast30d: number; + parsingErrorRate: number; +} + +// --- Error classes for control flow in transactions --- + +class UserNotFoundError extends Error {} +class LastAdminError extends Error {} + +// --- Server Actions --- + +/** + * Get all users with their repo counts. Admin only. + */ +export async function getUsers(): Promise> { + const authResult = await requireAdmin(); + if (!authResult.success) return authResult; + + try { + const users = await prisma.user.findMany({ + select: { + id: true, + name: true, + email: true, + image: true, + role: true, + createdAt: true, + _count: { select: { repos: true } }, + }, + orderBy: { createdAt: "desc" }, + }); + + return { success: true, data: users }; + } catch (error: unknown) { + return { success: false, error: sanitizeError(error, "DB_ERROR"), code: "DB_ERROR" }; + } +} + +const updateUserRoleSchema = z.object({ + userId: z.string().min(1), + newRole: z.enum(["user", "admin"]), +}); + +/** + * Update a user's role. Admin only. + * Prevents self-demotion and demoting the last admin. + */ +export async function updateUserRole( + input: z.infer +): Promise> { + const authResult = await requireAdmin(); + if (!authResult.success) return authResult; + + const parsed = updateUserRoleSchema.safeParse(input); + if (!parsed.success) { + return { success: false, error: "Invalid data", code: "VALIDATION_ERROR" }; + } + + // Prevent self-demotion (use authResult.data.userId instead of redundant getAuthenticatedSession call) + if (parsed.data.userId === authResult.data.userId) { + return { + success: false, + error: "Cannot change your own role", + code: "SELF_DEMOTION", + }; + } + + try { + const updatedUser = await prisma.$transaction(async (tx) => { + // Prevent demoting the last admin (inside transaction to avoid race condition) + if (parsed.data.newRole === "user") { + const target = await tx.user.findUnique({ + where: { id: parsed.data.userId }, + select: { role: true }, + }); + if (!target) { + throw new UserNotFoundError(); + } + if (target.role === "admin") { + const adminCount = await tx.user.count({ where: { role: "admin" } }); + if (adminCount <= 1) { + throw new LastAdminError(); + } + } + } else { + const target = await tx.user.findUnique({ + where: { id: parsed.data.userId }, + select: { id: true }, + }); + if (!target) { + throw new UserNotFoundError(); + } + } + + return tx.user.update({ + where: { id: parsed.data.userId }, + data: { role: parsed.data.newRole }, + select: { + id: true, + name: true, + email: true, + image: true, + role: true, + createdAt: true, + _count: { select: { repos: true } }, + }, + }); + }); + + revalidatePath("/admin"); + return { success: true, data: updatedUser }; + } catch (error: unknown) { + if (error instanceof UserNotFoundError) { + return { success: false, error: "User not found", code: "NOT_FOUND" }; + } + if (error instanceof LastAdminError) { + return { success: false, error: "Cannot demote the last administrator", code: "LAST_ADMIN" }; + } + return { success: false, error: sanitizeError(error, "DB_ERROR"), code: "DB_ERROR" }; + } +} + +/** + * Get usage metrics. Admin only. + */ +export async function getUsageMetrics(): Promise> { + const authResult = await requireAdmin(); + if (!authResult.success) return authResult; + + try { + const sevenDaysAgo = new Date(); + sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); + const thirtyDaysAgo = new Date(); + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); + + const [totalUsers, totalRepos, recentUsers, activeSessionUsers] = await Promise.all([ + prisma.user.count(), + prisma.repo.count(), + prisma.user.count({ where: { createdAt: { gte: sevenDaysAgo } } }), + prisma.session.findMany({ + where: { createdAt: { gte: thirtyDaysAgo } }, + select: { userId: true }, + distinct: ["userId"], + }), + ]); + + return { + success: true, + data: { + totalUsers, + totalRepos, + recentUsers, + activeUsersLast30d: activeSessionUsers.length, + parsingErrorRate: 0, // MVP : pas de table de logs parsing — retourne 0 + }, + }; + } catch (error: unknown) { + return { success: false, error: sanitizeError(error, "DB_ERROR"), code: "DB_ERROR" }; + } +} diff --git a/web/src/actions/repo-actions.ts b/web/src/actions/repo-actions.ts new file mode 100644 index 00000000..ffa534ee --- /dev/null +++ b/web/src/actions/repo-actions.ts @@ -0,0 +1,982 @@ +"use server"; + +import { revalidatePath, revalidateTag } from "next/cache"; +import { repoTag } from "@/lib/github/cache-tags"; +import { + createUserOctokit, + getGitHubToken, + getCachedUserRepoTree, + getCachedUserRawContent, +} from "@/lib/github/client"; +import { LocalProvider } from "@/lib/content-provider/local-provider"; +import { buildFileTree } from "@/lib/bmad/utils"; +import { parseBmadFile } from "@/lib/bmad/parser"; +import { + getBmadConfig, + resolveBmadOutputDir, + isPathOutsideNestedOutput, + DEFAULT_OUTPUT_DIR, +} from "@/lib/bmad/parse-config"; +import { prisma } from "@/lib/db/client"; +import { getAuthenticatedSession } from "@/lib/db/helpers"; +import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; +import { z } from "zod"; +import { createHash } from "node:crypto"; +import path from "node:path"; +import type { GitHubRepo } from "@/lib/github/types"; +import type { FileTreeNode, ParsedBmadFile } from "@/lib/bmad/types"; +import type { ActionResult } from "@/lib/types"; +import { sanitizeError } from "@/lib/errors"; +import { checkRateLimit } from "@/lib/rate-limit"; + +// GraphQL can handle ~30 repos per query safely (GitHub complexity limits) +const GRAPHQL_BATCH_SIZE = 30; + +const BMAD_CORE = "_bmad"; + +// --------------------------------------------------------------------------- +// Auth helpers +// --------------------------------------------------------------------------- + +/** + * Validate session and retrieve an authenticated Octokit instance. + * For GitHub-only actions. + */ +async function getAuthenticatedOctokit(): Promise< + ActionResult<{ octokit: ReturnType; userId: string }> +> { + const session = await getAuthenticatedSession(); + if (!session) { + return { success: false, error: "Not authenticated", code: "UNAUTHORIZED" }; + } + + const token = await getGitHubToken(session.userId); + if (!token) { + return { + success: false, + error: "GitHub OAuth token not found. Please reconnect.", + code: "TOKEN_MISSING", + }; + } + + return { + success: true, + data: { octokit: createUserOctokit(token), userId: session.userId }, + }; +} + +/** + * Get authenticated user ID only (no GitHub token required). + * For actions that work with both GitHub and local repos. + */ +async function requireAuthenticated(): Promise> { + const session = await getAuthenticatedSession(); + if (!session) { + return { success: false, error: "Not authenticated", code: "UNAUTHORIZED" }; + } + return { success: true, data: { userId: session.userId } }; +} + +// --------------------------------------------------------------------------- +// GitHub-only actions +// --------------------------------------------------------------------------- + +/** + * Phase 1: List repos (fast — no BMAD detection). + */ +export async function listUserRepos(): Promise> { + const authResult = await getAuthenticatedOctokit(); + if (!authResult.success) return authResult; + + const { octokit, userId } = authResult.data; + + if (!checkRateLimit(`list:${userId}`, 30, 60000)) { + return { success: false, error: "Trop de requêtes", code: "RATE_LIMIT" }; + } + + try { + const repos = await octokit.paginate( + octokit.rest.repos.listForAuthenticatedUser, + { per_page: 100, sort: "updated" } + ); + + const mapped: GitHubRepo[] = repos.map((r) => ({ + id: r.id, + fullName: r.full_name, + name: r.name, + owner: r.owner.login, + description: r.description ?? null, + isPrivate: r.private, + updatedAt: r.updated_at ?? "", + defaultBranch: r.default_branch ?? "main", + hasBmad: false, + })); + + return { success: true, data: mapped }; + } catch (error: unknown) { + if ( + typeof error === "object" && + error !== null && + "status" in error && + (error as { status: number }).status === 403 + ) { + return { + success: false, + error: "GitHub rate limit reached. Try again in a few minutes.", + code: "RATE_LIMITED", + }; + } + return { success: false, error: sanitizeError(error, "GITHUB_ERROR"), code: "GITHUB_ERROR" }; + } +} + +/** + * Phase 2: Detect BMAD via GraphQL (batch — ~30 repos per query). + */ +export async function detectBmadRepos( + repoIds: { fullName: string; owner: string; name: string }[] +): Promise>> { + const authResult = await getAuthenticatedOctokit(); + if (!authResult.success) return authResult; + + const { octokit } = authResult.data; + const results: Record = {}; + + for (let i = 0; i < repoIds.length; i += GRAPHQL_BATCH_SIZE) { + const chunk = repoIds.slice(i, i + GRAPHQL_BATCH_SIZE); + + const variables: Record = {}; + const repoFragments = chunk.map((repo, idx) => { + const alias = `repo_${idx}`; + const ownerVar = `$owner_${idx}`; + const nameVar = `$name_${idx}`; + variables[`owner_${idx}`] = repo.owner; + variables[`name_${idx}`] = repo.name; + return `${alias}: repository(owner: ${ownerVar}, name: ${nameVar}) { + bmad: object(expression: "HEAD:_bmad") { __typename } + bmadOutput: object(expression: "HEAD:_bmad-output") { __typename } + }`; + }); + + const variableDeclarations = chunk + .map((_, idx) => `$owner_${idx}: String!, $name_${idx}: String!`) + .join(", "); + + const query = `query BmadDetect(${variableDeclarations}) { ${repoFragments.join("\n")} }`; + + try { + const response: Record< + string, + { bmad: { __typename: string } | null; bmadOutput: { __typename: string } | null } | null + > = await octokit.graphql(query, variables); + + chunk.forEach((repo, idx) => { + const data = response[`repo_${idx}`]; + results[repo.fullName] = !!(data?.bmad || data?.bmadOutput); + }); + } catch (error: unknown) { + const msg = error instanceof Error ? error.message : String(error); + console.warn( + `[detectBmadRepos] GraphQL batch ${i / GRAPHQL_BATCH_SIZE + 1} failed: ${msg}` + ); + for (const repo of chunk) { + results[repo.fullName] = false; + } + } + } + + return { success: true, data: results }; +} + +const importRepoSchema = z.object({ + owner: z.string().min(1).max(255).trim(), + name: z.string().min(1).max(255).trim(), + description: z.string().max(1000).nullable(), + defaultBranch: z.string().min(1).max(255).trim(), + fullName: z.string().min(1).max(512).trim(), +}); + +/** + * Import a GitHub BMAD repo into the user's dashboard. + */ +export async function importRepo(input: { + owner: string; + name: string; + description: string | null; + defaultBranch: string; + fullName: string; +}): Promise< + ActionResult<{ id: string; owner: string; name: string; displayName: string }> +> { + const parsed = importRepoSchema.safeParse(input); + if (!parsed.success) { + return { + success: false, + error: "Invalid data: " + parsed.error.issues[0].message, + code: "VALIDATION_ERROR", + }; + } + const data = parsed.data; + + const authResult = await getAuthenticatedOctokit(); + if (!authResult.success) return authResult; + + const { userId } = authResult.data; + + if (!checkRateLimit(`import:${userId}`, 10, 60000)) { + return { success: false, error: "Trop de requêtes", code: "RATE_LIMIT" }; + } + + try { + const repo = await prisma.repo.create({ + data: { + owner: data.owner, + name: data.name, + branch: data.defaultBranch, + displayName: data.name, + description: data.description, + sourceType: "github", + lastSyncedAt: new Date(), + userId, + }, + select: { id: true, owner: true, name: true, displayName: true }, + }); + + revalidatePath("/(dashboard)"); + return { success: true, data: repo }; + } catch (error: unknown) { + if ( + error instanceof PrismaClientKnownRequestError && + error.code === "P2002" + ) { + return { + success: false, + error: "This repository is already imported.", + code: "DUPLICATE", + }; + } + return { success: false, error: sanitizeError(error, "DB_ERROR"), code: "DB_ERROR" }; + } +} + +// --------------------------------------------------------------------------- +// Source-type-aware actions (GitHub + Local) +// --------------------------------------------------------------------------- + +const deleteRepoSchema = z.object({ + owner: z.string().min(1).max(255).trim(), + name: z.string().min(1).max(255).trim(), +}); + +/** + * Delete an imported repo from the user's dashboard (GitHub or local). + */ +export async function deleteRepo(input: { + owner: string; + name: string; +}): Promise> { + const parsed = deleteRepoSchema.safeParse(input); + if (!parsed.success) { + return { success: false, error: "Invalid data", code: "VALIDATION_ERROR" }; + } + + // F15: Use session auth (no GitHub token required) + const authResult = await requireAuthenticated(); + if (!authResult.success) return authResult; + const { userId } = authResult.data; + + try { + // F5: Always scope by userId + const deleted = await prisma.repo.deleteMany({ + where: { userId, owner: parsed.data.owner, name: parsed.data.name }, + }); + + if (deleted.count === 0) { + return { success: false, error: "Repo not found", code: "NOT_FOUND" }; + } + + revalidatePath("/(dashboard)"); + return { success: true, data: { deleted: true } }; + } catch (error: unknown) { + return { success: false, error: sanitizeError(error, "DB_ERROR"), code: "DB_ERROR" }; + } +} + +const refreshRepoSchema = z.object({ + owner: z.string().min(1).max(255).trim(), + name: z.string().min(1).max(255).trim(), +}); + +/** + * Refresh repo data: re-fetch tree, count BMAD files, update lastSyncedAt. + * Routes by sourceType for GitHub vs Local repos. + */ +export async function refreshRepoData(input: { + owner: string; + name: string; +}): Promise> { + const parsed = refreshRepoSchema.safeParse(input); + if (!parsed.success) { + return { success: false, error: "Invalid data", code: "VALIDATION_ERROR" }; + } + + const authResult = await requireAuthenticated(); + if (!authResult.success) return authResult; + const { userId } = authResult.data; + + try { + // F5: Always scope by userId + const repoConfig = await prisma.repo.findFirst({ + where: { userId, owner: parsed.data.owner, name: parsed.data.name }, + select: { id: true, branch: true, sourceType: true, localPath: true }, + }); + + if (!repoConfig) { + return { success: false, error: "Project not found", code: "NOT_FOUND" }; + } + + if (repoConfig.sourceType === "local") { + return refreshLocalRepo(repoConfig); + } + + return refreshGitHubRepo(parsed.data, repoConfig, userId); + } catch (error: unknown) { + if ( + typeof error === "object" && + error !== null && + "status" in error && + (error as { status: number }).status === 403 + ) { + return { + success: false, + error: "GitHub rate limit reached. Cached data is displayed.", + code: "RATE_LIMITED", + }; + } + return { success: false, error: sanitizeError(error, "GITHUB_ERROR"), code: "GITHUB_ERROR" }; + } +} + +async function refreshLocalRepo( + repoConfig: { id: string; localPath: string | null }, +): Promise> { + if (!repoConfig.localPath) { + return { success: false, error: sanitizeError(null, "FS_ERROR"), code: "FS_ERROR" }; + } + + try { + const provider = new LocalProvider(repoConfig.localPath); + await provider.validateRoot(); + + const initialTree = await provider.getTree(); + const { outputDir, paths } = await resolveBmadOutputDir( + provider, + initialTree.paths, + ); + const totalFiles = paths.filter((p) => p.startsWith(outputDir + "/")).length; + + const now = new Date(); + await prisma.repo.update({ + where: { id: repoConfig.id }, + data: { lastSyncedAt: now, totalFiles }, + }); + + // F8: Revalidate dashboard RSC + revalidatePath("/(dashboard)"); + // F37: No revalidateTag for local repos (no unstable_cache) + + return { success: true, data: { totalFiles, lastSyncedAt: now.toISOString() } }; + } catch (error: unknown) { + const msg = error instanceof Error ? error.message : ""; + if (msg === "PATH_NOT_FOUND" || msg === "LOCAL_DISABLED") { + return { success: false, error: sanitizeError(error, "PATH_STALE"), code: "PATH_STALE" }; + } + return { success: false, error: sanitizeError(error, "FS_ERROR"), code: "FS_ERROR" }; + } +} + +async function refreshGitHubRepo( + input: { owner: string; name: string }, + repoConfig: { id: string; branch: string }, + userId: string, +): Promise> { + const token = await getGitHubToken(userId); + if (!token) { + return { success: false, error: "GitHub OAuth token not found.", code: "TOKEN_MISSING" }; + } + const octokit = createUserOctokit(token); + + revalidateTag(repoTag(input.owner, input.name), "default"); + + // Use the branch already configured for this repo — don't override it + const syncBranch = repoConfig.branch; + + const { data: tree } = await octokit.rest.git.getTree({ + owner: input.owner, + repo: input.name, + tree_sha: syncBranch, + recursive: "1", + }); + + const allPaths = tree.tree + .filter((item) => item.type === "blob" && typeof item.path === "string") + .map((item) => item.path as string); + + const ghProviderShim = { + async getTree() { + return { paths: allPaths, rootDirectories: [] }; + }, + async getFileContent(p: string) { + return getCachedUserRawContent( + octokit, + userId, + input.owner, + input.name, + syncBranch, + p, + ); + }, + async validateRoot() {}, + }; + const { outputDir } = await getBmadConfig(ghProviderShim, allPaths); + + const totalFiles = allPaths.filter((p) => p.startsWith(outputDir + "/")).length; + + const now = new Date(); + await prisma.repo.update({ + where: { id: repoConfig.id }, + data: { lastSyncedAt: now, totalFiles }, + }); + + return { success: true, data: { totalFiles, lastSyncedAt: now.toISOString() } }; +} + +// --------------------------------------------------------------------------- +// BMAD file browsing Server Actions +// --------------------------------------------------------------------------- + +const fetchBmadFilesSchema = z.object({ + owner: z.string().min(1).max(255).trim(), + name: z.string().min(1).max(255).trim(), +}); + +/** + * Fetch the BMAD file tree for a repo. + * Routes by sourceType for GitHub vs Local. + */ +export async function fetchBmadFiles(input: { + owner: string; + name: string; +}): Promise< + ActionResult<{ + fileTree: FileTreeNode[]; + docsTree: FileTreeNode[]; + bmadCoreTree: FileTreeNode[]; + bmadFiles: string[]; + }> +> { + const parsed = fetchBmadFilesSchema.safeParse(input); + if (!parsed.success) { + return { success: false, error: "Invalid data", code: "VALIDATION_ERROR" }; + } + + const authResult = await requireAuthenticated(); + if (!authResult.success) return authResult; + const { userId } = authResult.data; + + // F5: Always scope by userId + const repoConfig = await prisma.repo.findFirst({ + where: { userId, owner: parsed.data.owner, name: parsed.data.name }, + select: { branch: true, sourceType: true, localPath: true }, + }); + if (!repoConfig) { + return { success: false, error: "Project not found", code: "NOT_FOUND" }; + } + + try { + if (repoConfig.sourceType === "local") { + if (!repoConfig.localPath) { + return { success: false, error: sanitizeError(null, "FS_ERROR"), code: "FS_ERROR" }; + } + return fetchBmadFilesLocal(repoConfig.localPath); + } + return fetchBmadFilesGitHub(parsed.data, repoConfig.branch, userId); + } catch (error: unknown) { + const msg = error instanceof Error ? error.message : ""; + if (msg === "PATH_NOT_FOUND" || msg === "LOCAL_DISABLED") { + return { success: false, error: sanitizeError(error, "PATH_STALE"), code: "PATH_STALE" }; + } + if ( + typeof error === "object" && + error !== null && + "status" in error && + (error as { status: number }).status === 403 + ) { + return { + success: false, + error: "GitHub rate limit reached. Cached data is displayed.", + code: "RATE_LIMITED", + }; + } + return { success: false, error: sanitizeError(error, "GITHUB_ERROR"), code: "GITHUB_ERROR" }; + } +} + +async function fetchBmadFilesLocal(localPath: string) { + const provider = new LocalProvider(localPath); + await provider.validateRoot(); + const initialTree = await provider.getTree(); + const { outputDir, paths: allPaths } = await resolveBmadOutputDir( + provider, + initialTree.paths, + ); + + const bmadPaths = allPaths.filter((p) => p.startsWith(outputDir + "/")); + const fileTree = buildFileTree(bmadPaths, outputDir); + + const bmadCorePaths = allPaths.filter((p) => p.startsWith(BMAD_CORE + "/")); + const bmadCoreTree = buildFileTree(bmadCorePaths, BMAD_CORE); + + // F20/F35: Detect docs/ via rootDirectories + const docsFolderName = initialTree.rootDirectories.find( + (d) => d.toLowerCase() === "docs" + ) ?? null; + const docsTree = docsFolderName + ? buildFileTree( + allPaths.filter((p) => p.startsWith(docsFolderName + "/")), + docsFolderName, + ) + : []; + + return { success: true as const, data: { fileTree, docsTree, bmadCoreTree, bmadFiles: bmadPaths } }; +} + +async function fetchBmadFilesGitHub( + input: { owner: string; name: string }, + branch: string, + userId: string, +) { + const token = await getGitHubToken(userId); + if (!token) { + return { success: false as const, error: "GitHub OAuth token not found.", code: "TOKEN_MISSING" }; + } + const octokit = createUserOctokit(token); + + const tree = await getCachedUserRepoTree( + octokit, + userId, + input.owner, + input.name, + branch, + ); + + const allPaths: string[] = tree.tree + .filter((item) => item.type === "blob" && typeof item.path === "string") + .map((item) => item.path as string); + + const ghProviderShim = { + async getTree() { + return { paths: allPaths, rootDirectories: [] }; + }, + async getFileContent(p: string) { + return getCachedUserRawContent( + octokit, + userId, + input.owner, + input.name, + branch, + p, + ); + }, + async validateRoot() {}, + }; + const { outputDir } = await getBmadConfig(ghProviderShim, allPaths); + + const bmadPaths = allPaths.filter((p) => p.startsWith(outputDir + "/")); + const fileTree = buildFileTree(bmadPaths, outputDir); + + const bmadCorePaths = allPaths.filter((p) => p.startsWith(BMAD_CORE + "/")); + const bmadCoreTree = buildFileTree(bmadCorePaths, BMAD_CORE); + + // F20/F35: Detect docs/ via rootDirectories (from tree items) + const docsFolder = tree.tree.find( + (item) => + item.type === "tree" && + !item.path.includes("/") && + item.path.toLowerCase() === "docs", + ); + const docsFolderName = docsFolder?.path ?? null; + const docsTree = docsFolderName + ? buildFileTree( + allPaths.filter((p) => p.startsWith(docsFolderName + "/")), + docsFolderName, + ) + : []; + + return { success: true as const, data: { fileTree, docsTree, bmadCoreTree, bmadFiles: bmadPaths } }; +} + +const fetchFileContentSchema = z.object({ + owner: z.string().min(1).max(255).trim(), + name: z.string().min(1).max(255).trim(), + path: z + .string() + .min(1) + .max(1024) + .trim() + .refine((p) => !p.includes(".."), { message: "Invalid path" }), +}); + +/** + * Fetch individual file content (lazy loading). + * Routes by sourceType for GitHub vs Local. + */ +export async function fetchFileContent(input: { + owner: string; + name: string; + path: string; +}): Promise< + ActionResult<{ + content: string; + contentType: "markdown" | "yaml" | "json" | "text"; + }> +> { + const parsed = fetchFileContentSchema.safeParse(input); + if (!parsed.success) { + return { + success: false, + error: "Invalid data: " + parsed.error.issues[0].message, + code: "VALIDATION_ERROR", + }; + } + + const authResult = await requireAuthenticated(); + if (!authResult.success) return authResult; + const { userId } = authResult.data; + + // F5: Always scope by userId + const repoConfig = await prisma.repo.findFirst({ + where: { userId, owner: parsed.data.owner, name: parsed.data.name }, + select: { branch: true, sourceType: true, localPath: true }, + }); + if (!repoConfig) { + return { success: false, error: "Project not found", code: "NOT_FOUND" }; + } + + const ext = parsed.data.path.split(".").pop()?.toLowerCase() ?? ""; + let contentType: "markdown" | "yaml" | "json" | "text" = "text"; + if (ext === "md") contentType = "markdown"; + else if (ext === "yaml" || ext === "yml") contentType = "yaml"; + else if (ext === "json") contentType = "json"; + + try { + let content: string; + + if (repoConfig.sourceType === "local") { + if (!repoConfig.localPath) { + return { success: false, error: sanitizeError(null, "FS_ERROR"), code: "FS_ERROR" }; + } + const provider = new LocalProvider(repoConfig.localPath); + // The default LocalProvider whitelist covers `_bmad` and `_bmad-output`. + // When the project declares a custom (possibly nested) `output_folder`, + // we extend the whitelist to its top-level segment so the provider can + // read inside it — but the provider only validates by single segment. + // Re-check the requested path here so a nested config like + // `output_folder: custom/out` cannot be used to read `custom/secret.txt`. + const tree = await provider.getTree(); + const { outputDir } = await getBmadConfig(provider, tree.paths); + const requestedPath = parsed.data.path; + if ( + outputDir !== DEFAULT_OUTPUT_DIR && + isPathOutsideNestedOutput(requestedPath, outputDir) + ) { + return { + success: false, + error: sanitizeError(null, "ACCESS_DENIED"), + code: "ACCESS_DENIED", + }; + } + if (outputDir !== DEFAULT_OUTPUT_DIR) { + const topSegment = outputDir.split("/")[0]; + try { + provider.extendBmadDirs(topSegment); + } catch { + // Validation failed — fall through; getFileContent will deny if needed. + } + } + content = await provider.getFileContent(requestedPath); + } else { + const token = await getGitHubToken(userId); + if (!token) { + return { success: false, error: "GitHub OAuth token not found.", code: "TOKEN_MISSING" }; + } + const octokit = createUserOctokit(token); + content = await getCachedUserRawContent( + octokit, + userId, + parsed.data.owner, + parsed.data.name, + repoConfig.branch, + parsed.data.path, + ); + } + + return { success: true, data: { content, contentType } }; + } catch (error: unknown) { + const msg = error instanceof Error ? error.message : ""; + if (msg === "PATH_NOT_FOUND" || msg === "LOCAL_DISABLED") { + return { success: false, error: sanitizeError(error, "PATH_STALE"), code: "PATH_STALE" }; + } + if ( + typeof error === "object" && + error !== null && + "status" in error + ) { + const status = (error as { status: number }).status; + if (status === 403) { + return { success: false, error: "GitHub rate limit reached.", code: "RATE_LIMITED" }; + } + if (status === 404) { + return { success: false, error: "File not found.", code: "NOT_FOUND" }; + } + } + return { success: false, error: sanitizeError(error, "GITHUB_ERROR"), code: "GITHUB_ERROR" }; + } +} + +/** + * Fetch and parse a BMAD file in a single server action call. + */ +export async function fetchParsedFileContent(input: { + owner: string; + name: string; + path: string; +}): Promise> { + const result = await fetchFileContent(input); + if (!result.success) return result; + + const parsed = parseBmadFile(result.data.content, result.data.contentType); + return { success: true, data: parsed }; +} + +// --------------------------------------------------------------------------- +// Local folder import (Task 16) +// --------------------------------------------------------------------------- + +const importLocalFolderSchema = z.object({ + localPath: z + .string() + .min(1) + .max(4096) + .trim() + .refine((p) => !p.includes("\0"), { message: "Invalid path" }) // F12: null bytes + .refine((p) => !/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(p), { message: "Invalid path" }), // F33 + displayName: z.string().min(1).max(255).trim().optional(), +}); + +function shortHash(value: string): string { + return createHash("sha256").update(value).digest("hex").slice(0, 8); +} + +function sanitizeBasename(name: string): string { + return name + .replace(/[^a-z0-9-_]/gi, "-") + .replace(/-+/g, "-") + .toLowerCase(); +} + +/** + * Import a local folder as a BMAD project. + * F2: All FS operations go through LocalProvider (no direct fs calls). + */ +export async function importLocalFolder(input: { + localPath: string; + displayName?: string; +}): Promise< + ActionResult<{ id: string; owner: string; name: string; displayName: string }> +> { + // Guard: feature flag + if (process.env.ENABLE_LOCAL_FS !== "true") { + return { success: false, error: sanitizeError(null, "LOCAL_DISABLED"), code: "LOCAL_DISABLED" }; + } + + const parsed = importLocalFolderSchema.safeParse(input); + if (!parsed.success) { + return { + success: false, + error: "Invalid data: " + parsed.error.issues[0].message, + code: "VALIDATION_ERROR", + }; + } + + const authResult = await requireAuthenticated(); + if (!authResult.success) return authResult; + const { userId } = authResult.data; + + // F3: Rate limit + if (!checkRateLimit(`import-local:${userId}`, 10, 60000)) { + return { success: false, error: "Trop de requêtes", code: "RATE_LIMIT" }; + } + + try { + // F2: Delegate all FS operations to LocalProvider + const provider = new LocalProvider(parsed.data.localPath); + await provider.validateRoot(); + + const providerTree = await provider.getTree(); + + // F36: Check for _bmad or _bmad-output in rootDirectories + const hasBmad = providerTree.rootDirectories.some( + (d) => d === "_bmad" || d === "_bmad-output" + ); + if (!hasBmad) { + return { + success: false, + error: "No _bmad or _bmad-output directory found in this folder.", + code: "NO_BMAD", + }; + } + + // F7/F19/F45: URL-safe name with collision-resistant hash + const rawBasename = path.basename(parsed.data.localPath); + const sanitizedBasename = sanitizeBasename(rawBasename); + const hash = shortHash(parsed.data.localPath); + const repoName = `${sanitizedBasename}-${hash}`; + + // F11: displayName fallback to raw basename + const displayName = parsed.data.displayName ?? rawBasename; + + const { outputDir, paths: scannedPaths } = await resolveBmadOutputDir( + provider, + providerTree.paths, + ); + const bmadOutputCount = scannedPaths.filter((p) => + p.startsWith(outputDir + "/"), + ).length; + + const repo = await prisma.repo.create({ + data: { + owner: "local", + name: repoName, + branch: "local", + displayName, + sourceType: "local", + localPath: parsed.data.localPath, + totalFiles: bmadOutputCount, + lastSyncedAt: new Date(), + userId, + }, + select: { id: true, owner: true, name: true, displayName: true }, + }); + + revalidatePath("/(dashboard)"); + return { success: true, data: repo }; + } catch (error: unknown) { + if ( + error instanceof PrismaClientKnownRequestError && + error.code === "P2002" + ) { + return { + success: false, + error: "This folder is already imported.", + code: "DUPLICATE", + }; + } + const msg = error instanceof Error ? error.message : ""; + if (msg === "PATH_NOT_FOUND") { + return { success: false, error: sanitizeError(error, "PATH_NOT_FOUND"), code: "PATH_NOT_FOUND" }; + } + if (msg === "LOCAL_DISABLED") { + return { success: false, error: sanitizeError(error, "LOCAL_DISABLED"), code: "LOCAL_DISABLED" }; + } + return { success: false, error: sanitizeError(error, "FS_ERROR"), code: "FS_ERROR" }; + } +} + +// --------------------------------------------------------------------------- +// Branch management (GitHub-only) +// --------------------------------------------------------------------------- + +/** + * List available branches for a repo from GitHub. + * F21: Returns error for local repos (no branch concept). + */ +export async function listRepoBranches(input: { + owner: string; + name: string; +}): Promise> { + const authResult = await getAuthenticatedOctokit(); + if (!authResult.success) return authResult; + + const { octokit, userId } = authResult.data; + const parsed = z.object({ owner: z.string(), name: z.string() }).safeParse(input); + if (!parsed.success) { + return { success: false, error: "Invalid input", code: "VALIDATION" }; + } + + // F21: Guard — local repos don't have branches + const repoConfig = await prisma.repo.findFirst({ + where: { userId, owner: parsed.data.owner, name: parsed.data.name }, + select: { sourceType: true }, + }); + if (repoConfig?.sourceType === "local") { + return { success: false, error: "Branch management is not available for local projects", code: "NOT_APPLICABLE" }; + } + + try { + const branches = await octokit.paginate( + octokit.rest.repos.listBranches, + { owner: parsed.data.owner, repo: parsed.data.name, per_page: 100 }, + ); + return { success: true, data: branches.map((b) => b.name) }; + } catch (error: unknown) { + return { success: false, error: sanitizeError(error, "GITHUB_ERROR"), code: "GITHUB_ERROR" }; + } +} + +/** + * Update the tracked branch for a repo. + * F21: Returns error for local repos. + */ +export async function updateRepoBranch(input: { + owner: string; + name: string; + branch: string; +}): Promise> { + const authResult = await getAuthenticatedOctokit(); + if (!authResult.success) return authResult; + + const { userId } = authResult.data; + const parsed = z + .object({ owner: z.string(), name: z.string(), branch: z.string().min(1) }) + .safeParse(input); + if (!parsed.success) { + return { success: false, error: "Invalid input", code: "VALIDATION" }; + } + + // F21: Guard — local repos don't have branches + const repoConfig = await prisma.repo.findFirst({ + where: { userId, owner: parsed.data.owner, name: parsed.data.name }, + select: { id: true, sourceType: true }, + }); + if (!repoConfig) { + return { success: false, error: "Project not found", code: "NOT_FOUND" }; + } + if (repoConfig.sourceType === "local") { + return { success: false, error: "Branch management is not available for local projects", code: "NOT_APPLICABLE" }; + } + + try { + await prisma.repo.update({ + where: { id: repoConfig.id }, + data: { branch: parsed.data.branch }, + }); + + revalidateTag(repoTag(parsed.data.owner, parsed.data.name), "default"); + revalidatePath("/(dashboard)"); + + return { success: true, data: { branch: parsed.data.branch } }; + } catch (error: unknown) { + return { success: false, error: sanitizeError(error, "DB_ERROR"), code: "DB_ERROR" }; + } +} diff --git a/web/src/app/(dashboard)/admin/loading.tsx b/web/src/app/(dashboard)/admin/loading.tsx new file mode 100644 index 00000000..d0a90b3b --- /dev/null +++ b/web/src/app/(dashboard)/admin/loading.tsx @@ -0,0 +1,51 @@ +import { Skeleton } from "@/components/ui/skeleton"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; + +export default function AdminLoading() { + return ( +
+
+ + +
+ + {/* Stats skeleton — 5 cards */} +
+ {Array.from({ length: 5 }).map((_, i) => ( + + +
+
+ + +
+ +
+
+
+ ))} +
+ + {/* Table skeleton */} + + +
+ + +
+
+ +
+ + {Array.from({ length: 5 }).map((_, i) => ( + + ))} +
+
+
+
+ ); +} diff --git a/web/src/app/(dashboard)/admin/page.tsx b/web/src/app/(dashboard)/admin/page.tsx new file mode 100644 index 00000000..032d7371 --- /dev/null +++ b/web/src/app/(dashboard)/admin/page.tsx @@ -0,0 +1,54 @@ +import type { Metadata } from "next"; +import { getAuthenticatedSession } from "@/lib/db/helpers"; +import { redirect } from "next/navigation"; +import { getUsers, getUsageMetrics } from "@/actions/admin-actions"; +import { UsageMetrics } from "@/components/admin/usage-metrics"; +import { UsersTable } from "@/components/admin/users-table"; +import { AlertBanner } from "@/components/shared/alert-banner"; + +export const metadata: Metadata = { + title: "Administration", +}; + +export default async function AdminPage() { + const session = await getAuthenticatedSession(); + if (!session || session.role !== "admin") redirect("/"); + + const [usersResult, metricsResult] = await Promise.all([ + getUsers(), + getUsageMetrics(), + ]); + + return ( +
+
+
+

+ Administration Panel +

+

+ Usage overview and user management. +

+
+ + {metricsResult.success ? ( + + ) : ( + + )} + + {usersResult.success ? ( + + ) : ( + + )} +
+
+ ); +} diff --git a/web/src/app/(dashboard)/layout.tsx b/web/src/app/(dashboard)/layout.tsx new file mode 100644 index 00000000..81a31d18 --- /dev/null +++ b/web/src/app/(dashboard)/layout.tsx @@ -0,0 +1,45 @@ +import { SidebarProvider, SidebarInset } from "@/components/ui/sidebar"; +import { AppSidebar } from "@/components/layout/app-sidebar"; +import { AppHeader } from "@/components/layout/app-header"; +import { BreadcrumbProvider } from "@/contexts/breadcrumb-context"; +import { redirect } from "next/navigation"; +import { + getAuthenticatedSession, + getAuthenticatedRepos, +} from "@/lib/db/helpers"; +import { getGitHubToken } from "@/lib/github/client"; + +export default async function DashboardLayout({ + children, +}: { + children: React.ReactNode; +}) { + const session = await getAuthenticatedSession(); + if (!session) redirect("/login"); + + const repos = await getAuthenticatedRepos(session.userId); + + const localFsEnabled = process.env.ENABLE_LOCAL_FS === "true"; + const hasGitHubOAuth = + !!process.env.GITHUB_CLIENT_ID && !!process.env.GITHUB_CLIENT_SECRET; + const hasGitHubToken = hasGitHubOAuth + ? !!(await getGitHubToken(session.userId)) + : false; + + return ( + + + + + +
{children}
+
+
+
+ ); +} diff --git a/web/src/app/(dashboard)/loading.tsx b/web/src/app/(dashboard)/loading.tsx new file mode 100644 index 00000000..a4edc987 --- /dev/null +++ b/web/src/app/(dashboard)/loading.tsx @@ -0,0 +1,21 @@ +import { Skeleton } from "@/components/ui/skeleton"; + +export default function DashboardLoading() { + return ( +
+
+ {/* Header skeleton */} +
+ + +
+ {/* Content skeleton — generic card grid */} +
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
+
+
+ ); +} diff --git a/web/src/app/(dashboard)/page.tsx b/web/src/app/(dashboard)/page.tsx new file mode 100644 index 00000000..42d1ec29 --- /dev/null +++ b/web/src/app/(dashboard)/page.tsx @@ -0,0 +1,99 @@ +import { redirect } from "next/navigation"; +import { getBmadProject } from "@/lib/bmad/parser"; +import { createUserOctokit, getGitHubToken } from "@/lib/github/client"; +import { createContentProvider } from "@/lib/content-provider"; +import { ReposGrid } from "@/components/dashboard/repos-grid"; +import { GlobalStatsBar } from "@/components/dashboard/global-stats-bar"; +import { + getAuthenticatedUserId, + getAuthenticatedRepos, +} from "@/lib/db/helpers"; +import { AlertBanner } from "@/components/shared/alert-banner"; +import type { BmadProject } from "@/lib/bmad/types"; + +const localFsEnabled = process.env.ENABLE_LOCAL_FS === "true"; + +export default async function DashboardPage() { + const userId = await getAuthenticatedUserId(); + if (!userId) redirect("/login"); + + const repos = await getAuthenticatedRepos(userId); + + // Only fetch GitHub token if at least one repo is GitHub-sourced (F34) + const hasGithubRepos = repos.some((r) => r.sourceType === "github"); + const token = hasGithubRepos ? await getGitHubToken(userId) : null; + const octokit = token ? createUserOctokit(token) : undefined; + + const projects: BmadProject[] = []; + const errors: string[] = []; + const results = await Promise.allSettled( + repos.map((repo) => { + const provider = createContentProvider(repo, octokit, userId); + return getBmadProject(repo, provider); + }) + ); + + for (let i = 0; i < results.length; i++) { + const result = results[i]; + if (result.status === "fulfilled" && result.value !== null) { + projects.push(result.value); + } else if (result.status === "rejected") { + const repo = repos[i]; + const msg = result.reason?.message || String(result.reason); + console.error(`Failed to fetch ${repo.owner}/${repo.name}:`, msg); + errors.push(`${repo.displayName}: ${msg}`); + } + } + + // F44: Separate error messages by source type + const hasGithubErrors = errors.length > 0 && repos.some( + (r, i) => r.sourceType === "github" && results[i].status === "rejected" + ); + const hasLocalErrors = errors.length > 0 && repos.some( + (r, i) => r.sourceType === "local" && results[i].status === "rejected" + ); + + return ( +
+
+
+

Dashboard

+

+ Overview of all your BMAD projects +

+
+ {errors.length > 0 && ( + +
    + {errors.map((err, i) => ( +
  • {err}
  • + ))} +
+ {hasGithubErrors && errors.some((e) => /\b(404|Not Found)\b/i.test(e)) && ( +

+ If the repo is private, try reconnecting via GitHub to renew your OAuth authorization. +

+ )} + {hasLocalErrors && ( +

+ Check that the local folder still exists and is accessible on the server. +

+ )} +
+ )} + {projects.length > 0 && } + +
+
+ ); +} diff --git a/web/src/app/(dashboard)/profile/change-password-form.tsx b/web/src/app/(dashboard)/profile/change-password-form.tsx new file mode 100644 index 00000000..238e064a --- /dev/null +++ b/web/src/app/(dashboard)/profile/change-password-form.tsx @@ -0,0 +1,132 @@ +"use client"; + +import { useState } from "react"; +import { authClient } from "@/lib/auth/auth-client"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Loader2, Lock, Check } from "lucide-react"; + +export function ChangePasswordForm() { + const [currentPassword, setCurrentPassword] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(null); + setSuccess(false); + + if (!currentPassword || !newPassword || !confirmPassword) { + setError("All fields are required."); + return; + } + if (newPassword.length < 8) { + setError("New password must be at least 8 characters."); + return; + } + if (newPassword !== confirmPassword) { + setError("Passwords do not match."); + return; + } + if (currentPassword === newPassword) { + setError("New password must be different from the current one."); + return; + } + + setLoading(true); + try { + const result = await authClient.changePassword({ + currentPassword, + newPassword, + revokeOtherSessions: true, + }); + + if (result.error) { + const code = result.error.status; + setError( + code === 401 + ? "Current password is incorrect." + : "Failed to change password." + ); + setLoading(false); + return; + } + + setSuccess(true); + setCurrentPassword(""); + setNewPassword(""); + setConfirmPassword(""); + } catch { + setError("An unexpected error occurred."); + } finally { + setLoading(false); + } + } + + return ( + + + + + Change Password + + + + {error && ( +
+ {error} +
+ )} + {success && ( +
+ + Password changed successfully. +
+ )} +
+ setCurrentPassword(e.target.value)} + disabled={loading} + autoComplete="current-password" + /> + setNewPassword(e.target.value)} + disabled={loading} + autoComplete="new-password" + /> + setConfirmPassword(e.target.value)} + disabled={loading} + autoComplete="new-password" + /> + +
+
+
+ ); +} diff --git a/web/src/app/(dashboard)/profile/layout.tsx b/web/src/app/(dashboard)/profile/layout.tsx new file mode 100644 index 00000000..1bde1a0e --- /dev/null +++ b/web/src/app/(dashboard)/profile/layout.tsx @@ -0,0 +1,13 @@ +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Mon profil", +}; + +export default function ProfileLayout({ + children, +}: { + children: React.ReactNode; +}) { + return children; +} diff --git a/web/src/app/(dashboard)/profile/page.tsx b/web/src/app/(dashboard)/profile/page.tsx new file mode 100644 index 00000000..e51acafb --- /dev/null +++ b/web/src/app/(dashboard)/profile/page.tsx @@ -0,0 +1,270 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { authClient } from "@/lib/auth/auth-client"; +import { getInitials } from "@/lib/utils"; +import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Card, + CardHeader, + CardTitle, + CardContent, +} from "@/components/ui/card"; +import { Separator } from "@/components/ui/separator"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Calendar, Check, Loader2, Pencil, Shield, User } from "lucide-react"; +import { ChangePasswordForm } from "./change-password-form"; + +export default function ProfilePage() { + const { data: session, isPending } = authClient.useSession(); + const [hasCredentialAccount, setHasCredentialAccount] = useState(false); + const [loadingAccounts, setLoadingAccounts] = useState(true); + + // Profile editing state + const [editingName, setEditingName] = useState(false); + const [nameValue, setNameValue] = useState(""); + const [savingName, setSavingName] = useState(false); + const [nameError, setNameError] = useState(null); + + useEffect(() => { + if (!session?.user?.id) { + setLoadingAccounts(false); + return; + } + setLoadingAccounts(true); + authClient + .listAccounts() + .then((res) => { + if (res.data) { + setHasCredentialAccount( + res.data.some( + (a: { providerId: string }) => a.providerId === "credential" + ) + ); + } + }) + .catch((err) => { + console.error("Failed to load accounts:", err); + }) + .finally(() => setLoadingAccounts(false)); + }, [session?.user?.id]); + + const router = useRouter(); + + if (isPending) { + return ( +
+
+ +
+ + + + + + + + + + + + + + +
+
+
+ ); + } + + if (!session?.user) { + router.push("/login"); + return null; + } + + const { name, email, image, role, createdAt } = session.user; + + const formattedDate = createdAt + ? new Intl.DateTimeFormat("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }).format(new Date(createdAt)) + : null; + + async function handleSaveName() { + const trimmed = nameValue.trim(); + const currentName = session?.user?.name; + if (!trimmed || trimmed === currentName) { + setEditingName(false); + return; + } + setSavingName(true); + setNameError(null); + try { + const result = await authClient.updateUser({ name: trimmed }); + if (result.error) { + setNameError("Failed to update name."); + } else { + setEditingName(false); + } + } catch { + setNameError("An unexpected error occurred."); + } finally { + setSavingName(false); + } + } + + return ( +
+
+

My Profile

+ +
+ {/* Left column — Avatar & Password */} +
+ + + + {image && } + + {getInitials(name)} + + +
+

{name}

+

{email}

+
+ {role && ( + + {role} + + )} +
+
+ + {loadingAccounts ? ( + + + + + + + + + ) : ( + hasCredentialAccount && + )} +
+ + {/* Right column — Profile Information */} + + + + + Profile Information + + + + {/* Name */} +
+ + {nameError && ( +
+ {nameError} +
+ )} + {editingName ? ( +
+ setNameValue(e.target.value)} + disabled={savingName} + autoFocus + onKeyDown={(e) => { + if (e.key === "Enter") handleSaveName(); + if (e.key === "Escape") setEditingName(false); + }} + /> + +
+ ) : ( +
+ {name} + +
+ )} +
+ + + + {/* Email */} +
+ +
+ {email} +
+
+ + + + {/* Role */} +
+ +
+ + {role} +
+
+ + + + {/* Member since */} + {formattedDate && ( +
+ +
+ + {formattedDate} +
+
+ )} +
+
+
+
+
+ ); +} diff --git a/web/src/app/(dashboard)/repo/[owner]/[repo]/docs/page.tsx b/web/src/app/(dashboard)/repo/[owner]/[repo]/docs/page.tsx new file mode 100644 index 00000000..3bc6689f --- /dev/null +++ b/web/src/app/(dashboard)/repo/[owner]/[repo]/docs/page.tsx @@ -0,0 +1,65 @@ +import { redirect, notFound } from "next/navigation"; +import { DocsBrowser } from "@/components/docs/docs-browser"; +import { fetchBmadFiles } from "@/actions/repo-actions"; +import { + getAuthenticatedUserId, + getAuthenticatedRepoConfig, +} from "@/lib/db/helpers"; + +interface DocsPageProps { + params: Promise<{ owner: string; repo: string }>; + searchParams: Promise<{ file?: string }>; +} + +export default async function DocsPage({ + params, + searchParams, +}: DocsPageProps) { + const { owner, repo: repoName } = await params; + const { file: initialFile } = await searchParams; + const userId = await getAuthenticatedUserId(); + if (!userId) redirect("/login"); + + const repoConfig = await getAuthenticatedRepoConfig(userId, owner, repoName); + if (!repoConfig) return notFound(); + + const result = await fetchBmadFiles({ owner, name: repoName }); + + if (!result.success) { + return ( +
+
+

Library

+

+ Browse the project files +

+
+
+

{result.error}

+
+
+ ); + } + + return ( +
+
+

Library

+

+ Browse the project files +

+
+ +
+ ); +} diff --git a/web/src/app/(dashboard)/repo/[owner]/[repo]/epics/loading.tsx b/web/src/app/(dashboard)/repo/[owner]/[repo]/epics/loading.tsx new file mode 100644 index 00000000..9f979cb8 --- /dev/null +++ b/web/src/app/(dashboard)/repo/[owner]/[repo]/epics/loading.tsx @@ -0,0 +1,46 @@ +import { Skeleton } from "@/components/ui/skeleton"; + +export default function EpicsLoading() { + return ( +
+ {/* Title + progress ring */} +
+
+ + +
+ +
+ + {/* Timeline skeleton cards */} +
+ {/* Timeline line */} +
+ + {Array.from({ length: 4 }).map((_, i) => ( +
+ {/* Timeline dot */} +
+ +
+ {/* Card skeleton */} +
+
+
+ + +
+ +
+ + +
+ +
+
+
+ ))} +
+
+ ); +} diff --git a/web/src/app/(dashboard)/repo/[owner]/[repo]/epics/page.tsx b/web/src/app/(dashboard)/repo/[owner]/[repo]/epics/page.tsx new file mode 100644 index 00000000..10644429 --- /dev/null +++ b/web/src/app/(dashboard)/repo/[owner]/[repo]/epics/page.tsx @@ -0,0 +1,43 @@ +import { redirect, notFound } from "next/navigation"; +import { getCachedBmadProject } from "@/lib/bmad/cached-project"; +import { getGitHubToken } from "@/lib/github/client"; +import { EpicsBrowser } from "@/components/epics/epics-browser"; +import { + getAuthenticatedUserId, + getAuthenticatedRepoConfig, +} from "@/lib/db/helpers"; + +interface EpicsPageProps { + params: Promise<{ owner: string; repo: string }>; +} + +export default async function EpicsPage({ params }: EpicsPageProps) { + const { owner, repo: repoName } = await params; + const userId = await getAuthenticatedUserId(); + if (!userId) redirect("/login"); + + const repoConfig = await getAuthenticatedRepoConfig(userId, owner, repoName); + if (!repoConfig) return notFound(); + + const isLocal = repoConfig.sourceType === "local"; + const token = isLocal ? undefined : (await getGitHubToken(userId)) ?? undefined; + const project = await getCachedBmadProject(repoConfig, token, userId); + if (!project) return notFound(); + + const totalEpicProgress = project.epics.length > 0 + ? Math.round( + project.epics.reduce((sum, e) => sum + e.progressPercent, 0) / + project.epics.length + ) + : 0; + + return ( + + ); +} diff --git a/web/src/app/(dashboard)/repo/[owner]/[repo]/error.tsx b/web/src/app/(dashboard)/repo/[owner]/[repo]/error.tsx new file mode 100644 index 00000000..545c50f5 --- /dev/null +++ b/web/src/app/(dashboard)/repo/[owner]/[repo]/error.tsx @@ -0,0 +1,70 @@ +"use client"; + +import { AlertCircle, RefreshCw, ArrowLeft } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import Link from "next/link"; +import { useParams } from "next/navigation"; + +export default function RepoError({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + const params = useParams<{ owner: string }>(); + const isLocal = params?.owner === "local"; + + const is404 = error.message?.includes("404"); + const isAuthError = error.message?.includes("401") || error.message?.includes("403"); + const isPathStale = + error.message?.includes("PATH_STALE") || + error.message?.includes("PATH_NOT_FOUND"); + + return ( +
+ + +
+ +
+ +
+

+ {isPathStale + ? "Local folder not found" + : is404 + ? "Repository not found" + : isAuthError + ? "Authentication required" + : "Failed to load project"} +

+

+ {isPathStale || isLocal + ? "This local folder no longer exists or has been moved. You can remove it from your dashboard." + : is404 + ? "The repository does not exist or is private. Reconnect via GitHub to renew your OAuth authorization." + : isAuthError + ? "Your GitHub token is invalid or lacks the required permissions. Try reconnecting." + : error.message || "An unexpected error occurred while loading the project data."} +

+
+ +
+ + +
+
+
+
+ ); +} diff --git a/web/src/app/(dashboard)/repo/[owner]/[repo]/layout.tsx b/web/src/app/(dashboard)/repo/[owner]/[repo]/layout.tsx new file mode 100644 index 00000000..28338b37 --- /dev/null +++ b/web/src/app/(dashboard)/repo/[owner]/[repo]/layout.tsx @@ -0,0 +1,13 @@ +interface RepoLayoutProps { + children: React.ReactNode; +} + +export default function RepoLayout({ children }: RepoLayoutProps) { + return ( +
+
+ {children} +
+
+ ); +} diff --git a/web/src/app/(dashboard)/repo/[owner]/[repo]/loading.tsx b/web/src/app/(dashboard)/repo/[owner]/[repo]/loading.tsx new file mode 100644 index 00000000..f13afa43 --- /dev/null +++ b/web/src/app/(dashboard)/repo/[owner]/[repo]/loading.tsx @@ -0,0 +1,80 @@ +import { Skeleton } from "@/components/ui/skeleton"; + +export default function RepoOverviewLoading() { + return ( +
+ {/* Header skeleton */} +
+
+ + +
+ +
+ + {/* Stats cards skeleton */} +
+ {Array.from({ length: 4 }).map((_, i) => ( + + ))} +
+ + {/* Velocity metrics skeleton */} +
+ +
+ {Array.from({ length: 4 }).map((_, i) => ( + + ))} +
+
+ + {/* Key artifacts skeleton */} +
+ +
+ {Array.from({ length: 4 }).map((_, i) => ( + + ))} +
+
+ + {/* Epics list skeleton */} +
+ + {Array.from({ length: 4 }).map((_, i) => ( + + ))} +
+ + {/* Sprint summary skeleton */} +
+
+ + +
+
+
+ + +
+ +
+
+ +
+ {Array.from({ length: 3 }).map((_, i) => ( + + ))} +
+
+
+ + {Array.from({ length: 3 }).map((_, i) => ( + + ))} +
+
+
+ ); +} diff --git a/web/src/app/(dashboard)/repo/[owner]/[repo]/page.tsx b/web/src/app/(dashboard)/repo/[owner]/[repo]/page.tsx new file mode 100644 index 00000000..54c72cc8 --- /dev/null +++ b/web/src/app/(dashboard)/repo/[owner]/[repo]/page.tsx @@ -0,0 +1,138 @@ +import { redirect, notFound } from "next/navigation"; +import { getCachedBmadProject } from "@/lib/bmad/cached-project"; +import { getGitHubToken } from "@/lib/github/client"; +import { ProgressRing } from "@/components/shared/progress-ring"; +import { ProjectStatsGrid } from "@/components/dashboard/project-stats-grid"; +import { EpicsList } from "@/components/dashboard/epics-list"; +import { VelocityMetrics } from "@/components/dashboard/velocity-metrics"; +import { KeyArtifactsCard } from "@/components/dashboard/key-artifacts-card"; +import { GitBranch, Clock, FolderOpen } from "lucide-react"; +import { formatRelativeTime } from "@/lib/utils"; +import { DeleteRepoButton } from "@/components/shared/delete-repo-button"; +import { RefreshRepoButton } from "@/components/shared/refresh-repo-button"; +import { RepoSettingsModal } from "@/components/shared/repo-settings-modal"; +import { + getAuthenticatedUserId, + getAuthenticatedRepoConfig, +} from "@/lib/db/helpers"; +import type { FileTreeNode } from "@/lib/bmad/types"; + +interface RepoPageProps { + params: Promise<{ owner: string; repo: string }>; +} + +function extractPlanningArtifacts(fileTree: FileTreeNode[]): FileTreeNode[] { + const planningDir = fileTree.find( + (node) => + node.type === "directory" && + node.name.toLowerCase().includes("planning-artifacts"), + ); + return planningDir?.children ?? []; +} + +function getSprintProgress(project: { + sprintStatus: { stories: { status: string }[] } | null; +}): number | null { + if (!project.sprintStatus) return null; + const stories = project.sprintStatus.stories; + if (stories.length === 0) return null; + const done = stories.filter((s) => s.status === "done").length; + return Math.round((done / stories.length) * 100); +} + +export default async function RepoOverviewPage({ params }: RepoPageProps) { + const { owner, repo: repoName } = await params; + const userId = await getAuthenticatedUserId(); + if (!userId) redirect("/login"); + + const repoConfig = await getAuthenticatedRepoConfig(userId, owner, repoName); + if (!repoConfig) return notFound(); + + const isLocal = repoConfig.sourceType === "local"; + const token = isLocal ? undefined : (await getGitHubToken(userId)) ?? undefined; + const project = await getCachedBmadProject(repoConfig, token, userId); + if (!project) return notFound(); + + const planningArtifacts = extractPlanningArtifacts(project.fileTree); + + return ( +
+ {/* Header */} +
+
+
+

+ {project.displayName} +

+ + {!isLocal && ( + + )} + +
+
+ {isLocal ? ( + <> + + {repoConfig.localPath ?? "Local folder"} + + ) : ( + <> + + + {project.owner}/{project.repo} ({project.branch}) + + + )} + {repoConfig.lastSyncedAt && ( + <> + · + + {formatRelativeTime(repoConfig.lastSyncedAt)} + + )} +
+
+ +
+ + {/* 4 Stats Cards en grille */} + + + {/* Velocity metrics */} + {project.sprintStatus && ( +
+

Velocity Metrics

+ +
+ )} + + {/* Key documents */} + + + {/* Epics list */} + +
+ ); +} diff --git a/web/src/app/(dashboard)/repo/[owner]/[repo]/stories/loading.tsx b/web/src/app/(dashboard)/repo/[owner]/[repo]/stories/loading.tsx new file mode 100644 index 00000000..9a41acdb --- /dev/null +++ b/web/src/app/(dashboard)/repo/[owner]/[repo]/stories/loading.tsx @@ -0,0 +1,48 @@ +import { Skeleton } from "@/components/ui/skeleton"; + +export default function StoriesLoading() { + return ( +
+ {/* Title + subtitle */} +
+ + +
+ + {/* Filters bar + view toggle */} +
+
+ + + +
+ +
+ + {/* Table skeleton */} +
+ {/* Table header */} +
+ + + + + +
+ {/* Table rows */} + {Array.from({ length: 8 }).map((_, i) => ( +
+ + + + + +
+ ))} +
+
+ ); +} diff --git a/web/src/app/(dashboard)/repo/[owner]/[repo]/stories/page.tsx b/web/src/app/(dashboard)/repo/[owner]/[repo]/stories/page.tsx new file mode 100644 index 00000000..89cc7c52 --- /dev/null +++ b/web/src/app/(dashboard)/repo/[owner]/[repo]/stories/page.tsx @@ -0,0 +1,39 @@ +import { redirect, notFound } from "next/navigation"; +import { getCachedBmadProject } from "@/lib/bmad/cached-project"; +import { getGitHubToken } from "@/lib/github/client"; +import { StoriesView } from "@/components/stories/stories-view"; +import { + getAuthenticatedUserId, + getAuthenticatedRepoConfig, +} from "@/lib/db/helpers"; + +interface StoriesPageProps { + params: Promise<{ owner: string; repo: string }>; +} + +export default async function StoriesPage({ params }: StoriesPageProps) { + const { owner, repo: repoName } = await params; + const userId = await getAuthenticatedUserId(); + if (!userId) redirect("/login"); + + const repoConfig = await getAuthenticatedRepoConfig(userId, owner, repoName); + if (!repoConfig) return notFound(); + + const isLocal = repoConfig.sourceType === "local"; + const token = isLocal ? undefined : (await getGitHubToken(userId)) ?? undefined; + const project = await getCachedBmadProject(repoConfig, token, userId); + if (!project) return notFound(); + + return ( +
+
+

Stories

+

+ {project.stories.length} stories across {project.epics.length}{" "} + epics +

+
+ +
+ ); +} diff --git a/web/src/app/api/auth/[...all]/route.ts b/web/src/app/api/auth/[...all]/route.ts new file mode 100644 index 00000000..cb202ef0 --- /dev/null +++ b/web/src/app/api/auth/[...all]/route.ts @@ -0,0 +1,4 @@ +import { auth } from "@/lib/auth/auth"; +import { toNextJsHandler } from "better-auth/next-js"; + +export const { POST, GET } = toNextJsHandler(auth); diff --git a/web/src/app/api/health/route.ts b/web/src/app/api/health/route.ts new file mode 100644 index 00000000..d3174dfe --- /dev/null +++ b/web/src/app/api/health/route.ts @@ -0,0 +1,16 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/db/client"; + +export const dynamic = "force-dynamic"; + +export async function GET() { + try { + await prisma.$queryRaw`SELECT 1`; + return NextResponse.json({ status: "ok", db: "connected" }); + } catch { + return NextResponse.json( + { status: "error", db: "disconnected" }, + { status: 503 } + ); + } +} diff --git a/web/src/app/api/revalidate/route.ts b/web/src/app/api/revalidate/route.ts new file mode 100644 index 00000000..84068c00 --- /dev/null +++ b/web/src/app/api/revalidate/route.ts @@ -0,0 +1,34 @@ +import { revalidateTag } from "next/cache"; +import { NextRequest, NextResponse } from "next/server"; +import { createHash, timingSafeEqual } from "node:crypto"; +import { z } from "zod"; + +const bodySchema = z.object({ + tag: z.string().min(1).max(256), +}); + +function safeEqual(a: string, b: string): boolean { + const hashA = createHash("sha256").update(a).digest(); + const hashB = createHash("sha256").update(b).digest(); + return timingSafeEqual(hashA, hashB); +} + +export async function POST(request: NextRequest) { + const secret = process.env.REVALIDATE_SECRET; + if (!secret) { + return NextResponse.json({ error: "Not configured" }, { status: 503 }); + } + + const provided = request.headers.get("x-revalidate-secret"); + if (!provided || !safeEqual(provided, secret)) { + return NextResponse.json({ error: "Invalid secret" }, { status: 401 }); + } + + const parsed = bodySchema.safeParse(await request.json()); + if (!parsed.success) { + return NextResponse.json({ error: "Invalid body" }, { status: 400 }); + } + + revalidateTag(parsed.data.tag, "default"); + return NextResponse.json({ revalidated: true, tag: parsed.data.tag }); +} diff --git a/web/src/app/globals.css b/web/src/app/globals.css new file mode 100644 index 00000000..503635ce --- /dev/null +++ b/web/src/app/globals.css @@ -0,0 +1,425 @@ +@import "tailwindcss"; +@import "tw-animate-css"; +@import "shadcn/tailwind.css"; +@plugin "@tailwindcss/typography"; +@import "highlight.js/styles/github-dark.min.css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --radius-2xl: calc(var(--radius) + 8px); + --radius-3xl: calc(var(--radius) + 12px); + --radius-4xl: calc(var(--radius) + 16px); + --color-gradient-purple: #A97CF8; + --color-gradient-pink: #F38CB8; + --color-gradient-peach: #FDCC92; + --color-destructive-foreground: var(--destructive-foreground); + --color-info: var(--info); + --color-info-foreground: var(--info-foreground); + --color-success: var(--success); + --color-success-foreground: var(--success-foreground); + --color-warning: var(--warning); + --color-warning-foreground: var(--warning-foreground); + --color-invert: var(--invert); + --color-invert-foreground: var(--invert-foreground); +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.141 0.005 285.823); + --card: oklch(1 0 0); + --card-foreground: oklch(0.141 0.005 285.823); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.141 0.005 285.823); + --primary: oklch(0.21 0.006 285.885); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.967 0.001 286.375); + --secondary-foreground: oklch(0.21 0.006 285.885); + --muted: oklch(0.967 0.001 286.375); + --muted-foreground: oklch(0.552 0.016 285.938); + --accent: oklch(0.967 0.001 286.375); + --accent-foreground: oklch(0.21 0.006 285.885); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.92 0.004 286.32); + --input: oklch(0.92 0.004 286.32); + --ring: oklch(0.705 0.015 286.067); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(96.7% 0.001 286.375); + --sidebar-foreground: oklch(0.141 0.005 285.823); + --sidebar-primary: oklch(0.21 0.006 285.885); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.967 0.001 286.375); + --sidebar-accent-foreground: oklch(0.21 0.006 285.885); + --sidebar-border: oklch(0.92 0.004 286.32); + --sidebar-ring: oklch(0.705 0.015 286.067); + --destructive-foreground: oklch(0.444 0.177 26.899); + --info: oklch(0.606 0.215 269.532); + --info-foreground: oklch(0.444 0.177 267.089); + --success: oklch(0.648 0.2 156.114); + --success-foreground: oklch(0.448 0.145 155.044); + --warning: oklch(0.768 0.189 79.762); + --warning-foreground: oklch(0.554 0.135 66.442); + --invert: oklch(0.21 0.006 285.885); + --invert-foreground: oklch(0.985 0 0); + --dot-grid-color: oklch(0.55 0 0 / 8%); +} + +.dark { + --background: oklch(0.141 0.005 285.823); + --foreground: oklch(0.985 0 0); + --card: oklch(0.21 0.006 285.885); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.21 0.006 285.885); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.92 0.004 286.32); + --primary-foreground: oklch(0.21 0.006 285.885); + --secondary: oklch(0.274 0.006 286.033); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.274 0.006 286.033); + --muted-foreground: oklch(0.705 0.015 286.067); + --accent: oklch(0.274 0.006 286.033); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.552 0.016 285.938); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.21 0.006 285.885); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.274 0.006 286.033); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.552 0.016 285.938); + --destructive-foreground: oklch(0.704 0.191 22.216); + --info: oklch(0.606 0.215 269.532); + --info-foreground: oklch(0.746 0.16 270.396); + --success: oklch(0.648 0.2 156.114); + --success-foreground: oklch(0.765 0.166 156.525); + --warning: oklch(0.768 0.189 79.762); + --warning-foreground: oklch(0.828 0.143 78.604); + --invert: oklch(0.985 0 0); + --invert-foreground: oklch(0.21 0.006 285.885); + --dot-grid-color: oklch(1 0 0 / 4%); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + background-image: repeating-linear-gradient( + 45deg, + var(--dot-grid-color) 0, + transparent 1px, + transparent 0, + transparent 50% + ); + background-size: 12px 12px; + background-attachment: fixed; + } +} + +/* Glassmorphism utilities */ +.glass { + @apply bg-card/80 dark:bg-[oklch(0.27_0.006_286/75%)] backdrop-blur-lg border border-border/30 dark:border-[oklch(1_0_0/10%)]; +} + +.glass-card { + @apply bg-card/80 dark:bg-[oklch(0.27_0.006_286/75%)] backdrop-blur-lg border border-border/30 dark:border-[oklch(1_0_0/10%)] rounded-xl shadow-sm; +} + +/* Gradient mesh background */ +.mesh-gradient { + background-color: var(--sidebar); + border: 1px solid var(--sidebar-border); + padding: 0 15px; + margin: 0; + border-radius: 0.5rem; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); +} + +/* Custom font variable override */ +@theme inline { + --font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif; +} + +/* Prose overrides for Obsidian-like markdown rendering */ +.prose h1 { + padding-bottom: 0.3em; + border-bottom: 1px solid oklch(0.5 0 0 / 15%); +} + +.prose h2 { + padding-bottom: 0.25em; + border-bottom: 1px solid oklch(0.5 0 0 / 10%); +} + +.prose blockquote { + border-left: 3px solid oklch(0.6 0.15 250); + background: oklch(0.6 0.15 250 / 5%); + border-radius: 0 0.5rem 0.5rem 0; + padding: 0.5em 1em; +} + +.dark .prose blockquote { + border-left-color: oklch(0.65 0.18 250); + background: oklch(0.65 0.18 250 / 8%); +} + +.prose :not(pre) > code { + background: oklch(0.5 0 0 / 8%); + padding: 0.15em 0.4em; + border-radius: 0.3em; + font-size: 0.875em; +} + +.dark .prose :not(pre) > code { + background: oklch(1 0 0 / 10%); +} + +.prose pre { + border-radius: 0.5rem; + border: 1px solid oklch(0.5 0 0 / 10%); +} + +.dark .prose pre { + border-color: oklch(1 0 0 / 8%); +} + +.prose table { + border-collapse: collapse; +} + +.prose thead th { + background: oklch(0.5 0 0 / 5%); +} + +.dark .prose thead th { + background: oklch(1 0 0 / 5%); +} + +.prose tbody tr:nth-child(even) { + background: oklch(0.5 0 0 / 3%); +} + +.dark .prose tbody tr:nth-child(even) { + background: oklch(1 0 0 / 3%); +} + +.prose li:has(> input[type="checkbox"]) { + list-style: none; + margin-left: -1.5em; +} + +/* Heading anchor links */ +.prose :is(h1, h2, h3, h4)[id] { + scroll-margin-top: 6rem; +} + +.prose :is(h1, h2, h3, h4) a.autolink-heading { + color: var(--muted-foreground); + text-decoration: none; + opacity: 0; + transition: opacity 300ms; + margin-right: 0.25em; +} + +.prose :is(h1, h2, h3, h4):hover a.autolink-heading { + opacity: 1; +} + +.prose :is(h1, h2, h3, h4) a.autolink-heading::before { + content: "#"; +} + +/* GitHub-style callouts */ +.callout { + border-left: 3px solid; + border-radius: 0 0.5rem 0.5rem 0; + padding: 0.75rem 1rem; + margin: 1rem 0; +} + +.callout-title { + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 600; + font-size: 0.875rem; + margin-bottom: 0.25rem; +} + +.callout-content p { + margin: 0.25rem 0; + font-size: 0.875rem; +} + +.callout-note { + border-left-color: oklch(0.6 0.15 250); + background: oklch(0.6 0.15 250 / 5%); +} + +.callout-note .callout-title { + color: oklch(0.6 0.15 250); +} + +.callout-tip { + border-left-color: oklch(0.6 0.15 150); + background: oklch(0.6 0.15 150 / 5%); +} + +.callout-tip .callout-title { + color: oklch(0.6 0.15 150); +} + +.callout-important { + border-left-color: oklch(0.6 0.15 300); + background: oklch(0.6 0.15 300 / 5%); +} + +.callout-important .callout-title { + color: oklch(0.6 0.15 300); +} + +.callout-warning { + border-left-color: oklch(0.7 0.15 80); + background: oklch(0.7 0.15 80 / 5%); +} + +.callout-warning .callout-title { + color: oklch(0.7 0.15 80); +} + +.callout-caution { + border-left-color: oklch(0.6 0.15 25); + background: oklch(0.6 0.15 25 / 5%); +} + +.callout-caution .callout-title { + color: oklch(0.6 0.15 25); +} + +.dark .callout-note { + border-left-color: oklch(0.65 0.18 250); + background: oklch(0.65 0.18 250 / 8%); +} + +.dark .callout-note .callout-title { + color: oklch(0.7 0.18 250); +} + +.dark .callout-tip { + border-left-color: oklch(0.65 0.18 150); + background: oklch(0.65 0.18 150 / 8%); +} + +.dark .callout-tip .callout-title { + color: oklch(0.7 0.18 150); +} + +.dark .callout-important { + border-left-color: oklch(0.65 0.18 300); + background: oklch(0.65 0.18 300 / 8%); +} + +.dark .callout-important .callout-title { + color: oklch(0.7 0.18 300); +} + +.dark .callout-warning { + border-left-color: oklch(0.75 0.18 80); + background: oklch(0.75 0.18 80 / 8%); +} + +.dark .callout-warning .callout-title { + color: oklch(0.8 0.18 80); +} + +.dark .callout-caution { + border-left-color: oklch(0.65 0.18 25); + background: oklch(0.65 0.18 25 / 8%); +} + +.dark .callout-caution .callout-title { + color: oklch(0.7 0.18 25); +} + +/* Scope highlight.js theme to code blocks only */ +.hljs { + background: oklch(0.15 0.005 285) !important; +} + +.dark .hljs { + background: oklch(0.13 0.005 285) !important; +} + +/* Respect reduced motion preferences */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + .glass, + .glass-card { + backdrop-filter: none; + } +} + +::view-transition-old(root), +::view-transition-new(root) { + animation: none; + mix-blend-mode: normal; +} diff --git a/web/src/app/icon.png b/web/src/app/icon.png new file mode 100644 index 00000000..9ccefe05 Binary files /dev/null and b/web/src/app/icon.png differ diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx new file mode 100644 index 00000000..72781e61 --- /dev/null +++ b/web/src/app/layout.tsx @@ -0,0 +1,31 @@ +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import { ThemeProvider } from "next-themes"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import "./globals.css"; + +const inter = Inter({ + subsets: ["latin"], + variable: "--font-inter", +}); + +export const metadata: Metadata = { + title: "MyBMAD Dashboard", + description: "Visualize BMAD projects from GitHub", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + {children} + + + + ); +} diff --git a/web/src/app/login/login-form.tsx b/web/src/app/login/login-form.tsx new file mode 100644 index 00000000..9535aed9 --- /dev/null +++ b/web/src/app/login/login-form.tsx @@ -0,0 +1,197 @@ +"use client"; + +import { useState } from "react"; +import { useSearchParams, useRouter } from "next/navigation"; +import { authClient } from "@/lib/auth/auth-client"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Github, Loader2, Mail } from "lucide-react"; +import Image from "next/image"; + +interface LoginFormProps { + githubEnabled: boolean; + registrationEnabled: boolean; +} + +export function LoginForm({ githubEnabled, registrationEnabled }: LoginFormProps) { + const [loading, setLoading] = useState(false); + const [isSignUp, setIsSignUp] = useState(false); + const [error, setError] = useState(null); + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const searchParams = useSearchParams(); + const router = useRouter(); + const urlError = searchParams.get("error"); + + async function handleEmailSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(null); + + if (!email || !password) { + setError("Email et mot de passe requis."); + return; + } + if (password.length < 8) { + setError("Le mot de passe doit contenir au moins 8 caractères."); + return; + } + if (isSignUp && !name.trim()) { + setError("Le nom est requis pour l'inscription."); + return; + } + + setLoading(true); + try { + if (isSignUp) { + const result = await authClient.signUp.email({ + email, + password, + name: name.trim(), + }); + if (result.error) { + const code = result.error.status; + setError( + code === 403 + ? "L'inscription est désactivée sur ce serveur." + : code === 422 + ? "Un compte existe déjà avec cet email." + : "Erreur lors de l'inscription." + ); + setLoading(false); + return; + } + } else { + const result = await authClient.signIn.email({ + email, + password, + }); + if (result.error) { + setError("Email ou mot de passe incorrect."); + setLoading(false); + return; + } + } + router.push("/"); + } catch { + setError("Une erreur inattendue s'est produite."); + setLoading(false); + } + } + + const displayError = error ?? (urlError ? "Échec de la connexion. Veuillez réessayer." : null); + + return ( + + + MyBMAD + MyBMAD + + {isSignUp ? "Créer un compte" : "Connectez-vous à votre dashboard"} + + + + {displayError && ( +
+ {displayError} +
+ )} + +
+ {isSignUp && ( + setName(e.target.value)} + disabled={loading} + autoComplete="name" + /> + )} + setEmail(e.target.value)} + disabled={loading} + autoComplete="email" + /> + setPassword(e.target.value)} + disabled={loading} + autoComplete={isSignUp ? "new-password" : "current-password"} + /> + +
+ + {registrationEnabled && ( + + )} + + {githubEnabled && ( + <> +
+
+ ou +
+
+ + + )} + + + ); +} diff --git a/web/src/app/login/page.tsx b/web/src/app/login/page.tsx new file mode 100644 index 00000000..795ddc99 --- /dev/null +++ b/web/src/app/login/page.tsx @@ -0,0 +1,24 @@ +import { Suspense } from "react"; +import { LoginForm } from "./login-form"; + +export const dynamic = "force-dynamic"; + +export default function LoginPage() { + const githubEnabled = + !!process.env.GITHUB_CLIENT_ID && !!process.env.GITHUB_CLIENT_SECRET; + const registrationEnabled = process.env.ALLOW_REGISTRATION === "true"; + + return ( +
+ + + +

+ Made with ❤️ by Hichem +

+
+ ); +} diff --git a/web/src/components/admin/role-manager.tsx b/web/src/components/admin/role-manager.tsx new file mode 100644 index 00000000..cc0ab9e5 --- /dev/null +++ b/web/src/components/admin/role-manager.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { useState, useTransition } from "react"; +import { useRouter } from "next/navigation"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { updateUserRole } from "@/actions/admin-actions"; + +interface RoleManagerProps { + userId: string; + currentRole: string; + currentUserId: string; + userName?: string | null; +} + +export function RoleManager({ + userId, + currentRole, + currentUserId, + userName, +}: RoleManagerProps) { + const [error, setError] = useState(null); + const [isPending, startTransition] = useTransition(); + const router = useRouter(); + const isSelf = userId === currentUserId; + + function handleRoleChange(newRole: "user" | "admin") { + setError(null); + startTransition(async () => { + const result = await updateUserRole({ userId, newRole }); + if (!result.success) { + setError(result.error); + } else { + router.refresh(); + } + }); + } + + return ( +
+ + {error && ( +

+ {error} +

+ )} +
+ ); +} diff --git a/web/src/components/admin/usage-metrics.tsx b/web/src/components/admin/usage-metrics.tsx new file mode 100644 index 00000000..1573d68b --- /dev/null +++ b/web/src/components/admin/usage-metrics.tsx @@ -0,0 +1,60 @@ +import { Users, FolderGit2, UserPlus, Activity, AlertTriangle } from "lucide-react"; +import { StatsCard } from "@/components/shared/stats-card"; +import { StaggeredList, StaggeredItem } from "@/components/shared/staggered-list"; +import type { UsageMetrics as UsageMetricsData } from "@/actions/admin-actions"; + +export function UsageMetrics({ + totalUsers, + totalRepos, + recentUsers, + activeUsersLast30d, + parsingErrorRate, +}: UsageMetricsData) { + return ( + + + + + + + + + + + + + + + + + + ); +} diff --git a/web/src/components/admin/users-table.tsx b/web/src/components/admin/users-table.tsx new file mode 100644 index 00000000..cce5d897 --- /dev/null +++ b/web/src/components/admin/users-table.tsx @@ -0,0 +1,184 @@ +"use client"; + +import { useState, useMemo } from "react"; +import { + useReactTable, + getCoreRowModel, + getSortedRowModel, + getFilteredRowModel, + type ColumnDef, + type SortingState, +} from "@tanstack/react-table"; +import { Search } from "lucide-react"; +import { + DataGrid, + DataGridContainer, +} from "@/components/reui/data-grid/data-grid"; +import { DataGridTable } from "@/components/reui/data-grid/data-grid-table"; +import { DataGridColumnHeader } from "@/components/reui/data-grid/data-grid-column-header"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Input } from "@/components/ui/input"; +import { RoleManager } from "@/components/admin/role-manager"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { getInitials } from "@/lib/utils"; +import type { AdminUser } from "@/actions/admin-actions"; + +interface UsersTableProps { + users: AdminUser[]; + currentUserId: string; +} + +function formatDate(date: string | Date): string { + return new Date(date).toLocaleDateString("en-US", { + day: "numeric", + month: "short", + year: "numeric", + }); +} + +function makeColumns(currentUserId: string): ColumnDef[] { + return [ + { + accessorKey: "name", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const user = row.original; + return ( +
+ + {user.image && ( + + )} + + {getInitials(user.name, user.email[0]?.toUpperCase() ?? "?")} + + + + {user.name ?? "Unnamed"} + +
+ ); + }, + filterFn: (row, _columnId, filterValue: string) => { + const q = filterValue.toLowerCase(); + const user = row.original; + return ( + (user.name?.toLowerCase().includes(q) ?? false) || + user.email.toLowerCase().includes(q) + ); + }, + }, + { + accessorKey: "email", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + {row.getValue("email")} + ), + }, + { + accessorKey: "role", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const user = row.original; + return ( + + ); + }, + }, + { + accessorKey: "_count.repos", + header: "Repos", + cell: ({ row }) => ( + + {row.original._count.repos} + + ), + enableSorting: false, + }, + { + accessorKey: "createdAt", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + {formatDate(row.getValue("createdAt"))} + + ), + sortingFn: "datetime", + }, + ]; +} + +export function UsersTable({ users, currentUserId }: UsersTableProps) { + const [search, setSearch] = useState(""); + const [sorting, setSorting] = useState([ + { id: "createdAt", desc: true }, + ]); + + const columns = useMemo(() => makeColumns(currentUserId), [currentUserId]); + + const columnFilters = useMemo( + () => (search ? [{ id: "name", value: search }] : []), + [search] + ); + + const table = useReactTable({ + data: users, + columns, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onSortingChange: setSorting, + state: { + sorting, + columnFilters, + }, + }); + + return ( + + +
+ Users +
+ + setSearch(e.target.value)} + className="pl-9" + /> +
+
+
+ + + + + + + +
+ ); +} diff --git a/web/src/components/animate-ui/components/buttons/github-stars.tsx b/web/src/components/animate-ui/components/buttons/github-stars.tsx new file mode 100644 index 00000000..e622ef54 --- /dev/null +++ b/web/src/components/animate-ui/components/buttons/github-stars.tsx @@ -0,0 +1,113 @@ +import * as React from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { StarIcon } from 'lucide-react'; + +import { + Button as ButtonPrimitive, + type ButtonProps as ButtonPrimitiveProps, +} from '@/components/animate-ui/primitives/buttons/button'; +import { + GithubStars, + GithubStarsIcon, + GithubStarsLogo, + GithubStarsNumber, + GithubStarsParticles, + type GithubStarsProps, +} from '@/components/animate-ui/primitives/animate/github-stars'; +import { cn } from '@/lib/utils'; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[box-shadow,_color,_background-color,_border-color,_outline-color,_text-decoration-color,_fill,_stroke] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90', + accent: 'bg-accent text-accent-foreground shadow-xs hover:bg-accent/90', + outline: + 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50', + ghost: + 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', + }, + size: { + default: 'h-9 px-4 py-2 has-[>svg]:px-3', + sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', + lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +); + +const buttonStarVariants = cva('', { + variants: { + variant: { + default: + 'fill-neutral-700 stroke-neutral-700 dark:fill-neutral-300 dark:stroke-neutral-300', + accent: + 'fill-neutral-300 stroke-neutral-300 dark:fill-neutral-700 dark:stroke-neutral-700', + outline: + 'fill-neutral-300 stroke-neutral-300 dark:fill-neutral-700 dark:stroke-neutral-700', + ghost: + 'fill-neutral-300 stroke-neutral-300 dark:fill-neutral-700 dark:stroke-neutral-700', + }, + }, + defaultVariants: { + variant: 'default', + }, +}); + +type GitHubStarsButtonProps = Omit< + ButtonPrimitiveProps & GithubStarsProps, + 'asChild' | 'children' +> & + VariantProps; + +function GitHubStarsButton({ + className, + username, + repo, + value, + delay, + inView, + inViewMargin, + inViewOnce, + variant, + size, + ...props +}: GitHubStarsButtonProps) { + return ( + + + + + + + + + + ); +} + +export { GitHubStarsButton, type GitHubStarsButtonProps }; diff --git a/web/src/components/animate-ui/primitives/animate/github-stars.tsx b/web/src/components/animate-ui/primitives/animate/github-stars.tsx new file mode 100644 index 00000000..63a7f050 --- /dev/null +++ b/web/src/components/animate-ui/primitives/animate/github-stars.tsx @@ -0,0 +1,231 @@ +'use client'; + +import * as React from 'react'; +import { motion, type HTMLMotionProps } from 'motion/react'; + +import { + useIsInView, + type UseIsInViewOptions, +} from '@/hooks/use-is-in-view'; +import { getStrictContext } from '@/lib/get-strict-context'; +import { Slot, type WithAsChild } from '@/components/animate-ui/primitives/animate/slot'; +import { + SlidingNumber, + type SlidingNumberProps, +} from '@/components/animate-ui/primitives/texts/sliding-number'; +import { + Particles, + ParticlesEffect, + type ParticlesEffectProps, +} from '@/components/animate-ui/primitives/effects/particles'; +import { cn } from '@/lib/utils'; + +type GithubStarsContextType = { + stars: number; + setStars: (stars: number) => void; + currentStars: number; + setCurrentStars: (stars: number) => void; + isCompleted: boolean; + isLoading: boolean; +}; + +const [GithubStarsProvider, useGithubStars] = + getStrictContext('GithubStarsContext'); + +type GithubStarsProps = WithAsChild< + { + children: React.ReactNode; + username?: string; + repo?: string; + value?: number; + delay?: number; + } & UseIsInViewOptions & + HTMLMotionProps<'div'> +>; + +function GithubStars({ + ref, + children, + username, + repo, + value, + delay = 0, + inView = false, + inViewMargin = '0px', + inViewOnce = true, + asChild = false, + ...props +}: GithubStarsProps) { + const { ref: localRef, isInView } = useIsInView( + ref as React.Ref, + { inView, inViewOnce, inViewMargin }, + ); + + const [stars, setStars] = React.useState(value ?? 0); + const [currentStars, setCurrentStars] = React.useState(0); + const [isLoading, setIsLoading] = React.useState(true); + const isCompleted = React.useMemo( + () => currentStars === stars, + [currentStars, stars], + ); + + const Component = asChild ? Slot : motion.div; + + React.useEffect(() => { + if (value !== undefined && username && repo) return; + if (!isInView) { + setStars(0); + setIsLoading(true); + return; + } + + const timeout = setTimeout(() => { + fetch(`https://api.github.com/repos/${username}/${repo}`) + .then((response) => response.json()) + .then((data) => { + if (data && typeof data.stargazers_count === 'number') { + setStars(data.stargazers_count); + } + }) + .catch(console.error) + .finally(() => setIsLoading(false)); + }, delay); + + return () => clearTimeout(timeout); + }, [username, repo, value, isInView, delay]); + + return ( + + {!isLoading && ( + + {children} + + )} + + ); +} + +type GithubStarsNumberProps = Omit; + +function GithubStarsNumber({ + padStart = true, + ...props +}: GithubStarsNumberProps) { + const { stars, setCurrentStars } = useGithubStars(); + + return ( + + ); +} + +type GithubStarsIconProps = { + icon: React.ReactElement; + color?: string; + activeClassName?: string; +} & React.ComponentProps; + +function GithubStarsIcon({ + icon: Icon, + color = 'currentColor', + activeClassName, + className, + ...props +}: GithubStarsIconProps) { + const { stars, currentStars, isCompleted } = useGithubStars(); + const fillPercentage = (currentStars / stars) * 100; + + return ( +
+
+ ); +} + +type GithubStarsParticlesProps = ParticlesEffectProps & { + children: React.ReactElement; + size?: number; +}; + +function GithubStarsParticles({ + children, + size = 4, + style, + ...props +}: GithubStarsParticlesProps) { + const { isCompleted } = useGithubStars(); + + return ( + + {children} + + + ); +} + +type GithubStarsLogoProps = React.SVGProps; + +function GithubStarsLogo(props: GithubStarsLogoProps) { + return ( + + + + ); +} + +export { + GithubStars, + GithubStarsNumber, + GithubStarsIcon, + GithubStarsParticles, + GithubStarsLogo, + useGithubStars, + type GithubStarsProps, + type GithubStarsNumberProps, + type GithubStarsIconProps, + type GithubStarsParticlesProps, + type GithubStarsLogoProps, + type GithubStarsContextType, +}; diff --git a/web/src/components/animate-ui/primitives/animate/slot.tsx b/web/src/components/animate-ui/primitives/animate/slot.tsx new file mode 100644 index 00000000..3142cc40 --- /dev/null +++ b/web/src/components/animate-ui/primitives/animate/slot.tsx @@ -0,0 +1,96 @@ +'use client'; + +import * as React from 'react'; +import { motion, isMotionComponent, type HTMLMotionProps } from 'motion/react'; +import { cn } from '@/lib/utils'; + +type AnyProps = Record; + +type DOMMotionProps = Omit< + HTMLMotionProps, + 'ref' +> & { ref?: React.Ref }; + +type WithAsChild = + | (Base & { asChild: true; children: React.ReactElement }) + | (Base & { asChild?: false | undefined }); + +type SlotProps = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + children?: any; +} & DOMMotionProps; + +function mergeRefs( + ...refs: (React.Ref | undefined)[] +): React.RefCallback { + return (node) => { + refs.forEach((ref) => { + if (!ref) return; + if (typeof ref === 'function') { + ref(node); + } else { + (ref as React.RefObject).current = node; + } + }); + }; +} + +function mergeProps( + childProps: AnyProps, + slotProps: DOMMotionProps, +): AnyProps { + const merged: AnyProps = { ...childProps, ...slotProps }; + + if (childProps.className || slotProps.className) { + merged.className = cn( + childProps.className as string, + slotProps.className as string, + ); + } + + if (childProps.style || slotProps.style) { + merged.style = { + ...(childProps.style as React.CSSProperties), + ...(slotProps.style as React.CSSProperties), + }; + } + + return merged; +} + +function Slot({ + children, + ref, + ...props +}: SlotProps) { + const isAlreadyMotion = + typeof children.type === 'object' && + children.type !== null && + isMotionComponent(children.type); + + const Base = React.useMemo( + () => + isAlreadyMotion + ? (children.type as React.ElementType) + : motion.create(children.type as React.ElementType), + [isAlreadyMotion, children.type], + ); + + if (!React.isValidElement(children)) return null; + + const { ref: childRef, ...childProps } = children.props as AnyProps; + + const mergedProps = mergeProps(childProps, props); + + return ( + , ref)} /> + ); +} + +export { + Slot, + type SlotProps, + type WithAsChild, + type DOMMotionProps, + type AnyProps, +}; diff --git a/web/src/components/animate-ui/primitives/buttons/button.tsx b/web/src/components/animate-ui/primitives/buttons/button.tsx new file mode 100644 index 00000000..bb518790 --- /dev/null +++ b/web/src/components/animate-ui/primitives/buttons/button.tsx @@ -0,0 +1,32 @@ +'use client'; + +import * as React from 'react'; +import { motion, type HTMLMotionProps } from 'motion/react'; + +import { Slot, type WithAsChild } from '@/components/animate-ui/primitives/animate/slot'; + +type ButtonProps = WithAsChild< + HTMLMotionProps<'button'> & { + hoverScale?: number; + tapScale?: number; + } +>; + +function Button({ + hoverScale = 1.05, + tapScale = 0.95, + asChild = false, + ...props +}: ButtonProps) { + const Component = asChild ? Slot : motion.button; + + return ( + + ); +} + +export { Button, type ButtonProps }; diff --git a/web/src/components/animate-ui/primitives/effects/particles.tsx b/web/src/components/animate-ui/primitives/effects/particles.tsx new file mode 100644 index 00000000..cf2ec639 --- /dev/null +++ b/web/src/components/animate-ui/primitives/effects/particles.tsx @@ -0,0 +1,155 @@ +'use client'; + +import * as React from 'react'; +import { motion, AnimatePresence, type HTMLMotionProps } from 'motion/react'; + +import { Slot, type WithAsChild } from '@/components/animate-ui/primitives/animate/slot'; +import { + useIsInView, + type UseIsInViewOptions, +} from '@/hooks/use-is-in-view'; +import { getStrictContext } from '@/lib/get-strict-context'; + +type Side = 'top' | 'bottom' | 'left' | 'right'; +type Align = 'start' | 'center' | 'end'; + +type ParticlesContextType = { + animate: boolean; + isInView: boolean; +}; + +const [ParticlesProvider, useParticles] = + getStrictContext('ParticlesContext'); + +type ParticlesProps = WithAsChild< + Omit, 'children'> & { + animate?: boolean; + children: React.ReactNode; + } & UseIsInViewOptions +>; + +function Particles({ + ref, + animate = true, + asChild = false, + inView = false, + inViewMargin = '0px', + inViewOnce = true, + children, + style, + ...props +}: ParticlesProps) { + const { ref: localRef, isInView } = useIsInView( + ref as React.Ref, + { inView, inViewOnce, inViewMargin }, + ); + + const Component = asChild ? Slot : motion.div; + + return ( + + + {children} + + + ); +} + +type ParticlesEffectProps = Omit, 'children'> & { + side?: Side; + align?: Align; + count?: number; + radius?: number; + spread?: number; + duration?: number; + holdDelay?: number; + sideOffset?: number; + alignOffset?: number; + delay?: number; +}; + +function ParticlesEffect({ + side = 'top', + align = 'center', + count = 6, + radius = 30, + spread = 360, + duration = 0.8, + holdDelay = 0.05, + sideOffset = 0, + alignOffset = 0, + delay = 0, + transition, + style, + ...props +}: ParticlesEffectProps) { + const { animate, isInView } = useParticles(); + + const isVertical = side === 'top' || side === 'bottom'; + const alignPct = align === 'start' ? '0%' : align === 'end' ? '100%' : '50%'; + + const top = isVertical + ? side === 'top' + ? `calc(0% - ${sideOffset}px)` + : `calc(100% + ${sideOffset}px)` + : `calc(${alignPct} + ${alignOffset}px)`; + + const left = isVertical + ? `calc(${alignPct} + ${alignOffset}px)` + : side === 'left' + ? `calc(0% - ${sideOffset}px)` + : `calc(100% + ${sideOffset}px)`; + + const containerStyle: React.CSSProperties = { + position: 'absolute', + top, + left, + transform: 'translate(-50%, -50%)', + }; + + const angleStep = (spread * (Math.PI / 180)) / Math.max(1, count - 1); + + return ( + + {animate && + isInView && + [...Array(count)].map((_, i) => { + const angle = i * angleStep; + const x = Math.cos(angle) * radius; + const y = Math.sin(angle) * radius; + + return ( + + ); + })} + + ); +} + +export { + Particles, + ParticlesEffect, + type ParticlesProps, + type ParticlesEffectProps, +}; diff --git a/web/src/components/animate-ui/primitives/texts/sliding-number.tsx b/web/src/components/animate-ui/primitives/texts/sliding-number.tsx new file mode 100644 index 00000000..561173f5 --- /dev/null +++ b/web/src/components/animate-ui/primitives/texts/sliding-number.tsx @@ -0,0 +1,353 @@ +'use client'; + +import * as React from 'react'; +import { + useSpring, + useTransform, + motion, + useMotionValue, + type MotionValue, + type SpringOptions, + type HTMLMotionProps, +} from 'motion/react'; +import useMeasure from 'react-use-measure'; + +import { + useIsInView, + type UseIsInViewOptions, +} from '@/hooks/use-is-in-view'; + +type SlidingNumberRollerProps = { + prevValue: number; + value: number; + place: number; + transition: SpringOptions; + delay?: number; +}; + +function SlidingNumberRoller({ + prevValue, + value, + place, + transition, + delay = 0, +}: SlidingNumberRollerProps) { + const startNumber = Math.floor(prevValue / place) % 10; + const targetNumber = Math.floor(value / place) % 10; + const animatedValue = useSpring(startNumber, transition); + + React.useEffect(() => { + const timeoutId = setTimeout(() => { + animatedValue.set(targetNumber); + }, delay); + return () => clearTimeout(timeoutId); + }, [targetNumber, animatedValue, delay]); + + const [measureRef, { height }] = useMeasure(); + + return ( + + 0 + {Array.from({ length: 10 }, (_, i) => ( + + ))} + + ); +} + +type SlidingNumberDisplayProps = { + motionValue: MotionValue; + number: number; + height: number; + transition: SpringOptions; +}; + +function SlidingNumberDisplay({ + motionValue, + number, + height, + transition, +}: SlidingNumberDisplayProps) { + const y = useTransform(motionValue, (latest) => { + if (!height) return 0; + const currentNumber = latest % 10; + const offset = (10 + number - currentNumber) % 10; + let translateY = offset * height; + if (offset > 5) translateY -= 10 * height; + return translateY; + }); + + if (!height) { + return ( + + {number} + + ); + } + + return ( + + {number} + + ); +} + +type SlidingNumberProps = Omit, 'children'> & { + number: number; + fromNumber?: number; + onNumberChange?: (number: number) => void; + padStart?: boolean; + decimalSeparator?: string; + decimalPlaces?: number; + thousandSeparator?: string; + transition?: SpringOptions; + delay?: number; + initiallyStable?: boolean; +} & UseIsInViewOptions; + +function SlidingNumber({ + ref, + number, + fromNumber, + onNumberChange, + inView = false, + inViewMargin = '0px', + inViewOnce = true, + padStart = false, + decimalSeparator = '.', + decimalPlaces = 0, + thousandSeparator, + transition = { stiffness: 200, damping: 20, mass: 0.4 }, + delay = 0, + initiallyStable = false, + ...props +}: SlidingNumberProps) { + const { ref: localRef, isInView } = useIsInView( + ref as React.Ref, + { + inView, + inViewOnce, + inViewMargin, + }, + ); + + const initialNumeric = Math.abs(Number(number)); + const prevNumberRef = React.useRef( + initiallyStable ? initialNumeric : 0, + ); + + const hasAnimated = fromNumber !== undefined; + + const motionVal = useMotionValue( + initiallyStable ? initialNumeric : (fromNumber ?? 0), + ); + const springVal = useSpring(motionVal, { stiffness: 90, damping: 50 }); + + const skippedInitialWhenStable = React.useRef(false); + + React.useEffect(() => { + if (!hasAnimated) return; + if (initiallyStable && !skippedInitialWhenStable.current) { + skippedInitialWhenStable.current = true; + return; + } + const timeoutId = setTimeout(() => { + if (isInView) motionVal.set(number); + }, delay); + return () => clearTimeout(timeoutId); + }, [hasAnimated, initiallyStable, isInView, number, motionVal, delay]); + + const [effectiveNumber, setEffectiveNumber] = React.useState( + initiallyStable ? initialNumeric : 0, + ); + + React.useEffect(() => { + if (hasAnimated) { + const inferredDecimals = + typeof decimalPlaces === 'number' && decimalPlaces >= 0 + ? decimalPlaces + : (() => { + const s = String(number); + const idx = s.indexOf('.'); + return idx >= 0 ? s.length - idx - 1 : 0; + })(); + + const factor = Math.pow(10, inferredDecimals); + + const unsubscribe = springVal.on('change', (latest: number) => { + const newValue = + inferredDecimals > 0 + ? Math.round(latest * factor) / factor + : Math.round(latest); + + if (effectiveNumber !== newValue) { + setEffectiveNumber(newValue); + onNumberChange?.(newValue); + } + }); + return () => unsubscribe(); + } else { + setEffectiveNumber( + initiallyStable ? initialNumeric : !isInView ? 0 : initialNumeric, + ); + } + }, [ + hasAnimated, + springVal, + isInView, + number, + decimalPlaces, + onNumberChange, + effectiveNumber, + initiallyStable, + initialNumeric, + ]); + + const formatNumber = React.useCallback( + (num: number) => + decimalPlaces != null ? num.toFixed(decimalPlaces) : num.toString(), + [decimalPlaces], + ); + + const numberStr = formatNumber(effectiveNumber); + const [newIntStrRaw, newDecStrRaw = ''] = numberStr.split('.'); + + const finalIntLength = padStart + ? Math.max( + Math.floor(Math.abs(number)).toString().length, + newIntStrRaw.length, + ) + : newIntStrRaw.length; + + const newIntStr = padStart + ? newIntStrRaw.padStart(finalIntLength, '0') + : newIntStrRaw; + + const prevFormatted = formatNumber(prevNumberRef.current); + const [prevIntStrRaw = '', prevDecStrRaw = ''] = prevFormatted.split('.'); + const prevIntStr = padStart + ? prevIntStrRaw.padStart(finalIntLength, '0') + : prevIntStrRaw; + + const adjustedPrevInt = React.useMemo(() => { + return prevIntStr.length > finalIntLength + ? prevIntStr.slice(-finalIntLength) + : prevIntStr.padStart(finalIntLength, '0'); + }, [prevIntStr, finalIntLength]); + + const adjustedPrevDec = React.useMemo(() => { + if (!newDecStrRaw) return ''; + return prevDecStrRaw.length > newDecStrRaw.length + ? prevDecStrRaw.slice(0, newDecStrRaw.length) + : prevDecStrRaw.padEnd(newDecStrRaw.length, '0'); + }, [prevDecStrRaw, newDecStrRaw]); + + React.useEffect(() => { + if (isInView || initiallyStable) { + prevNumberRef.current = effectiveNumber; + } + }, [effectiveNumber, isInView, initiallyStable]); + + const intPlaces = React.useMemo( + () => + Array.from({ length: finalIntLength }, (_, i) => + Math.pow(10, finalIntLength - i - 1), + ), + [finalIntLength], + ); + const decPlaces = React.useMemo( + () => + newDecStrRaw + ? Array.from({ length: newDecStrRaw.length }, (_, i) => + Math.pow(10, newDecStrRaw.length - i - 1), + ) + : [], + [newDecStrRaw], + ); + + const newDecValue = newDecStrRaw ? parseInt(newDecStrRaw, 10) : 0; + const prevDecValue = adjustedPrevDec ? parseInt(adjustedPrevDec, 10) : 0; + + return ( + + {isInView && Number(number) < 0 && ( + - + )} + + {intPlaces.map((place, idx) => { + const digitsToRight = intPlaces.length - idx - 1; + const isSeparatorPosition = + typeof thousandSeparator !== 'undefined' && + digitsToRight > 0 && + digitsToRight % 3 === 0; + + return ( + + + {isSeparatorPosition && {thousandSeparator}} + + ); + })} + + {newDecStrRaw && ( + <> + {decimalSeparator} + {decPlaces.map((place) => ( + + ))} + + )} + + ); +} + +export { SlidingNumber, type SlidingNumberProps }; diff --git a/web/src/components/dashboard/add-repo-card.tsx b/web/src/components/dashboard/add-repo-card.tsx new file mode 100644 index 00000000..2ba86053 --- /dev/null +++ b/web/src/components/dashboard/add-repo-card.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { Card } from "@/components/ui/card"; +import { Plus } from "lucide-react"; +import { AddRepoDialog } from "@/components/layout/add-repo-dialog"; +import Image from "next/image"; + +interface AddRepoCardProps { + localFsEnabled?: boolean; + githubEnabled?: boolean; +} + +export function AddRepoCard({ localFsEnabled, githubEnabled }: AddRepoCardProps) { + return ( + +
+ MyBMAD + + Add a project +
+ + } + /> + ); +} diff --git a/web/src/components/dashboard/epics-list.tsx b/web/src/components/dashboard/epics-list.tsx new file mode 100644 index 00000000..75b58af2 --- /dev/null +++ b/web/src/components/dashboard/epics-list.tsx @@ -0,0 +1,82 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { StatusBadge } from "@/components/shared/status-badge"; +import { SegmentedProgressBar } from "@/components/shared/segmented-progress-bar"; +import { Info } from "lucide-react"; +import { cn } from "@/lib/utils"; +import Link from "next/link"; +import type { Epic } from "@/lib/bmad/types"; + +interface EpicsListProps { + epics: Epic[]; + owner: string; + repo: string; +} + +const statusBorderColor: Record = { + done: "border-l-success", + "in-progress": "border-l-info", + "not-started": "border-l-muted-foreground", +}; + +function getProgressColor(percent: number) { + return percent >= 100 ? "bg-success" : "bg-warning"; +} + +export function EpicsList({ epics, owner, repo }: EpicsListProps) { + if (epics.length === 0) { + return ( + + + + No epic found in this project + + + ); + } + + const sorted = [...epics].sort( + (a, b) => (parseInt(a.id, 10) || 0) - (parseInt(b.id, 10) || 0), + ); + + return ( + + + Epics + + +
+ {sorted.map((epic) => ( + +
+ + E{epic.id} + + + {epic.title} + +
+
+ + + {epic.completedStories}/{epic.totalStories} stories + + +
+ + ))} +
+
+
+ ); +} diff --git a/web/src/components/dashboard/global-stats-bar.tsx b/web/src/components/dashboard/global-stats-bar.tsx new file mode 100644 index 00000000..fc306816 --- /dev/null +++ b/web/src/components/dashboard/global-stats-bar.tsx @@ -0,0 +1,80 @@ +import { Layers, BookOpen, CheckCircle2, Clock, FolderGit2 } from "lucide-react"; +import { StatsCard } from "@/components/shared/stats-card"; +import { StaggeredList, StaggeredItem } from "@/components/shared/staggered-list"; +import type { BmadProject } from "@/lib/bmad/types"; + +interface GlobalStatsBarProps { + projects: BmadProject[]; +} + +export function GlobalStatsBar({ projects }: GlobalStatsBarProps) { + const totalEpics = projects.reduce((sum, p) => sum + p.epics.length, 0); + const totalStories = projects.reduce((sum, p) => sum + p.totalStories, 0); + const completedStories = projects.reduce( + (sum, p) => sum + p.completedStories, + 0 + ); + const inProgressStories = projects.reduce( + (sum, p) => sum + p.inProgressStories, + 0 + ); + const activeProjects = projects.filter((p) => p.inProgressStories > 0).length; + + return ( + + + 0 + ? `${activeProjects} active` + : projects.length > 0 + ? "All completed" + : undefined + } + /> + + + + + + + + + 0 + ? `${Math.round((completedStories / totalStories) * 100)}% completed` + : undefined + } + /> + + + + + + ); +} diff --git a/web/src/components/dashboard/key-artifacts-card.tsx b/web/src/components/dashboard/key-artifacts-card.tsx new file mode 100644 index 00000000..ab7078e1 --- /dev/null +++ b/web/src/components/dashboard/key-artifacts-card.tsx @@ -0,0 +1,54 @@ +import Link from "next/link"; +import { BookOpen } from "lucide-react"; +import { renderFileIcon } from "@/lib/bmad/file-icons"; +import type { FileTreeNode } from "@/lib/bmad/types"; + +function flattenFiles(nodes: FileTreeNode[]): FileTreeNode[] { + const files: FileTreeNode[] = []; + for (const node of nodes) { + if (node.type === "file") { + files.push(node); + } + if (node.children) { + files.push(...flattenFiles(node.children)); + } + } + return files; +} + +interface KeyArtifactsCardProps { + planningArtifacts: FileTreeNode[]; + owner: string; + repo: string; +} + +export function KeyArtifactsCard({ + planningArtifacts, + owner, + repo, +}: KeyArtifactsCardProps) { + const files = flattenFiles(planningArtifacts); + + if (files.length === 0) return null; + + return ( +
+
+ +

Key Files

+
+
+ {files.map((file) => ( + + {renderFileIcon(file.name, "h-4 w-4 shrink-0 text-muted-foreground")} + {file.name} + + ))} +
+
+ ); +} diff --git a/web/src/components/dashboard/project-stats-grid.tsx b/web/src/components/dashboard/project-stats-grid.tsx new file mode 100644 index 00000000..dd717c59 --- /dev/null +++ b/web/src/components/dashboard/project-stats-grid.tsx @@ -0,0 +1,60 @@ +import { StatsCard } from "@/components/shared/stats-card"; +import { StaggeredList, StaggeredItem } from "@/components/shared/staggered-list"; +import { Layers, BookOpen, CheckCircle2, Zap } from "lucide-react"; + +interface ProjectStatsGridProps { + totalEpics: number; + totalStories: number; + completedStories: number; + sprintProgress: number | null; +} + +export function ProjectStatsGrid({ + totalEpics, + totalStories, + completedStories, + sprintProgress, +}: ProjectStatsGridProps) { + return ( + + + + + + + + + + + + + + + ); +} diff --git a/web/src/components/dashboard/repo-card.tsx b/web/src/components/dashboard/repo-card.tsx new file mode 100644 index 00000000..db3754c7 --- /dev/null +++ b/web/src/components/dashboard/repo-card.tsx @@ -0,0 +1,127 @@ +import Link from "next/link"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { ProgressRing } from "@/components/shared/progress-ring"; +import { ParseErrorsDialog } from "@/components/shared/parse-errors-dialog"; +import { StatusBadge } from "@/components/shared/status-badge"; +import { Badge } from "@/components/ui/badge"; +import { GitBranch, FolderOpen, BookOpen, Layers } from "lucide-react"; +import type { BmadProject, Epic } from "@/lib/bmad/types"; + +interface RepoCardProps { + project: BmadProject; + description: string | null; +} + +function getBarColor(percent: number) { + if (percent >= 75) return "bg-success"; + if (percent >= 40) return "bg-warning"; + return "bg-destructive"; +} + +function EpicSummaryRow({ epic }: { epic: Epic }) { + return ( +
+ + + {epic.id}. {epic.title} + + + {epic.completedStories}/{epic.totalStories} + +
+
+
+
+ ); +} + +export function RepoCard({ project, description }: RepoCardProps) { + const visibleEpics = project.epics.slice(0, 3); + const remainingEpics = Math.max(0, project.epics.length - 3); + + return ( + + + +
+
+ + {project.displayName} + {(project.parseHealth?.errors.length ?? 0) > 0 && ( + + )} + +
+ {project.owner === "local" ? ( + + ) : ( + + )} + + {project.owner === "local" + ? project.displayName + : `${project.owner}/${project.repo}`} + +
+ {description && ( +

+ {description} +

+ )} +
+ +
+
+ +
+
+ + {project.epics.length} epics +
+
+ + {project.totalStories} stories +
+
+
+ {project.completedStories > 0 && ( + + {project.completedStories} completed + + )} + {project.inProgressStories > 0 && ( + + {project.inProgressStories} in progress + + )} +
+ {project.epics.length > 0 && ( +
+ {visibleEpics.map((epic) => ( + + ))} + {remainingEpics > 0 && ( +

+ +{remainingEpics} epics +

+ )} +
+ )} +
+
+ + ); +} diff --git a/web/src/components/dashboard/repos-grid.tsx b/web/src/components/dashboard/repos-grid.tsx new file mode 100644 index 00000000..e6f5df8b --- /dev/null +++ b/web/src/components/dashboard/repos-grid.tsx @@ -0,0 +1,61 @@ +import { RepoCard } from "./repo-card"; +import { AddRepoCard } from "./add-repo-card"; +import { StaggeredList, StaggeredItem } from "@/components/shared/staggered-list"; +import type { BmadProject } from "@/lib/bmad/types"; +import type { RepoConfig } from "@/lib/types"; + +interface ReposGridProps { + projects: BmadProject[]; + repos: RepoConfig[]; + localFsEnabled?: boolean; + githubEnabled?: boolean; +} + +export function ReposGrid({ projects, repos, localFsEnabled, githubEnabled }: ReposGridProps) { + if (repos.length === 0) { + return ( +
+

+ No project imported +

+

+ Add a BMAD repo to get started. +

+ +
+ ); + } + + if (projects.length === 0) { + return ( +
+

+ Failed to load projects +

+

+ Data from your {repos.length} repo{repos.length > 1 ? "s" : ""} could not be fetched. Check your connection or try again. +

+
+ ); + } + + const descriptionMap = new Map( + repos.map((r) => [`${r.owner}/${r.name}`, r.description]) + ); + + return ( + + {projects.map((project) => ( + + + + ))} + + + + + ); +} diff --git a/web/src/components/dashboard/sprint-summary-card.tsx b/web/src/components/dashboard/sprint-summary-card.tsx new file mode 100644 index 00000000..549992ab --- /dev/null +++ b/web/src/components/dashboard/sprint-summary-card.tsx @@ -0,0 +1,151 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { ProgressRing } from "@/components/shared/progress-ring"; +import { StatusBadge } from "@/components/shared/status-badge"; +import { Info, Calendar } from "lucide-react"; +import type { SprintStatus, StoryStatus } from "@/lib/bmad/types"; + +interface SprintSummaryCardProps { + sprintStatus: SprintStatus | null; +} + +const statusOrder: StoryStatus[] = [ + "done", + "review", + "in-progress", + "blocked", + "backlog", + "unknown", +]; + +export function SprintSummaryCard({ + sprintStatus, +}: SprintSummaryCardProps) { + if (!sprintStatus) { + return ( + + + + No sprint defined + + + ); + } + + const sprintStories = sprintStatus.stories; + const sprintTotal = sprintStories.length; + const sprintDone = sprintStories.filter((s) => s.status === "done").length; + const sprintPercent = + sprintTotal > 0 ? Math.round((sprintDone / sprintTotal) * 100) : 0; + + // Group stories by status + const byStatus = new Map(); + for (const story of sprintStories) { + byStatus.set(story.status, (byStatus.get(story.status) || 0) + 1); + } + + // Group stories by epic + const byEpic = new Map(); + for (const story of sprintStories) { + const epicKey = story.epicId || "other"; + const entry = byEpic.get(epicKey) || { total: 0, done: 0 }; + entry.total++; + if (story.status === "done") entry.done++; + byEpic.set(epicKey, entry); + } + + // Sort epics numerically + const sortedEpics = [...byEpic.entries()].sort((a, b) => { + const numA = parseInt(a[0], 10) || 0; + const numB = parseInt(b[0], 10) || 0; + return numA - numB; + }); + + return ( + + +
+ + Sprint: {sprintStatus.sprint || "Current"} + + {(sprintStatus.startDate || sprintStatus.endDate) && ( +
+ + + {sprintStatus.startDate && sprintStatus.endDate + ? `${sprintStatus.startDate} → ${sprintStatus.endDate}` + : sprintStatus.startDate || sprintStatus.endDate} + +
+ )} +
+
+ + {/* Overview avec progress ring */} +
+
+

+ Status: {sprintStatus.status || "Active"} +

+

+ {sprintDone}/{sprintTotal} stories completed +

+
+ +
+ + {/* Status breakdown */} +
+

Status breakdown

+
+ {statusOrder + .filter((status) => byStatus.has(status)) + .map((status) => ( +
+ + + {byStatus.get(status)} + +
+ ))} +
+
+ + {/* Breakdown par epic */} + {sortedEpics.length > 0 && ( +
+

Progress by epic

+
+ {sortedEpics.map(([epicId, data]) => { + const percent = + data.total > 0 + ? Math.round((data.done / data.total) * 100) + : 0; + return ( +
+
+ + Epic {epicId} + + + {data.done}/{data.total} + +
+
+
+
+
+ ); + })} +
+
+ )} + + + ); +} diff --git a/web/src/components/dashboard/velocity-metrics.tsx b/web/src/components/dashboard/velocity-metrics.tsx new file mode 100644 index 00000000..bffe2169 --- /dev/null +++ b/web/src/components/dashboard/velocity-metrics.tsx @@ -0,0 +1,54 @@ +import { StatsCard } from "@/components/shared/stats-card"; +import { StaggeredList, StaggeredItem } from "@/components/shared/staggered-list"; +import { TrendingUp, Activity, AlertTriangle } from "lucide-react"; +import type { SprintStatus } from "@/lib/bmad/types"; + +interface VelocityMetricsProps { + sprintStatus: SprintStatus | null; +} + +export function VelocityMetrics({ + sprintStatus, +}: VelocityMetricsProps) { + if (!sprintStatus) return null; + + const stories = sprintStatus.stories; + const totalStories = stories.length; + const doneCount = stories.filter((s) => s.status === "done").length; + const wipCount = stories.filter( + (s) => s.status === "in-progress" || s.status === "review", + ).length; + const blockedCount = stories.filter((s) => s.status === "blocked").length; + + return ( + + + + + + + + + 0 ? "Attention required" : "No blockers"} + color="destructive" + /> + + + ); +} diff --git a/web/src/components/docs/code-block-with-copy.tsx b/web/src/components/docs/code-block-with-copy.tsx new file mode 100644 index 00000000..4727fde1 --- /dev/null +++ b/web/src/components/docs/code-block-with-copy.tsx @@ -0,0 +1,79 @@ +"use client"; + +import React, { useState, useCallback, useRef, useEffect } from "react"; +import { Copy, Check, XCircle } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; + +function extractTextFromChildren(children: React.ReactNode): string { + if (typeof children === "string") return children; + if (typeof children === "number") return String(children); + if (Array.isArray(children)) return children.map(extractTextFromChildren).join(""); + if (React.isValidElement(children)) { + const props = children.props as Record; + return extractTextFromChildren(props.children as React.ReactNode); + } + return ""; +} + +export function CodeBlockWithCopy(props: React.ComponentProps<"pre">) { + const { children, ...rest } = props; + const [copyState, setCopyState] = useState<"idle" | "copied" | "error">("idle"); + const timerRef = useRef>(null); + + useEffect(() => { + return () => { + if (timerRef.current) clearTimeout(timerRef.current); + }; + }, []); + + const codeElement = React.Children.toArray(children).find( + (child): child is React.ReactElement => + React.isValidElement(child) && child.type === "code" + ); + + const codeProps = codeElement?.props as Record | undefined; + const className = (codeProps?.className as string) || ""; + const langMatch = className.match(/language-(\S+)/); + const language = langMatch ? langMatch[1] : ""; + + const codeText = codeElement + ? extractTextFromChildren(codeProps?.children as React.ReactNode) + : ""; + + const handleCopy = useCallback(async () => { + if (timerRef.current) clearTimeout(timerRef.current); + try { + await navigator.clipboard.writeText(codeText); + setCopyState("copied"); + } catch { + setCopyState("error"); + } + timerRef.current = setTimeout(() => setCopyState("idle"), 2000); + }, [codeText]); + + return ( +
+
+ {language && ( + + {language} + + )} + +
+
{children}
+
+ ); +} diff --git a/web/src/components/docs/docs-browser.tsx b/web/src/components/docs/docs-browser.tsx new file mode 100644 index 00000000..0e3fb59c --- /dev/null +++ b/web/src/components/docs/docs-browser.tsx @@ -0,0 +1,382 @@ +"use client"; + +import { useState, useEffect, useMemo, useCallback } from "react"; +import { FileTree } from "./file-tree"; +import { MarkdownRenderer } from "./markdown-renderer"; +import { TableOfContents } from "./table-of-contents"; +import { FileMetadataBar } from "./file-metadata-bar"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Card } from "@/components/ui/card"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; +import { Badge } from "@/components/ui/badge"; +import { Separator } from "@/components/ui/separator"; +import { StaggeredList, StaggeredItem } from "@/components/shared/staggered-list"; +import { + FileText, + FolderOpen, + Cog, + AlertTriangle, + FileOutput, + Settings, + ChevronRight, + ChevronDown, +} from "lucide-react"; +import { fetchParsedFileContent } from "@/actions/repo-actions"; +import hljs from "highlight.js/lib/core"; +import yamlLang from "highlight.js/lib/languages/yaml"; +import jsonLang from "highlight.js/lib/languages/json"; +import type { FileTreeNode, ParsedBmadFile } from "@/lib/bmad/types"; + +hljs.registerLanguage("yaml", yamlLang); +hljs.registerLanguage("json", jsonLang); + +function CodeRenderer({ + content, + language, +}: { + content: string; + language: string; +}) { + const html = useMemo(() => { + if (!hljs.getLanguage(language)) { + return content.replace(//g, ">"); + } + try { + return hljs.highlight(content, { language }).value; + } catch { + return content.replace(//g, ">"); + } + }, [content, language]); + + return ( +
+      
+    
+ ); +} + +function CollapsibleSection({ + title, + icon: Icon, + defaultOpen, + children, +}: { + title: string; + icon: React.ComponentType<{ className?: string }>; + defaultOpen: boolean; + children: React.ReactNode; +}) { + const [open, setOpen] = useState(defaultOpen); + + return ( +
+ +
+ {children} +
+
+ ); +} + +function treeContainsPath(nodes: FileTreeNode[], path: string): boolean { + for (const node of nodes) { + if (node.path === path) return true; + if (node.children && treeContainsPath(node.children, path)) return true; + } + return false; +} + +function FilePanel({ + fileTree, + secondaryTree, + owner, + repo, + initialSelectedFile, +}: { + fileTree: FileTreeNode[]; + secondaryTree?: FileTreeNode[]; + owner: string; + repo: string; + initialSelectedFile?: string; +}) { + const [selectedPath, setSelectedPath] = useState( + initialSelectedFile ?? null, + ); + const [parsedFile, setParsedFile] = useState(null); + const [loading, setLoading] = useState(!!initialSelectedFile); + const [error, setError] = useState(null); + + const handleSelect = useCallback((path: string) => { + setSelectedPath(path); + setLoading(true); + setError(null); + }, []); + + useEffect(() => { + if (!selectedPath) return; + + let cancelled = false; + + fetchParsedFileContent({ owner, name: repo, path: selectedPath }) + .then((result) => { + if (cancelled) return; + if (result.success) { + setParsedFile(result.data); + setError(null); + } else { + setParsedFile(null); + setError(result.error); + } + setLoading(false); + }) + .catch(() => { + if (cancelled) return; + setParsedFile(null); + setError("Failed to load file content."); + setLoading(false); + }); + + return () => { + cancelled = true; + }; + }, [selectedPath, owner, repo]); + + const hasSecondary = secondaryTree && secondaryTree.length > 0; + const initialInSecondary = + hasSecondary && initialSelectedFile + ? treeContainsPath(secondaryTree, initialSelectedFile) + : false; + const isEmpty = fileTree.length === 0 && !hasSecondary; + + if (isEmpty) { + return ( +
+ +

No files found

+
+ ); + } + + return ( + + + + + {hasSecondary ? ( +
+ + + + + + + +
+ ) : ( + + )} +
+
+
+ + + + +
+ {loading ? ( +
+ Loading... +
+ ) : error ? ( +
+ {error.includes("Limite") ? ( + + + GitHub rate limit reached. Cached data is displayed. + + ) : ( +

{error}

+ )} +
+ ) : selectedPath && parsedFile ? ( + <> + {parsedFile.metadata && ( + + )} + {parsedFile.parseError ? ( + <> +
+
+ + This file contains syntax errors. Raw content is + displayed below. +
+

+ {parsedFile.parseError} +

+
+ + + ) : parsedFile.contentType === "markdown" ? ( + <> + + + + ) : parsedFile.contentType === "yaml" ? ( + + ) : parsedFile.contentType === "json" ? ( + + ) : ( +
+                    {parsedFile.body}
+                  
+ )} + + ) : ( +
+ +

Select a file from the tree to view its content

+
+ )} +
+
+
+
+
+ ); +} + +interface DocsBrowserProps { + fileTree: FileTreeNode[]; + docsTree: FileTreeNode[]; + bmadCoreTree: FileTreeNode[]; + owner: string; + repo: string; + initialSelectedFile?: string; +} + +export function DocsBrowser({ + fileTree, + docsTree, + bmadCoreTree, + owner, + repo, + initialSelectedFile, +}: DocsBrowserProps) { + const hasDocs = docsTree.length > 0; + const hasBmad = fileTree.length > 0 || bmadCoreTree.length > 0; + + // If only one source exists, no tabs needed + if (!hasDocs) { + return ( + 0 ? bmadCoreTree : undefined} + owner={owner} + repo={repo} + initialSelectedFile={initialSelectedFile} + /> + ); + } + + if (!hasBmad) { + return ( + + ); + } + + // Determine which tab should be active based on initialSelectedFile location + const initialInDocs = + initialSelectedFile && treeContainsPath(docsTree, initialSelectedFile); + const defaultTab = initialInDocs ? "docs" : "bmad"; + + // Both exist — show tabs + return ( + + + + + BMAD Artifacts + + + + Project Documentation + + + + + 0 ? bmadCoreTree : undefined} + owner={owner} + repo={repo} + initialSelectedFile={initialInDocs ? undefined : initialSelectedFile} + /> + + + + + + + ); +} diff --git a/web/src/components/docs/file-metadata-bar.tsx b/web/src/components/docs/file-metadata-bar.tsx new file mode 100644 index 00000000..04e012ca --- /dev/null +++ b/web/src/components/docs/file-metadata-bar.tsx @@ -0,0 +1,66 @@ +import { StatusBadge } from "@/components/shared/status-badge"; +import { CheckCircle2, Calendar, Workflow } from "lucide-react"; +import type { BmadFileMetadata, StoryStatus } from "@/lib/bmad/types"; + +interface FileMetadataBarProps { + metadata: BmadFileMetadata; +} + +const statusValues = new Set([ + "done", + "in-progress", + "review", + "blocked", + "backlog", + "unknown", +]); + +export function FileMetadataBar({ metadata }: FileMetadataBarProps) { + const hasStatus = + metadata.status && + metadata.status !== "unknown" && + statusValues.has(metadata.status); + const hasSteps = + metadata.stepsCompleted && metadata.stepsCompleted.length > 0; + const hasWorkflowType = !!metadata.workflowType; + const hasCompletedAt = !!metadata.completedAt; + + if (!hasStatus && !hasSteps && !hasWorkflowType && !hasCompletedAt) { + return null; + } + + return ( +
+ {hasStatus && ( +
+ Status + +
+ )} + + {hasSteps && ( +
+ + + Steps completed: {metadata.stepsCompleted?.length} + +
+ )} + + {hasWorkflowType && ( +
+ + Type: {metadata.workflowType} +
+ )} + + {hasCompletedAt && ( +
+ + Completed on {metadata.completedAt} +
+ )} + +
+ ); +} diff --git a/web/src/components/docs/file-tree.tsx b/web/src/components/docs/file-tree.tsx new file mode 100644 index 00000000..32f4a95e --- /dev/null +++ b/web/src/components/docs/file-tree.tsx @@ -0,0 +1,130 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { ChevronRight, Folder, FolderOpen } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { renderFileIcon } from "@/lib/bmad/file-icons"; +import type { FileTreeNode } from "@/lib/bmad/types"; + +interface FileTreeProps { + nodes: FileTreeNode[]; + selectedPath?: string; + onSelect: (path: string) => void; +} + +export function FileTree({ nodes, selectedPath, onSelect }: FileTreeProps) { + return ( +
+ {nodes.map((node) => ( + + ))} +
+ ); +} + +function TreeNode({ + node, + selectedPath, + onSelect, + depth, +}: { + node: FileTreeNode; + selectedPath?: string; + onSelect: (path: string) => void; + depth: number; +}) { + const [expanded, setExpanded] = useState(depth < 2); + const [tooltipOpen, setTooltipOpen] = useState(false); + const buttonRef = useRef(null); + const spanRef = useRef(null); + const didScrollRef = useRef(false); + const isSelected = selectedPath === node.path; + + useEffect(() => { + if (isSelected && !didScrollRef.current && buttonRef.current) { + buttonRef.current.scrollIntoView({ block: "nearest" }); + didScrollRef.current = true; + } + }, [isSelected]); + const isDirectory = node.type === "directory"; + + function isTextClipped() { + const span = spanRef.current; + if (!span) return false; + return span.scrollWidth > span.clientWidth; + } + + return ( +
+ + {isDirectory && expanded && node.children && ( +
+ {node.children.map((child) => ( + + ))} +
+ )} +
+ ); +} diff --git a/web/src/components/docs/markdown-renderer.tsx b/web/src/components/docs/markdown-renderer.tsx new file mode 100644 index 00000000..cf54774b --- /dev/null +++ b/web/src/components/docs/markdown-renderer.tsx @@ -0,0 +1,249 @@ +"use client"; + +import React from "react"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import rehypeSanitize from "rehype-sanitize"; +import rehypeSlug from "rehype-slug"; +import rehypeAutolinkHeadings from "rehype-autolink-headings"; +import rehypeHighlight from "rehype-highlight"; +import type { Components } from "react-markdown"; +import { CodeBlockWithCopy } from "./code-block-with-copy"; +import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; +import { cn } from "@/lib/utils"; +import { bmadSanitizeSchema } from "@/lib/bmad/sanitize-schema"; +import { + ExternalLink, + Info, + Lightbulb, + AlertCircle, + AlertTriangle, + OctagonAlert, +} from "lucide-react"; + +interface MarkdownRendererProps { + content: string; +} + +const CALLOUT_CONFIG: Record< + string, + { icon: React.ElementType; label: string; className: string } +> = { + NOTE: { + icon: Info, + label: "Note", + className: "callout-note", + }, + TIP: { + icon: Lightbulb, + label: "Astuce", + className: "callout-tip", + }, + IMPORTANT: { + icon: AlertCircle, + label: "Important", + className: "callout-important", + }, + WARNING: { + icon: AlertTriangle, + label: "Avertissement", + className: "callout-warning", + }, + CAUTION: { + icon: OctagonAlert, + label: "Attention", + className: "callout-caution", + }, +}; + +function findCalloutPattern(node: React.ReactNode): string | null { + if (typeof node === "string") { + const match = node.match(/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]/); + return match ? match[1] : null; + } + if (React.isValidElement(node)) { + const props = node.props as Record; + const children = React.Children.toArray(props.children as React.ReactNode); + for (const child of children) { + const result = findCalloutPattern(child); + if (result) return result; + } + } + return null; +} + +function extractCalloutType( + children: React.ReactNode +): { type: string; restChildren: React.ReactNode } | null { + const childArray = React.Children.toArray(children); + if (childArray.length === 0) return null; + + const firstChild = childArray[0]; + if (!React.isValidElement(firstChild)) return null; + + const firstChildProps = firstChild.props as Record; + const firstChildChildren = firstChildProps.children as React.ReactNode; + if (!firstChildChildren) return null; + + const innerArray = React.Children.toArray(firstChildChildren); + const firstText = + typeof innerArray[0] === "string" ? innerArray[0] : null; + + // Direct text match in first paragraph child + if (firstText) { + const match = firstText.match(/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*/); + if (match) { + const calloutType = match[1]; + const remainingText = firstText.slice(match[0].length); + + const newInnerChildren = + remainingText.length > 0 + ? [remainingText, ...innerArray.slice(1)] + : innerArray.slice(1); + + const newFirstChild = React.cloneElement( + firstChild as React.ReactElement, + {}, + ...newInnerChildren + ); + + return { type: calloutType, restChildren: [newFirstChild, ...childArray.slice(1)] }; + } + } + + // Fallback: recursive search for callout pattern in nested structure + const calloutType = findCalloutPattern(firstChild); + if (!calloutType) return null; + + // Recursively remove the pattern text from the tree + function removeCalloutPattern(node: React.ReactNode): React.ReactNode { + if (typeof node === "string") { + return node.replace(/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*/, ""); + } + if (React.isValidElement(node)) { + const nodeProps = node.props as Record; + const nodeChildren = React.Children.toArray(nodeProps.children as React.ReactNode); + const cleaned = nodeChildren.map(removeCalloutPattern); + return React.cloneElement(node as React.ReactElement, {}, ...cleaned); + } + return node; + } + + const cleanChildren = childArray.map((child, i) => { + if (i > 0) return child; + return removeCalloutPattern(child); + }); + + return { type: calloutType, restChildren: cleanChildren }; +} + +function CalloutBlockquote(props: React.ComponentProps<"blockquote">) { + const { children, ...rest } = props; + const callout = extractCalloutType(children); + + if (!callout) { + return
{children}
; + } + + const config = CALLOUT_CONFIG[callout.type]; + if (!config) { + return
{children}
; + } + + const Icon = config.icon; + + return ( +
+
+ + {config.label} +
+
{callout.restChildren}
+
+ ); +} + +function SmartLink(props: React.ComponentProps<"a">) { + const { href, children, ...rest } = props; + const isExternal = + href && (href.startsWith("http://") || href.startsWith("https://")); + + if (isExternal) { + return ( + + {children} + + + ); + } + + return ( + + {children} + + ); +} + +function LazyImage(props: React.ComponentProps<"img">) { + const { className, alt, ...rest } = props; + /* eslint-disable @next/next/no-img-element */ + return ( + {alt + ); + /* eslint-enable @next/next/no-img-element */ +} + +function ScrollableTable(props: React.ComponentProps<"table">) { + const { children, ...rest } = props; + return ( + + {children}
+ +
+ ); +} + +const components: Components = { + pre: CodeBlockWithCopy, + blockquote: CalloutBlockquote, + a: SmartLink, + img: LazyImage, + table: ScrollableTable, + input(props) { + if (props.type === "checkbox") { + return ( + + ); + } + return ; + }, +}; + +export function MarkdownRenderer({ content }: MarkdownRendererProps) { + return ( +
+ + {content} + +
+ ); +} diff --git a/web/src/components/docs/table-of-contents.tsx b/web/src/components/docs/table-of-contents.tsx new file mode 100644 index 00000000..8f3f1ce5 --- /dev/null +++ b/web/src/components/docs/table-of-contents.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { useState, useMemo, useId } from "react"; +import { ChevronDown, List } from "lucide-react"; + +interface TocEntry { + level: number; + text: string; + slug: string; +} + +function slugify(text: string): string { + return text + .toLowerCase() + .replace(/[^\p{L}\p{N}\s-]/gu, "") + .replace(/\s+/g, "-") + .replace(/-+/g, "-") + .trim(); +} + +function extractHeadings(markdown: string): TocEntry[] { + const headings: TocEntry[] = []; + const lines = markdown.split("\n"); + let inCodeBlock = false; + + for (const line of lines) { + if (line.trimStart().startsWith("```")) { + inCodeBlock = !inCodeBlock; + continue; + } + if (inCodeBlock) continue; + + const match = line.match(/^(#{1,3})\s+(.+)$/); + if (match) { + const level = match[1].length; + const text = match[2] + .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1") // [text](url) → text + .replace(/\*\*|__|~~|`/g, "") // bold, strikethrough, code + .replace(/(? extractHeadings(content), [content]); + + if (headings.length < 3) return null; + + const minLevel = Math.min(...headings.map((h) => h.level)); + + return ( +
+ + {open && ( + + )} +
+ ); +} diff --git a/web/src/components/epics/epic-stories-sheet.tsx b/web/src/components/epics/epic-stories-sheet.tsx new file mode 100644 index 00000000..df361d32 --- /dev/null +++ b/web/src/components/epics/epic-stories-sheet.tsx @@ -0,0 +1,164 @@ +"use client"; + +import { useState } from "react"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetDescription, +} from "@/components/ui/sheet"; +import { Card, CardContent } from "@/components/ui/card"; +import { StatusBadge } from "@/components/shared/status-badge"; +import { SegmentedProgressBar } from "@/components/shared/segmented-progress-bar"; +import { StoryDetailView } from "./story-detail-view"; +import { ArrowLeft } from "lucide-react"; +import type { Epic, StoryDetail } from "@/lib/bmad/types"; + +interface EpicStoriesSheetProps { + open: boolean; + onOpenChange: (open: boolean) => void; + epic: Epic | null; + stories: StoryDetail[]; +} + +export function EpicStoriesSheet({ + open, + onOpenChange, + epic, + stories, +}: EpicStoriesSheetProps) { + const [selectedStoryId, setSelectedStoryId] = useState(null); + // Reset internal navigation when the targeted epic changes (covers both + // "open with a different epic" and "close then reopen") — derived from + // props at render time, per React 19 guidance. + const [trackedEpicId, setTrackedEpicId] = useState( + epic?.id, + ); + if (trackedEpicId !== epic?.id) { + setTrackedEpicId(epic?.id); + setSelectedStoryId(null); + } + const selectedStory = + stories.find((s) => s.id === selectedStoryId) ?? null; + + if (!epic) return null; + + const inDetailView = selectedStory !== null; + + return ( + + + + {inDetailView && selectedStory ? ( + <> + {/* SheetTitle stays mounted for the Radix Dialog accessible + name; the visible header in StoryDetailView already + displays the same information for sighted users. */} + + Story {selectedStory.id}: {selectedStory.title} + + + + ) : ( + <> +
+ + {epic.id} + + {epic.title} +
+ {epic.description && ( + + {epic.description} + + )} +
+
+ + {epic.completedStories}/{epic.totalStories} stories + + {epic.progressPercent}% +
+ = 100 + ? "bg-success" + : epic.progressPercent > 0 + ? "bg-info" + : "bg-muted-foreground" + } + className="h-1.5" + /> +
+ + )} +
+ +
+ {inDetailView && selectedStory ? ( + + ) : stories.length === 0 ? ( +
+

+ No story found for this epic +

+
+ ) : ( +
+ {stories.map((story) => ( + setSelectedStoryId(story.id)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + setSelectedStoryId(story.id); + } + }} + className="glass-card cursor-pointer hover:shadow-md hover:border-primary/30 transition-all duration-200" + aria-label={`Open story ${story.id}: ${story.title}`} + > + +
+
+ + {story.id} + + + {story.title} + +
+
+ {story.totalTasks > 0 && ( + + {story.completedTasks}/{story.totalTasks} + + )} + +
+
+
+
+ ))} +
+ )} +
+
+
+ ); +} diff --git a/web/src/components/epics/epic-timeline-card.tsx b/web/src/components/epics/epic-timeline-card.tsx new file mode 100644 index 00000000..4238732f --- /dev/null +++ b/web/src/components/epics/epic-timeline-card.tsx @@ -0,0 +1,66 @@ +import { Card, CardContent } from "@/components/ui/card"; +import { StatusBadge } from "@/components/shared/status-badge"; +import { SegmentedProgressBar } from "@/components/shared/segmented-progress-bar"; +import type { Epic } from "@/lib/bmad/types"; + +function getProgressColor(percent: number) { + return percent >= 100 ? "bg-success" : "bg-warning"; +} + +interface EpicTimelineCardProps { + epic: Epic; + onClick?: () => void; +} + +export function EpicTimelineCard({ epic, onClick }: EpicTimelineCardProps) { + return ( + { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onClick(); + } + }, + })} + > + +
+
+
+ + {epic.id} + +

{epic.title}

+
+ {epic.description && ( +

+ {epic.description} +

+ )} +
+
+
+ + {epic.completedStories} of {epic.totalStories} stories + + {epic.progressPercent}% +
+ +
+ +
+
+
+
+
+ ); +} diff --git a/web/src/components/epics/epics-browser.tsx b/web/src/components/epics/epics-browser.tsx new file mode 100644 index 00000000..f694a49b --- /dev/null +++ b/web/src/components/epics/epics-browser.tsx @@ -0,0 +1,349 @@ +"use client"; + +import { useState, useMemo, useCallback, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { StatusBadge } from "@/components/shared/status-badge"; +import { ProgressRing } from "@/components/shared/progress-ring"; +import { SegmentedProgressBar } from "@/components/shared/segmented-progress-bar"; +import { EpicsTimeline } from "./epics-timeline"; +import { EpicsKanban } from "./epics-kanban"; +import { EpicStoriesSheet } from "./epic-stories-sheet"; +import { StoryDetailView } from "./story-detail-view"; +import { useBreadcrumb } from "@/contexts/breadcrumb-context"; +import { ArrowLeft, GanttChartSquare, Columns3 } from "lucide-react"; +import type { Epic, StoryDetail } from "@/lib/bmad/types"; + +type EpicsLayout = "timeline" | "kanban"; + +type View = "epics" | "stories" | "story"; + +interface EpicsBrowserProps { + epics: Epic[]; + stories: StoryDetail[]; + totalEpics: number; + totalStories: number; + totalEpicProgress: number; +} + +export function EpicsBrowser({ + epics, + stories, + totalEpics, + totalStories, + totalEpicProgress, +}: EpicsBrowserProps) { + const [view, setView] = useState("epics"); + const [selectedEpicId, setSelectedEpicId] = useState(null); + const [selectedStoryId, setSelectedStoryId] = useState(null); + const [layout, setLayout] = useState("timeline"); + const [sheetEpicId, setSheetEpicId] = useState(null); + const { setExtraSegments, clearExtraSegments } = useBreadcrumb(); + + const sheetEpic = useMemo( + () => epics.find((e) => e.id === sheetEpicId) ?? null, + [epics, sheetEpicId], + ); + + const sheetEpicStories = useMemo( + () => (sheetEpicId ? stories.filter((s) => s.epicId === sheetEpicId) : []), + [stories, sheetEpicId], + ); + + const openEpicSheet = useCallback((epicId: string) => { + setSheetEpicId(epicId); + }, []); + + const handleSheetOpenChange = useCallback((open: boolean) => { + if (!open) setSheetEpicId(null); + }, []); + + const selectedEpic = useMemo( + () => epics.find((e) => e.id === selectedEpicId) ?? null, + [epics, selectedEpicId], + ); + + const epicStories = useMemo( + () => + selectedEpicId ? stories.filter((s) => s.epicId === selectedEpicId) : [], + [stories, selectedEpicId], + ); + + const selectedStory = useMemo( + () => stories.find((s) => s.id === selectedStoryId) ?? null, + [stories, selectedStoryId], + ); + + const goToEpics = useCallback(() => { + setView("epics"); + setSelectedEpicId(null); + setSelectedStoryId(null); + }, []); + + const goToStories = useCallback((epicId: string) => { + setView("stories"); + setSelectedEpicId(epicId); + setSelectedStoryId(null); + }, []); + + const goToStory = useCallback((storyId: string) => { + setView("story"); + setSelectedStoryId(storyId); + }, []); + + // Listen for section-reset event from sidebar to reset to epics list + useEffect(() => { + const handleReset = () => goToEpics(); + window.addEventListener("section-reset", handleReset); + return () => window.removeEventListener("section-reset", handleReset); + }, [goToEpics]); + + // Sync breadcrumb context with internal navigation state + useEffect(() => { + const segments: { label: string; onClick?: () => void }[] = []; + + if (view === "epics") { + // No extra segment — header already shows "Epics" + } else if (view === "stories" && selectedEpic) { + segments.push({ label: "Epics", onClick: goToEpics }); + segments.push({ + label: `Epic ${selectedEpic.id}`, + }); + } else if (view === "story" && selectedEpic) { + segments.push({ label: "Epics", onClick: goToEpics }); + segments.push({ + label: `Epic ${selectedEpic.id}`, + onClick: () => goToStories(selectedEpic.id), + }); + if (selectedStory) { + segments.push({ + label: `Story ${selectedStory.id}`, + }); + } + } + + if (segments.length > 0) { + setExtraSegments(segments); + } else { + clearExtraSegments(); + } + }, [ + view, + selectedEpic, + selectedStory, + setExtraSegments, + clearExtraSegments, + goToEpics, + goToStories, + ]); + + // Clear extra segments when unmounting + useEffect(() => { + return () => clearExtraSegments(); + }, [clearExtraSegments]); + + // Dynamic title based on current view + const renderTitle = () => { + if (view === "epics") { + return ( +
+
+

Epics

+

+ {totalEpics} epics · {totalStories} stories +

+
+ +
+ ); + } + + if ((view === "stories" || view === "story") && selectedEpic) { + return ( +
+

+ Epic {selectedEpic.id}: {selectedEpic.title} +

+

+ {epicStories.length} stories +

+
+ ); + } + + return null; + }; + + return ( +
+ {renderTitle()} + + {/* Back button */} + {view !== "epics" && ( + + )} + +
+ {/* Layout toggle (only on the epics list view) */} + {view === "epics" && ( +
+
+ + +
+
+ )} + + {/* Active view */} + {view === "epics" && layout === "timeline" && ( + + )} + + {view === "epics" && layout === "kanban" && ( + + )} + + {view === "stories" && selectedEpic && ( +
+ {/* Epic header summary */} + + +
+
+
+ + {selectedEpic.id} + +

+ {selectedEpic.title} +

+
+ {selectedEpic.description && ( +

+ {selectedEpic.description} +

+ )} +
+ +
+
+
+ + {selectedEpic.completedStories} of{" "} + {selectedEpic.totalStories} stories + + {selectedEpic.progressPercent}% +
+ = 100 + ? "bg-success" + : "bg-warning" + } + className="h-2" + /> +
+
+
+ + {/* Stories list */} + {epicStories.length === 0 ? ( +
+

+ No story found for this epic +

+
+ ) : ( +
+ {epicStories.map((story) => ( + goToStory(story.id)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + goToStory(story.id); + } + }} + > + +
+
+ + {story.id} + + + {story.title} + +
+
+ {story.totalTasks > 0 && ( + + {story.completedTasks}/{story.totalTasks} tasks + + )} + +
+
+
+
+ ))} +
+ )} +
+ )} + + {view === "story" && selectedStory && ( + + )} +
+ + +
+ ); +} diff --git a/web/src/components/epics/epics-kanban.tsx b/web/src/components/epics/epics-kanban.tsx new file mode 100644 index 00000000..a785c2a3 --- /dev/null +++ b/web/src/components/epics/epics-kanban.tsx @@ -0,0 +1,110 @@ +"use client"; + +import { Card, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { StatusBadge } from "@/components/shared/status-badge"; +import { SegmentedProgressBar } from "@/components/shared/segmented-progress-bar"; +import { StaggeredList, StaggeredItem } from "@/components/shared/staggered-list"; +import type { Epic, EpicStatus } from "@/lib/bmad/types"; + +const kanbanColumns: { status: EpicStatus; label: string; color: string }[] = [ + { status: "not-started", label: "Planning", color: "bg-muted-foreground" }, + { status: "in-progress", label: "In Progress", color: "bg-info" }, + { status: "done", label: "Done", color: "bg-success" }, +]; + +interface EpicsKanbanProps { + epics: Epic[]; + onSelectEpic: (epicId: string) => void; +} + +export function EpicsKanban({ epics, onSelectEpic }: EpicsKanbanProps) { + if (epics.length === 0) { + return ( +
+ No epic found in this project +
+ ); + } + + return ( + + {kanbanColumns.map((col) => { + const columnEpics = epics.filter((e) => e.status === col.status); + return ( + +
+ + +
+ {columnEpics.map((epic) => ( + onSelectEpic(epic.id)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onSelectEpic(epic.id); + } + }} + className="glass-card cursor-pointer hover:shadow-md hover:border-primary/30 transition-all duration-200" + aria-label={`Open epic ${epic.id}: ${epic.title}`} + > + +
+
+ + {epic.id} + + + {epic.title} + +
+ +
+ +
+
+ + {epic.completedStories}/{epic.totalStories} stories + + {epic.progressPercent}% +
+ = 100 + ? "bg-success" + : epic.progressPercent > 0 + ? "bg-info" + : "bg-muted-foreground" + } + className="h-1.5" + /> +
+
+
+ ))} + + {columnEpics.length === 0 && ( +
+ No epics +
+ )} +
+ + ); + })} + + ); +} diff --git a/web/src/components/epics/epics-timeline.tsx b/web/src/components/epics/epics-timeline.tsx new file mode 100644 index 00000000..d393c211 --- /dev/null +++ b/web/src/components/epics/epics-timeline.tsx @@ -0,0 +1,84 @@ +import { Check, Loader2, Circle } from "lucide-react"; +import { EpicTimelineCard } from "./epic-timeline-card"; +import { StaggeredList, StaggeredItem } from "@/components/shared/staggered-list"; +import type { Epic } from "@/lib/bmad/types"; + +interface EpicsTimelineProps { + epics: Epic[]; + onSelectEpic?: (epicId: string) => void; +} + +function StatusIcon({ status }: { status: string }) { + if (status === "done") { + return ( +
+ +
+ ); + } + if (status === "in-progress") { + return ( +
+ +
+ ); + } + return ( +
+ +
+ ); +} + +function connectorColor(status: string) { + if (status === "done") return "bg-success"; + if (status === "in-progress") return "bg-info/40"; + return "bg-border"; +} + +export function EpicsTimeline({ epics, onSelectEpic }: EpicsTimelineProps) { + if (epics.length === 0) { + return ( +
+

+ No epic found +

+

+ Epics will appear here once defined in the planning artifacts. +

+
+ ); + } + + return ( + + {epics.map((epic, i) => ( + +
+ {/* Stepper column */} +
+ {/* Top connector */} +
+ {/* Status icon */} + + {/* Bottom connector */} +
+
+ + {/* Card */} +
+ onSelectEpic(epic.id) : undefined} + /> +
+
+ + ))} + + ); +} diff --git a/web/src/components/epics/story-detail-view.tsx b/web/src/components/epics/story-detail-view.tsx new file mode 100644 index 00000000..09b4ec9a --- /dev/null +++ b/web/src/components/epics/story-detail-view.tsx @@ -0,0 +1,93 @@ +import { Card, CardContent } from "@/components/ui/card"; +import { StatusBadge } from "@/components/shared/status-badge"; +import { MarkdownRenderer } from "@/components/docs/markdown-renderer"; +import { CheckCircle2, Circle } from "lucide-react"; +import { SegmentedProgressBar } from "@/components/shared/segmented-progress-bar"; +import type { StoryDetail } from "@/lib/bmad/types"; + +interface StoryDetailViewProps { + story: StoryDetail; +} + +export function StoryDetailView({ story }: StoryDetailViewProps) { + return ( +
+ {/* Header */} +
+
+ + Story {story.id} + +

{story.title}

+
+ +
+ + {/* Description */} + {story.description && ( + + +

+ Description +

+ +
+
+ )} + + {/* Acceptance Criteria */} + {story.acceptanceCriteria.length > 0 && ( + + +

+ Acceptance Criteria +

+
    + {story.acceptanceCriteria.map((criterion, i) => ( +
  • + + {criterion} +
  • + ))} +
+
+
+ )} + + {/* Tasks */} + {story.tasks.length > 0 && ( + + +
+

+ Tasks +

+ + {story.completedTasks} of {story.totalTasks} completed + +
+ 0 ? Math.round((story.completedTasks / story.totalTasks) * 100) : 0} + color={story.completedTasks === story.totalTasks ? "bg-success" : "bg-warning"} + className="h-2 mb-4" + /> +
    + {story.tasks.map((task, i) => ( +
  • + {task.completed ? ( + + ) : ( + + )} + + {task.description} + +
  • + ))} +
+
+
+ )} +
+ ); +} diff --git a/web/src/components/layout/add-repo-dialog.tsx b/web/src/components/layout/add-repo-dialog.tsx new file mode 100644 index 00000000..9e3f6259 --- /dev/null +++ b/web/src/components/layout/add-repo-dialog.tsx @@ -0,0 +1,437 @@ +"use client"; + +import { useState, useMemo, useCallback } from "react"; +import { useRouter } from "next/navigation"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + Plus, + Search, + Lock, + Globe, + FolderGit2, + FolderOpen, + Loader2, + Github, +} from "lucide-react"; +import { + listUserRepos, + detectBmadRepos, + importRepo, + importLocalFolder, +} from "@/actions/repo-actions"; +import type { GitHubRepo } from "@/lib/github/types"; + +interface AddRepoDialogProps { + trigger?: React.ReactNode; + importedRepos?: { owner: string; name: string }[]; + localFsEnabled?: boolean; + githubEnabled?: boolean; +} + +export function AddRepoDialog({ + trigger, + importedRepos = [], + localFsEnabled = false, + githubEnabled = true, +}: AddRepoDialogProps) { + const importedSet = useMemo( + () => new Set(importedRepos.map((r) => `${r.owner}/${r.name}`)), + [importedRepos] + ); + const router = useRouter(); + const [open, setOpen] = useState(false); + const [repos, setRepos] = useState([]); + const [loading, setLoading] = useState(false); + const [detecting, setDetecting] = useState(false); + const [error, setError] = useState(""); + const [search, setSearch] = useState(""); + const [importing, setImporting] = useState(null); + const [importError, setImportError] = useState(""); + + // Local folder state + const [localPath, setLocalPath] = useState(""); + const [localImporting, setLocalImporting] = useState(false); + const [localError, setLocalError] = useState(""); + + const defaultTab = githubEnabled ? "github" : "local"; + + const fetchRepos = useCallback(async () => { + if (!githubEnabled) return; + setLoading(true); + setError(""); + setRepos([]); + setSearch(""); + setDetecting(false); + + const result = await listUserRepos(); + if (!result.success) { + setError(result.error); + setLoading(false); + return; + } + + setRepos(result.data); + setLoading(false); + + if (result.data.length > 0) { + setDetecting(true); + const ids = result.data.map((r) => ({ + fullName: r.fullName, + owner: r.owner, + name: r.name, + })); + + const bmadResult = await detectBmadRepos(ids); + if (bmadResult.success) { + setRepos((prev) => { + const updated = prev.map((r) => ({ + ...r, + hasBmad: bmadResult.data[r.fullName] ?? false, + })); + updated.sort((a, b) => { + if (a.hasBmad !== b.hasBmad) return a.hasBmad ? -1 : 1; + return ( + new Date(b.updatedAt).getTime() - + new Date(a.updatedAt).getTime() + ); + }); + return updated; + }); + } + setDetecting(false); + } + }, [githubEnabled]); + + function handleOpenChange(nextOpen: boolean) { + setOpen(nextOpen); + if (nextOpen && githubEnabled) { + fetchRepos(); + } + if (!nextOpen) { + setLocalPath(""); + setLocalError(""); + setLocalImporting(false); + } + } + + const filtered = useMemo(() => { + if (!search.trim()) return repos; + const q = search.toLowerCase(); + return repos.filter( + (r) => + r.fullName.toLowerCase().includes(q) || + r.description?.toLowerCase().includes(q) + ); + }, [repos, search]); + + async function handleSelectRepo(repo: GitHubRepo) { + setImporting(repo.fullName); + setImportError(""); + + const result = await importRepo({ + owner: repo.owner, + name: repo.name, + description: repo.description, + defaultBranch: repo.defaultBranch, + fullName: repo.fullName, + }); + + if (result.success) { + setOpen(false); + router.refresh(); + } else { + setImportError(result.error); + } + setImporting(null); + } + + async function handleImportLocal(e: React.FormEvent) { + e.preventDefault(); + if (!localPath.trim()) return; + + setLocalImporting(true); + setLocalError(""); + + const result = await importLocalFolder({ localPath: localPath.trim() }); + + if (result.success) { + setOpen(false); + router.refresh(); + } else { + setLocalError(result.error); + } + setLocalImporting(false); + } + + const showTabs = githubEnabled && localFsEnabled; + + return ( + + + {trigger || ( + + )} + + + + Add a project + + + {showTabs ? ( + + + + + GitHub + + + + Local Folder + + + + + + + + + + ) : localFsEnabled ? ( + + ) : ( + + )} + + + ); +} + +// --------------------------------------------------------------------------- +// Sub-components +// --------------------------------------------------------------------------- + +function GitHubRepoList({ + search, + setSearch, + loading, + detecting, + error, + importError, + filtered, + importing, + importedSet, + onSelect, +}: { + search: string; + setSearch: (v: string) => void; + loading: boolean; + detecting: boolean; + error: string; + importError: string; + filtered: GitHubRepo[]; + importing: string | null; + importedSet: Set; + onSelect: (repo: GitHubRepo) => void; +}) { + return ( +
+
+ + setSearch(e.target.value)} + className="pl-9" + disabled={loading} + /> +
+ + {error &&

{error}

} + {importError &&

{importError}

} + {detecting && ( +

+ Detecting BMAD files... +

+ )} + + + {loading ? ( +
+ {Array.from({ length: 6 }).map((_, i) => ( +
+ +
+ + +
+
+ ))} +
+ ) : ( +
+ {filtered.length === 0 && !error && ( +

+ {search ? "No repository found" : "No repository available"} +

+ )} + {filtered.map((repo) => { + const isImporting = importing === repo.fullName; + const isAlreadyImported = importedSet.has( + `${repo.owner}/${repo.name}` + ); + const isDisabled = importing !== null || isAlreadyImported; + + return ( + + ); + })} +
+ )} +
+
+ ); +} + +function LocalFolderForm({ + localPath, + setLocalPath, + localImporting, + localError, + onSubmit, +}: { + localPath: string; + setLocalPath: (v: string) => void; + localImporting: boolean; + localError: string; + onSubmit: (e: React.FormEvent) => void; +}) { + return ( +
+
+

+ Enter the absolute path to a local folder containing{" "} + _bmad/{" "} + or{" "} + + _bmad-output/ + + . +

+ setLocalPath(e.target.value)} + disabled={localImporting} + autoComplete="off" + /> +
+ + {localError && ( +

{localError}

+ )} + + +
+ ); +} diff --git a/web/src/components/layout/app-header.tsx b/web/src/components/layout/app-header.tsx new file mode 100644 index 00000000..68f7552c --- /dev/null +++ b/web/src/components/layout/app-header.tsx @@ -0,0 +1,98 @@ +"use client"; + +import { usePathname } from "next/navigation"; +import Link from "next/link"; +import { ChevronRight } from "lucide-react"; +import { SidebarTrigger } from "@/components/ui/sidebar"; +import { Separator } from "@/components/ui/separator"; +import { useBreadcrumb } from "@/contexts/breadcrumb-context"; +import { ScrollProgress } from "@/components/ui/scroll-progress"; +import { AnimatedThemeToggler } from "@/components/ui/animated-theme-toggler"; +import { GitHubStarsButton } from "@/components/animate-ui/components/buttons/github-stars"; + +const routeLabels: Record = { + profile: "Profile", + overview: "Overview", + epics: "Epics", + stories: "Stories", + docs: "Library", +}; + +function getRouteLabel(segment: string): string { + return routeLabels[segment] ?? segment.charAt(0).toUpperCase() + segment.slice(1); +} + +export function AppHeader() { + const pathname = usePathname(); + const { extraSegments } = useBreadcrumb(); + const segments = pathname.split("/").filter(Boolean); + + const breadcrumbs: { label: string; href?: string; onClick?: () => void }[] = [ + { label: "Dashboard", href: "/" }, + ]; + + if (segments[0] === "repo" && segments.length >= 3) { + const owner = segments[1]; + const repo = segments[2]; + if (segments[3]) { + // When extraSegments exist, they already include the section context, + // so only add the route segment when there are no extra segments. + if (extraSegments.length > 0) { + for (const seg of extraSegments) { + breadcrumbs.push({ label: seg.label, onClick: seg.onClick }); + } + } else { + breadcrumbs.push({ + label: getRouteLabel(segments[3]), + href: `/repo/${owner}/${repo}/${segments[3]}`, + }); + } + } + } else if (segments.length === 1 && segments[0] !== "") { + breadcrumbs.push({ + label: getRouteLabel(segments[0]), + }); + } + + return ( +
+ + + + + window.open("https://github.com/DevHDI/my-bmad", "_blank", "noopener,noreferrer")} + /> + +
+ ); +} diff --git a/web/src/components/layout/app-sidebar.tsx b/web/src/components/layout/app-sidebar.tsx new file mode 100644 index 00000000..199fcc85 --- /dev/null +++ b/web/src/components/layout/app-sidebar.tsx @@ -0,0 +1,210 @@ +"use client"; + +import { useState, useMemo } from "react"; +import Link from "next/link"; +import Image from "next/image"; +import { usePathname, useRouter } from "next/navigation"; +import { + LayoutDashboard, + FolderGit2, + FolderOpen, + ChevronRight, + Eye, + Map, + BookOpen, + FileText, + Shield, + PlusIcon, +} from "lucide-react"; +import { Collapsible } from "radix-ui"; +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubItem, + SidebarMenuSubButton, + SidebarFooter, +} from "@/components/ui/sidebar"; +import { Button } from "@/components/ui/button"; +import { AddRepoDialog } from "@/components/layout/add-repo-dialog"; +import { UserMenu } from "@/components/layout/user-menu"; +import type { RepoConfig } from "@/lib/types"; + +const SUPER_ADMIN_EMAIL = "dev@dahmani.fr"; + +interface AppSidebarProps { + repos: RepoConfig[]; + userEmail?: string; + localFsEnabled?: boolean; + githubEnabled?: boolean; +} + +const projectTabs = [ + { label: "Overview", segment: "", icon: Eye }, + { label: "Epics", segment: "epics", icon: Map }, + { label: "Stories", segment: "stories", icon: BookOpen }, + { label: "Library", segment: "docs", icon: FileText }, +]; + +export function AppSidebar({ repos, userEmail, localFsEnabled, githubEnabled }: AppSidebarProps) { + const pathname = usePathname(); + const router = useRouter(); + + // Track which repo key is expanded — only one at a time + const activeRepoKey = useMemo( + () => repos.find((r) => pathname.startsWith(`/repo/${r.owner}/${r.name}`)), + [repos, pathname] + ); + const derivedOpenRepo = activeRepoKey + ? `${activeRepoKey.owner}/${activeRepoKey.name}` + : null; + const [openRepo, setOpenRepo] = useState(derivedOpenRepo); + + // Sync with route changes (e.g. navigating via links outside sidebar) + if (derivedOpenRepo && derivedOpenRepo !== openRepo) { + setOpenRepo(derivedOpenRepo); + } + + return ( + + + + MyBMAD + MyBMAD + + + + + + + + + + + + Dashboard + + + + + + + + + Projects + + + {repos.map((repo) => { + const basePath = `/repo/${repo.owner}/${repo.name}`; + const isRepoActive = pathname.startsWith(basePath); + + const repoKey = `${repo.owner}/${repo.name}`; + + return ( + { + setOpenRepo(open ? repoKey : null); + if (open && !isRepoActive) { + router.push(basePath); + } + }} + className="group/collapsible" + > + + + + {repo.sourceType === "local" ? ( + + ) : ( + + )} + {repo.displayName} + + + + + + {projectTabs.map((tab) => { + const tabHref = tab.segment + ? `${basePath}/${tab.segment}` + : basePath; + const isTabActive = tab.segment === "" + ? pathname === basePath || pathname === basePath + "/" + : pathname.startsWith(`${basePath}/${tab.segment}`); + + return ( + + + { + if (isTabActive) { + window.dispatchEvent(new CustomEvent("section-reset")); + } + }} + > + + {tab.label} + + + + ); + })} + + + + + ); + })} + + + + + + +
+ +
+
+ +
+
+

Made with ❤️ by Hichem

+
+
+
+ ); +} diff --git a/web/src/components/layout/user-menu.tsx b/web/src/components/layout/user-menu.tsx new file mode 100644 index 00000000..07fd412d --- /dev/null +++ b/web/src/components/layout/user-menu.tsx @@ -0,0 +1,133 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { LogOut, ChevronsUpDown, Loader2, User } from "lucide-react"; +import { authClient } from "@/lib/auth/auth-client"; +import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + SidebarMenu, + SidebarMenuItem, + SidebarMenuButton, +} from "@/components/ui/sidebar"; +import { Skeleton } from "@/components/ui/skeleton"; +import { getInitials } from "@/lib/utils"; + +export function UserMenu() { + const { data: session, isPending } = authClient.useSession(); + const router = useRouter(); + const [isSigningOut, setIsSigningOut] = useState(false); + const [signOutError, setSignOutError] = useState(false); + + if (isPending) { + return ( + + + + +
+ + +
+
+
+
+ ); + } + + if (!session?.user) return null; + + const { name, email, image } = session.user; + + async function handleSignOut() { + setIsSigningOut(true); + setSignOutError(false); + try { + const result = await authClient.signOut(); + if (result.error) { + setSignOutError(true); + return; + } + router.push("/login"); + } catch { + setSignOutError(true); + } finally { + setIsSigningOut(false); + } + } + + return ( + + + { if (open) setSignOutError(false); }}> + + + + {image && } + {getInitials(name)} + +
+ {name} + + {email} + +
+ +
+
+ + +
+

{name}

+

+ {email} +

+
+
+ + router.push("/profile")}> + + My Profile + + + {signOutError && ( +

+ Sign out failed. Please try again. +

+ )} + + {isSigningOut ? ( + + ) : ( + + )} + Sign out + +
+
+
+
+ ); +} diff --git a/web/src/components/reui/badge.tsx b/web/src/components/reui/badge.tsx new file mode 100644 index 00000000..46d2980c --- /dev/null +++ b/web/src/components/reui/badge.tsx @@ -0,0 +1,95 @@ +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "relative inline-flex shrink-0 items-center justify-center w-fit border border-transparent font-medium whitespace-nowrap outline-none transition-shadow focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-3", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground", + outline: "border-border bg-transparent dark:bg-input/32", + secondary: "bg-secondary text-secondary-foreground", + info: "bg-info text-white", + success: "bg-success text-white", + warning: "bg-warning text-white", + destructive: "bg-destructive text-white", + focus: "bg-focus text-focus-foreground", + invert: "bg-invert text-invert-foreground", + "primary-light": + "bg-primary/10 border-none text-primary dark:bg-primary/20", + "warning-light": + "bg-warning/10 border-none text-warning-foreground dark:bg-warning/20", + "success-light": + "bg-success/10 border-none text-success-foreground dark:bg-success/20", + "info-light": + "bg-info/10 border-none text-info-foreground dark:bg-info/20", + "destructive-light": + "bg-destructive/10 border-none text-destructive-foreground dark:bg-destructive/20", + "invert-light": + "bg-invert/10 border-none text-foreground dark:bg-invert/20", + "focus-light": + "bg-focus/10 border-none text-focus-foreground dark:bg-focus/20", + "primary-outline": + "bg-background border-border text-primary dark:bg-input/30", + "warning-outline": + "bg-background border-border text-warning-foreground dark:bg-input/30", + "success-outline": + "bg-background border-border text-success-foreground dark:bg-input/30", + "info-outline": + "bg-background border-border text-info-foreground dark:bg-input/30", + "destructive-outline": + "bg-background border-border text-destructive-foreground dark:bg-input/30", + "invert-outline": + "bg-background border-border text-invert-foreground dark:bg-input/30", + "focus-outline": + "bg-background border-border text-focus-foreground dark:bg-input/30", + }, + size: { + xs: "px-1 py-0.25 text-[0.6rem] leading-none h-4 min-w-4 gap-1", + sm: "px-1 py-0.25 text-[0.625rem] leading-none h-4.5 min-w-4.5 gap-1", + default: "px-1.25 py-0.5 text-xs h-5 min-w-5 gap-1", + lg: "px-1.5 py-0.5 text-xs h-5.5 min-w-5.5 gap-1", + xl: "px-2 py-0.75 text-sm h-6 min-w-6 gap-1.5", + }, + /** `default`: per-theme radius. `full`: max radius per theme (Lyra stays `rounded-none`). */ + radius: { + default: + "rounded-sm", + full: "rounded-full", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + radius: "default", + }, + } +) + +interface BadgeProps + extends React.ComponentProps<"span">, VariantProps { + asChild?: boolean +} + +function Badge({ + className, + variant, + size, + radius, + asChild = false, + ...props +}: BadgeProps) { + const Comp = asChild ? Slot.Root : "span" + + return ( + + ) +} + +export { Badge, badgeVariants, type BadgeProps } \ No newline at end of file diff --git a/web/src/components/reui/data-grid/data-grid-column-filter.tsx b/web/src/components/reui/data-grid/data-grid-column-filter.tsx new file mode 100644 index 00000000..aca33904 --- /dev/null +++ b/web/src/components/reui/data-grid/data-grid-column-filter.tsx @@ -0,0 +1,165 @@ +"use client" + +import { useMemo, useState } from "react" +import { Badge } from "@/components/reui/badge" +import { Column } from "@tanstack/react-table" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { Separator } from "@/components/ui/separator" +import { CirclePlusIcon, CheckIcon } from "lucide-react" + +interface DataGridColumnFilterProps { + column?: Column + title?: string + options: { + label: string + value: string + icon?: React.ComponentType<{ className?: string }> + }[] +} + +function DataGridColumnFilter({ + column, + title, + options, +}: DataGridColumnFilterProps) { + const facets = column?.getFacetedUniqueValues() + const selectedValues = new Set(column?.getFilterValue() as string[]) + const [searchQuery, setSearchQuery] = useState("") + + const filteredOptions = useMemo(() => { + if (!searchQuery) return options + return options.filter((option) => + option.label.toLowerCase().includes(searchQuery.toLowerCase()) + ) + }, [options, searchQuery]) + + return ( + + + + + +
+ setSearchQuery(e.target.value)} + className="h-8" + /> +
+
+ {filteredOptions.length === 0 ? ( +
+ No results found. +
+ ) : ( +
+ {filteredOptions.map((option) => { + const isSelected = selectedValues.has(option.value) + return ( +
{ + if (isSelected) { + selectedValues.delete(option.value) + } else { + selectedValues.add(option.value) + } + const filterValues = Array.from(selectedValues) + column?.setFilterValue( + filterValues.length ? filterValues : undefined + ) + }} + className={cn( + "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none", + "hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground" + )} + > +
+ +
+ {option.icon && ( + + )} + {option.label} + {facets?.get(option.value) && ( + + {facets.get(option.value)} + + )} +
+ ) + })} +
+ )} + {selectedValues.size > 0 && ( + <> +
+
+
column?.setFilterValue(undefined)} + className="hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center justify-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none" + > + Clear filters +
+
+ + )} +
+ + + ) +} + +export { DataGridColumnFilter, type DataGridColumnFilterProps } \ No newline at end of file diff --git a/web/src/components/reui/data-grid/data-grid-column-header.tsx b/web/src/components/reui/data-grid/data-grid-column-header.tsx new file mode 100644 index 00000000..00fe9a0f --- /dev/null +++ b/web/src/components/reui/data-grid/data-grid-column-header.tsx @@ -0,0 +1,343 @@ +"use client" + +import { HTMLAttributes, memo, ReactNode, useMemo } from "react" +import { + getColumnHeaderLabel, + useDataGrid, +} from "@/components/reui/data-grid/data-grid" +import { Column } from "@tanstack/react-table" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { ArrowDownIcon, ArrowUpIcon, ChevronsUpDownIcon, CheckIcon, ArrowLeftToLineIcon, ArrowRightToLineIcon, ArrowLeftIcon, ArrowRightIcon, Settings2Icon, PinOffIcon } from "lucide-react" + +interface DataGridColumnHeaderProps< + TData, + TValue, +> extends HTMLAttributes { + column: Column + /** When omitted, uses `column.columnDef.meta.headerTitle`, then a string `columnDef.header`, then `column.id`. */ + title?: string + icon?: ReactNode + pinnable?: boolean + filter?: ReactNode + visibility?: boolean +} + +function DataGridColumnHeaderInner({ + column, + title, + icon, + className, + filter, + visibility = false, +}: DataGridColumnHeaderProps) { + const { isLoading, table, props, recordCount } = useDataGrid() + const resolvedTitle = title ?? getColumnHeaderLabel(column) + + const columnOrder = table.getState().columnOrder + const columnVisibilityKey = JSON.stringify(table.getState().columnVisibility) + const isSorted = column.getIsSorted() + const isPinned = column.getIsPinned() + const canSort = column.getCanSort() + const canPin = column.getCanPin() + const canResize = column.getCanResize() + + const columnIndex = columnOrder.indexOf(column.id) + const canMoveLeft = columnIndex > 0 + const canMoveRight = columnIndex < columnOrder.length - 1 + + const handleSort = () => { + if (isSorted === "asc") { + column.toggleSorting(true) + } else if (isSorted === "desc") { + column.clearSorting() + } else { + column.toggleSorting(false) + } + } + + const headerLabelClassName = cn( + "text-secondary-foreground/80 inline-flex h-full items-center gap-1.5 font-normal [&_svg]:opacity-60 text-[0.8125rem] leading-[calc(1.125/0.8125)] [&_svg]:size-3.5", + className + ) + + const headerButtonClassName = cn( + "text-secondary-foreground/80 hover:bg-secondary data-[state=open]:bg-secondary hover:text-foreground data-[state=open]:text-foreground -ms-2 px-2 font-normal h-6 rounded-lg", + className + ) + + const sortIcon = + canSort && + (isSorted === "desc" ? ( + + ) : isSorted === "asc" ? ( + + ) : ( + + )) + + const hasControls = + props.tableLayout?.columnsMovable || + (props.tableLayout?.columnsVisibility && visibility) || + (props.tableLayout?.columnsPinnable && canPin) || + filter + + const menuItems = useMemo(() => { + const items: ReactNode[] = [] + let hasPreviousSection = false + + // Filter section + if (filter) { + items.push( + + {filter} + + ) + hasPreviousSection = true + } + + // Sort section + if (canSort) { + if (hasPreviousSection) { + items.push() + } + items.push( + { + if (isSorted === "asc") { + column.clearSorting() + } else { + column.toggleSorting(false) + } + }} + disabled={!canSort} + > + + Asc + {isSorted === "asc" && ( + + )} + , + { + if (isSorted === "desc") { + column.clearSorting() + } else { + column.toggleSorting(true) + } + }} + disabled={!canSort} + > + + Desc + {isSorted === "desc" && ( + + )} + + ) + hasPreviousSection = true + } + + // Pin section + if (props.tableLayout?.columnsPinnable && canPin) { + if (hasPreviousSection) { + items.push() + } + items.push( + column.pin(isPinned === "left" ? false : "left")} + > + , + column.pin(isPinned === "right" ? false : "right")} + > + + ) + hasPreviousSection = true + } + + // Move section + if (props.tableLayout?.columnsMovable) { + if (hasPreviousSection) { + items.push() + } + items.push( + { + if (columnIndex > 0) { + const newOrder = [...columnOrder] + const [movedColumn] = newOrder.splice(columnIndex, 1) + newOrder.splice(columnIndex - 1, 0, movedColumn) + table.setColumnOrder(newOrder) + } + }} + disabled={!canMoveLeft || isPinned !== false} + > + , + { + if (columnIndex < columnOrder.length - 1) { + const newOrder = [...columnOrder] + const [movedColumn] = newOrder.splice(columnIndex, 1) + newOrder.splice(columnIndex + 1, 0, movedColumn) + table.setColumnOrder(newOrder) + } + }} + disabled={!canMoveRight || isPinned !== false} + > + + ) + hasPreviousSection = true + } + + // Visibility section + if (props.tableLayout?.columnsVisibility && visibility) { + if (hasPreviousSection) { + items.push() + } + items.push( + + + + Columns + + + {table + .getAllColumns() + .filter((col) => col.getCanHide()) + .map((col) => ( + event.preventDefault()} + onCheckedChange={(value) => col.toggleVisibility(!!value)} + className="capitalize" + > + {getColumnHeaderLabel(col)} + + ))} + + + ) + } + + return items + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + filter, + canSort, + isSorted, + column, + props.tableLayout?.columnsPinnable, + props.tableLayout?.columnsMovable, + props.tableLayout?.columnsVisibility, + canPin, + isPinned, + canMoveLeft, + canMoveRight, + visibility, + table, + columnIndex, + columnOrder, + columnVisibilityKey, // Needed to update checkbox states when visibility changes + ]) + + if (hasControls) { + return ( +
+ + + + + + {menuItems} + + + {props.tableLayout?.columnsPinnable && canPin && isPinned && ( + + )} +
+ ) + } + + if (canSort || (props.tableLayout?.columnsResizable && canResize)) { + return ( +
+ +
+ ) + } + + return ( +
+ {icon && icon} + {resolvedTitle} +
+ ) +} + +const DataGridColumnHeader = memo( + DataGridColumnHeaderInner +) as typeof DataGridColumnHeaderInner + +export { DataGridColumnHeader, type DataGridColumnHeaderProps } \ No newline at end of file diff --git a/web/src/components/reui/data-grid/data-grid-column-visibility.tsx b/web/src/components/reui/data-grid/data-grid-column-visibility.tsx new file mode 100644 index 00000000..1b80472d --- /dev/null +++ b/web/src/components/reui/data-grid/data-grid-column-visibility.tsx @@ -0,0 +1,53 @@ +"use client" + +import { ReactElement } from "react" +import { getColumnHeaderLabel } from "@/components/reui/data-grid/data-grid" +import { Table } from "@tanstack/react-table" + +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +function DataGridColumnVisibility({ + table, + trigger, +}: { + table: Table + trigger: ReactElement> +}) { + return ( + + {trigger} + + + + Toggle Columns + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + event.preventDefault()} + onCheckedChange={(value) => column.toggleVisibility(!!value)} + > + {getColumnHeaderLabel(column)} + + ) + })} + + + + ) +} + +export { DataGridColumnVisibility } \ No newline at end of file diff --git a/web/src/components/reui/data-grid/data-grid-pagination.tsx b/web/src/components/reui/data-grid/data-grid-pagination.tsx new file mode 100644 index 00000000..ba16eb6b --- /dev/null +++ b/web/src/components/reui/data-grid/data-grid-pagination.tsx @@ -0,0 +1,226 @@ +"use client" + +import React, { ReactNode } from "react" +import { useDataGrid } from "@/components/reui/data-grid/data-grid" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Skeleton } from "@/components/ui/skeleton" +import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react" + +interface DataGridPaginationProps { + sizes?: number[] + sizesInfo?: string + sizesLabel?: string + sizesDescription?: string + sizesSkeleton?: ReactNode + more?: boolean + moreLimit?: number + info?: string + infoSkeleton?: ReactNode + className?: string + rowsPerPageLabel?: string + previousPageLabel?: string + nextPageLabel?: string + ellipsisText?: string +} + +function DataGridPagination(props: DataGridPaginationProps): React.JSX.Element { + const { table, recordCount, isLoading } = useDataGrid() + + const defaultProps: Partial = { + sizes: [5, 10, 25, 50, 100], + sizesLabel: "Show", + sizesDescription: "per page", + sizesSkeleton: , + moreLimit: 5, + more: false, + info: "{from} - {to} of {count}", + infoSkeleton: , + rowsPerPageLabel: "Rows per page", + previousPageLabel: "Go to previous page", + nextPageLabel: "Go to next page", + ellipsisText: "...", + } + + const mergedProps: DataGridPaginationProps = { ...defaultProps, ...props } + + const btnBaseClasses = "size-7 p-0 text-sm" + const btnArrowClasses = btnBaseClasses + " rtl:transform rtl:rotate-180" + const pageIndex = table.getState().pagination.pageIndex + const pageSize = table.getState().pagination.pageSize + const from = pageIndex * pageSize + 1 + const to = Math.min((pageIndex + 1) * pageSize, recordCount) + const pageCount = table.getPageCount() + + // Replace placeholders in paginationInfo + const paginationInfo = mergedProps?.info + ? mergedProps.info + .replace("{from}", from.toString()) + .replace("{to}", to.toString()) + .replace("{count}", recordCount.toString()) + : `${from} - ${to} of ${recordCount}` + + // Pagination limit logic + const paginationMoreLimit = mergedProps?.moreLimit || 5 + + // Determine the start and end of the pagination group + const currentGroupStart = + Math.floor(pageIndex / paginationMoreLimit) * paginationMoreLimit + const currentGroupEnd = Math.min( + currentGroupStart + paginationMoreLimit, + pageCount + ) + + // Render page buttons based on the current group + const renderPageButtons = () => { + const buttons = [] + for (let i = currentGroupStart; i < currentGroupEnd; i++) { + buttons.push( + + ) + } + return buttons + } + + // Render a "previous" ellipsis button if there are previous pages to show + const renderEllipsisPrevButton = () => { + if (currentGroupStart > 0) { + return ( + + ) + } + return null + } + + // Render a "next" ellipsis button if there are more pages to show after the current group + const renderEllipsisNextButton = () => { + if (currentGroupEnd < pageCount) { + return ( + + ) + } + return null + } + + return ( +
+
+ {isLoading ? ( + mergedProps?.sizesSkeleton + ) : ( + <> +
+ {mergedProps.rowsPerPageLabel} +
+ + + )} +
+
+ {isLoading ? ( + mergedProps?.infoSkeleton + ) : ( + <> +
+ {paginationInfo} +
+ {pageCount > 1 && ( +
+ + + {renderEllipsisPrevButton()} + + {renderPageButtons()} + + {renderEllipsisNextButton()} + + +
+ )} + + )} +
+
+ ) +} + +export { DataGridPagination, type DataGridPaginationProps } \ No newline at end of file diff --git a/web/src/components/reui/data-grid/data-grid-scroll-area.tsx b/web/src/components/reui/data-grid/data-grid-scroll-area.tsx new file mode 100644 index 00000000..f2e933bb --- /dev/null +++ b/web/src/components/reui/data-grid/data-grid-scroll-area.tsx @@ -0,0 +1,423 @@ +"use client" + +import { + ComponentProps, + PointerEvent, + ReactNode, + useCallback, + useEffect, + useRef, + useState, +} from "react" +import { useDataGrid } from "@/components/reui/data-grid/data-grid" +import { ScrollArea as ScrollAreaPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +const MIN_THUMB_SIZE = 24 +const FALLBACK_SCROLLBAR_SIZE = 12 + +const INITIAL_METRICS = { + hasVerticalOverflow: false, + headerHeight: 0, + horizontalScrollbarSize: 0, + thumbHeight: 0, + thumbTop: 0, + trackHeight: 0, +} as const + +type DataGridScrollAreaOrientation = "horizontal" | "vertical" | "both" + +type ScrollbarMetrics = { + hasVerticalOverflow: boolean + headerHeight: number + horizontalScrollbarSize: number + thumbHeight: number + thumbTop: number + trackHeight: number +} + +type ObservedElements = { + header: HTMLElement | null + horizontalScrollbar: HTMLElement | null + table: HTMLElement | null + tableViewport: HTMLElement | null +} + +type DataGridScrollAreaProps = Omit< + ComponentProps, + "children" +> & { + children: ReactNode + orientation?: DataGridScrollAreaOrientation +} + +function clamp(value: number, min: number, max: number) { + return Math.min(max, Math.max(min, value)) +} + +function areMetricsEqual(next: ScrollbarMetrics, prev: ScrollbarMetrics) { + return ( + next.hasVerticalOverflow === prev.hasVerticalOverflow && + next.headerHeight === prev.headerHeight && + next.horizontalScrollbarSize === prev.horizontalScrollbarSize && + next.thumbHeight === prev.thumbHeight && + next.thumbTop === prev.thumbTop && + next.trackHeight === prev.trackHeight + ) +} + +function applyMetrics(element: HTMLElement, metrics: ScrollbarMetrics) { + element.style.setProperty( + "--data-grid-scrollbar-header-height", + `${metrics.headerHeight}px` + ) + element.style.setProperty( + "--data-grid-scrollbar-thumb-height", + `${metrics.thumbHeight}px` + ) + element.style.setProperty( + "--data-grid-scrollbar-thumb-top", + `${metrics.thumbTop}px` + ) + element.style.setProperty( + "--data-grid-scrollbar-track-height", + `${metrics.trackHeight}px` + ) +} + +function DataGridScrollArea({ + children, + className, + orientation = "both", + ...props +}: DataGridScrollAreaProps) { + const { props: dataGridProps } = useDataGrid() + const containerRef = useRef(null) + const viewportRef = useRef(null) + const dragRef = useRef<{ + pointerId: number + startScrollTop: number + startY: number + } | null>(null) + const metricsRef = useRef(INITIAL_METRICS) + const observedElementsRef = useRef({ + header: null, + horizontalScrollbar: null, + table: null, + tableViewport: null, + }) + + const showHorizontal = orientation !== "vertical" + const showVertical = orientation !== "horizontal" + const usesCustomVerticalScrollbar = + showVertical && !!dataGridProps.tableLayout?.headerSticky + const [hasCustomVerticalOverflow, setHasCustomVerticalOverflow] = + useState(false) + + const clearDragState = useCallback(() => { + dragRef.current = null + document.body.style.userSelect = "" + document.body.style.webkitUserSelect = "" + }, []) + + const resetMetrics = useCallback(() => { + const container = containerRef.current + + if (container && !areMetricsEqual(INITIAL_METRICS, metricsRef.current)) { + applyMetrics(container, INITIAL_METRICS) + metricsRef.current = INITIAL_METRICS + } + + setHasCustomVerticalOverflow((prev) => (prev ? false : prev)) + }, []) + + const syncCustomVerticalScrollbar = useCallback(() => { + const container = containerRef.current + const viewport = viewportRef.current + + if (!container || !viewport || !usesCustomVerticalScrollbar) { + resetMetrics() + return + } + + const { header, horizontalScrollbar } = observedElementsRef.current + const headerHeight = header?.getBoundingClientRect().height ?? 0 + const viewportHeight = viewport.clientHeight + const viewportWidth = viewport.clientWidth + const scrollHeight = viewport.scrollHeight + const scrollWidth = viewport.scrollWidth + const hasHorizontalOverflow = + showHorizontal && scrollWidth > viewportWidth + 0.5 + const horizontalScrollbarSize = hasHorizontalOverflow + ? horizontalScrollbar?.offsetHeight || FALLBACK_SCROLLBAR_SIZE + : 0 + const trackHeight = Math.max( + 0, + viewportHeight - headerHeight - horizontalScrollbarSize + ) + const maxScroll = Math.max(0, scrollHeight - viewportHeight) + + let nextMetrics: ScrollbarMetrics + + if (trackHeight === 0 || maxScroll === 0) { + nextMetrics = { + hasVerticalOverflow: false, + headerHeight, + horizontalScrollbarSize, + thumbHeight: trackHeight, + thumbTop: 0, + trackHeight, + } + } else { + const bodyContentHeight = Math.max( + trackHeight, + scrollHeight - headerHeight + ) + const thumbHeight = clamp( + trackHeight * (trackHeight / bodyContentHeight), + MIN_THUMB_SIZE, + trackHeight + ) + const maxThumbTop = Math.max(0, trackHeight - thumbHeight) + const thumbTop = + maxThumbTop > 0 ? (viewport.scrollTop / maxScroll) * maxThumbTop : 0 + + nextMetrics = { + hasVerticalOverflow: true, + headerHeight, + horizontalScrollbarSize, + thumbHeight, + thumbTop, + trackHeight, + } + } + + if (!areMetricsEqual(nextMetrics, metricsRef.current)) { + applyMetrics(container, nextMetrics) + metricsRef.current = nextMetrics + } + + setHasCustomVerticalOverflow((prev) => + prev === nextMetrics.hasVerticalOverflow + ? prev + : nextMetrics.hasVerticalOverflow + ) + }, [resetMetrics, showHorizontal, usesCustomVerticalScrollbar]) + + useEffect(() => { + const container = containerRef.current + const viewport = viewportRef.current + + if (!container || !viewport) return + + if (!usesCustomVerticalScrollbar) { + resetMetrics() + return + } + + observedElementsRef.current = { + header: container.querySelector( + '[data-slot="data-grid-table"] thead' + ) as HTMLElement | null, + horizontalScrollbar: container.querySelector( + '[data-slot="data-grid-scrollbar"][data-orientation="horizontal"]' + ) as HTMLElement | null, + table: container.querySelector( + '[data-slot="data-grid-table"]' + ) as HTMLElement | null, + tableViewport: container.querySelector( + '[data-slot="data-grid-table-viewport"]' + ) as HTMLElement | null, + } + + let frame = 0 + + const scheduleSync = () => { + cancelAnimationFrame(frame) + frame = window.requestAnimationFrame(syncCustomVerticalScrollbar) + } + + scheduleSync() + viewport.addEventListener("scroll", scheduleSync, { passive: true }) + + const observer = + typeof ResizeObserver === "undefined" + ? null + : new ResizeObserver(scheduleSync) + + observer?.observe(viewport) + observedElementsRef.current.header && + observer?.observe(observedElementsRef.current.header) + observedElementsRef.current.table && + observer?.observe(observedElementsRef.current.table) + observedElementsRef.current.tableViewport && + observer?.observe(observedElementsRef.current.tableViewport) + + return () => { + cancelAnimationFrame(frame) + observer?.disconnect() + viewport.removeEventListener("scroll", scheduleSync) + clearDragState() + } + }, [ + clearDragState, + resetMetrics, + syncCustomVerticalScrollbar, + usesCustomVerticalScrollbar, + ]) + + const scrollToThumbOffset = (nextThumbTop: number) => { + const viewport = viewportRef.current + const { thumbHeight, trackHeight } = metricsRef.current + + if (!viewport) return + + const maxScroll = Math.max(0, viewport.scrollHeight - viewport.clientHeight) + const maxThumbTop = Math.max(0, trackHeight - thumbHeight) + + if (maxScroll === 0 || maxThumbTop === 0) { + viewport.scrollTop = 0 + return + } + + const ratio = clamp(nextThumbTop, 0, maxThumbTop) / maxThumbTop + viewport.scrollTop = ratio * maxScroll + } + + const handleThumbPointerDown = (event: PointerEvent) => { + const viewport = viewportRef.current + + if (!viewport) return + + event.preventDefault() + event.stopPropagation() + event.currentTarget.setPointerCapture(event.pointerId) + + dragRef.current = { + pointerId: event.pointerId, + startScrollTop: viewport.scrollTop, + startY: event.clientY, + } + + document.body.style.userSelect = "none" + document.body.style.webkitUserSelect = "none" + } + + const handleThumbPointerMove = (event: PointerEvent) => { + const viewport = viewportRef.current + const dragState = dragRef.current + const { thumbHeight, trackHeight } = metricsRef.current + + if (!viewport || !dragState || dragState.pointerId !== event.pointerId) { + return + } + + const maxThumbTop = Math.max(0, trackHeight - thumbHeight) + const maxScroll = Math.max(0, viewport.scrollHeight - viewport.clientHeight) + + if (maxThumbTop === 0 || maxScroll === 0) return + + const deltaY = event.clientY - dragState.startY + const nextScrollTop = + dragState.startScrollTop + (deltaY / maxThumbTop) * maxScroll + + viewport.scrollTop = clamp(nextScrollTop, 0, maxScroll) + } + + const handleThumbPointerUp = (event: PointerEvent) => { + if (dragRef.current?.pointerId !== event.pointerId) return + clearDragState() + } + + const handleTrackPointerDown = (event: PointerEvent) => { + const { thumbHeight } = metricsRef.current + + if (event.target !== event.currentTarget) return + + event.preventDefault() + event.stopPropagation() + + const rect = event.currentTarget.getBoundingClientRect() + const offsetY = event.clientY - rect.top - thumbHeight / 2 + + scrollToThumbOffset(offsetY) + } + + return ( +
+ + +
{children}
+
+ + {showHorizontal && ( + + + + )} + + {showVertical && ( + + + + )} +
+ + {usesCustomVerticalScrollbar && hasCustomVerticalOverflow && ( + + ) +} + +export { DataGridScrollArea } +export type { DataGridScrollAreaOrientation, DataGridScrollAreaProps } \ No newline at end of file diff --git a/web/src/components/reui/data-grid/data-grid-table-dnd-rows.tsx b/web/src/components/reui/data-grid/data-grid-table-dnd-rows.tsx new file mode 100644 index 00000000..edc1a461 --- /dev/null +++ b/web/src/components/reui/data-grid/data-grid-table-dnd-rows.tsx @@ -0,0 +1,309 @@ +"use client" + +import { + createContext, + CSSProperties, + ReactNode, + useContext, + useEffect, + useId, + useMemo, + useRef, + useState, +} from "react" +import { useDataGrid } from "@/components/reui/data-grid/data-grid" +import { + DataGridTableBase, + DataGridTableBody, + DataGridTableBodyRow, + DataGridTableBodyRowCell, + DataGridTableBodyRowSkeleton, + DataGridTableBodyRowSkeletonCell, + DataGridTableEmpty, + DataGridTableFoot, + DataGridTableHead, + DataGridTableHeadRow, + DataGridTableHeadRowCell, + DataGridTableHeadRowCellResize, + DataGridTableRowSpacer, + DataGridTableViewport, +} from "@/components/reui/data-grid/data-grid-table" +import { + closestCenter, + DndContext, + KeyboardSensor, + MouseSensor, + TouchSensor, + UniqueIdentifier, + useSensor, + useSensors, + type DragEndEvent, + type Modifier, +} from "@dnd-kit/core" +import { restrictToVerticalAxis } from "@dnd-kit/modifiers" +import { + SortableContext, + useSortable, + verticalListSortingStrategy, +} from "@dnd-kit/sortable" +import { CSS } from "@dnd-kit/utilities" +import { Cell, flexRender, HeaderGroup, Row } from "@tanstack/react-table" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { GripHorizontalIcon } from "lucide-react" + +// Context to share sortable listeners from row to handle +type SortableContextValue = ReturnType +const SortableRowContext = createContext | null>(null) + +function DataGridTableDndRowHandle({ className }: { className?: string }) { + const context = useContext(SortableRowContext) + + if (!context) { + // Fallback if context is not available (shouldn't happen in normal usage) + return ( + + ) + } + + return ( + + ) +} + +function DataGridTableDndRow({ row }: { row: Row }) { + const { + transform, + transition, + setNodeRef, + isDragging, + attributes, + listeners, + } = useSortable({ + id: row.id, + }) + + const style: CSSProperties = { + transform: CSS.Transform.toString(transform), + transition: transition, + opacity: isDragging ? 0.8 : 1, + zIndex: isDragging ? 1 : 0, + position: "relative", + cursor: isDragging ? "grabbing" : undefined, + } + + return ( + + + {row.getVisibleCells().map((cell: Cell, colIndex) => { + return ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ) + })} + + + ) +} + +function DataGridTableDndRows({ + handleDragEnd, + dataIds, + footerContent, +}: { + handleDragEnd: (event: DragEndEvent) => void + dataIds: UniqueIdentifier[] + footerContent?: ReactNode +}) { + const { table, isLoading, props } = useDataGrid() + const pagination = table.getState().pagination + const tableContainerRef = useRef(null) + const [isDraggingRow, setIsDraggingRow] = useState(false) + + const sensors = useSensors( + useSensor(MouseSensor, {}), + useSensor(TouchSensor, {}), + useSensor(KeyboardSensor, {}) + ) + + useEffect(() => { + if (!isDraggingRow) return + + const { body, documentElement } = document + const previousBodyCursor = body.style.cursor + const previousDocumentCursor = documentElement.style.cursor + + body.style.cursor = "grabbing" + documentElement.style.cursor = "grabbing" + + return () => { + body.style.cursor = previousBodyCursor + documentElement.style.cursor = previousDocumentCursor + } + }, [isDraggingRow]) + + const modifiers = useMemo(() => { + const restrictToTableContainer: Modifier = ({ + transform, + draggingNodeRect, + }) => { + if (!tableContainerRef.current || !draggingNodeRect) { + return transform + } + + const containerRect = tableContainerRef.current.getBoundingClientRect() + const { x, y } = transform + + const minX = containerRect.left - draggingNodeRect.left + const maxX = containerRect.right - draggingNodeRect.right + const minY = containerRect.top - draggingNodeRect.top + const maxY = containerRect.bottom - draggingNodeRect.bottom + + return { + ...transform, + x: Math.max(minX, Math.min(maxX, x)), + y: Math.max(minY, Math.min(maxY, y)), + } + } + + return [restrictToVerticalAxis, restrictToTableContainer] + }, []) + + return ( + setIsDraggingRow(false)} + onDragEnd={(event) => { + setIsDraggingRow(false) + handleDragEnd(event) + }} + onDragStart={() => setIsDraggingRow(true)} + sensors={sensors} + > + + + + {table + .getHeaderGroups() + .map((headerGroup: HeaderGroup, index) => { + return ( + + {headerGroup.headers.map((header, index) => { + const { column } = header + + return ( + + {header.isPlaceholder ? null : props.tableLayout + ?.columnsResizable && column.getCanResize() ? ( +
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} +
+ ) : ( + flexRender( + header.column.columnDef.header, + header.getContext() + ) + )} + {props.tableLayout?.columnsResizable && + column.getCanResize() && ( + + )} +
+ ) + })} +
+ ) + })} +
+ + {(props.tableLayout?.stripped || !props.tableLayout?.rowBorder) && ( + + )} + + + {props.loadingMode === "skeleton" && + isLoading && + pagination?.pageSize ? ( + Array.from({ length: pagination.pageSize }).map((_, rowIndex) => ( + + {table.getVisibleFlatColumns().map((column, colIndex) => { + return ( + + {column.columnDef.meta?.skeleton} + + ) + })} + + )) + ) : table.getRowModel().rows.length ? ( + + {table.getRowModel().rows.map((row: Row) => { + return + })} + + ) : ( + + )} + + + {footerContent && ( + {footerContent} + )} +
+
+
+ ) +} + +export { DataGridTableDndRowHandle, DataGridTableDndRows } \ No newline at end of file diff --git a/web/src/components/reui/data-grid/data-grid-table-dnd.tsx b/web/src/components/reui/data-grid/data-grid-table-dnd.tsx new file mode 100644 index 00000000..713d45e2 --- /dev/null +++ b/web/src/components/reui/data-grid/data-grid-table-dnd.tsx @@ -0,0 +1,314 @@ +"use client" + +import { + CSSProperties, + Fragment, + ReactNode, + useEffect, + useId, + useRef, + useState, +} from "react" +import { useDataGrid } from "@/components/reui/data-grid/data-grid" +import { + DataGridTableBase, + DataGridTableBody, + DataGridTableBodyRow, + DataGridTableBodyRowCell, + DataGridTableBodyRowExpandded, + DataGridTableBodyRowSkeleton, + DataGridTableBodyRowSkeletonCell, + DataGridTableEmpty, + DataGridTableFoot, + DataGridTableHead, + DataGridTableHeadRow, + DataGridTableHeadRowCell, + DataGridTableHeadRowCellResize, + DataGridTableRowSpacer, + DataGridTableViewport, +} from "@/components/reui/data-grid/data-grid-table" +import { + closestCenter, + DndContext, + KeyboardSensor, + Modifier, + MouseSensor, + TouchSensor, + useSensor, + useSensors, + type DragEndEvent, +} from "@dnd-kit/core" +import { + horizontalListSortingStrategy, + SortableContext, + useSortable, +} from "@dnd-kit/sortable" +import { CSS } from "@dnd-kit/utilities" +import { + Cell, + flexRender, + Header, + HeaderGroup, + Row, +} from "@tanstack/react-table" + +import { Button } from "@/components/ui/button" +import { GripVerticalIcon } from "lucide-react" + +function DataGridTableDndHeader({ + header, +}: { + header: Header +}) { + const { props } = useDataGrid() + const { column } = header + + // Check if column ordering is enabled for this column + const canOrder = + (column.columnDef as { enableColumnOrdering?: boolean }) + .enableColumnOrdering !== false + + const { + attributes, + isDragging, + listeners, + setNodeRef, + transform, + transition, + } = useSortable({ + id: header.column.id, + }) + + const style: CSSProperties = { + opacity: isDragging ? 0.8 : 1, + position: "relative", + transform: CSS.Translate.toString(transform), + transition, + cursor: isDragging ? "grabbing" : undefined, + whiteSpace: "nowrap", + width: props.tableLayout?.columnsResizable + ? `calc(var(--header-${header.id}-size) * 1px)` + : header.column.getSize(), + zIndex: isDragging ? 1 : 0, + } + + return ( + +
+ {canOrder && ( + + )} + + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + + {props.tableLayout?.columnsResizable && column.getCanResize() && ( + + )} +
+
+ ) +} + +function DataGridTableDndCell({ cell }: { cell: Cell }) { + const { props } = useDataGrid() + const { isDragging, setNodeRef, transform, transition } = useSortable({ + id: cell.column.id, + }) + + const style: CSSProperties = { + opacity: isDragging ? 0.8 : 1, + position: "relative", + transform: CSS.Translate.toString(transform), + transition, + cursor: isDragging ? "grabbing" : undefined, + width: props.tableLayout?.columnsResizable + ? `calc(var(--col-${cell.column.id}-size) * 1px)` + : cell.column.getSize(), + zIndex: isDragging ? 1 : 0, + } + + return ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ) +} + +function DataGridTableDnd({ + handleDragEnd, + footerContent, +}: { + handleDragEnd: (event: DragEndEvent) => void + footerContent?: ReactNode +}) { + const { table, isLoading, props } = useDataGrid() + const pagination = table.getState().pagination + const containerRef = useRef(null) + const [isDraggingColumn, setIsDraggingColumn] = useState(false) + + const sensors = useSensors( + useSensor(MouseSensor, {}), + useSensor(TouchSensor, {}), + useSensor(KeyboardSensor, {}) + ) + + useEffect(() => { + if (!isDraggingColumn) return + + const { body, documentElement } = document + const previousBodyCursor = body.style.cursor + const previousDocumentCursor = documentElement.style.cursor + + body.style.cursor = "grabbing" + documentElement.style.cursor = "grabbing" + + return () => { + body.style.cursor = previousBodyCursor + documentElement.style.cursor = previousDocumentCursor + } + }, [isDraggingColumn]) + + // Custom modifier to restrict dragging within table bounds with edge offset + const restrictToTableBounds: Modifier = ({ draggingNodeRect, transform }) => { + if (!draggingNodeRect || !containerRef.current) { + return { ...transform, y: 0 } + } + + const containerRect = containerRef.current.getBoundingClientRect() + const edgeOffset = 0 + + const minX = containerRect.left - draggingNodeRect.left - edgeOffset + const maxX = + containerRect.right - + draggingNodeRect.left - + draggingNodeRect.width + + edgeOffset + + return { + ...transform, + x: Math.min(Math.max(transform.x, minX), maxX), + y: 0, // Lock vertical movement + } + } + + return ( + setIsDraggingColumn(false)} + onDragEnd={(event) => { + setIsDraggingColumn(false) + handleDragEnd(event) + }} + onDragStart={() => setIsDraggingColumn(true)} + sensors={sensors} + > + + + + {table + .getHeaderGroups() + .map((headerGroup: HeaderGroup, index) => { + return ( + + + {headerGroup.headers.map((header) => ( + + ))} + + + ) + })} + + + {(props.tableLayout?.stripped || !props.tableLayout?.rowBorder) && ( + + )} + + + {props.loadingMode === "skeleton" && + isLoading && + pagination?.pageSize ? ( + Array.from({ length: pagination.pageSize }).map((_, rowIndex) => ( + + {table.getVisibleFlatColumns().map((column, colIndex) => { + return ( + + {column.columnDef.meta?.skeleton} + + ) + })} + + )) + ) : table.getRowModel().rows.length ? ( + table.getRowModel().rows.map((row: Row) => { + return ( + + + {row + .getVisibleCells() + .map((cell: Cell) => { + return ( + + + + ) + })} + + {row.getIsExpanded() && ( + + )} + + ) + }) + ) : ( + + )} + + + {footerContent && ( + {footerContent} + )} + + + + ) +} + +export { DataGridTableDnd } \ No newline at end of file diff --git a/web/src/components/reui/data-grid/data-grid-table-virtual.tsx b/web/src/components/reui/data-grid/data-grid-table-virtual.tsx new file mode 100644 index 00000000..2612ffa4 --- /dev/null +++ b/web/src/components/reui/data-grid/data-grid-table-virtual.tsx @@ -0,0 +1,492 @@ +"use client" + +import { + memo, + ReactNode, + useCallback, + useEffect, + useMemo, + useState, +} from "react" +import { useDataGrid } from "@/components/reui/data-grid/data-grid" +import { + DataGridTableBase, + DataGridTableBody, + DataGridTableEmpty, + DataGridTableFoot, + DataGridTableHead, + DataGridTableHeadRow, + DataGridTableHeadRowCell, + DataGridTableHeadRowCellResize, + DataGridTableRenderedRow, + DataGridTableRowSpacer, + DataGridTableViewport, + getDataGridTableRowSections, +} from "@/components/reui/data-grid/data-grid-table" +import { flexRender, HeaderGroup, Row, Table } from "@tanstack/react-table" +import { + useVirtualizer, + VirtualItem, + Virtualizer, + VirtualizerOptions, +} from "@tanstack/react-virtual" + +import { cn } from "@/lib/utils" +import { Spinner } from "@/components/ui/spinner" + +type DataGridTableVirtualScrollElements = { + containerElement: HTMLDivElement | null + scrollElement: HTMLElement | null +} + +type DataGridTableVirtualizerInstance = Virtualizer< + HTMLElement, + HTMLTableRowElement +> + +type DataGridTableVirtualizerOptions = Omit< + VirtualizerOptions, + "count" | "estimateSize" | "getItemKey" | "getScrollElement" +> & { + estimateSize?: (index: number, row: Row) => number + getItemKey?: (index: number, row: Row) => string | number + getScrollElement?: ( + elements: DataGridTableVirtualScrollElements + ) => HTMLElement | null +} + +interface DataGridTableVirtualProps { + height?: number | string + estimateSize?: number + overscan?: number + footerContent?: ReactNode + renderHeader?: boolean + onFetchMore?: () => void + isFetchingMore?: boolean + hasMore?: boolean + fetchMoreOffset?: number + virtualizerOptions?: DataGridTableVirtualizerOptions +} + +interface VirtualBodyProps { + table: Table + columnCount: number + topRows: Row[] + centerRows: Row[] + bottomRows: Row[] + virtualItems: VirtualItem[] + totalSize: number + isVirtualizationEnabled: boolean + isInfiniteMode: boolean + isFetchingMore: boolean + hasMore?: boolean + loadingMoreMessage: ReactNode + allRowsLoadedMessage: ReactNode + measureRowRef?: (element: HTMLTableRowElement | null) => void +} + +function DataGridTableVirtualSpacer({ + columnCount, + height, +}: { + columnCount: number + height: number +}) { + if (height <= 0) return null + + return ( + + + + ) +} + +function DataGridTableVirtualStatusRow({ + children, + className, + columnCount, +}: { + children: ReactNode + className?: string + columnCount: number +}) { + return ( + + + {children} + + + ) +} + +function DataGridTableVirtualBody({ + table, + columnCount, + topRows, + centerRows, + bottomRows, + virtualItems, + totalSize, + isVirtualizationEnabled, + isInfiniteMode, + isFetchingMore, + hasMore, + loadingMoreMessage, + allRowsLoadedMessage, + measureRowRef, +}: VirtualBodyProps) { + const totalRows = topRows.length + centerRows.length + bottomRows.length + + if (!totalRows) return + + const hasCenterRows = centerRows.length > 0 + const showFetchingRow = isInfiniteMode && isFetchingMore + const showCompleteRow = isInfiniteMode && hasMore === false && totalRows > 0 + const hasMiddleSection = hasCenterRows || showFetchingRow || showCompleteRow + const leadingSpacerHeight = + isVirtualizationEnabled && hasCenterRows && virtualItems.length > 0 + ? (virtualItems[0]?.start ?? 0) + : 0 + const trailingSpacerHeight = + isVirtualizationEnabled && hasCenterRows && virtualItems.length > 0 + ? Math.max( + 0, + totalSize - (virtualItems[virtualItems.length - 1]?.end ?? 0) + ) + : 0 + + const renderedRows: ReactNode[] = [] + + topRows.forEach((row, index) => { + renderedRows.push( + + ) + }) + + if (isVirtualizationEnabled) { + if (leadingSpacerHeight > 0) { + renderedRows.push( + + ) + } + + virtualItems.forEach((virtualRow) => { + const row = centerRows[virtualRow.index] + + if (!row) return + + renderedRows.push( + + ) + }) + + if (trailingSpacerHeight > 0) { + renderedRows.push( + + ) + } + } else { + centerRows.forEach((row) => { + renderedRows.push() + }) + } + + if (showFetchingRow) { + renderedRows.push( + +
+ + {loadingMoreMessage} +
+
+ ) + } + + if (showCompleteRow) { + renderedRows.push( + + {allRowsLoadedMessage} + + ) + } + + bottomRows.forEach((row, index) => { + renderedRows.push( + 0 || hasMiddleSection) + ? "bottom" + : undefined + } + /> + ) + }) + + return <>{renderedRows} +} + +/** + * Memoized virtual body: skip re-renders during active column resize. + * Column widths update via CSS variables on the element, + * so the browser handles width changes without React re-renders. + */ +const MemoizedVirtualBody = memo( + DataGridTableVirtualBody, + (_prev, next) => !!next.table.getState().columnSizingInfo.isResizingColumn +) as typeof DataGridTableVirtualBody + +function DataGridTableVirtual({ + height, + estimateSize = 48, + overscan = 10, + footerContent, + renderHeader = true, + onFetchMore, + isFetchingMore = false, + hasMore, + fetchMoreOffset = 0, + virtualizerOptions, +}: DataGridTableVirtualProps) { + const { table, props } = useDataGrid() + const { topRows, centerRows, bottomRows } = getDataGridTableRowSections( + table, + props.tableLayout?.rowsPinnable + ) + const columnCount = + table.getVisibleFlatColumns().length + + (props.tableLayout?.columnsResizable ? 1 : 0) + const isInfiniteMode = typeof onFetchMore === "function" + const [viewportElements, setViewportElements] = + useState({ + containerElement: null, + scrollElement: null, + }) + + const { + estimateSize: customEstimateSize, + getItemKey: customGetItemKey, + getScrollElement: customGetScrollElement, + measureElement: customMeasureElement, + overscan: customOverscan, + ...virtualizerOptionsRest + } = virtualizerOptions ?? {} + + const isVirtualizationEnabled = virtualizerOptions?.enabled !== false + const loadingMoreMessage = + props.fetchingMoreMessage || props.loadingMessage || "Loading..." + const allRowsLoadedMessage = + props.allRowsLoadedMessage || "All records loaded" + + const handleViewportRef = useCallback((node: HTMLDivElement | null) => { + setViewportElements({ + containerElement: node, + scrollElement: + (node?.closest( + '[data-slot="scroll-area-viewport"]' + ) as HTMLElement | null) ?? node, + }) + }, []) + + const usesExternalScrollArea = + viewportElements.scrollElement !== null && + viewportElements.scrollElement !== viewportElements.containerElement + + const resolveScrollElement = useCallback(() => { + if (customGetScrollElement) { + return customGetScrollElement(viewportElements) + } + + return viewportElements.scrollElement + }, [customGetScrollElement, viewportElements]) + + const resolveItemKey = useCallback( + (index: number) => { + const row = centerRows[index] + + if (!row) return index + + return customGetItemKey?.(index, row) ?? row.id ?? index + }, + [centerRows, customGetItemKey] + ) + + const resolveEstimateSize = useCallback( + (index: number) => { + const row = centerRows[index] + + return row + ? (customEstimateSize?.(index, row) ?? estimateSize) + : estimateSize + }, + [centerRows, customEstimateSize, estimateSize] + ) + + const virtualizer = useVirtualizer({ + count: centerRows.length, + getScrollElement: resolveScrollElement, + getItemKey: resolveItemKey, + estimateSize: resolveEstimateSize, + overscan: customOverscan ?? overscan, + measureElement: customMeasureElement, + ...virtualizerOptionsRest, + }) as DataGridTableVirtualizerInstance + + const virtualItems = isVirtualizationEnabled + ? virtualizer.getVirtualItems() + : [] + const totalSize = isVirtualizationEnabled ? virtualizer.getTotalSize() : 0 + const measureRowRef = + isVirtualizationEnabled && customMeasureElement + ? virtualizer.measureElement + : undefined + const resolvedFetchMoreOffset = useMemo( + () => Math.max(0, fetchMoreOffset), + [fetchMoreOffset] + ) + + useEffect(() => { + if ( + !isVirtualizationEnabled || + !isInfiniteMode || + hasMore === false || + isFetchingMore + ) { + return + } + + const lastItem = virtualItems[virtualItems.length - 1] + if (!lastItem) return + + if (lastItem.index >= centerRows.length - 1 - resolvedFetchMoreOffset) { + onFetchMore?.() + } + }, [ + centerRows.length, + hasMore, + isFetchingMore, + isInfiniteMode, + isVirtualizationEnabled, + onFetchMore, + resolvedFetchMoreOffset, + virtualItems, + ]) + + return ( + + + {renderHeader && ( + + {table + .getHeaderGroups() + .map((headerGroup: HeaderGroup, index) => ( + + {headerGroup.headers.map((header, hIndex) => { + const { column } = header + + return ( + + {header.isPlaceholder ? null : props.tableLayout + ?.columnsResizable && column.getCanResize() ? ( +
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} +
+ ) : ( + flexRender( + header.column.columnDef.header, + header.getContext() + ) + )} + {props.tableLayout?.columnsResizable && + column.getCanResize() && ( + + )} +
+ ) + })} +
+ ))} +
+ )} + + {renderHeader && + (props.tableLayout?.stripped || !props.tableLayout?.rowBorder) && ( + + )} + + + + + + {footerContent && ( + {footerContent} + )} +
+
+ ) +} + +export { DataGridTableVirtual } +export type { + DataGridTableVirtualProps, + DataGridTableVirtualScrollElements, + DataGridTableVirtualizerOptions, +} \ No newline at end of file diff --git a/web/src/components/reui/data-grid/data-grid-table.tsx b/web/src/components/reui/data-grid/data-grid-table.tsx new file mode 100644 index 00000000..52f686a1 --- /dev/null +++ b/web/src/components/reui/data-grid/data-grid-table.tsx @@ -0,0 +1,1436 @@ +"use client" + +import { + CSSProperties, + Fragment, + memo, + MouseEvent as ReactMouseEvent, + ReactNode, + TouchEvent as ReactTouchEvent, + Ref, + useCallback, + useEffect, + useMemo, + useState, +} from "react" +import { useDataGrid } from "@/components/reui/data-grid/data-grid" +import { + Cell, + Column, + flexRender, + Header, + HeaderGroup, + Row, + Table, +} from "@tanstack/react-table" +import { cva } from "class-variance-authority" + +import { cn } from "@/lib/utils" +import { Checkbox } from "@/components/ui/checkbox" +import { Spinner } from "@/components/ui/spinner" + +const headerCellSpacingVariants = cva("", { + variants: { + size: { + dense: + "px-2 h-8", + default: + "px-3", + }, + }, + defaultVariants: { + size: "default", + }, +}) + +const bodyCellSpacingVariants = cva("", { + variants: { + size: { + dense: + "px-2 py-1.5", + default: + "px-3 py-2", + }, + }, + defaultVariants: { + size: "default", + }, +}) + +const footerCellSpacingVariants = cva("", { + variants: { + size: { + dense: + "px-2 py-1.5", + default: + "px-3 py-2", + }, + }, + defaultVariants: { + size: "default", + }, +}) + +function getPinningStyles(column: Column): CSSProperties { + const isPinned = column.getIsPinned() + + return { + left: isPinned === "left" ? `${column.getStart("left")}px` : undefined, + right: isPinned === "right" ? `${column.getAfter("right")}px` : undefined, + position: isPinned ? "sticky" : "relative", + width: column.getSize(), + zIndex: isPinned ? 1 : 0, + } +} + +function assignRef(ref: Ref | undefined, value: T | null) { + if (!ref) return + + if (typeof ref === "function") { + ref(value) + return + } + + ;(ref as { current: T | null }).current = value +} + +type DataGridResizeStartEvent = + | ReactMouseEvent + | ReactTouchEvent + +type DataGridResizeDocumentEvent = globalThis.MouseEvent | globalThis.TouchEvent + +function isDataGridTouchEvent( + event: DataGridResizeStartEvent | DataGridResizeDocumentEvent +): event is ReactTouchEvent | globalThis.TouchEvent { + return "touches" in event +} + +function getDataGridResizeEventClientX( + event: DataGridResizeStartEvent | DataGridResizeDocumentEvent +) { + if (isDataGridTouchEvent(event)) { + return event.touches[0]?.clientX ?? event.changedTouches[0]?.clientX + } + + return event.clientX +} + +function startDataGridColumnResizeOnEnd( + event: DataGridResizeStartEvent, + header: Header, + table: Table +) { + const column = table.getColumn(header.column.id) + + if (!column || !column.getCanResize()) return + if (isDataGridTouchEvent(event) && event.touches.length > 1) return + + event.persist?.() + + const ownerDocument = event.currentTarget.ownerDocument + const previousBodyCursor = ownerDocument.body.style.cursor + const previousDocumentCursor = ownerDocument.documentElement.style.cursor + const startSize = header.getSize() + const dragStartClientX = getDataGridResizeEventClientX(event) + const headerCell = event.currentTarget.closest("th") + const headerRect = headerCell?.getBoundingClientRect() + const startOffset = + headerRect && + Number.isFinite( + table.options.columnResizeDirection === "rtl" + ? headerRect.left + : headerRect.right + ) + ? table.options.columnResizeDirection === "rtl" + ? headerRect.left + : headerRect.right + : dragStartClientX + + if (typeof dragStartClientX !== "number" || typeof startOffset !== "number") { + return + } + + ownerDocument.body.style.cursor = "col-resize" + ownerDocument.documentElement.style.cursor = "col-resize" + + const columnSizingStart = header + .getLeafHeaders() + .map( + (leafHeader) => + [leafHeader.column.id, leafHeader.column.getSize()] as [string, number] + ) + const directionMultiplier = + table.options.columnResizeDirection === "rtl" ? -1 : 1 + + const updateOffset = (clientXPos?: number, commit = false) => { + if (typeof clientXPos !== "number") return + + const nextColumnSizing: Record = {} + const deltaOffset = (clientXPos - dragStartClientX) * directionMultiplier + const deltaPercentage = Math.max(deltaOffset / startSize, -0.999999) + + columnSizingStart.forEach(([columnId, headerSize]) => { + nextColumnSizing[columnId] = + Math.round( + Math.max(headerSize + headerSize * deltaPercentage, 0) * 100 + ) / 100 + }) + + table.setColumnSizingInfo((old) => ({ + ...old, + startOffset, + startSize, + deltaOffset, + deltaPercentage, + columnSizingStart, + isResizingColumn: column.id, + })) + + if (commit) { + table.setColumnSizing((old) => ({ + ...old, + ...nextColumnSizing, + })) + } + } + + const endResize = (clientXPos?: number) => { + updateOffset(clientXPos, true) + table.setColumnSizingInfo((old) => ({ + ...old, + isResizingColumn: false, + startOffset: null, + startSize: null, + deltaOffset: null, + deltaPercentage: null, + columnSizingStart: [], + })) + ownerDocument.body.style.cursor = previousBodyCursor + ownerDocument.documentElement.style.cursor = previousDocumentCursor + } + + const mouseMoveHandler = (moveEvent: globalThis.MouseEvent) => { + updateOffset(moveEvent.clientX) + } + const mouseUpHandler = (upEvent: globalThis.MouseEvent) => { + ownerDocument.removeEventListener("mousemove", mouseMoveHandler) + ownerDocument.removeEventListener("mouseup", mouseUpHandler) + endResize(upEvent.clientX) + } + const touchMoveHandler = (moveEvent: globalThis.TouchEvent) => { + if (moveEvent.cancelable) { + moveEvent.preventDefault() + moveEvent.stopPropagation() + } + + updateOffset(getDataGridResizeEventClientX(moveEvent)) + } + const touchEndHandler = (endEvent: globalThis.TouchEvent) => { + ownerDocument.removeEventListener("touchmove", touchMoveHandler) + ownerDocument.removeEventListener("touchend", touchEndHandler) + + if (endEvent.cancelable) { + endEvent.preventDefault() + endEvent.stopPropagation() + } + + endResize(getDataGridResizeEventClientX(endEvent)) + } + + const passiveIfSupported = { passive: false } as const + + if (isDataGridTouchEvent(event)) { + ownerDocument.addEventListener( + "touchmove", + touchMoveHandler, + passiveIfSupported + ) + ownerDocument.addEventListener( + "touchend", + touchEndHandler, + passiveIfSupported + ) + } else { + ownerDocument.addEventListener( + "mousemove", + mouseMoveHandler, + passiveIfSupported + ) + ownerDocument.addEventListener( + "mouseup", + mouseUpHandler, + passiveIfSupported + ) + } + + table.setColumnSizingInfo((old) => ({ + ...old, + startOffset, + startSize, + deltaOffset: 0, + deltaPercentage: 0, + columnSizingStart, + isResizingColumn: column.id, + })) +} + +type DataGridTablePinnedBoundary = "top" | "bottom" + +function getDataGridTableRowSections( + table: Table, + rowsPinnable?: boolean +) { + if (!rowsPinnable) { + return { + topRows: [] as Row[], + centerRows: table.getRowModel().rows as Row[], + bottomRows: [] as Row[], + } + } + + return { + topRows: table.getTopRows() as Row[], + centerRows: table.getCenterRows() as Row[], + bottomRows: table.getBottomRows() as Row[], + } +} + +function getDataGridTableResolvedRows( + table: Table, + rowsPinnable?: boolean +) { + const { topRows, centerRows, bottomRows } = getDataGridTableRowSections( + table, + rowsPinnable + ) + const resolvedRows: Array<{ + row: Row + pinnedBoundary?: DataGridTablePinnedBoundary + }> = [] + + topRows.forEach((row, index) => { + resolvedRows.push({ + row, + pinnedBoundary: + index === topRows.length - 1 && + (centerRows.length > 0 || bottomRows.length > 0) + ? "top" + : undefined, + }) + }) + + centerRows.forEach((row) => { + resolvedRows.push({ row }) + }) + + bottomRows.forEach((row, index) => { + resolvedRows.push({ + row, + pinnedBoundary: + index === 0 && (centerRows.length > 0 || topRows.length > 0) + ? "bottom" + : undefined, + }) + }) + + return resolvedRows +} + +function DataGridTableFillCol() { + const { props } = useDataGrid() + + if (!props.tableLayout?.columnsResizable) return null + + return ( +
+ ) +} + +function DataGridTableFillHeadCell() { + const { props } = useDataGrid() + + if (!props.tableLayout?.columnsResizable) return null + + return ( + + {children} + + ) +} + +function DataGridTableHeadRow({ + children, + headerGroup, +}: { + children: ReactNode + headerGroup: HeaderGroup +}) { + const { props } = useDataGrid() + + return ( + th]:border-b", + props.tableLayout?.cellBorder && "*:last:border-e-0", + props.tableLayout?.stripped && "bg-transparent", + props.tableLayout?.headerBackground === false && "bg-transparent", + props.tableClassNames?.headerRow + )} + > + {children} + + + ) +} + +function DataGridTableHeadRowCell({ + children, + header, + dndRef, + dndStyle, +}: { + children: ReactNode + header: Header + dndRef?: Ref + dndStyle?: CSSProperties +}) { + const { props } = useDataGrid() + + const { column } = header + const isPinned = column.getIsPinned() + const isLastLeftPinned = isPinned === "left" && column.getIsLastColumn("left") + const isFirstRightPinned = + isPinned === "right" && column.getIsFirstColumn("right") + const isLastVisibleColumn = + column.getIndex() === + header.getContext().table.getVisibleLeafColumns().length - 1 + const headerCellSpacing = headerCellSpacingVariants({ + size: props.tableLayout?.dense ? "dense" : "default", + }) + + return ( + + ) +} + +function DataGridTableHeadRowCellResize({ + header, +}: { + header: Header +}) { + const { props, table } = useDataGrid() + const { column } = header + const isLastVisibleColumn = + column.getIndex() === + header.getContext().table.getVisibleLeafColumns().length - 1 + const isResizeModeOnEnd = + (props.tableLayout?.columnsResizeMode ?? table.options.columnResizeMode) === + "onEnd" + + const handleMouseDown = (event: ReactMouseEvent) => { + event.preventDefault() + event.stopPropagation() + + if (isResizeModeOnEnd) { + startDataGridColumnResizeOnEnd(event, header, table) + return + } + + header.getResizeHandler()(event) + } + + const handleTouchStart = (event: ReactTouchEvent) => { + event.preventDefault() + event.stopPropagation() + + if (isResizeModeOnEnd) { + startDataGridColumnResizeOnEnd(event, header, table) + return + } + + header.getResizeHandler()(event) + } + + return ( +
column.resetSize(), + onMouseDown: handleMouseDown, + onTouchStart: handleTouchStart, + className: cn( + "absolute top-0 h-full cursor-col-resize user-select-none touch-none z-10 flex", + isLastVisibleColumn + ? "end-0 w-5 justify-end before:hidden" + : "-end-2 w-5 justify-center before:absolute before:inset-y-0 before:w-px before:-translate-x-px before:bg-border", + column.getIsResizing() && + (isResizeModeOnEnd + ? "opacity-100" + : isLastVisibleColumn + ? "before:absolute before:end-0 before:block before:inset-y-0 before:w-0.5 before:bg-primary opacity-100" + : "before:block before:bg-primary before:w-0.5 opacity-100") + ), + }} + /> + ) +} + +function DataGridTableResizeIndicator({ + viewportElement, +}: { + viewportElement: HTMLDivElement | null +}) { + const { props, table } = useDataGrid() + const columnSizingInfo = table.getState().columnSizingInfo + const resizingColumnId = columnSizingInfo.isResizingColumn + const resizeMode = + props.tableLayout?.columnsResizeMode ?? table.options.columnResizeMode + + if ( + !props.tableLayout?.columnsResizable || + resizeMode !== "onEnd" || + !resizingColumnId + ) { + return null + } + + const resizingHeader = table + .getFlatHeaders() + .find( + (header) => + header.column.id === resizingColumnId || header.id === resizingColumnId + ) + + if (!resizingHeader) return null + + const deltaOffset = columnSizingInfo.deltaOffset ?? 0 + const headerHeight = + viewportElement + ?.querySelector('[data-slot="data-grid-table"] thead') + ?.getBoundingClientRect().height ?? 0 + const indicatorLeft = + typeof columnSizingInfo.startOffset === "number" && viewportElement + ? columnSizingInfo.startOffset - + viewportElement.getBoundingClientRect().left + : resizingHeader.getStart() + resizingHeader.getSize() + + return ( +
+} + +function DataGridTableBody({ children }: { children: ReactNode }) { + const { props } = useDataGrid() + + return ( + + {children} + + ) +} + +function DataGridTableFoot({ children }: { children: ReactNode }) { + const { props } = useDataGrid() + return ( + + {children} + + ) +} + +function DataGridTableFootRow({ children }: { children: ReactNode }) { + const { props } = useDataGrid() + return ( + + {children} + + + ) +} + +function DataGridTableFootRowCell({ + children, + colSpan, + className, +}: { + children?: ReactNode + colSpan?: number + className?: string +}) { + const { props } = useDataGrid() + const spacing = footerCellSpacingVariants({ + size: props.tableLayout?.dense ? "dense" : "default", + }) + return ( + + ) +} + +function DataGridTableBodyRowSkeleton({ children }: { children: ReactNode }) { + const { table, props } = useDataGrid() + + return ( + td]:border-b", + props.tableLayout?.cellBorder && "*:last:border-e-0", + props.tableLayout?.stripped && + "odd:bg-muted/90 odd:hover:bg-muted hover:bg-transparent", + table.options.enableRowSelection && "*:first:relative", + props.tableClassNames?.bodyRow + )} + > + {children} + + + ) +} + +function DataGridTableBodyRowSkeletonCell({ + children, + column, +}: { + children: ReactNode + column: Column +}) { + const { props, table } = useDataGrid() + const bodyCellSpacing = bodyCellSpacingVariants({ + size: props.tableLayout?.dense ? "dense" : "default", + }) + + return ( + + ) +} + +function DataGridTableBodyRow({ + children, + row, + pinnedBoundary, + rowRef, + dndRef, + dndStyle, +}: { + children: ReactNode + row: Row + pinnedBoundary?: DataGridTablePinnedBoundary + rowRef?: Ref + dndRef?: Ref + dndStyle?: CSSProperties +}) { + const { props, table } = useDataGrid() + const isRowPinned = row.getIsPinned() + + return ( + { + assignRef(rowRef, node) + assignRef(dndRef, node) + }} + style={{ ...(dndStyle ? dndStyle : null) }} + data-state={ + table.options.enableRowSelection && row.getIsSelected() + ? "selected" + : undefined + } + data-row-pinned={isRowPinned || undefined} + data-row-pinned-boundary={pinnedBoundary} + onClick={() => props.onRowClick && props.onRowClick(row.original)} + className={cn( + "hover:bg-muted/40 data-[state=selected]:bg-muted/50", + props.onRowClick && "cursor-pointer", + !props.tableLayout?.stripped && + props.tableLayout?.rowBorder && + "border-border border-b [&:not(:last-child)>td]:border-b", + props.tableLayout?.cellBorder && "*:last:border-e-0", + props.tableLayout?.stripped && + "odd:bg-muted/90 odd:hover:bg-muted hover:bg-transparent", + table.options.enableRowSelection && "*:first:relative", + props.tableLayout?.rowsPinnable && + isRowPinned && + "bg-muted/30 hover:bg-muted/50", + pinnedBoundary === "top" && "[&>td]:shadow-[0_2px_0_rgba(0,0,0,0.03)]", + pinnedBoundary === "bottom" && + "[&>td]:shadow-[0_2px_0_rgba(0,0,0,0.03)]", + props.tableClassNames?.bodyRow + )} + > + {children} + + + ) +} + +function DataGridTableBodyRowExpandded({ row }: { row: Row }) { + const { props, table } = useDataGrid() + + return ( + td]:border-b" + )} + > + + + ) +} + +function DataGridTableBodyRowCell({ + children, + cell, + dndRef, + dndStyle, +}: { + children: ReactNode + cell: Cell + dndRef?: Ref + dndStyle?: CSSProperties +}) { + const { props } = useDataGrid() + + const { column, row } = cell + const isPinned = column.getIsPinned() + const isLastLeftPinned = isPinned === "left" && column.getIsLastColumn("left") + const isFirstRightPinned = + isPinned === "right" && column.getIsFirstColumn("right") + const bodyCellSpacing = bodyCellSpacingVariants({ + size: props.tableLayout?.dense ? "dense" : "default", + }) + + return ( + + ) +} + +function DataGridTableRenderedRow({ + row, + pinnedBoundary, + rowRef, +}: { + row: Row + pinnedBoundary?: DataGridTablePinnedBoundary + rowRef?: Ref +}) { + return ( + + + {row.getVisibleCells().map((cell: Cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + {row.getIsExpanded() && } + + ) +} + +function DataGridTableEmpty() { + const { table, props } = useDataGrid() + const visibleColumnCount = + table.getVisibleLeafColumns().length + + (props.tableLayout?.columnsResizable ? 1 : 0) + + return ( + + + + ) +} + +function DataGridTableLoader() { + const { props } = useDataGrid() + + return ( +
+
+ + {props.loadingMessage || "Loading..."} +
+
+ ) +} + +function DataGridTableRowPin({ row }: { row: Row }) { + const isPinned = row.getIsPinned() + + return ( + + ) +} + +function DataGridTableRowSelect({ row }: { row: Row }) { + return ( + <> + + row.toggleSelected(!!value)} + aria-label="Select row" + className="align-[inherit]" + /> + + ) +} + +function DataGridTableRowSelectAll() { + const { table, recordCount, isLoading } = useDataGrid() + + const isAllSelected = table.getIsAllPageRowsSelected() + const isSomeSelected = table.getIsSomePageRowsSelected() + + return ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + className="align-[inherit]" + /> + ) +} + +function DataGridTableBodyRows({ table }: { table: Table }) { + const { isLoading, props } = useDataGrid() + const pagination = table.getState().pagination + + if (isLoading && props.loadingMode === "skeleton" && pagination?.pageSize) { + return ( + <> + {Array.from({ length: pagination.pageSize }).map((_, rowIndex) => ( + + {table.getVisibleFlatColumns().map((column, colIndex) => ( + + {column.columnDef.meta?.skeleton} + + ))} + + ))} + + ) + } + + if (isLoading && props.loadingMode === "spinner") { + return ( + + + + ) + } + + const resolvedRows = getDataGridTableResolvedRows( + table, + props.tableLayout?.rowsPinnable + ) + + if (!resolvedRows.length) return + + return ( + <> + {resolvedRows.map(({ row, pinnedBoundary }) => ( + + ))} + + ) +} + +/** + * Memoized body rows: skip re-renders during active column resize. + * Column widths update via CSS variables on the
+ {children} +
+ {children} +
+ {children} +
+ {table + .getAllColumns() + .find((column) => column.columnDef.meta?.expandedContent) + ?.columnDef.meta?.expandedContent?.(row.original)} +
+ {children} +
+ {props.emptyMessage || "No data available"} +
+
+ + + + + {props.loadingMessage || "Loading..."} +
+
element, + * so the browser handles width changes without React re-renders. + */ +const MemoizedDataGridTableBodyRows = memo( + DataGridTableBodyRows, + (_prev, next) => !!next.table.getState().columnSizingInfo.isResizingColumn +) as typeof DataGridTableBodyRows + +function DataGridTableHeader() { + const { table, props } = useDataGrid() + + return ( + + + + {table + .getHeaderGroups() + .map((headerGroup: HeaderGroup, index) => { + return ( + + {headerGroup.headers.map((header, index) => { + const { column } = header + + return ( + + {header.isPlaceholder ? null : props.tableLayout + ?.columnsResizable && column.getCanResize() ? ( +
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} +
+ ) : ( + flexRender( + header.column.columnDef.header, + header.getContext() + ) + )} + {props.tableLayout?.columnsResizable && + column.getCanResize() && ( + + )} +
+ ) + })} +
+ ) + })} +
+
+
+ ) +} + +function DataGridTable({ + footerContent, + renderHeader = true, +}: { + footerContent?: ReactNode + renderHeader?: boolean +}) { + const { table, props } = useDataGrid() + + return ( + + + {renderHeader && ( + + {table + .getHeaderGroups() + .map((headerGroup: HeaderGroup, index) => { + return ( + + {headerGroup.headers.map((header, index) => { + const { column } = header + + return ( + + {header.isPlaceholder ? null : props.tableLayout + ?.columnsResizable && column.getCanResize() ? ( +
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} +
+ ) : ( + flexRender( + header.column.columnDef.header, + header.getContext() + ) + )} + {props.tableLayout?.columnsResizable && + column.getCanResize() && ( + + )} +
+ ) + })} +
+ ) + })} +
+ )} + + {renderHeader && + (props.tableLayout?.stripped || !props.tableLayout?.rowBorder) && ( + + )} + + + + + + {footerContent && ( + {footerContent} + )} +
+
+ ) +} + +export { + DataGridTable, + DataGridTableBase, + DataGridTableBody, + DataGridTableBodyRow, + DataGridTableBodyRowCell, + DataGridTableBodyRowExpandded, + DataGridTableRenderedRow, + DataGridTableBodyRowSkeleton, + DataGridTableBodyRowSkeletonCell, + DataGridTableEmpty, + DataGridTableFoot, + DataGridTableFootRow, + DataGridTableFootRowCell, + DataGridTableHeader, + DataGridTableHead, + DataGridTableHeadRow, + DataGridTableHeadRowCell, + DataGridTableHeadRowCellResize, + DataGridTableLoader, + DataGridTableRowPin, + DataGridTableRowSelect, + DataGridTableRowSelectAll, + DataGridTableRowSpacer, + DataGridTableViewport, + getDataGridTableResolvedRows, + getDataGridTableRowSections, +} + +export type { DataGridTablePinnedBoundary } \ No newline at end of file diff --git a/web/src/components/reui/data-grid/data-grid.tsx b/web/src/components/reui/data-grid/data-grid.tsx new file mode 100644 index 00000000..a0994b42 --- /dev/null +++ b/web/src/components/reui/data-grid/data-grid.tsx @@ -0,0 +1,270 @@ +"use client" + +import { createContext, ReactNode, useContext, useMemo } from "react" +import { + Column, + ColumnFiltersState, + RowData, + SortingState, + Table, +} from "@tanstack/react-table" + +import { cn } from "@/lib/utils" + +declare module "@tanstack/react-table" { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface ColumnMeta { + headerTitle?: string + headerClassName?: string + cellClassName?: string + skeleton?: ReactNode + expandedContent?: (row: TData) => ReactNode + } +} + +/** Label for headers / column visibility: `meta.headerTitle`, string `columnDef.header`, or `column.id`. */ +export function getColumnHeaderLabel( + column: Column +): string { + const meta = column.columnDef.meta as { headerTitle?: string } | undefined + if (typeof meta?.headerTitle === "string") return meta.headerTitle + const defHeader = column.columnDef.header + if (typeof defHeader === "string") return defHeader + return String(column.id) +} + +export type DataGridApiFetchParams = { + pageIndex: number + pageSize: number + sorting?: SortingState + filters?: ColumnFiltersState + searchQuery?: string +} + +export type DataGridApiResponse = { + data: T[] + empty: boolean + pagination: { + total: number + page: number + } +} + +export interface DataGridContextProps { + props: DataGridProps + table: Table + recordCount: number + isLoading: boolean +} + +export type DataGridRequestParams = { + pageIndex: number + pageSize: number + sorting?: SortingState + columnFilters?: ColumnFiltersState +} + +export interface DataGridProps { + className?: string + table?: Table + recordCount: number + children?: ReactNode + onRowClick?: (row: TData) => void + isLoading?: boolean + loadingMode?: "skeleton" | "spinner" + loadingMessage?: ReactNode | string + fetchingMoreMessage?: ReactNode | string + allRowsLoadedMessage?: ReactNode | string + emptyMessage?: ReactNode | string + tableLayout?: { + dense?: boolean + cellBorder?: boolean + rowBorder?: boolean + rowRounded?: boolean + stripped?: boolean + headerBackground?: boolean + headerBorder?: boolean + headerSticky?: boolean + width?: "auto" | "fixed" + columnsVisibility?: boolean + columnsResizable?: boolean + columnsResizeMode?: "onChange" | "onEnd" + columnsPinnable?: boolean + columnsMovable?: boolean + columnsDraggable?: boolean + rowsDraggable?: boolean + rowsPinnable?: boolean + } + tableClassNames?: { + base?: string + header?: string + headerRow?: string + headerSticky?: string + body?: string + bodyRow?: string + footer?: string + edgeCell?: string + } +} + +const DataGridContext = createContext< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + DataGridContextProps | undefined +>(undefined) + +function useDataGrid() { + const context = useContext(DataGridContext) + if (!context) { + throw new Error("useDataGrid must be used within a DataGridProvider") + } + return context +} + +function DataGridProvider({ + children, + table, + ...props +}: DataGridProps & { table: Table }) { + const tableState = table.getState() + const resolvedColumnsResizeMode = + props.tableLayout?.columnsResizeMode ?? "onEnd" + + // Keep resize mode aligned with the DataGrid contract every render so + // consumer-level useReactTable options cannot flip it back between drags. + if (props.tableLayout?.columnsResizable) { + table.options.columnResizeMode = resolvedColumnsResizeMode + } + + // Memoize context value so consumers don't re-render during column resize. + // Column sizing state is intentionally excluded from deps -- CSS variables + // on the
element handle width updates without React re-renders. + const value = useMemo( + () => ({ + props, + table, + recordCount: props.recordCount, + isLoading: props.isLoading || false, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + table, + props.recordCount, + props.isLoading, + props.loadingMode, + props.loadingMessage, + props.fetchingMoreMessage, + props.allRowsLoadedMessage, + props.emptyMessage, + props.onRowClick, + props.className, + // eslint-disable-next-line react-hooks/exhaustive-deps + JSON.stringify(props.tableLayout), + // eslint-disable-next-line react-hooks/exhaustive-deps + JSON.stringify(props.tableClassNames), + tableState.sorting, + tableState.pagination, + tableState.columnFilters, + tableState.rowSelection, + tableState.expanded, + tableState.columnVisibility, + tableState.columnOrder, + tableState.columnPinning, + tableState.globalFilter, + ] + ) + + return ( + + {children} + + ) +} + +function DataGrid({ + children, + table, + ...props +}: DataGridProps) { + const defaultProps: Partial> = { + loadingMode: "skeleton", + tableLayout: { + dense: false, + cellBorder: false, + rowBorder: true, + rowRounded: false, + stripped: false, + headerSticky: false, + headerBackground: true, + headerBorder: true, + width: "fixed", + columnsVisibility: false, + columnsResizable: false, + columnsResizeMode: "onEnd", + columnsPinnable: false, + columnsMovable: false, + columnsDraggable: false, + rowsDraggable: false, + rowsPinnable: false, + }, + tableClassNames: { + base: "", + header: "", + headerRow: "", + headerSticky: "sticky top-0 z-15 bg-background/90 backdrop-blur-xs", + body: "", + bodyRow: "", + footer: "", + edgeCell: "", + }, + } + + const mergedProps: DataGridProps = { + ...defaultProps, + ...props, + tableLayout: { + ...defaultProps.tableLayout, + ...(props.tableLayout || {}), + }, + tableClassNames: { + ...defaultProps.tableClassNames, + ...(props.tableClassNames || {}), + }, + } + + // Ensure table is provided + if (!table) { + throw new Error('DataGrid requires a "table" prop') + } + + return ( + + {children} + + ) +} + +function DataGridContainer({ + children, + className, + border = true, +}: { + children: ReactNode + className?: string + border?: boolean +}) { + return ( +
+ {children} +
+ ) +} + +export { useDataGrid, DataGridProvider, DataGrid, DataGridContainer } \ No newline at end of file diff --git a/web/src/components/reui/filters.tsx b/web/src/components/reui/filters.tsx new file mode 100644 index 00000000..734a3cc1 --- /dev/null +++ b/web/src/components/reui/filters.tsx @@ -0,0 +1,1888 @@ +"use client" + +import type React from "react" +import { + createContext, + useCallback, + useContext, + useEffect, + useId, + useMemo, + useRef, + useState, +} from "react" +import { cva } from "class-variance-authority" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { + ButtonGroup, + ButtonGroupText, +} from "@/components/ui/button-group" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Input } from "@/components/ui/input" +import { + InputGroup, + InputGroupAddon, + InputGroupButton, + InputGroupInput, + InputGroupText, +} from "@/components/ui/input-group" +import { Kbd } from "@/components/ui/kbd" +import { ScrollArea } from "@/components/ui/scroll-area" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { AlertCircleIcon, XIcon, CheckIcon, PlusIcon } from "lucide-react" + +// i18n Configuration Interface +export interface FilterI18nConfig { + // UI Labels + addFilter: string + searchFields: string + noFieldsFound: string + noResultsFound: string + select: string + true: string + false: string + min: string + max: string + to: string + typeAndPressEnter: string + selected: string + selectedCount: string + percent: string + defaultCurrency: string + defaultColor: string + addFilterTitle: string + + // Operators + operators: { + is: string + isNot: string + isAnyOf: string + isNotAnyOf: string + includesAll: string + excludesAll: string + before: string + after: string + between: string + notBetween: string + contains: string + notContains: string + startsWith: string + endsWith: string + isExactly: string + equals: string + notEquals: string + greaterThan: string + lessThan: string + overlaps: string + includes: string + excludes: string + includesAllOf: string + includesAnyOf: string + empty: string + notEmpty: string + } + + // Placeholders + placeholders: { + enterField: (fieldType: string) => string + selectField: string + searchField: (fieldName: string) => string + enterKey: string + enterValue: string + } + + // Helper functions + helpers: { + formatOperator: (operator: string) => string + } + + // Validation + validation: { + invalidEmail: string + invalidUrl: string + invalidTel: string + invalid: string + } +} + +// Default English i18n configuration +export const DEFAULT_I18N: FilterI18nConfig = { + // UI Labels + addFilter: "Filter", + searchFields: "Filter...", + noFieldsFound: "No filters found.", + noResultsFound: "No results found.", + select: "Select...", + true: "True", + false: "False", + min: "Min", + max: "Max", + to: "to", + typeAndPressEnter: "Type and press Enter to add tag", + selected: "selected", + selectedCount: "selected", + percent: "%", + defaultCurrency: "$", + defaultColor: "#000000", + addFilterTitle: "Add filter", + + // Operators + operators: { + is: "is", + isNot: "is not", + isAnyOf: "is any of", + isNotAnyOf: "is not any of", + includesAll: "includes all", + excludesAll: "excludes all", + before: "before", + after: "after", + between: "between", + notBetween: "not between", + contains: "contains", + notContains: "does not contain", + startsWith: "starts with", + endsWith: "ends with", + isExactly: "is exactly", + equals: "equals", + notEquals: "not equals", + greaterThan: "greater than", + lessThan: "less than", + overlaps: "overlaps", + includes: "includes", + excludes: "excludes", + includesAllOf: "includes all of", + includesAnyOf: "includes any of", + empty: "is empty", + notEmpty: "is not empty", + }, + + // Placeholders + placeholders: { + enterField: (fieldType: string) => `Enter ${fieldType}...`, + selectField: "Select...", + searchField: (fieldName: string) => `Search ${fieldName.toLowerCase()}...`, + enterKey: "Enter key...", + enterValue: "Enter value...", + }, + + // Helper functions + helpers: { + formatOperator: (operator: string) => operator.replace(/_/g, " "), + }, + + // Validation + validation: { + invalidEmail: "Invalid email format", + invalidUrl: "Invalid URL format", + invalidTel: "Invalid phone format", + invalid: "Invalid input format", + }, +} + +// Context for all Filter component props +interface FilterContextValue { + variant: "solid" | "default" + size: "sm" | "default" | "lg" + radius: "default" | "full" + i18n: FilterI18nConfig + className?: string + showSearchInput?: boolean + trigger?: React.ReactNode + allowMultiple?: boolean +} + +const FilterContext = createContext({ + variant: "default", + size: "default", + radius: "default", + i18n: DEFAULT_I18N, + className: undefined, + showSearchInput: true, + trigger: undefined, + allowMultiple: true, +}) + +const useFilterContext = () => useContext(FilterContext) + +// Container variant for filters wrapper +const filtersContainerVariants = cva("flex flex-wrap items-center", { + variants: { + variant: { + solid: "gap-2", + default: "", + }, + size: { + sm: "gap-1.5", + default: "gap-2.5", + lg: "gap-3.5", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, +}) + +function FilterInput({ + field, + onBlur, + onKeyDown, + className, + ...props +}: React.InputHTMLAttributes & { + className?: string + field?: FilterFieldConfig +}) { + const context = useFilterContext() + const [isValid, setIsValid] = useState(true) + const [validationMessage, setValidationMessage] = useState("") + const inputRef = useRef(null) + + useEffect(() => { + if (props.autoFocus) { + const timer = setTimeout(() => { + inputRef.current?.focus() + }, 300) + return () => clearTimeout(timer) + } + }, [props.autoFocus]) + + // Validation function to check if input matches pattern + const validateInput = (value: string, pattern?: string): boolean => { + if (!pattern || !value) return true + const regex = new RegExp(pattern) + return regex.test(value) + } + + // Get validation message for field type + const getValidationMessage = (): string => { + return context.i18n.validation.invalid + } + + // Handle blur event - validate when user leaves input + const handleBlur = (e: React.FocusEvent) => { + const value = e.target.value + const pattern = field?.pattern || props.pattern + + // Only validate if there's a value and (pattern or validation function) + if (value && (pattern || field?.validation)) { + let valid = true + let customMessage = "" + + // If there's a custom validation function, use it + if (field?.validation) { + const result = field.validation(value) + // Handle both boolean and object return types + if (typeof result === "boolean") { + valid = result + } else { + valid = result.valid + customMessage = result.message || "" + } + } else if (pattern) { + // Use pattern validation + valid = validateInput(value, pattern) + } + + setIsValid(valid) + setValidationMessage(valid ? "" : customMessage || getValidationMessage()) + } else { + // Reset validation state for empty values or no validation + setIsValid(true) + setValidationMessage("") + } + + // Call the original onBlur if provided + onBlur?.(e) + } + + // Handle keydown event - hide validation error when user starts typing + const handleKeyDown = (e: React.KeyboardEvent) => { + // Hide validation error when user starts typing (any key except special keys) + if ( + !isValid && + ![ + "Tab", + "Escape", + "Enter", + "ArrowUp", + "ArrowDown", + "ArrowLeft", + "ArrowRight", + ].includes(e.key) + ) { + setIsValid(true) + setValidationMessage("") + } + + // Call the original onKeyDown if provided + onKeyDown?.(e) + } + + return ( + + {field?.prefix && ( + + {field.prefix} + + )} + + {!isValid && validationMessage && ( + + + + + + + + + +

{validationMessage}

+
+
+
+
+ )} + + {field?.suffix && ( + + {field.suffix} + + )} +
+ ) +} + +interface FilterRemoveButtonProps extends React.ButtonHTMLAttributes { + icon?: React.ReactNode +} + +function FilterRemoveButton({ + className, + icon = ( + + ), + ...props +}: FilterRemoveButtonProps) { + const context = useFilterContext() + + const sizeMap = { + sm: "sm" as const, + default: "sm" as const, + lg: "default" as const, + } + + return ( + + ) +} + +// Generic types for flexible filter system +export interface FilterOption { + value: T + label: string + icon?: React.ReactNode + metadata?: Record + className?: string +} + +export interface FilterOperator { + value: string + label: string + supportsMultiple?: boolean +} + +// Custom renderer props interface +export interface CustomRendererProps { + field: FilterFieldConfig + values: T[] + onChange: (values: T[]) => void + operator: string +} + +// Grouped field configuration interface +export interface FilterFieldGroup { + group?: string + fields: FilterFieldConfig[] +} + +// Union type for both flat and grouped field configurations +export type FilterFieldsConfig = + | FilterFieldConfig[] + | FilterFieldGroup[] + +export interface FilterFieldConfig { + key?: string + label?: string + icon?: React.ReactNode + type?: "select" | "multiselect" | "text" | "custom" | "separator" + // Group-level configuration + group?: string + fields?: FilterFieldConfig[] + // Field-specific options + options?: FilterOption[] + operators?: FilterOperator[] + customRenderer?: (props: CustomRendererProps) => React.ReactNode + customValueRenderer?: ( + values: T[], + options: FilterOption[] + ) => React.ReactNode + placeholder?: string + searchable?: boolean + maxSelections?: number + min?: number + max?: number + step?: number + prefix?: string | React.ReactNode + suffix?: string | React.ReactNode + pattern?: string + validation?: ( + value: unknown + ) => boolean | { valid: boolean; message?: string } + allowCustomValues?: boolean + className?: string + menuPopupClassName?: string + // Grouping options (legacy support) + groupLabel?: string + // Boolean field options + onLabel?: string + offLabel?: string + // Input event handlers + onInputChange?: (e: React.ChangeEvent) => void + // Default operator to use when creating a filter for this field + defaultOperator?: string + // Controlled values support for this field + value?: T[] + onValueChange?: (values: T[]) => void +} + +// Helper functions to handle both flat and grouped field configurations +const isFieldGroup = ( + item: FilterFieldConfig | FilterFieldGroup +): item is FilterFieldGroup => { + return "fields" in item && Array.isArray(item.fields) +} + +// Helper function to check if a FilterFieldConfig is a group-level configuration +const isGroupLevelField = ( + field: FilterFieldConfig +): boolean => { + return Boolean(field.group && field.fields) +} + +const flattenFields = ( + fields: FilterFieldsConfig +): FilterFieldConfig[] => { + return fields.reduce[]>((acc, item) => { + if (isFieldGroup(item)) { + return [...acc, ...item.fields] + } + // Handle group-level fields (new structure) + if (isGroupLevelField(item)) { + return [...acc, ...item.fields!] + } + return [...acc, item] + }, []) +} + +const getFieldsMap = ( + fields: FilterFieldsConfig +): Record> => { + const flatFields = flattenFields(fields) + return flatFields.reduce( + (acc, field) => { + // Only add fields that have a key (skip group-level configurations) + if (field.key) { + acc[field.key] = field + } + return acc + }, + {} as Record> + ) +} + +// Helper function to create operators from i18n config +const createOperatorsFromI18n = ( + i18n: FilterI18nConfig +): Record => ({ + select: [ + { value: "is", label: i18n.operators.is }, + { value: "is_not", label: i18n.operators.isNot }, + { value: "empty", label: i18n.operators.empty }, + { value: "not_empty", label: i18n.operators.notEmpty }, + ], + multiselect: [ + { value: "is_any_of", label: i18n.operators.isAnyOf }, + { value: "is_not_any_of", label: i18n.operators.isNotAnyOf }, + { value: "includes_all", label: i18n.operators.includesAll }, + { value: "excludes_all", label: i18n.operators.excludesAll }, + { value: "empty", label: i18n.operators.empty }, + { value: "not_empty", label: i18n.operators.notEmpty }, + ], + text: [ + { value: "contains", label: i18n.operators.contains }, + { value: "not_contains", label: i18n.operators.notContains }, + { value: "starts_with", label: i18n.operators.startsWith }, + { value: "ends_with", label: i18n.operators.endsWith }, + { value: "is", label: i18n.operators.isExactly }, + { value: "empty", label: i18n.operators.empty }, + { value: "not_empty", label: i18n.operators.notEmpty }, + ], + custom: [ + { value: "is", label: i18n.operators.is }, + { value: "after", label: i18n.operators.after }, + { value: "is", label: i18n.operators.is }, + { value: "between", label: i18n.operators.between }, + { value: "empty", label: i18n.operators.empty }, + { value: "not_empty", label: i18n.operators.notEmpty }, + ], +}) + +// Default operators for different field types (using default i18n) +export const DEFAULT_OPERATORS: Record = + createOperatorsFromI18n(DEFAULT_I18N) + +// Helper function to get operators for a field +const getOperatorsForField = ( + field: FilterFieldConfig, + values: T[], + i18n: FilterI18nConfig +): FilterOperator[] => { + if (field.operators) return field.operators + + const operators = createOperatorsFromI18n(i18n) + + // Determine field type for operator selection + let fieldType = field.type || "select" + + // If it's a select field but has multiple values, treat as multiselect + if (fieldType === "select" && values.length > 1) { + fieldType = "multiselect" + } + + // If it's a multiselect field or has multiselect operators, use multiselect operators + if (fieldType === "multiselect" || field.type === "multiselect") { + return operators.multiselect + } + + return operators[fieldType] || operators.select +} + +interface FilterOperatorDropdownProps { + field: FilterFieldConfig + operator: string + values: T[] + onChange: (operator: string) => void +} + +function FilterOperatorDropdown({ + field, + operator, + values, + onChange, +}: FilterOperatorDropdownProps) { + const context = useFilterContext() + const operators = getOperatorsForField(field, values, context.i18n) + + // Find the operator label, with fallback to formatted operator name + const operatorLabel = + operators.find((op) => op.value === operator)?.label || + context.i18n.helpers.formatOperator(operator) + + return ( + + + + + + {operators.map((op) => ( + onChange(op.value)} + className={cn( + "data-highlighted:bg-accent data-highlighted:text-accent-foreground flex items-center justify-between" + )} + > + {op.label} + + + ))} + + + ) +} + +interface FilterValueSelectorProps { + field: FilterFieldConfig + values: T[] + onChange: (values: T[]) => void + operator: string + autoFocus?: boolean +} + +interface SelectOptionsPopoverProps { + field: FilterFieldConfig + values: T[] + onChange: (values: T[]) => void + onClose?: () => void + inline?: boolean +} + +function SelectOptionsPopover({ + field, + values, + onChange, + onClose, + inline = false, +}: SelectOptionsPopoverProps) { + const [open, setOpen] = useState(false) + const [searchInput, setSearchInput] = useState("") + const [highlightedIndex, setHighlightedIndex] = useState(-1) + const inputRef = useRef(null) + const context = useFilterContext() + const baseId = useId() + + useEffect(() => { + setHighlightedIndex(-1) + }, [searchInput, open]) + + useEffect(() => { + if (highlightedIndex >= 0 && open) { + const element = document.getElementById( + `${baseId}-item-${highlightedIndex}` + ) + element?.scrollIntoView({ block: "nearest" }) + } + }, [highlightedIndex, open, baseId]) + + const isMultiSelect = field.type === "multiselect" || values.length > 1 + const effectiveValues = + (field.value !== undefined ? (field.value as T[]) : values) || [] + + const selectedOptions = + field.options?.filter((opt) => effectiveValues.includes(opt.value)) || [] + const unselectedOptions = + field.options?.filter((opt) => !effectiveValues.includes(opt.value)) || [] + + // Filter options based on search input + const filteredSelectedOptions = selectedOptions // Keep all selected visible + const filteredUnselectedOptions = unselectedOptions.filter((opt) => + opt.label.toLowerCase().includes(searchInput.toLowerCase()) + ) + + const allFilteredOptions = useMemo( + () => [...filteredSelectedOptions, ...filteredUnselectedOptions], + [filteredSelectedOptions, filteredUnselectedOptions] + ) + + const handleClose = () => { + setOpen(false) + onClose?.() + } + + const renderMenuContent = () => ( + <> + {field.searchable !== false && ( + <> + = 0 + ? `${baseId}-item-${highlightedIndex}` + : undefined + } + placeholder={context.i18n.placeholders.searchField( + field.label || "" + )} + className={cn( + "border-input h-8 rounded-none border-0 bg-transparent! px-2 text-sm shadow-none", + "focus-visible:border-border focus-visible:ring-0 focus-visible:ring-offset-0" + )} + value={searchInput} + onChange={(e) => setSearchInput(e.target.value)} + onClick={(e) => e.stopPropagation()} + onKeyDown={(e) => { + if (e.key === "ArrowDown") { + e.preventDefault() + if (allFilteredOptions.length > 0) { + setHighlightedIndex((prev) => + prev < allFilteredOptions.length - 1 ? prev + 1 : 0 + ) + } + } else if (e.key === "ArrowUp") { + e.preventDefault() + if (allFilteredOptions.length > 0) { + setHighlightedIndex((prev) => + prev > 0 ? prev - 1 : allFilteredOptions.length - 1 + ) + } + } else if (e.key === "ArrowLeft") { + e.preventDefault() + setOpen(false) + } else if (e.key === "Enter" && highlightedIndex >= 0) { + e.preventDefault() + const option = allFilteredOptions[highlightedIndex] + if (option) { + const isSelected = effectiveValues.includes(option.value as T) + const next = isSelected + ? (effectiveValues.filter((v) => v !== option.value) as T[]) + : isMultiSelect + ? ([...effectiveValues, option.value] as T[]) + : ([option.value] as T[]) + + if ( + !isSelected && + isMultiSelect && + field.maxSelections && + next.length > field.maxSelections + ) { + return + } + + if (field.onValueChange) { + field.onValueChange(next) + } else { + onChange(next) + } + if (!isMultiSelect) handleClose() + } + } + e.stopPropagation() + }} + /> + + + )} +
+
+ + {allFilteredOptions.length === 0 && ( +
+ {context.i18n.noResultsFound} +
+ )} + + {/* Selected items */} + {filteredSelectedOptions.length > 0 && ( + + {filteredSelectedOptions.map((option, index) => { + const isHighlighted = highlightedIndex === index + const itemId = `${baseId}-item-${index}` + + return ( + setHighlightedIndex(index)} + checked={true} + className={cn( + "data-highlighted:bg-accent data-highlighted:text-accent-foreground", + option.className + )} + onSelect={(e) => { + if (isMultiSelect) e.preventDefault() + }} + onCheckedChange={() => { + const next = effectiveValues.filter( + (v) => v !== option.value + ) as T[] + if (field.onValueChange) { + field.onValueChange(next) + } else { + onChange(next) + } + if (!isMultiSelect) handleClose() + }} + > + {option.icon && option.icon} + {option.label} + + ) + })} + + )} + + {/* Separator */} + {filteredSelectedOptions.length > 0 && + filteredUnselectedOptions.length > 0 && ( + + )} + + {/* Available items */} + {filteredUnselectedOptions.length > 0 && ( + + {filteredUnselectedOptions.map((option, index) => { + const overallIndex = index + filteredSelectedOptions.length + const isHighlighted = highlightedIndex === overallIndex + const itemId = `${baseId}-item-${overallIndex}` + + return ( + setHighlightedIndex(overallIndex)} + checked={false} + className={cn( + "data-highlighted:bg-accent data-highlighted:text-accent-foreground", + option.className + )} + onSelect={(e) => { + if (isMultiSelect) e.preventDefault() + }} + onCheckedChange={() => { + const next = isMultiSelect + ? ([...effectiveValues, option.value] as T[]) + : ([option.value] as T[]) + + if ( + isMultiSelect && + field.maxSelections && + next.length > field.maxSelections + ) { + return + } + + if (field.onValueChange) { + field.onValueChange(next) + } else { + onChange(next) + } + if (!isMultiSelect) handleClose() + }} + > + {option.icon && option.icon} + {option.label} + + ) + })} + + )} +
+
+
+ + ) + + if (inline) { + return
{renderMenuContent()}
+ } + + return ( + { + setOpen(open) + if (!open) { + setTimeout(() => setSearchInput(""), 200) + } + }} + > + + + + + {renderMenuContent()} + + + ) +} + +function FilterValueSelector({ + field, + values, + onChange, + operator, + autoFocus, +}: FilterValueSelectorProps) { + const context = useFilterContext() + + if (operator === "empty" || operator === "not_empty") { + return null + } + + if (field.customRenderer) { + return ( + + {field.customRenderer({ field, values, onChange, operator })} + + ) + } + + if (field.type === "text") { + return ( + onChange([e.target.value] as T[])} + placeholder={field.placeholder} + pattern={field.pattern} + field={field} + className={cn("w-36", field.className)} + autoFocus={autoFocus} + /> + ) + } + + if (field.type === "select" || field.type === "multiselect") { + return ( + + ) + } + + return ( + + ) +} +export interface Filter { + id: string + field: string + operator: string + values: T[] +} + +export interface FilterGroup { + id: string + label?: string + filters: Filter[] + fields: FilterFieldConfig[] +} + +interface FiltersContentProps { + filters: Filter[] + fields: FilterFieldsConfig + onChange: (filters: Filter[]) => void +} + +export const FiltersContent = ({ + filters, + fields, + onChange, +}: FiltersContentProps) => { + const context = useFilterContext() + const fieldsMap = useMemo(() => getFieldsMap(fields), [fields]) + + const updateFilter = useCallback( + (filterId: string, updates: Partial>) => { + onChange( + filters.map((filter) => { + if (filter.id === filterId) { + const updatedFilter = { ...filter, ...updates } + if ( + updates.operator === "empty" || + updates.operator === "not_empty" + ) { + updatedFilter.values = [] as T[] + } + return updatedFilter + } + return filter + }) + ) + }, + [filters, onChange] + ) + + const removeFilter = useCallback( + (filterId: string) => { + onChange(filters.filter((filter) => filter.id !== filterId)) + }, + [filters, onChange] + ) + + return ( +
+ {filters.map((filter) => { + const field = fieldsMap[filter.field] + if (!field) return null + + return ( + + + {field.icon && field.icon} + {field.label} + + + + field={field} + operator={filter.operator} + values={filter.values} + onChange={(operator) => updateFilter(filter.id, { operator })} + /> + + + field={field} + values={filter.values} + onChange={(values) => updateFilter(filter.id, { values })} + operator={filter.operator} + autoFocus={false} + /> + + removeFilter(filter.id)} /> + + ) + })} +
+ ) +} + +interface FiltersProps { + filters: Filter[] + fields: FilterFieldsConfig + onChange: (filters: Filter[]) => void + className?: string + variant?: "solid" | "default" + size?: "sm" | "default" | "lg" + radius?: "default" | "full" + i18n?: Partial + showSearchInput?: boolean + trigger?: React.ReactNode + allowMultiple?: boolean + menuPopupClassName?: string + collapseAddButton?: boolean + enableShortcut?: boolean + shortcutKey?: string + shortcutLabel?: string +} + +interface FilterSubmenuContentProps { + field: FilterFieldConfig + currentValues: T[] + isMultiSelect: boolean + onToggle: (value: T, isSelected: boolean) => void + i18n: FilterI18nConfig + isActive?: boolean + onActive?: () => void + onBack?: () => void + onClose?: () => void +} + +function FilterSubmenuContent({ + field, + currentValues, + isMultiSelect, + onToggle, + i18n, + isActive, + onActive, + onBack, + onClose, +}: FilterSubmenuContentProps) { + const [searchInput, setSearchInput] = useState("") + const [highlightedIndex, setHighlightedIndex] = useState(-1) + const inputRef = useRef(null) + const baseId = useId() + + useEffect(() => { + setHighlightedIndex(-1) + }, [searchInput]) + + useEffect(() => { + if (highlightedIndex >= 0 && isActive) { + const element = document.getElementById( + `${baseId}-item-${highlightedIndex}` + ) + element?.scrollIntoView({ block: "nearest" }) + } + }, [highlightedIndex, isActive, baseId]) + + const filteredOptions = useMemo(() => { + return ( + field.options?.filter((option) => { + const isSelected = currentValues.includes(option.value) + if (isSelected) return true + if (!searchInput) return true + return option.label.toLowerCase().includes(searchInput.toLowerCase()) + }) || [] + ) + }, [field.options, searchInput, currentValues]) + + useEffect(() => { + if (isActive && filteredOptions.length > 0) { + setHighlightedIndex(0) + } + }, [isActive, filteredOptions.length]) + + return ( +
+ {field.searchable !== false && ( + <> + = 0 + ? `${baseId}-item-${highlightedIndex}` + : undefined + } + placeholder={i18n.placeholders.searchField(field.label || "")} + className={cn( + "h-8 rounded-none border-0 bg-transparent! px-2 text-sm shadow-none", + "focus-visible:border-border focus-visible:ring-0 focus-visible:ring-offset-0" + )} + value={searchInput} + onChange={(e) => setSearchInput(e.target.value)} + onClick={(e) => e.stopPropagation()} + onKeyDown={(e) => { + if (e.key === "ArrowDown") { + e.preventDefault() + if (filteredOptions.length > 0) { + setHighlightedIndex((prev) => + prev < filteredOptions.length - 1 ? prev + 1 : 0 + ) + } + } else if (e.key === "ArrowUp") { + e.preventDefault() + if (filteredOptions.length > 0) { + setHighlightedIndex((prev) => + prev > 0 ? prev - 1 : filteredOptions.length - 1 + ) + } + } else if (e.key === "ArrowLeft") { + e.preventDefault() + onBack?.() + } else if (e.key === "Enter" && highlightedIndex >= 0) { + e.preventDefault() + const option = filteredOptions[highlightedIndex] + if (option) { + onToggle( + option.value as T, + currentValues.includes(option.value) + ) + if (!isMultiSelect) { + onBack?.() + } + } + } else if (e.key === "Escape") { + e.preventDefault() + onClose?.() + } + e.stopPropagation() + }} + /> + + + )} +
+
{ + if (field.searchable === false) { + if (e.key === "ArrowDown") { + e.preventDefault() + if (filteredOptions.length > 0) { + setHighlightedIndex((prev) => + prev < filteredOptions.length - 1 ? prev + 1 : 0 + ) + } + } else if (e.key === "ArrowUp") { + e.preventDefault() + if (filteredOptions.length > 0) { + setHighlightedIndex((prev) => + prev > 0 ? prev - 1 : filteredOptions.length - 1 + ) + } + } else if (e.key === "ArrowLeft") { + e.preventDefault() + onBack?.() + } else if (e.key === "Enter" && highlightedIndex >= 0) { + e.preventDefault() + const option = filteredOptions[highlightedIndex] + if (option) { + onToggle( + option.value as T, + currentValues.includes(option.value) + ) + if (!isMultiSelect) { + onBack?.() + } + } + } else if (e.key === "Escape") { + e.preventDefault() + onClose?.() + } + e.stopPropagation() + } + }} + > + + {filteredOptions.length === 0 ? ( +
+ {i18n.noResultsFound} +
+ ) : ( + + {filteredOptions.map((option, index) => { + const isSelected = currentValues.includes(option.value) + const isHighlighted = highlightedIndex === index + const itemId = `${baseId}-item-${index}` + + return ( + setHighlightedIndex(index)} + checked={isSelected} + className={cn( + "data-highlighted:bg-accent data-highlighted:text-accent-foreground", + option.className + )} + onSelect={(e) => { + if (isMultiSelect) e.preventDefault() + }} + onCheckedChange={() => + onToggle(option.value as T, isSelected) + } + > + {option.icon && option.icon} + {option.label} + + ) + })} + + )} +
+
+
+
+ ) +} + +export function Filters({ + filters, + fields, + onChange, + className, + variant = "default", + size = "default", + radius = "default", + i18n, + showSearchInput = true, + trigger, + allowMultiple = true, + menuPopupClassName, + enableShortcut = false, + shortcutKey = "f", + shortcutLabel = "F", +}: FiltersProps) { + const [addFilterOpen, setAddFilterOpen] = useState(false) + const [menuSearchInput, setMenuSearchInput] = useState("") + const [activeMenu, setActiveMenu] = useState("root") + const [openSubMenu, setOpenSubMenu] = useState(null) + const [highlightedIndex, setHighlightedIndex] = useState(-1) + const [lastAddedFilterId, setLastAddedFilterId] = useState( + null + ) + const rootInputRef = useRef(null) + const rootId = useId() + + useEffect(() => { + if (!enableShortcut) return + + const handleKeyDown = (e: KeyboardEvent) => { + if ( + e.key.toLowerCase() === shortcutKey.toLowerCase() && + !addFilterOpen && + !( + document.activeElement instanceof HTMLInputElement || + document.activeElement instanceof HTMLTextAreaElement + ) + ) { + e.preventDefault() + setAddFilterOpen(true) + } + } + + window.addEventListener("keydown", handleKeyDown) + return () => window.removeEventListener("keydown", handleKeyDown) + }, [enableShortcut, shortcutKey, addFilterOpen]) + + useEffect(() => { + setHighlightedIndex(-1) + }, [menuSearchInput]) + + useEffect(() => { + if (highlightedIndex >= 0 && addFilterOpen) { + const element = document.getElementById( + `${rootId}-item-${highlightedIndex}` + ) + element?.scrollIntoView({ block: "nearest" }) + } + }, [highlightedIndex, addFilterOpen, rootId]) + + useEffect(() => { + if (!addFilterOpen) { + setOpenSubMenu(null) + } + }, [addFilterOpen]) + + // Track which filter instance is being built in the current Add Filter menu session + // Maps fieldKey -> unique filterId created during this open session + const [sessionFilterIds, setSessionFilterIds] = useState< + Record + >({}) + + useEffect(() => { + if (lastAddedFilterId) { + const timer = setTimeout(() => { + setLastAddedFilterId(null) + }, 1000) + return () => clearTimeout(timer) + } + }, [lastAddedFilterId]) + + const mergedI18n: FilterI18nConfig = { + ...DEFAULT_I18N, + ...i18n, + operators: { ...DEFAULT_I18N.operators, ...i18n?.operators }, + placeholders: { ...DEFAULT_I18N.placeholders, ...i18n?.placeholders }, + validation: { ...DEFAULT_I18N.validation, ...i18n?.validation }, + } + + const fieldsMap = useMemo(() => getFieldsMap(fields), [fields]) + + const updateFilter = useCallback( + (filterId: string, updates: Partial>) => { + onChange( + filters.map((filter) => { + if (filter.id === filterId) { + const updatedFilter = { ...filter, ...updates } + if ( + updates.operator === "empty" || + updates.operator === "not_empty" + ) { + updatedFilter.values = [] as T[] + } + return updatedFilter + } + return filter + }) + ) + }, + [filters, onChange] + ) + + const removeFilter = useCallback( + (filterId: string) => { + onChange(filters.filter((filter) => filter.id !== filterId)) + }, + [filters, onChange] + ) + + const addFilter = useCallback( + (fieldKey: string) => { + const field = fieldsMap[fieldKey] + if (field && field.key) { + const defaultOperator = + field.defaultOperator || + (field.type === "multiselect" ? "is_any_of" : "is") + const defaultValues: unknown[] = field.type === "text" ? [""] : [] + const newFilter = createFilter( + fieldKey, + defaultOperator, + defaultValues as T[] + ) + setLastAddedFilterId(newFilter.id) + onChange([...filters, newFilter]) + setAddFilterOpen(false) + setMenuSearchInput("") + } + }, + [fieldsMap, filters, onChange] + ) + + const selectableFields = useMemo(() => { + const flatFields = flattenFields(fields) + return flatFields.filter((field) => { + if (!field.key || field.type === "separator") return false + if (allowMultiple) return true + return !filters.some((filter) => filter.field === field.key) + }) + }, [fields, filters, allowMultiple]) + + const filteredFields = useMemo(() => { + return selectableFields.filter( + (f) => + !menuSearchInput || + f.label?.toLowerCase().includes(menuSearchInput.toLowerCase()) + ) + }, [selectableFields, menuSearchInput]) + + useEffect(() => { + if (addFilterOpen && filteredFields.length > 0) { + setHighlightedIndex(0) + } + }, [addFilterOpen, filteredFields.length]) + + return ( + +
+ {selectableFields.length > 0 && ( + { + setAddFilterOpen(open) + if (!open) { + setMenuSearchInput("") + setSessionFilterIds({}) + } else { + setActiveMenu("root") + } + }} + > + + {trigger || ( + + )} + + + {showSearchInput && ( + <> +
+ = 0 + ? `${rootId}-item-${highlightedIndex}` + : undefined + } + placeholder={mergedI18n.searchFields} + className={cn( + "h-8 rounded-none border-0 bg-transparent! px-2 text-sm shadow-none", + "focus-visible:border-border focus-visible:ring-0 focus-visible:ring-offset-0" + )} + value={menuSearchInput} + onChange={(e) => setMenuSearchInput(e.target.value)} + onClick={(e) => e.stopPropagation()} + onKeyDown={(e) => { + if (e.key === "ArrowDown") { + e.preventDefault() + if (filteredFields.length > 0) { + setHighlightedIndex((prev) => + prev < filteredFields.length - 1 ? prev + 1 : 0 + ) + } + } else if (e.key === "ArrowUp") { + e.preventDefault() + if (filteredFields.length > 0) { + setHighlightedIndex((prev) => + prev > 0 ? prev - 1 : filteredFields.length - 1 + ) + } + } else if ( + (e.key === "ArrowRight" || e.key === "ArrowLeft") && + highlightedIndex >= 0 + ) { + const field = filteredFields[highlightedIndex] + const hasSubMenu = + field && + (field.type === "select" || + field.type === "multiselect") && + field.options?.length + + if (e.key === "ArrowRight" && hasSubMenu) { + e.preventDefault() + setOpenSubMenu(field.key || null) + setActiveMenu(field.key || "root") + } else if (e.key === "ArrowLeft") { + e.preventDefault() + if (openSubMenu) { + setOpenSubMenu(null) + setActiveMenu("root") + } + } + } else if (e.key === "Enter" && highlightedIndex >= 0) { + e.preventDefault() + const field = filteredFields[highlightedIndex] + if (field.key) { + const hasSubMenu = + (field.type === "select" || + field.type === "multiselect") && + field.options?.length + if (!hasSubMenu) { + addFilter(field.key) + } else { + if (openSubMenu === field.key) { + setOpenSubMenu(null) + setActiveMenu("root") + } else { + setOpenSubMenu(field.key) + setActiveMenu(field.key) + } + } + } + } else if (e.key === "Escape") { + setAddFilterOpen(false) + } + e.stopPropagation() + }} + /> + {enableShortcut && shortcutLabel && ( + + {shortcutLabel} + + )} +
+ + + )} + +
+
+ + {(() => { + if (filteredFields.length === 0) { + return ( +
+ {mergedI18n.noFieldsFound} +
+ ) + } + + return filteredFields.map((field, index) => { + const isHighlighted = highlightedIndex === index + const itemId = `${rootId}-item-${index}` + const hasSubMenu = + (field.type === "select" || + field.type === "multiselect") && + field.options?.length + + if (hasSubMenu) { + const isMultiSelect = field.type === "multiselect" + const fieldKey = field.key as string + const sessionFilterId = sessionFilterIds[fieldKey] + const sessionFilter = sessionFilterId + ? filters.find((f) => f.id === sessionFilterId) + : null + const currentValues = sessionFilter?.values || [] + + return ( + { + if (open) { + setOpenSubMenu((prev) => + prev === fieldKey ? prev : fieldKey + ) + } else { + if (openSubMenu === fieldKey) { + setOpenSubMenu(null) + setActiveMenu("root") + } + } + }} + > + setHighlightedIndex(index)} + className="data-[state=open]:bg-accent data-[state=open]:text-accent-foreground data-highlighted:bg-accent data-highlighted:text-accent-foreground" + > + {field.icon} + {field.label} + + + { + if (field.searchable !== false) { + setActiveMenu(fieldKey) + } + }} + onBack={() => { + setOpenSubMenu(null) + setActiveMenu("root") + }} + onClose={() => setAddFilterOpen(false)} + onToggle={(value, isSelected) => { + if (isMultiSelect) { + const nextValues = isSelected + ? (currentValues.filter( + (v) => v !== value + ) as T[]) + : ([...currentValues, value] as T[]) + + if (sessionFilter) { + if (nextValues.length === 0) { + onChange( + filters.filter( + (f) => f.id !== sessionFilter.id + ) + ) + setSessionFilterIds((prev) => ({ + ...prev, + [fieldKey]: "", + })) + } else { + onChange( + filters.map((f) => + f.id === sessionFilter.id + ? { ...f, values: nextValues } + : f + ) + ) + } + } else { + const newFilter = createFilter( + fieldKey, + field.defaultOperator || "is_any_of", + nextValues + ) + onChange([...filters, newFilter]) + setSessionFilterIds((prev) => ({ + ...prev, + [fieldKey]: newFilter.id, + })) + } + } else { + const newFilter = createFilter( + fieldKey, + field.defaultOperator || "is", + [value] as T[] + ) + setLastAddedFilterId(newFilter.id) + onChange([...filters, newFilter]) + setAddFilterOpen(false) + } + }} + /> + + + ) + } + + return ( + setHighlightedIndex(index)} + onClick={() => field.key && addFilter(field.key)} + className="data-highlighted:bg-accent data-highlighted:text-accent-foreground" + > + {field.icon} + {field.label} + + ) + }) + })()} +
+
+
+
+
+ )} + + {filters.map((filter) => { + const field = fieldsMap[filter.field] + if (!field) return null + return ( + + + {field.icon && field.icon} + {field.label} + + + field={field} + operator={filter.operator} + values={filter.values} + onChange={(operator) => updateFilter(filter.id, { operator })} + /> + + field={field} + values={filter.values} + operator={filter.operator} + onChange={(values) => updateFilter(filter.id, { values })} + autoFocus={filter.id === lastAddedFilterId} + /> + removeFilter(filter.id)} /> + + ) + })} +
+
+ ) +} + +export const createFilter = ( + field: string, + operator?: string, + values: T[] = [] +): Filter => ({ + id: `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`, + field, + operator: operator || "is", + values, +}) + +export const createFilterGroup = ( + id: string, + label: string, + fields: FilterFieldConfig[], + initialFilters: Filter[] = [] +): FilterGroup => ({ + id, + label, + filters: initialFilters, + fields, +}) \ No newline at end of file diff --git a/web/src/components/shared/alert-banner.tsx b/web/src/components/shared/alert-banner.tsx new file mode 100644 index 00000000..7e4c13fc --- /dev/null +++ b/web/src/components/shared/alert-banner.tsx @@ -0,0 +1,22 @@ +import { cn } from "@/lib/utils"; + +interface AlertBannerProps { + variant?: "warning" | "error"; + title: string; + children?: React.ReactNode; + className?: string; +} + +const styles = { + warning: "border-warning/30 bg-warning/5 text-warning-foreground", + error: "border-destructive/30 bg-destructive/5 text-destructive-foreground", +}; + +export function AlertBanner({ variant = "warning", title, children, className }: AlertBannerProps) { + return ( +
+

{title}

+ {children} +
+ ); +} diff --git a/web/src/components/shared/delete-repo-button.tsx b/web/src/components/shared/delete-repo-button.tsx new file mode 100644 index 00000000..5310a747 --- /dev/null +++ b/web/src/components/shared/delete-repo-button.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { Trash2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { deleteRepo } from "@/actions/repo-actions"; + +interface DeleteRepoButtonProps { + owner: string; + name: string; + displayName: string; +} + +export function DeleteRepoButton({ owner, name, displayName }: DeleteRepoButtonProps) { + const router = useRouter(); + const [open, setOpen] = useState(false); + const [deleting, setDeleting] = useState(false); + const [error, setError] = useState(null); + + async function handleDelete() { + setDeleting(true); + setError(null); + try { + const result = await deleteRepo({ owner, name }); + if (result.success) { + setOpen(false); + router.push("/"); + router.refresh(); + return; + } + setError(result.error); + } catch { + setError("An unexpected error occurred."); + } finally { + setDeleting(false); + } + } + + return ( + { if (deleting) return; setOpen(value); if (!value) setError(null); }}> + + + + + + Remove project + + Do you want to remove {displayName} from the dashboard? The GitHub repository will not be deleted. + + + {error && ( +

{error}

+ )} + + + + +
+
+ ); +} diff --git a/web/src/components/shared/parse-errors-dialog.tsx b/web/src/components/shared/parse-errors-dialog.tsx new file mode 100644 index 00000000..f0c950a4 --- /dev/null +++ b/web/src/components/shared/parse-errors-dialog.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { Badge } from "@/components/ui/badge"; +import { AlertTriangle, FileWarning } from "lucide-react"; +import type { ParseErrorEntry } from "@/lib/bmad/types"; + +interface ParseErrorsDialogProps { + errors: ParseErrorEntry[]; +} + +export function ParseErrorsDialog({ errors }: ParseErrorsDialogProps) { + if (errors.length === 0) return null; + + return ( + + + + + + + + + {errors.length} file(s) with parsing errors + + + + + + + Parsing Errors + + +
+ {errors.map((entry) => ( +
+ +
+

+ {entry.file} +

+
+ + {entry.contentType} + +
+

+ {entry.error} +

+
+
+ ))} +
+
+
+ ); +} diff --git a/web/src/components/shared/progress-ring.tsx b/web/src/components/shared/progress-ring.tsx new file mode 100644 index 00000000..9ce9e11a --- /dev/null +++ b/web/src/components/shared/progress-ring.tsx @@ -0,0 +1,62 @@ +"use client"; + +import { cn } from "@/lib/utils"; + +interface ProgressRingProps { + percent: number; + size?: number; + strokeWidth?: number; + className?: string; +} + +function getProgressColor(percent: number) { + if (percent >= 75) return { stroke: "stroke-success", fill: "fill-success-foreground" }; + if (percent >= 40) return { stroke: "stroke-warning", fill: "fill-warning-foreground" }; + return { stroke: "stroke-destructive", fill: "fill-destructive-foreground" }; +} + +export function ProgressRing({ + percent, + size = 48, + strokeWidth = 4, + className, +}: ProgressRingProps) { + const radius = (size - strokeWidth) / 2; + const circumference = radius * 2 * Math.PI; + const offset = circumference - (percent / 100) * circumference; + const colors = getProgressColor(percent); + + return ( + + + + + {percent}% + + + ); +} diff --git a/web/src/components/shared/refresh-repo-button.tsx b/web/src/components/shared/refresh-repo-button.tsx new file mode 100644 index 00000000..a4bf35c5 --- /dev/null +++ b/web/src/components/shared/refresh-repo-button.tsx @@ -0,0 +1,115 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { RefreshCw, CheckCircle2, XCircle } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { refreshRepoData } from "@/actions/repo-actions"; + +interface RefreshRepoButtonProps { + owner: string; + name: string; +} + +function formatFileCount(count: number): string { + if (count === 0) return "No BMAD files detected."; + return `${count} BMAD file${count > 1 ? "s" : ""} detected.`; +} + +export function RefreshRepoButton({ owner, name }: RefreshRepoButtonProps) { + const router = useRouter(); + const [refreshing, setRefreshing] = useState(false); + const [dialogOpen, setDialogOpen] = useState(false); + const [result, setResult] = useState<{ + success: boolean; + totalFiles?: number; + error?: string; + code?: string; + } | null>(null); + + async function handleRefresh() { + setRefreshing(true); + try { + const res = await refreshRepoData({ owner, name }); + if (res.success) { + setResult({ success: true, totalFiles: res.data.totalFiles }); + } else { + setResult({ success: false, error: res.error, code: res.code }); + } + setDialogOpen(true); + if (res.success) { + router.refresh(); + } + } catch { + setResult({ success: false, error: "Unexpected error" }); + setDialogOpen(true); + } finally { + setRefreshing(false); + } + } + + return ( + <> + + + { + if (!refreshing) setDialogOpen(open); + }}> + + {result?.success === true ? ( + <> + +
+ + Refresh successful +
+ + {formatFileCount(result.totalFiles ?? 0)} + +
+ + + + + ) : result?.success === false ? ( + <> + +
+ + Refresh failed +
+ + + {result.code === "RATE_LIMITED" + ? "GitHub rate limit reached. Cached data is displayed." + : result.error || "An unknown error occurred."} + + +
+ + + + + ) : null} +
+
+ + ); +} diff --git a/web/src/components/shared/repo-settings-modal.tsx b/web/src/components/shared/repo-settings-modal.tsx new file mode 100644 index 00000000..0afc4ff2 --- /dev/null +++ b/web/src/components/shared/repo-settings-modal.tsx @@ -0,0 +1,158 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { Settings, Loader2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { listRepoBranches, updateRepoBranch } from "@/actions/repo-actions"; + +interface RepoSettingsModalProps { + owner: string; + name: string; + currentBranch: string; +} + +export function RepoSettingsModal({ + owner, + name, + currentBranch, +}: RepoSettingsModalProps) { + const router = useRouter(); + const [open, setOpen] = useState(false); + const [branches, setBranches] = useState([]); + const [selectedBranch, setSelectedBranch] = useState(currentBranch); + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + + async function handleOpenChange(nextOpen: boolean) { + if (saving) return; + setOpen(nextOpen); + setError(null); + + if (nextOpen) { + setSelectedBranch(currentBranch); + setLoading(true); + const result = await listRepoBranches({ owner, name }); + setLoading(false); + if (result.success) { + setBranches(result.data); + } else { + setError(result.error); + } + } + } + + async function handleSave() { + if (selectedBranch === currentBranch) { + setOpen(false); + return; + } + + setSaving(true); + setError(null); + const result = await updateRepoBranch({ + owner, + name, + branch: selectedBranch, + }); + setSaving(false); + + if (result.success) { + setOpen(false); + router.refresh(); + } else { + setError(result.error); + } + } + + const hasChanges = selectedBranch !== currentBranch; + + return ( + + + + + + + Project settings + + Configure the tracked branch for{" "} + + {owner}/{name} + + + + +
+ + {loading ? ( +
+ + Loading branches... +
+ ) : ( + + )} +
+ + {error && ( +

+ {error} +

+ )} + + + + + +
+
+ ); +} diff --git a/web/src/components/shared/segmented-progress-bar.tsx b/web/src/components/shared/segmented-progress-bar.tsx new file mode 100644 index 00000000..26170402 --- /dev/null +++ b/web/src/components/shared/segmented-progress-bar.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import { useRef, useState, useEffect } from "react"; + +interface SegmentedProgressBarProps { + percent: number; + color?: string; + className?: string; +} + +const SEGMENT_WIDTH = 7; +const GAP = 3; + +export function SegmentedProgressBar({ + percent, + color = "bg-primary", + className, +}: SegmentedProgressBarProps) { + const containerRef = useRef(null); + const [totalSegments, setTotalSegments] = useState(60); + + useEffect(() => { + const el = containerRef.current; + if (!el) return; + + const observer = new ResizeObserver((entries) => { + const width = entries[0].contentRect.width; + const count = Math.floor((width + GAP) / (SEGMENT_WIDTH + GAP)); + setTotalSegments(Math.max(count, 10)); + }); + + observer.observe(el); + return () => observer.disconnect(); + }, []); + + const filledCount = Math.round((percent / 100) * totalSegments); + + return ( +
+ {Array.from({ length: totalSegments }, (_, i) => ( +
+ ))} +
+ ); +} diff --git a/web/src/components/shared/staggered-list.tsx b/web/src/components/shared/staggered-list.tsx new file mode 100644 index 00000000..7d729b74 --- /dev/null +++ b/web/src/components/shared/staggered-list.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { motion } from "motion/react"; +import { cn } from "@/lib/utils"; + +interface StaggeredListProps { + children: React.ReactNode; + className?: string; + /** Delay before the first item animates (seconds) */ + initialDelay?: number; + /** Delay between each item (seconds) */ + staggerDelay?: number; + role?: string; + "aria-label"?: string; +} + +export function StaggeredList({ + children, + className, + initialDelay = 0, + staggerDelay = 0.06, + ...rest +}: StaggeredListProps) { + return ( + + {children} + + ); +} + +interface StaggeredItemProps { + children: React.ReactNode; + className?: string; +} + +export function StaggeredItem({ children, className }: StaggeredItemProps) { + return ( + + {children} + + ); +} diff --git a/web/src/components/shared/stats-card.tsx b/web/src/components/shared/stats-card.tsx new file mode 100644 index 00000000..aa942d2e --- /dev/null +++ b/web/src/components/shared/stats-card.tsx @@ -0,0 +1,59 @@ +import { Card, CardContent } from "@/components/ui/card"; +import { cn } from "@/lib/utils"; +import type { LucideIcon } from "lucide-react"; + +interface StatsCardProps { + title: string; + value: string | number; + icon: LucideIcon; + description?: string; + className?: string; + color?: "primary" | "info" | "success" | "warning" | "destructive" | "violet"; +} + +const colorStyles = { + primary: { bg: "bg-primary/10", text: "text-primary" }, + violet: { + bg: "bg-violet-500/15", + text: "text-violet-600 dark:text-violet-400", + }, + info: { bg: "bg-info/15", text: "text-info-foreground" }, + success: { bg: "bg-success/15", text: "text-success-foreground" }, + warning: { bg: "bg-warning/15", text: "text-warning-foreground" }, + destructive: { bg: "bg-destructive/15", text: "text-destructive-foreground" }, +}; + +export function StatsCard({ + title, + value, + icon: Icon, + description, + className, + color = "primary", +}: StatsCardProps) { + const c = colorStyles[color]; + + return ( + + +
+
+

{title}

+

{value}

+ {description && ( +

{description}

+ )} +
+
+ +
+
+
+
+ ); +} diff --git a/web/src/components/shared/status-badge.tsx b/web/src/components/shared/status-badge.tsx new file mode 100644 index 00000000..fc5cc1a3 --- /dev/null +++ b/web/src/components/shared/status-badge.tsx @@ -0,0 +1,55 @@ +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; +import type { StoryStatus, EpicStatus } from "@/lib/bmad/types"; + +const statusConfig: Record = { + done: { + label: "Done", + className: "bg-success/15 text-success-foreground border-success/25", + }, + "in-progress": { + label: "In Progress", + className: "bg-info/15 text-info-foreground border-info/25", + }, + review: { + label: "Review", + className: "bg-warning/15 text-warning-foreground border-warning/25", + }, + blocked: { + label: "Blocked", + className: "bg-destructive/15 text-destructive-foreground border-destructive/25", + }, + "ready-for-dev": { + label: "Ready for Dev", + className: "bg-purple-500/15 text-purple-700 dark:text-purple-400 border-purple-500/25", + }, + backlog: { + label: "Backlog", + className: "bg-muted text-muted-foreground border-border", + }, + "not-started": { + label: "Not Started", + className: "bg-muted text-muted-foreground border-border", + }, + unknown: { + label: "Unknown", + className: "bg-muted text-muted-foreground border-border", + }, +}; + +interface StatusBadgeProps { + status: StoryStatus | EpicStatus; + compact?: boolean; +} + +export function StatusBadge({ status, compact }: StatusBadgeProps) { + const config = statusConfig[status] || statusConfig.unknown; + return ( + + {config.label} + + ); +} diff --git a/web/src/components/shared/theme-toggle.tsx b/web/src/components/shared/theme-toggle.tsx new file mode 100644 index 00000000..61694a0f --- /dev/null +++ b/web/src/components/shared/theme-toggle.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { Moon, Sun } from "lucide-react"; +import { useTheme } from "next-themes"; +import { Button } from "@/components/ui/button"; + +export function ThemeToggle() { + const { theme, setTheme } = useTheme(); + + return ( + + ); +} diff --git a/web/src/components/stories/kanban-board.tsx b/web/src/components/stories/kanban-board.tsx new file mode 100644 index 00000000..63718185 --- /dev/null +++ b/web/src/components/stories/kanban-board.tsx @@ -0,0 +1,101 @@ +"use client"; + +import { Card, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { StaggeredList, StaggeredItem } from "@/components/shared/staggered-list"; +import type { StoryDetail, StoryStatus } from "@/lib/bmad/types"; + +const kanbanColumns: { status: StoryStatus; label: string; color: string }[] = [ + { status: "backlog", label: "Backlog", color: "bg-muted-foreground" }, + { status: "ready-for-dev", label: "Ready for Dev", color: "bg-purple-500" }, + { status: "in-progress", label: "In Progress", color: "bg-info" }, + { status: "review", label: "In Review", color: "bg-warning" }, + { status: "blocked", label: "Blocked", color: "bg-destructive" }, + { status: "done", label: "Done", color: "bg-success" }, +]; + +interface KanbanBoardProps { + stories: StoryDetail[]; +} + +export function KanbanBoard({ stories }: KanbanBoardProps) { + if (stories.length === 0) { + return ( +
+ No story matches the filters +
+ ); + } + + return ( + + {kanbanColumns.map((col) => { + const columnStories = stories.filter( + (s) => + s.status === col.status || + (col.status === "backlog" && s.status === "unknown") + ); + return ( + +
+ +
+ {columnStories.map((story) => ( + + +
+ + {story.title} + + + S{story.id} + +
+ {story.epicTitle && ( +

+ {story.epicTitle} +

+ )} + {story.totalTasks > 0 && ( +
+
+ Tasks + + {story.completedTasks}/{story.totalTasks} + +
+
+
+
+
+ )} + + + ))} + {columnStories.length === 0 && ( +
+ No stories +
+ )} +
+ + ); + })} + + ); +} diff --git a/web/src/components/stories/stories-table.tsx b/web/src/components/stories/stories-table.tsx new file mode 100644 index 00000000..fd9b251a --- /dev/null +++ b/web/src/components/stories/stories-table.tsx @@ -0,0 +1,158 @@ +"use client"; + +import { + useReactTable, + getCoreRowModel, + getSortedRowModel, + getPaginationRowModel, + type ColumnDef, + type SortingState, +} from "@tanstack/react-table"; +import { useState } from "react"; +import { + DataGrid, + DataGridContainer, +} from "@/components/reui/data-grid/data-grid"; +import { DataGridTable } from "@/components/reui/data-grid/data-grid-table"; +import { DataGridPagination } from "@/components/reui/data-grid/data-grid-pagination"; +import { DataGridColumnHeader } from "@/components/reui/data-grid/data-grid-column-header"; +import { StatusBadge } from "@/components/shared/status-badge"; +import type { StoryDetail } from "@/lib/bmad/types"; + +function TaskGauge({ completed, total }: { completed: number; total: number }) { + const size = 44; + const stroke = 5; + const radius = (size - stroke) / 2; + const circumference = Math.PI * radius; + const percent = total > 0 ? completed / total : 0; + const filledLength = circumference * percent; + const remainingLength = circumference - filledLength; + const svgH = size / 2 + stroke / 2; + + return ( +
+ + + {percent > 0 && ( + = 1 ? "text-success" : "text-success/70"} + /> + )} + + + {completed}/{total} + +
+ ); +} + +const columns: ColumnDef[] = [ + { + accessorKey: "id", + header: "ID", + cell: ({ row }) => ( + + S{row.getValue("id")} + + ), + size: 80, + enableSorting: false, + }, + { + accessorKey: "title", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + {row.getValue("title")} + ), + }, + { + accessorKey: "status", + header: ({ column }) => ( + + ), + cell: ({ row }) => , + }, + { + accessorKey: "epicTitle", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + {row.getValue("epicTitle") || "-"} + + ), + }, + { + accessorKey: "totalTasks", + header: "Tasks", + cell: ({ row }) => { + const story = row.original; + if (story.totalTasks === 0) return -; + return ; + }, + size: 80, + enableSorting: false, + }, +]; + +interface StoriesTableProps { + stories: StoryDetail[]; +} + +export function StoriesTable({ stories }: StoriesTableProps) { + const [sorting, setSorting] = useState([]); + + const table = useReactTable({ + data: stories, + columns, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setSorting, + state: { sorting }, + initialState: { pagination: { pageSize: 20 } }, + }); + + return ( + + + + + {table.getPageCount() > 1 && ( + + )} + + ); +} diff --git a/web/src/components/stories/stories-view.tsx b/web/src/components/stories/stories-view.tsx new file mode 100644 index 00000000..595c6455 --- /dev/null +++ b/web/src/components/stories/stories-view.tsx @@ -0,0 +1,103 @@ +"use client"; + +import { useState, useMemo, useCallback } from "react"; +import { Button } from "@/components/ui/button"; +import { LayoutList, Columns3 } from "lucide-react"; +import { StoryFilters, type Filter } from "./story-filters"; +import { StoriesTable } from "./stories-table"; +import { KanbanBoard } from "./kanban-board"; +import { StaggeredList, StaggeredItem } from "@/components/shared/staggered-list"; +import type { StoryDetail, Epic } from "@/lib/bmad/types"; + +interface StoriesViewProps { + stories: StoryDetail[]; + epics: Epic[]; +} + +export function StoriesView({ stories, epics }: StoriesViewProps) { + const [view, setView] = useState<"table" | "kanban">("table"); + const [search, setSearch] = useState(""); + const [filters, setFilters] = useState[]>([]); + + const applyFilters = useCallback( + (story: StoryDetail) => { + for (const filter of filters) { + if (filter.field === "status" && filter.values.length > 0) { + const match = + filter.operator === "is_not" || filter.operator === "is_not_any_of" + ? !filter.values.includes(story.status) + : filter.values.includes(story.status); + if (!match) return false; + } + if (filter.field === "epicId" && filter.values.length > 0) { + const match = + filter.operator === "is_not" || filter.operator === "is_not_any_of" + ? !filter.values.includes(story.epicId) + : filter.values.includes(story.epicId); + if (!match) return false; + } + } + return true; + }, + [filters] + ); + + const filtered = useMemo(() => { + return stories.filter((story) => { + if (search) { + const q = search.toLowerCase(); + if ( + !story.title.toLowerCase().includes(q) && + !story.id.toLowerCase().includes(q) + ) { + return false; + } + } + return applyFilters(story); + }); + }, [stories, search, applyFilters]); + + return ( + + + +
+ + +
+
+ + + {view === "table" ? ( + + ) : ( + + )} + +
+ ); +} diff --git a/web/src/components/stories/story-filters.tsx b/web/src/components/stories/story-filters.tsx new file mode 100644 index 00000000..8a02dbe2 --- /dev/null +++ b/web/src/components/stories/story-filters.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { Search } from "lucide-react"; +import { + Filters, + createFilter, + type Filter, + type FilterFieldConfig, +} from "@/components/reui/filters"; +import { Input } from "@/components/ui/input"; +import type { Epic } from "@/lib/bmad/types"; + +const STATUS_OPTIONS = [ + { value: "done", label: "Done" }, + { value: "in-progress", label: "In Progress" }, + { value: "review", label: "In Review" }, + { value: "blocked", label: "Blocked" }, + { value: "ready-for-dev", label: "Ready for Dev" }, + { value: "backlog", label: "Backlog" }, +]; + +interface StoryFiltersProps { + search: string; + onSearchChange: (value: string) => void; + filters: Filter[]; + onFiltersChange: (filters: Filter[]) => void; + epics: Epic[]; +} + +export function StoryFilters({ + search, + onSearchChange, + filters, + onFiltersChange, + epics, +}: StoryFiltersProps) { + const fields: FilterFieldConfig[] = [ + { + key: "status", + label: "Status", + type: "multiselect", + options: STATUS_OPTIONS, + }, + { + key: "epicId", + label: "Epic", + type: "select", + options: epics.map((e) => ({ + value: e.id, + label: `Epic ${e.id}: ${e.title}`, + })), + }, + ]; + + return ( +
+
+ + onSearchChange(e.target.value)} + className="pl-9" + /> +
+ +
+ ); +} + +export { createFilter, type Filter }; diff --git a/web/src/components/ui/animated-theme-toggler.tsx b/web/src/components/ui/animated-theme-toggler.tsx new file mode 100644 index 00000000..89636e51 --- /dev/null +++ b/web/src/components/ui/animated-theme-toggler.tsx @@ -0,0 +1,75 @@ +"use client" + +import { useCallback, useRef, useSyncExternalStore } from "react" +import { Moon, Sun } from "lucide-react" +import { useTheme } from "next-themes" +import { flushSync } from "react-dom" + +import { cn } from "@/lib/utils" + +interface AnimatedThemeTogglerProps extends React.ComponentPropsWithoutRef<"button"> { + duration?: number +} + +export const AnimatedThemeToggler = ({ + className, + duration = 400, + ...props +}: AnimatedThemeTogglerProps) => { + const { resolvedTheme, setTheme } = useTheme() + const mounted = useSyncExternalStore(() => () => {}, () => true, () => false) + const isDark = resolvedTheme === "dark" + const buttonRef = useRef(null) + + const toggleTheme = useCallback(async () => { + if (!buttonRef.current) return + + const newTheme = isDark ? "light" : "dark" + + if (!document.startViewTransition) { + setTheme(newTheme) + return + } + + await document.startViewTransition(() => { + flushSync(() => { + setTheme(newTheme) + }) + }).ready + + const { top, left, width, height } = + buttonRef.current.getBoundingClientRect() + const x = left + width / 2 + const y = top + height / 2 + const maxRadius = Math.hypot( + Math.max(left, window.innerWidth - left), + Math.max(top, window.innerHeight - top) + ) + + document.documentElement.animate( + { + clipPath: [ + `circle(0px at ${x}px ${y}px)`, + `circle(${maxRadius}px at ${x}px ${y}px)`, + ], + }, + { + duration, + easing: "ease-in-out", + pseudoElement: "::view-transition-new(root)", + } + ) + }, [isDark, duration, setTheme]) + + return ( + + ) +} diff --git a/web/src/components/ui/avatar.tsx b/web/src/components/ui/avatar.tsx new file mode 100644 index 00000000..1ac15704 --- /dev/null +++ b/web/src/components/ui/avatar.tsx @@ -0,0 +1,109 @@ +"use client" + +import * as React from "react" +import { Avatar as AvatarPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Avatar({ + className, + size = "default", + ...props +}: React.ComponentProps & { + size?: "default" | "sm" | "lg" +}) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) { + return ( + svg]:hidden", + "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", + "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", + className + )} + {...props} + /> + ) +} + +function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AvatarGroupCount({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3", + className + )} + {...props} + /> + ) +} + +export { + Avatar, + AvatarImage, + AvatarFallback, + AvatarBadge, + AvatarGroup, + AvatarGroupCount, +} diff --git a/web/src/components/ui/badge.tsx b/web/src/components/ui/badge.tsx new file mode 100644 index 00000000..b3f825db --- /dev/null +++ b/web/src/components/ui/badge.tsx @@ -0,0 +1,48 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-full border border-transparent px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + link: "text-primary underline-offset-4 [a&]:hover:underline", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant = "default", + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot.Root : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/web/src/components/ui/button-group.tsx b/web/src/components/ui/button-group.tsx new file mode 100644 index 00000000..cd550d7a --- /dev/null +++ b/web/src/components/ui/button-group.tsx @@ -0,0 +1,83 @@ +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" +import { Separator } from "@/components/ui/separator" + +const buttonGroupVariants = cva( + "flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1", + { + variants: { + orientation: { + horizontal: + "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none", + vertical: + "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none", + }, + }, + defaultVariants: { + orientation: "horizontal", + }, + } +) + +function ButtonGroup({ + className, + orientation, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function ButtonGroupText({ + className, + asChild = false, + ...props +}: React.ComponentProps<"div"> & { + asChild?: boolean +}) { + const Comp = asChild ? Slot.Root : "div" + + return ( + + ) +} + +function ButtonGroupSeparator({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + ButtonGroup, + ButtonGroupSeparator, + ButtonGroupText, + buttonGroupVariants, +} diff --git a/web/src/components/ui/button.tsx b/web/src/components/ui/button.tsx new file mode 100644 index 00000000..4d38506c --- /dev/null +++ b/web/src/components/ui/button.tsx @@ -0,0 +1,64 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot.Root : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/web/src/components/ui/card.tsx b/web/src/components/ui/card.tsx new file mode 100644 index 00000000..681ad980 --- /dev/null +++ b/web/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/web/src/components/ui/checkbox.tsx b/web/src/components/ui/checkbox.tsx new file mode 100644 index 00000000..f5a7e433 --- /dev/null +++ b/web/src/components/ui/checkbox.tsx @@ -0,0 +1,32 @@ +"use client" + +import * as React from "react" +import { CheckIcon } from "lucide-react" +import { Checkbox as CheckboxPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { Checkbox } diff --git a/web/src/components/ui/dialog.tsx b/web/src/components/ui/dialog.tsx new file mode 100644 index 00000000..a74e67d6 --- /dev/null +++ b/web/src/components/ui/dialog.tsx @@ -0,0 +1,158 @@ +"use client" + +import * as React from "react" +import { XIcon } from "lucide-react" +import { Dialog as DialogPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ + className, + showCloseButton = false, + children, + ...props +}: React.ComponentProps<"div"> & { + showCloseButton?: boolean +}) { + return ( +
+ {children} + {showCloseButton && ( + + + + )} +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/web/src/components/ui/dropdown-menu.tsx b/web/src/components/ui/dropdown-menu.tsx new file mode 100644 index 00000000..ae1fcf62 --- /dev/null +++ b/web/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,257 @@ +"use client" + +import * as React from "react" +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" +import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + + ) +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +} diff --git a/web/src/components/ui/input-group.tsx b/web/src/components/ui/input-group.tsx new file mode 100644 index 00000000..a7652d93 --- /dev/null +++ b/web/src/components/ui/input-group.tsx @@ -0,0 +1,170 @@ +"use client" + +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" + +function InputGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
textarea]:h-auto", + + // Variants based on alignment. + "has-[>[data-align=inline-start]]:[&>input]:pl-2", + "has-[>[data-align=inline-end]]:[&>input]:pr-2", + "has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3", + "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3", + + // Focus state. + "has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50", + + // Error state. + "has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40", + + className + )} + {...props} + /> + ) +} + +const inputGroupAddonVariants = cva( + "flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4", + { + variants: { + align: { + "inline-start": + "order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]", + "inline-end": + "order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]", + "block-start": + "order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-3", + "block-end": + "order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-3", + }, + }, + defaultVariants: { + align: "inline-start", + }, + } +) + +function InputGroupAddon({ + className, + align = "inline-start", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
{ + if ((e.target as HTMLElement).closest("button")) { + return + } + e.currentTarget.parentElement?.querySelector("input")?.focus() + }} + {...props} + /> + ) +} + +const inputGroupButtonVariants = cva( + "flex items-center gap-2 text-sm shadow-none", + { + variants: { + size: { + xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5", + sm: "h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5", + "icon-xs": + "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0", + "icon-sm": "size-8 p-0 has-[>svg]:p-0", + }, + }, + defaultVariants: { + size: "xs", + }, + } +) + +function InputGroupButton({ + className, + type = "button", + variant = "ghost", + size = "xs", + ...props +}: Omit, "size"> & + VariantProps) { + return ( +