From 79e025e788aaa5abb3375faad3697904ac60a275 Mon Sep 17 00:00:00 2001 From: PARTH J ROHIT Date: Tue, 1 Sep 2026 17:55:57 +0100 Subject: [PATCH 1/4] feat(marketing): free static marketing site with a scripted product simulation (#382) Pauses the real Render/Neon backend deployment (no funding right now, revisit later -- see #375/#377, left exactly as merged/paused, untouched here) in favor of a free, static marketing/awareness push: a scripted product simulation, a "run it yourself" CTA, and a working waitlist form, none of it needing a live backend. New, independent site (apps/marketing/), not a deploy of the existing apps/frontend SPA: that app's other routes (login, register, dashboard) all need a live backend this pivot explicitly doesn't have right now, and shipping dead/broken auth flows to the public would undercut the point of a clean marketing push. apps/frontend is completely untouched. Not part of the root npm workspace either -- its own package.json/lockfile, so Vercel can build it in total isolation with Root Directory set to apps/marketing. - Hero, a scripted "Run the simulation" walkthrough, a "run it yourself" section, and a waitlist modal -- all hand-built React/Tailwind (not a deploy of the image-based main LandingPage), sharing the main product's color tokens/logo for brand consistency without any cross-package dependency. - The simulation's canned data (src/data/sampleAnalysis.ts) uses the real product's actual Engineering Review category ids and labels (cross- checked against apps/backend/app/review/review_service.py's _CATEGORY_LABELS) and severity levels, and the real Insights response shape (cross-checked against the generated OpenAPI types) -- the repository and findings themselves are fictional, but the shape and vocabulary are not invented. Labeled persistently in the UI, in the hero, and in the waitlist copy as a scripted demo against a sample repository, never implied to be a live analysis. - "Run it yourself": the exact commands from the main README's own "Run PARTHA locally" section (checked against it directly, and that section was independently verified accurate this session -- dev:backend/ dev:frontend scripts confirmed to exist and match). - Waitlist: api/waitlist.ts, a Vercel serverless function, appends submissions to a private GitHub Gist via a fine-grained PAT -- chosen over a new third-party form service since it needs no new account (only GitHub and Vercel, both already in use) and has no realistic volume limit. Validated email, a honeypot field, and a clear 503 (not a crash or silent drop) when the required env vars aren't set yet. 6 unit tests (api/waitlist.test.ts, node's test runner, fetch mocked -- no real network/GitHub call) cover method/validation/honeypot/dedup-append/ upstream-failure paths. - Typed against a minimal local request/response interface instead of importing @vercel/node: that package's own dependency chain (ajv/path-to-regexp/undici via @vercel/static-config) currently carries real advisories, for type declarations this file didn't need more than a few lines of. Zero npm audit findings as a result. Verified, not just written: npm run typecheck/lint/test/build all pass; production build inspected (four small output files, no errors); the actual page loaded in-browser, the simulation run end to end (all 5 steps, both the Review and Insights tabs, all 4 sample findings with correct severity/category rendering), and the waitlist modal opened and submitted, confirmed via zero console errors and full-page screenshots plus get_page_text extraction -- not assumed from the code alone. Deploy instructions (Vercel project creation, waitlist Gist/PAT setup) are in apps/marketing/README.md -- I cannot create the Vercel account or project myself. --- .gitignore | 4 + apps/marketing/README.md | 71 + apps/marketing/api/waitlist.test.ts | 124 + apps/marketing/api/waitlist.ts | 156 + apps/marketing/eslint.config.js | 34 + apps/marketing/index.html | 20 + apps/marketing/package-lock.json | 4065 +++++++++++++++++ apps/marketing/package.json | 37 + apps/marketing/postcss.config.js | 6 + apps/marketing/public/favicon.svg | 4 + apps/marketing/src/App.tsx | 25 + apps/marketing/src/assets/partha-logo.svg | 15 + apps/marketing/src/components/Footer.tsx | 23 + apps/marketing/src/components/Hero.tsx | 56 + .../src/components/RunItYourself.tsx | 68 + .../src/components/SimulationDemo.tsx | 231 + .../marketing/src/components/WaitlistForm.tsx | 142 + apps/marketing/src/data/sampleAnalysis.ts | 155 + apps/marketing/src/main.tsx | 10 + apps/marketing/src/styles/globals.css | 48 + apps/marketing/tailwind.config.ts | 72 + apps/marketing/tsconfig.json | 24 + apps/marketing/vercel.json | 6 + apps/marketing/vite-env.d.ts | 1 + apps/marketing/vite.config.ts | 12 + 25 files changed, 5409 insertions(+) create mode 100644 apps/marketing/README.md create mode 100644 apps/marketing/api/waitlist.test.ts create mode 100644 apps/marketing/api/waitlist.ts create mode 100644 apps/marketing/eslint.config.js create mode 100644 apps/marketing/index.html create mode 100644 apps/marketing/package-lock.json create mode 100644 apps/marketing/package.json create mode 100644 apps/marketing/postcss.config.js create mode 100644 apps/marketing/public/favicon.svg create mode 100644 apps/marketing/src/App.tsx create mode 100644 apps/marketing/src/assets/partha-logo.svg create mode 100644 apps/marketing/src/components/Footer.tsx create mode 100644 apps/marketing/src/components/Hero.tsx create mode 100644 apps/marketing/src/components/RunItYourself.tsx create mode 100644 apps/marketing/src/components/SimulationDemo.tsx create mode 100644 apps/marketing/src/components/WaitlistForm.tsx create mode 100644 apps/marketing/src/data/sampleAnalysis.ts create mode 100644 apps/marketing/src/main.tsx create mode 100644 apps/marketing/src/styles/globals.css create mode 100644 apps/marketing/tailwind.config.ts create mode 100644 apps/marketing/tsconfig.json create mode 100644 apps/marketing/vercel.json create mode 100644 apps/marketing/vite-env.d.ts create mode 100644 apps/marketing/vite.config.ts diff --git a/.gitignore b/.gitignore index a3dea978..a1b8e4e3 100644 --- a/.gitignore +++ b/.gitignore @@ -73,6 +73,10 @@ apps/backend/.env !apps/frontend/.env.example !apps/backend/.env.example +# Vercel CLI project-link metadata (apps/marketing, #382) -- not secret, but +# machine-local and not meant to be committed. +apps/marketing/.vercel + # ========================================== # Local-only strategy / roadmap docs (never commit) # The Market-Fit Roadmap is the single source of truth for product direction, diff --git a/apps/marketing/README.md b/apps/marketing/README.md new file mode 100644 index 00000000..d1c16a8e --- /dev/null +++ b/apps/marketing/README.md @@ -0,0 +1,71 @@ +# PARTHA marketing site + +A free, static marketing site for PARTHA (#382): a scripted product simulation using canned sample data, a "run it +yourself" call to action pointing at the main repository, and a waitlist form. Deliberately independent of +`apps/frontend` — no backend dependency, and nothing here talks to the real Render/Neon deployment (which is paused, +not live — see #375/#377). It builds and runs entirely on its own. + +## Local development + +```bash +cd apps/marketing +npm install +npm run dev +``` + +Opens at `http://localhost:5173` (or the next free port Vite picks). The waitlist form will show a "not configured" +error locally unless you also run `vercel dev` with the env vars below set — that's expected; the rest of the page +works fully without it. + +## Verification + +```bash +npm run typecheck # tsc -b --noEmit +npm run lint # eslint . +npm run test # node --test against api/waitlist.ts, with fetch mocked -- no real network/GitHub call +npm run build # tsc -b && vite build -- outputs to dist/ +npm run preview # serve the production build locally +``` + +## Deploying on Vercel + +This project is intentionally **not** part of the root npm workspace (`apps/frontend` is) — it has its own +`package.json`/`node_modules`, so Vercel can build it in complete isolation from the rest of the monorepo. + +1. In the Vercel dashboard: **Add New → Project**, import the `Second-Origin/PARTHA` GitHub repository. +2. Set the project's **Root Directory** to `apps/marketing`. Vercel auto-detects the Vite framework preset from + there; `vercel.json` in this directory pins the build command and output directory explicitly regardless. +3. No required env vars to deploy the site itself — the page renders and the simulation works with zero + configuration. The waitlist form needs two, added under **Settings → Environment Variables**, before it will + actually store submissions (see below). +4. Deploy. Vercel gives you a `*.vercel.app` URL immediately; a custom domain can be attached afterward under + **Settings → Domains**. + +## Waitlist form setup (one-time) + +There's no live Postgres to write submissions to (the real backend is paused). Instead, `api/waitlist.ts` — a Vercel +serverless function, deployed automatically alongside the site — appends each submission to a private GitHub Gist. +This needs no new third-party service: just a GitHub token, since the project already has a GitHub account. + +1. Create an empty **secret** Gist at , with exactly one file named `waitlist.json` + containing `[]`. Copy its id from the URL (the part after the last `/`). +2. Create a **fine-grained** GitHub Personal Access Token at + , scoped to **only** that Gist (Permissions → Gists → + Read and write). Do not use a classic token with broader `repo` scope than this needs. +3. In the Vercel project, **Settings → Environment Variables**, add: + - `WAITLIST_GITHUB_TOKEN` — the token from step 2. + - `WAITLIST_GIST_ID` — the Gist id from step 1. +4. Redeploy (Vercel → Deployments → ⋯ → Redeploy) so the function picks up the new env vars. + +Until both are set, the form fails with a clear "not configured yet" message instead of silently dropping +submissions or crashing — the rest of the site is unaffected either way. + +To read submissions later: open the Gist directly, or `curl -H "Authorization: Bearer " +https://api.github.com/gists/`. + +## What this deliberately does not do + +- Does not call the real PARTHA API — the "simulation" is entirely canned data in `src/data/sampleAnalysis.ts`, + clearly labeled in the UI as a scripted sample-repository walkthrough, not a live analysis. +- Does not create or need a PARTHA account, login, or session. +- Does not touch `apps/frontend`, the backend, or `render.yaml` — those stay exactly as they are, paused. diff --git a/apps/marketing/api/waitlist.test.ts b/apps/marketing/api/waitlist.test.ts new file mode 100644 index 00000000..a89214ac --- /dev/null +++ b/apps/marketing/api/waitlist.test.ts @@ -0,0 +1,124 @@ +/** + * Unit tests for the waitlist serverless function (#382). Run with + * `npm run test` (node's built-in test runner + tsx loader) -- no real + * network call is ever made; `global.fetch` is replaced with a fake that + * only understands the exact GitHub Gist requests this handler issues. + */ + +import assert from 'node:assert/strict'; +import { afterEach, beforeEach, describe, it, mock } from 'node:test'; +import handler, { type WaitlistRequest, type WaitlistResponse } from './waitlist.ts'; + +interface JsonErrorBody { + error?: string; + message?: string; +} + +interface JsonOkBody { + ok?: boolean; +} + +function fakeGitHubResponse(body: unknown, ok = true, status = ok ? 200 : 500) { + return { ok, status, json: async () => body }; +} + +function makeRes(): { res: WaitlistResponse; calls: { status?: number; json?: unknown } } { + const calls: { status?: number; json?: unknown } = {}; + const res: WaitlistResponse = { + status(code: number) { + calls.status = code; + return res; + }, + json(body: unknown) { + calls.json = body; + }, + setHeader() {}, + }; + return { res, calls }; +} + +function request(overrides: WaitlistRequest): WaitlistRequest { + return { method: 'POST', ...overrides }; +} + +describe('POST /api/waitlist', () => { + const originalEnv = { ...process.env }; + + beforeEach(() => { + process.env.WAITLIST_GITHUB_TOKEN = 'fake-token-not-real'; + process.env.WAITLIST_GIST_ID = 'fake-gist-id'; + }); + + afterEach(() => { + process.env = { ...originalEnv }; + mock.reset(); + }); + + it('rejects non-POST methods', async () => { + const { res, calls } = makeRes(); + await handler(request({ method: 'GET' }), res); + assert.equal(calls.status, 405); + }); + + it('returns 503 when the env vars are not configured', async () => { + delete process.env.WAITLIST_GITHUB_TOKEN; + const { res, calls } = makeRes(); + await handler(request({ body: { email: 'a@example.com' } }), res); + assert.equal(calls.status, 503); + assert.equal((calls.json as JsonErrorBody).error, 'waitlist_unconfigured'); + }); + + it('rejects a missing or malformed email', async () => { + const { res, calls } = makeRes(); + await handler(request({ body: { email: 'not-an-email' } }), res); + assert.equal(calls.status, 400); + assert.equal((calls.json as JsonErrorBody).error, 'invalid_email'); + }); + + it('silently accepts (without storing) a submission with the honeypot field filled in', async () => { + const fetchMock = mock.fn(async () => { + throw new Error('fetch must not be called for a honeypot submission'); + }); + mock.method(globalThis, 'fetch', fetchMock); + + const { res, calls } = makeRes(); + await handler(request({ body: { email: 'a@example.com', company: 'a bot filled this in' } }), res); + assert.equal(calls.status, 200); + assert.equal(fetchMock.mock.callCount(), 0); + }); + + it('appends a valid submission to the Gist, deduping by email', async () => { + const existing = [{ email: 'existing@example.com', name: null, submittedAt: '2026-01-01T00:00:00.000Z' }]; + const fetchMock = mock.fn(async (url: string | URL, init?: RequestInit) => { + if (!init || init.method === undefined) { + // GET + assert.match(String(url), /\/gists\/fake-gist-id$/); + return fakeGitHubResponse({ files: { 'waitlist.json': { content: JSON.stringify(existing) } } }); + } + // PATCH + assert.equal(init.method, 'PATCH'); + const payload = JSON.parse(String(init.body)); + const rows = JSON.parse(payload.files['waitlist.json'].content); + assert.equal(rows.length, 2); + assert.equal(rows[1].email, 'new@example.com'); + assert.equal(rows[1].name, 'New Person'); + return fakeGitHubResponse({ ok: true }); + }); + mock.method(globalThis, 'fetch', fetchMock); + + const { res, calls } = makeRes(); + await handler(request({ body: { email: 'New@Example.com', name: 'New Person' } }), res); + assert.equal(calls.status, 200); + assert.equal((calls.json as JsonOkBody).ok, true); + assert.equal(fetchMock.mock.callCount(), 2); + }); + + it('returns 502 if the GitHub API call fails', async () => { + mock.method(globalThis, 'fetch', async () => fakeGitHubResponse(null, false, 401)); + + const { res, calls } = makeRes(); + await handler(request({ body: { email: 'a@example.com' } }), res); + assert.equal(calls.status, 502); + assert.equal((calls.json as JsonErrorBody).error, 'waitlist_storage_failed'); + }); +}); diff --git a/apps/marketing/api/waitlist.ts b/apps/marketing/api/waitlist.ts new file mode 100644 index 00000000..9f0a7e5c --- /dev/null +++ b/apps/marketing/api/waitlist.ts @@ -0,0 +1,156 @@ +/** + * Waitlist submission endpoint (#382), deployed by Vercel as a serverless + * function at /api/waitlist. + * + * No live Postgres to write to (the real backend is paused, not deployed -- + * see #375/#377). Instead of introducing a new third-party account nobody + * has yet, this appends each submission to a private GitHub Gist using a + * fine-grained Personal Access Token -- both GitHub and Vercel are + * platforms the project already has accounts on, this needs no new + * service, and a Gist has no realistic volume limit for a waitlist. + * + * Required environment variables (set in the Vercel project's Environment + * Variables, never committed): + * WAITLIST_GITHUB_TOKEN -- a fine-grained PAT with read/write access to + * exactly one Gist (Gists -> Read and write). Create it at + * https://github.com/settings/personal-access-tokens/new + * WAITLIST_GIST_ID -- the id of that Gist (the segment after the last + * slash in its URL). Create an empty secret Gist first, containing one + * file named waitlist.json with the content "[]". + * + * If either is unset, this returns 503 rather than silently dropping + * submissions or crashing -- see apps/marketing/README.md for the one-time + * setup steps. + * + * Typed against a minimal local interface (below) rather than importing + * @vercel/node for VercelRequest/VercelResponse: Vercel's actual runtime + * behavior (JSON body parsing onto `.body`, the `.status().json()` response + * helpers) is unaffected by which types this file imports -- it's a + * platform behavior, not something the npm package provides at runtime -- + * and @vercel/node pulls in a deep, currently-vulnerable dev-only + * dependency chain (ajv/path-to-regexp/undici via @vercel/static-config) + * purely for type declarations this file doesn't need more than a few + * lines of. + */ + +// Exported so tests can construct properly-typed requests/responses instead +// of casting through `any`. +export interface WaitlistRequest { + method?: string; + body?: unknown; +} + +export interface WaitlistResponse { + status(code: number): WaitlistResponse; + json(body: unknown): void; + setHeader(name: string, value: string): void; +} + +const GIST_FILENAME = 'waitlist.json'; +const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; +const MAX_EMAIL_LENGTH = 320; +const MAX_NAME_LENGTH = 200; + +interface WaitlistEntry { + email: string; + name: string | null; + submittedAt: string; +} + +function isValidBody(body: unknown): body is { email: string; name?: string; company?: string } { + if (typeof body !== 'object' || body === null) return false; + const record = body as Record; + return typeof record.email === 'string'; +} + +export default async function handler(req: WaitlistRequest, res: WaitlistResponse) { + if (req.method !== 'POST') { + res.setHeader('Allow', 'POST'); + return res.status(405).json({ error: 'method_not_allowed', message: 'Use POST.' }); + } + + const token = process.env.WAITLIST_GITHUB_TOKEN; + const gistId = process.env.WAITLIST_GIST_ID; + if (!token || !gistId) { + return res.status(503).json({ + error: 'waitlist_unconfigured', + message: 'The waitlist is not configured yet. Please try again later.', + }); + } + + if (!isValidBody(req.body)) { + return res.status(400).json({ error: 'invalid_request', message: 'A valid email is required.' }); + } + + // Honeypot: a hidden field real visitors never fill in. A bot that fills + // every field trips this silently -- reported as success so it doesn't + // learn to skip the field, but nothing is stored. + if (typeof req.body.company === 'string' && req.body.company.trim().length > 0) { + return res.status(200).json({ ok: true }); + } + + const email = req.body.email.trim().toLowerCase(); + const name = typeof req.body.name === 'string' ? req.body.name.trim() : ''; + + if (!email || email.length > MAX_EMAIL_LENGTH || !EMAIL_PATTERN.test(email)) { + return res.status(400).json({ error: 'invalid_email', message: 'Enter a valid email address.' }); + } + if (name.length > MAX_NAME_LENGTH) { + return res.status(400).json({ error: 'invalid_name', message: 'Name is too long.' }); + } + + const entry: WaitlistEntry = { email, name: name || null, submittedAt: new Date().toISOString() }; + + try { + await appendToGist(token, gistId, entry); + } catch (error) { + console.error('waitlist: failed to record submission', error); + return res.status(502).json({ + error: 'waitlist_storage_failed', + message: 'Could not record your submission right now. Please try again shortly.', + }); + } + + return res.status(200).json({ ok: true }); +} + +async function appendToGist(token: string, gistId: string, entry: WaitlistEntry): Promise { + const headers = { + Authorization: `Bearer ${token}`, + Accept: 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', + }; + + const getResponse = await fetch(`https://api.github.com/gists/${gistId}`, { headers }); + if (!getResponse.ok) { + throw new Error(`GET gist failed: ${getResponse.status}`); + } + const gist = await getResponse.json(); + const file = gist.files?.[GIST_FILENAME]; + const existing: WaitlistEntry[] = file?.content ? safeParseArray(file.content) : []; + + // Never store the same email twice -- a resubmission (e.g. a retried + // request) updates its position rather than duplicating the entry. + const deduped = existing.filter((row) => row.email !== entry.email); + deduped.push(entry); + + const patchResponse = await fetch(`https://api.github.com/gists/${gistId}`, { + method: 'PATCH', + headers: { ...headers, 'Content-Type': 'application/json' }, + body: JSON.stringify({ + files: { [GIST_FILENAME]: { content: JSON.stringify(deduped, null, 2) } }, + }), + }); + if (!patchResponse.ok) { + throw new Error(`PATCH gist failed: ${patchResponse.status}`); + } +} + +function safeParseArray(raw: string): WaitlistEntry[] { + try { + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +} diff --git a/apps/marketing/eslint.config.js b/apps/marketing/eslint.config.js new file mode 100644 index 00000000..e8d3b95f --- /dev/null +++ b/apps/marketing/eslint.config.js @@ -0,0 +1,34 @@ +import js from '@eslint/js'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { + ignores: ['dist', 'node_modules', '.vercel'], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + }, + }, + { + files: ['api/**/*.ts'], + languageOptions: { + globals: globals.node, + }, + }, +); diff --git a/apps/marketing/index.html b/apps/marketing/index.html new file mode 100644 index 00000000..32cbab3b --- /dev/null +++ b/apps/marketing/index.html @@ -0,0 +1,20 @@ + + + + + + + + PARTHA — Repository Intelligence + + + + + +
+ + + diff --git a/apps/marketing/package-lock.json b/apps/marketing/package-lock.json new file mode 100644 index 00000000..81b62ecc --- /dev/null +++ b/apps/marketing/package-lock.json @@ -0,0 +1,4065 @@ +{ + "name": "@partha/marketing", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@partha/marketing", + "version": "0.1.0", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^22.10.1", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^6.1.0", + "autoprefixer": "^10.5.4", + "eslint": "^10.8.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.4", + "globals": "^17.11.0", + "postcss": "^8.5.26", + "tailwindcss": "^3.4.10", + "tsx": "^4.19.2", + "typescript": "^5.5.4", + "typescript-eslint": "^8.67.0", + "vite": "^8.2.2" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.3.0.tgz", + "integrity": "sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.147.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.147.0.tgz", + "integrity": "sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.6.tgz", + "integrity": "sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.6.tgz", + "integrity": "sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.6.tgz", + "integrity": "sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.6.tgz", + "integrity": "sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.6.tgz", + "integrity": "sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.6.tgz", + "integrity": "sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.6.tgz", + "integrity": "sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.6.tgz", + "integrity": "sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.6.tgz", + "integrity": "sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.6.tgz", + "integrity": "sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.6.tgz", + "integrity": "sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.6.tgz", + "integrity": "sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.6.tgz", + "integrity": "sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.6.tgz", + "integrity": "sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.6.tgz", + "integrity": "sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.69.0.tgz", + "integrity": "sha512-t5jQTKPIgVW1PE6dR6H6Qz5gm8zjMlX5/2gRaOGd9eO6V7J+tQc6iWKukEe7dY8u9HyYasQ0yfF0/FSSTEO2gA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.69.0", + "@typescript-eslint/type-utils": "8.69.0", + "@typescript-eslint/utils": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.69.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.8.tgz", + "integrity": "sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.69.0.tgz", + "integrity": "sha512-l4b0DhWioGg6Gt2ebGlvfkFMOjRsauxtsnDRwUSRX1qHq3HdTfQHV8wW9zEXeciai6HfeaKOedQn2Zoofx3WBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.69.0", + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.69.0.tgz", + "integrity": "sha512-yi4obFrHMmnsesWehHbkg9zMA7Jt8cXT+mKM08G999pH1yT6nqgsHx7MYm0uY1wAj8CqiBXYRJ7WAT0QdQHQXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.69.0", + "@typescript-eslint/types": "^8.69.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.69.0.tgz", + "integrity": "sha512-ewfspqWvSxKSOaplqAUNbaSFO0eB6w1EtQ+esfYFRm3614Ty4uNtExkcbgd6nWsXphbqKyf9ZYdbZdv2xEoWEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.69.0.tgz", + "integrity": "sha512-xNqK7YTDZsLniQMV/4rpFR8Z5JlqeRvVjuG1YgF/mdPVH84HSD19L8CczMA0qg2RfwEV231GHH3VnToJDo4MfQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.69.0.tgz", + "integrity": "sha512-ZfoJAVg3JZndQEpEl9petVlxau3lRuElc4HRMuAlLCf8to04/iHz692RUSNmXKDjEuJmIL+KZ2/BsOcBc16dsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0", + "@typescript-eslint/utils": "8.69.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.69.0.tgz", + "integrity": "sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.69.0.tgz", + "integrity": "sha512-AdFkgqck3Vudb/kWnxlyafU/4aBhHrbQ9locP2N4psXTy5mOBg0SHJumnLvx7r6g1gV4DKvUFwV2nJZBoqOD8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.69.0", + "@typescript-eslint/tsconfig-utils": "8.69.0", + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.69.0.tgz", + "integrity": "sha512-tUbx60BBqQa31kXF5MCsOOLL5E/WzUuxIn7YpAvq+eaUlqvk8/NXnXMBNAdLCr0icjkzem7iUA5QqWHe/hJ1aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.69.0", + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.69.0.tgz", + "integrity": "sha512-+rmdgPA+EXkNgKYvHvFfhrs35utXbwaC5PGpDquSXcoXQDKUA5UjV0LmTucG/4JXkM31BTu4TilHtrN8IVBe8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.69.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz", + "integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.145.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.5.4", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.4.tgz", + "integrity": "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.6", + "caniuse-lite": "^1.0.30001806", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz", + "integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.418", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.418.tgz", + "integrity": "sha512-UzS26r3AEbG5wSoGVpJKqwHIU9zwQN7LHdVIThDrJpS0I5KdlXFMEb8543fhc9dVnIIAST6ar8rhwa00AL5MlA==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.9.1.tgz", + "integrity": "sha512-9VaAkDURekixUQJy0oJYl2DcN6oKMfxay7XzaGYAWQwsb6qfKf+x76R2k1L8kb1boc+FyCAaTA9GmiKaaiaF+A==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.5.tgz", + "integrity": "sha512-vG7yLURXNvCHy0FBdbZRwIu0BLPJMlUUJS2Ep7ud9w1YCLftFZtuEjyjhym0Qq9yuZ6LJUitNlu/hMk0gakXAw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.12.0.tgz", + "integrity": "sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.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" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/read-cache": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.2.tgz", + "integrity": "sha512-/peqiBB/n07gQGLsWaHho3WfvUyRscw0gYTsEFMhrIe/nWLkYaf5SbKYjGYqtRV3aPwykJgF2VEMo1ac4bnsGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.6.tgz", + "integrity": "sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.147.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.6", + "@rolldown/binding-android-arm64": "1.2.6", + "@rolldown/binding-darwin-arm64": "1.2.6", + "@rolldown/binding-darwin-x64": "1.2.6", + "@rolldown/binding-freebsd-x64": "1.2.6", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.6", + "@rolldown/binding-linux-arm64-gnu": "1.2.6", + "@rolldown/binding-linux-arm64-musl": "1.2.6", + "@rolldown/binding-linux-ppc64-gnu": "1.2.6", + "@rolldown/binding-linux-s390x-gnu": "1.2.6", + "@rolldown/binding-linux-x64-gnu": "1.2.6", + "@rolldown/binding-linux-x64-musl": "1.2.6", + "@rolldown/binding-openharmony-arm64": "1.2.6", + "@rolldown/binding-win32-arm64-msvc": "1.2.6", + "@rolldown/binding-win32-x64-msvc": "1.2.6" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsx": { + "version": "4.23.13", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz", + "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.69.0.tgz", + "integrity": "sha512-B3MltX0VqjUBNEe3b3sSuiRbfa6XrfHFtBiPamjT5AsW/dfq+y+bc0wyuS9DxAS1LyzCxRp2+rxzpLUvqM2BvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.69.0", + "@typescript-eslint/parser": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0", + "@typescript-eslint/utils": "8.69.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "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" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.5.4.tgz", + "integrity": "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/apps/marketing/package.json b/apps/marketing/package.json new file mode 100644 index 00000000..b0616065 --- /dev/null +++ b/apps/marketing/package.json @@ -0,0 +1,37 @@ +{ + "name": "@partha/marketing", + "private": true, + "version": "0.1.0", + "description": "Static marketing site for PARTHA: a scripted product simulation, a 'run it yourself' CTA to the main repository, and a waitlist form. Deliberately independent of apps/frontend -- no backend dependency, deployable to Vercel on its own (#382).", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "lint": "eslint .", + "typecheck": "tsc -b --noEmit", + "test": "node --import tsx --test api/*.test.ts" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^22.10.1", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^6.1.0", + "autoprefixer": "^10.5.4", + "eslint": "^10.8.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.4", + "globals": "^17.11.0", + "postcss": "^8.5.26", + "tailwindcss": "^3.4.10", + "tsx": "^4.19.2", + "typescript": "^5.5.4", + "typescript-eslint": "^8.67.0", + "vite": "^8.2.2" + } +} diff --git a/apps/marketing/postcss.config.js b/apps/marketing/postcss.config.js new file mode 100644 index 00000000..2aa7205d --- /dev/null +++ b/apps/marketing/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/apps/marketing/public/favicon.svg b/apps/marketing/public/favicon.svg new file mode 100644 index 00000000..715749be --- /dev/null +++ b/apps/marketing/public/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/marketing/src/App.tsx b/apps/marketing/src/App.tsx new file mode 100644 index 00000000..a3aa531d --- /dev/null +++ b/apps/marketing/src/App.tsx @@ -0,0 +1,25 @@ +import { useState } from 'react'; +import { Hero } from '@/components/Hero'; +import { SimulationDemo } from '@/components/SimulationDemo'; +import { RunItYourself } from '@/components/RunItYourself'; +import { WaitlistForm } from '@/components/WaitlistForm'; +import { Footer } from '@/components/Footer'; + +export function App() { + const [waitlistOpen, setWaitlistOpen] = useState(false); + + return ( +
+ setWaitlistOpen(true)} /> +
+ +
+
+
+ +
+
+ {waitlistOpen && setWaitlistOpen(false)} />} +
+ ); +} diff --git a/apps/marketing/src/assets/partha-logo.svg b/apps/marketing/src/assets/partha-logo.svg new file mode 100644 index 00000000..669ed86e --- /dev/null +++ b/apps/marketing/src/assets/partha-logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/apps/marketing/src/components/Footer.tsx b/apps/marketing/src/components/Footer.tsx new file mode 100644 index 00000000..f7aaf2d9 --- /dev/null +++ b/apps/marketing/src/components/Footer.tsx @@ -0,0 +1,23 @@ +const GITHUB_URL = 'https://github.com/Second-Origin/PARTHA'; +const DISCORD_URL = 'https://discord.gg/qvk9DcxDA'; + +export function Footer() { + return ( + + ); +} diff --git a/apps/marketing/src/components/Hero.tsx b/apps/marketing/src/components/Hero.tsx new file mode 100644 index 00000000..2143d25d --- /dev/null +++ b/apps/marketing/src/components/Hero.tsx @@ -0,0 +1,56 @@ +import logo from '@/assets/partha-logo.svg'; + +const GITHUB_URL = 'https://github.com/Second-Origin/PARTHA'; + +export function Hero({ onJoinWaitlist }: { onJoinWaitlist: () => void }) { + return ( +
+
+ ); +} diff --git a/apps/marketing/src/components/RunItYourself.tsx b/apps/marketing/src/components/RunItYourself.tsx new file mode 100644 index 00000000..cdaa2128 --- /dev/null +++ b/apps/marketing/src/components/RunItYourself.tsx @@ -0,0 +1,68 @@ +const GITHUB_URL = 'https://github.com/Second-Origin/PARTHA'; + +// Mirrors the exact commands in the repository's own README ("Run PARTHA +// locally") -- kept as a copy here (not a fetch of that file) since this +// site has no build-time dependency on the main repo at all, but checked +// against it directly rather than invented. +const BACKEND_COMMANDS = `git clone ${GITHUB_URL}.git +cd PARTHA/apps/backend +python3.13 -m venv .venv +source .venv/bin/activate +pip install -e . +cd ../.. +npm run dev:backend`; + +const FRONTEND_COMMANDS = `# in a second terminal +cd PARTHA +npm ci --prefix apps/frontend +npm run dev:frontend`; + +export function RunItYourself() { + return ( +
+

