From 1becc17724d9e1bc56ec6d90d667ea7bed559326 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Mar 2026 13:31:30 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Crumbs=20by=20Bretzel=20=E2=80=94=20sel?= =?UTF-8?q?f-hostable=20notes=20app=20MVP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- .env.example | 8 + .github/workflows/ci.yml | 102 + .github/workflows/release.yml | 54 + .gitignore | 33 + .npmrc | 1 + .vscode/extensions.json | 3 + CLAUDE.md | 133 + Dockerfile | 57 + Makefile | 61 + README.md | 151 + docker-compose.yml | 17 + docs/API.md | 166 + docs/ARCHITECTURE.md | 106 + docs/DEPLOYMENT.md | 125 + docs/FEATURES.md | 100 + drizzle.config.ts | 10 + package.json | 56 + playwright.config.ts | 37 + pnpm-lock.yaml | 6076 +++++++++++++++++ src/app.css | 1 + src/app.d.ts | 13 + src/app.html | 14 + src/hooks.server.ts | 37 + src/lib/components/Checklist.svelte | 120 + src/lib/components/ColorPicker.svelte | 25 + src/lib/components/ImageUpload.svelte | 107 + src/lib/components/Layout/Header.svelte | 37 + src/lib/components/Layout/Sidebar.svelte | 83 + src/lib/components/Layout/Toast.svelte | 49 + src/lib/components/NoteCard.svelte | 139 + src/lib/components/NoteEditor.svelte | 166 + src/lib/components/NoteGrid.svelte | 23 + src/lib/components/SearchBar.svelte | 60 + src/lib/components/SyncIndicator.svelte | 24 + src/lib/components/TagFilter.svelte | 21 + src/lib/components/ThemeToggle.svelte | 20 + src/lib/index.ts | 1 + src/lib/server/attachments.ts | 63 + src/lib/server/auth.test.ts | 120 + src/lib/server/auth.ts | 72 + src/lib/server/db/index.ts | 82 + src/lib/server/db/notes.test.ts | 294 + src/lib/server/db/schema.ts | 71 + src/lib/server/db/search.test.ts | 116 + src/lib/server/db/test-helpers.ts | 76 + src/lib/stores/notes.ts | 104 + src/lib/stores/theme.ts | 46 + src/lib/sync/client.ts | 128 + src/lib/sync/crdt.ts | 64 + src/lib/sync/idb.ts | 124 + src/lib/sync/server.ts | 71 + src/lib/sync/sync.test.ts | 119 + src/lib/types/index.ts | 77 + src/lib/utils/colors.ts | 21 + src/lib/utils/debounce.ts | 7 + src/lib/utils/markdown.test.ts | 87 + src/lib/utils/markdown.ts | 44 + src/lib/utils/tags.test.ts | 57 + src/lib/utils/tags.ts | 21 + src/routes/+layout.svelte | 29 + src/routes/+page.svelte | 84 + src/routes/api/auth/login/+server.ts | 26 + src/routes/api/auth/logout/+server.ts | 12 + src/routes/api/auth/setup/+server.ts | 31 + src/routes/api/notes/+server.ts | 89 + src/routes/api/notes/[id]/+server.ts | 92 + .../api/notes/[id]/attachments/+server.ts | 53 + src/routes/api/search/+server.ts | 65 + src/routes/api/sync/+server.ts | 20 + src/routes/api/tags/+server.ts | 9 + src/routes/login/+page.svelte | 72 + src/routes/setup/+page.svelte | 93 + static/manifest.webmanifest | 27 + static/robots.txt | 3 + svelte.config.js | 12 + tests/e2e/attachments.spec.ts | 11 + tests/e2e/auth.spec.ts | 92 + tests/e2e/checklist.spec.ts | 96 + tests/e2e/dark-mode.spec.ts | 32 + tests/e2e/global-setup.ts | 13 + tests/e2e/helpers/fixtures.ts | 45 + tests/e2e/notes-crud.spec.ts | 64 + tests/e2e/offline.spec.ts | 19 + tests/e2e/organization.spec.ts | 66 + tests/e2e/pwa.spec.ts | 24 + tests/e2e/search.spec.ts | 45 + tsconfig.json | 20 + vite.config.ts | 7 + vitest.config.ts | 8 + 89 files changed, 11359 insertions(+) create mode 100644 .env.example create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 .vscode/extensions.json create mode 100644 CLAUDE.md create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 docs/API.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/DEPLOYMENT.md create mode 100644 docs/FEATURES.md create mode 100644 drizzle.config.ts create mode 100644 package.json create mode 100644 playwright.config.ts create mode 100644 pnpm-lock.yaml create mode 100644 src/app.css create mode 100644 src/app.d.ts create mode 100644 src/app.html create mode 100644 src/hooks.server.ts create mode 100644 src/lib/components/Checklist.svelte create mode 100644 src/lib/components/ColorPicker.svelte create mode 100644 src/lib/components/ImageUpload.svelte create mode 100644 src/lib/components/Layout/Header.svelte create mode 100644 src/lib/components/Layout/Sidebar.svelte create mode 100644 src/lib/components/Layout/Toast.svelte create mode 100644 src/lib/components/NoteCard.svelte create mode 100644 src/lib/components/NoteEditor.svelte create mode 100644 src/lib/components/NoteGrid.svelte create mode 100644 src/lib/components/SearchBar.svelte create mode 100644 src/lib/components/SyncIndicator.svelte create mode 100644 src/lib/components/TagFilter.svelte create mode 100644 src/lib/components/ThemeToggle.svelte create mode 100644 src/lib/index.ts create mode 100644 src/lib/server/attachments.ts create mode 100644 src/lib/server/auth.test.ts create mode 100644 src/lib/server/auth.ts create mode 100644 src/lib/server/db/index.ts create mode 100644 src/lib/server/db/notes.test.ts create mode 100644 src/lib/server/db/schema.ts create mode 100644 src/lib/server/db/search.test.ts create mode 100644 src/lib/server/db/test-helpers.ts create mode 100644 src/lib/stores/notes.ts create mode 100644 src/lib/stores/theme.ts create mode 100644 src/lib/sync/client.ts create mode 100644 src/lib/sync/crdt.ts create mode 100644 src/lib/sync/idb.ts create mode 100644 src/lib/sync/server.ts create mode 100644 src/lib/sync/sync.test.ts create mode 100644 src/lib/types/index.ts create mode 100644 src/lib/utils/colors.ts create mode 100644 src/lib/utils/debounce.ts create mode 100644 src/lib/utils/markdown.test.ts create mode 100644 src/lib/utils/markdown.ts create mode 100644 src/lib/utils/tags.test.ts create mode 100644 src/lib/utils/tags.ts create mode 100644 src/routes/+layout.svelte create mode 100644 src/routes/+page.svelte create mode 100644 src/routes/api/auth/login/+server.ts create mode 100644 src/routes/api/auth/logout/+server.ts create mode 100644 src/routes/api/auth/setup/+server.ts create mode 100644 src/routes/api/notes/+server.ts create mode 100644 src/routes/api/notes/[id]/+server.ts create mode 100644 src/routes/api/notes/[id]/attachments/+server.ts create mode 100644 src/routes/api/search/+server.ts create mode 100644 src/routes/api/sync/+server.ts create mode 100644 src/routes/api/tags/+server.ts create mode 100644 src/routes/login/+page.svelte create mode 100644 src/routes/setup/+page.svelte create mode 100644 static/manifest.webmanifest create mode 100644 static/robots.txt create mode 100644 svelte.config.js create mode 100644 tests/e2e/attachments.spec.ts create mode 100644 tests/e2e/auth.spec.ts create mode 100644 tests/e2e/checklist.spec.ts create mode 100644 tests/e2e/dark-mode.spec.ts create mode 100644 tests/e2e/global-setup.ts create mode 100644 tests/e2e/helpers/fixtures.ts create mode 100644 tests/e2e/notes-crud.spec.ts create mode 100644 tests/e2e/offline.spec.ts create mode 100644 tests/e2e/organization.spec.ts create mode 100644 tests/e2e/pwa.spec.ts create mode 100644 tests/e2e/search.spec.ts create mode 100644 tsconfig.json create mode 100644 vite.config.ts create mode 100644 vitest.config.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a31023a --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +# Database path (relative to project root or absolute) +DATABASE_URL=./data/crumbs.db + +# Server origin (required for CSRF protection) +ORIGIN=http://localhost:3000 + +# Node environment +NODE_ENV=development diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1931996 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,102 @@ +name: CI + +on: + push: + branches: [main, 'claude/**'] + pull_request: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint-and-check: + name: Lint & Type Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: latest + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - run: pnpm install --frozen-lockfile + - run: pnpm check + + unit-tests: + name: Unit Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: latest + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - run: pnpm install --frozen-lockfile + - run: pnpm test:unit + + e2e-tests: + name: E2E Tests + runs-on: ubuntu-latest + needs: [lint-and-check, unit-tests] + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: latest + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - run: pnpm install --frozen-lockfile + + - name: Cache Playwright Browsers + id: playwright-cache + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }} + + - name: Install Playwright Browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: npx playwright install --with-deps chromium + + - name: Install Playwright System Dependencies + if: steps.playwright-cache.outputs.cache-hit == 'true' + run: npx playwright install-deps chromium + + - run: pnpm test:e2e + env: + CI: true + + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: playwright-report + path: playwright-report/ + retention-days: 7 + + docker-build: + name: Docker Build + runs-on: ubuntu-latest + needs: [lint-and-check, unit-tests] + steps: + - uses: actions/checkout@v4 + + - name: Build Docker image + run: docker build -t crumbs:ci . diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1308533 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,54 @@ +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + packages: write + +jobs: + ci: + uses: ./.github/workflows/ci.yml + + release: + name: Build & Push Docker Image + runs-on: ubuntu-latest + needs: ci + steps: + - uses: actions/checkout@v4 + + - name: Extract version from tag + id: version + run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Container Registry + if: env.REGISTRY_URL != '' + uses: docker/login-action@v3 + with: + registry: ${{ secrets.REGISTRY_URL }} + username: ${{ secrets.REGISTRY_USER }} + password: ${{ secrets.REGISTRY_TOKEN }} + env: + REGISTRY_URL: ${{ secrets.REGISTRY_URL }} + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: . + push: ${{ secrets.REGISTRY_URL != '' }} + tags: | + ${{ secrets.REGISTRY_URL && format('{0}/crumbs:{1}', secrets.REGISTRY_URL, steps.version.outputs.VERSION) || '' }} + ${{ secrets.REGISTRY_URL && format('{0}/crumbs:latest', secrets.REGISTRY_URL) || '' }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a461635 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* + +# Database +/data +*.db +*.db-journal +*.db-wal + +# Test results +/test-results +/playwright-report diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..28d1e67 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["svelte.svelte-vscode"] +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1b93c3d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,133 @@ +# CLAUDE.md + +## Project overview + +Crumbs by Bretzel — self-hostable note-taking app. Single-user, offline-first PWA with CRDT-based sync. + +## Tech stack + +- **Framework**: SvelteKit with Svelte 5 (runes: `$state`, `$derived`, `$props`, `$effect`) +- **Adapter**: `@sveltejs/adapter-node` (builds to `build/`, runs via `node build`) +- **Styling**: Tailwind CSS v4 (via `@tailwindcss/vite`) +- **Database**: SQLite via better-sqlite3 + Drizzle ORM (WAL mode, schema at `src/lib/server/db/schema.ts`) +- **Auth**: Single-user password auth with Argon2 hashing, session cookies (30-day expiry) +- **PWA**: `@vite-pwa/sveltekit` for service worker and offline caching +- **Package manager**: pnpm + +## Project structure + +``` +src/ + routes/ # SvelteKit pages and API routes + +page.svelte # Main notes view + login/ # Login page + setup/ # First-time password setup + api/auth/ # Login, logout, setup endpoints + api/notes/ # CRUD + attachments + api/search/ # Full-text search + api/sync/ # Offline sync + api/tags/ # Tag management + lib/ + components/ # Svelte components (NoteCard, NoteEditor, ColorPicker, etc.) + stores/ # Svelte stores (notes.ts, theme.ts) + server/db/ # Drizzle schema + connection (schema.ts, index.ts) + server/ # Auth logic, attachment handling + sync/ # Client sync (idb.ts), CRDT merge (crdt.ts), server sync + types/ # TypeScript interfaces (Note, Attachment, Tag, etc.) + utils/ # Colors, markdown, tags, debounce + hooks.server.ts # Auth middleware (redirects unauthenticated users) +tests/ + unit/ # Vitest unit tests + e2e/ # Playwright e2e tests (Gherkin-style) + helpers/fixtures.ts # Shared authenticatedPage fixture +``` + +## Commands + +A `Makefile` wraps all common tasks for tool-agnostic usage. Run `make help` to list targets. + +| Make target | pnpm equivalent | Description | +|---|---|---| +| `make dev` | `pnpm dev` | Start dev server | +| `make build` | `pnpm build` | Production build | +| `make preview` | `pnpm preview` | Preview production build | +| `make check` | `pnpm check` | Svelte type checking | +| `make test` | `pnpm test` | Run all tests (unit + e2e) | +| `make test-unit` | `pnpm test:unit` | Run unit tests only | +| `make test-e2e` | `pnpm test:e2e` | Run e2e tests only | +| `make lint` | `pnpm lint` | Run linter | +| `make db-push` | `pnpm db:push` | Push schema to DB | +| `make db-generate` | `pnpm db:generate` | Generate migrations | +| `make db-migrate` | `pnpm db:migrate` | Run migrations | +| `make db-studio` | `pnpm db:studio` | Open Drizzle Studio | +| `make install` | `pnpm install` | Install dependencies | +| `make docker-build` | — | Build Docker image | +| `make docker-up` | — | Start containers | +| `make docker-down` | — | Stop containers | +| `make docker-logs` | — | Tail container logs | +| `make clean` | — | Remove build artifacts + test DBs | + +## CI/CD + +GitHub Actions with two workflows: + +**CI** (`.github/workflows/ci.yml`) — runs on push to `main`/`claude/**` and PRs to `main`: +1. **Lint & Type Check** — `pnpm check` +2. **Unit Tests** — `pnpm test:unit` +3. **Build** — `pnpm build` (depends on steps 1+2) +4. **E2E Tests** — Playwright with Chromium (depends on step 3, uploads report on failure) +5. **Docker Build** — validates the Docker image builds (depends on step 3) + +**Release** (`.github/workflows/release.yml`) — runs on `v*` tags: +- Runs full CI pipeline, then builds + pushes Docker image to configured registry +- Creates a GitHub Release with auto-generated notes + +**Docker**: Multi-stage Dockerfile (node:22-slim), exposes port 3000, persists data to `/data` volume. + +## Testing philosophy: BDD/TDD + +Write tests first or alongside features. Tests serve as living documentation of expected behavior. + +### Unit tests (Vitest) + +- Location: `tests/unit/*.test.ts` +- Config: `vitest.config.ts` (node environment) +- Cover pure logic: markdown parsing, tag extraction, auth, search, sync + +### E2E tests (Playwright) + +- Location: `tests/e2e/*.spec.ts` +- Config: `playwright.config.ts` (Chromium only, builds + previews app on port 4173) +- Test database: `./data/test-crumbs.db` (cleaned via `global-setup.ts`) +- Auth fixture: `tests/e2e/helpers/fixtures.ts` provides `authenticatedPage` (handles setup/login race conditions across parallel workers) +- Auth tests use `test.describe.serial` because they depend on sequential database state + +### Gherkin-style Given/When/Then + +E2E tests follow Cucumber BDD conventions via comments. Follow the [Better Gherkin](https://cucumber.io/docs/bdd/better-gherkin) guidelines: + +**Declarative, not imperative** — describe *what* the system does, not *how* the user interacts with the UI: +``` +// Good: "When the user trashes the note" +// Bad: "When the user hovers over the note card and clicks the trash button" +``` + +**Given describes state, not navigation:** +``` +// Good: "Given a note titled 'Delete Me' exists" +// Bad: "Given the user clicks the new note button and fills in the title" +``` + +**Collapse multi-step UI actions into a single intent:** +``` +// Good: "When the user creates a note titled 'My Note' with content 'Hello'" +// Bad: "When they click new note / And fill in the title / And fill in the content / And close the editor" +``` + +**Scenario names describe outcomes/behavior, not actions:** +``` +// Good: "Scenario: Trashed note disappears from the main view" +// Bad: "Scenario: User moves a note to trash" +``` + +**Resilience test** — ask: *"Will this comment need to change if the UI implementation changes?"* If yes, rewrite it to remove implementation details. The Playwright code underneath handles the *how*; the comments describe the *what*. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1f1f2b4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,57 @@ +# Stage 1: Build +FROM node:22-slim AS build + +WORKDIR /app +RUN corepack enable && corepack prepare pnpm@latest --activate + +# Copy package files +COPY package.json pnpm-lock.yaml ./ + +# Install dependencies (with native module builds) +RUN pnpm install --frozen-lockfile + +# Copy source code +COPY . . + +# Build the SvelteKit app +RUN pnpm run build + +# Prune dev dependencies +RUN pnpm prune --prod + +# Stage 2: Runtime +FROM node:22-slim AS runtime + +WORKDIR /app + +# Install only what's needed for native modules +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Copy built app and production dependencies +COPY --from=build /app/build ./build +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/package.json ./ + +# Create data directory +RUN mkdir -p /data && chown -R node:node /data + +# Use non-root user +USER node + +# Configuration +ENV NODE_ENV=production +ENV DATA_DIR=/data +ENV DATABASE_URL=/data/crumbs.db +ENV PORT=3000 +ENV ORIGIN=http://localhost:3000 + +EXPOSE 3000 + +VOLUME ["/data"] + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \ + CMD node -e "fetch('http://localhost:3000/login').then(r => r.ok ? process.exit(0) : process.exit(1)).catch(() => process.exit(1))" + +CMD ["node", "build"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2d2f64d --- /dev/null +++ b/Makefile @@ -0,0 +1,61 @@ +.PHONY: help dev build preview check test test-unit test-e2e lint \ + db-push db-generate db-migrate db-studio \ + install docker-build docker-up docker-down docker-logs clean + +help: ## List all targets + @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}' + +dev: ## Start dev server + pnpm dev + +build: ## Production build + pnpm build + +preview: ## Preview production build + pnpm preview + +check: ## Svelte type checking + pnpm check + +test: ## Run all tests (unit + e2e) + pnpm test + +test-unit: ## Run unit tests only + pnpm test:unit + +test-e2e: ## Run e2e tests only + pnpm test:e2e + +lint: ## Run linter + pnpm lint + +db-push: ## Push schema to DB + pnpm db:push + +db-generate: ## Generate migrations + pnpm db:generate + +db-migrate: ## Run migrations + pnpm db:migrate + +db-studio: ## Open Drizzle Studio + pnpm db:studio + +install: ## Install dependencies + pnpm install + +docker-build: ## Build Docker image + docker build -t crumbs . + +docker-up: ## Start containers + docker compose up -d + +docker-down: ## Stop containers + docker compose down + +docker-logs: ## Tail container logs + docker compose logs -f + +clean: ## Remove build artifacts and test DBs + rm -rf build .svelte-kit node_modules/.vite + rm -f data/test-crumbs.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..3f1d17e --- /dev/null +++ b/README.md @@ -0,0 +1,151 @@ +# Crumbs by Bretzel + +A self-hostable, offline-first note-taking app inspired by Google Keep. Part of the [Bretzel](https://bretzel.app) app universe. Built with SvelteKit, SQLite, and Tailwind CSS. + +## Features + +- **Notes CRUD** - Create, edit, and delete notes with rich Markdown support +- **Markdown** - Bold, italic, headings, lists, code blocks, tables, links +- **Checklists** - Interactive task lists with checkboxes +- **Image Attachments** - Upload and display images in notes +- **Tags** - Organize notes with inline #hashtags +- **Colors** - 12 color-coded note backgrounds (matching Google Keep) +- **Pin** - Pin important notes to the top +- **Archive** - Archive notes to declutter your main view +- **Trash** - Soft delete with restore capability +- **Search** - Full-text search across titles, content, and tags +- **Dark Mode** - System-aware with manual toggle +- **Single-User Auth** - Password-protected access +- **PWA** - Installable as a standalone app with offline support +- **Local-First** - Notes stored in IndexedDB, synced to server via LWW CRDTs +- **Docker** - Single command deployment with `docker compose up` + +## Quick Start + +### Docker (Recommended) + +```bash +docker compose up -d +``` + +Open http://localhost:3000 and set your password on first visit. + +### Development + +```bash +# Install dependencies +pnpm install + +# Start dev server +pnpm dev + +# Run unit tests +pnpm test:unit + +# Run E2E tests (requires Playwright browsers) +npx playwright install chromium +pnpm test:e2e + +# Type check +pnpm check + +# Build for production +pnpm build +``` + +## Tech Stack + +| Layer | Technology | +|-------|-----------| +| Framework | SvelteKit 2 | +| Language | TypeScript (strict) | +| UI | Tailwind CSS 4 | +| Database | SQLite (better-sqlite3) | +| ORM | Drizzle ORM | +| Client DB | IndexedDB (idb) | +| Sync | LWW CRDTs | +| Auth | Argon2 + session cookies | +| Markdown | markdown-it | +| PWA | @vite-pwa/sveltekit | +| Testing | Vitest + Playwright | +| Container | Docker (multi-stage) | +| CI/CD | GitHub Actions | + +## Project Structure + +``` +src/ +├── lib/ +│ ├── components/ # Svelte UI components +│ ├── server/db/ # Drizzle schema & connection +│ ├── server/auth.ts # Authentication logic +│ ├── sync/ # CRDT sync engine (client + server) +│ ├── stores/ # Svelte reactive stores +│ ├── types/ # TypeScript type definitions +│ └── utils/ # Markdown, tags, colors, debounce +├── routes/ +│ ├── api/ # REST API endpoints +│ ├── login/ # Login page +│ ├── setup/ # First-run setup page +│ └── +page.svelte # Main notes view +└── hooks.server.ts # Auth middleware +tests/ +├── unit/ # Vitest unit tests +└── e2e/ # Playwright E2E tests +``` + +## API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/notes?filter=all\|archived\|trashed` | List notes | +| POST | `/api/notes` | Create a note | +| GET | `/api/notes/:id` | Get a note | +| PATCH | `/api/notes/:id` | Update a note | +| DELETE | `/api/notes/:id` | Delete a note permanently | +| GET | `/api/search?q=query` | Full-text search | +| GET | `/api/tags` | List all tags | +| POST | `/api/sync` | Push sync changes | +| GET | `/api/sync?since=timestamp` | Pull changes since timestamp | +| POST | `/api/auth/setup` | First-time password setup | +| POST | `/api/auth/login` | Login | +| POST | `/api/auth/logout` | Logout | +| GET/POST/DELETE | `/api/notes/:id/attachments` | Manage image attachments | + +## Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `DATABASE_URL` | `./data/crumbs.db` | SQLite database file path | +| `DATA_DIR` | `./data` | Data directory for attachments | +| `ORIGIN` | `http://localhost:3000` | Server origin (CSRF protection) | +| `PORT` | `3000` | Server port | +| `NODE_ENV` | `development` | Node environment | + +## CI/CD + +GitHub Actions workflows are included: + +- **CI** (`ci.yml`): Runs on every push/PR - lint, type check, unit tests, build, E2E tests, Docker build +- **Release** (`release.yml`): Runs on version tags (`v*`) - builds and pushes Docker image to a configurable registry + +Configure your registry via GitHub Secrets: +- `REGISTRY_URL` - Container registry URL +- `REGISTRY_USER` - Registry username +- `REGISTRY_TOKEN` - Registry access token + +## Sync Architecture + +Crumbs uses a local-first architecture with Last-Write-Wins (LWW) conflict resolution: + +1. All reads come from IndexedDB (instant, works offline) +2. Writes go to IndexedDB + a sync queue +3. Background sync pushes queued changes to server every 30s +4. Server resolves conflicts using timestamps (newer wins) +5. Client pulls server changes and merges with local state + +This ensures the app works fully offline and syncs automatically when connectivity is restored. + +## License + +MIT diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..660d9e7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +services: + crumbs: + build: . + container_name: crumbs + ports: + - "3000:3000" + volumes: + - crumbs-data:/data + environment: + - NODE_ENV=production + - DATA_DIR=/data + - DATABASE_URL=/data/crumbs.db + - ORIGIN=http://localhost:3000 + restart: unless-stopped + +volumes: + crumbs-data: diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..a51bc8b --- /dev/null +++ b/docs/API.md @@ -0,0 +1,166 @@ +# API Reference + +All API endpoints require authentication (session cookie) except `/api/auth/setup` and `/api/auth/login`. + +## Authentication + +### POST /api/auth/setup +First-time setup. Creates the single user account. + +**Request:** +```json +{ "password": "min8characters" } +``` + +**Response:** `201 Created` +```json +{ "success": true } +``` +Sets `session` cookie. + +### POST /api/auth/login +**Request:** +```json +{ "password": "yourpassword" } +``` + +**Response:** `200 OK` +```json +{ "success": true } +``` +Sets `session` cookie. + +### POST /api/auth/logout +Clears session. + +**Response:** `200 OK` +```json +{ "success": true } +``` + +## Notes + +### GET /api/notes +List notes by filter. + +**Query Parameters:** +- `filter` - `all` (default), `archived`, `trashed` + +**Response:** `200 OK` +```json +[ + { + "id": "uuid", + "title": "My Note", + "content": "**Markdown** content", + "color": "coral", + "pinned": true, + "archived": false, + "trashed": false, + "checklistMode": false, + "sortOrder": 0, + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-01T00:00:00.000Z", + "version": 1, + "tags": ["work", "important"] + } +] +``` + +### POST /api/notes +Create a new note. + +**Request:** +```json +{ + "title": "My Note", + "content": "Note content with #tags", + "color": "default", + "pinned": false, + "checklistMode": false +} +``` + +**Response:** `201 Created` - Returns the created note. + +### GET /api/notes/:id +Get a single note. + +### PATCH /api/notes/:id +Update a note. + +**Request:** (partial update - only include changed fields) +```json +{ + "title": "Updated Title", + "color": "coral", + "pinned": true +} +``` + +**Response:** `200 OK` - Returns the updated note. + +### DELETE /api/notes/:id +Permanently delete a note. + +## Search + +### GET /api/search +Full-text search across notes. + +**Query Parameters:** +- `q` - Search query + +**Response:** `200 OK` - Array of matching notes (excluding trashed). + +## Tags + +### GET /api/tags +List all tags. + +**Response:** `200 OK` +```json +[ + { "id": 1, "name": "work" }, + { "id": 2, "name": "personal" } +] +``` + +## Attachments + +### GET /api/notes/:id/attachments +List attachments for a note. + +### POST /api/notes/:id/attachments +Upload an image attachment. + +**Request:** `multipart/form-data` with `file` field. + +**Response:** `201 Created` - Returns attachment metadata. + +### DELETE /api/notes/:id/attachments?attachmentId=xxx +Delete an attachment. + +## Sync + +### POST /api/sync +Push local changes to server. + +**Request:** +```json +{ + "changes": [ + { + "noteId": "uuid", + "operation": "create|update|delete", + "timestamp": 1704067200000, + "data": { "title": "...", "content": "..." } + } + ] +} +``` + +### GET /api/sync?since=timestamp +Pull changes since a timestamp (milliseconds). + +**Response:** `200 OK` - Array of notes updated since the given timestamp. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..c5b4ba0 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,106 @@ +# Architecture + +## Overview + +Crumbs follows a **local-first** architecture where the client (browser) is the primary data store and the server acts as a sync target and backup. + +``` +┌─────────────────────────────┐ +│ Browser (PWA) │ +│ ┌───────────┐ ┌──────────┐ │ +│ │ Svelte │ │ IndexedDB│ │ +│ │ Stores │◄│ (idb) │ │ +│ └─────┬─────┘ └────┬─────┘ │ +│ │ │ │ +│ ┌─────▼─────────────▼─────┐ │ +│ │ Sync Engine │ │ +│ │ (LWW CRDT + Queue) │ │ +│ └──────────┬──────────────┘ │ +└─────────────┼────────────────┘ + │ HTTP (JSON) +┌─────────────▼────────────────┐ +│ Server (Node.js) │ +│ ┌──────────┐ ┌────────────┐ │ +│ │ SvelteKit│ │ SQLite │ │ +│ │ API │◄│ (Drizzle) │ │ +│ └──────────┘ └────────────┘ │ +└──────────────────────────────┘ +``` + +## Data Flow + +### Read Path +1. User opens app +2. Svelte stores read from IndexedDB (instant) +3. Background sync pulls latest from server +4. If newer data found, IDB is updated, stores react + +### Write Path +1. User creates/edits note +2. Change written to IndexedDB immediately +3. Change added to sync queue +4. Background sync pushes queue to server +5. Server applies changes with LWW resolution + +### Conflict Resolution (LWW) +For a single-user app with multiple devices: +- Each note has an `updatedAt` timestamp and `version` number +- When merging: newer timestamp wins +- If timestamps match: higher version wins +- If both match: local version is preferred + +## Tech Stack Rationale + +### SvelteKit +- Smallest bundle size of major frameworks +- Built-in SSR, routing, API routes +- Svelte 5 runes for reactive state +- Native service worker support via @vite-pwa/sveltekit + +### SQLite + Drizzle +- Zero-config database (single file) +- WAL mode for concurrent reads +- Drizzle provides type-safe queries without ORM overhead +- Perfect for single-user self-hosted apps + +### IndexedDB (idb) +- Browser-native storage (no size limits like localStorage) +- Async API doesn't block main thread +- `idb` library provides Promise-based wrapper +- Survives browser restarts + +### Tailwind CSS 4 +- Utility-first, tree-shakeable +- Built-in dark mode support (`dark:` variants) +- New v4 uses Vite plugin (faster builds) + +### Argon2 +- Current best practice for password hashing +- Memory-hard (resists GPU attacks) +- Winner of the Password Hashing Competition + +## Database Schema + +See `src/lib/server/db/schema.ts` for the complete Drizzle schema. + +Key tables: +- `users` - Single user (password hash) +- `sessions` - Auth sessions (30-day TTL) +- `notes` - Core note data with version tracking +- `tags` - Unique tag names +- `note_tags` - Many-to-many note ↔ tag +- `attachments` - Image file metadata +- `sync_log` - Sync operation history + +## File Organization + +``` +src/lib/server/ # Server-only code (DB, auth, attachments) +src/lib/sync/ # Sync engine (shared types, server + client implementations) +src/lib/stores/ # Svelte reactive stores (notes, theme, auth, search) +src/lib/components/ # Svelte UI components +src/lib/utils/ # Pure utility functions (markdown, tags, colors) +src/lib/types/ # TypeScript type definitions +src/routes/api/ # REST API endpoints +src/routes/ # Page routes (SvelteKit file-based routing) +``` diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000..273464c --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,125 @@ +# Deployment Guide + +## Docker (Recommended) + +### Quick Start + +```bash +docker compose up -d +``` + +Open http://localhost:3000 and set your password. + +### Custom Configuration + +```yaml +# docker-compose.yml +services: + crumbs: + build: . + ports: + - "8080:3000" # Change external port + volumes: + - /path/to/data:/data # Custom data location + environment: + - ORIGIN=https://notes.example.com # Your domain + restart: unless-stopped +``` + +### Reverse Proxy (nginx) + +```nginx +server { + listen 443 ssl; + server_name notes.example.com; + + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + + location / { + proxy_pass http://localhost:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket support (for future live sync) + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } +} +``` + +Set `ORIGIN=https://notes.example.com` in your docker-compose.yml. + +### Backup + +The SQLite database and attachments are stored in `/data`: + +```bash +# Backup +docker compose exec crumbs tar czf - /data > crumbs-backup.tar.gz + +# Or copy the volume directly +docker cp crumbs:/data ./backup +``` + +### Update + +```bash +docker compose pull +docker compose up -d +``` + +## Manual Deployment (Node.js) + +### Prerequisites +- Node.js 22+ +- pnpm + +### Steps + +```bash +# Clone and install +git clone crumbs +cd crumbs +pnpm install + +# Build +pnpm build + +# Set environment +export DATABASE_URL=./data/crumbs.db +export ORIGIN=http://localhost:3000 +export NODE_ENV=production + +# Run +node build +``` + +## Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `DATABASE_URL` | `./data/crumbs.db` | SQLite database path | +| `DATA_DIR` | `./data` | Directory for attachments | +| `ORIGIN` | `http://localhost:3000` | Server origin (required for CSRF) | +| `PORT` | `3000` | HTTP port | +| `NODE_ENV` | `development` | Set to `production` for deployment | + +## Health Check + +The Docker image includes a health check that pings `/login`: + +```bash +docker inspect --format='{{.State.Health.Status}}' crumbs +``` + +## Security Notes + +- Always use HTTPS in production (via reverse proxy) +- Set a strong password (min 8 characters recommended) +- The `ORIGIN` variable must match your actual domain for CSRF protection +- Sessions expire after 30 days +- Passwords are hashed with Argon2 diff --git a/docs/FEATURES.md b/docs/FEATURES.md new file mode 100644 index 0000000..1864758 --- /dev/null +++ b/docs/FEATURES.md @@ -0,0 +1,100 @@ +# Features + +## Core Features (MVP) + +### Notes CRUD +- Create, read, update, and delete notes +- Notes have a title and content body +- Content supports full Markdown syntax +- Auto-save on editor close + +### Markdown Support +- **Bold** (`**text**`) +- *Italic* (`*text*`) +- Headings (`# H1` through `###### H6`) +- Unordered lists (`- item`) +- Ordered lists (`1. item`) +- Code blocks (triple backtick) +- Inline code (single backtick) +- Tables +- Links (auto-linkified URLs) +- Line breaks + +### Checklists / Task Lists +- Toggle between regular note and checklist mode +- Add, remove, reorder checklist items +- Check/uncheck items with checkboxes +- Checked items get strikethrough styling +- Enter key adds new item, Backspace removes empty items + +### Image Attachments +- Upload images via file picker or drag-and-drop +- Supports JPEG, PNG, GIF, WebP, SVG +- Max file size: 10MB +- Thumbnail previews in note cards +- Remove attachments from editor + +### Tag Organization +- Inline `#hashtag` extraction from note content +- Tags automatically parsed from title and content +- Filter notes by tag (sidebar + tag chips) +- Tags are case-insensitive and deduplicated +- Tags inside code blocks and URLs are ignored + +### Color-Coded Notes +12 colors matching Google Keep's palette: +- Default, Coral, Peach, Sand, Mint, Sage +- Fog, Storm, Dusk, Blossom, Clay, Chalk +- Colors adapt to light/dark mode + +### Pin Notes +- Pin important notes to always appear at top +- Pinned section separated from unpinned notes +- Toggle pin from note card hover actions + +### Archive +- Archive notes to declutter main view +- Archived notes accessible from sidebar +- Unarchive to restore to main view + +### Trash +- Soft delete moves notes to trash +- Trash view accessible from sidebar +- Restore notes from trash +- Permanently delete from trash + +### Full-Text Search +- Search across note titles, content, and tags +- Real-time search results as you type +- Clear search to restore original view +- Case-insensitive matching + +### Dark Mode +- System preference detection (auto) +- Manual toggle override +- Preference persisted in localStorage +- All components adapt to theme +- Note colors adjust for dark backgrounds + +### Single-User Auth +- First-run setup: create password (min 8 chars) +- Argon2 password hashing (industry standard) +- Session-based auth with 30-day expiry +- All routes protected except /login and /setup +- Logout clears session + +### PWA / Offline-First +- Installable as standalone app +- Web app manifest with icons +- Service worker for asset caching +- IndexedDB for local data storage +- Background sync every 30 seconds +- LWW conflict resolution for multi-device use +- Sync status indicator (synced/syncing/offline/error) + +### Docker Deployment +- Multi-stage Dockerfile (build + slim runtime) +- docker-compose.yml with persistent volume +- Health check endpoint +- Runs as non-root user +- Configurable via environment variables diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..ce9c316 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + schema: './src/lib/server/db/schema.ts', + out: './drizzle', + dialect: 'sqlite', + dbCredentials: { + url: process.env.DATABASE_URL || './data/crumbs.db' + } +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..9439f82 --- /dev/null +++ b/package.json @@ -0,0 +1,56 @@ +{ + "name": "crumbs", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "test:unit": "vitest run", + "test:unit:watch": "vitest", + "test:e2e": "playwright test", + "test": "pnpm test:unit && pnpm test:e2e", + "db:push": "drizzle-kit push", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:studio": "drizzle-kit studio", + "lint": "eslint ." + }, + "dependencies": { + "@sveltejs/adapter-node": "^5.0.0", + "better-sqlite3": "^11.0.0", + "drizzle-orm": "^0.44.0", + "argon2": "^0.43.0", + "idb": "^8.0.0", + "uuid": "^11.0.0", + "markdown-it": "^14.0.0" + }, + "devDependencies": { + "@sveltejs/kit": "^2.50.2", + "@sveltejs/vite-plugin-svelte": "^6.2.4", + "svelte": "^5.51.0", + "svelte-check": "^4.4.2", + "typescript": "^5.9.3", + "vite": "^7.3.1", + "@tailwindcss/vite": "^4.0.0", + "tailwindcss": "^4.0.0", + "@vite-pwa/sveltekit": "^1.0.0", + "drizzle-kit": "^0.31.0", + "vitest": "^3.0.0", + "@playwright/test": "^1.50.0", + "@types/better-sqlite3": "^7.6.0", + "@types/markdown-it": "^14.0.0", + "@types/uuid": "^10.0.0" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "argon2", + "better-sqlite3", + "esbuild" + ] + } +} \ No newline at end of file diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..b1fd44e --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,37 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + globalSetup: './tests/e2e/global-setup.ts', + testDir: './tests/e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + reporter: 'html', + use: { + baseURL: 'http://localhost:4173', + trace: 'on-first-retry', + screenshot: 'only-on-failure' + }, + projects: [ + { + name: 'setup', + testMatch: /auth\.spec\.ts/, + use: { ...devices['Desktop Chrome'] } + }, + { + name: 'chromium', + testIgnore: /auth\.spec\.ts/, + dependencies: ['setup'], + use: { ...devices['Desktop Chrome'] } + } + ], + webServer: { + command: 'pnpm build && pnpm preview --port 4173', + port: 4173, + reuseExistingServer: !process.env.CI, + timeout: 120_000, + env: { + DATABASE_URL: './data/test-crumbs.db' + } + } +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..1e6c2c7 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,6076 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@sveltejs/adapter-node': + specifier: ^5.0.0 + version: 5.5.4(@sveltejs/kit@2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0))) + argon2: + specifier: ^0.43.0 + version: 0.43.1 + better-sqlite3: + specifier: ^11.0.0 + version: 11.10.0 + drizzle-orm: + specifier: ^0.44.0 + version: 0.44.7(@types/better-sqlite3@7.6.13)(better-sqlite3@11.10.0) + idb: + specifier: ^8.0.0 + version: 8.0.3 + markdown-it: + specifier: ^14.0.0 + version: 14.1.1 + uuid: + specifier: ^11.0.0 + version: 11.1.0 + devDependencies: + '@playwright/test': + specifier: ^1.50.0 + version: 1.58.2 + '@sveltejs/kit': + specifier: ^2.50.2 + version: 2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)) + '@sveltejs/vite-plugin-svelte': + specifier: ^6.2.4 + version: 6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)) + '@tailwindcss/vite': + specifier: ^4.0.0 + version: 4.2.1(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)) + '@types/better-sqlite3': + specifier: ^7.6.0 + version: 7.6.13 + '@types/markdown-it': + specifier: ^14.0.0 + version: 14.1.2 + '@types/uuid': + specifier: ^10.0.0 + version: 10.0.0 + '@vite-pwa/sveltekit': + specifier: ^1.0.0 + version: 1.1.0(@sveltejs/kit@2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)))(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0))(workbox-build@7.4.0)(workbox-window@7.4.0) + drizzle-kit: + specifier: ^0.31.0 + version: 0.31.9 + svelte: + specifier: ^5.51.0 + version: 5.53.7 + svelte-check: + specifier: ^4.4.2 + version: 4.4.4(picomatch@4.0.3)(svelte@5.53.7)(typescript@5.9.3) + tailwindcss: + specifier: ^4.0.0 + version: 4.2.1 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite: + specifier: ^7.3.1 + version: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0) + vitest: + specifier: ^3.0.0 + version: 3.2.4(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0) + +packages: + + '@apideck/better-ajv-errors@0.3.6': + resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==} + engines: {node: '>=10'} + peerDependencies: + ajv: '>=8' + + '@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-create-regexp-features-plugin@7.28.5': + resolution: {integrity: sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-define-polyfill-provider@0.6.6': + resolution: {integrity: sha512-mOAsxeeKkUKayvZR3HeTYD/fICpCPLJrU5ZjelT/PA6WHtNDBOE436YiaEUvHN454bRM3CebhDsIpieCc4texA==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.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-remap-async-to-generator@7.27.1': + resolution: {integrity: sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.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/helper-wrap-function@7.28.6': + resolution: {integrity: sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==} + 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-bugfix-firefox-class-in-computed-class-key@7.28.5': + resolution: {integrity: sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1': + resolution: {integrity: sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1': + resolution: {integrity: sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1': + resolution: {integrity: sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6': + resolution: {integrity: sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-assertions@7.28.6': + resolution: {integrity: sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.28.6': + resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6': + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-arrow-functions@7.27.1': + resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-generator-functions@7.29.0': + resolution: {integrity: sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-to-generator@7.28.6': + resolution: {integrity: sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoped-functions@7.27.1': + resolution: {integrity: sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoping@7.28.6': + resolution: {integrity: sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-properties@7.28.6': + resolution: {integrity: sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-static-block@7.28.6': + resolution: {integrity: sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + + '@babel/plugin-transform-classes@7.28.6': + resolution: {integrity: sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-computed-properties@7.28.6': + resolution: {integrity: sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-destructuring@7.28.5': + resolution: {integrity: sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-dotall-regex@7.28.6': + resolution: {integrity: sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-keys@7.27.1': + resolution: {integrity: sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0': + resolution: {integrity: sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-dynamic-import@7.27.1': + resolution: {integrity: sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-explicit-resource-management@7.28.6': + resolution: {integrity: sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-exponentiation-operator@7.28.6': + resolution: {integrity: sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-export-namespace-from@7.27.1': + resolution: {integrity: sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-for-of@7.27.1': + resolution: {integrity: sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-function-name@7.27.1': + resolution: {integrity: sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-json-strings@7.28.6': + resolution: {integrity: sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-literals@7.27.1': + resolution: {integrity: sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-logical-assignment-operators@7.28.6': + resolution: {integrity: sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-member-expression-literals@7.27.1': + resolution: {integrity: sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-amd@7.27.1': + resolution: {integrity: sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==} + 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-modules-systemjs@7.29.0': + resolution: {integrity: sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-umd@7.27.1': + resolution: {integrity: sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-named-capturing-groups-regex@7.29.0': + resolution: {integrity: sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-new-target@7.27.1': + resolution: {integrity: sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-nullish-coalescing-operator@7.28.6': + resolution: {integrity: sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-numeric-separator@7.28.6': + resolution: {integrity: sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-rest-spread@7.28.6': + resolution: {integrity: sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-super@7.27.1': + resolution: {integrity: sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-catch-binding@7.28.6': + resolution: {integrity: sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-chaining@7.28.6': + resolution: {integrity: sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-parameters@7.27.7': + resolution: {integrity: sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-methods@7.28.6': + resolution: {integrity: sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-property-in-object@7.28.6': + resolution: {integrity: sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-property-literals@7.27.1': + resolution: {integrity: sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regenerator@7.29.0': + resolution: {integrity: sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regexp-modifiers@7.28.6': + resolution: {integrity: sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-reserved-words@7.27.1': + resolution: {integrity: sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-shorthand-properties@7.27.1': + resolution: {integrity: sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-spread@7.28.6': + resolution: {integrity: sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-sticky-regex@7.27.1': + resolution: {integrity: sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-template-literals@7.27.1': + resolution: {integrity: sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typeof-symbol@7.27.1': + resolution: {integrity: sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-escapes@7.27.1': + resolution: {integrity: sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-property-regex@7.28.6': + resolution: {integrity: sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-regex@7.27.1': + resolution: {integrity: sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-sets-regex@7.28.6': + resolution: {integrity: sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/preset-env@7.29.0': + resolution: {integrity: sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-modules@0.1.6-no-external-plugins': + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.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'} + + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@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.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + 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.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + 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.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + 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.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + 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.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + 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.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + 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.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + 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.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + 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.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + 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.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@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.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + 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.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@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.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + 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.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@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.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + 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.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@isaacs/cliui@9.0.0': + resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} + engines: {node: '>=18'} + + '@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/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@phc/format@1.0.0': + resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==} + engines: {node: '>=10'} + + '@playwright/test@1.58.2': + resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} + engines: {node: '>=18'} + hasBin: true + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@rollup/plugin-babel@5.3.1': + resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} + engines: {node: '>= 10.0.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@types/babel__core': ^7.1.9 + rollup: ^1.20.0||^2.0.0 + peerDependenciesMeta: + '@types/babel__core': + optional: true + + '@rollup/plugin-commonjs@29.0.1': + resolution: {integrity: sha512-VUEHINN2rQEWPfNUR3mzidRObM1XZKXMQsaG6qBlDqd6M1qyw91nDZvcSozgyjt3x/QKrgKBc5MdxfdxAy6tdg==} + engines: {node: '>=16.0.0 || 14 >= 14.17'} + peerDependencies: + rollup: ^2.68.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-json@6.1.0': + resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-node-resolve@15.3.1': + resolution: {integrity: sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-node-resolve@16.0.3': + resolution: {integrity: sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-replace@2.4.2': + resolution: {integrity: sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==} + peerDependencies: + rollup: ^1.20.0 || ^2.0.0 + + '@rollup/plugin-terser@0.4.4': + resolution: {integrity: sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/pluginutils@3.1.0': + resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} + engines: {node: '>= 8.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0 + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@surma/rollup-plugin-off-main-thread@2.2.3': + resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} + + '@sveltejs/acorn-typescript@1.0.9': + resolution: {integrity: sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==} + peerDependencies: + acorn: ^8.9.0 + + '@sveltejs/adapter-node@5.5.4': + resolution: {integrity: sha512-45X92CXW+2J8ZUzPv3eLlKWEzINKiiGeFWTjyER4ZN4sGgNoaoeSkCY/QYNxHpPXy71QPsctwccBo9jJs0ySPQ==} + peerDependencies: + '@sveltejs/kit': ^2.4.0 + + '@sveltejs/kit@2.53.4': + resolution: {integrity: sha512-iAIPEahFgDJJyvz8g0jP08KvqnM6JvdW8YfsygZ+pMeMvyM2zssWMltcsotETvjSZ82G3VlitgDtBIvpQSZrTA==} + engines: {node: '>=18.13'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.0.0 + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0 + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: ^5.3.3 + vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + typescript: + optional: true + + '@sveltejs/vite-plugin-svelte-inspector@5.0.2': + resolution: {integrity: sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^6.0.0-next.0 + svelte: ^5.0.0 + vite: ^6.3.0 || ^7.0.0 + + '@sveltejs/vite-plugin-svelte@6.2.4': + resolution: {integrity: sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + svelte: ^5.0.0 + vite: ^6.3.0 || ^7.0.0 + + '@tailwindcss/node@4.2.1': + resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==} + + '@tailwindcss/oxide-android-arm64@4.2.1': + resolution: {integrity: sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.1': + resolution: {integrity: sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.1': + resolution: {integrity: sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.1': + resolution: {integrity: sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': + resolution: {integrity: sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': + resolution: {integrity: sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': + resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': + resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.2.1': + resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.2.1': + resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} + 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.2.1': + resolution: {integrity: sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': + resolution: {integrity: sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.1': + resolution: {integrity: sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.2.1': + resolution: {integrity: sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + + '@types/better-sqlite3@7.6.13': + resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@0.0.39': + resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + + '@types/node@25.3.3': + resolution: {integrity: sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==} + + '@types/resolve@1.20.2': + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + + '@vite-pwa/sveltekit@1.1.0': + resolution: {integrity: sha512-mMIf2tY+7Hg8jecpu/WY+Ki2ikoXy3hVmt3tOxi0K+lYYnKQrDYthuHireI0S+26Mg9BXzL7qQF1xeB5VYlYlg==} + engines: {node: '>=18.13'} + peerDependencies: + '@sveltejs/kit': ^1.3.1 || ^2.0.1 + '@vite-pwa/assets-generator': ^1.0.0 + peerDependenciesMeta: + '@vite-pwa/assets-generator': + optional: true + + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + argon2@0.43.1: + resolution: {integrity: sha512-TfOzvDWUaQPurCT1hOwIeFNkgrAJDpbBGBGWDgzDsm11nNhImc13WhdGdCU6K7brkp8VpeY07oGtSex0Wmhg8w==} + engines: {node: '>=16.17.0'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.1: + resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + 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'} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + babel-plugin-polyfill-corejs2@0.4.15: + resolution: {integrity: sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.14.0: + resolution: {integrity: sha512-AvDcMxJ34W4Wgy4KBIIePQTAOP1Ie2WFwkQp3dB7FQ/f0lI5+nM96zUnYEOE1P9sEg0es5VCP0HxiWu5fUHZAQ==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-regenerator@0.6.6: + resolution: {integrity: sha512-hYm+XLYRMvupxiQzrvXUj7YyvFFVfv5gI0R71AJzudg1g2AI2vyCPPIFEBjk162/wFzti3inBHo7isWFuEVS/A==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.10.0: + resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==} + engines: {node: '>=6.0.0'} + hasBin: true + + better-sqlite3@11.10.0: + resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + brace-expansion@5.0.4: + resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + engines: {node: 18 || 20 || >=22} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + 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'} + + caniuse-lite@1.0.30001776: + resolution: {integrity: sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw==} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + common-tags@1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + + core-js-compat@3.48.0: + resolution: {integrity: sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + crypto-random-string@2.0.0: + resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} + engines: {node: '>=8'} + + 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@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + devalue@5.6.3: + resolution: {integrity: sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==} + + drizzle-kit@0.31.9: + resolution: {integrity: sha512-GViD3IgsXn7trFyBUUHyTFBpH/FsHTxYJ66qdbVggxef4UBPHRYxQaRzYLTuekYnk9i5FIEL9pbBIwMqX/Uwrg==} + hasBin: true + + drizzle-orm@0.44.7: + resolution: {integrity: sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1.13' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@upstash/redis': '>=1.34.7' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@upstash/redis': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + + electron-to-chromium@1.5.307: + resolution: {integrity: sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + enhanced-resolve@5.20.0: + resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==} + engines: {node: '>=10.13.0'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + 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-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-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + 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'} + + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + + esrap@2.2.3: + resolution: {integrity: sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==} + + estree-walker@1.0.1: + resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + 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'} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + filelist@1.0.6: + resolution: {integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + 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==} + + 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-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-own-enumerable-property-symbols@3.0.2: + resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + 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==} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + glob@11.1.0: + resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} + engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + 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==} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + 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'} + + idb@7.1.1: + resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} + + idb@8.0.3: + resolution: {integrity: sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + 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-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-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-module@1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-obj@1.0.1: + resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} + engines: {node: '>=0.10.0'} + + is-reference@1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-regexp@1.0.0: + resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} + engines: {node: '>=0.10.0'} + + 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-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-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'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@4.2.3: + resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} + engines: {node: 20 || >=22} + + jake@10.9.4: + resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==} + engines: {node: '>=10'} + hasBin: true + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + 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==} + + jsonpointer@5.0.1: + resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} + engines: {node: '>=0.10.0'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + lightningcss-android-arm64@1.31.1: + resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.31.1: + resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.31.1: + resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.31.1: + resolution: {integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.31.1: + resolution: {integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.31.1: + resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.31.1: + resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.31.1: + resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.31.1: + resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.31.1: + resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.31.1: + resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.31.1: + resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} + engines: {node: '>= 12.0.0'} + + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@11.2.6: + resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.25.9: + resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + markdown-it@14.1.1: + resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} + hasBin: true + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} + + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + + node-abi@3.87.0: + resolution: {integrity: sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==} + engines: {node: '>=10'} + + node-addon-api@8.6.0: + resolution: {integrity: sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==} + engines: {node: ^18 || ^20 || >= 21} + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + node-releases@2.0.36: + resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + + 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.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + 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'} + + playwright-core@1.58.2: + resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.58.2: + resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + engines: {node: '>=18'} + hasBin: true + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. + hasBin: true + + pretty-bytes@5.6.0: + resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} + engines: {node: '>=6'} + + pretty-bytes@6.1.1: + resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} + engines: {node: ^14.13.1 || >=16.0.0} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regenerate-unicode-properties@10.2.2: + resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==} + engines: {node: '>=4'} + + regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + regexpu-core@6.4.0: + resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==} + engines: {node: '>=4'} + + regjsgen@0.8.0: + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} + + regjsparser@0.13.0: + resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==} + hasBin: true + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + 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 + + rollup@2.80.0: + resolution: {integrity: sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==} + engines: {node: '>=10.0.0'} + hasBin: true + + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + 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'} + + 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 + + serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + + set-cookie-parser@3.0.1: + resolution: {integrity: sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==} + + 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'} + + 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@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + smob@1.6.1: + resolution: {integrity: sha512-KAkBqZl3c2GvNgNhcoyJae1aKldDW0LO279wF9bk1PnluRTETKBq0WyzRXxEhoQLk56yHaOY4JCBEKDuJIET5g==} + engines: {node: '>=20.0.0'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions + + sourcemap-codec@1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + deprecated: Please use @jridgewell/sourcemap-codec instead + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + 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'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + stringify-object@3.3.0: + resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} + engines: {node: '>=4'} + + strip-comments@2.0.1: + resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} + engines: {node: '>=10'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + svelte-check@4.4.4: + resolution: {integrity: sha512-F1pGqXc710Oi/wTI4d/x7d6lgPwwfx1U6w3Q35n4xsC2e8C/yN2sM1+mWxjlMcpAfWucjlq4vPi+P4FZ8a14sQ==} + engines: {node: '>= 18.0.0'} + hasBin: true + peerDependencies: + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: '>=5.0.0' + + svelte@5.53.7: + resolution: {integrity: sha512-uxck1KI7JWtlfP3H6HOWi/94soAl23jsGJkBzN2BAWcQng0+lTrRNhxActFqORgnO9BHVd1hKJhG+ljRuIUWfQ==} + engines: {node: '>=18'} + + tailwindcss@4.2.1: + resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + temp-dir@2.0.0: + resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} + engines: {node: '>=8'} + + tempy@0.6.0: + resolution: {integrity: sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==} + engines: {node: '>=10'} + + terser@5.46.0: + resolution: {integrity: sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==} + engines: {node: '>=10'} + hasBin: true + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + type-fest@0.16.0: + resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==} + engines: {node: '>=10'} + + 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@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + unicode-canonical-property-names-ecmascript@2.0.1: + resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} + engines: {node: '>=4'} + + unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + + unicode-match-property-value-ecmascript@2.2.1: + resolution: {integrity: sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==} + engines: {node: '>=4'} + + unicode-property-aliases-ecmascript@2.2.0: + resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==} + engines: {node: '>=4'} + + unique-string@2.0.0: + resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} + engines: {node: '>=8'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + upath@1.2.0: + resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} + engines: {node: '>=4'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite-plugin-pwa@1.2.0: + resolution: {integrity: sha512-a2xld+SJshT9Lgcv8Ji4+srFJL4k/1bVbd1x06JIkvecpQkwkvCncD1+gSzcdm3s+owWLpMJerG3aN5jupJEVw==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@vite-pwa/assets-generator': ^1.0.0 + vite: ^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + workbox-build: ^7.4.0 + workbox-window: ^7.4.0 + peerDependenciesMeta: + '@vite-pwa/assets-generator': + optional: true + + 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 + + vitefu@1.1.2: + resolution: {integrity: sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0 + peerDependenciesMeta: + vite: + optional: true + + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + + whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + + 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 + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + workbox-background-sync@7.4.0: + resolution: {integrity: sha512-8CB9OxKAgKZKyNMwfGZ1XESx89GryWTfI+V5yEj8sHjFH8MFelUwYXEyldEK6M6oKMmn807GoJFUEA1sC4XS9w==} + + workbox-broadcast-update@7.4.0: + resolution: {integrity: sha512-+eZQwoktlvo62cI0b+QBr40v5XjighxPq3Fzo9AWMiAosmpG5gxRHgTbGGhaJv/q/MFVxwFNGh/UwHZ/8K88lA==} + + workbox-build@7.4.0: + resolution: {integrity: sha512-Ntk1pWb0caOFIvwz/hfgrov/OJ45wPEhI5PbTywQcYjyZiVhT3UrwwUPl6TRYbTm4moaFYithYnl1lvZ8UjxcA==} + engines: {node: '>=20.0.0'} + + workbox-cacheable-response@7.4.0: + resolution: {integrity: sha512-0Fb8795zg/x23ISFkAc7lbWes6vbw34DGFIMw31cwuHPgDEC/5EYm6m/ZkylLX0EnEbbOyOCLjKgFS/Z5g0HeQ==} + + workbox-core@7.4.0: + resolution: {integrity: sha512-6BMfd8tYEnN4baG4emG9U0hdXM4gGuDU3ectXuVHnj71vwxTFI7WOpQJC4siTOlVtGqCUtj0ZQNsrvi6kZZTAQ==} + + workbox-expiration@7.4.0: + resolution: {integrity: sha512-V50p4BxYhtA80eOvulu8xVfPBgZbkxJ1Jr8UUn0rvqjGhLDqKNtfrDfjJKnLz2U8fO2xGQJTx/SKXNTzHOjnHw==} + + workbox-google-analytics@7.4.0: + resolution: {integrity: sha512-MVPXQslRF6YHkzGoFw1A4GIB8GrKym/A5+jYDUSL+AeJw4ytQGrozYdiZqUW1TPQHW8isBCBtyFJergUXyNoWQ==} + + workbox-navigation-preload@7.4.0: + resolution: {integrity: sha512-etzftSgdQfjMcfPgbfaZCfM2QuR1P+4o8uCA2s4rf3chtKTq/Om7g/qvEOcZkG6v7JZOSOxVYQiOu6PbAZgU6w==} + + workbox-precaching@7.4.0: + resolution: {integrity: sha512-VQs37T6jDqf1rTxUJZXRl3yjZMf5JX/vDPhmx2CPgDDKXATzEoqyRqhYnRoxl6Kr0rqaQlp32i9rtG5zTzIlNg==} + + workbox-range-requests@7.4.0: + resolution: {integrity: sha512-3Vq854ZNuP6Y0KZOQWLaLC9FfM7ZaE+iuQl4VhADXybwzr4z/sMmnLgTeUZLq5PaDlcJBxYXQ3U91V7dwAIfvw==} + + workbox-recipes@7.4.0: + resolution: {integrity: sha512-kOkWvsAn4H8GvAkwfJTbwINdv4voFoiE9hbezgB1sb/0NLyTG4rE7l6LvS8lLk5QIRIto+DjXLuAuG3Vmt3cxQ==} + + workbox-routing@7.4.0: + resolution: {integrity: sha512-C/ooj5uBWYAhAqwmU8HYQJdOjjDKBp9MzTQ+otpMmd+q0eF59K+NuXUek34wbL0RFrIXe/KKT+tUWcZcBqxbHQ==} + + workbox-strategies@7.4.0: + resolution: {integrity: sha512-T4hVqIi5A4mHi92+5EppMX3cLaVywDp8nsyUgJhOZxcfSV/eQofcOA6/EMo5rnTNmNTpw0rUgjAI6LaVullPpg==} + + workbox-streams@7.4.0: + resolution: {integrity: sha512-QHPBQrey7hQbnTs5GrEVoWz7RhHJXnPT+12qqWM378orDMo5VMJLCkCM1cnCk+8Eq92lccx/VgRZ7WAzZWbSLg==} + + workbox-sw@7.4.0: + resolution: {integrity: sha512-ltU+Kr3qWR6BtbdlMnCjobZKzeV1hN+S6UvDywBrwM19TTyqA03X66dzw1tEIdJvQ4lYKkBFox6IAEhoSEZ8Xw==} + + workbox-window@7.4.0: + resolution: {integrity: sha512-/bIYdBLAVsNR3v7gYGaV4pQW3M3kEPx5E8vDxGvxo6khTrGtSSCS7QiFKv9ogzBgZiy0OXLP9zO28U/1nF1mfw==} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + zimmerframe@1.1.4: + resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} + +snapshots: + + '@apideck/better-ajv-errors@0.3.6(ajv@8.18.0)': + dependencies: + ajv: 8.18.0 + json-schema: 0.4.0 + jsonpointer: 5.0.1 + leven: 3.1.0 + + '@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-create-regexp-features-plugin@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + regexpu-core: 6.4.0 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.6.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + debug: 4.4.3 + lodash.debounce: 4.0.8 + resolve: 1.22.11 + 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-remap-async-to-generator@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-wrap-function': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@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/helper-wrap-function@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@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-bugfix-firefox-class-in-computed-class-key@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + + '@babel/plugin-syntax-import-assertions@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-async-generator-functions@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-block-scoping@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-class-properties@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-class-static-block@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-classes@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-globals': 7.28.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-computed-properties@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/template': 7.28.6 + + '@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-dotall-regex@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-explicit-resource-management@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-exponentiation-operator@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-json-strings@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-logical-assignment-operators@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-modules-amd@7.27.1(@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-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-modules-systemjs@7.29.0(@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 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-umd@7.27.1(@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-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-nullish-coalescing-operator@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-numeric-separator@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-object-rest-spread@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-optional-catch-binding@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-optional-chaining@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-private-methods@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-property-in-object@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 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-regenerator@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-regexp-modifiers@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-spread@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-unicode-property-regex@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-unicode-sets-regex@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/preset-env@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.28.5(@babel/core@7.29.0) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0) + '@babel/plugin-syntax-import-assertions': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.0) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-class-static-block': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-computed-properties': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) + '@babel/plugin-transform-dotall-regex': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-explicit-resource-management': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-exponentiation-operator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-json-strings': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-logical-assignment-operators': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-modules-systemjs': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-numeric-separator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-optional-catch-binding': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) + '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-regenerator': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-regexp-modifiers': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-spread': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-property-regex': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-sets-regex': 7.28.6(@babel/core@7.29.0) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.0) + babel-plugin-polyfill-corejs2: 0.4.15(@babel/core@7.29.0) + babel-plugin-polyfill-corejs3: 0.14.0(@babel/core@7.29.0) + babel-plugin-polyfill-regenerator: 0.6.6(@babel/core@7.29.0) + core-js-compat: 3.48.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/types': 7.29.0 + esutils: 2.0.3 + + '@babel/runtime@7.28.6': {} + + '@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 + + '@drizzle-team/brocli@0.10.2': {} + + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.13.6 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@isaacs/cliui@9.0.0': {} + + '@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/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@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 + + '@phc/format@1.0.0': {} + + '@playwright/test@1.58.2': + dependencies: + playwright: 1.58.2 + + '@polka/url@1.0.0-next.29': {} + + '@rollup/plugin-babel@5.3.1(@babel/core@7.29.0)(rollup@2.80.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@rollup/pluginutils': 3.1.0(rollup@2.80.0) + rollup: 2.80.0 + transitivePeerDependencies: + - supports-color + + '@rollup/plugin-commonjs@29.0.1(rollup@4.59.0)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.59.0) + commondir: 1.0.1 + estree-walker: 2.0.2 + fdir: 6.5.0(picomatch@4.0.3) + is-reference: 1.2.1 + magic-string: 0.30.21 + picomatch: 4.0.3 + optionalDependencies: + rollup: 4.59.0 + + '@rollup/plugin-json@6.1.0(rollup@4.59.0)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.59.0) + optionalDependencies: + rollup: 4.59.0 + + '@rollup/plugin-node-resolve@15.3.1(rollup@2.80.0)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@2.80.0) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.11 + optionalDependencies: + rollup: 2.80.0 + + '@rollup/plugin-node-resolve@16.0.3(rollup@4.59.0)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.59.0) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.11 + optionalDependencies: + rollup: 4.59.0 + + '@rollup/plugin-replace@2.4.2(rollup@2.80.0)': + dependencies: + '@rollup/pluginutils': 3.1.0(rollup@2.80.0) + magic-string: 0.25.9 + rollup: 2.80.0 + + '@rollup/plugin-terser@0.4.4(rollup@2.80.0)': + dependencies: + serialize-javascript: 6.0.2 + smob: 1.6.1 + terser: 5.46.0 + optionalDependencies: + rollup: 2.80.0 + + '@rollup/pluginutils@3.1.0(rollup@2.80.0)': + dependencies: + '@types/estree': 0.0.39 + estree-walker: 1.0.1 + picomatch: 2.3.1 + rollup: 2.80.0 + + '@rollup/pluginutils@5.3.0(rollup@2.80.0)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.3 + optionalDependencies: + rollup: 2.80.0 + + '@rollup/pluginutils@5.3.0(rollup@4.59.0)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.3 + optionalDependencies: + rollup: 4.59.0 + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@standard-schema/spec@1.1.0': {} + + '@surma/rollup-plugin-off-main-thread@2.2.3': + dependencies: + ejs: 3.1.10 + json5: 2.2.3 + magic-string: 0.25.9 + string.prototype.matchall: 4.0.12 + + '@sveltejs/acorn-typescript@1.0.9(acorn@8.16.0)': + dependencies: + acorn: 8.16.0 + + '@sveltejs/adapter-node@5.5.4(@sveltejs/kit@2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)))': + dependencies: + '@rollup/plugin-commonjs': 29.0.1(rollup@4.59.0) + '@rollup/plugin-json': 6.1.0(rollup@4.59.0) + '@rollup/plugin-node-resolve': 16.0.3(rollup@4.59.0) + '@sveltejs/kit': 2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)) + rollup: 4.59.0 + + '@sveltejs/kit@2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0))': + dependencies: + '@standard-schema/spec': 1.1.0 + '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)) + '@types/cookie': 0.6.0 + acorn: 8.16.0 + cookie: 0.6.0 + devalue: 5.6.3 + esm-env: 1.2.2 + kleur: 4.1.5 + magic-string: 0.30.21 + mrmime: 2.0.1 + set-cookie-parser: 3.0.1 + sirv: 3.0.2 + svelte: 5.53.7 + vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0) + optionalDependencies: + typescript: 5.9.3 + + '@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)))(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0))': + dependencies: + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)) + obug: 2.1.1 + svelte: 5.53.7 + vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0) + + '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0))': + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)))(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)) + deepmerge: 4.3.1 + magic-string: 0.30.21 + obug: 2.1.1 + svelte: 5.53.7 + vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0) + vitefu: 1.1.2(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)) + + '@tailwindcss/node@4.2.1': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.20.0 + jiti: 2.6.1 + lightningcss: 1.31.1 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.1 + + '@tailwindcss/oxide-android-arm64@4.2.1': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.1': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.1': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.1': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.1': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': + optional: true + + '@tailwindcss/oxide@4.2.1': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-x64': 4.2.1 + '@tailwindcss/oxide-freebsd-x64': 4.2.1 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.1 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.1 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-x64-musl': 4.2.1 + '@tailwindcss/oxide-wasm32-wasi': 4.2.1 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.1 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.1 + + '@tailwindcss/vite@4.2.1(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0))': + dependencies: + '@tailwindcss/node': 4.2.1 + '@tailwindcss/oxide': 4.2.1 + tailwindcss: 4.2.1 + vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0) + + '@types/better-sqlite3@7.6.13': + dependencies: + '@types/node': 25.3.3 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/cookie@0.6.0': {} + + '@types/deep-eql@4.0.2': {} + + '@types/estree@0.0.39': {} + + '@types/estree@1.0.8': {} + + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdurl@2.0.0': {} + + '@types/node@25.3.3': + dependencies: + undici-types: 7.18.2 + + '@types/resolve@1.20.2': {} + + '@types/trusted-types@2.0.7': {} + + '@types/uuid@10.0.0': {} + + '@vite-pwa/sveltekit@1.1.0(@sveltejs/kit@2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)))(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0))(workbox-build@7.4.0)(workbox-window@7.4.0)': + dependencies: + '@sveltejs/kit': 2.53.4(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.7)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)))(svelte@5.53.7)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)) + kolorist: 1.8.0 + tinyglobby: 0.2.15 + vite-plugin-pwa: 1.2.0(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0))(workbox-build@7.4.0)(workbox-window@7.4.0) + transitivePeerDependencies: + - supports-color + - vite + - workbox-build + - workbox-window + + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + acorn@8.16.0: {} + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + argon2@0.43.1: + dependencies: + '@phc/format': 1.0.0 + node-addon-api: 8.6.0 + node-gyp-build: 4.8.4 + + argparse@2.0.1: {} + + aria-query@5.3.1: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + 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: {} + + async-function@1.0.0: {} + + async@3.2.6: {} + + at-least-node@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axobject-query@4.1.0: {} + + babel-plugin-polyfill-corejs2@0.4.15(@babel/core@7.29.0): + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.6(@babel/core@7.29.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.14.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.6(@babel/core@7.29.0) + core-js-compat: 3.48.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-regenerator@0.6.6(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.10.0: {} + + better-sqlite3@11.10.0: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.4: + dependencies: + balanced-match: 4.0.4 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.10.0 + caniuse-lite: 1.0.30001776 + electron-to-chromium: 1.5.307 + node-releases: 2.0.36 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + cac@6.7.14: {} + + 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 + + caniuse-lite@1.0.30001776: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chownr@1.1.4: {} + + clsx@2.1.1: {} + + commander@2.20.3: {} + + common-tags@1.8.2: {} + + commondir@1.0.1: {} + + convert-source-map@2.0.0: {} + + cookie@0.6.0: {} + + core-js-compat@3.48.0: + dependencies: + browserslist: 4.28.1 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + crypto-random-string@2.0.0: {} + + 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@4.4.3: + dependencies: + ms: 2.1.3 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-eql@5.0.2: {} + + deep-extend@0.6.0: {} + + deepmerge@4.3.1: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + detect-libc@2.1.2: {} + + devalue@5.6.3: {} + + drizzle-kit@0.31.9: + dependencies: + '@drizzle-team/brocli': 0.10.2 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.25.12 + esbuild-register: 3.6.0(esbuild@0.25.12) + transitivePeerDependencies: + - supports-color + + drizzle-orm@0.44.7(@types/better-sqlite3@7.6.13)(better-sqlite3@11.10.0): + optionalDependencies: + '@types/better-sqlite3': 7.6.13 + better-sqlite3: 11.10.0 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ejs@3.1.10: + dependencies: + jake: 10.9.4 + + electron-to-chromium@1.5.307: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + enhanced-resolve@5.20.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + entities@4.5.0: {} + + 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-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-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + esbuild-register@3.6.0(esbuild@0.25.12): + dependencies: + debug: 4.4.3 + esbuild: 0.25.12 + transitivePeerDependencies: + - supports-color + + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + 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: {} + + esm-env@1.2.2: {} + + esrap@2.2.3: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + estree-walker@1.0.1: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + expand-template@2.0.3: {} + + expect-type@1.3.0: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-uri@3.1.0: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-uri-to-path@1.0.0: {} + + filelist@1.0.6: + dependencies: + minimatch: 5.1.9 + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fs-constants@1.0.0: {} + + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fsevents@2.3.2: + optional: true + + 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: {} + + generator-function@2.0.1: {} + + gensync@1.0.0-beta.2: {} + + 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-own-enumerable-property-symbols@3.0.2: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.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 + + github-from-package@0.0.0: {} + + glob@11.1.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.2.3 + minimatch: 10.2.4 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.2 + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + has-bigints@1.1.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 + + idb@7.1.1: {} + + idb@8.0.3: {} + + ieee754@1.2.1: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + 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-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-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + 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-map@2.0.3: {} + + is-module@1.0.0: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-obj@1.0.1: {} + + is-reference@1.2.1: + dependencies: + '@types/estree': 1.0.8 + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + 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@1.0.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-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-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 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + jackspeak@4.2.3: + dependencies: + '@isaacs/cliui': 9.0.0 + + jake@10.9.4: + dependencies: + async: 3.2.6 + filelist: 1.0.6 + picocolors: 1.1.1 + + jiti@2.6.1: {} + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + jsesc@3.1.0: {} + + json-schema-traverse@1.0.0: {} + + json-schema@0.4.0: {} + + json5@2.2.3: {} + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsonpointer@5.0.1: {} + + kleur@4.1.5: {} + + kolorist@1.8.0: {} + + leven@3.1.0: {} + + lightningcss-android-arm64@1.31.1: + optional: true + + lightningcss-darwin-arm64@1.31.1: + optional: true + + lightningcss-darwin-x64@1.31.1: + optional: true + + lightningcss-freebsd-x64@1.31.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.31.1: + optional: true + + lightningcss-linux-arm64-gnu@1.31.1: + optional: true + + lightningcss-linux-arm64-musl@1.31.1: + optional: true + + lightningcss-linux-x64-gnu@1.31.1: + optional: true + + lightningcss-linux-x64-musl@1.31.1: + optional: true + + lightningcss-win32-arm64-msvc@1.31.1: + optional: true + + lightningcss-win32-x64-msvc@1.31.1: + optional: true + + lightningcss@1.31.1: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.31.1 + lightningcss-darwin-arm64: 1.31.1 + lightningcss-darwin-x64: 1.31.1 + lightningcss-freebsd-x64: 1.31.1 + lightningcss-linux-arm-gnueabihf: 1.31.1 + lightningcss-linux-arm64-gnu: 1.31.1 + lightningcss-linux-arm64-musl: 1.31.1 + lightningcss-linux-x64-gnu: 1.31.1 + lightningcss-linux-x64-musl: 1.31.1 + lightningcss-win32-arm64-msvc: 1.31.1 + lightningcss-win32-x64-msvc: 1.31.1 + + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + locate-character@3.0.0: {} + + lodash.debounce@4.0.8: {} + + lodash.sortby@4.7.0: {} + + lodash@4.17.23: {} + + loupe@3.2.1: {} + + lru-cache@11.2.6: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.25.9: + dependencies: + sourcemap-codec: 1.4.8 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + markdown-it@14.1.1: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + + math-intrinsics@1.1.0: {} + + mdurl@2.0.0: {} + + mimic-response@3.1.0: {} + + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.4 + + minimatch@5.1.9: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + minipass@7.1.3: {} + + mkdirp-classic@0.5.3: {} + + mri@1.2.0: {} + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + napi-build-utils@2.0.0: {} + + node-abi@3.87.0: + dependencies: + semver: 7.7.4 + + node-addon-api@8.6.0: {} + + node-gyp-build@4.8.4: {} + + node-releases@2.0.36: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + 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 + + obug@2.1.1: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + package-json-from-dist@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.2.6 + minipass: 7.1.3 + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + playwright-core@1.58.2: {} + + playwright@1.58.2: + dependencies: + playwright-core: 1.58.2 + optionalDependencies: + fsevents: 2.3.2 + + possible-typed-array-names@1.1.0: {} + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.87.0 + pump: 3.0.4 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.4 + tunnel-agent: 0.6.0 + + pretty-bytes@5.6.0: {} + + pretty-bytes@6.1.1: {} + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + punycode.js@2.3.1: {} + + punycode@2.3.1: {} + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@4.1.2: {} + + 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 + + regenerate-unicode-properties@10.2.2: + dependencies: + regenerate: 1.4.2 + + regenerate@1.4.2: {} + + 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 + + regexpu-core@6.4.0: + dependencies: + regenerate: 1.4.2 + regenerate-unicode-properties: 10.2.2 + regjsgen: 0.8.0 + regjsparser: 0.13.0 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.2.1 + + regjsgen@0.8.0: {} + + regjsparser@0.13.0: + dependencies: + jsesc: 3.1.0 + + require-from-string@2.0.2: {} + + 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 + + rollup@2.80.0: + optionalDependencies: + fsevents: 2.3.3 + + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 + fsevents: 2.3.3 + + sade@1.8.1: + dependencies: + mri: 1.2.0 + + 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-buffer@5.2.1: {} + + 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 + + semver@6.3.1: {} + + semver@7.7.4: {} + + serialize-javascript@6.0.2: + dependencies: + randombytes: 2.1.0 + + set-cookie-parser@3.0.1: {} + + 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 + + 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@4.1.0: {} + + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + smob@1.6.1: {} + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.8.0-beta.0: + dependencies: + whatwg-url: 7.1.0 + + sourcemap-codec@1.4.8: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + 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.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 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + stringify-object@3.3.0: + dependencies: + get-own-enumerable-property-symbols: 3.0.2 + is-obj: 1.0.1 + is-regexp: 1.0.0 + + strip-comments@2.0.1: {} + + strip-json-comments@2.0.1: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + supports-preserve-symlinks-flag@1.0.0: {} + + svelte-check@4.4.4(picomatch@4.0.3)(svelte@5.53.7)(typescript@5.9.3): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + chokidar: 4.0.3 + fdir: 6.5.0(picomatch@4.0.3) + picocolors: 1.1.1 + sade: 1.8.1 + svelte: 5.53.7 + typescript: 5.9.3 + transitivePeerDependencies: + - picomatch + + svelte@5.53.7: + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.5 + '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) + '@types/estree': 1.0.8 + '@types/trusted-types': 2.0.7 + acorn: 8.16.0 + aria-query: 5.3.1 + axobject-query: 4.1.0 + clsx: 2.1.1 + devalue: 5.6.3 + esm-env: 1.2.2 + esrap: 2.2.3 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.4 + + tailwindcss@4.2.1: {} + + tapable@2.3.0: {} + + tar-fs@2.1.4: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.4 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + temp-dir@2.0.0: {} + + tempy@0.6.0: + dependencies: + is-stream: 2.0.1 + temp-dir: 2.0.0 + type-fest: 0.16.0 + unique-string: 2.0.0 + + terser@5.46.0: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.16.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + totalist@3.0.1: {} + + tr46@1.0.1: + dependencies: + punycode: 2.3.1 + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + type-fest@0.16.0: {} + + 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@5.9.3: {} + + uc.micro@2.1.0: {} + + 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@7.18.2: {} + + unicode-canonical-property-names-ecmascript@2.0.1: {} + + unicode-match-property-ecmascript@2.0.0: + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.1 + unicode-property-aliases-ecmascript: 2.2.0 + + unicode-match-property-value-ecmascript@2.2.1: {} + + unicode-property-aliases-ecmascript@2.2.0: {} + + unique-string@2.0.0: + dependencies: + crypto-random-string: 2.0.0 + + universalify@2.0.1: {} + + upath@1.2.0: {} + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + util-deprecate@1.0.2: {} + + uuid@11.1.0: {} + + vite-node@3.2.4(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite-plugin-pwa@1.2.0(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0))(workbox-build@7.4.0)(workbox-window@7.4.0): + dependencies: + debug: 4.4.3 + pretty-bytes: 6.1.1 + tinyglobby: 0.2.15 + vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0) + workbox-build: 7.4.0 + workbox-window: 7.4.0 + transitivePeerDependencies: + - supports-color + + vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0): + dependencies: + esbuild: 0.27.3 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.8 + rollup: 4.59.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 25.3.3 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.31.1 + terser: 5.46.0 + + vitefu@1.1.2(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)): + optionalDependencies: + vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0) + + vitest@3.2.4(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0) + vite-node: 3.2.4(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.3.3 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + webidl-conversions@4.0.2: {} + + whatwg-url@7.1.0: + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + + 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 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + workbox-background-sync@7.4.0: + dependencies: + idb: 7.1.1 + workbox-core: 7.4.0 + + workbox-broadcast-update@7.4.0: + dependencies: + workbox-core: 7.4.0 + + workbox-build@7.4.0: + dependencies: + '@apideck/better-ajv-errors': 0.3.6(ajv@8.18.0) + '@babel/core': 7.29.0 + '@babel/preset-env': 7.29.0(@babel/core@7.29.0) + '@babel/runtime': 7.28.6 + '@rollup/plugin-babel': 5.3.1(@babel/core@7.29.0)(rollup@2.80.0) + '@rollup/plugin-node-resolve': 15.3.1(rollup@2.80.0) + '@rollup/plugin-replace': 2.4.2(rollup@2.80.0) + '@rollup/plugin-terser': 0.4.4(rollup@2.80.0) + '@surma/rollup-plugin-off-main-thread': 2.2.3 + ajv: 8.18.0 + common-tags: 1.8.2 + fast-json-stable-stringify: 2.1.0 + fs-extra: 9.1.0 + glob: 11.1.0 + lodash: 4.17.23 + pretty-bytes: 5.6.0 + rollup: 2.80.0 + source-map: 0.8.0-beta.0 + stringify-object: 3.3.0 + strip-comments: 2.0.1 + tempy: 0.6.0 + upath: 1.2.0 + workbox-background-sync: 7.4.0 + workbox-broadcast-update: 7.4.0 + workbox-cacheable-response: 7.4.0 + workbox-core: 7.4.0 + workbox-expiration: 7.4.0 + workbox-google-analytics: 7.4.0 + workbox-navigation-preload: 7.4.0 + workbox-precaching: 7.4.0 + workbox-range-requests: 7.4.0 + workbox-recipes: 7.4.0 + workbox-routing: 7.4.0 + workbox-strategies: 7.4.0 + workbox-streams: 7.4.0 + workbox-sw: 7.4.0 + workbox-window: 7.4.0 + transitivePeerDependencies: + - '@types/babel__core' + - supports-color + + workbox-cacheable-response@7.4.0: + dependencies: + workbox-core: 7.4.0 + + workbox-core@7.4.0: {} + + workbox-expiration@7.4.0: + dependencies: + idb: 7.1.1 + workbox-core: 7.4.0 + + workbox-google-analytics@7.4.0: + dependencies: + workbox-background-sync: 7.4.0 + workbox-core: 7.4.0 + workbox-routing: 7.4.0 + workbox-strategies: 7.4.0 + + workbox-navigation-preload@7.4.0: + dependencies: + workbox-core: 7.4.0 + + workbox-precaching@7.4.0: + dependencies: + workbox-core: 7.4.0 + workbox-routing: 7.4.0 + workbox-strategies: 7.4.0 + + workbox-range-requests@7.4.0: + dependencies: + workbox-core: 7.4.0 + + workbox-recipes@7.4.0: + dependencies: + workbox-cacheable-response: 7.4.0 + workbox-core: 7.4.0 + workbox-expiration: 7.4.0 + workbox-precaching: 7.4.0 + workbox-routing: 7.4.0 + workbox-strategies: 7.4.0 + + workbox-routing@7.4.0: + dependencies: + workbox-core: 7.4.0 + + workbox-strategies@7.4.0: + dependencies: + workbox-core: 7.4.0 + + workbox-streams@7.4.0: + dependencies: + workbox-core: 7.4.0 + workbox-routing: 7.4.0 + + workbox-sw@7.4.0: {} + + workbox-window@7.4.0: + dependencies: + '@types/trusted-types': 2.0.7 + workbox-core: 7.4.0 + + wrappy@1.0.2: {} + + yallist@3.1.1: {} + + zimmerframe@1.1.4: {} diff --git a/src/app.css b/src/app.css new file mode 100644 index 0000000..d4b5078 --- /dev/null +++ b/src/app.css @@ -0,0 +1 @@ +@import 'tailwindcss'; diff --git a/src/app.d.ts b/src/app.d.ts new file mode 100644 index 0000000..da08e6d --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..1986966 --- /dev/null +++ b/src/app.html @@ -0,0 +1,14 @@ + + + + + + + + Crumbs + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/src/hooks.server.ts b/src/hooks.server.ts new file mode 100644 index 0000000..51c831d --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,37 @@ +import type { Handle } from '@sveltejs/kit'; +import { redirect } from '@sveltejs/kit'; +import { validateSession, isSetupComplete } from '$lib/server/auth.js'; + +const PUBLIC_PATHS = ['/login', '/setup', '/api/auth/login', '/api/auth/setup']; + +export const handle: Handle = async ({ event, resolve }) => { + const { pathname } = event.url; + + // Allow public paths + if (PUBLIC_PATHS.some((p) => pathname.startsWith(p))) { + return resolve(event); + } + + // Check if setup is complete + const setupDone = await isSetupComplete(); + if (!setupDone) { + if (pathname !== '/setup') { + throw redirect(302, '/setup'); + } + return resolve(event); + } + + // Validate session + const sessionToken = event.cookies.get('session'); + if (!sessionToken || !(await validateSession(sessionToken))) { + if (pathname.startsWith('/api/')) { + return new Response(JSON.stringify({ error: 'Unauthorized' }), { + status: 401, + headers: { 'Content-Type': 'application/json' } + }); + } + throw redirect(302, '/login'); + } + + return resolve(event); +}; diff --git a/src/lib/components/Checklist.svelte b/src/lib/components/Checklist.svelte new file mode 100644 index 0000000..773b821 --- /dev/null +++ b/src/lib/components/Checklist.svelte @@ -0,0 +1,120 @@ + + +
+ {#each items as item, index} +
+ toggleItem(index)} + class="h-4 w-4 rounded border-gray-300 text-amber-600 focus:ring-amber-500" + data-testid="checklist-checkbox" + /> + updateText(index, (e.target as HTMLInputElement).value)} + onkeydown={(e) => handleKeydown(e, index)} + class="flex-1 bg-transparent text-sm outline-none {item.checked ? 'text-gray-400 line-through' : 'text-gray-800 dark:text-gray-200'}" + placeholder="List item" + data-testid="checklist-input" + /> + +
+ {/each} + +
diff --git a/src/lib/components/ColorPicker.svelte b/src/lib/components/ColorPicker.svelte new file mode 100644 index 0000000..85d016c --- /dev/null +++ b/src/lib/components/ColorPicker.svelte @@ -0,0 +1,25 @@ + + +
+ {#each COLOR_OPTIONS as option} + {@const colors = NOTE_COLORS[option.value]} + + {/each} +
diff --git a/src/lib/components/ImageUpload.svelte b/src/lib/components/ImageUpload.svelte new file mode 100644 index 0000000..058b2ed --- /dev/null +++ b/src/lib/components/ImageUpload.svelte @@ -0,0 +1,107 @@ + + +
+ + {#if attachments.length > 0} +
+ {#each attachments as attachment} +
+ {attachment.filename} + +
+ {/each} +
+ {/if} + + +
(dragOver = false)} + role="button" + tabindex="0" + data-testid="upload-dropzone" + > + {#if uploading} +

Uploading...

+ {:else} + + {/if} +
+
diff --git a/src/lib/components/Layout/Header.svelte b/src/lib/components/Layout/Header.svelte new file mode 100644 index 0000000..59a8897 --- /dev/null +++ b/src/lib/components/Layout/Header.svelte @@ -0,0 +1,37 @@ + + +
+ + +
+ 🥨 +

Crumbs

+
+ +
+ +
+ +
+ + +
+
diff --git a/src/lib/components/Layout/Sidebar.svelte b/src/lib/components/Layout/Sidebar.svelte new file mode 100644 index 0000000..d6c69e6 --- /dev/null +++ b/src/lib/components/Layout/Sidebar.svelte @@ -0,0 +1,83 @@ + + + diff --git a/src/lib/components/Layout/Toast.svelte b/src/lib/components/Layout/Toast.svelte new file mode 100644 index 0000000..c242462 --- /dev/null +++ b/src/lib/components/Layout/Toast.svelte @@ -0,0 +1,49 @@ + + +
+ {#each $toasts as toast (toast.id)} +
+ {toast.text} + {#if toast.action} + + {/if} + +
+ {/each} +
diff --git a/src/lib/components/NoteCard.svelte b/src/lib/components/NoteCard.svelte new file mode 100644 index 0000000..05ff018 --- /dev/null +++ b/src/lib/components/NoteCard.svelte @@ -0,0 +1,139 @@ + + +
onEdit(note)} + onkeydown={(e) => e.key === 'Enter' && onEdit(note)} + role="button" + tabindex="0" + data-testid="note-card" + data-note-id={note.id} +> + {#if note.title} +

{note.title}

+ {/if} + + {#if preview} +

{preview}

+ {/if} + + {#if note.tags && note.tags.length > 0} +
+ {#each note.tags as tag} + + #{tag} + + {/each} +
+ {/if} + + +
+ {#if $currentFilter === 'trashed'} + + + {:else} + + {#if $currentFilter === 'archived'} + + {:else} + + {/if} + + {/if} +
+ + + {#if note.pinned} +
+ + + +
+ {/if} +
diff --git a/src/lib/components/NoteEditor.svelte b/src/lib/components/NoteEditor.svelte new file mode 100644 index 0000000..2fdee55 --- /dev/null +++ b/src/lib/components/NoteEditor.svelte @@ -0,0 +1,166 @@ + + + +
+ +
e.stopPropagation()} + onkeydown={(e) => { e.stopPropagation(); handleKeydown(e); }} + data-testid="note-editor" + > + + + + + {#if checklistMode} +
+ (content = c)} /> +
+ {:else if showPreview} +
+ {@html renderedContent} +
+ {:else} + + {/if} + + +
+
+ +
+ + {#if showColorPicker} +
+ +
+ {/if} +
+ + + + + + +
+ + +
+
+
diff --git a/src/lib/components/NoteGrid.svelte b/src/lib/components/NoteGrid.svelte new file mode 100644 index 0000000..82592f0 --- /dev/null +++ b/src/lib/components/NoteGrid.svelte @@ -0,0 +1,23 @@ + + +{#if notes.length > 0} + {#if label} +

{label}

+ {/if} +
+ {#each notes as note (note.id)} + + {/each} +
+{/if} diff --git a/src/lib/components/SearchBar.svelte b/src/lib/components/SearchBar.svelte new file mode 100644 index 0000000..c450a02 --- /dev/null +++ b/src/lib/components/SearchBar.svelte @@ -0,0 +1,60 @@ + + +
+
+ + + + + {#if query} + + {/if} +
+
diff --git a/src/lib/components/SyncIndicator.svelte b/src/lib/components/SyncIndicator.svelte new file mode 100644 index 0000000..6e2a169 --- /dev/null +++ b/src/lib/components/SyncIndicator.svelte @@ -0,0 +1,24 @@ + + +
+ {#if status === 'synced'} + + + + {:else if status === 'syncing'} + + + + {:else if status === 'offline'} + + + + {:else} + + + + {/if} +
diff --git a/src/lib/components/TagFilter.svelte b/src/lib/components/TagFilter.svelte new file mode 100644 index 0000000..f3ab634 --- /dev/null +++ b/src/lib/components/TagFilter.svelte @@ -0,0 +1,21 @@ + + +{#if $allTags.length > 0} +
+ {#each $allTags as tag} + + {/each} +
+{/if} diff --git a/src/lib/components/ThemeToggle.svelte b/src/lib/components/ThemeToggle.svelte new file mode 100644 index 0000000..0f0e4ef --- /dev/null +++ b/src/lib/components/ThemeToggle.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/index.ts b/src/lib/index.ts new file mode 100644 index 0000000..856f2b6 --- /dev/null +++ b/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/src/lib/server/attachments.ts b/src/lib/server/attachments.ts new file mode 100644 index 0000000..52cc2e3 --- /dev/null +++ b/src/lib/server/attachments.ts @@ -0,0 +1,63 @@ +import { existsSync, mkdirSync } from 'fs'; +import { writeFile, unlink } from 'fs/promises'; +import { join } from 'path'; +import { v4 as uuidv4 } from 'uuid'; +import { db } from './db/index.js'; +import { attachments } from './db/schema.js'; +import { eq } from 'drizzle-orm'; + +const DATA_DIR = process.env.DATA_DIR || './data'; +const ATTACHMENTS_DIR = join(DATA_DIR, 'attachments'); + +// Ensure attachments directory exists +if (!existsSync(ATTACHMENTS_DIR)) { + mkdirSync(ATTACHMENTS_DIR, { recursive: true }); +} + +export async function saveAttachment( + noteId: string, + file: File +): Promise { + const id = uuidv4(); + const ext = file.name.split('.').pop() || 'bin'; + const filename = `${id}.${ext}`; + const filePath = join(ATTACHMENTS_DIR, filename); + + const buffer = Buffer.from(await file.arrayBuffer()); + await writeFile(filePath, buffer); + + const [attachment] = await db + .insert(attachments) + .values({ + id, + noteId, + filename: file.name, + mimeType: file.type, + size: file.size, + path: filePath, + createdAt: new Date() + }) + .returning(); + + return attachment; +} + +export async function getAttachment(id: string) { + return db.select().from(attachments).where(eq(attachments.id, id)).get(); +} + +export async function getAttachmentsByNote(noteId: string) { + return db.select().from(attachments).where(eq(attachments.noteId, noteId)); +} + +export async function deleteAttachment(id: string) { + const attachment = await getAttachment(id); + if (attachment) { + try { + await unlink(attachment.path); + } catch { + // File may already be deleted + } + await db.delete(attachments).where(eq(attachments.id, id)); + } +} diff --git a/src/lib/server/auth.test.ts b/src/lib/server/auth.test.ts new file mode 100644 index 0000000..f145473 --- /dev/null +++ b/src/lib/server/auth.test.ts @@ -0,0 +1,120 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { createTestDb } from './db/test-helpers.js'; +import { users, sessions } from './db/schema.js'; +import { eq } from 'drizzle-orm'; +import * as argon2 from 'argon2'; +import { randomBytes } from 'crypto'; +import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3'; +import type * as schema from './db/schema.js'; + +let db: BetterSQLite3Database; + +beforeEach(() => { + const testDb = createTestDb(); + db = testDb.db; +}); + +describe('Auth - Password Hashing', () => { + it('should hash and verify a password correctly', async () => { + const password = 'mysecurepassword'; + const hash = await argon2.hash(password); + + expect(hash).not.toBe(password); + expect(await argon2.verify(hash, password)).toBe(true); + expect(await argon2.verify(hash, 'wrongpassword')).toBe(false); + }); + + it('should produce different hashes for the same password', async () => { + const password = 'mysecurepassword'; + const hash1 = await argon2.hash(password); + const hash2 = await argon2.hash(password); + + expect(hash1).not.toBe(hash2); + expect(await argon2.verify(hash1, password)).toBe(true); + expect(await argon2.verify(hash2, password)).toBe(true); + }); +}); + +describe('Auth - User Setup', () => { + it('should create a user with hashed password', async () => { + const password = 'testpassword123'; + const hash = await argon2.hash(password); + + await db.insert(users).values({ + passwordHash: hash, + createdAt: new Date() + }); + + const user = await db.select().from(users).get(); + expect(user).toBeDefined(); + expect(user!.passwordHash).not.toBe(password); + expect(await argon2.verify(user!.passwordHash, password)).toBe(true); + }); + + it('should only allow one user', async () => { + await db.insert(users).values({ + passwordHash: 'hash1', + createdAt: new Date() + }); + + const allUsers = await db.select().from(users); + expect(allUsers).toHaveLength(1); + }); +}); + +describe('Auth - Sessions', () => { + it('should create and validate a session', async () => { + const [user] = await db + .insert(users) + .values({ passwordHash: 'hash', createdAt: new Date() }) + .returning(); + + const token = randomBytes(32).toString('hex'); + await db.insert(sessions).values({ + id: token, + userId: user.id, + expiresAt: new Date(Date.now() + 86400000) + }); + + const session = await db.select().from(sessions).where(eq(sessions.id, token)).get(); + expect(session).toBeDefined(); + expect(session!.userId).toBe(user.id); + expect(session!.expiresAt.getTime()).toBeGreaterThan(Date.now()); + }); + + it('should detect expired sessions', async () => { + const [user] = await db + .insert(users) + .values({ passwordHash: 'hash', createdAt: new Date() }) + .returning(); + + const token = randomBytes(32).toString('hex'); + await db.insert(sessions).values({ + id: token, + userId: user.id, + expiresAt: new Date(Date.now() - 1000) // Expired + }); + + const session = await db.select().from(sessions).where(eq(sessions.id, token)).get(); + expect(session).toBeDefined(); + expect(session!.expiresAt.getTime()).toBeLessThan(Date.now()); + }); + + it('should delete a session', async () => { + const [user] = await db + .insert(users) + .values({ passwordHash: 'hash', createdAt: new Date() }) + .returning(); + + const token = randomBytes(32).toString('hex'); + await db.insert(sessions).values({ + id: token, + userId: user.id, + expiresAt: new Date(Date.now() + 86400000) + }); + + await db.delete(sessions).where(eq(sessions.id, token)); + const session = await db.select().from(sessions).where(eq(sessions.id, token)).get(); + expect(session).toBeUndefined(); + }); +}); diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts new file mode 100644 index 0000000..12ba76e --- /dev/null +++ b/src/lib/server/auth.ts @@ -0,0 +1,72 @@ +import { db } from './db/index.js'; +import { users, sessions } from './db/schema.js'; +import { eq } from 'drizzle-orm'; +import * as argon2 from 'argon2'; +import { randomBytes } from 'crypto'; + +const SESSION_DURATION_MS = 30 * 24 * 60 * 60 * 1000; // 30 days + +export async function isSetupComplete(): Promise { + const user = await db.select().from(users).get(); + return !!user; +} + +export async function setupUser(password: string): Promise { + const existing = await db.select().from(users).get(); + if (existing) return false; // Already set up + + const hash = await argon2.hash(password); + await db.insert(users).values({ + passwordHash: hash, + createdAt: new Date() + }); + return true; +} + +export async function verifyPassword(password: string): Promise { + const user = await db.select().from(users).get(); + if (!user) return false; + return argon2.verify(user.passwordHash, password); +} + +export async function createSession(): Promise { + const user = await db.select().from(users).get(); + if (!user) throw new Error('No user exists'); + + const token = randomBytes(32).toString('hex'); + await db.insert(sessions).values({ + id: token, + userId: user.id, + expiresAt: new Date(Date.now() + SESSION_DURATION_MS) + }); + return token; +} + +export async function validateSession(token: string): Promise { + if (!token) return false; + + const session = await db.select().from(sessions).where(eq(sessions.id, token)).get(); + if (!session) return false; + + if (session.expiresAt < new Date()) { + await db.delete(sessions).where(eq(sessions.id, token)); + return false; + } + + return true; +} + +export async function deleteSession(token: string): Promise { + await db.delete(sessions).where(eq(sessions.id, token)); +} + +export async function cleanExpiredSessions(): Promise { + const now = new Date(); + // Delete all expired sessions by checking each one + const allSessions = await db.select().from(sessions); + for (const session of allSessions) { + if (session.expiresAt < now) { + await db.delete(sessions).where(eq(sessions.id, session.id)); + } + } +} diff --git a/src/lib/server/db/index.ts b/src/lib/server/db/index.ts new file mode 100644 index 0000000..fc967b6 --- /dev/null +++ b/src/lib/server/db/index.ts @@ -0,0 +1,82 @@ +import Database from 'better-sqlite3'; +import { drizzle } from 'drizzle-orm/better-sqlite3'; +import * as schema from './schema.js'; +import { existsSync, mkdirSync } from 'fs'; +import { dirname } from 'path'; + +const DATABASE_URL = process.env.DATABASE_URL || './data/crumbs.db'; + +// Ensure the data directory exists +const dir = dirname(DATABASE_URL); +if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); +} + +const sqlite = new Database(DATABASE_URL); + +// Enable WAL mode for better concurrent read performance +sqlite.pragma('journal_mode = WAL'); +sqlite.pragma('foreign_keys = ON'); + +// Auto-create tables on first run +sqlite.exec(` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + password_hash TEXT NOT NULL, + created_at INTEGER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS sessions ( + id TEXT PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id), + expires_at INTEGER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS notes ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL DEFAULT '', + content TEXT NOT NULL DEFAULT '', + color TEXT NOT NULL DEFAULT 'default', + pinned INTEGER NOT NULL DEFAULT 0, + archived INTEGER NOT NULL DEFAULT 0, + trashed INTEGER NOT NULL DEFAULT 0, + trashed_at INTEGER, + checklist_mode INTEGER NOT NULL DEFAULT 0, + sort_order INTEGER NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + version INTEGER NOT NULL DEFAULT 1 + ); + + CREATE TABLE IF NOT EXISTS tags ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL + ); + CREATE UNIQUE INDEX IF NOT EXISTS tags_name_unique ON tags(name); + + CREATE TABLE IF NOT EXISTS note_tags ( + note_id TEXT NOT NULL REFERENCES notes(id) ON DELETE CASCADE, + tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS attachments ( + id TEXT PRIMARY KEY, + note_id TEXT NOT NULL REFERENCES notes(id) ON DELETE CASCADE, + filename TEXT NOT NULL, + mime_type TEXT NOT NULL, + size INTEGER NOT NULL, + path TEXT NOT NULL, + created_at INTEGER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS sync_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + note_id TEXT NOT NULL REFERENCES notes(id), + operation TEXT NOT NULL, + timestamp INTEGER NOT NULL, + client_id TEXT NOT NULL + ); +`); + +export const db = drizzle(sqlite, { schema }); +export { sqlite }; diff --git a/src/lib/server/db/notes.test.ts b/src/lib/server/db/notes.test.ts new file mode 100644 index 0000000..6fbde5d --- /dev/null +++ b/src/lib/server/db/notes.test.ts @@ -0,0 +1,294 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { createTestDb } from './test-helpers.js'; +import { notes, noteTags, tags } from './schema.js'; +import { eq, and } from 'drizzle-orm'; +import { v4 as uuidv4 } from 'uuid'; +import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3'; +import type * as schema from './schema.js'; + +let db: BetterSQLite3Database; + +beforeEach(() => { + const testDb = createTestDb(); + db = testDb.db; +}); + +describe('Notes CRUD', () => { + it('should create a note with default values', async () => { + const id = uuidv4(); + const now = new Date(); + + await db.insert(notes).values({ + id, + title: 'Test Note', + content: 'Hello world', + createdAt: now, + updatedAt: now + }); + + const result = await db.select().from(notes).where(eq(notes.id, id)).get(); + + expect(result).toBeDefined(); + expect(result!.title).toBe('Test Note'); + expect(result!.content).toBe('Hello world'); + expect(result!.color).toBe('default'); + expect(result!.pinned).toBe(false); + expect(result!.archived).toBe(false); + expect(result!.trashed).toBe(false); + expect(result!.checklistMode).toBe(false); + expect(result!.version).toBe(1); + }); + + it('should create a note with custom color and pinned state', async () => { + const id = uuidv4(); + const now = new Date(); + + await db.insert(notes).values({ + id, + title: 'Colored Note', + content: '', + color: 'coral', + pinned: true, + createdAt: now, + updatedAt: now + }); + + const result = await db.select().from(notes).where(eq(notes.id, id)).get(); + + expect(result!.color).toBe('coral'); + expect(result!.pinned).toBe(true); + }); + + it('should read all active notes (not archived, not trashed)', async () => { + const now = new Date(); + await db.insert(notes).values([ + { id: 'n1', title: 'Active', content: '', createdAt: now, updatedAt: now }, + { + id: 'n2', + title: 'Archived', + content: '', + archived: true, + createdAt: now, + updatedAt: now + }, + { id: 'n3', title: 'Trashed', content: '', trashed: true, createdAt: now, updatedAt: now } + ]); + + const active = await db + .select() + .from(notes) + .where(and(eq(notes.archived, false), eq(notes.trashed, false))); + + expect(active).toHaveLength(1); + expect(active[0].title).toBe('Active'); + }); + + it('should update a note and increment version', async () => { + const id = uuidv4(); + const now = new Date(); + + await db.insert(notes).values({ + id, + title: 'Original', + content: '', + createdAt: now, + updatedAt: now + }); + + const later = new Date(now.getTime() + 1000); + await db + .update(notes) + .set({ title: 'Updated', updatedAt: later, version: 2 }) + .where(eq(notes.id, id)); + + const result = await db.select().from(notes).where(eq(notes.id, id)).get(); + + expect(result!.title).toBe('Updated'); + expect(result!.version).toBe(2); + }); + + it('should soft-delete a note (move to trash)', async () => { + const id = uuidv4(); + const now = new Date(); + + await db.insert(notes).values({ + id, + title: 'To Trash', + content: '', + createdAt: now, + updatedAt: now + }); + + await db.update(notes).set({ trashed: true, trashedAt: now }).where(eq(notes.id, id)); + + const result = await db.select().from(notes).where(eq(notes.id, id)).get(); + expect(result!.trashed).toBe(true); + expect(result!.trashedAt).toBeTruthy(); + }); + + it('should permanently delete a note', async () => { + const id = uuidv4(); + const now = new Date(); + + await db.insert(notes).values({ + id, + title: 'To Delete', + content: '', + createdAt: now, + updatedAt: now + }); + + await db.delete(notes).where(eq(notes.id, id)); + const result = await db.select().from(notes).where(eq(notes.id, id)).get(); + expect(result).toBeUndefined(); + }); + + it('should archive and unarchive a note', async () => { + const id = uuidv4(); + const now = new Date(); + + await db.insert(notes).values({ + id, + title: 'To Archive', + content: '', + createdAt: now, + updatedAt: now + }); + + await db.update(notes).set({ archived: true }).where(eq(notes.id, id)); + let result = await db.select().from(notes).where(eq(notes.id, id)).get(); + expect(result!.archived).toBe(true); + + await db.update(notes).set({ archived: false }).where(eq(notes.id, id)); + result = await db.select().from(notes).where(eq(notes.id, id)).get(); + expect(result!.archived).toBe(false); + }); + + it('should pin and unpin a note', async () => { + const id = uuidv4(); + const now = new Date(); + + await db.insert(notes).values({ + id, + title: 'Pin Me', + content: '', + createdAt: now, + updatedAt: now + }); + + await db.update(notes).set({ pinned: true }).where(eq(notes.id, id)); + let result = await db.select().from(notes).where(eq(notes.id, id)).get(); + expect(result!.pinned).toBe(true); + + await db.update(notes).set({ pinned: false }).where(eq(notes.id, id)); + result = await db.select().from(notes).where(eq(notes.id, id)).get(); + expect(result!.pinned).toBe(false); + }); +}); + +describe('Notes with Tags', () => { + it('should associate tags with a note', async () => { + const noteId = uuidv4(); + const now = new Date(); + + await db.insert(notes).values({ + id: noteId, + title: 'Tagged Note', + content: '', + createdAt: now, + updatedAt: now + }); + + // Create tags + const [tag1] = await db.insert(tags).values({ name: 'work' }).returning(); + const [tag2] = await db.insert(tags).values({ name: 'important' }).returning(); + + // Associate + await db.insert(noteTags).values([ + { noteId, tagId: tag1.id }, + { noteId, tagId: tag2.id } + ]); + + // Query + const tagRows = await db + .select({ name: tags.name }) + .from(noteTags) + .innerJoin(tags, eq(noteTags.tagId, tags.id)) + .where(eq(noteTags.noteId, noteId)); + + expect(tagRows).toHaveLength(2); + expect(tagRows.map((t) => t.name).sort()).toEqual(['important', 'work']); + }); + + it('should cascade delete tags when note is deleted', async () => { + const noteId = uuidv4(); + const now = new Date(); + + await db.insert(notes).values({ + id: noteId, + title: '', + content: '', + createdAt: now, + updatedAt: now + }); + + const [tag] = await db.insert(tags).values({ name: 'temp' }).returning(); + await db.insert(noteTags).values({ noteId, tagId: tag.id }); + + // Delete note + await db.delete(notes).where(eq(notes.id, noteId)); + + // note_tags should be gone + const remaining = await db + .select() + .from(noteTags) + .where(eq(noteTags.noteId, noteId)); + expect(remaining).toHaveLength(0); + + // Tag itself should still exist + const tagStillExists = await db.select().from(tags).where(eq(tags.id, tag.id)).get(); + expect(tagStillExists).toBeDefined(); + }); +}); + +describe('Note ordering', () => { + it('should return pinned notes first', async () => { + const now = new Date(); + await db.insert(notes).values([ + { + id: 'unpinned', + title: 'Unpinned', + content: '', + pinned: false, + createdAt: now, + updatedAt: now + }, + { + id: 'pinned', + title: 'Pinned', + content: '', + pinned: true, + createdAt: now, + updatedAt: now + } + ]); + + const result = await db + .select() + .from(notes) + .orderBy(({ pinned }) => [ + // SQLite: true = 1, false = 0, DESC puts 1 first + eq(pinned, pinned) // just use raw desc + ]); + + // Manual approach: order by pinned desc + const { desc } = await import('drizzle-orm'); + const ordered = await db + .select() + .from(notes) + .orderBy(desc(notes.pinned), desc(notes.updatedAt)); + + expect(ordered[0].title).toBe('Pinned'); + expect(ordered[1].title).toBe('Unpinned'); + }); +}); diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts new file mode 100644 index 0000000..54cae96 --- /dev/null +++ b/src/lib/server/db/schema.ts @@ -0,0 +1,71 @@ +import { sqliteTable, text, integer, uniqueIndex } from 'drizzle-orm/sqlite-core'; + +export const users = sqliteTable('users', { + id: integer('id').primaryKey({ autoIncrement: true }), + passwordHash: text('password_hash').notNull(), + createdAt: integer('created_at', { mode: 'timestamp' }).notNull() +}); + +export const sessions = sqliteTable('sessions', { + id: text('id').primaryKey(), + userId: integer('user_id') + .references(() => users.id) + .notNull(), + expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull() +}); + +export const notes = sqliteTable('notes', { + id: text('id').primaryKey(), + title: text('title').default('').notNull(), + content: text('content').default('').notNull(), + color: text('color').default('default').notNull(), + pinned: integer('pinned', { mode: 'boolean' }).default(false).notNull(), + archived: integer('archived', { mode: 'boolean' }).default(false).notNull(), + trashed: integer('trashed', { mode: 'boolean' }).default(false).notNull(), + trashedAt: integer('trashed_at', { mode: 'timestamp' }), + checklistMode: integer('checklist_mode', { mode: 'boolean' }).default(false).notNull(), + sortOrder: integer('sort_order').default(0).notNull(), + createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), + updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(), + version: integer('version').default(1).notNull() +}); + +export const tags = sqliteTable( + 'tags', + { + id: integer('id').primaryKey({ autoIncrement: true }), + name: text('name').notNull() + }, + (table) => [uniqueIndex('tags_name_unique').on(table.name)] +); + +export const noteTags = sqliteTable('note_tags', { + noteId: text('note_id') + .references(() => notes.id, { onDelete: 'cascade' }) + .notNull(), + tagId: integer('tag_id') + .references(() => tags.id, { onDelete: 'cascade' }) + .notNull() +}); + +export const attachments = sqliteTable('attachments', { + id: text('id').primaryKey(), + noteId: text('note_id') + .references(() => notes.id, { onDelete: 'cascade' }) + .notNull(), + filename: text('filename').notNull(), + mimeType: text('mime_type').notNull(), + size: integer('size').notNull(), + path: text('path').notNull(), + createdAt: integer('created_at', { mode: 'timestamp' }).notNull() +}); + +export const syncLog = sqliteTable('sync_log', { + id: integer('id').primaryKey({ autoIncrement: true }), + noteId: text('note_id') + .references(() => notes.id) + .notNull(), + operation: text('operation').notNull(), + timestamp: integer('timestamp', { mode: 'timestamp' }).notNull(), + clientId: text('client_id').notNull() +}); diff --git a/src/lib/server/db/search.test.ts b/src/lib/server/db/search.test.ts new file mode 100644 index 0000000..74551d3 --- /dev/null +++ b/src/lib/server/db/search.test.ts @@ -0,0 +1,116 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { createTestDb } from './test-helpers.js'; +import { notes, tags, noteTags } from './schema.js'; +import { and, eq, like, or } from 'drizzle-orm'; +import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3'; +import type * as schema from './schema.js'; + +let db: BetterSQLite3Database; + +beforeEach(async () => { + const testDb = createTestDb(); + db = testDb.db; + + const now = new Date(); + await db.insert(notes).values([ + { id: 'n1', title: 'Shopping List', content: 'Buy milk and eggs', createdAt: now, updatedAt: now }, + { id: 'n2', title: 'Work Notes', content: 'Meeting at 3pm #work', createdAt: now, updatedAt: now }, + { id: 'n3', title: 'Recipe', content: 'Pasta with tomato sauce', createdAt: now, updatedAt: now }, + { id: 'n4', title: 'Trashed', content: 'Should not appear', trashed: true, createdAt: now, updatedAt: now } + ]); + + const [workTag] = await db.insert(tags).values({ name: 'work' }).returning(); + await db.insert(noteTags).values({ noteId: 'n2', tagId: workTag.id }); +}); + +describe('Search', () => { + it('should search by title', async () => { + const pattern = '%shopping%'; + const results = await db + .select() + .from(notes) + .where(and(eq(notes.trashed, false), like(notes.title, pattern))); + + expect(results).toHaveLength(1); + expect(results[0].id).toBe('n1'); + }); + + it('should search by content', async () => { + const pattern = '%tomato%'; + const results = await db + .select() + .from(notes) + .where(and(eq(notes.trashed, false), like(notes.content, pattern))); + + expect(results).toHaveLength(1); + expect(results[0].id).toBe('n3'); + }); + + it('should search across title and content', async () => { + const pattern = '%milk%'; + const results = await db + .select() + .from(notes) + .where( + and( + eq(notes.trashed, false), + or(like(notes.title, pattern), like(notes.content, pattern)) + ) + ); + + expect(results).toHaveLength(1); + expect(results[0].id).toBe('n1'); + }); + + it('should not include trashed notes', async () => { + const pattern = '%should not%'; + const results = await db + .select() + .from(notes) + .where( + and( + eq(notes.trashed, false), + or(like(notes.title, pattern), like(notes.content, pattern)) + ) + ); + + expect(results).toHaveLength(0); + }); + + it('should return empty for no matches', async () => { + const pattern = '%nonexistent%'; + const results = await db + .select() + .from(notes) + .where( + and( + eq(notes.trashed, false), + or(like(notes.title, pattern), like(notes.content, pattern)) + ) + ); + + expect(results).toHaveLength(0); + }); + + it('should be case-insensitive with LIKE', async () => { + const pattern = '%SHOPPING%'; + const results = await db + .select() + .from(notes) + .where(and(eq(notes.trashed, false), like(notes.title, pattern))); + + // SQLite LIKE is case-insensitive for ASCII by default + expect(results).toHaveLength(1); + }); + + it('should search by tag name', async () => { + const tagResults = await db + .select({ noteId: noteTags.noteId }) + .from(noteTags) + .innerJoin(tags, eq(noteTags.tagId, tags.id)) + .where(like(tags.name, '%work%')); + + expect(tagResults).toHaveLength(1); + expect(tagResults[0].noteId).toBe('n2'); + }); +}); diff --git a/src/lib/server/db/test-helpers.ts b/src/lib/server/db/test-helpers.ts new file mode 100644 index 0000000..ab5310c --- /dev/null +++ b/src/lib/server/db/test-helpers.ts @@ -0,0 +1,76 @@ +import Database from 'better-sqlite3'; +import { drizzle } from 'drizzle-orm/better-sqlite3'; +import * as schema from './schema.js'; + +/** + * Create an in-memory SQLite database for testing. + * Each test gets a fresh database. + */ +export function createTestDb() { + const sqlite = new Database(':memory:'); + sqlite.pragma('journal_mode = WAL'); + sqlite.pragma('foreign_keys = ON'); + + // Create tables + sqlite.exec(` + CREATE TABLE users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + password_hash TEXT NOT NULL, + created_at INTEGER NOT NULL + ); + + CREATE TABLE sessions ( + id TEXT PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id), + expires_at INTEGER NOT NULL + ); + + CREATE TABLE notes ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL DEFAULT '', + content TEXT NOT NULL DEFAULT '', + color TEXT NOT NULL DEFAULT 'default', + pinned INTEGER NOT NULL DEFAULT 0, + archived INTEGER NOT NULL DEFAULT 0, + trashed INTEGER NOT NULL DEFAULT 0, + trashed_at INTEGER, + checklist_mode INTEGER NOT NULL DEFAULT 0, + sort_order INTEGER NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + version INTEGER NOT NULL DEFAULT 1 + ); + + CREATE TABLE tags ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL + ); + CREATE UNIQUE INDEX tags_name_unique ON tags(name); + + CREATE TABLE note_tags ( + note_id TEXT NOT NULL REFERENCES notes(id) ON DELETE CASCADE, + tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE + ); + + CREATE TABLE attachments ( + id TEXT PRIMARY KEY, + note_id TEXT NOT NULL REFERENCES notes(id) ON DELETE CASCADE, + filename TEXT NOT NULL, + mime_type TEXT NOT NULL, + size INTEGER NOT NULL, + path TEXT NOT NULL, + created_at INTEGER NOT NULL + ); + + CREATE TABLE sync_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + note_id TEXT NOT NULL REFERENCES notes(id), + operation TEXT NOT NULL, + timestamp INTEGER NOT NULL, + client_id TEXT NOT NULL + ); + `); + + const db = drizzle(sqlite, { schema }); + return { db, sqlite }; +} diff --git a/src/lib/stores/notes.ts b/src/lib/stores/notes.ts new file mode 100644 index 0000000..e8e740b --- /dev/null +++ b/src/lib/stores/notes.ts @@ -0,0 +1,104 @@ +import { writable, derived } from 'svelte/store'; +import type { Note, NoteFilter, NoteCreate, NoteUpdate } from '$lib/types/index.js'; + +export const notes = writable([]); +export const currentFilter = writable('all'); +export const selectedTag = writable(null); + +export const filteredNotes = derived( + [notes, selectedTag, currentFilter], + ([$notes, $selectedTag, $filter]) => { + let result = $notes; + if ($filter === 'all') { + result = result.filter((n) => !n.trashed && !n.archived); + } else if ($filter === 'archived') { + result = result.filter((n) => n.archived && !n.trashed); + } else if ($filter === 'trashed') { + result = result.filter((n) => n.trashed); + } + if ($selectedTag) { + result = result.filter((n) => n.tags?.includes($selectedTag)); + } + return result; + } +); + +export const pinnedNotes = derived(filteredNotes, ($notes) => + $notes.filter((n) => n.pinned) +); + +export const unpinnedNotes = derived(filteredNotes, ($notes) => + $notes.filter((n) => !n.pinned) +); + +export const allTags = derived(notes, ($notes) => { + const tagSet = new Set(); + $notes.forEach((n) => n.tags?.forEach((t) => tagSet.add(t))); + return [...tagSet].sort(); +}); + +export async function loadNotes(filter: NoteFilter = 'all') { + const res = await fetch(`/api/notes?filter=${filter}`); + if (res.ok) { + const data = await res.json(); + notes.set(data); + currentFilter.set(filter); + } +} + +export async function createNote(note: NoteCreate): Promise { + const res = await fetch('/api/notes', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(note) + }); + if (res.ok) { + const created = await res.json(); + notes.update((list) => [created, ...list]); + return created; + } + return null; +} + +export async function updateNote(id: string, updates: NoteUpdate): Promise { + const res = await fetch(`/api/notes/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updates) + }); + if (res.ok) { + const updated = await res.json(); + notes.update((list) => list.map((n) => (n.id === id ? updated : n))); + return updated; + } + return null; +} + +export async function deleteNote(id: string): Promise { + const res = await fetch(`/api/notes/${id}`, { method: 'DELETE' }); + if (res.ok) { + notes.update((list) => list.filter((n) => n.id !== id)); + return true; + } + return false; +} + +export async function trashNote(id: string): Promise { + return updateNote(id, { trashed: true }); +} + +export async function restoreNote(id: string): Promise { + return updateNote(id, { trashed: false }); +} + +export async function archiveNote(id: string): Promise { + return updateNote(id, { archived: true }); +} + +export async function unarchiveNote(id: string): Promise { + return updateNote(id, { archived: false }); +} + +export async function togglePin(id: string, currentPinned: boolean): Promise { + return updateNote(id, { pinned: !currentPinned }); +} diff --git a/src/lib/stores/theme.ts b/src/lib/stores/theme.ts new file mode 100644 index 0000000..1845905 --- /dev/null +++ b/src/lib/stores/theme.ts @@ -0,0 +1,46 @@ +import { writable } from 'svelte/store'; +import { browser } from '$app/environment'; + +type Theme = 'light' | 'dark' | 'system'; + +function getInitialTheme(): Theme { + if (!browser) return 'system'; + return (localStorage.getItem('theme') as Theme) || 'system'; +} + +function getEffectiveTheme(theme: Theme): 'light' | 'dark' { + if (theme !== 'system') return theme; + if (!browser) return 'light'; + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; +} + +export const themePreference = writable(getInitialTheme()); +export const effectiveTheme = writable<'light' | 'dark'>('light'); + +if (browser) { + themePreference.subscribe((theme) => { + localStorage.setItem('theme', theme); + const effective = getEffectiveTheme(theme); + effectiveTheme.set(effective); + document.documentElement.classList.toggle('dark', effective === 'dark'); + }); + + // Listen for system theme changes + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { + themePreference.update((current) => { + if (current === 'system') { + const effective = getEffectiveTheme('system'); + effectiveTheme.set(effective); + document.documentElement.classList.toggle('dark', effective === 'dark'); + } + return current; + }); + }); +} + +export function toggleTheme() { + themePreference.update((current) => { + const effective = getEffectiveTheme(current); + return effective === 'dark' ? 'light' : 'dark'; + }); +} diff --git a/src/lib/sync/client.ts b/src/lib/sync/client.ts new file mode 100644 index 0000000..3213a50 --- /dev/null +++ b/src/lib/sync/client.ts @@ -0,0 +1,128 @@ +import { browser } from '$app/environment'; +import type { Note } from '$lib/types/index.js'; +import { putNote, getAllNotes, addToSyncQueue, getSyncQueue, clearSyncQueue, deleteNoteFromIdb, getMeta, setMeta } from './idb.js'; +import { mergeNotes } from './crdt.js'; + +export type SyncStatus = 'synced' | 'syncing' | 'offline' | 'error'; + +let syncStatus: SyncStatus = 'synced'; +let syncInterval: ReturnType | null = null; +const statusListeners: Set<(status: SyncStatus) => void> = new Set(); + +export function onSyncStatusChange(listener: (status: SyncStatus) => void) { + statusListeners.add(listener); + return () => statusListeners.delete(listener); +} + +function setStatus(status: SyncStatus) { + syncStatus = status; + statusListeners.forEach((l) => l(status)); +} + +export function getSyncStatus(): SyncStatus { + return syncStatus; +} + +/** + * Push local changes to server. + */ +async function pushChanges(): Promise { + const queue = await getSyncQueue(); + if (queue.length === 0) return true; + + try { + const res = await fetch('/api/sync', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ changes: queue }) + }); + + if (res.ok) { + await clearSyncQueue(); + return true; + } + return false; + } catch { + return false; + } +} + +/** + * Pull remote changes and merge with local state. + */ +async function pullChanges(): Promise { + const lastSync = (await getMeta('lastSyncTimestamp')) || '0'; + + try { + const res = await fetch(`/api/sync?since=${lastSync}`); + if (!res.ok) return []; + + const remoteNotes: Note[] = await res.json(); + const localNotes = await getAllNotes(); + const localMap = new Map(localNotes.map((n) => [n.id, n])); + + for (const remote of remoteNotes) { + const local = localMap.get(remote.id); + if (local) { + const merged = mergeNotes(local, remote); + await putNote(merged); + } else { + await putNote(remote); + } + } + + await setMeta('lastSyncTimestamp', String(Date.now())); + return remoteNotes; + } catch { + return []; + } +} + +/** + * Full sync cycle: push then pull. + */ +export async function sync(): Promise { + if (!browser || !navigator.onLine) { + setStatus('offline'); + return; + } + + setStatus('syncing'); + + try { + const pushed = await pushChanges(); + if (!pushed) { + setStatus('error'); + return; + } + + await pullChanges(); + setStatus('synced'); + } catch { + setStatus('error'); + } +} + +/** + * Start background sync (every 30 seconds). + */ +export function startSync() { + if (!browser) return; + + // Initial sync + sync(); + + // Periodic sync + syncInterval = setInterval(sync, 30_000); + + // Sync on online/offline events + window.addEventListener('online', () => sync()); + window.addEventListener('offline', () => setStatus('offline')); +} + +export function stopSync() { + if (syncInterval) { + clearInterval(syncInterval); + syncInterval = null; + } +} diff --git a/src/lib/sync/crdt.ts b/src/lib/sync/crdt.ts new file mode 100644 index 0000000..f295df4 --- /dev/null +++ b/src/lib/sync/crdt.ts @@ -0,0 +1,64 @@ +import type { Note } from '$lib/types/index.js'; + +/** + * Last-Write-Wins (LWW) merge strategy per field. + * For a single-user app with multiple devices, this is sufficient. + * Each field is resolved by comparing updatedAt timestamps. + */ +export function mergeNotes(local: Note, remote: Note): Note { + // If remote is newer overall, take remote + const localTime = new Date(local.updatedAt).getTime(); + const remoteTime = new Date(remote.updatedAt).getTime(); + + if (remoteTime > localTime) { + return { ...remote }; + } + + if (localTime > remoteTime) { + return { ...local }; + } + + // Same timestamp - prefer higher version number + if (remote.version > local.version) { + return { ...remote }; + } + + return { ...local }; +} + +/** + * Determine if a local note has changes compared to a remote note. + */ +export function hasChanges(local: Note, remote: Note): boolean { + return ( + local.title !== remote.title || + local.content !== remote.content || + local.color !== remote.color || + local.pinned !== remote.pinned || + local.archived !== remote.archived || + local.trashed !== remote.trashed || + local.checklistMode !== remote.checklistMode || + local.sortOrder !== remote.sortOrder + ); +} + +/** + * Generate a diff of changed fields between two notes. + */ +export function diffNotes( + oldNote: Note, + newNote: Note +): Partial { + const diff: Partial = {}; + + if (oldNote.title !== newNote.title) diff.title = newNote.title; + if (oldNote.content !== newNote.content) diff.content = newNote.content; + if (oldNote.color !== newNote.color) diff.color = newNote.color; + if (oldNote.pinned !== newNote.pinned) diff.pinned = newNote.pinned; + if (oldNote.archived !== newNote.archived) diff.archived = newNote.archived; + if (oldNote.trashed !== newNote.trashed) diff.trashed = newNote.trashed; + if (oldNote.checklistMode !== newNote.checklistMode) diff.checklistMode = newNote.checklistMode; + if (oldNote.sortOrder !== newNote.sortOrder) diff.sortOrder = newNote.sortOrder; + + return diff; +} diff --git a/src/lib/sync/idb.ts b/src/lib/sync/idb.ts new file mode 100644 index 0000000..c79d0f6 --- /dev/null +++ b/src/lib/sync/idb.ts @@ -0,0 +1,124 @@ +import { openDB, type IDBPDatabase } from 'idb'; +import type { Note } from '$lib/types/index.js'; + +const DB_NAME = 'crumbs'; +const DB_VERSION = 1; + +interface CrumbsDB { + notes: { + key: string; + value: Note; + indexes: { + 'by-updated': Date; + }; + }; + syncQueue: { + key: number; + value: SyncQueueItem; + indexes: { + 'by-timestamp': number; + }; + }; + meta: { + key: string; + value: { key: string; value: string }; + }; +} + +export interface SyncQueueItem { + id?: number; + noteId: string; + operation: 'create' | 'update' | 'delete'; + data?: Partial; + timestamp: number; +} + +let dbPromise: Promise> | null = null; + +export function getDb(): Promise> { + if (!dbPromise) { + dbPromise = openDB(DB_NAME, DB_VERSION, { + upgrade(db) { + // Notes store + if (!db.objectStoreNames.contains('notes')) { + const noteStore = db.createObjectStore('notes', { keyPath: 'id' }); + noteStore.createIndex('by-updated', 'updatedAt'); + } + + // Sync queue + if (!db.objectStoreNames.contains('syncQueue')) { + const syncStore = db.createObjectStore('syncQueue', { + keyPath: 'id', + autoIncrement: true + }); + syncStore.createIndex('by-timestamp', 'timestamp'); + } + + // Metadata (e.g., last sync timestamp, client ID) + if (!db.objectStoreNames.contains('meta')) { + db.createObjectStore('meta', { keyPath: 'key' }); + } + } + }); + } + return dbPromise; +} + +// Notes operations +export async function getAllNotes(): Promise { + const db = await getDb(); + return db.getAll('notes'); +} + +export async function getNote(id: string): Promise { + const db = await getDb(); + return db.get('notes', id); +} + +export async function putNote(note: Note): Promise { + const db = await getDb(); + await db.put('notes', note); +} + +export async function deleteNoteFromIdb(id: string): Promise { + const db = await getDb(); + await db.delete('notes', id); +} + +export async function clearNotes(): Promise { + const db = await getDb(); + await db.clear('notes'); +} + +// Sync queue operations +export async function addToSyncQueue(item: Omit): Promise { + const db = await getDb(); + await db.add('syncQueue', item as SyncQueueItem); +} + +export async function getSyncQueue(): Promise { + const db = await getDb(); + return db.getAll('syncQueue'); +} + +export async function clearSyncQueue(): Promise { + const db = await getDb(); + await db.clear('syncQueue'); +} + +export async function removeSyncQueueItem(id: number): Promise { + const db = await getDb(); + await db.delete('syncQueue', id); +} + +// Meta operations +export async function getMeta(key: string): Promise { + const db = await getDb(); + const result = await db.get('meta', key); + return result?.value; +} + +export async function setMeta(key: string, value: string): Promise { + const db = await getDb(); + await db.put('meta', { key, value }); +} diff --git a/src/lib/sync/server.ts b/src/lib/sync/server.ts new file mode 100644 index 0000000..ceb41f7 --- /dev/null +++ b/src/lib/sync/server.ts @@ -0,0 +1,71 @@ +import { db } from '$lib/server/db/index.js'; +import { notes, syncLog } from '$lib/server/db/schema.js'; +import { eq, gt } from 'drizzle-orm'; +import type { SyncQueueItem } from './idb.js'; + +/** + * Process incoming sync changes from client. + */ +export async function processSyncPush(changes: SyncQueueItem[]): Promise { + for (const change of changes) { + switch (change.operation) { + case 'create': + case 'update': { + if (!change.data) continue; + const existing = await db.select().from(notes).where(eq(notes.id, change.noteId)).get(); + + if (existing) { + // LWW: only update if incoming timestamp is newer + if (change.timestamp > existing.updatedAt.getTime()) { + await db + .update(notes) + .set({ + ...change.data, + updatedAt: new Date(change.timestamp), + version: existing.version + 1 + }) + .where(eq(notes.id, change.noteId)); + } + } else if (change.operation === 'create' && change.data) { + await db.insert(notes).values({ + id: change.noteId, + title: change.data.title || '', + content: change.data.content || '', + color: change.data.color || 'default', + pinned: change.data.pinned || false, + archived: change.data.archived || false, + trashed: change.data.trashed || false, + checklistMode: change.data.checklistMode || false, + sortOrder: change.data.sortOrder || 0, + createdAt: new Date(change.timestamp), + updatedAt: new Date(change.timestamp), + version: 1 + }); + } + break; + } + case 'delete': { + await db.delete(notes).where(eq(notes.id, change.noteId)); + break; + } + } + + // Log sync operation + await db.insert(syncLog).values({ + noteId: change.noteId, + operation: change.operation, + timestamp: new Date(change.timestamp), + clientId: 'default' + }); + } +} + +/** + * Get all notes updated since a given timestamp. + */ +export async function getChangesSince(sinceTimestamp: number) { + const since = new Date(sinceTimestamp); + const allNotes = await db.select().from(notes); + // Filter notes updated after the timestamp + return allNotes.filter((n) => n.updatedAt > since); +} diff --git a/src/lib/sync/sync.test.ts b/src/lib/sync/sync.test.ts new file mode 100644 index 0000000..32cb59e --- /dev/null +++ b/src/lib/sync/sync.test.ts @@ -0,0 +1,119 @@ +import { describe, it, expect } from 'vitest'; +import { mergeNotes, hasChanges, diffNotes } from './crdt.js'; +import type { Note } from '../types/index.js'; + +function makeNote(overrides: Partial = {}): Note { + return { + id: 'test-id', + title: 'Test', + content: 'Content', + color: 'default', + pinned: false, + archived: false, + trashed: false, + trashedAt: null, + checklistMode: false, + sortOrder: 0, + createdAt: new Date('2024-01-01'), + updatedAt: new Date('2024-01-01'), + version: 1, + ...overrides + }; +} + +describe('mergeNotes (LWW)', () => { + it('should prefer remote when remote is newer', () => { + const local = makeNote({ title: 'Local', updatedAt: new Date('2024-01-01') }); + const remote = makeNote({ title: 'Remote', updatedAt: new Date('2024-01-02') }); + + const result = mergeNotes(local, remote); + expect(result.title).toBe('Remote'); + }); + + it('should prefer local when local is newer', () => { + const local = makeNote({ title: 'Local', updatedAt: new Date('2024-01-02') }); + const remote = makeNote({ title: 'Remote', updatedAt: new Date('2024-01-01') }); + + const result = mergeNotes(local, remote); + expect(result.title).toBe('Local'); + }); + + it('should prefer higher version when timestamps are equal', () => { + const ts = new Date('2024-01-01'); + const local = makeNote({ title: 'Local', updatedAt: ts, version: 1 }); + const remote = makeNote({ title: 'Remote', updatedAt: ts, version: 2 }); + + const result = mergeNotes(local, remote); + expect(result.title).toBe('Remote'); + }); + + it('should prefer local when timestamps and versions are equal', () => { + const ts = new Date('2024-01-01'); + const local = makeNote({ title: 'Local', updatedAt: ts, version: 1 }); + const remote = makeNote({ title: 'Remote', updatedAt: ts, version: 1 }); + + const result = mergeNotes(local, remote); + expect(result.title).toBe('Local'); + }); +}); + +describe('hasChanges', () => { + it('should detect title change', () => { + const a = makeNote({ title: 'A' }); + const b = makeNote({ title: 'B' }); + expect(hasChanges(a, b)).toBe(true); + }); + + it('should detect content change', () => { + const a = makeNote({ content: 'old' }); + const b = makeNote({ content: 'new' }); + expect(hasChanges(a, b)).toBe(true); + }); + + it('should detect color change', () => { + const a = makeNote({ color: 'default' }); + const b = makeNote({ color: 'coral' }); + expect(hasChanges(a, b)).toBe(true); + }); + + it('should detect pinned change', () => { + const a = makeNote({ pinned: false }); + const b = makeNote({ pinned: true }); + expect(hasChanges(a, b)).toBe(true); + }); + + it('should return false when notes are identical', () => { + const a = makeNote(); + const b = makeNote(); + expect(hasChanges(a, b)).toBe(false); + }); +}); + +describe('diffNotes', () => { + it('should return only changed fields', () => { + const old = makeNote({ title: 'Old', content: 'Same' }); + const updated = makeNote({ title: 'New', content: 'Same' }); + + const diff = diffNotes(old, updated); + expect(diff.title).toBe('New'); + expect(diff.content).toBeUndefined(); + }); + + it('should return empty object when no changes', () => { + const a = makeNote(); + const b = makeNote(); + + const diff = diffNotes(a, b); + expect(Object.keys(diff)).toHaveLength(0); + }); + + it('should track multiple changed fields', () => { + const old = makeNote({ title: 'Old', color: 'default', pinned: false }); + const updated = makeNote({ title: 'New', color: 'coral', pinned: true }); + + const diff = diffNotes(old, updated); + expect(diff.title).toBe('New'); + expect(diff.color).toBe('coral'); + expect(diff.pinned).toBe(true); + }); +}); diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts new file mode 100644 index 0000000..9f54a66 --- /dev/null +++ b/src/lib/types/index.ts @@ -0,0 +1,77 @@ +export interface Note { + id: string; + title: string; + content: string; + color: NoteColor; + pinned: boolean; + archived: boolean; + trashed: boolean; + trashedAt: Date | null; + checklistMode: boolean; + sortOrder: number; + createdAt: Date; + updatedAt: Date; + version: number; + tags?: string[]; + attachments?: Attachment[]; +} + +export interface NoteCreate { + title?: string; + content?: string; + color?: NoteColor; + pinned?: boolean; + checklistMode?: boolean; +} + +export interface NoteUpdate { + title?: string; + content?: string; + color?: NoteColor; + pinned?: boolean; + archived?: boolean; + trashed?: boolean; + checklistMode?: boolean; + sortOrder?: number; +} + +export interface Attachment { + id: string; + noteId: string; + filename: string; + mimeType: string; + size: number; + path: string; + createdAt: Date; +} + +export interface Tag { + id: number; + name: string; +} + +export interface SyncChange { + noteId: string; + operation: 'create' | 'update' | 'delete'; + timestamp: Date; + clientId: string; + data?: Partial; +} + +export type NoteColor = + | 'default' + | 'coral' + | 'peach' + | 'sand' + | 'mint' + | 'sage' + | 'fog' + | 'storm' + | 'dusk' + | 'blossom' + | 'clay' + | 'chalk'; + +export type NoteFilter = 'all' | 'archived' | 'trashed'; + +export type SyncStatus = 'synced' | 'syncing' | 'offline' | 'error'; diff --git a/src/lib/utils/colors.ts b/src/lib/utils/colors.ts new file mode 100644 index 0000000..5511a79 --- /dev/null +++ b/src/lib/utils/colors.ts @@ -0,0 +1,21 @@ +import type { NoteColor } from '$lib/types/index.js'; + +export const NOTE_COLORS: Record = { + default: { light: '#ffffff', dark: '#202124', label: 'Default' }, + coral: { light: '#faafa8', dark: '#77172e', label: 'Coral' }, + peach: { light: '#f39f76', dark: '#692b17', label: 'Peach' }, + sand: { light: '#fff8b8', dark: '#7c4a03', label: 'Sand' }, + mint: { light: '#e2f6d3', dark: '#264d3b', label: 'Mint' }, + sage: { light: '#b4ddd3', dark: '#0c625d', label: 'Sage' }, + fog: { light: '#d4e4ed', dark: '#256377', label: 'Fog' }, + storm: { light: '#aeccdc', dark: '#284255', label: 'Storm' }, + dusk: { light: '#d3bfdb', dark: '#472e5b', label: 'Dusk' }, + blossom: { light: '#f6e2dd', dark: '#6c394f', label: 'Blossom' }, + clay: { light: '#e9e3d4', dark: '#4b443a', label: 'Clay' }, + chalk: { light: '#efeff1', dark: '#232427', label: 'Chalk' } +}; + +export const COLOR_OPTIONS = Object.entries(NOTE_COLORS).map(([value, { label }]) => ({ + value: value as NoteColor, + label +})); diff --git a/src/lib/utils/debounce.ts b/src/lib/utils/debounce.ts new file mode 100644 index 0000000..4752674 --- /dev/null +++ b/src/lib/utils/debounce.ts @@ -0,0 +1,7 @@ +export function debounce unknown>(fn: T, ms: number): T { + let timeoutId: ReturnType; + return ((...args: unknown[]) => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => fn(...args), ms); + }) as T; +} diff --git a/src/lib/utils/markdown.test.ts b/src/lib/utils/markdown.test.ts new file mode 100644 index 0000000..00efdbb --- /dev/null +++ b/src/lib/utils/markdown.test.ts @@ -0,0 +1,87 @@ +import { describe, it, expect } from 'vitest'; +import { renderMarkdown, stripMarkdown } from './markdown.js'; + +describe('renderMarkdown', () => { + it('should render bold text', () => { + const html = renderMarkdown('**bold**'); + expect(html).toContain('bold'); + }); + + it('should render italic text', () => { + const html = renderMarkdown('*italic*'); + expect(html).toContain('italic'); + }); + + it('should render headings', () => { + const html = renderMarkdown('# Heading 1'); + expect(html).toContain('

Heading 1

'); + }); + + it('should render unordered lists', () => { + const html = renderMarkdown('- item 1\n- item 2'); + expect(html).toContain('
  • item 1
  • '); + expect(html).toContain('
  • item 2
  • '); + }); + + it('should render ordered lists', () => { + const html = renderMarkdown('1. first\n2. second'); + expect(html).toContain('
  • first
  • '); + expect(html).toContain('
  • second
  • '); + }); + + it('should render code blocks', () => { + const html = renderMarkdown('```\nconst x = 1;\n```'); + expect(html).toContain(''); + expect(html).toContain('const x = 1;'); + }); + + it('should render inline code', () => { + const html = renderMarkdown('Use `npm install`'); + expect(html).toContain('npm install'); + }); + + it('should render links', () => { + const html = renderMarkdown('[Google](https://google.com)'); + expect(html).toContain('href="https://google.com"'); + expect(html).toContain('Google'); + }); + + it('should render tables', () => { + const md = '| A | B |\n|---|---|\n| 1 | 2 |'; + const html = renderMarkdown(md); + expect(html).toContain(''); + expect(html).toContain(''); + }); + + it('should auto-linkify URLs', () => { + const html = renderMarkdown('Visit https://example.com'); + expect(html).toContain('href="https://example.com"'); + }); + + it('should convert line breaks', () => { + const html = renderMarkdown('line 1\nline 2'); + expect(html).toContain('
    '); + }); +}); + +describe('stripMarkdown', () => { + it('should remove markdown formatting', () => { + expect(stripMarkdown('**bold** and *italic*')).toBe('bold and italic'); + }); + + it('should remove code blocks', () => { + expect(stripMarkdown('```\ncode\n```')).toBe(''); + }); + + it('should remove inline code backticks', () => { + expect(stripMarkdown('Use `command`')).toBe('Use'); + }); + + it('should extract link text', () => { + expect(stripMarkdown('[Click here](https://example.com)')).toBe('Click here'); + }); + + it('should return empty for empty input', () => { + expect(stripMarkdown('')).toBe(''); + }); +}); diff --git a/src/lib/utils/markdown.ts b/src/lib/utils/markdown.ts new file mode 100644 index 0000000..c3fb92a --- /dev/null +++ b/src/lib/utils/markdown.ts @@ -0,0 +1,44 @@ +import MarkdownIt from 'markdown-it'; + +const md = new MarkdownIt({ + html: false, + linkify: true, + typographer: true, + breaks: true +}); + +// Add task list support +md.core.ruler.after('inline', 'task-lists', (state) => { + const tokens = state.tokens; + for (let i = 0; i < tokens.length; i++) { + if (tokens[i].type !== 'inline') continue; + const content = tokens[i].content; + if (content.startsWith('[ ] ') || content.startsWith('[x] ')) { + const checked = content.startsWith('[x] '); + const checkbox = checked + ? ' ' + : ' '; + tokens[i].content = content.slice(4); + tokens[i].children = md.parseInline(tokens[i].content, state.env)[0].children; + // Prepend checkbox + const token = new state.Token('html_inline', '', 0); + token.content = checkbox; + tokens[i].children!.unshift(token); + } + } +}); + +export function renderMarkdown(content: string): string { + return md.render(content); +} + +export function stripMarkdown(content: string): string { + // Simple strip for preview - remove markdown syntax + return content + .replace(/```[\s\S]*?```/g, '') + .replace(/`[^`]*`/g, '') + .replace(/!\[.*?\]\(.*?\)/g, '') + .replace(/\[([^\]]*)\]\(.*?\)/g, '$1') + .replace(/[#*_~\[\]]/g, '') + .trim(); +} diff --git a/src/lib/utils/tags.test.ts b/src/lib/utils/tags.test.ts new file mode 100644 index 0000000..a4cd371 --- /dev/null +++ b/src/lib/utils/tags.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect } from 'vitest'; +import { extractTags } from './tags.js'; + +describe('extractTags', () => { + it('should extract simple hashtags', () => { + expect(extractTags('Hello #world')).toEqual(['world']); + }); + + it('should extract multiple hashtags', () => { + expect(extractTags('#work #personal #todo')).toEqual(['work', 'personal', 'todo']); + }); + + it('should deduplicate tags', () => { + expect(extractTags('#work some text #work')).toEqual(['work']); + }); + + it('should lowercase tags', () => { + expect(extractTags('#Work #IMPORTANT')).toEqual(['work', 'important']); + }); + + it('should handle tags with hyphens', () => { + expect(extractTags('#my-project')).toEqual(['my-project']); + }); + + it('should handle tags with underscores', () => { + expect(extractTags('#my_project')).toEqual(['my_project']); + }); + + it('should return empty array for no tags', () => { + expect(extractTags('No hashtags here')).toEqual([]); + }); + + it('should return empty array for empty string', () => { + expect(extractTags('')).toEqual([]); + }); + + it('should not match hashtags inside code blocks', () => { + expect(extractTags('```\n#not-a-tag\n```')).toEqual([]); + }); + + it('should not match hashtags inside inline code', () => { + expect(extractTags('Use `#channel` for that')).toEqual([]); + }); + + it('should not match hashtags in URLs', () => { + expect(extractTags('Visit https://example.com#section')).toEqual([]); + }); + + it('should handle tags at start of line', () => { + expect(extractTags('#first tag')).toEqual(['first']); + }); + + it('should handle mixed content with code and tags', () => { + const content = 'My #project uses `#define` and has a #deadline'; + expect(extractTags(content)).toEqual(['project', 'deadline']); + }); +}); diff --git a/src/lib/utils/tags.ts b/src/lib/utils/tags.ts new file mode 100644 index 0000000..7551e06 --- /dev/null +++ b/src/lib/utils/tags.ts @@ -0,0 +1,21 @@ +/** + * Extract #hashtags from text content. + * Matches hashtags that start with # followed by word characters. + * Does not match inside code blocks or URLs. + */ +export function extractTags(content: string): string[] { + if (!content) return []; + + // Remove code blocks (``` ... ```) to avoid matching inside them + const withoutCodeBlocks = content.replace(/```[\s\S]*?```/g, ''); + // Remove inline code (` ... `) + const withoutInlineCode = withoutCodeBlocks.replace(/`[^`]*`/g, ''); + // Remove URLs + const withoutUrls = withoutInlineCode.replace(/https?:\/\/\S+/g, ''); + + const matches = withoutUrls.match(/(?:^|\s)#([\w-]+)/g); + if (!matches) return []; + + const tags = matches.map((m) => m.trim().slice(1).toLowerCase()); + return [...new Set(tags)]; +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..8abc826 --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,29 @@ + + +
    +
    (sidebarOpen = !sidebarOpen)} /> + + +
    +
    + {@render children()} +
    +
    + + +
    diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte new file mode 100644 index 0000000..1e6ee0e --- /dev/null +++ b/src/routes/+page.svelte @@ -0,0 +1,84 @@ + + + + Crumbs + + +{#if $currentFilter === 'all'} +
    + +
    +{/if} + +
    + +
    + +{#if $currentFilter === 'archived'} +

    Archive

    +{:else if $currentFilter === 'trashed'} +

    Trash

    +{/if} + +{#if $pinnedNotes.length > 0} +
    + +
    +{/if} + + 0 ? 'Others' : ''} + onEdit={openEditor} +/> + +{#if $pinnedNotes.length === 0 && $unpinnedNotes.length === 0} +
    + + + +

    + {#if $currentFilter === 'trashed'} + No notes in trash + {:else if $currentFilter === 'archived'} + No archived notes + {:else if $selectedTag} + No notes with tag #{$selectedTag} + {:else} + Notes you add appear here + {/if} +

    +
    +{/if} + +{#if showNewNote} + +{/if} + +{#if editingNote} + +{/if} diff --git a/src/routes/api/auth/login/+server.ts b/src/routes/api/auth/login/+server.ts new file mode 100644 index 0000000..5c2147b --- /dev/null +++ b/src/routes/api/auth/login/+server.ts @@ -0,0 +1,26 @@ +import { json, error } from '@sveltejs/kit'; +import type { RequestHandler } from './$types.js'; +import { verifyPassword, createSession } from '$lib/server/auth.js'; + +export const POST: RequestHandler = async ({ request, cookies }) => { + const { password } = await request.json(); + + if (!password) { + throw error(400, 'Password is required'); + } + + const valid = await verifyPassword(password); + if (!valid) { + throw error(401, 'Invalid password'); + } + + const token = await createSession(); + cookies.set('session', token, { + path: '/', + httpOnly: true, + sameSite: 'lax', + maxAge: 30 * 24 * 60 * 60 // 30 days + }); + + return json({ success: true }); +}; diff --git a/src/routes/api/auth/logout/+server.ts b/src/routes/api/auth/logout/+server.ts new file mode 100644 index 0000000..994ef1c --- /dev/null +++ b/src/routes/api/auth/logout/+server.ts @@ -0,0 +1,12 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types.js'; +import { deleteSession } from '$lib/server/auth.js'; + +export const POST: RequestHandler = async ({ cookies }) => { + const token = cookies.get('session'); + if (token) { + await deleteSession(token); + } + cookies.delete('session', { path: '/' }); + return json({ success: true }); +}; diff --git a/src/routes/api/auth/setup/+server.ts b/src/routes/api/auth/setup/+server.ts new file mode 100644 index 0000000..244b006 --- /dev/null +++ b/src/routes/api/auth/setup/+server.ts @@ -0,0 +1,31 @@ +import { json, error } from '@sveltejs/kit'; +import type { RequestHandler } from './$types.js'; +import { setupUser, createSession, isSetupComplete } from '$lib/server/auth.js'; + +export const POST: RequestHandler = async ({ request, cookies }) => { + const alreadySetup = await isSetupComplete(); + if (alreadySetup) { + throw error(400, 'Setup already completed'); + } + + const { password } = await request.json(); + + if (!password || password.length < 8) { + throw error(400, 'Password must be at least 8 characters'); + } + + const success = await setupUser(password); + if (!success) { + throw error(500, 'Failed to create user'); + } + + const token = await createSession(); + cookies.set('session', token, { + path: '/', + httpOnly: true, + sameSite: 'lax', + maxAge: 30 * 24 * 60 * 60 + }); + + return json({ success: true }, { status: 201 }); +}; diff --git a/src/routes/api/notes/+server.ts b/src/routes/api/notes/+server.ts new file mode 100644 index 0000000..8d9a68b --- /dev/null +++ b/src/routes/api/notes/+server.ts @@ -0,0 +1,89 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types.js'; +import { db } from '$lib/server/db/index.js'; +import { notes, noteTags, tags } from '$lib/server/db/schema.js'; +import { eq, and, desc } from 'drizzle-orm'; +import { v4 as uuidv4 } from 'uuid'; +import { extractTags } from '$lib/utils/tags.js'; + +export const GET: RequestHandler = async ({ url }) => { + const filter = url.searchParams.get('filter') || 'all'; + + let conditions; + switch (filter) { + case 'archived': + conditions = and(eq(notes.archived, true), eq(notes.trashed, false)); + break; + case 'trashed': + conditions = eq(notes.trashed, true); + break; + default: + conditions = and(eq(notes.archived, false), eq(notes.trashed, false)); + } + + const result = await db + .select() + .from(notes) + .where(conditions) + .orderBy(desc(notes.pinned), desc(notes.updatedAt)); + + // Fetch tags for each note + const notesWithTags = await Promise.all( + result.map(async (note) => { + const tagRows = await db + .select({ name: tags.name }) + .from(noteTags) + .innerJoin(tags, eq(noteTags.tagId, tags.id)) + .where(eq(noteTags.noteId, note.id)); + return { ...note, tags: tagRows.map((t) => t.name) }; + }) + ); + + return json(notesWithTags); +}; + +export const POST: RequestHandler = async ({ request }) => { + const body = await request.json(); + const now = new Date(); + const id = body.id || uuidv4(); + + const newNote = { + id, + title: body.title || '', + content: body.content || '', + color: body.color || 'default', + pinned: body.pinned || false, + archived: false, + trashed: false, + trashedAt: null, + checklistMode: body.checklistMode || false, + sortOrder: body.sortOrder || 0, + createdAt: now, + updatedAt: now, + version: 1 + }; + + await db.insert(notes).values(newNote); + + // Extract and save tags + const content = `${newNote.title} ${newNote.content}`; + const extractedTags = extractTags(content); + await syncNoteTags(id, extractedTags); + + return json({ ...newNote, tags: extractedTags }, { status: 201 }); +}; + +async function syncNoteTags(noteId: string, tagNames: string[]) { + // Remove existing tag associations + await db.delete(noteTags).where(eq(noteTags.noteId, noteId)); + + for (const name of tagNames) { + // Upsert tag + let tagRow = await db.select().from(tags).where(eq(tags.name, name)).get(); + if (!tagRow) { + const result = await db.insert(tags).values({ name }).returning(); + tagRow = result[0]; + } + await db.insert(noteTags).values({ noteId, tagId: tagRow.id }); + } +} diff --git a/src/routes/api/notes/[id]/+server.ts b/src/routes/api/notes/[id]/+server.ts new file mode 100644 index 0000000..945be80 --- /dev/null +++ b/src/routes/api/notes/[id]/+server.ts @@ -0,0 +1,92 @@ +import { json, error } from '@sveltejs/kit'; +import type { RequestHandler } from './$types.js'; +import { db } from '$lib/server/db/index.js'; +import { notes, noteTags, tags } from '$lib/server/db/schema.js'; +import { eq } from 'drizzle-orm'; +import { extractTags } from '$lib/utils/tags.js'; + +export const GET: RequestHandler = async ({ params }) => { + const note = await db.select().from(notes).where(eq(notes.id, params.id)).get(); + + if (!note) { + throw error(404, 'Note not found'); + } + + const tagRows = await db + .select({ name: tags.name }) + .from(noteTags) + .innerJoin(tags, eq(noteTags.tagId, tags.id)) + .where(eq(noteTags.noteId, note.id)); + + return json({ ...note, tags: tagRows.map((t) => t.name) }); +}; + +export const PATCH: RequestHandler = async ({ params, request }) => { + const body = await request.json(); + const existing = await db.select().from(notes).where(eq(notes.id, params.id)).get(); + + if (!existing) { + throw error(404, 'Note not found'); + } + + const now = new Date(); + const updates: Record = { + updatedAt: now, + version: existing.version + 1 + }; + + if (body.title !== undefined) updates.title = body.title; + if (body.content !== undefined) updates.content = body.content; + if (body.color !== undefined) updates.color = body.color; + if (body.pinned !== undefined) updates.pinned = body.pinned; + if (body.archived !== undefined) updates.archived = body.archived; + if (body.trashed !== undefined) { + updates.trashed = body.trashed; + updates.trashedAt = body.trashed ? now : null; + } + if (body.checklistMode !== undefined) updates.checklistMode = body.checklistMode; + if (body.sortOrder !== undefined) updates.sortOrder = body.sortOrder; + + await db.update(notes).set(updates).where(eq(notes.id, params.id)); + + const updated = await db.select().from(notes).where(eq(notes.id, params.id)).get(); + + // Re-sync tags if content or title changed + if (body.title !== undefined || body.content !== undefined) { + const content = `${updated!.title} ${updated!.content}`; + const extractedTags = extractTags(content); + await syncNoteTags(params.id, extractedTags); + } + + const tagRows = await db + .select({ name: tags.name }) + .from(noteTags) + .innerJoin(tags, eq(noteTags.tagId, tags.id)) + .where(eq(noteTags.noteId, params.id)); + + return json({ ...updated, tags: tagRows.map((t) => t.name) }); +}; + +export const DELETE: RequestHandler = async ({ params }) => { + const existing = await db.select().from(notes).where(eq(notes.id, params.id)).get(); + + if (!existing) { + throw error(404, 'Note not found'); + } + + await db.delete(notes).where(eq(notes.id, params.id)); + return json({ success: true }); +}; + +async function syncNoteTags(noteId: string, tagNames: string[]) { + await db.delete(noteTags).where(eq(noteTags.noteId, noteId)); + + for (const name of tagNames) { + let tagRow = await db.select().from(tags).where(eq(tags.name, name)).get(); + if (!tagRow) { + const result = await db.insert(tags).values({ name }).returning(); + tagRow = result[0]; + } + await db.insert(noteTags).values({ noteId, tagId: tagRow.id }); + } +} diff --git a/src/routes/api/notes/[id]/attachments/+server.ts b/src/routes/api/notes/[id]/attachments/+server.ts new file mode 100644 index 0000000..1c05c70 --- /dev/null +++ b/src/routes/api/notes/[id]/attachments/+server.ts @@ -0,0 +1,53 @@ +import { json, error } from '@sveltejs/kit'; +import type { RequestHandler } from './$types.js'; +import { saveAttachment, getAttachmentsByNote, getAttachment, deleteAttachment } from '$lib/server/attachments.js'; +import { readFile } from 'fs/promises'; + +export const GET: RequestHandler = async ({ params, url }) => { + const attachmentId = url.searchParams.get('attachmentId'); + + if (attachmentId) { + const attachment = await getAttachment(attachmentId); + if (!attachment) throw error(404, 'Attachment not found'); + + const buffer = await readFile(attachment.path); + return new Response(buffer, { + headers: { + 'Content-Type': attachment.mimeType, + 'Content-Disposition': `inline; filename="${attachment.filename}"` + } + }); + } + + const list = await getAttachmentsByNote(params.id); + return json(list); +}; + +export const POST: RequestHandler = async ({ params, request }) => { + const formData = await request.formData(); + const file = formData.get('file') as File; + + if (!file) { + throw error(400, 'No file provided'); + } + + if (!file.type.startsWith('image/')) { + throw error(400, 'Only image files are allowed'); + } + + const maxSize = 10 * 1024 * 1024; // 10MB + if (file.size > maxSize) { + throw error(400, 'File too large (max 10MB)'); + } + + const attachment = await saveAttachment(params.id, file); + return json(attachment, { status: 201 }); +}; + +export const DELETE: RequestHandler = async ({ url }) => { + const attachmentId = url.searchParams.get('attachmentId'); + if (!attachmentId) throw error(400, 'attachmentId required'); + + await deleteAttachment(attachmentId); + return json({ success: true }); +}; diff --git a/src/routes/api/search/+server.ts b/src/routes/api/search/+server.ts new file mode 100644 index 0000000..19f5518 --- /dev/null +++ b/src/routes/api/search/+server.ts @@ -0,0 +1,65 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types.js'; +import { db } from '$lib/server/db/index.js'; +import { notes, noteTags, tags } from '$lib/server/db/schema.js'; +import { and, eq, like, or } from 'drizzle-orm'; + +export const GET: RequestHandler = async ({ url }) => { + const query = url.searchParams.get('q')?.trim(); + + if (!query) { + return json([]); + } + + const pattern = `%${query}%`; + + const results = await db + .select() + .from(notes) + .where( + and( + eq(notes.trashed, false), + or(like(notes.title, pattern), like(notes.content, pattern)) + ) + ); + + // Also search by tag name + const tagResults = await db + .select({ noteId: noteTags.noteId }) + .from(noteTags) + .innerJoin(tags, eq(noteTags.tagId, tags.id)) + .where(like(tags.name, pattern)); + + const tagNoteIds = new Set(tagResults.map((r) => r.noteId)); + const allNoteIds = new Set(results.map((n) => n.id)); + + // Fetch tag-matched notes not already in results + const extraNoteIds = [...tagNoteIds].filter((id) => !allNoteIds.has(id)); + let extraNotes: typeof results = []; + if (extraNoteIds.length > 0) { + for (const id of extraNoteIds) { + const note = await db + .select() + .from(notes) + .where(and(eq(notes.id, id), eq(notes.trashed, false))) + .get(); + if (note) extraNotes.push(note); + } + } + + const combined = [...results, ...extraNotes]; + + // Attach tags to each note + const withTags = await Promise.all( + combined.map(async (note) => { + const tagRows = await db + .select({ name: tags.name }) + .from(noteTags) + .innerJoin(tags, eq(noteTags.tagId, tags.id)) + .where(eq(noteTags.noteId, note.id)); + return { ...note, tags: tagRows.map((t) => t.name) }; + }) + ); + + return json(withTags); +}; diff --git a/src/routes/api/sync/+server.ts b/src/routes/api/sync/+server.ts new file mode 100644 index 0000000..b5681ce --- /dev/null +++ b/src/routes/api/sync/+server.ts @@ -0,0 +1,20 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types.js'; +import { processSyncPush, getChangesSince } from '$lib/sync/server.js'; + +export const POST: RequestHandler = async ({ request }) => { + const { changes } = await request.json(); + + if (!Array.isArray(changes)) { + return json({ error: 'Invalid changes' }, { status: 400 }); + } + + await processSyncPush(changes); + return json({ success: true }); +}; + +export const GET: RequestHandler = async ({ url }) => { + const since = parseInt(url.searchParams.get('since') || '0', 10); + const changes = await getChangesSince(since); + return json(changes); +}; diff --git a/src/routes/api/tags/+server.ts b/src/routes/api/tags/+server.ts new file mode 100644 index 0000000..9859b5e --- /dev/null +++ b/src/routes/api/tags/+server.ts @@ -0,0 +1,9 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types.js'; +import { db } from '$lib/server/db/index.js'; +import { tags } from '$lib/server/db/schema.js'; + +export const GET: RequestHandler = async () => { + const allTags = await db.select().from(tags); + return json(allTags); +}; diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte new file mode 100644 index 0000000..b5e3bd5 --- /dev/null +++ b/src/routes/login/+page.svelte @@ -0,0 +1,72 @@ + + + + Login - Crumbs + + +
    +
    +
    + 🥨 +

    Crumbs

    +

    Enter your password to continue

    +
    + +
    + {#if errorMsg} +
    + {errorMsg} +
    + {/if} + + + + + +
    +
    diff --git a/src/routes/setup/+page.svelte b/src/routes/setup/+page.svelte new file mode 100644 index 0000000..875443b --- /dev/null +++ b/src/routes/setup/+page.svelte @@ -0,0 +1,93 @@ + + + + Setup - Crumbs + + +
    +
    +
    + 🥨 +

    Welcome to Crumbs

    +

    Set up your password to get started

    +
    + +
    + {#if errorMsg} +
    + {errorMsg} +
    + {/if} + + + + + + + +
    +
    diff --git a/static/manifest.webmanifest b/static/manifest.webmanifest new file mode 100644 index 0000000..ba0e03a --- /dev/null +++ b/static/manifest.webmanifest @@ -0,0 +1,27 @@ +{ + "name": "Crumbs by Bretzel", + "short_name": "Crumbs", + "description": "A self-hostable, offline-first note-taking app by Bretzel", + "start_url": "/", + "display": "standalone", + "background_color": "#202124", + "theme_color": "#f59e0b", + "icons": [ + { + "src": "/icons/icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/static/robots.txt b/static/robots.txt new file mode 100644 index 0000000..b6dd667 --- /dev/null +++ b/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000..a619c96 --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,12 @@ +import adapter from '@sveltejs/adapter-node'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + kit: { + adapter: adapter({ + out: 'build' + }) + } +}; + +export default config; diff --git a/tests/e2e/attachments.spec.ts b/tests/e2e/attachments.spec.ts new file mode 100644 index 0000000..c802cc3 --- /dev/null +++ b/tests/e2e/attachments.spec.ts @@ -0,0 +1,11 @@ +import { test, expect } from './helpers/fixtures.js'; + +test.describe('Image Attachments', () => { + test('Scenario: Note editor is available for creating a new note', async ({ authenticatedPage: page }) => { + // When the user starts creating a new note + await page.getByTestId('new-note-btn').click(); + + // Then the note editor is displayed + await expect(page.getByTestId('note-editor')).toBeVisible(); + }); +}); diff --git a/tests/e2e/auth.spec.ts b/tests/e2e/auth.spec.ts new file mode 100644 index 0000000..7835242 --- /dev/null +++ b/tests/e2e/auth.spec.ts @@ -0,0 +1,92 @@ +import { test, expect } from '@playwright/test'; + +test.describe.serial('Authentication', () => { + test('Scenario: Unauthenticated user is prompted to set up or log in', async ({ page }) => { + // Given a user has not authenticated + // When they access the application + await page.goto('/'); + + // Then they are directed to the setup or login page + await expect(page).toHaveURL(/\/(setup|login)/); + }); + + test('Scenario: First-time user sees the account creation form', async ({ page }) => { + // Given no account has been created yet + // When the user visits the setup page + await page.goto('/setup'); + + // Then the password and confirmation fields and setup action are available + await expect(page.getByTestId('password-input')).toBeVisible(); + await expect(page.getByTestId('confirm-password-input')).toBeVisible(); + await expect(page.getByTestId('setup-btn')).toBeVisible(); + }); + + test('Scenario: Account creation is rejected for a password below minimum length', async ({ page }) => { + // Given the user is setting up their account + await page.goto('/setup'); + + // When they submit a password shorter than 8 characters + await page.getByTestId('password-input').fill('short'); + await page.getByTestId('confirm-password-input').fill('short'); + await page.getByTestId('setup-btn').click(); + + // Then a validation error is shown + await expect(page.getByTestId('error-message')).toBeVisible(); + }); + + test('Scenario: Account creation is rejected when passwords do not match', async ({ page }) => { + // Given the user is setting up their account + await page.goto('/setup'); + + // When they submit two different passwords + await page.getByTestId('password-input').fill('longpassword1'); + await page.getByTestId('confirm-password-input').fill('longpassword2'); + await page.getByTestId('setup-btn').click(); + + // Then a validation error is shown + await expect(page.getByTestId('error-message')).toBeVisible(); + }); + + test('Scenario: User creates an account and gains access to the application', async ({ page }) => { + // Given the user accesses the application + await page.goto('/'); + const url = page.url(); + + // When they authenticate with valid credentials + if (url.includes('/setup')) { + await page.getByTestId('password-input').fill('testpassword123'); + await page.getByTestId('confirm-password-input').fill('testpassword123'); + await page.getByTestId('setup-btn').click(); + } else if (url.includes('/login')) { + await page.getByTestId('password-input').fill('testpassword123'); + await page.getByTestId('login-btn').click(); + } + + // Then they have access to the main application + await page.waitForURL('/'); + await expect(page.getByTestId('new-note-btn')).toBeVisible(); + }); + + test('Scenario: Logged-out user loses access to the application', async ({ page }) => { + // Given the user is authenticated + await page.goto('/'); + const url = page.url(); + if (url.includes('/setup')) { + await page.getByTestId('password-input').fill('testpassword123'); + await page.getByTestId('confirm-password-input').fill('testpassword123'); + await page.getByTestId('setup-btn').click(); + await page.waitForURL('/'); + } else if (url.includes('/login')) { + await page.getByTestId('password-input').fill('testpassword123'); + await page.getByTestId('login-btn').click(); + await page.waitForURL('/'); + } + + // When they log out + await page.evaluate(() => fetch('/api/auth/logout', { method: 'POST' })); + + // Then they are redirected to the login page + await page.goto('/'); + await expect(page).toHaveURL(/\/login/); + }); +}); diff --git a/tests/e2e/checklist.spec.ts b/tests/e2e/checklist.spec.ts new file mode 100644 index 0000000..4a9eae1 --- /dev/null +++ b/tests/e2e/checklist.spec.ts @@ -0,0 +1,96 @@ +import { test, expect } from './helpers/fixtures.js'; + +test.describe('Checklist', () => { + test('Scenario: Checklist UI replaces textarea when checklist mode is enabled', async ({ authenticatedPage: page }) => { + // Given the user is creating a new note + await page.getByTestId('new-note-btn').click(); + + // When the user enables checklist mode + await page.getByTestId('checklist-toggle').click(); + + // Then the checklist component is displayed + await expect(page.getByTestId('checklist')).toBeVisible(); + + // And the plain text area is hidden + await expect(page.getByTestId('note-content-input')).not.toBeVisible(); + }); + + test('Scenario: Checklist item can be added and persisted', async ({ authenticatedPage: page }) => { + // When the user creates a checklist note with an item "Buy milk" + await page.getByTestId('new-note-btn').click(); + await page.getByTestId('note-title-input').fill('Shopping List'); + await page.getByTestId('checklist-toggle').click(); + await page.getByTestId('checklist-input').first().fill('Buy milk'); + await page.getByTestId('close-editor-btn').click(); + + // Then the note appears in the list + await expect(page.getByText('Shopping List')).toBeVisible(); + + // When the user reopens the note + await page.getByTestId('note-card').click(); + + // Then the checklist is displayed with the saved item + await expect(page.getByTestId('checklist')).toBeVisible(); + await expect(page.getByTestId('checklist-input').first()).toHaveValue('Buy milk'); + }); + + test('Scenario: Checking an item marks it as completed', async ({ authenticatedPage: page }) => { + // Given a checklist note with an item "Buy milk" + await page.getByTestId('new-note-btn').click(); + await page.getByTestId('note-title-input').fill('Tasks'); + await page.getByTestId('checklist-toggle').click(); + await page.getByTestId('checklist-input').first().fill('Buy milk'); + await page.getByTestId('close-editor-btn').click(); + + // When the user reopens the note and checks the item + await page.getByTestId('note-card').click(); + await page.getByTestId('checklist-checkbox').first().check(); + await page.getByTestId('close-editor-btn').click(); + + // Then reopening the note shows the item is still checked + await page.getByTestId('note-card').click(); + await expect(page.getByTestId('checklist-checkbox').first()).toBeChecked(); + }); + + test('Scenario: New checklist item is added by pressing Enter', async ({ authenticatedPage: page }) => { + // Given the user is editing a checklist note + await page.getByTestId('new-note-btn').click(); + await page.getByTestId('checklist-toggle').click(); + await page.getByTestId('checklist-input').first().fill('First item'); + + // When the user presses Enter + await page.getByTestId('checklist-input').first().press('Enter'); + + // Then a second checklist item appears + await expect(page.getByTestId('checklist-input')).toHaveCount(2); + }); + + test('Scenario: Empty checklist item is removed by pressing Backspace', async ({ authenticatedPage: page }) => { + // Given a checklist note with two items + await page.getByTestId('new-note-btn').click(); + await page.getByTestId('checklist-toggle').click(); + await page.getByTestId('checklist-input').first().fill('First item'); + await page.getByTestId('checklist-input').first().press('Enter'); + await expect(page.getByTestId('checklist-input')).toHaveCount(2); + + // When the user presses Backspace on the empty second item + await page.getByTestId('checklist-input').nth(1).press('Backspace'); + + // Then only the first item remains + await expect(page.getByTestId('checklist-input')).toHaveCount(1); + }); + + test('Scenario: Switching back from checklist mode restores the text area', async ({ authenticatedPage: page }) => { + // Given the user has checklist mode enabled + await page.getByTestId('new-note-btn').click(); + await page.getByTestId('checklist-toggle').click(); + await expect(page.getByTestId('checklist')).toBeVisible(); + + // When the user disables checklist mode + await page.getByTestId('checklist-toggle').click(); + + // Then the plain text area is displayed again + await expect(page.getByTestId('note-content-input')).toBeVisible(); + await expect(page.getByTestId('checklist')).not.toBeVisible(); + }); +}); diff --git a/tests/e2e/dark-mode.spec.ts b/tests/e2e/dark-mode.spec.ts new file mode 100644 index 0000000..812d2b0 --- /dev/null +++ b/tests/e2e/dark-mode.spec.ts @@ -0,0 +1,32 @@ +import { test, expect } from './helpers/fixtures.js'; + +test.describe('Dark Mode', () => { + test('Scenario: Dark mode can be toggled on and off', async ({ authenticatedPage: page }) => { + // When the user enables dark mode + await page.getByTestId('theme-toggle').click(); + + // Then the application is displayed in dark mode + const isDark = await page.evaluate(() => + document.documentElement.classList.contains('dark') + ); + expect(isDark).toBe(true); + + // When the user disables dark mode + await page.getByTestId('theme-toggle').click(); + + // Then the application is displayed in light mode + const isLight = await page.evaluate(() => + !document.documentElement.classList.contains('dark') + ); + expect(isLight).toBe(true); + }); + + test('Scenario: Theme preference is remembered across page loads', async ({ authenticatedPage: page }) => { + // When the user enables dark mode + await page.getByTestId('theme-toggle').click(); + + // Then the preference is persisted + const theme = await page.evaluate(() => localStorage.getItem('theme')); + expect(theme).toBeTruthy(); + }); +}); diff --git a/tests/e2e/global-setup.ts b/tests/e2e/global-setup.ts new file mode 100644 index 0000000..c448303 --- /dev/null +++ b/tests/e2e/global-setup.ts @@ -0,0 +1,13 @@ +import { existsSync, unlinkSync } from 'fs'; + +const TEST_DB = './data/test-crumbs.db'; + +export default function globalSetup() { + // Clean test database before E2E run so each run starts fresh + for (const ext of ['', '-journal', '-wal', '-shm']) { + const path = `${TEST_DB}${ext}`; + if (existsSync(path)) { + unlinkSync(path); + } + } +} diff --git a/tests/e2e/helpers/fixtures.ts b/tests/e2e/helpers/fixtures.ts new file mode 100644 index 0000000..5034100 --- /dev/null +++ b/tests/e2e/helpers/fixtures.ts @@ -0,0 +1,45 @@ +import { test as base, expect, type Page } from '@playwright/test'; + +const TEST_PASSWORD = 'testpassword123'; + +/** + * Extended test fixture that handles setup/auth. + */ +export const test = base.extend<{ authenticatedPage: Page }>({ + authenticatedPage: async ({ page }, use) => { + // Setup: create user and login + await setupAndLogin(page); + await use(page); + } +}); + +export async function setupAndLogin(page: Page) { + await page.goto('/'); + + const url = page.url(); + if (url.includes('/setup')) { + // First-time setup - may race with other workers + await page.getByTestId('password-input').fill(TEST_PASSWORD); + await page.getByTestId('confirm-password-input').fill(TEST_PASSWORD); + await page.getByTestId('setup-btn').click(); + + // Wait for either redirect to main page or an error (another worker completed setup first) + try { + await page.waitForURL('/', { timeout: 5000 }); + } catch { + // Setup was completed by another worker - go to login instead + await page.goto('/login'); + await page.getByTestId('password-input').fill(TEST_PASSWORD); + await page.getByTestId('login-btn').click(); + await page.waitForURL('/'); + } + } else if (url.includes('/login')) { + // Login + await page.getByTestId('password-input').fill(TEST_PASSWORD); + await page.getByTestId('login-btn').click(); + await page.waitForURL('/'); + } + // else: already on main page (session still valid) +} + +export { expect }; diff --git a/tests/e2e/notes-crud.spec.ts b/tests/e2e/notes-crud.spec.ts new file mode 100644 index 0000000..2fc72a5 --- /dev/null +++ b/tests/e2e/notes-crud.spec.ts @@ -0,0 +1,64 @@ +import { test, expect } from './helpers/fixtures.js'; + +test.describe('Notes CRUD', () => { + test('Scenario: Fresh account shows an empty notes list', async ({ authenticatedPage: page }) => { + // Given the user has no notes + // Then the empty state message is displayed + await expect(page.getByText('Notes you add appear here')).toBeVisible(); + }); + + test('Scenario: New note appears in the notes list after creation', async ({ authenticatedPage: page }) => { + // When the user creates a note titled "My First Note" with content "Hello world!" + await page.getByTestId('new-note-btn').click(); + await page.getByTestId('note-title-input').fill('My First Note'); + await page.getByTestId('note-content-input').fill('Hello world!'); + await page.getByTestId('close-editor-btn').click(); + + // Then the note is visible in the notes list + await expect(page.getByTestId('note-card')).toBeVisible(); + await expect(page.getByText('My First Note')).toBeVisible(); + }); + + test('Scenario: Edited note title is reflected in the notes list', async ({ authenticatedPage: page }) => { + // Given a note titled "Original Title" exists + await page.getByTestId('new-note-btn').click(); + await page.getByTestId('note-title-input').fill('Original Title'); + await page.getByTestId('note-content-input').fill('Original content'); + await page.getByTestId('close-editor-btn').click(); + + // When the user changes the title to "Updated Title" + await page.getByTestId('note-card').click(); + await page.getByTestId('note-title-input').clear(); + await page.getByTestId('note-title-input').fill('Updated Title'); + await page.getByTestId('close-editor-btn').click(); + + // Then the updated title is displayed + await expect(page.getByText('Updated Title')).toBeVisible(); + }); + + test('Scenario: Trashed note disappears from the main view', async ({ authenticatedPage: page }) => { + // Given a note titled "Delete Me" exists + await page.getByTestId('new-note-btn').click(); + await page.getByTestId('note-title-input').fill('Delete Me'); + await page.getByTestId('close-editor-btn').click(); + + // When the user trashes the note + await page.getByTestId('note-card').hover(); + await page.getByTestId('trash-btn').click(); + + // Then the note is no longer visible + await expect(page.getByText('Delete Me')).not.toBeVisible(); + }); + + test('Scenario: Markdown content is rendered in the note preview', async ({ authenticatedPage: page }) => { + // Given a note with markdown content "**bold text**" + await page.getByTestId('new-note-btn').click(); + await page.getByTestId('note-content-input').fill('**bold text**'); + + // When the user switches to preview mode + await page.getByTestId('preview-toggle').click(); + + // Then the rendered preview contains the formatted text + await expect(page.getByTestId('note-preview')).toContainText('bold text'); + }); +}); diff --git a/tests/e2e/offline.spec.ts b/tests/e2e/offline.spec.ts new file mode 100644 index 0000000..da5de80 --- /dev/null +++ b/tests/e2e/offline.spec.ts @@ -0,0 +1,19 @@ +import { test, expect } from './helpers/fixtures.js'; + +test.describe('Offline Support', () => { + test('Scenario: Sync status is visible to the user', async ({ authenticatedPage: page }) => { + // Given the user is authenticated + // Then the sync status indicator is displayed + await expect(page.getByTestId('sync-indicator')).toBeVisible(); + }); + + test('Scenario: Created note is immediately visible in the notes list', async ({ authenticatedPage: page }) => { + // When the user creates a note titled "Offline Test Note" + await page.getByTestId('new-note-btn').click(); + await page.getByTestId('note-title-input').fill('Offline Test Note'); + await page.getByTestId('close-editor-btn').click(); + + // Then the note appears in the list + await expect(page.getByText('Offline Test Note')).toBeVisible(); + }); +}); diff --git a/tests/e2e/organization.spec.ts b/tests/e2e/organization.spec.ts new file mode 100644 index 0000000..a06f3ac --- /dev/null +++ b/tests/e2e/organization.spec.ts @@ -0,0 +1,66 @@ +import { test, expect } from './helpers/fixtures.js'; + +test.describe('Organization Features', () => { + test('Scenario: Pinned note appears under the Pinned section', async ({ authenticatedPage: page }) => { + // Given a note titled "Pin Me" exists + await page.getByTestId('new-note-btn').click(); + await page.getByTestId('note-title-input').fill('Pin Me'); + await page.getByTestId('close-editor-btn').click(); + + // When the user pins the note + await page.getByTestId('note-card').hover(); + await page.getByTestId('pin-btn').click(); + + // Then the "Pinned" section is visible + await expect(page.getByText('Pinned')).toBeVisible(); + }); + + test('Scenario: Archived note is removed from the main view', async ({ authenticatedPage: page }) => { + // Given a note titled "Archive Me" exists + await page.getByTestId('new-note-btn').click(); + await page.getByTestId('note-title-input').fill('Archive Me'); + await page.getByTestId('close-editor-btn').click(); + + // When the user archives the note + await page.getByTestId('note-card').hover(); + await page.getByTestId('archive-btn').click(); + + // Then the note is no longer visible in the main view + await expect(page.getByText('Archive Me')).not.toBeVisible(); + }); + + test('Scenario: Note color is changed via the color picker', async ({ authenticatedPage: page }) => { + // Given the user is creating a note titled "Colored Note" + await page.getByTestId('new-note-btn').click(); + await page.getByTestId('note-title-input').fill('Colored Note'); + + // When the user sets the note color to "Coral" + await page.getByTestId('color-picker-toggle').click(); + await page.getByTestId('color-coral').click(); + await page.getByTestId('close-editor-btn').click(); + + // Then the note is saved and visible in the notes list + await expect(page.getByTestId('note-card')).toBeVisible(); + }); + + test('Scenario: Filtering by tag shows only matching notes', async ({ authenticatedPage: page }) => { + // Given a note tagged #important and an untagged note exist + await page.getByTestId('new-note-btn').click(); + await page.getByTestId('note-title-input').fill('Tagged Note'); + await page.getByTestId('note-content-input').fill('This is #important'); + await page.getByTestId('close-editor-btn').click(); + + await page.getByTestId('new-note-btn').click(); + await page.getByTestId('note-title-input').fill('Untagged Note'); + await page.getByTestId('close-editor-btn').click(); + + // When the user filters by the #important tag + const tagChip = page.getByTestId('tag-chip').filter({ hasText: '#important' }); + if (await tagChip.isVisible()) { + await tagChip.click(); + + // Then only the tagged note is visible + await expect(page.getByText('Tagged Note')).toBeVisible(); + } + }); +}); diff --git a/tests/e2e/pwa.spec.ts b/tests/e2e/pwa.spec.ts new file mode 100644 index 0000000..32d9d02 --- /dev/null +++ b/tests/e2e/pwa.spec.ts @@ -0,0 +1,24 @@ +import { test, expect } from '@playwright/test'; + +test.describe('PWA', () => { + test('Scenario: Web manifest provides correct app metadata', async ({ page }) => { + // When the browser requests the web manifest + const response = await page.goto('/manifest.webmanifest'); + + // Then it returns successfully with the correct app name, display mode, and icons + expect(response?.status()).toBe(200); + const manifest = await response?.json(); + expect(manifest?.name).toBe('Crumbs by Bretzel'); + expect(manifest?.display).toBe('standalone'); + expect(manifest?.icons?.length).toBeGreaterThan(0); + }); + + test('Scenario: Application declares a theme color for mobile browsers', async ({ page }) => { + // Given the application is loaded + await page.goto('/login'); + + // Then a theme-color meta tag is present + const themeColor = await page.getAttribute('meta[name="theme-color"]', 'content'); + expect(themeColor).toBeTruthy(); + }); +}); diff --git a/tests/e2e/search.spec.ts b/tests/e2e/search.spec.ts new file mode 100644 index 0000000..d90e5a5 --- /dev/null +++ b/tests/e2e/search.spec.ts @@ -0,0 +1,45 @@ +import { test, expect } from './helpers/fixtures.js'; + +test.describe('Search', () => { + test('Scenario: Searching by title returns the matching note', async ({ authenticatedPage: page }) => { + // Given a note titled "Shopping List" and a note titled "Work Meeting" exist + await page.getByTestId('new-note-btn').click(); + await page.getByTestId('note-title-input').fill('Shopping List'); + await page.getByTestId('close-editor-btn').click(); + + await page.getByTestId('new-note-btn').click(); + await page.getByTestId('note-title-input').fill('Work Meeting'); + await page.getByTestId('close-editor-btn').click(); + + // When the user searches for "Shopping" + await page.getByTestId('search-input').fill('Shopping'); + await page.waitForTimeout(500); + + // Then "Shopping List" is visible in the results + await expect(page.getByText('Shopping List')).toBeVisible(); + }); + + test('Scenario: Searching by content returns the matching note', async ({ authenticatedPage: page }) => { + // Given a note titled "Recipe" with content "Pasta with tomato sauce" exists + await page.getByTestId('new-note-btn').click(); + await page.getByTestId('note-title-input').fill('Recipe'); + await page.getByTestId('note-content-input').fill('Pasta with tomato sauce'); + await page.getByTestId('close-editor-btn').click(); + + // When the user searches for "tomato" + await page.getByTestId('search-input').fill('tomato'); + await page.waitForTimeout(500); + + // Then "Recipe" is visible in the results + await expect(page.getByText('Recipe')).toBeVisible(); + }); + + test('Scenario: Searching for a nonexistent term yields no results', async ({ authenticatedPage: page }) => { + // When the user searches for a term that matches nothing + await page.getByTestId('search-input').fill('xyznonexistent'); + await page.waitForTimeout(500); + + // Then no notes are displayed + await expect(page.getByTestId('note-card')).not.toBeVisible(); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2c2ed3c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "rewriteRelativeImportExtensions": true, + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // To make changes to top-level options such as include and exclude, we recommend extending + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..bf699a8 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,7 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import tailwindcss from '@tailwindcss/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [tailwindcss(), sveltekit()] +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..b4cb01b --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['src/**/*.test.ts'], + environment: 'node' + } +});
    1