diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..210aa45 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,19 @@ +# node_modules is intentionally IN the build context: the Dockerfile copies it straight +# into the image (deps are installed by the CI job / dev host, which have working network; +# the runner's dind daemon has broken outbound DNS for build containers). +.next +/db +test-results +*.tsbuildinfo +.git +.env +.codenomad +# e2e harness: the suite runs on the host / CI job, never inside the image +# (@playwright/test is a devDependency of the runner, excluded from prod-deps) +e2e +playwright.config.ts +# docs/deployment files never affect the app build — excluding them keeps +# changelog-only commits from invalidating the COPY layer and re-running next build +*.md +Dockerfile* +docker-compose* diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..a7e8079 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,229 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + workflow_dispatch: + +jobs: + ci: + name: build + unit + e2e + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + # The runner provides a working docker CLI out of the box (full-dind) — no endpoint + # resolution needed. Job and app containers share the daemon, so put them on one + # user-defined network with a pinned subnet; the app gets a STATIC IP (172.39.0.10, + # declared in docker-compose.ci.yml) because Chromium got ERR_SSL_PROTOCOL_ERROR via + # the DNS alias while node-side HTTP to the same name worked — bypassing DNS removes + # that variable entirely. A stale network with a different subnet is recreated. + - name: Prepare e2e network + run: | + set -u + # Interrupted runs skip the end-of-run cleanup, leaking a USED DB volume into later + # runs (tests assume seeded state) — drop it so every run starts fresh. + docker volume rm space-wanderer-ci_space-wanderer-db 2>/dev/null || true + SUBNET=172.39.0.0/24 + if docker network inspect space-wanderer-ci-e2e >/dev/null 2>&1; then + cur=$(docker network inspect space-wanderer-ci-e2e --format '{{(index .IPAM.Config 0).Subnet}}') + if [ "$cur" != "$SUBNET" ]; then + echo "stale space-wanderer-ci-e2e with subnet $cur — recreating with $SUBNET" + docker network rm space-wanderer-ci-e2e || true + docker network create --subnet "$SUBNET" space-wanderer-ci-e2e + fi + else + docker network create --subnet "$SUBNET" space-wanderer-ci-e2e + fi + if ! docker network connect space-wanderer-ci-e2e "$(hostname)"; then + echo "::error::failed to attach job container '$(hostname)' to space-wanderer-ci-e2e — the job must run in a container on the shared daemon (check runner config)" >&2 + exit 1 + fi + docker network inspect space-wanderer-ci-e2e --format 'containers on space-wanderer-ci-e2e: {{range .Containers}}{{.Name}} {{end}}' + echo "E2E_BASE_URL=http://172.39.0.10:3000" >> "$GITHUB_ENV" + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 + + # bun drives installs (repo ships bun.lock, no package-lock.json); pinned to 1.4.2. + # Retried: the runner's outbound network/DNS is flaky (transient EAI_AGAIN seen in CI). + - name: Install bun + run: | + set -u + ok=0 + for i in 1 2 3; do + if npm i -g bun@1.4.2; then ok=1; break; fi + echo "::warning::bun install attempt $i/3 failed (likely transient network) — retrying in 5s" >&2 + sleep 5 + done + [ "$ok" = "1" ] || { echo "::error::bun install failed after 3 attempts" >&2; exit 1; } + bun --version + + # Job-level dep cache via the runner's built-in cache server. continue-on-error on + # both steps: an unreachable/broken cache server must never break the run. + - name: Restore deps cache + uses: actions/cache@v4 + continue-on-error: true + with: + path: | + node_modules + $HOME/.bun/install/cache + key: ${{ runner.os }}-deps-bun-${{ hashFiles('bun.lock') }} + + # Retried: the runner's outbound network/DNS is flaky (transient EAI_AGAIN seen in CI). + - name: Install dependencies + run: | + set -u + ok=0 + for i in 1 2 3; do + if bun install --frozen-lockfile; then ok=1; break; fi + echo "::warning::dependency install attempt $i/3 failed (likely transient network) — retrying in 5s" >&2 + sleep 5 + done + [ "$ok" = "1" ] || { echo "::error::dependency install failed after 3 attempts" >&2; exit 1; } + + # No explicit save step: caches marked by the restore steps are uploaded automatically at job end. + + - name: Build + run: npm run build + + - name: Unit tests + run: npx vitest run + + # Browsers run locally in CI (PW_REMOTE_BROWSER=0 below): the shared remote browser + # server at 192.168.1.4 cannot reach a stack inside dind, and local chromium keeps + # the job self-contained. + # Browser binaries (~187 MiB) cached across runs: the runner's uplink is flaky and + # cdn.playwright.dev downloads time out mid-transfer; once seeded, installs skip the download. + - name: Restore browser cache + uses: actions/cache@v4 + continue-on-error: true + with: + path: $HOME/.cache/ms-playwright + key: ${{ runner.os }}-pw-browsers-${{ hashFiles('package.json') }} + + # Retried: cdn.playwright.dev is flaky from this runner — Playwright's internal 5-retry + # window (~40-90s) is often not enough, but runs get through with a longer one. Progress + # accumulates across attempts: playwright skips already-installed browsers. + - name: Install browsers (local) + run: | + set -u + ok=0 + for i in 1 2 3 4 5 6; do + if npx playwright install --with-deps chromium; then ok=1; break; fi + echo "::warning::browser install attempt $i/6 failed (flaky cdn.playwright.dev) — retrying in 5s" >&2 + sleep 5 + done + [ "$ok" = "1" ] || { echo "::error::browser install failed after 6 attempts" >&2; exit 1; } + + # Cache the base image on the shared daemon up front: a single loud failure here + # beats a 6-minute BuildKit "failed to solve" when the daemon's outbound net is down. + - name: Pull e2e base image + run: docker pull node:22-slim + + # Pre-build the e2e image with retries (flaky runner network); the E2E step's own + # build then becomes a fast no-op. Plain progress keeps BuildKit output visible. + - name: Build e2e image (retries) + run: | + set -u + ok=0 + for i in 1 2 3; do + if docker compose -f docker-compose.ci.yml -p space-wanderer-ci build --progress=plain; then ok=1; break; fi + echo "::warning::e2e image build attempt $i/3 failed (likely transient network) — retrying in 5s" >&2 + sleep 5 + done + [ "$ok" = "1" ] || { echo "::error::e2e image build failed after 3 attempts" >&2; exit 1; } + + # E2E against an isolated stack on the shared daemon — never the production instance. + # docker-compose.ci.yml joins the space-wanderer-ci-e2e network prepared above; project name + # space-wanderer-ci keeps it distinct from production. E2E_DEPLOY_CMD makes the config's + # webServer block deploy + wait for readiness (http://app:3000 via service DNS); + # E2E_COMPOSE_ARGS points state.spec.ts' mid-run `docker compose restart space-wanderer` at + # the same file + project. E2E_BASE_URL comes from GITHUB_ENV (Prepare e2e network). + - name: E2E (isolated stack in dind) + env: + PW_REMOTE_BROWSER: "0" + # Build separately with plain progress so BuildKit step output is visible in the + # job log (auto progress hides it). Playwright's webServer needs a long-running + # command — `up -d` returns immediately and triggers "exited early"; `logs -f` + # keeps it alive, streams app logs into CI output for debugging, and is killed with + # the job (an orphaned `logs -f` dies on its own when cleanup removes the container). + E2E_DEPLOY_CMD: "docker compose -f docker-compose.ci.yml -p space-wanderer-ci build --progress=plain && docker compose -f docker-compose.ci.yml -p space-wanderer-ci up -d && docker compose -f docker-compose.ci.yml -p space-wanderer-ci logs -f" + E2E_COMPOSE_ARGS: "-f docker-compose.ci.yml -p space-wanderer-ci" + # Chromium honors proxy env vars on Linux (node-side HTTP does not) — keep the + # in-network app host out of any proxy path. + NO_PROXY: app,localhost,127.0.0.1,.kkhost.pl + no_proxy: app,localhost,127.0.0.1,.kkhost.pl + # Machine-readable e2e results for the job-summary step below (Playwright's built-in + # json reporter; without this env var it would dump to stdout). test-results/ is git-ignored. + PLAYWRIGHT_JSON_OUTPUT_FILE: test-results/e2e.json + # Connectivity diagnostics now run post-deploy in e2e/ci-diagnostics.ts (globalSetup). + # --reporter=list prints each test as it completes (name + ✓/× + duration) for progress + # visibility in the job log; failure error context/snapshots are unchanged. The json + # reporter adds a machine-readable report (PLAYWRIGHT_JSON_OUTPUT_FILE above) — no + # execution behavior changes, reporters only observe. + run: npx playwright test --reporter=list,json + + # Appends e2e results to the job-summary panel — mirrors how unit tests get there + # (vitest's built-in github-actions reporter auto-appends $GITHUB_STEP_SUMMARY in CI). + # if: always() so a FAILED suite still gets summarized; non-fatal by design — a missing + # or malformed report must never mask the real e2e exit code. The script is single-quoted + # for bash, so it contains no apostrophes. + - name: E2E job summary + if: always() + run: | + node -e ' + const fs = require("fs"); + let report; + try { + report = JSON.parse(fs.readFileSync("test-results/e2e.json", "utf8")); + } catch { + process.exit(0); // e2e never ran (earlier step failed) or no report — nothing to summarize + } + let passed = 0, skipped = 0; + const failed = []; + const walk = (suite) => { + for (const spec of suite.specs ?? []) { + if (!spec.ok) failed.push(`${suite.title} › ${spec.title}`); + else if ((spec.tests ?? []).length && spec.tests.every((t) => t.expectedStatus === "skipped")) skipped++; + else passed++; + } + for (const sub of suite.suites ?? []) walk(sub); + }; + for (const s of report.suites ?? []) walk(s); + const lines = [ + "", "## E2E results", "", + `- Passed: ${passed}`, + `- Failed: ${failed.length}`, + `- Skipped: ${skipped}`, + ]; + if (failed.length) { + lines.push("", "### Failed specs"); + for (const f of failed) lines.push(`- \`${f}\``); + } + const summaryFile = process.env.GITHUB_STEP_SUMMARY; + if (summaryFile) fs.appendFileSync(summaryFile, lines.join("\n") + "\n"); + ' || true + + # The daemon is shared with production — never leave our stack or network behind. + # -v drops the named DB volume so every run seeds fresh (no schema drift between PRs). + - name: Cleanup e2e stack + network + if: always() + run: | + docker compose -f docker-compose.ci.yml -p space-wanderer-ci down --remove-orphans -v || true + docker network remove space-wanderer-ci-e2e 2>/dev/null || true + + # The shared runner dind still accumulates between runs what the steps above can't reach: + # dangling images/layers from the per-run space-wanderer-ci rebuilds, BuildKit cache, and job + # containers leaked by interrupted runs. Prune them here (after stack + network cleanup). + # Deliberately NO --volumes: the daemon is shared with production and named volumes may hold + # other workflows' persistent data — this project's own space-wanderer-ci_space-wanderer-db volume is already + # dropped by the steps above. Non-fatal so a prune failure can never mask or break the run. + - name: Prune runner dind leftovers + if: always() + run: docker system prune -af || echo "::warning::dind prune failed — see logs" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0108e0c --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# env — never commit secrets +.env + +# deps +node_modules/ + +# build output +.next/ +dist/ +test-results/ +*.tsbuildinfo + +# agent tooling state +.codenomad/ + +# game state SQLite: lives on the host, volume-mounted into the container as +# ./db → /app/db so it survives rebuilds — never tracked. Anchored to repo root: +# an unanchored "db/" would also ignore lib/db/ (source). +/db/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0a20daf --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +## v0.1.0 — 2026-09-28 15:47 +- Initial scaffold (stack proof): base stack copied/adapted from Flight-Ops (`../flight`) — Next.js 16 App Router + React 19 + TypeScript strict, bun package manager, Node 22 in CI/Docker. +- Single-player only by design: NO auth/accounts/multiplayer anywhere (Flight-Ops's users/sessions/admin/leaderboard patterns deliberately not copied). +- Frontend component layer: scificn-ui v0.1.0 integrated copy-paste (Radix + Tailwind v4, SCI-FI theme) — Button, Panel, Badge, Card, Alert in `components/ui/`; theme tokens in `app/globals.css` with a brightened `--text-muted` override (`#4A4A40` → `#6B6B5C`). +- Interface base: the scificn-ui **Mission Control** showcase (https://www.scificn.dev/showcase/mission-control) rebuilt as the app's main UI — `app/page.tsx` renders `components/mission-control.tsx` (client dashboard: sticky header, stat-card row, 3-column grid of Panel/Tabs/StatusGrid/ProgressRing/BarChart/LineChart/Terminal panels, sticky footer), placeholder ship content only, no game logic; additional upstream components copied into `components/ui/` (Tabs, StatCard, Terminal, StatusGrid, ProgressRing, BarChart, LineChart) + `@radix-ui/react-tabs` dependency. +- DB plumbing: SQLite via Drizzle ORM + sql.js (WASM) at `db/space-wanderer.db`, volume-mounted; empty schema placeholder — game tables land with the first game systems. +- API pattern established: `/api/version` + OpenAPI 3.x spec generated in code (`lib/openapi.ts`) served at `/api/openapi`. +- CI/e2e/docker pipeline: single "build + unit + e2e" job on the dind runner, isolated stack `space-wanderer-ci` with static IP 172.39.0.10 (fresh DB volume per run), Playwright e2e (`smoke.spec.ts`, `state.spec.ts`) CI-only against that stack; prod compose behind Traefik at `https://space-wanderer.dev.kkhost.pl` (not deployed yet). diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d5a820f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +# Two-stage build needing ZERO outbound network at image-build time: +# (a) deps are installed by the CI job / dev host (`bun install`) and copied in via the +# build context — the runner's dind daemon has broken outbound DNS for build containers, +# so any RUN that fetches packages fails; +# (b) base images must be glibc (node:22-slim), NOT alpine/musl: native modules (@next/swc-*) +# are prebuilt for the platform that ran `bun install` — CI job and prod host are +# x86_64 glibc linux. Building on another platform requires reinstalling node_modules there first. +FROM node:22-slim AS build +WORKDIR /app +COPY package.json bun.lock ./ +COPY node_modules ./node_modules +COPY . . +ENV NEXT_TELEMETRY_DISABLED=1 +RUN npm run build + +# No `output: 'standalone'`: the runtime stage keeps node_modules because sql.js is kept +# external (next.config.ts serverExternalPackages) and require('sql.js') resolves its WASM +# blob from node_modules at runtime. +FROM node:22-slim AS runtime +WORKDIR /app +ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 +# Full tree (dev+prod deps): a prod-only slimmed copy would need an extra install step, +# revisit if image size matters. +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/.next ./.next +COPY --from=build /app/package.json ./package.json +EXPOSE 3000 +CMD ["npm", "run", "start"] diff --git a/ORCHESTRATOR.md b/ORCHESTRATOR.md index 49613ef..46041de 100644 --- a/ORCHESTRATOR.md +++ b/ORCHESTRATOR.md @@ -1,6 +1,6 @@ # ORCHESTRATOR.md — Operating Manual for Successor Agents -You are taking over the autonomous development loop on **SpaceWanderer** (browser game: turn-based space adventure — build & expand your ship/base from found parts and resources, survive random encounters, manage a crew of flawed humans with good/bad traits; server-authoritative turn engine). Gameplay reference: Out There (Android), but more building-focused. This document captures operational knowledge that is NOT written down in AGENTS.md. Read AGENTS.md first — it defines the mandatory change process and this file complements it. +You are taking over the autonomous development loop on **SpaceWanderer** (browser game: dungeon-crawler-type, very replayable — turn-based space adventure where you build & expand your ship/base from found parts and resources, survive random encounters, manage a crew of flawed humans with good/bad traits; server-authoritative turn engine). Gameplay reference: Out There (Android), but more building-focused. **Single player only, by design** — no auth/accounts/multiplayer, ever, unless the user changes that decision. This document captures operational knowledge that is NOT written down in AGENTS.md. Read AGENTS.md first — it defines the mandatory change process and this file complements it. Credentials are deliberately NOT stored in this file — see §1 for where they live. Never paste token values into any tracked file. @@ -10,7 +10,7 @@ Credentials are deliberately NOT stored in this file — see §1 for where they - Prod URL (planned, not deployed yet): https://space-wanderer.dev.kkhost.pl (Traefik → docker-compose stack on this host); first deploy happens after the initial scaffold. Once live, `GET /api/version` reports the running version. - Gitea API access for git.kkhost.pl: prefer the gitea MCP tools — they are already configured with a token. For raw REST/curl, read the current token from the local .env file at repo root (variable `GITEA_TOKEN`; .env is gitignored and must never be committed): `source .env && curl -s -H "Authorization: token $GITEA_TOKEN" https://git.kkhost.pl/api/v1/user`. (Incident note, Sept 2026: an early draft of the Flight-Ops doc committed a live token to the repo; that commit was purged from all refs and the token has since been rotated — never repeat this.) - Current user: kamilkrzywda (id 1). -- **Current state:** as of writing, the repo is empty (fresh `git init`, no commits). Step zero for any successor: scaffold from `../flight` — copy architecture + `.gitea/workflows/ci.yml` + `docker-compose*.yml` patterns and adapt names — then this manual's loop applies. +- **Current state:** initial scaffold (v0.1.0) is done on branch `scaffold-base` off master — base stack copied/adapted from `../flight`, scificn-ui frontend integrated, CI/e2e pipeline in place; not merged/deployed yet (first deploy happens after the scaffold lands). From here the loop below applies as-is. ## 2. The loop (per issue) 1. Small incremental change on a feature branch off `master`; connected issues may share one branch/PR with a single version bump. @@ -30,7 +30,7 @@ Credentials are deliberately NOT stored in this file — see §1 for where they - CI watcher pattern that works: `run_background_process` (notify=true) running a loop that polls `GET /actions/runs/{id}` every 60 s and exits when status=completed, printing status+conclusion each tick. Don't poll in the foreground; don't sleep-loop on pty_read. ## 4. CI facts (.gitea/workflows/ci.yml + docker-compose.ci.yml) -- Mirrors Flight-Ops: single job "build + unit + e2e" on runner gitea-runner (docker-in-docker). Fresh DB per run (volume removed); compose project `space-wanderer-ci`; app container on a static IP → `E2E_BASE_URL=http://:3000` (exact IP TBD when scaffolding — copy `../flight/.gitea/workflows/ci.yml` + `docker-compose.ci.yml` and adapt); `PW_REMOTE_BROWSER=0` (CI runs local chromium — the shared remote browser server at 192.168.1.4 cannot reach a stack inside dind); `E2E_COMPOSE_ARGS="-f docker-compose.ci.yml -p space-wanderer-ci"` is read by e2e/state.spec.ts for its mid-run `docker compose restart`. +- Mirrors Flight-Ops: single job "build + unit + e2e" on runner gitea-runner (docker-in-docker). Fresh DB per run (volume removed); compose project `space-wanderer-ci`; app container on a static IP → `E2E_BASE_URL=http://172.39.0.10:3000` (network `space-wanderer-ci-e2e`, subnet 172.39.0.0/24 — same mechanism as Flight-Ops, different network name so both can coexist on the shared daemon); `PW_REMOTE_BROWSER=0` (CI runs local chromium — the shared remote browser server at 192.168.1.4 cannot reach a stack inside dind); `E2E_COMPOSE_ARGS="-f docker-compose.ci.yml -p space-wanderer-ci"` is read by e2e/state.spec.ts for its mid-run `docker compose restart`. - Flaky uplink → retry loops in ci.yml; an occasional "flaky" test (failed once, passed on retry) does NOT fail the run. - Branch protection requires status check `CI / build + unit + e2e (pull request)` — PRs cannot merge until green; CI never deploys to production. @@ -44,12 +44,13 @@ Credentials are deliberately NOT stored in this file — see §1 for where they - None recorded yet — this is where hard-won knowledge accumulates as it is discovered. (Flight-Ops's equivalent sections documented Playwright evaluate serialization dropping prototype getters and MapLibre tile quantization — expect similar "the tool lies to you" surprises with whatever rendering approach lands.) ## 7. App architecture quick map -- **TARGET ARCHITECTURE — code not written yet; scaffold from `../flight`.** +- **Scaffolded (v0.1.0)** — stack proof only, no game systems yet; the rest of this section is what's built + what lands next. - Next.js 16 App Router + React 19 + TypeScript strict, bun package manager (bun.lock), Node 22 in CI/Docker. -- SQLite via Drizzle ORM + sql.js (WASM, kept external in next.config.ts) at db/space-wanderer.db volume-mounted; schema designed to switch to Postgres later. +- **Frontend component layer: scificn-ui v0.1.0** (https://scificn.dev) — copy-paste model on Radix + Tailwind v4; components live in `components/ui/` (Button, Panel, Badge, Card, Alert, Tabs, StatCard, Terminal, StatusGrid, ProgressRing, BarChart, LineChart), theme tokens in `app/globals.css`. The app's interface base is the scificn-ui **Mission Control** showcase layout (https://www.scificn.dev/showcase/mission-control) rebuilt as `components/mission-control.tsx` and rendered from `app/page.tsx`, with placeholder ship content — no game logic yet. Deliberate deviation from upstream: `--text-muted` brightened `#4A4A40` → `#6B6B5C` (+12pt lightness) for readability on the near-black background. Upstream repo has **no LICENSE file** — user-approved risk, keep components minimal and re-verify licensing before any redistribution of this codebase. +- SQLite via Drizzle ORM + sql.js (WASM, kept external in next.config.ts) at db/space-wanderer.db volume-mounted; schema designed to switch to Postgres later. Schema is EMPTY at scaffold stage — first game tables land with the turn engine. - Vitest unit tests colocated (`**/*.test.ts`); Playwright e2e in `e2e/`, CI-only. - Server-authoritative TURN engine in lib/ — unlike Flight-Ops there is NO tick loop or sim clock: state advances only when a player action resolves a turn, and every action endpoint returns the full post-action snapshot which the client applies immediately (polling optional/later for multiplayer). -- Plain React hooks for client state, no external store; session via stored id + custom header. +- Plain React hooks for client state, no external store. **NO auth/accounts/multiplayer by design** (single player only) — there is no users/sessions table and no session headers; a "session" means the local single-player save slot in the DB, not an identity system. Do not copy Flight-Ops's lib/auth.ts / SignInScreen / admin patterns. - Dev/e2e hooks: `window.__sw*` in dev builds + `/api/test/*` behind env flag not set in prod. - Core game systems to build: turn engine, random encounter resolution, ship module grid (build/expand from found parts), resource management, crew with good/bad traits. diff --git a/app/api/openapi/route.ts b/app/api/openapi/route.ts new file mode 100644 index 0000000..4fdbda4 --- /dev/null +++ b/app/api/openapi/route.ts @@ -0,0 +1,5 @@ +import { openApiSpec } from '@/lib/openapi'; + +export async function GET() { + return Response.json(openApiSpec); +} diff --git a/app/api/version/route.ts b/app/api/version/route.ts new file mode 100644 index 0000000..50bafe8 --- /dev/null +++ b/app/api/version/route.ts @@ -0,0 +1,5 @@ +import pkg from '@/package.json'; + +export async function GET() { + return Response.json({ version: pkg.version }); +} diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..bea462a --- /dev/null +++ b/app/globals.css @@ -0,0 +1,369 @@ +@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;0,700;1,400&family=Space+Mono:ital,wght@0,400;0,700;1,400&display=swap'); + +@import "tailwindcss"; + +/* ============================================================ + TAILWIND V4 THEME — tokens that generate utility classes + ============================================================ */ +@theme { + /* Original palette — retained */ + --color-pink: #B53082; + --color-purple: #381B57; + --color-orange: #F2963A; + --color-teal: #6DC3BB; + --color-beige: #C8B99A; + --color-navy: #0E1628; + --color-void: #07050F; + + /* New palette */ + --color-green: #00ed3f; + --color-amber: #ff8800; + --color-amber-light: #ffa238; + --color-red: #cc2200; + --color-blue: #4466cc; + --color-bone: #E0D5BE; + + /* Neutral dark backgrounds */ + --color-bg: #050505; + --color-surface: #0D0D0D; + --color-surface-raised: #141414; + + /* Typography */ + --font-mono: 'IBM Plex Mono', 'Space Mono', ui-monospace, 'Courier New', monospace; + + /* Zero radius */ + --radius: 0px; +} + +/* ============================================================ + SEMANTIC TOKENS — contextual roles mapped to palette + ============================================================ */ +:root { + /* Backgrounds — neutral dark, no color tint */ + --background: #050505; + --surface: #0D0D0D; + --surface-raised: #141414; + + /* Borders */ + --border: #252525; + --border-active: #00ed3f; + + /* Text */ + --text-primary: #00ed3f; + --text-secondary: #E0D5BE; + /* SpaceWanderer tweak (v0.1.0): upstream default is #4A4A40 (HSL 60,7%,27%) — too dim for + secondary text on the near-black background. Brightened to HSL 60,8%,39% (+12pt lightness, + same warm-gray hue) so muted labels stay readable while remaining clearly muted. */ + --text-muted: #6B6B5C; + --text-warning: #ff8800; + --text-danger: #cc2200; + + /* Accent shortcuts */ + --accent-primary: #00ed3f; + --accent-secondary: #4466cc; + --accent-warning: #ff8800; + + /* Glow — original (kept for backward compat) */ + --glow-teal: 0 0 8px #6DC3BB66, 0 0 20px #6DC3BB33; + --glow-pink: 0 0 8px #B5308266, 0 0 20px #B5308233; + --glow-orange: 0 0 8px #F2963A66, 0 0 20px #F2963A33; + + /* Glow — new palette */ + --glow-green: 0 0 8px #00ed3f66, 0 0 20px #00ed3f33; + --glow-amber: 0 0 8px #ff880066, 0 0 20px #ff880033; + --glow-red: 0 0 8px #cc220066, 0 0 20px #cc220033; + --glow-blue: 0 0 8px #4466cc66, 0 0 20px #4466cc33; + + /* Text glow — original (kept) */ + --text-glow-teal: 0 0 6px #6DC3BB99, 0 0 14px #6DC3BB55; + --text-glow-orange: 0 0 6px #F2963A99, 0 0 14px #F2963A55; + --text-glow-pink: 0 0 6px #B5308299, 0 0 14px #B5308255; + + /* Text glow — new palette */ + --text-glow-green: 0 0 6px #00ed3f99, 0 0 14px #00ed3f55; + --text-glow-amber: 0 0 6px #ff880099, 0 0 14px #ff880055; + --text-glow-red: 0 0 6px #cc220099, 0 0 14px #cc220055; + --text-glow-blue: 0 0 6px #4466cc99, 0 0 14px #4466cc55; + + /* Corner notch clip-paths */ + --clip-corner-sm: polygon(6px 0%, 100% 0%, 100% calc(100% - 6px), calc(100% - 6px) 100%, 0% 100%, 0% 6px); + --clip-corner-md: polygon(10px 0%, 100% 0%, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0% 100%, 0% 10px); + --clip-corner-lg: polygon(16px 0%, 100% 0%, 100% calc(100% - 16px), calc(100% - 16px) 100%, 0% 100%, 0% 16px); + + /* Geometry */ + --border-width: 1px; +} + +/* ============================================================ + KEYFRAME ANIMATIONS + ============================================================ */ +@keyframes flicker { + 0%, 89%, 91%, 93%, 100% { opacity: 1; } + 90% { opacity: 0.4; } + 92% { opacity: 0.8; } + 94% { opacity: 0.6; } +} + +@keyframes blink-cursor { + 0%, 49% { opacity: 1; } + 50%, 100% { opacity: 0; } +} + +@keyframes glitch { + 0%, 100% { transform: translate(0); } + 10% { transform: translate(-2px, 1px); } + 20% { transform: translate(2px, -1px); } + 30% { transform: translate(-1px, 2px); } + 40% { transform: translate(1px, -1px); } + 50% { transform: translate(0); } +} + +@keyframes scanline-move { + 0% { transform: translateY(-100%); } + 100% { transform: translateY(100vh); } +} + +@keyframes pulse-glow { + 0%, 100% { opacity: 0.6; } + 50% { opacity: 1; } +} + +@keyframes data-scroll { + 0% { transform: translateY(0); } + 100% { transform: translateY(-50%); } +} + +/* ============================================================ + BASE RESET + ============================================================ */ +*, +*::before, +*::after { + box-sizing: border-box; + border-radius: 0 !important; +} + +html { + font-size: 14px; + -webkit-text-size-adjust: 100%; +} + +body { + margin: 0; + padding: 0; + background-color: var(--background); + color: var(--text-secondary); + font-family: var(--font-mono); + font-size: 1rem; + line-height: 1.6; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +#root { + min-height: 100vh; +} + +button, +input, +textarea, +select { + font-family: var(--font-mono); + font-size: inherit; + border-radius: 0; +} + +a { + color: var(--text-primary); + text-decoration: none; +} + +a:hover { + text-shadow: var(--text-glow-green); +} + +h1, h2, h3, h4, h5, h6 { + font-family: var(--font-mono); + font-weight: 700; + letter-spacing: 0.05em; + color: var(--text-secondary); + margin: 0; +} + +p { + margin: 0; +} + +/* Scrollbar */ +::-webkit-scrollbar { + width: 5px; + height: 5px; +} +::-webkit-scrollbar-track { + background: var(--background); +} +::-webkit-scrollbar-thumb { + background: var(--border); +} +::-webkit-scrollbar-thumb:hover { + background: var(--border-active); + box-shadow: var(--glow-green); +} + +/* Text selection */ +::selection { + background: #00ed3f; + color: #050505; +} + +/* ============================================================ + UTILITY CLASSES + ============================================================ */ + +/* CRT scanline overlay — apply to panels/containers */ +.scanlines { + position: relative; + overflow: hidden; +} +.scanlines::after { + content: ''; + position: absolute; + inset: 0; + background: repeating-linear-gradient( + to bottom, + transparent 0px, + transparent 3px, + rgba(0, 0, 0, 0.12) 3px, + rgba(0, 0, 0, 0.12) 4px + ); + pointer-events: none; + z-index: 10; +} + +/* Phosphor flicker text */ +.flicker { + animation: flicker 8s infinite; +} + +/* Glitch on hover */ +.glitch-hover:hover { + animation: glitch 0.3s ease-in-out; +} + +/* Pulsing glow */ +.pulse-glow { + animation: pulse-glow 2s ease-in-out infinite; +} + +/* Corner notch variants */ +.clip-corner-sm { + clip-path: var(--clip-corner-sm); +} +.clip-corner-md { + clip-path: var(--clip-corner-md); +} +.clip-corner-lg { + clip-path: var(--clip-corner-lg); +} + +/* ============================================================ + THEME: STAR WARS + Holographic blue primary · bone/gold warnings · teal info + Deep blue-black backgrounds + ============================================================ */ +[data-theme="star-wars"] { + /* Remap color primitives used by components */ + --color-green: #1a6dff; /* blue hologram = "active" */ + --color-amber: #E0D5BE; /* bone/gold = "warning" */ + --color-red: #cc0000; /* Sith red = "danger" */ + --color-blue: #6DC3BB; /* teal = "info" */ + + /* Backgrounds — deep blue-black */ + --background: #020509; + --surface: #060E18; + --surface-raised: #0C1826; + + /* Borders */ + --border: #152030; + --border-active: #1a6dff; + + /* Text */ + --text-primary: #1a6dff; + --text-secondary: #E0D5BE; + --text-muted: #2A3A52; + --text-warning: #E0D5BE; + --text-danger: #cc0000; + + /* Accent shortcuts */ + --accent-primary: #1a6dff; + --accent-secondary: #6DC3BB; + --accent-warning: #E0D5BE; + + /* Glow — remap green→blue, amber→bone, blue→teal */ + --glow-green: 0 0 8px rgba(26,109,255,0.40), 0 0 20px rgba(26,109,255,0.20); + --glow-amber: 0 0 8px rgba(224,213,190,0.35), 0 0 20px rgba(224,213,190,0.15); + --glow-red: 0 0 8px rgba(204,0,0,0.40), 0 0 20px rgba(204,0,0,0.20); + --glow-blue: 0 0 8px rgba(109,195,187,0.40), 0 0 20px rgba(109,195,187,0.20); + + /* Text glow — remap */ + --text-glow-green: 0 0 6px rgba(26,109,255,0.60), 0 0 14px rgba(26,109,255,0.30); + --text-glow-amber: 0 0 6px rgba(224,213,190,0.55), 0 0 14px rgba(224,213,190,0.25); + --text-glow-red: 0 0 6px rgba(204,0,0,0.60), 0 0 14px rgba(204,0,0,0.30); + --text-glow-blue: 0 0 6px rgba(109,195,187,0.60), 0 0 14px rgba(109,195,187,0.30); +} + +[data-theme="star-wars"] ::selection { + background: #1a6dff; + color: #020509; +} + +/* ============================================================ + THEME: ALIEN + Bone/ivory primary · amber warnings · acid green info + Warm organic dark backgrounds + ============================================================ */ +[data-theme="alien"] { + /* Remap color primitives used by components */ + --color-green: #E0D5BE; /* bone = "active" (bleached, eerie) */ + --color-amber: #ff8800; /* amber = "warning" (Nostromo lights) */ + --color-red: #cc2200; /* blood red = "danger" */ + --color-blue: #00ed3f; /* acid green = "info" (bioluminescent) */ + + /* Backgrounds — warm organic dark */ + --background: #040302; + --surface: #080604; + --surface-raised: #0E0A06; + + /* Borders */ + --border: #201808; + --border-active: #E0D5BE; + + /* Text */ + --text-primary: #E0D5BE; + --text-secondary: #C8B99A; + --text-muted: #403020; + --text-warning: #ff8800; + --text-danger: #cc2200; + + /* Accent shortcuts */ + --accent-primary: #E0D5BE; + --accent-secondary: #00ed3f; + --accent-warning: #ff8800; + + /* Glow — remap green→bone, blue→acid-green */ + --glow-green: 0 0 8px rgba(224,213,190,0.35), 0 0 20px rgba(224,213,190,0.15); + --glow-amber: 0 0 8px rgba(255,136,0,0.40), 0 0 20px rgba(255,136,0,0.20); + --glow-red: 0 0 8px rgba(204,34,0,0.40), 0 0 20px rgba(204,34,0,0.20); + --glow-blue: 0 0 8px rgba(0,237,63,0.40), 0 0 20px rgba(0,237,63,0.20); + + /* Text glow — remap */ + --text-glow-green: 0 0 6px rgba(224,213,190,0.55), 0 0 14px rgba(224,213,190,0.25); + --text-glow-amber: 0 0 6px rgba(255,136,0,0.60), 0 0 14px rgba(255,136,0,0.30); + --text-glow-red: 0 0 6px rgba(204,34,0,0.60), 0 0 14px rgba(204,34,0,0.30); + --text-glow-blue: 0 0 6px rgba(0,237,63,0.60), 0 0 14px rgba(0,237,63,0.30); +} + +[data-theme="alien"] ::selection { + background: #E0D5BE; + color: #040302; +} diff --git a/app/icon.svg b/app/icon.svg new file mode 100644 index 0000000..34a4efa --- /dev/null +++ b/app/icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..ce6dfd6 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,15 @@ +import type { Metadata } from 'next'; +import './globals.css'; + +export const metadata: Metadata = { + title: 'SpaceWanderer', + description: 'Turn-based space adventure — build & expand your ship, survive the void.', +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..658166b --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,9 @@ +import { MissionControl } from '@/components/mission-control'; + +export const metadata = { + title: 'SpaceWanderer', +}; + +export default function Home() { + return ; +} diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..2c4a8ee --- /dev/null +++ b/bun.lock @@ -0,0 +1,445 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "space-wanderer", + "dependencies": { + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "drizzle-orm": "^0.45.2", + "next": "^16.3.5", + "react": "^19.3.0", + "react-dom": "^19.3.0", + "sql.js": "^1.14.2", + "tailwind-merge": "^3.5.0", + }, + "devDependencies": { + "@playwright/test": "1.63.0", + "@tailwindcss/postcss": "^4.2.0", + "@types/node": "^22.20.3", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@types/sql.js": "^1.4.11", + "tailwindcss": "^4.2.0", + "typescript": "^7.0.2", + "vitest": "^5.0.1", + }, + }, + }, + "packages": { + "@alloc/quick-lru": ["@alloc/quick-lru@5.3.0", "", {}, "sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], + + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.4" }, "os": "darwin", "cpu": "x64" }, "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw=="], + + "@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.5", "", { "dependencies": { "@img/sharp-wasm32": "0.35.5" }, "os": "freebsd" }, "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.4", "", { "os": "linux", "cpu": "arm" }, "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.4", "", { "os": "linux", "cpu": "none" }, "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.4" }, "os": "linux", "cpu": "arm" }, "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.4" }, "os": "linux", "cpu": "arm64" }, "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.4" }, "os": "linux", "cpu": "none" }, "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.4" }, "os": "linux", "cpu": "s390x" }, "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.4" }, "os": "linux", "cpu": "x64" }, "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" }, "os": "linux", "cpu": "arm64" }, "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.4" }, "os": "linux", "cpu": "x64" }, "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.5", "", { "dependencies": { "@emnapi/runtime": "^1.11.3" } }, "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ=="], + + "@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.5", "", { "dependencies": { "@img/sharp-wasm32": "0.35.5" }, "cpu": "none" }, "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.5", "", { "os": "win32", "cpu": "x64" }, "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@next/env": ["@next/env@16.3.6", "", {}, "sha512-x9Vblze1EbtltQYnNH38xCPWU3TVfBd1eXqA3+w9+BTpedkkdNpAaltXlGQ/nsc1+E0mVTNrtcbX3GoO09zeLQ=="], + + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.3.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-E/7GEqaUkt8mk/T8v9lAnrhzR06kdq1ZBkC12F8tAMkdIadwNp3H1KqHynDHrpcTlGCUdq/qu6vUL2aYVyYBdw=="], + + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.3.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-yBE893/nDWTlaiBD1p+qgt7NUen4U5R6FXyH0s67Npq1S3E0cVSef1WIXC2xBRgQvwAvJq6DnS6Y6PrY0cy4Ew=="], + + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.3.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-KJDpjBqBPYlvkivmyrp+Qys6k/7ksbqGQvRVc6ZEGfR+cjQxx+nUkJaWmNZJsmoOrqYNbaXByF8wa0lBwDhB3Q=="], + + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.3.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-mqNg2K+hvWskSRb/QM+Ix412DvBsuSF0XV+frTSw5vmoucNnIlynFwKYew8D01bfATErMOM7Bujrf0BA5DRKFA=="], + + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.3.6", "", { "os": "linux", "cpu": "x64" }, "sha512-nFncBNGAYouRHjRVaITs9beZRfhX4ssVwpnvPIAbkZVH6LtGoAVlH4bJ8Cnf9SOo9bsXgPFer/GdHtEE3JNOkw=="], + + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.3.6", "", { "os": "linux", "cpu": "x64" }, "sha512-5Mf3cHDGR/Iz0ng2Bj3zUR3p5QS9YK3Hn2QiAfavFmyF48zwThAjpFoiTKNIcOHLYS4zEk+gzyJ/9deQ2ZB8yQ=="], + + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.3.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-0jkJy0C2kbrJWTk4YLa3xk80pVBpx8FCHJym7CnUfDAXe/FWv5qT7SQJbR0KuemyxaEDlEx5WT4VQJoTW+/9Qw=="], + + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.3.6", "", { "os": "win32", "cpu": "x64" }, "sha512-/YXjI1e5OXcZ7YpxRwgP/1jAV/SBKTzeVKqN2mk7mLpcICsyn3Gl5+dIfDTJp70M0ccMhyMMRso4v6mPDCGepg=="], + + "@oxc-project/types": ["@oxc-project/types@0.151.0", "", {}, "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA=="], + + "@playwright/test": ["@playwright/test@1.63.0", "", { "dependencies": { "playwright": "1.63.0" }, "bin": { "playwright": "cli.js" } }, "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ=="], + + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.7", "", {}, "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q=="], + + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.15", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA=="], + + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.5", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA=="], + + "@radix-ui/react-context": ["@radix-ui/react-context@1.2.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA=="], + + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg=="], + + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.4", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA=="], + + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.10", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw=="], + + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.10", "", { "dependencies": { "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg=="], + + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.19", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-collection": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-is-hydrated": "0.1.3", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ=="], + + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q=="], + + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.21", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-roving-focus": "1.1.19", "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog=="], + + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ=="], + + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-use-effect-event": "0.0.5", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ=="], + + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.5", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg=="], + + "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw=="], + + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw=="], + + "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.11", "", { "os": "android", "cpu": "arm" }, "sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw=="], + + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.11", "", { "os": "android", "cpu": "arm64" }, "sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.11", "", { "os": "linux", "cpu": "arm" }, "sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.11", "", { "os": "linux", "cpu": "ppc64" }, "sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.11", "", { "os": "linux", "cpu": "s390x" }, "sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.11", "", { "os": "linux", "cpu": "x64" }, "sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.11", "", { "os": "linux", "cpu": "x64" }, "sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.11", "", { "os": "none", "cpu": "arm64" }, "sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.11", "", { "os": "win32", "cpu": "x64" }, "sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], + + "@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.3", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.3", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="], + + "@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.3", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "postcss": "^8.5.16", "tailwindcss": "4.3.3" } }, "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/emscripten": ["@types/emscripten@1.41.6", "", {}, "sha512-uN+9i8bFT5CUcZfyIEYDrSueACEyKGbUs5kC/72DGlZZoinh84sJfVV0i8UOJD1asdzkvLPBRrKs41kZ8MdEXg=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "@types/node": ["@types/node@22.20.4", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-zJRE40jpHtKqE/C4fgHrAKQLJuSpzEnP9ff9Y7YtoR3Wd2pwqzlekDeEuUQXjRd+QCYnVnNwuJYmhdk9XV8gvA=="], + + "@types/react": ["@types/react@19.3.0", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg=="], + + "@types/react-dom": ["@types/react-dom@19.3.0", "", { "peerDependencies": { "@types/react": "^19.3.0" } }, "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q=="], + + "@types/sql.js": ["@types/sql.js@1.4.11", "", { "dependencies": { "@types/emscripten": "*", "@types/node": "*" } }, "sha512-QXIx38p2ZThJaK9vP5ZdqdlRe1FG9I8SmCZOS7FHfB/2qPAjZwkL7/vlfPg6N/oWHuuOaGg/P/IRwfP2W0kWVQ=="], + + "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], + + "@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="], + + "@typescript/typescript-darwin-x64": ["@typescript/typescript-darwin-x64@7.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="], + + "@typescript/typescript-freebsd-arm64": ["@typescript/typescript-freebsd-arm64@7.0.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="], + + "@typescript/typescript-freebsd-x64": ["@typescript/typescript-freebsd-x64@7.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="], + + "@typescript/typescript-linux-arm": ["@typescript/typescript-linux-arm@7.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="], + + "@typescript/typescript-linux-arm64": ["@typescript/typescript-linux-arm64@7.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="], + + "@typescript/typescript-linux-loong64": ["@typescript/typescript-linux-loong64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="], + + "@typescript/typescript-linux-mips64el": ["@typescript/typescript-linux-mips64el@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="], + + "@typescript/typescript-linux-ppc64": ["@typescript/typescript-linux-ppc64@7.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="], + + "@typescript/typescript-linux-riscv64": ["@typescript/typescript-linux-riscv64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="], + + "@typescript/typescript-linux-s390x": ["@typescript/typescript-linux-s390x@7.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="], + + "@typescript/typescript-linux-x64": ["@typescript/typescript-linux-x64@7.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="], + + "@typescript/typescript-netbsd-arm64": ["@typescript/typescript-netbsd-arm64@7.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="], + + "@typescript/typescript-netbsd-x64": ["@typescript/typescript-netbsd-x64@7.0.2", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="], + + "@typescript/typescript-openbsd-arm64": ["@typescript/typescript-openbsd-arm64@7.0.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="], + + "@typescript/typescript-openbsd-x64": ["@typescript/typescript-openbsd-x64@7.0.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="], + + "@typescript/typescript-sunos-x64": ["@typescript/typescript-sunos-x64@7.0.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="], + + "@typescript/typescript-win32-arm64": ["@typescript/typescript-win32-arm64@7.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="], + + "@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="], + + "@vitest/mocker": ["@vitest/mocker@5.0.2", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.31", "@vitest/spy": "5.0.2", "estree-walker": "^3.0.3", "magic-string": "^1.2.3" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-Z5FS00Q1SJHkB35xATsmWGdQ5WA1/0MV3CDjqyv7GavHv1OfOj145MNfHOlHk7QLes21dKFDHr8EO2zvL+9WGA=="], + + "@vitest/spy": ["@vitest/spy@5.0.2", "", {}, "sha512-Ijc7T1nT9efNb5LxvjaBrEqw3f/QwUv5EE0nKqZxgqsaV/FxAAZ8baGylA8X/Z2oS4Lp+K74Jr6dTJsDKxJDeg=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.11.26", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-GLQdD3y6UF8iVuMJl5fHgE4jdn/ua7n+toKfLgNlg3BqQtOZjpy68T8Tup8/wGWZCDlm7KMg7tPb4MPn7oN0TQ=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001813", "", {}, "sha512-zfjJo4rM0+fUomGDBW/xcDjhIwz/210DGvip2MAMDZ8KHcRPnOHmEgHPZP0UHlZoxr8fYKVJOqcORhYQcG4FKQ=="], + + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + + "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "drizzle-orm": ["drizzle-orm@0.45.3", "", { "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", "@netlify/db": ">=0.4.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" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@netlify/db", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "prisma", "sql.js", "sqlite3"] }, "sha512-CAloER21cDdcgZ1OmjrZX82EeRNsdP+y0onX/eyoYDaYzki/adrAa91lzM5jWh7zZPK4qUbw/6LGv9J+A+uizA=="], + + "enhanced-resolve": ["enhanced-resolve@5.25.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w=="], + + "es-module-lexer": ["es-module-lexer@2.3.2", "", {}, "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], + + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "magic-string": ["magic-string@1.4.2", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g=="], + + "nanoid": ["nanoid@3.3.19", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug=="], + + "next": ["next@16.3.6", "", { "dependencies": { "@next/env": "16.3.6", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.5.23", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.3.6", "@next/swc-darwin-x64": "16.3.6", "@next/swc-linux-arm64-gnu": "16.3.6", "@next/swc-linux-arm64-musl": "16.3.6", "@next/swc-linux-x64-gnu": "16.3.6", "@next/swc-linux-x64-musl": "16.3.6", "@next/swc-win32-arm64-msvc": "16.3.6", "@next/swc-win32-x64-msvc": "16.3.6", "sharp": "^0.35.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-L+otWM/aQbYTx98aZhgEoMb4bZAXx1YVW4UMA/vuCyCoWG5HJyZUili8QAkqzrcC+5///tsz3s0M+SlyB5bLMw=="], + + "obug": ["obug@2.2.1", "", {}, "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], + + "playwright": ["playwright@1.63.0", "", { "dependencies": { "playwright-core": "1.63.0" }, "bin": { "playwright": "cli.js" } }, "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg=="], + + "playwright-core": ["playwright-core@1.63.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg=="], + + "postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="], + + "react": ["react@19.3.0", "", {}, "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog=="], + + "react-dom": ["react-dom@19.3.0", "", { "dependencies": { "scheduler": "^0.28.0" }, "peerDependencies": { "react": "^19.3.0" } }, "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q=="], + + "rolldown": ["rolldown@1.2.11", "", { "dependencies": { "@oxc-project/types": "=0.151.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.11", "@rolldown/binding-android-arm64": "1.2.11", "@rolldown/binding-darwin-arm64": "1.2.11", "@rolldown/binding-darwin-x64": "1.2.11", "@rolldown/binding-freebsd-x64": "1.2.11", "@rolldown/binding-linux-arm-gnueabihf": "1.2.11", "@rolldown/binding-linux-arm64-gnu": "1.2.11", "@rolldown/binding-linux-arm64-musl": "1.2.11", "@rolldown/binding-linux-ppc64-gnu": "1.2.11", "@rolldown/binding-linux-s390x-gnu": "1.2.11", "@rolldown/binding-linux-x64-gnu": "1.2.11", "@rolldown/binding-linux-x64-musl": "1.2.11", "@rolldown/binding-openharmony-arm64": "1.2.11", "@rolldown/binding-win32-arm64-msvc": "1.2.11", "@rolldown/binding-win32-x64-msvc": "1.2.11" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw=="], + + "scheduler": ["scheduler@0.28.0", "", {}, "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw=="], + + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "sharp": ["sharp@0.35.5", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.5", "@img/sharp-darwin-x64": "0.35.5", "@img/sharp-freebsd-wasm32": "0.35.5", "@img/sharp-libvips-darwin-arm64": "1.3.4", "@img/sharp-libvips-darwin-x64": "1.3.4", "@img/sharp-libvips-linux-arm": "1.3.4", "@img/sharp-libvips-linux-arm64": "1.3.4", "@img/sharp-libvips-linux-ppc64": "1.3.4", "@img/sharp-libvips-linux-riscv64": "1.3.4", "@img/sharp-libvips-linux-s390x": "1.3.4", "@img/sharp-libvips-linux-x64": "1.3.4", "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", "@img/sharp-libvips-linuxmusl-x64": "1.3.4", "@img/sharp-linux-arm": "0.35.5", "@img/sharp-linux-arm64": "0.35.5", "@img/sharp-linux-ppc64": "0.35.5", "@img/sharp-linux-riscv64": "0.35.5", "@img/sharp-linux-s390x": "0.35.5", "@img/sharp-linux-x64": "0.35.5", "@img/sharp-linuxmusl-arm64": "0.35.5", "@img/sharp-linuxmusl-x64": "0.35.5", "@img/sharp-webcontainers-wasm32": "0.35.5", "@img/sharp-win32-arm64": "0.35.5", "@img/sharp-win32-ia32": "0.35.5", "@img/sharp-win32-x64": "0.35.5" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "sql.js": ["sql.js@1.14.2", "", {}, "sha512-3ZGPovObMFrdw79zrUHbfdE/DLIsy8jdNdssmMSQuRAymedU6q84asPt0kgiqrdMYlPegDItiIMfmIXzZnYFcw=="], + + "std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="], + + "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "@babel/core": "*", "babel-plugin-macros": "*", "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" }, "optionalPeers": ["@babel/core", "babel-plugin-macros"] }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], + + "tailwind-merge": ["tailwind-merge@3.7.0", "", {}, "sha512-XPPUyAc+cvspz3lHTcR/QgPfW2A0lv/xQNIjX3HGhLR+Nq2lHaLq5MtTesHn8GUr3W3DguT2KT5x3NVgRtYwmA=="], + + "tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="], + + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], + + "tinybench": ["tinybench@6.2.0", "", {}, "sha512-78U2TlB2CnVenajOFzf3BKSm0J6oz5L0NV7g32LCPccvYc0lbWvys4d3uUUCS2B1N8PAf2+aekR8i1KbC3HO7Q=="], + + "tinyexec": ["tinyexec@1.3.1", "", {}, "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "vite": ["vite@8.3.1", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.28", "rolldown": "~1.2.9", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.7.1", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA=="], + + "vitest": ["vitest@5.0.2", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/mocker": "5.0.2", "chai": "^6.2.2", "es-module-lexer": "^2.3.2", "expect-type": "^1.4.0", "magic-string": "^1.2.3", "obug": "^2.1.4", "picomatch": "^4.0.7", "std-env": "^4.2.0", "tinybench": "^6.1.4", "tinyexec": "^1.3.0", "tinyglobby": "^0.2.17", "why-is-node-running": "^3.2.1" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "5.0.2", "@vitest/browser-preview": "5.0.2", "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", "@vitest/coverage-istanbul": "5.0.2", "@vitest/coverage-v8": "5.0.2", "@vitest/ui": "5.0.2", "happy-dom": "*", "jsdom": "*", "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-7MQrx9pDv5aHiUcovIb/70Ys3tgtkUVgCtledvKdCmEO+/1Dicq5ZqoSxOW034m03oqC+oHOKui2dM6qtMLoJg=="], + + "why-is-node-running": ["why-is-node-running@3.2.2", "", { "bin": { "why-is-node-running": "cli.js" } }, "sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A=="], + + "@tailwindcss/node/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" }, "bundled": true }, "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.4", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@types/sql.js/@types/node": ["@types/node@26.6.3", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg=="], + + "next/postcss": ["postcss@8.5.23", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg=="], + + "vite/lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], + + "@types/sql.js/@types/node/undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="], + + "vite/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], + + "vite/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="], + + "vite/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="], + + "vite/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="], + + "vite/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="], + + "vite/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="], + + "vite/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="], + + "vite/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="], + + "vite/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="], + + "vite/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="], + + "vite/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], + } +} diff --git a/components/mission-control.tsx b/components/mission-control.tsx new file mode 100644 index 0000000..bceb7f4 --- /dev/null +++ b/components/mission-control.tsx @@ -0,0 +1,653 @@ +'use client' + +import { useEffect, useState } from 'react' +import { Badge } from '@/components/ui/badge' +import { BarChart } from '@/components/ui/bar-chart' +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' +import { LineChart } from '@/components/ui/line-chart' +import { Panel, PanelContent, PanelHeader, PanelTitle } from '@/components/ui/panel' +import { ProgressRing } from '@/components/ui/progress-ring' +import { StatCard } from '@/components/ui/stat-card' +import { StatusGrid, type SystemEntry } from '@/components/ui/status-grid' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { Terminal, type TerminalLine } from '@/components/ui/terminal' + +// Interface base: layout + visual treatment rebuilt from the scificn-ui Mission Control +// showcase (src/app/_pages/showcase/mission-control-content.tsx in baxy5/scificn-ui), with +// placeholder SpaceWanderer content — no game logic yet. Single player, no auth, by design. + +function useNarrow(threshold = 900): boolean { + const [narrow, setNarrow] = useState( + () => typeof window !== 'undefined' && window.innerWidth < threshold + ) + useEffect(() => { + const handle = () => setNarrow(window.innerWidth < threshold) + window.addEventListener('resize', handle, { passive: true }) + return () => window.removeEventListener('resize', handle) + }, [threshold]) + return narrow +} + +function now(): string { + return new Date().toLocaleTimeString('en-GB', { hour12: false }) +} + +// ── Placeholder content (no game logic — real values land with the turn engine) ── + +const shipSystems: SystemEntry[] = [ + { name: 'REACTOR', status: 'ACTIVE', detail: '94.2%' }, + { name: 'LIFE SUPPORT', status: 'ACTIVE' }, + { name: 'NAV COMPUTER', status: 'SCANNING' }, + { name: 'WEAPONS', status: 'OFFLINE' }, + { name: 'COMMS', status: 'ACTIVE' }, +] + +const bayOutput = [ + { label: 'PWR', value: 14 }, + { label: 'NAV', value: 9 }, + { label: 'COM', value: 6 }, + { label: 'WPN', value: 3 }, + { label: 'LSS', value: 12 }, + { label: 'ENG', value: 11 }, + { label: 'BAY', value: 7 }, + { label: 'MED', value: 5 }, +] + +const hullTrack = [100, 98, 97, 96, 94, 95, 92, 90, 89, 87] +const hullLabels = hullTrack.map((_, i) => `T-${hullTrack.length - 1 - i}`) + +const encounters = [ + { name: 'DERELICT FREIGHTER', when: 'T-07:14', id: '#E-102' }, + { name: 'PIRATE SKIRMISH', when: 'T-22:40', id: '#E-103' }, + { name: 'ANOMALY FIELD', when: 'T-48:12', id: '#E-104' }, +] + +const shipData = [ + { label: 'TOTAL MODULES', value: '34' }, + { label: 'HULL INTEGRITY', value: '87%' }, + { label: 'REACTOR OUTPUT', value: '94.2%' }, + { label: 'CREW ACTIVE', value: '3 / 6' }, + { label: 'SECTOR', value: '7G // DRIFT LANE' }, + { label: 'DRIFT VELOCITY', value: '0.4 KM/S' }, + { label: 'TURN', value: '12' }, +] + +const logLines: TerminalLine[] = [ + { type: 'system', text: 'SPACEWANDERER // INITIALIZING SHIP SYSTEMS...', timestamp: now() }, + { type: 'output', text: 'HULL SCAN COMPLETE — INTEGRITY 87%', timestamp: now() }, + { type: 'output', text: 'REACTOR OUTPUT STABLE AT 94.2%', timestamp: now() }, + { type: 'output', text: 'CREW MANIFEST LOADED — 3 ACTIVE PERSONNEL', timestamp: now() }, + { type: 'warn', text: 'WEAPONS OFFLINE — MODULE NOT INSTALLED', timestamp: now() }, + { type: 'system', text: 'ALL SYSTEMS NOMINAL. AWAITING FIRST TURN.', timestamp: now() }, +] + +export function MissionControl() { + const narrow = useNarrow() + const [tick, setTick] = useState(0) + + useEffect(() => { + const id = setInterval(() => setTick((t) => t + 1), 1000) + return () => clearInterval(id) + }, []) + + const timeStr = new Date().toLocaleTimeString('en-GB', { hour12: false }) + + return ( +
+ {/* ── HEADER ── */} +
+ + ◄ SHIP + + + │ + +