Run the real thing yourself

+

+ PARTHA is open source and runs locally with no external service beyond a public GitHub repository to analyze. + No account, no waitlist, no hosted deployment required. +

+ +
+
+

1. Start the backend

+
+            {BACKEND_COMMANDS}
+          
+
+
+

2. Start the frontend

+
+            {FRONTEND_COMMANDS}
+          
+

+ Open localhost:5173, register a local + account, add a repository, and start analysis. +

+
+
+ + +
+ ); +} diff --git a/apps/marketing/src/components/SimulationDemo.tsx b/apps/marketing/src/components/SimulationDemo.tsx new file mode 100644 index 00000000..44b40794 --- /dev/null +++ b/apps/marketing/src/components/SimulationDemo.tsx @@ -0,0 +1,231 @@ +import { useEffect, useRef, useState } from 'react'; +import { + CATEGORY_LABELS, + SAMPLE_CATEGORIES, + SAMPLE_FINDINGS, + SAMPLE_LANGUAGES, + SAMPLE_METRICS, + SAMPLE_REPO, + SIMULATION_STEPS, + type ReviewSeverity, +} from '@/data/sampleAnalysis'; + +type Phase = 'idle' | 'running' | 'done'; + +const SEVERITY_STYLE: Record = { + critical: 'bg-destructive/10 text-destructive', + high: 'bg-destructive/10 text-destructive', + medium: 'bg-warning/10 text-warning', + low: 'bg-primary/10 text-primary', + info: 'bg-muted text-muted-foreground', +}; + +const CATEGORY_STATE_STYLE: Record = { + assessed: 'bg-success/10 text-success', + partially_assessed: 'bg-warning/10 text-warning', + not_assessed: 'bg-muted text-muted-foreground', + insufficient_evidence: 'bg-muted text-muted-foreground', +}; + +export function SimulationDemo() { + const [phase, setPhase] = useState('idle'); + const [stepIndex, setStepIndex] = useState(0); + const [tab, setTab] = useState<'review' | 'insights'>('review'); + const timers = useRef[]>([]); + + useEffect(() => () => timers.current.forEach(clearTimeout), []); + + const run = () => { + if (phase === 'running') return; + timers.current.forEach(clearTimeout); + timers.current = []; + setPhase('running'); + setStepIndex(0); + SIMULATION_STEPS.forEach((_, index) => { + const timer = setTimeout(() => setStepIndex(index + 1), 550 * (index + 1)); + timers.current.push(timer); + }); + const finish = setTimeout(() => setPhase('done'), 550 * SIMULATION_STEPS.length + 300); + timers.current.push(finish); + }; + + const reset = () => { + timers.current.forEach(clearTimeout); + timers.current = []; + setPhase('idle'); + setStepIndex(0); + setTab('review'); + }; + + return ( +
+
+ Scripted simulation · sample repository +
+

See what a PARTHA analysis produces

+

+ This is a scripted walkthrough of a made-up sample repository ({SAMPLE_REPO.name}), not a live analysis of any + real code. It uses PARTHA's actual finding categories and output shape so you can see what a real run looks + like without needing an account or a live backend. +

+ +
+
+
+

{SAMPLE_REPO.name}

+

+ revision {SAMPLE_REPO.revision} · {SAMPLE_REPO.languages} +

+
+ {phase !== 'idle' && ( + + )} +
+ + {phase === 'idle' && ( +
+

+ Run the simulation to watch PARTHA walk through a sample repository and produce Engineering Review and + Repository Insights output. +

+ +
+ )} + + {phase !== 'idle' && ( +
+
    + {SIMULATION_STEPS.map((step, index) => { + const complete = index < stepIndex; + const active = index === stepIndex && phase === 'running'; + return ( +
  1. + + {complete ? '✓' : index + 1} + + {step} +
  2. + ); + })} +
+
+ )} + + {phase === 'done' && ( +
+
+ {(['review', 'insights'] as const).map((value) => ( + + ))} +
+ + {tab === 'review' && ( +
+
+ {SAMPLE_CATEGORIES.map((category) => ( + + {category.label} + {category.findingCount > 0 ? ` (${category.findingCount})` : ''} + + ))} +
+
    + {SAMPLE_FINDINGS.map((finding) => ( +
  • +
    +

    {finding.title}

    + + {finding.severity} + +
    +

    + {CATEGORY_LABELS[finding.category]} +

    +

    {finding.explanation}

    +

    + Remediation: + {finding.remediationGuidance} +

    +

    + {finding.path}:{finding.startLine} + {finding.endLine !== finding.startLine ? `–${finding.endLine}` : ''} +

    +
  • + ))} +
+
+ )} + + {tab === 'insights' && ( +
+
+ {SAMPLE_METRICS.map((metric) => ( +
+

{metric.value}

+

{metric.label}

+
+ ))} +
+
+

Languages

+
+ {SAMPLE_LANGUAGES.map((language, index) => ( +
+ ))} +
+
+ {SAMPLE_LANGUAGES.map((language) => ( + + {language.label} {language.value}% + + ))} +
+
+
+ )} +
+ )} +
+
+ ); +} diff --git a/apps/marketing/src/components/WaitlistForm.tsx b/apps/marketing/src/components/WaitlistForm.tsx new file mode 100644 index 00000000..e2344a3d --- /dev/null +++ b/apps/marketing/src/components/WaitlistForm.tsx @@ -0,0 +1,142 @@ +import { useState, type FormEvent } from 'react'; + +interface WaitlistFormProps { + onClose: () => void; +} + +type Status = 'idle' | 'submitting' | 'done'; + +export function WaitlistForm({ onClose }: WaitlistFormProps) { + const [email, setEmail] = useState(''); + const [name, setName] = useState(''); + // Honeypot: never rendered visibly, never touched by a real visitor. + const [company, setCompany] = useState(''); + const [status, setStatus] = useState('idle'); + const [error, setError] = useState(null); + + const submit = async (event: FormEvent) => { + event.preventDefault(); + if (status === 'submitting') return; + setStatus('submitting'); + setError(null); + try { + const response = await fetch('/api/waitlist', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, name, company }), + }); + if (!response.ok) { + const body = await response.json().catch(() => null); + throw new Error(body?.message ?? 'Something went wrong. Please try again.'); + } + setStatus('done'); + } catch (caught) { + setError(caught instanceof Error ? caught.message : 'Something went wrong. Please try again.'); + setStatus('idle'); + } + }; + + return ( +
+
+ {status === 'done' ? ( +
+

+ You're on the list +

+

+ We'll reach out by email when there's room. In the meantime, the real product is open source — + you can run it yourself right now. +

+ +
+ ) : ( + <> +
+
+

Waitlist

+

+ Join the waitlist +

+
+ +
+

+ PARTHA isn't running as a hosted service right now. Leave your email and we'll follow up when + a hosted beta is ready — or run it yourself in the meantime. +

+
+
+ + setEmail(event.target.value)} + className="w-full rounded-xl border border-input bg-background px-3 py-2.5 text-sm text-foreground" + /> +
+
+ + setName(event.target.value)} + className="w-full rounded-xl border border-input bg-background px-3 py-2.5 text-sm text-foreground" + /> +
+ {/* Honeypot -- visually hidden and unreachable by tab order, so a + real visitor never sees or fills it. */} + + + {error && ( +

+ {error} +

+ )} + + +
+ + )} +
+
+ ); +} diff --git a/apps/marketing/src/data/sampleAnalysis.ts b/apps/marketing/src/data/sampleAnalysis.ts new file mode 100644 index 00000000..810641ab --- /dev/null +++ b/apps/marketing/src/data/sampleAnalysis.ts @@ -0,0 +1,155 @@ +/** + * Canned data for the scripted product simulation (#382). + * + * Every category id, severity level, and field name here matches the real + * product's actual Engineering Review / Repository Insights response shape + * (apps/backend/app/schemas -- cross-checked against + * apps/frontend/src/shared/services/api/generated.ts, the generated OpenAPI + * types) and the real category labels + * (apps/backend/app/review/review_service.py's `_CATEGORY_LABELS`). The + * repository, findings, and numbers themselves are entirely made up for a + * fictional sample repo -- nothing here is captured from a real analysis. + */ + +export const SAMPLE_REPO = { + name: 'acme/checkout-service', + revision: 'a3f9c21', + languages: 'Python, TypeScript', +}; + +export const SIMULATION_STEPS = [ + 'Cloning acme/checkout-service at a3f9c21', + 'Extracting structural facts (Python, TypeScript)', + 'Resolving module and dependency relationships', + 'Sealing the ri.v1 snapshot', + 'Running Engineering Review and Repository Insights', +] as const; + +export type ReviewSeverity = 'info' | 'low' | 'medium' | 'high' | 'critical'; + +export type ReviewCategoryId = + | 'architecture_boundaries' + | 'relationship_resolution' + | 'source_extraction' + | 'dependency_declarations' + | 'security_vulnerability_scanning' + | 'authentication_evidence' + | 'repository_structure' + | 'analysis_integrity'; + +// Matches apps/backend/app/review/review_service.py's _CATEGORY_LABELS +// exactly, so the demo never invents category names the real product +// doesn't have. +export const CATEGORY_LABELS: Record = { + architecture_boundaries: 'Architecture and boundaries', + relationship_resolution: 'Relationship resolution', + source_extraction: 'Source extraction', + dependency_declarations: 'Dependency declarations', + security_vulnerability_scanning: 'Security vulnerability scanning', + authentication_evidence: 'Authentication evidence', + repository_structure: 'Repository structure', + analysis_integrity: 'Analysis integrity', +}; + +export interface SampleFinding { + id: string; + title: string; + category: ReviewCategoryId; + severity: ReviewSeverity; + explanation: string; + remediationGuidance: string; + path: string; + startLine: number; + endLine: number; +} + +export const SAMPLE_FINDINGS: SampleFinding[] = [ + { + id: 'finding-1', + title: 'Payment adapter imports directly from the checkout domain layer', + category: 'architecture_boundaries', + severity: 'medium', + explanation: + 'src/payments/stripe_adapter.py imports CheckoutOrder directly from src/checkout/domain.py, crossing the boundary the module layout otherwise keeps between payment adapters and the checkout domain.', + remediationGuidance: + 'Route the dependency through the shared checkout interface (src/checkout/ports.py) instead of importing the domain model directly.', + path: 'src/payments/stripe_adapter.py', + startLine: 12, + endLine: 14, + }, + { + id: 'finding-2', + title: 'Declared dependency pin resolves to a version not requested anywhere', + category: 'dependency_declarations', + severity: 'low', + explanation: + 'package-lock.json resolves "fast-xml-parser" to 4.2.5, but no direct or transitive declaration in package.json requests it -- likely a stale lockfile entry from a removed dependency.', + remediationGuidance: 'Regenerate the lockfile and confirm the resolved dependency tree still matches package.json.', + path: 'package-lock.json', + startLine: 1841, + endLine: 1841, + }, + { + id: 'finding-3', + title: 'Session cookie is issued without an explicit SameSite attribute', + category: 'authentication_evidence', + severity: 'high', + explanation: + 'src/auth/session.py sets the session cookie without a SameSite attribute, so browsers fall back to a permissive default that varies by browser rather than an explicit, reviewable policy.', + remediationGuidance: 'Set SameSite explicitly (Lax or Strict) when issuing the session cookie.', + path: 'src/auth/session.py', + startLine: 47, + endLine: 52, + }, + { + id: 'finding-4', + title: 'Two modules independently resolve the same order-total calculation', + category: 'relationship_resolution', + severity: 'info', + explanation: + 'src/checkout/domain.py and src/reporting/order_summary.py both implement order-total logic independently rather than one calling the other, a duplicate-interpretation risk if the two drift.', + remediationGuidance: 'Consider extracting one shared calculation both call, if the duplication was not intentional.', + path: 'src/reporting/order_summary.py', + startLine: 88, + endLine: 104, + }, +]; + +export interface SampleCategoryAssessment { + id: ReviewCategoryId; + label: string; + state: 'assessed' | 'partially_assessed' | 'not_assessed' | 'insufficient_evidence'; + findingCount: number; + explanation: string; +} + +export const SAMPLE_CATEGORIES: SampleCategoryAssessment[] = [ + { id: 'architecture_boundaries', label: CATEGORY_LABELS.architecture_boundaries, state: 'assessed', findingCount: 1, explanation: 'Module boundaries were assessed from resolved import relationships.' }, + { id: 'relationship_resolution', label: CATEGORY_LABELS.relationship_resolution, state: 'assessed', findingCount: 1, explanation: 'Cross-module relationships were assessed from resolved facts.' }, + { id: 'source_extraction', label: CATEGORY_LABELS.source_extraction, state: 'assessed', findingCount: 0, explanation: 'No source-extraction diagnostics were raised for this snapshot.' }, + { id: 'dependency_declarations', label: CATEGORY_LABELS.dependency_declarations, state: 'assessed', findingCount: 1, explanation: 'Direct declarations and lockfile pins were assessed for this snapshot.' }, + { id: 'security_vulnerability_scanning', label: CATEGORY_LABELS.security_vulnerability_scanning, state: 'not_assessed', findingCount: 0, explanation: 'Vulnerability scanning is not implemented; this category is not assessed.' }, + { id: 'authentication_evidence', label: CATEGORY_LABELS.authentication_evidence, state: 'assessed', findingCount: 1, explanation: 'The supported Python/FastAPI authentication subgraph was assessed.' }, + { id: 'repository_structure', label: CATEGORY_LABELS.repository_structure, state: 'assessed', findingCount: 0, explanation: 'No repository-structure diagnostics were raised for this snapshot.' }, + { id: 'analysis_integrity', label: CATEGORY_LABELS.analysis_integrity, state: 'assessed', findingCount: 0, explanation: 'The analysis completed with no integrity diagnostics.' }, +]; + +export interface SampleMetric { + id: string; + label: string; + value: string; + definition: string; +} + +export const SAMPLE_METRICS: SampleMetric[] = [ + { id: 'files_analyzed', label: 'Files analyzed', value: '212', definition: 'Files included in this snapshot after extraction.' }, + { id: 'modules', label: 'Modules identified', value: '18', definition: 'Distinct modules resolved from the repository layout.' }, + { id: 'relationships', label: 'Relationships resolved', value: '341', definition: 'Import and dependency edges resolved between facts.' }, + { id: 'evidence_records', label: 'Evidence records', value: '1,204', definition: 'Stored source spans backing supported facts.' }, +]; + +export const SAMPLE_LANGUAGES = [ + { key: 'python', label: 'Python', value: 63 }, + { key: 'typescript', label: 'TypeScript', value: 29 }, + { key: 'other', label: 'Other', value: 8 }, +]; diff --git a/apps/marketing/src/main.tsx b/apps/marketing/src/main.tsx new file mode 100644 index 00000000..0f4a228a --- /dev/null +++ b/apps/marketing/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { App } from './App'; +import './styles/globals.css'; + +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/apps/marketing/src/styles/globals.css b/apps/marketing/src/styles/globals.css new file mode 100644 index 00000000..4da2f457 --- /dev/null +++ b/apps/marketing/src/styles/globals.css @@ -0,0 +1,48 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* Same HSL token values as apps/frontend/src/styles/globals.css's :root + (light mode only -- this site doesn't need a dark-mode switcher), copied + rather than shared so this project has no cross-package dependency on + apps/frontend at all. */ +@layer base { + :root { + --background: 38 27% 94%; + --foreground: 258 19% 13%; + --card: 0 0% 100%; + --card-foreground: 258 19% 13%; + --primary: 18 99% 38%; + --primary-foreground: 0 0% 100%; + --muted: 38 25% 92%; + --muted-foreground: 263 8% 43%; + --accent: 18 100% 94%; + --accent-foreground: 258 19% 13%; + --success: 142 76% 26%; + --success-foreground: 210 40% 98%; + --warning: 38 92% 29%; + --warning-foreground: 258 19% 13%; + --destructive: 0 84% 60%; + --destructive-foreground: 210 40% 98%; + --border: 18 66% 76%; + --input: 18 42% 84%; + --ring: 17 91% 50%; + --radius: 1rem; + } + + body { + @apply bg-background text-foreground font-sans antialiased; + } + + button, + a, + input { + @apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background; + } +} + +@layer components { + .partha-surface { + @apply rounded-2xl border border-border bg-card shadow-[0_12px_24px_hsl(var(--foreground)/0.06)]; + } +} diff --git a/apps/marketing/tailwind.config.ts b/apps/marketing/tailwind.config.ts new file mode 100644 index 00000000..ce9405ca --- /dev/null +++ b/apps/marketing/tailwind.config.ts @@ -0,0 +1,72 @@ +import type { Config } from 'tailwindcss'; + +// A deliberately smaller copy of apps/frontend/tailwind.config.ts's token +// system -- same HSL custom-property convention and brand primary, but only +// the tokens this site's own components actually use. Kept as an +// independent file (not imported across the package boundary) so this site +// stays a genuinely standalone, independently buildable/deployable project +// (#382) -- the whole point of splitting it out from apps/frontend. +export default { + darkMode: 'class', + content: ['./index.html', './src/**/*.{ts,tsx}'], + theme: { + extend: { + colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + success: { + DEFAULT: 'hsl(var(--success))', + foreground: 'hsl(var(--success-foreground))', + }, + warning: { + DEFAULT: 'hsl(var(--warning))', + foreground: 'hsl(var(--warning-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + }, + fontFamily: { + sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'], + mono: ['JetBrains Mono', 'Fira Code', 'monospace'], + }, + fontSize: { + '2xs': ['0.6875rem', { lineHeight: '1rem' }], + }, + keyframes: { + fadeIn: { '0%': { opacity: '0' }, '100%': { opacity: '1' } }, + slideIn: { '0%': { opacity: '0', transform: 'translateY(8px)' }, '100%': { opacity: '1', transform: 'translateY(0)' } }, + }, + animation: { + 'fade-in': 'fadeIn 0.4s ease-out', + 'slide-in': 'slideIn 0.35s ease-out', + }, + }, + }, + plugins: [], +} satisfies Config; diff --git a/apps/marketing/tsconfig.json b/apps/marketing/tsconfig.json new file mode 100644 index 00000000..ec4fb101 --- /dev/null +++ b/apps/marketing/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src", "api", "vite-env.d.ts"] +} diff --git a/apps/marketing/vercel.json b/apps/marketing/vercel.json new file mode 100644 index 00000000..4974736e --- /dev/null +++ b/apps/marketing/vercel.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "framework": "vite", + "buildCommand": "npm run build", + "outputDirectory": "dist" +} diff --git a/apps/marketing/vite-env.d.ts b/apps/marketing/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/apps/marketing/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/apps/marketing/vite.config.ts b/apps/marketing/vite.config.ts new file mode 100644 index 00000000..d1be0f6c --- /dev/null +++ b/apps/marketing/vite.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { fileURLToPath } from 'node:url'; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, +}); From 6de108d8fac09ab5216b8eeb680fe9e26ea55b94 Mon Sep 17 00:00:00 2001 From: PARTH J ROHIT Date: Tue, 1 Sep 2026 19:20:46 +0100 Subject: [PATCH 2/4] refactor(marketing): rebuild on the real landing page component (#382) Replace the marketing site's custom-built Hero/SimulationDemo/RunItYourself composition with the actual apps/frontend landing page as its visual basis: the same 1728px authored artwork, light/dark/system theme system, brand tokens, FAQ, and footer -- ported in directly rather than re-invented. Two behavioral differences from the real app, since this standalone site has no backend or accounts at all: - The "Log In" nav hotspot has nothing to log in to, so it opens a scripted product simulation instead (DemoModal), using the same canned sample data as before. - Every "Analyze a Repository" hotspot has no live backend to analyze against, so it opens fork/clone setup instructions instead (RunItYourselfModal), with a note encouraging visitors to star the repo and a path back to the waitlist for anyone who'd rather wait for a hosted beta. Ported useLandingTheme/ThemeSwitcher/cn verbatim from apps/frontend, copied the two landing SVG assets directly (no cross-package import, keeping this project genuinely standalone), and rewrote globals.css/tailwind.config.ts to match the real frontend's full token set including .landing-dark. WaitlistModal keeps its ported visual design (literal light-mode hex colors, tokenized dark mode) and now lives inside RunItYourselfModal as a secondary path. Removed the five now-superseded custom components (Hero, Footer, SimulationDemo, RunItYourself, WaitlistForm). Verified: typecheck/lint/test/build all clean; real in-browser click-through in both light and dark mode covering every remapped hotspot (Log In -> demo run-through on both result tabs, Analyze a Repository -> run-it-yourself -> nested waitlist, FAQ, theme switcher, footer links and notices). --- apps/marketing/README.md | 37 +- apps/marketing/index.html | 20 +- apps/marketing/package-lock.json | 69 +- apps/marketing/package.json | 6 +- apps/marketing/src/App.tsx | 198 +++++- .../assets/landing/landing-reference-dark.svg | 658 ++++++++++++++++++ .../src/assets/landing/landing-reference.svg | 653 +++++++++++++++++ apps/marketing/src/components/DemoModal.tsx | 252 +++++++ apps/marketing/src/components/Footer.tsx | 23 - apps/marketing/src/components/Hero.tsx | 56 -- .../src/components/RunItYourself.tsx | 68 -- .../src/components/RunItYourselfModal.tsx | 114 +++ .../src/components/SimulationDemo.tsx | 231 ------ .../src/components/ThemeSwitcher.tsx | 41 ++ .../marketing/src/components/WaitlistForm.tsx | 142 ---- .../src/components/WaitlistModal.tsx | 146 ++++ apps/marketing/src/hooks/useLandingTheme.ts | 51 ++ apps/marketing/src/hooks/useWaitlistForm.ts | 38 + apps/marketing/src/styles/globals.css | 34 +- apps/marketing/src/utils/cn.ts | 6 + apps/marketing/tailwind.config.ts | 33 +- 21 files changed, 2304 insertions(+), 572 deletions(-) create mode 100644 apps/marketing/src/assets/landing/landing-reference-dark.svg create mode 100644 apps/marketing/src/assets/landing/landing-reference.svg create mode 100644 apps/marketing/src/components/DemoModal.tsx delete mode 100644 apps/marketing/src/components/Footer.tsx delete mode 100644 apps/marketing/src/components/Hero.tsx delete mode 100644 apps/marketing/src/components/RunItYourself.tsx create mode 100644 apps/marketing/src/components/RunItYourselfModal.tsx delete mode 100644 apps/marketing/src/components/SimulationDemo.tsx create mode 100644 apps/marketing/src/components/ThemeSwitcher.tsx delete mode 100644 apps/marketing/src/components/WaitlistForm.tsx create mode 100644 apps/marketing/src/components/WaitlistModal.tsx create mode 100644 apps/marketing/src/hooks/useLandingTheme.ts create mode 100644 apps/marketing/src/hooks/useWaitlistForm.ts create mode 100644 apps/marketing/src/utils/cn.ts diff --git a/apps/marketing/README.md b/apps/marketing/README.md index d1c16a8e..18a094cc 100644 --- a/apps/marketing/README.md +++ b/apps/marketing/README.md @@ -1,9 +1,17 @@ # PARTHA marketing site -A free, static marketing site for PARTHA (#382): a scripted product simulation using canned sample data, a "run it -yourself" call to action pointing at the main repository, and a waitlist form. Deliberately independent of -`apps/frontend` — no backend dependency, and nothing here talks to the real Render/Neon deployment (which is paused, -not live — see #375/#377). It builds and runs entirely on its own. +A free, static marketing site for PARTHA (#382). It reuses the real `apps/frontend` landing page — the same +1728px-wide authored design, the same light/dark theme system, the same FAQ and footer — as its visual basis, ported +in rather than re-invented, with two behavioral differences since this site has no backend or accounts at all: + +- The "Log In" nav hotspot has nothing to log in to, so it opens a scripted product simulation instead + (`src/components/DemoModal.tsx`), using canned sample data. +- Every "Analyze a Repository" hotspot has no live backend to analyze against, so it opens fork/clone setup + instructions instead (`src/components/RunItYourselfModal.tsx`), plus a note encouraging people to star the repo. + +Deliberately independent of `apps/frontend` — no backend dependency, and nothing here talks to the real Render/Neon +deployment (which is paused, not live — see #375/#377). It builds and runs entirely on its own; shared visual assets +(SVG artwork, theme hook, tokens) are copied in directly rather than imported cross-package, to keep it that way. ## Local development @@ -13,9 +21,10 @@ npm install npm run dev ``` -Opens at `http://localhost:5173` (or the next free port Vite picks). The waitlist form will show a "not configured" -error locally unless you also run `vercel dev` with the env vars below set — that's expected; the rest of the page -works fully without it. +Opens at `http://localhost:5173` (or the next free port Vite picks; the repo's `.claude/launch.json` runs it on +`5174` alongside the real frontend dev server on `5173`). The waitlist form will show a "not configured" error +locally unless you also run `vercel dev` with the env vars below set — that's expected; the rest of the page works +fully without it. ## Verification @@ -63,9 +72,21 @@ submissions or crashing — the rest of the site is unaffected either way. To read submissions later: open the Gist directly, or `curl -H "Authorization: Bearer " https://api.github.com/gists/`. +## Structure + +- `src/App.tsx` — the ported landing page: renders the authored SVG artwork (`src/assets/landing/`), overlays the + invisible nav/hero/footer hotspots on top, and wires the two behavioral differences described above. +- `src/components/DemoModal.tsx`, `RunItYourselfModal.tsx`, `WaitlistModal.tsx` — the three interactive surfaces + reached from those hotspots. +- `src/hooks/useLandingTheme.ts`, `src/components/ThemeSwitcher.tsx` — verbatim ports of the real frontend's + light/dark/system theme store and toggle. +- `src/data/sampleAnalysis.ts` — the canned data behind the demo simulation, using PARTHA's real finding + categories/severities/output shape. + ## What this deliberately does not do - Does not call the real PARTHA API — the "simulation" is entirely canned data in `src/data/sampleAnalysis.ts`, clearly labeled in the UI as a scripted sample-repository walkthrough, not a live analysis. -- Does not create or need a PARTHA account, login, or session. +- Does not create or need a PARTHA account, login, or session — the reused landing page's "Log In" hotspot opens the + demo instead of a login flow. - Does not touch `apps/frontend`, the backend, or `render.yaml` — those stay exactly as they are, paused. diff --git a/apps/marketing/index.html b/apps/marketing/index.html index 32cbab3b..db5de1f0 100644 --- a/apps/marketing/index.html +++ b/apps/marketing/index.html @@ -11,7 +11,25 @@ PARTHA — Repository Intelligence - + + +
diff --git a/apps/marketing/package-lock.json b/apps/marketing/package-lock.json index 81b62ecc..577b2478 100644 --- a/apps/marketing/package-lock.json +++ b/apps/marketing/package-lock.json @@ -8,8 +8,12 @@ "name": "@partha/marketing", "version": "0.1.0", "dependencies": { + "clsx": "^2.1.1", + "lucide-react": "^0.577.0", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "tailwind-merge": "^2.5.2", + "zustand": "^5.0.15" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -1314,14 +1318,14 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.31", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -1881,6 +1885,15 @@ "node": ">= 6" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -1930,7 +1943,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/debug": { @@ -2974,6 +2987,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.577.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", + "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3634,6 +3656,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwind-merge": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.1.tgz", + "integrity": "sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "3.4.19", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", @@ -4060,6 +4092,35 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "node_modules/zustand": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.15.tgz", + "integrity": "sha512-MpSEjRiBkA9crSYeOUH32rJC7SVqAbm0Fqcqge/bUi2PPoLcBWKOsG+C8mevmpr8TwXHBVkChbbJiyvkE+i/3A==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/apps/marketing/package.json b/apps/marketing/package.json index b0616065..12b32365 100644 --- a/apps/marketing/package.json +++ b/apps/marketing/package.json @@ -13,8 +13,12 @@ "test": "node --import tsx --test api/*.test.ts" }, "dependencies": { + "clsx": "^2.1.1", + "lucide-react": "^0.577.0", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "tailwind-merge": "^2.5.2", + "zustand": "^5.0.15" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/apps/marketing/src/App.tsx b/apps/marketing/src/App.tsx index a3aa531d..3d87f369 100644 --- a/apps/marketing/src/App.tsx +++ b/apps/marketing/src/App.tsx @@ -1,25 +1,185 @@ -import { useState } from 'react'; -import { Hero } from '@/components/Hero'; -import { SimulationDemo } from '@/components/SimulationDemo'; -import { RunItYourself } from '@/components/RunItYourself'; -import { WaitlistForm } from '@/components/WaitlistForm'; -import { Footer } from '@/components/Footer'; +import { useEffect, useState } from 'react'; +import { ThemeSwitcher } from '@/components/ThemeSwitcher'; +import { useLandingTheme } from '@/hooks/useLandingTheme'; +import { DemoModal } from '@/components/DemoModal'; +import { RunItYourselfModal } from '@/components/RunItYourselfModal'; +import landingReference from '@/assets/landing/landing-reference.svg'; +import landingReferenceDark from '@/assets/landing/landing-reference-dark.svg'; +/** + * Adapted from apps/frontend/src/app/pages/LandingPage.tsx (#382 redesign): + * the real, signed-off marketing page -- same 1728-wide authored design + * canvas, same light/dark theme system, same FAQ and footer -- reused + * directly rather than re-invented, per the project owner's explicit + * direction. Two behavioral differences from the real app, since this + * standalone site has no backend and no accounts at all: + * + * - The nav's "Log in" hotspot has nothing to log in to here, so it opens + * the scripted demo simulation (DemoModal) instead of a dead route. + * - Every "Analyze a Repository" hotspot has nothing to analyze against + * (no live backend), so it opens setup instructions for running the real + * product locally (RunItYourselfModal) instead. + * + * Everything else -- the artwork, the FAQ, the footer, the theme toggle -- + * is the same interaction shape as the real page. + */ export function App() { - const [waitlistOpen, setWaitlistOpen] = useState(false); + const [faqIndex, setFaqIndex] = useState(null); + const [footerNotice, setFooterNotice] = useState(null); + const [demoOpen, setDemoOpen] = useState(false); + const [runItYourselfOpen, setRunItYourselfOpen] = useState(false); + const theme = useLandingTheme(); + const dark = theme.resolved === 'dark'; + + useEffect(() => { + document.documentElement.removeAttribute('data-landing-theme-boot'); + }, []); + + const analyzeCta = (className: string) => ( + + ); return ( -
- setWaitlistOpen(true)} /> -
- -
-
-
- -
-
- {waitlistOpen && setWaitlistOpen(false)} />} -
+
+

Reveal the system behind the code.

+
+ PARTHA repository intelligence system overview, capabilities, frequently asked questions, and call to action. + + + + + + +
); } + +const faqQuestions = [ + 'Is PARTHA an AI product?', + 'How do you handle dynamic dispatch, reflection, and generated code?', + 'Which languages are supported?', + 'Where does PARTHA run?', + 'How does PARTHA integrate with our CI?', + 'What is the ri.v1 format?', +] as const; + +const faqAnswers = [ + 'PARTHA creates deterministic, evidence-backed repository models. AI assistance is optional and is limited to structural facts that have already been computed from a sealed snapshot.', + 'PARTHA makes supported evidence and limits visible. Findings that cannot be verified from the selected revision are not presented as facts.', + 'Language support is determined by the extractors available for the selected repository. The sealed snapshot records exactly what was assessed.', + 'PARTHA analyses a repository at a specific revision and retains a reproducible model for the workspace.', + 'Connect a repository, select a revision, then use the generated evidence and exports in the engineering workflow that suits your team.', + 'ri.v1 is PARTHA’s sealed repository-intelligence snapshot format. It records the exact revision, extracted facts, and available evidence.', +] as const; + +type FooterControl = { + label: string; + left: string; + top: string; + href?: string; + external?: boolean; + message?: string; +}; + +const footerControls: FooterControl[] = [ + { label: 'How it works', left: '39.1%', top: '93.65%', href: '#how-it-works' }, + { label: 'Capabilities', left: '39.1%', top: '94.45%', href: '#capabilities' }, + { label: 'FAQ', left: '39.1%', top: '95.25%', href: '#faq' }, + { label: 'Privacy', left: '39.1%', top: '96.05%', message: 'Privacy details will be published with the public release.' }, + { label: 'Docs', left: '55%', top: '93.65%', href: 'https://github.com/Second-Origin/PARTHA/blob/dev/docs/README.md', external: true }, + { label: 'ri.v1 spec', left: '55%', top: '94.45%', message: 'The ri.v1 specification will be available shortly.' }, + { label: 'Language matrix', left: '55%', top: '95.25%', message: 'The language matrix will be available shortly.' }, + { label: 'Changelog', left: '55%', top: '96.05%', message: 'The changelog will be available shortly.' }, + { label: 'About', left: '70.6%', top: '93.65%', message: 'About PARTHA will be available shortly.' }, + { label: 'Security', left: '70.6%', top: '94.45%', href: 'https://github.com/Second-Origin/PARTHA/blob/dev/SECURITY.md', external: true }, + { label: 'Contact', left: '70.6%', top: '95.25%', href: 'https://discord.gg/qvk9DcxDA', external: true }, + { label: 'Legal', left: '70.6%', top: '96.05%', message: 'Legal information will be available shortly.' }, + { label: 'LinkedIn', left: '86.3%', top: '93.65%', href: 'https://www.linkedin.com', external: true }, + { label: 'X', left: '86.3%', top: '94.45%', href: 'https://x.com', external: true }, + { label: 'GitHub', left: '86.3%', top: '95.25%', href: 'https://github.com/Second-Origin/PARTHA', external: true }, +]; + +function FooterControl({ item, onUnavailable }: { item: FooterControl; onUnavailable: (message: string) => void }) { + const className = 'absolute z-20 h-[0.9%] w-[10.5%] border-0 bg-transparent p-0'; + const style = { left: item.left, top: item.top }; + if (item.message) { + return + + +
+
+
+
+

{SAMPLE_REPO.name}

+

+ revision {SAMPLE_REPO.revision} · {SAMPLE_REPO.languages} +

+
+ {phase !== 'idle' && ( + + )} +
+ + {phase === 'idle' && ( +
+

+ Run the simulation to watch PARTHA walk through a sample repository and produce Engineering Review + and Repository Insights output. +

+ +
+ )} + + {phase !== 'idle' && ( +
+
    + {SIMULATION_STEPS.map((step, index) => { + const complete = index < stepIndex; + const active = index === stepIndex && phase === 'running'; + return ( +
  1. + + {complete ? '✓' : index + 1} + + {step} +
  2. + ); + })} +
+
+ )} + + {phase === 'done' && ( +
+
+ {(['review', 'insights'] as const).map((value) => ( + + ))} +
+ + {tab === 'review' && ( +
+
+ {SAMPLE_CATEGORIES.map((category) => ( + + {category.label} + {category.findingCount > 0 ? ` (${category.findingCount})` : ''} + + ))} +
+
    + {SAMPLE_FINDINGS.map((finding) => ( +
  • +
    +

    {finding.title}

    + + {finding.severity} + +
    +

    + {CATEGORY_LABELS[finding.category]} +

    +

    {finding.explanation}

    +

    + Remediation: + {finding.remediationGuidance} +

    +

    + {finding.path}:{finding.startLine} + {finding.endLine !== finding.startLine ? `–${finding.endLine}` : ''} +

    +
  • + ))} +
+
+ )} + + {tab === 'insights' && ( +
+
+ {SAMPLE_METRICS.map((metric) => ( +
+

{metric.value}

+

{metric.label}

+
+ ))} +
+
+

Languages

+
+ {SAMPLE_LANGUAGES.map((language, index) => ( +
+ ))} +
+
+ {SAMPLE_LANGUAGES.map((language) => ( + + {language.label} {language.value}% + + ))} +
+
+
+ )} +
+ )} +
+
+
+ + ); +} diff --git a/apps/marketing/src/components/Footer.tsx b/apps/marketing/src/components/Footer.tsx deleted file mode 100644 index f7aaf2d9..00000000 --- a/apps/marketing/src/components/Footer.tsx +++ /dev/null @@ -1,23 +0,0 @@ -const GITHUB_URL = 'https://github.com/Second-Origin/PARTHA'; -const DISCORD_URL = 'https://discord.gg/qvk9DcxDA'; - -export function Footer() { - return ( -
- ); -} diff --git a/apps/marketing/src/components/Hero.tsx b/apps/marketing/src/components/Hero.tsx deleted file mode 100644 index 2143d25d..00000000 --- a/apps/marketing/src/components/Hero.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import logo from '@/assets/partha-logo.svg'; - -const GITHUB_URL = 'https://github.com/Second-Origin/PARTHA'; - -export function Hero({ onJoinWaitlist }: { onJoinWaitlist: () => void }) { - return ( -
-
- ); -} diff --git a/apps/marketing/src/components/RunItYourself.tsx b/apps/marketing/src/components/RunItYourself.tsx deleted file mode 100644 index cdaa2128..00000000 --- a/apps/marketing/src/components/RunItYourself.tsx +++ /dev/null @@ -1,68 +0,0 @@ -const GITHUB_URL = 'https://github.com/Second-Origin/PARTHA'; - -// Mirrors the exact commands in the repository's own README ("Run PARTHA -// locally") -- kept as a copy here (not a fetch of that file) since this -// site has no build-time dependency on the main repo at all, but checked -// against it directly rather than invented. -const BACKEND_COMMANDS = `git clone ${GITHUB_URL}.git -cd PARTHA/apps/backend -python3.13 -m venv .venv -source .venv/bin/activate -pip install -e . -cd ../.. -npm run dev:backend`; - -const FRONTEND_COMMANDS = `# in a second terminal -cd PARTHA -npm ci --prefix apps/frontend -npm run dev:frontend`; - -export function RunItYourself() { - return ( -
-

Run the real thing yourself

-

- PARTHA is open source and runs locally with no external service beyond a public GitHub repository to analyze. - No account, no waitlist, no hosted deployment required. -

- -
-
-

1. Start the backend

-
-            {BACKEND_COMMANDS}
-          
-
-
-

2. Start the frontend

-
-            {FRONTEND_COMMANDS}
-          
-

- Open localhost:5173, register a local - account, add a repository, and start analysis. -

-
-
- - -
- ); -} diff --git a/apps/marketing/src/components/RunItYourselfModal.tsx b/apps/marketing/src/components/RunItYourselfModal.tsx new file mode 100644 index 00000000..b2b2d5b6 --- /dev/null +++ b/apps/marketing/src/components/RunItYourselfModal.tsx @@ -0,0 +1,114 @@ +import { useState } from 'react'; +import { WaitlistModal } from '@/components/WaitlistModal'; + +const GITHUB_URL = 'https://github.com/Second-Origin/PARTHA'; + +// Mirrors the exact commands in the repository's own README ("Run PARTHA +// locally") -- kept as a copy here (not a fetch of that file), but checked +// against it directly rather than invented. +const BACKEND_COMMANDS = `git clone ${GITHUB_URL}.git +cd PARTHA/apps/backend +python3.13 -m venv .venv +source .venv/bin/activate +pip install -e . +cd ../.. +npm run dev:backend`; + +const FRONTEND_COMMANDS = `# in a second terminal +cd PARTHA +npm ci --prefix apps/frontend +npm run dev:frontend`; + +/** Opened from the reused LandingPage's "Analyze a Repository" hotspots + * (#382 redesign) -- there is no live backend for this button to actually + * analyze anything against, so instead of running the scripted demo (see + * DemoModal, reached from "Log in" instead) it shows how to run the real + * product against a visitor's own code. */ +export function RunItYourselfModal({ onClose, dark = false }: { onClose: () => void; dark?: boolean }) { + const [waitlistOpen, setWaitlistOpen] = useState(false); + + return ( +
+
+
+
+