+ SPACEWANDERER +

+ + {!narrow && ( + + // SHIP STATUS // CREW MANIFEST + + )} + +
+ + {!narrow && ( + SHIP NOMINAL + )} + + SECTOR SCAN + + + {timeStr} + +
+ + {/* ── BODY ── */} +
+ {/* STAT CARDS — placeholder ship readouts */} +
+ + + + +
+ + {/* 3-COLUMN GRID */} +
+ {/* ── LEFT: SHIP SYSTEMS + VESSEL METRICS ── */} +
+ + + SHIP SYSTEMS +
+ 4 ONLINE +
+
+ + +
+ + + + VESSEL METRICS + + +
+ + + +
+ +
+ {[ + { label: 'HULL INTEGRITY', note: 'last scan T-02' }, + { label: 'REACTOR OUTPUT', note: 'idle cycle' }, + { label: 'CREW ROSTER', note: 'of 6 berths' }, + ].map((item) => ( +
+
+ {item.label} +
+
+ {item.note} +
+
+ ))} +
+
+
+
+ + {/* ── CENTER: OPERATIONS TABS ── */} +
+ + + OPERATIONS // SHIP COMMAND +
+ SYS NOMINAL +
+
+ + + + + SYSTEMS + NEXT MISSION + LOG + HULL TRACK + + + {/* SYSTEMS TAB */} + + +
+ {[ + { label: 'TOTAL MODULES', value: 34 }, + { label: 'ONLINE', value: 29 }, + { label: 'EFFICIENCY', value: '85%' }, + ].map((s) => ( +
+
+ {s.label} +
+
+ {s.value} +
+
+ ))} +
+
+ + {/* NEXT MISSION TAB */} + +
+ + ENCOUNTER #E-102 — DERELICT FREIGHTER + + Placeholder briefing. Real encounter resolution lands with the turn engine. + + + +
+ {[ + { label: 'DESTINATION', value: 'SECTOR 7G // DRIFT LANE' }, + { label: 'ETA', value: 'T-07:14 (PLACEHOLDER)' }, + { label: 'THREAT LEVEL', value: 'LOW — UNMANNED' }, + { label: 'CARGO MANIFEST', value: 'UNKNOWN' }, + ].map((item) => ( +
+
+ {item.label} +
+
+ {item.value} +
+
+ ))} +
+
+
+ + {/* LOG TAB */} + + + + + {/* HULL TRACK TAB */} + + + +
+
+
+
+ + {/* ── RIGHT: UPCOMING ENCOUNTERS + SHIP DATA ── */} +
+ + + UPCOMING ENCOUNTERS +
+ {encounters.length} QUEUED +
+
+ +
+ {encounters.map((enc, i) => { + const isNext = i === 0 + const isLast = i === encounters.length - 1 + return ( +
+
+
+
+ {enc.name} +
+
+ {enc.when} // {enc.id} +
+
+ + {isNext ? 'NEXT' : 'SCHED'} + +
+
+ ) + })} +
+
+
+ + + + SHIP DATA + + +
+ {shipData.map((item, i) => ( +
+ + {item.label} + + + {item.value} + +
+ ))} +
+
+
+
+
+
+ + {/* ── FOOTER ── */} +
+ + INTERFACE BASE: SCIFICN MISSION CONTROL + + │ + + CONTENT: PLACEHOLDER — NO GAME LOGIC YET + + │ + + SINGLE PLAYER // NO AUTH + + +
+ + SYS ONLINE +
+
+ ) +} diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx new file mode 100644 index 0000000..0e4f310 --- /dev/null +++ b/components/ui/alert.tsx @@ -0,0 +1,119 @@ +import * as React from 'react' +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '@/lib/utils' + +const alertVariants = cva( + [ + 'relative w-full', + 'border border-solid border-l-[3px]', + 'p-4', + 'font-mono', + 'rounded-none', + ], + { + variants: { + variant: { + STATUS: [ + 'border-[var(--border)]', + 'border-l-[var(--color-green)]', + 'bg-[var(--surface)]', + ], + WARNING: [ + 'border-[var(--border)]', + 'border-l-[var(--color-amber)]', + 'bg-[var(--surface)]', + ], + CRITICAL: [ + 'border-[var(--border)]', + 'border-l-[var(--color-red)]', + 'bg-[var(--surface)]', + ], + INFO: [ + 'border-[var(--border)]', + 'border-l-[var(--color-blue)]', + 'bg-[var(--surface)]', + ], + }, + }, + defaultVariants: { variant: 'STATUS' }, + } +) + +const prefixMap = { + STATUS: { symbol: '◈', color: 'var(--color-green)', glow: 'var(--text-glow-green)' }, + WARNING: { symbol: '⚠', color: 'var(--color-amber)', glow: 'var(--text-glow-amber)' }, + CRITICAL: { symbol: '✕', color: 'var(--color-red)', glow: 'var(--text-glow-red)' }, + INFO: { symbol: 'ℹ', color: 'var(--color-blue)', glow: 'var(--text-glow-blue)' }, +} + +export interface AlertProps + extends React.HTMLAttributes, + VariantProps {} + +const Alert = React.forwardRef( + ({ className, variant = 'STATUS', children, ...props }, ref) => { + const v = variant ?? 'STATUS' + const prefix = prefixMap[v] + + return ( +
+
+ + {prefix.symbol} + +
{children}
+
+
+ ) + } +) +Alert.displayName = 'Alert' + +/* ── AlertTitle ── */ +const AlertTitle = React.forwardRef>( + ({ className, style, ...props }, ref) => ( +
+ ) +) +AlertTitle.displayName = 'AlertTitle' + +/* ── AlertDescription ── */ +const AlertDescription = React.forwardRef>( + ({ className, style, ...props }, ref) => ( +

+ ) +) +AlertDescription.displayName = 'AlertDescription' + +export { Alert, AlertTitle, AlertDescription, alertVariants } diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx new file mode 100644 index 0000000..29c7f69 --- /dev/null +++ b/components/ui/badge.tsx @@ -0,0 +1,83 @@ +import * as React from 'react' +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '@/lib/utils' + +const badgeVariants = cva( + [ + 'inline-flex items-center gap-1.5', + 'px-2 py-0.5', + 'text-[0.65rem] font-mono font-medium uppercase tracking-widest', + 'border border-solid', + 'rounded-none', + 'whitespace-nowrap', + ], + { + variants: { + variant: { + ACTIVE: [ + 'border-[var(--color-green)]', + 'text-[var(--color-green)]', + ], + OFFLINE: [ + 'border-[var(--border)]', + 'text-[var(--text-muted)]', + ], + WARNING: [ + 'border-[var(--color-amber)]', + 'text-[var(--color-amber)]', + ], + CRITICAL: [ + 'border-[var(--color-red)]', + 'text-[var(--color-red)]', + ], + SCANNING: [ + 'border-[var(--color-green)]', + 'text-[var(--color-green)]', + ], + }, + }, + defaultVariants: { + variant: 'ACTIVE', + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, children, ...props }: BadgeProps) { + const isScanning = variant === 'SCANNING' + + return ( + + {isScanning && ( + + ● + + )} + {!isScanning && variant === 'ACTIVE' && ●} + {!isScanning && variant === 'OFFLINE' && ○} + {!isScanning && variant === 'WARNING' && ⚠} + {!isScanning && variant === 'CRITICAL' && ✕} + {children} + + ) +} + +export { Badge, badgeVariants } diff --git a/components/ui/bar-chart.tsx b/components/ui/bar-chart.tsx new file mode 100644 index 0000000..5c578ff --- /dev/null +++ b/components/ui/bar-chart.tsx @@ -0,0 +1,257 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' + +export type BarChartVariant = 'DEFAULT' | 'ACTIVE' | 'WARNING' | 'CRITICAL' + +export interface BarChartEntry { + label: string + value: number +} + +export interface BarChartProps extends React.HTMLAttributes { + data: BarChartEntry[] + orientation?: 'horizontal' | 'vertical' + variant?: BarChartVariant + title?: string + showValues?: boolean + /** Override the max value for scaling. Defaults to max(data). */ + max?: number +} + +const VERTICAL_CHART_HEIGHT = 144 // px — 9rem + +const BarChart = React.forwardRef( + ( + { + className, + data, + orientation = 'horizontal', + variant = 'ACTIVE', + title, + showValues = true, + max, + style, + ...props + }, + ref + ) => { + const computedMax = max ?? Math.max(...data.map((d) => d.value), 1) + + const accentColor = + variant === 'ACTIVE' ? 'var(--color-green)' : + variant === 'WARNING' ? 'var(--color-amber)' : + variant === 'CRITICAL' ? 'var(--color-red)' : + 'var(--text-secondary)' + + const accentGlow = + variant === 'ACTIVE' ? 'var(--glow-green)' : + variant === 'WARNING' ? 'var(--glow-amber)' : + variant === 'CRITICAL' ? 'var(--glow-red)' : + 'none' + + return ( +

+ {/* Title bar */} + {title && ( +
+ {title} +
+ )} + + {orientation === 'horizontal' ? ( + /* ── Horizontal bars ── */ +
+ {data.map((entry, i) => { + const pct = Math.max((entry.value / computedMax) * 100, 0) + return ( +
+ {/* Label */} + + {entry.label} + + + {/* Track */} +
+
+
+ + {/* Value */} + {showValues && ( + + {entry.value} + + )} +
+ ) + })} +
+ ) : ( + /* ── Vertical bars ── */ +
+ {/* Bar area */} +
+ {data.map((entry, i) => { + const pct = Math.max((entry.value / computedMax) * 100, 0) + const barPx = Math.round((pct / 100) * VERTICAL_CHART_HEIGHT) + + return ( +
+ {/* Value above bar */} + {showValues && ( + + {entry.value} + + )} + + {/* Bar */} +
0 ? barPx : 0, + minHeight: entry.value > 0 ? 2 : 0, + background: accentColor, + boxShadow: accentGlow, + opacity: 0.85, + flexShrink: 0, + transition: 'height 0.4s ease', + }} + /> +
+ ) + })} +
+ + {/* Labels row */} +
+ {data.map((entry, i) => ( +
+ {entry.label} +
+ ))} +
+
+ )} +
+ ) + } +) +BarChart.displayName = 'BarChart' + +export { BarChart } diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000..a71ca92 --- /dev/null +++ b/components/ui/button.tsx @@ -0,0 +1,91 @@ +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '@/lib/utils' + +const buttonVariants = cva( + [ + 'inline-flex items-center justify-center', + 'font-mono font-medium uppercase tracking-widest', + 'border border-solid', + 'cursor-pointer select-none', + 'transition-all duration-150', + 'focus-visible:outline-none', + 'disabled:opacity-40 disabled:cursor-not-allowed disabled:pointer-events-none', + 'rounded-none', + 'whitespace-nowrap', + ], + { + variants: { + variant: { + EXEC: [ + 'bg-transparent', + 'border-[var(--color-green)]', + 'text-[var(--color-green)]', + 'hover:bg-[var(--color-green)]', + 'hover:text-[var(--color-bg)]', + 'hover:shadow-[var(--glow-green)]', + 'focus-visible:shadow-[var(--glow-green)]', + ], + GHOST: [ + 'bg-transparent', + 'border-transparent', + 'text-[var(--text-secondary)]', + 'hover:text-[var(--color-green)]', + 'hover:border-[var(--border)]', + 'focus-visible:border-[var(--border)]', + ], + OUTLINE: [ + 'bg-[var(--surface)]', + 'border-[var(--border)]', + 'text-[var(--text-secondary)]', + 'hover:border-[var(--border-active)]', + 'hover:text-[var(--color-green)]', + 'hover:shadow-[var(--glow-green)]', + 'focus-visible:border-[var(--border-active)]', + 'focus-visible:shadow-[var(--glow-green)]', + ], + ABORT: [ + 'bg-transparent', + 'border-[var(--color-red)]', + 'text-[var(--color-red)]', + 'hover:bg-[var(--color-red)]', + 'hover:text-[var(--color-bg)]', + 'hover:shadow-[var(--glow-red)]', + 'focus-visible:shadow-[var(--glow-red)]', + ], + }, + size: { + SM: 'h-7 px-3 text-[0.65rem] gap-1.5', + MD: 'h-9 px-4 text-[0.75rem] gap-2', + LG: 'h-11 px-6 text-[0.875rem] gap-2.5', + }, + }, + defaultVariants: { + variant: 'OUTLINE', + size: 'MD', + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button' + return ( + + ) + } +) +Button.displayName = 'Button' + +export { Button, buttonVariants } diff --git a/components/ui/card.tsx b/components/ui/card.tsx new file mode 100644 index 0000000..4a959a5 --- /dev/null +++ b/components/ui/card.tsx @@ -0,0 +1,73 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' + +const Card = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +Card.displayName = 'Card' + +const CardHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardHeader.displayName = 'CardHeader' + +const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +

+ ) +) +CardTitle.displayName = 'CardTitle' + +const CardDescription = React.forwardRef>( + ({ className, ...props }, ref) => ( +

+ ) +) +CardDescription.displayName = 'CardDescription' + +const CardContent = React.forwardRef>( + ({ className, ...props }, ref) => ( +

+ ) +) +CardContent.displayName = 'CardContent' + +const CardFooter = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardFooter.displayName = 'CardFooter' + +export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } diff --git a/components/ui/line-chart.tsx b/components/ui/line-chart.tsx new file mode 100644 index 0000000..66f62e2 --- /dev/null +++ b/components/ui/line-chart.tsx @@ -0,0 +1,312 @@ +'use client' + +import * as React from 'react' +import { cn } from '@/lib/utils' + +export type LineChartVariant = 'DEFAULT' | 'ACTIVE' | 'WARNING' | 'CRITICAL' + +export interface LineChartSeries { + id: string + label: string + data: number[] + variant?: LineChartVariant +} + +export interface LineChartProps extends React.HTMLAttributes { + series: LineChartSeries[] + labels: string[] + title?: string + height?: number + min?: number + max?: number + showDots?: boolean + showArea?: boolean + showGrid?: boolean + showLegend?: boolean + animated?: boolean +} + +const LEFT = 40 +const RIGHT = 8 +const TOP = 8 +const BOTTOM = 24 + +const SERIES_COLORS = [ + 'var(--color-green)', + 'var(--color-amber)', + 'var(--color-red)', + 'var(--color-blue)', +] + +const VARIANT_COLORS: Record = { + DEFAULT: 'var(--text-secondary)', + ACTIVE: 'var(--color-green)', + WARNING: 'var(--color-amber)', + CRITICAL: 'var(--color-red)', +} + +function seriesColor(s: LineChartSeries, idx: number): string { + if (s.variant) return VARIANT_COLORS[s.variant] + return SERIES_COLORS[idx % SERIES_COLORS.length] +} + +const LineChart = React.forwardRef( + ( + { + className, + series, + labels, + title, + height = 160, + min, + max, + showDots = true, + showArea = false, + showGrid = true, + showLegend, + animated = true, + style, + ...props + }, + ref + ) => { + const containerRef = React.useRef(null) + const [svgW, setSvgW] = React.useState(500) + const [drawn, setDrawn] = React.useState(false) + + React.useEffect(() => { + if (!containerRef.current) return + const ro = new ResizeObserver(entries => { + setSvgW(entries[0].contentRect.width) + }) + ro.observe(containerRef.current) + return () => ro.disconnect() + }, []) + + React.useEffect(() => { + const id = requestAnimationFrame(() => setDrawn(true)) + return () => cancelAnimationFrame(id) + }, []) + + const allValues = series.flatMap(s => s.data) + const dataMin = allValues.length > 0 ? Math.min(...allValues) : 0 + const dataMax = allValues.length > 0 ? Math.max(...allValues) : 100 + const minV = min ?? dataMin + const maxV = max ?? dataMax + const range = Math.max(maxV - minV, 1) + + const plotW = svgW - LEFT - RIGHT + const plotH = height - TOP - BOTTOM + const n = labels.length + + function xPos(i: number) { + return LEFT + (n <= 1 ? plotW / 2 : (i / (n - 1)) * plotW) + } + + function yPos(v: number) { + return TOP + (1 - (v - minV) / range) * plotH + } + + function buildLinePath(data: number[]): string { + return data + .map((v, i) => `${i === 0 ? 'M' : 'L'} ${xPos(i).toFixed(1)} ${yPos(v).toFixed(1)}`) + .join(' ') + } + + function buildAreaPath(data: number[]): string { + if (data.length === 0) return '' + const line = buildLinePath(data) + const firstX = xPos(0).toFixed(1) + const lastX = xPos(data.length - 1).toFixed(1) + const bottomY = (TOP + plotH).toFixed(1) + return `${line} L ${lastX} ${bottomY} L ${firstX} ${bottomY} Z` + } + + const gridFracs = [0, 1 / 3, 2 / 3, 1] + const showLeg = showLegend ?? series.length > 1 + const step = Math.max(1, Math.ceil(n / 8)) + + const shouldAnimate = animated + + return ( +
+ {title && ( +
+ {title} +
+ )} + +
+ + {/* Grid */} + {showGrid && gridFracs.map((f, gi) => { + const y = TOP + f * plotH + const val = minV + (1 - f) * range + return ( + + + + {val.toFixed(0)} + + + ) + })} + + {/* X-axis labels */} + {labels.map((label, i) => { + if (i % step !== 0 && i !== n - 1) return null + return ( + + {label} + + ) + })} + + {/* Series */} + {series.map((s, si) => { + if (s.data.length === 0) return null + const color = seriesColor(s, si) + const linePath = buildLinePath(s.data) + const areaPath = buildAreaPath(s.data) + + return ( + + {/* Area fill */} + {showArea && ( + + )} + + {/* Glow layer */} + + + {/* Main line */} + + + {/* Dots */} + {showDots && s.data.map((v, i) => ( + + ))} + + ) + })} + +
+ + {/* Legend */} + {showLeg && ( +
+ {series.map((s, si) => { + const color = seriesColor(s, si) + return ( +
+
+ + {s.label} + +
+ ) + })} +
+ )} +
+ ) + } +) +LineChart.displayName = 'LineChart' + +export { LineChart } diff --git a/components/ui/panel.tsx b/components/ui/panel.tsx new file mode 100644 index 0000000..189b63b --- /dev/null +++ b/components/ui/panel.tsx @@ -0,0 +1,112 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' + +/* ── Panel (outer container) ── */ +export interface PanelProps extends React.HTMLAttributes { + notch?: 'sm' | 'md' | 'lg' | 'none' +} + +const Panel = React.forwardRef( + ({ className, notch = 'md', style, ...props }, ref) => { + const clipMap = { + sm: 'var(--clip-corner-sm)', + md: 'var(--clip-corner-md)', + lg: 'var(--clip-corner-lg)', + none: 'none', + } + + return ( +
+ ) + } +) +Panel.displayName = 'Panel' + +/* ── PanelHeader ── */ +const PanelHeader = React.forwardRef>( + ({ className, style, ...props }, ref) => ( +
+ ) +) +PanelHeader.displayName = 'PanelHeader' + +/* ── PanelTitle ── */ +const PanelTitle = React.forwardRef>( + ({ className, style, ...props }, ref) => ( +

+ ) +) +PanelTitle.displayName = 'PanelTitle' + +/* ── PanelContent ── */ +const PanelContent = React.forwardRef>( + ({ className, style, ...props }, ref) => ( +
+ ) +) +PanelContent.displayName = 'PanelContent' + +/* ── PanelFooter ── */ +const PanelFooter = React.forwardRef>( + ({ className, style, ...props }, ref) => ( +
+ ) +) +PanelFooter.displayName = 'PanelFooter' + +export { Panel, PanelHeader, PanelTitle, PanelContent, PanelFooter } diff --git a/components/ui/progress-ring.tsx b/components/ui/progress-ring.tsx new file mode 100644 index 0000000..1b00352 --- /dev/null +++ b/components/ui/progress-ring.tsx @@ -0,0 +1,158 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' + +export type ProgressRingVariant = 'DEFAULT' | 'ACTIVE' | 'WARNING' | 'CRITICAL' + +export interface ProgressRingProps extends React.HTMLAttributes { + value: number + size?: number + strokeWidth?: number + label?: string + showValue?: boolean + variant?: ProgressRingVariant +} + +const ProgressRing = React.forwardRef( + ( + { + className, + value = 0, + size = 120, + strokeWidth = 6, + label, + showValue = true, + variant = 'DEFAULT', + style, + ...props + }, + ref + ) => { + const pct = Math.min(Math.max(value, 0), 100) + const cx = size / 2 + const cy = size / 2 + const r = (size - strokeWidth) / 2 + const circ = 2 * Math.PI * r + const offset = circ * (1 - pct / 100) + + const accentColor = + variant === 'ACTIVE' ? 'var(--color-green)' : + variant === 'WARNING' ? 'var(--color-amber)' : + variant === 'CRITICAL' ? 'var(--color-red)' : + 'var(--text-secondary)' + + const textGlow = + variant === 'ACTIVE' ? 'var(--text-glow-green)' : + variant === 'WARNING' ? 'var(--text-glow-amber)' : + variant === 'CRITICAL' ? 'var(--text-glow-red)' : + 'none' + + const isSmall = size < 80 + + return ( +
+ + + {/* Center label */} +
+ {showValue && ( + + {Math.round(pct)}% + + )} + {label && ( + + {label} + + )} +
+
+ ) + } +) +ProgressRing.displayName = 'ProgressRing' + +export { ProgressRing } diff --git a/components/ui/stat-card.tsx b/components/ui/stat-card.tsx new file mode 100644 index 0000000..daddc40 --- /dev/null +++ b/components/ui/stat-card.tsx @@ -0,0 +1,146 @@ +import * as React from 'react' +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '@/lib/utils' + +const statCardVariants = cva( + [ + 'relative flex flex-col font-mono', + 'border border-solid rounded-none', + ], + { + variants: { + variant: { + DEFAULT: ['border-[var(--border)]'], + ACTIVE: ['border-[var(--color-green)]'], + WARNING: ['border-[var(--color-amber)]'], + CRITICAL: ['border-[var(--color-red)]'], + }, + }, + defaultVariants: { variant: 'DEFAULT' }, + } +) + +export interface StatCardProps + extends React.HTMLAttributes, + VariantProps { + label: string + value: string | number + /** Delta text, e.g. "+2.3%" */ + delta?: string + /** true = positive (▲ green), false = negative (▼ red), undefined = neutral */ + deltaPositive?: boolean + /** Small status line shown at the bottom of the card */ + sublabel?: string +} + +const StatCard = React.forwardRef( + ( + { + className, + variant = 'DEFAULT', + label, + value, + delta, + deltaPositive, + sublabel, + style, + ...props + }, + ref + ) => { + const accentColor = + variant === 'ACTIVE' ? 'var(--color-green)' : + variant === 'WARNING' ? 'var(--color-amber)' : + variant === 'CRITICAL' ? 'var(--color-red)' : + 'var(--text-secondary)' + + const accentGlow = + variant === 'ACTIVE' ? 'var(--text-glow-green)' : + variant === 'WARNING' ? 'var(--text-glow-amber)' : + variant === 'CRITICAL' ? 'var(--text-glow-red)' : + 'none' + + const deltaColor = + deltaPositive === true ? 'var(--color-green)' : + deltaPositive === false ? 'var(--color-red)' : + 'var(--text-muted)' + + const deltaSymbol = + deltaPositive === true ? '▲ ' : + deltaPositive === false ? '▼ ' : + '— ' + + return ( +
+ {/* Label */} +
+ {label} +
+ + {/* Value */} +
+ {value} +
+ + {/* Delta */} + {delta !== undefined && ( +
+ {deltaSymbol}{delta} +
+ )} + + {/* Sublabel */} + {sublabel && ( +
+ {sublabel} +
+ )} +
+ ) + } +) +StatCard.displayName = 'StatCard' + +export { StatCard, statCardVariants } diff --git a/components/ui/status-grid.tsx b/components/ui/status-grid.tsx new file mode 100644 index 0000000..035d3ba --- /dev/null +++ b/components/ui/status-grid.tsx @@ -0,0 +1,158 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' + +export type SystemStatus = 'ACTIVE' | 'OFFLINE' | 'WARNING' | 'CRITICAL' | 'SCANNING' + +export interface SystemEntry { + name: string + status: SystemStatus + /** Optional secondary text shown beside the name */ + detail?: string +} + +export interface StatusGridProps extends React.HTMLAttributes { + systems: SystemEntry[] + title?: string + columns?: 1 | 2 | 3 +} + +function statusColor(s: SystemStatus): string { + switch (s) { + case 'ACTIVE': return 'var(--color-green)' + case 'OFFLINE': return 'var(--text-muted)' + case 'WARNING': return 'var(--color-amber)' + case 'CRITICAL': return 'var(--color-red)' + case 'SCANNING': return 'var(--color-green)' + } +} + +function statusGlow(s: SystemStatus): string { + switch (s) { + case 'ACTIVE': return 'var(--glow-green)' + case 'SCANNING': return 'var(--glow-green)' + case 'WARNING': return 'var(--glow-amber)' + case 'CRITICAL': return 'var(--glow-red)' + default: return 'none' + } +} + +const StatusGrid = React.forwardRef( + ({ className, systems, title, columns = 1, style, ...props }, ref) => { + return ( +
+ {/* Header */} + {title && ( +
+ {title} +
+ )} + + {/* Grid of rows */} +
+ {systems.map((system, i) => { + const color = statusColor(system.status) + const glow = statusGlow(system.status) + const isScanning = system.status === 'SCANNING' + + // border logic: no bottom border on last row(s), no right on last column + const totalRows = Math.ceil(systems.length / columns) + const rowIndex = Math.floor(i / columns) + const colIndex = i % columns + const isLastRow = rowIndex === totalRows - 1 + const isLastCol = colIndex === columns - 1 + + return ( +
+ {/* Name + optional detail */} + + {system.name} + {system.detail && ( + + {system.detail} + + )} + + + {/* Status dot + label */} +
+ + + {system.status} + +
+
+ ) + })} +
+
+ ) + } +) +StatusGrid.displayName = 'StatusGrid' + +export { StatusGrid } diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx new file mode 100644 index 0000000..6a6a0b9 --- /dev/null +++ b/components/ui/tabs.tsx @@ -0,0 +1,72 @@ +import * as React from 'react' +import * as TabsPrimitive from '@radix-ui/react-tabs' +import { cn } from '@/lib/utils' + +const Tabs = TabsPrimitive.Root +const TabsGroup = TabsPrimitive.List // alias + +/* ── List ── */ +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +/* ── Trigger ── */ +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +/* ── Content ── */ +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsGroup, TabsList, TabsTrigger, TabsContent } diff --git a/components/ui/terminal.tsx b/components/ui/terminal.tsx new file mode 100644 index 0000000..b5a8f7f --- /dev/null +++ b/components/ui/terminal.tsx @@ -0,0 +1,187 @@ +'use client' + +import * as React from 'react' +import { cn } from '@/lib/utils' + +export type TerminalLineType = 'input' | 'output' | 'warn' | 'error' | 'system' + +export interface TerminalLine { + type?: TerminalLineType + text: string + timestamp?: string +} + +export interface TerminalProps extends React.HTMLAttributes { + lines?: TerminalLine[] + prompt?: string + title?: string + height?: string | number + blinkCursor?: boolean +} + +function lineColor(type: TerminalLineType = 'output'): string { + switch (type) { + case 'input': return 'var(--color-green)' + case 'output': return 'var(--text-secondary)' + case 'warn': return 'var(--color-amber)' + case 'error': return 'var(--color-red)' + case 'system': return 'var(--text-muted)' + } +} + +function linePrefix(type: TerminalLineType = 'output'): string { + switch (type) { + case 'input': return '>' + case 'output': return ' ' + case 'warn': return '⚠' + case 'error': return '✕' + case 'system': return '#' + } +} + +const Terminal = React.forwardRef( + ( + { + className, + lines = [], + prompt = '>', + title = 'TERMINAL', + height = '16rem', + blinkCursor = true, + style, + ...props + }, + ref + ) => { + const bodyRef = React.useRef(null) + + React.useEffect(() => { + if (bodyRef.current) { + bodyRef.current.scrollTop = bodyRef.current.scrollHeight + } + }, [lines]) + + return ( +
+ {/* Title bar */} +
+ + {title} + + {/* Status dot */} + +
+ + {/* Output body */} +
+ {lines.map((line, i) => ( +
+ {line.timestamp && ( + + {line.timestamp} + + )} + + {linePrefix(line.type)} + + {line.text} +
+ ))} + + {/* Blinking cursor */} + {blinkCursor && ( +
+ {prompt} + +
+ )} +
+
+ ) + } +) +Terminal.displayName = 'Terminal' + +export { Terminal } diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml new file mode 100644 index 0000000..b086377 --- /dev/null +++ b/docker-compose.ci.yml @@ -0,0 +1,29 @@ +# Isolated e2e stack for CI (.gitea/workflows/ci.yml) — runs on the runner's shared dind +# daemon under project name space-wanderer-ci (distinct from production). Joins the external +# network space-wanderer-ci-e2e that the workflow attaches the job container to; the app is reached +# at its STATIC IP http://172.39.0.10:3000 — DNS bypassed for the browser because Chromium +# got ERR_SSL_PROTOCOL_ERROR via the hostname while node-side HTTP worked (the `app` alias +# stays as a convenience). Single deterministic address; survives `docker compose restart space-wanderer`. +# The build context is tarred by the compose client in the job and sent over DOCKER_HOST, +# so nothing here needs to be visible from inside dind; game state lives in a named volume +# (fresh per run — CI cleanup removes it — survives `docker compose restart space-wanderer` for +# e2e/state.spec.ts). +services: + space-wanderer: + build: . + image: space-wanderer-ci + volumes: + - space-wanderer-db:/app/db + networks: + space-wanderer-ci-e2e: + aliases: + - app + ipv4_address: 172.39.0.10 + +networks: + space-wanderer-ci-e2e: + name: space-wanderer-ci-e2e + external: true + +volumes: + space-wanderer-db: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f6af7ff --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +services: + space-wanderer: + build: . + volumes: + # Game state SQLite: created on first run, must survive rebuilds. + - "./db:/app/db" + networks: + - dev_net + labels: + - "traefik.enable=true" + - "traefik.http.routers.space-wanderer.rule=Host(`space-wanderer.dev.kkhost.pl`)" + - "traefik.http.routers.space-wanderer.entrypoints=web" + - "traefik.http.services.space-wanderer.loadbalancer.server.port=3000" + +networks: + dev_net: + external: true diff --git a/e2e/ci-diagnostics.ts b/e2e/ci-diagnostics.ts new file mode 100644 index 0000000..1ae1c99 --- /dev/null +++ b/e2e/ci-diagnostics.ts @@ -0,0 +1,36 @@ +// CI-only connectivity diagnostics (globalSetup runs after the e2e stack is ready). +// Evidence for the Chromium ERR_SSL_PROTOCOL_ERROR hunt: compares node-side resolution/HTTP +// against Chromium's own navigation, hostname vs static IP. Never fails the run. +import { promises as dnsPromises } from 'node:dns'; +import { chromium } from '@playwright/test'; + +export default async function ciDiagnostics() { + if (process.env.PW_REMOTE_BROWSER !== '0') return; // local runs: no-op + const base = process.env.E2E_BASE_URL ?? ''; + try { + console.log('[diag] node resolve4(app):', JSON.stringify(await dnsPromises.resolve4('app').catch((e) => `failed: ${e.code}`))); + try { + const r = await fetch(`${base}/api/version`); + console.log('[diag] node fetch', base, '->', r.status, JSON.stringify(await r.json().catch(() => null))); + } catch (e) { + console.log('[diag] node fetch failed:', String(e)); + } + const browser = await chromium.launch(); + try { + for (const url of ['http://app:3000/', base]) { + const page = await (await browser.newContext()).newPage(); + try { + await page.goto(url, { timeout: 15_000 }); + console.log('[diag] chromium goto', url, '-> OK, title:', await page.title().catch(() => '?')); + } catch (e) { + console.log('[diag] chromium goto', url, '-> FAILED:', String(e).split('\n')[0]); + } + await page.context().close(); + } + } finally { + await browser.close(); + } + } catch (e) { + console.log('[diag] diagnostics error (non-fatal):', String(e)); + } +} diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts new file mode 100644 index 0000000..1b89f6e --- /dev/null +++ b/e2e/smoke.spec.ts @@ -0,0 +1,29 @@ +import { expect, test } from '@playwright/test'; + +test('landing page loads with the SpaceWanderer title', async ({ page }) => { + await page.goto('/'); + await expect(page.getByRole('heading', { name: 'SpaceWanderer' })).toBeVisible(); +}); + +test('mission control layout renders scificn-ui components', async ({ page }) => { + await page.goto('/'); + // Stat card, tab trigger and footer badge prove the copied component layer is wired in. + await expect(page.getByText('NEXT ENCOUNTER')).toBeVisible(); + // Radix Tabs.Trigger renders role="tab", not a plain button. + await expect(page.getByRole('tab', { name: 'SYSTEMS' })).toBeVisible(); + await expect(page.getByText('SYS ONLINE')).toBeVisible(); +}); + +test('operations tabs switch panels client-side', async ({ page }) => { + await page.goto('/'); + await page.getByRole('tab', { name: 'LOG' }).click(); + await expect(page.getByText('SHIP LOG // PLACEHOLDER FEED')).toBeVisible(); +}); + +test('GET /api/version reports the app version as JSON', async ({ page }) => { + const res = await page.request.get('/api/version'); + expect(res.status()).toBe(200); + expect(res.headers()['content-type']).toContain('application/json'); + const body = (await res.json()) as { version: string }; + expect(body.version).toMatch(/^\d+\.\d+\.\d+$/); +}); diff --git a/e2e/state.spec.ts b/e2e/state.spec.ts new file mode 100644 index 0000000..0e0a046 --- /dev/null +++ b/e2e/state.spec.ts @@ -0,0 +1,43 @@ +import { execSync } from 'node:child_process'; +import path from 'node:path'; +import { expect, test } from '@playwright/test'; + +// Repo root — docker-compose.yml lives here; `docker compose restart` runs on +// this host (the test runner), browsers stay remote. CI points E2E_COMPOSE_ARGS at the +// isolated stack file + project (-f docker-compose.ci.yml -p space-wanderer-ci) instead of the default one. +const repoRoot = path.join(__dirname, '..'); +const composeArgs = process.env.E2E_COMPOSE_ARGS ?? ''; + +test('app survives a container restart (stack + volume persistence)', async ({ page }) => { + test.setTimeout(180_000); + + // Baseline: the app serves and reports its version. + const resBefore = await page.request.get('/api/version'); + expect(resBefore.status()).toBe(200); + const before = (await resBefore.json()) as { version: string }; + expect(before.version).toMatch(/^\d+\.\d+\.\d+$/); + + // Restart the app container mid-run — the named DB volume must survive it. + execSync(`docker compose ${composeArgs} restart space-wanderer`, { cwd: repoRoot, stdio: 'pipe' }); + + // Wait for the app to come back serving. + let after: { version: string } | undefined; + await expect + .poll(async () => { + try { + const res = await page.request.get('/api/version'); + if (res.status() === 200) after = (await res.json()) as { version: string }; + return !!after; + } catch { + return false; // connection refused / 502 while the container starts + } + }, { timeout: 120_000, message: 'waiting for /api/version after docker compose restart' }) + .toBe(true); + + expect(after).toBeDefined(); + expect(after!.version).toBe(before.version); // same build, not a re-seeded stack + + // The page itself serves again with the landing title. + await page.goto('/'); + await expect(page.getByRole('heading', { name: 'SpaceWanderer' })).toBeVisible(); +}); diff --git a/instrumentation.ts b/instrumentation.ts new file mode 100644 index 0000000..8e31b2e --- /dev/null +++ b/instrumentation.ts @@ -0,0 +1,10 @@ +// Open the game DB once at server boot (Next ≥15 instrumentation hook; runs under `next dev` +// and `next start`) so the ./db volume file exists from first start, before any request. +// Dynamic import inside the runtime guard: lib/db/client.ts uses node:fs + sql.js, which must +// never be pulled into the Edge Runtime instrumentation bundle (Next bundles register() for both). +export async function register() { + if (process.env.NEXT_RUNTIME === 'nodejs') { + const { getGameDb } = await import('@/lib/db/client'); + void getGameDb(); + } +} diff --git a/lib/db/client.test.ts b/lib/db/client.test.ts new file mode 100644 index 0000000..e5bd553 --- /dev/null +++ b/lib/db/client.test.ts @@ -0,0 +1,51 @@ +import { mkdtemp, rm, stat } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { afterAll, describe, expect, it } from 'vitest'; +import { openGameDb, persist } from './client'; + +const dir = await mkdtemp(path.join(tmpdir(), 'sw-db-test-')); +const file = path.join(dir, 'test.db'); + +afterAll(async () => { + await rm(dir, { recursive: true, force: true }); +}); + +function metaValue(db: Awaited>, key: string): string | undefined { + const rows = db.raw.exec('SELECT value FROM _meta WHERE key = ?', [key]); + return rows[0]?.values[0][0] as string | undefined; +} + +describe('db client (sql.js/Drizzle plumbing)', () => { + it('creates a valid DB file on disk when opening a fresh path', async () => { + const db = await openGameDb(file); + expect(db.file).toBe(file); + const s = await stat(file); + expect(s.size).toBeGreaterThan(0); // persisted at first open, not left in-memory only + expect(metaValue(db, 'created_at')).toMatch(/^\d{4}-\d{2}-\d{2}T/); + }); + + it('preserves state across reopen (volume persistence)', async () => { + const created = metaValue(await openGameDb(file), 'created_at'); + expect(created).toBeDefined(); + const db = await openGameDb(file); // second process would see the same file + expect(db.raw.exec('PRAGMA integrity_check')[0].values[0][0]).toBe('ok'); + expect(metaValue(db, 'created_at')).toBe(created); // not re-stamped on reopen + }); + + it('persist() rewrites the file atomically (tmp + rename)', async () => { + const db = await openGameDb(file); + await persist(db); + const s = await stat(file); + expect(s.size).toBeGreaterThan(0); + // no leftover tmp file after a clean persist + let tmpGone = true; + try { + await stat(`${file}.tmp`); + tmpGone = false; + } catch { + /* expected */ + } + expect(tmpGone).toBe(true); + }); +}); diff --git a/lib/db/client.ts b/lib/db/client.ts new file mode 100644 index 0000000..54b2984 --- /dev/null +++ b/lib/db/client.ts @@ -0,0 +1,75 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { drizzle, type SQLJsDatabase } from 'drizzle-orm/sql-js'; +import initSqlJs, { type Database as RawSqlJsDatabase } from 'sql.js'; +import * as schema from './schema'; + +// SQLite via sql.js (WASM) — no native compilation, so it builds and runs in the +// Docker image without a toolchain. Trade-off: the DB lives in memory and every write +// must be exported to disk; persist() does that atomically (tmp file + rename). + +export interface GameDb { + db: SQLJsDatabase; + raw: RawSqlJsDatabase; + file: string; +} + +// CREATE TABLE statements mirroring lib/db/schema.ts — keep in sync. Game tables land with +// the turn engine; _meta is DB plumbing (not game state): a brand-new sql.js database exports +// as ZERO bytes, so without at least one real table there would be nothing valid to persist +// on the ./db volume from first boot. +const DDL = ` +CREATE TABLE IF NOT EXISTS _meta (key TEXT PRIMARY KEY, value TEXT); +`; + +// SPACE_WANDERER_DB_PATH override exists so unit tests can point at a temp file instead +// of the real ./db/space-wanderer.db. +export function defaultDbPath(): string { + return process.env.SPACE_WANDERER_DB_PATH ?? path.join(process.cwd(), 'db', 'space-wanderer.db'); +} + +async function open(file: string): Promise { + const SQL = await initSqlJs(); + let bytes: Buffer | undefined; + try { + bytes = await fs.readFile(file); + } catch (err) { + if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err; + } + const raw = bytes ? new SQL.Database(new Uint8Array(bytes)) : new SQL.Database(); + raw.exec(DDL); // no-op for tables that already exist in an older file + + const instance: GameDb = { db: drizzle(raw, { schema }), raw, file }; + + // First run: stamp when this DB was created and land the file on disk so the ./db volume + // mount has a valid SQLite file to persist from boot (instrumentation.ts opens at server start). + if (!bytes) { + raw.run('INSERT OR IGNORE INTO _meta (key, value) VALUES (?, ?)', ['created_at', new Date().toISOString()]); + await persist(instance); + } + return instance; +} + +// Durably save the in-memory DB: export bytes, write to a temp file, rename over +// the target so a crash mid-write never leaves a truncated DB. +export async function persist(instance: GameDb): Promise { + const bytes = instance.raw.export(); + await fs.mkdir(path.dirname(instance.file), { recursive: true }); + const tmp = `${instance.file}.tmp`; + await fs.writeFile(tmp, Buffer.from(bytes)); + await fs.rename(tmp, instance.file); +} + +// Singleton for the app process; openGameDb() is the uncached variant (tests). +// Lives on globalThis because Next.js can load this module more than once per process — +// instrumentation.ts and route handlers get separate module instances, so a plain `let` +// would give each its own in-memory sql.js DB (both persisting to the same file). One world per process. +const g = globalThis as unknown as { __spaceWandererGameDb?: Promise }; + +export function getGameDb(): Promise { + return (g.__spaceWandererGameDb ??= open(defaultDbPath())); +} + +export async function openGameDb(file: string): Promise { + return open(file); +} diff --git a/lib/db/schema.ts b/lib/db/schema.ts new file mode 100644 index 0000000..f7f7410 --- /dev/null +++ b/lib/db/schema.ts @@ -0,0 +1,6 @@ +// Drizzle schema for game state. EMPTY on purpose at scaffold stage (v0.1.0) — no game +// tables yet; the first turn-engine/ship/crew tables land with the first game systems. +// Column names/types here are the source of truth; matching CREATE TABLE statements live +// in lib/db/client.ts (DDL) and must stay in sync with this file. + +export {}; diff --git a/lib/openapi.test.ts b/lib/openapi.test.ts new file mode 100644 index 0000000..1a2c35b --- /dev/null +++ b/lib/openapi.test.ts @@ -0,0 +1,27 @@ +import pkg from '@/package.json'; +import { describe, expect, it } from 'vitest'; +import { openApiSpec } from './openapi'; + +describe('OpenAPI spec', () => { + it('documents exactly the one public endpoint (scaffold stage)', () => { + expect(Object.keys(openApiSpec.paths).sort()).toEqual(['/api/version']); + }); + + it('keeps info.version in sync with package.json', () => { + expect(openApiSpec.info.title).toBe('SpaceWanderer API'); + expect(openApiSpec.info.version).toBe(pkg.version); + }); + + it('uses an empty server url so "try it" targets the current origin', () => { + expect(openApiSpec.servers).toEqual([{ url: '' }]); + }); + + it('describes /api/version as { version } with a string version', () => { + const schema = openApiSpec.paths['/api/version'].get.responses[200].content['application/json'].schema; + expect(schema.$ref).toBe('#/components/schemas/AppVersion'); + + const appVersion = openApiSpec.components.schemas.AppVersion.properties; + expect(Object.keys(appVersion)).toEqual(['version']); + expect(appVersion.version.type).toBe('string'); + }); +}); diff --git a/lib/openapi.ts b/lib/openapi.ts new file mode 100644 index 0000000..e2e9262 --- /dev/null +++ b/lib/openapi.ts @@ -0,0 +1,47 @@ +import pkg from '@/package.json'; + +// OpenAPI 3.1 document for the app's public JSON API, kept in code so it can be +// served machine-readable at /api/openapi (see AGENTS.md "## API"). Must be updated +// whenever an endpoint changes. +export const openApiSpec = { + openapi: '3.1.0', + info: { + title: 'SpaceWanderer API', + description: + 'JSON API for SpaceWanderer — a single-player turn-based space adventure. Scaffold stage: app metadata only; game endpoints (turn actions, state snapshots) land with the first game systems.', + // Sourced from package.json so it stays in sync with every version bump. + version: pkg.version, + }, + // Empty url = current origin (the spec is served from the same host that runs the API). + servers: [{ url: '' }], + paths: { + '/api/version': { + get: { + summary: 'App version', + description: 'The running app version, read from package.json.', + operationId: 'getVersion', + responses: { + 200: { + description: 'Current app version', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/AppVersion' }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + AppVersion: { + type: 'object', + properties: { + version: { type: 'string', description: 'Semver of the running build' }, + }, + required: ['version'], + }, + }, + }, +}; diff --git a/lib/utils.ts b/lib/utils.ts new file mode 100644 index 0000000..b057456 --- /dev/null +++ b/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]): string { + return twMerge(clsx(inputs)) +} diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 0000000..ce4e94a --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,7 @@ +/// +/// +import "./.next/types/routes.d.ts"; +import "./.next/types/root-params.d.ts"; + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/next.config.ts b/next.config.ts new file mode 100644 index 0000000..9f5d608 --- /dev/null +++ b/next.config.ts @@ -0,0 +1,10 @@ +import type { NextConfig } from 'next'; + +const nextConfig: NextConfig = { + // sql.js loads its SQLite WASM blob relative to its own dist/ dir at runtime; + // bundling it into a .next chunk would break that resolution. Keeping it + // external means require('sql.js') hits node_modules (kept in the image). + serverExternalPackages: ['sql.js'], +}; + +export default nextConfig; diff --git a/package.json b/package.json new file mode 100644 index 0000000..45b1db7 --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "space-wanderer", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev -p 3000", + "build": "next build", + "start": "next start -p 3000", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "drizzle-orm": "^0.45.2", + "next": "^16.3.5", + "react": "^19.3.0", + "react-dom": "^19.3.0", + "sql.js": "^1.14.2", + "tailwind-merge": "^3.5.0" + }, + "devDependencies": { + "@playwright/test": "1.63.0", + "@tailwindcss/postcss": "^4.2.0", + "tailwindcss": "^4.2.0", + "@types/node": "^22.20.3", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@types/sql.js": "^1.4.11", + "typescript": "^7.0.2", + "vitest": "^5.0.1" + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..132cb39 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,33 @@ +import { defineConfig } from '@playwright/test'; + +// Browsers run on the shared remote Playwright server by default (AGENTS.md). +// PW_REMOTE_BROWSER=0 → local browsers instead (CI, or boxes without LAN access to 192.168.1.4). +const remoteBrowser = process.env.PW_REMOTE_BROWSER !== '0'; + +if (!process.env.PW_TEST_CONNECT_WS_ENDPOINT && remoteBrowser) { + process.env.PW_TEST_CONNECT_WS_ENDPOINT = 'ws://192.168.1.4:3000/'; +} + +// App under test. Default: the live compose instance behind Traefik, deployed manually +// before running the suite (AGENTS.md). When E2E_DEPLOY_CMD is set it runs as a webServer +// and the suite waits for baseURL to come up — CI uses this to target an isolated stack +// inside dind instead of production. All vars optional; unset = current local behavior. +const deployCmd = process.env.E2E_DEPLOY_CMD; +const baseUrl = process.env.E2E_BASE_URL ?? 'https://space-wanderer.dev.kkhost.pl'; + +export default defineConfig({ + testDir: './e2e', + globalSetup: './e2e/ci-diagnostics.ts', + // Serial execution: e2e/state.spec.ts restarts the compose container mid-run, + // which would break any other spec running concurrently in another worker. + workers: 1, + timeout: 300_000, + retries: 1, + use: { + baseURL: baseUrl, + trace: 'on-first-retry', + }, + webServer: deployCmd + ? { command: deployCmd, url: baseUrl, reuseExistingServer: true, timeout: 600_000 } + : undefined, +}); diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..53808c6 --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,2 @@ +const config = { plugins: { "@tailwindcss/postcss": {} } } +export default config diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e7ff3a2 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..01cbcc6 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vitest/config'; +import path from 'node:path'; + +export default defineConfig({ + resolve: { + alias: { '@': path.resolve(process.cwd()) }, + }, + test: { + environment: 'node', + include: ['**/*.test.ts'], + }, +});