Run it yourself

+

+ Analyze your own repository +

+
+ +
+

+ PARTHA isn't running as a hosted service right now, so there's nothing here to submit a repository + URL to. It's open source and runs locally with no external service beyond a public GitHub repository to + analyze -- fork or clone it, run it, and try it on your own code. +

+ +
+
+

1. Start the backend

+
+              {BACKEND_COMMANDS}
+            
+
+
+

2. Start the frontend

+
+              {FRONTEND_COMMANDS}
+            
+

+ Open localhost:5173, register a local + account, add a repository, and start analysis. +

+
+
+ + + +
+

+ Finding this useful?{' '} + + Star the repo + {' '} + to support the project and help others find it. +

+

+ Prefer a hosted version instead of running it yourself?{' '} + + . +

+
+
+ + {waitlistOpen && setWaitlistOpen(false)} />} +
+ ); +} diff --git a/apps/marketing/src/components/SimulationDemo.tsx b/apps/marketing/src/components/SimulationDemo.tsx deleted file mode 100644 index 44b40794..00000000 --- a/apps/marketing/src/components/SimulationDemo.tsx +++ /dev/null @@ -1,231 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; -import { - CATEGORY_LABELS, - SAMPLE_CATEGORIES, - SAMPLE_FINDINGS, - SAMPLE_LANGUAGES, - SAMPLE_METRICS, - SAMPLE_REPO, - SIMULATION_STEPS, - type ReviewSeverity, -} from '@/data/sampleAnalysis'; - -type Phase = 'idle' | 'running' | 'done'; - -const SEVERITY_STYLE: Record = { - critical: 'bg-destructive/10 text-destructive', - high: 'bg-destructive/10 text-destructive', - medium: 'bg-warning/10 text-warning', - low: 'bg-primary/10 text-primary', - info: 'bg-muted text-muted-foreground', -}; - -const CATEGORY_STATE_STYLE: Record = { - assessed: 'bg-success/10 text-success', - partially_assessed: 'bg-warning/10 text-warning', - not_assessed: 'bg-muted text-muted-foreground', - insufficient_evidence: 'bg-muted text-muted-foreground', -}; - -export function SimulationDemo() { - const [phase, setPhase] = useState('idle'); - const [stepIndex, setStepIndex] = useState(0); - const [tab, setTab] = useState<'review' | 'insights'>('review'); - const timers = useRef[]>([]); - - useEffect(() => () => timers.current.forEach(clearTimeout), []); - - const run = () => { - if (phase === 'running') return; - timers.current.forEach(clearTimeout); - timers.current = []; - setPhase('running'); - setStepIndex(0); - SIMULATION_STEPS.forEach((_, index) => { - const timer = setTimeout(() => setStepIndex(index + 1), 550 * (index + 1)); - timers.current.push(timer); - }); - const finish = setTimeout(() => setPhase('done'), 550 * SIMULATION_STEPS.length + 300); - timers.current.push(finish); - }; - - const reset = () => { - timers.current.forEach(clearTimeout); - timers.current = []; - setPhase('idle'); - setStepIndex(0); - setTab('review'); - }; - - return ( -
-
- Scripted simulation · sample repository -
-

See what a PARTHA analysis produces

-

- This is a scripted walkthrough of a made-up sample repository ({SAMPLE_REPO.name}), not a live analysis of any - real code. It uses PARTHA's actual finding categories and output shape so you can see what a real run looks - like without needing an account or a live backend. -

- -
-
-
-

{SAMPLE_REPO.name}

-

- revision {SAMPLE_REPO.revision} · {SAMPLE_REPO.languages} -

-
- {phase !== 'idle' && ( - - )} -
- - {phase === 'idle' && ( -
-

- Run the simulation to watch PARTHA walk through a sample repository and produce Engineering Review and - Repository Insights output. -

- -
- )} - - {phase !== 'idle' && ( -
-
    - {SIMULATION_STEPS.map((step, index) => { - const complete = index < stepIndex; - const active = index === stepIndex && phase === 'running'; - return ( -
  1. - - {complete ? '✓' : index + 1} - - {step} -
  2. - ); - })} -
-
- )} - - {phase === 'done' && ( -
-
- {(['review', 'insights'] as const).map((value) => ( - - ))} -
- - {tab === 'review' && ( -
-
- {SAMPLE_CATEGORIES.map((category) => ( - - {category.label} - {category.findingCount > 0 ? ` (${category.findingCount})` : ''} - - ))} -
-
    - {SAMPLE_FINDINGS.map((finding) => ( -
  • -
    -

    {finding.title}

    - - {finding.severity} - -
    -

    - {CATEGORY_LABELS[finding.category]} -

    -

    {finding.explanation}

    -

    - Remediation: - {finding.remediationGuidance} -

    -

    - {finding.path}:{finding.startLine} - {finding.endLine !== finding.startLine ? `–${finding.endLine}` : ''} -

    -
  • - ))} -
-
- )} - - {tab === 'insights' && ( -
-
- {SAMPLE_METRICS.map((metric) => ( -
-

{metric.value}

-

{metric.label}

-
- ))} -
-
-

Languages

-
- {SAMPLE_LANGUAGES.map((language, index) => ( -
- ))} -
-
- {SAMPLE_LANGUAGES.map((language) => ( - - {language.label} {language.value}% - - ))} -
-
-
- )} -
- )} -
-
- ); -} diff --git a/apps/marketing/src/components/ThemeSwitcher.tsx b/apps/marketing/src/components/ThemeSwitcher.tsx new file mode 100644 index 00000000..836f2724 --- /dev/null +++ b/apps/marketing/src/components/ThemeSwitcher.tsx @@ -0,0 +1,41 @@ +// Ported verbatim from +// apps/frontend/src/features/landing/components/ThemeSwitcher.tsx (#382). +import { Laptop, Moon, Sun } from 'lucide-react'; +import { cn } from '@/utils/cn'; +import type { LandingThemePreference } from '@/hooks/useLandingTheme'; + +interface ThemeSwitcherProps { + preference: LandingThemePreference; + onChange: (preference: LandingThemePreference) => void; + className?: string; +} + +const OPTIONS: { value: LandingThemePreference; label: string; icon: typeof Sun }[] = [ + { value: 'system', label: 'System', icon: Laptop }, + { value: 'light', label: 'Light', icon: Sun }, + { value: 'dark', label: 'Dark', icon: Moon }, +]; + +export function ThemeSwitcher({ preference, onChange, className }: ThemeSwitcherProps) { + return ( +
+ {OPTIONS.map(({ value, label, icon: Icon }) => ( + + ))} +
+ ); +} diff --git a/apps/marketing/src/components/WaitlistForm.tsx b/apps/marketing/src/components/WaitlistForm.tsx deleted file mode 100644 index e2344a3d..00000000 --- a/apps/marketing/src/components/WaitlistForm.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { useState, type FormEvent } from 'react'; - -interface WaitlistFormProps { - onClose: () => void; -} - -type Status = 'idle' | 'submitting' | 'done'; - -export function WaitlistForm({ onClose }: WaitlistFormProps) { - const [email, setEmail] = useState(''); - const [name, setName] = useState(''); - // Honeypot: never rendered visibly, never touched by a real visitor. - const [company, setCompany] = useState(''); - const [status, setStatus] = useState('idle'); - const [error, setError] = useState(null); - - const submit = async (event: FormEvent) => { - event.preventDefault(); - if (status === 'submitting') return; - setStatus('submitting'); - setError(null); - try { - const response = await fetch('/api/waitlist', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email, name, company }), - }); - if (!response.ok) { - const body = await response.json().catch(() => null); - throw new Error(body?.message ?? 'Something went wrong. Please try again.'); - } - setStatus('done'); - } catch (caught) { - setError(caught instanceof Error ? caught.message : 'Something went wrong. Please try again.'); - setStatus('idle'); - } - }; - - return ( -
-
- {status === 'done' ? ( -
-

- You're on the list -

-

- We'll reach out by email when there's room. In the meantime, the real product is open source — - you can run it yourself right now. -

- -
- ) : ( - <> -
-
-

Waitlist

-

- Join the waitlist -

-
- -
-

- PARTHA isn't running as a hosted service right now. Leave your email and we'll follow up when - a hosted beta is ready — or run it yourself in the meantime. -

-
-
- - setEmail(event.target.value)} - className="w-full rounded-xl border border-input bg-background px-3 py-2.5 text-sm text-foreground" - /> -
-
- - setName(event.target.value)} - className="w-full rounded-xl border border-input bg-background px-3 py-2.5 text-sm text-foreground" - /> -
- {/* Honeypot -- visually hidden and unreachable by tab order, so a - real visitor never sees or fills it. */} - - - {error && ( -

- {error} -

- )} - - -
- - )} -
-
- ); -} diff --git a/apps/marketing/src/components/WaitlistModal.tsx b/apps/marketing/src/components/WaitlistModal.tsx new file mode 100644 index 00000000..a5e92e0c --- /dev/null +++ b/apps/marketing/src/components/WaitlistModal.tsx @@ -0,0 +1,146 @@ +import { useState } from 'react'; +import { Loader2 } from 'lucide-react'; +import { useWaitlistForm } from '@/hooks/useWaitlistForm'; +import { cn } from '@/utils/cn'; + +interface WaitlistModalProps { + onClose: () => void; + dark?: boolean; +} + +/** Ported from apps/frontend/src/features/waitlist/components/WaitlistModal.tsx + * (#382) -- same visual design (including its light-mode literal hex + * colors, kept pixel-identical rather than switched to tokens, exactly as + * the original component's own comment explains), adapted with a honeypot + * field and pointed at this project's own /api/waitlist instead of the + * real backend. */ +export function WaitlistModal({ onClose, dark = false }: WaitlistModalProps) { + const [submitted, setSubmitted] = useState(false); + const { email, setEmail, name, setName, company, setCompany, submitting, error, submit } = useWaitlistForm(() => + setSubmitted(true), + ); + + return ( +
+
+ {submitted ? ( +
+

+ You're on the list +

+

+ PARTHA isn't running as a hosted service right now. We'll reach out by email when a hosted + beta is ready. +

+ +
+ ) : ( + <> +
+
+

+ Waitlist +

+

+ Join the waitlist +

+
+ +
+

+ PARTHA isn't running as a hosted service right now. Leave your email and we'll follow up when + a hosted beta is ready -- or run it yourself in the meantime. +

+
+
+ + setEmail(event.target.value)} + className={cn( + 'w-full rounded-xl border px-3 py-2.5 text-sm', + dark ? 'border-input bg-background text-foreground' : 'border-[#fa4d01]/20 bg-white text-[#30152f]', + )} + /> +
+
+ + setName(event.target.value)} + className={cn( + 'w-full rounded-xl border px-3 py-2.5 text-sm', + dark ? 'border-input bg-background text-foreground' : 'border-[#fa4d01]/20 bg-white text-[#30152f]', + )} + /> +
+ {/* Honeypot -- visually hidden, unreachable by tab order. */} + + + {error && ( +

+ {error} +

+ )} + + +
+ + )} +
+
+ ); +} diff --git a/apps/marketing/src/hooks/useLandingTheme.ts b/apps/marketing/src/hooks/useLandingTheme.ts new file mode 100644 index 00000000..713e5bb3 --- /dev/null +++ b/apps/marketing/src/hooks/useLandingTheme.ts @@ -0,0 +1,51 @@ +// Ported verbatim from apps/frontend/src/features/landing/hooks/useLandingTheme.ts +// (#382 redesign): this marketing site reuses the real landing page as its +// visual basis, so the light/dark toggle needs to behave identically, not +// just look similar. No cross-package import -- copied so this project +// stays fully independent and buildable/deployable on its own. +import { create } from 'zustand'; + +export type LandingThemePreference = 'light' | 'dark' | 'system'; +type ResolvedLandingTheme = 'light' | 'dark'; + +const STORAGE_KEY = 'partha-landing-theme'; + +function systemPrefersDark(): boolean { + return window.matchMedia('(prefers-color-scheme: dark)').matches; +} + +function resolve(preference: LandingThemePreference): ResolvedLandingTheme { + return preference === 'system' ? (systemPrefersDark() ? 'dark' : 'light') : preference; +} + +function readStoredPreference(): LandingThemePreference { + const stored = window.localStorage.getItem(STORAGE_KEY); + return stored === 'light' || stored === 'dark' || stored === 'system' ? stored : 'system'; +} + +interface LandingThemeState { + preference: LandingThemePreference; + resolved: ResolvedLandingTheme; + setPreference: (preference: LandingThemePreference) => void; +} + +export const useLandingThemeStore = create((set) => ({ + preference: readStoredPreference(), + resolved: resolve(readStoredPreference()), + setPreference: (preference) => { + window.localStorage.setItem(STORAGE_KEY, preference); + set({ preference, resolved: resolve(preference) }); + }, +})); + +if (typeof window !== 'undefined') { + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { + const { preference } = useLandingThemeStore.getState(); + if (preference !== 'system') return; + useLandingThemeStore.setState({ resolved: resolve('system') }); + }); +} + +export function useLandingTheme() { + return useLandingThemeStore(); +} diff --git a/apps/marketing/src/hooks/useWaitlistForm.ts b/apps/marketing/src/hooks/useWaitlistForm.ts new file mode 100644 index 00000000..2471c8f3 --- /dev/null +++ b/apps/marketing/src/hooks/useWaitlistForm.ts @@ -0,0 +1,38 @@ +import { useState, type FormEvent } from 'react'; + +/** Adapted from apps/frontend/src/features/waitlist/hooks/useWaitlistForm.ts + * (#382): same shape, but posts to this project's own /api/waitlist + * serverless function instead of the real backend's /waitlist route -- + * there is no live backend behind this site. */ +export function useWaitlistForm(onSuccess: () => void) { + const [email, setEmail] = useState(''); + const [name, setName] = useState(''); + const [company, setCompany] = useState(''); // honeypot, never shown + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const submit = async (event: FormEvent) => { + event.preventDefault(); + if (submitting) return; + setSubmitting(true); + setError(null); + try { + const response = await fetch('/api/waitlist', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: email.trim(), name: name.trim() || undefined, company }), + }); + if (!response.ok) { + const body = await response.json().catch(() => null); + throw new Error(body?.message ?? 'Something went wrong. Please try again.'); + } + onSuccess(); + } catch (caught) { + setError(caught instanceof Error ? caught.message : 'Something went wrong. Please try again.'); + } finally { + setSubmitting(false); + } + }; + + return { email, setEmail, name, setName, company, setCompany, submitting, error, submit }; +} diff --git a/apps/marketing/src/styles/globals.css b/apps/marketing/src/styles/globals.css index 4da2f457..de2dead1 100644 --- a/apps/marketing/src/styles/globals.css +++ b/apps/marketing/src/styles/globals.css @@ -2,34 +2,56 @@ @tailwind components; @tailwind utilities; -/* Same HSL token values as apps/frontend/src/styles/globals.css's :root - (light mode only -- this site doesn't need a dark-mode switcher), copied - rather than shared so this project has no cross-package dependency on - apps/frontend at all. */ +/* Full token set copied verbatim from apps/frontend/src/styles/globals.css + (#382 redesign) -- including .landing-dark, which the reused LandingPage + applies to its own root element only (see App.tsx). Copied rather than + shared so this project has no cross-package dependency on apps/frontend + at all. */ @layer base { :root { --background: 38 27% 94%; --foreground: 258 19% 13%; --card: 0 0% 100%; --card-foreground: 258 19% 13%; + --popover: 0 0% 100%; + --popover-foreground: 258 19% 13%; --primary: 18 99% 38%; --primary-foreground: 0 0% 100%; + --secondary: 198 39% 85%; + --secondary-foreground: 260 10% 34%; --muted: 38 25% 92%; --muted-foreground: 263 8% 43%; --accent: 18 100% 94%; --accent-foreground: 258 19% 13%; + --destructive: 0 84% 60%; + --destructive-foreground: 210 40% 98%; --success: 142 76% 26%; --success-foreground: 210 40% 98%; --warning: 38 92% 29%; --warning-foreground: 258 19% 13%; - --destructive: 0 84% 60%; - --destructive-foreground: 210 40% 98%; --border: 18 66% 76%; --input: 18 42% 84%; --ring: 17 91% 50%; --radius: 1rem; } + /* Scoped to the landing page's own root element only (never applied to + ) -- see App.tsx and useLandingTheme.ts. */ + .landing-dark { + --background: 0 0% 0%; + --foreground: 0 0% 100%; + --card: 0 2% 10%; + --card-foreground: 0 0% 100%; + --primary: 18 99% 49%; + --primary-foreground: 0 0% 100%; + --muted-foreground: 0 0% 48%; + --accent: 0 0% 16%; + --accent-foreground: 0 0% 100%; + --border: 0 0% 20%; + --input: 0 0% 20%; + --ring: 18 99% 49%; + } + body { @apply bg-background text-foreground font-sans antialiased; } diff --git a/apps/marketing/src/utils/cn.ts b/apps/marketing/src/utils/cn.ts new file mode 100644 index 00000000..2819a830 --- /dev/null +++ b/apps/marketing/src/utils/cn.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/apps/marketing/tailwind.config.ts b/apps/marketing/tailwind.config.ts index ce9405ca..8abc88f5 100644 --- a/apps/marketing/tailwind.config.ts +++ b/apps/marketing/tailwind.config.ts @@ -1,11 +1,10 @@ import type { Config } from 'tailwindcss'; -// A deliberately smaller copy of apps/frontend/tailwind.config.ts's token -// system -- same HSL custom-property convention and brand primary, but only -// the tokens this site's own components actually use. Kept as an -// independent file (not imported across the package boundary) so this site -// stays a genuinely standalone, independently buildable/deployable project -// (#382) -- the whole point of splitting it out from apps/frontend. +// Matches apps/frontend/tailwind.config.ts's token system (#382 redesign: +// this site now reuses the real LandingPage component, so its Tailwind +// config needs to resolve the exact same tokens that component's classes +// reference). Copied rather than shared so this project stays a genuinely +// standalone, independently buildable/deployable project. export default { darkMode: 'class', content: ['./index.html', './src/**/*.{ts,tsx}'], @@ -21,6 +20,14 @@ export default { DEFAULT: 'hsl(var(--primary))', foreground: 'hsl(var(--primary-foreground))', }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, muted: { DEFAULT: 'hsl(var(--muted))', foreground: 'hsl(var(--muted-foreground))', @@ -37,14 +44,14 @@ export default { DEFAULT: 'hsl(var(--warning))', foreground: 'hsl(var(--warning-foreground))', }, - destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))', - }, card: { DEFAULT: 'hsl(var(--card))', foreground: 'hsl(var(--card-foreground))', }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, }, borderRadius: { lg: 'var(--radius)', @@ -56,15 +63,15 @@ export default { mono: ['JetBrains Mono', 'Fira Code', 'monospace'], }, fontSize: { - '2xs': ['0.6875rem', { lineHeight: '1rem' }], + '2xs': ['0.625rem', { lineHeight: '0.875rem' }], }, keyframes: { fadeIn: { '0%': { opacity: '0' }, '100%': { opacity: '1' } }, slideIn: { '0%': { opacity: '0', transform: 'translateY(8px)' }, '100%': { opacity: '1', transform: 'translateY(0)' } }, }, animation: { - 'fade-in': 'fadeIn 0.4s ease-out', - 'slide-in': 'slideIn 0.35s ease-out', + 'fade-in': 'fadeIn 0.3s ease-out', + 'slide-in': 'slideIn 0.3s ease-out', }, }, }, From f1b168679d504b41a724569d71f618af92286739 Mon Sep 17 00:00:00 2001 From: PARTH J ROHIT Date: Tue, 1 Sep 2026 19:43:50 +0100 Subject: [PATCH 3/4] refactor(marketing): remove waitlist, demo the simulation from "See how it works" (#382) Product-direction clarification: PARTHA stays self-hostable only for the foreseeable future, with no hosted service planned. There's nothing to put anyone on a waitlist for. - Remove WaitlistModal and its useWaitlistForm hook entirely, and every reference/CTA to it in RunItYourselfModal and DemoModal's own comments. - Remove the now-dead api/waitlist.ts serverless function and its test, along with the WAITLIST_GITHUB_TOKEN/WAITLIST_GIST_ID setup instructions in the README (the whole "Waitlist form setup" section). - Drop the now-unused tsx devDependency and the test script that ran only against api/waitlist.test.ts. - RunItYourselfModal no longer needs a dark prop now that it has nothing left to thread it to. Also: "See how it works" now opens the scripted demo simulation directly (DemoModal), same as "Log in" already does, instead of scrolling to an anchor section -- there's nothing live to log into or a walkthrough to scroll to, so both hotspots lead to the same demo. Everything else about the page is untouched. Verified: typecheck/lint/build clean; real in-browser click-through in both light and dark mode confirming "See how it works" opens the demo and "Analyze a Repository" no longer offers a waitlist path. --- apps/marketing/README.md | 46 ++---- apps/marketing/api/waitlist.test.ts | 124 -------------- apps/marketing/api/waitlist.ts | 156 ------------------ apps/marketing/package-lock.json | 31 +++- apps/marketing/package.json | 6 +- apps/marketing/src/App.tsx | 16 +- apps/marketing/src/components/DemoModal.tsx | 13 +- .../src/components/RunItYourselfModal.tsx | 26 +-- .../src/components/WaitlistModal.tsx | 146 ---------------- apps/marketing/src/hooks/useWaitlistForm.ts | 38 ----- 10 files changed, 71 insertions(+), 531 deletions(-) delete mode 100644 apps/marketing/api/waitlist.test.ts delete mode 100644 apps/marketing/api/waitlist.ts delete mode 100644 apps/marketing/src/components/WaitlistModal.tsx delete mode 100644 apps/marketing/src/hooks/useWaitlistForm.ts diff --git a/apps/marketing/README.md b/apps/marketing/README.md index 18a094cc..8730958b 100644 --- a/apps/marketing/README.md +++ b/apps/marketing/README.md @@ -4,11 +4,15 @@ A free, static marketing site for PARTHA (#382). It reuses the real `apps/fronte 1728px-wide authored design, the same light/dark theme system, the same FAQ and footer — as its visual basis, ported in rather than re-invented, with two behavioral differences since this site has no backend or accounts at all: -- The "Log In" nav hotspot has nothing to log in to, so it opens a scripted product simulation instead - (`src/components/DemoModal.tsx`), using canned sample data. +- The "Log In" nav hotspot and the "See how it works" hero hotspot both open a scripted product simulation instead + (`src/components/DemoModal.tsx`), using canned sample data — there's nothing live to log into or scroll to a + walkthrough of, so both lead to the same demo. - Every "Analyze a Repository" hotspot has no live backend to analyze against, so it opens fork/clone setup instructions instead (`src/components/RunItYourselfModal.tsx`), plus a note encouraging people to star the repo. +There is no waitlist or hosted-beta path anywhere on this site: PARTHA is self-hosted only for the foreseeable +future, with no hosted service planned, so there's nothing to wait for. + Deliberately independent of `apps/frontend` — no backend dependency, and nothing here talks to the real Render/Neon deployment (which is paused, not live — see #375/#377). It builds and runs entirely on its own; shared visual assets (SVG artwork, theme hook, tokens) are copied in directly rather than imported cross-package, to keep it that way. @@ -22,16 +26,14 @@ npm run dev ``` Opens at `http://localhost:5173` (or the next free port Vite picks; the repo's `.claude/launch.json` runs it on -`5174` alongside the real frontend dev server on `5173`). The waitlist form will show a "not configured" error -locally unless you also run `vercel dev` with the env vars below set — that's expected; the rest of the page works -fully without it. +`5174` alongside the real frontend dev server on `5173`). The entire page works with zero configuration — there is +no backend call anywhere on the site. ## Verification ```bash npm run typecheck # tsc -b --noEmit npm run lint # eslint . -npm run test # node --test against api/waitlist.ts, with fetch mocked -- no real network/GitHub call npm run build # tsc -b && vite build -- outputs to dist/ npm run preview # serve the production build locally ``` @@ -44,40 +46,16 @@ This project is intentionally **not** part of the root npm workspace (`apps/fron 1. In the Vercel dashboard: **Add New → Project**, import the `Second-Origin/PARTHA` GitHub repository. 2. Set the project's **Root Directory** to `apps/marketing`. Vercel auto-detects the Vite framework preset from there; `vercel.json` in this directory pins the build command and output directory explicitly regardless. -3. No required env vars to deploy the site itself — the page renders and the simulation works with zero - configuration. The waitlist form needs two, added under **Settings → Environment Variables**, before it will - actually store submissions (see below). +3. No env vars needed to deploy — the page renders and the simulation works with zero configuration. 4. Deploy. Vercel gives you a `*.vercel.app` URL immediately; a custom domain can be attached afterward under **Settings → Domains**. -## Waitlist form setup (one-time) - -There's no live Postgres to write submissions to (the real backend is paused). Instead, `api/waitlist.ts` — a Vercel -serverless function, deployed automatically alongside the site — appends each submission to a private GitHub Gist. -This needs no new third-party service: just a GitHub token, since the project already has a GitHub account. - -1. Create an empty **secret** Gist at , with exactly one file named `waitlist.json` - containing `[]`. Copy its id from the URL (the part after the last `/`). -2. Create a **fine-grained** GitHub Personal Access Token at - , scoped to **only** that Gist (Permissions → Gists → - Read and write). Do not use a classic token with broader `repo` scope than this needs. -3. In the Vercel project, **Settings → Environment Variables**, add: - - `WAITLIST_GITHUB_TOKEN` — the token from step 2. - - `WAITLIST_GIST_ID` — the Gist id from step 1. -4. Redeploy (Vercel → Deployments → ⋯ → Redeploy) so the function picks up the new env vars. - -Until both are set, the form fails with a clear "not configured yet" message instead of silently dropping -submissions or crashing — the rest of the site is unaffected either way. - -To read submissions later: open the Gist directly, or `curl -H "Authorization: Bearer " -https://api.github.com/gists/`. - ## Structure - `src/App.tsx` — the ported landing page: renders the authored SVG artwork (`src/assets/landing/`), overlays the invisible nav/hero/footer hotspots on top, and wires the two behavioral differences described above. -- `src/components/DemoModal.tsx`, `RunItYourselfModal.tsx`, `WaitlistModal.tsx` — the three interactive surfaces - reached from those hotspots. +- `src/components/DemoModal.tsx`, `RunItYourselfModal.tsx` — the two interactive surfaces reached from those + hotspots. - `src/hooks/useLandingTheme.ts`, `src/components/ThemeSwitcher.tsx` — verbatim ports of the real frontend's light/dark/system theme store and toggle. - `src/data/sampleAnalysis.ts` — the canned data behind the demo simulation, using PARTHA's real finding @@ -89,4 +67,6 @@ https://api.github.com/gists/`. clearly labeled in the UI as a scripted sample-repository walkthrough, not a live analysis. - Does not create or need a PARTHA account, login, or session — the reused landing page's "Log In" hotspot opens the demo instead of a login flow. +- Does not offer a waitlist or any hosted-beta path — the product is self-hosted only, with no hosted service + planned, so "run it yourself" is the only call to action. - Does not touch `apps/frontend`, the backend, or `render.yaml` — those stay exactly as they are, paused. diff --git a/apps/marketing/api/waitlist.test.ts b/apps/marketing/api/waitlist.test.ts deleted file mode 100644 index a89214ac..00000000 --- a/apps/marketing/api/waitlist.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Unit tests for the waitlist serverless function (#382). Run with - * `npm run test` (node's built-in test runner + tsx loader) -- no real - * network call is ever made; `global.fetch` is replaced with a fake that - * only understands the exact GitHub Gist requests this handler issues. - */ - -import assert from 'node:assert/strict'; -import { afterEach, beforeEach, describe, it, mock } from 'node:test'; -import handler, { type WaitlistRequest, type WaitlistResponse } from './waitlist.ts'; - -interface JsonErrorBody { - error?: string; - message?: string; -} - -interface JsonOkBody { - ok?: boolean; -} - -function fakeGitHubResponse(body: unknown, ok = true, status = ok ? 200 : 500) { - return { ok, status, json: async () => body }; -} - -function makeRes(): { res: WaitlistResponse; calls: { status?: number; json?: unknown } } { - const calls: { status?: number; json?: unknown } = {}; - const res: WaitlistResponse = { - status(code: number) { - calls.status = code; - return res; - }, - json(body: unknown) { - calls.json = body; - }, - setHeader() {}, - }; - return { res, calls }; -} - -function request(overrides: WaitlistRequest): WaitlistRequest { - return { method: 'POST', ...overrides }; -} - -describe('POST /api/waitlist', () => { - const originalEnv = { ...process.env }; - - beforeEach(() => { - process.env.WAITLIST_GITHUB_TOKEN = 'fake-token-not-real'; - process.env.WAITLIST_GIST_ID = 'fake-gist-id'; - }); - - afterEach(() => { - process.env = { ...originalEnv }; - mock.reset(); - }); - - it('rejects non-POST methods', async () => { - const { res, calls } = makeRes(); - await handler(request({ method: 'GET' }), res); - assert.equal(calls.status, 405); - }); - - it('returns 503 when the env vars are not configured', async () => { - delete process.env.WAITLIST_GITHUB_TOKEN; - const { res, calls } = makeRes(); - await handler(request({ body: { email: 'a@example.com' } }), res); - assert.equal(calls.status, 503); - assert.equal((calls.json as JsonErrorBody).error, 'waitlist_unconfigured'); - }); - - it('rejects a missing or malformed email', async () => { - const { res, calls } = makeRes(); - await handler(request({ body: { email: 'not-an-email' } }), res); - assert.equal(calls.status, 400); - assert.equal((calls.json as JsonErrorBody).error, 'invalid_email'); - }); - - it('silently accepts (without storing) a submission with the honeypot field filled in', async () => { - const fetchMock = mock.fn(async () => { - throw new Error('fetch must not be called for a honeypot submission'); - }); - mock.method(globalThis, 'fetch', fetchMock); - - const { res, calls } = makeRes(); - await handler(request({ body: { email: 'a@example.com', company: 'a bot filled this in' } }), res); - assert.equal(calls.status, 200); - assert.equal(fetchMock.mock.callCount(), 0); - }); - - it('appends a valid submission to the Gist, deduping by email', async () => { - const existing = [{ email: 'existing@example.com', name: null, submittedAt: '2026-01-01T00:00:00.000Z' }]; - const fetchMock = mock.fn(async (url: string | URL, init?: RequestInit) => { - if (!init || init.method === undefined) { - // GET - assert.match(String(url), /\/gists\/fake-gist-id$/); - return fakeGitHubResponse({ files: { 'waitlist.json': { content: JSON.stringify(existing) } } }); - } - // PATCH - assert.equal(init.method, 'PATCH'); - const payload = JSON.parse(String(init.body)); - const rows = JSON.parse(payload.files['waitlist.json'].content); - assert.equal(rows.length, 2); - assert.equal(rows[1].email, 'new@example.com'); - assert.equal(rows[1].name, 'New Person'); - return fakeGitHubResponse({ ok: true }); - }); - mock.method(globalThis, 'fetch', fetchMock); - - const { res, calls } = makeRes(); - await handler(request({ body: { email: 'New@Example.com', name: 'New Person' } }), res); - assert.equal(calls.status, 200); - assert.equal((calls.json as JsonOkBody).ok, true); - assert.equal(fetchMock.mock.callCount(), 2); - }); - - it('returns 502 if the GitHub API call fails', async () => { - mock.method(globalThis, 'fetch', async () => fakeGitHubResponse(null, false, 401)); - - const { res, calls } = makeRes(); - await handler(request({ body: { email: 'a@example.com' } }), res); - assert.equal(calls.status, 502); - assert.equal((calls.json as JsonErrorBody).error, 'waitlist_storage_failed'); - }); -}); diff --git a/apps/marketing/api/waitlist.ts b/apps/marketing/api/waitlist.ts deleted file mode 100644 index 9f0a7e5c..00000000 --- a/apps/marketing/api/waitlist.ts +++ /dev/null @@ -1,156 +0,0 @@ -/** - * Waitlist submission endpoint (#382), deployed by Vercel as a serverless - * function at /api/waitlist. - * - * No live Postgres to write to (the real backend is paused, not deployed -- - * see #375/#377). Instead of introducing a new third-party account nobody - * has yet, this appends each submission to a private GitHub Gist using a - * fine-grained Personal Access Token -- both GitHub and Vercel are - * platforms the project already has accounts on, this needs no new - * service, and a Gist has no realistic volume limit for a waitlist. - * - * Required environment variables (set in the Vercel project's Environment - * Variables, never committed): - * WAITLIST_GITHUB_TOKEN -- a fine-grained PAT with read/write access to - * exactly one Gist (Gists -> Read and write). Create it at - * https://github.com/settings/personal-access-tokens/new - * WAITLIST_GIST_ID -- the id of that Gist (the segment after the last - * slash in its URL). Create an empty secret Gist first, containing one - * file named waitlist.json with the content "[]". - * - * If either is unset, this returns 503 rather than silently dropping - * submissions or crashing -- see apps/marketing/README.md for the one-time - * setup steps. - * - * Typed against a minimal local interface (below) rather than importing - * @vercel/node for VercelRequest/VercelResponse: Vercel's actual runtime - * behavior (JSON body parsing onto `.body`, the `.status().json()` response - * helpers) is unaffected by which types this file imports -- it's a - * platform behavior, not something the npm package provides at runtime -- - * and @vercel/node pulls in a deep, currently-vulnerable dev-only - * dependency chain (ajv/path-to-regexp/undici via @vercel/static-config) - * purely for type declarations this file doesn't need more than a few - * lines of. - */ - -// Exported so tests can construct properly-typed requests/responses instead -// of casting through `any`. -export interface WaitlistRequest { - method?: string; - body?: unknown; -} - -export interface WaitlistResponse { - status(code: number): WaitlistResponse; - json(body: unknown): void; - setHeader(name: string, value: string): void; -} - -const GIST_FILENAME = 'waitlist.json'; -const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; -const MAX_EMAIL_LENGTH = 320; -const MAX_NAME_LENGTH = 200; - -interface WaitlistEntry { - email: string; - name: string | null; - submittedAt: string; -} - -function isValidBody(body: unknown): body is { email: string; name?: string; company?: string } { - if (typeof body !== 'object' || body === null) return false; - const record = body as Record; - return typeof record.email === 'string'; -} - -export default async function handler(req: WaitlistRequest, res: WaitlistResponse) { - if (req.method !== 'POST') { - res.setHeader('Allow', 'POST'); - return res.status(405).json({ error: 'method_not_allowed', message: 'Use POST.' }); - } - - const token = process.env.WAITLIST_GITHUB_TOKEN; - const gistId = process.env.WAITLIST_GIST_ID; - if (!token || !gistId) { - return res.status(503).json({ - error: 'waitlist_unconfigured', - message: 'The waitlist is not configured yet. Please try again later.', - }); - } - - if (!isValidBody(req.body)) { - return res.status(400).json({ error: 'invalid_request', message: 'A valid email is required.' }); - } - - // Honeypot: a hidden field real visitors never fill in. A bot that fills - // every field trips this silently -- reported as success so it doesn't - // learn to skip the field, but nothing is stored. - if (typeof req.body.company === 'string' && req.body.company.trim().length > 0) { - return res.status(200).json({ ok: true }); - } - - const email = req.body.email.trim().toLowerCase(); - const name = typeof req.body.name === 'string' ? req.body.name.trim() : ''; - - if (!email || email.length > MAX_EMAIL_LENGTH || !EMAIL_PATTERN.test(email)) { - return res.status(400).json({ error: 'invalid_email', message: 'Enter a valid email address.' }); - } - if (name.length > MAX_NAME_LENGTH) { - return res.status(400).json({ error: 'invalid_name', message: 'Name is too long.' }); - } - - const entry: WaitlistEntry = { email, name: name || null, submittedAt: new Date().toISOString() }; - - try { - await appendToGist(token, gistId, entry); - } catch (error) { - console.error('waitlist: failed to record submission', error); - return res.status(502).json({ - error: 'waitlist_storage_failed', - message: 'Could not record your submission right now. Please try again shortly.', - }); - } - - return res.status(200).json({ ok: true }); -} - -async function appendToGist(token: string, gistId: string, entry: WaitlistEntry): Promise { - const headers = { - Authorization: `Bearer ${token}`, - Accept: 'application/vnd.github+json', - 'X-GitHub-Api-Version': '2022-11-28', - }; - - const getResponse = await fetch(`https://api.github.com/gists/${gistId}`, { headers }); - if (!getResponse.ok) { - throw new Error(`GET gist failed: ${getResponse.status}`); - } - const gist = await getResponse.json(); - const file = gist.files?.[GIST_FILENAME]; - const existing: WaitlistEntry[] = file?.content ? safeParseArray(file.content) : []; - - // Never store the same email twice -- a resubmission (e.g. a retried - // request) updates its position rather than duplicating the entry. - const deduped = existing.filter((row) => row.email !== entry.email); - deduped.push(entry); - - const patchResponse = await fetch(`https://api.github.com/gists/${gistId}`, { - method: 'PATCH', - headers: { ...headers, 'Content-Type': 'application/json' }, - body: JSON.stringify({ - files: { [GIST_FILENAME]: { content: JSON.stringify(deduped, null, 2) } }, - }), - }); - if (!patchResponse.ok) { - throw new Error(`PATCH gist failed: ${patchResponse.status}`); - } -} - -function safeParseArray(raw: string): WaitlistEntry[] { - try { - const parsed = JSON.parse(raw); - return Array.isArray(parsed) ? parsed : []; - } catch { - return []; - } -} diff --git a/apps/marketing/package-lock.json b/apps/marketing/package-lock.json index 577b2478..980a8867 100644 --- a/apps/marketing/package-lock.json +++ b/apps/marketing/package-lock.json @@ -28,7 +28,6 @@ "globals": "^17.11.0", "postcss": "^8.5.26", "tailwindcss": "^3.4.10", - "tsx": "^4.19.2", "typescript": "^5.5.4", "typescript-eslint": "^8.67.0", "vite": "^8.2.2" @@ -300,6 +299,7 @@ "os": [ "aix" ], + "peer": true, "engines": { "node": ">=18" } @@ -317,6 +317,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -334,6 +335,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -351,6 +353,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -368,6 +371,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=18" } @@ -385,6 +389,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=18" } @@ -402,6 +407,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -419,6 +425,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -436,6 +443,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -453,6 +461,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -470,6 +479,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -487,6 +497,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -504,6 +515,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -521,6 +533,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -538,6 +551,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -555,6 +569,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -572,6 +587,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -589,6 +605,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -606,6 +623,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -623,6 +641,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -640,6 +659,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -657,6 +677,7 @@ "os": [ "openharmony" ], + "peer": true, "engines": { "node": ">=18" } @@ -674,6 +695,7 @@ "os": [ "sunos" ], + "peer": true, "engines": { "node": ">=18" } @@ -691,6 +713,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -708,6 +731,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -725,6 +749,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -2019,6 +2044,8 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "optional": true, + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -3814,6 +3841,8 @@ "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "esbuild": "~0.28.0" }, diff --git a/apps/marketing/package.json b/apps/marketing/package.json index 12b32365..530e74cf 100644 --- a/apps/marketing/package.json +++ b/apps/marketing/package.json @@ -2,15 +2,14 @@ "name": "@partha/marketing", "private": true, "version": "0.1.0", - "description": "Static marketing site for PARTHA: a scripted product simulation, a 'run it yourself' CTA to the main repository, and a waitlist form. Deliberately independent of apps/frontend -- no backend dependency, deployable to Vercel on its own (#382).", + "description": "Static marketing site for PARTHA: the real product's landing page, repurposed with a scripted product simulation and a 'run it yourself' CTA to the main repository. Deliberately independent of apps/frontend -- no backend dependency, deployable to Vercel on its own (#382).", "type": "module", "scripts": { "dev": "vite", "build": "tsc -b && vite build", "preview": "vite preview", "lint": "eslint .", - "typecheck": "tsc -b --noEmit", - "test": "node --import tsx --test api/*.test.ts" + "typecheck": "tsc -b --noEmit" }, "dependencies": { "clsx": "^2.1.1", @@ -33,7 +32,6 @@ "globals": "^17.11.0", "postcss": "^8.5.26", "tailwindcss": "^3.4.10", - "tsx": "^4.19.2", "typescript": "^5.5.4", "typescript-eslint": "^8.67.0", "vite": "^8.2.2" diff --git a/apps/marketing/src/App.tsx b/apps/marketing/src/App.tsx index 3d87f369..372328b9 100644 --- a/apps/marketing/src/App.tsx +++ b/apps/marketing/src/App.tsx @@ -14,8 +14,10 @@ import landingReferenceDark from '@/assets/landing/landing-reference-dark.svg'; * direction. Two behavioral differences from the real app, since this * standalone site has no backend and no accounts at all: * - * - The nav's "Log in" hotspot has nothing to log in to here, so it opens - * the scripted demo simulation (DemoModal) instead of a dead route. + * - The nav's "Log in" hotspot and the hero's "See how it works" hotspot + * both open the scripted demo simulation (DemoModal) instead of a dead + * login route / an anchor scroll -- seeing PARTHA "work" here means + * watching the simulation, since there's nothing live to log into. * - Every "Analyze a Repository" hotspot has nothing to analyze against * (no live backend), so it opens setup instructions for running the real * product locally (RunItYourselfModal) instead. @@ -78,7 +80,13 @@ export function App() {
- See how PARTHA works + {analyzeCta('absolute left-[49.4%] top-[13.5%] z-20 h-[1.35%] w-[22.4%]')} {faqQuestions.map((question, index) => ( @@ -117,7 +125,7 @@ export function App() { )} {demoOpen && setDemoOpen(false)} />} - {runItYourselfOpen && setRunItYourselfOpen(false)} />} + {runItYourselfOpen && setRunItYourselfOpen(false)} />} {footerNotice && (
diff --git a/apps/marketing/src/components/DemoModal.tsx b/apps/marketing/src/components/DemoModal.tsx index 89c95219..fa6d582f 100644 --- a/apps/marketing/src/components/DemoModal.tsx +++ b/apps/marketing/src/components/DemoModal.tsx @@ -27,12 +27,13 @@ const CATEGORY_STATE_STYLE: Record = { insufficient_evidence: 'bg-muted text-muted-foreground', }; -/** Opened from the reused LandingPage's "Log in" nav hotspot (#382 redesign - * -- there is no login flow or dashboard in this standalone site, so that - * hotspot now leads here instead of a dead route). A modal, not a page - * section: keeps the reused landing artwork's own length and layout - * completely untouched, the same interaction shape the real page already - * uses for its FAQ answers and waitlist prompt. */ +/** Opened from the reused LandingPage's "Log in" nav hotspot and its "See + * how it works" hero hotspot (#382 redesign) -- there is no login flow, no + * dashboard, and no anchor-scrollable walkthrough in this standalone site, + * so both hotspots lead here instead. A modal, not a page section: keeps + * the reused landing artwork's own length and layout completely untouched, + * the same interaction shape the real page already uses for its FAQ + * answers. */ export function DemoModal({ onClose }: { onClose: () => void }) { const [phase, setPhase] = useState('idle'); const [stepIndex, setStepIndex] = useState(0); diff --git a/apps/marketing/src/components/RunItYourselfModal.tsx b/apps/marketing/src/components/RunItYourselfModal.tsx index b2b2d5b6..50f95178 100644 --- a/apps/marketing/src/components/RunItYourselfModal.tsx +++ b/apps/marketing/src/components/RunItYourselfModal.tsx @@ -1,6 +1,3 @@ -import { useState } from 'react'; -import { WaitlistModal } from '@/components/WaitlistModal'; - const GITHUB_URL = 'https://github.com/Second-Origin/PARTHA'; // Mirrors the exact commands in the repository's own README ("Run PARTHA @@ -23,10 +20,10 @@ npm run dev:frontend`; * (#382 redesign) -- there is no live backend for this button to actually * analyze anything against, so instead of running the scripted demo (see * DemoModal, reached from "Log in" instead) it shows how to run the real - * product against a visitor's own code. */ -export function RunItYourselfModal({ onClose, dark = false }: { onClose: () => void; dark?: boolean }) { - const [waitlistOpen, setWaitlistOpen] = useState(false); - + * product against a visitor's own code. No waitlist/hosted-beta path: the + * product direction is self-host-only for the foreseeable future, so + * "run it yourself" is the only call to action here. */ +export function RunItYourselfModal({ onClose }: { onClose: () => void }) { return (
@@ -47,9 +44,9 @@ export function RunItYourselfModal({ onClose, dark = false }: { onClose: () => v

- PARTHA isn't running as a hosted service right now, so there's nothing here to submit a repository - URL to. It's open source and runs locally with no external service beyond a public GitHub repository to - analyze -- fork or clone it, run it, and try it on your own code. + PARTHA isn't running as a hosted service, and there are no plans to host it -- it's open source and + self-hosted only. It runs locally with no external service beyond a public GitHub repository to analyze -- + fork or clone it, run it, and try it on your own code.

@@ -98,17 +95,8 @@ export function RunItYourselfModal({ onClose, dark = false }: { onClose: () => v {' '} to support the project and help others find it.

-

- Prefer a hosted version instead of running it yourself?{' '} - - . -

- - {waitlistOpen && setWaitlistOpen(false)} />}
); } diff --git a/apps/marketing/src/components/WaitlistModal.tsx b/apps/marketing/src/components/WaitlistModal.tsx deleted file mode 100644 index a5e92e0c..00000000 --- a/apps/marketing/src/components/WaitlistModal.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { useState } from 'react'; -import { Loader2 } from 'lucide-react'; -import { useWaitlistForm } from '@/hooks/useWaitlistForm'; -import { cn } from '@/utils/cn'; - -interface WaitlistModalProps { - onClose: () => void; - dark?: boolean; -} - -/** Ported from apps/frontend/src/features/waitlist/components/WaitlistModal.tsx - * (#382) -- same visual design (including its light-mode literal hex - * colors, kept pixel-identical rather than switched to tokens, exactly as - * the original component's own comment explains), adapted with a honeypot - * field and pointed at this project's own /api/waitlist instead of the - * real backend. */ -export function WaitlistModal({ onClose, dark = false }: WaitlistModalProps) { - const [submitted, setSubmitted] = useState(false); - const { email, setEmail, name, setName, company, setCompany, submitting, error, submit } = useWaitlistForm(() => - setSubmitted(true), - ); - - return ( -
-
- {submitted ? ( -
-

- You're on the list -

-

- PARTHA isn't running as a hosted service right now. We'll reach out by email when a hosted - beta is ready. -

- -
- ) : ( - <> -
-
-

- Waitlist -

-

- Join the waitlist -

-
- -
-

- PARTHA isn't running as a hosted service right now. Leave your email and we'll follow up when - a hosted beta is ready -- or run it yourself in the meantime. -

-
-
- - setEmail(event.target.value)} - className={cn( - 'w-full rounded-xl border px-3 py-2.5 text-sm', - dark ? 'border-input bg-background text-foreground' : 'border-[#fa4d01]/20 bg-white text-[#30152f]', - )} - /> -
-
- - setName(event.target.value)} - className={cn( - 'w-full rounded-xl border px-3 py-2.5 text-sm', - dark ? 'border-input bg-background text-foreground' : 'border-[#fa4d01]/20 bg-white text-[#30152f]', - )} - /> -
- {/* Honeypot -- visually hidden, unreachable by tab order. */} - - - {error && ( -

- {error} -

- )} - - -
- - )} -
-
- ); -} diff --git a/apps/marketing/src/hooks/useWaitlistForm.ts b/apps/marketing/src/hooks/useWaitlistForm.ts deleted file mode 100644 index 2471c8f3..00000000 --- a/apps/marketing/src/hooks/useWaitlistForm.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useState, type FormEvent } from 'react'; - -/** Adapted from apps/frontend/src/features/waitlist/hooks/useWaitlistForm.ts - * (#382): same shape, but posts to this project's own /api/waitlist - * serverless function instead of the real backend's /waitlist route -- - * there is no live backend behind this site. */ -export function useWaitlistForm(onSuccess: () => void) { - const [email, setEmail] = useState(''); - const [name, setName] = useState(''); - const [company, setCompany] = useState(''); // honeypot, never shown - const [submitting, setSubmitting] = useState(false); - const [error, setError] = useState(null); - - const submit = async (event: FormEvent) => { - event.preventDefault(); - if (submitting) return; - setSubmitting(true); - setError(null); - try { - const response = await fetch('/api/waitlist', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email: email.trim(), name: name.trim() || undefined, company }), - }); - if (!response.ok) { - const body = await response.json().catch(() => null); - throw new Error(body?.message ?? 'Something went wrong. Please try again.'); - } - onSuccess(); - } catch (caught) { - setError(caught instanceof Error ? caught.message : 'Something went wrong. Please try again.'); - } finally { - setSubmitting(false); - } - }; - - return { email, setEmail, name, setName, company, setCompany, submitting, error, submit }; -} From 844fa8b480c771cab5e1307aedbde444f5bcfeac Mon Sep 17 00:00:00 2001 From: PARTH J ROHIT Date: Tue, 1 Sep 2026 20:02:02 +0100 Subject: [PATCH 4/4] refactor(marketing): slide-in side panel instead of centered popups (#382) Replace the centered overlay-popup pattern used by DemoModal, RunItYourselfModal, and the FAQ dialog with a shared slide-in drawer (SlidePanel) that animates in from the right edge over part of the screen, instead of appearing as a popup overlapping the page. Purely a presentation/interaction change -- every dialog's actual content and behavior is unchanged, only the container. Along the way, added two behaviors the old centered popups didn't have: closing on Escape and on a backdrop click (both standard for a drawer, neither carried over from the previous pattern since it never had them). Each dialog keeps its own width (DemoModal wider at max-w-3xl for its two-tab results view, RunItYourselfModal and the FAQ panel narrower) and its existing internal scroll/sticky-header behavior, now scoped to the panel's own scroll container instead of the old centered box's. Verified: typecheck/lint/build clean; real in-browser click-through in both light and dark mode confirming all three panels slide in from the right, the sticky header inside DemoModal stays pinned while its content scrolls, the Close button works, and a backdrop click dismisses the panel. --- apps/marketing/src/App.tsx | 11 +- apps/marketing/src/components/DemoModal.tsx | 344 +++++++++--------- .../src/components/RunItYourselfModal.tsx | 8 +- apps/marketing/src/components/SlidePanel.tsx | 58 +++ 4 files changed, 241 insertions(+), 180 deletions(-) create mode 100644 apps/marketing/src/components/SlidePanel.tsx diff --git a/apps/marketing/src/App.tsx b/apps/marketing/src/App.tsx index 372328b9..f484bcb5 100644 --- a/apps/marketing/src/App.tsx +++ b/apps/marketing/src/App.tsx @@ -3,6 +3,7 @@ import { ThemeSwitcher } from '@/components/ThemeSwitcher'; import { useLandingTheme } from '@/hooks/useLandingTheme'; import { DemoModal } from '@/components/DemoModal'; import { RunItYourselfModal } from '@/components/RunItYourselfModal'; +import { SlidePanel } from '@/components/SlidePanel'; import landingReference from '@/assets/landing/landing-reference.svg'; import landingReferenceDark from '@/assets/landing/landing-reference-dark.svg'; @@ -23,7 +24,9 @@ import landingReferenceDark from '@/assets/landing/landing-reference-dark.svg'; * product locally (RunItYourselfModal) instead. * * Everything else -- the artwork, the FAQ, the footer, the theme toggle -- - * is the same interaction shape as the real page. + * is the same interaction shape as the real page. The demo, run-it-yourself, + * and FAQ dialogs all share the SlidePanel container, which slides in from + * the right rather than popping up centered over the page. */ export function App() { const [faqIndex, setFaqIndex] = useState(null); @@ -110,8 +113,8 @@ export function App() { ))} {faqIndex !== null && ( -
-
+ setFaqIndex(null)} labelledBy="landing-faq-title" maxWidthClassName="max-w-xl"> +

FAQ

@@ -121,7 +124,7 @@ export function App() {

{faqAnswers[faqIndex]}

-
+
)} {demoOpen && setDemoOpen(false)} />} diff --git a/apps/marketing/src/components/DemoModal.tsx b/apps/marketing/src/components/DemoModal.tsx index fa6d582f..de285e66 100644 --- a/apps/marketing/src/components/DemoModal.tsx +++ b/apps/marketing/src/components/DemoModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useState } from 'react'; +import { SlidePanel } from '@/components/SlidePanel'; import { CATEGORY_LABELS, SAMPLE_CATEGORIES, @@ -30,10 +31,9 @@ const CATEGORY_STATE_STYLE: Record = { /** Opened from the reused LandingPage's "Log in" nav hotspot and its "See * how it works" hero hotspot (#382 redesign) -- there is no login flow, no * dashboard, and no anchor-scrollable walkthrough in this standalone site, - * so both hotspots lead here instead. A modal, not a page section: keeps - * the reused landing artwork's own length and layout completely untouched, - * the same interaction shape the real page already uses for its FAQ - * answers. */ + * so both hotspots lead here instead. A slide-in side panel, not a page + * section: keeps the reused landing artwork's own length and layout + * completely untouched. */ export function DemoModal({ onClose }: { onClose: () => void }) { const [phase, setPhase] = useState('idle'); const [stepIndex, setStepIndex] = useState(0); @@ -63,191 +63,189 @@ export function DemoModal({ onClose }: { onClose: () => void }) { }; return ( -
-
-
-
-
- Scripted simulation · sample repository -
-

- See what a PARTHA analysis produces -

-

- A scripted walkthrough of a made-up sample repository ({SAMPLE_REPO.name}) -- not a live analysis of any - real code. It uses PARTHA's actual finding categories and output shape. -

+ +
+
+
+ Scripted simulation · sample repository
- +

+ See what a PARTHA analysis produces +

+

+ A scripted walkthrough of a made-up sample repository ({SAMPLE_REPO.name}) -- not a live analysis of any + real code. It uses PARTHA's actual finding categories and output shape. +

+ +
-
-
-
-
-

{SAMPLE_REPO.name}

-

- revision {SAMPLE_REPO.revision} · {SAMPLE_REPO.languages} -

-
- {phase !== 'idle' && ( - - )} +
+
+
+
+

{SAMPLE_REPO.name}

+

+ revision {SAMPLE_REPO.revision} · {SAMPLE_REPO.languages} +

- - {phase === 'idle' && ( -
-

- Run the simulation to watch PARTHA walk through a sample repository and produce Engineering Review - and Repository Insights output. -

- -
+ {phase !== 'idle' && ( + )} +
- {phase !== 'idle' && ( -
-
    - {SIMULATION_STEPS.map((step, index) => { - const complete = index < stepIndex; - const active = index === stepIndex && phase === 'running'; - return ( -
  1. - - {complete ? '✓' : index + 1} - - {step} -
  2. - ); - })} -
+ {phase === 'idle' && ( +
+

+ Run the simulation to watch PARTHA walk through a sample repository and produce Engineering Review + and Repository Insights output. +

+ +
+ )} + + {phase !== 'idle' && ( +
+
    + {SIMULATION_STEPS.map((step, index) => { + const complete = index < stepIndex; + const active = index === stepIndex && phase === 'running'; + return ( +
  1. + + {complete ? '✓' : index + 1} + + {step} +
  2. + ); + })} +
+
+ )} + + {phase === 'done' && ( +
+
+ {(['review', 'insights'] as const).map((value) => ( + + ))}
- )} - {phase === 'done' && ( -
-
- {(['review', 'insights'] as const).map((value) => ( - - ))} + {tab === 'review' && ( +
+
+ {SAMPLE_CATEGORIES.map((category) => ( + + {category.label} + {category.findingCount > 0 ? ` (${category.findingCount})` : ''} + + ))} +
+
    + {SAMPLE_FINDINGS.map((finding) => ( +
  • +
    +

    {finding.title}

    + + {finding.severity} + +
    +

    + {CATEGORY_LABELS[finding.category]} +

    +

    {finding.explanation}

    +

    + Remediation: + {finding.remediationGuidance} +

    +

    + {finding.path}:{finding.startLine} + {finding.endLine !== finding.startLine ? `–${finding.endLine}` : ''} +

    +
  • + ))} +
+ )} - {tab === 'review' && ( -
-
- {SAMPLE_CATEGORIES.map((category) => ( - - {category.label} - {category.findingCount > 0 ? ` (${category.findingCount})` : ''} - - ))} -
-
    - {SAMPLE_FINDINGS.map((finding) => ( -
  • -
    -

    {finding.title}

    - - {finding.severity} - -
    -

    - {CATEGORY_LABELS[finding.category]} -

    -

    {finding.explanation}

    -

    - Remediation: - {finding.remediationGuidance} -

    -

    - {finding.path}:{finding.startLine} - {finding.endLine !== finding.startLine ? `–${finding.endLine}` : ''} -

    -
  • - ))} -
+ {tab === 'insights' && ( +
+
+ {SAMPLE_METRICS.map((metric) => ( +
+

{metric.value}

+

{metric.label}

+
+ ))}
- )} - - {tab === 'insights' && ( -
-
- {SAMPLE_METRICS.map((metric) => ( -
-

{metric.value}

-

{metric.label}

-
+
+

Languages

+
+ {SAMPLE_LANGUAGES.map((language, index) => ( +
))}
-
-

Languages

-
- {SAMPLE_LANGUAGES.map((language, index) => ( -
- ))} -
-
- {SAMPLE_LANGUAGES.map((language) => ( - - {language.label} {language.value}% - - ))} -
+
+ {SAMPLE_LANGUAGES.map((language) => ( + + {language.label} {language.value}% + + ))}
- )} -
- )} -
+
+ )} +
+ )}
-
+ ); } diff --git a/apps/marketing/src/components/RunItYourselfModal.tsx b/apps/marketing/src/components/RunItYourselfModal.tsx index 50f95178..febcf49f 100644 --- a/apps/marketing/src/components/RunItYourselfModal.tsx +++ b/apps/marketing/src/components/RunItYourselfModal.tsx @@ -1,3 +1,5 @@ +import { SlidePanel } from '@/components/SlidePanel'; + const GITHUB_URL = 'https://github.com/Second-Origin/PARTHA'; // Mirrors the exact commands in the repository's own README ("Run PARTHA @@ -25,8 +27,8 @@ npm run dev:frontend`; * "run it yourself" is the only call to action here. */ export function RunItYourselfModal({ onClose }: { onClose: () => void }) { return ( -
-
+ +

Run it yourself

@@ -97,6 +99,6 @@ export function RunItYourselfModal({ onClose }: { onClose: () => void }) {

-
+
); } diff --git a/apps/marketing/src/components/SlidePanel.tsx b/apps/marketing/src/components/SlidePanel.tsx new file mode 100644 index 00000000..81254609 --- /dev/null +++ b/apps/marketing/src/components/SlidePanel.tsx @@ -0,0 +1,58 @@ +import { useEffect, useState, type ReactNode } from 'react'; + +interface SlidePanelProps { + onClose: () => void; + labelledBy: string; + children: ReactNode; + /** Tailwind max-width class controlling how much of the screen the panel + * covers on wide viewports. Full-bleed on narrow ones either way. */ + maxWidthClassName?: string; +} + +/** Shared slide-in drawer used by every dialog on the reused landing page + * (DemoModal, RunItYourselfModal, the FAQ panel in App.tsx) -- a centered + * overlay popup read as an unpolished pattern for a marketing page; a + * panel that slides in from the side over part of the screen reads as a + * more deliberate one. Purely presentational: what each dialog shows and + * does is unchanged, only the container animates and positions + * differently. + * + * Closes on Escape and on a backdrop click, neither of which the previous + * centered-popup version had -- both are standard drawer behavior and + * were added here, not carried over from the old pattern. */ +export function SlidePanel({ onClose, labelledBy, children, maxWidthClassName = 'max-w-2xl' }: SlidePanelProps) { + const [entered, setEntered] = useState(false); + + useEffect(() => { + // Mount closed (off-screen) then flip open on the next frame, so the + // transform transition actually has something to animate instead of + // the panel just appearing already in place. + const frame = requestAnimationFrame(() => setEntered(true)); + return () => cancelAnimationFrame(frame); + }, []); + + useEffect(() => { + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') onClose(); + }; + window.addEventListener('keydown', onKeyDown); + return () => window.removeEventListener('keydown', onKeyDown); + }, [onClose]); + + return ( +
+ + ); +}