diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 0000000..9f6f463 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,3 @@ + +@../AGENTS.md + diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..7d522d5 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,52 @@ +{ + "hooks": { + "PostToolUse": [ + { + "hooks": [ + { + "command": "C:\\Users\\Patrick\\.cargo\\bin\\sinapsi.exe hook post-tool-use", + "type": "command" + } + ], + "matcher": "Edit|Write|MultiEdit" + } + ], + "SessionStart": [ + { + "hooks": [ + { + "command": "C:\\Users\\Patrick\\.cargo\\bin\\sinapsi.exe hook session-start", + "type": "command" + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "command": "C:\\Users\\Patrick\\.cargo\\bin\\sinapsi.exe hook stop", + "type": "command" + } + ] + } + ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "command": "C:\\Users\\Patrick\\.cargo\\bin\\sinapsi.exe hook user-prompt-submit", + "type": "command" + } + ] + } + ] + }, + "permissions": { + "allow": [ + "Bash(sinapsi:*)", + "mcp__sinapsi__*", + "Read(.sinapsi/**)" + ] + } +} \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..52bd55d --- /dev/null +++ b/.env.example @@ -0,0 +1,19 @@ +# Application +NEXT_PUBLIC_APP_URL=http://localhost:3000 +DATABASE_URL=file:./data/autoblog.db +DATABASE_AUTH_TOKEN= +BETTER_AUTH_SECRET=replace-with-at-least-32-random-characters +AUTH_SIGN_IN_RATE_LIMIT=8 +DEMO_ENABLED=true + +# AI: mock is deterministic and makes no provider call +AI_MODE=mock +GEMINI_API_KEY= +GEMINI_MODEL=gemini-2.5-flash + +# Durable job runner / deployment scheduler +CRON_SECRET=replace-with-a-distinct-random-secret + +# Limits +AI_MONTHLY_CHARACTER_QUOTA=200000 +MEDIA_MAX_BYTES=5242880 diff --git a/.env.local b/.env.local deleted file mode 100644 index b402e8a..0000000 --- a/.env.local +++ /dev/null @@ -1,22 +0,0 @@ -# Database -MONGODB_URI="" -DBNAME="" - -# Cloudinary -NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME="" -CLOUDINARY_CLOUD_NAME="" -CLOUDINARY_API_KEY="" -CLOUDINARY_API_SECRET="" -CLOUDINARY_WEBSITE_FOLDER="" - -# Gemini -GEMINI_API_KEY="" - -# Firebase (client-safe public variables) -NEXT_PUBLIC_FIREBASE_API_KEY="" -NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN="" -NEXT_PUBLIC_FIREBASE_PROJECT_ID="" -NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET="" -NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID="" -NEXT_PUBLIC_FIREBASE_APP_ID="" -NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID="" \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..319b78f --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,44 @@ + +This project uses Sinapsi for code context engineering. + +Read `.sinapsi/AGENT_INSTRUCTIONS.md` first — it defines the workflow for this repository. + +Before you think, plan or open any source file, read one file: + +- `.sinapsi/summary.md` — directory tree, the last 10 sessions, and a short recap + +That is the cardinal rule. `.sinapsi/session.md` (what happened) and +`.sinapsi/handoff.md` (current working state) are the sources it is built from — open +them only when the summary leaves your actual question unanswered. + +The last action of every patch, in this order: append `session.md`, rewrite `handoff.md` +(≤ 150 lines), then update `summary.md` from both — leaving its mechanical tree block +untouched. A patch that leaves any of the three stale is incomplete. + +Start code exploration with Sinapsi MCP — it is the cheaper first move, not a rule +against searching. Pick the tool by what you already know: + +- behaviour only → `query_graph` +- exact symbol → `get_node` +- exact symbol + impact → `get_neighbors` +- project shape → `graph_stats` (once) + +The graph is a derived index built from the source: it lags edits and drops what its +extractors cannot see. The filesystem is the authority — confirm with `grep`/`rg` +whenever you want to. + +Every pack carries four lines. Read them independently; none implies another: + +- `RELEVANCE` → how much of your question the top result explains +- `COVERAGE` → the token budget only, never what was not nominated +- `STRUCTURE` → compiler-resolved relationships +- `FRESHNESS` → index staleness + +`RELEVANCE: weak` means the answering symbol may be absent: retry with project +vocabulary, then search the filesystem. + +`COVERAGE: partial` means the budget cut something: raise it, narrow the scope, or +search — `complete` is not a promise that the answer is present. + +`STRUCTURE: absent` means relationships are unknown, not absent. + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..55dfdbc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: / + schedule: { interval: weekly } + open-pull-requests-limit: 5 + - package-ecosystem: github-actions + directory: / + schedule: { interval: monthly } + open-pull-requests-limit: 3 diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..04811d8 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,143 @@ +name: Quality gates + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +env: + BUN_VERSION: 1.3.12 + DATABASE_URL: file:./data/ci.db + BETTER_AUTH_SECRET: ci-only-secret-with-at-least-thirty-two-characters + NEXT_PUBLIC_APP_URL: http://127.0.0.1:3000 + DEMO_ENABLED: "true" + AI_MODE: mock + CRON_SECRET: ci-only-distinct-cron-secret-000000000000 + +jobs: + dependency-integrity: + name: Frozen install + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: { bun-version: "1.3.12" } + - run: bun install --frozen-lockfile + + typecheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 + with: { bun-version: "1.3.12" } + - run: bun install --frozen-lockfile + - run: bun run typecheck + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 + with: { bun-version: "1.3.12" } + - run: bun install --frozen-lockfile + - run: bun run lint + + unit-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 + with: { bun-version: "1.3.12" } + - run: bun install --frozen-lockfile + - run: bun run test + + integration-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 + with: { bun-version: "1.3.12" } + - run: bun install --frozen-lockfile + - run: bun run test:integration + + e2e: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 + with: { bun-version: "1.3.12" } + - run: bun install --frozen-lockfile + - run: bunx playwright install --with-deps chromium + - run: bun run test:e2e + + accessibility: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 + with: { bun-version: "1.3.12" } + - run: bun install --frozen-lockfile + - run: bunx playwright install --with-deps chromium + - run: bun run test:a11y + + visual-regression: + runs-on: windows-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 + with: { bun-version: "1.3.12" } + - run: bun install --frozen-lockfile + - run: bunx playwright install chromium + - run: bun run test:visual + + performance-budgets: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: { node-version: "22.19.0" } + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 + with: { bun-version: "1.3.12" } + - run: bun install --frozen-lockfile + - run: bunx playwright install --with-deps chromium + - run: bun run build + - run: bun run test:performance + - run: bun run test:lighthouse + - name: Preserve Lighthouse and bundle reports + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: performance-reports + path: | + .lighthouse/ + .performance/ + include-hidden-files: true + if-no-files-found: error + retention-days: 7 + + production-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 + with: { bun-version: "1.3.12" } + - run: bun install --frozen-lockfile + - run: bun run build + + security-audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: { fetch-depth: 0 } + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 + with: { bun-version: "1.3.12" } + - run: bun install --frozen-lockfile + - run: bun run audit + - run: bun run security:secrets + - name: Scan Git history without exposing values + uses: trufflesecurity/trufflehog@27b0417c16317ca9a472a9a8092acce143b49c55 # v3.95.9 + with: + extra_args: --results=verified,unknown diff --git a/.gitignore b/.gitignore index b7bab6e..2cb8780 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,21 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts -CMS blog.code-workspace \ No newline at end of file +CMS blog.code-workspace +.sinapsi/graph.json +.sinapsi/.watcher +.sinapsi/journal + +# environment and durable local state +.env +.env.* +!.env.example +data/ + +# test and quality artifacts +coverage/ +playwright-report/ +test-results/ +tests/visual/surfaces.spec.ts-snapshots/*-chromium-win32.png +.lighthouse/ +.performance/ diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..a16727e --- /dev/null +++ b/.mcp.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "sinapsi": { + "args": [ + "serve", + ".sinapsi/graph.json" + ], + "command": "C:\\Users\\Patrick\\.cargo\\bin\\sinapsi.exe" + } + } +} \ No newline at end of file diff --git a/.sinapsi/AGENT_INSTRUCTIONS.md b/.sinapsi/AGENT_INSTRUCTIONS.md new file mode 100644 index 0000000..2115540 --- /dev/null +++ b/.sinapsi/AGENT_INSTRUCTIONS.md @@ -0,0 +1,67 @@ +# Sinapsi — operating instructions + +Short on purpose. This file says how to work *here*; it does not repeat your project's +stack, style or test conventions — those live in your own AGENTS.md, untouched. + +## Memory: read one file first, write three last + +Before you think, plan or open a source file, read **one** file: + +- `.sinapsi/summary.md` — the directory tree, the last 10 sessions, and a short recap + +That is the cardinal rule. `session.md` and `handoff.md` are the sources `summary.md` is +built from; open them only when the summary leaves your actual question unanswered. + +At the end of every patch, in this order: + +1. **append** `.sinapsi/session.md` — the full entry for this patch +2. **rewrite** `.sinapsi/handoff.md` — current working state, **≤ 150 lines** +3. **update** `.sinapsi/summary.md` from both — add one dated line under *Recent + sessions* (keep the last 10), rewrite *Where things stand* in 5–10 lines, and leave + the tree block at the top alone: Sinapsi maintains it itself, live, and any edit you + make inside its markers is thrown away on the next change + +A patch that leaves any of the three stale is not finished. + +`session.md` archives itself: past 150 lines (or its token budget) Sinapsi moves it to +`.sinapsi/archive/` and starts a fresh one, whose header names the latest archive. Read +that one if you must — never the whole folder. `sinapsi compact` forces the same check. + +## Exploring + +Start with Sinapsi MCP rather than a filesystem sweep — it is the cheaper first move, +not a prohibition. Pick the tool by what you already know: + +- behaviour only → `query_graph` +- exact symbol, need its location → `get_node` +- exact symbol, need callers/impact → `get_neighbors` +- no idea of the shape → `graph_stats` (once) + +The graph (`.sinapsi/graph.json`) is a derived index, not the truth: it is built from the +source, it lags edits, and it drops what its extractors cannot see. The filesystem is +the authority. Reach for `grep`/`rg` whenever you want to confirm what the graph told +you — verifying a result is never a failure, and each header below says what it does +*not* cover. + +## Reading the headers + +Four independent claims. None implies another. + +- `RELEVANCE` — how much of your question the top result explains. `RELEVANCE: weak` + means the answering symbol may be absent entirely: re-query in the project's + vocabulary, then search the filesystem. +- `COVERAGE` — the token budget only. It says nothing about symbols the ranker never + nominated, so `complete` is not a claim that the answer is present. +- `STRUCTURE` — `absent` means relationships are unknown, never "no callers". +- `FRESHNESS` — how stale the index may be. + +## Decisions + +`.sinapsi/rfc/` before a boundary moves: public API, schema, storage format, an +invariant, a dependency. An RFC states the problem, the measured evidence, the +alternatives rejected, and the test that would prove it wrong. Propose one when the +boundary moves — do not wait to be asked. + +`.sinapsi/adr/` after it is decided. An ADR is immutable: supersede it, never edit it. + +Small choices get no RFC — one line in `decisions.md` is the whole ceremony. diff --git a/.sinapsi/adr/000-template.md b/.sinapsi/adr/000-template.md new file mode 100644 index 0000000..d2f0e7e --- /dev/null +++ b/.sinapsi/adr/000-template.md @@ -0,0 +1,15 @@ +# ADR 000 — + +> Copy to `NNN-slug.md`. **Immutable once accepted** — to change a decision, write a new ADR that supersedes this one. + +## Status +<proposed | accepted | superseded by ADR-NNN> + +## Context +<forces at play> + +## Decision +<what was decided> + +## Consequences +<results, positive and negative> diff --git a/.sinapsi/adr/001-modular-libsql-better-auth.md b/.sinapsi/adr/001-modular-libsql-better-auth.md new file mode 100644 index 0000000..3582239 --- /dev/null +++ b/.sinapsi/adr/001-modular-libsql-better-auth.md @@ -0,0 +1,30 @@ +# ADR 001 — Modular monolith on libSQL with Better Auth + +## Status + +Accepted on 2026-07-22. Immutable; supersede with a new ADR. + +## Context + +AutoBlog needs one durable, workspace-isolated behavior in local development, CI and a serverless +public demo. It also requires revocable server-side sessions, relational revision history, +optimistic writes and idempotent scheduled jobs. RFC 001 contains baseline measurements, +alternatives and falsification criteria. + +## Decision + +Use a Next.js 16 modular monolith. Use Drizzle ORM over `@libsql/client` for both local `file:` and +remote libSQL databases. Use Better Auth database sessions and secure HTTP-only cookies. Keep +authorization, workflow and concurrency rules in application modules; route handlers and React +components are adapters only. Use the same relational database for durable jobs and outbox-style +compensation. + +## Consequences + +- Schema and migrations become the persistence authority; Mongoose and the in-memory simulator + are removed. +- Local and CI execution remain free and deterministic; deployment needs externally owned libSQL + and auth configuration. +- The bounded write rate is compatible with SQLite/libSQL. Higher sustained write concurrency + would trigger a measured PostgreSQL migration RFC. +- Better Auth owns identity/session tables while workspace roles remain an application concern. diff --git a/.sinapsi/adr/002-versioned-workflow-durable-jobs.md b/.sinapsi/adr/002-versioned-workflow-durable-jobs.md new file mode 100644 index 0000000..6adb2e3 --- /dev/null +++ b/.sinapsi/adr/002-versioned-workflow-durable-jobs.md @@ -0,0 +1,26 @@ +# ADR 002 — Versioned workflow with database-leased publication jobs + +## Status + +Accepted on 2026-07-22. Immutable; supersede with a new ADR. + +## Context + +RFC 002 requires review decisions, immutable public artifacts, stale-command protection and scheduled +execution that survives restarts without paid queue infrastructure. + +## Decision + +Keep the state machine in the editorial domain and authorize each action through the central RBAC +policy. Use the post version as the precondition for state and revision commands. Store the exact +revision in each publication record. Schedule a unique publish job in the same database transaction, +claim work through a conditional lease, retry at most three times and make completion idempotent against +the publication status. Public reads resolve the published revision pointer, never the mutable draft. + +## Consequences + +- Review and publication invariants are testable without React or provider infrastructure. +- A scheduler only triggers `jobs:run`; it cannot select workspace, post or revision data. +- SQLite/libSQL conditional writes serialize claims for the bounded workload. Sustained multi-worker + contention or throughput above the measured portfolio load would require a superseding queue ADR. +- Version gaps document non-content transitions and are intentional. diff --git a/.sinapsi/adr/003-verified-media-governed-ai.md b/.sinapsi/adr/003-verified-media-governed-ai.md new file mode 100644 index 0000000..bc9975b --- /dev/null +++ b/.sinapsi/adr/003-verified-media-governed-ai.md @@ -0,0 +1,27 @@ +# ADR 003 — Verified durable media and explicit governed AI suggestions + +## Status + +Accepted on 2026-07-22. Immutable; supersede with a new ADR. + +## Context + +RFC 003 requires provider failures to preserve active media and requires AI limits to survive process +restart. Demo and CI cannot depend on paid/private providers. + +## Decision + +Use bounded multipart upload and decode every image before provider storage. Implement a media provider +port with a durable libSQL object adapter; finalize or replace workspace metadata only after provider +success and clean stale objects through durable jobs. Use a partial unique index for one active asset per +post. Use one validated AI port, deterministic mock and structured Gemini adapter. Reserve quota and +consume rate limits in database transactions. Store provider/model/latency/character counts and only +provider-reported token counts; never infer tokens or retain prompt content. + +## Consequences + +- Local, CI and configured deployments retain one business/repository path. +- Media storage is capped per request and suitable for the bounded demo, not a general asset CDN. +- Gemini is opt-in configuration; mock results are visibly labeled and contain no fabricated usage. +- Provider operations have application timeouts, but upstream Gemini work may continue or be billed + after abort, as documented by the SDK. diff --git a/.sinapsi/adr/004-bounded-demo-measured-quality.md b/.sinapsi/adr/004-bounded-demo-measured-quality.md new file mode 100644 index 0000000..f55eb30 --- /dev/null +++ b/.sinapsi/adr/004-bounded-demo-measured-quality.md @@ -0,0 +1,24 @@ +# ADR 004 — Bounded reset and measured product quality + +## Status + +Accepted on 2026-07-22. Immutable; supersede with a new ADR. + +## Context + +RFC 004 requires a first-time evaluator to repeat the complete demo without weakening tenant safety, +and requires accessibility/performance claims to be backed by executable production evidence. + +## Decision + +Persist demo-reset idempotency outside the reset workspace foreign-key cascade while retaining an +explicit workspace identifier. Enforce Owner policy, demo flag and database rate limit before reset. +Delete only the demo workspace's object bytes and relational graph, then invoke the canonical seed. +Measure direct Web Vitals/transfer budgets under `next start`; use axe, keyboard assertions and +Chromium screenshots as targeted quality gates. Cache by route semantics rather than globally. + +## Consequences + +- Repeated requests are safe and auditable; unrelated workspaces and user sessions survive. +- Reset execution remains bounded to one known demo workspace and cannot become a general delete API. +- Performance and accessibility language must remain “tested targets,” not universal compliance. diff --git a/.sinapsi/adr/005-lighthouse-release-gate.md b/.sinapsi/adr/005-lighthouse-release-gate.md new file mode 100644 index 0000000..acaf0de --- /dev/null +++ b/.sinapsi/adr/005-lighthouse-release-gate.md @@ -0,0 +1,30 @@ +# ADR 005 — Supplement direct budgets with local Lighthouse reports + +## Status + +Accepted on 2026-07-22. Immutable; supersede with a new ADR. + +## Context + +Direct production measurements pass at LCP 188/300 ms, CLS 0, interaction 16 ms and JavaScript +141,959/221,420 bytes, but root RFC P-15 separately names Lighthouse reports as acceptance evidence. +Official `lighthouse` 13.4.1 is the current audited engine; the release job pins its required Node +22.19 runtime and reuses the already-installed Playwright Chromium. + +## Decision + +Run three local-only desktop Lighthouse passes for marketing and sign-in from the existing production +performance job. Enforce category, LCP and CLS thresholds while retaining the direct interaction and +JavaScript assertions as primary exact budgets. The direct suite writes a public/editor bundle report. +GitHub Actions retains both report sets for seven days inside the authorized repository; no +Lighthouse hosting account is used. + +## Consequences + +- P-15 evidence now includes both explicit metrics and a standard audited report. +- Lighthouse variability is bounded with three runs and conservative release thresholds. +- Reports may contain public route output, so artifacts remain ignored locally and limited to a + seven-day repository-scoped CI retention. +- Lighthouse CI was rejected because its current CLI adds audited legacy transitive risk without + providing necessary capability for this local gate. +- Any dependency audit regression falsifies this decision and blocks release. diff --git a/.sinapsi/decisions.md b/.sinapsi/decisions.md new file mode 100644 index 0000000..909c688 --- /dev/null +++ b/.sinapsi/decisions.md @@ -0,0 +1,7 @@ +# Decisions + +Semantic memory: one line per decision — the reasoning that must survive, not the diff. +Format: `- YYYY-MM-DD: chose X over Y because Z`. Consult before re-opening a settled choice. +Big, irreversible architectural decisions belong in `adr/` instead. + +- 2026-07-22: rely on the pinned TruffleHog action's built-in fail/no-update flags because duplicating them in `extra_args` aborts before scanning. diff --git a/.sinapsi/handoff.md b/.sinapsi/handoff.md new file mode 100644 index 0000000..4d43041 --- /dev/null +++ b/.sinapsi/handoff.md @@ -0,0 +1,46 @@ +# Handoff + +## Terminal state: EXTERNALLY BLOCKED + +- Branch `feat/rfc-editorial-cms`; draft PR + `https://github.com/PatrickDev-it/AutoBlog-CMS/pull/3` remains intentionally unmerged. +- P-01 through P-17 are Verified. P-18 repository implementation is complete but release/public + acceptance is externally blocked. +- `main` protection requires all eleven CI jobs, strict freshness and resolved conversations; admins + cannot bypass, force-push or delete the branch. + +## Verified evidence + +- Local: frozen install, migrations `0000`–`0003`/seed from empty, type/lint/build, 19 unit, + 25 integration, 10 E2E, 6 a11y, 5 visual, 2 direct performance and 6 Lighthouse runs pass. +- Direct lab: LCP 188/300 ms, CLS 0, interaction 16 ms, response 146 ms and JavaScript + 141,959/221,420 bytes. Bundle JSON is generated for representative public/editor routes. +- Lighthouse medians: marketing/sign-in performance 0.92/0.91, all other category scores 1.00, + LCP 1,718/1,709 ms and CLS 0. CI retains six HTML reports plus bundle evidence for seven days. +- Runtime dependency audit and current-tree secret scan pass with zero findings. +- Public GitHub is verified; description/topics and release documentation are current. + +## Current remote gate + +- Run `29909234038` attempt 2 passes all eleven protected jobs: frozen install, typecheck, lint, unit, + integration, E2E, accessibility, visual regression, performance/Lighthouse, production build and + security audit/history-range scan. +- The corrected TruffleHog step executes successfully with its pinned action-owned fail semantics; + the visual rerun also confirms the preceding 691-pixel mismatch was transient rather than a code change. +- PR #3 remains draft because green repository checks do not close the owner-controlled release gates. + +## External blockers + +- Provider owners must rotate/revoke historical MongoDB, Cloudinary and Gemini values in `8f83cec`. + Never print, suppress or rewrite them. After rotation, record owner confirmation. +- Deployment owner must configure remote libSQL URL/token, unique auth/cron secrets and a one-minute + scheduler. No paid service may be provisioned without approval. +- Vercel built the candidate, but anonymous access returns HTTP 302 to Vercel SSO. Existing production + remains old commit `485f037`; homepage correctly remains blank. + +## Owner completion sequence + +Rotate/revoke credentials; configure durable runtime and scheduler; expose an approved public URL; rerun +PR #3. After every protected check passes, mark ready, merge normally, deploy merged SHA, execute the +clean-browser workflow, set homepage and create/verify `v2.0.0`. Until then do not merge, tag, release, +pin or advertise the old/protected deployment as v2. diff --git a/.sinapsi/known-issues.md b/.sinapsi/known-issues.md new file mode 100644 index 0000000..73fbc46 --- /dev/null +++ b/.sinapsi/known-issues.md @@ -0,0 +1,5 @@ +# Known issues & pitfalls + +Operational memory: what bit us, so it never bites twice. +Format per entry: `symptom → root cause → fix / how to avoid`. Consult before debugging. + diff --git a/.sinapsi/project-map.md b/.sinapsi/project-map.md new file mode 100644 index 0000000..463a70d --- /dev/null +++ b/.sinapsi/project-map.md @@ -0,0 +1,34 @@ +# Project map + +<!-- Auto-generated by `sinapsi build` and `sinapsi index` — do not edit by hand. --> +<!-- Agents: do NOT read this to orient; use the MCP (query_graph/get_neighbors), which is scoped and always fresh. This file is for people browsing the repo. --> + +**552** nodes · **797** edges · JavaScript 1, TypeScript 95 + +> **Structure: absent.** No compiler-resolved index covers this repo, so the edges below are `defines` only — who contains whom, not who calls whom. Run `sinapsi index`. The hub list is a containment ranking until you do. + +## Levels +root (419 code, 133 docs) + +## Node kinds +- class: 15 +- constant: 115 +- dependency: 20 +- document: 23 +- function: 73 +- heading: 110 +- interface: 6 +- method: 94 +- module: 96 + +## Most connected (hubs / entry points) +- schema (37) — src/platform/db/schema.ts +- client (35) — src/platform/db/client.ts +- errors (26) — src/platform/observability/errors.ts +- domain (21) — src/modules/identity/domain.ts +- service (20) — src/modules/ai/service.ts +- domain (20) — src/modules/editorial/domain.ts +- drizzle-repository (20) — src/modules/editorial/drizzle-repository.ts +- service (20) — src/modules/editorial/service.ts +- env (20) — src/platform/config/env.ts +- 4. Problem register and decisions (19) — RFC.md diff --git a/.sinapsi/rfc/000-template.md b/.sinapsi/rfc/000-template.md new file mode 100644 index 0000000..705ebf3 --- /dev/null +++ b/.sinapsi/rfc/000-template.md @@ -0,0 +1,18 @@ +# RFC 000 — <title> + +> Copy this file to `NNN-slug.md` and fill it in **before** implementing an architectural change. + +## Context +<what problem, what constraints> + +## Proposal +<what to do> + +## Alternatives +<what else was considered, and why not> + +## Decision +<chosen option> + +## Consequences +<trade-offs, follow-ups, risks> diff --git a/.sinapsi/rfc/001-application-foundation.md b/.sinapsi/rfc/001-application-foundation.md new file mode 100644 index 0000000..4c95944 --- /dev/null +++ b/.sinapsi/rfc/001-application-foundation.md @@ -0,0 +1,78 @@ +# RFC 001 — Application foundation + +- Status: Accepted +- Date: 2026-07-22 +- Root RFC coverage: P-01, P-03, P-04, P-06, P-07, P-08, P-09, P-16, P-18 + +## Context and measured evidence + +The baseline has two incompatible data paths: a 500+ line process-local simulator and +unprotected Mongoose route handlers. `bun install --frozen-lockfile` fails, TypeScript reports +42 errors, source lint reports 93 errors and 39 warnings with the current toolchain, and the +runtime audit reports 48 findings (2 critical, 22 high). The production build succeeds only +while explicitly skipping type and lint validation. No test or CI workflow exists. + +The product needs relational workspace boundaries, immutable revisions, conditional writes, +durable idempotency records and database-backed sessions. Local development and CI must not +require a paid service, while the public serverless deployment must use the same repository +implementation. + +## Proposal + +1. Upgrade to the current supported Next.js 16 and React 19 line and use strict TypeScript. +2. Use Drizzle ORM with the libSQL driver. Local/CI use a durable `file:` database; deployment + uses the same driver and schema with a remote libSQL URL. +3. Use Better Auth with database-backed, revocable email/password sessions. Demo role entry + authenticates bounded seeded accounts; it does not bypass the session or policy layer. +4. Organize one modular monolith into identity, editorial, media and AI modules behind server + adapters. React code never imports the database or provider clients. +5. Represent scheduled work and compensation in the relational `jobs` table with unique + idempotency keys, leases and bounded retries. +6. Use stable application errors with correlation IDs at every public adapter. + +## Alternatives considered + +- Keep MongoDB: rejected because the existing models do not enforce tenant joins, revision + relations or transactional job invariants, and would preserve the contradictory data path. +- Local-only SQLite: rejected because a serverless deployment cannot durably write its local + filesystem. +- PostgreSQL plus a test fake: rejected because it introduces a second persistence behavior or + requires external infrastructure for deterministic CI. +- Prisma: rejected because Drizzle/libSQL has a smaller runtime boundary and direct SQL migration + artifacts while retaining explicit constraints. +- Auth.js credentials sessions: rejected because the current credentials-provider path is less + direct for database-backed password accounts; Better Auth documents native Next.js 16, + database sessions, secure cookies and Drizzle integration. +- Custom signed cookies: rejected because the RFC requires a maintained authentication solution + and revocation semantics. +- Microservices or an external queue: rejected as operationally disproportionate to this modular + monolith and incompatible with the no-billing constraint. + +## Boundary and invariants + +- All mutable records contain `workspace_id`; membership is reloaded from the database. +- Route workspace IDs are locators only and must match an authenticated membership. +- Revision rows are append-only. A published pointer identifies one immutable revision. +- Post mutations condition on `expectedVersion`; a zero-row update is `VERSION_CONFLICT`. +- Job idempotency keys are unique and claims use a lease before execution. +- AI output is persisted as usage metadata and returned as a suggestion only. +- Media replacement activates the new asset only after verified storage succeeds. + +## Falsification tests + +The decision is invalid if any of the following remains true: + +- local and remote database URLs require different application repositories; +- a cross-workspace ID can be read or mutated through an authenticated route; +- a stale writer changes the current revision; +- a repeated scheduled job publishes or audits twice; +- session revocation still authorizes a mutation; +- a failed media replacement removes the active asset; +- a mock AI response is presented as a configured provider result. + +## Consequences + +The public deployment requires a remote libSQL URL/token and an authentication secret. Local and +CI remain provider-free. SQLite serializes writes, which is acceptable for this bounded portfolio +CMS; optimistic version checks remain mandatory. A future PostgreSQL move would require a new RFC +and migration rather than an alternate live adapter. diff --git a/.sinapsi/rfc/002-editorial-reliability.md b/.sinapsi/rfc/002-editorial-reliability.md new file mode 100644 index 0000000..33a5fd7 --- /dev/null +++ b/.sinapsi/rfc/002-editorial-reliability.md @@ -0,0 +1,53 @@ +# RFC 002 — Editorial reliability and durable publication + +- Status: Accepted +- Date: 2026-07-22 +- Root RFC coverage: P-07, P-08, P-09 + +## Context and measured evidence + +Phase 1 persists every content save as an immutable revision and rejects stale versions, but the +seven declared states have no transition implementation. The database already contains publication +and job records, while the application exposes zero workflow, history, restore, scheduling or public +preview commands. Nine unit, five integration and five browser tests pass before this change; none +can prove the author-reviewer-publication path. + +## Proposal + +1. Implement one pure transition table for Draft, InReview, ChangesRequested, Approved, Scheduled, + Published and Archived. Each command declares its permission, source states and destination. +2. Condition every transition and restore on `expectedVersion`; transition versions may create gaps + in revision numbering because only content mutations append revisions. +3. Lock content during review, approval and scheduling. Editing a published post creates a new Draft + revision while retaining the immutable published pointer. +4. Pin scheduled and direct publications to the selected immutable draft revision. Scheduled work is + recorded with a unique idempotency key and executed by a database-leased, three-attempt worker. +5. Expose revision history, restore-as-new, transition commands and a public preview read model through + server adapters. All private commands retain session, membership, origin and policy enforcement. + +## Alternatives considered + +- Encode transitions in React: rejected because server authorization and non-UI clients could bypass it. +- Mutable article snapshots: rejected because they cannot prove what was reviewed or published. +- Platform-specific cron plus in-memory queue: rejected because retries and duplicate executions would + disappear on restart and diverge between local, CI and deployment. +- External queue: rejected because the bounded portfolio workload does not justify billing or another + operational system. +- Publish the current draft when a delayed job runs: rejected because edits after scheduling would + silently change the approved artifact. + +## Falsification tests + +- An Author or Reviewer can directly publish or schedule. +- A transition from an undeclared source state succeeds. +- A stale transition, save or restore changes the post. +- Restore mutates an old revision instead of appending a new one. +- A scheduled job publishes a revision other than the one pinned at scheduling time. +- Reclaiming or rerunning a job creates a second publication or audit event. +- Editing after publication changes the public response before another explicit publication. + +## Consequences + +The post version is a concurrency token, not a count of revision rows. Scheduled execution requires a +recurring invocation of the repository-provided worker command; local and CI use the same command as a +deployment scheduler. Provider-specific scheduling remains deployment configuration, not business logic. diff --git a/.sinapsi/rfc/003-media-ai-boundaries.md b/.sinapsi/rfc/003-media-ai-boundaries.md new file mode 100644 index 0000000..ff799e3 --- /dev/null +++ b/.sinapsi/rfc/003-media-ai-boundaries.md @@ -0,0 +1,61 @@ +# RFC 003 — Secure media and governed AI boundaries + +- Status: Accepted +- Date: 2026-07-22 +- Root RFC coverage: P-10, P-11, P-12 + +## Context and measured evidence + +The accepted foundation contains media/AI metadata tables but exposes no provider ports or user +paths. The current tree has no upload endpoint and no AI endpoint; all previous base64, Cloudinary +and Gemini routes were removed in Phase 0. Runtime audit is zero. The installed `sharp` and +`@google/genai` packages are unused. Official `@google/genai` documentation confirms structured JSON +schemas, bounded output tokens, response usage metadata and an abort signal, while warning that +client cancellation does not necessarily cancel provider billing. + +## Proposal + +1. Accept only authenticated multipart images up to 5 MiB (configurable downward/up to 10 MiB). + Verify decoded PNG/JPEG/WebP format, dimensions up to 4096×4096 and 16 megapixels with `sharp`. +2. Use a media provider port. The durable default adapter writes opaque objects to libSQL, preserving + the same local/CI/deployment repository path. Metadata activates only after object persistence. +3. Replace in one metadata transaction: mark the prior asset replaced and activate the verified new + asset. Durable cleanup jobs remove old/orphan objects with leases and three retries. +4. Define one AI suggestion contract. The deterministic mock and configured Gemini adapters share + validated input/output. Gemini uses structured JSON, a fixed output bound and an eight-second + abort signal; public errors never include provider details. +5. Reserve monthly workspace character quota transactionally before provider invocation and enforce + a per-user five-request/minute database rate limit. Release reservations on failure and record only + bounded usage metadata on success. +6. Return suggestions without mutating posts. React provides separate Preview and Apply actions and + labels mock/configured mode. + +## Alternatives considered + +- Base64 JSON uploads: rejected because parser allocation occurs before meaningful limits. +- Arbitrary remote URLs: rejected because they create SSRF and host-trust problems; the allowlist is + intentionally empty. +- Local filesystem objects: rejected because serverless instances are ephemeral. +- Direct Cloudinary dependency: rejected because CI would require private credentials and the old path + deleted active data before replacement succeeded. +- Store prompts/responses for analytics: rejected as unnecessary sensitive content retention. +- Mutate posts directly from AI: rejected because assistance must remain an explicit user decision. +- In-memory quotas/rate limits: rejected because restart and multi-instance behavior would bypass them. + +## Falsification tests + +- A forged extension/MIME, oversized image, excessive dimension or non-image reaches storage. +- A provider/finalization failure changes or deletes the active asset. +- Another workspace can list, replace, read or delete an asset by ID. +- Repeated cleanup execution deletes a new active replacement or audits twice. +- Anonymous/forbidden AI invocation reaches an adapter. +- Concurrent quota reservations exceed the configured workspace budget. +- Timeout, malformed provider JSON or excessive output is returned as a suggestion. +- Any suggestion changes a revision before the user chooses Apply. + +## Consequences + +Database object storage is intentionally bounded and operationally sufficient for this portfolio +deployment; high media volume would trigger a new direct-upload object-store RFC. Configured Gemini +requires an externally owned API key and may incur provider charges, so demo/CI remain deterministic +mock mode. Abort limits local waiting but cannot guarantee provider-side cancellation or avoid charges. diff --git a/.sinapsi/rfc/004-product-quality-demo-performance.md b/.sinapsi/rfc/004-product-quality-demo-performance.md new file mode 100644 index 0000000..54ec96c --- /dev/null +++ b/.sinapsi/rfc/004-product-quality-demo-performance.md @@ -0,0 +1,57 @@ +# RFC 004 — Recruiter demo, accessibility and performance evidence + +- Status: Accepted +- Date: 2026-07-22 +- Root RFC coverage: P-13, P-14, P-15, P-17 + +## Context and measured evidence + +The flagship browser workflow passes, but reset exists only as a local seed option, the checklist is +static, axe covers only marketing and no visual/performance evidence is versioned. The production +build currently exposes a static marketing route, dynamic authenticated editor and a 60-second +revalidated public preview. No global no-cache header exists. + +## Proposal + +1. Add Owner-only demo reset with a persisted idempotency record, a three-request/hour database rate + limit and a hard `is_demo` guard. Reset deletes only bounded demo data and its object bytes, then + runs the same validated seed path. +2. Derive the recruiter checklist from persisted post state and provide concise in-product guidance, + mode/role disclosure and reset feedback. +3. Add axe scans for marketing, sign-in, workspace and public preview; keyboard/focus-order tests for + primary navigation and controls; retain explicit form labels and reduced-motion CSS. +4. Add versioned desktop/mobile visual snapshots for marketing, sign-in, workspace and public preview. +5. Measure production-mode Web Vitals and transferred initial JavaScript with budgets set before + measurement: LCP ≤ 2.5 s, INP/event duration ≤ 200 ms, CLS ≤ 0.10, landing JS ≤ 180 KiB and + authenticated workspace JS ≤ 320 KiB. Fail the test when a budget regresses. +6. Keep authenticated routes dynamic/no-store by server semantics; retain 60-second public preview + revalidation and static marketing generation. + +## Alternatives considered + +- Reset through a public seed endpoint: rejected because it weakens authorization and tenant scope. +- Delete/recreate the entire database: rejected because configured workspaces and sessions would be + destroyed. +- Claim broad WCAG compliance from axe: rejected because automated checks are targeted evidence, not + a complete audit. +- Measure development server performance: rejected because HMR and unoptimized bundles invalidate + release budgets. +- Lighthouse-only scores: rejected as hardware-sensitive aggregates; direct Web Vital/bundle budgets + are retained as primary evidence. Lighthouse can supplement them in release documentation. +- Global no-cache: rejected because marketing and published preview have different data semantics. + +## Falsification tests + +- A non-Owner, non-demo workspace or anonymous caller can reset data. +- Repeating an idempotency key performs another reset or audit. +- Reset deletes a configured workspace, identity/session or leaves demo media objects behind. +- Flagship actions cannot be completed with a keyboard or lose visible focus. +- Axe reports serious/critical violations on a primary surface. +- A versioned major surface changes without an reviewed snapshot update. +- A production route exceeds any declared LCP/INP/CLS/JavaScript budget. + +## Consequences + +Performance measurements run against `next start` and therefore require a completed production +build. Results are local/CI targets rather than universal field guarantees. Visual baselines are +Chromium-specific review artifacts. Idempotency records intentionally survive demo workspace deletion. diff --git a/.sinapsi/rfc/005-lighthouse-release-evidence.md b/.sinapsi/rfc/005-lighthouse-release-evidence.md new file mode 100644 index 0000000..da99f96 --- /dev/null +++ b/.sinapsi/rfc/005-lighthouse-release-evidence.md @@ -0,0 +1,50 @@ +# RFC 005 — Reproducible Lighthouse release evidence + +- Status: Accepted +- Date: 2026-07-22 +- Root RFC coverage: P-15, P-18 + +## Context and measured evidence + +The production Playwright gate measures LCP 188/300 ms, CLS 0, a 16 ms interaction proxy and exact +initial JavaScript transfer 141,959/221,420 bytes. These direct assertions are stronger than an +aggregate score, but root RFC P-15 also explicitly requires a Lighthouse report. No reproducible +Lighthouse artifact or CI assertion currently exists. Official npm metadata reports `lighthouse` +13.4.1 as current on 2026-07-22; it requires Node 22.19 or newer. + +## Proposal + +1. Add exact dev dependency `lighthouse@13.4.1` and retain it in the frozen Bun lockfile. +2. Run three desktop Lighthouse passes against marketing and sign-in under `next start`. +3. Use the installed Playwright Chromium and pin Node 22.19 in the performance job. +4. Persist reports as ignored local output and a seven-day GitHub CI artifact; do not upload route + output to a Lighthouse SaaS or any provider outside the authorized repository. +5. Require performance ≥0.80, accessibility ≥0.95, best-practices ≥0.90, SEO ≥0.90, LCP ≤2.5 s + and CLS ≤0.10. Direct interaction and JavaScript budgets remain separately mandatory. +6. Execute Lighthouse inside the existing independent performance CI job after the direct budgets. +7. Emit representative marketing/workspace JavaScript measurements as a separate bundle report. + +## Alternatives considered + +- Keep only the custom PerformanceObserver gate: rejected because it leaves the exact P-15 + Lighthouse acceptance criterion unmet. +- Manual DevTools export: rejected because it is not reproducible or mandatory in CI. +- `@lhci/cli` 0.15.1: rejected after `bun audit` exposed high-severity legacy transitive findings + absent from current Lighthouse; the hosted/collection features are unnecessary for this gate. +- Replace direct metrics with Lighthouse scores: rejected because aggregate scores are noisier and do + not enforce the explicit transferred-JavaScript or interaction budgets. +- Upload to LHCI server or paid monitoring: rejected because no approved external service exists. + +## Falsification tests + +- Frozen install changes the manifest or lockfile. +- Lighthouse cannot audit the production server or produces fewer than six local reports. +- The public/editor bundle report or seven-day CI evidence artifact is absent. +- Either tested URL falls below a category threshold or exceeds LCP/CLS budgets. +- The performance CI job omits either direct budgets or Lighthouse assertions. +- Runtime audit gains an unreviewed high/critical vulnerability. + +## Consequences + +CI time increases by three Lighthouse runs per route. Scores remain lab evidence and do not become +field-performance or WCAG claims. P-15 requires both the direct budget suite and Lighthouse to pass. diff --git a/.sinapsi/session.md b/.sinapsi/session.md new file mode 100644 index 0000000..0de7a94 --- /dev/null +++ b/.sinapsi/session.md @@ -0,0 +1,302 @@ +# Session log + +Operational changelog, append-only, chronological order — never delete previous entries. + +Every entry must include: timestamp, patch goal, changes made + files touched, breaking changes, regressions introduced/removed, validation performed, final status. + +Append this first — before rewriting `handoff.md`, and before updating `summary.md` from both. + +Sinapsi archives this file on its own once it passes 150 lines (or its token budget): it moves to `archive/`, a fresh log starts, and the fresh log's header names the latest archive. Read that one if you need history — not the whole folder. + +<!-- The agent writes the first entry on the first patch. --> + +## 2026-07-22T01:34:41+02:00 — Establish measured RFC foundation + +- Goal: reproduce containment evidence and accept the architecture before moving public, + persistence, authentication or dependency boundaries (P-01–P-09, P-16, P-18). +- Changed: added RFC 001, immutable ADR 001, the measured engineering baseline and the P-01–P-18 + closure ledger under `.sinapsi/{rfc,adr}` and `docs/engineering`. +- Decisions: Next.js 16 modular monolith; Drizzle over libSQL for local/remote parity; Better Auth + database sessions; relational jobs and stable application errors. +- Security: identified non-empty historical MongoDB, Cloudinary and Gemini environment values in + commit `8f83cec` without printing them. Rotation is required before release; no current source + secret was added. +- Validation: fetched `origin`; branched from `origin/main`; frozen install failure, 42 type errors, + 93 lint errors/39 warnings, bypassed build and 48 audit findings were reproduced. +- Breaking changes: none; documentation-only decision patch. +- Regressions: none introduced. Final status: complete. + +## 2026-07-22T01:47:42+02:00 — Close Phase 0 containment + +- Goal: close P-02, P-03 and current-tree secret containment for P-18. +- Changed: removed tracked `.env.local`; added `.env.example`, SECURITY and MIT license; upgraded + to Next 16.2.11/React 19.2.8; replaced vulnerable dependencies; rebuilt `bun.lock`; enabled + strict TypeScript and zero-warning ESLint; removed all build validation bypasses and permissive + image/cache/body configuration. +- Changed: deleted 141 unreachable simulator, MongoDB, Cloudinary, Gemini, Firebase, duplicate UI + and parallel-route files (12,201 deleted lines). Added the recruiter marketing entry, bounded + constants, unit/E2E/axe smoke tests, current-tree secret scanner, Dependabot and eight independent + pinned-action CI gates. +- API/schema: all unsafe legacy APIs were removed; the durable authenticated API arrives in the + accepted Phase 1 schema patch. `/` and `/sign-in` are the only current product routes. +- Security: runtime audit moved from 48 findings (2 critical/22 high) to zero; current-tree secret + scan passes. Historical provider rotation remains required before release. +- Validation: frozen install, strict typecheck, lint with zero warnings, unit test, integration + harness, production build, runtime audit, recruiter E2E and marketing axe test all pass locally. +- Visual/accessibility: responsive dark editorial landing page retains the established identity; + skip link, visible focus, semantic headings and reduced-motion behavior were added. +- Breaking changes: legacy routes and dependency APIs are intentionally deleted. Rollback is the + preceding `83c6689` commit; no data migration exists yet. +- Regressions: sign-in is intentionally a Phase 0 placeholder until the Phase 1 session slice. + Final status: complete. + +## 2026-07-22T02:13:25+02:00 — Deliver authenticated durable application core + +- Goal: close the Phase 1 exit gate for P-01, P-04, P-05, P-06 and P-16. +- Changed: added two checksum-verified libSQL migrations and Drizzle schemas for Better Auth, + workspaces, memberships, posts, immutable revisions, publications, media, audit events, AI usage, + durable jobs and database-backed rate limits. Added validated idempotent five-role seed/setup. +- Changed: connected Better Auth database sessions, 12-hour HTTP-only cookies, disabled sign-up, + persistent login throttling, origin checks, audited login/logout and membership-derived workspace + context. Added a complete policy matrix including Author ownership constraints. +- Changed: implemented one editorial repository/service for list, get, create and conditional save; + every save appends a revision and a stale version maps to HTTP 409. Added stable public errors, + correlation IDs, structured logs and readiness. +- Product: replaced the sign-in placeholder with five bounded demo identities and added the + responsive workspace/editor, active role/AI mode, seeded selection, create flow, debounced + autosave states, conflict controls and recruiter checklist. +- API/schema: added protected workspace post GET/POST/PATCH routes and Better Auth adapter. JSON + payloads never carry trusted workspace/user identity. API, architecture, schema and RBAC docs + were added with executable contract references. +- Security: arbitrary credentials, revoked sessions, cross-workspace reads, Reviewer creation, + Author cross-owner edits and anonymous mutations are negatively tested. No provider value is + stored or returned. +- Validation: frozen install, strict typecheck, zero-warning lint, 9 unit tests, 5 integration + tests, production build, zero-vulnerability audit and 5 Chromium E2E/axe tests pass. +- Breaking changes: schema `0000`/`0001` are the new persistence boundary. Rollback requires + restoring the prior application commit and removing a new local/remote database; migrations are + forward-only and checksum protected. +- Remaining risk: workflow transitions, history UI, jobs, media and AI adapters are Phase 2/3. + Final status: complete. + +## 2026-07-22T02:39:02+02:00 — Deliver reliable editorial workflow and publication + +- Goal: close P-07, P-08 and P-09 with an executable author-reviewer-publication path. +- Decision: accepted RFC 002/ADR 002 for a versioned domain state machine, revision-pinned + publications and database-leased jobs with three bounded attempts. +- Changed: implemented six state commands across seven lifecycle states, action-specific RBAC, + version preconditions, timestamps, append-only audits and locked review/approval content. +- Changed: added workspace-scoped revision history, comparison and restore-as-new. Editing or + restoring a Published post opens a new Draft while retaining its immutable published pointer. +- Reliability: scheduling writes a pinned publication and unique job atomically. Claims use a + conditional 30-second lease; retry, recovery and repeated execution cannot duplicate the public + mutation or audit. Archival cancels only its matching scheduled work. +- Product/API: added workflow controls, scheduling input, history/compare/restore panels, resilient + serialized autosave, public preview and protected transition/revision/job adapters. +- Testing: added pure transition falsification, restore immutability, stale transition/restore, + permission denial, pinned scheduling, malformed-job retry and duplicate-execution coverage. +- Validation: frozen install, strict typecheck, zero-warning lint, 11 unit tests, 10 integration + tests, production build, zero-vulnerability audit and 7 deterministic Chromium E2E/axe tests pass. +- E2E evidence: Author creates/saves/restores/submits; Reviewer approves; Editor publishes; public + preview renders the pinned revision. A second browser writer receives and compares HTTP 409. +- Breaking changes: post version is explicitly a concurrency token and may have revision gaps after + transitions. Deployment scheduling must invoke `jobs:run`; no new migration is required. +- Remaining risk: media and AI provider boundaries are Phase 3. Final status: complete. + +## 2026-07-22T03:07:40+02:00 — Harden media and AI provider boundaries + +- Goal: close P-10, P-11 and P-12 without provider credentials or a parallel demo path. +- Decision: accepted RFC 003/ADR 003 for decoded bounded multipart media, durable libSQL object + storage, transactional replacement/compensation and explicit governed AI suggestions. +- Schema: migration `0002_media_ai_governance.sql` replaces the coupled blob table, constrains one + active asset per post and adds durable monthly AI quota reservations. +- Media: PNG/JPEG/WebP are stream-capped, decoded with pixel/dimension limits, MIME-matched, + checksummed and filename-sanitized before storage. Workspace ownership is policy-enforced. +- Reliability: verified new media is stored before metadata activation. Replacement is atomic; + provider/finalization failure preserves the active asset, while leased cleanup jobs retry three + times and never delete an active replacement. +- AI: added one validated adapter contract, deterministic mock and structured Gemini adapter with + bounded prompt/output, eight-second abort, persisted rate/quota controls and real usage metadata. +- Privacy/product: prompts and suggestions are not retained in usage/audit rows. Mock/live mode is + labeled; preview is non-mutating and Apply creates a user-authored autosave revision. +- Scheduler security: a distinct secret with constant-time comparison authorizes machine execution; + session-based Owner/Admin operation retains origin and RBAC checks. +- Tests: forged MIME, size/dimension abuse, cross-workspace access, provider failure, metadata + compensation, cleanup recovery, malformed AI output, timeout, quota, rate and anonymous access pass. +- Validation: frozen install, strict typecheck, zero-warning lint, 17 unit tests, 21 integration + tests, production build, current-tree secret scan, zero-vulnerability audit and 10 E2E/axe tests. +- Breaking changes: forward-only migration `0002` drops unused `media_blobs`; rollback requires + restoring the prior commit/database snapshot. Configured Gemini remains opt-in and billable. +- Remaining risk: demo reset, accessibility depth, performance/visual evidence and release delivery + are Phase 4/5. Final status: complete. + +## 2026-07-22T03:29:49+02:00 — Deliver bounded demo and measured product quality + +- Goal: close P-13, P-14, P-15 and P-17 with repeatable recruiter and quality evidence. +- Decision: accepted RFC 004/ADR 004 for an idempotent bounded reset, route-semantic caching, + targeted accessibility claims, versioned visuals and production-mode performance budgets. +- Demo: added Owner-only origin-protected reset with durable idempotency and a three/hour database + limit. It verifies `is_demo`, deletes only demo objects/relations and preserves identities, sessions + and configured workspaces before running the canonical validated seed. +- Product: checklist progress now derives from persisted version/workflow/publication state; creation + focuses the title, role/mode/data-path disclosure is visible and reset requires explicit confirmation. +- Accessibility: added serious/critical axe scans on marketing, sign-in, workspace and preview; + keyboard draft/review flow, focus management and reduced-motion assertions pass. +- Visual: added six reviewed cross-CI-stable Chromium baselines for desktop/mobile major surfaces and + removed 17 unreachable legacy/canned public assets. +- Performance: accepted LCP ≤2.5 s, CLS ≤0.10, INP/event ≤200 ms and JS 180/320 KiB budgets. + Production measurements pass at 180/256 ms LCP, 0 CLS, <16 ms event upper bound, 143 ms workflow + response and 141,959/221,420 transferred JavaScript bytes. +- Caching: marketing remains static; protected JSON is `private, no-store`; authenticated media is + private five-minute; public preview retains 60-second revalidation; health remains no-store. +- CI: separated E2E, accessibility, Windows visual regression and Linux production performance jobs. +- Validation: frozen install, strict typecheck/lint, 17 unit, 25 integration, 10 E2E, 6 accessibility, + 5 visual and 2 performance tests, build, secrets and zero-vulnerability audit pass. +- Breaking changes: migration `0003` adds immutable reset idempotency. Visual baselines are reviewed + on Windows Chromium to control font rasterization. Final status: complete. + +## 2026-07-22T03:37:58+02:00 — Replace legacy claims with executable release evidence + +- Goal: advance P-18 by making every public product claim map to a reachable path and named proof. +- Documentation: replaced the legacy simulator README/FEATURES with a recruiter brief, explicit + Implemented/Tested/Demo-only/Planned matrix, actual application visuals and guided role workflow. +- Operations: added deployment, migration, rollback/recovery, known-limitations and 2.0 candidate + release notes with the exact external configuration required for durable public operation. +- Security: added a trust-boundary threat model, control/evidence/residual-risk table and an explicit + non-suppressible historical credential rotation gate; expanded the operator security policy. +- Integrity: added a documentation contract test for local evidence links, all P-01–P-18 ledger rows + and superseded claim exclusion. +- GitHub discovery: authenticated owner is `PatrickDev-it`; repository is public. The current public + production deployment is still commit `485f037` and is not presented as AutoBlog 2.0 evidence. +- Validation: documentation contract 2/2 and zero-warning lint pass; local evidence links resolve. +- Remaining gate: P-18 stays in progress pending full clean gates, remote PR/CI, owner credential + rotation, durable deployment configuration, merge/tag/release and clean-session public verification. + +## 2026-07-22T03:48:11+02:00 — Stabilize and execute the complete local release matrix + +- Goal: validate the AutoBlog 2.0 candidate from frozen install through production performance and + surface any nondeterministic release defect before remote CI. +- Finding: the first E2E run correctly failed because the fixed `data/e2e.db` retained the intentional + hourly demo-reset window across runs. This made repeat execution dependent on prior local state. +- Fix: Playwright now gives every process an isolated durable libSQL file, optionally overridden by + `PLAYWRIGHT_DATABASE_URL`; production rate-limit and idempotency behavior are unchanged. +- Strictness: removed the only strict TypeScript defect in the new Markdown-link parser without an + assertion/cast. The documentation contract remains 2/2 and full unit count is 19. +- Empty database: migrations `0000`–`0003` and validated demo seed pass from a new file. +- Browser evidence: 10 E2E, 6 accessibility and 5 visual tests pass on pinned Chromium. +- Production evidence: build passes; LCP is 156/168 ms, CLS 0, interaction 16 ms, history response + 106 ms and transferred JavaScript 141,959/221,420 bytes, within accepted budgets. +- Security/reproducibility: frozen install, strict typecheck, zero-warning lint, 19 unit, 25 integration, + zero-vulnerability runtime audit and current-tree secret scan pass. +- Remaining release risk is external: historical credential rotation, remote durable configuration, + full-history CI, merge/tag/release and clean-session public deployment verification. + +## 2026-07-22T03:55:11+02:00 — Publish the candidate and prove the external release boundary + +- Goal: complete all authorized GitHub-side release work and distinguish repository failures from + actions that require provider/account ownership. +- Delivery: pushed `feat/rfc-editorial-cms` and opened draft PR + `https://github.com/PatrickDev-it/AutoBlog-CMS/pull/3` at candidate commit `3e9bc9a`. +- CI: frozen install, typecheck, lint, unit, integration, E2E, accessibility, visual regression, + performance and production build all pass remotely. Security audit/current-tree scan pass; the + required TruffleHog history step fails on the known historical provider finding. +- Governance: updated the official pinned `actions/checkout` dependency from v4.2.2 to v7.0.1 + (`3d3c42e5aac5ba805825da76410c181273ba90b1`) to remove the Node 20 deprecation warning. The + existing `setup-bun` pin is already the official v2.2.0 release. +- GitHub metadata: verified `PatrickDev-it`, updated the public repository description and focused + topics, removed obsolete `docker`; homepage remains blank because no public v2 URL is verified. +- Deployment: Vercel built the correct candidate, but anonymous requests redirect to Vercel login. + Existing production still points to old commit `485f037`; neither is advertised as public v2. +- Ledger: P-18 is `Externally blocked`; P-01 through P-17 remain Verified. +- External actions: account owners must rotate/revoke the named historical credentials, configure + remote libSQL/auth/cron and one-minute scheduling, and expose an approved public URL. +- Validation: documentation contract 2/2 and diff integrity pass. No merge, tag or release created. + +## 2026-07-22T04:00:33+02:00 — Enforce the terminal release block + +- Goal: make the RFC release gates technically enforceable and leave a stable terminal handoff. +- Final head `78af05b`: the official `actions/checkout@v7.0.1` pin executes successfully; remote run + `29884450920` passes frozen install, typecheck, lint, unit, integration, E2E, accessibility, visual, + performance and production build. Only the known full-history finding fails. +- Protection: `main` now requires all eleven RFC jobs with strict branch freshness, admin enforcement, + resolved conversations, no force pushes and no branch deletion. +- Public repository: an unauthenticated request returns HTTP 200. Description and focused topics are + current; homepage remains unset because an accessible v2 deployment does not exist. +- Deployment: Vercel built `78af05b`, but the anonymous candidate request returns HTTP 302 to Vercel + SSO. The clean-session recruiter and runtime persistence gates therefore remain unverified. +- PR: posted the final evidence and minimum owner actions on draft PR #3. Merge/tag/release are blocked + both procedurally and by branch protection; no bypass was used. +- Terminal status: `EXTERNALLY BLOCKED`. Repository work is complete; credential rotation, remote + persistence/scheduler configuration and deployment access require external account ownership. + +## 2026-07-22T04:21:56+02:00 — Add reproducible Lighthouse and bundle release evidence + +- Goal: close the literal P-15 acceptance gap requiring Lighthouse and bundle reports without + weakening the existing direct production budgets. +- Decision: accepted RFC 005/ADR 005 for six local Lighthouse 13.4.1 reports, a representative + public/editor bundle report and seven-day repository-scoped CI artifacts. +- Dependency review: rejected `@lhci/cli` 0.15.1 after its legacy transitive dependencies introduced + additional high-severity development findings. Current Lighthouse adds no runtime finding; the + performance job pins its required Node 22.19 and reuses Playwright Chromium. +- Direct evidence: marketing/workspace LCP 188/300 ms, CLS 0, interaction upper bound 16 ms, + workflow response 146 ms and JavaScript 141,959/221,420 bytes all pass accepted budgets. +- Lighthouse evidence: marketing/sign-in median performance 0.92/0.91, accessibility 1.00, + best-practices 1.00, SEO 1.00, LCP 1,718/1,709 ms and CLS 0 across six reports. +- CI: the independent performance job now requires build, direct measurements, Lighthouse budgets + and pinned seven-day report upload. No Lighthouse SaaS, paid service or product data is used. +- Validation: frozen install, strict typecheck, zero-warning lint, 19 unit, production build, two + direct performance tests, six Lighthouse reports, runtime audit and current-tree scan pass. +- Terminal status remains `EXTERNALLY BLOCKED`: P-01–P-17 are Verified; P-18 still requires provider + credential rotation, durable public runtime/scheduler configuration and anonymous deployment. + +## 2026-07-22T04:27:00+02:00 — Preserve dot-prefixed performance artifacts in CI + +- Goal: make the already-green Lighthouse and bundle outputs downloadable from the protected job. +- Evidence: remote run `29885689908` passed build, both direct performance tests and all six + Lighthouse runs; only artifact upload failed because the action excludes hidden paths by default. +- Fix: enabled the pinned upload action's `include-hidden-files` option for `.lighthouse/` and + `.performance/`; paths, seven-day retention, missing-file failure and all budgets are unchanged. +- Scope: workflow-only correction. No application, test assertion, dependency or report content + changed. Terminal status remains `EXTERNALLY BLOCKED` for the documented external release gates. + +## 2026-07-22T11:39:38+02:00 — Restore the executable history-scan gate + +- Goal: remove a false-red security result before CTO-facing portfolio review without weakening the + required Git-history scan. +- Evidence: run `29885890517` passed the runtime dependency audit and current-tree secret scan, then + TruffleHog exited before scanning because `--fail` was supplied by both the pinned action and + `extra_args`. +- Fix: retained the pinned TruffleHog action and `verified,unknown` result policy while removing only + duplicate action-owned `--fail` and `--no-update` arguments. +- Governance: marked the implemented root RFC Accepted; no application boundary, dependency, test, + credential, protection rule or external release gate changed. +- Local verification: runtime audit and current-tree secret scan pass; remote history-scan rerun is + pending on draft PR #3. +- Terminal status remains `EXTERNALLY BLOCKED`: historical provider rotation, durable public runtime, + scheduler, anonymous demo, merge and release remain owner-controlled gates. + +## 2026-07-22T11:44:31+02:00 — Verify the protected candidate after history-scan repair + +- Remote evidence: PR run `29908929694` passes all eleven required jobs, including the TruffleHog + PR-range history scan; the security job completes in 22 seconds instead of aborting on CLI parsing. +- Security posture: runtime audit and current-tree scanning remain zero-finding. The green range scan + does not revoke provider values already known in shared historical commit `8f83cec`; rotation remains + a hard external release condition. +- Presentation: repository description and topics now identify the candidate's actual architectural + evidence—database sessions, workspace RBAC, immutable revisions, conflict-safe autosave, libSQL and + tested media/AI boundaries. +- Scope: no application behavior, assertion, dependency, branch protection, credential, release or + deployment setting changed. +- Terminal status remains `EXTERNALLY BLOCKED`; PR #3 stays draft and AutoBlog remains intentionally + absent from the recommended profile pins until the public release gates close. + +## 2026-07-22T11:53:23+02:00 — Align final protected evidence and release narrative + +- Remote evidence: run `29909234038` attempt 2 passes all eleven required jobs; the isolated visual + rerun passed all five baselines after the preceding 691-pixel mismatch on a documentation-only SHA. +- Documentation: replaced the obsolete claim that history scanning must remain red with the actual + distinction between an automated zero-finding range scan and owner-confirmed credential revocation. +- Scope: release evidence and Sinapsi state only; no application behavior, assertion, dependency, + credential, protection rule, deployment or release state changed. +- Terminal status remains `EXTERNALLY BLOCKED`: provider rotation, durable remote libSQL/scheduler + configuration, anonymous public verification, merge, deployment and `v2.0.0` remain outstanding. diff --git a/.sinapsi/summary.md b/.sinapsi/summary.md new file mode 100644 index 0000000..15fd1f1 --- /dev/null +++ b/.sinapsi/summary.md @@ -0,0 +1,134 @@ +# Summary + +<!-- Sinapsi keeps the directory tree below current on its own — on every build, and live + from the watcher whenever a file or folder is created, moved or deleted. There is no + command to run and nothing to ask an agent to do. Do not edit between its markers; + your edits are replaced. Everything else in this file is yours. --> + +<!-- sinapsi:start v0.2.6 — kept current automatically by Sinapsi — refreshed on every build and by the watcher whenever files or folders are created, moved or deleted. No command to run; edits between these markers are replaced --> +``` +_client/ + @inset/ + @sidebar/ +actions/ +app/ + (workspace)/ + @inset/ + @sidebar/ + api/ + preview/ + sign-in/ + favicon.ico + globals.css + icon.png + layout.tsx + page.tsx +components/ + chat/ + groups/ + webapp/ +config/ + css/ +constants/ +data/ + tests/ + autoblog.db + e2e-2956.db + e2e-4868.db + e2e-8696.db + e2e.db + lighthouse.db + performance.db + release-empty-20260722.db +docs/ + engineering/ + releases/ + accessibility.md + ai-data-handling.md + api.md + architecture.md + authentication-rbac.md + data-model.md + demo-guide.md + deployment.md + editorial-workflow.md + known-limitations.md + media-security.md + migrations.md + performance.md + rollback-recovery.md + security-threat-model.md +drizzle/ + 0000_editorial_core.sql + 0001_auth_rate_limit.sql + 0002_media_ai_governance.sql + 0003_demo_reset_idempotency.sql +hooks/ +lib/ +plugins/ +scripts/ + db/ + jobs/ + security/ +src/ + modules/ + platform/ + ui/ +test-results/ + .last-run.json +tests/ + accessibility/ + e2e/ + integration/ + lighthouse/ + performance/ + unit/ + visual/ +types/ +ui/ +utils/ +.env.example +.gitignore +.mcp.json +AGENTS.md +CMS blog.code-workspace +… 19 more +``` +<!-- sinapsi:end --> + +**Read this first, and usually only this.** It is the cardinal read at the start of every +patch: the project's shape (above), the last sessions at a glance, and a short recap. Open +`session.md` or `handoff.md` only when this file leaves your actual question unanswered. + +## Recent sessions + +<!-- The last 10 patches, newest first: `- <timestamp> — <one line>`. Appended by the + agent at the end of every patch, at the same time it appends session.md. Drop the + 11th; the full history is in session.md and, once archived, in archive/. --> + +- 2026-07-22T11:53:23+02:00 — Aligned release evidence with the final 11/11 green protected rerun. +- 2026-07-22T11:44:31+02:00 — Verified all eleven protected PR jobs after the history-scan repair. +- 2026-07-22T11:39:38+02:00 — Removed duplicate action-owned flags that aborted TruffleHog before scanning. +- 2026-07-22T04:27:00+02:00 — Preserved dot-prefixed performance artifacts in CI. +- 2026-07-22T04:21:56+02:00 — Added reproducible Lighthouse and bundle release evidence. +- 2026-07-22T04:00:33+02:00 — Enforced all release checks and finalized the external-blocker handoff. +- 2026-07-22T03:55:11+02:00 — Published PR #3, proved CI, metadata and external release blockers. +- 2026-07-22T03:48:11+02:00 — Stabilized isolated Playwright runs and passed the complete local release matrix. +- 2026-07-22T03:37:58+02:00 — Replaced legacy product claims with tested release, security and operations evidence. +- 2026-07-22T03:29:49+02:00 — Delivered isolated demo reset, accessibility, visuals and production performance budgets. + +## Where things stand + +<!-- 5–10 lines, no more. What the project is doing right now, what is in flight, what is + fragile, what the next action is. Rewritten (not appended) from session.md + handoff.md + at the end of every patch. If it grows past 10 lines it has stopped being a summary. --> + +- Terminal status is EXTERNALLY BLOCKED; P-01–P-17 Verified, P-18 repository work complete. +- Draft PR #3; run `29909234038` attempt 2 passes all eleven protected jobs including the history-range scan. +- Local P-15 now includes two direct budgets, six Lighthouse reports and bundle JSON evidence. +- Hidden report artifacts are explicitly included with seven-day CI retention. +- Runtime audit, current-tree scan and corrected TruffleHog PR-range scan pass. +- `main` requires all eleven checks with admin enforcement and no force-push/deletion. +- Public GitHub is verified; homepage remains blank because no public v2 is verified. +- Vercel candidate returns HTTP 302 to SSO; production remains old commit `485f037`. +- Owner action: rotate credentials, configure durable runtime/scheduler and expose public URL. diff --git a/.sinapsi/verification.md b/.sinapsi/verification.md new file mode 100644 index 0000000..6a8876b --- /dev/null +++ b/.sinapsi/verification.md @@ -0,0 +1,21 @@ +# Verification checklist + +No patch is complete until every box is ticked: + +- [ ] builds / compiles +- [ ] lint & typecheck clean +- [ ] existing tests pass +- [ ] no obvious regressions +- [ ] architectural consistency & naming +- [ ] session.md + handoff.md updated + +<!-- The concrete commands for this repo's stack are auto-filled below by `sinapsi init` (RFC 002 L5). --> + +## Detected stack — run these before ticking the boxes +<!-- Auto-detected from the root manifests; edit if your scripts differ. --> + +```sh +npm run build # if defined +npm test +npm run lint # if defined +``` diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..319b78f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,44 @@ +<!-- sinapsi:start v0.2.6 — auto-generated by `sinapsi init`; do not edit between these markers (re-run to update) --> +This project uses Sinapsi for code context engineering. + +Read `.sinapsi/AGENT_INSTRUCTIONS.md` first — it defines the workflow for this repository. + +Before you think, plan or open any source file, read one file: + +- `.sinapsi/summary.md` — directory tree, the last 10 sessions, and a short recap + +That is the cardinal rule. `.sinapsi/session.md` (what happened) and +`.sinapsi/handoff.md` (current working state) are the sources it is built from — open +them only when the summary leaves your actual question unanswered. + +The last action of every patch, in this order: append `session.md`, rewrite `handoff.md` +(≤ 150 lines), then update `summary.md` from both — leaving its mechanical tree block +untouched. A patch that leaves any of the three stale is incomplete. + +Start code exploration with Sinapsi MCP — it is the cheaper first move, not a rule +against searching. Pick the tool by what you already know: + +- behaviour only → `query_graph` +- exact symbol → `get_node` +- exact symbol + impact → `get_neighbors` +- project shape → `graph_stats` (once) + +The graph is a derived index built from the source: it lags edits and drops what its +extractors cannot see. The filesystem is the authority — confirm with `grep`/`rg` +whenever you want to. + +Every pack carries four lines. Read them independently; none implies another: + +- `RELEVANCE` → how much of your question the top result explains +- `COVERAGE` → the token budget only, never what was not nominated +- `STRUCTURE` → compiler-resolved relationships +- `FRESHNESS` → index staleness + +`RELEVANCE: weak` means the answering symbol may be absent: retry with project +vocabulary, then search the filesystem. + +`COVERAGE: partial` means the budget cut something: raise it, narrow the scope, or +search — `complete` is not a promise that the answer is present. + +`STRUCTURE: absent` means relationships are unknown, not absent. +<!-- sinapsi:end --> diff --git a/FEATURES.md b/FEATURES.md index 134b783..31a4de0 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -1,449 +1,44 @@ -# Autoblog CMS - Feature Highlights - -A modern, AI-powered blog content management system built with Next.js, featuring intelligent content guidance, intuitive editing, and -professional-grade organization tools. - ---- - -## 🤖 AI Assistant Integration - -**Gemini-Powered Content Guidance** - -The integrated AI assistant provides real-time, intelligent support for every aspect of blogging: - -- **Multi-Model Intelligence**: Choose between Gemini 1.5 Flash (fast), Flash 8B (lightweight), Pro (advanced), and 2.0 Flash (latest) -- **Typewriter Effect Display**: Beautiful character-by-character animation (~15ms per character) makes complex responses easier to digest -- **Smart Context Awareness**: - - Content creation strategies and best practices - - SEO optimization recommendations - - Editorial quality guidelines - - Analytics interpretation and insights - - Publishing workflow guidance - - Design and branding advice -- **Session Persistence**: Chat history stored in browser sessionStorage -- **Conversational**: Full conversation history maintained for context-aware responses -- **Markdown Formatting**: AI responses render with proper formatting, lists, and emphasis -- **Responsive Guidance**: Suggestions adapt based on your blog section and content type - -**Example Uses:** - -- "How do I create compelling blog content?" → Detailed 3-phase writing guide -- "What are SEO best practices?" → Technical optimization checklist -- "How to maintain editorial standards?" → Brand voice and quality guidelines -- Ask anything about blog strategy, CMS features, or content optimization - ---- - -## ✏️ Effortless Content Organization - -**Intuitive Hierarchical Structure** - -Organize content with a simple, three-level hierarchy: - -``` -Section (Featured, Design, Culture, Insights, Resources) -└── Group (e.g., "2025 Highlights", "Digital Design") - └── Subgroup (e.g., "January", "UI/UX Trends") - └── Post (individual articles) -``` - -### One-Click Renaming - -- **Right-click context menu** on any item (group, subgroup, post) -- **Instant inline editing** with real-time validation -- **Keyboard shortcuts** for efficiency (Enter to save, Escape to cancel) -- **Undo capability** - Simply rename again if needed -- Renames instantly reflected across the entire dashboard - -### Bulk Operations - -- **Create groups** with a single click -- **Nested subgroups** for fine-grained organization -- **Delete hierarchies** with confirmation dialogs -- **Drag-and-drop** for reordering (when implemented) - ---- - -## 📝 Smart Post Creation & Editing - -**Rapid Content Creation** - -- **Right-click to create**: New group, subgroup, or post from context menu -- **Modal dialogs** for quick naming without page refresh -- **Auto-population**: Posts auto-load default template fields: - - Title, description, publication state (Draft/Published) - - Date fields with inline date picker - - Content type classification (Featured, Tutorial, Analysis, etc.) -- **Rich editor support**: Markdown formatting for complex content -- **Image management**: - - Click-to-upload featured images - - Image library browser - - Alt text and display name fields - - Optimized next/image integration for fast loading - -### Dual-View Editing - -- **Preview mode**: See how content renders to readers -- **Edit mode**: Full editor with all metadata fields -- **Tab switching**: Seamlessly toggle between views without losing changes - ---- - -## 💾 Autosave & Real-Time Persistence - -**Never Lose Your Work** - -- **Instant save**: Changes persist immediately to your content store -- **Auto-recovery**: Session data saved to browser storage -- **Visual feedback**: Subtle save indicators show state changes -- **No manual save button required**: Write and the system handles persistence -- **Undo/Redo support**: Full history of recent changes -- **Conflict resolution**: Safe handling of simultaneous edits - ---- - -## 📱 Mobile-Responsive Design - -**Professional Experience Across All Devices** - -### Desktop - -- Full sidebar navigation with collapsible groups -- Two-column post editor (metadata + rich editor) -- 2x2 grid for image management -- Optimized spacing and typography - -### Tablet - -- Touch-friendly buttons (48x48px minimum) -- Optimized grid layouts (1-2 columns) -- Accessible navigation with tap targets -- Responsive typography scaling - -### Mobile - -- Full-screen modal navigation -- Single-column layouts -- Thumb-friendly navigation buttons -- Swipe gestures for sheet navigation -- Touch-optimized form controls -- Proper viewport configuration - -**Technical Excellence:** - -- Next.js Image optimization (lazy loading, WebP support) -- CSS Grid and Flexbox for responsive layouts -- Mobile-first CSS approach -- Tailwind CSS for rapid responsive design -- Tested across Safari, Chrome, Firefox, Edge - ---- - -## 🖼️ Professional Image Management - -**Seamless Media Integration** - -- **Image Upload**: Click-upload interface with preview -- **Multiple Image Formats**: Support for JPG, PNG, WebP -- **Automatic Optimization**: - - Lazy loading with placeholder backgrounds - - WebP format support for modern browsers - - Responsive image sizing based on container - - SVG fallback for missing images -- **Image Organization**: - - Separate images for each content section - - Hero images for homepage - - Featured images for blog posts - - Icon and logo management -- **Alt Text Support**: Accessibility-first image metadata -- **Batch Operations**: Manage multiple images simultaneously - -### Example Image Locations: - -- Hero section: `digital-innovation-01.webp` -- Featured content: `architecture-design-01.jpg` -- Culture/Arts: `fashion-exhibition-01.jpg`, `installation-art-01.jpg` -- Design: `digital-interface-01.png`, `digital-interface-02.png` -- Community: `community-engagement-01.jpg` -- Advisory: `board-culture.jpg` - ---- - -## 📊 Data Organization & Management - -**5 Professional Content Sections** - -Each section comes pre-configured with realistic dummy data and professional templates: - -### 1. **Featured Stories** - -- Monthly highlights and trending content -- Curated showcase pieces -- Latest industry updates -- _Example posts: Digital Revolution, Sustainable Design, Community Impact_ - -### 2. **Design & Creative** - -- Design trends and visual systems -- UI/UX expertise and tutorials -- Typography and color theory -- Brand strategy and identity -- _Example posts: UX Trends 2025, Accessibility Design, Brand Identity_ - -### 3. **Culture & Arts** - -- Cultural commentary and exhibitions -- Interview series with creators -- Event coverage and retrospectives -- Artistic discourse and analysis -- _Example posts: Installation Art, Fashion Week, Artist Interviews_ - -### 4. **Insights & Analysis** - -- Industry reports and market analysis -- Technology forecasts -- Thought leadership pieces -- Research and data synthesis -- _Example posts: Q1 Market Analysis, Technology Forecast, Future of Work_ - -### 5. **Resources & Guides** - -- Technical tutorials and guides -- Tools and software recommendations -- Best practices documentation -- Professional development content -- _Example posts: Web Performance, Security Best Practices, Designer's Toolkit_ - -**Automatic Demo Data:** - -- Each section pre-loads realistic, multi-level content structure -- 20+ example posts across all sections -- Professional titles and descriptions -- Properly assigned dates and publication states -- Linked to professional demo images - ---- - -## 🎨 Professional UI/UX - -**Beautiful, Intuitive Interface** - -### Visual Design - -- **Dark & Light Themes**: Full theme switcher with system preference detection -- **Tailwind CSS**: Modern utility-first design system -- **Shadcn/UI Components**: Production-grade accessible components -- **Custom Layout System**: Flexible CSS variable system for spacing (--p, --sidebar-p, etc.) -- **Smooth Animations**: Transitions and micro-interactions for feedback - -### Navigation - -- **Sidebar with Groups**: Collapse and expand content hierarchies -- **Breadcrumb Navigation**: Always know where you are in the structure -- **Quick Search**: Find posts and sections instantly -- **Smart Links**: Contextual navigation to related content - -### Accessibility - -- **WCAG AA Compliant**: Color contrast ratios, keyboard navigation -- **Screen Reader Support**: Semantic HTML and ARIA labels -- **Keyboard Navigation**: Full keyboard support for all features -- **Focus Management**: Clear focus indicators for keyboard users -- **Alt Text**: All images have descriptive alt text - ---- - -## 🔄 Advanced Workflow Features - -### Draft & Publishing - -- **Publication States**: Draft, Scheduled, Published, Archived -- **Date Scheduling**: Set future publish dates -- **Status Indicators**: Visual icons show current state -- **Bulk Operations**: Change multiple posts' states at once - -### Editorial Standards - -- **Editorial Advisory Panel**: Built-in advisory system for brand consistency -- **Content Template System**: Default templates for each content type -- **Quality Checklist**: Pre-publish checklist prevents common issues -- **Version Control**: Track and restore previous versions - -### Analytics Ready - -- **Date Tracking**: All content timestamped and sortable -- **Category Analytics**: Organize by section for performance tracking -- **Search Integration**: Posts discoverable by title and content - ---- - -## 🚀 Performance & Technical Excellence - -**Built on Modern Technology Stack** - -- **Next.js 14+**: App Router with server components for speed -- **React 18+**: Latest hooks and concurrent features -- **TypeScript**: Full type safety across codebase -- **Tailwind CSS**: Responsive design with minimal CSS -- **shadcn/ui**: Accessible component library -- **Markdown Support**: Rich text rendering with react-markdown -- **Session Storage**: Browser-based state persistence -- **API Mocking**: Demo mode with full offline functionality - -### Performance Features - -- **Lazy Loading**: Images load on-demand -- **Code Splitting**: Automatic route-based code splitting -- **Optimized Images**: 40-80% size reduction with Next Image optimization -- **Minimal JS**: Server-rendered components reduce client-side overhead -- **Caching**: Smart caching strategies for faster page loads - ---- - -## 📚 Comprehensive Admin Dashboard - -### Sidebar Management - -- **Current Section Navigation**: Quick navigation between Featured, Design, Culture, Insights, Resources -- **Home & Advisory Links**: Access landing page and editorial content -- **Style Switching**: Environment switcher for different blog modes -- **Theme Toggle**: Dark/light mode with system preference sync - -### Inset Panel Features - -- **Chat Assistant**: Right sidebar AI panel (collapsible) -- **Model Selection**: Switch between different Gemini models on-the-fly -- **Session Persistence**: Chat history maintained during session -- **Typing Indicator**: Shows when AI is composing responses - -### Context Menu (Right-Click) - -- **Create Group**: New content group -- **Create Subgroup**: Nested organization -- **Create Post**: Instant post creation -- **Rename**: Quick inline editing -- **Delete**: With confirmation dialogs - ---- - -## 🎯 Use Cases & Workflows - -### Content Creator Workflow - -1. Click AI Assistant → Request content strategy -2. Create new post with right-click menu -3. Type content while assistant offers suggestions -4. Upload featured image via click interface -5. Autosave handles persistence automatically -6. Switch to preview mode to verify appearance -7. Set publication state and date -8. Content live immediately (or scheduled) - -### Editorial Manager Workflow - -1. Review dashboard for all upcoming content -2. Use AI assistant to evaluate editorial quality -3. Bulk edit publication states -4. Organize by theme with group management -5. Rename groups to match campaign names -6. Track analytics by section -7. Maintain advisory standards - -### Content Strategist Workflow - -1. Access AI assistant for market trends -2. Ask for content calendar recommendations -3. Create multiple themed content groups -4. Plan cross-section content strategy -5. Use date scheduling for coordinated releases -6. Monitor performance by category -7. Iterate based on engagement metrics - ---- - -## 🔒 Data & Security - -**Safe, Private Operation** - -- **Demo Mode**: All changes stored locally (no external API calls) -- **Session-Based Storage**: Data persists in browser, not sent to servers -- **No Real Credentials**: Environment variables sanitized and safe -- **Type Safety**: Full TypeScript validation prevents common errors -- **Error Boundaries**: Graceful error handling with clear messaging - ---- - -## 📖 Keyboard Shortcuts & Quick Actions - -| Action | Method | -| ----------------- | ------------------------------------ | -| Create new item | Right-click menu or + button | -| Rename item | Double-click or right-click → Rename | -| Save changes | Automatic (Ctrl+S also works) | -| Switch theme | Theme button in top navigation | -| Open AI Chat | AI button in top-right corner | -| Navigate sections | Sidebar menu or breadcrumbs | -| Quick search | Search icon in navigation | - ---- - -## 🌟 Why Choose Autoblog CMS? - -✨ **AI-First Approach**: Integrated assistant that learns your writing style and provides contextual guidance - -⚡ **Speed**: Create and publish content in seconds, not minutes - -🎯 **Organization**: Hierarchical structure keeps content organized at scale - -📱 **Responsive**: Works perfectly on any device—no desktop-only limitations - -💡 **Intuitive**: Right-click menus and inline editing reduce menu diving - -🔄 **Autosave**: Never worry about losing work—changes persist automatically - -🎨 **Beautiful**: Professional design with dark/light theme support - -📊 **Scalable**: Organize hundreds of posts across multiple sections - -🚀 **Modern Stack**: Built with latest web technologies for performance - -🔐 **Safe**: Demo mode for testing without external dependencies - ---- - -## 🚀 Getting Started - -### Installation - -```bash -# Install dependencies -bun install - -# Start development server -bun dev - -# Build for production -bun build -``` - -### First Steps - -1. Navigate to any section (Featured, Design, Culture, Insights, Resources) -2. Right-click in the sidebar to create a group -3. Click the AI button to ask for writing suggestions -4. Create your first post and watch it autosave -5. Upload an image by clicking the image area -6. Switch to preview mode to see your content -7. Publish when ready! - ---- - -## 📞 Support & Documentation - -- **AI Assistant**: Click the "AI" button to ask for help on any feature -- **Comments & Feedback**: All changes are automatically saved -- **Demo Data**: Multiple sample posts in each section to learn from - ---- - -**Autoblog CMS v1.0** - Built for modern content creators who value speed, intelligence, and elegance. +# AutoBlog CMS feature evidence + +Status is explicit: **Implemented** means a reachable persisted path exists; **Tested** names its +automated proof; **Demo-only** is bounded and labeled; **Planned** is not represented as available. + +| Product capability | Implementation | Automated evidence | Status | +| --- | --- | --- | --- | +| Workspace-isolated persistence | one Drizzle repository over local or remote libSQL | empty migration, restart, cross-tenant integration | Implemented, Tested | +| Database-backed login/logout | Better Auth HTTP-only SameSite sessions, revocation and rate limit | identity integration and anonymous E2E | Implemented, Tested | +| Owner/Admin/Editor/Author/Reviewer | server permission matrix plus Author ownership | complete role/command unit matrix and denial E2E | Implemented, Tested | +| Draft autosave | 850 ms debounce, serialized conditional writes | revision integration and reload E2E | Implemented, Tested | +| Concurrent editing safety | post version precondition and stable `VERSION_CONFLICT` | concurrent writers and stale browser E2E | Implemented, Tested | +| Immutable revisions | insert-only rows, compare and restore-as-new | trigger, compare/restore integration and E2E | Implemented, Tested | +| Editorial workflow | Draft, In Review, Changes Requested, Approved, Scheduled, Published, Archived | transition/permission unit tests and flagship E2E | Implemented, Tested | +| Scheduled publication | transactional job/publication, lease, retry and idempotency | duplicate execution and lease-recovery integration | Implemented, Tested | +| Immutable public preview | publication pins a reviewed revision | later-draft and publication E2E | Implemented, Tested | +| Media upload | bounded multipart, decoded PNG/JPEG/WebP verification | MIME, size, dimension, pixel and isolation tests | Implemented, Tested | +| Safe media replacement | new object verification before atomic activation and cleanup | injected provider/finalization failure tests | Implemented, Tested | +| AI suggestion preview | authenticated bounded command with explicit Apply | adapter contracts, abuse tests and E2E | Implemented, Tested | +| Gemini adapter | structured `@google/genai` response with abort and real metadata | stubbed provider contract/malformed/timeout tests | Implemented, Tested; live provider not asserted | +| Guided recruiter checklist | progress derived from durable version/workflow/publication state | recruiter and role-flow E2E | Demo-only, Tested | +| Demo reset | Owner-only, origin-checked, idempotent, three operations/hour | isolation/retry/rate integration and E2E | Demo-only, Tested | +| Responsive product surfaces | desktop and 390×844 layouts | six Chromium visual baselines | Implemented, Tested target | +| Accessibility targets | labels, focus, keyboard flow, reduced motion | axe on four surfaces plus keyboard/focus tests | Implemented, Tested target | +| Performance budgets | route-semantic caching, direct budgets and local Lighthouse reports | executable LCP/CLS/interaction/JS plus six Lighthouse runs | Implemented, Tested target | +| Analytics | no fake metrics or dashboard | absence is deliberate | Planned | +| Live co-author merging | conflicts are surfaced; automatic merge is not implemented | no claim | Planned | +| External object store/CDN | current bounded adapter uses durable libSQL objects | no claim | Planned | +| Hosted v2 demo | repository side is complete; remote persistence/scheduler are external | release verification pending | Planned external gate | + +## Primary paths + +- Author: create → autosave → history/restore → submit. +- Reviewer: request changes or approve; cannot mutate or publish. +- Editor: schedule/publish a pinned revision and manage media. +- Public reader: view only the immutable published revision. +- AI: preview a labeled mock or configured suggestion; Apply follows normal autosave rules. +- Owner: reset only the seeded `ws-demo` workspace without deleting identities or sessions. + +## Deliberate non-claims + +AutoBlog does not claim blanket WCAG compliance, universal browser support, field performance, +real-time co-authoring, analytics, live-provider output quality, automatic image conversion or +high-volume media delivery. Evidence and residual limits are linked from [README.md](./README.md). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1a5e126 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024-2026 PatrickDev-it + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 4126714..3108573 100644 --- a/README.md +++ b/README.md @@ -1,366 +1,138 @@ -# Autoblog CMS + Gemini AI Assistant - -A modern, intelligent blog content management system built with Next.js and Tailwind CSS. Features AI-assisted content creation, intuitive -hierarchical organization, professional editorial workflows, and beautiful responsive design. - -**[📚 Full Feature Guide →](./FEATURES.md)** - ---- - -## ✨ Key Strengths - -### 🤖 Integrated AI Assistant - -- **Gemini 1.5 Integration**: Select from Flash, Flash 8B, Pro, or 2.0 models -- **Real-Time Guidance**: Get suggestions on content creation, SEO, editorial standards, analytics -- **Typewriter Effect**: Beautiful character-by-character animations for engaging presentation -- **Conversation History**: Full session persistence for context-aware responses -- **Markdown Rendering**: Professional formatting with lists, emphasis, and embedded content - -### ✏️ Effortless Content Management - -- **Right-Click Organization**: Context menus for creating groups, subgroups, and posts -- **One-Click Renaming**: Instant inline editing for any content item -- **Hierarchical Structure**: Section → Group → Subgroup → Post organization -- **Professional Templates**: Pre-configured content categories (Featured, Design, Culture, Insights, Resources) -- **Publication States**: Draft, Scheduled, Published, Archived with date scheduling - -### 💾 Smart Persistence - -- **Autosave Everything**: Changes persist instantly without manual saving -- **Session Storage**: Secure browser-based data handling -- **Real-Time Sync**: All edits reflected immediately across dashboard -- **No Data Loss**: Automatic recovery from browser cache - -### 📱 Truly Responsive Design - -- **Desktop**: Full-featured sidebar + dual-column editor -- **Tablet**: Optimized grid layouts and touch targets -- **Mobile**: Full-screen modals and finger-friendly controls -- **Tested Across Devices**: Works seamlessly on all modern browsers - -### 🖼️ Professional Image Management - -- **Click-to-Upload**: Intuitive image selection and preview -- **Optimized Loading**: Next.js Image optimization with lazy loading -- **Multiple Formats**: JPG, PNG, WebP with automatic conversion -- **Responsive Images**: Proper sizing for all device types -- **8 Professional Demo Images**: Ready-to-use for all content sections - -### 🎨 Enterprise-Grade UI - -- **Dark & Light Themes**: Full theme switching with system preference detection -- **Accessible Components**: WCAG AA compliant with keyboard navigation -- **Smooth Animations**: Typewriter effects, pulsing indicators, hover states -- **Breadcrumb Navigation**: Always know where you are in the structure - -### 📊 Comprehensive Organization - -- **5 Content Sections**: Featured, Design, Culture, Insights, Resources -- **20+ Demo Posts**: Realistic content examples in every section -- **Multi-Level Groups**: Organize content by themes, months, topics -- **Editorial Advisory**: Built-in system for maintaining brand standards - -### ⚡ Lightning-Fast Performance - -- **Server Components**: React 18 with server-side rendering -- **Lazy Loading**: Images and components load on-demand -- **Code Splitting**: Automatic optimization by Next.js -- **Zero External Calls**: Demo mode with full offline functionality - ---- - -## 🚀 Quick Start - -### Installation - -```bash -# Install dependencies -bun install - -# Start development -bun dev - -# Production build -bun build -bun start -``` - -### First Steps - -1. Navigate to any content section in the sidebar -2. Right-click to create a new group or post -3. Click **AI** button (top-right) to ask for content guidance -4. Write and watch as your content autosaves -5. Upload featured images with one click -6. Switch between preview and edit modes -7. Publish or schedule for later! - ---- - -## 📖 Technology Stack - -- **Frontend**: Next.js 14+, React 18+, TypeScript -- **Styling**: Tailwind CSS, shadcn/ui components -- **State**: React hooks, Context API, Browser SessionStorage -- **AI**: Google Generative AI (Gemini) -- **Images**: Next.js Image optimization -- **Theme**: Dark/Light mode with system preference detection - ---- - -## 🎯 Core Features - -| Feature | Benefit | -| -------------------------- | ------------------------------------------------ | -| **AI Assistant** | Intelligent guidance on every aspect of blogging | -| **Right-Click Menu** | Create content 3x faster than traditional CMS | -| **Autosave** | Never lose work again—saves automatically | -| **Mobile Responsive** | Works perfectly on phone, tablet, desktop | -| **Image Management** | Optimized uploads with multiple format support | -| **Editorial Standards** | Advisory panel maintains brand consistency | -| **Publication Scheduling** | Schedule posts for future release | -| **Demo Mode** | Test everything without external dependencies | -| **Dark Theme** | Eye-friendly interface for extended use | -| **Loading States** | Beautiful skeleton loaders while fetching data | - ---- - -## 📁 Project Structure - +# AutoBlog CMS + +An AI-assisted editorial CMS built to demonstrate durable workflow engineering: real sessions, +workspace-scoped RBAC, immutable revisions, optimistic concurrency, scheduled publication, bounded +media and explicit AI suggestions. + +[![Quality gates](https://github.com/PatrickDev-it/AutoBlog-CMS/actions/workflows/quality.yml/badge.svg)](https://github.com/PatrickDev-it/AutoBlog-CMS/actions/workflows/quality.yml) +[![MIT license](https://img.shields.io/badge/license-MIT-111111.svg)](./LICENSE) + +> AutoBlog 2.0 is release-ready in the repository, but the hosted v2 demo is not advertised yet. +> Provider credential rotation and remote libSQL/scheduler configuration remain owner actions. + +![AutoBlog recruiter landing page](./tests/visual/surfaces.spec.ts-snapshots/marketing-desktop.png) + +## Product outcome + +AutoBlog takes a post from Draft through review, approval and immutable publication. Every protected +request resolves a database session and workspace membership before invoking an application service. +Stale autosaves return HTTP 409, scheduled jobs are leased and idempotent, and generated text remains +a suggestion until a user applies it. + +```mermaid +flowchart LR + Browser --> Adapter[Next.js server adapter] + Adapter --> Session[Better Auth session] + Session --> Policy[Workspace membership and RBAC] + Policy --> Service[Application command or query] + Service --> Domain[Editorial invariants] + Service --> DB[(Drizzle and libSQL)] + Service --> Jobs[(Durable jobs)] + Service --> Media[Verified media port] + Service --> AI[Bounded AI port] ``` -├── app/ # Next.js app router -│ ├── @inset/ # Main content editor layout -│ │ ├── [section]/ # Dynamic section pages -│ │ ├── [id]/ # Individual post editor -│ │ ├── home/ # Homepage image management -│ │ └── advisory/ # Editorial advisory panel -│ ├── @sidebar/ # Navigation sidebar -│ ├── api/ # API routes (demo mode) -│ └── layout.tsx # Root layout -├── components/ # React components -│ ├── chat/ # AI assistant interface -│ ├── groups/ # Group/subgroup components -│ ├── webapp/ # Post preview components -│ └── imageLoader.tsx # Image management UI -├── utils/ # Utility functions -│ ├── api-fetch.ts # Mock API with demo data -│ └── revalidate.ts # Cache invalidation -├── constants/ # Configuration -│ ├── sections.ts # Content sections -│ └── post-init-template.ts # Default post fields -├── hooks/ # Custom React hooks -│ └── use-typewriter.ts # Typewriter animation -├── public/demo/ # Demo images (8 professional images) -└── types/ # TypeScript type definitions -``` - ---- - -## 🔧 Configuration - -### Environment Variables - -All environment variables are pre-configured with safe demo values. No real credentials are committed. - -```env -# Local demo configuration (no external dependencies needed) -MONGODB_URI="" -DBNAME="" -NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME="" -GEMINI_API_KEY="" -``` - -### Customization - -- **Sections**: Edit `constants/sections.ts` to add categories -- **Demo Data**: Modify `utils/api-fetch.ts` to change sample content -- **Styling**: Tailwind CSS with custom CSS variables in `app/globals.css` -- **Components**: Use shadcn/ui for building new features - ---- - -## 🎨 Content Sections - -The CMS comes with 5 professionally-organized sections: -### Featured Stories +## Guided evaluation -Curated highlights and trending content +1. Run the local setup below and open `http://localhost:3000/sign-in`. +2. Enter as **Author**, create a draft, edit it, reload, inspect history and submit it. +3. Sign in as **Reviewer** to request changes or approve. +4. Sign in as **Editor** to publish or schedule the pinned revision, then open public preview. +5. Request a visibly labeled mock AI suggestion and verify that content changes only after **Apply**. +6. Sign in as **Owner** to reset only the bounded demo workspace. -- Examples: Digital Revolution, Sustainable Design, Community Impact +The application checklist derives its progress from persisted state. Seeded role buttons create +ordinary HTTP-only sessions; they are not an authentication bypass. -### Design & Creative +![Authenticated editorial workspace](./tests/visual/surfaces.spec.ts-snapshots/workspace-desktop.png) -Design trends, visual systems, UI/UX expertise +## Evidence-backed feature matrix -- Examples: UX Trends 2025, Accessibility Design, Typography Mastery +| Capability | Status | Executable evidence | +| --- | --- | --- | +| Durable workspace persistence | Implemented, tested | migration-from-empty, restart and isolation integration tests | +| Real authentication and five-role RBAC | Implemented, tested | complete policy matrix, route denials and E2E role switching | +| Immutable history, compare and restore | Implemented, tested | append-only triggers, integration tests and flagship E2E | +| Conflict-safe debounced autosave | Implemented, tested | concurrent writer integration test and HTTP 409 E2E | +| Review, scheduling and publication | Implemented, tested | state-machine tests, leased job tests and public-preview E2E | +| Bounded PNG/JPEG/WebP media | Implemented, tested | MIME/size/dimension/isolation and compensation tests | +| AI suggestions | Implemented, tested | shared mock/Gemini contract, quota/rate/timeout and explicit-Apply E2E | +| Guided resettable workspace | Demo-only, tested | Owner-only bounded reset integration and E2E | +| Analytics dashboard | Planned | no product claim or simulated analytics path | +| High-volume object storage | Planned | current adapter deliberately stores bounded objects in libSQL | +| Hosted AutoBlog 2.0 demo | Planned external gate | remote database, scheduler and historical credential rotation required | -### Culture & Arts +See [FEATURES.md](./FEATURES.md) for the complete claim-to-test map. -Cultural commentary, exhibitions, interviews +## Local setup -- Examples: Installation Art Showcase, Fashion Week, Artist Interviews +Requirements: Bun 1.3.12 and Node.js 20.9 or later. -### Insights & Analysis - -Industry trends, research, thought leadership - -- Examples: Market Analysis, Technology Forecast, Future of Work - -### Resources & Guides - -Tutorials, guides, tools, and best practices - -- Examples: Web Performance, Security Practices, Designer's Toolkit - -Each section includes realistic multi-level organization and professional demo content. - ---- - -## 📊 Dashboard Features - -### Sidebar Navigation - -- Content sections with collapsible groups -- Quick access to Home and Advisory pages -- Theme switcher for dark/light mode -- Environment configuration - -### Main Editor - -- Dual-view: Preview & Edit modes -- Rich text editing with Markdown support -- Image upload and management -- Publication state and scheduling -- Auto-save indicator - -### AI Assistant - -- Pulsing "AI" indicator showing availability -- Model selection for different use cases -- Conversation history maintained in session -- Responsive markdown rendering -- Smart category-based responses - -### Loading States - -- Skeleton loaders for all async operations -- Beautiful transitions while fetching -- Matches actual layout structure -- Prevents layout shift - ---- - -## 🚀 Performance Optimizations - -- **Server Components**: React Server Components for reduced JS -- **Image Optimization**: Next/Image with lazy loading -- **Code Splitting**: Automatic route-based splitting -- **Type Safety**: Full TypeScript coverage prevents errors -- **Responsive Design**: Mobile-first CSS approach -- **Caching**: Smart cache strategies - ---- - -## 🔐 Security & Best Practices - -- **Type-Safe**: Full TypeScript validation -- **Demo Mode**: No real API calls or credentials in code -- **WCAG AA**: Accessible design for all users -- **Semantic HTML**: Proper DOM semantics -- **Environment Variables**: Secure configuration handling - ---- - -## 💡 Example Workflows - -### Content Creator - -``` -1. Ask AI: "How do I write engaging content?" -2. Right-click → Create Post -3. Type while AI offers suggestions -4. Upload featured image -5. Autosave handles persistence -6. Preview to verify layout -7. Publish or schedule -``` - -### Editorial Manager - -``` -1. Open Dashboard -2. Ask AI: "Evaluate editorial quality" -3. Right-click group → Rename for campaign -4. Update publication states -5. Set scheduling dates -6. Monitor analytics by section -``` - -### Content Strategist - -``` -1. Chat with AI for trend analysis -2. Request content calendar ideas -3. Create thematic content groups -4. Plan cross-section strategy -5. Schedule coordinated releases -6. Track performance metrics +```bash +cp .env.example .env.local +bun install --frozen-lockfile +bun run db:setup +bun run dev ``` ---- - -## 📚 Additional Resources - -- **Features Guide**: See [FEATURES.md](./FEATURES.md) for comprehensive feature documentation -- **Keyboard Shortcuts**: Right-click context menus + keyboard support -- **AI Assistance**: Click AI button to learn about any feature - ---- - -## 🎯 Use Cases - -✅ **Professional Bloggers**: Multi-category blog organization with editorial workflow ✅ **Content Teams**: Collaborative content creation with -advisory standards ✅ **Marketing Teams**: Campaign-based content organization with scheduling ✅ **Agencies**: Client portfolio and case study -management ✅ **Thought Leaders**: Publish insights and establish authority ✅ **Educational Platforms**: Course and learning content organization - ---- - -## 📝 License - -This project is provided as-is for educational and commercial use. - ---- - -**Autoblog CMS v1.0** - Where AI meets intelligent content management. - -Built with modern web technologies. Optimized for performance. Designed for scale. - -- `CLOUDINARY_API_SECRET` -- `CLOUDINARY_WEBSITE_FOLDER` -- `GEMINI_API_KEY` -- `NEXT_PUBLIC_FIREBASE_API_KEY` -- `NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN` -- `NEXT_PUBLIC_FIREBASE_PROJECT_ID` -- `NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET` -- `NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID` -- `NEXT_PUBLIC_FIREBASE_APP_ID` -- `NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID` - -## Security and Compliance Notes - -- Do not commit real credentials, API keys, tokens, or passwords. -- Keep `.env.local` environment-specific and secret-scoped. -- Rotate any credential that was previously committed. +The default `.env.example` uses a durable local SQLite/libSQL file and deterministic mock AI. Do not +use a `file:` database in a serverless public deployment. -## Deployment +| Variable | Purpose | +| --- | --- | +| `NEXT_PUBLIC_APP_URL` | canonical origin used for cookies and origin validation | +| `DATABASE_URL` | local `file:` URL or remote `libsql:`/`https:` URL | +| `DATABASE_AUTH_TOKEN` | remote libSQL credential; omit locally | +| `BETTER_AUTH_SECRET` | unique 32+ character session secret | +| `DEMO_ENABLED` | installs and permits reset of the bounded demo workspace | +| `AI_MODE` | `mock` or `gemini` | +| `GEMINI_API_KEY`, `GEMINI_MODEL` | optional configured AI provider settings | +| `CRON_SECRET` | distinct 24+ character scheduler credential | +| `AI_MONTHLY_CHARACTER_QUOTA` | durable workspace AI character budget | +| `MEDIA_MAX_BYTES` | upload cap, constrained to at most 10 MiB | -Build and run in production mode: +## Quality gates ```bash -npm run build -npm run start +bun run typecheck +bun run lint +bun run test +bun run test:integration +bun run test:e2e +bun run test:a11y +bun run test:visual +bun run build +bun run test:performance +bun run test:lighthouse +bun run audit +bun run security:secrets ``` -Use your preferred hosting provider (e.g., Vercel) with environment variables configured in the deployment platform. +The measured local release evidence is 19 unit, 25 integration, 10 E2E, 6 accessibility, 5 visual, +2 direct performance tests and 6 Lighthouse runs. Current budgets and measurements are recorded in +[docs/performance.md](./docs/performance.md); these are lab targets, not universal field claims. + +## Engineering documentation + +- [Architecture and boundaries](./docs/architecture.md) +- [Data model and migrations](./docs/data-model.md) +- [Authentication and RBAC](./docs/authentication-rbac.md) +- [Editorial workflow](./docs/editorial-workflow.md) +- [API and stable errors](./docs/api.md) +- [Security threat model](./docs/security-threat-model.md) +- [Media security](./docs/media-security.md) +- [AI data handling](./docs/ai-data-handling.md) +- [Accessibility evidence](./docs/accessibility.md) +- [Performance evidence](./docs/performance.md) +- [Deployment](./docs/deployment.md) +- [Rollback and recovery](./docs/rollback-recovery.md) +- [Known limitations](./docs/known-limitations.md) +- [2.0 release notes](./docs/releases/v2.0.0.md) + +## Security and license + +Current source and runtime dependencies scan clean. An early shared commit contains historical +MongoDB, Cloudinary and Gemini values; their account owners must rotate or revoke them before public +v2 release. Values are never reproduced in documentation or fixtures. See [SECURITY.md](./SECURITY.md). + +Licensed under the [MIT License](./LICENSE), copyright 2024–2026 PatrickDev-it. diff --git a/RFC.md b/RFC.md new file mode 100644 index 0000000..669b26a --- /dev/null +++ b/RFC.md @@ -0,0 +1,385 @@ +# RFC — AI-Assisted Editorial CMS + +- **Status:** Accepted — implementation authorized 2026-07-22 +- **Scope:** conversion of the current AutoBlog dashboard into a verifiable portfolio product +- **Audience:** implementation agents, frontend/full-stack reviewers, product and security reviewers +- **Decision horizon:** one stable public demo and one production-shaped application core +- **Out of scope:** Privacy organization and unrelated repositories + +## 1. Executive decision + +AutoBlog will become an authenticated editorial CMS with one coherent persistence path, explicit workflow +states and an intentionally designed recruiter demo. The current split between an in-memory demo adapter +and dormant MongoDB/Cloudinary routes will be removed. A feature is not considered implemented merely +because a component or route exists; it must be reachable, persisted, authorized and tested. + +The product will retain its strongest asset—the visual editing surface—but reduce duplicated UI machinery, +remove unverifiable enterprise claims and prioritize a reliable editorial workflow over feature volume. + +## 2. Product projection + +### 2.1 Intended product + +A small editorial team should be able to organize posts, edit content, review revisions, schedule +publication, manage media and request bounded AI assistance while maintaining role-based control and a +complete audit trail. + +### 2.2 Portfolio role + +This is the portfolio's product-engineering and full-stack project. It should demonstrate: + +- polished responsive UI and accessible interaction design; +- modern React/Next.js architecture with correct server/client boundaries; +- authentication, authorization and multi-user workflows; +- durable persistence and concurrency handling; +- secure media and AI-provider integrations; +- end-to-end testing and web performance discipline; +- product analytics and evidence-driven UX decisions. + +### 2.3 Success statement + +A recruiter should open the public demo, enter through a documented demo account, complete a guided +draft-to-publication workflow and understand the architecture and quality evidence without configuring +private services. + +## 3. Verified baseline + +The current public deployment responds and presents a visually consistent dark dashboard, but its first +screen is largely empty and does not explain how to evaluate the product. + +The verified engineering baseline is: + +- frozen dependency installation fails because `bun.lock` and `package.json` disagree; +- typecheck produces 42 errors across 16 files; +- lint produces 57 errors and 26 warnings; +- production build succeeds only because type and lint validation are disabled; +- dependency audit reports 55 vulnerabilities, including critical and high runtime findings; +- no automated tests or project CI exist; +- most visible data flows use an in-memory `demoStore` that resets with the process; +- database, image and AI routes are not protected by real authentication; +- `.env.local` is versioned with empty values; no live secret was observed, but the convention is unsafe; +- README claims exceed the behavior demonstrably connected to the UI; +- the public repository lacks license, topics, description and Actions workflow. + +## 4. Problem register and decisions + +### P-01 — The application has two contradictory backends + +- **Severity:** Critical +- **Evidence:** server-rendered pages call a large in-memory API simulator while MongoDB route handlers + implement a separate persistence path. +- **Impact:** reviewers cannot determine what is real; mutations may disappear after restart. +- **Decision:** introduce one application service interface and one selected persistence implementation. + A demo environment uses seeded durable data, not a separate behavioral codebase. +- **Acceptance:** all product flows traverse the same commands, policies and repository contracts. + +### P-02 — Build configuration conceals defects + +- **Severity:** Critical +- **Evidence:** `ignoreBuildErrors` and `ignoreDuringBuilds` permit deployment despite type and lint failure. +- **Impact:** a green deployment is not evidence of source quality. +- **Decision:** remove both bypasses after an explicit defect burn-down. +- **Acceptance:** typecheck, lint, tests and build are mandatory independent CI jobs. + +### P-03 — Dependency state is not reproducible or secure + +- **Severity:** Critical +- **Evidence:** frozen install fails; audit includes critical Next.js and high Cloudinary/Mongoose findings. +- **Impact:** clean builds and public exposure are unsafe. +- **Decision:** reconcile the lockfile, upgrade to supported patched versions and define an audit budget of + zero unreviewed critical/high runtime vulnerabilities. +- **Acceptance:** clean frozen install and security audit pass in CI. + +### P-04 — Authentication is simulated + +- **Severity:** Critical +- **Evidence:** user check succeeds for any non-empty username and token; API routes have no session guard. +- **Impact:** database, media and AI operations are publicly mutable if configured. +- **Decision:** use a maintained server-side authentication system with secure cookies and explicit session + validation on every mutation. +- **Acceptance:** anonymous and insufficient-role requests fail in route and end-to-end tests. + +### P-05 — Authorization and editorial roles are undefined + +- **Severity:** Critical +- **Evidence:** UI mentions members and workflow but no consistent server policy exists. +- **Impact:** the product cannot support collaborative editorial work safely. +- **Decision:** define `Owner`, `Admin`, `Editor`, `Author` and `Reviewer` capabilities. Authorization lives + in application policies, not hidden component conditions. +- **Acceptance:** a permission matrix and negative test suite cover every command. + +### P-06 — Content schema is weak and duplicated + +- **Severity:** High +- **Evidence:** mixed `id`/`_id`, pervasive `any`, ad hoc post shapes and duplicated demo/API mappings. +- **Impact:** type errors, migration ambiguity and UI condition sprawl. +- **Decision:** establish canonical schemas for Workspace, User, Post, Revision, MediaAsset and Publication. +- **Acceptance:** database, API and UI use generated or shared validated types without unsafe casting. + +### P-07 — Editorial workflow is presented but not modeled + +- **Severity:** High +- **Evidence:** draft/scheduled/published/archived values exist without transition policy or durable jobs. +- **Impact:** invalid state changes and misleading product claims. +- **Decision:** model an explicit state machine with transition permissions, timestamps and audit events. +- **Acceptance:** illegal transitions fail and scheduled publication is idempotently executed by a worker. + +### P-08 — Version control claims are unsupported + +- **Severity:** High +- **Evidence:** no durable revision entity or restore operation is connected to the editor. +- **Impact:** a headline feature is non-functional. +- **Decision:** each meaningful content save produces an immutable revision; drafts track a current revision + and restore creates another revision rather than mutating history. +- **Acceptance:** create, diff, list and restore revision flows have integration and E2E coverage. + +### P-09 — Autosave lacks concurrency semantics + +- **Severity:** High +- **Evidence:** local/session persistence and generic updates do not protect simultaneous editing. +- **Impact:** lost updates and false “no data loss” claims. +- **Decision:** debounce client writes, use revision/version preconditions and surface merge conflicts. +- **Acceptance:** stale update test returns conflict and UI offers reload or compare behavior. + +### P-10 — Image upload boundary is unsafe and inefficient + +- **Severity:** Critical +- **Evidence:** large base64 request bodies, arbitrary remote images, public-id deletion before successful + replacement and no visible MIME/dimension policy. +- **Impact:** memory pressure, SSRF/image abuse and irreversible media loss. +- **Decision:** use signed direct upload or bounded multipart upload, validate media and finalize DB changes + only after provider success. Destructive cleanup becomes compensating background work. +- **Acceptance:** invalid media is rejected; replacement failure preserves the existing asset. + +### P-11 — AI endpoint is unauthenticated and unbounded + +- **Severity:** Critical +- **Evidence:** prompt requests can reach the provider without role, quota, rate limit or usage accounting. +- **Impact:** cost abuse, provider exhaustion and prompt/data leakage. +- **Decision:** provider adapter, authenticated server command, per-workspace quota, rate limit, timeout, + structured usage log and explicit user confirmation before applying generated content. +- **Acceptance:** AI calls expose latency/token metadata and never mutate content automatically. + +### P-12 — AI product behavior is split between canned and live paths + +- **Severity:** High +- **Evidence:** a large canned-response action coexists with a live provider route. +- **Impact:** demonstration can imply capabilities that are not being exercised. +- **Decision:** use one interface with explicit `demo`, `mock` and configured-provider adapters. UI must label + the active mode. +- **Acceptance:** identical contract tests run against mock and provider adapters. + +### P-13 — UI architecture contains dead and duplicated code + +- **Severity:** High +- **Evidence:** missing imports, unused components, duplicated sidebar implementations and very large custom + files coexist with generated UI primitives. +- **Impact:** reviewer navigation is slow and type debt grows. +- **Decision:** delete unreachable legacy branches, isolate generated primitives and split product features + by domain rather than page fragment. +- **Acceptance:** no missing import, dead route or duplicate component ownership remains. + +### P-14 — Accessibility claims are unverified + +- **Severity:** High +- **Evidence:** README declares WCAG compliance while lint finds interaction issues and no automated or + manual audit artifact exists. +- **Impact:** compliance language is not credible. +- **Decision:** state accessibility targets, not compliance, until axe and manual keyboard/screen-reader + checks are stored. +- **Acceptance:** critical flows pass axe, keyboard navigation and focus-order review. + +### P-15 — Performance configuration works against the framework + +- **Severity:** Medium +- **Evidence:** global no-cache headers, permissive remote images and broad client-component usage reduce + Next.js optimization value. +- **Impact:** higher JS cost and unclear caching model. +- **Decision:** define cache behavior per read model, keep authenticated editor dynamic and allow public + preview/content to use explicit revalidation. +- **Acceptance:** Lighthouse and bundle reports establish budgets for LCP, INP, CLS and client JS. + +### P-16 — Error handling leaks internal information + +- **Severity:** High +- **Evidence:** generic helper returns raw error messages and logs complete stack details without a public + error taxonomy. +- **Impact:** inconsistent status codes and potential disclosure. +- **Decision:** map domain/application errors to stable public codes and log internal details with request ID. +- **Acceptance:** API contract tests verify status, code and non-sensitive message. + +### P-17 — Product onboarding is absent + +- **Severity:** High +- **Evidence:** deployment opens to “select a page to edit” with limited guidance or populated navigation. +- **Impact:** recruiter may abandon the review before discovering features. +- **Decision:** add a marketing/demo entry, seeded workspace, guided checklist and reset-demo action. +- **Acceptance:** first-time reviewer completes the main flow without external instructions. + +### P-18 — Repository evidence is incomplete + +- **Severity:** High +- **Evidence:** no tests, CI, license, release, concise architecture or truthful feature matrix. +- **Impact:** visual polish is not supported by engineering proof. +- **Decision:** add layered tests, CI, screenshots, architecture, security notes and a live-demo badge. +- **Acceptance:** README claims map to demo steps and automated checks. + +## 5. Target architecture + +```mermaid +flowchart LR + Browser[Next.js UI] --> BFF[Server actions / route adapters] + BFF --> Session[Session + RBAC policy] + Session --> App[Application commands and queries] + App --> Domain[Editorial domain] + App --> Repo[Repository ports] + Repo --> DB[(Database)] + App --> Jobs[(Job/outbox queue)] + Jobs --> Publish[Publication worker] + Jobs --> Media[Media cleanup worker] + App --> AI[AI provider adapter] + App --> Storage[Media provider adapter] + BFF --> Telemetry[Errors, traces, product events] +``` + +Recommended feature layout: + +```text +app/ + (marketing)/ + (auth)/ + (workspace)/[workspaceId]/... +src/ + modules/{identity,editorial,media,ai,analytics}/ + platform/{db,auth,config,observability}/ + ui/{primitives,patterns}/ +tests/{unit,integration,e2e,accessibility}/ +``` + +## 6. Core product invariants + +- Every mutable entity belongs to a workspace. +- Identity and workspace context come from the authenticated session. +- Only legal role/state combinations can execute a transition. +- Published content points to an immutable revision. +- Autosave never silently overwrites a newer revision. +- AI output is a suggestion until a user explicitly applies it. +- Media replacement cannot destroy the active asset before success. +- Demo mode uses the same domain logic as configured production mode. + +## 7. What must be simplified + +- Delete the 500+ line in-memory route simulator after durable seed infrastructure exists. +- Delete canned content duplicated across code; store demo fixtures as validated seed data. +- Consolidate sidebar ownership and eliminate copied primitive implementations. +- Remove Firebase remnants if Firebase is not an accepted architecture dependency. +- Remove unused “members”, analytics and workflow UI until their server behavior is real. +- Prefer a small number of feature modules over `_client`, `actions`, `utils`, `components` catch-all layers. +- Avoid micro-frontends, event streaming platforms and multi-database architecture for this scope. + +## 8. Implementation plan + +### Phase 0 — Security and build containment + +- Upgrade vulnerable runtime dependencies. +- Reconcile and freeze lockfile. +- Fix type/lint errors and remove build bypasses. +- Replace tracked `.env.local` with `.env.example`; add secret scanning. +- Disable or guard destructive public routes until real auth exists. + +**Exit gate:** frozen install, audit, typecheck, lint and build pass. + +### Phase 1 — One coherent application core + +- Define canonical schemas and application ports. +- Replace demoStore with seeded repository implementation. +- Add session authentication and workspace/role policies. +- Connect current UI to commands and queries through one boundary. + +**Exit gate:** login, list, create and edit persist across process restart and enforce roles. + +### Phase 2 — Editorial workflow + +- Add revision model, optimistic concurrency and autosave. +- Add state machine, review transitions and scheduled publication worker. +- Add durable audit events and public preview. + +**Exit gate:** complete author-reviewer-publication E2E flow passes. + +### Phase 3 — Media and AI hardening + +- Replace base64 flow with validated upload protocol. +- Add provider adapters, quotas, usage metadata and user-controlled AI application. +- Add retry/cleanup jobs and integration tests. + +**Exit gate:** failure injection proves no media loss and no unbounded AI access. + +### Phase 4 — Recruiter experience + +- Add landing page, guided demo, reset action and seeded editorial workspace. +- Add Lighthouse, accessibility and visual regression checks. +- Publish architecture, threat model, screenshots, video and release notes. + +**Exit gate:** public demo and README present the same tested feature set. + +## 9. Skill projection + +| Skill | Required evidence | Recruiter interpretation | +|---|---|---| +| Product engineering | guided workflow and deliberate scope | builds usable outcomes, not component collections | +| Frontend architecture | feature boundaries and server/client discipline | understands modern React beyond UI styling | +| UX/accessibility | keyboard, axe and onboarding evidence | treats usability as an engineering requirement | +| Full-stack design | shared schemas, commands and durable persistence | can own vertical features end to end | +| Security | real sessions, RBAC, safe media and AI quotas | protects costly and destructive operations | +| Data consistency | revisions, state machine and optimistic locking | handles collaborative mutation correctly | +| Testing | unit, integration, E2E, visual and accessibility | validates both behavior and experience | +| Performance | measured web-vital and bundle budgets | optimizes from evidence rather than slogans | + +## 10. Definition of done + +- Frozen installation and all CI gates pass. +- No build validation bypass remains. +- No unreviewed critical/high runtime vulnerability remains. +- Real session and permission checks protect every mutation. +- One durable data path serves both demo and configured environments. +- Revision, autosave conflict and publication transitions are tested. +- Media and AI integrations enforce quotas, size limits and failure compensation. +- Core flows pass Playwright and accessibility checks. +- Public demo is populated, guided and resettable. +- README feature table matches tested behavior. +- GitHub metadata, license, topics, CI and release are complete. + +## 11. Agent operating contract + +An implementation agent must: + +1. read this RFC and map work to one or more problem IDs; +2. treat authentication and workspace context as mandatory for every mutation; +3. avoid creating a second demo-only business-logic path; +4. add a test at the lowest useful layer and an E2E test for changed flagship flows; +5. preserve existing visual identity unless the task explicitly changes product design; +6. remove dead code when replacing a path rather than retaining indefinite compatibility branches; +7. update schemas, API contracts and feature matrix together; +8. run frozen install, audit, typecheck, lint, tests and build before handoff; +9. capture screenshots or visual-regression baselines for visible changes; +10. report changes to authorization, data migration, SEO and accessibility explicitly. + +Every patch handoff must include: RFC IDs addressed, user flow affected, schema/API changes, test evidence, +security impact, visual evidence, rollback/migration notes and remaining risk. + +## 12. Rejected alternatives + +- **Keep demoStore and Mongo routes side by side:** rejected because behavior divergence is the primary + architecture defect. +- **Fix only lint and presentation:** rejected because authorization and persistence remain fictitious. +- **Client-only localStorage CMS:** rejected because it cannot demonstrate collaboration, workflow or + protected integrations. +- **Add more AI features first:** rejected because the costly provider boundary is currently unprotected. +- **Split into microservices:** rejected because module and data contracts are not yet stable. + +## 13. Final goal + +The completed project should be perceived as the portfolio's strongest visible product: polished but not +superficial, collaborative rather than simulated, and backed by secure full-stack architecture. Its role is +to demonstrate product judgment, frontend depth and end-to-end ownership; it should not duplicate the +backend reliability narrative of the loyalty platform or the model-evaluation narrative of the ML project. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..796a746 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,27 @@ +# Security policy + +Report vulnerabilities privately through GitHub Security Advisories. Do not open a public issue +containing exploit details, credentials, session material or user data. + +Supported releases receive dependency and application-security fixes on the latest `2.x` release +line. Security-sensitive mutations require a database-validated session, workspace membership and +an application permission check. Stable public errors include a correlation ID and exclude stack, +database and provider details. + +## Operational requirements + +- Use unique production values for `BETTER_AUTH_SECRET` and `CRON_SECRET`; never reuse a provider key. +- Use HTTPS and a durable remote libSQL database for serverless deployment. +- Invoke the leased job runner at least once per minute and alert on repeated failures. +- Keep `AI_MODE=mock` unless the operator has accepted provider data-processing and billing terms. +- Run `bun run audit`, `bun run security:secrets` and the full-history CI scan before release. +- Back up the database before schema deployment; applied migrations are checksum protected. + +The detailed assets, trust boundaries, mitigations and residual risks are in +[docs/security-threat-model.md](./docs/security-threat-model.md). + +## Historical credential notice + +An early repository commit contains non-empty MongoDB, Cloudinary and Gemini environment values. +The values are not used by AutoBlog 2.x and must be rotated or revoked by their provider account +owner. Shared history is intentionally not rewritten. diff --git a/_client/@inset/( advisory ).tsx b/_client/@inset/( advisory ).tsx deleted file mode 100644 index ca01966..0000000 --- a/_client/@inset/( advisory ).tsx +++ /dev/null @@ -1,87 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { ImageLoader } from '@/components/imageLoader'; -import { Button } from '@/ui/button'; -import { ServerProps } from '@/app/@inset/advisory/page'; -import { useToast } from '@/hooks/use-toast'; - -export default ({ api, data }: ServerProps) => { - const { toast } = useToast(); - const [about, setAbout] = useState(data.about); - const [services, setServices] = useState(data.services); - - const [image, setImage] = useState<{ display_name: string; public_id: string }>(data.image ?? ({} as any)); - const [file, setFile] = useState<File | null>(null); - const [timeout, startTimeout] = useState<NodeJS.Timeout | null>(null); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (file) { - const reader = new FileReader(); - reader.readAsDataURL(file); // Converte il file in Base64 - reader.onload = async () => { - const base64 = reader.result; - if (typeof base64 !== 'string' || !base64) return; - // Demo mode: Update local state only - setImage({ - public_id: `demo-image-${Date.now()}`, - display_name: 'Advisory Image', - }); - - toast({ - title: '🖼️ Advisory Image Updated', - description: 'Cover image changed successfully', - duration: 3000, - }); - }; - reader.onerror = () => { - alert('Error reading file!'); - }; - } - - // Demo mode: Local state update only - no API call - console.log('Demo mode: Advisory updated locally', { about, services }); - - toast({ - title: '✅ Advisory Panel Saved', - description: 'About and services information updated', - duration: 3000, - }); - }; - - return ( - <div className="relative flex flex-col lg:grid lg:grid-cols-12 lg:grid-rows-[repeat(12,minmax(2.5rem,auto))] gap-[--p] size-full overflow-scroll"> - <div className="grid grid-rows-[1.3rem_auto] grid-cols-1 -col-end-1 col-span-5 row-span-full w-full gap-[--p]"> - <div className="flex flex-row justify-between items-center row-span-1 size-full"> - <h3 className="text-xl leading-none ml-1">Immagine di copertina</h3> - - <Button onClick={handleSubmit} className=" text-xs w-fit h-full"> - Save - </Button> - </div> - <ImageLoader className="flex row-span-1 w-full h-[70svh] lg:h-full" image={data.image} onFileChange={setFile} /> - </div> - <div className="flex flex-col col-start-1 col-span-7 row-span-5 size-full gap-[--p]"> - <h3 className="text-xl leading-none ml-1">About</h3> - <textarea - spellCheck={false} - value={about} - onChange={e => setAbout(e.target.value)} - className="flex size-full resize-none rounded-md border border-input bg-sidebar-accent px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none ring-0 ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 lg:text-sm" - /> - </div> - - <div className="flex flex-col col-start-1 col-span-7 row-span-7 size-full gap-[--p]"> - <h3 className="text-xl leading-none ml-1">Services Offered</h3> - - <textarea - spellCheck={false} - value={services} - onChange={e => setServices(e.target.value)} - className="flex size-full resize-none rounded-md bg-sidebar-accent border border-input px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none ring-0 ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 lg:text-sm" - /> - </div> - </div> - ); -}; diff --git a/_client/@inset/( home ).tsx b/_client/@inset/( home ).tsx deleted file mode 100644 index 3d97f34..0000000 --- a/_client/@inset/( home ).tsx +++ /dev/null @@ -1,59 +0,0 @@ -'use client'; - -import { ImageLoader } from '@/components/imageLoader'; -import { ServerProps } from '@/app/@inset/home/page'; -import { useState } from 'react'; -import { useToast } from '@/hooks/use-toast'; - -export default ({ api, images: initImages }: ServerProps) => { - const { toast } = useToast(); - const [images, setImages] = useState<typeof initImages>(initImages ?? []); - - const handleChange = async (file: File, image: (typeof initImages)[0]) => { - if (!file) return; - - const reader = new FileReader(); - reader.readAsDataURL(file); // Converte il file in Base64 - reader.onload = async () => { - const base64 = reader.result as string; - - // Demo mode: Update local state only - const demoImage = { - ...image, - public_id: `/demo/section-${image.section}-${Date.now()}.jpg`, - display_name: `${image.section}-image`, - }; - // @ts-ignore - setImages(p => p.map(i => (i.section === image.section ? demoImage : i))); - - toast({ - title: '🖼️ Home Image Updated', - description: `${image.section.charAt(0).toUpperCase() + image.section.slice(1)} section image changed`, - duration: 3000, - }); - }; - reader.onerror = () => { - alert('Error reading file!'); - }; - }; - - return ( - <div className="relative flex flex-col justify-between items-start gap-[--p] size-full overflow-hidden"> - <h3 className="text-xl leading-none ml-1">Images of home sections</h3> - - <div className="grow shrink grid grid-cols-1 lg:grid-cols-2 lg:grid-rows-2 w-full lg:max-h-[95%] place-content-between content-between max-lg:overflow-y-scroll gap-[--p] p-px"> - { - images.map(image => ( - <div className="relative flex row-span-1 col-span-1 size-full rounded-lg overflow-hidden"> - <label className="z-30 absolute top-0 left-0 px-4 py-1.5 leading-none text-xs font-bold text-background bg-foreground rounded-br-lg"> - {image.section} - </label> - <ImageLoader onFileChange={file => handleChange(file, image)} className="size-full" image={image} /> - </div> - )) - // Aggiorna l'immagine nel database - } - </div> - </div> - ); -}; diff --git a/_client/@inset/( posts ) [ id ].tsx b/_client/@inset/( posts ) [ id ].tsx deleted file mode 100644 index 92bfe32..0000000 --- a/_client/@inset/( posts ) [ id ].tsx +++ /dev/null @@ -1,334 +0,0 @@ -'use client'; - -import { useRef, useState } from 'react'; -import { Input } from '@/ui/input'; -import { Button } from '@/ui/button'; - -import { DatePicker } from '@/components/data-picker'; -import WebApp from '@/components/webapp/_export'; -import Image from 'next/image'; - -import type { ServerProps } from '@/app/@inset/[section]/[id]/page'; - -import { useToast } from '@/hooks/use-toast'; - -const DEMO_PLACEHOLDER = - 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720"><rect width="100%" height="100%" fill="%23111827"/><text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" fill="%23ffffff" font-family="Arial" font-size="28">Demo Image</text></svg>'; - -const resolveImageSrc = (value?: string) => { - if (!value) return DEMO_PLACEHOLDER; - if (value.startsWith('http://') || value.startsWith('https://') || value.startsWith('/') || value.startsWith('data:')) return value; - - return DEMO_PLACEHOLDER; -}; - -export default ({ section, post: defaultPost }: ServerProps) => { - const { toast } = useToast(); - - const [post, _setPost] = useState(defaultPost); - const [timeout, startTimeout] = useState<NodeJS.Timeout | null>(null); - - const [page, setPage] = useState<'preview' | 'inner'>('preview'); - const [file, setFile] = useState<File | null>(null); - - const setImage = (file: File) => { - const reader = new FileReader(); - reader.readAsDataURL(file); // Converts the file to Base64 - reader.onload = async () => { - const base64 = reader.result; - if (typeof base64 !== 'string' || !base64) return; - - _setPost(prev => ({ - ...prev, - image: { - public_id: `demo-${Date.now()}`, - display_name: prev.name.replace(/[^a-zA-Z0-9]/g, ' ') || 'demo-image', - }, - })); - - toast({ - title: '🖼️ Featured Image Updated', - description: 'New cover image set successfully', - duration: 3000, - }); - }; - reader.onerror = () => { - alert('Error reading file!'); - }; - setFile(file); - }; - - const handleSubmit = async (updatedPost: typeof defaultPost = post) => { - _setPost(updatedPost); - toast({ - title: '✅ Post Saved', - description: 'All changes have been saved successfully', - duration: 3000, - }); - }; - - const setPost = async (callback: Parameters<typeof _setPost>[0]) => { - clearTimeout(timeout); - - const updatedPost = await new Promise(r => - _setPost(prev => { - const updated = typeof callback === 'function' ? callback(prev) : callback; - - r(updated); - return updated; - }), - ); - - startTimeout( - setTimeout(async () => { - await handleSubmit(updatedPost as any); - - toast({ - title: 'Post autosaved!', - duration: 10000, - }); - }, 500), - ); - - return updatedPost; - }; - - return ( - <div className="relative grow shrink flex flex-col md:grid grid-cols-12 grid-rows-10 gap-[--p] size-full overflow-hidden"> - <ImageLoader - className="row-start-1 max-xl:col-span-full col-span-8 row-span-4 w-full max-md:aspect-video md:size-full overflow-hidden" - image={post.image} - onFileChange={fileChange => { - setImage(fileChange); - }} - /> - - <textarea - className="resize-none !m-0 text-base col-start-1 max-xl:col-span-full col-span-8 row-span-1 md:size-full flex flex-col justify-start size-full rounded-md border border-input bg-sidebar-accent p-[calc(var(--p)/2)] ring-offset-transparent file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none ring-0 ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm" - placeholder="Title" - value={post.title} - onInput={e => { - // @ts-ignore - setPost(p => ({ ...p, title: e.target.value })); - }} - spellCheck={false} - /> - - <textarea - spellCheck={false} - value={post.description} - onChange={e => { - setPost(p => ({ ...p, description: e.target.value })); - }} - placeholder="Description" - className="flex col-start-1 max-xl:col-span-full col-span-8 row-span-5 md:size-full resize-none rounded-md border border-input bg-sidebar-accent px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none ring-0 ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm" - /> - {typeof post.date === 'object' ? - <> - <div className="relative col-span-2 row-span-1 row-start-1 gap-[inherit]"> - <label className="absolute top-[calc(var(--p)/2)] left-[--p] opacity-65 text-xs">From</label> - <DatePicker - defaultDate={post.date.from} - onSelect={date => - setPost(p => ({ - ...p, - date: { - // @ts-ignore - ...p.date, - from: date, - }, - })) - } - className="col-span-2 size-full bg-sidebar-accent" - /> - </div> - <div className="relative col-span-2 row-span-1 row-start-1 gap-[inherit]"> - <label className="absolute top-[calc(var(--p)/2)] left-[--p] opacity-65 text-xs">To</label> - <DatePicker - defaultDate={post.date.to} - onSelect={date => - setPost(p => ({ - ...p, - date: { - // @ts-ignore - ...p.date, - to: date, - }, - })) - } - className="col-span-2 size-full bg-sidebar-accent" - /> - </div> - </> - : <div className="relative col-span-4 row-span-1 row-start-1 gap-[inherit]"> - <label className="absolute top-[calc(var(--p)/2)] left-[--p] opacity-65 text-xs">Publication Date</label> - <DatePicker - defaultDate={post.date} - onSelect={date => - setPost(p => ({ - ...p, - date: date.toISOString(), - })) - } - className="size-full bg-sidebar-accent" - placeholder="Publication date" - /> - </div> - } - - <Input - className="bg-sidebar-accent col-span-2 max-xl:col-span-3 row-span-1 row-start-2 md:size-full [&>input]:cursor-text" - placeholder="Kind" - value={post.kind} - spellCheck={false} - onChange={e => setPost(p => ({ ...p, kind: e.target.value }))} - /> - <Input - className="bg-sidebar-accent col-span-2 max-xl:col-span-3 row-span-1 row-start-2 md:size-full [&>input]:cursor-text" - placeholder="State" - value={post.state} - spellCheck={false} - onChange={e => setPost(p => ({ ...p, state: e.target.value }))} - /> - - <Button className="hidden max-xl:flex col-span-2 row-span-1 row-start-2 md:size-full" onClick={() => handleSubmit()}> - Submit - </Button> - <div className="max-xl:hidden z-10 relative flex flex-col items-center justify-end -row-end-1 row-span-8 -col-end-1 col-span-4 md:size-full rounded-xl overflow-hidden"> - <div className="absolute size-full flex flex-col items-center justify-start px-6 "> - <div className="relative flex items-start justify-center size-fit min-h-[110%] max-2xl:w-11/12 !min-w-[15vw] 2xl:max-w-[65%] [mask-image:linear-gradient(to_bottom,#000_60%,transparent_95%)] transition-all duration-300 "> - <div className="absolute inset-auto bg-[#f8f8f8] mt-1 h-[calc(100%-2rem)] w-[calc(100%-1rem)] rounded-xl 2xl:rounded-[2rem] min-[1921px]:rounded-[3rem] min-[2500px]:rounded-[5rem] -z-[1] overflow-hidden transition-all duration-300"> - <WebApp - {...{ - page, - ...post, - }} - /> - </div> - <Image - className="!relative !h-auto min-h-[110%] !min-w-[15vw] !inset-auto z-30 object-top transition-all duration-300" - src="/iphone.png" - alt="Fitmatiz.." - objectFit="contain" - priority - fill - /> - </div> - </div> - <div className="z-30 flex flex-col justify-around items-center bg-background w-full h-1/4 rounded-xl p-[--p] border border-input"> - <div className="flex flex-row items-center justify-between w-4/5"> - <Button - style={{ - opacity: page === 'preview' ? 0.5 : 1, - }} - className="w-1/3" - onClick={() => setPage('preview')}> - Preview - </Button> - <Button - style={{ - opacity: page === 'inner' ? 0.5 : 1, - }} - className="w-1/3" - onClick={() => setPage('inner')}> - Inner - </Button> - </div> - {/* @ts-ignore */} - <Button className="w-4/5" onClick={handleSubmit}> - Submit - </Button> - </div> - </div> - </div> - ); -}; - -const ImageLoader = ({ - image: cdlImage, - onFileChange, - ...props -}: React.ComponentProps<'div'> & { - image: { display_name: string; public_id: string }; - onFileChange: (file: File) => void; -}) => { - const fileInput = useRef<HTMLInputElement>(null); - - const [image, setImage] = useState<{ display_name: string; public_id: string } | null>(cdlImage); - const [file, setFile] = useState<File | null>(null); - const [preview, setPreview] = useState<string | null>(null); // Preview state - - const handleFileChange = () => { - const file = fileInput.current?.files?.[0]; - - if (!file) return; - // Validate the selected file format - const validFormats = ['image/webp', 'image/png', 'image/jpeg', 'image/jpg']; - if (!validFormats.includes(file.type)) { - alert('Unsupported file format. Use WebP, PNG, JPEG, or JPG.'); - setPreview(null); - setFile(null); - return; - } - - // Create a temporary preview - setPreview(URL.createObjectURL(file)); - setFile(file); - onFileChange(file); - setImage(null); - }; - - return ( - <div {...props}> - <Input - type="file" - name="file" - value={''} - ref={fileInput} - onChange={handleFileChange} - accept="image/webp, image/png, image/jpeg, image/jpg" - className="hidden" - /> - <div className="relative flex justify-center items-center size-full rounded-lg overflow-hidden"> - <div className="absolute inset-auto aspect-video size-full"> - <div className="relative flex justify-center items-center size-full"> - {image ? - <> - <Image - className="object-cover" - fill - unoptimized - src={resolveImageSrc(image.public_id)} - alt={image.display_name} - /> - <Button - className="z-50 absolute top-2 right-2 shadow-[0_0_7px_3px_#00000024]" - onClick={() => setImage(null)}> - X - </Button> - </> - : !!preview ? - <> - <img - src={preview} - alt="Preview" - className="absolute !size-full object-cover object-center rounded-lg inset-auto" - /> - <Button - className="z-50 absolute top-2 right-2 shadow-[0_0_10px_2px_#00000024]" - onClick={() => (setPreview(null), setFile(null))}> - X - </Button> - </> - : <Button - className="size-full border-dashed border-2 bg-transparent hover:bg-transparent text-foreground" - onClick={() => fileInput.current?.click()}> - Upload cover image - </Button> - } - </div> - </div> - </div> - </div> - ); -}; diff --git a/_client/@inset/( users ) [ id ].tsx b/_client/@inset/( users ) [ id ].tsx deleted file mode 100644 index 9ab4154..0000000 --- a/_client/@inset/( users ) [ id ].tsx +++ /dev/null @@ -1,133 +0,0 @@ -'use client'; - -import { cn } from '@/utils/shadcn'; - -import { MdDelete } from 'react-icons/md'; -import { RiEdit2Fill } from 'react-icons/ri'; -import { BiSolidSave } from 'react-icons/bi'; - -import { Checkbox } from '@/ui/checkbox'; -import { DetailedHTMLProps, HtmlHTMLAttributes, useEffect, useState } from 'react'; - -import Copy from '@/components/Copy'; -import { Button } from '@/ui/button'; -import { Separator } from '@/ui/separator'; - -import type { CheckedState } from '@radix-ui/react-checkbox'; -import type { ServerProps } from '@/app/@inset/users/page'; -import { Input } from '@/ui/input'; - -export { Sidebar } from './( users )'; - -export const Main = ({ api, users }: ServerProps) => { - const [checkAll, setCheckAll] = useState<CheckedState>(false); - - return ( - <div className="flex flex-col w-full h-full "> - <div className="relative flex flex-col w-full h-8 shrink-0 items-center"> - <label - key="all" - htmlFor="all" - className="flex flex-row items-start w-full gap-[--p]"> - <Checkbox - id="all" - onCheckedChange={check => setCheckAll(check)} - /> - <span className="leading-5 text-foreground/50 text-sm"> - Username - </span> - </label> - - <Separator className="absolute w-full bottom-0" /> - </div> - - <div className="grid grow grid-cols-12 col-span-full -row-end-1 size-full items-start gap-[--p]"> - {users.map(user => ( - <User - key={user.id} - user={user} - checkAll={checkAll} - className="col-span-full row-span-1 h-12" - /> - ))} - </div> - </div> - ); -}; - -const User = ({ - user: _user, - checkAll, - className, - ...props -}: { - user: ServerProps['users'][0]; - checkAll: CheckedState; -} & Partial<DetailedHTMLProps<HtmlHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>>) => { - const [user, setUser] = useState<ServerProps['users'][0]>(_user); - const [edit, setEdit] = useState(false); - const [checked, setChecked] = useState<CheckedState>(false); - - useEffect(() => { - setChecked(checkAll); - }, [checkAll]); - - return ( - <label - key={user.id} - htmlFor={user.id} - className={cn( - 'relative grid grid-cols-subgrid col-span-12 row-span-1 h-10 items-center gap-[--p] cursor-pointer [&>span]:flex [&>span]:flex-row [&>span]:items-center [&>span]:gap-[--p]', - className - )} - {...props}> - <span className="col-span-2 leading-none"> - <Checkbox - id={user.id} - checked={checked} - onCheckedChange={check => setChecked(check)} - /> - - <input - className="!px-0 p-1 !m-0 text-base cursor-default bg-transparent rounded-md ring-offset-transparent file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none ring-0 ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm" - type="text" - value={user.username} - readOnly={!edit} - style={{ - backgroundColor: edit - ? 'hsl(var(--background))' - : 'transparent', - }} - onChange={e => - setUser(p => ({ ...p, username: e.target.value })) - } - /> - </span> - <span className=" col-span-3"> - {user.email} - <Copy text={user.email} className="h-1/2 p-0.5" /> - </span> - <span className="col-span-6 line-clamp-1"> - {user.password} - <Copy text={user.password} className="h-1/2 p-0.5" /> - </span> - <div className="flex flex-row items-center justify-end col-span-1 size-full gap-x-[--p]"> - <Button - onClick={() => setEdit(p => !p)} - className="border-input border !bg-background rounded-full aspect-square h-2/3 p-0"> - <BiSolidSave className="text-foreground" /> - </Button> - <Button - onClick={() => setEdit(p => !p)} - className="border-input border rounded-full aspect-square h-2/3 p-0"> - <RiEdit2Fill /> - </Button> - - <Button className="border-input border rounded-full aspect-square h-2/3 p-0 dark:bg-red-400 bg-red-600"> - <MdDelete /> - </Button> - </div> - <Separator className="absolute w-full bottom-0" /> - </label> - ); -}; diff --git a/_client/@inset/( users ).tsx b/_client/@inset/( users ).tsx deleted file mode 100644 index 8ae949f..0000000 --- a/_client/@inset/( users ).tsx +++ /dev/null @@ -1,84 +0,0 @@ -'use client'; - -import { ServerProps } from '@/app/@sidebar/users/page'; -import Context from '@/components/groups/context'; -import MembersPage from '@/components/members-page'; -import { Input } from '@/ui/input'; -import { - SidebarGroup, - SidebarGroupLabel, - SidebarMenu, - SidebarMenuItem, - SidebarMenuButton, - SidebarMenuSub, -} from '@/ui/sidebar'; -import { SectionIcon } from '@radix-ui/react-icons'; -import Link from 'next/link'; -import { useState } from 'react'; - -export const Sidebar = ({ api, roles }: ServerProps) => { - return ( - <SidebarGroup className="!p-[calc(var(--p)/2)]"> - <SidebarGroupLabel className="flex flex-row gap-x-2 w-full capitalize justify-between items-center"> - <span className="w-10/12 h-fit">Roles</span> - </SidebarGroupLabel> - <SidebarMenu> - {roles.map(role => ( - <Role key={role.id} {...role} /> - ))} - </SidebarMenu> - </SidebarGroup> - ); -}; - -export { Main } from './( users ) [ id ]'; - -const Role = ({ - id, - name, -}: { - children?: React.ReactNode; - id: string; - name: string; - onRename: (name: string) => void; -}) => { - const [rename, setRename] = useState<string | null>(null); - - return ( - <SidebarMenuItem className="w-full"> - <Context - actions={[ - { children: 'Rename', onClick: () => setRename(name) }, - { children: 'Delete', className: 'text-red-400' }, - ]}> - <SidebarMenuButton - asChild - tooltip={name} - className="group-data-[collapsible=icon]:!p-1.5 group-data-[collapsible=icon]:hover:[&>svg]:text-foreground" - onBlur={() => setRename(null)}> - {typeof rename === 'string' ? ( - <> - <SectionIcon className="group-data-[collapsible=icon]:size-5 text-zinc-500 group-data-[state=open]/collapsible:text-white" /> - <Input - className="h-fit w-full py-1 px-1.5" - autoFocus - value={rename} - onChange={e => - setRename(e.target.value) - } - onBlur={() => setRename(null)} - /> - </> - ) : ( - <Link href={`/users/${id}`}> - <SectionIcon className="group-data-[collapsible=icon]:size-5 text-zinc-500 group-data-[state=open]/collapsible:text-white" /> - <span className=" !line-clamp-1 leading-7"> - {name} - </span> - </Link> - )} - </SidebarMenuButton> - </Context> - </SidebarMenuItem> - ); -}; diff --git a/_client/@sidebar/( advisory ).tsx b/_client/@sidebar/( advisory ).tsx deleted file mode 100644 index 1ffee91..0000000 --- a/_client/@sidebar/( advisory ).tsx +++ /dev/null @@ -1,3 +0,0 @@ -'use client'; - -export default () => null; diff --git a/_client/@sidebar/( posts ) [ id ].tsx b/_client/@sidebar/( posts ) [ id ].tsx deleted file mode 100644 index cbbc7e2..0000000 --- a/_client/@sidebar/( posts ) [ id ].tsx +++ /dev/null @@ -1,3 +0,0 @@ -'use client'; - -export { Sidebar } from './( posts )'; diff --git a/_client/@sidebar/( posts ).tsx b/_client/@sidebar/( posts ).tsx deleted file mode 100644 index a97c66b..0000000 --- a/_client/@sidebar/( posts ).tsx +++ /dev/null @@ -1,174 +0,0 @@ -'use client'; - -import { useCallback, useState } from 'react'; -import { Plus } from 'lucide-react'; -import { CgSortAz } from 'react-icons/cg'; - -import Group, { EditableGroup } from '@/components/groups/group'; -import SubGroup from '@/components/groups/subGroup'; -import Item from '@/components/groups/item'; - -import type { ServerProps } from '@/app/@sidebar/[section]/page'; - -import { useParams } from 'next/navigation'; -import type { Group as GroupType } from '@/types/group'; -import { Post } from '@/types/post'; -import { useToast } from '@/hooks/use-toast'; - -export const Main = ({ text }: { text: string }) => ( - <div className="flex size-full "> - <h2 className="text-xl font-semibold m-auto">{text}</h2> - </div> -); - -export const Sidebar = ({ api, groups: initGroups, section }: ServerProps) => { - const params = useParams(); - const [postId] = [params.id].flat(); - const { toast } = useToast(); - - const [groups, setGroups] = useState<GroupType<Post>[]>(initGroups as any); - const [addGroup, setAddGroup] = useState<string | null>(null); - - return ( - <> - <div className="!p-[calc(var(--p)/2)] group-data-[collapsible=icon]:invisible"> - <div className="flex flex-row items-center justify-between w-full opacity-70 px-2"> - <p className="text-sm tracking-wider">Groups</p> - - <button - onClick={() => { - console.log('addGroup', addGroup); - setAddGroup(''); - }}> - <Plus className="size-4" /> - </button> - </div> - </div> - {typeof addGroup === 'string' && ( - <EditableGroup - toRename - onRename={async name => { - setAddGroup(null); - if (!name || !name.trim().length) return; - - await api.createGroup(name); - - setGroups(await api.getGroups()); - toast({ - title: '✨ New Group Created', - description: `"${name}" added to ${section}`, - duration: 3000, - }); - }} - /> - )} - {groups?.length > 0 && - groups.map(group => ( - <Group - key={group.id} - {...group} - onCreateSubGroup={async subGroupName => { - await api.createSubGroup({ - group, - name: subGroupName, - }); - - setGroups(await api.getGroups()); - toast({ - title: '📂 Subgroup Created', - description: `"${subGroupName}" in ${group.name}`, - duration: 3000, - }); - }} - onRename={async ({ id, name }) => { - await api.renameGroup({ id, name }); - setGroups(await api.getGroups()); - toast({ - title: '✏️ Group Renamed', - description: `Updated to "${name}"`, - duration: 2500, - }); - }} - onDelete={async id => { - await api.deleteGroup(id); - setGroups(await api.getGroups()); - toast({ - title: '🗑️ Group Deleted', - description: 'Group removed permanently', - duration: 2500, - }); - }}> - {group?.sub_groups?.length > 0 && - group.sub_groups.map(subGroup => ( - <SubGroup - key={subGroup.id} - defaultOpen={!!params.id && subGroup.items.some(item => item.id === postId)} - {...subGroup} - onDelete={async subGroupId => { - await api.deleteSubGroup(subGroupId); - setGroups(await api.getGroups()); - toast({ - title: '🗑️ Subgroup Deleted', - description: 'Subgroup removed permanently', - duration: 2500, - }); - }} - onRename={async sub_group => { - await api.renameSubGroup(sub_group); - setGroups(await api.getGroups()); - toast({ - title: '✏️ Subgroup Renamed', - description: `Updated to "${sub_group.name}"`, - duration: 2500, - }); - }} - onCreateNewPost={async name => { - await api.createPost({ - group, - sub_group: subGroup, - name, - }); - setGroups(await api.getGroups()); - toast({ - title: '📝 New Post Created', - description: `"${name}" ready for editing`, - duration: 3000, - }); - }}> - {subGroup?.items?.length > 0 && - subGroup.items.map(item => ( - <Item - key={item.id} - {...item} - section={section} - selected={item.id === postId} - onRename={async name => { - await api.renamePost({ - id: item.id, - name, - }); - setGroups(await api.getGroups()); - toast({ - title: '✏️ Post Renamed', - description: `Title updated to "${name}"`, - duration: 2500, - }); - }} - onDelete={async id => { - await api.deletePost(id); - setGroups(await api.getGroups()); - toast({ - title: '🗑️ Post Deleted', - description: 'Post removed permanently', - duration: 2500, - }); - }} - /> - ))} - </SubGroup> - ))} - </Group> - ))} - </> - ); -}; diff --git a/_client/@sidebar/( users ) [ id ].tsx b/_client/@sidebar/( users ) [ id ].tsx deleted file mode 100644 index 9ab4154..0000000 --- a/_client/@sidebar/( users ) [ id ].tsx +++ /dev/null @@ -1,133 +0,0 @@ -'use client'; - -import { cn } from '@/utils/shadcn'; - -import { MdDelete } from 'react-icons/md'; -import { RiEdit2Fill } from 'react-icons/ri'; -import { BiSolidSave } from 'react-icons/bi'; - -import { Checkbox } from '@/ui/checkbox'; -import { DetailedHTMLProps, HtmlHTMLAttributes, useEffect, useState } from 'react'; - -import Copy from '@/components/Copy'; -import { Button } from '@/ui/button'; -import { Separator } from '@/ui/separator'; - -import type { CheckedState } from '@radix-ui/react-checkbox'; -import type { ServerProps } from '@/app/@inset/users/page'; -import { Input } from '@/ui/input'; - -export { Sidebar } from './( users )'; - -export const Main = ({ api, users }: ServerProps) => { - const [checkAll, setCheckAll] = useState<CheckedState>(false); - - return ( - <div className="flex flex-col w-full h-full "> - <div className="relative flex flex-col w-full h-8 shrink-0 items-center"> - <label - key="all" - htmlFor="all" - className="flex flex-row items-start w-full gap-[--p]"> - <Checkbox - id="all" - onCheckedChange={check => setCheckAll(check)} - /> - <span className="leading-5 text-foreground/50 text-sm"> - Username - </span> - </label> - - <Separator className="absolute w-full bottom-0" /> - </div> - - <div className="grid grow grid-cols-12 col-span-full -row-end-1 size-full items-start gap-[--p]"> - {users.map(user => ( - <User - key={user.id} - user={user} - checkAll={checkAll} - className="col-span-full row-span-1 h-12" - /> - ))} - </div> - </div> - ); -}; - -const User = ({ - user: _user, - checkAll, - className, - ...props -}: { - user: ServerProps['users'][0]; - checkAll: CheckedState; -} & Partial<DetailedHTMLProps<HtmlHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>>) => { - const [user, setUser] = useState<ServerProps['users'][0]>(_user); - const [edit, setEdit] = useState(false); - const [checked, setChecked] = useState<CheckedState>(false); - - useEffect(() => { - setChecked(checkAll); - }, [checkAll]); - - return ( - <label - key={user.id} - htmlFor={user.id} - className={cn( - 'relative grid grid-cols-subgrid col-span-12 row-span-1 h-10 items-center gap-[--p] cursor-pointer [&>span]:flex [&>span]:flex-row [&>span]:items-center [&>span]:gap-[--p]', - className - )} - {...props}> - <span className="col-span-2 leading-none"> - <Checkbox - id={user.id} - checked={checked} - onCheckedChange={check => setChecked(check)} - /> - - <input - className="!px-0 p-1 !m-0 text-base cursor-default bg-transparent rounded-md ring-offset-transparent file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none ring-0 ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm" - type="text" - value={user.username} - readOnly={!edit} - style={{ - backgroundColor: edit - ? 'hsl(var(--background))' - : 'transparent', - }} - onChange={e => - setUser(p => ({ ...p, username: e.target.value })) - } - /> - </span> - <span className=" col-span-3"> - {user.email} - <Copy text={user.email} className="h-1/2 p-0.5" /> - </span> - <span className="col-span-6 line-clamp-1"> - {user.password} - <Copy text={user.password} className="h-1/2 p-0.5" /> - </span> - <div className="flex flex-row items-center justify-end col-span-1 size-full gap-x-[--p]"> - <Button - onClick={() => setEdit(p => !p)} - className="border-input border !bg-background rounded-full aspect-square h-2/3 p-0"> - <BiSolidSave className="text-foreground" /> - </Button> - <Button - onClick={() => setEdit(p => !p)} - className="border-input border rounded-full aspect-square h-2/3 p-0"> - <RiEdit2Fill /> - </Button> - - <Button className="border-input border rounded-full aspect-square h-2/3 p-0 dark:bg-red-400 bg-red-600"> - <MdDelete /> - </Button> - </div> - <Separator className="absolute w-full bottom-0" /> - </label> - ); -}; diff --git a/_client/@sidebar/( users ).tsx b/_client/@sidebar/( users ).tsx deleted file mode 100644 index 8ae949f..0000000 --- a/_client/@sidebar/( users ).tsx +++ /dev/null @@ -1,84 +0,0 @@ -'use client'; - -import { ServerProps } from '@/app/@sidebar/users/page'; -import Context from '@/components/groups/context'; -import MembersPage from '@/components/members-page'; -import { Input } from '@/ui/input'; -import { - SidebarGroup, - SidebarGroupLabel, - SidebarMenu, - SidebarMenuItem, - SidebarMenuButton, - SidebarMenuSub, -} from '@/ui/sidebar'; -import { SectionIcon } from '@radix-ui/react-icons'; -import Link from 'next/link'; -import { useState } from 'react'; - -export const Sidebar = ({ api, roles }: ServerProps) => { - return ( - <SidebarGroup className="!p-[calc(var(--p)/2)]"> - <SidebarGroupLabel className="flex flex-row gap-x-2 w-full capitalize justify-between items-center"> - <span className="w-10/12 h-fit">Roles</span> - </SidebarGroupLabel> - <SidebarMenu> - {roles.map(role => ( - <Role key={role.id} {...role} /> - ))} - </SidebarMenu> - </SidebarGroup> - ); -}; - -export { Main } from './( users ) [ id ]'; - -const Role = ({ - id, - name, -}: { - children?: React.ReactNode; - id: string; - name: string; - onRename: (name: string) => void; -}) => { - const [rename, setRename] = useState<string | null>(null); - - return ( - <SidebarMenuItem className="w-full"> - <Context - actions={[ - { children: 'Rename', onClick: () => setRename(name) }, - { children: 'Delete', className: 'text-red-400' }, - ]}> - <SidebarMenuButton - asChild - tooltip={name} - className="group-data-[collapsible=icon]:!p-1.5 group-data-[collapsible=icon]:hover:[&>svg]:text-foreground" - onBlur={() => setRename(null)}> - {typeof rename === 'string' ? ( - <> - <SectionIcon className="group-data-[collapsible=icon]:size-5 text-zinc-500 group-data-[state=open]/collapsible:text-white" /> - <Input - className="h-fit w-full py-1 px-1.5" - autoFocus - value={rename} - onChange={e => - setRename(e.target.value) - } - onBlur={() => setRename(null)} - /> - </> - ) : ( - <Link href={`/users/${id}`}> - <SectionIcon className="group-data-[collapsible=icon]:size-5 text-zinc-500 group-data-[state=open]/collapsible:text-white" /> - <span className=" !line-clamp-1 leading-7"> - {name} - </span> - </Link> - )} - </SidebarMenuButton> - </Context> - </SidebarMenuItem> - ); -}; diff --git a/actions/check-user.ts b/actions/check-user.ts deleted file mode 100644 index 166efa0..0000000 --- a/actions/check-user.ts +++ /dev/null @@ -1,11 +0,0 @@ -'use server'; - -export default async (username: string, secretToken: string) => { - const valid = !!username?.trim?.() && !!secretToken?.trim?.(); - - return { - success: valid ? 1 : 0, - data: { authorized: valid }, - message: valid ? 'Demo mode: access granted.' : 'Demo mode: missing credentials.', - }; -}; diff --git a/actions/firebase.ts b/actions/firebase.ts deleted file mode 100644 index 5b27658..0000000 --- a/actions/firebase.ts +++ /dev/null @@ -1,15 +0,0 @@ -const app = { mode: 'demo' }; -const auth = { mode: 'demo' }; - -async function signInWithGoogle() { - return { - user: { - uid: 'demo-user', - email: 'demo.user@example.com', - displayName: 'Demo User', - }, - credential: null, - }; -} - -export { app, auth, signInWithGoogle }; diff --git a/actions/indexed-db.ts b/actions/indexed-db.ts deleted file mode 100644 index 822f0b8..0000000 --- a/actions/indexed-db.ts +++ /dev/null @@ -1,94 +0,0 @@ -'use client'; - -export const openDatabase = (dbName: string, storeName: string, version = 1): Promise<IDBDatabase> => { - return new Promise((resolve, reject) => { - if (typeof window === 'undefined') { - reject('IndexedDB is not supported in server-side environments.'); - return; - } - - const request = indexedDB.open(dbName, version); - - request.onupgradeneeded = event => { - if (!event.target) return; - // @ts-ignore - const db = (event.target as IDBOpenDBRequest).result; - - // Creates the object store if it does not exist - if (!db.objectStoreNames.contains(storeName)) { - db.createObjectStore(storeName, { autoIncrement: true }); - } - }; - - request.onsuccess = event => { - if (!event.target) return; - resolve((event.target as IDBOpenDBRequest).result); - }; - - request.onerror = event => { - // @ts-ignore - reject(`Database open error: ${event.target.error}`); - }; - }); -}; - -export const putItem = async (dbName: string, storeName: string, key: string, item: any) => { - const db = await openDatabase(dbName, storeName); - - const transaction = db.transaction(storeName, 'readwrite'); - const store = transaction.objectStore(storeName); - - const request = store.put(item, key); - - request.onsuccess = event => { - if (!event.target) return; - return (event.target as IDBRequest).result; - }; - - request.onerror = event => { - // @ts-ignore - throw new Error(`Item write error: ${event.target.error}`); - }; -}; - -export const getItem = async (dbName: string, storeName: string, itemKey: string) => { - const db = await openDatabase(dbName, storeName); - - return await new Promise((resolve, reject) => { - const transaction = db.transaction(storeName, 'readonly'); - const store = transaction.objectStore(storeName); - - const request = store.get(itemKey); - - request.onsuccess = event => { - if (!event.target) return; - resolve((event.target as IDBRequest).result); - }; - - request.onerror = event => { - // @ts-ignore - reject(`Item read error: ${event.target.error}`); - }; - }); -}; - -export const deleteItem = async (dbName: string, storeName: string, itemKey: string) => { - const db = await openDatabase(dbName, storeName); - - return await new Promise((resolve, reject) => { - const transaction = db.transaction(storeName, 'readwrite'); - const store = transaction.objectStore(storeName); - - const request = store.delete(itemKey); - - request.onsuccess = event => { - if (!event.target) return; - resolve((event.target as IDBRequest).result); - }; - - request.onerror = event => { - // @ts-ignore - reject(`Item delete error: ${event.target.error}`); - }; - }); -}; diff --git a/actions/send-message.ts b/actions/send-message.ts deleted file mode 100644 index 0080490..0000000 --- a/actions/send-message.ts +++ /dev/null @@ -1,593 +0,0 @@ -'use server'; - -interface DemoMessage { - keywords: string[]; - responses: string[]; -} - -const demoResponses: DemoMessage[] = [ - { - keywords: ['content', 'create', 'post', 'write', 'article'], - responses: [ - `Creating compelling content is at the heart of a successful blog. Here are the key steps to get started: - -1. **Choose Your Topic**: Select a subject that resonates with your audience. Consider trending topics, user questions, or gaps in existing coverage related to your niche. - -2. **Research Thoroughly**: Gather credible sources, statistics, and expert insights. This adds authority to your content and provides value to readers. - -3. **Structure Your Outline**: Organize your thoughts into a logical flow. A strong outline typically includes: - - Engaging introduction (hook your reader) - - Clear sections with subheadings - - Relevant examples and case studies - - Actionable takeaways - - Compelling conclusion - -4. **Write Your Draft**: Focus on clarity over perfection. Use conversational language, break up long paragraphs, and incorporate relevant keywords naturally. - -5. **Optimize for SEO**: Include your primary keyword in the title, introduction, and headers. Use alt text for images and create descriptive slugs. - -6. **Review and Edit**: Proofread carefully, check links, and ensure your formatting is consistent. Consider different perspectives and refine your message. - -In our Autoblog CMS, you can structure posts within categories and subcategories, add featured images from your media library, and set publish states (Draft, Published, Scheduled). The system supports markdown formatting for rich text editing, making it easy to create polished content.`, - `Effective content creation starts with understanding your audience and their pain points. Here's a structured approach: - -**Pre-Writing Phase**: -- Define your target reader persona -- Identify the problem you're solving -- Research competitor content to find gaps - -**Writing Process**: -- Start with a compelling headline that promises value -- Use the inverted pyramid: most important info first -- Include visuals to break up text and improve comprehension -- Add internal links to related content -- Use numbered lists and bullet points for scannability - -**Post-Writing Optimization**: -- Fact-check all claims and citations -- Optimize images for web (compression, alt text) -- Add a clear call-to-action -- Create an engaging meta description -- Test readability across devices - -With our Autoblog CMS, you can manage multiple content sections simultaneously. Group related posts, maintain version control through draft status, and schedule publication dates. The typewriter effect in our AI assistant helps you see detailed guidance as it's composed, making complex topics easier to digest.`, - ], - }, - { - keywords: ['design', 'layout', 'template', 'customize', 'styling'], - responses: [ - `Designing a professional blog is crucial for both aesthetics and user experience. Here's how to approach your design strategy: - -**Visual Hierarchy**: -- Use consistent typography (typically 2-3 font families max) -- Create clear size distinctions between headings and body text -- Apply adequate whitespace to improve readability -- Establish a color palette of 3-5 complementary colors - -**Layout Best Practices**: -- Keep the main content column between 600-800px wide (optimal reading width) -- Place call-to-action buttons consistently (usually above the fold) -- Use responsive grids that adapt to mobile and tablet screens -- Ensure navigation is intuitive and accessible - -**Visual Elements**: -- Feature images with proper aspect ratios (16:9 for headers) -- Include author avatars and bylines for credibility -- Use icons to break up lengthy text sections -- Incorporate brand elements consistently - -**Performance Considerations**: -- Optimize all images for web (WebP format when possible) -- Use lazy loading for images below the fold -- Minimize CSS and JavaScript -- Implement a CDN for faster content delivery - -Our Autoblog CMS integrates with Tailwind CSS for rapid styling and Next.js Image optimization. You can upload custom images for each section and maintain visual consistency across your blog without touching code.`, - `Modern blog design combines aesthetics with functionality. Consider these design principles: - -**User Experience Design**: -- Create user journeys that guide readers to key content -- Implement breadcrumb navigation for clarity -- Use consistent button styles and hover states -- Add search functionality for content discovery - -**Mobile-First Approach**: -- Design for mobile first, then enhance for larger screens -- Ensure touch targets are at least 48x48px -- Test all interactive elements on touch devices -- Minimize redirects and loading times - -**Accessibility Standards**: -- Maintain sufficient color contrast (WCAG AA minimum) -- Use semantic HTML for screen reader compatibility -- Provide alt text for all images -- Ensure keyboard navigation works throughout - -**Branding Integration**: -- Display logo consistently in header and footer -- Use brand colors throughout the design -- Maintain a consistent tone in microcopy -- Include social media links prominently - -The Autoblog CMS provides a modular component library with pre-built sections like featured posts, category navigation, and related articles. You can customize colors, spacing, and layouts through the admin interface.`, - ], - }, - { - keywords: ['seo', 'optimize', 'ranking', 'traffic', 'visibility'], - responses: [ - `Search Engine Optimization is essential for blog visibility. Here's a comprehensive SEO strategy: - -**On-Page SEO**: -- Include target keyword in title (front-loaded ideally) -- Write compelling meta descriptions (155-160 characters) -- Use H1, H2, H3 headers logically to structure content -- Maintain keyword density around 1-2% naturally -- Include internal links to related articles with descriptive anchor text -- Optimize URL slugs to include keywords - -**Technical SEO**: -- Ensure fast page load times (target under 3 seconds) -- Implement SSL/HTTPS for security -- Create an XML sitemap and submit to search engines -- Use structured data markup (schema.org) for rich snippets -- Ensure mobile responsiveness and mobile-first indexing compatibility -- Fix broken links regularly - -**Content Strategy**: -- Create comprehensive, in-depth content (1500+ words typically performs better) -- Target long-tail keywords with less competition -- Update existing content regularly to maintain freshness -- Create content clusters with pillar pages and supporting articles -- Build topical authority by covering related subjects deeply - -**Link Building**: -- Earn backlinks from authoritative sites -- Use internal linking to distribute page authority -- Create linkable assets (guides, research, tools) -- Guest post on relevant industry blogs -- Monitor backlinks regularly - -The Autoblog CMS automatically supports URL optimization, image alt text management, and internal linking. Our advisory feature helps you maintain editorial standards for SEO best practices.`, - `Improving your blog's search engine ranking requires a multi-faceted approach: - -**Keyword Research & Strategy**: -- Use tools like Google Search Console to identify search queries -- Analyze competitors ranking for your target keywords -- Target keywords with commercial intent and decent search volume -- Create topic clusters around core themes -- Include semantic variations and LSI keywords naturally - -**Content Quality Signals**: -- Write original, authoritative content that demonstrates expertise -- Back claims with credible sources and citations -- Include multimedia content (images, videos, infographics) -- Update content regularly to maintain freshness signals -- Maintain consistent publishing schedule - -**User Experience Signals**: -- Keep bounce rate low through engaging introductions -- Improve dwell time with well-structured, readable content -- Use internal links strategically to keep users engaged -- Optimize for Core Web Vitals (LCP, FID, CLS) -- Ensure smooth navigation and intuitive site structure - -**Authority Building**: -- Develop E-E-A-T signals (Experience, Expertise, Authoritativeness, Trustworthiness) -- Create author bios with credentials and social links -- Engage with comments and community discussions -- Establish yourself as a thought leader through original research -- Build a network of quality backlinks in your niche - -Monitor your progress with Google Analytics and Google Search Console. The Autoblog CMS provides fields for optimizing every article's discoverability.`, - ], - }, - { - keywords: ['editorial', 'quality', 'standards', 'guidelines', 'brand'], - responses: [ - `Maintaining editorial standards is crucial for building a strong, trustworthy blog brand: - -**Editorial Guidelines**: -- Develop a comprehensive brand voice and tone guide -- Define your target audience and their preferences clearly -- Create writing standards (grammar, punctuation, style) -- Establish fact-checking and verification procedures -- Set clear approval workflows before publication - -**Content Quality Checklist**: -- ✓ Fact-check all claims and statistics -- ✓ Verify sources are credible and current -- ✓ Check for plagiarism using detection tools -- ✓ Ensure consistency with brand voice and style -- ✓ Proofread for grammar, spelling, and punctuation -- ✓ Verify all links are working -- ✓ Optimize images for web and accessibility -- ✓ Review for SEO best practices -- ✓ Ensure meta descriptions and titles are compelling - -**Consistency Standards**: -- Use consistent date formats and time zones -- Maintain uniform heading hierarchy -- Apply consistent formatting to lists and quotes -- Use template structures for recurring content types -- Keep image dimensions and aspect ratios consistent - -**Governance**: -- Assign editorial roles (writer, editor, reviewer) -- Document approval processes and timelines -- Maintain content calendar with deadlines -- Archive editorial decisions and guidelines -- Regular audits of published content for quality - -Our Autoblog CMS includes an editorial advisory section and content staging system, allowing you to maintain draft status, schedule publications, and manage multiple contributors. The sidebar organization helps maintain thematic consistency across your content library.`, - `A strong editorial strategy ensures your blog maintains credibility and professional standards: - -**Voice & Tone Guidelines**: -- Define primary and secondary tone attributes -- Create examples of correct and incorrect voice applications -- Establish guidelines for audience interaction -- Document how to handle corrections and updates -- Create templates for common content types - -**Content Standards**: -- Set minimum word counts for comprehensive coverage -- Define image requirements (resolution, formats, alt text) -- Establish fact-checking procedures -- Create citation standards and bibliography practices -- Document content freshness policies - -**Review Process**: -- Multiple-eye review before publication -- Subject matter expert validation for technical topics -- Legal review if covering potentially sensitive areas -- Performance tracking post-publication -- A/B testing of headlines and meta descriptions - -**Brand Protection**: -- Consistency guidelines across all sections -- Approved terminology and language standards -- Visual identity standards for images and graphics -- Attribution policies for sourced content -- Crisis communication procedures - -**Author Development**: -- Onboarding materials for new writers -- Style guide with examples -- Feedback mechanisms for continuous improvement -- Regular training on company standards -- Performance analytics and optimization feedback - -The Autoblog CMS allows you to manage different content sections (Featured, Design, Culture, Insights, Resources) with their own editorial standards, tracked through our demo data structure.`, - ], - }, - { - keywords: ['analytics', 'metrics', 'performance', 'data', 'insights'], - responses: [ - `Understanding your blog's performance is key to continuous improvement: - -**Key Metrics to Track**: -- **Traffic Metrics**: Page views, unique visitors, traffic sources -- **Engagement Metrics**: Average session duration, bounce rate, pages per session -- **Conversion Metrics**: Click-through rates, goal completions, conversion funnels -- **Search Metrics**: Keywords, rankings, click position, impressions -- **Content Metrics**: Top performing articles, scroll depth, time on page - -**Tools for Analysis**: -- Google Analytics 4 for comprehensive web analytics -- Google Search Console for search performance tracking -- Heatmapping tools (Hotjar, Microsoft Clarity) for user behavior -- A/B testing platforms for headline and layout optimization -- Social media analytics for content distribution - -**Dashboard Creation**: -- Set up custom dashboards for different stakeholder needs -- Monitor KPIs that align with business objectives -- Create automated reports for regular review -- Set up alerts for significant changes (positive or negative) -- Compare metrics across time periods and segments - -**Optimization Based on Data**: -- Identify top-performing content themes -- Analyze underperforming content for improvements -- Test variations in headlines, images, and CTAs -- Optimize publish times based on audience behavior -- Refine content promotion strategies - -**Advanced Analysis**: -- Cohort analysis to understand user behavior patterns -- Attribution modeling to understand conversion paths -- Competitor benchmarking to contextualize performance -- Predictive analytics for forecasting trends -- Audience segmentation for personalized content - -Implement consistent tracking across your Autoblog CMS to understand which sections, categories, and individual articles drive the most value for your business.`, - `Data-driven content strategy maximizes your blog's impact: - -**Baseline Metrics**: -- Establish baseline metrics for all key indicators -- Document current state before implementing changes -- Set realistic improvement targets -- Create a measurement timeline -- Define success criteria clearly - -**Traffic Analysis**: -- Identify which traffic sources convert best -- Analyze user journey from first click to conversion -- Track referral sources and affiliate performance -- Monitor brand vs. non-brand search traffic -- Analyze geographic and demographic distribution - -**Content Performance**: -- Calculate revenue per article (if monetized) -- Identify content with highest engagement -- Track content decay over time (freshness impact) -- Analyze topic clustering performance -- Measure impact of internal linking strategies - -**User Behavior Insights**: -- Map user journeys through your content -- Identify decision points where users drop off -- Analyze scroll depth to understand engagement -- Track form submission patterns -- Monitor search behavior on-site - -**Business Impact**: -- Connect content metrics to business outcomes -- Calculate customer acquisition cost by channel -- Track lifetime value of organic traffic -- Measure brand awareness metrics -- Quantify thought leadership impact - -Use these insights to continuously evolve your Autoblog CMS strategy, focusing resources on content types and topics that drive measurable business value.`, - ], - }, - { - keywords: ['publish', 'schedule', 'launch', 'deploy', 'release'], - responses: [ - `Publishing your content strategically maximizes its impact: - -**Pre-Publication Checklist**: -- ✓ Final proofreading and fact-checking -- ✓ Optimize meta description and title tag -- ✓ Confirm featured image is selected and optimized -- ✓ Review all internal links are working -- ✓ Verify formatting on mobile devices -- ✓ Check social media preview appearances -- ✓ Assign appropriate categories and tags -- ✓ Schedule any social media promotion - -**Publication Timing Strategy**: -- Publish when your audience is most active -- Avoid publishing during major news cycles (unless relevant) -- Space content to maintain consistent blog activity -- Consider email list engagement timing -- Analyze historical data for optimal publish times - -**Social Media Promotion**: -- Create multiple social media variations -- Pull important quotes for graphics -- Create teaser content driving to full article -- Engage with comments and shares actively -- Schedule follow-up posts for evergreen content - -**Email Strategy**: -- Include article in newsletter campaigns -- Segment list for targeted promotion -- Create standalone email series for pillar content -- Encourage subscriptions throughout article -- Track email-driven traffic separately - -**Post-Publication**: -- Monitor comments and respond promptly -- Track initial traffic and performance metrics -- Share with relevant communities and networks -- Update internal links across the site -- Monitor for any technical issues -- Plan refresh schedule for ongoing updates - -The Autoblog CMS allows you to set publish status (Draft/Published), schedule content, and manage multiple sections with coordinated releases.`, - `Effective publishing workflow ensures consistent, high-quality content delivery: - -**Content Calendar Management**: -- Plan content 2-3 months in advance -- Balance evergreen and timely content -- Distribute content types across publishing schedule -- Account for seasonal trends and events -- Build flexibility for trending topics - -**Workflow Automation**: -- Set up automated reminders for upcoming deadlines -- Create templates for different content types -- Automate social media sharing -- Set up distribution to multiple channels -- Configure email notifications for approvals - -**Version Control**: -- Track revisions and editorial changes -- Document approval workflows -- Maintain changelog of content updates -- Archive previous versions for reference -- Track by-line attributions - -**Multi-Channel Distribution**: -- Repurpose content for different platforms -- Adapt format for LinkedIn, Twitter, Medium, etc. -- Create downloadable resources (PDFs, checklists) -- Distribute via email, RSS, social media -- Track performance across each channel - -**Launch Coordination**: -- Coordinate with marketing and PR teams -- Plan influencer outreach and promotion -- Schedule paid promotion if applicable -- Time publications to maximize visibility -- Prepare crisis communication plans if needed - -Our Autoblog CMS supports scheduling, draft management, and cross-section organization for coordinated content launches.`, - ], - }, - { - keywords: ['help', 'assist', 'advice', 'recommend', 'suggestion'], - responses: [ - `I'm here to help you maximize your Autoblog CMS! Here's what I can assist with: - -**Content Creation Support**: -- Help you brainstorm article ideas and topics -- Provide writing templates and structures -- Suggest content optimization strategies -- Recommend relevant keywords and topics -- Guide you through editorial best practices - -**CMS Navigation & Features**: -- Explain how to use different CMS sections -- Help organize content into categories and subcategories -- Guide you through publishing workflows -- Assist with media library management -- Support scheduling and planning features - -**Strategy & Planning**: -- Help develop your content calendar -- Suggest audience-focused topics -- Recommend content distribution strategies -- Guide SEO optimization efforts -- Support analytics interpretation - -**Quality & Standards**: -- Review content against editorial guidelines -- Suggest improvements for clarity and engagement -- Help with tone and voice consistency -- Provide feedback on structure and flow -- Recommend fact-checking sources - -How can I best support your blogging goals today? Feel free to ask about writing techniques, CMS features, SEO strategies, content planning, or anything else related to your Autoblog CMS platform.`, - `I'm your Autoblog CMS assistant, ready to help with: - -**Writing & Content Strategy**: -- Develop compelling article outlines -- Improve existing content for better performance -- Suggest engaging headlines and meta descriptions -- Create content calendars and posting schedules -- Research trending topics in your niche - -**Technical Guidance**: -- Navigate CMS features and sections -- Organize content effectively -- Manage media and images -- Optimize for search engines -- Troubleshoot common issues - -**Editorial Excellence**: -- Maintain quality and consistency -- Develop brand voice guidelines -- Create editorial standards -- Improve readability and engagement -- Ensure factual accuracy - -**Growth & Analytics**: -- Interpret performance metrics -- Identify top-performing content -- Spot opportunities for improvement -- Develop audience engagement strategies -- Plan content iterations based on data - -Whether you're a new blogger or experienced content creator, I'm here to provide practical, actionable guidance. What would you like to focus on?`, - ], - }, - { - keywords: ['ai', 'assistant', 'gemini', 'intelligence', 'feature'], - responses: [ - `The AI Assistant in your Autoblog CMS is designed to enhance your content creation workflow intelligently: - -**Intelligent Features**: -- **Smart Ideation**: Generate content ideas based on audience interests and trends -- **Content Analysis**: Analyze your existing content for performance patterns -- **SEO Guidance**: Provide keyword suggestions and optimization recommendations -- **Writing Enhancement**: Suggest improvements for clarity, engagement, and tone -- **Structure Support**: Help organize thoughts into compelling narratives - -**How This Integration Works**: -The AI assistant learns from your content patterns and audience behavior. Each question you ask helps refine future recommendations. The typewriter effect you see simulates real-time thinking, making complex guidance easier to absorb incrementally rather than all at once. - -**Supported Use Cases**: -- Brainstorming article topics and angles -- Outlining comprehensive guides and tutorials -- Optimizing headlines for click-through -- Improving content readability and flow -- Developing content strategy and editorial calendars -- Analyzing performance metrics and insights -- Creating consistent brand voice and tone - -**Learning & Improvement**: -The more you interact with the AI assistant, the better it understands your: -- Writing style and preferences -- Audience and niche focus -- Content performance patterns -- Brand voice and values -- Content creation goals - -This creates a personalized assistant that improves recommendations over time.`, - `Your Autoblog CMS AI Assistant represents the future of blogging technology: - -**Why AI Integration Matters**: -- **Speed**: Generate quality content faster with intelligent suggestions -- **Quality**: Improve content quality through evidence-based recommendations -- **Consistency**: Maintain brand voice across all content automatically -- **Scale**: Handle larger content volumes without proportional effort increase -- **Learning**: System improves as it learns your preferences and patterns - -**Deep Integration Benefits**: -- Every section of your CMS has AI insights available -- Recommendations adapt based on your publish history -- Performance analytics inform future suggestions -- Content calendar integration for strategic planning -- Real-time optimization tips as you write - -**Building Better Content Together**: -The AI assistant doesn't replace human creativity—it amplifies it. Think of it as: -- A brainstorming partner for ideas and angles -- An editor suggesting improvements -- A researcher providing data and insights -- A strategist analyzing performance patterns -- A guide on best practices and techniques - -**Privacy & Control**: -All interactions are session-based and don't permanently modify your content without approval. You maintain complete control over what gets published. The AI serves as advisor, not decision-maker. - -This integration transforms your blogging from manual work into a collaborative, intelligent process designed to maximize impact and efficiency.`, - ], - }, -]; - -export default async (_model: string, _history: any[], message: string) => { - const normalized = message?.trim?.() ?? ''; - - if (!normalized.length) { - return 'Demo mode: no input received.'; - } - - // Find matching response category - for (const category of demoResponses) { - for (const keyword of category.keywords) { - if (normalized.toLowerCase().includes(keyword)) { - // Pick a random response from the category - const responses = category.responses; - const randomIndex = Math.floor(Math.random() * responses.length); - return responses[randomIndex]; - } - } - } - - // Default response if no keywords match - return `I appreciate your question: "${normalized}" - -In a production environment, I would provide a detailed, intelligent response powered by Gemini AI. The Autoblog CMS integrates with Google's generative AI to provide: - -- **Contextual Writing Assistance**: Tailored suggestions based on your type of blog -- **Content Strategy Guidance**: Data-driven recommendations for better performance -- **Editorial Review**: Quality checks for tone, clarity, and completeness -- **SEO Optimization**: Keyword suggestions and ranking optimization tips -- **Performance Analysis**: Insights on what's working and what needs improvement - -Feel free to ask about content creation, blog strategy, CMS features, SEO optimization, editorial standards, analytics insights, publishing workflows, or anything else related to managing and growing your blog. - -In demo mode, I'm providing representative responses to showcase these capabilities. With real Gemini integration, I would understand context, follow conversation history, and provide increasingly personalized guidance based on your unique situation.`; -}; diff --git a/app/(workspace)/workspace/[workspaceId]/page.tsx b/app/(workspace)/workspace/[workspaceId]/page.tsx new file mode 100644 index 0000000..6a5bd9a --- /dev/null +++ b/app/(workspace)/workspace/[workspaceId]/page.tsx @@ -0,0 +1,24 @@ +import { headers } from 'next/headers'; +import { redirect } from 'next/navigation'; + +import { getEditorialService } from '@/src/modules/editorial/service'; +import { requireMembership } from '@/src/platform/auth/session'; +import { AppError } from '@/src/platform/observability/errors'; +import { WorkspaceEditor } from '@/src/ui/patterns/workspace-editor'; + +export const dynamic = 'force-dynamic'; + +export default async function WorkspacePage({ params }: Readonly<{ params: Promise<{ workspaceId: string }> }>) { + const { workspaceId } = await params; + let membership: Awaited<ReturnType<typeof requireMembership>>; + try { + membership = await requireMembership(await headers(), workspaceId); + } catch (error) { + if (error instanceof AppError && error.code === 'UNAUTHENTICATED') redirect('/sign-in'); + throw error; + } + const service = getEditorialService(); + const posts = await service.list(membership); + const selected = posts[0] ? await service.get(membership, posts[0].id) : null; + return <WorkspaceEditor context={membership} initialPosts={posts} initialPost={selected} aiMode={process.env.AI_MODE ?? 'mock'} />; +} diff --git a/app/@inset/[section]/[id]/loading.tsx b/app/@inset/[section]/[id]/loading.tsx deleted file mode 100644 index 49ac50e..0000000 --- a/app/@inset/[section]/[id]/loading.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { Skeleton } from '@/ui/skeleton'; - -export default function Loading() { - return ( - <div className="relative flex flex-col lg:grid lg:grid-cols-12 lg:grid-rows-[repeat(12,minmax(2.5rem,auto))] gap-[--p] size-full overflow-scroll"> - {/* Left panel - Image & metadata */} - <div className="grid grid-rows-[1.3rem_auto] grid-cols-1 -col-end-1 col-span-5 row-span-full w-full gap-[--p]"> - {/* Header */} - <div className="flex flex-row justify-between items-center row-span-1 size-full"> - <Skeleton className="h-6 w-32" /> - <Skeleton className="h-8 w-20" /> - </div> - - {/* Image preview */} - <Skeleton className="w-full aspect-video rounded-lg" /> - - {/* Image upload area */} - <div className="flex flex-col gap-[--p]"> - <Skeleton className="h-4 w-24" /> - <Skeleton className="w-full h-32 rounded-lg" /> - </div> - - {/* Metadata fields */} - <div className="flex flex-col gap-3"> - <div> - <Skeleton className="h-4 w-20 mb-2" /> - <Skeleton className="w-full h-8" /> - </div> - <div> - <Skeleton className="h-4 w-20 mb-2" /> - <Skeleton className="w-full h-8" /> - </div> - </div> - </div> - - {/* Right panel - Content tabs */} - <div className="flex flex-col col-span-7 row-span-full w-full gap-[--p]"> - {/* Tab buttons */} - <div className="flex flex-row gap-2"> - <Skeleton className="h-8 w-24" /> - <Skeleton className="h-8 w-24" /> - </div> - - {/* Tab content - Editor area */} - <div className="flex-1 flex flex-col gap-[--p] overflow-hidden"> - {/* Title field */} - <div> - <Skeleton className="h-4 w-16 mb-2" /> - <Skeleton className="w-full h-10" /> - </div> - - {/* Description field */} - <div> - <Skeleton className="h-4 w-24 mb-2" /> - <Skeleton className="w-full h-32" /> - </div> - - {/* Additional fields */} - <div className="grid grid-cols-2 gap-[--p]"> - <div> - <Skeleton className="h-4 w-16 mb-2" /> - <Skeleton className="w-full h-8" /> - </div> - <div> - <Skeleton className="h-4 w-16 mb-2" /> - <Skeleton className="w-full h-8" /> - </div> - </div> - - {/* Rich text editor */} - <div className="flex-1 min-h-[200px]"> - <Skeleton className="h-4 w-20 mb-2" /> - <Skeleton className="w-full h-full rounded-lg" /> - </div> - </div> - - {/* Action buttons */} - <div className="flex flex-row gap-2"> - <Skeleton className="h-10 w-24" /> - <Skeleton className="h-10 w-24" /> - </div> - </div> - </div> - ); -} diff --git a/app/@inset/[section]/[id]/page.tsx b/app/@inset/[section]/[id]/page.tsx deleted file mode 100644 index fe23bf2..0000000 --- a/app/@inset/[section]/[id]/page.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import Client from '@/_client/@inset/( posts ) [ id ]'; -import apiFetch, { ApiRes } from '@/utils/api-fetch'; - -import revalidate from '@/utils/revalidate'; - -import type { Post } from '@/types/post'; - -type Params = { section: string; id: string }; - -export default async function Page({ params }: { params: Promise<Params> }) { - const { id, section } = await params; - const api = generateApi({ id, section }); - - const props: ServerProps = { - section, - post: await api.getPost(), - api, - }; - - return <Client {...props} />; -} - -export const generateApi = ({ id, section }: Params) => ({ - getPost: async () => { - 'use server'; - - const { success, ...rest }: ApiRes<Post> = await apiFetch(`/inset/${section}/${id}`, { - method: 'GET', - }); - - if (!success) throw new Error(rest.message); - - return rest.data; - }, - updateImage: async (image: { - _id: string; - base64: string; - display_name: string; - public_id?: string; - }) => { - 'use server'; - image._id ??= id; - - const { success, ...rest } = await apiFetch(`/inset/${section}/${id}/image`, { - method: 'PATCH', - body: image, - }); - - if (!success) throw new Error(rest.message); - - await revalidate({ kind: 'tags', tags: [section, id] }); - await revalidate({ kind: 'paths', paths: [`/${section}`, `/${section}/${id}`] }); - - return rest.data; - }, - updatePost: async (post: Partial<Post>) => { - 'use server'; - - const { success, ...rest } = await apiFetch(`/inset/${section}/${post._id}/post`, { - method: 'PATCH', - body: post, - }); - - if (!success) throw new Error(rest.message); - - await revalidate({ kind: 'tags', tags: [section, id] }); - await revalidate({ kind: 'paths', paths: [`/${section}`, `/${section}/${id}`] }); - - return rest.data; - }, -}); - -export interface ServerProps { - post: Post; - section: string; - api: ReturnType<typeof generateApi>; -} diff --git a/app/@inset/[section]/loading.tsx b/app/@inset/[section]/loading.tsx deleted file mode 100644 index 6e438a7..0000000 --- a/app/@inset/[section]/loading.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Skeleton } from '@/ui/skeleton'; - -export default function Loading() { - return ( - <div className="relative flex flex-col size-full gap-[--p] overflow-y-scroll p-[--p]"> - {/* Header section */} - <div className="flex flex-row items-center justify-between w-full shrink-0"> - <Skeleton className="h-7 w-40" /> - <Skeleton className="h-8 w-8 rounded-full" /> - </div> - - {/* Group skeletons */} - <div className="flex flex-col gap-[--p]"> - {[...Array(3)].map((_, groupIdx) => ( - <div key={groupIdx} className="flex flex-col gap-2 border-b pb-4"> - {/* Group header */} - <div className="flex flex-row items-center justify-between px-2"> - <Skeleton className="h-5 w-32" /> - <Skeleton className="h-6 w-6 rounded" /> - </div> - - {/* Subgroups */} - <div className="flex flex-col gap-2 ml-4"> - {[...Array(2)].map((_, sgIdx) => ( - <div key={sgIdx} className="flex flex-col gap-1"> - {/* Subgroup header */} - <div className="flex flex-row items-center justify-between px-2"> - <Skeleton className="h-4 w-24" /> - <Skeleton className="h-5 w-5 rounded" /> - </div> - - {/* Posts */} - <div className="flex flex-col gap-1 ml-3"> - {[...Array(2)].map((_, pIdx) => ( - <Skeleton key={pIdx} className="h-8 w-full rounded px-2" /> - ))} - </div> - </div> - ))} - </div> - </div> - ))} - </div> - </div> - ); -} diff --git a/app/@inset/[section]/page.tsx b/app/@inset/[section]/page.tsx deleted file mode 100644 index 52872a6..0000000 --- a/app/@inset/[section]/page.tsx +++ /dev/null @@ -1,10 +0,0 @@ -export default async function Page({ params }: { params: Promise<{ section: string }> }) { - const { section } = await params; - return ( - <div className="flex size-full "> - <h2 className="text-xl font-semibold m-auto"> - Select the {section} you want to edit - </h2> - </div> - ); -} diff --git a/app/@inset/_components/chat.tsx b/app/@inset/_components/chat.tsx deleted file mode 100644 index d24b3c9..0000000 --- a/app/@inset/_components/chat.tsx +++ /dev/null @@ -1,151 +0,0 @@ -'use client'; - -import { Button } from '@/ui/button'; - -import { Dispatch, SetStateAction, useEffect, useState } from 'react'; - -import { ReceiverMessage, SenderMessage } from '../_ui/message'; -import { FiArrowUp } from 'react-icons/fi'; - -import type { Content } from '@google/generative-ai'; - -export default ({ onSend }: { onSend: (history: Content[]) => Promise<string> }) => { - const [input, setInput] = useState(''); - const [writing, setWriting] = useState(false); - const [timeout, setTimeoutState] = useState<NodeJS.Timeout>(); - const [history, setHistory] = useState([]); - - const addMessage = (role: 'user' | 'model', text: string) => { - if (role === 'user') setInput(''); - setHistory(p => { - const last = p[p.length - 1]; - console.log('last => ', p, history); - return last?.role !== role ? [...p, { role, parts: [{ text }] }] : [...p.slice(0, -1), { role, parts: [...last.parts, { text }] }]; - }); - }; - - const updatedState: <T>(setState: Dispatch<SetStateAction<T>>) => Promise<T> = async setState => { - return new Promise(r => { - setState(p => { - r(p); - return p; - }); - }); - }; - - useEffect(() => { - const history = JSON.parse(sessionStorage.getItem('history')); - if (history) setHistory(history); - }, []); - - useEffect(() => { - if (!history || !history.length) return; - sessionStorage.setItem('history', JSON.stringify(history)); - }, [history]); - - return ( - <div className="relative grow shrink flex flex-col items-center justify-start overflow-hidden"> - <div className="flex flex-col size-full justify-start items-center gap-[--p] pb-60 overflow-x-hidden overflow-y-scroll"> - {history.length === 0 && ( - <div className="flex flex-col items-center justify-center h-full w-full gap-[--p] px-[--p] py-[--p]"> - <div className="flex justify-center items-center size-12 rounded-full bg-foreground/5 mb-2"> - <span className="text-2xl">✨</span> - </div> - <h3 className="text-lg font-semibold text-foreground">Autoblog CMS AI Assistant</h3> - <p className="text-sm text-foreground/60 text-center max-w-sm"> - Get intelligent guidance on content creation, SEO strategy, editorial standards, and blog growth. - </p> - <div className="flex flex-col gap-2 mt-4 w-full max-w-sm"> - <p className="text-xs font-semibold text-foreground/50 uppercase">Try asking about:</p> - <div className="flex flex-col gap-2"> - <button - onClick={() => setInput('How do I create compelling blog content?')} - className="text-left px-3 py-2 rounded-md bg-foreground/5 hover:bg-foreground/10 transition-colors text-sm text-foreground/70 hover:text-foreground"> - How do I create compelling blog content? - </button> - <button - onClick={() => setInput('What are SEO best practices for my blog?')} - className="text-left px-3 py-2 rounded-md bg-foreground/5 hover:bg-foreground/10 transition-colors text-sm text-foreground/70 hover:text-foreground"> - What are SEO best practices for my blog? - </button> - <button - onClick={() => setInput('How to maintain editorial quality standards?')} - className="text-left px-3 py-2 rounded-md bg-foreground/5 hover:bg-foreground/10 transition-colors text-sm text-foreground/70 hover:text-foreground"> - How to maintain editorial quality standards? - </button> - <button - onClick={() => setInput('How can I track blog performance metrics?')} - className="text-left px-3 py-2 rounded-md bg-foreground/5 hover:bg-foreground/10 transition-colors text-sm text-foreground/70 hover:text-foreground"> - How can I track blog performance metrics? - </button> - </div> - </div> - </div> - )} - {history.map(({ role, parts }, index) => - parts.map(({ text: message }, i) => { - return role === 'user' ? - <SenderMessage key={role + index + i}>{message}</SenderMessage> - : <ReceiverMessage key={role + index + i}>{message}</ReceiverMessage>; - }), - )} - </div> - <form - onSubmit={e => e.preventDefault()} - className="absolute flex flex-col h-fit max-h-40 w-full bottom-0 inset-x-auto bg-sidebar-accent !text-foreground rounded-md"> - <textarea - className="relative grow shrink size-full px-[--p] pt-[--p] bg-transparent resize-none ring-offset-transparent file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none ring-0 ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm" - placeholder="Chat with our AI ..." - spellCheck={false} - value={input} - onChange={e => { - setInput(e.target.value); - setWriting(true); - clearTimeout(timeout); - setTimeoutState(setTimeout(() => setWriting(false), 2000)); - }} - onKeyDown={async e => { - if (e.key !== 'Enter' || e.shiftKey) return; - if (!input || !input.trim().length) return; - - e.preventDefault(); - addMessage('user', input); - - await new Promise(r => setTimeout(r, 2500)); - - const updatedWriting = await updatedState(setWriting); - - if (updatedWriting) return; - const updatedMessages = await updatedState(setHistory); - - const response = await onSend(updatedMessages); - addMessage('model', response); - }} - /> - <div className="p-[--p]"> - <div className="flex flex-row items-center justify-end h-7"> - <Button - type="submit" - className="flex flex-row shrink-0 h-full text-background text-sm font-bold rounded-full aspect-square" - onClick={async () => { - if (!input || !input.trim().length) return; - - addMessage('user', input); - - await new Promise(r => setTimeout(r, 2500)); - - const updatedWriting = await updatedState(setWriting); - if (updatedWriting) return; - const updatedMessages = await updatedState(setHistory); - - const response = await onSend(updatedMessages); - addMessage('model', response); - }}> - <FiArrowUp className="size-5" /> - </Button> - </div> - </div> - </form> - </div> - ); -}; diff --git a/app/@inset/_ui/message.tsx b/app/@inset/_ui/message.tsx deleted file mode 100644 index 0d98646..0000000 --- a/app/@inset/_ui/message.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useTypewriter } from '@/hooks/use-typewriter'; -import Image from 'next/image'; -import Markdown from 'react-markdown'; - -export const SenderMessage = ({ children }: { children: string }) => { - return ( - <Markdown className="self-end ml-auto size-fit max-w-[75%] bg-foreground rounded-md px-[--p] py-1.5 text-background">{children}</Markdown> - ); -}; - -export const ReceiverMessage = ({ children }: { children: string }) => { - const text = useTypewriter(children, 0, 2); - const isTyping = text.length < children.length; - - return ( - <div className="relative flex flex-row items-start justify-start size-fit max-w-[75%] gap-[--p] self-start mr-auto"> - <div className="relative flex justify-center items-center size-6 rounded-full shrink-0"> - <Image src="/ai.svg" alt="ai" fill className="drop-shadow-[0_0_3px_hsl(var(--foreground)/0.5)] p-0.5" /> - </div> - - <div className="flex flex-col gap-2 w-full"> - <Markdown - components={{ - ul: ({ node, ...props }) => <ul className="list-disc ml-[--p] my-2" {...props} />, - li: ({ node, ...props }) => <li className="ml-[--p]" {...props} />, - }} - className="self-start mr-auto size-fit bg-transparent text-foreground [&_*]:!select-text"> - {text} - </Markdown> - {isTyping && ( - <div className="flex items-center gap-1 mt-1"> - <div className="w-1.5 h-1.5 bg-green-500/60 rounded-full animate-pulse"></div> - <div className="w-1.5 h-1.5 bg-green-500/60 rounded-full animate-pulse"></div> - <div className="w-1.5 h-1.5 bg-green-500/60 rounded-full animate-pulse"></div> - </div> - )} - </div> - </div> - ); -}; diff --git a/app/@inset/advisory/loading.tsx b/app/@inset/advisory/loading.tsx deleted file mode 100644 index 594313d..0000000 --- a/app/@inset/advisory/loading.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Skeleton } from '@/ui/skeleton'; - -export default function Loading() { - return ( - <div className="relative flex flex-col lg:grid lg:grid-cols-12 lg:grid-rows-[repeat(12,minmax(2.5rem,auto))] gap-[--p] size-full overflow-scroll"> - {/* Left panel - Image */} - <div className="grid grid-rows-[1.3rem_auto] grid-cols-1 -col-end-1 col-span-5 row-span-full w-full gap-[--p]"> - {/* Header */} - <div className="flex flex-row justify-between items-center row-span-1 size-full"> - <Skeleton className="h-6 w-48" /> - <Skeleton className="h-8 w-20" /> - </div> - - {/* Image preview */} - <Skeleton className="w-full aspect-video rounded-lg" /> - - {/* Upload area */} - <div className="flex flex-col gap-[--p]"> - <Skeleton className="h-4 w-32" /> - <Skeleton className="w-full h-32 rounded-lg" /> - </div> - </div> - - {/* Right panel - Content */} - <div className="flex flex-col col-span-7 row-span-full w-full gap-[--p]"> - {/* About section */} - <div className="flex flex-col gap-2"> - <Skeleton className="h-5 w-24" /> - <Skeleton className="w-full h-24" /> - </div> - - {/* Services section */} - <div className="flex flex-col gap-2"> - <Skeleton className="h-5 w-32" /> - <Skeleton className="w-full h-32" /> - </div> - - {/* Submit button */} - <Skeleton className="h-10 w-28 mt-auto" /> - </div> - </div> - ); -} diff --git a/app/@inset/advisory/page.tsx b/app/@inset/advisory/page.tsx deleted file mode 100644 index b257284..0000000 --- a/app/@inset/advisory/page.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import Client from '@/_client/@inset/( advisory )'; -import revalidate from '@/utils/revalidate'; -import apiFetch from '@/utils/api-fetch'; - -import type { ApiRes } from '@/utils/api-fetch'; - -export const dynamic = 'force-dynamic'; - -export default async function Page() { - const data = await api.getAdvisory(); - - const props: ServerProps = { data, api }; - - return <Client {...props} />; -} - -export const api = { - getAdvisory: async () => { - 'use server'; - - const { success, ...rest }: ApiRes<ServerProps['data']> = await apiFetch( - '/inset/advisory', - { - method: 'GET', - } - ); - if (!success) throw new Error(rest.message); - - return rest.data; - }, - updateAdvisory: async (body: Omit<Pick<ServerProps, 'data'>, 'image'>) => { - 'use server'; - - const { success, ...rest } = await apiFetch('/inset/advisory', { - method: 'PATCH', - body, - }); - - if (!success) throw new Error(rest.message); - - await revalidate({ kind: 'tags', tags: ['advisory'] }); - await revalidate({ kind: 'paths', paths: ['/advisory'] }); - - return rest.data; - }, - uploadImage: async (image: { base64: string; public_id?: string; display_name?: string }) => { - 'use server'; - - const { success, ...rest } = await apiFetch('/inset/advisory/image', { - method: 'PATCH', - body: image, - }); - - if (!success) throw new Error(rest.message); - - await revalidate({ kind: 'tags', tags: ['advisory'] }); - await revalidate({ kind: 'paths', paths: ['/advisory'] }); - - return rest.data; - }, -}; - -export type ServerProps = { - api: typeof api; - data: { - id: string; - about: string; - services: string; - image: { public_id: string; display_name: string }; - }; -}; diff --git a/app/@inset/default.tsx b/app/@inset/default.tsx deleted file mode 100644 index 461f67a..0000000 --- a/app/@inset/default.tsx +++ /dev/null @@ -1 +0,0 @@ -export default () => null; diff --git a/app/@inset/error.tsx b/app/@inset/error.tsx deleted file mode 100644 index 8a18f17..0000000 --- a/app/@inset/error.tsx +++ /dev/null @@ -1,18 +0,0 @@ -'use client'; - -import { Button } from '@/ui/button'; - -export default ({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) => { - return ( - <div className="mx-auto text-center text-[clamp(1rem,1.5vw,1.5rem)] font-raleway"> - <h2>Something went wrong!</h2> - <Button - onClick={ - // Attempt to recover by trying to re-render the segment - () => reset() - }> - Try again - </Button> - </div> - ); -}; diff --git a/app/@inset/home/loading.tsx b/app/@inset/home/loading.tsx deleted file mode 100644 index f1658ff..0000000 --- a/app/@inset/home/loading.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Skeleton } from '@/ui/skeleton'; - -export default function Loading() { - return ( - <div className="relative flex flex-col justify-between items-start gap-[--p] size-full overflow-hidden"> - {/* Header */} - <div className="w-full px-[--p] pt-[--p]"> - <Skeleton className="h-7 w-48" /> - </div> - - {/* Grid of image sections */} - <div className="grow shrink grid grid-cols-1 lg:grid-cols-2 lg:grid-rows-2 w-full lg:max-h-[95%] place-content-between content-between max-lg:overflow-y-scroll gap-[--p] p-[--p] px-[--p]"> - {[...Array(4)].map((_, i) => ( - <div key={i} className="relative flex row-span-1 col-span-1 size-full rounded-lg overflow-hidden"> - {/* Label skeleton */} - <Skeleton className="absolute top-0 left-0 h-6 w-20 rounded-br-lg" /> - - {/* Image placeholder */} - <Skeleton className="size-full rounded-lg" /> - </div> - ))} - </div> - </div> - ); -} diff --git a/app/@inset/home/page.tsx b/app/@inset/home/page.tsx deleted file mode 100644 index 1169c46..0000000 --- a/app/@inset/home/page.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import Client from '@/_client/@inset/( home )'; -import apiFetch, { ApiRes } from '@/utils/api-fetch'; -import revalidate from '@/utils/revalidate'; - -export const dynamic = 'force-dynamic'; - -export default async function Page() { - const images = await api.getHome(); - const props: ServerProps = { images, api }; - - return <Client {...props} />; -} - -export const api = { - getHome: async () => { - 'use server'; - const { success, ...rest }: ApiRes<ServerProps['images']> = await apiFetch( - '/inset/home', - { - method: 'GET', - } - ); - - if (!success) throw new Error(rest.message); - - await revalidate({ kind: 'tags', tags: ['home'] }); - await revalidate({ kind: 'paths', paths: ['/'] }); - - return rest.data; - }, - uploadImage: async ({ - section, - ...image - }: ServerProps['images'][number] & { base64: string }) => { - 'use server'; - - const { success, ...rest } = await apiFetch(`/inset/home/${section}/image`, { - method: 'PATCH', - body: { display_name: 'main', ...image }, - }); - - if (!success) throw new Error(rest.message); - - await revalidate({ kind: 'tags', tags: ['home'] }); - await revalidate({ kind: 'paths', paths: ['/'] }); - - return rest.data; - }, -}; - -export type ServerProps = { - api: typeof api; - images: { section: string; public_id: string; display_name: string }[]; -}; diff --git a/app/@inset/layout.tsx b/app/@inset/layout.tsx deleted file mode 100644 index e6ab738..0000000 --- a/app/@inset/layout.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Separator } from '@/ui/separator'; -import BreadcrumbNav from '@/components/BreadCrumbNav'; -import { SidebarInset, SidebarRail, SidebarTrigger } from '@/ui/sidebar'; - -import type { PropsWithChildren } from 'react'; -import ThemeSwitcher from '@/components/theme-switcher'; -import InnerChat, { InnerChatTrigger } from '@/components/chat/inner'; -import { Sheet } from '@/ui/sheet'; - -export default function Layout({ children }: PropsWithChildren) { - return ( - <SidebarInset className="!max-h-full md:!max-h-svh shrink-0 pt-[--external-p]"> - <div className="shrink-0 flex flex-row items-center justify-between h-fit px-[--p] pb-[--p] pt-[--sidebar-p] "> - <div - id="header" - className='relative flex items-center h-clamp-10 md:[&:is(.group[data-collapsible="offcanvas"]_+_*_#header)]:h-4 w-36 aspect-[8/1] transition-[height] ease-linear duration-[2500ms]'> - {/* <Toolbar /> */} - </div> - <ThemeSwitcher /> - </div> - <div - id="chatContainer" - className="relative shrink-1 flex flex-col bg-inset dark:shadow-[0_0_10px_-1px_#ffffff55] shadow-[0_0_10px_-1px_#00000055] h-full max-lg:rounded-t-3xl rounded-tl-lg gap-[--p] p-[--p] overflow-hidden"> - <SidebarRail className="!bottom-0 !top-auto !-left-px !right-auto h-[calc(100%-.5rem)] w-[--p] cursor-e-resize" /> - - <Sheet modal={false}> - <div className="shrink-0 flex flex-row justify-between items-center w-full gap-[--p]"> - <div className="flex flex-row items-center h-full gap-x-[--p]"> - <div className="flex flex-row items-center h-full gap-x-[--p] max-lg:hidden"> - <SidebarTrigger className="justify-start h-full w-fit [&>svg]:text-zinc-400 [&>svg]:!size-5 [&>svg]:!p-0 " /> - <Separator orientation="vertical" className="h-3/5 bg-white/45" /> - </div> - - <BreadcrumbNav /> - </div> - <InnerChatTrigger /> - </div> - <InnerChat containerSelector="#chatContainer" /> - <main className="relative flex-1 flex flex-col size-full gap-[--p] transition-all duration-500 p-px lg:overflow-y-scroll overflow-x-hidden max-lg:overflow-hidden "> - {children} - </main> - </Sheet> - </div> - </SidebarInset> - ); -} diff --git a/app/@inset/loading.tsx b/app/@inset/loading.tsx deleted file mode 100644 index 8f0b19b..0000000 --- a/app/@inset/loading.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Skeleton } from '@/ui/skeleton'; - -export default function Loading() { - return ( - <main className="relative flex-1 flex flex-col size-full gap-[--p] transition-all duration-500 p-px lg:overflow-y-scroll overflow-x-hidden max-lg:overflow-hidden"> - <div className="flex flex-col size-full items-center justify-center gap-[--p] p-[--p]"> - {/* Large skeleton for main content area */} - <Skeleton className="w-32 h-8 rounded" /> - <Skeleton className="w-3/4 h-64 rounded-lg" /> - <div className="flex gap-2 w-full max-w-md justify-center"> - <Skeleton className="flex-1 h-10 rounded" /> - <Skeleton className="flex-1 h-10 rounded" /> - </div> - </div> - </main> - ); -} diff --git a/app/@inset/page.tsx b/app/@inset/page.tsx deleted file mode 100644 index 972a041..0000000 --- a/app/@inset/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function Page() { - return ( - <div className="flex size-full "> - <h2 className="text-xl font-semibold m-auto">Seleziona la pagina da editare</h2> - </div> - ); -} diff --git a/app/@sidebar/[section]/[id]/page.tsx b/app/@sidebar/[section]/[id]/page.tsx deleted file mode 100644 index 2ecfb26..0000000 --- a/app/@sidebar/[section]/[id]/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Sidebar } from '@/_client/@sidebar/( posts ) [ id ]'; -import { generateApi } from '../page'; - -import type { Group } from '@/types/group'; -import type { Post } from '@/types/post'; - -type Params = { section: string; id: string }; - -export default async function Page({ params }: { params: Promise<Params> }) { - const { section } = await params; - const api = generateApi({ section }); - - const groups = await api.getGroups(); - const props: ServerProps = { - section, - groups, - api, - }; - - return <Sidebar {...props} />; -} - -export interface ServerProps { - section: string; - groups: Group<Post>[]; - api: ReturnType<typeof generateApi>; -} diff --git a/app/@sidebar/[section]/page.tsx b/app/@sidebar/[section]/page.tsx deleted file mode 100644 index 6e5c941..0000000 --- a/app/@sidebar/[section]/page.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { Sidebar } from '@/_client/@sidebar/( posts )'; -import revalidate from '@/utils/revalidate'; -import apiFetch from '@/utils/api-fetch'; - -import type { ApiRes } from '@/utils/api-fetch'; -import type { Group } from '@/types/group'; -import type { Post } from '@/types/post'; - -type Params = { section: string }; - -export default async function Page({ params }: { params: Promise<Params> }) { - const { section } = await params; - - const api = generateApi({ section }); - - const groups = await api.getGroups(); - const props: ServerProps = { - section, - groups, - api, - }; - - return <Sidebar {...props} />; -} - -export const generateApi = ({ section }: Params) => ({ - getGroups: async () => { - 'use server'; - const { success, ...rest }: ApiRes<Group<Post>[]> = await apiFetch( - `/sidebar/${section}`, - { - method: 'GET', - } - ); - - if (!success) throw new Error(rest.message); - - return rest.data; - }, - createGroup: async (name: string) => { - 'use server'; - - const { data } = await apiFetch(`/sidebar/${section}/group/id`, { - method: 'POST', - body: { name }, - }); - - return data; - }, - createSubGroup: async (body: { name: string; group: { id: string; name: string } }) => { - 'use server'; - console.log('body', body); - const { data } = await apiFetch(`/sidebar/${section}/subgroup/id`, { - method: 'POST', - body, - }); - - return data; - }, - createPost: async (body: { - name: string; - group: { id: string; name: string }; - sub_group: { id: string; name: string }; - }) => { - 'use server'; - - const { data } = await apiFetch(`/sidebar/${section}/id`, { - method: 'POST', - body, - }); - - return data; - }, - - renameGroup: async (body: { id: string; name: string }) => { - 'use server'; - - await apiFetch(`/sidebar/${section}/group/${body.id}`, { method: 'PATCH', body }); - }, - renameSubGroup: async (body: { id: string; name: string }) => { - 'use server'; - - await apiFetch(`/sidebar/${section}/subgroup/${body.id}`, { - method: 'PATCH', - body, - }); - }, - renamePost: async (body: { id: string; name: string }) => { - 'use server'; - - await apiFetch(`/sidebar/${section}/${body.id}`, { method: 'PATCH', body }); - }, - - deleteGroup: async (_id: string) => { - 'use server'; - - await apiFetch(`/sidebar/${section}/group/${_id}`, { - method: 'DELETE', - body: { id: _id }, - }); - }, - deleteSubGroup: async (_id: string) => { - 'use server'; - - await apiFetch(`/sidebar/${section}/subgroup/${_id}`, { - method: 'DELETE', - body: { id: _id }, - }); - }, - deletePost: async (_id: string) => { - 'use server'; - - const { success, ...rest } = await apiFetch(`/sidebar/${section}/${_id}`, { - method: 'DELETE', - body: { id: _id }, - }); - - if (!success) throw new Error(rest.message); - - revalidate({ kind: 'path', path: `/${section}` }); - revalidate({ kind: 'path', path: `/${section}/${_id}` }); - - return rest.data; - }, -}); - -export interface ServerProps { - section: string; - groups: Group<Post>[]; - api: ReturnType<typeof generateApi>; -} diff --git a/app/@sidebar/default.tsx b/app/@sidebar/default.tsx deleted file mode 100644 index 04d20f3..0000000 --- a/app/@sidebar/default.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import EnvSwitcher from '@/components/env-switcher'; -import { Sidebar, SidebarHeader } from '@/ui/sidebar'; - -import { Separator } from '@/ui/separator'; - -export default () => { - return ( - <Sidebar className="p-[--external-p] bg-transparent !border-none gap-[--p]"> - <SidebarHeader className="!pt-[--sidebar-p] !pb-0 !px-0"> - <div className="relative h-clamp-10 max-md:min-h-12 w-full"> - <EnvSwitcher envs={envs} /> - </div> - </SidebarHeader> - <div className="px-[--sidebar-p]"> - <Separator /> - </div> - </Sidebar> - ); -}; - -const envs = [ - { - name: 'Home', - }, - { - name: 'Advisory', - }, - { - name: 'Journals', - }, - { - name: 'Exhibitions', - }, - { - name: 'Lifestyle', - }, -]; diff --git a/app/@sidebar/error.tsx b/app/@sidebar/error.tsx deleted file mode 100644 index 8a18f17..0000000 --- a/app/@sidebar/error.tsx +++ /dev/null @@ -1,18 +0,0 @@ -'use client'; - -import { Button } from '@/ui/button'; - -export default ({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) => { - return ( - <div className="mx-auto text-center text-[clamp(1rem,1.5vw,1.5rem)] font-raleway"> - <h2>Something went wrong!</h2> - <Button - onClick={ - // Attempt to recover by trying to re-render the segment - () => reset() - }> - Try again - </Button> - </div> - ); -}; diff --git a/app/@sidebar/layout.tsx b/app/@sidebar/layout.tsx deleted file mode 100644 index f62ed8a..0000000 --- a/app/@sidebar/layout.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import EnvSwitcher from '@/components/env-switcher'; -import { Sidebar, SidebarHeader } from '@/ui/sidebar'; - -import type { PropsWithChildren } from 'react'; -import { Separator } from '@/ui/separator'; -import { sections } from '@/constants/sections'; - -export default ({ children }: PropsWithChildren) => { - return ( - <Sidebar className="p-[--external-p] bg-transparent !border-none gap-[--p]"> - <SidebarHeader className="!pt-[--sidebar-p] !pb-0 !px-0"> - <div className="relative h-clamp-10 max-md:min-h-12 w-full"> - <EnvSwitcher envs={envs} /> - </div> - </SidebarHeader> - <div className="px-[--sidebar-p]"> - <Separator /> - </div> - {children} - </Sidebar> - ); -}; - -const envs = [ - { - name: 'Home', - }, - - ...sections.map(section => ({ - name: section.charAt(0).toUpperCase() + section.slice(1), - })), -]; diff --git a/app/@sidebar/page.tsx b/app/@sidebar/page.tsx deleted file mode 100644 index ec30acc..0000000 --- a/app/@sidebar/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Page() { - return <></>; -} diff --git a/app/api/auth/[...all]/route.ts b/app/api/auth/[...all]/route.ts new file mode 100644 index 0000000..2ecc0e7 --- /dev/null +++ b/app/api/auth/[...all]/route.ts @@ -0,0 +1,50 @@ +import { eq } from 'drizzle-orm'; +import { toNextJsHandler } from 'better-auth/next-js'; + +import { auth } from '@/src/platform/auth/auth'; +import { assertRuntimeConfiguration } from '@/src/platform/config/env'; +import { getDatabase } from '@/src/platform/db/client'; +import { auditEvents, memberships } from '@/src/platform/db/schema'; + +const handlers = toNextJsHandler(auth); + +async function recordIdentityEvent(userId: string, action: 'identity.login' | 'identity.logout', requestId: string): Promise<void> { + const workspaceRows = await getDatabase().db.select({ workspaceId: memberships.workspaceId }).from(memberships).where(eq(memberships.userId, userId)); + if (workspaceRows.length === 0) return; + await getDatabase().db.insert(auditEvents).values(workspaceRows.map(({ workspaceId }) => ({ + id: crypto.randomUUID(), workspaceId, actorId: userId, action, + targetType: 'session', targetId: null, requestId, metadata: {}, createdAt: new Date(), + }))); +} + +async function handle(request: Request, method: 'GET' | 'POST'): Promise<Response> { + const requestId = request.headers.get('x-request-id')?.slice(0, 100) || crypto.randomUUID(); + try { + assertRuntimeConfiguration(); + } catch { + return Response.json({ error: { code: 'PROVIDER_UNAVAILABLE', message: 'Authentication is not configured.', requestId } }, { status: 503, headers: { 'x-request-id': requestId } }); + } + + const path = new URL(request.url).pathname; + const beforeSession = path.endsWith('/sign-out') ? await auth.api.getSession({ headers: request.headers }) : null; + const response = await handlers[method](request); + response.headers.set('x-request-id', requestId); + if (!response.ok) { + const code = response.status === 429 ? 'RATE_LIMITED' : response.status === 401 ? 'UNAUTHENTICATED' : response.status < 500 ? 'VALIDATION_FAILED' : 'INTERNAL_FAILURE'; + const message = code === 'RATE_LIMITED' ? 'Too many authentication attempts. Try again later.' : code === 'UNAUTHENTICATED' ? 'The supplied credentials are invalid.' : code === 'VALIDATION_FAILED' ? 'The authentication request is invalid.' : 'Authentication could not be completed.'; + return Response.json({ code, message, requestId }, { status: response.status, headers: { 'x-request-id': requestId } }); + } + + if (response.ok && path.endsWith('/sign-in/email')) { + const sessionCookie = response.headers.get('set-cookie')?.split(';', 1)[0]; + if (sessionCookie) { + const session = await auth.api.getSession({ headers: new Headers({ cookie: sessionCookie }) }); + if (session) await recordIdentityEvent(session.user.id, 'identity.login', requestId); + } + } + if (response.ok && beforeSession && path.endsWith('/sign-out')) await recordIdentityEvent(beforeSession.user.id, 'identity.logout', requestId); + return response; +} + +export const GET = (request: Request) => handle(request, 'GET'); +export const POST = (request: Request) => handle(request, 'POST'); diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts deleted file mode 100644 index 66bbce4..0000000 --- a/app/api/chat/route.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { GoogleGenerativeAI } from '@google/generative-ai'; -import _try from '@/utils/_try'; - -export const POST = async (req: Request) => - await _try(async () => { - const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY); - const model = genAI.getGenerativeModel({ model: 'gemini-2.0-flash' }); - - const { prompt } = await req.json(); - - const result = await model.generateContent(prompt); - return result.response.text(); - }); diff --git a/app/api/health/route.ts b/app/api/health/route.ts new file mode 100644 index 0000000..481695d --- /dev/null +++ b/app/api/health/route.ts @@ -0,0 +1,11 @@ +import { getDatabase } from '@/src/platform/db/client'; + +export async function GET(): Promise<Response> { + const requestId = crypto.randomUUID(); + try { + await getDatabase().client.execute('SELECT 1 AS ready'); + return Response.json({ status: 'ready', requestId }, { headers: { 'cache-control': 'no-store', 'x-request-id': requestId } }); + } catch { + return Response.json({ status: 'unavailable', requestId }, { status: 503, headers: { 'cache-control': 'no-store', 'x-request-id': requestId } }); + } +} diff --git a/app/api/inset/[section]/[id]/image/route.ts b/app/api/inset/[section]/[id]/image/route.ts deleted file mode 100644 index a6ae118..0000000 --- a/app/api/inset/[section]/[id]/image/route.ts +++ /dev/null @@ -1,32 +0,0 @@ -import _try from '@/utils/_try'; -import cloudinary from '@/lib/cloudinary'; -import { env } from 'node:process'; - -// export const POST = async (req: Request) => -// await _try(async () => { -// const doc = await req.json(); -// const conn = await db(); -// const coll = conn.collection(section); -// return await coll.insertOne(doc); -// }); - -export const PATCH = async ( - req: Request, - { params }: { params: Promise<{ id: string; section: string }> } -) => - await _try(async () => { - const { section } = await params; - const { _id, base64, public_id, display_name } = await req.json(); - - if (public_id) await cloudinary.uploader.destroy(public_id); - - const res = await cloudinary.uploader.upload(base64, { - folder: `${env.CLOUDINARY_WEBSITE_FOLDER}/${section}/${_id}`, - public_id, - display_name, - }); - - console.log('res', res); - - return res; - }); diff --git a/app/api/inset/[section]/[id]/post/route.ts b/app/api/inset/[section]/[id]/post/route.ts deleted file mode 100644 index 671bb15..0000000 --- a/app/api/inset/[section]/[id]/post/route.ts +++ /dev/null @@ -1,21 +0,0 @@ -import db from '@/lib/db'; -import _try from '@/utils/_try'; -import { Types } from 'mongoose'; -import { post as ObjectIdCheck } from '@/utils/objectId_check'; - -export const PATCH = async ( - req: Request, - { params }: { params: Promise<{ id: string; section: string }> } -) => - await _try(async () => { - const { section } = await params; - const { _id, ...body } = await req.json(); - - const conn = await db(); - const coll = conn.collection(section); - - return await coll.updateOne( - { _id: new Types.ObjectId(_id) }, - { $set: ObjectIdCheck(body) } - ); - }); diff --git a/app/api/inset/[section]/[id]/route.ts b/app/api/inset/[section]/[id]/route.ts deleted file mode 100644 index c257ae5..0000000 --- a/app/api/inset/[section]/[id]/route.ts +++ /dev/null @@ -1,27 +0,0 @@ -import db from '@/lib/db'; -import _try from '@/utils/_try'; -import { Types } from 'mongoose'; -import cloudinary from '@/lib/cloudinary'; -import { env } from 'node:process'; - -export const GET = async ( - req: Request, - { params }: { params: Promise<{ id: string; section: string }> } -) => - await _try(async () => { - const { id, section } = await params; - - const conn = await db(); - const coll = conn.collection(section); - - const { - resources: [image], - } = await cloudinary.search - .expression(`folder:${env.CLOUDINARY_WEBSITE_FOLDER}/${section}/${id}`) - .max_results(1) - // Filtra per cartella - .execute(); - const post = await coll.findOne({ _id: new Types.ObjectId(id) }); - - return { ...post, image }; - }); diff --git a/app/api/inset/advisory/image/route.ts b/app/api/inset/advisory/image/route.ts deleted file mode 100644 index 5cf4656..0000000 --- a/app/api/inset/advisory/image/route.ts +++ /dev/null @@ -1,24 +0,0 @@ -import _try from '@/utils/_try'; -import cloudinary from '@/lib/cloudinary'; -import { env } from 'node:process'; - -// export const POST = async (req: Request) => -// await _try(async () => { -// const doc = await req.json(); -// const conn = await db(); -// const coll = conn.collection(section); -// return await coll.insertOne(doc); -// }); - -export const PATCH = async (req: Request) => - await _try(async () => { - const { base64, public_id, display_name } = await req.json(); - - if (public_id) await cloudinary.uploader.destroy(public_id); - - return await cloudinary.uploader.upload(base64, { - folder: `${env.CLOUDINARY_WEBSITE_FOLDER}/advisory`, - public_id, - display_name: display_name ?? 'main', - }); - }); diff --git a/app/api/inset/advisory/route.ts b/app/api/inset/advisory/route.ts deleted file mode 100644 index 4593f15..0000000 --- a/app/api/inset/advisory/route.ts +++ /dev/null @@ -1,40 +0,0 @@ -import db from '@/lib/db'; -import _try from '@/utils/_try'; -import { Types } from 'mongoose'; -import cloudinary from '@/lib/cloudinary'; -import { env } from 'node:process'; - -export const GET = async () => - await _try(async () => { - const conn = await db(); - const doc = await conn.collection('advisory').findOne( - {}, - { - projection: { - _id: 0, - id: { $toString: '$_id' }, - about: 1, - services: 1, - }, - } - ); - - const { resources } = await cloudinary.search - .expression(`folder:${env.CLOUDINARY_WEBSITE_FOLDER}/advisory`) // Filtra per cartella - .execute(); - - return { ...doc, image: resources[0] }; - }); - -export const PATCH = async (req: Request) => - await _try(async () => { - const { id, about, services } = await req.json(); - - const conn = await db(); - const coll = conn.collection('advisory'); - return await coll.updateOne( - { _id: new Types.ObjectId(id) }, - { $set: { about, services } }, - { upsert: true } - ); - }); diff --git a/app/api/inset/home/[section]/image/route.ts b/app/api/inset/home/[section]/image/route.ts deleted file mode 100644 index 51da132..0000000 --- a/app/api/inset/home/[section]/image/route.ts +++ /dev/null @@ -1,20 +0,0 @@ -import _try from '@/utils/_try'; -import cloudinary from '@/lib/cloudinary'; -import { env } from 'node:process'; - -export const PATCH = async ( - req: Request, - { params }: { params: Promise<{ id: string; section: string }> } -) => - await _try(async () => { - const { section } = await params; - const { base64, public_id, display_name } = await req.json(); - - if (public_id) await cloudinary.uploader.destroy(public_id); - - return await cloudinary.uploader.upload(base64, { - folder: `${env.CLOUDINARY_WEBSITE_FOLDER}/home/${section}`, - public_id, - display_name: display_name ?? 'main', - }); - }); diff --git a/app/api/inset/home/route.ts b/app/api/inset/home/route.ts deleted file mode 100644 index e79db43..0000000 --- a/app/api/inset/home/route.ts +++ /dev/null @@ -1,22 +0,0 @@ -import _try from '@/utils/_try'; -import cloudinary from '@/lib/cloudinary'; -import { sections } from '@/constants/sections'; -import { env } from 'node:process'; - -export const GET = async () => - await _try(async () => { - return await Promise.all( - sections.map(async section => { - const { - resources: [image], - } = await cloudinary.search - .expression( - `folder:${env.CLOUDINARY_WEBSITE_FOLDER}/home/${section}` - ) - .max_results(1) // Filtra per cartella - .execute(); - - return { section, ...image }; - }) - ); - }); diff --git a/app/api/jobs/run/route.ts b/app/api/jobs/run/route.ts new file mode 100644 index 0000000..78df21a --- /dev/null +++ b/app/api/jobs/run/route.ts @@ -0,0 +1,16 @@ +import { getEditorialService } from '@/src/modules/editorial/service'; +import { MediaCleanupWorker } from '@/src/modules/media/cleanup-worker'; +import { DatabaseMediaProvider } from '@/src/modules/media/database-provider'; +import { authorizeJobRunner } from '@/src/platform/auth/job-runner'; +import { dataResponse, withApi } from '@/src/platform/observability/api'; +import { getDatabase } from '@/src/platform/db/client'; + +export async function POST(request: Request): Promise<Response> { + return withApi(async () => { + await authorizeJobRunner(request); + const publication = await getEditorialService().runDueJobs(); + const database = getDatabase(); + const media = await new MediaCleanupWorker(database, new DatabaseMediaProvider(database)).run(); + return dataResponse({ publication, media }); + })(request); +} diff --git a/app/api/sidebar/[section]/[id]/route.ts b/app/api/sidebar/[section]/[id]/route.ts deleted file mode 100644 index ea4f776..0000000 --- a/app/api/sidebar/[section]/[id]/route.ts +++ /dev/null @@ -1,72 +0,0 @@ -import db from '@/lib/db'; -import _try from '@/utils/_try'; -import { Types } from 'mongoose'; -import { sections } from '@/constants/sections'; -import postInitTemplate from '@/constants/post-init-template'; -import { post as ObjectIdCheck } from '@/utils/objectId_check'; - -export const POST = async ( - req: Request, - { params }: { params: Promise<{ section: string; id: string }> } -) => - await _try(async () => { - const { section } = await params; - if (!sections.includes(section)) throw new Error('Section not found'); - const { name, group, sub_group } = await req.json(); - console.log('body', { name, group, sub_group }); - - const conn = await db(); - const coll = conn.collection(section); - console.log('group', group, sub_group); - const template = { - group: { - name: group.name || 'Group', - _id: new Types.ObjectId(group.id), - }, - sub_group: { - name: sub_group.name || 'Sub group', - _id: new Types.ObjectId(sub_group.id), - }, - ...postInitTemplate[section], - name: name || postInitTemplate[section].name, - }; - console.log('template', template); - - return await coll.insertOne(ObjectIdCheck(template)); - }); - -export const PATCH = async ( - req: Request, - { params }: { params: Promise<{ section: string; id: string }> } -) => - await _try(async () => { - const { section } = await params; - if (!sections.includes(section)) throw new Error('Section not found'); - - const { id, ...body } = await req.json(); - const conn = await db(); - const coll = conn.collection(section); - - delete body._id; - return await coll.updateOne( - { _id: new Types.ObjectId(id) }, - { $set: ObjectIdCheck(body) } - ); - }); - -export const DELETE = async ( - req: Request, - { params }: { params: Promise<{ section: string; id: string }> } -) => - await _try(async () => { - const { section } = await params; - if (!sections.includes(section)) throw new Error('Section not found'); - - const { id } = await req.json(); - if (!id) throw new Error('Invalid id'); - - const conn = await db(); - const exercises = conn.collection(section); - - return await exercises.deleteMany({ _id: new Types.ObjectId(id) }); - }); diff --git a/app/api/sidebar/[section]/group/[id]/route.ts b/app/api/sidebar/[section]/group/[id]/route.ts deleted file mode 100644 index be59438..0000000 --- a/app/api/sidebar/[section]/group/[id]/route.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { env } from 'node:process'; - -import db from '@/lib/db'; -import _try from '@/utils/_try'; -import { Types } from 'mongoose'; -import cloudinary from '@/lib/cloudinary'; -import { sections } from '@/constants/sections'; -import postInitTemplate from '@/constants/post-init-template'; -import { post as ObjectIdCheck } from '@/utils/objectId_check'; - -export const POST = async (req: Request, { params }: { params: Promise<{ section: string }> }) => - await _try(async () => { - const { section } = await params; - if (!sections.includes(section)) throw new Error('Section not found'); - - const { name } = await req.json(); - - const conn = await db(); - const coll = conn.collection(section); - - const template = { - group: { - name: name || 'Group', - _id: new Types.ObjectId(), - }, - sub_group: { - name: 'Sub group', - _id: new Types.ObjectId(), - }, - ...postInitTemplate[section], - }; - - return await coll.insertOne(ObjectIdCheck(template)); - }); - -export const PATCH = async ( - req: Request, - { params }: { params: Promise<{ id: string; section: string }> } -) => - await _try(async () => { - const { section } = await params; - if (!sections.includes(section)) throw new Error('Section not found'); - const { id, name } = await req.json(); - - const conn = await db(); - const coll = conn.collection(section); - - return await coll.updateMany( - { 'group._id': new Types.ObjectId(id) }, - { $set: { 'group.name': name || '' } } - ); - }); - -export const DELETE = async ( - req: Request, - { params }: { params: Promise<{ id: string; section: string }> } -) => - await _try(async () => { - const { section } = await params; - if (!sections.includes(section)) throw new Error('Section not found'); - - const { id } = await req.json(); - if (!id) throw new Error('Invalid id'); - - const conn = await db(); - const coll = conn.collection(section); - const docs = await coll.find({ 'group._id': new Types.ObjectId(id) }).toArray(); - - const { folders } = await cloudinary.api.sub_folders( - `${env.CLOUDINARY_WEBSITE_FOLDER}/${section}` - ); - - return await Promise.all( - docs.map(async ({ _id }) => { - const postId = _id.toString(); - if (folders.some(({ name }) => name === postId)) { - await cloudinary.api.delete_resources_by_prefix( - `${env.CLOUDINARY_WEBSITE_FOLDER}/${section}/${postId}/`, - { - all: true, - } - ); - await cloudinary.api.delete_folder( - `${env.CLOUDINARY_WEBSITE_FOLDER}/${section}/${postId}` - ); - } - return await coll.deleteOne({ _id }); - }) - ); - }); diff --git a/app/api/sidebar/[section]/route.ts b/app/api/sidebar/[section]/route.ts deleted file mode 100644 index f4d14b6..0000000 --- a/app/api/sidebar/[section]/route.ts +++ /dev/null @@ -1,100 +0,0 @@ -import db from '@/lib/db'; -import _try from '@/utils/_try'; -import { Types } from 'mongoose'; -import { sections } from '@/constants/sections'; - -export const GET = async (req: Request, { params }: { params: Promise<{ section: string }> }) => - await _try(async () => { - const { section } = await params; - console.log('from server api section', section); - if (!sections.includes(section)) return; - - const conn = await db(); - const coll = conn.collection(section); - - const groups = coll.aggregate([ - { - $group: { - _id: { - group: '$group._id', - sub_group: '$sub_group._id', - }, // Raggruppa per group e sub_group - group: { $first: '$group' }, - sub_group: { $first: '$sub_group' }, // Usa il valore di sub_group come titolo - items: { - $push: '$$ROOT', - }, - }, - }, - { - $group: { - _id: '$_id.group', // Raggruppa per group - id: { $first: '$group._id' }, - name: { $first: '$group.name' }, - sub_groups: { - $push: { - id: '$sub_group._id', - name: '$sub_group.name', - items: { - $map: { - input: '$items', - as: 'item', - in: { - $mergeObjects: [ - { - id: { - $toString: '$$item._id', - }, - }, - { - $arrayToObject: - { - $objectToArray: - '$$item', - }, - }, - ], - }, - }, - }, - }, - }, - }, - }, - { - // Aggiungi la fase di ordinamento per sub_groups - $addFields: { - sub_groups: { - $sortArray: { - input: '$sub_groups', - sortBy: { id: 1 }, // Ordina per id (o _id) in ordine crescente - }, - }, - }, - }, - { - $project: { - _id: 0, // Esclude _id - }, - }, - ]); - if (!groups) throw new Error('No groups found'); - return await groups.toArray(); - }); - -export const PATCH = async (req: Request, { params }: { params: Promise<{ section: string }> }) => - await _try(async () => { - const { section } = await params; - if (!sections.includes(section)) throw new Error('Section not found'); - - const conn = await db(); - const coll = conn.collection(section); - - const { id, ...body } = await req.json(); - - return await coll.updateMany( - { _id: new Types.ObjectId(body.id) }, - { $set: body }, - { upsert: true } - ); - }); diff --git a/app/api/sidebar/[section]/subgroup/[id]/route.ts b/app/api/sidebar/[section]/subgroup/[id]/route.ts deleted file mode 100644 index 97d6a1c..0000000 --- a/app/api/sidebar/[section]/subgroup/[id]/route.ts +++ /dev/null @@ -1,88 +0,0 @@ -import postInitTemplate from '@/constants/post-init-template'; -import { sections } from '@/constants/sections'; -import db from '@/lib/db'; -import _try from '@/utils/_try'; -import { Types } from 'mongoose'; -import { env } from 'node:process'; - -export const POST = async ( - req: Request, - { params }: { params: Promise<{ id: string; section: string }> } -) => - await _try(async () => { - const { section } = await params; - if (!sections.includes(section)) throw new Error('Section not found'); - - const conn = await db(); - const coll = conn.collection(section); - const { group, name } = await req.json(); - - const template = { - group: { name: group.name, _id: new Types.ObjectId(group.id) }, - sub_group: { - name: name ?? postInitTemplate[section].name, - _id: new Types.ObjectId(), - }, - ...postInitTemplate[section], - }; - const { insertedId } = await coll.insertOne(template); - - return { - id: insertedId, - name: template.sub_group.name, - items: [{ id: insertedId.toString(), ...postInitTemplate[section] }], - }; - }); - -export const PATCH = async ( - req: Request, - { params }: { params: Promise<{ id: string; section: string }> } -) => - await _try(async () => { - const { id, section } = await params; - if (!sections.includes(section)) throw new Error('Section not found'); - - const conn = await db(); - const coll = conn.collection(section); - const { name } = await req.json(); - - return await coll.updateMany( - { 'sub_group._id': new Types.ObjectId(id) }, - { $set: { 'sub_group.name': name || '' } } - ); - }); - -export const DELETE = async ( - req: Request, - { params }: { params: Promise<{ id: string; section: string }> } -) => - await _try(async () => { - const { id, section } = await params; - if (!sections.includes(section)) throw new Error('Section not found'); - - const conn = await db(); - const coll = conn.collection(section); - - const docs = await coll.find({ 'sub_group._id': new Types.ObjectId(id) }).toArray(); - const { folders } = await cloudinary.api.sub_folders( - `${env.CLOUDINARY_WEBSITE_FOLDER}/journals` - ); - - return await Promise.all( - docs.map(async ({ _id }) => { - const postId = _id.toString(); - if (folders.some(({ name }) => name === postId)) { - await cloudinary.api.delete_resources_by_prefix( - `${env.CLOUDINARY_WEBSITE_FOLDER}/journals/${postId}/`, - { - all: true, - } - ); - await cloudinary.api.delete_folder( - `${env.CLOUDINARY_WEBSITE_FOLDER}/journals/${postId}` - ); - } - return await coll.deleteOne({ _id }); - }) - ); - }); diff --git a/app/api/workspaces/[workspaceId]/ai/suggest/route.ts b/app/api/workspaces/[workspaceId]/ai/suggest/route.ts new file mode 100644 index 0000000..025f1b3 --- /dev/null +++ b/app/api/workspaces/[workspaceId]/ai/suggest/route.ts @@ -0,0 +1,15 @@ +import { getAIService } from '@/src/modules/ai/service'; +import { assertTrustedMutationOrigin } from '@/src/platform/auth/origin'; +import { requireMembership } from '@/src/platform/auth/session'; +import { dataResponse, withApi } from '@/src/platform/observability/api'; + +type RouteContext = { params: Promise<{ workspaceId: string }> }; + +export async function POST(request: Request, context: RouteContext): Promise<Response> { + const { workspaceId } = await context.params; + return withApi(async (_request, requestId) => { + const membership = await requireMembership(request.headers, workspaceId); + assertTrustedMutationOrigin(request); + return dataResponse(await getAIService().suggest(membership, await request.json(), requestId)); + })(request); +} diff --git a/app/api/workspaces/[workspaceId]/demo/reset/route.ts b/app/api/workspaces/[workspaceId]/demo/reset/route.ts new file mode 100644 index 0000000..bf2cb20 --- /dev/null +++ b/app/api/workspaces/[workspaceId]/demo/reset/route.ts @@ -0,0 +1,15 @@ +import { getDemoService } from '@/src/modules/identity/demo-service'; +import { assertTrustedMutationOrigin } from '@/src/platform/auth/origin'; +import { requireMembership } from '@/src/platform/auth/session'; +import { dataResponse, withApi } from '@/src/platform/observability/api'; + +type RouteContext = { params: Promise<{ workspaceId: string }> }; + +export async function POST(request: Request, context: RouteContext): Promise<Response> { + const { workspaceId } = await context.params; + return withApi(async (_request, requestId) => { + const membership = await requireMembership(request.headers, workspaceId); + assertTrustedMutationOrigin(request); + return dataResponse(await getDemoService().reset(membership, await request.json(), requestId)); + })(request); +} diff --git a/app/api/workspaces/[workspaceId]/media/[assetId]/route.ts b/app/api/workspaces/[workspaceId]/media/[assetId]/route.ts new file mode 100644 index 0000000..f11753f --- /dev/null +++ b/app/api/workspaces/[workspaceId]/media/[assetId]/route.ts @@ -0,0 +1,27 @@ +import { getMediaService } from '@/src/modules/media/service'; +import { assertTrustedMutationOrigin } from '@/src/platform/auth/origin'; +import { requireMembership } from '@/src/platform/auth/session'; +import { dataResponse, withApi } from '@/src/platform/observability/api'; + +type RouteContext = { params: Promise<{ workspaceId: string; assetId: string }> }; + +export async function GET(request: Request, context: RouteContext): Promise<Response> { + const { workspaceId, assetId } = await context.params; + return withApi(async () => { + const membership = await requireMembership(request.headers, workspaceId); + const { asset, data } = await getMediaService().read(membership, assetId); + return new Response(new Uint8Array(data), { + headers: { 'content-type': asset.mimeType, 'content-length': String(asset.byteSize), 'cache-control': 'private, max-age=300', 'x-content-type-options': 'nosniff' }, + }); + })(request); +} + +export async function DELETE(request: Request, context: RouteContext): Promise<Response> { + const { workspaceId, assetId } = await context.params; + return withApi(async (_request, requestId) => { + const membership = await requireMembership(request.headers, workspaceId); + assertTrustedMutationOrigin(request); + await getMediaService().delete(membership, assetId, requestId); + return dataResponse({ deleted: true }); + })(request); +} diff --git a/app/api/workspaces/[workspaceId]/media/route.ts b/app/api/workspaces/[workspaceId]/media/route.ts new file mode 100644 index 0000000..ad08a97 --- /dev/null +++ b/app/api/workspaces/[workspaceId]/media/route.ts @@ -0,0 +1,54 @@ +import { getMediaService } from '@/src/modules/media/service'; +import { assertTrustedMutationOrigin } from '@/src/platform/auth/origin'; +import { requireMembership } from '@/src/platform/auth/session'; +import { getEnvironment } from '@/src/platform/config/env'; +import { dataResponse, withApi } from '@/src/platform/observability/api'; +import { AppError } from '@/src/platform/observability/errors'; + +type RouteContext = { params: Promise<{ workspaceId: string }> }; + +async function boundedFormData(request: Request, maxBytes: number): Promise<FormData> { + const limit = maxBytes + 256 * 1024; + const declared = Number(request.headers.get('content-length') ?? '0'); + if (declared > limit) throw new AppError('VALIDATION_FAILED', { field: 'file', reason: 'size' }); + const reader = request.body?.getReader(); + if (!reader) throw new AppError('VALIDATION_FAILED'); + const chunks: Uint8Array[] = []; + let received = 0; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + received += value.byteLength; + if (received > limit) { await reader.cancel(); throw new AppError('VALIDATION_FAILED', { field: 'file', reason: 'size' }); } + chunks.push(value); + } + const body = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))); + return new Request(request.url, { method: 'POST', headers: request.headers, body }).formData(); +} + +export async function GET(request: Request, context: RouteContext): Promise<Response> { + const { workspaceId } = await context.params; + return withApi(async () => { + const membership = await requireMembership(request.headers, workspaceId); + const postId = new URL(request.url).searchParams.get('postId'); + if (!postId) throw new AppError('VALIDATION_FAILED', { field: 'postId' }); + return dataResponse(await getMediaService().list(membership, postId)); + })(request); +} + +export async function POST(request: Request, context: RouteContext): Promise<Response> { + const { workspaceId } = await context.params; + return withApi(async (_request, requestId) => { + const membership = await requireMembership(request.headers, workspaceId); + assertTrustedMutationOrigin(request); + const form = await boundedFormData(request, getEnvironment().MEDIA_MAX_BYTES); + const file = form.get('file'); + if (!(file instanceof File)) throw new AppError('VALIDATION_FAILED', { field: 'file' }); + const asset = await getMediaService().upload(membership, { + postId: form.get('postId'), replaceAssetId: form.get('replaceAssetId') ?? undefined, + altText: form.get('altText') ?? undefined, fileName: file.name, + declaredMimeType: file.type, data: Buffer.from(await file.arrayBuffer()), + }, requestId); + return dataResponse(asset, { status: 201 }); + })(request); +} diff --git a/app/api/workspaces/[workspaceId]/posts/[postId]/revisions/restore/route.ts b/app/api/workspaces/[workspaceId]/posts/[postId]/revisions/restore/route.ts new file mode 100644 index 0000000..279026c --- /dev/null +++ b/app/api/workspaces/[workspaceId]/posts/[postId]/revisions/restore/route.ts @@ -0,0 +1,15 @@ +import { getEditorialService } from '@/src/modules/editorial/service'; +import { assertTrustedMutationOrigin } from '@/src/platform/auth/origin'; +import { requireMembership } from '@/src/platform/auth/session'; +import { dataResponse, withApi } from '@/src/platform/observability/api'; + +type RouteContext = { params: Promise<{ workspaceId: string; postId: string }> }; + +export async function POST(request: Request, context: RouteContext): Promise<Response> { + const { workspaceId, postId } = await context.params; + return withApi(async (_request, requestId) => { + const membership = await requireMembership(request.headers, workspaceId); + assertTrustedMutationOrigin(request); + return dataResponse(await getEditorialService().restore(membership, postId, await request.json(), requestId)); + })(request); +} diff --git a/app/api/workspaces/[workspaceId]/posts/[postId]/revisions/route.ts b/app/api/workspaces/[workspaceId]/posts/[postId]/revisions/route.ts new file mode 100644 index 0000000..649eb74 --- /dev/null +++ b/app/api/workspaces/[workspaceId]/posts/[postId]/revisions/route.ts @@ -0,0 +1,13 @@ +import { getEditorialService } from '@/src/modules/editorial/service'; +import { requireMembership } from '@/src/platform/auth/session'; +import { dataResponse, withApi } from '@/src/platform/observability/api'; + +type RouteContext = { params: Promise<{ workspaceId: string; postId: string }> }; + +export async function GET(request: Request, context: RouteContext): Promise<Response> { + const { workspaceId, postId } = await context.params; + return withApi(async () => { + const membership = await requireMembership(request.headers, workspaceId); + return dataResponse(await getEditorialService().revisions(membership, postId)); + })(request); +} diff --git a/app/api/workspaces/[workspaceId]/posts/[postId]/route.ts b/app/api/workspaces/[workspaceId]/posts/[postId]/route.ts new file mode 100644 index 0000000..13394e4 --- /dev/null +++ b/app/api/workspaces/[workspaceId]/posts/[postId]/route.ts @@ -0,0 +1,23 @@ +import { getEditorialService } from '@/src/modules/editorial/service'; +import { requireMembership } from '@/src/platform/auth/session'; +import { assertTrustedMutationOrigin } from '@/src/platform/auth/origin'; +import { dataResponse, withApi } from '@/src/platform/observability/api'; + +type RouteContext = { params: Promise<{ workspaceId: string; postId: string }> }; + +export async function GET(request: Request, context: RouteContext): Promise<Response> { + const { workspaceId, postId } = await context.params; + return withApi(async () => { + const membership = await requireMembership(request.headers, workspaceId); + return dataResponse(await getEditorialService().get(membership, postId)); + })(request); +} + +export async function PATCH(request: Request, context: RouteContext): Promise<Response> { + const { workspaceId, postId } = await context.params; + return withApi(async (_request, requestId) => { + const membership = await requireMembership(request.headers, workspaceId); + assertTrustedMutationOrigin(request); + return dataResponse(await getEditorialService().save(membership, postId, await request.json(), requestId)); + })(request); +} diff --git a/app/api/workspaces/[workspaceId]/posts/[postId]/transitions/route.ts b/app/api/workspaces/[workspaceId]/posts/[postId]/transitions/route.ts new file mode 100644 index 0000000..8d9b4c8 --- /dev/null +++ b/app/api/workspaces/[workspaceId]/posts/[postId]/transitions/route.ts @@ -0,0 +1,15 @@ +import { getEditorialService } from '@/src/modules/editorial/service'; +import { assertTrustedMutationOrigin } from '@/src/platform/auth/origin'; +import { requireMembership } from '@/src/platform/auth/session'; +import { dataResponse, withApi } from '@/src/platform/observability/api'; + +type RouteContext = { params: Promise<{ workspaceId: string; postId: string }> }; + +export async function POST(request: Request, context: RouteContext): Promise<Response> { + const { workspaceId, postId } = await context.params; + return withApi(async (_request, requestId) => { + const membership = await requireMembership(request.headers, workspaceId); + assertTrustedMutationOrigin(request); + return dataResponse(await getEditorialService().transition(membership, postId, await request.json(), requestId)); + })(request); +} diff --git a/app/api/workspaces/[workspaceId]/posts/route.ts b/app/api/workspaces/[workspaceId]/posts/route.ts new file mode 100644 index 0000000..618029c --- /dev/null +++ b/app/api/workspaces/[workspaceId]/posts/route.ts @@ -0,0 +1,25 @@ +import { getEditorialService } from '@/src/modules/editorial/service'; +import { requireMembership } from '@/src/platform/auth/session'; +import { assertTrustedMutationOrigin } from '@/src/platform/auth/origin'; +import { dataResponse, withApi } from '@/src/platform/observability/api'; + +type RouteContext = { params: Promise<{ workspaceId: string }> }; + +export async function GET(request: Request, context: RouteContext): Promise<Response> { + const { workspaceId } = await context.params; + return withApi(async (_request, requestId) => { + const membership = await requireMembership(request.headers, workspaceId); + const data = await getEditorialService().list(membership); + return dataResponse(data, { headers: { 'x-operation-id': requestId } }); + })(request); +} + +export async function POST(request: Request, context: RouteContext): Promise<Response> { + const { workspaceId } = await context.params; + return withApi(async (_request, requestId) => { + const membership = await requireMembership(request.headers, workspaceId); + assertTrustedMutationOrigin(request); + const data = await getEditorialService().create(membership, await request.json(), requestId); + return dataResponse(data, { status: 201 }); + })(request); +} diff --git a/app/default.tsx b/app/default.tsx deleted file mode 100644 index 461f67a..0000000 --- a/app/default.tsx +++ /dev/null @@ -1 +0,0 @@ -export default () => null; diff --git a/app/globals.css b/app/globals.css index d17cc8b..5f3bbdb 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,124 +1,272 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +:root { + color-scheme: dark; + --bg: #090b0d; + --surface: #111519; + --surface-strong: #171c21; + --text: #f5f7f8; + --muted: #9ba6af; + --line: #293139; + --accent: #c8ff5a; + --accent-ink: #172004; + --danger: #ff857f; + --radius: 18px; +} + +* { box-sizing: border-box; } + +html { background: var(--bg); scroll-behavior: smooth; } body { - font-family: Arial, Helvetica, sans-serif; + margin: 0; + min-width: 320px; + background: + radial-gradient(circle at 84% 5%, rgb(200 255 90 / 9%), transparent 28rem), + var(--bg); + color: var(--text); + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-synthesis: none; +} + +a { color: inherit; } +button, input, textarea, select { font: inherit; } + +:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; } + +.skip-link { + position: fixed; + top: 12px; + left: 12px; + z-index: 100; + transform: translateY(-160%); + border-radius: 999px; + background: var(--accent); + color: var(--accent-ink); + padding: 10px 16px; + font-weight: 800; +} + +.skip-link:focus { transform: translateY(0); } + +.marketing-shell { width: min(1180px, calc(100% - 40px)); margin: 0 auto; } + +.marketing-nav { + display: flex; + align-items: center; + justify-content: space-between; + min-height: 84px; + border-bottom: 1px solid var(--line); } -@layer base { - :root { - --background: 0 0% 98%; - --foreground: 0 0% 3.9%; - --card: 0 0% 100%; - --card-foreground: 0 0% 3.9%; - --popover: 0 0% 100%; - --popover-foreground: 0 0% 3.9%; - --primary: 0 0% 9%; - --primary-foreground: 0 0% 98%; - --secondary: 0 0% 96.1%; - --secondary-foreground: 0 0% 9%; - --muted: 0 0% 96.1%; - --muted-foreground: 0 0% 45.1%; - --accent: 0 0% 96.1%; - --accent-foreground: 0 0% 9%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 89.8%; - --input: 0 0% 89.8%; - --ring: 0 0% 3.9%; - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - --radius: 0.5rem; - --sidebar-background: 0 0% 93%; - --sidebar-foreground: 240 5.3% 26.1%; - --sidebar-primary: 240 5.9% 10%; - --sidebar-primary-foreground: 0 0% 98%; - --sidebar-accent: 240 4.8% 95.9%; - --sidebar-accent-foreground: 240 5.9% 10%; - --sidebar-border: 220 13% 91%; - --sidebar-ring: 217.2 91.2% 59.8%; - } - .dark { - --background: 240 5.9% 8% ; - --foreground: 0 0% 98%; - --card: 0 0% 3.9%; - --card-foreground: 0 0% 98%; - --popover: 0 0% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 0 0% 98%; - --primary-foreground: 0 0% 9%; - --secondary: 0 0% 14.9%; - --secondary-foreground: 0 0% 98%; - --muted: 0 0% 14.9%; - --muted-foreground: 0 0% 63.9%; - --accent: 0 0% 14.9%; - --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 14.9%; - --input: 0 0% 14.9%; - --ring: 0 0% 83.1%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - --sidebar-background: 0 0% 3.90%; - --sidebar-foreground: 240 4.8% 95.9%; - --sidebar-primary: 224.3 76.3% 48%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 240 3.7% 15.9%; - --sidebar-accent-foreground: 240 4.8% 95.9%; - --sidebar-border: 240 3.7% 15.9%; - --sidebar-ring: 217.2 91.2% 59.8%; - } - - * { - -webkit-touch-callout: none; /* Safari */ - -webkit-user-select: none; /* Chrome */ - -moz-user-select: none; /* Firefox */ - -ms-user-select: none; /* Internet Explorer/Edge */ - user-select: none; - - /* disable all kind of selection text */ - @apply border-border select-none; - } - - body { - @apply bg-background text-foreground; - } - +.brand { display: inline-flex; align-items: center; gap: 12px; font-weight: 850; letter-spacing: -.03em; } +.brand-mark { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 11px; background: var(--accent); color: var(--accent-ink); } +.nav-links { display: flex; align-items: center; gap: 24px; color: var(--muted); font-size: .92rem; } +.nav-links a { text-decoration: none; } + +.button { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 46px; + border: 1px solid transparent; + border-radius: 999px; + background: var(--accent); + color: var(--accent-ink); + padding: 0 20px; + font-weight: 800; + text-decoration: none; } +.button.secondary { border-color: var(--line); background: transparent; color: var(--text); } + +.hero { display: grid; grid-template-columns: 1.15fr .85fr; gap: 72px; align-items: center; min-height: 680px; padding: 72px 0; } +.eyebrow { color: var(--accent); font-size: .78rem; font-weight: 850; letter-spacing: .12em; text-transform: uppercase; } +h1 { margin: 18px 0 22px; max-width: 800px; font-size: clamp(3rem, 7vw, 6.8rem); line-height: .92; letter-spacing: -.07em; } +.hero-copy { max-width: 650px; color: var(--muted); font-size: clamp(1.05rem, 2vw, 1.28rem); line-height: 1.65; } +.hero-actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 34px; } +.proof { display: flex; flex-wrap: wrap; gap: 20px; margin: 42px 0 0; padding: 0; list-style: none; color: var(--muted); font-size: .86rem; } +.proof strong { display: block; color: var(--text); font-size: 1.12rem; } + +.editor-preview { border: 1px solid var(--line); border-radius: 28px; background: rgb(17 21 25 / 92%); padding: 14px; box-shadow: 0 36px 90px rgb(0 0 0 / 45%); } +.preview-toolbar { display: flex; align-items: center; justify-content: space-between; padding: 10px 10px 20px; color: var(--muted); font-size: .8rem; } +.preview-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 18px var(--accent); } +.preview-canvas { min-height: 420px; border: 1px solid var(--line); border-radius: 18px; background: linear-gradient(145deg, #151a1f, #0d1013); padding: 24px; } +.preview-state { display: inline-flex; border-radius: 999px; background: rgb(200 255 90 / 10%); color: var(--accent); padding: 7px 11px; font-size: .72rem; font-weight: 800; } +.preview-title { max-width: 340px; margin: 74px 0 18px; font-size: 2.5rem; line-height: 1.02; letter-spacing: -.05em; } +.preview-lines { display: grid; gap: 10px; } +.preview-lines span { height: 8px; border-radius: 999px; background: #2a3239; } +.preview-lines span:last-child { width: 62%; } +.preview-footer { display: flex; justify-content: space-between; margin-top: 64px; border-top: 1px solid var(--line); padding-top: 18px; color: var(--muted); font-size: .76rem; } +.capabilities { padding: 100px 0 120px; border-top: 1px solid var(--line); } +.section-heading { display: flex; justify-content: space-between; gap: 40px; align-items: end; margin-bottom: 36px; } +.section-heading h2 { margin: 0; max-width: 620px; font-size: clamp(2rem, 5vw, 4.2rem); line-height: 1; letter-spacing: -.055em; } +.section-heading p { max-width: 400px; color: var(--muted); line-height: 1.65; } +.capability-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; } +.capability-card { min-height: 230px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); padding: 26px; } +.capability-card span { color: var(--accent); font: 800 .75rem/1 ui-monospace, monospace; } +.capability-card h3 { margin: 56px 0 10px; font-size: 1.25rem; } +.capability-card p { margin: 0; color: var(--muted); line-height: 1.6; } +.site-footer { display: flex; justify-content: space-between; gap: 24px; border-top: 1px solid var(--line); padding: 30px 0 44px; color: var(--muted); font-size: .84rem; } -@keyframes fade-in { - 0% { - transform: translateX(50px); - } +.auth-shell { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(420px, .9fr); min-height: 100svh; } +.auth-intro { display: flex; flex-direction: column; justify-content: space-between; gap: 80px; padding: clamp(28px, 5vw, 72px); border-right: 1px solid var(--line); } +.auth-intro h1 { max-width: 880px; font-size: clamp(3rem, 6vw, 6.4rem); } +.auth-note, .privacy-note { color: var(--muted); font-size: .82rem; line-height: 1.55; } +.auth-panel { display: flex; flex-direction: column; justify-content: center; padding: clamp(24px, 5vw, 72px); background: rgb(17 21 25 / 72%); } +.panel-heading h2 { margin: 10px 0 28px; font-size: 2rem; letter-spacing: -.04em; } +.role-list { display: grid; gap: 10px; } +.role-card { display: flex; align-items: center; justify-content: space-between; width: 100%; min-height: 76px; border: 1px solid var(--line); border-radius: 16px; background: var(--surface); color: var(--text); padding: 14px 18px; text-align: left; cursor: pointer; } +.role-card:hover { border-color: #4a5742; background: var(--surface-strong); } +.role-card:disabled { cursor: wait; opacity: .7; } +.role-card strong, .role-card small { display: block; } +.role-card small { margin-top: 5px; color: var(--muted); } +.form-error { border: 1px solid rgb(255 133 127 / 45%); border-radius: 12px; background: rgb(255 133 127 / 8%); color: var(--danger); padding: 12px; } +.privacy-note { margin-top: 24px; } - to { - transform: translateX(0); - } +.workspace-shell { display: grid; grid-template-columns: 290px minmax(0, 1fr); min-height: 100svh; background: var(--bg); } +.workspace-sidebar { position: sticky; top: 0; display: flex; flex-direction: column; gap: 28px; height: 100svh; overflow-y: auto; border-right: 1px solid var(--line); background: #0c0f12; padding: 24px 18px; } +.workspace-label { display: grid; gap: 4px; border-radius: 12px; background: var(--surface); padding: 13px; } +.workspace-label span, .post-nav-heading { color: var(--muted); font-size: .72rem; text-transform: uppercase; letter-spacing: .08em; } +.workspace-label strong { font-size: .9rem; } +.workspace-label small { color: var(--muted); font-size: .66rem; line-height: 1.4; } +.post-nav { display: grid; gap: 7px; } +.post-nav-heading { display: flex; align-items: center; justify-content: space-between; padding: 0 8px 8px; } +.post-nav-heading button { display: grid; place-items: center; width: 30px; height: 30px; border: 1px solid var(--line); border-radius: 9px; background: transparent; color: var(--text); cursor: pointer; } +.post-nav > button { display: grid; gap: 5px; border: 1px solid transparent; border-radius: 12px; background: transparent; color: var(--text); padding: 11px; text-align: left; cursor: pointer; } +.post-nav > button:hover, .post-nav > button[aria-current="page"] { border-color: var(--line); background: var(--surface); } +.post-nav > button span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: .86rem; font-weight: 700; } +.post-nav > button small { color: var(--muted); font-size: .7rem; } +.guided-checklist { margin-top: auto; border-top: 1px solid var(--line); padding: 22px 8px 0; } +.guided-checklist ol { display: grid; gap: 9px; margin: 16px 0 0; padding-left: 18px; color: var(--muted); font-size: .76rem; } +.guided-checklist li.done { color: var(--text); } +.guided-checklist li.done::marker { color: var(--accent); } +.reset-demo { width: 100%; min-height: 38px; margin-top: 18px; border: 1px solid var(--line); border-radius: 999px; background: transparent; color: var(--muted); font-size: .7rem; cursor: pointer; } +.reset-demo.armed { border-color: rgb(255 133 127 / 55%); color: var(--danger); } + +.editor-shell { min-width: 0; } +.editor-topbar { position: sticky; top: 0; z-index: 20; display: flex; align-items: center; justify-content: space-between; min-height: 72px; border-bottom: 1px solid var(--line); background: rgb(9 11 13 / 88%); padding: 0 28px; backdrop-filter: blur(18px); } +.editor-topbar > div { display: flex; align-items: center; gap: 10px; } +.role-pill, .mode-pill, .save-state { display: inline-flex; align-items: center; min-height: 28px; border-radius: 999px; padding: 0 10px; font-size: .72rem; font-weight: 800; } +.role-pill { background: var(--accent); color: var(--accent-ink); } +.mode-pill, .save-state { border: 1px solid var(--line); color: var(--muted); } +.save-state.conflict, .save-state.error { border-color: rgb(255 133 127 / 50%); color: var(--danger); } +.text-button { border: 0; background: transparent; color: var(--muted); cursor: pointer; } +.editor-grid { display: grid; grid-template-columns: minmax(0, 1fr) 310px; min-height: calc(100svh - 72px); } +.editor-form { min-width: 0; padding: clamp(24px, 5vw, 72px); } +.document-meta { display: flex; flex-wrap: wrap; gap: 9px; margin-bottom: 28px; color: var(--muted); font: 700 .72rem/1 ui-monospace, monospace; } +.document-meta span { border-right: 1px solid var(--line); padding-right: 9px; } +.editor-form label { display: block; margin: 18px 0 8px; color: var(--muted); font-size: .75rem; font-weight: 800; text-transform: uppercase; letter-spacing: .08em; } +.editor-form textarea { width: 100%; resize: vertical; border: 1px solid transparent; border-radius: 12px; background: transparent; color: var(--text); padding: 10px; } +.editor-form textarea:hover, .editor-form textarea:focus { border-color: var(--line); background: var(--surface); } +.editor-form textarea:disabled { opacity: .75; } +.title-input { min-height: 124px; font-size: clamp(2.2rem, 4.5vw, 5.2rem); font-weight: 850; line-height: .98; letter-spacing: -.055em; } +.excerpt-input { min-height: 90px; color: var(--muted) !important; font-size: 1.15rem; line-height: 1.55; } +.content-input { min-height: 460px; font: 1rem/1.75 ui-monospace, "SFMono-Regular", Consolas, monospace !important; } +.workflow-panel { display: flex; align-items: end; justify-content: space-between; gap: 24px; margin-top: 30px; border-top: 1px solid var(--line); padding-top: 24px; } +.workflow-panel h2 { margin: 8px 0 0; font-size: 1.35rem; } +.workflow-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; } +.workflow-actions > span { display: flex; align-items: end; gap: 8px; } +.workflow-actions label { display: grid; gap: 5px; color: var(--muted); font-size: .7rem; } +.workflow-actions input { min-height: 40px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface); color: var(--text); padding: 0 9px; color-scheme: dark; } +.workflow-actions button, .history-toggle, .revision-history button, .preview-link { display: inline-flex; align-items: center; justify-content: center; min-height: 40px; border: 1px solid var(--line); border-radius: 999px; background: var(--surface); color: var(--text); padding: 0 14px; font-size: .76rem; font-weight: 750; text-decoration: none; cursor: pointer; } +.workflow-actions button:first-of-type { border-color: rgb(200 255 90 / 45%); } +.workflow-actions button:disabled, .revision-history button:disabled { cursor: not-allowed; opacity: .45; } +.ai-panel { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 18px; align-items: end; margin-top: 28px; border: 1px solid var(--line); border-radius: 18px; background: linear-gradient(145deg, rgb(200 255 90 / 4%), var(--surface)); padding: 20px; } +.ai-panel h2 { margin: 7px 0 0; font-size: 1.35rem; } +.ai-panel > label { grid-column: 1 / -1; display: grid; gap: 7px; color: var(--muted); font-size: .72rem; font-weight: 800; text-transform: uppercase; letter-spacing: .06em; } +.ai-panel textarea { min-height: 72px; border-color: var(--line); background: #0c0f12; font-size: .88rem; } +.ai-panel > button, .suggestion-preview button, .media-form button, .danger-button { min-height: 42px; border: 1px solid rgb(200 255 90 / 45%); border-radius: 999px; background: var(--accent); color: var(--accent-ink); padding: 0 15px; font-size: .78rem; font-weight: 800; cursor: pointer; } +.ai-panel button:disabled, .media-panel button:disabled { cursor: not-allowed; opacity: .45; } +.ai-panel > .panel-message { margin: 0; text-align: right; } +.suggestion-preview { grid-column: 1 / -1; min-width: 0; border-top: 1px solid var(--line); padding-top: 18px; } +.suggestion-preview > span, .suggestion-preview small { color: var(--muted); font-size: .68rem; } +.suggestion-preview h3 { margin: 12px 0 6px; font-size: 1.4rem; } +.suggestion-preview pre { max-height: 260px; overflow: auto; white-space: pre-wrap; border-radius: 12px; background: #0c0f12; color: var(--muted); padding: 12px; } +.suggestion-preview button { display: block; margin-top: 16px; } +.panel-message, .panel-empty { color: var(--muted); font-size: .72rem; line-height: 1.5; } +.editor-message { color: var(--muted); font-size: .82rem; } +.editor-message.conflict, .editor-message.error { color: var(--danger); } +.conflict-actions { display: flex; flex-wrap: wrap; gap: 10px; } +.conflict-actions button { min-height: 42px; border: 1px solid var(--line); border-radius: 999px; background: var(--surface); color: var(--text); padding: 0 15px; cursor: pointer; } +.compare-panel { margin-top: 24px; border: 1px solid var(--line); border-radius: 16px; padding: 18px; } +.compare-panel > div { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } +.compare-panel article { min-width: 0; border-radius: 12px; background: var(--surface); padding: 14px; } +.compare-panel pre { overflow: auto; white-space: pre-wrap; color: var(--muted); } +.evidence-panel { border-left: 1px solid var(--line); background: #0c0f12; padding: 38px 24px; } +.evidence-panel h2 { margin: 12px 0 32px; font-size: 1.6rem; letter-spacing: -.04em; } +.evidence-panel dl { display: grid; gap: 18px; } +.evidence-panel dl div { display: grid; gap: 5px; border-bottom: 1px solid var(--line); padding-bottom: 14px; } +.evidence-panel dt { color: var(--muted); font-size: .72rem; text-transform: uppercase; letter-spacing: .06em; } +.evidence-panel dd { margin: 0; font-size: .85rem; word-break: break-word; } +.evidence-panel > p:last-child { margin-top: 30px; color: var(--muted); font-size: .82rem; line-height: 1.65; } +.preview-link, .history-toggle { width: 100%; margin-top: 10px; } +.history-toggle { background: transparent; } +.revision-history { display: grid; gap: 8px; margin-top: 14px; } +.revision-history article { border: 1px solid var(--line); border-radius: 12px; background: var(--surface); padding: 11px; } +.revision-history article > div { display: flex; align-items: center; justify-content: space-between; gap: 6px; } +.revision-history p { margin: 9px 0; font-size: .76rem; } +.revision-history small { color: var(--muted); font-size: .62rem; } +.revision-history button { min-height: 30px; padding: 0 9px; font-size: .66rem; } +.history-comparison { margin-top: 12px; border: 1px solid var(--line); border-radius: 12px; padding: 11px; } +.history-comparison h3 { margin: 0 0 9px; font-size: .8rem; } +.history-comparison pre { max-height: 220px; overflow: auto; white-space: pre-wrap; color: var(--muted); font-size: .7rem; } +.media-panel { margin-top: 24px; border-top: 1px solid var(--line); padding-top: 20px; } +.media-panel h3 { margin: 8px 0 14px; } +.asset-card { display: grid; gap: 6px; border-radius: 12px; background: var(--surface); padding: 12px; } +.asset-card strong { overflow: hidden; text-overflow: ellipsis; } +.asset-card span { color: var(--muted); font-size: .68rem; } +.media-form { display: grid; gap: 10px; margin-top: 12px; } +.media-form label { display: grid; gap: 6px; color: var(--muted); font-size: .68rem; } +.media-form input { width: 100%; min-height: 38px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface); color: var(--text); padding: 7px; } +.media-form input[type="file"] { font-size: .66rem; } +.danger-button { width: 100%; margin-top: 8px; border-color: rgb(255 133 127 / 45%); background: transparent; color: var(--danger); } + +.public-article { width: min(920px, calc(100% - 32px)); margin: 0 auto; padding-bottom: 100px; } +.public-article nav { display: flex; align-items: center; justify-content: space-between; min-height: 76px; border-bottom: 1px solid var(--line); color: var(--muted); font-size: .8rem; } +.public-article nav a { color: var(--text); font-weight: 850; text-decoration: none; } +.public-article article { padding: clamp(70px, 12vw, 140px) 0; } +.public-article h1 { max-width: 900px; font-size: clamp(3.2rem, 9vw, 7rem); } +.public-standfirst { max-width: 720px; color: var(--muted); font-size: clamp(1.1rem, 2vw, 1.45rem); line-height: 1.6; } +.public-content { margin-top: 70px; white-space: pre-wrap; color: #dce2e6; font: 1.05rem/1.85 ui-monospace, Consolas, monospace; } +.public-article footer { margin-top: 80px; border-top: 1px solid var(--line); padding-top: 22px; color: var(--muted); font-size: .76rem; } + +@media (max-width: 860px) { + .nav-links a:not(.button) { display: none; } + .hero { grid-template-columns: 1fr; gap: 36px; min-height: auto; padding: 80px 0; } + .editor-preview { max-width: 620px; } + .capability-grid { grid-template-columns: 1fr; } + .section-heading { align-items: start; flex-direction: column; } + .auth-shell { grid-template-columns: 1fr; } + .auth-intro { min-height: 64svh; border-right: 0; border-bottom: 1px solid var(--line); } + .workspace-shell { grid-template-columns: 1fr; } + .workspace-sidebar { position: static; height: auto; max-height: 46svh; border-right: 0; border-bottom: 1px solid var(--line); } + .guided-checklist { display: none; } + .editor-grid { grid-template-columns: 1fr; } + .evidence-panel { border-top: 1px solid var(--line); border-left: 0; } } -@keyframes fade { - from { - opacity: 0; - } - to { - opacity: 1; - } +@media (max-width: 520px) { + .marketing-shell { width: min(100% - 24px, 1180px); } + .brand-word { display: none; } + h1 { font-size: clamp(3rem, 16vw, 5rem); } + .hero-actions .button { width: 100%; } + .preview-canvas { min-height: 340px; } + .preview-title { margin-top: 50px; font-size: 2rem; } + .site-footer { flex-direction: column; } + .auth-intro, .auth-panel { padding: 24px 18px; } + .auth-intro { min-height: 58svh; } + .editor-topbar { align-items: flex-start; min-height: 90px; padding: 14px; } + .editor-topbar > div { align-items: flex-start; flex-direction: column; gap: 5px; } + .editor-form { padding: 24px 14px; } + .workflow-panel { align-items: stretch; flex-direction: column; } + .workflow-actions { justify-content: flex-start; } + .ai-panel { grid-template-columns: 1fr; } + .ai-panel > .panel-message { text-align: left; } + .title-input { min-height: 96px; } + .compare-panel > div { grid-template-columns: 1fr; } } -@keyframes backToPosition { - to { - transform: translate(0); - opacity: 1; - } -} \ No newline at end of file +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; } +} diff --git a/app/layout.tsx b/app/layout.tsx index 25c94a4..5b7aca5 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,50 +1,31 @@ -import '@/config/_export'; - import './globals.css'; -import '@/lib/cloudinary'; - -import { Toaster } from '@/ui/toaster'; -import { SidebarProvider } from '@/ui/sidebar'; -import { ThemeProvider } from '@/hooks/use-theme'; - -import type { ReactNode } from 'react'; import type { Metadata, Viewport } from 'next'; +import type { ReactNode } from 'react'; export const metadata: Metadata = { - title: 'BT - Dashboard', - description: 'Developed by PatrickDev', - robots: { index: false, follow: false }, + title: { + default: 'AutoBlog CMS — Editorial control for AI-assisted teams', + template: '%s | AutoBlog CMS', + }, + description: + 'An authenticated editorial CMS with immutable revisions, role-based workflow and bounded AI assistance.', + metadataBase: new URL(process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000'), + robots: { index: true, follow: true }, }; export const viewport: Viewport = { width: 'device-width', initialScale: 1, - maximumScale: 1, - userScalable: false, - themeColor: '#000', + themeColor: '#090b0d', }; -export default function RootLayout({ sidebar, inset }: Layouts) { +export default function RootLayout({ children }: Readonly<{ children: ReactNode }>) { return ( - <html lang="en" className="dark"> - <ThemeProvider defaultValue="dark"> - <body className="antialiased"> - <div id="content"> - <div> - <SidebarProvider className="p-0 bg-transparent size-full"> - {sidebar} - {inset} - </SidebarProvider> - </div> - </div> - <Toaster /> - </body> - </ThemeProvider> + <html lang="en" data-scroll-behavior="smooth"> + <body> + <a className="skip-link" href="#main-content">Skip to content</a> + {children} + </body> </html> ); } - -export interface Layouts { - inset: ReactNode; - sidebar: ReactNode; -} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..6327d8b --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,75 @@ +import Link from 'next/link'; + +const capabilities = [ + ['01', 'Immutable by design', 'Every meaningful save becomes a revision; published content remains pinned to an auditable snapshot.'], + ['02', 'Policy before pixels', 'Owner, Admin, Editor, Author and Reviewer permissions execute on the server, not in hidden buttons.'], + ['03', 'AI with a boundary', 'Suggestions are quota-bound, mode-labeled and applied only after an explicit editorial decision.'], +] as const; + +export default function MarketingPage() { + return ( + <div className="marketing-shell"> + <header className="marketing-nav"> + <Link className="brand" href="/" aria-label="AutoBlog CMS home"> + <span className="brand-mark" aria-hidden="true">A</span> + <span className="brand-word">AutoBlog CMS</span> + </Link> + <nav className="nav-links" aria-label="Primary navigation"> + <a href="#architecture">Architecture</a> + <Link href="/sign-in" className="button">Try the demo</Link> + </nav> + </header> + + <main id="main-content"> + <section className="hero" aria-labelledby="hero-title"> + <div> + <p className="eyebrow">Editorial systems, made defensible</p> + <h1 id="hero-title">Publish with evidence.</h1> + <p className="hero-copy"> + AutoBlog gives AI-assisted teams one durable workflow for drafting, review, + approval and publication—without silent overwrites or simulated security. + </p> + <div className="hero-actions"> + <Link className="button" href="/sign-in">Enter guided demo</Link> + <a className="button secondary" href="#architecture">Inspect the system</a> + </div> + <ul className="proof" aria-label="Engineering proof points"> + <li><strong>5 roles</strong>server-enforced</li> + <li><strong>409</strong>stale-write contract</li> + <li><strong>1 path</strong>demo to production</li> + </ul> + </div> + + <div className="editor-preview" aria-label="Editorial workspace preview"> + <div className="preview-toolbar"> + <span>autoblog / spring-edition</span> + <span className="preview-dot" role="status" aria-label="Saved" /> + </div> + <div className="preview-canvas"> + <span className="preview-state">IN REVIEW · REV 12</span> + <h2 className="preview-title">Designing the next editorial operating system</h2> + <div className="preview-lines" aria-hidden="true"><span /><span /><span /></div> + <div className="preview-footer"><span>Reviewer: Maya Chen</span><span>AI mode: Mock</span></div> + </div> + </div> + </section> + + <section className="capabilities" id="architecture" aria-labelledby="architecture-title"> + <div className="section-heading"> + <h2 id="architecture-title">A polished surface over explicit invariants.</h2> + <p>One modular monolith owns identity, editorial state, media and AI boundaries. Every advertised path is designed to be persisted, authorized and tested.</p> + </div> + <div className="capability-grid"> + {capabilities.map(([number, title, copy]) => ( + <article className="capability-card" key={number}> + <span>{number}</span><h3>{title}</h3><p>{copy}</p> + </article> + ))} + </div> + </section> + </main> + + <footer className="site-footer"><span>AutoBlog CMS · portfolio engineering release</span><span>Next.js · TypeScript · libSQL</span></footer> + </div> + ); +} diff --git a/app/preview/[workspaceSlug]/[postSlug]/page.tsx b/app/preview/[workspaceSlug]/[postSlug]/page.tsx new file mode 100644 index 0000000..8d08350 --- /dev/null +++ b/app/preview/[workspaceSlug]/[postSlug]/page.tsx @@ -0,0 +1,30 @@ +import { notFound } from 'next/navigation'; +import Link from 'next/link'; + +import { getEditorialService } from '@/src/modules/editorial/service'; +import { AppError } from '@/src/platform/observability/errors'; + +export const revalidate = 60; + +export default async function PublicPreviewPage({ params }: Readonly<{ params: Promise<{ workspaceSlug: string; postSlug: string }> }>) { + const { workspaceSlug, postSlug } = await params; + let post; + try { + post = await getEditorialService().publicPost(workspaceSlug, postSlug); + } catch (error) { + if (error instanceof AppError && error.code === 'NOT_FOUND') notFound(); + throw error; + } + return ( + <main id="main-content" className="public-article"> + <nav aria-label="Preview context"><Link href="/">AutoBlog</Link><span>Published preview</span></nav> + <article> + <p className="eyebrow">{post.workspaceName}</p> + <h1>{post.title}</h1> + <p className="public-standfirst">{post.excerpt}</p> + <div className="public-content">{post.content}</div> + <footer>Immutable revision {post.revisionId} · Published {post.publishedAt.toLocaleDateString('en-GB')}</footer> + </article> + </main> + ); +} diff --git a/app/sign-in/page.tsx b/app/sign-in/page.tsx new file mode 100644 index 0000000..1f6733f --- /dev/null +++ b/app/sign-in/page.tsx @@ -0,0 +1,27 @@ +import Link from 'next/link'; + +import { DEMO_IDENTITIES } from '@/src/modules/identity/demo'; +import { getEnvironment } from '@/src/platform/config/env'; +import { SignInForm } from '@/src/ui/patterns/sign-in-form'; + +export const dynamic = 'force-dynamic'; + +export default function SignInPage() { + const environment = getEnvironment(); + return ( + <main id="main-content" className="auth-shell"> + <section className="auth-intro" aria-labelledby="sign-in-title"> + <Link className="brand" href="/" aria-label="AutoBlog CMS home"> + <span className="brand-mark" aria-hidden="true">A</span><span>AutoBlog CMS</span> + </Link> + <div> + <p className="eyebrow">Bounded recruiter demo</p> + <h1 id="sign-in-title">Choose a seat at the editorial table.</h1> + <p className="hero-copy">Each role is a real database identity with a revocable HTTP-only session. The demo workspace is isolated from every configured workspace.</p> + </div> + <p className="auth-note">Demo data uses the production command, policy and repository path.</p> + </section> + <SignInForm demoEnabled={environment.DEMO_ENABLED} identities={DEMO_IDENTITIES} /> + </main> + ); +} diff --git a/app/template.tsx b/app/template.tsx deleted file mode 100644 index 422a3e8..0000000 --- a/app/template.tsx +++ /dev/null @@ -1,10 +0,0 @@ -"use client"; -import switchTheme from "@/utils/switch-theme"; -import { PropsWithChildren, useEffect } from "react"; - -export default function Template({ children }: PropsWithChildren) { - useEffect(() => { - switchTheme(false); - }, []); - return children; -} diff --git a/auto_cleaner_restore.bat b/auto_cleaner_restore.bat deleted file mode 100644 index 5fdd18f..0000000 --- a/auto_cleaner_restore.bat +++ /dev/null @@ -1 +0,0 @@ -npm install diff --git a/bun.lock b/bun.lock index 32c7f85..3ea14c8 100644 --- a/bun.lock +++ b/bun.lock @@ -5,103 +5,190 @@ "": { "name": "dashboard", "dependencies": { - "@google/generative-ai": "^0.21.0", - "@radix-ui/react-alert-dialog": "^1.1.6", - "@radix-ui/react-avatar": "^1.1.3", - "@radix-ui/react-checkbox": "^1.1.4", - "@radix-ui/react-collapsible": "^1.1.2", - "@radix-ui/react-context-menu": "^2.2.4", - "@radix-ui/react-dialog": "^1.1.6", - "@radix-ui/react-dropdown-menu": "^2.1.6", - "@radix-ui/react-icons": "^1.3.2", - "@radix-ui/react-label": "^2.1.2", - "@radix-ui/react-popover": "^1.1.6", - "@radix-ui/react-radio-group": "^1.2.3", - "@radix-ui/react-scroll-area": "^1.2.3", - "@radix-ui/react-select": "^2.1.4", - "@radix-ui/react-separator": "^1.1.2", - "@radix-ui/react-slot": "^1.1.2", - "@radix-ui/react-switch": "^1.1.2", - "@radix-ui/react-tabs": "^1.1.3", - "@radix-ui/react-toast": "^1.2.4", - "@radix-ui/react-toggle": "^1.1.2", - "@radix-ui/react-toggle-group": "^1.1.2", - "@radix-ui/react-tooltip": "^1.1.6", - "@vercel/speed-insights": "^1.1.0", - "class-variance-authority": "^0.7.1", - "cloudinary": "^2.5.1", - "clsx": "^2.1.1", - "cmdk": "^1.0.0", - "date-fns": "^3.6.0", - "framer-motion": "^12.4.1", - "html2canvas": "^1.4.1", - "lucide-react": "^0.469.0", - "mongoose": "^8.9.3", - "next": "^15.1.6", - "next-cloudinary": "^6.16.0", - "react": "^19.0.0", - "react-day-picker": "^8.10.1", - "react-dom": "^19.0.0", - "react-icons": "^5.4.0", - "react-loading-indicators": "^1.0.0", - "react-markdown": "^9.0.3", - "tailwind-merge": "^2.6.0", - "tailwindcss-animate": "^1.0.7", - "vaul": "^1.1.2", + "@google/genai": "2.13.0", + "@libsql/client": "0.17.4", + "better-auth": "1.6.23", + "drizzle-orm": "0.45.2", + "next": "16.2.11", + "react": "19.2.8", + "react-dom": "19.2.8", + "sharp": "0.35.3", + "zod": "4.4.3", }, "devDependencies": { - "@eslint/eslintrc": "^3", - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", - "eslint": "^9", - "eslint-config-next": "15.1.1", - "postcss": "^8", - "tailwindcss": "^3.4.1", - "typescript": "^5", + "@axe-core/playwright": "4.12.1", + "@playwright/test": "1.61.1", + "@types/bun": "1.3.14", + "@types/node": "24.10.13", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", + "drizzle-kit": "0.31.10", + "eslint": "9.39.5", + "eslint-config-next": "16.2.11", + "lighthouse": "13.4.1", + "typescript": "6.0.3", + "vitest": "4.1.10", }, }, }, + "overrides": { + "esbuild": "0.28.1", + "picomatch": "4.0.5", + "postcss": "8.5.21", + "sharp": "0.35.3", + }, "packages": { - "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + "@apm-js-collab/code-transformer": ["@apm-js-collab/code-transformer@0.18.0", "", { "dependencies": { "@types/estree": "^1.0.8", "astring": "^1.9.0", "esquery": "^1.7.0", "meriyah": "^6.1.4", "semifies": "^1.0.0", "source-map": "^0.6.0" }, "bin": { "code-transformer": "cli.js" } }, "sha512-aN3Oq8r1J3gPJtCwErP664gM0+HhM1I1lujPr9TMTCcEl/joQQbpGpeMdts9B1+W2wHMsvioDMv5F4PvMWE6gw=="], + + "@apm-js-collab/code-transformer-bundler-plugins": ["@apm-js-collab/code-transformer-bundler-plugins@0.7.2", "", { "dependencies": { "@apm-js-collab/code-transformer": "^0.18.0", "es-module-lexer": "^2.1.0", "magic-string": "^0.30.21", "module-details-from-path": "^1.0.4" } }, "sha512-GvpKWDmzBFzbtVElU+tEDMDYyMY8SnMH4NNwwlUQioNHZvE6sSuHEqNkfU3iYhWL+/XO5Ej+MzhVJoqM10NYCQ=="], + + "@apm-js-collab/tracing-hooks": ["@apm-js-collab/tracing-hooks@0.13.0", "", { "dependencies": { "@apm-js-collab/code-transformer": "^0.18.0", "debug": "^4.4.1", "module-details-from-path": "^1.0.4" } }, "sha512-mTvWz9rnQwx1U3h0XPTHaX7bgfkpipLLTQyjlC2cdhQpQEuoLT0AGzoydeoq2NxfEVv6fWOOETcSbb2nptleyw=="], + + "@axe-core/playwright": ["@axe-core/playwright@4.12.1", "", { "dependencies": { "axe-core": "~4.12.1" }, "peerDependencies": { "playwright-core": ">= 1.0.0" } }, "sha512-rMd7xriptqKpP+w5265i4Hdkv2X5kbu6uiBi/B2I7uf3hieRBM3qDCfaKPtxfiYb2mKXfF+yLODJwIx+Jv1GDw=="], + + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], + + "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], + + "@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], + + "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], + + "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + + "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], + + "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], + + "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + + "@better-auth/core": ["@better-auth/core@1.6.23", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.7", "jose": "^6.1.0", "kysely": "^0.28.5 || ^0.29.0", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types", "@opentelemetry/api"] }, "sha512-beEhOs0uVeOxYOZKUfIEBd/nQV2Bd4/6wyLxZ0OFkn6CMTK2Vi+hXuZLnyPBeB6RdHpebEoJWiHqwHxBIxgPDQ=="], + + "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.23", "", { "peerDependencies": { "@better-auth/core": "^1.6.23", "@better-auth/utils": "0.4.2", "drizzle-orm": "^0.45.2" }, "optionalPeers": ["drizzle-orm"] }, "sha512-2+/PTVfIP9E7iz6af8TB3lhnowHUj9ljC66kECmHaFEdUqPgzHoWux9epotKwO7XDg2ui4ttWQ8CMeNFLvQeKQ=="], + + "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.23", "", { "peerDependencies": { "@better-auth/core": "^1.6.23", "@better-auth/utils": "0.4.2", "kysely": "^0.28.17 || ^0.29.0" }, "optionalPeers": ["kysely"] }, "sha512-zbNJsMbG09exfkGyvFqBLLqWoMPAUWjxCuUnEK5AsjbYoZeIjj/QGZgdf4CapVWryKxjA9Q6Jlr6fbiPpC3VAg=="], + + "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.6.23", "", { "peerDependencies": { "@better-auth/core": "^1.6.23", "@better-auth/utils": "0.4.2" } }, "sha512-krIiR0pIVkaKlAzm690n5bcMW4NGbqeMg0HQSD9fz/KcQF/eWLqcq9gG/BhHTj2i/y96qH+W5JWPmaSOS5iTgQ=="], + + "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.6.23", "", { "peerDependencies": { "@better-auth/core": "^1.6.23", "@better-auth/utils": "0.4.2", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-7+QdevitGlKBbP6JbiSk5SBnzPsKV/mDrQBGBn8hwByQLeJwqpqbuBPw7ZI8vzUlFfAAnyFiqwP3Eb8mxnp7pA=="], + + "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.6.23", "", { "peerDependencies": { "@better-auth/core": "^1.6.23", "@better-auth/utils": "0.4.2", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-2qSdzidq4tkb1eS5TTqb4Nzg0mdZWm3Qky9SYeXeb8PpVQbC2sxqJhEM5mK7y12uU6I8hc64wO9f7AFVNL+6UQ=="], + + "@better-auth/telemetry": ["@better-auth/telemetry@1.6.23", "", { "peerDependencies": { "@better-auth/core": "^1.6.23", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1" } }, "sha512-/R2Kb+z2BpDOOWwVHqOk+c0VNpuwfCv4Hp5Yr9003WIZPax/zyNraGLB9CFE8qF2gZW8Dsz419k4I8CPrGzpDA=="], + + "@better-auth/utils": ["@better-auth/utils@0.4.2", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A=="], + + "@better-fetch/fetch": ["@better-fetch/fetch@1.3.1", "", {}, "sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g=="], + + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + + "@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], + + "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], + + "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], - "@cloudinary-util/types": ["@cloudinary-util/types@1.5.10", "", {}, "sha512-n5lrm7SdAXhgWEbkSJKHZGnaoO9G/g4WYS6HYnq/k4nLj79sYfQZOoKjyR8hF2iyLRdLkT+qlk68RNFFv5tKew=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], - "@cloudinary-util/url-loader": ["@cloudinary-util/url-loader@5.10.4", "", { "dependencies": { "@cloudinary-util/types": "1.5.10", "@cloudinary-util/util": "3.3.2", "@cloudinary/url-gen": "1.15.0", "zod": "^3.22.4" } }, "sha512-gHkdvOaV+rlcwuIT7Vqd0ts/H5bsH4+bwFten/gIZ8oRjzdTBvgIY3R6F8bbJt0pFIEfpFEQLe4rPkl0NNqEWg=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], - "@cloudinary-util/util": ["@cloudinary-util/util@4.0.0", "", {}, "sha512-S4xcou/3A7l5o+bcKlw2VHBNgwups7/0lbVDT/cO5YmtrcEYXgj6LGmwnjvpTm/x571VPVN8x5jWdT3rLZiKJQ=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], - "@cloudinary/transformation-builder-sdk": ["@cloudinary/transformation-builder-sdk@1.16.1", "", { "dependencies": { "@cloudinary/url-gen": "^1.7.0" } }, "sha512-Mh1qYMkoDxSAzbt0qY9NJaZrdH/vFBcrpeVWmbTXbPVDZHLaaLyJ2+RDFGger5lycbrehPLoNp2hh22BvhkvbQ=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], - "@cloudinary/url-gen": ["@cloudinary/url-gen@1.15.0", "", { "dependencies": { "@cloudinary/transformation-builder-sdk": "^1.10.0" } }, "sha512-bjU67eZxLUgoRy/Plli4TQio7q6P31OYqnEgXxeN9TKXrzr6h0DeEdIUhKI9gy3HkEBWXWWJIPh7j7gkOJPnyA=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], - "@emnapi/runtime": ["@emnapi/runtime@1.3.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.4.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA=="], + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], - "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], - "@eslint/config-array": ["@eslint/config-array@0.19.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], - "@eslint/core": ["@eslint/core@0.10.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], - "@eslint/eslintrc": ["@eslint/eslintrc@3.2.0", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w=="], + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], - "@eslint/js": ["@eslint/js@9.19.0", "", {}, "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ=="], + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], - "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], - "@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.5", "", { "dependencies": { "@eslint/core": "^0.10.0", "levn": "^0.4.1" } }, "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A=="], + "@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="], - "@floating-ui/core": ["@floating-ui/core@1.6.9", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw=="], + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], - "@floating-ui/dom": ["@floating-ui/dom@1.6.13", "", { "dependencies": { "@floating-ui/core": "^1.6.0", "@floating-ui/utils": "^0.2.9" } }, "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w=="], + "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], - "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.2", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A=="], + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.6", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.3.0", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA=="], - "@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="], + "@eslint/js": ["@eslint/js@9.39.5", "", {}, "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A=="], - "@google/generative-ai": ["@google/generative-ai@0.21.0", "", {}, "sha512-7XhUbtnlkSEZK15kN3t+tzIMxsbKm/dSkKBFalj+20NvPKe1kBY7mR2P7vuijEn+f06z5+A8bVGKO0v39cr6Wg=="], + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + + "@formatjs/ecma402-abstract": ["@formatjs/ecma402-abstract@2.3.6", "", { "dependencies": { "@formatjs/fast-memoize": "2.2.7", "@formatjs/intl-localematcher": "0.6.2", "decimal.js": "^10.4.3", "tslib": "^2.8.0" } }, "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw=="], + + "@formatjs/fast-memoize": ["@formatjs/fast-memoize@2.2.7", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ=="], + + "@formatjs/icu-messageformat-parser": ["@formatjs/icu-messageformat-parser@2.11.4", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "@formatjs/icu-skeleton-parser": "1.8.16", "tslib": "^2.8.0" } }, "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw=="], + + "@formatjs/icu-skeleton-parser": ["@formatjs/icu-skeleton-parser@1.8.16", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "tslib": "^2.8.0" } }, "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ=="], + + "@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.6.2", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA=="], + + "@google/genai": ["@google/genai@2.13.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-GM7C8Kaomvjz05x5JEO6+l3d/pciL9LxAG9dUjJLD7nTPZ9X0Cfsf2Z7eET6UjgWyUmxXCHtYnQoQ77F9+ZIOQ=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], @@ -109,269 +196,311 @@ "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], - "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.1", "", {}, "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA=="], + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.2" }, "os": "darwin", "cpu": "arm64" }, "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.2" }, "os": "darwin", "cpu": "x64" }, "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w=="], + + "@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.3", "", { "dependencies": { "@img/sharp-wasm32": "0.35.3" }, "os": "freebsd" }, "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw=="], - "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.2", "", { "os": "linux", "cpu": "arm" }, "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ=="], - "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA=="], - "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="], + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw=="], - "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="], + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.2", "", { "os": "linux", "cpu": "none" }, "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w=="], - "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="], + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ=="], - "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="], + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w=="], - "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.0.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA=="], + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw=="], - "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="], + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ=="], - "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="], + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.2" }, "os": "linux", "cpu": "arm" }, "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA=="], - "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="], + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ=="], - "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="], + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.2" }, "os": "linux", "cpu": "ppc64" }, "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA=="], - "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="], + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.2" }, "os": "linux", "cpu": "none" }, "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ=="], - "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.0.4" }, "os": "linux", "cpu": "s390x" }, "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q=="], + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.2" }, "os": "linux", "cpu": "s390x" }, "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw=="], - "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="], + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA=="], - "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="], + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w=="], - "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="], + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg=="], - "@img/sharp-wasm32": ["@img/sharp-wasm32@0.33.5", "", { "dependencies": { "@emnapi/runtime": "^1.2.0" }, "cpu": "none" }, "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg=="], + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.3", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w=="], - "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.33.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ=="], + "@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.3", "", { "dependencies": { "@img/sharp-wasm32": "0.35.3" }, "cpu": "none" }, "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q=="], - "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w=="], - "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw=="], - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.3", "", { "os": "win32", "cpu": "x64" }, "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="], + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], + "@libsql/client": ["@libsql/client@0.17.4", "", { "dependencies": { "@libsql/core": "^0.17.4", "@libsql/hrana-client": "^0.10.0", "js-base64": "^3.7.5", "libsql": "^0.5.28", "promise-limit": "^2.7.0" } }, "sha512-lYayFWasDV78A+TjlEhr6ubb3odBV6OHjb+wdp8VQcyWWAEIjuwbCHaraEUS4m4yWoo0BvZo96It4VdzZRmRWw=="], - "@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.1.9", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw=="], + "@libsql/core": ["@libsql/core@0.17.4", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-LqF9gIvnJ38nmAH1y/ChizHqDO/MO1wLgA96XrraulEEbqXxLjleSH92YWTolbuJKgPUmGu4aJk9W3UnAcxLOQ=="], - "@next/env": ["@next/env@15.1.6", "", {}, "sha512-d9AFQVPEYNr+aqokIiPLNK/MTyt3DWa/dpKveiAaVccUadFbhFEvY6FXYX2LJO2Hv7PHnLBu2oWwB4uBuHjr/w=="], + "@libsql/darwin-arm64": ["@libsql/darwin-arm64@0.5.29", "", { "os": "darwin", "cpu": "arm64" }, "sha512-K+2RIB1OGFPYQbfay48GakLhqf3ArcbHqPFu7EZiaUcRgFcdw8RoltsMyvbj5ix2fY0HV3Q3Ioa/ByvQdaSM0A=="], - "@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.1.1", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-yACipsS2HI9ktcfz/1UsO0/sDbVjXWKDE/fzzMLnYES+K4KJyqHChyBQeaxiK7/NDnxrdk7Ow2i9LRm3ZTAWow=="], + "@libsql/darwin-x64": ["@libsql/darwin-x64@0.5.29", "", { "os": "darwin", "cpu": "x64" }, "sha512-OtT+KFHsKFy1R5FVadr8FJ2Bb1mghtXTyJkxv0trocq7NuHntSki1eUbxpO5ezJesDvBlqFjnWaYYY516QNLhQ=="], - "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.1.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-u7lg4Mpl9qWpKgy6NzEkz/w0/keEHtOybmIl0ykgItBxEM5mYotS5PmqTpo+Rhg8FiOiWgwr8USxmKQkqLBCrw=="], + "@libsql/hrana-client": ["@libsql/hrana-client@0.10.0", "", { "dependencies": { "@libsql/isomorphic-ws": "^0.1.5", "js-base64": "^3.7.5" } }, "sha512-OoA4EMqRAC7kn7V2P6EQqRcpZf2W+AjsNIyCizBg339Tq/aMC7sRnzs3SklderhmQWAqEzvv8A2vhxVmWpkVvw=="], - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.1.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-x1jGpbHbZoZ69nRuogGL2MYPLqohlhnT9OCU6E6QFewwup+z+M6r8oU47BTeJcWsF2sdBahp5cKiAcDbwwK/lg=="], + "@libsql/isomorphic-ws": ["@libsql/isomorphic-ws@0.1.5", "", { "dependencies": { "@types/ws": "^8.5.4", "ws": "^8.13.0" } }, "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg=="], - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-jar9sFw0XewXsBzPf9runGzoivajeWJUc/JkfbLTC4it9EhU8v7tCRLH7l5Y1ReTMN6zKJO0kKAGqDk8YSO2bg=="], + "@libsql/linux-arm-gnueabihf": ["@libsql/linux-arm-gnueabihf@0.5.29", "", { "os": "linux", "cpu": "arm" }, "sha512-CD4n4zj7SJTHso4nf5cuMoWoMSS7asn5hHygsDuhRl8jjjCTT3yE+xdUvI4J7zsyb53VO5ISh4cwwOtf6k2UhQ=="], - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-+n3u//bfsrIaZch4cgOJ3tXCTbSxz0s6brJtU3SzLOvkJlPQMJ+eHVRi6qM2kKKKLuMY+tcau8XD9CJ1OjeSQQ=="], + "@libsql/linux-arm-musleabihf": ["@libsql/linux-arm-musleabihf@0.5.29", "", { "os": "linux", "cpu": "arm" }, "sha512-2Z9qBVpEJV7OeflzIR3+l5yAd4uTOLxklScYTwpZnkm2vDSGlC1PRlueLaufc4EFITkLKXK2MWBpexuNJfMVcg=="], - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-SpuDEXixM3PycniL4iVCLyUyvcl6Lt0mtv3am08sucskpG0tYkW1KlRhTgj4LI5ehyxriVVcfdoxuuP8csi3kQ=="], + "@libsql/linux-arm64-gnu": ["@libsql/linux-arm64-gnu@0.5.29", "", { "os": "linux", "cpu": "arm64" }, "sha512-gURBqaiXIGGwFNEaUj8Ldk7Hps4STtG+31aEidCk5evMMdtsdfL3HPCpvys+ZF/tkOs2MWlRWoSq7SOuCE9k3w=="], - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-L4druWmdFSZIIRhF+G60API5sFB7suTbDRhYWSjiw0RbE+15igQvE2g2+S973pMGvwN3guw7cJUjA/TmbPWTHQ=="], + "@libsql/linux-arm64-musl": ["@libsql/linux-arm64-musl@0.5.29", "", { "os": "linux", "cpu": "arm64" }, "sha512-fwgYZ0H8mUkyVqXZHF3mT/92iIh1N94Owi/f66cPVNsk9BdGKq5gVpoKO+7UxaNzuEH1roJp2QEwsCZMvBLpqg=="], - "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.1.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-s8w6EeqNmi6gdvM19tqKKWbCyOBvXFbndkGHl+c9YrzsLARRdCHsD9S1fMj8gsXm9v8vhC8s3N8rjuC/XrtkEg=="], + "@libsql/linux-x64-gnu": ["@libsql/linux-x64-gnu@0.5.29", "", { "os": "linux", "cpu": "x64" }, "sha512-y14V0vY0nmMC6G0pHeJcEarcnGU2H6cm21ZceRkacWHvQAEhAG0latQkCtoS2njFOXiYIg+JYPfAoWKbi82rkg=="], - "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.1.6", "", { "os": "win32", "cpu": "x64" }, "sha512-6xomMuu54FAFxttYr5PJbEfu96godcxBTRk1OhAvJq0/EnmFU/Ybiax30Snis4vdWZ9LGpf7Roy5fSs7v/5ROQ=="], + "@libsql/linux-x64-musl": ["@libsql/linux-x64-musl@0.5.29", "", { "os": "linux", "cpu": "x64" }, "sha512-gquqwA/39tH4pFl+J9n3SOMSymjX+6kZ3kWgY3b94nXFTwac9bnFNMffIomgvlFaC4ArVqMnOZD3nuJ3H3VO1w=="], - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + "@libsql/win32-x64-msvc": ["@libsql/win32-x64-msvc@0.5.29", "", { "os": "win32", "cpu": "x64" }, "sha512-4/0CvEdhi6+KjMxMaVbFM2n2Z44escBRoEYpR+gZg64DdetzGnYm8mcNLcoySaDJZNaBd6wz5DNdgRmcI4hXcg=="], - "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="], - "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="], - "@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="], + "@next/env": ["@next/env@16.2.11", "", {}, "sha512-0do5A3BJ2gxWr0ZCMcD6BhW+e595jyxdTl3rXTS6lOtD8ektMiW6CO+EPwt1Eca1DBnm90r/7GdiKWBKxH++DA=="], - "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + "@next/eslint-plugin-next": ["@next/eslint-plugin-next@16.2.11", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-vMEf/aXOpzFFdtIvFYOnIDPKb0xBbrXONsz83CcKdRrekfxNdL8PNkq5qHqAHSXVlIifnX68LOMaxr3z5PkeLQ=="], - "@radix-ui/number": ["@radix-ui/number@1.1.0", "", {}, "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ=="], + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wryL4pjKmDwGv2ox6+GZDFxvmtSRLqApBR8kL1j4+vhB7Z5vJC/zAnXpiR9Xkfzl0AS8WLMnsuGV/UKI67/rrw=="], - "@radix-ui/primitive": ["@radix-ui/primitive@1.1.1", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="], + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-aZl2j4f/fLyjQvOhv0Oe9UaMAQHolYpKhctsoYzplSumKJKPUmgjcf6545aBtysLTcu994TREd0+pSgNE4ohmg=="], - "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dialog": "1.1.6", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ=="], + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-5jEriyEnH/LWFy27L2ZG0XaLlyEJIjhsImEsiS9P563PKEVp2BVups/xfOucIrsvVntp11oNcZwjHvaDPYVB5g=="], - "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg=="], + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-eIjcpx2fnnFSSkZDbTxy74KnokUXDjfoLClpWelfgHLf621aTqswhwXQ7GkD5K5rplrS6LZ/Bj+mVuvzluBOEg=="], - "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.3", "", { "dependencies": { "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Paen00T4P8L8gd9bNsRMw7Cbaz85oxiv+hzomsRZgFm2byltPFDtfcoqlWJ8GyZlIBWgLssJlzLCnKU0G0302g=="], + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.11", "", { "os": "linux", "cpu": "x64" }, "sha512-8WgzpaWMs46qJT9kiV47cje86L0x/Mu9t8/Gwj+pnbgW3rETVfCnaScPjlYUwNScpOozdcIMHWmAvuZJUonR2w=="], - "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.1.4", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wP0CPAHq+P5I4INKe3hJrIa1WoNqqrejzW+zoU0rOvo1b9gDEJJFl2rYfO1PYJUQCc2H1WZxIJmyv9BS8i5fLw=="], + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.11", "", { "os": "linux", "cpu": "x64" }, "sha512-I3UgPds7G4ZYnTb/H+5GBGuUT2DhAk6j0mL6A4s63RjFs74wB2hOWP0vaxsK+3NJraExt3eYEPQ/UtT0x/64Nw=="], - "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-PliMB63vxz7vggcyq0IxNYk8vGDrLXVWw4+W4B8YnwI1s18x7YZYqlG9PLX7XxAJUi0g2DxP4XKJMFHh/iVh9A=="], + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-n89CjtcThnjrwgJMAiI5xbqwLY51zvwC9tSlArmVndAJLYVl9T9UAdlkXTmZvE++idoXe8KdglQlhNRdUp1c6g=="], - "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA=="], + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.11", "", { "os": "win32", "cpu": "x64" }, "sha512-md8CLNggS1Dx9pUgApzps5uAf+N8GN9xywzmNx9vHAWo94HtBwCCqkSnhIrdfQe83Dhz8Lfo/20Nb1Zxal092w=="], + + "@noble/ciphers": ["@noble/ciphers@2.2.0", "", {}, "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA=="], + + "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], - "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="], + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - "@radix-ui/react-context": ["@radix-ui/react-context@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q=="], + "@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="], - "@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.2.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-menu": "2.1.5", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-MY5PFCwo/ICaaQtpQBQ0g19AyjzI0mhz+a2GUWA2pJf4XFkvglAdcgDV2Iqm+lLbXn8hb+6rbLgcmRtc6ImPvg=="], + "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], - "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.2", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw=="], + "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.220.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w=="], - "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg=="], + "@opentelemetry/core": ["@opentelemetry/core@2.10.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ=="], - "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg=="], + "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.220.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.220.0", "import-in-the-middle": "^3.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw=="], - "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-menu": "2.1.6", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA=="], + "@opentelemetry/resources": ["@opentelemetry/resources@2.10.0", "", { "dependencies": { "@opentelemetry/core": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA=="], - "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg=="], + "@opentelemetry/sdk-trace": ["@opentelemetry/sdk-trace@2.10.0", "", { "dependencies": { "@opentelemetry/core": "2.10.0", "@opentelemetry/resources": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ=="], - "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA=="], + "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.10.0", "", { "dependencies": { "@opentelemetry/core": "2.10.0", "@opentelemetry/resources": "2.10.0", "@opentelemetry/sdk-trace": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ=="], - "@radix-ui/react-icons": ["@radix-ui/react-icons@1.3.2", "", { "peerDependencies": { "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" } }, "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g=="], + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="], - "@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="], + "@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="], - "@radix-ui/react-label": ["@radix-ui/react-label@2.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zo1uGMTaNlHehDyFQcDZXRJhUPDuukcnHz0/jnrup0JA6qL+AFpAnty+7VKa9esuU5xTblAZzTGYJKSKaBxBhw=="], + "@paulirish/trace_engine": ["@paulirish/trace_engine@0.0.65", "", { "dependencies": { "legacy-javascript": "latest", "third-party-web": "latest" } }, "sha512-Qsm6F5C8xf6ZzQXbQc2+wcpe6sggfs/gvc/ytqSurdvYg3kyW0ECHCqE0CWBKZpqgjVfPNX9c7SCS3r2nEIRGg=="], - "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-dismissable-layer": "1.1.4", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.1", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.1", "@radix-ui/react-portal": "1.1.3", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-roving-focus": "1.1.1", "@radix-ui/react-slot": "1.1.1", "@radix-ui/react-use-callback-ref": "1.1.0", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-uH+3w5heoMJtqVCgYOtYVMECk1TOrkUn0OG0p5MqXC0W2ppcuVeESbou8PTHoqAjbdTEK19AGXBWcEtR5WpEQg=="], + "@playwright/test": ["@playwright/test@1.61.1", "", { "dependencies": { "playwright": "1.61.1" }, "bin": { "playwright": "cli.js" } }, "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig=="], - "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.2", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg=="], + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], - "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.2", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA=="], + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], - "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA=="], + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="], - "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg=="], + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.1", "", {}, "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg=="], - "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.2", "", { "dependencies": { "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w=="], + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="], - "@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.2.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-roving-focus": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-xtCsqt8Rp09FK50ItqEqTJ7Sxanz8EM8dnkVIhJrc/wkMMomSmXHvYbhv3E7Zx4oXh98aaLt9W679SUYXg4IDA=="], + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], - "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw=="], + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], - "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.3", "", { "dependencies": { "@radix-ui/number": "1.1.0", "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-l7+NNBfBYYJa9tNqVcP2AGvxdE3lmE6kFTBXdvHgUaZuy+4wGCL1Cl2AfaR7RKyimj7lZURGLwFO59k4eBnDJQ=="], + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], - "@radix-ui/react-select": ["@radix-ui/react-select@2.1.5", "", { "dependencies": { "@radix-ui/number": "1.1.0", "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-dismissable-layer": "1.1.4", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.1", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.1", "@radix-ui/react-portal": "1.1.3", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-slot": "1.1.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-eVV7N8jBXAXnyrc+PsOF89O9AfVgGnbLxUtBb0clJ8y8ENMWLARGMI/1/SBRLz7u4HqxLgN71BJ17eono3wcjA=="], + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="], - "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ=="], + "@puppeteer/browsers": ["@puppeteer/browsers@3.0.6", "", { "dependencies": { "modern-tar": "^0.7.6", "yargs": "^18.0.0" }, "peerDependencies": { "proxy-agent": ">=8.0.1", "yauzl": "^2.10.0 || ^3.4.0" }, "optionalPeers": ["proxy-agent", "yauzl"], "bin": { "browsers": "lib/main-cli.js" } }, "sha512-B/gKoqlFkzhvzsI6jo9K1cZz9o5ypviVv/xu8CwA4grZzyVwN+XfkT+tu8T1zrauuEXv6VhS2oGX+6NL95WcKA=="], - "@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.5", "", { "os": "android", "cpu": "arm64" }, "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ=="], - "@radix-ui/react-switch": ["@radix-ui/react-switch@1.1.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zGukiWHjEdBCRyXvKR6iXAQG6qXm2esuAD6kDOi9Cn+1X6ev3ASo4+CsYaD6Fov9r/AQFekqnD/7+V0Cs6/98g=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw=="], - "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-roving-focus": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng=="], + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g=="], - "@radix-ui/react-toast": ["@radix-ui/react-toast@1.2.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.4", "@radix-ui/react-portal": "1.1.3", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ZzUsAaOx8NdXZZKcFNDhbSlbsCUy8qQWmzTdgrlrhhZAOx2ofLtKrBDW9fkqhFvXgmtv560Uj16pkLkqML7SHA=="], + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA=="], - "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lntKchNWx3aCHuWKiDY+8WudiegQvBpDRAYL8dKLRvKEH8VOpl0XX6SSU/bUBqIRJbcTy4+MW06Wv8vgp10rzQ=="], + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.5", "", { "os": "linux", "cpu": "arm" }, "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw=="], - "@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-roving-focus": "1.1.2", "@radix-ui/react-toggle": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-JBm6s6aVG/nwuY5eadhU2zDi/IwYS0sDM5ZWb4nymv/hn3hZdkw+gENn0LP4iY1yCd7+bgJaCwueMYJIU3vk4A=="], + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q=="], - "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.1.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.4", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.1", "@radix-ui/react-portal": "1.1.3", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-slot": "1.1.1", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ss0s80BC0+g0+Zc53MvilcnTYSOi4mSuFWBPYPuTOFGjx+pUU+ZrmamMNwS56t8MTFlniA5ocjd4jYm/CdhbOg=="], + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA=="], - "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw=="], + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg=="], - "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="], + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA=="], - "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw=="], + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ=="], - "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w=="], + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg=="], - "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og=="], + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.5", "", { "os": "none", "cpu": "arm64" }, "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw=="], - "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.0", "", { "dependencies": { "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ=="], + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.5", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA=="], - "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw=="], + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw=="], - "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.1.1", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg=="], + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA=="], - "@radix-ui/rect": ["@radix-ui/rect@1.1.0", "", {}, "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], - "@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.10.5", "", {}, "sha512-kkKUDVlII2DQiKy7UstOR1ErJP8kUKAQ4oa+SQtM0K+lPdmmjj0YnnxBgtTVYH7mUKtbsxeFC9y0AmK7Yb78/A=="], + "@sentry/conventions": ["@sentry/conventions@0.16.0", "", {}, "sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ=="], + + "@sentry/core": ["@sentry/core@10.67.0", "", { "dependencies": { "@sentry/conventions": "^0.16.0" } }, "sha512-b6U3pJ8AUvN9aouq0vl+VZI8KT8RslBsfGMFuNwRr313zOmdmFJBZqTiUw9VGgJ2jGKxLO9alm9rlxBfX4hf+w=="], + + "@sentry/node": ["@sentry/node@10.67.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.1", "@opentelemetry/instrumentation": "^0.220.0", "@opentelemetry/sdk-trace-base": "^2.9.0", "@sentry/conventions": "^0.16.0", "@sentry/core": "10.67.0", "@sentry/node-core": "10.67.0", "@sentry/opentelemetry": "10.67.0", "@sentry/server-utils": "10.67.0", "import-in-the-middle": "^3.0.0" } }, "sha512-SFKpZGqOCEFSmP93NdDP6ikZp4NS7A/JR8+2ofK3jF6Y9Vyox7pX0pxdOnPLpFcPtMybMahfWqSAWJvsFs4RmA=="], + + "@sentry/node-core": ["@sentry/node-core@10.67.0", "", { "dependencies": { "@sentry/conventions": "^0.16.0", "@sentry/core": "10.67.0", "@sentry/opentelemetry": "10.67.0", "import-in-the-middle": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/core", "@opentelemetry/exporter-trace-otlp-http", "@opentelemetry/instrumentation", "@opentelemetry/sdk-trace-base"] }, "sha512-dBHHRwZyan1pOnFJ+sNBvR8TkXbZAfZU/jpxmALS3JZ2/8AGR7cQKL+b7SleKuJ7iUDZyklN3Nqi0i5JkcA+HA=="], + + "@sentry/opentelemetry": ["@sentry/opentelemetry@10.67.0", "", { "dependencies": { "@sentry/conventions": "^0.16.0", "@sentry/core": "10.67.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" } }, "sha512-oLTOrAK1rOqmYRktOJZwz37B1seXPx1W2FTMVtzTVNjMFA/LZwGzePeZzhUOgzZgfLHixMd/ceWtGqoxAndcjQ=="], + + "@sentry/server-utils": ["@sentry/server-utils@10.67.0", "", { "dependencies": { "@apm-js-collab/code-transformer-bundler-plugins": "^0.7.1", "@apm-js-collab/tracing-hooks": "^0.13.0", "@sentry/conventions": "^0.16.0", "@sentry/core": "10.67.0" } }, "sha512-GQ9t+RSTx5s3b/aZrLFuL4nrwPLMah5NiZk5cjxJmgmOSgm3nMdO8gdqCedDch5B13F3YsxtaQYjSJnzLz8M5A=="], - "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], - "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], - "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], - "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], - "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], - "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + "@types/node": ["@types/node@24.10.13", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg=="], - "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + "@types/pg": ["@types/pg@8.6.1", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w=="], - "@types/node": ["@types/node@20.17.16", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-vOTpLduLkZXePLxHiHsBLp98mHGnl8RptV4YAO3HfKO5UHjDvySGbxKtpYfy8Sx5+WKcgc45qNreJJRVM3L6mw=="], + "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], - "@types/react": ["@types/react@19.0.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw=="], + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], - "@types/react-dom": ["@types/react-dom@19.0.3", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA=="], + "@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="], - "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@types/webidl-conversions": ["@types/webidl-conversions@7.0.3", "", {}, "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.65.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.65.0", "@typescript-eslint/type-utils": "8.65.0", "@typescript-eslint/utils": "8.65.0", "@typescript-eslint/visitor-keys": "8.65.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.65.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA=="], - "@types/whatwg-url": ["@types/whatwg-url@11.0.5", "", { "dependencies": { "@types/webidl-conversions": "*" } }, "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.65.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.65.0", "@typescript-eslint/types": "8.65.0", "@typescript-eslint/typescript-estree": "8.65.0", "@typescript-eslint/visitor-keys": "8.65.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.22.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.22.0", "@typescript-eslint/type-utils": "8.22.0", "@typescript-eslint/utils": "8.22.0", "@typescript-eslint/visitor-keys": "8.22.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-4Uta6REnz/xEJMvwf72wdUnC3rr4jAQf5jnTkeRQ9b6soxLxhDEbS/pfMPoJLDfFPNVRdryqWUIV/2GZzDJFZw=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.65.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.65.0", "@typescript-eslint/types": "^8.65.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.22.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.22.0", "@typescript-eslint/types": "8.22.0", "@typescript-eslint/typescript-estree": "8.22.0", "@typescript-eslint/visitor-keys": "8.22.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-MqtmbdNEdoNxTPzpWiWnqNac54h8JDAmkWtJExBVVnSrSmi9z+sZUt0LfKqk9rjqmKOIeRhO4fHHJ1nQIjduIQ=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.65.0", "", { "dependencies": { "@typescript-eslint/types": "8.65.0", "@typescript-eslint/visitor-keys": "8.65.0" } }, "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.22.0", "", { "dependencies": { "@typescript-eslint/types": "8.22.0", "@typescript-eslint/visitor-keys": "8.22.0" } }, "sha512-/lwVV0UYgkj7wPSw0o8URy6YI64QmcOdwHuGuxWIYznO6d45ER0wXUbksr9pYdViAofpUCNJx/tAzNukgvaaiQ=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.65.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.22.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.22.0", "@typescript-eslint/utils": "8.22.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-NzE3aB62fDEaGjaAYZE4LH7I1MUwHooQ98Byq0G0y3kkibPJQIXVUspzlFOmOfHhiDLwKzMlWxaNv+/qcZurJA=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.65.0", "", { "dependencies": { "@typescript-eslint/types": "8.65.0", "@typescript-eslint/typescript-estree": "8.65.0", "@typescript-eslint/utils": "8.65.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.22.0", "", {}, "sha512-0S4M4baNzp612zwpD4YOieP3VowOARgK2EkN/GBn95hpyF8E2fbMT55sRHWBq+Huaqk3b3XK+rxxlM8sPgGM6A=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.65.0", "", {}, "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.22.0", "", { "dependencies": { "@typescript-eslint/types": "8.22.0", "@typescript-eslint/visitor-keys": "8.22.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.8.0" } }, "sha512-SJX99NAS2ugGOzpyhMza/tX+zDwjvwAtQFLsBo3GQxiGcvaKlqGBkmZ+Y1IdiSi9h4Q0Lr5ey+Cp9CGWNY/F/w=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.65.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.65.0", "@typescript-eslint/tsconfig-utils": "8.65.0", "@typescript-eslint/types": "8.65.0", "@typescript-eslint/visitor-keys": "8.65.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.22.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.22.0", "@typescript-eslint/types": "8.22.0", "@typescript-eslint/typescript-estree": "8.22.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-T8oc1MbF8L+Bk2msAvCUzjxVB2Z2f+vXYfcucE2wOmYs7ZUwco5Ep0fYZw8quNwOiw9K8GYVL+Kgc2pETNTLOg=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.65.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.65.0", "@typescript-eslint/types": "8.65.0", "@typescript-eslint/typescript-estree": "8.65.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.22.0", "", { "dependencies": { "@typescript-eslint/types": "8.22.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-AWpYAXnUgvLNabGTy3uBylkgZoosva/miNd1I8Bz3SjotmQPbVqhO4Cczo8AsZ44XVErEBPr/CRSgaj8sG7g0w=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.65.0", "", { "dependencies": { "@typescript-eslint/types": "8.65.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A=="], - "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + "@vitest/expect": ["@vitest/expect@4.1.10", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA=="], - "@vercel/speed-insights": ["@vercel/speed-insights@1.1.0", "", { "peerDependencies": { "@sveltejs/kit": "^1 || ^2", "next": ">= 13", "react": "^18 || ^19 || ^19.0.0-rc", "svelte": ">= 4", "vue": "^3", "vue-router": "^4" }, "optionalPeers": ["@sveltejs/kit", "next", "react", "svelte", "vue", "vue-router"] }, "sha512-rAXxuhhO4mlRGC9noa5F7HLMtGg8YF1zAN6Pjd1Ny4pII4cerhtwSG4vympbCl+pWkH7nBS9kVXRD4FAn54dlg=="], + "@vitest/mocker": ["@vitest/mocker@4.1.10", "", { "dependencies": { "@vitest/spy": "4.1.10", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow=="], - "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.10", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q=="], - "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + "@vitest/runner": ["@vitest/runner@4.1.10", "", { "dependencies": { "@vitest/utils": "4.1.10", "pathe": "^2.0.3" } }, "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg=="], - "ajv": ["ajv@6.12.6", "", { "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" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "@vitest/snapshot": ["@vitest/snapshot@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw=="], - "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "@vitest/spy": ["@vitest/spy@4.1.10", "", {}, "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw=="], - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "@vitest/utils": ["@vitest/utils@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA=="], - "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + "acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], - "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "ajv": ["ajv@6.15.0", "", { "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" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], + + "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], - "aria-hidden": ["aria-hidden@1.2.4", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], - "array-includes": ["array-includes@3.1.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" } }, "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ=="], + "array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="], "array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="], - "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ=="], + "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="], "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], @@ -381,133 +510,135 @@ "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="], + "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], + "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], + "atomically": ["atomically@2.1.1", "", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ=="], + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - "axe-core": ["axe-core@4.10.2", "", {}, "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w=="], + "axe-core": ["axe-core@4.12.1", "", {}, "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA=="], "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], - "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "base64-arraybuffer": ["base64-arraybuffer@1.0.2", "", {}, "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.11.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-oCu2wfipvX3AePSgmOuKkIywOu+8n9psz7hXYmk56ghpu3+7KzNIBopaOs4c9BrtdnTtW30unG9GTfHo7EwERQ=="], - "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + "better-auth": ["better-auth@1.6.23", "", { "dependencies": { "@better-auth/core": "1.6.23", "@better-auth/drizzle-adapter": "1.6.23", "@better-auth/kysely-adapter": "1.6.23", "@better-auth/memory-adapter": "1.6.23", "@better-auth/mongo-adapter": "1.6.23", "@better-auth/prisma-adapter": "1.6.23", "@better-auth/telemetry": "1.6.23", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.7", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.17 || ^0.29.0", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": "^0.45.2", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-4vOaRd9UiKGKm9R+ej0jjU1es3MiJIiNc9Qq3VCnYqOZ4/nb5272QqTxWYoDxyUXl5x6A2x2we5KZKQO9teTQQ=="], + + "better-call": ["better-call@1.3.7", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-Al51/hjp2SSp6CRTa3F2ptcx4yQVS1xWKoY6jcVXqNYOap6mHFP2jUBn5EwIL4iIed1/Sq4hlQ+Umm6EflZG+w=="], + + "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "bson": ["bson@6.10.2", "", {}, "sha512-5afhLTjqDSA3akH56E+/2J6kTDuSIlBxyXPdQslj9hcIgOUE378xdOfZvC/9q3LifJNI6KR/juZ+d0NRNYBwXg=="], + "browserslist": ["browserslist@4.28.7", "", { "dependencies": { "baseline-browser-mapping": "^2.10.44", "caniuse-lite": "^1.0.30001806", "electron-to-chromium": "^1.5.393", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw=="], + + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], - "busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="], + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g=="], - "call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], - "caniuse-lite": ["caniuse-lite@1.0.30001696", "", {}, "sha512-pDCPkvzfa39ehJtJ+OwGT/2yvT2SbjfHhiIW2LWOAcMQ7BzwxT/XuyUp4OTOd0XFWA6BKw0JalnBHgSi5DGJBQ=="], - "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], - - "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], - - "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + "chrome-launcher": ["chrome-launcher@1.2.1", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^2.0.1" }, "bin": { "print-chrome-path": "bin/print-chrome-path.cjs" } }, "sha512-qmFR5PLMzHyuNJHwOloHPAHhbaNglkfeV/xDtt5b7xiFFyU1I+AZZX0PYseMuhenJSSirgxELYIbswcoc+5H4A=="], - "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], + "chromium-bidi": ["chromium-bidi@16.0.1", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-J63PGu/9PpeCwLIcKYyzWP6yaVL5pxuBc0shlYCYM8BaAkmlwiQboXO1iNbOgSDbVklEyYFfNEcHD8oOAWacUA=="], - "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], - - "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + "cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], - "cloudinary": ["cloudinary@2.5.1", "", { "dependencies": { "lodash": "^4.17.21", "q": "^1.5.1" } }, "sha512-CNg6uU53Hl4FEVynkTGpt5bQEAQWDHi3H+Sm62FzKf5uQHipSN2v7qVqS8GRVqeb0T1WNV+22+75DOJeRXYeSQ=="], - - "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], - - "cmdk": ["cmdk@1.0.4", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.0", "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-AnsjfHyHpQ/EFeAnG216WY7A5LiYCoZzCSygiLvfXC3H3LFGCprErteUcszaVluGOhuOTbJS3jWHrSDYPBBygg=="], - - "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], - - "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + "configstore": ["configstore@7.1.0", "", { "dependencies": { "atomically": "^2.0.3", "dot-prop": "^9.0.0", "graceful-fs": "^4.2.11", "xdg-basedir": "^5.1.0" } }, "sha512-N4oog6YJWbR9kGyXvS7jEykLDXIE2C0ILYqNBZBp9iwiJpoCBWYsuAdW6PPFn6w06jjnC+3JstVvWHO4cZqvRg=="], - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - "css-line-break": ["css-line-break@2.1.0", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w=="], - - "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + "csp_evaluator": ["csp_evaluator@1.1.8", "", {}, "sha512-EwOnfYuNbTytvbMKsLixTrRgnjOa0WZCxGy8A9nnSYAicrdwn+T/epU/yjgymmOxlgKnvH+8wXt+7p/8ak5Feg=="], - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="], + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], - "date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - - "decode-named-character-reference": ["decode-named-character-reference@1.0.2", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg=="], + "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + "define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], - "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], - "detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + "devtools-protocol": ["devtools-protocol@0.0.1663043", "", {}, "sha512-33aOY3ZnBP1dgZsshgaL+/XlsQleiFZgyUaDtdZkEa1nbZhVY1MoDeWjk+wxg25fU924l1ZJfoGNmjjeA/5s1w=="], - "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], - "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], + "dot-prop": ["dot-prop@9.0.0", "", { "dependencies": { "type-fest": "^4.18.2" } }, "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ=="], - "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], + "drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="], - "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], + "drizzle-orm": ["drizzle-orm@0.45.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.395", "", {}, "sha512-7zt9Aw+SrmxLWLN0zhaTWZQiCdryLVrYTq5R7iZakLvi2UQPYMMsROYV/2qVCzMeCiSXHwKOU+sZ4zOVVlrtKA=="], "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "enhanced-resolve": ["enhanced-resolve@5.18.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ=="], - "es-abstract": ["es-abstract@1.23.9", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-regex": "^1.2.1", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.0", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.18" } }, "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA=="], + "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], + + "es-abstract": ["es-abstract@1.24.2", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], @@ -515,55 +646,63 @@ "es-iterator-helpers": ["es-iterator-helpers@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.0.3", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.6", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.4", "safe-array-concat": "^1.1.3" } }, "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w=="], + "es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="], + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], - "es-shim-unscopables": ["es-shim-unscopables@1.0.2", "", { "dependencies": { "hasown": "^2.0.0" } }, "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw=="], + "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="], "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + "esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@9.19.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.0", "@eslint/core": "^0.10.0", "@eslint/eslintrc": "^3.2.0", "@eslint/js": "9.19.0", "@eslint/plugin-kit": "^0.2.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.1", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.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", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA=="], + "eslint": ["eslint@9.39.5", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.6", "@eslint/js": "9.39.5", "@eslint/plugin-kit": "^0.4.1", "@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", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.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", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw=="], - "eslint-config-next": ["eslint-config-next@15.1.1", "", { "dependencies": { "@next/eslint-plugin-next": "15.1.1", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^5.0.0" }, "peerDependencies": { "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-St2CvkRaqwbEXot9XdivAqeznlZt5bACFyOy821k67SVJEgw4ansmrwhtlFUTRdYGmgmqapFKy4RN2yz/znHcg=="], + "eslint-config-next": ["eslint-config-next@16.2.11", "", { "dependencies": { "@next/eslint-plugin-next": "16.2.11", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^7.0.0", "globals": "16.4.0", "typescript-eslint": "^8.46.0" }, "peerDependencies": { "eslint": ">=9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-FIpbK/dUyxUExchDB7eBg3k+VU8R2iR/Cx9/kqTBUTFv2bOIR9aRrpno4rvAQ9VhiPQAyFKNA2NlZwouGWtclA=="], "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="], "eslint-import-resolver-typescript": ["eslint-import-resolver-typescript@3.7.0", "", { "dependencies": { "@nolyfill/is-core-module": "1.0.39", "debug": "^4.3.7", "enhanced-resolve": "^5.15.0", "fast-glob": "^3.3.2", "get-tsconfig": "^4.7.5", "is-bun-module": "^1.0.2", "is-glob": "^4.0.3", "stable-hash": "^0.0.4" }, "peerDependencies": { "eslint": "*", "eslint-plugin-import": "*", "eslint-plugin-import-x": "*" }, "optionalPeers": ["eslint-plugin-import", "eslint-plugin-import-x"] }, "sha512-Vrwyi8HHxY97K5ebydMtffsWAn1SCR9eol49eCd5fJS4O1WV7PaAjbcjmbfJJSMz/t4Mal212Uz/fQZrOB8mow=="], - "eslint-module-utils": ["eslint-module-utils@2.12.0", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg=="], + "eslint-module-utils": ["eslint-module-utils@2.14.0", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig=="], - "eslint-plugin-import": ["eslint-plugin-import@2.31.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", "array.prototype.findlastindex": "^1.2.5", "array.prototype.flat": "^1.3.2", "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.0", "hasown": "^2.0.2", "is-core-module": "^2.15.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.0", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.8", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A=="], + "eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="], "eslint-plugin-jsx-a11y": ["eslint-plugin-jsx-a11y@6.10.2", "", { "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", "axe-core": "^4.10.0", "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", "string.prototype.includes": "^2.0.1" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q=="], "eslint-plugin-react": ["eslint-plugin-react@7.37.4", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.8", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ=="], - "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.1.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw=="], + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "", { "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" }, "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" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="], - "eslint-scope": ["eslint-scope@8.2.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A=="], + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], - "eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], - "espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="], + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], - "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + "fast-glob": ["fast-glob@3.3.1", "", { "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.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="], "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], @@ -571,6 +710,10 @@ "fastq": ["fastq@1.19.0", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], @@ -583,11 +726,9 @@ "for-each": ["for-each@0.3.4", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-kKaIINnFpzW6ffJNDjjyjrk21BkDx38c0xa/klsT8VzLCaMEefv4ZTacrcVR4DmgTeBra++jMDAfS/tS799YDw=="], - "foreground-child": ["foreground-child@3.3.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" } }, "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg=="], + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], - "framer-motion": ["framer-motion@12.4.1", "", { "dependencies": { "motion-dom": "^12.0.0", "motion-utils": "^12.0.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5Ijbea3topSZjadQ0hgc/TcWj2ldMZmNREM7RvAhvsThYOA1HHOA8TT1yKvMu1YXP3jWaFwoZ6Vo9Nw+DUZrzA=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -595,9 +736,17 @@ "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], - "get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + "gaxios": ["gaxios@7.2.0", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-CUVb4wcYe+771XevyH6HtGmXFAGGKkIC3kswAP8Z1JCe0j80JMaTPZH930DWFrvo0atjh18Arc0pEyUCWa5bfg=="], + + "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], @@ -605,20 +754,20 @@ "get-tsconfig": ["get-tsconfig@4.10.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A=="], - "glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], - "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + "globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="], "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + "google-auth-library": ["google-auth-library@10.9.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-xtvUqvINPhTaBm7nXqlYPcrMHJPm1lCNdSovxnKKhTm+4JsvQ+KGVYJViLoH9Yxu8w+T0Qv5HubzYT9BLrppJg=="], + + "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], - "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], @@ -633,38 +782,34 @@ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.2", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "style-to-object": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg=="], + "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], - "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], - "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], + "http-link-header": ["http-link-header@1.1.4", "", {}, "sha512-xT3GPW6/ZbGuw4UvwHqErSCEjNUlwbQJuZn9/q5U4WEKfp2kENVCAlousG1zLxHeaQ/ffOHUNpWamvkbBW0eNw=="], - "html2canvas": ["html2canvas@1.4.1", "", { "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" } }, "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA=="], + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - "import-fresh": ["import-fresh@3.3.0", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw=="], + "image-ssim": ["image-ssim@0.2.0", "", {}, "sha512-W7+sO6/yhxy83L0G7xR8YAc5Z5QFtYEXXRV6EaE8tuYBZJnA3gVgp3q7X7muhLZVodeb9UfvjSbwt9VJwjIYAg=="], - "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], - "inline-style-parser": ["inline-style-parser@0.2.4", "", {}, "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="], + "import-in-the-middle": ["import-in-the-middle@3.3.2", "", { "dependencies": { "cjs-module-lexer": "^2.2.0", "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-jTd2FfOgOWOdgjkHuk/1Ms8VKFXkPs15ymYBETw1sAOrO/dY3XeGVRWir9qBbw7pXr0T2eTFwfCZ+N02HmiNGA=="], - "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], - "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], + "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], - "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], + "intl-messageformat": ["intl-messageformat@10.7.18", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "@formatjs/fast-memoize": "2.2.7", "@formatjs/icu-messageformat-parser": "2.11.4", "tslib": "^2.8.0" } }, "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g=="], "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], - "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], - "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], - "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], - "is-boolean-object": ["is-boolean-object@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng=="], "is-bun-module": ["is-bun-module@1.3.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA=="], @@ -677,7 +822,7 @@ "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], - "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], + "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], @@ -689,16 +834,14 @@ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], - "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], + "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], - "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], @@ -713,23 +856,33 @@ "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], - "is-weakref": ["is-weakref@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2" } }, "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q=="], + "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], + "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="], - "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + "jose": ["jose@6.2.4", "", {}, "sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA=="], - "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + "jpeg-js": ["jpeg-js@0.4.4", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="], + + "js-base64": ["js-base64@3.9.1", "", {}, "sha512-U73qptcvf/HIOauFOmqT3a0mDUp0MYlfd15oqoe9kqZt5XhiXVb+HG09sLvI9PQ9tZIBFS4nlErai8zbWazP0g=="], + + "js-library-detector": ["js-library-detector@6.7.0", "", {}, "sha512-c80Qupofp43y4cJ7+8TTDN/AsDwLi5oOm/plBrWI+iQt485vKXCco+yVmOwEgdo9VOdsYTuV0UlTeetVPTriXA=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + "js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], @@ -741,139 +894,109 @@ "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], - "kareem": ["kareem@2.6.3", "", {}, "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q=="], + "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], + + "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + "kysely": ["kysely@0.29.4", "", {}, "sha512-y5mVgQNkMbs1eK9Xyc0pmNdabN2wHhRYY/5r4W5HrUT1rYCEPeVNSj1RUJeSDKT3U0p+mXCvLgkrFuIafYI6BA=="], + "language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="], "language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="], - "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], - - "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], - - "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], - - "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - - "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], - - "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], - - "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], - - "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], - - "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - - "lucide-react": ["lucide-react@0.469.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw=="], + "legacy-javascript": ["legacy-javascript@0.0.1", "", {}, "sha512-lPyntS4/aS7jpuvOlitZDFifBCb4W8L/3QU0PLbUTUj+zYah8rfVjYic88yG7ZKTxhS5h9iz7duT8oUXKszLhg=="], - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - - "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], - - "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], - - "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], - "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], + "libsql": ["libsql@0.5.29", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.29", "@libsql/darwin-x64": "0.5.29", "@libsql/linux-arm-gnueabihf": "0.5.29", "@libsql/linux-arm-musleabihf": "0.5.29", "@libsql/linux-arm64-gnu": "0.5.29", "@libsql/linux-arm64-musl": "0.5.29", "@libsql/linux-x64-gnu": "0.5.29", "@libsql/linux-x64-musl": "0.5.29", "@libsql/win32-x64-msvc": "0.5.29" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg=="], - "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], + "lighthouse": ["lighthouse@13.4.1", "", { "dependencies": { "@paulirish/trace_engine": "0.0.65", "@sentry/node": "^10.0.0", "axe-core": "^4.12.1", "chrome-launcher": "^1.2.1", "configstore": "^7.0.0", "csp_evaluator": "1.1.8", "devtools-protocol": "0.0.1663043", "enquirer": "^2.3.6", "http-link-header": "^1.1.1", "intl-messageformat": "^10.5.3", "jpeg-js": "^0.4.4", "js-library-detector": "^6.7.0", "lighthouse-logger": "^2.0.2", "lighthouse-stack-packs": "1.12.3", "lodash-es": "^4.17.21", "lookup-closest-locale": "6.2.0", "open": "^8.4.0", "puppeteer-core": "^25.3.0", "robots-parser": "^3.0.1", "speedline-core": "^1.4.3", "third-party-web": "^0.29.2", "tldts-icann": "^7.4.9", "web-features": "^3.34.0", "ws": "^7.0.0", "yargs": "^17.3.1", "yargs-parser": "^21.0.0" }, "bin": { "lighthouse": "cli/index.js", "smokehouse": "cli/test/smokehouse/frontends/smokehouse-bin.js", "chrome-debug": "core/scripts/manual-chrome-launcher.js" } }, "sha512-fDu8lt3QLK/lTqIxtp1HkzQNJ32rsFHhbadYOepcMZFLgA8oINhxutMbMv8XXnpTOvZ0TXCo4JCk1LDTWaRLnA=="], - "mdast-util-to-hast": ["mdast-util-to-hast@13.2.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA=="], + "lighthouse-logger": ["lighthouse-logger@2.0.2", "", { "dependencies": { "debug": "^4.4.1", "marky": "^1.2.2" } }, "sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg=="], - "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], + "lighthouse-stack-packs": ["lighthouse-stack-packs@1.12.3", "", {}, "sha512-d8IsOpE83kbANgnM+Tp8+x6HcMpX9o2ITBiUERssgzAIFdZCQzs/f4k6D0DLQTE59enml9mbAOU52Wu35exWtg=="], - "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], - "memory-pager": ["memory-pager@1.5.0", "", {}, "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="], + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="], - "micromark": ["micromark@4.0.1", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-eBPdkcoCNvYcxQOAKAlceo5SNdzZWfF+FcSupREAzdAh9rRmE239CEQAiTwIgblwnoM8zzj35sZ5ZwvSEOF6Kw=="], + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="], - "micromark-core-commonmark": ["micromark-core-commonmark@2.0.2", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w=="], + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="], - "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="], - "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="], - "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="], - "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="], - "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="], - "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="], - "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], - "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], + "lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], - "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], - "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], - "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + "lookup-closest-locale": ["lookup-closest-locale@6.2.0", "", {}, "sha512-/c2kL+Vnp1jnV6K6RpDTHK3dgg0Tu2VVp+elEiJpjfS1UyY7AjOYHohRug6wT0OpoX2qFgNORndE9RqesfVxWQ=="], - "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], - "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + "marky": ["marky@1.3.0", "", {}, "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ=="], - "micromark-util-subtokenize": ["micromark-util-subtokenize@2.0.4", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-N6hXjrin2GTJDe3MVjf5FuXpm12PGm80BrUAeub9XFXca8JZbP+oIwY4LJSVwFUCL1IPm/WwSVUN7goFHmSGGQ=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - "micromark-util-types": ["micromark-util-types@2.0.1", "", {}, "sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ=="], + "meriyah": ["meriyah@6.1.4", "", {}, "sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ=="], "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - - "mongodb": ["mongodb@6.12.0", "", { "dependencies": { "@mongodb-js/saslprep": "^1.1.9", "bson": "^6.10.1", "mongodb-connection-string-url": "^3.0.0" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.188.0", "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", "mongodb-client-encryption": ">=6.0.0 <7", "snappy": "^7.2.2", "socks": "^2.7.1" }, "optionalPeers": ["@aws-sdk/credential-providers", "@mongodb-js/zstd", "gcp-metadata", "kerberos", "mongodb-client-encryption", "snappy", "socks"] }, "sha512-RM7AHlvYfS7jv7+BXund/kR64DryVI+cHbVAy9P61fnb1RcWZqOW1/Wj2YhqMCx+MuYhqTRGv7AwHBzmsCKBfA=="], + "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], - "mongodb-connection-string-url": ["mongodb-connection-string-url@3.0.2", "", { "dependencies": { "@types/whatwg-url": "^11.0.2", "whatwg-url": "^14.1.0 || ^13.0.0" } }, "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA=="], + "modern-tar": ["modern-tar@0.7.7", "", {}, "sha512-t9VmxaqrmANnEOBhpSDI6HD192Ge48k8vmWqQQL7hSFEqHEYwZbbsu49+aKLWZeRvFs3j1pMhXOqqF4kPlvjkQ=="], - "mongoose": ["mongoose@8.9.6", "", { "dependencies": { "bson": "^6.10.1", "kareem": "2.6.3", "mongodb": "~6.12.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", "sift": "17.1.3" } }, "sha512-ipLvXwNPVuuuq5H3lnSD0lpaRH3DlCoC6emnMVJvweTwxU29uxDJWxMsNpERDQt8JMvYF1HGVuTK+Id2BlQLCA=="], - - "motion-dom": ["motion-dom@12.0.0", "", { "dependencies": { "motion-utils": "^12.0.0" } }, "sha512-CvYd15OeIR6kHgMdonCc1ihsaUG4MYh/wrkz8gZ3hBX/uamyZCXN9S9qJoYF03GqfTt7thTV/dxnHYX4+55vDg=="], - - "motion-utils": ["motion-utils@12.0.0", "", {}, "sha512-MNFiBKbbqnmvOjkPyOKgHUp3Q6oiokLkI1bEwm5QA28cxMZrv0CbbBGDNmhF6DIXsi1pCQBSs0dX8xjeER1tmA=="], - - "mpath": ["mpath@0.9.0", "", {}, "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew=="], - - "mquery": ["mquery@5.0.0", "", { "dependencies": { "debug": "4.x" } }, "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg=="], + "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + "nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], - "nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="], + "nanostores": ["nanostores@1.4.1", "", {}, "sha512-PGd3uPojJB9Z07d5NX3Db/SOSBbyy3wLMUGq0GpnEEJfVzY9mq7daPMAZ3jObV5D3Jn+YKND636eI5ULg7F80Q=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - "next": ["next@15.1.6", "", { "dependencies": { "@next/env": "15.1.6", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.1.6", "@next/swc-darwin-x64": "15.1.6", "@next/swc-linux-arm64-gnu": "15.1.6", "@next/swc-linux-arm64-musl": "15.1.6", "@next/swc-linux-x64-gnu": "15.1.6", "@next/swc-linux-x64-musl": "15.1.6", "@next/swc-win32-arm64-msvc": "15.1.6", "@next/swc-win32-x64-msvc": "15.1.6", "sharp": "^0.33.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-Hch4wzbaX0vKQtalpXvUiw5sYivBy4cm5rzUKrBnUB/y436LGrvOUqYvlSeNVCWFO/770gDlltR9gqZH62ct4Q=="], + "next": ["next@16.2.11", "", { "dependencies": { "@next/env": "16.2.11", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.11", "@next/swc-darwin-x64": "16.2.11", "@next/swc-linux-arm64-gnu": "16.2.11", "@next/swc-linux-arm64-musl": "16.2.11", "@next/swc-linux-x64-gnu": "16.2.11", "@next/swc-linux-x64-musl": "16.2.11", "@next/swc-win32-arm64-msvc": "16.2.11", "@next/swc-win32-x64-msvc": "16.2.11", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-B339zaqbyK8cmxhoAvLrcwoabwCP1wz21zSzfqxqXAemTu2BXnH7tQnfcglKv1vnMUIDBc+Hth7XODQriTZiRQ=="], - "next-cloudinary": ["next-cloudinary@6.16.0", "", { "dependencies": { "@cloudinary-util/types": "1.5.10", "@cloudinary-util/url-loader": "5.10.4", "@cloudinary-util/util": "4.0.0" }, "peerDependencies": { "next": "^12 || ^13 || ^14 || >=15.0.0-rc || ^15", "react": "^17 || ^18 || >=19.0.0-beta || ^19" } }, "sha512-gbw/A1aBHJBC2thm4/veI77IkmbAQXuRUj8kNEuxf1zgjGkUWkShho/cXS4VsFhnHuDieqb30gMjZnZdBDrQ/Q=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], - "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + "node-releases": ["node-releases@2.0.51", "", {}, "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ=="], - "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - "object-inspect": ["object-inspect@1.13.3", "", {}, "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA=="], + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], @@ -887,6 +1010,10 @@ "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + "obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="], + + "open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], + "optionator": ["optionator@0.9.4", "", { "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" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], @@ -895,87 +1022,71 @@ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + "p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="], "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], - "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], - "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "pg-protocol": ["pg-protocol@1.15.0", "", {}, "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ=="], - "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], + "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], - "pirates": ["pirates@4.0.6", "", {}, "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "possible-typed-array-names": ["possible-typed-array-names@1.0.0", "", {}, "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q=="], + "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], - "postcss": ["postcss@8.5.1", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ=="], + "playwright": ["playwright@1.61.1", "", { "dependencies": { "playwright-core": "1.61.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ=="], - "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], + "playwright-core": ["playwright-core@1.61.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg=="], - "postcss-js": ["postcss-js@4.0.1", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw=="], + "possible-typed-array-names": ["possible-typed-array-names@1.0.0", "", {}, "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q=="], - "postcss-load-config": ["postcss-load-config@4.0.2", "", { "dependencies": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ=="], + "postcss": ["postcss@8.5.21", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-v4sDNP3fdNiWMfabO7OwOQdOX8TiQSztKyT1Wj0w+j7LDallJThJRBBBmzVGyYj0crMh7jlV4zepPkiNu9UwDQ=="], - "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], + "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], - "postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], + "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], - "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + "postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="], + + "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + "promise-limit": ["promise-limit@2.7.0", "", {}, "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="], + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], - "property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="], + "protobufjs": ["protobufjs@7.6.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "q": ["q@1.5.1", "", {}, "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw=="], + "puppeteer-core": ["puppeteer-core@25.3.0", "", { "dependencies": { "@puppeteer/browsers": "3.0.6", "chromium-bidi": "16.0.1", "devtools-protocol": "0.0.1638949", "typed-query-selector": "^2.12.2", "webdriver-bidi-protocol": "0.4.2", "ws": "^8.21.0" } }, "sha512-fm+wpUr2oigH1PXZvwgATrM2tYWHMDG8ASzTEe9uukCye4X5Ldx1K5BTHPFKITrIWvQQAQ256d1NpbEveBcKjA=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - "react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="], - - "react-day-picker": ["react-day-picker@8.10.1", "", { "peerDependencies": { "date-fns": "^2.28.0 || ^3.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA=="], + "react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="], - "react-dom": ["react-dom@19.0.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="], - - "react-icons": ["react-icons@5.4.0", "", { "peerDependencies": { "react": "*" } }, "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ=="], + "react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="], "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], - "react-loading-indicators": ["react-loading-indicators@1.0.0", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-0xI07RxuT62NcdKB+6pphR++cnkk9iu8DO0H9kSKE9X71oY/M+gkqTaCjoYA1T3UYrRKdm52QvnwY/YTAU5/cg=="], - - "react-markdown": ["react-markdown@9.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-Yk7Z94dbgYTOrdk41Z74GoKA7rThnsbbqBTRYuxoe08qvfQ9tJVhmAKw6BJS/ZORG7kTy/s1QvYzSuaoBA1qfw=="], - - "react-remove-scroll": ["react-remove-scroll@2.6.3", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ=="], - - "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], - - "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], - - "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], - - "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], - "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], - "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - "remark-rehype": ["remark-rehype@11.1.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ=="], + "require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], @@ -983,19 +1094,33 @@ "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], + "reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="], + "robots-parser": ["robots-parser@3.0.1", "", {}, "sha512-s+pyvQeIKIZ0dx5iJiQk1tPLJAWln39+MI5jtM8wnyws+G5azk+dMnMX0qfbqNetKKNgcWWOdi0sfm+FbQbgdQ=="], + + "rolldown": ["rolldown@1.1.5", "", { "dependencies": { "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.5", "@rolldown/binding-darwin-arm64": "1.1.5", "@rolldown/binding-darwin-x64": "1.1.5", "@rolldown/binding-freebsd-x64": "1.1.5", "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", "@rolldown/binding-linux-arm64-gnu": "1.1.5", "@rolldown/binding-linux-arm64-musl": "1.1.5", "@rolldown/binding-linux-ppc64-gnu": "1.1.5", "@rolldown/binding-linux-s390x-gnu": "1.1.5", "@rolldown/binding-linux-x64-gnu": "1.1.5", "@rolldown/binding-linux-x64-musl": "1.1.5", "@rolldown/binding-openharmony-arm64": "1.1.5", "@rolldown/binding-wasm32-wasi": "1.1.5", "@rolldown/binding-win32-arm64-msvc": "1.1.5", "@rolldown/binding-win32-x64-msvc": "1.1.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA=="], + + "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], - "scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], - "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "semifies": ["semifies@1.0.0", "", {}, "sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw=="], + + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "set-cookie-parser": ["set-cookie-parser@3.1.2", "", {}, "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw=="], "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], @@ -1003,7 +1128,7 @@ "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], - "sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], + "sharp": ["sharp@0.35.3", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.3", "@img/sharp-darwin-x64": "0.35.3", "@img/sharp-freebsd-wasm32": "0.35.3", "@img/sharp-libvips-darwin-arm64": "1.3.2", "@img/sharp-libvips-darwin-x64": "1.3.2", "@img/sharp-libvips-linux-arm": "1.3.2", "@img/sharp-libvips-linux-arm64": "1.3.2", "@img/sharp-libvips-linux-ppc64": "1.3.2", "@img/sharp-libvips-linux-riscv64": "1.3.2", "@img/sharp-libvips-linux-s390x": "1.3.2", "@img/sharp-libvips-linux-x64": "1.3.2", "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", "@img/sharp-libvips-linuxmusl-x64": "1.3.2", "@img/sharp-linux-arm": "0.35.3", "@img/sharp-linux-arm64": "0.35.3", "@img/sharp-linux-ppc64": "0.35.3", "@img/sharp-linux-riscv64": "0.35.3", "@img/sharp-linux-s390x": "0.35.3", "@img/sharp-linux-x64": "0.35.3", "@img/sharp-linuxmusl-arm64": "0.35.3", "@img/sharp-linuxmusl-x64": "0.35.3", "@img/sharp-webcontainers-wasm32": "0.35.3", "@img/sharp-win32-arm64": "0.35.3", "@img/sharp-win32-ia32": "0.35.3", "@img/sharp-win32-x64": "0.35.3" } }, "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -1017,25 +1142,25 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - "sift": ["sift@17.1.3", "", {}, "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ=="], - - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], - "sparse-bitfield": ["sparse-bitfield@3.0.3", "", { "dependencies": { "memory-pager": "^1.0.2" } }, "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ=="], + "speedline-core": ["speedline-core@1.4.3", "", { "dependencies": { "@types/node": "*", "image-ssim": "^0.2.0", "jpeg-js": "^0.4.1" } }, "sha512-DI7/OuAUD+GMpR6dmu8lliO2Wg5zfeh+/xsdyJZCzd8o5JgFUjCeLsBDuZjIQJdwXS3J0L/uZYrELKYqx+PXog=="], "stable-hash": ["stable-hash@0.0.4", "", {}, "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g=="], - "streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="], + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], - "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + "std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="], - "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="], @@ -1049,58 +1174,52 @@ "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], - "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], - - "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], - - "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - "style-to-object": ["style-to-object@1.0.8", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g=="], + "stubborn-fs": ["stubborn-fs@2.0.0", "", { "dependencies": { "stubborn-utils": "^1.0.1" } }, "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA=="], - "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], + "stubborn-utils": ["stubborn-utils@1.0.2", "", {}, "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg=="], - "sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="], + "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - "tailwind-merge": ["tailwind-merge@2.6.0", "", {}, "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA=="], - - "tailwindcss": ["tailwindcss@3.4.17", "", { "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.6", "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", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og=="], - - "tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="], - "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], - "text-segmentation": ["text-segmentation@1.0.3", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw=="], + "third-party-web": ["third-party-web@0.29.2", "", {}, "sha512-fegtha91tq2DHphyoiBXVHjVi2YG9zFaRnboT9C28tO1en9Y3wJsfspuy40F+u5wl3hHVbw7cnd1b67kEGHb8g=="], - "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + "tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], - "tr46": ["tr46@5.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g=="], + "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], - "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + "tldts-core": ["tldts-core@7.4.9", "", {}, "sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg=="], - "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + "tldts-icann": ["tldts-icann@7.4.9", "", { "dependencies": { "tldts-core": "^7.4.9" } }, "sha512-q6gyxCkcFPu5OZd2hi2quysxCG3ejIi53EACesbCEZHhH7FO3HnliqwO5zUs0TV6dA54SxhCrTGAc4ugl7rTiw=="], - "ts-api-utils": ["ts-api-utils@2.0.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tsx": ["tsx@4.23.1", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ=="], + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], @@ -1109,214 +1228,538 @@ "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], - "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], + "typed-query-selector": ["typed-query-selector@2.12.2", "", {}, "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ=="], + + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], + + "typescript-eslint": ["typescript-eslint@8.65.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.65.0", "@typescript-eslint/parser": "8.65.0", "@typescript-eslint/typescript-estree": "8.65.0", "@typescript-eslint/utils": "8.65.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA=="], "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], - "undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], - "unist-util-is": ["unist-util-is@6.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw=="], + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + "vite": ["vite@8.1.5", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.17", "rolldown": "~1.1.5", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.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" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw=="], - "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + "vitest": ["vitest@4.1.10", "", { "dependencies": { "@vitest/expect": "4.1.10", "@vitest/mocker": "4.1.10", "@vitest/pretty-format": "4.1.10", "@vitest/runner": "4.1.10", "@vitest/snapshot": "4.1.10", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.10", "@vitest/browser-preview": "4.1.10", "@vitest/browser-webdriverio": "4.1.10", "@vitest/coverage-istanbul": "4.1.10", "@vitest/coverage-v8": "4.1.10", "@vitest/ui": "4.1.10", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw=="], - "unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], + "web-features": ["web-features@3.34.1", "", {}, "sha512-x+TGFo3TzB8dmXHNvveQXmaoYhzNH6blW93TO0u1Ygvr8/Bk3KIFZC9LVofHNnkcEmeodh8P3Mes5fPWyO7ATA=="], - "unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="], + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], - "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "webdriver-bidi-protocol": ["webdriver-bidi-protocol@0.4.2", "", {}, "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA=="], - "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + "when-exit": ["when-exit@2.1.5", "", {}, "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg=="], - "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "use-sync-external-store": ["use-sync-external-store@1.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw=="], + "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], - "utrie": ["utrie@1.0.2", "", { "dependencies": { "base64-arraybuffer": "^1.0.2" } }, "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw=="], + "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], - "vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="], + "which-typed-array": ["which-typed-array@1.1.22", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw=="], - "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - "vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="], + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "whatwg-url": ["whatwg-url@14.1.0", "", { "dependencies": { "tr46": "^5.0.0", "webidl-conversions": "^7.0.0" } }, "sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w=="], + "ws": ["ws@7.5.13", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA=="], - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "xdg-basedir": ["xdg-basedir@5.1.0", "", {}, "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ=="], - "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], - "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "which-typed-array": ["which-typed-array@1.1.18", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA=="], + "yargs": ["yargs@17.7.3", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="], - "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "yaml": ["yaml@2.7.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA=="], + "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "@babel/core/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - "zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="], + "@babel/core/json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], - "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@cloudinary-util/url-loader/@cloudinary-util/util": ["@cloudinary-util/util@3.3.2", "", {}, "sha512-Cc0iFxzfl7fcOXuznpeZFGYC885Of/vDgccRDnhTe/8Rf8YKv2PjLtezyo0VgmdA/CpeZy29NCXAsf6liokbwg=="], + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], - "@next/eslint-plugin-next/fast-glob": ["fast-glob@3.3.1", "", { "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.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="], + "@google/genai/ws": ["ws@8.21.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw=="], - "@radix-ui/react-collapsible/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.1", "", { "dependencies": { "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg=="], + "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], - "@radix-ui/react-collection/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.1", "", { "dependencies": { "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg=="], + "@libsql/isomorphic-ws/ws": ["ws@8.21.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw=="], - "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="], + "@puppeteer/browsers/yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="], - "@radix-ui/react-context-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.1", "", { "dependencies": { "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg=="], + "@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], - "@radix-ui/react-dropdown-menu/@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.2", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-roving-focus": "1.1.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-callback-ref": "1.1.0", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="], - "@radix-ui/react-menu/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.4", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XDUI0IVYVSwjMXxM6P4Dfti7AH+Y4oS/TB+sglZ/EXc7cqLwGAmp1NlMrcUjj7ks6R5WTZuWKv44FBbLpwU3sA=="], + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - "@radix-ui/react-menu/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA=="], + "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], - "@radix-ui/react-menu/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.1", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw=="], + "array-buffer-byte-length/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], - "@radix-ui/react-menu/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw=="], + "array.prototype.findlast/es-abstract": ["es-abstract@1.23.9", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-regex": "^1.2.1", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.0", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.18" } }, "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA=="], - "@radix-ui/react-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.1", "", { "dependencies": { "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg=="], + "array.prototype.findlast/es-shim-unscopables": ["es-shim-unscopables@1.0.2", "", { "dependencies": { "hasown": "^2.0.0" } }, "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw=="], - "@radix-ui/react-menu/@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.1", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw=="], + "array.prototype.findlastindex/es-abstract": ["es-abstract@1.23.9", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-regex": "^1.2.1", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.0", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.18" } }, "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA=="], - "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="], + "array.prototype.flat/es-abstract": ["es-abstract@1.23.9", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-regex": "^1.2.1", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.0", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.18" } }, "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA=="], - "@radix-ui/react-roving-focus/@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw=="], + "array.prototype.flat/es-shim-unscopables": ["es-shim-unscopables@1.0.2", "", { "dependencies": { "hasown": "^2.0.0" } }, "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw=="], - "@radix-ui/react-select/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.4", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XDUI0IVYVSwjMXxM6P4Dfti7AH+Y4oS/TB+sglZ/EXc7cqLwGAmp1NlMrcUjj7ks6R5WTZuWKv44FBbLpwU3sA=="], + "array.prototype.flatmap/es-abstract": ["es-abstract@1.23.9", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-regex": "^1.2.1", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.0", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.18" } }, "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA=="], - "@radix-ui/react-select/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA=="], + "array.prototype.flatmap/es-shim-unscopables": ["es-shim-unscopables@1.0.2", "", { "dependencies": { "hasown": "^2.0.0" } }, "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw=="], - "@radix-ui/react-select/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.1", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw=="], + "array.prototype.tosorted/es-abstract": ["es-abstract@1.23.9", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-regex": "^1.2.1", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.0", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.18" } }, "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA=="], - "@radix-ui/react-select/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw=="], + "array.prototype.tosorted/es-shim-unscopables": ["es-shim-unscopables@1.0.2", "", { "dependencies": { "hasown": "^2.0.0" } }, "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw=="], - "@radix-ui/react-select/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.1", "", { "dependencies": { "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg=="], + "arraybuffer.prototype.slice/es-abstract": ["es-abstract@1.23.9", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-regex": "^1.2.1", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.0", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.18" } }, "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA=="], - "@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="], + "arraybuffer.prototype.slice/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], - "@radix-ui/react-switch/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.1", "", { "dependencies": { "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg=="], + "browserslist/caniuse-lite": ["caniuse-lite@1.0.30001806", "", {}, "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw=="], - "@radix-ui/react-toast/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.4", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XDUI0IVYVSwjMXxM6P4Dfti7AH+Y4oS/TB+sglZ/EXc7cqLwGAmp1NlMrcUjj7ks6R5WTZuWKv44FBbLpwU3sA=="], + "call-bind/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], - "@radix-ui/react-toast/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw=="], + "call-bound/call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - "@radix-ui/react-toast/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.1", "", { "dependencies": { "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg=="], + "chromium-bidi/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@radix-ui/react-tooltip/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.4", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XDUI0IVYVSwjMXxM6P4Dfti7AH+Y4oS/TB+sglZ/EXc7cqLwGAmp1NlMrcUjj7ks6R5WTZuWKv44FBbLpwU3sA=="], + "data-view-buffer/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], - "@radix-ui/react-tooltip/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.1", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw=="], + "data-view-byte-length/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], - "@radix-ui/react-tooltip/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw=="], + "data-view-byte-offset/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], - "@radix-ui/react-tooltip/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.1", "", { "dependencies": { "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg=="], + "es-iterator-helpers/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], - "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="], + "es-iterator-helpers/es-abstract": ["es-abstract@1.23.9", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-regex": "^1.2.1", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.0", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.18" } }, "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA=="], - "@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.1", "", { "dependencies": { "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg=="], + "es-iterator-helpers/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "es-set-tostringtag/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], - "@typescript-eslint/typescript-estree/semver": ["semver@7.7.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ=="], + "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "eslint-import-resolver-typescript/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + "eslint-import-resolver-typescript/fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + "eslint-plugin-import/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "eslint-plugin-import/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "eslint-plugin-jsx-a11y/array-includes": ["array-includes@3.1.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" } }, "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ=="], + + "eslint-plugin-jsx-a11y/axe-core": ["axe-core@4.10.2", "", {}, "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w=="], + + "eslint-plugin-jsx-a11y/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "eslint-plugin-react/array-includes": ["array-includes@3.1.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" } }, "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ=="], + + "eslint-plugin-react/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "eslint-plugin-react/resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="], + "eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "function.prototype.name/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "get-intrinsic/call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "get-symbol-description/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "get-symbol-description/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "is-array-buffer/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "is-array-buffer/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "is-async-function/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "is-boolean-object/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], "is-bun-module/semver": ["semver@7.7.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ=="], - "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + "is-data-view/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "is-data-view/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "is-date-object/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "is-finalizationregistry/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "is-generator-function/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "is-number-object/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "is-regex/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "is-shared-array-buffer/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "is-string/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "is-symbol/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "is-typed-array/which-typed-array": ["which-typed-array@1.1.18", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA=="], + + "is-weakset/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "is-weakset/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "iterator.prototype/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "jsx-ast-utils/array-includes": ["array-includes@3.1.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" } }, "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ=="], + + "libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], + + "object.assign/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "object.fromentries/es-abstract": ["es-abstract@1.23.9", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-regex": "^1.2.1", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.0", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.18" } }, "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA=="], + + "object.groupby/es-abstract": ["es-abstract@1.23.9", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-regex": "^1.2.1", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.0", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.18" } }, "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA=="], + + "object.values/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "own-keys/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "puppeteer-core/devtools-protocol": ["devtools-protocol@0.0.1638949", "", {}, "sha512-mXwg4Fqnv0WR4iuAT/gYUmctNkjILwXFHyZ+m7Ty1dfr0ezZt2U3gnrrJTfRobJTHoXf+IbuFvFITzLrLFjwJA=="], + + "puppeteer-core/ws": ["ws@8.21.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw=="], + + "reflect.getprototypeof/es-abstract": ["es-abstract@1.23.9", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-regex": "^1.2.1", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.0", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.18" } }, "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA=="], + + "reflect.getprototypeof/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "safe-array-concat/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "safe-array-concat/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "safe-regex-test/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "set-function-length/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "side-channel/object-inspect": ["object-inspect@1.13.3", "", {}, "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA=="], + + "side-channel-list/object-inspect": ["object-inspect@1.13.3", "", {}, "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA=="], + + "side-channel-map/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "side-channel-map/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "side-channel-map/object-inspect": ["object-inspect@1.13.3", "", {}, "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA=="], + + "side-channel-weakmap/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "side-channel-weakmap/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "side-channel-weakmap/object-inspect": ["object-inspect@1.13.3", "", {}, "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA=="], + + "string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "string.prototype.includes/es-abstract": ["es-abstract@1.23.9", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-regex": "^1.2.1", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.0", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.18" } }, "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA=="], + + "string.prototype.matchall/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "string.prototype.matchall/es-abstract": ["es-abstract@1.23.9", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-regex": "^1.2.1", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.0", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.18" } }, "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA=="], + + "string.prototype.matchall/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "string.prototype.repeat/es-abstract": ["es-abstract@1.23.9", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-regex": "^1.2.1", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.0", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.18" } }, "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA=="], + + "string.prototype.trim/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "string.prototype.trim/es-abstract": ["es-abstract@1.23.9", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-regex": "^1.2.1", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.0", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.18" } }, "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA=="], + + "string.prototype.trimend/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "tsx/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "typed-array-buffer/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "unbox-primitive/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "which-builtin-type/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "which-builtin-type/is-weakref": ["is-weakref@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2" } }, "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q=="], + + "which-builtin-type/which-typed-array": ["which-typed-array@1.1.18", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA=="], + + "which-typed-array/call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="], + + "which-typed-array/for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + + "@puppeteer/browsers/yargs/cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], + + "@puppeteer/browsers/yargs/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "@puppeteer/browsers/yargs/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.7", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA=="], + + "array-buffer-byte-length/call-bound/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "array.prototype.findlast/es-abstract/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "array.prototype.findlast/es-abstract/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "array.prototype.findlast/es-abstract/is-weakref": ["is-weakref@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2" } }, "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q=="], + + "array.prototype.findlast/es-abstract/object-inspect": ["object-inspect@1.13.3", "", {}, "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA=="], + + "array.prototype.findlast/es-abstract/which-typed-array": ["which-typed-array@1.1.18", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA=="], + + "array.prototype.findlastindex/es-abstract/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "array.prototype.findlastindex/es-abstract/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "array.prototype.findlastindex/es-abstract/is-weakref": ["is-weakref@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2" } }, "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q=="], + + "array.prototype.findlastindex/es-abstract/object-inspect": ["object-inspect@1.13.3", "", {}, "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA=="], + + "array.prototype.findlastindex/es-abstract/which-typed-array": ["which-typed-array@1.1.18", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA=="], + + "array.prototype.flat/es-abstract/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "array.prototype.flat/es-abstract/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "array.prototype.flat/es-abstract/is-weakref": ["is-weakref@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2" } }, "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q=="], + + "array.prototype.flat/es-abstract/object-inspect": ["object-inspect@1.13.3", "", {}, "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA=="], + + "array.prototype.flat/es-abstract/which-typed-array": ["which-typed-array@1.1.18", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA=="], + + "array.prototype.flatmap/es-abstract/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "array.prototype.flatmap/es-abstract/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "array.prototype.flatmap/es-abstract/is-weakref": ["is-weakref@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2" } }, "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q=="], + + "array.prototype.flatmap/es-abstract/object-inspect": ["object-inspect@1.13.3", "", {}, "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA=="], + + "array.prototype.flatmap/es-abstract/which-typed-array": ["which-typed-array@1.1.18", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA=="], + + "array.prototype.tosorted/es-abstract/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "array.prototype.tosorted/es-abstract/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "array.prototype.tosorted/es-abstract/is-weakref": ["is-weakref@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2" } }, "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q=="], + + "array.prototype.tosorted/es-abstract/object-inspect": ["object-inspect@1.13.3", "", {}, "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA=="], + + "array.prototype.tosorted/es-abstract/which-typed-array": ["which-typed-array@1.1.18", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA=="], + + "arraybuffer.prototype.slice/es-abstract/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "arraybuffer.prototype.slice/es-abstract/is-weakref": ["is-weakref@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2" } }, "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q=="], + + "arraybuffer.prototype.slice/es-abstract/object-inspect": ["object-inspect@1.13.3", "", {}, "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA=="], + + "arraybuffer.prototype.slice/es-abstract/which-typed-array": ["which-typed-array@1.1.18", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA=="], + + "data-view-buffer/call-bound/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "data-view-byte-length/call-bound/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "data-view-byte-offset/call-bound/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "es-iterator-helpers/es-abstract/is-weakref": ["is-weakref@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2" } }, "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q=="], + + "es-iterator-helpers/es-abstract/object-inspect": ["object-inspect@1.13.3", "", {}, "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA=="], + + "es-iterator-helpers/es-abstract/which-typed-array": ["which-typed-array@1.1.18", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA=="], + + "eslint-import-resolver-typescript/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "eslint-plugin-jsx-a11y/array-includes/es-abstract": ["es-abstract@1.23.9", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-regex": "^1.2.1", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.0", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.18" } }, "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA=="], + + "eslint-plugin-jsx-a11y/array-includes/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "eslint-plugin-react/array-includes/es-abstract": ["es-abstract@1.23.9", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-regex": "^1.2.1", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.0", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.18" } }, "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA=="], + + "eslint-plugin-react/array-includes/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "function.prototype.name/call-bound/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "is-async-function/call-bound/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "is-boolean-object/call-bound/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "is-date-object/call-bound/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "is-finalizationregistry/call-bound/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "is-generator-function/call-bound/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "is-number-object/call-bound/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "is-regex/call-bound/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "is-shared-array-buffer/call-bound/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "is-string/call-bound/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "is-symbol/call-bound/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "is-typed-array/which-typed-array/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "jsx-ast-utils/array-includes/es-abstract": ["es-abstract@1.23.9", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-regex": "^1.2.1", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.0", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.18" } }, "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA=="], + + "jsx-ast-utils/array-includes/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "object.assign/call-bound/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "object.fromentries/es-abstract/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "object.fromentries/es-abstract/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "object.fromentries/es-abstract/is-weakref": ["is-weakref@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2" } }, "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q=="], + + "object.fromentries/es-abstract/object-inspect": ["object-inspect@1.13.3", "", {}, "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA=="], + + "object.fromentries/es-abstract/which-typed-array": ["which-typed-array@1.1.18", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA=="], + + "object.groupby/es-abstract/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "object.groupby/es-abstract/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "object.groupby/es-abstract/is-weakref": ["is-weakref@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2" } }, "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q=="], + + "object.groupby/es-abstract/object-inspect": ["object-inspect@1.13.3", "", {}, "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA=="], + + "object.groupby/es-abstract/which-typed-array": ["which-typed-array@1.1.18", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA=="], + + "object.values/call-bound/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "reflect.getprototypeof/es-abstract/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "reflect.getprototypeof/es-abstract/is-weakref": ["is-weakref@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2" } }, "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q=="], + + "reflect.getprototypeof/es-abstract/object-inspect": ["object-inspect@1.13.3", "", {}, "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA=="], + + "reflect.getprototypeof/es-abstract/which-typed-array": ["which-typed-array@1.1.18", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA=="], + + "safe-regex-test/call-bound/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "string.prototype.includes/es-abstract/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "string.prototype.includes/es-abstract/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "string.prototype.includes/es-abstract/is-weakref": ["is-weakref@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2" } }, "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q=="], + + "string.prototype.includes/es-abstract/object-inspect": ["object-inspect@1.13.3", "", {}, "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA=="], + + "string.prototype.includes/es-abstract/which-typed-array": ["which-typed-array@1.1.18", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA=="], + + "string.prototype.matchall/es-abstract/is-weakref": ["is-weakref@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2" } }, "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q=="], + + "string.prototype.matchall/es-abstract/object-inspect": ["object-inspect@1.13.3", "", {}, "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA=="], + + "string.prototype.matchall/es-abstract/which-typed-array": ["which-typed-array@1.1.18", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA=="], + + "string.prototype.repeat/es-abstract/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "string.prototype.repeat/es-abstract/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "string.prototype.repeat/es-abstract/is-weakref": ["is-weakref@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2" } }, "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q=="], + + "string.prototype.repeat/es-abstract/object-inspect": ["object-inspect@1.13.3", "", {}, "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA=="], + + "string.prototype.repeat/es-abstract/which-typed-array": ["which-typed-array@1.1.18", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA=="], + + "string.prototype.trim/call-bound/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "string.prototype.trim/es-abstract/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], - "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + "string.prototype.trim/es-abstract/is-weakref": ["is-weakref@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2" } }, "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q=="], - "sharp/semver": ["semver@7.7.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ=="], + "string.prototype.trim/es-abstract/object-inspect": ["object-inspect@1.13.3", "", {}, "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA=="], - "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "string.prototype.trim/es-abstract/which-typed-array": ["which-typed-array@1.1.18", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA=="], - "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "string.prototype.trimend/call-bound/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], - "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "typed-array-buffer/call-bound/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], - "vaul/@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.4", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.1", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-portal": "1.1.3", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-slot": "1.1.1", "@radix-ui/react-use-controllable-state": "1.1.0", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-LaO3e5h/NOEL4OfXjxD43k9Dx+vn+8n+PCFt6uhX/BADFflllyv3WJG6rgvvSVBxpTch938Qq/LGc2MMxipXPw=="], + "unbox-primitive/call-bound/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], - "wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + "which-builtin-type/call-bound/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], - "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "which-typed-array/call-bind/call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@puppeteer/browsers/yargs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], - "@next/eslint-plugin-next/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "@puppeteer/browsers/yargs/cliui/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], - "@radix-ui/react-collapsible/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="], + "@puppeteer/browsers/yargs/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], - "@radix-ui/react-context-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="], + "@puppeteer/browsers/yargs/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], - "@radix-ui/react-dropdown-menu/@radix-ui/react-menu/@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - "@radix-ui/react-menu/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.1", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w=="], + "eslint-plugin-jsx-a11y/array-includes/es-abstract/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], - "@radix-ui/react-select/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.1", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w=="], + "eslint-plugin-jsx-a11y/array-includes/es-abstract/is-weakref": ["is-weakref@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2" } }, "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q=="], - "@radix-ui/react-switch/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="], + "eslint-plugin-jsx-a11y/array-includes/es-abstract/object-inspect": ["object-inspect@1.13.3", "", {}, "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA=="], - "@radix-ui/react-toast/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="], + "eslint-plugin-jsx-a11y/array-includes/es-abstract/which-typed-array": ["which-typed-array@1.1.18", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA=="], - "@radix-ui/react-tooltip/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.1", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w=="], + "eslint-plugin-react/array-includes/es-abstract/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], - "@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="], + "eslint-plugin-react/array-includes/es-abstract/is-weakref": ["is-weakref@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2" } }, "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + "eslint-plugin-react/array-includes/es-abstract/object-inspect": ["object-inspect@1.13.3", "", {}, "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA=="], - "glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + "eslint-plugin-react/array-includes/es-abstract/which-typed-array": ["which-typed-array@1.1.18", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA=="], - "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "is-typed-array/which-typed-array/call-bound/get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], - "vaul/@radix-ui/react-dialog/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.4", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XDUI0IVYVSwjMXxM6P4Dfti7AH+Y4oS/TB+sglZ/EXc7cqLwGAmp1NlMrcUjj7ks6R5WTZuWKv44FBbLpwU3sA=="], + "jsx-ast-utils/array-includes/es-abstract/call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], - "vaul/@radix-ui/react-dialog/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA=="], + "jsx-ast-utils/array-includes/es-abstract/is-weakref": ["is-weakref@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2" } }, "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q=="], - "vaul/@radix-ui/react-dialog/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw=="], + "jsx-ast-utils/array-includes/es-abstract/object-inspect": ["object-inspect@1.13.3", "", {}, "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA=="], - "vaul/@radix-ui/react-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.1", "", { "dependencies": { "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg=="], + "jsx-ast-utils/array-includes/es-abstract/which-typed-array": ["which-typed-array@1.1.18", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA=="], - "vaul/@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="], + "@puppeteer/browsers/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "@puppeteer/browsers/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@puppeteer/browsers/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], } } diff --git a/components.json b/components.json deleted file mode 100644 index 964f355..0000000 --- a/components.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "default", - "rsc": true, - "tsx": true, - "tailwind": { - "config": "tailwind.config.ts", - "css": "app/globals.css", - "baseColor": "zinc", - "cssVariables": true, - "prefix": "" - }, - "aliases": { - "hooks": "@/hooks", - "components": "@/components", - "ui": "@/ui", - "lib": "@/utils", - "utils": "@/utils/shadcn" - }, - "iconLibrary": "lucide" -} diff --git a/components/BreadCrumbNav.tsx b/components/BreadCrumbNav.tsx deleted file mode 100644 index d13a806..0000000 --- a/components/BreadCrumbNav.tsx +++ /dev/null @@ -1,42 +0,0 @@ -'use client'; - -import { usePathname } from 'next/navigation'; -import { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbSeparator } from '@/ui/breadcrumb'; -import { MdMapsHomeWork } from 'react-icons/md'; - -import Link from 'next/link'; - -export default () => { - const path = usePathname(); - const segments = path.split('/').filter(Boolean); - - return ( - <Breadcrumb className="flex h-full w-fit shrink-0 items-center gap-2.5"> - <BreadcrumbList key={'list--0'} className="!gap-2.5 w-full"> - <BreadcrumbItem key={'home--0'}> - <Link className="hover:text-zinc-200 capitalize" href={'/'}> - <MdMapsHomeWork className="size-5" /> - </Link> - </BreadcrumbItem> - - {segments.map((segment, i) => ( - <> - <BreadcrumbSeparator key={'separator--' + segment + i} /> - - <BreadcrumbItem - key={segment + i} - className="max-md:max-w-[25%]"> - <Link - className="hover:text-zinc-200 capitalize line-clamp-1 " - href={'/' + segments.slice(0, i + 1).join('/')}> - {segment} - </Link> - </BreadcrumbItem> - </> - ))} - </BreadcrumbList> - </Breadcrumb> - ); - - return; -}; diff --git a/components/Copy.tsx b/components/Copy.tsx deleted file mode 100644 index b9aa0d6..0000000 --- a/components/Copy.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { cn } from '@/utils/shadcn'; -import { Button } from '@/ui/button'; -import { CopyIcon } from '@radix-ui/react-icons'; - -export default ({ text, className }: { text: string; className?: string }) => { - const handleClick = () => { - navigator.clipboard.writeText(text); - }; - - return ( - <Button - onClick={handleClick} - className={cn( - 'group flex justify-center items-center bg-background aspect-square border-input border', - className - )}> - <CopyIcon className="!size-4/5 p-0.5 text-foreground group-hover:text-background" /> - </Button> - ); -}; diff --git a/components/add-member-dialog.tsx b/components/add-member-dialog.tsx deleted file mode 100644 index 8244dcb..0000000 --- a/components/add-member-dialog.tsx +++ /dev/null @@ -1,254 +0,0 @@ -'use client'; - -import type React from 'react'; -import { useState } from 'react'; -import { - AlertDialog, - AlertDialogContent, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogCancel, -} from '@/ui/alert-dialog'; -import { Button } from '@/ui/button'; -import { Checkbox } from '@/ui/checkbox'; -import { Input } from '@/ui/input'; -import { Label } from '@/ui/label'; -import { RadioGroup, RadioGroupItem } from '@/ui/radio-group'; -import { Separator } from '@/ui/separator'; -import { Copy, Mail } from 'lucide-react'; - -const permissions = [ - { - id: 'view-projects', - label: 'View projects', - description: 'Can view all projects in the organization', - }, - { - id: 'create-projects', - label: 'Create projects', - description: 'Can create new projects', - }, - { - id: 'edit-projects', - label: 'Edit projects', - description: 'Can edit existing projects', - }, - { - id: 'delete-projects', - label: 'Delete projects', - description: 'Can delete existing projects', - }, - { - id: 'manage-members', - label: 'Manage members', - description: 'Can add/remove members and modify their roles', - }, - { - id: 'billing-access', - label: 'Billing access', - description: 'Can view and modify billing information', - }, -]; - -interface AddMemberDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export function AddMemberDialog({ open, onOpenChange }: AddMemberDialogProps) { - const [email, setEmail] = useState(''); - const [username, setUsername] = useState(''); - const [role, setRole] = useState('member'); - const [selectedPermissions, setSelectedPermissions] = useState<string[]>([]); - const [useGoogleAccount, setUseGoogleAccount] = useState(false); - - const inviteLink = 'https://acme.com/invite/xyz123'; // This would be generated dynamically - - function onSubmit(e: React.FormEvent) { - e.preventDefault(); - console.log({ email, username, role, selectedPermissions, useGoogleAccount }); - onOpenChange(false); - } - - return ( - <AlertDialog open={open} onOpenChange={onOpenChange}> - <AlertDialogContent className="max-w-2xl"> - <AlertDialogHeader> - <AlertDialogTitle>Add new member</AlertDialogTitle> - <AlertDialogDescription> - Invite a new member to your organization. They will - receive an email invitation. - </AlertDialogDescription> - </AlertDialogHeader> - - <form onSubmit={onSubmit} className="space-y-6"> - <div className="space-y-4"> - <div className="space-y-2"> - <Label htmlFor="email">Email address</Label> - <Input - id="email" - type="email" - placeholder="member@example.com" - value={email} - onChange={e => - setEmail(e.target.value) - } - required - /> - </div> - - <div className="space-y-2"> - <Label htmlFor="username">Username</Label> - <Input - id="username" - placeholder="johndoe" - value={username} - onChange={e => - setUsername(e.target.value) - } - required - /> - </div> - - <div className="flex items-center space-x-2"> - <Checkbox - id="useGoogleAccount" - checked={useGoogleAccount} - onCheckedChange={checked => - setUseGoogleAccount( - checked as boolean - ) - } - /> - <div className="grid gap-1.5 leading-none"> - <Label htmlFor="useGoogleAccount"> - Use Google Account - </Label> - <p className="text-sm text-muted-foreground"> - Allow sign in with Google - account using this email - </p> - </div> - </div> - - <div className="space-y-2"> - <Label>Invitation link</Label> - <div className="flex space-x-2"> - <Input - readOnly - value={inviteLink} - /> - <Button - type="button" - variant="outline" - size="icon" - onClick={() => - navigator.clipboard.writeText( - inviteLink - ) - }> - <Copy className="h-4 w-4" /> - </Button> - </div> - <p className="text-sm text-muted-foreground"> - Share this link to invite members - directly - </p> - </div> - </div> - - <Separator /> - - <div className="space-y-4"> - <div className="space-y-2"> - <Label>Role</Label> - <RadioGroup - value={role} - onValueChange={setRole}> - <div className="flex items-center space-x-2"> - <RadioGroupItem - value="admin" - id="admin" - /> - <Label htmlFor="admin"> - Admin - Full access - to all resources - </Label> - </div> - <div className="flex items-center space-x-2"> - <RadioGroupItem - value="member" - id="member" - /> - <Label htmlFor="member"> - Member - Limited - access to resources - </Label> - </div> - </RadioGroup> - </div> - - <div className="space-y-2"> - <Label>Permissions</Label> - <p className="text-sm text-muted-foreground"> - Select specific permissions for this - member - </p> - {permissions.map(permission => ( - <div - key={permission.id} - className="flex items-start space-x-2"> - <Checkbox - id={permission.id} - checked={selectedPermissions.includes( - permission.id - )} - onCheckedChange={checked => { - setSelectedPermissions( - checked - ? [ - ...selectedPermissions, - permission.id, - ] - : selectedPermissions.filter( - id => - id !== - permission.id - ) - ); - }} - /> - <div className="grid gap-1.5 leading-none"> - <Label - htmlFor={ - permission.id - }> - { - permission.label - } - </Label> - <p className="text-sm text-muted-foreground"> - { - permission.description - } - </p> - </div> - </div> - ))} - </div> - </div> - - <AlertDialogFooter> - <AlertDialogCancel>Cancel</AlertDialogCancel> - <Button type="submit"> - <Mail className="mr-2 h-4 w-4" /> - Send invitation - </Button> - </AlertDialogFooter> - </form> - </AlertDialogContent> - </AlertDialog> - ); -} diff --git a/components/chat/_export.tsx b/components/chat/_export.tsx deleted file mode 100644 index 64740cc..0000000 --- a/components/chat/_export.tsx +++ /dev/null @@ -1,821 +0,0 @@ -'use client'; - -import * as React from 'react'; -import { Slot } from '@radix-ui/react-slot'; -import { VariantProps, cva } from 'class-variance-authority'; - -import { useIsMobile } from '@/hooks/use-mobile'; -import { cn } from '@/utils/shadcn'; -import { Button } from '@/ui/button'; -import { Input } from '@/ui/input'; -import { Separator } from '@/ui/separator'; -import { Sheet, SheetContent, SheetTrigger } from '@/ui/sheet'; -import { Skeleton } from '@/ui/skeleton'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/ui/tooltip'; -import { useIsTablet } from '@/hooks/use-tablet'; -import { Drawer, DrawerContent, DrawerTrigger } from './drawer'; -import { TbLayoutSidebarRightCollapseFilled } from 'react-icons/tb'; -import { TbLayoutSidebarRightExpandFilled } from 'react-icons/tb'; -import { BsChevronBarExpand } from 'react-icons/bs'; - -const SIDEBAR_COOKIE_NAME = 'sidebar:state'; -const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; -const SIDEBAR_WIDTH = '16rem'; -const SIDEBAR_WIDTH_MOBILE = '18rem'; -const SIDEBAR_WIDTH_ICON = '5rem'; -const SIDEBAR_KEYBOARD_SHORTCUT = 'b'; - -type SidebarContext = { - state: 'expanded' | 'collapsed'; - open: boolean; - setOpen: (open: boolean) => void; - openMobile: boolean; - setOpenMobile: (open: boolean) => void; - isMobile: boolean; - openTablet: boolean; - setOpenTablet: (open: boolean) => void; - isTablet: boolean; - toggleSidebar: () => void; -}; - -const SidebarContext = React.createContext<SidebarContext | null>(null); - -function useSidebar() { - const context = React.useContext(SidebarContext); - if (!context) { - throw new Error('useSidebar must be used within a SidebarProvider.'); - } - - return context; -} - -const SidebarProvider = React.forwardRef< - HTMLDivElement, - React.ComponentProps<'div'> & { - defaultOpen?: boolean; - open?: boolean; - onOpenChange?: (open: boolean) => void; - } ->( - ( - { - defaultOpen = true, - open: openProp, - onOpenChange: setOpenProp, - className, - style, - children, - ...props - }, - ref - ) => { - const isMobile = useIsMobile(); - const isTablet = useIsTablet(); - const [openMobile, setOpenMobile] = React.useState(false); - const [openTablet, setOpenTablet] = React.useState(false); - - // This is the internal state of the sidebar. - // We use openProp and setOpenProp for control from outside the component. - const [_open, _setOpen] = React.useState(defaultOpen); - const open = openProp ?? _open; - const setOpen = React.useCallback( - (value: boolean | ((value: boolean) => boolean)) => { - const openState = typeof value === 'function' ? value(open) : value; - if (setOpenProp) { - setOpenProp(openState); - } else { - _setOpen(openState); - } - - // This sets the cookie to keep the sidebar state. - document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; - }, - [setOpenProp, open] - ); - - // Helper to toggle the sidebar. - const toggleSidebar = React.useCallback(() => { - if (isMobile) return setOpenMobile(open => !open); - if (isTablet) return setOpenTablet(open => !open); - return setOpen(open => !open); - }, [isMobile, isTablet, setOpen, setOpenMobile, setOpenTablet]); - - // Adds a keyboard shortcut to toggle the sidebar. - React.useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if ( - event.key === SIDEBAR_KEYBOARD_SHORTCUT && - (event.metaKey || event.ctrlKey) - ) { - event.preventDefault(); - toggleSidebar(); - } - }; - - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [toggleSidebar]); - - // We add a state so that we can do data-state="expanded" or "collapsed". - // This makes it easier to style the sidebar with Tailwind classes. - const state = open ? 'expanded' : 'collapsed'; - - const contextValue = React.useMemo<SidebarContext>( - () => ({ - state, - open, - setOpen, - isMobile, - openMobile, - setOpenMobile, - isTablet, - setOpenTablet, - openTablet, - toggleSidebar, - }), - [ - state, - open, - setOpen, - isMobile, - openMobile, - setOpenMobile, - isTablet, - setOpenTablet, - openTablet, - toggleSidebar, - ] - ); - - return ( - <SidebarContext.Provider value={contextValue}> - <TooltipProvider delayDuration={0}> - <div - style={ - { - '--sidebar-width': SIDEBAR_WIDTH, - '--sidebar-width-icon': - SIDEBAR_WIDTH_ICON, - ...style, - } as React.CSSProperties - } - className={cn( - 'group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar', - className - )} - ref={ref} - {...props}> - {children} - </div> - </TooltipProvider> - </SidebarContext.Provider> - ); - } -); -SidebarProvider.displayName = 'SidebarProvider'; - -const Sidebar = React.forwardRef< - HTMLDivElement, - React.ComponentProps<'div'> & { - side?: 'left' | 'right'; - variant?: 'sidebar' | 'floating' | 'inset'; - collapsible?: 'offcanvas' | 'icon' | 'none'; - } ->( - ( - { - side = 'left', - variant = 'sidebar', - collapsible = 'offcanvas', - className, - children, - ...props - }, - ref - ) => { - const { - isMobile, - isTablet, - openTablet, - setOpenTablet, - state, - openMobile, - setOpenMobile, - } = useSidebar(); - - if (collapsible === 'none') { - return ( - <div - className={cn( - 'flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground', - className - )} - ref={ref} - {...props}> - {children} - </div> - ); - } - - if (isMobile) - return ( - <Drawer open={openMobile} onOpenChange={setOpenMobile} {...props}> - <DrawerTrigger asChild> - <div className="z-50 fixed bottom-[--p] right-[--p] flex flex-row items-center gap-x-2.5 bg-foreground p-3 rounded-full"> - <div className="relative size-full flex justify-center items-center "> - <SidebarTrigger className="absolute inset-auto z-50 opacity-0" /> - <BsChevronBarExpand className="size-5 bg-background" /> - </div> - </div> - </DrawerTrigger> - <DrawerContent - data-sidebar="sidebar" - data-mobile="true" - className="w-screen bg-sidebar p-[--external-p] text-sidebar-foreground [&>button]:hidden min-h-96" - style={ - { - '--sidebar-width': - SIDEBAR_WIDTH_MOBILE, - } as React.CSSProperties - }> - <div className="flex h-full w-full flex-col gap-[--external-p]"> - {children} - </div> - </DrawerContent> - </Drawer> - ); - if (isTablet) - return ( - <Sheet open={openTablet} onOpenChange={setOpenTablet} {...props}> - <SheetTrigger asChild> - <div className="z-50 fixed bottom-[--p] left-[--p] flex flex-row items-center gap-x-2.5 bg-foreground p-3 rounded-full"> - <div className="relative size-full flex justify-center items-center "> - <SidebarTrigger className="absolute inset-auto z-50 opacity-0" /> - {!openTablet ? ( - <TbLayoutSidebarRightCollapseFilled className="bg-background size-8" /> - ) : ( - <TbLayoutSidebarRightExpandFilled className="bg-background size-8" /> - )} - </div> - </div> - </SheetTrigger> - <SheetContent - data-sidebar="sidebar" - data-mobile="true" - className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden" - style={ - { - '--sidebar-width': - SIDEBAR_WIDTH_MOBILE, - } as React.CSSProperties - } - side={side}> - <div className="flex h-full w-full flex-col"> - {children} - </div> - </SheetContent> - </Sheet> - ); - return ( - <div - ref={ref} - className="group peer hidden md:block text-sidebar-foreground" - data-state={state} - data-collapsible={state === 'collapsed' ? collapsible : ''} - data-variant={variant} - data-side={side}> - {/* This is what handles the sidebar gap on desktop */} - <div - className={cn( - 'duration-200 relative h-svh w-[--sidebar-width] bg-transparent transition-[width] ease-linear ', - 'group-data-[collapsible=offcanvas]:w-[--p]', - // "group-data-[collapsible=offcanvas]:w-0", - 'group-data-[side=right]:rotate-180', - variant === 'floating' || variant === 'inset' - ? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]' - : 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]' - )} - /> - <div - className={cn( - 'duration-200 fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] ease-linear md:flex', - side === 'left' - ? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]' - : 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]', - // Adjust the padding for floating and inset variants. - variant === 'floating' || variant === 'inset' - ? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]' - : 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l', - '!bg-transparent !border-none' - )} - {...props}> - <div - data-sidebar="sidebar" - className={cn( - 'flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow', - className - )}> - {children} - </div> - </div> - </div> - ); - } -); -Sidebar.displayName = 'Sidebar'; - -const SidebarTrigger = React.forwardRef< - React.ElementRef<typeof Button>, - React.ComponentProps<typeof Button> ->(({ className, onClick, children, ...props }, ref) => { - const { toggleSidebar, open } = useSidebar(); - - return ( - <Button - ref={ref} - data-sidebar="trigger" - variant="ghost" - size="icon" - className={cn('size-8 [&>svg]:size-6 !p-0', className)} - onClick={event => { - onClick?.(event); - toggleSidebar(); - }} - {...props}> - {children ?? - (!open ? ( - <TbLayoutSidebarRightCollapseFilled className="size-8" /> - ) : ( - <TbLayoutSidebarRightExpandFilled className="size-8" /> - ))} - <span className="sr-only">Toggle Sidebar</span> - </Button> - ); -}); -SidebarTrigger.displayName = 'SidebarTrigger'; - -const SidebarRail = React.forwardRef<HTMLButtonElement, React.ComponentProps<'button'>>( - ({ className, ...props }, ref) => { - const { toggleSidebar } = useSidebar(); - - return ( - <button - ref={ref} - data-sidebar="rail" - aria-label="Toggle Sidebar" - tabIndex={-1} - onClick={toggleSidebar} - title="Toggle Sidebar" - className={cn( - 'absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex', - '[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize', - '[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize', - 'group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar', - '[[data-side=left][data-collapsible=offcanvas]_&]:-right-2', - '[[data-side=right][data-collapsible=offcanvas]_&]:-left-2', - className - )} - {...props} - /> - ); - } -); -SidebarRail.displayName = 'SidebarRail'; - -const SidebarInset = React.forwardRef<HTMLDivElement, React.ComponentProps<'main'>>( - ({ className, ...props }, ref) => { - return ( - <div - ref={ref} - className={cn( - 'relative flex max-h-svh h-full flex-1 flex-col', - 'peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow', - className - )} - {...props} - /> - ); - } -); -SidebarInset.displayName = 'SidebarInset'; - -const SidebarInput = React.forwardRef< - React.ElementRef<typeof Input>, - React.ComponentProps<typeof Input> ->(({ className, ...props }, ref) => { - return ( - <Input - ref={ref} - data-sidebar="input" - className={cn( - 'h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring', - className - )} - {...props} - /> - ); -}); -SidebarInput.displayName = 'SidebarInput'; - -const SidebarHeader = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>( - ({ className, ...props }, ref) => { - return ( - <div - ref={ref} - data-sidebar="header" - className={cn('flex flex-col gap-2 p-2', className)} - {...props} - /> - ); - } -); -SidebarHeader.displayName = 'SidebarHeader'; - -const SidebarFooter = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>( - ({ className, ...props }, ref) => { - return ( - <div - ref={ref} - data-sidebar="footer" - className={cn('flex flex-col gap-2 p-2', className)} - {...props} - /> - ); - } -); -SidebarFooter.displayName = 'SidebarFooter'; - -const SidebarSeparator = React.forwardRef< - React.ElementRef<typeof Separator>, - React.ComponentProps<typeof Separator> ->(({ className, ...props }, ref) => { - return ( - <Separator - ref={ref} - data-sidebar="separator" - className={cn('mx-2 w-auto bg-sidebar-border', className)} - {...props} - /> - ); -}); -SidebarSeparator.displayName = 'SidebarSeparator'; - -const SidebarContent = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>( - ({ className, ...props }, ref) => { - return ( - <div - ref={ref} - data-sidebar="content" - className={cn( - 'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden', - className - )} - {...props} - /> - ); - } -); -SidebarContent.displayName = 'SidebarContent'; - -const SidebarGroup = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>( - ({ className, ...props }, ref) => { - return ( - <div - ref={ref} - data-sidebar="group" - className={cn('relative flex w-full min-w-0 flex-col', className)} - {...props} - /> - ); - } -); -SidebarGroup.displayName = 'SidebarGroup'; - -const SidebarGroupLabel = React.forwardRef< - HTMLDivElement, - React.ComponentProps<'div'> & { asChild?: boolean } ->(({ className, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : 'div'; - - return ( - <Comp - ref={ref} - data-sidebar="group-label" - className={cn( - 'duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', - 'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0', - className - )} - {...props} - /> - ); -}); -SidebarGroupLabel.displayName = 'SidebarGroupLabel'; - -const SidebarGroupAction = React.forwardRef< - HTMLButtonElement, - React.ComponentProps<'button'> & { asChild?: boolean } ->(({ className, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : 'button'; - - return ( - <Comp - ref={ref} - data-sidebar="group-action" - className={cn( - 'absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', - // Increases the hit area of the button on mobile. - 'after:absolute after:-inset-2 after:md:hidden', - 'group-data-[collapsible=icon]:hidden', - className - )} - {...props} - /> - ); -}); -SidebarGroupAction.displayName = 'SidebarGroupAction'; - -const SidebarGroupContent = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>( - ({ className, ...props }, ref) => ( - <div - ref={ref} - data-sidebar="group-content" - className={cn('w-full text-sm', className)} - {...props} - /> - ) -); -SidebarGroupContent.displayName = 'SidebarGroupContent'; - -const SidebarMenu = React.forwardRef<HTMLUListElement, React.ComponentProps<'ul'>>( - ({ className, ...props }, ref) => ( - <ul - ref={ref} - data-sidebar="menu" - className={cn('flex w-full min-w-0 flex-col gap-1', className)} - {...props} - /> - ) -); -SidebarMenu.displayName = 'SidebarMenu'; - -const SidebarMenuItem = React.forwardRef<HTMLLIElement, React.ComponentProps<'li'>>( - ({ className, ...props }, ref) => ( - <li - ref={ref} - data-sidebar="menu-item" - className={cn('group/menu-item relative', className)} - {...props} - /> - ) -); -SidebarMenuItem.displayName = 'SidebarMenuItem'; - -const sidebarMenuButtonVariants = cva( - 'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0', - { - variants: { - variant: { - default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground', - outline: 'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]', - }, - size: { - default: 'h-8 text-sm', - sm: 'h-7 text-xs', - lg: 'h-12 text-sm group-data-[collapsible=icon]:!p-0', - }, - }, - defaultVariants: { - variant: 'default', - size: 'default', - }, - } -); - -const SidebarMenuButton = React.forwardRef< - HTMLButtonElement, - React.ComponentProps<'button'> & { - asChild?: boolean; - isActive?: boolean; - tooltip?: string | React.ComponentProps<typeof TooltipContent>; - } & VariantProps<typeof sidebarMenuButtonVariants> ->( - ( - { - asChild = false, - isActive = false, - variant = 'default', - size = 'default', - tooltip, - className, - ...props - }, - ref - ) => { - const Comp = asChild ? Slot : 'button'; - const { isMobile, state } = useSidebar(); - - const button = ( - <Comp - ref={ref} - data-sidebar="menu-button" - data-size={size} - data-active={isActive} - className={cn( - sidebarMenuButtonVariants({ variant, size }), - className - )} - {...props} - /> - ); - - if (!tooltip) { - return button; - } - - if (typeof tooltip === 'string') { - tooltip = { - children: tooltip, - }; - } - - return ( - <Tooltip> - <TooltipTrigger asChild>{button}</TooltipTrigger> - <TooltipContent - side="right" - align="center" - hidden={state !== 'collapsed' || isMobile} - {...tooltip} - /> - </Tooltip> - ); - } -); -SidebarMenuButton.displayName = 'SidebarMenuButton'; - -const SidebarMenuAction = React.forwardRef< - HTMLButtonElement, - React.ComponentProps<'button'> & { - asChild?: boolean; - showOnHover?: boolean; - } ->(({ className, asChild = false, showOnHover = false, ...props }, ref) => { - const Comp = asChild ? Slot : 'button'; - - return ( - <Comp - ref={ref} - data-sidebar="menu-action" - className={cn( - 'absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0', - // Increases the hit area of the button on mobile. - 'after:absolute after:-inset-2 after:md:hidden', - 'peer-data-[size=sm]/menu-button:top-1', - 'peer-data-[size=default]/menu-button:top-1.5', - 'peer-data-[size=lg]/menu-button:top-2.5', - 'group-data-[collapsible=icon]:hidden', - showOnHover && - 'group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0', - className - )} - {...props} - /> - ); -}); -SidebarMenuAction.displayName = 'SidebarMenuAction'; - -const SidebarMenuBadge = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>( - ({ className, ...props }, ref) => ( - <div - ref={ref} - data-sidebar="menu-badge" - className={cn( - 'absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground select-none pointer-events-none', - 'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground', - 'peer-data-[size=sm]/menu-button:top-1', - 'peer-data-[size=default]/menu-button:top-1.5', - 'peer-data-[size=lg]/menu-button:top-2.5', - 'group-data-[collapsible=icon]:hidden', - className - )} - {...props} - /> - ) -); -SidebarMenuBadge.displayName = 'SidebarMenuBadge'; - -const SidebarMenuSkeleton = React.forwardRef< - HTMLDivElement, - React.ComponentProps<'div'> & { - showIcon?: boolean; - } ->(({ className, showIcon = false, ...props }, ref) => { - // Random width between 50 to 90%. - const width = React.useMemo(() => { - return `${Math.floor(Math.random() * 40) + 50}%`; - }, []); - - return ( - <div - ref={ref} - data-sidebar="menu-skeleton" - className={cn('rounded-md h-8 flex gap-2 px-2 items-center', className)} - {...props}> - {showIcon && ( - <Skeleton - className="size-4 rounded-md" - data-sidebar="menu-skeleton-icon" - /> - )} - <Skeleton - className="h-4 flex-1 max-w-[--skeleton-width]" - data-sidebar="menu-skeleton-text" - style={ - { - '--skeleton-width': width, - } as React.CSSProperties - } - /> - </div> - ); -}); -SidebarMenuSkeleton.displayName = 'SidebarMenuSkeleton'; - -const SidebarMenuSub = React.forwardRef<HTMLUListElement, React.ComponentProps<'ul'>>( - ({ className, ...props }, ref) => ( - <ul - ref={ref} - data-sidebar="menu-sub" - className={cn( - 'mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5', - 'group-data-[collapsible=icon]:hidden', - className - )} - {...props} - /> - ) -); -SidebarMenuSub.displayName = 'SidebarMenuSub'; - -const SidebarMenuSubItem = React.forwardRef<HTMLLIElement, React.ComponentProps<'li'>>( - ({ ...props }, ref) => <li ref={ref} {...props} /> -); -SidebarMenuSubItem.displayName = 'SidebarMenuSubItem'; - -const SidebarMenuSubButton = React.forwardRef< - HTMLAnchorElement, - React.ComponentProps<'a'> & { - asChild?: boolean; - size?: 'sm' | 'md'; - isActive?: boolean; - } ->(({ asChild = false, size = 'md', isActive, className, ...props }, ref) => { - const Comp = asChild ? Slot : 'a'; - - return ( - <Comp - ref={ref} - data-sidebar="menu-sub-button" - data-size={size} - data-active={isActive} - className={cn( - 'flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground', - 'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground', - size === 'sm' && 'text-xs', - size === 'md' && 'text-sm', - 'group-data-[collapsible=icon]:hidden', - className - )} - {...props} - /> - ); -}); -SidebarMenuSubButton.displayName = 'SidebarMenuSubButton'; - -export { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarGroup, - SidebarGroupAction, - SidebarGroupContent, - SidebarGroupLabel, - SidebarHeader, - SidebarInput, - SidebarInset, - SidebarMenu, - SidebarMenuAction, - SidebarMenuBadge, - SidebarMenuButton, - SidebarMenuItem, - SidebarMenuSkeleton, - SidebarMenuSub, - SidebarMenuSubButton, - SidebarMenuSubItem, - SidebarProvider, - SidebarRail, - SidebarSeparator, - SidebarTrigger, - useSidebar, -}; diff --git a/components/chat/inner.tsx b/components/chat/inner.tsx deleted file mode 100644 index a6ac04c..0000000 --- a/components/chat/inner.tsx +++ /dev/null @@ -1,85 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; -import { SheetContent, SheetTrigger } from '@/ui/sheet'; - -import { SidebarTrigger } from '@/ui/sidebar'; -import { IoMdChatbubbles } from 'react-icons/io'; -import { IoClose } from 'react-icons/io5'; -import { IoIosArrowDown } from 'react-icons/io'; - -import Chat from '@/app/@inset/_components/chat'; - -import sendMessage from '@/actions/send-message'; -import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from '@/ui/select'; -import { Separator } from '@/ui/separator'; - -export const InnerChatTrigger = ({ Icon }: { Icon?: any }) => ( - <SheetTrigger className="relative group flex items-center gap-2 px-3 py-1.5 rounded-full hover:bg-foreground/5 transition-colors duration-200"> - {/* Pulsing AI Badge */} - <div className="flex items-center gap-1.5"> - <span className="relative flex items-center justify-center"> - <span className="absolute inline-flex h-2 w-2 rounded-full bg-green-500 opacity-75 animate-ping"></span> - <span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span> - </span> - <span className="text-xs font-bold tracking-wider text-foreground/70 group-hover:text-foreground/90 transition-colors">AI</span> - </div> - - {/* Icon */} - {Icon ? Icon : <IoMdChatbubbles className="size-5 text-foreground/60 group-hover:text-foreground/80 transition-colors" />} - </SheetTrigger> -); - -export default ({ containerSelector }: { containerSelector: string }) => { - const [model, setModel] = useState<any>('gemini-1.5-flash-8b'); - const [container, setContainer] = useState<Element>(null); - - useEffect(() => { - setContainer(document.querySelector(containerSelector)!); - }, []); - - if (!container) return null; - - return ( - <SheetContent - container={container} - onInteractOutside={e => e.preventDefault()} - className="z-50 absolute flex flex-col gap-[--p] p-[--p] size-full !max-w-full !bg-none !bg-inset overflow-hidden"> - <div className="shrink-0 flex flex-row justify-between items-start w-full gap-[--p]"> - <SidebarTrigger className="justify-start size-fit [&>svg]:text-zinc-400 [&>svg]:!size-5 [&>svg]:!p-0 " /> - <Select value={model} onValueChange={setModel}> - <SelectTrigger className="size-fit !p-0 [&>span]:!text-base [&>span]:!font-righteous [&>span]:pr-4 [&>svg]:hidden h-full !border-none !ring-0 !ring-transparent !outline-none"> - <SelectValue placeholder="Select a model" /> - <IoIosArrowDown className="!block size-4 text-foreground/45" /> - </SelectTrigger> - <SelectContent sideOffset={15}> - <SelectGroup> - <SelectLabel>Frequent use</SelectLabel> - <SelectItem value="gemini-1.5-flash">Gemini 1.5 flash</SelectItem> - <SelectItem value="gemini-1.5-flash-8b">Gemini 1.5 flash 8b</SelectItem> - </SelectGroup> - <Separator /> - <SelectGroup> - <SelectLabel>High reasoning</SelectLabel> - <SelectItem value="gemini-1.5-pro">Gemini 1.5 pro</SelectItem> - <SelectItem value="gemini-2.0-flash">Gemini 2 flash</SelectItem> - </SelectGroup> - </SelectContent> - </Select> - <InnerChatTrigger Icon={<IoClose className="size-5" />} /> - </div> - <Chat - onSend={async history => - await sendMessage( - model, - history.slice(0, -1), - history - .slice(-1)[0] - .parts.map(p => p.text) - .join('\n'), - ) - } - /> - </SheetContent> - ); -}; diff --git a/components/chat/sidebar.tsx b/components/chat/sidebar.tsx deleted file mode 100644 index 0c07867..0000000 --- a/components/chat/sidebar.tsx +++ /dev/null @@ -1,34 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; -import { SheetContent, SheetTrigger } from '@/ui/sheet'; -import { BsChevronBarExpand } from 'react-icons/bs'; -import { SidebarTrigger } from '@/ui/sidebar'; - -export default ({ containerSelector }: { containerSelector: string }) => { - const [container, setContainer] = useState<Element>(null); - - useEffect(() => { - setContainer(document.querySelector(containerSelector)!); - }, []); - - if (!container) return null; - - return ( - <SheetContent - side="left" - container={container} - className="z-50 absolute size-full !max-w-full !bg-none !bg-background"> - <div className="shrink-0 flex flex-row justify-between items-center w-full gap-[--p]"> - <div className="flex flex-row items-center h-full gap-x-[--p]"> - <div className="flex flex-row items-center h-full gap-x-[--p] max-lg:hidden"> - <SidebarTrigger className="justify-start h-full w-fit [&>svg]:text-zinc-400 [&>svg]:!size-5 [&>svg]:!p-0 " /> - </div> - </div> - <SheetTrigger> - <BsChevronBarExpand className="size-5" /> - </SheetTrigger> - </div> - </SheetContent> - ); -}; diff --git a/components/check-progress.tsx b/components/check-progress.tsx deleted file mode 100644 index fc10589..0000000 --- a/components/check-progress.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { motion, useMotionValue, useTransform } from 'framer-motion'; -import { useState } from 'react'; - -export default function CircularProgress() { - const [ended, setEnded] = useState(false); - let progress = useMotionValue(90); - const circleLength = useTransform(progress, [0, 100], [0, 1]); - const checkmarkPathLength = useTransform(progress, [0, 95, 100], [0, 0, 1]); - const circleColor = useTransform(progress, [0, 95, 100], ['#FFCC66', '#FFCC66', '#3c79dd']); - - return ( - <> - <motion.div - className="absolute" - initial={{ x: 0 }} - animate={{ x: 100 }} - style={{ x: progress }} - transition={{ duration: 0.85, ease: 'easeInOut' }} - onAnimationComplete={() => { - setEnded(true); - }} - /> - <motion.svg - className="aspect-square size-full" - xmlns="http://www.w3.org/2000/svg" - width="258" - height="258" - viewBox="0 0 258 258"> - {/* Circle */} - <motion.path - d="M 130 6 C 198.483 6 254 61.517 254 130 C 254 198.483 198.483 254 130 254 C 61.517 254 6 198.483 6 130 C 6 61.517 61.517 6 130 6 Z" - fill={ended ? '#3c79dd' : 'transparent'} - strokeWidth={12} - stroke={circleColor} - style={{ - pathLength: circleLength, - }} - /> - {/* Check mark */} - <motion.path - transform="translate(60 85)" - d="M3 50L45 92L134 3" - fill="transparent" - stroke={ended ? '#000000' : '#3c79dd'} - strokeWidth={14} - style={{ pathLength: checkmarkPathLength }} - /> - </motion.svg> - </> - ); -} diff --git a/components/copy-popup.tsx b/components/copy-popup.tsx deleted file mode 100644 index 5abe7dc..0000000 --- a/components/copy-popup.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { motion, AnimatePresence } from "framer-motion" -import { Check } from "lucide-react" - -interface CopyPopupProps { - isVisible: boolean -} - -export function CopyPopup({ isVisible }: CopyPopupProps) { - return ( - <AnimatePresence> - {isVisible && ( - <motion.div - initial={{ opacity: 0, y: 50 }} - animate={{ opacity: 1, y: 0 }} - exit={{ opacity: 0, y: 50 }} - className="fixed bottom-4 right-4 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg flex items-center space-x-2" - > - <Check className="h-5 w-5" /> - <span>Copied to clipboard!</span> - </motion.div> - )} - </AnimatePresence> - ) -} - diff --git a/components/data-picker.tsx b/components/data-picker.tsx deleted file mode 100644 index 4c1108e..0000000 --- a/components/data-picker.tsx +++ /dev/null @@ -1,61 +0,0 @@ -"use client"; - -import * as React from "react"; -import { format } from "date-fns"; -import { CalendarIcon } from "lucide-react"; - -import { cn } from "@/utils/shadcn"; -import { Button, ButtonProps } from "@/ui/button"; -import { Calendar } from "@/ui/calendar"; -import { Popover, PopoverContent, PopoverTrigger } from "@/ui/popover"; - -export function DatePicker({ - onSelect, - defaultDate, - className, - placeholder, - ...props -}: ButtonProps & { - onSelect: (date: Date) => void; - defaultDate?: string | Date; - placeholder?: string; -}) { - const [date, setDate] = React.useState<Date>( - defaultDate instanceof Date - ? defaultDate - : defaultDate && defaultDate?.trim().length - ? new Date(defaultDate) - : null - ); - - const select = (date: Date) => { - setDate(date); - if (onSelect) onSelect(date); - }; - - return ( - <Popover> - <PopoverTrigger asChild> - <Button - variant={"outline"} - className={cn( - "w-[240px] justify-start text-left font-normal pt-[calc(var(--p)*1.5)] ", - !date && "text-muted-foreground", - className - )} - {...props} - > - <CalendarIcon /> - {!date || isNaN(date?.getTime()) ? ( - <span>{placeholder ?? "Pick a date"}</span> - ) : ( - format(date, "PPP") - )} - </Button> - </PopoverTrigger> - <PopoverContent className="w-auto p-0" align="start"> - <Calendar mode="single" selected={date} onSelect={select} initialFocus /> - </PopoverContent> - </Popover> - ); -} diff --git a/components/env-switcher.tsx b/components/env-switcher.tsx deleted file mode 100644 index 497735a..0000000 --- a/components/env-switcher.tsx +++ /dev/null @@ -1,82 +0,0 @@ -'use client'; - -import * as React from 'react'; - -import Image from 'next/image'; -import { Separator } from '@/ui/separator'; -import { useRouter, usePathname } from 'next/navigation'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/ui/select'; - -const Env = ({ name, description }: { name: string; description?: string }) => { - return ( - <SelectItem - value={name.toLowerCase()} - className=" cursor-pointer !rounded-none !p-2"> - <div className="flex flex-col w-full justify-center items-start gap-y-2 !px-2"> - <h2 className=" text-sm font-bold leading-none [font-family:var(--font-righteous)]"> - {name} - </h2> - <p className="line-clamp-1 text-left text-xs leading-none"> - {description} - </p> - </div> - </SelectItem> - ); -}; - -export default ({ - envs, -}: { - envs: { - name: string; - description?: string; - }[]; -}) => { - envs = envs.filter(env => env.name.toLowerCase() !== 'users'); - const router = useRouter(); - const path = usePathname(); - const segments = path.split('/').filter(Boolean); - - return ( - <Select - onValueChange={value => { - router.replace(`/${value}`); - }} - value={segments[0]}> - <SelectTrigger className="[&>span]:flex [&>span]:h-fit [&>span]:w-full z-50 shadow-none bg-transparent relative flex-row justify-start items-start size-full !px-p-[--sidebar-p] !py-0 gap-x-3.5 cursor-pointer !outline-none focus-within:outline-none !ring-0 !border-none focus-visible:ring-0"> - <div className={'relative h-clamp-10 w-fit min-w-16'}> - <Image - className="dark:invert !w-auto my-auto" - src="/logo.svg" - alt="Acme Logo" - fill - /> - </div> - <SelectValue - placeholder={ - <div className="flex flex-col justify-start items-start w-full h-full"> - <h2>Seleziona la pagina</h2> - <p className="text-xs">Edita i contenuti</p> - </div> - } - /> - </SelectTrigger> - <SelectContent - side="bottom" - align="start" - sideOffset={6} - className="items-center w-full text-neutral-500 rounded-md !p-0 !border-none !outline-none shadow-[0_0_0_1px_#ffffff24] light:shadow-[0_0_0_1px_#00000024]"> - {envs.map((env, i) => ( - <div - key={i} - className="relative group grid grid-rows-subgrid grid-cols-subgrid !p-0 w-full overflow-hidden"> - {i !== 0 && ( - <Separator className="absolute top-0 left-0 bg-neutral-800 w-full" /> - )} - <Env {...env} /> - </div> - ))} - </SelectContent> - </Select> - ); -}; diff --git a/components/formats.tsx b/components/formats.tsx deleted file mode 100644 index d0782cd..0000000 --- a/components/formats.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Bold, Italic, Underline } from 'lucide-react'; - -import { ToggleGroup, ToggleGroupItem } from '@/ui/toggle-group'; - -export function Formats() { - return ( - <ToggleGroup type="multiple"> - <ToggleGroupItem value="bold" aria-label="Toggle bold"> - <Bold className="h-4 w-4" /> - </ToggleGroupItem> - <ToggleGroupItem value="italic" aria-label="Toggle italic"> - <Italic className="h-4 w-4" /> - </ToggleGroupItem> - <ToggleGroupItem value="strikethrough" aria-label="Toggle strikethrough"> - <Underline className="h-4 w-4" /> - </ToggleGroupItem> - </ToggleGroup> - ); -} diff --git a/components/groups/_export.tsx b/components/groups/_export.tsx deleted file mode 100644 index 8b22ec1..0000000 --- a/components/groups/_export.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { useState } from 'react'; -import { Plus } from 'lucide-react'; -import { CgSortAz } from 'react-icons/cg'; - -import Group, { EditableGroup } from './group'; -import { SidebarGroup, SidebarMenu } from '@/ui/sidebar'; - -import type { Group as GroupType } from '@/_server/@dashboard/( journals )'; -import SubGroup from './subGroup'; - -export default function Groups({ groups: initGroups, section, ...actions }: Props) { - const [groups, setGroups] = useState<GroupType[]>(initGroups); - const [addGroup, setAddGroup] = useState<string | null>(null); - const [addSubGroup, setAddSubGroup] = useState<string | null>(null); - - return ( - <> - <div className="!p-[--sidebar-p]"> - <div className="flex flex-row items-center justify-between w-full opacity-70 px-2"> - <button> - <CgSortAz className="size-5 -ml-1" /> - </button> - - <button - onClick={() => { - console.log('addGroup', addGroup); - setAddGroup(''); - }}> - <Plus className="size-4" /> - </button> - </div> - </div> - {typeof addGroup === 'string' && ( - <EditableGroup - active - setActive={active => setAddGroup(y => (active ? y : null))} - onSendChanges={name => { - console.log('onSendChanges', name); - actions.group.onCreate(name); - setAddGroup(null); - }} - /> - )} - {groups.map(group => ( - <SidebarGroup className="!p-[calc(var(--p)/2)]"> - <Group - key={group._id} - {...group} - section={section} - onDelete={_id => { - actions.group.onDelete(_id); - setGroups(y => - y.filter(x => x._id !== _id) - ); - }} - onRename={name => { - actions.group.onRename({ - _id: group._id, - name, - }); - setGroups(y => - y.map(x => - x._id === group._id - ? { ...x, name } - : x - ) - ); - }} - onAddSubGroup={() => setAddSubGroup(group._id)} - /> - </SidebarGroup> - ))} - </> - ); -} - -interface Props extends Actions { - groups: Group[]; - section: string; -} - -interface Actions { - group: { - onCreate: (name: string) => { _id: string; name: string }; - onRename: ({ _id, name }: { _id: string; name: string }) => void; - onDelete: (groupId: string) => void; - }; - subGroup: { - onCreate: (groupId: string, subGroupName: string) => void; - onRename: ({ _id, name }: { _id: string; name: string }) => void; - onDelete: (subGroupId: string) => void; - }; -} diff --git a/components/groups/context.tsx b/components/groups/context.tsx deleted file mode 100644 index a30f153..0000000 --- a/components/groups/context.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { cn } from '@/utils/shadcn'; -import { Separator } from '@/ui/separator'; -import { - ContextMenu, - ContextMenuContent, - ContextMenuItem, - ContextMenuTrigger, -} from '@/ui/context-menu'; - -import useLongPress from '@/hooks/use-longPress'; - -import { useRef, type PropsWithChildren } from 'react'; - -export default function Context({ - actions, - children, -}: PropsWithChildren & { actions: React.ComponentPropsWithoutRef<typeof ContextMenuItem>[] }) { - const trigger = useRef<HTMLSpanElement>(null); - - const onLongPress = () => { - if (!trigger.current) return; - - const rect = trigger.current.getBoundingClientRect(); - - trigger.current.dispatchEvent( - new MouseEvent('contextmenu', { - bubbles: true, - clientX: rect.left, // Centro dell'elemento - clientY: rect.top, // Centro dell'elemento - }) - ); - }; - - const longPressEvent = useLongPress(onLongPress, { - shouldPreventDefault: false, - delay: 300, - }); - - return ( - <ContextMenu> - <ContextMenuTrigger ref={trigger} {...longPressEvent}> - {children} - </ContextMenuTrigger> - <ContextMenuContent className='w-full p-0 [&>[role="menuitem"]]:px-3 max-lg:-translate-y-[115%] '> - <ContextMenuItem {...actions[0]} className="" /> - {actions.slice(1).map(({ className, ...props }, i) => ( - <> - <Separator key={'separator--' + i} /> - <ContextMenuItem - key={i} - className={cn( - 'text-sidebar-foreground/50 hover:bg-accent hover:text-accent-foreground', - className - )} - {...props} - /> - </> - ))} - </ContextMenuContent> - </ContextMenu> - ); -} diff --git a/components/groups/group.tsx b/components/groups/group.tsx deleted file mode 100644 index efae047..0000000 --- a/components/groups/group.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { SidebarGroup, SidebarGroupLabel, SidebarMenuButton } from '@/ui/sidebar'; -import { Input } from '@/ui/input'; -import { useState } from 'react'; -import Context from './context'; - -import { Plus } from 'lucide-react'; - -import type { ReactNode } from 'react'; -import { SectionIcon } from '@radix-ui/react-icons'; -import { MdDeleteSweep, MdOutlineDriveFileRenameOutline } from 'react-icons/md'; - -export default function Group({ children, id, name, ...actions }: Props) { - const [toRename, setToRename] = useState<boolean>(false); - - return ( - <SidebarGroup className="!p-[--sidebar-p]"> - <Context - actions={[ - { - children: ( - <span className="flex flex-row justify-between items-center w-full gap-x-3.5"> - Rename - <MdOutlineDriveFileRenameOutline className="size-5" /> - </span> - ), - onClick: () => setToRename(true), - }, - { - children: ( - <span className="flex flex-row justify-between items-center w-full gap-x-3.5"> - Delete - <MdDeleteSweep className="size-5" /> - </span> - ), - className: 'text-red-400', - onClick: () => actions.onDelete(id), - }, - ]}> - <EditableGroup - group={{ id, name }} - toRename={toRename} - onRename={rename => { - setToRename(false); - if (!rename || !rename.trim().length || rename === name) return; - actions.onRename({ id, name: rename }); - }} - onCreateSubGroup={subGroupName => - actions.onCreateSubGroup(subGroupName) - } - /> - </Context> - {children} - </SidebarGroup> - ); -} - -export const EditableGroup = ({ - group, - toRename = false, - onRename, - onCreateSubGroup, -}: { - group?: { id: string; name: string }; - toRename?: boolean; - onRename?: (name: string) => void; - - onCreateSubGroup?: (subGroupName: string) => void; - onSort?: () => void; -}) => { - const [rename, setRename] = useState<string>(group?.name ?? ''); - const [addSubGroup, setAddSubGroup] = useState<{ active: boolean; subGroup: string }>({ - active: false, - subGroup: '', - }); - - return ( - <> - <SidebarGroupLabel - className="flex flex-row gap-x-2 w-full capitalize justify-between items-center" - onBlur={() => onRename('')}> - <span className="w-full h-fit"> - {toRename ? ( - <div className="w-full h-fit px-1.5"> - <Input - autoFocus - value={rename} - onChange={e => setRename(e.target.value)} - onKeyDown={e => { - if (e.key === 'Enter') { - onRename(rename); - } - }} - className="!text-xs h-fit w-full py-1 " - /> - </div> - ) : ( - group?.name - )} - </span> - {onCreateSubGroup && ( - <button - className="col-span-1 opacity-70" - onClick={() => - setAddSubGroup(p => ({ - ...p, - active: true, - })) - }> - <Plus className="size-4" /> - </button> - )} - </SidebarGroupLabel> - {addSubGroup.active && ( - <SidebarMenuButton - className="group-data-[collapsible=icon]:!p-1.5 group-data-[collapsible=icon]:hover:[&>svg]:text-foreground" - onBlur={() => setAddSubGroup(p => ({ ...p, active: false }))}> - <SectionIcon className="group-data-[collapsible=icon]:size-5 text-zinc-500 group-data-[state=open]/collapsible:text-white" /> - <Input - className="h-fit w-full py-1 px-1.5" - autoFocus - value={addSubGroup.subGroup} - onChange={e => - setAddSubGroup(p => ({ - ...p, - subGroup: e.target.value, - })) - } - onBlur={() => - setAddSubGroup(p => ({ - ...p, - active: false, - })) - } - onKeyDown={e => { - if (e.key === 'Enter') { - setAddSubGroup(p => ({ - ...p, - active: false, - })); - onCreateSubGroup(addSubGroup.subGroup); - } - }} - /> - </SidebarMenuButton> - )} - </> - ); -}; - -interface Actions { - onCreateSubGroup: (subGroupName: string) => void; - onRename: ({ id, name }: { id: string; name: string }) => void; - onDelete: (groupId: string) => void; -} - -interface Props extends Actions { - children?: ReactNode | ReactNode[]; - id: string; - name: string; -} diff --git a/components/groups/item.tsx b/components/groups/item.tsx deleted file mode 100644 index 77bb667..0000000 --- a/components/groups/item.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import Link from 'next/link'; -import { SidebarMenuSubButton, SidebarMenuSubItem } from '@/ui/sidebar'; -import { useState } from 'react'; -import Context from './context'; -import { Input } from '@/ui/input'; - -import { MdDeleteSweep } from 'react-icons/md'; -import { MdOutlineDriveFileRenameOutline } from 'react-icons/md'; - -export default function Item({ id, name, title, section, selected, ...actions }: Props) { - const [toRename, setToRename] = useState<boolean>(false); - - return ( - <SidebarMenuSubItem key={id} data-selected={selected}> - <Context - actions={[ - { - children: ( - <span className="flex flex-row justify-between items-center w-full gap-x-3.5"> - Rename - <MdOutlineDriveFileRenameOutline className="size-5" /> - </span> - ), - onClick: () => setToRename(true), - }, - { - children: ( - <span className="flex flex-row justify-between items-center w-full gap-x-3.5"> - Delete - <MdDeleteSweep className="size-5" /> - </span> - ), - className: 'text-red-400', - onClick: () => actions.onDelete(id), - }, - ]}> - <EditableItem - {...{ id, name, title, section }} - toRename={toRename} - onRename={rename => { - setToRename(false); - if (!rename || !rename.trim().length || rename === name) return; - actions.onRename(rename); - }} - onDelete={() => actions.onDelete(id)} - /> - </Context> - </SidebarMenuSubItem> - ); -} - -export const EditableItem = ({ - id, - name, - section, - toRename = false, - onRename, -}: - | (Partial<Props> & { toRename: true; onRename: (name: string) => void }) - | (Props & { toRename: false })) => { - const [rename, setRename] = useState<string>(name ?? ''); - - return toRename ? ( - <SidebarMenuSubButton - className="hover:bg-background bg-background" - asChild - onBlur={() => onRename('')}> - <Input - autoFocus - value={rename} - onChange={e => setRename(e.target.value)} - onKeyDown={e => { - if (e.key === 'Enter') onRename(rename); - }} - className="leading-7 w-full p-0 !ring-0 placeholder:text-xs" - /> - </SidebarMenuSubButton> - ) : ( - <SidebarMenuSubButton - asChild - className=" [*&:is([data-selected=true]_*)]:!bg-sidebar-accent"> - <Link href={`/${section}/${id}`} className="!line-clamp-1 leading-7"> - {name} - </Link> - </SidebarMenuSubButton> - ); -}; - -export type Actions = { - onRename: (name: string) => void; - onDelete: (groupId: string) => void; -}; - -export interface Props extends Actions { - id: string; - name: string; - title: string; - section: string; - selected: boolean; -} diff --git a/components/groups/subGroup.tsx b/components/groups/subGroup.tsx deleted file mode 100644 index 538cfeb..0000000 --- a/components/groups/subGroup.tsx +++ /dev/null @@ -1,156 +0,0 @@ -'use client'; - -import { type ReactNode, useState } from 'react'; - -import { ChevronRight } from 'lucide-react'; -import { SectionIcon } from '@radix-ui/react-icons'; -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/ui/collapsible'; -import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarMenuSub } from '@/ui/sidebar'; - -import { Input } from '@/ui/input'; -import Context from './context'; -import { EditableItem } from './item'; - -import { MdNoteAdd } from 'react-icons/md'; -import { MdDeleteSweep } from 'react-icons/md'; -import { MdOutlineDriveFileRenameOutline } from 'react-icons/md'; - -export default function SubGroup({ - children, - onCreateNewPost, - defaultOpen, - onRename, - onDelete, - ...subGroup -}: Props) { - const [deleted, setDeleted] = useState<boolean>(false); - const [toRename, setToRename] = useState<boolean>(false); - - const [name, setName] = useState<string | null>(subGroup.name); - const [newPost, setNewPost] = useState<string | null>(null); - - if (deleted) return null; - - return ( - <SidebarMenu> - <Collapsible - key={subGroup.id} - asChild - defaultOpen={defaultOpen} - className="group/collapsible"> - <SidebarMenuItem className="w-full"> - {toRename ? ( - <SidebarMenuButton - tooltip={name} - className="group-data-[collapsible=icon]:!p-1.5 group-data-[collapsible=icon]:hover:[&>svg]:text-foreground" - onBlur={() => { - setToRename(false); - if (name !== subGroup.name) - onRename({ - id: subGroup.id, - name, - }); - }}> - <SectionIcon className="group-data-[collapsible=icon]:size-5 text-foreground/50 group-data-[state=open]/collapsible:text-foreground" /> - <Input - className="h-fit w-full py-1 px-1.5" - autoFocus - value={name} - onChange={e => setName(e.target.value)} - onKeyDown={e => { - if (e.key === 'Enter') { - setToRename(false); - if (name !== subGroup.name) - onRename({ - id: subGroup.id, - name, - }); - } - }} - /> - </SidebarMenuButton> - ) : ( - <Context - actions={[ - { - children: ( - <span className="flex flex-row justify-between items-center w-full gap-x-3.5"> - New Post - <MdNoteAdd className="size-5" /> - </span> - ), - - onClick: () => setNewPost(''), - }, - { - children: ( - <span className="flex flex-row justify-between items-center w-full gap-x-3.5"> - Rename - <MdOutlineDriveFileRenameOutline className="size-5" /> - </span> - ), - onClick: () => { - setName(subGroup.name); - setToRename(true); - }, - }, - { - children: ( - <span className="flex flex-row justify-between items-center w-full gap-x-3.5"> - Delete - <MdDeleteSweep className="size-5" /> - </span> - ), - className: 'text-red-400', - onClick: () => onDelete(subGroup.id), - }, - ]}> - <CollapsibleTrigger className="mx-auto" asChild> - <SidebarMenuButton - tooltip={name} - className="group-data-[collapsible=icon]:!p-1.5 group-data-[collapsible=icon]:hover:[&>svg]:text-foreground"> - <SectionIcon className="group-data-[collapsible=icon]:size-5 text-foreground/50 group-data-[state=open]/collapsible:text-foreground" /> - <span>{name}</span> - <ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" /> - </SidebarMenuButton> - </CollapsibleTrigger> - </Context> - )} - <CollapsibleContent> - <SidebarMenuSub onBlur={() => setNewPost(null)}> - {typeof newPost === 'string' && ( - <EditableItem - toRename - onRename={rename => { - if ( - !rename || - !rename.trim().length || - rename === name - ) - return setNewPost(null); - onCreateNewPost(rename); - setNewPost(null); - }} - /> - )} - {children} - </SidebarMenuSub> - </CollapsibleContent> - </SidebarMenuItem> - </Collapsible> - </SidebarMenu> - ); -} - -interface Actions { - onCreateNewPost: (name: string) => void; - onRename: ({ id, name }: { id: string; name: string }) => void; - onDelete: (subGroupId: string) => void; -} - -interface Props extends Actions { - children?: ReactNode | ReactNode[]; - id: string; - name: string; - defaultOpen?: boolean; -} diff --git a/components/imageLoader.tsx b/components/imageLoader.tsx deleted file mode 100644 index cc31b63..0000000 --- a/components/imageLoader.tsx +++ /dev/null @@ -1,99 +0,0 @@ -'use client'; -import { useRef, useState } from 'react'; -import Image from 'next/image'; - -import { Input } from '@/ui/input'; -import { Button } from '@/ui/button'; - -const DEMO_PLACEHOLDER = - 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720"><rect width="100%" height="100%" fill="%23111827"/><text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" fill="%23ffffff" font-family="Arial" font-size="28">Demo Image</text></svg>'; - -const resolveImageSrc = (value?: string) => { - if (!value) return DEMO_PLACEHOLDER; - if (value.startsWith('http://') || value.startsWith('https://') || value.startsWith('/') || value.startsWith('data:')) return value; - - return DEMO_PLACEHOLDER; -}; - -export const ImageLoader = ({ - image: cdlImage, - onFileChange, - ...props -}: React.ComponentProps<'div'> & { - image: { display_name: string; public_id: string }; - onFileChange: (file: File) => void; -}) => { - const fileInput = useRef<HTMLInputElement>(null); - - const [image, setImage] = useState<{ display_name: string; public_id: string } | null>(cdlImage); - const [preview, setPreview] = useState<string | null>(null); // Preview state - - const handleFileChange = () => { - const file = fileInput.current?.files?.[0]; - - if (!file) return; - // Validate the selected file format - const validFormats = ['image/webp', 'image/png', 'image/jpeg', 'image/jpg']; - if (!validFormats.includes(file.type)) { - alert('Unsupported file format. Use WebP, PNG, JPEG, or JPG.'); - setPreview(null); - - return; - } - - // Create a temporary preview - setPreview(URL.createObjectURL(file)); - onFileChange(file); - setImage(null); - }; - - return ( - <div {...props}> - <Input - type="file" - name="file" - value={''} - ref={fileInput} - onChange={handleFileChange} - accept="image/webp, image/png, image/jpeg, image/jpg" - className="hidden" - /> - <div className="relative flex justify-center items-center aspect-video size-full rounded-lg overflow-hidden"> - {!!preview ? - <> - <img - src={preview} - alt="Preview" - className="absolute !size-full object-cover object-center rounded-lg inset-auto" - /> - <Button - className="z-10 absolute top-0 right-0 rounded-tl-none rounded-br-none rounded-bl-lg shadow-[0_0_10px_2px_#00000024] bg-foreground/35 backdrop-blur-lg p-1 aspect-square" - onClick={() => setPreview(null)}> - X - </Button> - </> - : image?.public_id ? - <> - <Image - className="object-cover rounded-lg" - fill - unoptimized - src={resolveImageSrc(image.public_id)} - alt={image.display_name} - /> - <Button - className="z-10 absolute top-0 right-0 rounded-tl-none rounded-br-none rounded-bl-lg shadow-[0_0_7px_3px_#00000024] bg-foreground/35 backdrop-blur-lg p-1 aspect-square" - onClick={() => setImage(null)}> - X - </Button> - </> - : <Button - className="size-full border-foreground/25 border-dashed border-2 bg-transparent hover:bg-transparent text-foreground" - onClick={() => fileInput.current?.click()}> - Upload cover image - </Button> - } - </div> - </div> - ); -}; diff --git a/components/loader.tsx b/components/loader.tsx deleted file mode 100644 index 91ba706..0000000 --- a/components/loader.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import { ThreeDot, type ThreeDotProps } from 'react-loading-indicators'; -export default (props: ThreeDotProps) => <ThreeDot color="#32cd32" text="" textColor="" {...props} />; diff --git a/components/members-page.tsx b/components/members-page.tsx deleted file mode 100644 index 94862d0..0000000 --- a/components/members-page.tsx +++ /dev/null @@ -1,313 +0,0 @@ -'use client'; - -import { Avatar, AvatarFallback, AvatarImage } from '@/ui/avatar'; -import { Button } from '@/ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/ui/dropdown-menu'; -import { Input } from '@/ui/input'; -import { Popover, PopoverContent, PopoverTrigger } from '@/ui/popover'; -import { Copy, RefreshCw, Search, ChevronDown, Plus, MoreVertical } from 'lucide-react'; -import { Check } from 'lucide-react'; -import Image from 'next/image'; -import Link from 'next/link'; -import { useState } from 'react'; -import { AddMemberDialog } from '@/components/add-member-dialog'; -import { ShareableCard } from '@/components/shareable-card'; -import { CopyPopup } from '@/components/copy-popup'; - -interface Member { - id: string; - name: string; - email: string; - role: 'Admin' | 'Cooperator' | 'Supervisor' | string; - avatar: string; - invitationPending?: boolean; - secretToken: string; -} - -const roleDescriptions = { - Admin: 'Full access to all resources and settings', - Cooperator: 'Can collaborate on projects and access most resources', - Supervisor: 'Can oversee projects and team members, with limited admin capabilities', -}; - -export default function MembersPage({ members }: { members: Member[] }) { - const [showAddMember, setShowAddMember] = useState(false); - const [memberList, setMemberList] = useState(members); - const [showCopyPopup, setShowCopyPopup] = useState(false); - const [customRoles, setCustomRoles] = useState<string[]>([]); - - const resetToken = (memberId: string) => { - setMemberList( - memberList.map(member => - member.id === memberId - ? { - ...member, - secretToken: Math.random() - .toString(36) - .substring(2, 15), - } - : member - ) - ); - }; - - const copyToken = (token: string) => { - navigator.clipboard.writeText(token); - setShowCopyPopup(true); - setTimeout(() => setShowCopyPopup(false), 2000); - }; - - const addCustomRole = () => { - const newRole = prompt('Enter the name of the new role:'); - if (newRole && !customRoles.includes(newRole)) { - setCustomRoles([...customRoles, newRole]); - } - }; - - const changeRole = (memberId: string, newRole: string) => { - setMemberList( - memberList.map(member => - member.id === memberId ? { ...member, role: newRole } : member - ) - ); - }; - - return ( - <div className="grow flex flex-col size-full"> - <div className="mb-6"> - <h1 className="text-xl font-semibold mb-1">Users</h1> - <p className="text-sm text-gray-500">Manage members access</p> - </div> - - <div className="flex flex-row items-center justify-between "> - <div className="flex flex-col lg:flex-row lg:items-center space-y-2 lg:space-y-0 lg:space-x-2"> - <p className="text-sm text-gray-600">13 members</p> - </div> - <Button className="w-fit" onClick={() => setShowAddMember(true)}> - Add member - </Button> - </div> - - <div className="space-y-4"> - {memberList.map(member => ( - <div - key={member.id} - className="flex flex-row items-center justify-between bg-white shadow rounded-lg p-[--p] gap-[--p] w-full"> - <div className="grid grid-cols-5 gap-[--p] max-w-96 w-full"> - <div className="flex flex-row items-center space-x-[--p] col-span-3 size-full"> - <Avatar> - <AvatarImage - src={member.avatar} - /> - <AvatarFallback> - {member.name.charAt( - 0 - )} - </AvatarFallback> - </Avatar> - <div className="flex flex-col "> - <p className="font-medium line-clamp-1"> - {member.name} - </p> - <p className="text-sm text-gray-500 line-clamp-1"> - {member.email} - </p> - </div> - </div> - - <div className="flex flex-col items-start justify-between col-span-2 size-full"> - <span className="text-sm text-gray-500 line-clamp-1"> - Secret token - </span> - <span className="text-sm font-medium line-clamp-1"> - {member.secretToken} - </span> - </div> - </div> - - {member.invitationPending ? ( - <div className="flex items-center justify-between"> - <span className="text-sm text-gray-500"> - Invitation pending - </span> - <Button - variant="secondary" - size="sm"> - Resend invitation - </Button> - </div> - ) : ( - <> - <DropdownMenu> - <DropdownMenuTrigger className="lg:hidden"> - <Button - variant="ghost" - size="icon"> - <MoreVertical className="h-4 w-4" /> - </Button> - </DropdownMenuTrigger> - <DropdownMenuContent align="end"> - <DropdownMenuItem - onClick={() => - copyToken( - member.secretToken - ) - }> - Copy token - </DropdownMenuItem> - <DropdownMenuItem - onClick={() => - resetToken( - member.id - ) - }> - Reset token - </DropdownMenuItem> - <DropdownMenuItem> - Remove - member - </DropdownMenuItem> - </DropdownMenuContent> - </DropdownMenu> - <div className="flex flex-row items-center gap-[--p] max-lg:hidden"> - <Button - variant="outline" - size="sm" - onClick={() => - resetToken( - member.id - ) - }> - <RefreshCw className="h-4 w-4 mr-1" /> - Reset - </Button> - <Button - variant="outline" - size="sm" - onClick={() => - copyToken( - member.secretToken - ) - }> - <Copy className="h-4 w-4 mr-1" /> - Copy - </Button> - - <ShareableCard - member={member} - /> - <Popover> - <PopoverTrigger - asChild> - <Button - variant="outline" - className="w-[130px] justify-between"> - { - member.role - } - <ChevronDown className="h-4 w-4 opacity-50" /> - </Button> - </PopoverTrigger> - <PopoverContent - className="w-[300px] p-0" - align="end"> - <div className="p-4 space-y-2"> - {Object.entries( - roleDescriptions - ).map( - ([ - role, - description, - ]) => ( - <div - key={ - role - } - className="flex items-center justify-between hover:bg-gray-100 p-2 rounded cursor-pointer" - onClick={() => - changeRole( - member.id, - role - ) - }> - <div> - <p className="font-medium"> - { - role - } - </p> - <p className="text-sm text-gray-500"> - { - description - } - </p> - </div> - {member.role === - role && ( - <Check className="h-4 w-4 text-green-500" /> - )} - </div> - ) - )} - {customRoles.map( - role => ( - <div - key={ - role - } - className="flex items-center justify-between hover:bg-gray-100 p-2 rounded cursor-pointer" - onClick={() => - changeRole( - member.id, - role - ) - }> - <div> - <p className="font-medium"> - { - role - } - </p> - <p className="text-sm text-gray-500"> - Custom - role - </p> - </div> - {member.role === - role && ( - <Check className="h-4 w-4 text-green-500" /> - )} - </div> - ) - )} - <Button - variant="outline" - className="w-full mt-2" - onClick={ - addCustomRole - }> - <Plus className="h-4 w-4 mr-2" /> - Add - custom - role - </Button> - </div> - </PopoverContent> - </Popover> - </div> - </> - )} - </div> - ))} - </div> - <AddMemberDialog open={showAddMember} onOpenChange={setShowAddMember} /> - - <CopyPopup isVisible={showCopyPopup} /> - </div> - ); -} diff --git a/components/nav-main.tsx b/components/nav-main.tsx deleted file mode 100644 index 51414b5..0000000 --- a/components/nav-main.tsx +++ /dev/null @@ -1,370 +0,0 @@ -'use client'; - -import { useCallback, useState } from 'react'; - -import { ChevronRight, type LucideIcon } from 'lucide-react'; -import { SectionIcon } from '@radix-ui/react-icons'; -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/ui/collapsible'; -import { - ContextMenu, - ContextMenuContent, - ContextMenuItem, - ContextMenuTrigger, -} from '@/ui/context-menu'; -import { - SidebarGroup, - SidebarGroupLabel, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - SidebarMenuSub, - SidebarMenuSubButton, - SidebarMenuSubItem, -} from '@/ui/sidebar'; - -import Link from 'next/link'; -import { Input } from './ui/input'; -import { CgSortAz } from 'react-icons/cg'; -import { Plus } from 'lucide-react'; -import type { PropsWithChildren } from 'react'; -import { Separator } from './ui/separator'; -import { cn } from '@/utils/shadcn'; - -import type { Group } from '@/_server/@dashboard/( journals )'; - -const Context = ({ - actions, - children, -}: PropsWithChildren & { actions: React.ComponentPropsWithoutRef<typeof ContextMenuItem>[] }) => { - return ( - <ContextMenu> - <ContextMenuTrigger>{children}</ContextMenuTrigger> - <ContextMenuContent className='w-full p-0 [&>[role="menuitem"]]:px-3'> - <ContextMenuItem {...actions[0]} /> - {actions.slice(1).map(({ className, ...props }, i) => ( - <> - <Separator key={'separator--' + i} /> - <ContextMenuItem - key={i} - className={cn( - 'text-sidebar-foreground/50 hover:bg-accent hover:text-accent-foreground', - className - )} - {...props} - /> - </> - ))} - </ContextMenuContent> - </ContextMenu> - ); -}; - -interface Actions { - group: { - onCreate: (name: string) => void; - onRename: ({ _id, name }: { _id: string; name: string }) => void; - onDelete: (groupId: string) => void; - }; - subGroup: { - onCreate: (groupId: string, subGroupName: string) => void; - onRename: ({ _id, name }: { _id: string; name: string }) => void; - onDelete: (subGroupId: string) => void; - }; -} - -interface Props extends Actions { - groups: Group[]; - section: string; -} - -export default function NavMain({ section, groups: initGroups, ...actions }: Props) { - const [groups, setGroups] = useState<Group[]>(initGroups); - const [addGroup, setAddGroup] = useState<string | null>(null); - - return ( - <> - <div className="!p-[calc(var(--p)/2)]"> - <div className="flex flex-row items-center justify-between w-full opacity-70 px-2"> - <button> - <CgSortAz className="size-5 -ml-1" /> - </button> - - <button - onClick={() => { - console.log('addGroup', addGroup); - setAddGroup(''); - }}> - <Plus className="size-4" /> - </button> - </div> - </div> - {typeof addGroup === 'string' && ( - <Edit.group - addGroup={addGroup} - setAddGroup={setAddGroup} - {...actions} - /> - )} - {groups.map(group => ( - <Group - key={group._id} - {...group} - {...actions} - onDeleteGroup={_id => { - actions.group.onDelete(_id); - setGroups(prev => prev.filter(g => g._id !== _id)); - }} - onRenameGroup={({ _id, name }) => { - actions.group.onRename({ _id, name }); - setGroups(prev => { - return prev.map(g => { - if (g._id === _id) { - return { - ...g, - name, - }; - } - return g; - }); - }); - }} - /> - ))} - </> - ); -} - -const Edit = { - group: ({ addGroup, setAddGroup, onCreateGroup, setGroups }) => { - return ( - <div className="w-full p-[calc(var(--p)/2)]"> - <Input - className="w-full h-8 !border-none !border-0 !outline-none !ring-0 !ring-transparent placeholder:text-xs" - placeholder="Group name" - autoFocus - value={addGroup} - onChange={e => setAddGroup(e.target.value)} - onBlur={e => setAddGroup(null)} - onKeyDown={e => { - if (e.key === 'Enter') { - onCreateGroup(addGroup); - setGroups(prev => [ - { - id: addGroup, - name: addGroup, - sub_groups: [], - }, - ...prev, - ]); - setAddGroup(null); - } - }} - /> - </div> - ); - }, - subGroup: ({ addSubGroup, setAddSubGroup, onCreateSubGroup }) => ( - <SidebarMenuItem className="w-full"> - <SidebarMenuButton onBlur={() => setAddSubGroup(null)}> - <SectionIcon className="size-4 h-full text-zinc-500 group-data-[state=open]/collapsible:text-white" /> - <Input - className="w-full !px-0 !border-none !outline-none !ring-0 !bg-transparent rounded-lg placeholder:text-xs" - placeholder="Subgroup name" - autoFocus - value={addSubGroup?.subGroup} - onChange={e => - setAddSubGroup({ - ...addSubGroup, - subGroup: e.target.value, - }) - } - onBlur={() => setAddSubGroup(null)} - onKeyDown={e => { - if (e.key === 'Enter') { - const { group, subGroup } = addSubGroup; - onCreateSubGroup(group._id, subGroup); - } - }} - /> - </SidebarMenuButton> - </SidebarMenuItem> - ), -}; - -const Group = ({ - _id, - name, - section, - sub_groups, - onDeleteGroup, - onRenameGroup, - onCreateSubGroup, -}: Group & Actions) => { - const [rename, setRename] = useState<string | null>(null); - const [subGroups, setSubGroups] = useState(sub_groups); - const [addGroup, setAddGroup] = useState<string | null>(null); - const [addSubGroup, setAddSubGroup] = useState<{ group: Group; subGroup: string } | null>( - null - ); - return ( - <SidebarGroup className="!p-[calc(var(--p)/2)]"> - <Context - actions={[ - { children: 'Rename', onClick: () => setRename(name) }, - { - children: 'Delete', - className: 'text-red-400', - onClick: () => onDeleteGroup(_id), - }, - ]}> - <SidebarGroupLabel className="flex flex-row items-center justify-between w-full capitalize"> - {typeof rename === 'string' ? ( - <Input - autoFocus - value={rename} - onChange={e => setRename(e.target.value)} - onBlur={() => setRename(null)} - onKeyDown={e => { - if (e.key === 'Enter') { - onRenameGroup({ - _id, - name: rename, - }); - setRename(null); - } - }} - /> - ) : ( - <> - {name} - <div className="flex flex-row items-center justify-between w-fit gap-2 opacity-70"> - <button> - <CgSortAz className="size-4" /> - </button> - - <button - onClick={() => - setAddSubGroup({ - group: { - _id, - name, - }, - subGroup: '', - }) - }> - <Plus className="size-4" /> - </button> - </div> - </> - )} - </SidebarGroupLabel> - </Context> - <SidebarMenu className=""> - {addSubGroup && ( - <SidebarMenuItem className="w-full"> - <SidebarMenuButton - onBlur={() => setAddSubGroup(null)}> - <SectionIcon className="size-4 h-full text-zinc-500 group-data-[state=open]/collapsible:text-white" /> - <Input - className="w-full !px-0 !border-none !outline-none !ring-0 !bg-transparent rounded-lg placeholder:text-xs" - placeholder="Subgroup name" - autoFocus - value={addSubGroup?.subGroup} - onChange={e => - setAddSubGroup({ - ...addSubGroup, - subGroup: e.target - .value, - }) - } - onBlur={() => setAddSubGroup(null)} - onKeyDown={e => { - if (e.key === 'Enter') { - const { - group, - subGroup, - } = addSubGroup; - onCreateSubGroup( - group._id, - subGroup - ); - } - }} - /> - </SidebarMenuButton> - </SidebarMenuItem> - )} - {subGroups && - subGroups.map(sub_group => ( - <SubGroup key={_id} {...sub_group} /> - ))} - </SidebarMenu> - </SidebarGroup> - ); -}; - -const Item = ({ _id, name, url, icon: Icon }: Group['sub_groups'][number]['items'][number]) => { - return ( - <SidebarMenuSubItem key={subItem.title}> - <SidebarMenuSubButton asChild> - <Link - href={`/${section}/${subItem._id}`} - className="!line-clamp-1 leading-7"> - {subItem.title} - </Link> - </SidebarMenuSubButton> - </SidebarMenuSubItem> - ); -}; - -const SubGroup = ({ _id, name, items }: Group['sub_groups'][number] & Actions['']) => { - const [rename, setRename] = useState<string | null>(null); - - return ( - <Context actions={[{ children: 'Rename', onClick: () => setRename(name) }]}> - <Collapsible key={_id} asChild className="group/collapsible"> - <SidebarMenuItem className="w-full"> - {typeof rename === 'string' ? ( - <SidebarMenuButton - tooltip={name} - className="group-data-[collapsible=icon]:!p-1.5 group-data-[collapsible=icon]:hover:[&>svg]:text-foreground" - onBlur={() => setRename(null)}> - <SectionIcon className="group-data-[collapsible=icon]:size-5 text-zinc-500 group-data-[state=open]/collapsible:text-white" /> - <Input - className="!px-0 !border-none !outline-none !ring-0 !bg-transparent placeholder:text-xs !cursor-pointer" - autoFocus - value={name} - onChange={e => - setRename(e.target.value) - } - onBlur={() => setRename(null)} - /> - <ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" /> - </SidebarMenuButton> - ) : ( - <CollapsibleTrigger className="mx-auto" asChild> - <SidebarMenuButton - tooltip={name} - className="group-data-[collapsible=icon]:!p-1.5 group-data-[collapsible=icon]:hover:[&>svg]:text-foreground"> - <SectionIcon className="group-data-[collapsible=icon]:size-5 text-zinc-500 group-data-[state=open]/collapsible:text-white" /> - <span>{name}</span> - <ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" /> - </SidebarMenuButton> - </CollapsibleTrigger> - )} - - <CollapsibleContent> - <SidebarMenuSub> - {items?.map(subItem => ( - <Item - key={subItem._id} - {...subItem} - /> - ))} - </SidebarMenuSub> - </CollapsibleContent> - </SidebarMenuItem> - </Collapsible> - </Context> - ); -}; diff --git a/components/nav-projects.tsx b/components/nav-projects.tsx deleted file mode 100644 index 662777f..0000000 --- a/components/nav-projects.tsx +++ /dev/null @@ -1,83 +0,0 @@ -"use client"; - -import { Folder, MoreHorizontal, Share, Trash2, type LucideIcon } from "lucide-react"; - -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/ui/dropdown-menu"; -import { - SidebarGroup, - SidebarGroupLabel, - SidebarMenu, - SidebarMenuAction, - SidebarMenuButton, - SidebarMenuItem, - useSidebar, -} from "@/ui/sidebar"; - -export function NavProjects({ - projects, -}: { - projects: { - name: string; - url: string; - icon: LucideIcon; - }[]; -}) { - const { isMobile } = useSidebar(); - - return ( - <SidebarGroup className="group-data-[collapsible=icon]:hidden"> - <SidebarGroupLabel>Projects</SidebarGroupLabel> - <SidebarMenu> - {projects.map((item) => ( - <SidebarMenuItem key={item.name}> - <SidebarMenuButton asChild> - <a href={item.url}> - <item.icon /> - <span>{item.name}</span> - </a> - </SidebarMenuButton> - <DropdownMenu> - <DropdownMenuTrigger asChild> - <SidebarMenuAction showOnHover> - <MoreHorizontal /> - <span className="sr-only">More</span> - </SidebarMenuAction> - </DropdownMenuTrigger> - <DropdownMenuContent - className="w-48" - side={isMobile ? "bottom" : "right"} - align={isMobile ? "end" : "start"} - > - <DropdownMenuItem> - <Folder className="text-muted-foreground" /> - <span>View Project</span> - </DropdownMenuItem> - <DropdownMenuItem> - <Share className="text-muted-foreground" /> - <span>Share Project</span> - </DropdownMenuItem> - <DropdownMenuSeparator /> - <DropdownMenuItem> - <Trash2 className="text-muted-foreground" /> - <span>Delete Project</span> - </DropdownMenuItem> - </DropdownMenuContent> - </DropdownMenu> - </SidebarMenuItem> - ))} - <SidebarMenuItem> - <SidebarMenuButton> - <MoreHorizontal /> - <span>More</span> - </SidebarMenuButton> - </SidebarMenuItem> - </SidebarMenu> - </SidebarGroup> - ); -} diff --git a/components/nav-secondary.tsx b/components/nav-secondary.tsx deleted file mode 100644 index 2e5fff2..0000000 --- a/components/nav-secondary.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import * as React from "react"; -import { type LucideIcon } from "lucide-react"; - -import { SidebarGroup, SidebarGroupContent, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "@/ui/sidebar"; - -export function NavSecondary({ - items, - ...props -}: { - items: { - title: string; - url: string; - icon: LucideIcon; - }[]; -} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) { - return ( - <SidebarGroup {...props}> - <SidebarGroupContent> - <SidebarMenu> - {items.map((item) => ( - <SidebarMenuItem key={item.title}> - <SidebarMenuButton asChild size="sm"> - <a href={item.url}> - <item.icon /> - <span>{item.title}</span> - </a> - </SidebarMenuButton> - </SidebarMenuItem> - ))} - </SidebarMenu> - </SidebarGroupContent> - </SidebarGroup> - ); -} diff --git a/components/nav-user.tsx b/components/nav-user.tsx deleted file mode 100644 index a90564e..0000000 --- a/components/nav-user.tsx +++ /dev/null @@ -1,100 +0,0 @@ -"use client"; - -import { BadgeCheck, Bell, ChevronsUpDown, CreditCard, LogOut, Sparkles } from "lucide-react"; - -import { Avatar, AvatarFallback, AvatarImage } from "@/ui/avatar"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/ui/dropdown-menu"; -import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from "@/ui/sidebar"; - -export function NavUser({ - user, -}: { - user: { - name: string; - email: string; - avatar: string; - }; -}) { - const { isMobile } = useSidebar(); - - return ( - <SidebarMenu> - <SidebarMenuItem> - <DropdownMenu> - <DropdownMenuTrigger asChild> - <SidebarMenuButton - size="lg" - className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground" - > - <Avatar className="h-8 w-8 rounded-lg"> - <AvatarImage src={user.avatar} alt={user.name} /> - <AvatarFallback className="rounded-lg">CN</AvatarFallback> - </Avatar> - <div className="grid flex-1 text-left text-sm leading-tight"> - <span className="truncate font-semibold">{user.name}</span> - <span className="truncate text-xs">{user.email}</span> - </div> - <ChevronsUpDown className="ml-auto size-4" /> - </SidebarMenuButton> - </DropdownMenuTrigger> - <DropdownMenuContent - className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg" - side={isMobile ? "bottom" : "right"} - align="end" - sideOffset={4} - > - <DropdownMenuLabel className="p-0 font-normal"> - <div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm"> - <Avatar className="h-8 w-8 rounded-lg"> - <AvatarImage src={user.avatar} alt={user.name} /> - <AvatarFallback className="rounded-lg">CN</AvatarFallback> - </Avatar> - <div className="grid flex-1 text-left text-sm leading-tight"> - <span className="truncate font-semibold"> - {user.name} - </span> - <span className="truncate text-xs">{user.email}</span> - </div> - </div> - </DropdownMenuLabel> - <DropdownMenuSeparator /> - <DropdownMenuGroup> - <DropdownMenuItem> - <Sparkles /> - Upgrade to Pro - </DropdownMenuItem> - </DropdownMenuGroup> - <DropdownMenuSeparator /> - <DropdownMenuGroup> - <DropdownMenuItem> - <BadgeCheck /> - Account - </DropdownMenuItem> - <DropdownMenuItem> - <CreditCard /> - Billing - </DropdownMenuItem> - <DropdownMenuItem> - <Bell /> - Notifications - </DropdownMenuItem> - </DropdownMenuGroup> - <DropdownMenuSeparator /> - <DropdownMenuItem> - <LogOut /> - Log out - </DropdownMenuItem> - </DropdownMenuContent> - </DropdownMenu> - </SidebarMenuItem> - </SidebarMenu> - ); -} diff --git a/components/shareable-card.tsx b/components/shareable-card.tsx deleted file mode 100644 index 1ee87e1..0000000 --- a/components/shareable-card.tsx +++ /dev/null @@ -1,89 +0,0 @@ -'use client'; - -import { Button } from '@/ui/button'; -import { Share2 } from 'lucide-react'; -import Image from 'next/image'; -import { useRef, useState } from 'react'; -import html2canvas from 'html2canvas'; - -interface ShareableCardProps { - member: { - name: string; - role: string; - avatar: string; - }; -} - -export function ShareableCard({ member }: ShareableCardProps) { - const [makeCard, setMakeCard] = useState(false); - const cardRef = useRef<HTMLDivElement>(null); - - const generateCard = async () => { - if (!cardRef.current) return; - - setMakeCard(true); - await new Promise(resolve => setTimeout(resolve, 1000)); - const canvas = await html2canvas(cardRef.current); - const dataUrl = canvas.toDataURL('image/webp'); - const link = document.createElement('a'); - link.href = dataUrl; - link.download = `${member.name.replace(' ', '_')}_card.webp`; - link.click(); - - setMakeCard(false); - }; - - return ( - <> - <Button variant="outline" size="sm" onClick={generateCard}> - <Share2 className="h-4 w-4 mr-1" /> - Share Card - </Button> - <div - className="z-[9999] fixed inset-0 flex justify-center items-center m-auto bg-black/35 backdrop-blur-sm w-screen h-screen h-svh" - style={{ - zIndex: 9999, - display: makeCard ? 'flex' : 'none', - }}> - <div - ref={cardRef} - className="bg-white p-6 rounded-lg shadow-lg w-64"> - <div className="flex items-center space-x-4 mb-4"> - <Image - src={member.avatar || '/placeholder.svg'} - alt={member.name} - width={64} - height={64} - className="rounded-full" - /> - <div> - <h2 className="text-xl font-bold"> - {member.name} - </h2> - <p className="text-gray-600"> - {member.role} - </p> - </div> - </div> - <div className="text-center mb-4"> - <p className="text-sm text-gray-500"> - Proud member of - </p> - <div className="flex justify-center items-center space-x-2 mt-2"> - <Image - src="https://hebbkx1anhila5yf.public.blob.vercel-storage.com/original-7fbbd2e655385d29c6c9f1902c10b284-YJOa6ZmbohzjVXSW5coU1whjjxURks.webp" - alt="Acme Logo" - width={24} - height={24} - className="rounded" - /> - <span className="text-lg font-semibold"> - Acme - </span> - </div> - </div> - </div> - </div> - </> - ); -} diff --git a/components/theme-switcher.tsx b/components/theme-switcher.tsx deleted file mode 100644 index ed2d440..0000000 --- a/components/theme-switcher.tsx +++ /dev/null @@ -1,23 +0,0 @@ -'use client'; -import { useState } from 'react'; -import switchTheme from '@/utils/switch-theme'; -import { Switch } from '@/ui/switch'; -import { BsMoonStars } from 'react-icons/bs'; -import { LucideSunMoon } from 'lucide-react'; -import { useTheme } from '@/hooks/use-theme'; - -export default function ThemeSwitcher() { - const [theme, setTheme] = useTheme(); - const [checked, setChecked] = useState(theme === 'dark'); - return ( - <Switch - className="w-10 h-5 !py-0.5 !px-px" - checked={checked} - onCheckedChange={c => { - setChecked(c); - setTheme(switchTheme(true)); - }} - icon={checked ? <BsMoonStars size={8} /> : <LucideSunMoon size={8} />} - /> - ); -} diff --git a/components/typewriter.tsx b/components/typewriter.tsx deleted file mode 100644 index 6e8b6cc..0000000 --- a/components/typewriter.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { useTypewriter } from '@/hooks/use-typewriter'; - -const Typewriter = ({ text, delay, speed, ...props }: { text: string; delay?: number; speed?: number } & any) => { - const displayText = useTypewriter(text, delay, speed); - - return <p {...props}>{displayText}</p>; -}; - -export default Typewriter; diff --git a/components/webapp/_export.tsx b/components/webapp/_export.tsx deleted file mode 100644 index 32d9bac..0000000 --- a/components/webapp/_export.tsx +++ /dev/null @@ -1,24 +0,0 @@ -'use client'; - -import Status_bar from './status_bar'; -import Header from './header'; -import Inner from './inner'; -import Preview from './preview'; -import type { Post } from '@/types/post'; - -export default ({ - page, - className, - ...props -}: { - page: 'preview' | 'inner'; - className?: string; -} & Post) => { - return ( - <div className="relative size-full flex flex-col justify-start items-center [&>*]:px-[--p] z-50 overflow-scroll !whitespace-pre-wrap"> - <Status_bar /> - <Header /> - {page === 'preview' ? <Preview {...props} /> : <Inner {...props} />} - </div> - ); -}; diff --git a/components/webapp/header.tsx b/components/webapp/header.tsx deleted file mode 100644 index cc7e5e3..0000000 --- a/components/webapp/header.tsx +++ /dev/null @@ -1,53 +0,0 @@ -"use client"; -import Image from "next/image"; -import { useEffect, useState } from "react"; -import { motion } from "framer-motion"; - -import Link from "next/link"; -import { BiStats } from "react-icons/bi"; -import { CgGym } from "react-icons/cg"; -import { IoPersonSharp } from "react-icons/io5"; -import { LuSunMoon } from "react-icons/lu"; -import { BsMoonStars } from "react-icons/bs"; - -import { usePathname } from "next/navigation"; -import { Switch } from "@/ui/switch"; -import switchTheme from "@/utils/switch-theme"; -import { TbMenuDeep } from "react-icons/tb"; - -export default () => { - const tabs = [ - { href: "/stats", label: "Stats", icon: { Element: BiStats, color: "#F34284" } }, - { href: "/workout", label: "Workout", icon: { Element: CgGym, color: "#84f342" } }, - { href: "/profile", label: "Profile", icon: { Element: IoPersonSharp, color: "#4a7ef2" } }, - ]; - - const pathname = usePathname(); - const [active, setActive] = useState(1); - - const [theme, setTheme] = useState(); - const [checked, setChecked] = useState(theme === "dark"); - - useEffect(() => { - const resolvedTheme = switchTheme(false); - setTheme(resolvedTheme); - setChecked(resolvedTheme === "dark"); - }, []); - - return ( - <> - <div className="flex flex-row items-start justify-between w-full h-14 !pb-2.5 light:bg-zinc-100 shadow-[inset_0_-1px_#ffffff24] light:shadow-[inset_0_-1px_#00000024]"> - <div className="relative h-10 w-fit min-w-16 -ml-1"> - <Image className="!w-auto my-auto" src={"/logo.svg"} alt="logo" fill /> - </div> - <TbMenuDeep className="text-black w-8 h-8 cursor-pointer" /> - </div> - - <div - className={ - "z-50 absolute bottom-4 mx-auto flex flex-row justify-between h-9 w-fit !px-1 !py-0.5 gap-x-1 bg-[#1d1d1f] light:bg-zinc-100 shadow-[0_0_5px_1px_#ffffff36] light:shadow-[0_0_5px_1px_#00000024] rounded-full" - } - ></div> - </> - ); -}; diff --git a/components/webapp/heading.tsx b/components/webapp/heading.tsx deleted file mode 100644 index 9cd67b3..0000000 --- a/components/webapp/heading.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import Typewriter from '@/components/typewriter'; -import { BsCalendar2WeekFill } from 'react-icons/bs'; - -export default () => { - const date = new Date(Date.now()).toDateString().split(' ').slice(0, -1).join(' '); - - return ( - <div className="flex flex-col items-start justify-between w-full gap-y-3 py-8"> - <div className="flex flex-row items-end gap-x-2"> - <Typewriter - text="Workout" - className="font-righteous text-3xl text-zinc-200 light:text-zinc-800 leading-none -mb-px" - /> - </div> - <span className="flex flex-row items-center gap-x-2"> - <BsCalendar2WeekFill className="text-[#84f342] size-4" /> - <Typewriter - delay={550} - text={date} - className="text-base text-zinc-400 light:text-zinc-600 leading-none -mb-px text-end" - /> - </span> - </div> - ); -}; diff --git a/components/webapp/inner.tsx b/components/webapp/inner.tsx deleted file mode 100644 index 4f369c8..0000000 --- a/components/webapp/inner.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { useCallback } from 'react'; - -import Image from 'next/image'; -import { Separator } from '@/ui/separator'; - -import type { Post } from '@/types/post'; - -const DEMO_PLACEHOLDER = - 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720"><rect width="100%" height="100%" fill="%23111827"/><text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" fill="%23ffffff" font-family="Arial" font-size="28">Demo Image</text></svg>'; - -const resolveImageSrc = (value?: string) => { - if (!value) return DEMO_PLACEHOLDER; - if (value.startsWith('http://') || value.startsWith('https://') || value.startsWith('/') || value.startsWith('data:')) return value; - - return DEMO_PLACEHOLDER; -}; - -export default ({ image, title, date, kind, description }: Omit<Post, 'image'> & { image: { public_id?: string; src?: string } }) => { - const RenderImage = useCallback( - ({ public_id, src }: { public_id?: string; src?: string }) => ( - <Image - src={resolveImageSrc(public_id ?? src)} - fill - unoptimized - alt={title} - className=" object-cover object-center rounded-md group-hover:scale-110 transition-all duration-200" - /> - ), - [image], - ); - return ( - <div className="relative flex flex-col w-full gap-[--p] !text-black"> - <div className="flex flex-col gap-1 !text-black animate-[fade-in_0.5s_ease-in-out_forwards] delay-75"> - {kind && <label className="text-[.7rem] font-semibold text-gray-500 tracking-wide uppercase">{kind}</label>} - <h1 className="font-extralight leading-5">{title}</h1> - <p className="text-[.7rem] font-semibold tracking-wide text-black/65"> - {typeof date === 'object' ? - <> - {date && - date.from && - new Date(date.from).toLocaleString(undefined, { - month: 'long', - day: 'numeric', - year: 'numeric', - })} - - {date && - date.to && - ' — ' + - new Date(date.to).toLocaleString(undefined, { - month: 'long', - day: 'numeric', - year: 'numeric', - })} - </> - : date && - new Date(date).toLocaleString(undefined, { - month: 'long', - day: 'numeric', - year: 'numeric', - }) - } - </p> - </div> - <div className="relative flex justify-center items-center w-full h-fit !aspect-video rounded-md shadow-[0_0_10px_1px_#00000035] overflow-hidden"> - <RenderImage {...image} /> - </div> - <div className="flex flex-col gap-[--p] mt-[--p]"> - <span className="leading-none"> - Description - <Separator className="mt-[calc(var(--p)/2)]" /> - </span> - <div className="flex flex-col gap-y-[calc(var(--p)/2)] tracking-tight leading-5 -mt-0.5 text-sm font-light"> - <p>{description}</p> - </div> - </div> - </div> - ); -}; diff --git a/components/webapp/preview.tsx b/components/webapp/preview.tsx deleted file mode 100644 index 0c9a64e..0000000 --- a/components/webapp/preview.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import Image from 'next/image'; -import { useCallback } from 'react'; - -import type { Post } from '@/types/post'; - -const DEMO_PLACEHOLDER = - 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720"><rect width="100%" height="100%" fill="%23111827"/><text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" fill="%23ffffff" font-family="Arial" font-size="28">Demo Image</text></svg>'; - -const resolveImageSrc = (value?: string) => { - if (!value) return DEMO_PLACEHOLDER; - if (value.startsWith('http://') || value.startsWith('https://') || value.startsWith('/') || value.startsWith('data:')) return value; - - return DEMO_PLACEHOLDER; -}; - -export default ({ image, title, date, kind }: Omit<Post, 'image'> & { image: { public_id?: string; src?: string } }) => { - console.log('date =>', date); - const RenderImage = useCallback( - ({ public_id, src }: { public_id?: string; src?: string }) => ( - <Image - src={resolveImageSrc(public_id ?? src)} - fill - unoptimized - alt={title} - className=" object-cover object-center rounded-md group-hover:scale-110 transition-all duration-200" - /> - ), - [image], - ); - return ( - <div className="relative flex flex-col w-full gap-[--p] "> - <div className="relative flex justify-center items-center w-full h-fit !aspect-video rounded-md shadow-[0_0_10px_1px_#00000035] overflow-hidden"> - {kind && ( - <label className="z-50 absolute top-0 left-0 text-[.7rem] font-light text-white bg-gray-500 px-1.5 py-1 rounded-tl-md rounded-br-md uppercase"> - {kind} - </label> - )} - <RenderImage {...image} /> - </div> - <div className="flex flex-col gap-1 !text-black animate-[fade-in_0.5s_ease-in-out_forwards] delay-75"> - <h1 className="font-extralight leading-5">{title}</h1> - <p className="text-xs font-semibold tracking-wide text-black/65"> - {typeof date === 'object' ? - <> - {date && - date.from && - new Date(date.from).toLocaleString(undefined, { - month: 'long', - day: 'numeric', - year: 'numeric', - })} - - {date && - date.to && - ' — ' + - new Date(date.to).toLocaleString(undefined, { - month: 'long', - day: 'numeric', - year: 'numeric', - })} - </> - : date && - new Date(date).toLocaleString(undefined, { - month: 'long', - day: 'numeric', - year: 'numeric', - }) - } - </p> - </div> - </div> - ); -}; diff --git a/components/webapp/status_bar.tsx b/components/webapp/status_bar.tsx deleted file mode 100644 index a4b3043..0000000 --- a/components/webapp/status_bar.tsx +++ /dev/null @@ -1 +0,0 @@ -export default () => <div className="w-full h-10 !p-0 shrink-0 light:bg-zinc-100" />; diff --git a/config/_export.ts b/config/_export.ts deleted file mode 100644 index 291673c..0000000 --- a/config/_export.ts +++ /dev/null @@ -1,2 +0,0 @@ -import './css/init.css'; -import './css/vars.css'; diff --git a/config/css/init.css b/config/css/init.css deleted file mode 100644 index bf82693..0000000 --- a/config/css/init.css +++ /dev/null @@ -1,133 +0,0 @@ -@tailwind base; - - -@layer base { - * { - @apply border-border; - } - - html { - background-color: transparent !important; - position: relative !important; - max-width: 100vw !important; - min-height: 100vh !important; - min-height: 100dvh !important; - min-height: -webkit-fill-available !important; - display: block; - border: 0; - outline: 0; - margin: 0; - padding: 0; - overflow-x: hidden !important; - overflow-y: scroll !important; - -webkit-overflow-scrolling: touch !important; - -webkit-text-size-adjust: 100%; - line-height: 1.5; - -webkit-text-size-adjust: 100%; - -moz-tab-size: 4; - -o-tab-size: 4; - tab-size: 4; - font-family: system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, - Droid Sans, Helvetica Neue, sans-serif; - font-feature-settings: normal; - font-variation-settings: normal; - } - - body { - --_max-width: var(--max-width, 1680px); - --_padding-n: var(--padding-n, 1.15); - --_padding: var(--padding, calc(1rem * var(--_padding-n))); - --_border: var(--border, #dddae7); - --_shadow-offset: var(--border, 0 0 3px -1px); - --_shadow-color: var(--border, #fff); - - height: 100vh; - height: 100svh; - - width: 100vw; - max-width: 100vw; - /* max-width: var(--_max-width, 1680px); */ - - @apply /* Layout */ - relative block overflow-hidden - /* Alignments */ - justify-center items-center - /* Spacing */ - my-0 mx-auto - /* Themes */ - bg-background text-foreground; - } - - #content { - @apply contents; - - & > div { - @apply relative flex justify-center items-center w-screen h-svh gap-[--external-p] mx-auto; - } - } - - input[type="number"]::-webkit-inner-spin-button, - input[type="number"]::-webkit-outer-spin-button { - -webkit-appearance: none; - margin: 0; - } - - input:-webkit-autofill, - input:-webkit-autofill:hover, - input:-webkit-autofill:focus, - input:-webkit-autofill:active { - -webkit-background-clip: text; - -webkit-text-fill-color: hsl(var(--foreground)) !important; - box-shadow: inset 0 0 20px 20px hsl(var(--background)) !important; - } - - ::-webkit-scrollbar-thumb { - display: none !important; - } - - ::-webkit-scrollbar { - display: none !important; - } - - [style*="overflow-x:scroll"]::-webkit-scrollbar, - .overflow-x-scroll::-webkit-scrollbar { - display: none !important; - } - - ::-webkit-scrollbar:horizontal { - height: 0; - width: 0; - display: none; - -ms-overflow-style: none; - scrollbar-width: none; - } - - ::-webkit-scrollbar { - z-index: 100; - overflow: visible; - width: 0px !important; - margin-left: 0px !important; - } - - ::-webkit-scrollbar-track { - box-shadow: inset 0 0 3px grey; - border-radius: 25px; - } - - ::-webkit-scrollbar-thumb { - box-shadow: inset 0 0 0 1px #0000001f; - border-radius: 25px; - } - - ::-moz-scrollbar { - width: calc(0.5rem + 0.05vw) !important; - } - - ::-webkit-scrollbar-thumb { - background: #454547; - } - - ::-webkit-scrollbar-thumb:active { - background: #000000 !important; - } -} diff --git a/config/css/vars.css b/config/css/vars.css deleted file mode 100644 index 1c27dec..0000000 --- a/config/css/vars.css +++ /dev/null @@ -1,11 +0,0 @@ -@tailwind base; - - -@layer base { - :root { - --sidebar-p: clamp(0.35rem, 0.65vw, 0.6rem); - --external-p: calc(var(--sidebar-p) * 1.1); - --p: calc(var(--sidebar-p) + var(--external-p)); - --header-h: clamp(4rem, 8vw, 8rem); - } -} diff --git a/constants/post-init-template.ts b/constants/post-init-template.ts deleted file mode 100644 index ab05245..0000000 --- a/constants/post-init-template.ts +++ /dev/null @@ -1,37 +0,0 @@ -export default { - featured: { - name: 'New Featured Story', - title: 'Add Title', - description: 'Add description', - date: new Date().toISOString(), - state: 'Draft', - }, - design: { - name: 'New Design Article', - title: 'Add Title', - description: 'Add description', - date: new Date().toISOString(), - state: 'Draft', - }, - culture: { - name: 'New Culture Story', - title: 'Add Title', - description: 'Add description', - date: new Date().toISOString(), - state: 'Draft', - }, - insights: { - name: 'New Insight', - title: 'Add Title', - description: 'Add description', - date: new Date().toISOString(), - state: 'Draft', - }, - resources: { - name: 'New Resource', - title: 'Add Title', - description: 'Add description', - date: new Date().toISOString(), - state: 'Draft', - }, -}; diff --git a/constants/sections.ts b/constants/sections.ts deleted file mode 100644 index a7e28fc..0000000 --- a/constants/sections.ts +++ /dev/null @@ -1,24 +0,0 @@ -export const sections = ['featured', 'design', 'culture', 'insights', 'resources'] as const; - -export const sectionLabels: Record<typeof sections[number], { title: string; description: string }> = { - featured: { - title: 'Featured Stories', - description: 'Curated highlights and trending content', - }, - design: { - title: 'Design & Creative', - description: 'Design trends, visual systems, and creative insights', - }, - culture: { - title: 'Culture & Arts', - description: 'Cultural commentary, exhibitions, and creative discourse', - }, - insights: { - title: 'Insights & Analysis', - description: 'Industry trends, research, and thought leadership', - }, - resources: { - title: 'Resources & Guides', - description: 'Tutorials, guides, and tools for professional growth', - }, -}; diff --git a/docs/accessibility.md b/docs/accessibility.md new file mode 100644 index 0000000..658a4ff --- /dev/null +++ b/docs/accessibility.md @@ -0,0 +1,26 @@ +# Accessibility evidence + +AutoBlog makes targeted, tested accessibility claims; this is not a blanket WCAG certification. + +## Automated and keyboard evidence + +- axe-core serious/critical checks: marketing, sign-in, authenticated workspace and public preview. +- Keyboard E2E: role activation, draft creation, title/body entry, autosave and submit for review. +- Focus management: draft creation and post selection move focus to the editable title; skip link and + visible `:focus-visible` outline remain available. +- Form/error semantics: every editor, media, schedule and AI field has a programmatic label; blocking + save errors use `role=alert`, progress messages use `role=status`. +- Contrast: axe color-contrast rules execute on all four primary surfaces; neon accent uses dark ink. +- Reduced motion: the browser test emulates `prefers-reduced-motion: reduce` and verifies smooth + scrolling/animation duration are disabled. +- Responsive review: versioned Chromium baselines cover desktop marketing/sign-in/workspace/preview + and 390×844 marketing/workspace surfaces. + +Commands: `bun run test:a11y` and `bun run test:visual`. Visual baselines are reviewed on the pinned +Windows/Chromium CI job to avoid cross-platform font rasterization noise. + +## Manual review scope + +The flagship focus order follows sidebar navigation → editor controls → evidence rail on desktop and +the same DOM order on mobile. No primary command depends only on hover, drag or pointer gestures. +Screen-reader combinations beyond axe semantic inspection remain a known manual-audit limitation. diff --git a/docs/ai-data-handling.md b/docs/ai-data-handling.md new file mode 100644 index 0000000..1def82a --- /dev/null +++ b/docs/ai-data-handling.md @@ -0,0 +1,34 @@ +# AI data handling and governance + +## Contract + +AI is an authenticated suggestion command, not an editorial mutation. Input is capped at 180 title, +320 excerpt, 20,000 content and 500 instruction characters. Output is parsed through the same bounded +schema for mock and Gemini modes. The UI renders a preview and creates a revision only after the user +selects **Apply suggestion**. + +The deterministic mock adapter is labeled `Mock / demo`, makes no network call and reports no token +counts. Configured mode uses `@google/genai`, structured JSON, 2,048 maximum output tokens, low +temperature and an eight-second abort signal. The official SDK notes that client abort does not +guarantee provider cancellation and applicable work may still be charged: +[GenerateContentConfig](https://googleapis.github.io/js-genai/release_docs/interfaces/types.GenerateContentConfig.html). + +## Limits and retained data + +- Five requests per authenticated user/workspace per minute, persisted in the database. +- Monthly workspace character budget configured by `AI_MONTHLY_CHARACTER_QUOTA`. +- The worst-case request/output budget is reserved transactionally before provider invocation and + released on any failure; concurrent calls cannot oversubscribe the workspace. +- Success stores mode, provider, model, latency, input/output character counts and only token counts + returned by the provider. No token count or cost is estimated. +- Prompt, draft body, suggestion body, API key, raw provider response and error details are not stored + in usage or audit records. + +Gemini receives the bounded draft and instruction when and only when `AI_MODE=gemini`; configured mode +therefore requires the operator to accept Google's data-processing terms. Demo/CI remain mock mode. + +## Failure behavior + +Timeout, cancellation, malformed JSON, schema violations and provider errors map to +`PROVIDER_UNAVAILABLE` with a correlation ID. Quota and rate failures occur before adapter access. +Secrets and provider messages are diagnostic-only and never enter public envelopes. diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..f82c5d0 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,47 @@ +# API contract + +Protected application routes use the envelope `{ "data": ... }`. Failures use: + +```json +{ + "error": { + "code": "VERSION_CONFLICT", + "message": "A newer version exists. Reload or compare before saving.", + "requestId": "correlation-id" + } +} +``` + +The `x-request-id` response header mirrors the correlation ID. Stack traces, SQL/provider messages +and credentials are diagnostic-only and are never returned. + +| Endpoint | Method | Permission | Success | +| --- | --- | --- | --- | +| `/api/auth/[...all]` | Better Auth GET/POST | public/session | session contract; stable error code + request ID | +| `/api/workspaces/:workspaceId/posts` | GET | `workspace.read` | post summaries | +| `/api/workspaces/:workspaceId/posts` | POST | `post.create` | revision-backed post, HTTP 201 | +| `/api/workspaces/:workspaceId/posts/:postId` | GET | `workspace.read` | current draft revision | +| `/api/workspaces/:workspaceId/posts/:postId` | PATCH | `post.update` | new immutable revision or HTTP 409 | +| `/api/workspaces/:workspaceId/posts/:postId/revisions` | GET | `workspace.read` | immutable history, newest first | +| `/api/workspaces/:workspaceId/posts/:postId/revisions/restore` | POST | `revision.restore` | restored content as a new revision | +| `/api/workspaces/:workspaceId/posts/:postId/transitions` | POST | action-specific policy | versioned workflow result | +| `/api/workspaces/:workspaceId/media?postId=…` | GET | `workspace.read` | active workspace/post asset | +| `/api/workspaces/:workspaceId/media` | POST multipart | `media.upload` + ownership | decoded and activated asset | +| `/api/workspaces/:workspaceId/media/:assetId` | GET | `workspace.read` | private verified bytes | +| `/api/workspaces/:workspaceId/media/:assetId` | DELETE | `media.delete` | logical deletion + cleanup job | +| `/api/workspaces/:workspaceId/ai/suggest` | POST | `ai.suggest` | labeled, metered suggestion only | +| `/api/workspaces/:workspaceId/demo/reset` | POST | Owner + demo guard | idempotent bounded fixture result | +| `/api/jobs/run` | POST | `jobs.run` or scheduler secret | bounded publication/media execution results | +| `/preview/:workspaceSlug/:postSlug` | GET | public | immutable published revision or 404 | +| `/api/health` | GET | public | readiness without secret details | + +Mutation adapters require a trusted `Origin` in addition to the HTTP-only session cookie. Workspace +and actor identity are always derived from that session; neither is accepted from JSON payloads. +The scheduler adapter instead uses the distinct `x-autoblog-cron-secret`, compared by SHA-256 digest +with constant-time equality; it never accepts workspace/job payloads from the caller. +Application JSON responses are `private, no-store`; this does not alter static marketing or explicit +public-preview revalidation. + +Stable application codes are `UNAUTHENTICATED`, `FORBIDDEN`, `NOT_FOUND`, +`VALIDATION_FAILED`, `VERSION_CONFLICT`, `ILLEGAL_TRANSITION`, `QUOTA_EXCEEDED`, +`RATE_LIMITED`, `PROVIDER_UNAVAILABLE` and `INTERNAL_FAILURE`. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..0c2495d --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,33 @@ +# Architecture + +AutoBlog is a modular monolith. Browser code calls Next.js adapters; adapters establish a +database-backed session and workspace membership before invoking an application service. Domain +policies execute before a repository or provider. React components never import database clients. + +```mermaid +flowchart LR + Browser --> Next[Next.js route / server adapter] + Next --> Session[Better Auth session] + Session --> Membership[Workspace membership + RBAC] + Membership --> Service[Application command/query] + Service --> Domain[Editorial invariants] + Service --> Repository[Repository port] + Repository --> LibSQL[(Drizzle / libSQL)] + Service --> Jobs[(Durable jobs)] + Service --> Media[Media port] + Service --> AI[AI suggestion port] +``` + +## Ownership boundaries + +- `src/modules/identity`: roles, permission matrix and workspace context. +- `src/modules/editorial`: canonical post inputs, service and repository contract. +- `src/modules/media`: verified asset storage and replacement policy. +- `src/modules/ai`: bounded suggestion adapters and usage accounting. +- `src/platform/auth`: Better Auth and origin/session adapters. +- `src/platform/db`: schema, libSQL connection, versioned migration and seed infrastructure. +- `src/platform/observability`: stable application errors and structured diagnostic logging. +- `src/ui/patterns`: client product surfaces; no database/provider imports. + +The same Drizzle repository connects to a local durable `file:` URL and a remote libSQL URL. Demo +accounts and content are validated seed records, not a separate business-logic path. diff --git a/docs/authentication-rbac.md b/docs/authentication-rbac.md new file mode 100644 index 0000000..5457c5b --- /dev/null +++ b/docs/authentication-rbac.md @@ -0,0 +1,29 @@ +# Authentication and RBAC + +Better Auth issues 12-hour database sessions in signed, HTTP-only, SameSite=Lax cookies. Production +requires a unique 32+ character secret and HTTPS enables Secure cookies. Sign-up is disabled; +bounded demo accounts are installed by the validated seed. Sign-out deletes the session, and login +and logout append workspace audit events. Authentication rate limits persist in libSQL. + +Every custom mutation first validates the session and requested membership, then verifies the +request origin, then executes the application permission. Workspace identity is never read from a +JSON payload. + +| Command | Owner | Admin | Editor | Author | Reviewer | +| --- | --- | --- | --- | --- | --- | +| Read workspace | Allow | Allow | Allow | Allow | Allow | +| Create post | Allow | Allow | Allow | Allow | Deny | +| Update/submit/restore own post | Allow | Allow | Allow | Allow own | Deny | +| Delete post | Allow | Allow | Allow | Deny | Deny | +| Request changes / approve | Allow | Allow | Allow | Deny | Allow | +| Schedule / publish / archive | Allow | Allow | Allow | Deny | Deny | +| Upload media | Allow | Allow | Allow | Allow own | Deny | +| Delete media | Allow | Allow | Allow | Deny | Deny | +| Request AI suggestion | Allow | Allow | Allow | Allow | Allow | +| Manage membership | Allow | Allow | Deny | Deny | Deny | +| Reset bounded demo | Allow | Deny | Deny | Deny | Deny | +| Run jobs manually | Allow | Allow | Deny | Deny | Deny | + +`tests/unit/rbac-policy.test.ts` evaluates every role/command pair and explicitly verifies Author +ownership denial. Integration and E2E tests verify arbitrary credentials, revocation, Reviewer +denial, anonymous denial and cross-workspace isolation. diff --git a/docs/data-model.md b/docs/data-model.md new file mode 100644 index 0000000..d0633cb --- /dev/null +++ b/docs/data-model.md @@ -0,0 +1,40 @@ +# Data model and persistence invariants + +Migration `drizzle/0000_editorial_core.sql` creates the application model; later migrations add +authentication limits, media/AI governance and demo-reset idempotency. Checks, foreign keys and +uniqueness are validated from an empty database in integration tests. Operational commands and the +complete sequence are documented in [migrations.md](./migrations.md). + +```mermaid +erDiagram + USER ||--o{ SESSION : owns + USER ||--o{ MEMBERSHIP : joins + WORKSPACE ||--o{ MEMBERSHIP : contains + WORKSPACE ||--o{ POST : owns + POST ||--o{ REVISION : records + POST ||--o{ PUBLICATION : publishes + REVISION ||--o{ PUBLICATION : pins + WORKSPACE ||--o{ MEDIA_ASSET : owns + MEDIA_ASSET }o--|| MEDIA_OBJECT : references + WORKSPACE ||--o{ AUDIT_EVENT : records + WORKSPACE ||--o{ AI_USAGE : meters + WORKSPACE ||--o{ JOB : schedules +``` + +- Every mutable business entity has `workspace_id`; application queries filter it and relational + constraints preserve ownership relationships. +- `revisions` are insert-only: a database trigger rejects updates. Restore creates a new row. +- `posts.version` is positive. Save inserts revision `expectedVersion + 1` and conditionally updates + the post; a concurrent duplicate or zero-row update maps to `VERSION_CONFLICT`. +- `published_revision_id` and `publications.revision_id` point at immutable content. +- Job and publication idempotency keys are unique. +- Scheduled publications and jobs are inserted in one transaction. The payload pins the immutable + revision, and conditional 30-second leases support recovery without duplicate publication. +- Audit events are append-only; a database trigger rejects updates. +- Migration `0002` replaces the coupled media blob table with opaque `media_objects`, constrains one + active asset per post and adds transactional monthly AI quota windows. +- Migration `0003` adds immutable operation/idempotency records that intentionally survive the demo + workspace cascade so a retry cannot trigger a second reset. + +Migrations are checksum-verified. Changing an applied SQL file fails with +`MIGRATION_CHECKSUM_MISMATCH`; schema evolution requires a new numbered file. diff --git a/docs/demo-guide.md b/docs/demo-guide.md new file mode 100644 index 0000000..52872c6 --- /dev/null +++ b/docs/demo-guide.md @@ -0,0 +1,18 @@ +# Guided demo + +1. Open `/sign-in` and enter as **Author**. These are seeded database identities with normal + HTTP-only sessions; no authentication bypass is used. +2. Select the first seeded draft, edit it and observe the saving/saved state. Reload to prove + persistence; open history to compare and restore an old revision as a new one. +3. Submit for review. Sign out, enter as **Reviewer**, select the post and approve or request changes. +4. Enter as **Editor**, publish or schedule the approved immutable revision. Open the public preview. +5. Generate an AI suggestion in visibly labeled mock mode. Confirm the post remains unchanged until + **Apply suggestion**; the accepted result then follows ordinary autosave/version rules. +6. Upload a PNG/JPEG/WebP cover. Replacement activates the verified object before cleanup of the old + one. Editor can delete it. +7. Enter as **Owner** and use **Reset demo data**, then **Confirm reset**. Only `ws-demo` is rebuilt; + user sessions and any configured workspace remain intact. + +The checklist in the workspace derives completion from persisted version/state/publication data. +Reset is Owner-only, origin-checked, limited to three distinct operations per hour and durable across +restart. Repeating the same idempotency key returns the original result without another reset. diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..d79a01c --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,55 @@ +# Deployment runbook + +## Supported shape + +Deploy the Next.js modular monolith with a durable remote libSQL-compatible database and an external +scheduler that calls the protected job adapter at least once per minute. A local `file:` database is +valid for development and CI, not for ephemeral/serverless production storage. + +Required production configuration: + +```text +NEXT_PUBLIC_APP_URL=https://your-canonical-origin.example +DATABASE_URL=libsql://your-database.example +DATABASE_AUTH_TOKEN=<secret> +BETTER_AUTH_SECRET=<unique random 32+ characters> +DEMO_ENABLED=true|false +AI_MODE=mock|gemini +GEMINI_API_KEY=<secret only when gemini> +GEMINI_MODEL=gemini-2.5-flash +CRON_SECRET=<distinct random 24+ characters> +AI_MONTHLY_CHARACTER_QUOTA=200000 +MEDIA_MAX_BYTES=5242880 +AUTH_SIGN_IN_RATE_LIMIT=8 +``` + +Never copy example values into production. `NEXT_PUBLIC_APP_URL` must exactly match the public HTTPS +origin used by browsers or origin validation and session cookies will fail. + +## Release procedure + +1. Rotate/revoke all historical provider values identified in `SECURITY.md`. +2. Create a database backup or restore point. +3. Configure secrets in the deployment platform; do not place them in GitHub logs or source. +4. Run `bun install --frozen-lockfile`, `bun run db:migrate`, `bun run build`. +5. Deploy the immutable commit and call `GET /api/health`; require HTTP 200. +6. Configure the scheduler to `POST /api/jobs/run` with header + `x-autoblog-cron-secret: <CRON_SECRET>` at least once per minute. +7. From a clean browser, execute the Author → Reviewer → Editor → public-preview path. +8. Verify anonymous mutation denial, demo reset isolation and public preview after reload. + +The scheduler accepts no job/workspace payload. Each invocation claims a bounded due batch; leases, +idempotency keys and retry state live in libSQL. + +## Vercel notes + +The repository can be connected to Vercel, but AutoBlog does not enable or assume a paid plan. Use a +remote database; do not rely on Vercel's ephemeral filesystem. Configure a scheduler only when the +existing account tier supports the required cadence, otherwise use an already-authorized external +scheduler. Provider/account creation is outside this repository. + +## Verification and rollback + +Record the commit SHA, deployment URL, migration result, health request ID and clean-session E2E +result in release notes. For any failure, stop the scheduler and follow +[rollback-recovery.md](./rollback-recovery.md). diff --git a/docs/editorial-workflow.md b/docs/editorial-workflow.md new file mode 100644 index 0000000..19f8c8a --- /dev/null +++ b/docs/editorial-workflow.md @@ -0,0 +1,44 @@ +# Editorial workflow and publication + +## State machine + +| Command | From | To | Authorized roles | Preconditions | +| --- | --- | --- | --- | --- | +| Submit | Draft, Changes Requested | In Review | Owner, Admin, Editor, owning Author | non-empty immutable draft revision | +| Request changes | In Review | Changes Requested | Owner, Admin, Editor, Reviewer | current expected version | +| Approve | In Review | Approved | Owner, Admin, Editor, Reviewer | current expected version | +| Schedule | Approved | Scheduled | Owner, Admin, Editor | future ISO timestamp and idempotency key | +| Publish | Approved, Scheduled | Published | Owner, Admin, Editor | idempotency key and current draft revision | +| Archive | any non-archived state | Archived | Owner, Admin, Editor | current expected version | + +Every transition is checked in the application service and again against state/version in the +repository transaction. Illegal transitions return `ILLEGAL_TRANSITION`; stale commands return +`VERSION_CONFLICT`. Audit rows record actor, request ID, source/destination and revision pointer. + +Content is locked during review, approval and scheduling. Editing or restoring a Published post +opens a new Draft revision but does not change `published_revision_id`. Public preview therefore +continues to render the reviewed artifact until another explicit publication. + +## Revision behavior + +- Autosave is debounced by 850 ms and serializes in-flight writes. +- Each successful content save appends a row; database triggers reject revision updates. +- History is workspace scoped and newest-first. Compare never mutates state. +- Restore copies an old revision into a new row with `restored_from_revision_id`; the source remains + byte-for-byte unchanged. +- The post version is a concurrency token. Workflow transitions can create intentional gaps between + revision version numbers. + +## Scheduled worker + +Scheduling inserts the publication and `publish` job atomically. The job payload pins post, +publication and revision IDs. `bun run jobs:run` claims at most 50 due jobs; the protected +`POST /api/jobs/run` adapter provides the same operation for an authorized deployment trigger. + +Claims use a 30-second conditional lease. Failures retain a stable error code, apply bounded +exponential delay and stop after three attempts. Completion first checks publication status, making +lease recovery and repeated execution no-ops. Archiving a scheduled post cancels only its matching +publication/job. + +Deployment must invoke the worker at least once per minute. No external queue or paid scheduler is +required by the application contract. diff --git a/docs/engineering/baseline.md b/docs/engineering/baseline.md new file mode 100644 index 0000000..2422310 --- /dev/null +++ b/docs/engineering/baseline.md @@ -0,0 +1,38 @@ +# Engineering baseline — 2026-07-22 + +Measurements were taken on commit `485f037` using Bun 1.3.12 on the dedicated +`feat/rfc-editorial-cms` branch. Counts below are command output, not estimates. + +| Gate | Command | Baseline result | +| --- | --- | --- | +| Frozen install | `bun install --frozen-lockfile` | Failed: lockfile would change | +| Typecheck | `bunx tsc --noEmit --pretty false` | Failed: 42 errors | +| Source lint | `bunx eslint . --ignore-pattern .next` | Failed: 93 errors, 39 warnings | +| Production build | `bun run build` | Passed only while skipping type and lint validation | +| Runtime audit | `bun audit --production` | Failed: 48 findings; 2 critical, 22 high | +| Tests | package scripts and repository search | No automated test command or test suite | +| CI | `.github/workflows` search | No workflow | + +## Security containment finding + +`.env.local` is tracked. Its current values are empty, but commit `8f83cec` contains non-empty +values for MongoDB, Cloudinary and Gemini variables. Values were not printed or copied. Because +shared history will not be rewritten, the affected provider credentials must be rotated or +revoked by their account owner before release. Current-tree removal and automated secret scanning +are Phase 0 gates. + +## Product-path evidence + +- Visible post flows call `utils/api-fetch.ts`, a process-local simulator. +- Separate Mongoose, Cloudinary and Gemini routes are directly reachable without a real session. +- `actions/check-user.ts` accepts any two non-empty strings. +- `next.config.ts` permits every HTTPS image host, accepts 20 MB server-action bodies, applies + global no-cache headers, and suppresses type and lint build validation. +- README and FEATURES claim versioning, conflict resolution, WCAG conformance and durable autosave + without corresponding executable paths or tests. + +## Reproduction integrity + +The baseline intentionally records the current toolchain result, which differs from older RFC +counts after advisory and lint-rule updates. Improved metrics will only be added after the named +commands pass in a clean worktree. diff --git a/docs/engineering/rfc-closure-ledger.md b/docs/engineering/rfc-closure-ledger.md new file mode 100644 index 0000000..61c5dec --- /dev/null +++ b/docs/engineering/rfc-closure-ledger.md @@ -0,0 +1,34 @@ +# RFC closure ledger + +Status values are `Open`, `In progress`, `Verified`, or `Externally blocked`. A row becomes +`Verified` only when its user path and named automated evidence both pass. + +| ID | Required implementation | Primary evidence | Status | +| --- | --- | --- | --- | +| P-01 | One durable application/repository path | integration persistence + restart tests | Verified | +| P-02 | Strict type/lint/build gates | independent CI jobs | Verified | +| P-03 | Reproducible, patched dependencies | frozen install + runtime audit | Verified | +| P-04 | Real revocable sessions on every protected adapter | anonymous/session route tests | Verified | +| P-05 | Five-role application policy | complete positive/negative matrix tests | Verified | +| P-06 | Canonical validated workspace domain schemas | schema/validation tests | Verified | +| P-07 | Explicit workflow and durable publisher | state machine + duplicate/retry job tests + flagship E2E | Verified | +| P-08 | Immutable revisions, compare and restore | append-only integration + history/restore E2E | Verified | +| P-09 | Debounced conditional autosave and conflict UI | concurrent integration + stale-writer E2E | Verified | +| P-10 | Bounded verified media with safe replacement | MIME/size/dimension/isolation + compensation/cleanup + E2E | Verified | +| P-11 | Authorized, metered, bounded AI command | quota/rate/timeout/anonymous + explicit-Apply E2E | Verified | +| P-12 | Explicit mock/configured adapter contract | shared mock/Gemini structured-output tests | Verified | +| P-13 | Domain ownership; obsolete paths removed | modular tree + type/lint + legacy asset deletion | Verified | +| P-14 | Tested accessibility targets | four-surface axe + keyboard/focus/reduced-motion + visual baselines | Verified | +| P-15 | Semantic caching and measured budgets | production LCP/INP/CLS/JS budgets + six Lighthouse reports | Verified | +| P-16 | Stable non-leaking public errors | API contract tests | Verified | +| P-17 | Guided bounded recruiter demo | dynamic checklist + isolated idempotent reset integration/E2E | Verified | +| P-18 | CI, truthful docs, screenshots and release | versioned visuals/docs + PR #3; release/public verification externally blocked | Externally blocked | + +## Release-only external dependencies + +- Rotate/revoke the historical MongoDB, Cloudinary and Gemini credentials named in the baseline. +- Configure the existing deployment with a remote libSQL URL/token and a unique Better Auth secret. +- Disable Vercel preview/production deployment protection for the intended public URL, or supply an + approved public domain; the current candidate redirects anonymous reviewers to Vercel login. +- GitHub and deployment mutations proceed only after local gates pass and authenticated ownership + is verified as `PatrickDev-it`. diff --git a/docs/known-limitations.md b/docs/known-limitations.md new file mode 100644 index 0000000..0610716 --- /dev/null +++ b/docs/known-limitations.md @@ -0,0 +1,19 @@ +# Known limitations + +- AutoBlog surfaces concurrent-write conflicts but does not merge text or provide real-time co-editing. +- The default media provider stores bounded objects in libSQL; it is not a high-volume CDN/object-store + design and does not perform malware scanning or image conversion. +- Gemini behavior is contract-tested with a controlled provider boundary; no live output quality, + token, cost or cancellation guarantee is claimed. +- Scheduled publication requires an external one-minute trigger. No paid scheduler is provisioned. +- Accessibility evidence covers automated axe, keyboard/focus/reduced-motion and scoped visual review; + it is not a blanket WCAG certification or full screen-reader/browser matrix. +- Performance values are Chromium lab measurements on the recorded machine, not field telemetry. +- Visual regression is pinned to Windows Chromium to control font rasterization variance. +- Demo authentication uses bounded seeded identities selected by role; production user invitation, + password recovery, MFA and identity-provider federation are not implemented. +- Membership management is policy-defined but has no recruiter-demo UI. +- Analytics and engagement metrics are intentionally absent rather than simulated. +- Public v2 deployment remains blocked until historical provider credentials are rotated, the + deployment owner configures durable remote libSQL plus the scheduler, and Vercel deployment + protection is removed from the intended recruiter-facing URL. diff --git a/docs/media-security.md b/docs/media-security.md new file mode 100644 index 0000000..c5d3869 --- /dev/null +++ b/docs/media-security.md @@ -0,0 +1,34 @@ +# Media security policy + +## Accepted input + +- Authenticated bounded multipart only; remote URL ingestion is not implemented and the remote-host + allowlist is empty, eliminating this SSRF path. +- PNG, JPEG and WebP only. The claimed MIME must match the decoded `sharp` format. +- Default maximum: 5 MiB; configuration cannot exceed 10 MiB. +- Maximum width/height: 4096 pixels; maximum decoded area: 16 megapixels. +- Filenames are display metadata only. Path components and unsafe characters are removed; provider + keys are generated from authenticated workspace identity plus a UUID. + +The route reads the request stream with an explicit cap before invoking multipart parsing. It returns +stable validation errors without decoder/provider details. Authenticated reads send `nosniff` and +private cache headers. + +## Storage and replacement + +`MediaProvider` is the object boundary. The default production adapter stores opaque bounded objects +in durable libSQL; metadata remains in `media_assets`. This keeps local, CI and remote libSQL behavior +identical without private infrastructure. + +Upload order is verify → provider put → metadata finalization. Replacement marks the old object +`replaced` and activates the new object in one transaction protected by a partial unique index. If +provider put or finalization fails, the prior active row is unchanged. Orphan and replaced objects are +deleted by leased, idempotent `media_cleanup` jobs with three attempts. + +Authors may upload/replace only for their own posts. Owner/Admin/Editor may delete. Every operation is +workspace-filtered and audited; hiding a control is not relied upon for enforcement. + +## Operational limitation + +Database object storage is deliberate for a bounded portfolio demo. A high-volume deployment should +supersede RFC 003 with signed direct uploads to an explicitly approved object store/CDN. diff --git a/docs/migrations.md b/docs/migrations.md new file mode 100644 index 0000000..d9fbd7f --- /dev/null +++ b/docs/migrations.md @@ -0,0 +1,33 @@ +# Migration and seed operations + +Numbered SQL files under `drizzle/` are forward-only and checksum verified. Applied SQL must never be +edited; add a new migration instead. + +## Empty database validation + +```bash +DATABASE_URL=file:./data/empty-verification.db bun run db:migrate +DATABASE_URL=file:./data/empty-verification.db DEMO_ENABLED=true bun run db:seed +``` + +`bun run db:setup` applies pending migrations and seeds only when `DEMO_ENABLED=true`. +`bun run db:setup -- --reset-demo` is intended for isolated local/E2E setup; product reset uses the +authenticated Owner command. + +## Current sequence + +| Migration | Change | Compatibility | +| --- | --- | --- | +| `0000_editorial_core.sql` | workspace identity, posts, immutable revisions, publications, jobs and audit | foundational | +| `0001_auth_rate_limit.sql` | durable authentication rate limiting | additive | +| `0002_media_ai_governance.sql` | verified media object boundary, active-asset constraint and AI quota windows | replaces legacy media blob table | +| `0003_demo_reset_idempotency.sql` | immutable reset operation keys outside workspace cascade | additive | + +Integration tests create an empty isolated database, apply the complete sequence, verify checksums +and exercise seed/reset behavior. Remote migration uses the same client and SQL path. + +## Production discipline + +Back up first, migrate once, deploy compatible application code, then start the scheduler. A checksum +mismatch is an incident: restore the expected migration file from source control; do not alter the +database ledger manually. diff --git a/docs/performance.md b/docs/performance.md new file mode 100644 index 0000000..860c0bf --- /dev/null +++ b/docs/performance.md @@ -0,0 +1,49 @@ +# Performance budgets and measurements + +Budgets were accepted before measurement in RFC 004 and execute against `next start`, not the HMR +development server. + +| Route / metric | Budget | 2026-07-22 local Chromium | Status | +| --- | ---: | ---: | --- | +| Marketing LCP | ≤ 2,500 ms | 188 ms | Pass | +| Marketing CLS | ≤ 0.10 | 0.000 | Pass | +| Marketing initial JS transfer | ≤ 180 KiB | 138.6 KiB (141,959 B) | Pass | +| Workspace LCP | ≤ 2,500 ms | 300 ms | Pass | +| Workspace CLS | ≤ 0.10 | 0.000 | Pass | +| Workspace INP event-duration upper bound | ≤ 200 ms | 16 ms | Pass | +| Workspace history response/paint proxy | ≤ 500 ms | 146 ms | Pass | +| Workspace cold initial JS transfer | ≤ 320 KiB | 216.2 KiB (221,420 B) | Pass | + +Chromium recorded a 16 ms Event Timing duration for the measured workspace interaction. This is a +bounded lab proxy rather than a field INP claim. The 146 ms workflow measurement includes fetch, +rendering and visibility of revision history. + +Run `bun run build` followed by `bun run test:performance`. CI fails on any declared regression. +Results are lab targets and do not claim universal field performance across devices/networks. +The command writes the representative public/editor bundle and Web Vital measurements to +`.performance/bundle-report.json`; CI retains it with the Lighthouse output for seven days. + +## Lighthouse reports + +`bun run test:lighthouse` performs three desktop Lighthouse runs on marketing and sign-in under the +same production server. Median-run assertions require performance ≥0.80, accessibility ≥0.95, +best-practices ≥0.90, SEO ≥0.90, LCP ≤2,500 ms and CLS ≤0.10. Reports remain local in the ignored +`.lighthouse/` directory; no hosted report service receives route output. Direct interaction and +JavaScript budgets above remain mandatory because Lighthouse does not replace them. + +Measured medians on 2026-07-22: + +| Route | Performance | Accessibility | Best practices | SEO | LCP | CLS | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | +| Marketing | 0.92 | 1.00 | 1.00 | 1.00 | 1,718 ms | 0 | +| Sign-in | 0.91 | 1.00 | 1.00 | 1.00 | 1,709 ms | 0 | + +## Cache semantics + +- Marketing is statically generated. +- Authenticated workspace/API data is dynamic; JSON application responses send `private, no-store`. +- Media bytes are authenticated and privately cached for five minutes with `nosniff`. +- Public preview pins an immutable revision and declares 60-second revalidation. +- Health/readiness is `no-store`. + +No global cache disablement is configured. diff --git a/docs/releases/v2.0.0.md b/docs/releases/v2.0.0.md new file mode 100644 index 0000000..94926af --- /dev/null +++ b/docs/releases/v2.0.0.md @@ -0,0 +1,45 @@ +# AutoBlog CMS 2.0.0 release notes + +Status: repository candidate on [draft PR #3](https://github.com/PatrickDev-it/AutoBlog-CMS/pull/3); +public release is blocked by the external actions below. + +## Product + +- Replaced the process-local simulator with one durable workspace-scoped application path. +- Added database-backed demo sessions and Owner/Admin/Editor/Author/Reviewer authorization. +- Added immutable revisions, compare, restore-as-new, debounced conditional autosave and conflict UI. +- Added review/approval, idempotent scheduling, immutable publication and public preview. +- Added bounded verified media with failure compensation and explicit metered AI suggestions. +- Added recruiter landing, seeded guided workflow, bounded reset and responsive product states. + +## Engineering evidence + +- Bun frozen install; strict TypeScript and zero-warning ESLint; production build without bypasses. +- 19 unit, 25 integration, 10 E2E, 6 accessibility, 5 visual, 2 direct performance tests and six + Lighthouse runs locally pass. +- Runtime dependency audit reports zero vulnerabilities; current-tree secret scan passes. +- Migrations `0000`–`0003` reproduce the database from empty and reject modified checksums. +- CI separates install, type, lint, unit, integration, E2E, accessibility, visual, performance, build, + runtime audit and full-history secret scanning. +- Protected PR run `29909234038` passes all eleven independent jobs, including the pinned TruffleHog + history-range scan. That automated zero-finding result does not prove revocation of provider values + identified during the manual baseline review; owner-confirmed rotation remains a release gate. + +## Migration and rollback + +This is a breaking replacement of the legacy simulator/MongoDB/Cloudinary paths. Back up the target +database, apply all migrations and deploy 2.0 together. There is no supported data migration from the +legacy demo simulator because it was not durable. See [migrations](../migrations.md) and +[rollback/recovery](../rollback-recovery.md). + +## External release blockers + +1. Provider owners must rotate/revoke historical MongoDB, Cloudinary and Gemini values from commit + `8f83cec` and record completion without publishing values. +2. Deployment owner must configure remote libSQL, unique auth/cron secrets and a one-minute scheduler. +3. Deployment owner must expose an approved public URL. The candidate Vercel preview is protected and + redirects an anonymous clean session to Vercel login. +4. The release manager must then verify CI, merge the pull request, deploy the merged SHA, execute the + clean-browser workflow and create the `v2.0.0` tag/release. + +No old production URL is presented as AutoBlog 2.0 evidence. diff --git a/docs/rollback-recovery.md b/docs/rollback-recovery.md new file mode 100644 index 0000000..8060194 --- /dev/null +++ b/docs/rollback-recovery.md @@ -0,0 +1,32 @@ +# Rollback and recovery + +## Application rollback + +1. Stop scheduler invocations to prevent new state transitions during triage. +2. Preserve diagnostic logs by request ID and take a database snapshot. +3. Roll back to the previous immutable deployment only if it supports every applied migration. +4. Run `GET /api/health`, authenticate and verify workspace isolation before resuming jobs. + +Migrations are forward-only. Do not delete migration ledger rows or rewrite applied SQL. If the prior +application cannot read the new schema, restore the pre-migration database snapshot together with the +prior application commit. + +## Data recovery + +- Editorial recovery uses revision history: restore creates a new revision and preserves evidence. +- Published content remains pinned while later drafts are repaired. +- Failed scheduled jobs retain attempt/error state and resume after the lease expires. +- Media cleanup is idempotent; the active asset is never a cleanup candidate. +- Demo reset affects only the workspace with `is_demo=true`; it is not a production recovery tool. + +## Provider incidents + +Set `AI_MODE=mock` to stop Gemini calls without disabling editing. A media-provider failure rejects the +new upload and retains the active object. Rotate compromised `CRON_SECRET` and provider/database +tokens in their owner consoles, then redeploy all consumers atomically. + +## Recovery verification + +Run integration tests against a restored copy, then exercise login, stale-write conflict, publication, +public preview and one no-op duplicate job. Record recovery point, recovery time and any permanently +lost data; the project does not publish unmeasured RPO/RTO claims. diff --git a/docs/security-threat-model.md b/docs/security-threat-model.md new file mode 100644 index 0000000..5b2d526 --- /dev/null +++ b/docs/security-threat-model.md @@ -0,0 +1,56 @@ +# Security and threat model + +## Scope and assets + +This model covers the Next.js application, Better Auth sessions, workspace content, immutable +revisions/publications, media objects, AI usage metadata, libSQL and the scheduled worker. Primary +assets are session integrity, tenant isolation, unpublished content, published-revision integrity, +provider credentials, media availability and AI quota. + +## Trust boundaries + +```mermaid +flowchart LR + Internet --> Next[Next.js adapters] + Next --> Auth[Session and Origin boundary] + Auth --> Policy[Workspace RBAC] + Policy --> Domain[Application services] + Domain --> DB[(libSQL)] + Domain --> Gemini[Optional Gemini provider] + Scheduler --> Cron[Distinct scheduler secret] + Cron --> Domain +``` + +Browser payload identity is untrusted. Workspace and actor come only from the validated session. +Gemini and the deployment scheduler are external principals. Local database object storage remains +inside the application persistence boundary. + +## Threats, controls and evidence + +| Threat | Control | Verification | Residual risk | +| --- | --- | --- | --- | +| Session theft/fixation | HTTP-only SameSite=Lax cookie, 12-hour expiry, DB revocation, HTTPS Secure cookie | login/logout/revocation integration | compromised endpoint/browser remains outside application control | +| CSRF | trusted Origin check on custom mutations plus SameSite cookie | invalid-origin integration/E2E | upstream proxy must preserve canonical scheme/host | +| Cross-workspace access | session-derived membership, policy first, workspace-filtered repositories and FKs | negative role matrix and isolation integration | operator SQL access is privileged | +| Privilege escalation | five-role command matrix enforced in services | every role/command pair plus route denials | membership administration needs operational review | +| Lost update | expected post version and HTTP 409 | concurrent-writer integration and conflict E2E | no automatic text merge | +| Revision/publication tampering | insert-only triggers and pinned published revision | trigger, restore and later-edit tests | database owner can alter infrastructure directly | +| Upload abuse/decompression | stream byte cap, decoded type/dimension/pixel checks, no remote URL ingestion | forged MIME/size/dimension tests | malware scanning is not included | +| Media replacement loss | verify/store before atomic activation, leased compensation job | provider/finalization injection tests | prolonged worker outage delays orphan cleanup | +| AI cost/data abuse | RBAC, persistent rate/quota reservation, bounded fields/output, timeout | quota/rate/timeout/anonymous tests | SDK abort may not cancel provider billing | +| Scheduler forgery/replay | distinct constant-time secret and idempotent leased jobs | unauthorized and duplicate job tests | secret rotation requires coordinated deployment | +| Information disclosure | stable error taxonomy, request ID, structured diagnostic log | API contract tests | deployment logs remain operator-controlled | +| Dependency/secret compromise | frozen lockfile, pinned Actions, production audit and secret scans | independent CI jobs | historic provider values require owner rotation | + +## Data minimization + +AI usage rows retain provider/model, latency and actual count metadata; they do not retain prompts, +drafts, suggestions, raw responses or API keys. Audit events store actor/action/resource metadata, not +editorial bodies. Public preview exposes only the pinned published revision. + +## Security release gate + +Commit `8f83cec` contains historical MongoDB, Cloudinary and Gemini values. The current tree neither +uses nor reproduces them, but repository history is still a distribution channel. Provider account +owners must rotate/revoke all affected credentials and record completion before the v2 release. The +history scan must not be suppressed to manufacture a green build. diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..dd192b5 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + dialect: 'sqlite', + schema: './src/platform/db/schema.ts', + out: './drizzle/generated', + dbCredentials: { url: process.env.DATABASE_URL ?? 'file:./data/autoblog.db' }, + strict: true, +}); diff --git a/drizzle/0000_editorial_core.sql b/drizzle/0000_editorial_core.sql new file mode 100644 index 0000000..5f21a18 --- /dev/null +++ b/drizzle/0000_editorial_core.sql @@ -0,0 +1,215 @@ +PRAGMA foreign_keys = ON; + +CREATE TABLE "user" ( + "id" TEXT PRIMARY KEY NOT NULL, + "name" TEXT NOT NULL, + "email" TEXT NOT NULL UNIQUE, + "email_verified" INTEGER NOT NULL DEFAULT 0, + "image" TEXT, + "created_at" INTEGER NOT NULL, + "updated_at" INTEGER NOT NULL +); + +CREATE TABLE "session" ( + "id" TEXT PRIMARY KEY NOT NULL, + "expires_at" INTEGER NOT NULL, + "token" TEXT NOT NULL UNIQUE, + "created_at" INTEGER NOT NULL, + "updated_at" INTEGER NOT NULL, + "ip_address" TEXT, + "user_agent" TEXT, + "user_id" TEXT NOT NULL REFERENCES "user"("id") ON DELETE CASCADE +); +CREATE INDEX "session_user_idx" ON "session" ("user_id"); + +CREATE TABLE "account" ( + "id" TEXT PRIMARY KEY NOT NULL, + "account_id" TEXT NOT NULL, + "provider_id" TEXT NOT NULL, + "user_id" TEXT NOT NULL REFERENCES "user"("id") ON DELETE CASCADE, + "access_token" TEXT, + "refresh_token" TEXT, + "id_token" TEXT, + "access_token_expires_at" INTEGER, + "refresh_token_expires_at" INTEGER, + "scope" TEXT, + "password" TEXT, + "created_at" INTEGER NOT NULL, + "updated_at" INTEGER NOT NULL, + UNIQUE ("provider_id", "account_id") +); +CREATE INDEX "account_user_idx" ON "account" ("user_id"); + +CREATE TABLE "verification" ( + "id" TEXT PRIMARY KEY NOT NULL, + "identifier" TEXT NOT NULL, + "value" TEXT NOT NULL, + "expires_at" INTEGER NOT NULL, + "created_at" INTEGER, + "updated_at" INTEGER +); +CREATE INDEX "verification_identifier_idx" ON "verification" ("identifier"); + +CREATE TABLE "workspaces" ( + "id" TEXT PRIMARY KEY NOT NULL, + "slug" TEXT NOT NULL UNIQUE, + "name" TEXT NOT NULL, + "is_demo" INTEGER NOT NULL DEFAULT 0, + "created_at" INTEGER NOT NULL, + "updated_at" INTEGER NOT NULL +); + +CREATE TABLE "memberships" ( + "workspace_id" TEXT NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE, + "user_id" TEXT NOT NULL REFERENCES "user"("id") ON DELETE CASCADE, + "role" TEXT NOT NULL CHECK ("role" IN ('Owner','Admin','Editor','Author','Reviewer')), + "created_at" INTEGER NOT NULL, + PRIMARY KEY ("workspace_id", "user_id") +); +CREATE INDEX "membership_user_idx" ON "memberships" ("user_id"); + +CREATE TABLE "posts" ( + "id" TEXT PRIMARY KEY NOT NULL, + "workspace_id" TEXT NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE, + "slug" TEXT NOT NULL, + "title" TEXT NOT NULL, + "excerpt" TEXT NOT NULL DEFAULT '', + "state" TEXT NOT NULL DEFAULT 'Draft' CHECK ("state" IN ('Draft','InReview','ChangesRequested','Approved','Scheduled','Published','Archived')), + "version" INTEGER NOT NULL DEFAULT 1 CHECK ("version" > 0), + "author_id" TEXT NOT NULL REFERENCES "user"("id") ON DELETE RESTRICT, + "draft_revision_id" TEXT REFERENCES "revisions"("id") ON DELETE SET NULL, + "published_revision_id" TEXT REFERENCES "revisions"("id") ON DELETE SET NULL, + "scheduled_for" INTEGER, + "submitted_at" INTEGER, + "approved_at" INTEGER, + "published_at" INTEGER, + "archived_at" INTEGER, + "created_at" INTEGER NOT NULL, + "updated_at" INTEGER NOT NULL, + UNIQUE ("workspace_id", "slug"), + UNIQUE ("workspace_id", "id") +); +CREATE INDEX "post_workspace_state_idx" ON "posts" ("workspace_id", "state"); + +CREATE TABLE "revisions" ( + "id" TEXT PRIMARY KEY NOT NULL, + "workspace_id" TEXT NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE, + "post_id" TEXT NOT NULL, + "version" INTEGER NOT NULL, + "title" TEXT NOT NULL, + "excerpt" TEXT NOT NULL DEFAULT '', + "content" TEXT NOT NULL, + "author_id" TEXT NOT NULL REFERENCES "user"("id") ON DELETE RESTRICT, + "restored_from_revision_id" TEXT, + "created_at" INTEGER NOT NULL, + UNIQUE ("post_id", "version"), + UNIQUE ("workspace_id", "id"), + FOREIGN KEY ("workspace_id", "post_id") REFERENCES "posts"("workspace_id", "id") ON DELETE CASCADE +); +CREATE INDEX "revision_workspace_post_idx" ON "revisions" ("workspace_id", "post_id"); + +CREATE TABLE "publications" ( + "id" TEXT PRIMARY KEY NOT NULL, + "workspace_id" TEXT NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE, + "post_id" TEXT NOT NULL, + "revision_id" TEXT NOT NULL REFERENCES "revisions"("id") ON DELETE RESTRICT, + "status" TEXT NOT NULL CHECK ("status" IN ('scheduled','published','cancelled')), + "scheduled_for" INTEGER, + "published_at" INTEGER, + "idempotency_key" TEXT NOT NULL UNIQUE, + "created_by" TEXT NOT NULL REFERENCES "user"("id") ON DELETE RESTRICT, + "created_at" INTEGER NOT NULL, + FOREIGN KEY ("workspace_id", "post_id") REFERENCES "posts"("workspace_id", "id") ON DELETE CASCADE +); +CREATE INDEX "publication_due_idx" ON "publications" ("status", "scheduled_for"); + +CREATE TABLE "media_assets" ( + "id" TEXT PRIMARY KEY NOT NULL, + "workspace_id" TEXT NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE, + "post_id" TEXT, + "status" TEXT NOT NULL CHECK ("status" IN ('pending','active','replaced','deleted','cleanup_pending')), + "storage_key" TEXT NOT NULL UNIQUE, + "file_name" TEXT NOT NULL, + "mime_type" TEXT NOT NULL, + "byte_size" INTEGER NOT NULL CHECK ("byte_size" > 0), + "width" INTEGER NOT NULL, + "height" INTEGER NOT NULL, + "checksum" TEXT NOT NULL, + "alt_text" TEXT NOT NULL DEFAULT '', + "created_by" TEXT NOT NULL REFERENCES "user"("id") ON DELETE RESTRICT, + "created_at" INTEGER NOT NULL, + "updated_at" INTEGER NOT NULL, + FOREIGN KEY ("workspace_id", "post_id") REFERENCES "posts"("workspace_id", "id") ON DELETE SET NULL +); +CREATE INDEX "media_workspace_post_idx" ON "media_assets" ("workspace_id", "post_id"); + +CREATE TABLE "media_blobs" ( + "asset_id" TEXT PRIMARY KEY NOT NULL REFERENCES "media_assets"("id") ON DELETE CASCADE, + "data" BLOB NOT NULL +); + +CREATE TABLE "audit_events" ( + "id" TEXT PRIMARY KEY NOT NULL, + "workspace_id" TEXT NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE, + "actor_id" TEXT REFERENCES "user"("id") ON DELETE SET NULL, + "action" TEXT NOT NULL, + "target_type" TEXT NOT NULL, + "target_id" TEXT, + "request_id" TEXT NOT NULL, + "metadata" TEXT NOT NULL DEFAULT '{}', + "created_at" INTEGER NOT NULL +); +CREATE INDEX "audit_workspace_created_idx" ON "audit_events" ("workspace_id", "created_at"); + +CREATE TABLE "ai_usage" ( + "id" TEXT PRIMARY KEY NOT NULL, + "workspace_id" TEXT NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE, + "user_id" TEXT NOT NULL REFERENCES "user"("id") ON DELETE CASCADE, + "mode" TEXT NOT NULL CHECK ("mode" IN ('mock','gemini')), + "provider" TEXT NOT NULL, + "model" TEXT NOT NULL, + "latency_ms" INTEGER NOT NULL, + "input_characters" INTEGER NOT NULL, + "output_characters" INTEGER NOT NULL, + "input_tokens" INTEGER, + "output_tokens" INTEGER, + "created_at" INTEGER NOT NULL +); +CREATE INDEX "ai_usage_workspace_created_idx" ON "ai_usage" ("workspace_id", "created_at"); + +CREATE TABLE "jobs" ( + "id" TEXT PRIMARY KEY NOT NULL, + "workspace_id" TEXT NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE, + "type" TEXT NOT NULL CHECK ("type" IN ('publish','media_cleanup')), + "payload" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'pending' CHECK ("status" IN ('pending','leased','completed','failed')), + "attempts" INTEGER NOT NULL DEFAULT 0, + "run_at" INTEGER NOT NULL, + "lease_until" INTEGER, + "last_error_code" TEXT, + "idempotency_key" TEXT NOT NULL UNIQUE, + "created_at" INTEGER NOT NULL, + "updated_at" INTEGER NOT NULL +); +CREATE INDEX "job_claim_idx" ON "jobs" ("status", "run_at", "lease_until"); + +CREATE TABLE "rate_limits" ( + "key" TEXT NOT NULL, + "window_started_at" INTEGER NOT NULL, + "count" INTEGER NOT NULL DEFAULT 0, + "expires_at" INTEGER NOT NULL, + PRIMARY KEY ("key", "window_started_at") +); +CREATE INDEX "rate_limit_expiry_idx" ON "rate_limits" ("expires_at"); + +CREATE TRIGGER "revision_immutable_update" +BEFORE UPDATE ON "revisions" +BEGIN + SELECT RAISE(ABORT, 'IMMUTABLE_REVISION'); +END; + +CREATE TRIGGER "audit_event_immutable_update" +BEFORE UPDATE ON "audit_events" +BEGIN + SELECT RAISE(ABORT, 'IMMUTABLE_AUDIT_EVENT'); +END; diff --git a/drizzle/0001_auth_rate_limit.sql b/drizzle/0001_auth_rate_limit.sql new file mode 100644 index 0000000..2dcf1fe --- /dev/null +++ b/drizzle/0001_auth_rate_limit.sql @@ -0,0 +1,6 @@ +CREATE TABLE "rateLimit" ( + "id" TEXT PRIMARY KEY NOT NULL, + "key" TEXT NOT NULL UNIQUE, + "count" INTEGER NOT NULL, + "lastRequest" INTEGER NOT NULL +); diff --git a/drizzle/0002_media_ai_governance.sql b/drizzle/0002_media_ai_governance.sql new file mode 100644 index 0000000..016ae69 --- /dev/null +++ b/drizzle/0002_media_ai_governance.sql @@ -0,0 +1,22 @@ +CREATE TABLE "media_objects" ( + "storage_key" TEXT PRIMARY KEY NOT NULL, + "data" BLOB NOT NULL, + "created_at" INTEGER NOT NULL +); + +DROP TABLE "media_blobs"; + +ALTER TABLE "media_assets" ADD COLUMN "replaces_asset_id" TEXT; + +CREATE UNIQUE INDEX "media_one_active_per_post" +ON "media_assets" ("workspace_id", "post_id") +WHERE "status" = 'active' AND "post_id" IS NOT NULL; + +CREATE TABLE "ai_quota_windows" ( + "workspace_id" TEXT NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE, + "window_started_at" INTEGER NOT NULL, + "reserved_characters" INTEGER NOT NULL DEFAULT 0 CHECK ("reserved_characters" >= 0), + "used_characters" INTEGER NOT NULL DEFAULT 0 CHECK ("used_characters" >= 0), + "updated_at" INTEGER NOT NULL, + PRIMARY KEY ("workspace_id", "window_started_at") +); diff --git a/drizzle/0003_demo_reset_idempotency.sql b/drizzle/0003_demo_reset_idempotency.sql new file mode 100644 index 0000000..e995fd7 --- /dev/null +++ b/drizzle/0003_demo_reset_idempotency.sql @@ -0,0 +1,14 @@ +CREATE TABLE "idempotency_records" ( + "workspace_id" TEXT NOT NULL, + "operation" TEXT NOT NULL, + "key" TEXT NOT NULL, + "result" TEXT NOT NULL, + "created_at" INTEGER NOT NULL, + PRIMARY KEY ("workspace_id", "operation", "key") +); + +CREATE TRIGGER "idempotency_record_immutable_update" +BEFORE UPDATE ON "idempotency_records" +BEGIN + SELECT RAISE(ABORT, 'IMMUTABLE_IDEMPOTENCY_RECORD'); +END; diff --git a/eslint.config.mjs b/eslint.config.mjs index ea598c1..9a2a789 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,22 +1,24 @@ -import { dirname } from "path"; -import { fileURLToPath } from "url"; -import { FlatCompat } from "@eslint/eslintrc"; +import { defineConfig, globalIgnores } from 'eslint/config'; +import nextVitals from 'eslint-config-next/core-web-vitals'; +import nextTypeScript from 'eslint-config-next/typescript'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -const compat = new FlatCompat({ - baseDirectory: __dirname, -}); - -const eslintConfig = [ - ...compat.extends("next/core-web-vitals", "next/typescript"), +export default defineConfig([ + ...nextVitals, + ...nextTypeScript, + globalIgnores([ + '.next/**', + 'coverage/**', + 'data/**', + 'playwright-report/**', + 'test-results/**', + '.sinapsi/**', + ]), { + files: ['**/*.{ts,tsx}'], rules: { - "@typescript-eslint/no-explicit-any": "off", // Disabilita il controllo su 'any' - "@typescript-eslint/ban-ts-comment": "off", + '@typescript-eslint/consistent-type-imports': 'error', + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-non-null-assertion': 'error', }, }, -]; - -export default eslintConfig; +]); diff --git a/global.d.ts b/global.d.ts deleted file mode 100644 index a78ebf2..0000000 --- a/global.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Connection, Mongoose } from 'mongoose'; -import { v2 as Cloudinary } from 'cloudinary'; -import type { Env } from './env'; - -declare global { - // process.env - namespace NodeJS { - interface ProcessEnv { - MONGODB_URI: string; - DBNAME: string; - } - } - - var db: (args?: { - MONGODB_URI: string; - DBNAME: string; - newConnection?: boolean; - }) => Promise<Connection>; - var mongoose: Mongoose; - var connection: Connection; - - var cloudinary: typeof Cloudinary; -} - -export {}; diff --git a/hooks/use-longPress.ts b/hooks/use-longPress.ts deleted file mode 100644 index 32e479f..0000000 --- a/hooks/use-longPress.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { useCallback, useRef, useState } from 'react'; - -export default ( - onLongPress, - { shouldPreventDefault = true, delay = 300 } = {}, - onClick = () => {} -) => { - const [longPressTriggered, setLongPressTriggered] = useState(false); - const timeout = useRef(null); - const target = useRef(null); - - const start = useCallback( - event => { - if (shouldPreventDefault && event.target) { - event.target.addEventListener('touchend', preventDefault, { - passive: false, - }); - target.current = event.target; - } - timeout.current = setTimeout(() => { - onLongPress(event); - console.log('onLongPress => ', event); - setLongPressTriggered(true); - }, delay); - }, - [onLongPress, delay, shouldPreventDefault] - ); - - const clear = useCallback( - (event, shouldTriggerClick = true) => { - timeout.current && clearTimeout(timeout.current); - shouldTriggerClick && !longPressTriggered && onClick(); - setLongPressTriggered(false); - if (shouldPreventDefault && target.current) { - target.current.removeEventListener('touchend', preventDefault); - } - }, - [shouldPreventDefault, onClick, longPressTriggered] - ); - - return { - onMouseDown: e => start(e), - onTouchStart: e => start(e), - onMouseUp: e => clear(e), - onMouseLeave: e => clear(e, false), - onTouchEnd: e => clear(e), - }; -}; - -const isTouchEvent = event => { - return 'touches' in event; -}; - -const preventDefault = event => { - if (!isTouchEvent(event)) return; - - if (event.touches.length < 2 && event.preventDefault) { - event.preventDefault(); - } -}; diff --git a/hooks/use-mobile.tsx b/hooks/use-mobile.tsx deleted file mode 100644 index 2b0fe1d..0000000 --- a/hooks/use-mobile.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import * as React from "react" - -const MOBILE_BREAKPOINT = 768 - -export function useIsMobile() { - const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined) - - React.useEffect(() => { - const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) - const onChange = () => { - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) - } - mql.addEventListener("change", onChange) - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) - return () => mql.removeEventListener("change", onChange) - }, []) - - return !!isMobile -} diff --git a/hooks/use-tablet.tsx b/hooks/use-tablet.tsx deleted file mode 100644 index a7db307..0000000 --- a/hooks/use-tablet.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import * as React from 'react'; - -const TABLET_BREAKPOINT = 1024; - -export function useIsTablet() { - const [isTablet, setIsTablet] = React.useState<boolean | undefined>(undefined); - - React.useEffect(() => { - const mql = window.matchMedia(`(max-width: ${TABLET_BREAKPOINT - 1}px)`); - const onChange = () => { - setIsTablet(window.innerWidth < TABLET_BREAKPOINT); - }; - mql.addEventListener('change', onChange); - setIsTablet(window.innerWidth < TABLET_BREAKPOINT); - return () => mql.removeEventListener('change', onChange); - }, []); - - return !!isTablet; -} diff --git a/hooks/use-theme.tsx b/hooks/use-theme.tsx deleted file mode 100644 index 54000c4..0000000 --- a/hooks/use-theme.tsx +++ /dev/null @@ -1,31 +0,0 @@ -'use client'; - -import switchTheme from '@/utils/switch-theme'; -import { createContext, useContext, useEffect, useState } from 'react'; - -import type { Dispatch, PropsWithChildren, SetStateAction } from 'react'; - -type Theme = 'dark' | 'light'; -type ThemeContext = [Theme, Dispatch<SetStateAction<Theme>>]; - -const themeContext = createContext<ThemeContext>(['dark', () => {}]); - -export const ThemeProvider = ({ children, defaultValue }: PropsWithChildren & { defaultValue?: Theme }) => { - const [theme, setTheme] = useState<Theme>(defaultValue ?? 'dark'); - - useEffect(() => { - const resolvedTheme = switchTheme(false); - setTheme(resolvedTheme); - }, []); - - return <themeContext.Provider value={[theme, setTheme]}>{children}</themeContext.Provider>; -}; - -export const useTheme = () => { - const context = useContext(themeContext); - - if (!context) { - throw new Error('useTheme must be used within a ThemeProvider.'); - } - return context; -}; diff --git a/hooks/use-toast.ts b/hooks/use-toast.ts deleted file mode 100644 index 2e5e38c..0000000 --- a/hooks/use-toast.ts +++ /dev/null @@ -1,189 +0,0 @@ -"use client"; - -// Inspired by react-hot-toast library -import * as React from "react"; - -import type { ToastActionElement, ToastProps } from "@/ui/toast"; - -const TOAST_LIMIT = 1; -const TOAST_REMOVE_DELAY = 1000000; - -type ToasterToast = ToastProps & { - id: string; - title?: React.ReactNode; - description?: React.ReactNode; - action?: ToastActionElement; -}; - -const actionTypes = { - ADD_TOAST: "ADD_TOAST", - UPDATE_TOAST: "UPDATE_TOAST", - DISMISS_TOAST: "DISMISS_TOAST", - REMOVE_TOAST: "REMOVE_TOAST", -} as const; - -let count = 0; - -function genId() { - count = (count + 1) % Number.MAX_SAFE_INTEGER; - return count.toString(); -} - -type ActionType = typeof actionTypes; - -type Action = - | { - type: ActionType["ADD_TOAST"]; - toast: ToasterToast; - } - | { - type: ActionType["UPDATE_TOAST"]; - toast: Partial<ToasterToast>; - } - | { - type: ActionType["DISMISS_TOAST"]; - toastId?: ToasterToast["id"]; - } - | { - type: ActionType["REMOVE_TOAST"]; - toastId?: ToasterToast["id"]; - }; - -interface State { - toasts: ToasterToast[]; -} - -const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>(); - -const addToRemoveQueue = (toastId: string) => { - if (toastTimeouts.has(toastId)) { - return; - } - - const timeout = setTimeout(() => { - toastTimeouts.delete(toastId); - dispatch({ - type: "REMOVE_TOAST", - toastId: toastId, - }); - }, TOAST_REMOVE_DELAY); - - toastTimeouts.set(toastId, timeout); -}; - -export const reducer = (state: State, action: Action): State => { - switch (action.type) { - case "ADD_TOAST": - return { - ...state, - toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), - }; - - case "UPDATE_TOAST": - return { - ...state, - toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)), - }; - - case "DISMISS_TOAST": { - const { toastId } = action; - - // ! Side effects ! - This could be extracted into a dismissToast() action, - // but I'll keep it here for simplicity - if (toastId) { - addToRemoveQueue(toastId); - } else { - state.toasts.forEach((toast) => { - addToRemoveQueue(toast.id); - }); - } - - return { - ...state, - toasts: state.toasts.map((t) => - t.id === toastId || toastId === undefined - ? { - ...t, - open: false, - } - : t - ), - }; - } - case "REMOVE_TOAST": - if (action.toastId === undefined) { - return { - ...state, - toasts: [], - }; - } - return { - ...state, - toasts: state.toasts.filter((t) => t.id !== action.toastId), - }; - } -}; - -const listeners: Array<(state: State) => void> = []; - -let memoryState: State = { toasts: [] }; - -function dispatch(action: Action) { - memoryState = reducer(memoryState, action); - listeners.forEach((listener) => { - listener(memoryState); - }); -} - -type Toast = Omit<ToasterToast, "id">; - -function toast({ ...props }: Toast) { - const id = genId(); - - const update = (props: ToasterToast) => - dispatch({ - type: "UPDATE_TOAST", - toast: { ...props, id }, - }); - const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }); - - dispatch({ - type: "ADD_TOAST", - toast: { - ...props, - id, - open: true, - onOpenChange: (open) => { - if (!open) dismiss(); - }, - }, - }); - - return { - id: id, - dismiss, - update, - }; -} - -function useToast() { - const [state, setState] = React.useState<State>(memoryState); - - React.useEffect(() => { - listeners.push(setState); - return () => { - const index = listeners.indexOf(setState); - if (index > -1) { - listeners.splice(index, 1); - } - }; - }, [state]); - - return { - ...state, - toast, - dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), - }; -} - -export { useToast, toast }; diff --git a/hooks/use-typewriter.ts b/hooks/use-typewriter.ts deleted file mode 100644 index 850eab6..0000000 --- a/hooks/use-typewriter.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useEffect, useState } from 'react'; - -const typing = async ({ text, callback }: { text: string; callback: (i: number) => any }, { delay, speed }: { delay: number; speed: number }) => { - await new Promise(r => setTimeout(r, delay)); - - let i = 1; - while (i < text.length) { - await new Promise(r => setTimeout(r, speed)); - i++; - callback(i); - } -}; - -export const useTypewriter = (text: string, delay = 0, speed = 2) => { - const [displayText, setDisplayText] = useState(''); - - useEffect(() => { - typing({ text, callback: i => setDisplayText(text.slice(0, i)) }, { delay, speed }); - }, [text, speed]); - - return displayText; -}; diff --git a/lib/cloudinary.ts b/lib/cloudinary.ts deleted file mode 100644 index 36eecd0..0000000 --- a/lib/cloudinary.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { v2 as cloudinary } from 'cloudinary'; - -if (!globalThis.cloudinary) { - cloudinary.config({ - cloud_name: process.env.CLOUDINARY_CLOUD_NAME, - api_key: process.env.CLOUDINARY_API_KEY, - api_secret: process.env.CLOUDINARY_API_SECRET, - }); - globalThis.cloudinary = cloudinary; -} - -export default globalThis.cloudinary; diff --git a/lib/db.ts b/lib/db.ts deleted file mode 100644 index edaed9c..0000000 --- a/lib/db.ts +++ /dev/null @@ -1,49 +0,0 @@ -'use server'; - -import mongoose from 'mongoose'; -import { env } from 'process'; - -globalThis.db = async ( - { - MONGODB_URI, - DBNAME, - newConnection, - }: { MONGODB_URI: string; DBNAME: string; newConnection?: boolean } = { - MONGODB_URI: env.MONGODB_URI!, - DBNAME: env.DBNAME!, - newConnection: false, - } -) => { - if (newConnection) return (await mongoose.connect(MONGODB_URI)).connection.useDb(DBNAME!); - - if (!globalThis.connection) { - // Create connection - globalThis.mongoose = await mongoose.connect(MONGODB_URI); - globalThis.connection = globalThis.mongoose.connection.useDb(DBNAME!); - - return globalThis.connection; - } - if ([0, 3, 99].some(state => state === globalThis.connection.readyState)) { - // First close all connections - await Promise.all( - globalThis.mongoose.connections.map( - async connection => await connection.close(true) - ) - ); - await globalThis.mongoose.disconnect(); - - // Then redefine the connection - globalThis.mongoose = await mongoose.connect(MONGODB_URI); - globalThis.connection = globalThis.mongoose.connection.useDb(DBNAME!); - - return globalThis.connection; - } - return globalThis.connection; -}; - -export default async (DBNAME?: string) => - await db({ - MONGODB_URI: env.MONGODB_URI!, - DBNAME: DBNAME ?? env.DBNAME!, - newConnection: DBNAME !== undefined, - }); diff --git a/lib/gemini.ts b/lib/gemini.ts deleted file mode 100644 index d4bb4d4..0000000 --- a/lib/gemini.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { env } from 'process'; -import { GoogleGenerativeAI } from '@google/generative-ai'; - -import type { GenerativeModel, ModelParams } from '@google/generative-ai'; - -const genAI = new GoogleGenerativeAI(env.GEMINI_API_KEY); - -const modelsParams: ModelParams[] = [ - { - model: 'gemini-1.5-flash', - generationConfig: { temperature: 0.75, maxOutputTokens: 1650 }, - }, - { - model: 'gemini-1.5-flash-8b', - generationConfig: { temperature: 0.63, maxOutputTokens: 1380 }, - }, - { model: 'gemini-1.5-pro', generationConfig: { temperature: 1.1, maxOutputTokens: 1085 } }, - { - model: 'gemini-2.0-flash', - generationConfig: { temperature: 0.875, maxOutputTokens: 975 }, - }, -]; - -const models = new Map( - modelsParams.map(({ model, ...params }, i) => [ - model, - genAI.getGenerativeModel({ - model, - ...params, - }), - ]) -); - -export default (model: string = 'gemini-1.5-flash'): GenerativeModel => models.get(model)!; diff --git a/next.config.ts b/next.config.ts index b832b5c..956aecc 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,43 +1,20 @@ import type { NextConfig } from 'next'; const nextConfig: NextConfig = { - // add all url available in your app - images: { - remotePatterns: [ - { - protocol: 'https', - hostname: '*', // Allow images from all domains - }, - ], - }, - eslint: { - ignoreDuringBuilds: true, - }, - typescript: { - ignoreBuildErrors: true, - }, - experimental: { - staleTimes: { - dynamic: 0, - }, - serverActions: { - bodySizeLimit: '20mb', - }, - }, + allowedDevOrigins: ['127.0.0.1'], + poweredByHeader: false, + reactStrictMode: true, + serverExternalPackages: ['@libsql/client', 'sharp'], async headers() { - return [ - { - source: '/:path*', // Match all routes - headers: [ - { - key: 'Cache-Control', - value: 'no-cache, must-revalidate, proxy-revalidate', - }, - { key: 'Pragma', value: 'no-cache' }, - { key: 'Expires', value: '0' }, - ], - }, - ]; + return [{ + source: '/:path*', + headers: [ + { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }, + { key: 'X-Content-Type-Options', value: 'nosniff' }, + { key: 'X-Frame-Options', value: 'DENY' }, + { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' }, + ], + }]; }, }; diff --git a/package.json b/package.json index 0413176..a8f9899 100644 --- a/package.json +++ b/package.json @@ -1,67 +1,65 @@ { - "name": "dashboard", - "version": "0.1.0", + "name": "autoblog-cms", + "version": "2.0.0", "private": true, + "description": "Workspace-isolated AI-assisted editorial CMS with immutable revisions.", + "packageManager": "bun@1.3.12", + "engines": { + "node": ">=20.9.0", + "bun": ">=1.3.12" + }, "scripts": { - "dev": "next dev", + "dev": "bun run db:setup && next dev", + "dev:e2e": "bun scripts/db/setup.ts --reset-demo && next dev", "build": "next build", "start": "next start", - "lint": "next lint" + "start:performance": "bun scripts/db/setup.ts --reset-demo && next start -p 3100", + "typecheck": "next typegen && tsc --noEmit --pretty false", + "lint": "eslint . --max-warnings 0", + "test": "vitest run --config vitest.config.ts", + "test:integration": "vitest run --config vitest.integration.config.ts", + "test:e2e": "playwright test tests/e2e --project=chromium", + "test:a11y": "playwright test tests/accessibility --project=chromium", + "test:visual": "playwright test --config=playwright.visual.config.ts --project=chromium", + "test:performance": "playwright test --config=playwright.performance.config.ts", + "test:lighthouse": "playwright test --config=playwright.lighthouse.config.ts", + "audit": "bun audit --production", + "security:secrets": "bun scripts/security/scan-secrets.ts", + "db:generate": "drizzle-kit generate", + "db:migrate": "bun scripts/db/migrate.ts", + "db:seed": "bun scripts/db/seed.ts", + "db:setup": "bun scripts/db/setup.ts", + "jobs:run": "bun scripts/jobs/run.ts" }, "dependencies": { - "@google/generative-ai": "^0.21.0", - "@radix-ui/react-alert-dialog": "^1.1.6", - "@radix-ui/react-avatar": "^1.1.3", - "@radix-ui/react-checkbox": "^1.1.4", - "@radix-ui/react-collapsible": "^1.1.2", - "@radix-ui/react-context-menu": "^2.2.4", - "@radix-ui/react-dialog": "^1.1.6", - "@radix-ui/react-dropdown-menu": "^2.1.6", - "@radix-ui/react-icons": "^1.3.2", - "@radix-ui/react-label": "^2.1.2", - "@radix-ui/react-popover": "^1.1.6", - "@radix-ui/react-radio-group": "^1.2.3", - "@radix-ui/react-scroll-area": "^1.2.3", - "@radix-ui/react-select": "^2.1.4", - "@radix-ui/react-separator": "^1.1.2", - "@radix-ui/react-slot": "^1.1.2", - "@radix-ui/react-switch": "^1.1.2", - "@radix-ui/react-tabs": "^1.1.3", - "@radix-ui/react-toast": "^1.2.4", - "@radix-ui/react-toggle": "^1.1.2", - "@radix-ui/react-toggle-group": "^1.1.2", - "@radix-ui/react-tooltip": "^1.1.6", - "@vercel/speed-insights": "^1.1.0", - "class-variance-authority": "^0.7.1", - "cloudinary": "^2.5.1", - "clsx": "^2.1.1", - "cmdk": "^1.0.4", - "date-fns": "^3.6.0", - "framer-motion": "^12.4.1", - "html2canvas": "^1.4.1", - "lucide-react": "^0.469.0", - "mongoose": "^8.9.3", - "next": "15.1.11", - "next-cloudinary": "^6.16.0", - "react": "^19.0.0", - "react-day-picker": "^8.10.1", - "react-dom": "^19.0.0", - "react-icons": "^5.4.0", - "react-loading-indicators": "^1.0.0", - "react-markdown": "^9.0.3", - "tailwind-merge": "^2.6.0", - "tailwindcss-animate": "^1.0.7", - "vaul": "^1.1.2" + "@google/genai": "2.13.0", + "@libsql/client": "0.17.4", + "better-auth": "1.6.23", + "drizzle-orm": "0.45.2", + "next": "16.2.11", + "react": "19.2.8", + "react-dom": "19.2.8", + "sharp": "0.35.3", + "zod": "4.4.3" }, "devDependencies": { - "@eslint/eslintrc": "^3", - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", - "eslint": "^9", - "eslint-config-next": "15.1.1", - "postcss": "^8", - "tailwindcss": "^3.4.1", - "typescript": "^5" + "@axe-core/playwright": "4.12.1", + "@playwright/test": "1.61.1", + "@types/bun": "1.3.14", + "@types/node": "24.10.13", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", + "drizzle-kit": "0.31.10", + "eslint": "9.39.5", + "eslint-config-next": "16.2.11", + "lighthouse": "13.4.1", + "typescript": "6.0.3", + "vitest": "4.1.10" + }, + "overrides": { + "esbuild": "0.28.1", + "picomatch": "4.0.5", + "postcss": "8.5.21", + "sharp": "0.35.3" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..157a485 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,37 @@ +import { defineConfig, devices } from '@playwright/test'; + +const testDatabaseUrl = process.env.PLAYWRIGHT_DATABASE_URL ?? `file:./data/e2e-${process.pid}.db`; + +export default defineConfig({ + testDir: './tests', + testMatch: '**/*.spec.ts', + testIgnore: ['**/visual/**', '**/performance/**'], + fullyParallel: false, + workers: 1, + forbidOnly: Boolean(process.env.CI), + retries: process.env.CI ? 2 : 0, + reporter: process.env.CI ? [['html', { open: 'never' }], ['github']] : 'list', + snapshotPathTemplate: '{testDir}/{testFilePath}-snapshots/{arg}{ext}', + use: { + baseURL: 'http://127.0.0.1:3000', + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + }, + projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }], + webServer: { + command: 'bun run dev:e2e', + url: 'http://127.0.0.1:3000', + reuseExistingServer: !process.env.CI, + timeout: 120_000, + env: { + DATABASE_URL: testDatabaseUrl, + BETTER_AUTH_SECRET: 'e2e-only-secret-with-at-least-32-characters', + NEXT_PUBLIC_APP_URL: 'http://127.0.0.1:3000', + DEMO_ENABLED: 'true', + AI_MODE: 'mock', + CRON_SECRET: 'e2e-only-distinct-cron-secret-0000000', + AUTH_SIGN_IN_RATE_LIMIT: '60', + }, + }, +}); diff --git a/playwright.lighthouse.config.ts b/playwright.lighthouse.config.ts new file mode 100644 index 0000000..6706a2d --- /dev/null +++ b/playwright.lighthouse.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests/lighthouse', + workers: 1, + fullyParallel: false, + retries: 0, + reporter: 'list', + timeout: 180_000, + webServer: { + command: 'bun run start:performance', + url: 'http://127.0.0.1:3100', + reuseExistingServer: false, + timeout: 120_000, + env: { + DATABASE_URL: 'file:./data/lighthouse.db', + BETTER_AUTH_SECRET: 'lighthouse-only-secret-with-32-characters', + NEXT_PUBLIC_APP_URL: 'http://127.0.0.1:3100', + DEMO_ENABLED: 'true', + AI_MODE: 'mock', + CRON_SECRET: 'lighthouse-only-cron-secret-000000', + AUTH_SIGN_IN_RATE_LIMIT: '30', + }, + }, +}); diff --git a/playwright.performance.config.ts b/playwright.performance.config.ts new file mode 100644 index 0000000..492cb70 --- /dev/null +++ b/playwright.performance.config.ts @@ -0,0 +1,24 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests/performance', + workers: 1, + fullyParallel: false, + retries: 0, + reporter: 'list', + use: { baseURL: 'http://127.0.0.1:3100', ...devices['Desktop Chrome'] }, + webServer: { + command: 'bun run start:performance', + url: 'http://127.0.0.1:3100', + reuseExistingServer: false, + timeout: 120_000, + env: { + DATABASE_URL: 'file:./data/performance.db', + BETTER_AUTH_SECRET: 'performance-only-secret-with-32-characters', + NEXT_PUBLIC_APP_URL: 'http://127.0.0.1:3100', + DEMO_ENABLED: 'true', AI_MODE: 'mock', + CRON_SECRET: 'performance-only-cron-secret-000000', + AUTH_SIGN_IN_RATE_LIMIT: '30', + }, + }, +}); diff --git a/playwright.visual.config.ts b/playwright.visual.config.ts new file mode 100644 index 0000000..abfb2b6 --- /dev/null +++ b/playwright.visual.config.ts @@ -0,0 +1,10 @@ +import base from './playwright.config'; +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + ...base, + testDir: './tests/visual', + testIgnore: [], + retries: 0, + workers: 1, +}); diff --git a/plugins/dynamic-size.ts b/plugins/dynamic-size.ts deleted file mode 100644 index 1e728a9..0000000 --- a/plugins/dynamic-size.ts +++ /dev/null @@ -1,30 +0,0 @@ -import plugin from 'tailwindcss/plugin'; - -export default plugin(({ matchUtilities, theme }) => { - const toRem = rem => `${rem}rem`; // Converts px to rem for min and max values - const dynamicClamp = (value, type) => { - if (!value) return value; - const min = parseFloat(value) * 0.8 + value.split(parseFloat(value).toString())[1]; // Minimum value in rem - const max = value; // Maximum value in rem - const preferred = type === 'width' ? `${parseFloat(value) * 1.6}vw` : `${parseFloat(value) * 1.6}vh`; // Preferred viewport-based value - - return `clamp(${min}, ${preferred}, ${max})`; - }; - - // Replaces default Tailwind sizing utilities with clamp-based variants - matchUtilities( - { - 'w-clamp': value => ({ - width: dynamicClamp(value, 'width'), - }), - 'h-clamp': value => ({ - height: dynamicClamp(value, 'height'), - }), - 'size-clamp': value => ({ - width: dynamicClamp(value, 'width'), - height: dynamicClamp(value, 'height'), - }), - }, - { values: theme('spacing'), type: 'any' }, - ); -}); diff --git a/postcss.config.mjs b/postcss.config.mjs deleted file mode 100644 index 1a69fd2..0000000 --- a/postcss.config.mjs +++ /dev/null @@ -1,8 +0,0 @@ -/** @type {import('postcss-load-config').Config} */ -const config = { - plugins: { - tailwindcss: {}, - }, -}; - -export default config; diff --git a/public/ai.png b/public/ai.png deleted file mode 100644 index 5157a3f..0000000 Binary files a/public/ai.png and /dev/null differ diff --git a/public/ai.svg b/public/ai.svg deleted file mode 100644 index 74cd7e6..0000000 --- a/public/ai.svg +++ /dev/null @@ -1,10 +0,0 @@ -<!-- sample rectangle --> -<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> - <radialGradient id="gradient" cx="0.5" cy="0.5" r="1" fx="0.65" fy="0.55" > - <stop offset="0%" stop-color="#7bfff7" style="stop-color: #7bfff7;"></stop> - <stop offset="31%" stop-color="#b1a7ed" style="stop-color: #b1a7ed;"></stop> - <stop offset="63%" stop-color="#6c54ff" style="stop-color: #6c54ff;"></stop> - - </radialGradient> - <path fill="url(#gradient)" fill-rule="evenodd" d="M9.322 4.272a.428.428 0 0 0-.428.429v13.975a2.428 2.428 0 1 1-4.856 0V8.585a.428.428 0 1 0-.856 0v4.398h-2V8.585a2.428 2.428 0 0 1 4.856 0v10.09a.428.428 0 1 0 .856 0V4.701a2.428 2.428 0 0 1 4.856 0v10.414a.428.428 0 0 0 .856 0v-5.09a2.25 2.25 0 0 1 4.5 0v4.766a.428.428 0 0 0 .856 0V6.643a2.428 2.428 0 1 1 4.856 0v6.016h-2V6.643a.428.428 0 0 0-.856 0v8.148a2.428 2.428 0 0 1-4.856 0v-4.767a.25.25 0 0 0-.5 0v5.091a2.428 2.428 0 0 1-4.856 0V4.701a.428.428 0 0 0-.428-.429Z" clip-rule="evenodd"></path> -</svg> \ No newline at end of file diff --git a/public/auth/bg.jpg b/public/auth/bg.jpg deleted file mode 100644 index de66b5b..0000000 Binary files a/public/auth/bg.jpg and /dev/null differ diff --git a/public/demo/architecture-design-01.jpg b/public/demo/architecture-design-01.jpg deleted file mode 100644 index 1435cb9..0000000 Binary files a/public/demo/architecture-design-01.jpg and /dev/null differ diff --git a/public/demo/board-culture.jpg b/public/demo/board-culture.jpg deleted file mode 100644 index de66b5b..0000000 Binary files a/public/demo/board-culture.jpg and /dev/null differ diff --git a/public/demo/community-engagement-01.jpg b/public/demo/community-engagement-01.jpg deleted file mode 100644 index 9a60410..0000000 Binary files a/public/demo/community-engagement-01.jpg and /dev/null differ diff --git a/public/demo/digital-innovation-01.webp b/public/demo/digital-innovation-01.webp deleted file mode 100644 index be5765f..0000000 Binary files a/public/demo/digital-innovation-01.webp and /dev/null differ diff --git a/public/demo/digital-interface-01.png b/public/demo/digital-interface-01.png deleted file mode 100644 index b4b7975..0000000 Binary files a/public/demo/digital-interface-01.png and /dev/null differ diff --git a/public/demo/digital-interface-02.png b/public/demo/digital-interface-02.png deleted file mode 100644 index 12b98c0..0000000 Binary files a/public/demo/digital-interface-02.png and /dev/null differ diff --git a/public/demo/fashion-exhibition-01.jpg b/public/demo/fashion-exhibition-01.jpg deleted file mode 100644 index 4bf796a..0000000 Binary files a/public/demo/fashion-exhibition-01.jpg and /dev/null differ diff --git a/public/demo/installation-art-01.jpg b/public/demo/installation-art-01.jpg deleted file mode 100644 index fea58e1..0000000 Binary files a/public/demo/installation-art-01.jpg and /dev/null differ diff --git a/public/file.svg b/public/file.svg deleted file mode 100644 index 004145c..0000000 --- a/public/file.svg +++ /dev/null @@ -1 +0,0 @@ -<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg> \ No newline at end of file diff --git a/public/globe.svg b/public/globe.svg deleted file mode 100644 index 567f17b..0000000 --- a/public/globe.svg +++ /dev/null @@ -1 +0,0 @@ -<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg> \ No newline at end of file diff --git a/public/iphone.png b/public/iphone.png deleted file mode 100644 index 6a65dce..0000000 Binary files a/public/iphone.png and /dev/null differ diff --git a/public/logo.png b/public/logo.png deleted file mode 100644 index d85fc9a..0000000 Binary files a/public/logo.png and /dev/null differ diff --git a/public/logo.svg b/public/logo.svg deleted file mode 100644 index f19b9ba..0000000 --- a/public/logo.svg +++ /dev/null @@ -1,5 +0,0 @@ - -<svg version="1.1" viewBox="0 0 2048 1064" width="3048" height="1064" xmlns="http://www.w3.org/2000/svg"> - -<path transform="translate(1939,47)" d="m0 0 1 2-9 14-8 10-11 12-17 17-17 13-13 8-23 12-26 10-28 9-15 4-23 5-19 3-16 1-6 1-14 1-5 1h-82l-27-1-53-8-26-6-9-3-4 11-8 29-10 46-10 56-7 39-13 76-13 75-8 49-11 52-9 36-10 35-9 27-11 30-6 14-13 29-8 16-11 20-22 33-8 11-12 14-11 12-24 24-16 12-15 9-16 8-22 10-16 6-21 5h-11l-4-3v-2l11-3 12-3 17-8 15-9 16-12 9-8 11-9 19-19 11-14 7-10 12-21 14-27 10-25 10-28 7-23 8-32 8-36 3-16 4-28 6-33 5-36 4-20 7-40 6-30 6-33 5-21 6-28 19-74 10-35 10-31 12-37 9-25 3-8-49-6-12-2-18-2-42-3-63 1-24 4-15 4-16 4-30 10-23 11-18 11-15 11v2l16 11 16 13 12 11 7 7 9 11 8 11 10 17 8 19 5 20 3 21v17l-4 26-6 20-10 19-9 12-11 12-10 9-18 12-16 9-24 10-21 7-35 9 3 2 21 7 31 15 14 9 16 12 10 9 11 11 10 13 10 16 9 19 7 22 4 20 2 18v28l-2 19-4 21-9 27-12 25-12 19-10 13-9 11-12 13-13 13-11 9-13 11-15 10-18 11-19 10-25 10-15 5-25 6-25 4-13 1h-38l-18-3-18-6-17-9-12-9v-3h7l24 9 26 6 8 1h16l18-2 25-6 21-8 17-9 11-7 16-12 11-10 8-7 7-7 7-8 11-13 14-19 13-20 12-22 10-22 8-24 6-26 3-20 1-10v-38l-4-26-5-17-8-18-8-13-8-10-9-10-11-10-14-10-18-10-13-6-37 6-22 2h-22l-14-2-8-5-3-4 1-6 5-4 11-3 21-2h14l29 3 20 4h6l18-8 17-9 15-10 10-8 13-12 12-14 10-15 8-14 9-23 5-19 2-14 1-15v-13l-2-20-4-17-6-16-11-21-9-12-7-8v-2h-2l-6-7-3-1-13 12-32 32-9 11-11 12-11 14-10 13-12 17-16 23-10 16-15 25-15 26-15 29-10 19-15 30-13 29-15 34-13 30-19 41-16 32-13 23-10 18-13 21-10 16-13 18-11 14-12 14-26 26-8 7-13 11-20 15-17 12-17 11-21 13-22 12-27 14-28 13-28 11-26 9-27 8-18 4-29 5-24 2h-32l-19-1-26-5-30-10-16-8-16-9-13-10-12-12-9-12-9-17-5-17v-41l3-8 2 1 5 25 3 20 6 18 8 14 8 10 12 12 13 9 18 10 15 6 15 5 24 5 19 2h42l26-3 26-5 28-8 21-8 25-12 17-9 18-12 18-13 11-9 14-12 10-9 17-17 7-8 11-12 9-11 12-15 12-17 21-32 14-23 12-19 13-22 17-28 15-25 38-64 9-15 17-28 24-38 12-17 11-16 13-18 11-15 16-21 22-28 14-17 9-11 13-15 24-28 8-8 7-8 14-14 1-2h2l2-4 12-12 8-7 7-7 7-8 9-10-5-5-25-15-22-11-26-10-31-9-38-7-37-4-25-2h-41l-43 3-34 4-34 6-27 6-28 8-30 10-27 11-26 12-27 14-25 15-25 17-19 14-16 13-11 9-14 12-16 15-29 29-7 8-12 13-9 11-13 16-21 28-17 26-12 20-15 29-8 19-8 22-7 27-3 18-1 10v42l3 19 4 15 7 16 8 14 11 14 8 8 13 10 13 8 16 7 18 6 25 4 11 1h20l31-3 5 3 3 7-4 2-30 4h-28l-27-4-23-6-21-9-14-8-12-9-14-12-12-13-12-17-9-17-7-19-4-13-4-22-2-22v-24l2-23 4-25 6-24 7-20 11-25 12-23 15-24 12-16 10-13 9-11 12-13 11-12 17-17 8-7 13-12 11-9 9-8 13-10 17-13 17-12 24-16 21-13 27-15 23-12 29-13 31-12 35-12 37-10 31-7 31-5 33-4 25-2 20-1h51l39 3 37 5 34 7 31 9 28 10 25 11 19 9 7 4 4-1 17-14 13-9 18-11 25-13 25-10 19-7 33-9 15-2 28-5 9-2 22-2 18-1 40-1h20l24 1 58 4 23 3 52 5 21 3 17 2 5 1 16 1 8 2 15 1 18 2 35 1h28l37-1 35-4 36-9 20-8 23-10 14-9 10-9 10-8 10-9 9-9z" fill="currentColor"/> -</svg> diff --git a/public/window.svg b/public/window.svg deleted file mode 100644 index b2b2a44..0000000 --- a/public/window.svg +++ /dev/null @@ -1 +0,0 @@ -<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg> \ No newline at end of file diff --git a/scripts/db/migrate.ts b/scripts/db/migrate.ts new file mode 100644 index 0000000..3b9c3fd --- /dev/null +++ b/scripts/db/migrate.ts @@ -0,0 +1,13 @@ +import { getEnvironment } from '@/src/platform/config/env'; +import { createDatabase } from '@/src/platform/db/client'; +import { migrateDatabase } from '@/src/platform/db/migrate'; + +const environment = getEnvironment(); +const database = createDatabase(environment.DATABASE_URL, environment.DATABASE_AUTH_TOKEN); + +try { + const applied = await migrateDatabase(database.client); + console.log(applied.length > 0 ? `Applied migrations: ${applied.join(', ')}` : 'Database schema is current.'); +} finally { + database.client.close(); +} diff --git a/scripts/db/seed.ts b/scripts/db/seed.ts new file mode 100644 index 0000000..8ff2462 --- /dev/null +++ b/scripts/db/seed.ts @@ -0,0 +1,15 @@ +import { getEnvironment } from '@/src/platform/config/env'; +import { createDatabase } from '@/src/platform/db/client'; +import { migrateDatabase } from '@/src/platform/db/migrate'; +import { seedDemo } from '@/src/platform/db/seed'; + +const environment = getEnvironment(); +const database = createDatabase(environment.DATABASE_URL, environment.DATABASE_AUTH_TOKEN); + +try { + await migrateDatabase(database.client); + await seedDemo(database); + console.log('Validated demo fixture is ready.'); +} finally { + database.client.close(); +} diff --git a/scripts/db/setup.ts b/scripts/db/setup.ts new file mode 100644 index 0000000..6850373 --- /dev/null +++ b/scripts/db/setup.ts @@ -0,0 +1,15 @@ +import { getEnvironment } from '@/src/platform/config/env'; +import { createDatabase } from '@/src/platform/db/client'; +import { migrateDatabase } from '@/src/platform/db/migrate'; +import { seedDemo } from '@/src/platform/db/seed'; + +const environment = getEnvironment(); +const database = createDatabase(environment.DATABASE_URL, environment.DATABASE_AUTH_TOKEN); + +try { + const applied = await migrateDatabase(database.client); + if (environment.DEMO_ENABLED) await seedDemo(database, { reset: process.argv.includes('--reset-demo'), requestId: 'setup-script' }); + console.log(`Database ready (${applied.length} migration${applied.length === 1 ? '' : 's'} applied).`); +} finally { + database.client.close(); +} diff --git a/scripts/jobs/run.ts b/scripts/jobs/run.ts new file mode 100644 index 0000000..9c60693 --- /dev/null +++ b/scripts/jobs/run.ts @@ -0,0 +1,10 @@ +import { DrizzleEditorialRepository } from '@/src/modules/editorial/drizzle-repository'; +import { MediaCleanupWorker } from '@/src/modules/media/cleanup-worker'; +import { DatabaseMediaProvider } from '@/src/modules/media/database-provider'; +import { getDatabase } from '@/src/platform/db/client'; + +const repository = new DrizzleEditorialRepository(getDatabase()); +const publication = await repository.runDuePublicationJobs(new Date(), 50); +const media = await new MediaCleanupWorker(getDatabase(), new DatabaseMediaProvider(getDatabase())).run(new Date(), 50); +process.stdout.write(`${JSON.stringify({ publication, media })}\n`); +process.exitCode = publication.failed > 0 || media.failed > 0 ? 1 : 0; diff --git a/scripts/security/scan-secrets.ts b/scripts/security/scan-secrets.ts new file mode 100644 index 0000000..0b37423 --- /dev/null +++ b/scripts/security/scan-secrets.ts @@ -0,0 +1,33 @@ +export {}; + +const patterns = [ + '-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----', + 'AIza[0-9A-Za-z_-]{30,}', + 'gh[pousr]_[0-9A-Za-z]{30,}', + 'sk-[0-9A-Za-z]{24,}', + 'mongodb(\\+srv)?://[^[:space:]<>]+', +].join('|'); + +const child = Bun.spawn(['git', 'grep', '-nI', '-E', '-e', patterns, '--', ':!bun.lock'], { + stdout: 'pipe', + stderr: 'pipe', +}); +const output = await new Response(child.stdout).text(); +const errorOutput = await new Response(child.stderr).text(); +const exitCode = await child.exited; + +if (exitCode === 0) { + const locations = output.trim().split(/\r?\n/u).map((line) => { + const match = /^([^:]+):(\d+):/u.exec(line); + return match ? `${match[1]}:${match[2]}` : 'tracked file (location withheld)'; + }); + console.error(`Potential secret material found at:\n${locations.join('\n')}`); + globalThis.process.exitCode = 1; +} + +if (exitCode !== 1) { + console.error(errorOutput.trim() || 'Secret scan could not inspect tracked files.'); + globalThis.process.exitCode = exitCode; +} + +if (exitCode === 1) console.log('Current-tree secret scan passed.'); diff --git a/src/modules/ai/adapter.ts b/src/modules/ai/adapter.ts new file mode 100644 index 0000000..2958d32 --- /dev/null +++ b/src/modules/ai/adapter.ts @@ -0,0 +1,6 @@ +import type { AIAdapterResult, AISuggestionInput } from '@/src/modules/ai/domain'; + +export interface AIAdapter { + readonly mode: 'mock' | 'gemini'; + suggest(input: AISuggestionInput, signal: AbortSignal): Promise<AIAdapterResult>; +} diff --git a/src/modules/ai/domain.ts b/src/modules/ai/domain.ts new file mode 100644 index 0000000..cbb7848 --- /dev/null +++ b/src/modules/ai/domain.ts @@ -0,0 +1,38 @@ +import { z } from 'zod'; + +export const AI_MAX_INPUT_CONTENT = 20_000; +export const AI_MAX_OUTPUT_CHARACTERS = 24_000; + +export const aiSuggestionInputSchema = z.object({ + postId: z.string().min(1).max(120), + title: z.string().trim().min(3).max(180), + excerpt: z.string().trim().max(320), + content: z.string().max(AI_MAX_INPUT_CONTENT), + instruction: z.string().trim().min(3).max(500), +}); +export type AISuggestionInput = z.infer<typeof aiSuggestionInputSchema>; + +export const aiSuggestionSchema = z.object({ + title: z.string().trim().min(3).max(180), + excerpt: z.string().trim().max(320), + content: z.string().max(AI_MAX_INPUT_CONTENT), + rationale: z.string().trim().min(3).max(500), +}); +export type AISuggestion = z.infer<typeof aiSuggestionSchema>; + +export type AIAdapterResult = Readonly<{ + suggestion: AISuggestion; + provider: string; + model: string; + inputTokens?: number; + outputTokens?: number; +}>; + +export type AISuggestionResult = Readonly<{ + suggestion: AISuggestion; + mode: 'mock' | 'gemini'; + provider: string; + model: string; + latencyMs: number; + remainingCharacters: number; +}>; diff --git a/src/modules/ai/drizzle-repository.ts b/src/modules/ai/drizzle-repository.ts new file mode 100644 index 0000000..611eaa1 --- /dev/null +++ b/src/modules/ai/drizzle-repository.ts @@ -0,0 +1,70 @@ +import { and, eq, sql } from 'drizzle-orm'; + +import type { AIRepository } from '@/src/modules/ai/repository'; +import type { DatabaseContext } from '@/src/platform/db/client'; +import { aiQuotaWindows, aiUsage, auditEvents, rateLimits } from '@/src/platform/db/schema'; +import { AppError } from '@/src/platform/observability/errors'; + +function monthStart(now: Date): Date { + return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)); +} + +export class DrizzleAIRepository implements AIRepository { + constructor(private readonly database: DatabaseContext) {} + + async consumeRateLimit(key: string, limit: number, windowMs: number, now: Date): Promise<void> { + const windowStartedAt = new Date(Math.floor(now.getTime() / windowMs) * windowMs); + const expiresAt = new Date(windowStartedAt.getTime() + windowMs); + await this.database.db.transaction(async (transaction) => { + const inserted = await transaction.insert(rateLimits).values({ key, windowStartedAt, count: 1, expiresAt }).onConflictDoNothing(); + if (inserted.rowsAffected === 1) return; + const updated = await transaction.update(rateLimits).set({ count: sql`${rateLimits.count} + 1` }) + .where(and(eq(rateLimits.key, key), eq(rateLimits.windowStartedAt, windowStartedAt), sql`${rateLimits.count} < ${limit}`)); + if (updated.rowsAffected !== 1) throw new AppError('RATE_LIMITED', { retryAfterSeconds: Math.ceil((expiresAt.getTime() - now.getTime()) / 1000) }); + }); + } + + async reserveQuota(workspaceId: string, amount: number, limit: number, now: Date): Promise<Readonly<{ windowStartedAt: Date }>> { + const windowStartedAt = monthStart(now); + await this.database.db.transaction(async (transaction) => { + await transaction.insert(aiQuotaWindows).values({ workspaceId, windowStartedAt, reservedCharacters: 0, usedCharacters: 0, updatedAt: now }).onConflictDoNothing(); + const updated = await transaction.update(aiQuotaWindows).set({ reservedCharacters: sql`${aiQuotaWindows.reservedCharacters} + ${amount}`, updatedAt: now }) + .where(and(eq(aiQuotaWindows.workspaceId, workspaceId), eq(aiQuotaWindows.windowStartedAt, windowStartedAt), sql`${aiQuotaWindows.reservedCharacters} + ${aiQuotaWindows.usedCharacters} + ${amount} <= ${limit}`)); + if (updated.rowsAffected !== 1) throw new AppError('QUOTA_EXCEEDED'); + }); + return { windowStartedAt }; + } + + async releaseQuota(workspaceId: string, windowStartedAt: Date, amount: number, now: Date): Promise<void> { + await this.database.db.update(aiQuotaWindows).set({ reservedCharacters: sql`max(0, ${aiQuotaWindows.reservedCharacters} - ${amount})`, updatedAt: now }) + .where(and(eq(aiQuotaWindows.workspaceId, workspaceId), eq(aiQuotaWindows.windowStartedAt, windowStartedAt))); + } + + async complete(command: Parameters<AIRepository['complete']>[0]): Promise<number> { + const inputCharacters = command.input.title.length + command.input.excerpt.length + command.input.content.length + command.input.instruction.length; + const outputCharacters = command.adapter.suggestion.title.length + command.adapter.suggestion.excerpt.length + command.adapter.suggestion.content.length + command.adapter.suggestion.rationale.length; + const actual = inputCharacters + outputCharacters; + return this.database.db.transaction(async (transaction) => { + await transaction.update(aiQuotaWindows).set({ + reservedCharacters: sql`max(0, ${aiQuotaWindows.reservedCharacters} - ${command.reservedCharacters})`, + usedCharacters: sql`${aiQuotaWindows.usedCharacters} + ${actual}`, updatedAt: command.now, + }).where(and(eq(aiQuotaWindows.workspaceId, command.workspaceId), eq(aiQuotaWindows.windowStartedAt, command.windowStartedAt))); + await transaction.insert(aiUsage).values({ + id: crypto.randomUUID(), workspaceId: command.workspaceId, userId: command.userId, + mode: command.mode, provider: command.adapter.provider, model: command.adapter.model, + latencyMs: command.latencyMs, inputCharacters, outputCharacters, + inputTokens: command.adapter.inputTokens, outputTokens: command.adapter.outputTokens, createdAt: command.now, + }); + await transaction.insert(auditEvents).values({ + id: crypto.randomUUID(), workspaceId: command.workspaceId, actorId: command.userId, + action: 'ai.suggested', targetType: 'post', targetId: command.input.postId, + requestId: command.requestId, + metadata: { mode: command.mode, provider: command.adapter.provider, model: command.adapter.model, inputCharacters, outputCharacters }, + createdAt: command.now, + }); + const [window] = await transaction.select({ used: aiQuotaWindows.usedCharacters, reserved: aiQuotaWindows.reservedCharacters }).from(aiQuotaWindows) + .where(and(eq(aiQuotaWindows.workspaceId, command.workspaceId), eq(aiQuotaWindows.windowStartedAt, command.windowStartedAt))).limit(1); + return Math.max(0, command.quotaLimit - (window?.used ?? command.quotaLimit) - (window?.reserved ?? 0)); + }); + } +} diff --git a/src/modules/ai/gemini-adapter.ts b/src/modules/ai/gemini-adapter.ts new file mode 100644 index 0000000..bf9b63b --- /dev/null +++ b/src/modules/ai/gemini-adapter.ts @@ -0,0 +1,71 @@ +import { GoogleGenAI } from '@google/genai'; + +import type { AIAdapter } from '@/src/modules/ai/adapter'; +import { aiSuggestionSchema, type AIAdapterResult, type AISuggestionInput } from '@/src/modules/ai/domain'; +import { AppError } from '@/src/platform/observability/errors'; + +type GeminiRawResult = Readonly<{ text?: string; inputTokens?: number; outputTokens?: number }>; +type GeminiGenerate = (input: AISuggestionInput, signal: AbortSignal) => Promise<GeminiRawResult>; + +const responseJsonSchema = { + type: 'object', + additionalProperties: false, + required: ['title', 'excerpt', 'content', 'rationale'], + properties: { + title: { type: 'string', maxLength: 180 }, + excerpt: { type: 'string', maxLength: 320 }, + content: { type: 'string', maxLength: 20_000 }, + rationale: { type: 'string', maxLength: 500 }, + }, +} as const; + +export class GeminiAIAdapter implements AIAdapter { + readonly mode = 'gemini' as const; + private readonly generate: GeminiGenerate; + + constructor(apiKey: string, private readonly model: string, generate?: GeminiGenerate) { + this.generate = generate ?? this.createGenerator(apiKey); + } + + async suggest(input: AISuggestionInput, signal: AbortSignal): Promise<AIAdapterResult> { + try { + const result = await this.generate(input, signal); + if (!result.text) throw new AppError('PROVIDER_UNAVAILABLE'); + const suggestion = aiSuggestionSchema.parse(JSON.parse(result.text)); + return { + suggestion, provider: 'google-gemini', model: this.model, + ...(result.inputTokens === undefined ? {} : { inputTokens: result.inputTokens }), + ...(result.outputTokens === undefined ? {} : { outputTokens: result.outputTokens }), + }; + } catch (error) { + if (error instanceof AppError) throw error; + throw new AppError('PROVIDER_UNAVAILABLE', undefined, error); + } + } + + private createGenerator(apiKey: string): GeminiGenerate { + const client = new GoogleGenAI({ apiKey }); + return async (input, signal) => { + const response = await client.models.generateContent({ + model: this.model, + contents: JSON.stringify({ + task: 'Return an editorial suggestion. Preserve factual meaning; do not claim external verification.', + instruction: input.instruction, + draft: { title: input.title, excerpt: input.excerpt, content: input.content }, + }), + config: { + abortSignal: signal, + maxOutputTokens: 2_048, + temperature: 0.2, + responseMimeType: 'application/json', + responseJsonSchema, + }, + }); + return { + ...(response.text === undefined ? {} : { text: response.text }), + ...(response.usageMetadata?.promptTokenCount === undefined ? {} : { inputTokens: response.usageMetadata.promptTokenCount }), + ...(response.usageMetadata?.candidatesTokenCount === undefined ? {} : { outputTokens: response.usageMetadata.candidatesTokenCount }), + }; + }; + } +} diff --git a/src/modules/ai/mock-adapter.ts b/src/modules/ai/mock-adapter.ts new file mode 100644 index 0000000..11d4c1a --- /dev/null +++ b/src/modules/ai/mock-adapter.ts @@ -0,0 +1,23 @@ +import type { AIAdapter } from '@/src/modules/ai/adapter'; +import { aiSuggestionSchema, type AIAdapterResult, type AISuggestionInput } from '@/src/modules/ai/domain'; + +export class MockAIAdapter implements AIAdapter { + readonly mode = 'mock' as const; + + async suggest(input: AISuggestionInput, signal: AbortSignal): Promise<AIAdapterResult> { + signal.throwIfAborted(); + const title = input.title.includes(':') ? input.title : `${input.title}: editorial proof`.slice(0, 180); + const excerpt = input.excerpt || `A concise editorial angle shaped around: ${input.instruction}`.slice(0, 320); + const content = input.content.trim() + ? `${input.content.trim()}\n\n## Editorial next step\n\n${input.instruction}`.slice(0, 20_000) + : `## Editorial brief\n\n${input.instruction}`; + return { + suggestion: aiSuggestionSchema.parse({ + title, excerpt, content, + rationale: 'Deterministic demo adapter: structure is reproducible and no external provider was called.', + }), + provider: 'deterministic-mock', + model: 'editorial-suggestion-v1', + }; + } +} diff --git a/src/modules/ai/repository.ts b/src/modules/ai/repository.ts new file mode 100644 index 0000000..b0acb82 --- /dev/null +++ b/src/modules/ai/repository.ts @@ -0,0 +1,12 @@ +import type { AIAdapterResult, AISuggestionInput } from '@/src/modules/ai/domain'; + +export interface AIRepository { + consumeRateLimit(key: string, limit: number, windowMs: number, now: Date): Promise<void>; + reserveQuota(workspaceId: string, amount: number, limit: number, now: Date): Promise<Readonly<{ windowStartedAt: Date }>>; + releaseQuota(workspaceId: string, windowStartedAt: Date, amount: number, now: Date): Promise<void>; + complete(command: Readonly<{ + workspaceId: string; userId: string; requestId: string; mode: 'mock' | 'gemini'; + input: AISuggestionInput; adapter: AIAdapterResult; latencyMs: number; + windowStartedAt: Date; reservedCharacters: number; quotaLimit: number; now: Date; + }>): Promise<number>; +} diff --git a/src/modules/ai/service.ts b/src/modules/ai/service.ts new file mode 100644 index 0000000..1a3c003 --- /dev/null +++ b/src/modules/ai/service.ts @@ -0,0 +1,81 @@ +import type { AIAdapter } from '@/src/modules/ai/adapter'; +import { AI_MAX_OUTPUT_CHARACTERS, aiSuggestionInputSchema, type AISuggestionResult } from '@/src/modules/ai/domain'; +import { DrizzleAIRepository } from '@/src/modules/ai/drizzle-repository'; +import { GeminiAIAdapter } from '@/src/modules/ai/gemini-adapter'; +import { MockAIAdapter } from '@/src/modules/ai/mock-adapter'; +import type { AIRepository } from '@/src/modules/ai/repository'; +import { DrizzleEditorialRepository } from '@/src/modules/editorial/drizzle-repository'; +import type { PostDetail } from '@/src/modules/editorial/domain'; +import type { MembershipContext } from '@/src/modules/identity/domain'; +import { authorize } from '@/src/modules/identity/policy'; +import { getEnvironment } from '@/src/platform/config/env'; +import { getDatabase } from '@/src/platform/db/client'; +import { AppError } from '@/src/platform/observability/errors'; + +const AI_RATE_LIMIT = 5; +const AI_RATE_WINDOW_MS = 60_000; +const AI_TIMEOUT_MS = 8_000; + +type PostReader = (workspaceId: string, postId: string) => Promise<PostDetail | null>; + +export class AIService { + constructor( + private readonly repository: AIRepository, + private readonly adapter: AIAdapter, + private readonly quotaLimit: number, + private readonly readPost: PostReader, + private readonly timeoutMs = AI_TIMEOUT_MS, + ) {} + + async suggest(context: MembershipContext, rawInput: unknown, requestId: string): Promise<AISuggestionResult> { + authorize(context.role, 'ai.suggest'); + const input = aiSuggestionInputSchema.parse(rawInput); + if (!await this.readPost(context.workspaceId, input.postId)) throw new AppError('NOT_FOUND'); + const now = new Date(); + await this.repository.consumeRateLimit(`ai:${context.workspaceId}:${context.userId}`, AI_RATE_LIMIT, AI_RATE_WINDOW_MS, now); + const inputCharacters = input.title.length + input.excerpt.length + input.content.length + input.instruction.length; + const reservedCharacters = inputCharacters + AI_MAX_OUTPUT_CHARACTERS; + const reservation = await this.repository.reserveQuota(context.workspaceId, reservedCharacters, this.quotaLimit, now); + const controller = new AbortController(); + let timeout: ReturnType<typeof setTimeout> | undefined; + const startedAt = performance.now(); + try { + const adapter = await Promise.race([ + this.adapter.suggest(input, controller.signal), + new Promise<never>((_resolve, reject) => { + timeout = setTimeout(() => { controller.abort(); reject(new AppError('PROVIDER_UNAVAILABLE')); }, this.timeoutMs); + }), + ]); + const latencyMs = Math.max(0, Math.round(performance.now() - startedAt)); + const remainingCharacters = await this.repository.complete({ + workspaceId: context.workspaceId, userId: context.userId, requestId, mode: this.adapter.mode, + input, adapter, latencyMs, windowStartedAt: reservation.windowStartedAt, + reservedCharacters, quotaLimit: this.quotaLimit, now: new Date(), + }); + return { suggestion: adapter.suggestion, mode: this.adapter.mode, provider: adapter.provider, model: adapter.model, latencyMs, remainingCharacters }; + } catch (error) { + await this.repository.releaseQuota(context.workspaceId, reservation.windowStartedAt, reservedCharacters, new Date()); + if (error instanceof AppError) throw error; + throw new AppError('PROVIDER_UNAVAILABLE', undefined, error); + } finally { + if (timeout) clearTimeout(timeout); + } + } +} + +let aiService: AIService | undefined; + +export function getAIService(): AIService { + if (!aiService) { + const database = getDatabase(); + const environment = getEnvironment(); + const adapter: AIAdapter = environment.AI_MODE === 'gemini' + ? environment.GEMINI_API_KEY + ? new GeminiAIAdapter(environment.GEMINI_API_KEY, environment.GEMINI_MODEL) + : (() => { throw new AppError('PROVIDER_UNAVAILABLE'); })() + : new MockAIAdapter(); + const editorial = new DrizzleEditorialRepository(database); + aiService = new AIService(new DrizzleAIRepository(database), adapter, environment.AI_MONTHLY_CHARACTER_QUOTA, (workspaceId, postId) => editorial.find(workspaceId, postId)); + } + return aiService; +} diff --git a/src/modules/editorial/domain.ts b/src/modules/editorial/domain.ts new file mode 100644 index 0000000..1e38b7b --- /dev/null +++ b/src/modules/editorial/domain.ts @@ -0,0 +1,85 @@ +import { z } from 'zod'; +import { transitionActionValues } from '@/src/modules/editorial/workflow'; + +export const postStateSchema = z.enum(['Draft', 'InReview', 'ChangesRequested', 'Approved', 'Scheduled', 'Published', 'Archived']); +export type PostState = z.infer<typeof postStateSchema>; + +const titleSchema = z.string().trim().min(3).max(180); +const excerptSchema = z.string().trim().max(320); +const contentSchema = z.string().max(100_000); + +export const createPostSchema = z.object({ + title: titleSchema, + excerpt: excerptSchema.default(''), + content: contentSchema.default(''), +}); +export type CreatePostInput = z.infer<typeof createPostSchema>; + +export const savePostSchema = z.object({ + expectedVersion: z.number().int().positive(), + title: titleSchema, + excerpt: excerptSchema, + content: contentSchema, +}); +export type SavePostInput = z.infer<typeof savePostSchema>; + +export const transitionPostSchema = z.object({ + expectedVersion: z.number().int().positive(), + action: z.enum(transitionActionValues), + scheduledFor: z.iso.datetime().optional(), + idempotencyKey: z.string().trim().min(8).max(120).optional(), +}).superRefine((input, context) => { + if (input.action === 'schedule' && !input.scheduledFor) context.addIssue({ code: 'custom', path: ['scheduledFor'], message: 'Scheduling requires a date.' }); + if ((input.action === 'schedule' || input.action === 'publish') && !input.idempotencyKey) context.addIssue({ code: 'custom', path: ['idempotencyKey'], message: 'Publication requires an idempotency key.' }); +}); +export type TransitionPostInput = z.infer<typeof transitionPostSchema>; + +export const restoreRevisionSchema = z.object({ + expectedVersion: z.number().int().positive(), + revisionId: z.string().min(1).max(120), +}); +export type RestoreRevisionInput = z.infer<typeof restoreRevisionSchema>; + +export type PostSummary = Readonly<{ + id: string; + slug: string; + title: string; + excerpt: string; + state: PostState; + version: number; + authorId: string; + updatedAt: Date; +}>; + +export type PostDetail = PostSummary & Readonly<{ + content: string; + draftRevisionId: string; + publishedRevisionId: string | null; + createdAt: Date; + scheduledFor: Date | null; + publishedAt: Date | null; +}>; + +export type RevisionDetail = Readonly<{ + id: string; + postId: string; + version: number; + title: string; + excerpt: string; + content: string; + authorId: string; + restoredFromRevisionId: string | null; + createdAt: Date; +}>; + +export type PublicPost = Readonly<{ + workspaceName: string; + slug: string; + title: string; + excerpt: string; + content: string; + revisionId: string; + publishedAt: Date; +}>; + +export type JobRunResult = Readonly<{ claimed: number; completed: number; failed: number }>; diff --git a/src/modules/editorial/drizzle-repository.ts b/src/modules/editorial/drizzle-repository.ts new file mode 100644 index 0000000..2371df9 --- /dev/null +++ b/src/modules/editorial/drizzle-repository.ts @@ -0,0 +1,400 @@ +import { and, asc, desc, eq, lt, lte, or, sql } from 'drizzle-orm'; +import { z } from 'zod'; + +import { postStateSchema, type PostDetail, type PostSummary, type PublicPost, type RevisionDetail } from '@/src/modules/editorial/domain'; +import type { EditorialRepository } from '@/src/modules/editorial/repository'; +import { resolveTransition } from '@/src/modules/editorial/workflow'; +import type { DatabaseContext } from '@/src/platform/db/client'; +import { auditEvents, jobs, posts, publications, revisions, workspaces } from '@/src/platform/db/schema'; +import { AppError } from '@/src/platform/observability/errors'; + +type PostRow = typeof posts.$inferSelect; +type RevisionRow = typeof revisions.$inferSelect; + +const publishJobPayloadSchema = z.object({ + postId: z.string().min(1), + publicationId: z.string().min(1), + revisionId: z.string().min(1), +}); + +function slugify(title: string): string { + const slug = title.toLowerCase().normalize('NFKD').replace(/[\u0300-\u036f]/gu, '').replace(/[^a-z0-9]+/gu, '-').replace(/^-|-$/gu, '').slice(0, 72); + return slug || `post-${crypto.randomUUID().slice(0, 8)}`; +} + +function toSummary(row: PostRow): PostSummary { + return { + id: row.id, + slug: row.slug, + title: row.title, + excerpt: row.excerpt, + state: postStateSchema.parse(row.state), + version: row.version, + authorId: row.authorId, + updatedAt: row.updatedAt, + }; +} + +function toRevision(row: RevisionRow): RevisionDetail { + return { + id: row.id, + postId: row.postId, + version: row.version, + title: row.title, + excerpt: row.excerpt, + content: row.content, + authorId: row.authorId, + restoredFromRevisionId: row.restoredFromRevisionId, + createdAt: row.createdAt, + }; +} + +function isRevisionVersionConflict(error: unknown): boolean { + let current = error; + for (let depth = 0; depth < 4 && current instanceof Error; depth += 1) { + if (/revision_post_version_unique|UNIQUE constraint failed: revisions\.post_id, revisions\.version/iu.test(current.message)) return true; + current = current.cause; + } + return false; +} + +export class DrizzleEditorialRepository implements EditorialRepository { + constructor(private readonly database: DatabaseContext) {} + + async list(workspaceId: string): Promise<PostSummary[]> { + const rows = await this.database.db.select().from(posts).where(eq(posts.workspaceId, workspaceId)).orderBy(desc(posts.updatedAt)); + return rows.map(toSummary); + } + + async find(workspaceId: string, postId: string): Promise<PostDetail | null> { + const [row] = await this.database.db.select({ post: posts, content: revisions.content }) + .from(posts) + .innerJoin(revisions, and(eq(revisions.id, posts.draftRevisionId), eq(revisions.workspaceId, posts.workspaceId))) + .where(and(eq(posts.workspaceId, workspaceId), eq(posts.id, postId))) + .limit(1); + if (!row || !row.post.draftRevisionId) return null; + return { + ...toSummary(row.post), + content: row.content, + draftRevisionId: row.post.draftRevisionId, + publishedRevisionId: row.post.publishedRevisionId, + createdAt: row.post.createdAt, + scheduledFor: row.post.scheduledFor, + publishedAt: row.post.publishedAt, + }; + } + + async create(command: Parameters<EditorialRepository['create']>[0]): Promise<PostDetail> { + const postId = crypto.randomUUID(); + const revisionId = crypto.randomUUID(); + const now = new Date(); + const baseSlug = slugify(command.input.title); + const slug = `${baseSlug}-${postId.slice(0, 6)}`; + + await this.database.db.transaction(async (transaction) => { + await transaction.insert(posts).values({ + id: postId, + workspaceId: command.workspaceId, + slug, + title: command.input.title, + excerpt: command.input.excerpt, + state: 'Draft', + version: 1, + authorId: command.actorId, + createdAt: now, + updatedAt: now, + }); + await transaction.insert(revisions).values({ + id: revisionId, + workspaceId: command.workspaceId, + postId, + version: 1, + title: command.input.title, + excerpt: command.input.excerpt, + content: command.input.content, + authorId: command.actorId, + createdAt: now, + }); + await transaction.update(posts).set({ draftRevisionId: revisionId }).where(and(eq(posts.workspaceId, command.workspaceId), eq(posts.id, postId))); + await transaction.insert(auditEvents).values({ + id: crypto.randomUUID(), workspaceId: command.workspaceId, actorId: command.actorId, + action: 'post.created', targetType: 'post', targetId: postId, requestId: command.requestId, + metadata: { version: 1 }, createdAt: now, + }); + }); + + const created = await this.find(command.workspaceId, postId); + if (!created) throw new AppError('INTERNAL_FAILURE'); + return created; + } + + async save(command: Parameters<EditorialRepository['save']>[0]): Promise<PostDetail> { + const revisionId = crypto.randomUUID(); + const newVersion = command.input.expectedVersion + 1; + const now = new Date(); + + try { + await this.database.db.transaction(async (transaction) => { + await transaction.insert(revisions).values({ + id: revisionId, + workspaceId: command.workspaceId, + postId: command.postId, + version: newVersion, + title: command.input.title, + excerpt: command.input.excerpt, + content: command.input.content, + authorId: command.actorId, + createdAt: now, + }); + const result = await transaction.update(posts).set({ + title: command.input.title, + excerpt: command.input.excerpt, + state: sql`case when ${posts.state} = 'Published' then 'Draft' else ${posts.state} end`, + version: newVersion, + draftRevisionId: revisionId, + updatedAt: now, + }).where(and(eq(posts.workspaceId, command.workspaceId), eq(posts.id, command.postId), eq(posts.version, command.input.expectedVersion))); + if (result.rowsAffected !== 1) throw new AppError('VERSION_CONFLICT', { expectedVersion: command.input.expectedVersion }); + await transaction.insert(auditEvents).values({ + id: crypto.randomUUID(), workspaceId: command.workspaceId, actorId: command.actorId, + action: 'post.saved', targetType: 'post', targetId: command.postId, requestId: command.requestId, + metadata: { version: newVersion }, createdAt: now, + }); + }); + } catch (error) { + if (error instanceof AppError) throw error; + if (isRevisionVersionConflict(error)) { + throw new AppError('VERSION_CONFLICT', { expectedVersion: command.input.expectedVersion }); + } + throw error; + } + + const saved = await this.find(command.workspaceId, command.postId); + if (!saved) throw new AppError('INTERNAL_FAILURE'); + return saved; + } + + async listRevisions(workspaceId: string, postId: string): Promise<RevisionDetail[]> { + const rows = await this.database.db.select().from(revisions) + .where(and(eq(revisions.workspaceId, workspaceId), eq(revisions.postId, postId))) + .orderBy(desc(revisions.version)); + return rows.map(toRevision); + } + + async restore(command: Parameters<EditorialRepository['restore']>[0]): Promise<PostDetail> { + const [target] = await this.database.db.select().from(revisions) + .where(and(eq(revisions.workspaceId, command.workspaceId), eq(revisions.postId, command.postId), eq(revisions.id, command.input.revisionId))) + .limit(1); + if (!target) throw new AppError('NOT_FOUND'); + const revisionId = crypto.randomUUID(); + const newVersion = command.input.expectedVersion + 1; + const now = new Date(); + + try { + await this.database.db.transaction(async (transaction) => { + await transaction.insert(revisions).values({ + id: revisionId, workspaceId: command.workspaceId, postId: command.postId, + version: newVersion, title: target.title, excerpt: target.excerpt, content: target.content, + authorId: command.actorId, restoredFromRevisionId: target.id, createdAt: now, + }); + const result = await transaction.update(posts).set({ + title: target.title, excerpt: target.excerpt, draftRevisionId: revisionId, + state: sql`case when ${posts.state} = 'Published' then 'Draft' else ${posts.state} end`, + version: newVersion, updatedAt: now, + }).where(and(eq(posts.workspaceId, command.workspaceId), eq(posts.id, command.postId), eq(posts.version, command.input.expectedVersion))); + if (result.rowsAffected !== 1) throw new AppError('VERSION_CONFLICT', { expectedVersion: command.input.expectedVersion }); + await transaction.insert(auditEvents).values({ + id: crypto.randomUUID(), workspaceId: command.workspaceId, actorId: command.actorId, + action: 'revision.restored', targetType: 'post', targetId: command.postId, + requestId: command.requestId, metadata: { version: newVersion, restoredFromRevisionId: target.id }, createdAt: now, + }); + }); + } catch (error) { + if (error instanceof AppError) throw error; + if (isRevisionVersionConflict(error)) throw new AppError('VERSION_CONFLICT', { expectedVersion: command.input.expectedVersion }); + throw error; + } + const restored = await this.find(command.workspaceId, command.postId); + if (!restored) throw new AppError('INTERNAL_FAILURE'); + return restored; + } + + async transition(command: Parameters<EditorialRepository['transition']>[0]): Promise<PostDetail> { + if (command.input.idempotencyKey) { + const existing = await this.findPublicationByKey(command.input.idempotencyKey); + if (existing) { + if (existing.workspaceId !== command.workspaceId || existing.postId !== command.postId) throw new AppError('VERSION_CONFLICT'); + const post = await this.find(command.workspaceId, command.postId); + if (!post) throw new AppError('NOT_FOUND'); + return post; + } + } + + const current = await this.find(command.workspaceId, command.postId); + if (!current) throw new AppError('NOT_FOUND'); + const rule = resolveTransition(current.state, command.input.action); + const now = new Date(); + const newVersion = command.input.expectedVersion + 1; + const scheduledFor = command.input.scheduledFor ? new Date(command.input.scheduledFor) : null; + const idempotencyKey = command.input.idempotencyKey; + if (command.input.action === 'schedule' && (!scheduledFor || !idempotencyKey)) throw new AppError('VALIDATION_FAILED'); + if (command.input.action === 'publish' && !idempotencyKey) throw new AppError('VALIDATION_FAILED'); + const publicationId = crypto.randomUUID(); + + try { + await this.database.db.transaction(async (transaction) => { + const scheduledPublications = command.input.action === 'archive' && current.state === 'Scheduled' + ? await transaction.select({ idempotencyKey: publications.idempotencyKey }).from(publications) + .where(and(eq(publications.workspaceId, command.workspaceId), eq(publications.postId, command.postId), eq(publications.status, 'scheduled'))) + : []; + const result = await transaction.update(posts).set({ + state: rule.to, + version: newVersion, + updatedAt: now, + ...(command.input.action === 'submit' ? { submittedAt: now } : {}), + ...(command.input.action === 'request_changes' ? { approvedAt: null } : {}), + ...(command.input.action === 'approve' ? { approvedAt: now } : {}), + ...(command.input.action === 'schedule' ? { scheduledFor } : {}), + ...(command.input.action === 'publish' ? { publishedRevisionId: current.draftRevisionId, publishedAt: now, scheduledFor: null } : {}), + ...(command.input.action === 'archive' ? { archivedAt: now, scheduledFor: null } : {}), + }).where(and(eq(posts.workspaceId, command.workspaceId), eq(posts.id, command.postId), eq(posts.version, command.input.expectedVersion), eq(posts.state, current.state))); + if (result.rowsAffected !== 1) throw new AppError('VERSION_CONFLICT', { expectedVersion: command.input.expectedVersion }); + + if (command.input.action === 'schedule') { + if (!scheduledFor || !idempotencyKey) throw new AppError('VALIDATION_FAILED'); + await transaction.insert(publications).values({ + id: publicationId, workspaceId: command.workspaceId, postId: command.postId, + revisionId: current.draftRevisionId, status: 'scheduled', scheduledFor, + idempotencyKey, createdBy: command.actorId, createdAt: now, + }); + await transaction.insert(jobs).values({ + id: crypto.randomUUID(), workspaceId: command.workspaceId, type: 'publish', + payload: { postId: command.postId, publicationId, revisionId: current.draftRevisionId }, + status: 'pending', runAt: scheduledFor, idempotencyKey: `job:${idempotencyKey}`, + createdAt: now, updatedAt: now, + }); + } + + if (command.input.action === 'publish') { + if (!idempotencyKey) throw new AppError('VALIDATION_FAILED'); + await transaction.insert(publications).values({ + id: publicationId, workspaceId: command.workspaceId, postId: command.postId, + revisionId: current.draftRevisionId, status: 'published', publishedAt: now, + idempotencyKey, createdBy: command.actorId, createdAt: now, + }); + } + + if (command.input.action === 'archive' && current.state === 'Scheduled') { + await transaction.update(publications).set({ status: 'cancelled' }) + .where(and(eq(publications.workspaceId, command.workspaceId), eq(publications.postId, command.postId), eq(publications.status, 'scheduled'))); + for (const publication of scheduledPublications) { + await transaction.update(jobs).set({ status: 'completed', leaseUntil: null, updatedAt: now }) + .where(and(eq(jobs.workspaceId, command.workspaceId), eq(jobs.idempotencyKey, `job:${publication.idempotencyKey}`), or(eq(jobs.status, 'pending'), eq(jobs.status, 'failed'), eq(jobs.status, 'leased')))); + } + } + + await transaction.insert(auditEvents).values({ + id: crypto.randomUUID(), workspaceId: command.workspaceId, actorId: command.actorId, + action: `post.${command.input.action}`, targetType: 'post', targetId: command.postId, + requestId: command.requestId, + metadata: { from: current.state, to: rule.to, version: newVersion, revisionId: current.draftRevisionId }, + createdAt: now, + }); + }); + } catch (error) { + if (error instanceof AppError) throw error; + if (command.input.idempotencyKey && await this.findPublicationByKey(command.input.idempotencyKey)) { + const post = await this.find(command.workspaceId, command.postId); + if (post) return post; + } + throw error; + } + + const transitioned = await this.find(command.workspaceId, command.postId); + if (!transitioned) throw new AppError('INTERNAL_FAILURE'); + return transitioned; + } + + async findPublic(workspaceSlug: string, postSlug: string): Promise<PublicPost | null> { + const [row] = await this.database.db.select({ + workspaceName: workspaces.name, + slug: posts.slug, + title: revisions.title, + excerpt: revisions.excerpt, + content: revisions.content, + revisionId: revisions.id, + publishedAt: posts.publishedAt, + }).from(posts) + .innerJoin(workspaces, eq(workspaces.id, posts.workspaceId)) + .innerJoin(revisions, and(eq(revisions.id, posts.publishedRevisionId), eq(revisions.workspaceId, posts.workspaceId), eq(revisions.postId, posts.id))) + .where(and(eq(workspaces.slug, workspaceSlug), eq(posts.slug, postSlug))) + .limit(1); + if (!row || !row.publishedAt) return null; + return { ...row, publishedAt: row.publishedAt }; + } + + async runDuePublicationJobs(now: Date, limit = 10): Promise<Readonly<{ claimed: number; completed: number; failed: number }>> { + const candidates = await this.database.db.select().from(jobs).where(and( + eq(jobs.type, 'publish'), + lte(jobs.runAt, now), + or(eq(jobs.status, 'pending'), and(eq(jobs.status, 'failed'), lt(jobs.attempts, 3)), and(eq(jobs.status, 'leased'), lte(jobs.leaseUntil, now))), + )).orderBy(asc(jobs.runAt)).limit(Math.min(Math.max(limit, 1), 100)); + let claimed = 0; + let completed = 0; + let failed = 0; + + for (const job of candidates) { + const leaseUntil = new Date(now.getTime() + 30_000); + const claim = await this.database.db.update(jobs).set({ status: 'leased', leaseUntil, attempts: sql`${jobs.attempts} + 1`, updatedAt: now }) + .where(and(eq(jobs.id, job.id), eq(jobs.attempts, job.attempts), or(eq(jobs.status, 'pending'), eq(jobs.status, 'failed'), and(eq(jobs.status, 'leased'), lte(jobs.leaseUntil, now))))); + if (claim.rowsAffected !== 1) continue; + claimed += 1; + try { + await this.completePublicationJob(job.id, job.workspaceId, job.payload, now); + completed += 1; + } catch (error) { + failed += 1; + const code = error instanceof AppError ? error.code : 'INTERNAL_FAILURE'; + await this.database.db.update(jobs).set({ + status: 'failed', leaseUntil: null, lastErrorCode: code, + runAt: new Date(now.getTime() + Math.min(60_000, 1000 * 2 ** job.attempts)), updatedAt: now, + }).where(eq(jobs.id, job.id)); + } + } + return { claimed, completed, failed }; + } + + private async findPublicationByKey(idempotencyKey: string): Promise<typeof publications.$inferSelect | null> { + const [publication] = await this.database.db.select().from(publications).where(eq(publications.idempotencyKey, idempotencyKey)).limit(1); + return publication ?? null; + } + + private async completePublicationJob(jobId: string, workspaceId: string, rawPayload: unknown, now: Date): Promise<void> { + const payload = publishJobPayloadSchema.parse(rawPayload); + await this.database.db.transaction(async (transaction) => { + const [publication] = await transaction.select().from(publications) + .where(and(eq(publications.id, payload.publicationId), eq(publications.workspaceId, workspaceId), eq(publications.postId, payload.postId), eq(publications.revisionId, payload.revisionId))).limit(1); + if (!publication) throw new AppError('NOT_FOUND'); + if (publication.status === 'published') { + await transaction.update(jobs).set({ status: 'completed', leaseUntil: null, lastErrorCode: null, updatedAt: now }).where(eq(jobs.id, jobId)); + return; + } + if (publication.status !== 'scheduled') throw new AppError('ILLEGAL_TRANSITION'); + const [revision] = await transaction.select({ id: revisions.id }).from(revisions) + .where(and(eq(revisions.id, payload.revisionId), eq(revisions.workspaceId, workspaceId), eq(revisions.postId, payload.postId))).limit(1); + if (!revision) throw new AppError('NOT_FOUND'); + const update = await transaction.update(posts).set({ + state: 'Published', publishedRevisionId: payload.revisionId, publishedAt: now, + scheduledFor: null, version: sql`${posts.version} + 1`, updatedAt: now, + }).where(and(eq(posts.id, payload.postId), eq(posts.workspaceId, workspaceId), eq(posts.state, 'Scheduled'))); + if (update.rowsAffected !== 1) throw new AppError('ILLEGAL_TRANSITION'); + await transaction.update(publications).set({ status: 'published', publishedAt: now }).where(eq(publications.id, publication.id)); + await transaction.insert(auditEvents).values({ + id: crypto.randomUUID(), workspaceId, actorId: publication.createdBy, + action: 'post.publish_scheduled', targetType: 'post', targetId: payload.postId, + requestId: `job:${jobId}`, metadata: { revisionId: payload.revisionId }, createdAt: now, + }); + await transaction.update(jobs).set({ status: 'completed', leaseUntil: null, lastErrorCode: null, updatedAt: now }).where(eq(jobs.id, jobId)); + }); + } +} diff --git a/src/modules/editorial/repository.ts b/src/modules/editorial/repository.ts new file mode 100644 index 0000000..6f88f14 --- /dev/null +++ b/src/modules/editorial/repository.ts @@ -0,0 +1,13 @@ +import type { CreatePostInput, JobRunResult, PostDetail, PostSummary, PublicPost, RestoreRevisionInput, RevisionDetail, SavePostInput, TransitionPostInput } from '@/src/modules/editorial/domain'; + +export interface EditorialRepository { + list(workspaceId: string): Promise<PostSummary[]>; + find(workspaceId: string, postId: string): Promise<PostDetail | null>; + create(command: Readonly<{ workspaceId: string; actorId: string; requestId: string; input: CreatePostInput }>): Promise<PostDetail>; + save(command: Readonly<{ workspaceId: string; postId: string; actorId: string; requestId: string; input: SavePostInput }>): Promise<PostDetail>; + listRevisions(workspaceId: string, postId: string): Promise<RevisionDetail[]>; + restore(command: Readonly<{ workspaceId: string; postId: string; actorId: string; requestId: string; input: RestoreRevisionInput }>): Promise<PostDetail>; + transition(command: Readonly<{ workspaceId: string; postId: string; actorId: string; requestId: string; input: TransitionPostInput }>): Promise<PostDetail>; + findPublic(workspaceSlug: string, postSlug: string): Promise<PublicPost | null>; + runDuePublicationJobs(now: Date, limit?: number): Promise<JobRunResult>; +} diff --git a/src/modules/editorial/service.ts b/src/modules/editorial/service.ts new file mode 100644 index 0000000..0557a52 --- /dev/null +++ b/src/modules/editorial/service.ts @@ -0,0 +1,81 @@ +import { createPostSchema, restoreRevisionSchema, savePostSchema, transitionPostSchema, type JobRunResult, type PostDetail, type PostSummary, type PublicPost, type RevisionDetail } from '@/src/modules/editorial/domain'; +import { TRANSITION_RULES } from '@/src/modules/editorial/workflow'; +import type { EditorialRepository } from '@/src/modules/editorial/repository'; +import type { MembershipContext } from '@/src/modules/identity/domain'; +import { authorize } from '@/src/modules/identity/policy'; +import { getDatabase } from '@/src/platform/db/client'; +import { DrizzleEditorialRepository } from '@/src/modules/editorial/drizzle-repository'; +import { AppError } from '@/src/platform/observability/errors'; + +export class EditorialService { + constructor(private readonly repository: EditorialRepository) {} + + async list(context: MembershipContext): Promise<PostSummary[]> { + authorize(context.role, 'workspace.read'); + return this.repository.list(context.workspaceId); + } + + async get(context: MembershipContext, postId: string): Promise<PostDetail> { + authorize(context.role, 'workspace.read'); + const post = await this.repository.find(context.workspaceId, postId); + if (!post) throw new AppError('NOT_FOUND'); + return post; + } + + async create(context: MembershipContext, input: unknown, requestId: string): Promise<PostDetail> { + authorize(context.role, 'post.create', { actorId: context.userId, ownerId: context.userId }); + return this.repository.create({ workspaceId: context.workspaceId, actorId: context.userId, requestId, input: createPostSchema.parse(input) }); + } + + async save(context: MembershipContext, postId: string, input: unknown, requestId: string): Promise<PostDetail> { + const existing = await this.repository.find(context.workspaceId, postId); + if (!existing) throw new AppError('NOT_FOUND'); + authorize(context.role, 'post.update', { actorId: context.userId, ownerId: existing.authorId }); + if (!['Draft', 'ChangesRequested', 'Published'].includes(existing.state)) throw new AppError('ILLEGAL_TRANSITION', { state: existing.state, operation: 'save' }); + return this.repository.save({ workspaceId: context.workspaceId, postId, actorId: context.userId, requestId, input: savePostSchema.parse(input) }); + } + + async revisions(context: MembershipContext, postId: string): Promise<RevisionDetail[]> { + authorize(context.role, 'workspace.read'); + if (!await this.repository.find(context.workspaceId, postId)) throw new AppError('NOT_FOUND'); + return this.repository.listRevisions(context.workspaceId, postId); + } + + async restore(context: MembershipContext, postId: string, input: unknown, requestId: string): Promise<PostDetail> { + const existing = await this.repository.find(context.workspaceId, postId); + if (!existing) throw new AppError('NOT_FOUND'); + authorize(context.role, 'revision.restore', { actorId: context.userId, ownerId: existing.authorId }); + if (!['Draft', 'ChangesRequested', 'Published'].includes(existing.state)) throw new AppError('ILLEGAL_TRANSITION', { state: existing.state, operation: 'restore' }); + return this.repository.restore({ workspaceId: context.workspaceId, postId, actorId: context.userId, requestId, input: restoreRevisionSchema.parse(input) }); + } + + async transition(context: MembershipContext, postId: string, input: unknown, requestId: string): Promise<PostDetail> { + const parsed = transitionPostSchema.parse(input); + const existing = await this.repository.find(context.workspaceId, postId); + if (!existing) throw new AppError('NOT_FOUND'); + const rule = TRANSITION_RULES[parsed.action]; + authorize(context.role, rule.permission, { actorId: context.userId, ownerId: existing.authorId }); + if (parsed.action === 'submit' && existing.content.trim().length === 0) throw new AppError('VALIDATION_FAILED', { field: 'content' }); + if (parsed.action === 'schedule') { + if (!parsed.scheduledFor || new Date(parsed.scheduledFor).getTime() <= Date.now()) throw new AppError('VALIDATION_FAILED', { field: 'scheduledFor' }); + } + return this.repository.transition({ workspaceId: context.workspaceId, postId, actorId: context.userId, requestId, input: parsed }); + } + + async publicPost(workspaceSlug: string, postSlug: string): Promise<PublicPost> { + const post = await this.repository.findPublic(workspaceSlug, postSlug); + if (!post) throw new AppError('NOT_FOUND'); + return post; + } + + async runDueJobs(now = new Date(), limit = 10): Promise<JobRunResult> { + return this.repository.runDuePublicationJobs(now, limit); + } +} + +let editorialService: EditorialService | undefined; + +export function getEditorialService(): EditorialService { + if (!editorialService) editorialService = new EditorialService(new DrizzleEditorialRepository(getDatabase())); + return editorialService; +} diff --git a/src/modules/editorial/workflow.ts b/src/modules/editorial/workflow.ts new file mode 100644 index 0000000..ebe80a4 --- /dev/null +++ b/src/modules/editorial/workflow.ts @@ -0,0 +1,27 @@ +import type { Permission } from '@/src/modules/identity/domain'; +import { AppError } from '@/src/platform/observability/errors'; +import type { PostState } from '@/src/modules/editorial/domain'; + +export const transitionActionValues = ['submit', 'request_changes', 'approve', 'schedule', 'publish', 'archive'] as const; +export type TransitionAction = (typeof transitionActionValues)[number]; + +type TransitionRule = Readonly<{ + from: readonly PostState[]; + to: PostState; + permission: Permission; +}>; + +export const TRANSITION_RULES: Readonly<Record<TransitionAction, TransitionRule>> = { + submit: { from: ['Draft', 'ChangesRequested'], to: 'InReview', permission: 'post.submit' }, + request_changes: { from: ['InReview'], to: 'ChangesRequested', permission: 'review.request_changes' }, + approve: { from: ['InReview'], to: 'Approved', permission: 'review.approve' }, + schedule: { from: ['Approved'], to: 'Scheduled', permission: 'post.schedule' }, + publish: { from: ['Approved', 'Scheduled'], to: 'Published', permission: 'post.publish' }, + archive: { from: ['Draft', 'InReview', 'ChangesRequested', 'Approved', 'Scheduled', 'Published'], to: 'Archived', permission: 'post.archive' }, +}; + +export function resolveTransition(state: PostState, action: TransitionAction): TransitionRule { + const rule = TRANSITION_RULES[action]; + if (!rule.from.includes(state)) throw new AppError('ILLEGAL_TRANSITION', { from: state, action }); + return rule; +} diff --git a/src/modules/identity/demo-repository.ts b/src/modules/identity/demo-repository.ts new file mode 100644 index 0000000..9891dc4 --- /dev/null +++ b/src/modules/identity/demo-repository.ts @@ -0,0 +1,9 @@ +export type DemoResetResult = Readonly<{ fixtureVersion: number; resetAt: string }>; + +export interface DemoRepository { + isDemoWorkspace(workspaceId: string): Promise<boolean>; + findReset(workspaceId: string, idempotencyKey: string): Promise<DemoResetResult | null>; + consumeResetLimit(workspaceId: string, userId: string, now: Date): Promise<void>; + reset(workspaceId: string, requestId: string): Promise<DemoResetResult>; + recordReset(workspaceId: string, idempotencyKey: string, result: DemoResetResult): Promise<void>; +} diff --git a/src/modules/identity/demo-service.ts b/src/modules/identity/demo-service.ts new file mode 100644 index 0000000..d81183b --- /dev/null +++ b/src/modules/identity/demo-service.ts @@ -0,0 +1,33 @@ +import { z } from 'zod'; + +import { DrizzleDemoRepository } from '@/src/modules/identity/drizzle-demo-repository'; +import type { DemoRepository, DemoResetResult } from '@/src/modules/identity/demo-repository'; +import type { MembershipContext } from '@/src/modules/identity/domain'; +import { authorize } from '@/src/modules/identity/policy'; +import { getDatabase } from '@/src/platform/db/client'; +import { AppError } from '@/src/platform/observability/errors'; + +const demoResetSchema = z.object({ idempotencyKey: z.string().trim().min(12).max(120) }); +export type DemoResetResponse = DemoResetResult & Readonly<{ alreadyApplied: boolean }>; + +export class DemoService { + constructor(private readonly repository: DemoRepository) {} + + async reset(context: MembershipContext, rawInput: unknown, requestId: string): Promise<DemoResetResponse> { + authorize(context.role, 'demo.reset'); + const input = demoResetSchema.parse(rawInput); + if (!await this.repository.isDemoWorkspace(context.workspaceId)) throw new AppError('FORBIDDEN'); + const existing = await this.repository.findReset(context.workspaceId, input.idempotencyKey); + if (existing) return { ...existing, alreadyApplied: true }; + await this.repository.consumeResetLimit(context.workspaceId, context.userId, new Date()); + const result = await this.repository.reset(context.workspaceId, requestId); + await this.repository.recordReset(context.workspaceId, input.idempotencyKey, result); + return { ...result, alreadyApplied: false }; + } +} + +let demoService: DemoService | undefined; +export function getDemoService(): DemoService { + if (!demoService) demoService = new DemoService(new DrizzleDemoRepository(getDatabase())); + return demoService; +} diff --git a/src/modules/identity/demo.ts b/src/modules/identity/demo.ts new file mode 100644 index 0000000..eaa8153 --- /dev/null +++ b/src/modules/identity/demo.ts @@ -0,0 +1,13 @@ +import type { Role } from '@/src/modules/identity/domain'; + +export const DEMO_WORKSPACE_ID = 'ws-demo'; +export const DEMO_WORKSPACE_SLUG = 'demo'; +export const DEMO_PASSWORD = 'AutoBlogDemo!2026'; + +export const DEMO_IDENTITIES: readonly Readonly<{ id: string; name: string; email: string; role: Role }>[] = [ + { id: 'demo-owner', name: 'Olivia Owner', email: 'owner@demo.autoblog.local', role: 'Owner' }, + { id: 'demo-admin', name: 'Amir Admin', email: 'admin@demo.autoblog.local', role: 'Admin' }, + { id: 'demo-editor', name: 'Elena Editor', email: 'editor@demo.autoblog.local', role: 'Editor' }, + { id: 'demo-author', name: 'Avery Author', email: 'author@demo.autoblog.local', role: 'Author' }, + { id: 'demo-reviewer', name: 'Ravi Reviewer', email: 'reviewer@demo.autoblog.local', role: 'Reviewer' }, +]; diff --git a/src/modules/identity/domain.ts b/src/modules/identity/domain.ts new file mode 100644 index 0000000..e075489 --- /dev/null +++ b/src/modules/identity/domain.ts @@ -0,0 +1,35 @@ +import { z } from 'zod'; + +export const roleSchema = z.enum(['Owner', 'Admin', 'Editor', 'Author', 'Reviewer']); +export type Role = z.infer<typeof roleSchema>; + +export const permissionSchema = z.enum([ + 'workspace.read', + 'post.create', + 'post.update', + 'post.delete', + 'post.submit', + 'review.request_changes', + 'review.approve', + 'post.schedule', + 'post.publish', + 'post.archive', + 'revision.restore', + 'media.upload', + 'media.delete', + 'ai.suggest', + 'membership.manage', + 'demo.reset', + 'jobs.run', +]); +export type Permission = z.infer<typeof permissionSchema>; + +export type MembershipContext = Readonly<{ + workspaceId: string; + workspaceSlug: string; + workspaceName: string; + userId: string; + userName: string; + userEmail: string; + role: Role; +}>; diff --git a/src/modules/identity/drizzle-demo-repository.ts b/src/modules/identity/drizzle-demo-repository.ts new file mode 100644 index 0000000..b296007 --- /dev/null +++ b/src/modules/identity/drizzle-demo-repository.ts @@ -0,0 +1,58 @@ +import { and, eq, sql } from 'drizzle-orm'; + +import type { DemoRepository, DemoResetResult } from '@/src/modules/identity/demo-repository'; +import { DEMO_WORKSPACE_ID } from '@/src/modules/identity/demo'; +import type { DatabaseContext } from '@/src/platform/db/client'; +import { idempotencyRecords, rateLimits, workspaces } from '@/src/platform/db/schema'; +import { seedDemo } from '@/src/platform/db/seed'; +import { AppError } from '@/src/platform/observability/errors'; + +const RESET_LIMIT = 3; +const RESET_WINDOW_MS = 60 * 60 * 1000; + +export class DrizzleDemoRepository implements DemoRepository { + constructor(private readonly database: DatabaseContext) {} + + async isDemoWorkspace(workspaceId: string): Promise<boolean> { + const [workspace] = await this.database.db.select({ isDemo: workspaces.isDemo }).from(workspaces).where(eq(workspaces.id, workspaceId)).limit(1); + return workspace?.isDemo === true; + } + + async findReset(workspaceId: string, idempotencyKey: string): Promise<DemoResetResult | null> { + const [record] = await this.database.db.select({ result: idempotencyRecords.result }).from(idempotencyRecords).where(and( + eq(idempotencyRecords.workspaceId, workspaceId), eq(idempotencyRecords.operation, 'demo.reset'), eq(idempotencyRecords.key, idempotencyKey), + )).limit(1); + if (!record) return null; + const result = record.result; + return typeof result.fixtureVersion === 'number' && typeof result.resetAt === 'string' + ? { fixtureVersion: result.fixtureVersion, resetAt: result.resetAt } + : null; + } + + async consumeResetLimit(workspaceId: string, userId: string, now: Date): Promise<void> { + const windowStartedAt = new Date(Math.floor(now.getTime() / RESET_WINDOW_MS) * RESET_WINDOW_MS); + const expiresAt = new Date(windowStartedAt.getTime() + RESET_WINDOW_MS); + const key = `demo-reset:${workspaceId}:${userId}`; + await this.database.db.transaction(async (transaction) => { + const inserted = await transaction.insert(rateLimits).values({ key, windowStartedAt, count: 1, expiresAt }).onConflictDoNothing(); + if (inserted.rowsAffected === 1) return; + const updated = await transaction.update(rateLimits).set({ count: sql`${rateLimits.count} + 1` }).where(and( + eq(rateLimits.key, key), eq(rateLimits.windowStartedAt, windowStartedAt), sql`${rateLimits.count} < ${RESET_LIMIT}`, + )); + if (updated.rowsAffected !== 1) throw new AppError('RATE_LIMITED', { retryAfterSeconds: Math.ceil((expiresAt.getTime() - now.getTime()) / 1000) }); + }); + } + + async reset(workspaceId: string, requestId: string): Promise<DemoResetResult> { + if (workspaceId !== DEMO_WORKSPACE_ID) throw new AppError('FORBIDDEN'); + const resetAt = new Date().toISOString(); + await seedDemo(this.database, { reset: true, requestId }); + return { fixtureVersion: 1, resetAt }; + } + + async recordReset(workspaceId: string, idempotencyKey: string, result: DemoResetResult): Promise<void> { + await this.database.db.insert(idempotencyRecords).values({ + workspaceId, operation: 'demo.reset', key: idempotencyKey, result, createdAt: new Date(result.resetAt), + }).onConflictDoNothing(); + } +} diff --git a/src/modules/identity/policy.ts b/src/modules/identity/policy.ts new file mode 100644 index 0000000..fd87bb9 --- /dev/null +++ b/src/modules/identity/policy.ts @@ -0,0 +1,35 @@ +import type { Permission, Role } from '@/src/modules/identity/domain'; +import { AppError } from '@/src/platform/observability/errors'; + +type PermissionRule = Readonly<{ roles: readonly Role[]; ownOnlyFor?: readonly Role[] }>; + +export const PERMISSION_MATRIX: Readonly<Record<Permission, PermissionRule>> = { + 'workspace.read': { roles: ['Owner', 'Admin', 'Editor', 'Author', 'Reviewer'] }, + 'post.create': { roles: ['Owner', 'Admin', 'Editor', 'Author'] }, + 'post.update': { roles: ['Owner', 'Admin', 'Editor', 'Author'], ownOnlyFor: ['Author'] }, + 'post.delete': { roles: ['Owner', 'Admin', 'Editor'] }, + 'post.submit': { roles: ['Owner', 'Admin', 'Editor', 'Author'], ownOnlyFor: ['Author'] }, + 'review.request_changes': { roles: ['Owner', 'Admin', 'Editor', 'Reviewer'] }, + 'review.approve': { roles: ['Owner', 'Admin', 'Editor', 'Reviewer'] }, + 'post.schedule': { roles: ['Owner', 'Admin', 'Editor'] }, + 'post.publish': { roles: ['Owner', 'Admin', 'Editor'] }, + 'post.archive': { roles: ['Owner', 'Admin', 'Editor'] }, + 'revision.restore': { roles: ['Owner', 'Admin', 'Editor', 'Author'], ownOnlyFor: ['Author'] }, + 'media.upload': { roles: ['Owner', 'Admin', 'Editor', 'Author'], ownOnlyFor: ['Author'] }, + 'media.delete': { roles: ['Owner', 'Admin', 'Editor'] }, + 'ai.suggest': { roles: ['Owner', 'Admin', 'Editor', 'Author', 'Reviewer'] }, + 'membership.manage': { roles: ['Owner', 'Admin'] }, + 'demo.reset': { roles: ['Owner'] }, + 'jobs.run': { roles: ['Owner', 'Admin'] }, +}; + +export function can(role: Role, permission: Permission, context?: Readonly<{ actorId: string; ownerId?: string }>): boolean { + const rule = PERMISSION_MATRIX[permission]; + if (!rule.roles.includes(role)) return false; + if (rule.ownOnlyFor?.includes(role)) return Boolean(context?.ownerId && context.ownerId === context.actorId); + return true; +} + +export function authorize(role: Role, permission: Permission, context?: Readonly<{ actorId: string; ownerId?: string }>): void { + if (!can(role, permission, context)) throw new AppError('FORBIDDEN'); +} diff --git a/src/modules/media/cleanup-worker.ts b/src/modules/media/cleanup-worker.ts new file mode 100644 index 0000000..c1dd6ff --- /dev/null +++ b/src/modules/media/cleanup-worker.ts @@ -0,0 +1,60 @@ +import { and, asc, eq, lt, lte, or, sql } from 'drizzle-orm'; +import { z } from 'zod'; + +import type { JobRunResult } from '@/src/modules/editorial/domain'; +import type { MediaProvider } from '@/src/modules/media/provider'; +import type { DatabaseContext } from '@/src/platform/db/client'; +import { jobs, mediaAssets } from '@/src/platform/db/schema'; +import { AppError } from '@/src/platform/observability/errors'; + +const cleanupPayloadSchema = z.object({ storageKey: z.string().min(1), assetId: z.string().min(1).optional() }); + +export class MediaCleanupWorker { + constructor(private readonly database: DatabaseContext, private readonly provider: MediaProvider) {} + + async run(now = new Date(), limit = 20): Promise<JobRunResult> { + const candidates = await this.database.db.select().from(jobs).where(and( + eq(jobs.type, 'media_cleanup'), lte(jobs.runAt, now), + or(eq(jobs.status, 'pending'), and(eq(jobs.status, 'failed'), lt(jobs.attempts, 3)), and(eq(jobs.status, 'leased'), lte(jobs.leaseUntil, now))), + )).orderBy(asc(jobs.runAt)).limit(Math.min(Math.max(limit, 1), 100)); + let claimed = 0; let completed = 0; let failed = 0; + for (const job of candidates) { + const claim = await this.database.db.update(jobs).set({ + status: 'leased', leaseUntil: new Date(now.getTime() + 30_000), attempts: sql`${jobs.attempts} + 1`, updatedAt: now, + }).where(and(eq(jobs.id, job.id), eq(jobs.attempts, job.attempts), or(eq(jobs.status, 'pending'), eq(jobs.status, 'failed'), and(eq(jobs.status, 'leased'), lte(jobs.leaseUntil, now))))); + if (claim.rowsAffected !== 1) continue; + claimed += 1; + try { + const payload = cleanupPayloadSchema.parse(job.payload); + if (payload.assetId) { + const [asset] = await this.database.db.select({ status: mediaAssets.status, storageKey: mediaAssets.storageKey }).from(mediaAssets) + .where(and(eq(mediaAssets.workspaceId, job.workspaceId), eq(mediaAssets.id, payload.assetId), eq(mediaAssets.storageKey, payload.storageKey))).limit(1); + if (asset?.status === 'active') throw new AppError('ILLEGAL_TRANSITION'); + } + const controller = new AbortController(); + let timeout: ReturnType<typeof setTimeout> | undefined; + try { + await Promise.race([ + this.provider.delete(payload.storageKey, controller.signal), + new Promise<never>((_resolve, reject) => { + timeout = setTimeout(() => { controller.abort(); reject(new AppError('PROVIDER_UNAVAILABLE')); }, 8_000); + }), + ]); + } finally { if (timeout) clearTimeout(timeout); } + await this.database.db.transaction(async (transaction) => { + if (payload.assetId) await transaction.update(mediaAssets).set({ status: 'deleted', updatedAt: now }) + .where(and(eq(mediaAssets.workspaceId, job.workspaceId), eq(mediaAssets.id, payload.assetId))); + await transaction.update(jobs).set({ status: 'completed', leaseUntil: null, lastErrorCode: null, updatedAt: now }).where(eq(jobs.id, job.id)); + }); + completed += 1; + } catch (error) { + failed += 1; + await this.database.db.update(jobs).set({ + status: 'failed', leaseUntil: null, lastErrorCode: error instanceof AppError ? error.code : 'PROVIDER_UNAVAILABLE', + runAt: new Date(now.getTime() + Math.min(60_000, 1000 * 2 ** job.attempts)), updatedAt: now, + }).where(eq(jobs.id, job.id)); + } + } + return { claimed, completed, failed }; + } +} diff --git a/src/modules/media/database-provider.ts b/src/modules/media/database-provider.ts new file mode 100644 index 0000000..6c5277e --- /dev/null +++ b/src/modules/media/database-provider.ts @@ -0,0 +1,27 @@ +import { eq } from 'drizzle-orm'; + +import type { MediaProvider } from '@/src/modules/media/provider'; +import type { DatabaseContext } from '@/src/platform/db/client'; +import { mediaObjects } from '@/src/platform/db/schema'; + +export class DatabaseMediaProvider implements MediaProvider { + constructor(private readonly database: DatabaseContext) {} + + async put(storageKey: string, data: Buffer, signal: AbortSignal): Promise<void> { + signal.throwIfAborted(); + await this.database.db.insert(mediaObjects).values({ storageKey, data, createdAt: new Date() }); + signal.throwIfAborted(); + } + + async get(storageKey: string, signal: AbortSignal): Promise<Buffer | null> { + signal.throwIfAborted(); + const [object] = await this.database.db.select({ data: mediaObjects.data }).from(mediaObjects).where(eq(mediaObjects.storageKey, storageKey)).limit(1); + signal.throwIfAborted(); + return object?.data ?? null; + } + + async delete(storageKey: string, signal: AbortSignal): Promise<void> { + signal.throwIfAborted(); + await this.database.db.delete(mediaObjects).where(eq(mediaObjects.storageKey, storageKey)); + } +} diff --git a/src/modules/media/domain.ts b/src/modules/media/domain.ts new file mode 100644 index 0000000..7a2ffd5 --- /dev/null +++ b/src/modules/media/domain.ts @@ -0,0 +1,79 @@ +import sharp from 'sharp'; +import { z } from 'zod'; +import { createHash } from 'node:crypto'; + +import { AppError } from '@/src/platform/observability/errors'; + +export const MEDIA_MIME_TYPES = ['image/jpeg', 'image/png', 'image/webp'] as const; +export const MEDIA_MAX_DIMENSION = 4096; +export const MEDIA_MAX_PIXELS = 16_000_000; + +export const mediaUploadFieldsSchema = z.object({ + postId: z.string().min(1).max(120), + replaceAssetId: z.string().min(1).max(120).optional(), + altText: z.string().trim().max(500).default(''), +}); + +export type MediaAsset = Readonly<{ + id: string; + workspaceId: string; + postId: string | null; + status: 'pending' | 'active' | 'replaced' | 'deleted' | 'cleanup_pending'; + fileName: string; + mimeType: string; + byteSize: number; + width: number; + height: number; + altText: string; + createdBy: string; + replacesAssetId: string | null; + createdAt: Date; +}>; + +export type VerifiedImage = Readonly<{ + data: Buffer; + fileName: string; + mimeType: (typeof MEDIA_MIME_TYPES)[number]; + byteSize: number; + width: number; + height: number; + checksum: string; +}>; + +const FORMAT_MIME: Readonly<Record<string, (typeof MEDIA_MIME_TYPES)[number] | undefined>> = { + jpeg: 'image/jpeg', + png: 'image/png', + webp: 'image/webp', +}; + +export function safeFileName(name: string): string { + const leaf = name.replaceAll('\\', '/').split('/').at(-1) ?? 'upload'; + const normalized = leaf.normalize('NFKC').replace(/[^a-zA-Z0-9._-]+/gu, '-').replace(/^-+|-+$/gu, '').slice(0, 120); + return normalized || 'upload'; +} + +export async function verifyImage(input: Readonly<{ data: Buffer; fileName: string; declaredMimeType: string; maxBytes: number }>): Promise<VerifiedImage> { + if (input.data.byteLength === 0 || input.data.byteLength > input.maxBytes) throw new AppError('VALIDATION_FAILED', { field: 'file', reason: 'size' }); + if (!MEDIA_MIME_TYPES.includes(input.declaredMimeType as (typeof MEDIA_MIME_TYPES)[number])) throw new AppError('VALIDATION_FAILED', { field: 'file', reason: 'mime' }); + try { + const metadata = await sharp(input.data, { failOn: 'warning', limitInputPixels: MEDIA_MAX_PIXELS }).metadata(); + const detectedMime = metadata.format ? FORMAT_MIME[metadata.format] : undefined; + if (!detectedMime || detectedMime !== input.declaredMimeType) throw new AppError('VALIDATION_FAILED', { field: 'file', reason: 'content_type' }); + if (!metadata.width || !metadata.height || metadata.width > MEDIA_MAX_DIMENSION || metadata.height > MEDIA_MAX_DIMENSION || metadata.width * metadata.height > MEDIA_MAX_PIXELS) { + throw new AppError('VALIDATION_FAILED', { field: 'file', reason: 'dimensions' }); + } + const checksum = createHash('sha256').update(input.data).digest('hex'); + return { + data: input.data, + fileName: safeFileName(input.fileName), + mimeType: detectedMime, + byteSize: input.data.byteLength, + width: metadata.width, + height: metadata.height, + checksum, + }; + } catch (error) { + if (error instanceof AppError) throw error; + throw new AppError('VALIDATION_FAILED', { field: 'file', reason: 'decode' }); + } +} diff --git a/src/modules/media/drizzle-repository.ts b/src/modules/media/drizzle-repository.ts new file mode 100644 index 0000000..906e077 --- /dev/null +++ b/src/modules/media/drizzle-repository.ts @@ -0,0 +1,111 @@ +import { and, desc, eq } from 'drizzle-orm'; + +import type { MediaAsset } from '@/src/modules/media/domain'; +import type { MediaRepository } from '@/src/modules/media/repository'; +import type { DatabaseContext } from '@/src/platform/db/client'; +import { auditEvents, jobs, mediaAssets, posts } from '@/src/platform/db/schema'; +import { AppError } from '@/src/platform/observability/errors'; + +type MediaRow = typeof mediaAssets.$inferSelect; + +function toAsset(row: MediaRow): MediaAsset { + return { + id: row.id, workspaceId: row.workspaceId, postId: row.postId, status: row.status, + fileName: row.fileName, mimeType: row.mimeType, byteSize: row.byteSize, + width: row.width, height: row.height, altText: row.altText, createdBy: row.createdBy, + replacesAssetId: row.replacesAssetId, createdAt: row.createdAt, + }; +} + +export class DrizzleMediaRepository implements MediaRepository { + constructor(private readonly database: DatabaseContext) {} + + async findPostOwner(workspaceId: string, postId: string): Promise<string | null> { + const [post] = await this.database.db.select({ authorId: posts.authorId }).from(posts) + .where(and(eq(posts.workspaceId, workspaceId), eq(posts.id, postId))).limit(1); + return post?.authorId ?? null; + } + + async list(workspaceId: string, postId: string): Promise<MediaAsset[]> { + const rows = await this.database.db.select().from(mediaAssets) + .where(and(eq(mediaAssets.workspaceId, workspaceId), eq(mediaAssets.postId, postId), eq(mediaAssets.status, 'active'))) + .orderBy(desc(mediaAssets.createdAt)); + return rows.map(toAsset); + } + + async find(workspaceId: string, assetId: string): Promise<(MediaAsset & { storageKey: string }) | null> { + const [row] = await this.database.db.select().from(mediaAssets) + .where(and(eq(mediaAssets.workspaceId, workspaceId), eq(mediaAssets.id, assetId))).limit(1); + return row ? { ...toAsset(row), storageKey: row.storageKey } : null; + } + + async finalize(command: Parameters<MediaRepository['finalize']>[0]): Promise<MediaAsset> { + const assetId = crypto.randomUUID(); + const now = new Date(); + await this.database.db.transaction(async (transaction) => { + let replaced: MediaRow | undefined; + if (command.replaceAssetId) { + [replaced] = await transaction.select().from(mediaAssets).where(and( + eq(mediaAssets.id, command.replaceAssetId), eq(mediaAssets.workspaceId, command.workspaceId), + eq(mediaAssets.postId, command.postId), eq(mediaAssets.status, 'active'), + )).limit(1); + if (!replaced) throw new AppError('NOT_FOUND'); + await transaction.update(mediaAssets).set({ status: 'replaced', updatedAt: now }).where(eq(mediaAssets.id, replaced.id)); + } + await transaction.insert(mediaAssets).values({ + id: assetId, workspaceId: command.workspaceId, postId: command.postId, status: 'active', + storageKey: command.storageKey, fileName: command.image.fileName, mimeType: command.image.mimeType, + byteSize: command.image.byteSize, width: command.image.width, height: command.image.height, + checksum: command.image.checksum, altText: command.altText, createdBy: command.actorId, + replacesAssetId: replaced?.id, createdAt: now, updatedAt: now, + }); + if (replaced) await transaction.insert(jobs).values({ + id: crypto.randomUUID(), workspaceId: command.workspaceId, type: 'media_cleanup', + payload: { assetId: replaced.id, storageKey: replaced.storageKey }, status: 'pending', runAt: now, + idempotencyKey: `media-cleanup:${replaced.id}`, createdAt: now, updatedAt: now, + }); + await transaction.insert(auditEvents).values({ + id: crypto.randomUUID(), workspaceId: command.workspaceId, actorId: command.actorId, + action: replaced ? 'media.replaced' : 'media.uploaded', targetType: 'media', targetId: assetId, + requestId: command.requestId, metadata: { postId: command.postId, replacedAssetId: replaced?.id ?? null }, createdAt: now, + }); + }); + const [asset] = await this.database.db.select().from(mediaAssets) + .where(and(eq(mediaAssets.workspaceId, command.workspaceId), eq(mediaAssets.id, assetId))).limit(1); + if (!asset) throw new AppError('INTERNAL_FAILURE'); + return toAsset(asset); + } + + async markForDeletion(command: Parameters<MediaRepository['markForDeletion']>[0]): Promise<void> { + const now = new Date(); + await this.database.db.transaction(async (transaction) => { + const [asset] = await transaction.select().from(mediaAssets).where(and(eq(mediaAssets.workspaceId, command.workspaceId), eq(mediaAssets.id, command.assetId))).limit(1); + if (!asset) throw new AppError('NOT_FOUND'); + if (asset.status === 'deleted' || asset.status === 'cleanup_pending') return; + await transaction.update(mediaAssets).set({ status: 'cleanup_pending', updatedAt: now }).where(eq(mediaAssets.id, asset.id)); + await transaction.insert(jobs).values({ + id: crypto.randomUUID(), workspaceId: command.workspaceId, type: 'media_cleanup', + payload: { assetId: asset.id, storageKey: asset.storageKey }, status: 'pending', runAt: now, + idempotencyKey: `media-delete:${asset.id}`, createdAt: now, updatedAt: now, + }).onConflictDoNothing(); + await transaction.insert(auditEvents).values({ + id: crypto.randomUUID(), workspaceId: command.workspaceId, actorId: command.actorId, + action: 'media.deleted', targetType: 'media', targetId: asset.id, + requestId: command.requestId, metadata: { postId: asset.postId }, createdAt: now, + }); + }); + } + + async enqueueOrphanCleanup(workspaceId: string, storageKey: string, requestId: string): Promise<void> { + const now = new Date(); + await this.database.db.insert(jobs).values({ + id: crypto.randomUUID(), workspaceId, type: 'media_cleanup', payload: { storageKey }, + status: 'pending', runAt: now, idempotencyKey: `media-orphan:${storageKey}`, + createdAt: now, updatedAt: now, + }).onConflictDoNothing(); + await this.database.db.insert(auditEvents).values({ + id: crypto.randomUUID(), workspaceId, actorId: null, action: 'media.cleanup_queued', + targetType: 'media_object', targetId: storageKey, requestId, metadata: {}, createdAt: now, + }); + } +} diff --git a/src/modules/media/provider.ts b/src/modules/media/provider.ts new file mode 100644 index 0000000..d1be2c0 --- /dev/null +++ b/src/modules/media/provider.ts @@ -0,0 +1,5 @@ +export interface MediaProvider { + put(storageKey: string, data: Buffer, signal: AbortSignal): Promise<void>; + get(storageKey: string, signal: AbortSignal): Promise<Buffer | null>; + delete(storageKey: string, signal: AbortSignal): Promise<void>; +} diff --git a/src/modules/media/repository.ts b/src/modules/media/repository.ts new file mode 100644 index 0000000..0ea6d14 --- /dev/null +++ b/src/modules/media/repository.ts @@ -0,0 +1,13 @@ +import type { MediaAsset, VerifiedImage } from '@/src/modules/media/domain'; + +export interface MediaRepository { + findPostOwner(workspaceId: string, postId: string): Promise<string | null>; + list(workspaceId: string, postId: string): Promise<MediaAsset[]>; + find(workspaceId: string, assetId: string): Promise<(MediaAsset & { storageKey: string }) | null>; + finalize(command: Readonly<{ + workspaceId: string; postId: string; actorId: string; requestId: string; storageKey: string; + image: VerifiedImage; altText: string; replaceAssetId?: string; + }>): Promise<MediaAsset>; + markForDeletion(command: Readonly<{ workspaceId: string; assetId: string; actorId: string; requestId: string }>): Promise<void>; + enqueueOrphanCleanup(workspaceId: string, storageKey: string, requestId: string): Promise<void>; +} diff --git a/src/modules/media/service.ts b/src/modules/media/service.ts new file mode 100644 index 0000000..8a1ed08 --- /dev/null +++ b/src/modules/media/service.ts @@ -0,0 +1,100 @@ +import { DrizzleMediaRepository } from '@/src/modules/media/drizzle-repository'; +import { DatabaseMediaProvider } from '@/src/modules/media/database-provider'; +import { mediaUploadFieldsSchema, verifyImage, type MediaAsset } from '@/src/modules/media/domain'; +import type { MediaProvider } from '@/src/modules/media/provider'; +import type { MediaRepository } from '@/src/modules/media/repository'; +import type { MembershipContext } from '@/src/modules/identity/domain'; +import { authorize } from '@/src/modules/identity/policy'; +import { getEnvironment } from '@/src/platform/config/env'; +import { getDatabase } from '@/src/platform/db/client'; +import { AppError } from '@/src/platform/observability/errors'; + +const PROVIDER_TIMEOUT_MS = 8_000; + +async function providerOperation<T>(operation: (signal: AbortSignal) => Promise<T>, timeoutMs = PROVIDER_TIMEOUT_MS): Promise<T> { + const controller = new AbortController(); + let timeout: ReturnType<typeof setTimeout> | undefined; + try { + return await Promise.race([ + operation(controller.signal), + new Promise<T>((_resolve, reject) => { + timeout = setTimeout(() => { controller.abort(); reject(new AppError('PROVIDER_UNAVAILABLE')); }, timeoutMs); + }), + ]); + } catch (error) { + if (error instanceof AppError) throw error; + throw new AppError('PROVIDER_UNAVAILABLE', undefined, error); + } finally { + if (timeout) clearTimeout(timeout); + } +} + +export class MediaService { + constructor( + private readonly repository: MediaRepository, + private readonly provider: MediaProvider, + private readonly maxBytes: number, + ) {} + + async list(context: MembershipContext, postId: string): Promise<MediaAsset[]> { + authorize(context.role, 'workspace.read'); + if (!await this.repository.findPostOwner(context.workspaceId, postId)) throw new AppError('NOT_FOUND'); + return this.repository.list(context.workspaceId, postId); + } + + async upload(context: MembershipContext, raw: Readonly<{ + postId: unknown; replaceAssetId?: unknown; altText?: unknown; fileName: string; + declaredMimeType: string; data: Buffer; + }>, requestId: string): Promise<MediaAsset> { + const fields = mediaUploadFieldsSchema.parse({ postId: raw.postId, replaceAssetId: raw.replaceAssetId || undefined, altText: raw.altText }); + const ownerId = await this.repository.findPostOwner(context.workspaceId, fields.postId); + if (!ownerId) throw new AppError('NOT_FOUND'); + authorize(context.role, 'media.upload', { actorId: context.userId, ownerId }); + const image = await verifyImage({ data: raw.data, fileName: raw.fileName, declaredMimeType: raw.declaredMimeType, maxBytes: this.maxBytes }); + const storageKey = `${context.workspaceId}/${crypto.randomUUID()}`; + try { + await providerOperation((signal) => this.provider.put(storageKey, image.data, signal)); + } catch (error) { + await this.repository.enqueueOrphanCleanup(context.workspaceId, storageKey, requestId); + throw error; + } + try { + return await this.repository.finalize({ + workspaceId: context.workspaceId, postId: fields.postId, actorId: context.userId, + requestId, storageKey, image, altText: fields.altText, + ...(fields.replaceAssetId ? { replaceAssetId: fields.replaceAssetId } : {}), + }); + } catch (error) { + try { + await providerOperation((signal) => this.provider.delete(storageKey, signal)); + } catch { + await this.repository.enqueueOrphanCleanup(context.workspaceId, storageKey, requestId); + } + throw error; + } + } + + async read(context: MembershipContext, assetId: string): Promise<Readonly<{ asset: MediaAsset; data: Buffer }>> { + authorize(context.role, 'workspace.read'); + const asset = await this.repository.find(context.workspaceId, assetId); + if (!asset || asset.status !== 'active') throw new AppError('NOT_FOUND'); + const data = await providerOperation((signal) => this.provider.get(asset.storageKey, signal)); + if (!data) throw new AppError('NOT_FOUND'); + return { asset, data }; + } + + async delete(context: MembershipContext, assetId: string, requestId: string): Promise<void> { + authorize(context.role, 'media.delete'); + await this.repository.markForDeletion({ workspaceId: context.workspaceId, assetId, actorId: context.userId, requestId }); + } +} + +let mediaService: MediaService | undefined; + +export function getMediaService(): MediaService { + if (!mediaService) { + const database = getDatabase(); + mediaService = new MediaService(new DrizzleMediaRepository(database), new DatabaseMediaProvider(database), getEnvironment().MEDIA_MAX_BYTES); + } + return mediaService; +} diff --git a/src/platform/auth/auth.ts b/src/platform/auth/auth.ts new file mode 100644 index 0000000..a571922 --- /dev/null +++ b/src/platform/auth/auth.ts @@ -0,0 +1,50 @@ +import { betterAuth } from 'better-auth'; +import { drizzleAdapter } from 'better-auth/adapters/drizzle'; + +import { getEnvironment } from '@/src/platform/config/env'; +import { getDatabase, type DatabaseContext } from '@/src/platform/db/client'; +import { accounts, authRateLimits, sessions, users, verifications } from '@/src/platform/db/schema'; + +export function createAuthentication(database: DatabaseContext, options: Readonly<{ baseURL: string; secret: string; signInRateLimit?: number }>) { + return betterAuth({ + appName: 'AutoBlog CMS', + baseURL: options.baseURL, + secret: options.secret, + database: drizzleAdapter(database.db, { + provider: 'sqlite', + schema: { user: users, session: sessions, account: accounts, verification: verifications, rateLimit: authRateLimits }, + }), + trustedOrigins: [options.baseURL], + emailAndPassword: { + enabled: true, + disableSignUp: true, + minPasswordLength: 12, + maxPasswordLength: 128, + }, + session: { + expiresIn: 60 * 60 * 12, + updateAge: 60 * 60, + cookieCache: { enabled: false }, + }, + rateLimit: { + enabled: true, + storage: 'database', + window: 60, + max: 60, + customRules: { + '/sign-in/email': { window: 60, max: options.signInRateLimit ?? 8 }, + }, + }, + advanced: { + cookiePrefix: 'autoblog', + useSecureCookies: options.baseURL.startsWith('https://'), + }, + }); +} + +const environment = getEnvironment(); +export const auth = createAuthentication(getDatabase(), { + baseURL: environment.NEXT_PUBLIC_APP_URL, + secret: environment.BETTER_AUTH_SECRET, + signInRateLimit: environment.AUTH_SIGN_IN_RATE_LIMIT, +}); diff --git a/src/platform/auth/client.ts b/src/platform/auth/client.ts new file mode 100644 index 0000000..9268ef7 --- /dev/null +++ b/src/platform/auth/client.ts @@ -0,0 +1,5 @@ +'use client'; + +import { createAuthClient } from 'better-auth/react'; + +export const authClient = createAuthClient(); diff --git a/src/platform/auth/job-runner.ts b/src/platform/auth/job-runner.ts new file mode 100644 index 0000000..67aaf01 --- /dev/null +++ b/src/platform/auth/job-runner.ts @@ -0,0 +1,22 @@ +import { createHash, timingSafeEqual } from 'node:crypto'; + +import { DEMO_WORKSPACE_ID } from '@/src/modules/identity/demo'; +import { authorize } from '@/src/modules/identity/policy'; +import { assertTrustedMutationOrigin } from '@/src/platform/auth/origin'; +import { requireMembership } from '@/src/platform/auth/session'; +import { getEnvironment } from '@/src/platform/config/env'; + +function matchesSecret(candidate: string | null, expected: string): boolean { + if (!candidate) return false; + const candidateHash = createHash('sha256').update(candidate).digest(); + const expectedHash = createHash('sha256').update(expected).digest(); + return timingSafeEqual(candidateHash, expectedHash); +} + +export async function authorizeJobRunner(request: Request): Promise<'scheduler' | 'member'> { + if (matchesSecret(request.headers.get('x-autoblog-cron-secret'), getEnvironment().CRON_SECRET)) return 'scheduler'; + const membership = await requireMembership(request.headers, DEMO_WORKSPACE_ID); + assertTrustedMutationOrigin(request); + authorize(membership.role, 'jobs.run'); + return 'member'; +} diff --git a/src/platform/auth/origin.ts b/src/platform/auth/origin.ts new file mode 100644 index 0000000..eb24f53 --- /dev/null +++ b/src/platform/auth/origin.ts @@ -0,0 +1,9 @@ +import { getEnvironment } from '@/src/platform/config/env'; +import { AppError } from '@/src/platform/observability/errors'; + +export function assertTrustedMutationOrigin(request: Request): void { + const origin = request.headers.get('origin'); + const fetchSite = request.headers.get('sec-fetch-site'); + if (fetchSite === 'cross-site') throw new AppError('FORBIDDEN'); + if (!origin || origin !== new URL(getEnvironment().NEXT_PUBLIC_APP_URL).origin) throw new AppError('FORBIDDEN'); +} diff --git a/src/platform/auth/session.ts b/src/platform/auth/session.ts new file mode 100644 index 0000000..b307ac1 --- /dev/null +++ b/src/platform/auth/session.ts @@ -0,0 +1,33 @@ +import { and, eq } from 'drizzle-orm'; + +import { auth } from '@/src/platform/auth/auth'; +import { getDatabase } from '@/src/platform/db/client'; +import { memberships, workspaces } from '@/src/platform/db/schema'; +import { roleSchema, type MembershipContext } from '@/src/modules/identity/domain'; +import { AppError } from '@/src/platform/observability/errors'; + +export async function requireMembership(requestHeaders: Headers, requestedWorkspaceId: string): Promise<MembershipContext> { + const session = await auth.api.getSession({ headers: requestHeaders }); + if (!session) throw new AppError('UNAUTHENTICATED'); + + const [row] = await getDatabase().db + .select({ + workspaceId: workspaces.id, + workspaceSlug: workspaces.slug, + workspaceName: workspaces.name, + role: memberships.role, + }) + .from(memberships) + .innerJoin(workspaces, eq(workspaces.id, memberships.workspaceId)) + .where(and(eq(memberships.userId, session.user.id), eq(memberships.workspaceId, requestedWorkspaceId))) + .limit(1); + + if (!row) throw new AppError('FORBIDDEN'); + return { + ...row, + role: roleSchema.parse(row.role), + userId: session.user.id, + userName: session.user.name, + userEmail: session.user.email, + }; +} diff --git a/src/platform/config/env.ts b/src/platform/config/env.ts new file mode 100644 index 0000000..aafeee0 --- /dev/null +++ b/src/platform/config/env.ts @@ -0,0 +1,44 @@ +import { z } from 'zod'; + +const DEVELOPMENT_SECRET = 'development-only-autoblog-secret-change-me'; +const DEVELOPMENT_CRON_SECRET = 'development-only-cron-secret'; + +const booleanValue = z.string().optional().transform((value) => value === 'true'); + +const environmentSchema = z.object({ + NODE_ENV: z.enum(['development', 'test', 'production']).default('development'), + NEXT_PUBLIC_APP_URL: z.url().default('http://localhost:3000'), + DATABASE_URL: z.string().min(1).default('file:./data/autoblog.db'), + DATABASE_AUTH_TOKEN: z.string().optional(), + BETTER_AUTH_SECRET: z.string().min(32).default(DEVELOPMENT_SECRET), + DEMO_ENABLED: booleanValue, + AI_MODE: z.enum(['mock', 'gemini']).default('mock'), + GEMINI_API_KEY: z.string().optional(), + GEMINI_MODEL: z.string().min(1).default('gemini-2.5-flash'), + CRON_SECRET: z.string().min(24).default(DEVELOPMENT_CRON_SECRET), + AI_MONTHLY_CHARACTER_QUOTA: z.coerce.number().int().positive().default(200_000), + MEDIA_MAX_BYTES: z.coerce.number().int().positive().max(10 * 1024 * 1024).default(5 * 1024 * 1024), + AUTH_SIGN_IN_RATE_LIMIT: z.coerce.number().int().min(3).max(100).default(8), +}); + +export type Environment = z.infer<typeof environmentSchema>; + +export function getEnvironment(source: NodeJS.ProcessEnv = process.env): Environment { + return environmentSchema.parse(source); +} + +export function assertRuntimeConfiguration(environment = getEnvironment()): void { + if (environment.NODE_ENV === 'production' && environment.BETTER_AUTH_SECRET === DEVELOPMENT_SECRET) { + throw new Error('BETTER_AUTH_SECRET_REQUIRED'); + } + if (environment.NODE_ENV === 'production' && environment.CRON_SECRET === DEVELOPMENT_CRON_SECRET) { + throw new Error('CRON_SECRET_REQUIRED'); + } + if (environment.AI_MODE === 'gemini' && !environment.GEMINI_API_KEY) { + throw new Error('GEMINI_API_KEY_REQUIRED'); + } +} + +export function isRemoteDatabase(url: string): boolean { + return !url.startsWith('file:') && url !== ':memory:'; +} diff --git a/src/platform/config/limits.ts b/src/platform/config/limits.ts new file mode 100644 index 0000000..32139e5 --- /dev/null +++ b/src/platform/config/limits.ts @@ -0,0 +1,8 @@ +export const PRODUCT_LIMITS = Object.freeze({ + mediaMaxBytes: 5 * 1024 * 1024, + mediaMaxWidth: 4_096, + mediaMaxHeight: 4_096, + aiPromptMaxCharacters: 8_000, + aiResponseMaxCharacters: 16_000, + jobMaxAttempts: 5, +}); diff --git a/src/platform/db/client.ts b/src/platform/db/client.ts new file mode 100644 index 0000000..c395e18 --- /dev/null +++ b/src/platform/db/client.ts @@ -0,0 +1,33 @@ +import { createClient, type Client } from '@libsql/client'; +import { drizzle, type LibSQLDatabase } from 'drizzle-orm/libsql'; +import { mkdirSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; + +import { getEnvironment } from '@/src/platform/config/env'; +import * as schema from '@/src/platform/db/schema'; + +export type Database = LibSQLDatabase<typeof schema>; +export type DatabaseContext = Readonly<{ client: Client; db: Database; url: string }>; + +export function createDatabase(url: string, authToken?: string): DatabaseContext { + if (url.startsWith('file:')) { + mkdirSync(dirname(resolve(url.slice('file:'.length))), { recursive: true }); + } + const client = createClient(authToken ? { url, authToken } : { url }); + return { client, db: drizzle(client, { schema }), url }; +} + +let applicationDatabase: DatabaseContext | undefined; + +export function getDatabase(): DatabaseContext { + if (!applicationDatabase) { + const environment = getEnvironment(); + applicationDatabase = createDatabase(environment.DATABASE_URL, environment.DATABASE_AUTH_TOKEN); + } + return applicationDatabase; +} + +export function resetDatabaseSingleton(): void { + applicationDatabase?.client.close(); + applicationDatabase = undefined; +} diff --git a/src/platform/db/migrate.ts b/src/platform/db/migrate.ts new file mode 100644 index 0000000..66d7d87 --- /dev/null +++ b/src/platform/db/migrate.ts @@ -0,0 +1,44 @@ +import { createHash } from 'node:crypto'; +import { readFile, readdir } from 'node:fs/promises'; +import { join } from 'node:path'; + +import type { Client } from '@libsql/client'; + +export async function migrateDatabase(client: Client, directory = join(process.cwd(), 'drizzle')): Promise<string[]> { + await client.execute(`CREATE TABLE IF NOT EXISTS schema_migrations ( + name TEXT PRIMARY KEY, + checksum TEXT NOT NULL, + applied_at INTEGER NOT NULL + )`); + + const names = (await readdir(directory)).filter((name) => /^\d+_[a-z0-9_-]+\.sql$/u.test(name)).sort(); + const applied: string[] = []; + + for (const name of names) { + const sql = await readFile(join(directory, name), 'utf8'); + const checksum = createHash('sha256').update(sql).digest('hex'); + const existing = await client.execute({ sql: 'SELECT checksum FROM schema_migrations WHERE name = ?', args: [name] }); + if (existing.rows.length > 0) { + if (existing.rows[0]?.checksum !== checksum) throw new Error(`MIGRATION_CHECKSUM_MISMATCH:${name}`); + continue; + } + + const transaction = await client.transaction('write'); + try { + await transaction.executeMultiple(sql); + await transaction.execute({ + sql: 'INSERT INTO schema_migrations (name, checksum, applied_at) VALUES (?, ?, ?)', + args: [name, checksum, Date.now()], + }); + await transaction.commit(); + applied.push(name); + } catch (error) { + await transaction.rollback(); + throw error; + } finally { + transaction.close(); + } + } + + return applied; +} diff --git a/src/platform/db/schema.ts b/src/platform/db/schema.ts new file mode 100644 index 0000000..241a015 --- /dev/null +++ b/src/platform/db/schema.ts @@ -0,0 +1,283 @@ +import { sql } from 'drizzle-orm'; +import { + blob, + check, + foreignKey, + index, + integer, + primaryKey, + sqliteTable, + text, + uniqueIndex, + type AnySQLiteColumn, +} from 'drizzle-orm/sqlite-core'; + +export const ROLE_VALUES = ['Owner', 'Admin', 'Editor', 'Author', 'Reviewer'] as const; +export const POST_STATE_VALUES = [ + 'Draft', + 'InReview', + 'ChangesRequested', + 'Approved', + 'Scheduled', + 'Published', + 'Archived', +] as const; + +const timestamp = (name: string) => integer(name, { mode: 'timestamp_ms' }); + +export const users = sqliteTable('user', { + id: text('id').primaryKey(), + name: text('name').notNull(), + email: text('email').notNull().unique(), + emailVerified: integer('email_verified', { mode: 'boolean' }).notNull().default(false), + image: text('image'), + createdAt: timestamp('created_at').notNull(), + updatedAt: timestamp('updated_at').notNull(), +}); + +export const sessions = sqliteTable('session', { + id: text('id').primaryKey(), + expiresAt: timestamp('expires_at').notNull(), + token: text('token').notNull().unique(), + createdAt: timestamp('created_at').notNull(), + updatedAt: timestamp('updated_at').notNull(), + ipAddress: text('ip_address'), + userAgent: text('user_agent'), + userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), +}, (table) => [index('session_user_idx').on(table.userId)]); + +export const accounts = sqliteTable('account', { + id: text('id').primaryKey(), + accountId: text('account_id').notNull(), + providerId: text('provider_id').notNull(), + userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), + accessToken: text('access_token'), + refreshToken: text('refresh_token'), + idToken: text('id_token'), + accessTokenExpiresAt: timestamp('access_token_expires_at'), + refreshTokenExpiresAt: timestamp('refresh_token_expires_at'), + scope: text('scope'), + password: text('password'), + createdAt: timestamp('created_at').notNull(), + updatedAt: timestamp('updated_at').notNull(), +}, (table) => [ + index('account_user_idx').on(table.userId), + uniqueIndex('account_provider_unique').on(table.providerId, table.accountId), +]); + +export const verifications = sqliteTable('verification', { + id: text('id').primaryKey(), + identifier: text('identifier').notNull(), + value: text('value').notNull(), + expiresAt: timestamp('expires_at').notNull(), + createdAt: timestamp('created_at'), + updatedAt: timestamp('updated_at'), +}, (table) => [index('verification_identifier_idx').on(table.identifier)]); + +export const authRateLimits = sqliteTable('rateLimit', { + id: text('id').primaryKey(), + key: text('key').notNull().unique(), + count: integer('count').notNull(), + lastRequest: integer('lastRequest').notNull(), +}); + +export const workspaces = sqliteTable('workspaces', { + id: text('id').primaryKey(), + slug: text('slug').notNull().unique(), + name: text('name').notNull(), + isDemo: integer('is_demo', { mode: 'boolean' }).notNull().default(false), + createdAt: timestamp('created_at').notNull(), + updatedAt: timestamp('updated_at').notNull(), +}); + +export const memberships = sqliteTable('memberships', { + workspaceId: text('workspace_id').notNull().references(() => workspaces.id, { onDelete: 'cascade' }), + userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), + role: text('role', { enum: ROLE_VALUES }).notNull(), + createdAt: timestamp('created_at').notNull(), +}, (table) => [ + primaryKey({ columns: [table.workspaceId, table.userId] }), + index('membership_user_idx').on(table.userId), + check('membership_role_check', sql`${table.role} in ('Owner','Admin','Editor','Author','Reviewer')`), +]); + +export const posts = sqliteTable('posts', { + id: text('id').primaryKey(), + workspaceId: text('workspace_id').notNull().references(() => workspaces.id, { onDelete: 'cascade' }), + slug: text('slug').notNull(), + title: text('title').notNull(), + excerpt: text('excerpt').notNull().default(''), + state: text('state', { enum: POST_STATE_VALUES }).notNull().default('Draft'), + version: integer('version').notNull().default(1), + authorId: text('author_id').notNull().references(() => users.id, { onDelete: 'restrict' }), + draftRevisionId: text('draft_revision_id').references((): AnySQLiteColumn => revisions.id, { onDelete: 'set null' }), + publishedRevisionId: text('published_revision_id').references((): AnySQLiteColumn => revisions.id, { onDelete: 'set null' }), + scheduledFor: timestamp('scheduled_for'), + submittedAt: timestamp('submitted_at'), + approvedAt: timestamp('approved_at'), + publishedAt: timestamp('published_at'), + archivedAt: timestamp('archived_at'), + createdAt: timestamp('created_at').notNull(), + updatedAt: timestamp('updated_at').notNull(), +}, (table) => [ + uniqueIndex('post_workspace_slug_unique').on(table.workspaceId, table.slug), + uniqueIndex('post_workspace_id_unique').on(table.workspaceId, table.id), + index('post_workspace_state_idx').on(table.workspaceId, table.state), + check('post_version_positive', sql`${table.version} > 0`), +]); + +export const revisions = sqliteTable('revisions', { + id: text('id').primaryKey(), + workspaceId: text('workspace_id').notNull().references(() => workspaces.id, { onDelete: 'cascade' }), + postId: text('post_id').notNull(), + version: integer('version').notNull(), + title: text('title').notNull(), + excerpt: text('excerpt').notNull().default(''), + content: text('content').notNull(), + authorId: text('author_id').notNull().references(() => users.id, { onDelete: 'restrict' }), + restoredFromRevisionId: text('restored_from_revision_id'), + createdAt: timestamp('created_at').notNull(), +}, (table) => [ + foreignKey({ columns: [table.workspaceId, table.postId], foreignColumns: [posts.workspaceId, posts.id] }).onDelete('cascade'), + uniqueIndex('revision_post_version_unique').on(table.postId, table.version), + uniqueIndex('revision_workspace_id_unique').on(table.workspaceId, table.id), + index('revision_workspace_post_idx').on(table.workspaceId, table.postId), +]); + +export const publications = sqliteTable('publications', { + id: text('id').primaryKey(), + workspaceId: text('workspace_id').notNull().references(() => workspaces.id, { onDelete: 'cascade' }), + postId: text('post_id').notNull(), + revisionId: text('revision_id').notNull().references(() => revisions.id, { onDelete: 'restrict' }), + status: text('status', { enum: ['scheduled', 'published', 'cancelled'] }).notNull(), + scheduledFor: timestamp('scheduled_for'), + publishedAt: timestamp('published_at'), + idempotencyKey: text('idempotency_key').notNull().unique(), + createdBy: text('created_by').notNull().references(() => users.id, { onDelete: 'restrict' }), + createdAt: timestamp('created_at').notNull(), +}, (table) => [ + foreignKey({ columns: [table.workspaceId, table.postId], foreignColumns: [posts.workspaceId, posts.id] }).onDelete('cascade'), + index('publication_due_idx').on(table.status, table.scheduledFor), +]); + +export const mediaAssets = sqliteTable('media_assets', { + id: text('id').primaryKey(), + workspaceId: text('workspace_id').notNull().references(() => workspaces.id, { onDelete: 'cascade' }), + postId: text('post_id'), + status: text('status', { enum: ['pending', 'active', 'replaced', 'deleted', 'cleanup_pending'] }).notNull(), + storageKey: text('storage_key').notNull().unique(), + fileName: text('file_name').notNull(), + mimeType: text('mime_type').notNull(), + byteSize: integer('byte_size').notNull(), + width: integer('width').notNull(), + height: integer('height').notNull(), + checksum: text('checksum').notNull(), + altText: text('alt_text').notNull().default(''), + createdBy: text('created_by').notNull().references(() => users.id, { onDelete: 'restrict' }), + createdAt: timestamp('created_at').notNull(), + updatedAt: timestamp('updated_at').notNull(), + replacesAssetId: text('replaces_asset_id'), +}, (table) => [ + index('media_workspace_post_idx').on(table.workspaceId, table.postId), + uniqueIndex('media_one_active_per_post').on(table.workspaceId, table.postId).where(sql`${table.status} = 'active' and ${table.postId} is not null`), + check('media_byte_size_positive', sql`${table.byteSize} > 0`), +]); + +export const mediaObjects = sqliteTable('media_objects', { + storageKey: text('storage_key').primaryKey(), + data: blob('data', { mode: 'buffer' }).notNull(), + createdAt: timestamp('created_at').notNull(), +}); + +export const auditEvents = sqliteTable('audit_events', { + id: text('id').primaryKey(), + workspaceId: text('workspace_id').notNull().references(() => workspaces.id, { onDelete: 'cascade' }), + actorId: text('actor_id').references(() => users.id, { onDelete: 'set null' }), + action: text('action').notNull(), + targetType: text('target_type').notNull(), + targetId: text('target_id'), + requestId: text('request_id').notNull(), + metadata: text('metadata', { mode: 'json' }).$type<Record<string, unknown>>().notNull().default({}), + createdAt: timestamp('created_at').notNull(), +}, (table) => [index('audit_workspace_created_idx').on(table.workspaceId, table.createdAt)]); + +export const aiUsage = sqliteTable('ai_usage', { + id: text('id').primaryKey(), + workspaceId: text('workspace_id').notNull().references(() => workspaces.id, { onDelete: 'cascade' }), + userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }), + mode: text('mode', { enum: ['mock', 'gemini'] }).notNull(), + provider: text('provider').notNull(), + model: text('model').notNull(), + latencyMs: integer('latency_ms').notNull(), + inputCharacters: integer('input_characters').notNull(), + outputCharacters: integer('output_characters').notNull(), + inputTokens: integer('input_tokens'), + outputTokens: integer('output_tokens'), + createdAt: timestamp('created_at').notNull(), +}, (table) => [index('ai_usage_workspace_created_idx').on(table.workspaceId, table.createdAt)]); + +export const jobs = sqliteTable('jobs', { + id: text('id').primaryKey(), + workspaceId: text('workspace_id').notNull().references(() => workspaces.id, { onDelete: 'cascade' }), + type: text('type', { enum: ['publish', 'media_cleanup'] }).notNull(), + payload: text('payload', { mode: 'json' }).$type<Record<string, unknown>>().notNull(), + status: text('status', { enum: ['pending', 'leased', 'completed', 'failed'] }).notNull().default('pending'), + attempts: integer('attempts').notNull().default(0), + runAt: timestamp('run_at').notNull(), + leaseUntil: timestamp('lease_until'), + lastErrorCode: text('last_error_code'), + idempotencyKey: text('idempotency_key').notNull().unique(), + createdAt: timestamp('created_at').notNull(), + updatedAt: timestamp('updated_at').notNull(), +}, (table) => [index('job_claim_idx').on(table.status, table.runAt, table.leaseUntil)]); + +export const rateLimits = sqliteTable('rate_limits', { + key: text('key').notNull(), + windowStartedAt: timestamp('window_started_at').notNull(), + count: integer('count').notNull().default(0), + expiresAt: timestamp('expires_at').notNull(), +}, (table) => [ + primaryKey({ columns: [table.key, table.windowStartedAt] }), + index('rate_limit_expiry_idx').on(table.expiresAt), +]); + +export const aiQuotaWindows = sqliteTable('ai_quota_windows', { + workspaceId: text('workspace_id').notNull().references(() => workspaces.id, { onDelete: 'cascade' }), + windowStartedAt: timestamp('window_started_at').notNull(), + reservedCharacters: integer('reserved_characters').notNull().default(0), + usedCharacters: integer('used_characters').notNull().default(0), + updatedAt: timestamp('updated_at').notNull(), +}, (table) => [ + primaryKey({ columns: [table.workspaceId, table.windowStartedAt] }), + check('ai_quota_reserved_nonnegative', sql`${table.reservedCharacters} >= 0`), + check('ai_quota_used_nonnegative', sql`${table.usedCharacters} >= 0`), +]); + +export const idempotencyRecords = sqliteTable('idempotency_records', { + workspaceId: text('workspace_id').notNull(), + operation: text('operation').notNull(), + key: text('key').notNull(), + result: text('result', { mode: 'json' }).$type<Record<string, unknown>>().notNull(), + createdAt: timestamp('created_at').notNull(), +}, (table) => [primaryKey({ columns: [table.workspaceId, table.operation, table.key] })]); + +export const schema = { + users, + sessions, + accounts, + verifications, + authRateLimits, + workspaces, + memberships, + posts, + revisions, + publications, + mediaAssets, + mediaObjects, + auditEvents, + aiUsage, + jobs, + rateLimits, + aiQuotaWindows, + idempotencyRecords, +}; diff --git a/src/platform/db/seed.ts b/src/platform/db/seed.ts new file mode 100644 index 0000000..6bceeb9 --- /dev/null +++ b/src/platform/db/seed.ts @@ -0,0 +1,174 @@ +import { hashPassword } from 'better-auth/crypto'; +import { eq, like } from 'drizzle-orm'; +import { z } from 'zod'; + +import { DEMO_IDENTITIES, DEMO_PASSWORD, DEMO_WORKSPACE_ID, DEMO_WORKSPACE_SLUG } from '@/src/modules/identity/demo'; +import type { DatabaseContext } from '@/src/platform/db/client'; +import { + accounts, + auditEvents, + memberships, + mediaObjects, + posts, + publications, + revisions, + users, + workspaces, +} from '@/src/platform/db/schema'; + +const revisionFixtureSchema = z.object({ + id: z.string().min(1), + version: z.number().int().positive(), + title: z.string().min(1).max(180), + excerpt: z.string().max(320), + content: z.string().min(1).max(100_000), +}); + +const postFixtureSchema = z.object({ + id: z.string().min(1), + slug: z.string().regex(/^[a-z0-9-]+$/u), + state: z.enum(['Draft', 'InReview', 'ChangesRequested', 'Approved', 'Scheduled', 'Published', 'Archived']), + version: z.number().int().positive(), + revisions: z.array(revisionFixtureSchema).min(1), +}); + +const DEMO_POSTS = postFixtureSchema.array().parse([ + { + id: 'post-editorial-systems', + slug: 'designing-editorial-systems', + state: 'Draft', + version: 3, + revisions: [ + { id: 'rev-editorial-1', version: 1, title: 'Designing editorial systems', excerpt: 'A working outline for the spring edition.', content: '# Designing editorial systems\n\nEditorial confidence starts with explicit ownership.' }, + { id: 'rev-editorial-2', version: 2, title: 'Designing the editorial operating system', excerpt: 'Why reliable publishing needs more than a polished editor.', content: '# Designing the editorial operating system\n\nA durable workflow connects authors, reviewers and publication evidence.' }, + { id: 'rev-editorial-3', version: 3, title: 'Designing the next editorial operating system', excerpt: 'A practical model for fast teams that refuse silent data loss.', content: '# Designing the next editorial operating system\n\nFast editorial teams need visible state, immutable history and bounded automation.\n\n## The operating model\n\nDraft deliberately. Review with context. Publish an immutable revision.' }, + ], + }, + { + id: 'post-ai-governance', + slug: 'ai-governance-for-editors', + state: 'InReview', + version: 2, + revisions: [ + { id: 'rev-ai-1', version: 1, title: 'AI governance for editors', excerpt: 'A checklist for responsible assistance.', content: '# AI governance for editors\n\nTreat generation as a suggestion, never a silent mutation.' }, + { id: 'rev-ai-2', version: 2, title: 'AI governance that editors can operate', excerpt: 'Quotas, disclosure and explicit acceptance in one workflow.', content: '# AI governance that editors can operate\n\nLabel the active provider mode, meter usage and keep the editor accountable for acceptance.' }, + ], + }, + { + id: 'post-immutable-publishing', + slug: 'immutable-publishing', + state: 'Published', + version: 1, + revisions: [ + { id: 'rev-published-1', version: 1, title: 'Immutable publishing, practical trust', excerpt: 'How revision pointers protect the public record.', content: '# Immutable publishing, practical trust\n\nThe public page points to one revision. Later draft work cannot rewrite what readers already saw.' }, + ], + }, +]); + +export async function seedDemo(database: DatabaseContext, options: Readonly<{ reset?: boolean; requestId?: string }> = {}): Promise<void> { + const now = new Date(); + const password = await hashPassword(DEMO_PASSWORD); + + for (const identity of DEMO_IDENTITIES) { + await database.db.insert(users).values({ + id: identity.id, + name: identity.name, + email: identity.email, + emailVerified: true, + createdAt: now, + updatedAt: now, + }).onConflictDoNothing(); + await database.db.insert(accounts).values({ + id: `account-${identity.id}`, + accountId: identity.id, + providerId: 'credential', + userId: identity.id, + password, + createdAt: now, + updatedAt: now, + }).onConflictDoNothing(); + } + + if (options.reset) { + await database.db.delete(mediaObjects).where(like(mediaObjects.storageKey, `${DEMO_WORKSPACE_ID}/%`)); + await database.db.delete(workspaces).where(eq(workspaces.id, DEMO_WORKSPACE_ID)); + } + + const existing = await database.db.select({ id: workspaces.id }).from(workspaces).where(eq(workspaces.id, DEMO_WORKSPACE_ID)).limit(1); + if (existing.length > 0) return; + + await database.db.insert(workspaces).values({ + id: DEMO_WORKSPACE_ID, + slug: DEMO_WORKSPACE_SLUG, + name: 'AutoBlog Editorial Lab', + isDemo: true, + createdAt: now, + updatedAt: now, + }); + + await database.db.insert(memberships).values(DEMO_IDENTITIES.map((identity) => ({ + workspaceId: DEMO_WORKSPACE_ID, + userId: identity.id, + role: identity.role, + createdAt: now, + }))); + + for (const post of DEMO_POSTS) { + const authorId = 'demo-author'; + const currentRevision = post.revisions.at(-1); + if (!currentRevision) throw new Error('SEED_REVISION_REQUIRED'); + await database.db.insert(posts).values({ + id: post.id, + workspaceId: DEMO_WORKSPACE_ID, + slug: post.slug, + title: currentRevision.title, + excerpt: currentRevision.excerpt, + state: post.state, + version: post.version, + authorId, + createdAt: now, + updatedAt: now, + ...(post.state === 'InReview' ? { submittedAt: now } : {}), + ...(post.state === 'Published' ? { approvedAt: now, publishedAt: now } : {}), + }); + + await database.db.insert(revisions).values(post.revisions.map((revision) => ({ + ...revision, + workspaceId: DEMO_WORKSPACE_ID, + postId: post.id, + authorId, + createdAt: new Date(now.getTime() - (post.version - revision.version) * 3_600_000), + }))); + + await database.db.update(posts).set({ + draftRevisionId: currentRevision.id, + ...(post.state === 'Published' ? { publishedRevisionId: currentRevision.id } : {}), + }).where(eq(posts.id, post.id)); + + if (post.state === 'Published') { + await database.db.insert(publications).values({ + id: 'publication-seeded', + workspaceId: DEMO_WORKSPACE_ID, + postId: post.id, + revisionId: currentRevision.id, + status: 'published', + publishedAt: now, + idempotencyKey: `seed-publish:${post.id}:${currentRevision.id}`, + createdBy: 'demo-editor', + createdAt: now, + }); + } + } + + await database.db.insert(auditEvents).values({ + id: crypto.randomUUID(), + workspaceId: DEMO_WORKSPACE_ID, + actorId: 'demo-owner', + action: options.reset ? 'demo.reset' : 'demo.seed', + targetType: 'workspace', + targetId: DEMO_WORKSPACE_ID, + requestId: options.requestId ?? 'seed-script', + metadata: { fixtureVersion: 1 }, + createdAt: now, + }); +} diff --git a/src/platform/observability/api.ts b/src/platform/observability/api.ts new file mode 100644 index 0000000..997c037 --- /dev/null +++ b/src/platform/observability/api.ts @@ -0,0 +1,25 @@ +import { normalizeError } from '@/src/platform/observability/errors'; + +type ApiHandler = (request: Request, requestId: string) => Promise<Response>; + +export function withApi(handler: ApiHandler): (request: Request) => Promise<Response> { + return async (request) => { + const requestId = request.headers.get('x-request-id')?.slice(0, 100) || crypto.randomUUID(); + try { + const response = await handler(request, requestId); + response.headers.set('x-request-id', requestId); + return response; + } catch (unknownError) { + const error = normalizeError(unknownError); + const internal = error.cause instanceof Error ? error.cause.message : undefined; + console.error(JSON.stringify({ level: 'error', requestId, code: error.code, path: new URL(request.url).pathname, internal })); + return Response.json({ error: { code: error.code, message: error.message, requestId, ...(error.details ? { details: error.details } : {}) } }, { status: error.status, headers: { 'x-request-id': requestId } }); + } + }; +} + +export function dataResponse<T>(data: T, init?: ResponseInit): Response { + const headers = new Headers(init?.headers); + if (!headers.has('cache-control')) headers.set('cache-control', 'private, no-store'); + return Response.json({ data }, { ...init, headers }); +} diff --git a/src/platform/observability/errors.ts b/src/platform/observability/errors.ts new file mode 100644 index 0000000..4282bb2 --- /dev/null +++ b/src/platform/observability/errors.ts @@ -0,0 +1,48 @@ +import { z } from 'zod'; + +export const errorCodeSchema = z.enum([ + 'UNAUTHENTICATED', + 'FORBIDDEN', + 'NOT_FOUND', + 'VALIDATION_FAILED', + 'VERSION_CONFLICT', + 'ILLEGAL_TRANSITION', + 'QUOTA_EXCEEDED', + 'RATE_LIMITED', + 'PROVIDER_UNAVAILABLE', + 'INTERNAL_FAILURE', +]); +export type ErrorCode = z.infer<typeof errorCodeSchema>; + +const PUBLIC_ERRORS: Readonly<Record<ErrorCode, { status: number; message: string }>> = { + UNAUTHENTICATED: { status: 401, message: 'Authentication is required.' }, + FORBIDDEN: { status: 403, message: 'You do not have permission to perform this action.' }, + NOT_FOUND: { status: 404, message: 'The requested resource was not found.' }, + VALIDATION_FAILED: { status: 422, message: 'The request did not pass validation.' }, + VERSION_CONFLICT: { status: 409, message: 'A newer version exists. Reload or compare before saving.' }, + ILLEGAL_TRANSITION: { status: 409, message: 'The requested editorial transition is not allowed.' }, + QUOTA_EXCEEDED: { status: 429, message: 'The workspace quota has been reached.' }, + RATE_LIMITED: { status: 429, message: 'Too many requests. Try again later.' }, + PROVIDER_UNAVAILABLE: { status: 503, message: 'The configured provider is temporarily unavailable.' }, + INTERNAL_FAILURE: { status: 500, message: 'The request could not be completed.' }, +}; + +export class AppError extends Error { + readonly code: ErrorCode; + readonly status: number; + readonly details: Readonly<Record<string, unknown>> | undefined; + + constructor(code: ErrorCode, details?: Readonly<Record<string, unknown>>, cause?: unknown) { + super(PUBLIC_ERRORS[code].message, { cause }); + this.name = 'AppError'; + this.code = code; + this.status = PUBLIC_ERRORS[code].status; + this.details = details; + } +} + +export function normalizeError(error: unknown): AppError { + if (error instanceof AppError) return error; + if (error instanceof z.ZodError) return new AppError('VALIDATION_FAILED', { issues: error.issues.map((issue) => ({ path: issue.path.join('.'), code: issue.code })) }); + return new AppError('INTERNAL_FAILURE', undefined, error); +} diff --git a/src/ui/patterns/ai-suggestion-panel.tsx b/src/ui/patterns/ai-suggestion-panel.tsx new file mode 100644 index 0000000..32eddb0 --- /dev/null +++ b/src/ui/patterns/ai-suggestion-panel.tsx @@ -0,0 +1,49 @@ +'use client'; + +import { useState } from 'react'; + +import type { AISuggestionResult } from '@/src/modules/ai/domain'; +import type { PostDetail } from '@/src/modules/editorial/domain'; +import type { MembershipContext } from '@/src/modules/identity/domain'; + +type ApiEnvelope<T> = { data: T } | { error: { code: string; message: string; requestId: string } }; +const hasData = <T,>(envelope: ApiEnvelope<T>): envelope is { data: T } => 'data' in envelope; + +export function AISuggestionPanel({ context, post, canApply, configuredMode, onApply }: Readonly<{ + context: MembershipContext; + post: PostDetail; + canApply: boolean; + configuredMode: string; + onApply: (suggestion: AISuggestionResult['suggestion']) => void; +}>) { + const [instruction, setInstruction] = useState('Improve structure and sharpen the editorial angle.'); + const [result, setResult] = useState<AISuggestionResult | null>(null); + const [busy, setBusy] = useState(false); + const [message, setMessage] = useState('AI never changes the post until Apply suggestion is selected.'); + + async function suggest() { + setBusy(true); setResult(null); setMessage('Generating a bounded suggestion…'); + const response = await fetch(`/api/workspaces/${context.workspaceId}/ai/suggest`, { + method: 'POST', headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ postId: post.id, title: post.title, excerpt: post.excerpt, content: post.content, instruction }), + }); + const envelope = await response.json() as ApiEnvelope<AISuggestionResult>; + setBusy(false); + if (!response.ok || !hasData(envelope)) { setMessage('Suggestion unavailable. Quota, rate or provider policy rejected the request.'); return; } + setResult(envelope.data); + setMessage(`${envelope.data.mode === 'mock' ? 'Mock demo' : 'Live Gemini'} suggestion ready. ${envelope.data.remainingCharacters.toLocaleString()} characters remain in this workspace window.`); + } + + return <section className="ai-panel" aria-labelledby="ai-heading"> + <div><p className="eyebrow">AI assistance · {configuredMode === 'gemini' ? 'Live configured mode' : 'Deterministic mock mode'}</p><h2 id="ai-heading">Suggest, inspect, then apply</h2></div> + <label htmlFor={`ai-instruction-${post.id}`}>Editorial instruction<textarea id={`ai-instruction-${post.id}`} maxLength={500} value={instruction} onChange={(event) => setInstruction(event.target.value)} /></label> + <button type="button" disabled={busy || instruction.trim().length < 3} onClick={() => void suggest()}>{busy ? 'Generating…' : 'Generate suggestion'}</button> + <p className="panel-message" role="status">{message}</p> + {result ? <article className="suggestion-preview" aria-label="AI suggestion preview"> + <span>{result.provider} · {result.model} · {result.latencyMs} ms</span> + <h3>{result.suggestion.title}</h3><p>{result.suggestion.excerpt}</p><pre>{result.suggestion.content}</pre><small>{result.suggestion.rationale}</small> + <button type="button" disabled={!canApply} onClick={() => { onApply(result.suggestion); setMessage('Suggestion applied locally. Autosave will create a user-authored revision.'); }}>Apply suggestion</button> + {!canApply ? <p>Read-only role: preview is available, application is not authorized.</p> : null} + </article> : null} + </section>; +} diff --git a/src/ui/patterns/media-panel.tsx b/src/ui/patterns/media-panel.tsx new file mode 100644 index 0000000..3f4586a --- /dev/null +++ b/src/ui/patterns/media-panel.tsx @@ -0,0 +1,66 @@ +'use client'; + +import { useEffect, useRef, useState } from 'react'; + +import type { PostDetail } from '@/src/modules/editorial/domain'; +import type { MembershipContext } from '@/src/modules/identity/domain'; +import { can } from '@/src/modules/identity/policy'; +import type { MediaAsset } from '@/src/modules/media/domain'; + +type ApiEnvelope<T> = { data: T } | { error: { code: string; message: string; requestId: string } }; +const hasData = <T,>(envelope: ApiEnvelope<T>): envelope is { data: T } => 'data' in envelope; + +export function MediaPanel({ context, post }: Readonly<{ context: MembershipContext; post: PostDetail }>) { + const [asset, setAsset] = useState<MediaAsset | null>(null); + const [file, setFile] = useState<File | null>(null); + const [altText, setAltText] = useState(''); + const [busy, setBusy] = useState(false); + const [message, setMessage] = useState(''); + const fileInput = useRef<HTMLInputElement>(null); + const canUpload = can(context.role, 'media.upload', { actorId: context.userId, ownerId: post.authorId }); + const canDelete = can(context.role, 'media.delete'); + + useEffect(() => { + let current = true; + void fetch(`/api/workspaces/${context.workspaceId}/media?postId=${encodeURIComponent(post.id)}`) + .then(async (response) => response.json() as Promise<ApiEnvelope<MediaAsset[]>>) + .then((envelope) => { if (current && hasData(envelope)) setAsset(envelope.data[0] ?? null); }) + .catch(() => { if (current) setMessage('Media metadata is temporarily unavailable.'); }); + return () => { current = false; }; + }, [context.workspaceId, post.id]); + + async function upload() { + if (!file) { setMessage('Choose a PNG, JPEG or WebP image.'); return; } + setBusy(true); setMessage('Verifying image…'); + const body = new FormData(); + body.set('file', file); body.set('postId', post.id); body.set('altText', altText); + if (asset) body.set('replaceAssetId', asset.id); + const response = await fetch(`/api/workspaces/${context.workspaceId}/media`, { method: 'POST', body }); + const envelope = await response.json() as ApiEnvelope<MediaAsset>; + setBusy(false); + if (!response.ok || !hasData(envelope)) { setMessage('Upload rejected. Verify type, size and ownership.'); return; } + setAsset(envelope.data); setFile(null); if (fileInput.current) fileInput.current.value = ''; + setMessage(asset ? 'Replacement activated; prior object queued for cleanup.' : 'Verified image activated.'); + } + + async function remove() { + if (!asset) return; + setBusy(true); + const response = await fetch(`/api/workspaces/${context.workspaceId}/media/${asset.id}`, { method: 'DELETE' }); + setBusy(false); + if (!response.ok) { setMessage('Deletion was rejected.'); return; } + setAsset(null); setMessage('Asset removed and cleanup queued.'); + } + + return <section className="media-panel" aria-labelledby="media-heading"> + <p className="eyebrow">Verified media</p><h3 id="media-heading">Post cover</h3> + {asset ? <div className="asset-card"><strong>{asset.fileName}</strong><span>{asset.mimeType} · {asset.width}×{asset.height} · {Math.ceil(asset.byteSize / 1024)} KB</span><span>{asset.altText || 'No alternative text supplied'}</span></div> : <p className="panel-empty">No active asset.</p>} + {canUpload ? <div className="media-form"> + <label htmlFor={`media-file-${post.id}`}>PNG, JPEG or WebP · 5 MB maximum<input ref={fileInput} id={`media-file-${post.id}`} type="file" accept="image/png,image/jpeg,image/webp" onChange={(event) => setFile(event.target.files?.[0] ?? null)} /></label> + <label htmlFor={`media-alt-${post.id}`}>Alternative text<input id={`media-alt-${post.id}`} value={altText} maxLength={500} onChange={(event) => setAltText(event.target.value)} /></label> + <button type="button" disabled={busy || !file} onClick={() => void upload()}>{asset ? 'Replace safely' : 'Upload verified image'}</button> + </div> : null} + {asset && canDelete ? <button className="danger-button" type="button" disabled={busy} onClick={() => void remove()}>Delete asset</button> : null} + {message ? <p className="panel-message" role="status">{message}</p> : null} + </section>; +} diff --git a/src/ui/patterns/sign-in-form.tsx b/src/ui/patterns/sign-in-form.tsx new file mode 100644 index 0000000..8a7c345 --- /dev/null +++ b/src/ui/patterns/sign-in-form.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; + +import { DEMO_PASSWORD, DEMO_WORKSPACE_ID } from '@/src/modules/identity/demo'; +import type { Role } from '@/src/modules/identity/domain'; +import { authClient } from '@/src/platform/auth/client'; + +type DemoIdentity = Readonly<{ id: string; name: string; email: string; role: Role }>; + +export function SignInForm({ demoEnabled, identities }: Readonly<{ demoEnabled: boolean; identities: readonly DemoIdentity[] }>) { + const router = useRouter(); + const [pendingRole, setPendingRole] = useState<Role | null>(null); + const [error, setError] = useState(''); + + async function enterDemo(identity: DemoIdentity) { + setPendingRole(identity.role); + setError(''); + const result = await authClient.signIn.email({ email: identity.email, password: DEMO_PASSWORD }); + if (result.error) { + setError('The demo session could not be created. Verify that migrations and seed data are current.'); + setPendingRole(null); + return; + } + router.push(`/workspace/${DEMO_WORKSPACE_ID}`); + router.refresh(); + } + + return ( + <section className="auth-panel" aria-labelledby="role-heading"> + <div className="panel-heading"><p className="eyebrow">Active policy</p><h2 id="role-heading">Evaluate by role</h2></div> + {demoEnabled ? ( + <div className="role-list"> + {identities.map((identity) => ( + <button className="role-card" type="button" key={identity.id} disabled={pendingRole !== null} onClick={() => void enterDemo(identity)}> + <span><strong>{identity.role}</strong><small>{identity.name}</small></span> + <span aria-hidden="true">{pendingRole === identity.role ? 'Opening…' : '→'}</span> + </button> + ))} + </div> + ) : <p className="empty-state">Public demo entry is disabled in this environment.</p>} + {error ? <p className="form-error" role="alert">{error}</p> : null} + <p className="privacy-note">Mode: deterministic demo identities. No social provider, billing account or live AI call is used.</p> + </section> + ); +} diff --git a/src/ui/patterns/workspace-editor.tsx b/src/ui/patterns/workspace-editor.tsx new file mode 100644 index 0000000..ce47819 --- /dev/null +++ b/src/ui/patterns/workspace-editor.tsx @@ -0,0 +1,252 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { useEffect, useMemo, useRef, useState } from 'react'; + +import type { PostDetail, PostSummary, RevisionDetail } from '@/src/modules/editorial/domain'; +import { TRANSITION_RULES, type TransitionAction } from '@/src/modules/editorial/workflow'; +import type { MembershipContext } from '@/src/modules/identity/domain'; +import { can } from '@/src/modules/identity/policy'; +import { authClient } from '@/src/platform/auth/client'; +import { AISuggestionPanel } from '@/src/ui/patterns/ai-suggestion-panel'; +import { MediaPanel } from '@/src/ui/patterns/media-panel'; + +type SaveState = 'idle' | 'saving' | 'saved' | 'error' | 'conflict'; +type ApiEnvelope<T> = { data: T } | { error: { code: string; message: string; requestId: string } }; + +function isData<T>(envelope: ApiEnvelope<T>): envelope is { data: T } { + return 'data' in envelope; +} + +const ACTION_LABELS: Readonly<Record<TransitionAction, string>> = { + submit: 'Submit for review', + request_changes: 'Request changes', + approve: 'Approve', + schedule: 'Schedule', + publish: 'Publish now', + archive: 'Archive', +}; + +export function WorkspaceEditor({ context, initialPosts, initialPost, aiMode }: Readonly<{ + context: MembershipContext; + initialPosts: PostSummary[]; + initialPost: PostDetail | null; + aiMode: string; +}>) { + const router = useRouter(); + const [posts, setPosts] = useState(initialPosts); + const [post, setPost] = useState(initialPost); + const [saveState, setSaveState] = useState<SaveState>('idle'); + const [message, setMessage] = useState(''); + const [serverConflict, setServerConflict] = useState<PostDetail | null>(null); + const [revisions, setRevisions] = useState<RevisionDetail[]>([]); + const [comparison, setComparison] = useState<RevisionDetail | null>(null); + const [historyOpen, setHistoryOpen] = useState(false); + const [workflowBusy, setWorkflowBusy] = useState(false); + const [scheduledFor, setScheduledFor] = useState(''); + const [resetArmed, setResetArmed] = useState(false); + const [resetBusy, setResetBusy] = useState(false); + const saveInFlight = useRef(false); + const titleInput = useRef<HTMLTextAreaElement>(null); + const [baseline, setBaseline] = useState(initialPost ? JSON.stringify({ title: initialPost.title, excerpt: initialPost.excerpt, content: initialPost.content }) : ''); + + const canCreate = can(context.role, 'post.create', { actorId: context.userId, ownerId: context.userId }); + const canEdit = post ? can(context.role, 'post.update', { actorId: context.userId, ownerId: post.authorId }) && ['Draft', 'ChangesRequested', 'Published'].includes(post.state) : false; + const draft = useMemo(() => post ? JSON.stringify({ title: post.title, excerpt: post.excerpt, content: post.content }) : '', [post]); + const hasUnsavedChanges = draft !== baseline; + const availableActions = post ? (Object.entries(TRANSITION_RULES) as [TransitionAction, (typeof TRANSITION_RULES)[TransitionAction]][]) + .filter(([, rule]) => rule.from.includes(post.state) && can(context.role, rule.permission, { actorId: context.userId, ownerId: post.authorId })) : []; + + useEffect(() => { + if (!post || !canEdit || draft === baseline || saveInFlight.current || ['conflict', 'error'].includes(saveState)) return; + const savedDraft = draft; + const timer = window.setTimeout(async () => { + saveInFlight.current = true; + setSaveState('saving'); + try { + const response = await fetch(`/api/workspaces/${context.workspaceId}/posts/${post.id}`, { + method: 'PATCH', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ expectedVersion: post.version, title: post.title, excerpt: post.excerpt, content: post.content }), + }); + const envelope = await response.json() as ApiEnvelope<PostDetail>; + if (response.status === 409) { + setSaveState('conflict'); + setMessage('A newer revision exists. Your local draft has not overwritten it.'); + return; + } + if (!response.ok || !isData(envelope)) throw new Error('AUTOSAVE_REJECTED'); + setBaseline(savedDraft); + setPost((current) => current ? { ...envelope.data, title: current.title, excerpt: current.excerpt, content: current.content } : envelope.data); + setPosts((current) => current.map((item) => item.id === envelope.data.id ? envelope.data : item)); + setSaveState('saved'); + setMessage(`Revision ${envelope.data.version} saved.`); + } catch { + setSaveState('error'); + setMessage('Autosave failed. Your text remains in this browser.'); + } finally { + saveInFlight.current = false; + } + }, 850); + return () => window.clearTimeout(timer); + }, [baseline, canEdit, context.workspaceId, draft, post, saveState]); + + function editPost(changes: Partial<Pick<PostDetail, 'title' | 'excerpt' | 'content'>>) { + if (!saveInFlight.current) setSaveState('idle'); + setServerConflict(null); + setPost((current) => current ? { ...current, ...changes } : current); + } + + async function selectPost(postId: string) { + setSaveState('idle'); setMessage(''); setServerConflict(null); setHistoryOpen(false); setRevisions([]); setComparison(null); + const response = await fetch(`/api/workspaces/${context.workspaceId}/posts/${postId}`); + const envelope = await response.json() as ApiEnvelope<PostDetail>; + if (response.ok && isData(envelope)) { + setPost(envelope.data); + setBaseline(JSON.stringify({ title: envelope.data.title, excerpt: envelope.data.excerpt, content: envelope.data.content })); + window.requestAnimationFrame(() => titleInput.current?.focus()); + } + } + + async function createPost() { + const response = await fetch(`/api/workspaces/${context.workspaceId}/posts`, { + method: 'POST', headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ title: 'Untitled editorial brief', excerpt: '', content: '' }), + }); + const envelope = await response.json() as ApiEnvelope<PostDetail>; + if (response.ok && isData(envelope)) { + setPosts((current) => [envelope.data, ...current]); + setPost(envelope.data); + setBaseline(JSON.stringify({ title: envelope.data.title, excerpt: '', content: '' })); + setMessage('Draft created. Start writing.'); + window.requestAnimationFrame(() => titleInput.current?.focus()); + } + } + + async function reloadConflict(compareOnly = false) { + if (!post) return; + const response = await fetch(`/api/workspaces/${context.workspaceId}/posts/${post.id}`); + const envelope = await response.json() as ApiEnvelope<PostDetail>; + if (!response.ok || !isData(envelope)) return; + if (compareOnly) { setServerConflict(envelope.data); return; } + setPost(envelope.data); setServerConflict(null); setSaveState('saved'); + setBaseline(JSON.stringify({ title: envelope.data.title, excerpt: envelope.data.excerpt, content: envelope.data.content })); + setMessage(`Reloaded revision ${envelope.data.version}.`); + } + + async function loadHistory() { + if (!post) return; + if (historyOpen) { setHistoryOpen(false); return; } + const response = await fetch(`/api/workspaces/${context.workspaceId}/posts/${post.id}/revisions`); + const envelope = await response.json() as ApiEnvelope<RevisionDetail[]>; + if (response.ok && isData(envelope)) { setRevisions(envelope.data); setHistoryOpen(true); } + } + + async function restoreRevision(revisionId: string) { + if (!post || hasUnsavedChanges) return; + setWorkflowBusy(true); + const response = await fetch(`/api/workspaces/${context.workspaceId}/posts/${post.id}/revisions/restore`, { + method: 'POST', headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ expectedVersion: post.version, revisionId }), + }); + const envelope = await response.json() as ApiEnvelope<PostDetail>; + setWorkflowBusy(false); + if (!response.ok || !isData(envelope)) { setMessage('Restore failed. Reload before retrying.'); setSaveState(response.status === 409 ? 'conflict' : 'error'); return; } + setPost(envelope.data); setPosts((items) => items.map((item) => item.id === envelope.data.id ? envelope.data : item)); + setBaseline(JSON.stringify({ title: envelope.data.title, excerpt: envelope.data.excerpt, content: envelope.data.content })); + setMessage(`Revision ${envelope.data.version} created from history.`); setSaveState('saved'); setHistoryOpen(false); setComparison(null); + } + + async function runTransition(action: TransitionAction) { + if (!post || hasUnsavedChanges || saveState === 'saving') { setMessage('Wait for autosave before changing workflow state.'); return; } + setWorkflowBusy(true); + const body: Record<string, unknown> = { expectedVersion: post.version, action }; + if (action === 'schedule') { + if (!scheduledFor) { setMessage('Choose a future publication time.'); setWorkflowBusy(false); return; } + body.scheduledFor = new Date(scheduledFor).toISOString(); + } + if (action === 'schedule' || action === 'publish') body.idempotencyKey = `browser:${post.id}:${post.version}:${action}`; + const response = await fetch(`/api/workspaces/${context.workspaceId}/posts/${post.id}/transitions`, { + method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), + }); + const envelope = await response.json() as ApiEnvelope<PostDetail>; + setWorkflowBusy(false); + if (!response.ok || !isData(envelope)) { + setSaveState(response.status === 409 ? 'conflict' : 'error'); + setMessage('Workflow command was rejected. Reload the current revision before retrying.'); + return; + } + setPost(envelope.data); setPosts((items) => items.map((item) => item.id === envelope.data.id ? envelope.data : item)); + setBaseline(JSON.stringify({ title: envelope.data.title, excerpt: envelope.data.excerpt, content: envelope.data.content })); + setSaveState('saved'); setMessage(`${ACTION_LABELS[action]} completed.`); + } + + async function signOut() { + await authClient.signOut(); + router.push('/sign-in'); router.refresh(); + } + + async function resetDemo() { + if (!resetArmed) { setResetArmed(true); setMessage('Reset affects only the bounded demo workspace. Select Confirm reset to continue.'); return; } + setResetBusy(true); + const response = await fetch(`/api/workspaces/${context.workspaceId}/demo/reset`, { + method: 'POST', headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ idempotencyKey: `browser-reset:${crypto.randomUUID()}` }), + }); + if (!response.ok) { setResetBusy(false); setResetArmed(false); setMessage('Demo reset was rejected or rate-limited.'); return; } + window.location.reload(); + } + + return ( + <main id="main-content" className="workspace-shell"> + <aside className="workspace-sidebar"> + <div className="brand"><span className="brand-mark" aria-hidden="true">A</span><span>AutoBlog</span></div> + <div className="workspace-label"><span>Workspace</span><strong>{context.workspaceName}</strong><small>Guided data · same policy and repository path</small></div> + <nav aria-label="Posts" className="post-nav"> + <div className="post-nav-heading"><span>Editorial queue</span>{canCreate ? <button type="button" onClick={() => void createPost()} aria-label="Create draft">+</button> : null}</div> + {posts.map((item) => <button type="button" key={item.id} aria-current={post?.id === item.id ? 'page' : undefined} onClick={() => void selectPost(item.id)}><span>{item.title}</span><small>{item.state} · v{item.version}</small></button>)} + </nav> + <div className="guided-checklist"><span className="eyebrow">Guided run</span><ol><li className="done">Enter as a role</li><li className={post ? 'done' : ''}>Select a seeded draft</li><li className={post && post.version > 1 ? 'done' : ''}>Edit and observe autosave</li><li className={post && !['Draft', 'ChangesRequested'].includes(post.state) ? 'done' : ''}>Submit for review</li><li className={post?.publishedRevisionId ? 'done' : ''}>Publish immutable revision</li></ol> + {can(context.role, 'demo.reset') ? <button className={resetArmed ? 'reset-demo armed' : 'reset-demo'} type="button" disabled={resetBusy} onClick={() => void resetDemo()}>{resetBusy ? 'Resetting…' : resetArmed ? 'Confirm reset' : 'Reset demo data'}</button> : null} + </div> + </aside> + + <section className="editor-shell"> + <header className="editor-topbar"> + <div><span className="role-pill">{context.role}</span><span className="mode-pill">AI: {aiMode === 'gemini' ? 'Live Gemini' : 'Mock / demo'}</span></div> + <div><span className={`save-state ${saveState}`} role="status">{saveState === 'saving' ? 'Saving…' : saveState === 'conflict' ? 'Conflict' : saveState === 'error' ? 'Save error' : saveState === 'saved' ? 'Saved' : 'Ready'}</span><button className="text-button" type="button" onClick={() => void signOut()}>Sign out</button></div> + </header> + + {post ? <div className="editor-grid"> + <div className="editor-form"> + <div className="document-meta"><span>{post.state}</span><span>Revision {post.version}</span><span>/{post.slug}</span></div> + <label htmlFor="post-title">Title</label><textarea ref={titleInput} id="post-title" className="title-input" value={post.title} disabled={!canEdit} onChange={(event) => editPost({ title: event.target.value })} /> + <label htmlFor="post-excerpt">Standfirst</label><textarea id="post-excerpt" className="excerpt-input" value={post.excerpt} disabled={!canEdit} onChange={(event) => editPost({ excerpt: event.target.value })} /> + <label htmlFor="post-content">Article body</label><textarea id="post-content" className="content-input" value={post.content} disabled={!canEdit} onChange={(event) => editPost({ content: event.target.value })} /> + <section className="workflow-panel" aria-labelledby="workflow-heading"> + <div><p className="eyebrow">Editorial workflow</p><h2 id="workflow-heading">Move with evidence</h2></div> + <div className="workflow-actions"> + {availableActions.map(([action]) => <span key={action}> + {action === 'schedule' ? <label htmlFor="schedule-time">Publication time<input id="schedule-time" type="datetime-local" value={scheduledFor} onChange={(event) => setScheduledFor(event.target.value)} /></label> : null} + <button type="button" disabled={workflowBusy || hasUnsavedChanges || saveState === 'saving'} onClick={() => void runTransition(action)}>{ACTION_LABELS[action]}</button> + </span>)} + </div> + </section> + <AISuggestionPanel key={post.id} context={context} post={post} canApply={canEdit} configuredMode={aiMode} onApply={(suggestion) => editPost(suggestion)} /> + {message ? <p className={`editor-message ${saveState}`} role={saveState === 'error' || saveState === 'conflict' ? 'alert' : 'status'}>{message}</p> : null} + {saveState === 'conflict' ? <div className="conflict-actions"><button type="button" onClick={() => void reloadConflict()}>Reload server revision</button><button type="button" onClick={() => void reloadConflict(true)}>Compare versions</button></div> : null} + {serverConflict ? <section className="compare-panel" aria-labelledby="compare-heading"><h2 id="compare-heading">Conflict comparison</h2><div><article><h3>Your local draft</h3><pre>{post.content}</pre></article><article><h3>Server revision {serverConflict.version}</h3><pre>{serverConflict.content}</pre></article></div></section> : null} + </div> + <aside className="evidence-panel"><p className="eyebrow">Evidence rail</p><h2>Draft with context</h2><dl><div><dt>Author</dt><dd>{post.authorId}</dd></div><div><dt>Published pointer</dt><dd>{post.publishedRevisionId ? 'Immutable revision set' : 'Not published'}</dd></div><div><dt>Concurrency</dt><dd>Expected version {post.version}</dd></div></dl> + {post.publishedRevisionId ? <a className="preview-link" href={`/preview/${context.workspaceSlug}/${post.slug}`} target="_blank" rel="noreferrer">Open public preview</a> : null} + <button className="history-toggle" type="button" onClick={() => void loadHistory()}>{historyOpen ? 'Close revision history' : 'Open revision history'}</button> + {historyOpen ? <section className="revision-history" aria-label="Revision history">{revisions.map((revision) => <article key={revision.id}><div><strong>v{revision.version}</strong><small>{new Date(revision.createdAt).toLocaleString()}</small></div><p>{revision.title}</p><div><button type="button" onClick={() => setComparison(revision)}>Compare</button>{can(context.role, 'revision.restore', { actorId: context.userId, ownerId: post.authorId }) && ['Draft', 'ChangesRequested', 'Published'].includes(post.state) ? <button type="button" disabled={workflowBusy || hasUnsavedChanges} onClick={() => void restoreRevision(revision.id)}>Restore as new</button> : null}</div></article>)}</section> : null} + {comparison ? <section className="history-comparison"><h3>Current v{post.version} vs v{comparison.version}</h3><pre>{comparison.content}</pre></section> : null} + <MediaPanel key={`media-${post.id}`} context={context} post={post} /> + <p>Autosave creates a revision. A stale version returns HTTP 409 and preserves the newer content.</p> + </aside> + </div> : <div className="empty-state">No posts are available for this workspace.</div>} + </section> + </main> + ); +} diff --git a/tailwind.config.ts b/tailwind.config.ts deleted file mode 100644 index 5354a82..0000000 --- a/tailwind.config.ts +++ /dev/null @@ -1,80 +0,0 @@ -import dynamicSize from './plugins/dynamic-size'; - -import type { Config } from 'tailwindcss'; - -export default { - darkMode: ['class'], - content: [ - './_client/**/*.{js,ts,jsx,tsx,mdx}', - './app/**/*.{js,ts,jsx,tsx,mdx}', - './config/**/*.{js,ts,jsx,tsx,mdx}', - './components/**/*.{js,ts,jsx,tsx,mdx}', - './editor/**/*.{js,ts,jsx,tsx,mdx}', - './ui/**/*.{js,ts,jsx,tsx,mdx}', - './hooks/**/*.{js,ts,jsx,tsx,mdx}', - ], - theme: { - extend: { - colors: { - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', - card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))', - }, - popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))', - }, - primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))', - }, - secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))', - }, - muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))', - }, - accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))', - }, - destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))', - }, - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - chart: { - '1': 'hsl(var(--chart-1))', - '2': 'hsl(var(--chart-2))', - '3': 'hsl(var(--chart-3))', - '4': 'hsl(var(--chart-4))', - '5': 'hsl(var(--chart-5))', - }, - sidebar: { - DEFAULT: 'hsl(var(--sidebar-background))', - foreground: 'hsl(var(--sidebar-foreground))', - primary: 'hsl(var(--sidebar-primary))', - 'primary-foreground': - 'hsl(var(--sidebar-primary-foreground))', - accent: 'hsl(var(--sidebar-accent))', - 'accent-foreground': - 'hsl(var(--sidebar-accent-foreground))', - border: 'hsl(var(--sidebar-border))', - ring: 'hsl(var(--sidebar-ring))', - }, - }, - borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)', - }, - }, - }, - plugins: [dynamicSize, require('tailwindcss-animate')], -} satisfies Config; diff --git a/tests/accessibility/core-surfaces.spec.ts b/tests/accessibility/core-surfaces.spec.ts new file mode 100644 index 0000000..50bfaeb --- /dev/null +++ b/tests/accessibility/core-surfaces.spec.ts @@ -0,0 +1,51 @@ +import AxeBuilder from '@axe-core/playwright'; +import { expect, test } from '@playwright/test'; + +for (const surface of [ + { name: 'sign-in', path: '/sign-in' }, + { name: 'public preview', path: '/preview/demo/immutable-publishing' }, +] as const) { + test(`${surface.name} has no serious or critical axe findings`, async ({ page }) => { + await page.goto(surface.path); + const results = await new AxeBuilder({ page }).analyze(); + expect(results.violations.filter((violation) => ['serious', 'critical'].includes(violation.impact ?? ''))).toEqual([]); + }); +} + +test('authenticated workspace has no serious or critical axe findings', async ({ page }) => { + await page.goto('/sign-in'); + await page.getByRole('button', { name: /^Author/u }).click(); + await expect(page).toHaveURL(/\/workspace\/ws-demo$/u, { timeout: 20_000 }); + const results = await new AxeBuilder({ page }).analyze(); + expect(results.violations.filter((violation) => ['serious', 'critical'].includes(violation.impact ?? ''))).toEqual([]); +}); + +test('primary author flow is keyboard operable and moves focus into a new draft', async ({ page }) => { + await page.goto('/sign-in'); + const author = page.getByRole('button', { name: /^Author/u }); + await author.focus(); await page.keyboard.press('Enter'); + await expect(page).toHaveURL(/\/workspace\/ws-demo$/u, { timeout: 20_000 }); + const create = page.getByRole('button', { name: 'Create draft' }); + await create.focus(); await page.keyboard.press('Enter'); + await expect(page.getByLabel('Title')).toBeFocused(); + await page.keyboard.press('Control+A'); await page.keyboard.type('Keyboard-authored evidence'); + await page.getByLabel('Article body').focus(); await page.keyboard.type('Keyboard input persists.'); + await expect(page.getByText(/Revision \d+ saved\./u)).toBeVisible({ timeout: 12_000 }); + const submit = page.getByRole('button', { name: 'Submit for review' }); + await submit.focus(); await page.keyboard.press('Enter'); + await expect(page.getByText('InReview', { exact: true }).first()).toBeVisible(); +}); + +test('reduced-motion preference disables smooth scrolling and animation duration', async ({ page }) => { + await page.emulateMedia({ reducedMotion: 'reduce' }); + await page.goto('/'); + const styles = await page.locator('html').evaluate((element) => { + const html = getComputedStyle(element); + const previewDot = document.querySelector('.preview-dot'); + if (!previewDot) throw new Error('Preview status marker not found.'); + const child = getComputedStyle(previewDot); + return { scrollBehavior: html.scrollBehavior, animationDuration: child.animationDuration }; + }); + expect(styles.scrollBehavior).toBe('auto'); + expect(Number.parseFloat(styles.animationDuration)).toBeLessThanOrEqual(0.01); +}); diff --git a/tests/accessibility/landing.spec.ts b/tests/accessibility/landing.spec.ts new file mode 100644 index 0000000..4f6a984 --- /dev/null +++ b/tests/accessibility/landing.spec.ts @@ -0,0 +1,8 @@ +import AxeBuilder from '@axe-core/playwright'; +import { expect, test } from '@playwright/test'; + +test('marketing entry has no detectable critical accessibility violations', async ({ page }) => { + await page.goto('/'); + const results = await new AxeBuilder({ page }).analyze(); + expect(results.violations.filter((violation) => ['critical', 'serious'].includes(violation.impact ?? ''))).toEqual([]); +}); diff --git a/tests/e2e/authenticated-editor.spec.ts b/tests/e2e/authenticated-editor.spec.ts new file mode 100644 index 0000000..c26a96e --- /dev/null +++ b/tests/e2e/authenticated-editor.spec.ts @@ -0,0 +1,49 @@ +import { expect, test, type Page } from '@playwright/test'; + +async function signInAs(page: Page, role: 'Author' | 'Reviewer') { + await page.goto('/sign-in'); + await page.getByRole('button', { name: new RegExp(`^${role}`) }).click(); + await expect(page).toHaveURL(/\/workspace\/ws-demo$/, { timeout: 20_000 }); + await expect(page.getByText(role, { exact: true }).first()).toBeVisible(); +} + +test('author autosave survives reload through a real session and durable repository', async ({ page }) => { + await signInAs(page, 'Author'); + await page.getByRole('button', { name: 'Create draft' }).click(); + await expect(page.getByLabel('Title')).toHaveValue('Untitled editorial brief'); + const content = page.getByLabel('Article body'); + const marker = `Durable E2E marker ${Date.now()}`; + const title = `Durable editorial proof ${Date.now()}`; + await page.getByLabel('Title').fill(title); + await content.fill(marker); + await expect(page.getByText(/Revision \d+ saved\./)).toBeVisible({ timeout: 12_000 }); + await page.reload(); + await page.getByRole('button', { name: new RegExp(title) }).click(); + await expect(page.getByLabel('Article body')).toHaveValue(marker); +}); + +test('reviewer is denied a create command by the server policy', async ({ page }) => { + await signInAs(page, 'Reviewer'); + await expect(page.getByRole('button', { name: 'Create draft' })).toHaveCount(0); + const response = await page.context().request.post('/api/workspaces/ws-demo/posts', { + headers: { origin: 'http://127.0.0.1:3000' }, + data: { title: 'Forbidden reviewer draft', excerpt: '', content: '' }, + }); + expect(response.status()).toBe(403); + const payload = await response.json() as { error: { code: string; requestId: string } }; + expect(payload.error.code).toBe('FORBIDDEN'); + expect(payload.error.requestId).toBeTruthy(); +}); + +test('anonymous mutation receives a stable unauthenticated error', async ({ playwright }) => { + const anonymous = await playwright.request.newContext({ baseURL: 'http://127.0.0.1:3000' }); + const response = await anonymous.post('/api/workspaces/ws-demo/posts', { + data: { title: 'Anonymous draft', excerpt: '', content: '' }, + }); + expect(response.status()).toBe(401); + const payload = await response.json() as { error: { code: string; message: string; requestId: string } }; + expect(payload.error.code).toBe('UNAUTHENTICATED'); + expect(payload.error.message).not.toContain('database'); + expect(payload.error.requestId).toBeTruthy(); + await anonymous.dispose(); +}); diff --git a/tests/e2e/demo-reset.spec.ts b/tests/e2e/demo-reset.spec.ts new file mode 100644 index 0000000..776a1de --- /dev/null +++ b/tests/e2e/demo-reset.spec.ts @@ -0,0 +1,30 @@ +import { expect, test } from '@playwright/test'; + +test('Owner resets only the bounded demo and keeps an authenticated session', async ({ page }) => { + test.setTimeout(60_000); + await page.goto('/sign-in'); + await page.getByRole('button', { name: /^Owner/u }).click(); + await expect(page).toHaveURL(/\/workspace\/ws-demo$/u, { timeout: 20_000 }); + await page.getByRole('button', { name: 'Create draft' }).click(); + await expect(page.getByLabel('Title')).toBeFocused(); + const marker = `Reset removes only demo mutation ${Date.now()}`; + await page.getByLabel('Title').fill(marker); + await page.getByLabel('Article body').fill('Temporary bounded demo content.'); + await expect(page.getByText(/Revision \d+ saved\./u)).toBeVisible({ timeout: 12_000 }); + await page.getByRole('button', { name: 'Reset demo data' }).click(); + await expect(page.getByText(/affects only the bounded demo workspace/u)).toBeVisible(); + await page.getByRole('button', { name: 'Confirm reset' }).click(); + await expect(page.getByText('Owner', { exact: true }).first()).toBeVisible({ timeout: 20_000 }); + await expect(page.getByRole('button', { name: new RegExp(marker) })).toHaveCount(0); + await expect(page.getByLabel('Title')).toHaveValue('Designing the next editorial operating system'); + + const key = `e2e-idempotency-${Date.now()}`; + const first = await page.context().request.post('/api/workspaces/ws-demo/demo/reset', { + headers: { origin: 'http://127.0.0.1:3000' }, data: { idempotencyKey: key }, + }); + const second = await page.context().request.post('/api/workspaces/ws-demo/demo/reset', { + headers: { origin: 'http://127.0.0.1:3000' }, data: { idempotencyKey: key }, + }); + expect(first.status()).toBe(200); expect(second.status()).toBe(200); + expect((await second.json() as { data: { alreadyApplied: boolean } }).data.alreadyApplied).toBe(true); +}); diff --git a/tests/e2e/editorial-workflow.spec.ts b/tests/e2e/editorial-workflow.spec.ts new file mode 100644 index 0000000..f5eff0a --- /dev/null +++ b/tests/e2e/editorial-workflow.spec.ts @@ -0,0 +1,84 @@ +import { expect, test, type Page } from '@playwright/test'; + +async function signInAs(page: Page, role: 'Author' | 'Reviewer' | 'Editor') { + await page.goto('/sign-in'); + await page.getByRole('button', { name: new RegExp(`^${role}`) }).click(); + await expect(page).toHaveURL(/\/workspace\/ws-demo$/, { timeout: 20_000 }); +} + +async function changeRole(page: Page, role: 'Author' | 'Reviewer' | 'Editor') { + await page.getByRole('button', { name: 'Sign out' }).click(); + await expect(page).toHaveURL(/\/sign-in$/); + await signInAs(page, role); +} + +test('author, reviewer and editor complete an immutable publication', async ({ page }) => { + test.setTimeout(60_000); + await signInAs(page, 'Author'); + await page.getByRole('button', { name: 'Create draft' }).click(); + await expect(page.getByLabel('Title')).toHaveValue('Untitled editorial brief'); + const marker = `Flagship publication ${Date.now()}`; + await page.getByLabel('Title').fill(marker); + await page.getByLabel('Standfirst').fill('A browser-tested editorial workflow.'); + await page.getByLabel('Article body').fill('First accepted version.'); + const savedMessage = page.getByText(/Revision \d+ saved\./); + await expect(savedMessage).toBeVisible({ timeout: 12_000 }); + const firstSavedText = await savedMessage.textContent(); + const firstVersion = Number(firstSavedText?.match(/\d+/u)?.[0]); + expect(firstVersion).toBeGreaterThan(1); + await page.getByLabel('Article body').fill('Second version restored through immutable history.'); + await expect.poll(async () => Number((await savedMessage.textContent())?.match(/\d+/u)?.[0])).toBeGreaterThan(firstVersion); + const secondVersion = Number((await savedMessage.textContent())?.match(/\d+/u)?.[0]); + + await page.getByRole('button', { name: 'Open revision history' }).click(); + await expect(page.getByRole('region', { name: 'Revision history' })).toBeVisible(); + await page.getByRole('button', { name: 'Compare' }).nth(1).click(); + await expect(page.getByText(`Current v${secondVersion} vs v${firstVersion}`)).toBeVisible(); + await page.getByRole('button', { name: 'Restore as new' }).nth(1).click(); + await expect(page.getByText(`Revision ${secondVersion + 1} created from history.`)).toBeVisible(); + await expect(page.getByLabel('Article body')).toHaveValue('First accepted version.'); + + await page.getByRole('button', { name: 'Submit for review' }).click(); + await expect(page.getByText('InReview', { exact: true }).first()).toBeVisible(); + await changeRole(page, 'Reviewer'); + await page.getByRole('button', { name: new RegExp(marker) }).click(); + await page.getByRole('button', { name: 'Approve' }).click(); + await expect(page.getByText('Approved', { exact: true }).first()).toBeVisible(); + + await changeRole(page, 'Editor'); + await page.getByRole('button', { name: new RegExp(marker) }).click(); + await page.getByRole('button', { name: 'Publish now' }).click(); + await expect(page.getByText('Published', { exact: true }).first()).toBeVisible(); + const preview = page.getByRole('link', { name: 'Open public preview' }); + const path = await preview.getAttribute('href'); + expect(path).toMatch(/^\/preview\/demo\//u); + if (!path) throw new Error('Preview path was not rendered.'); + await page.goto(path); + await expect(page.getByRole('heading', { level: 1, name: marker })).toBeVisible(); + await expect(page.getByText('First accepted version.')).toBeVisible(); +}); + +test('autosave surfaces a stale writer conflict without overwriting server content', async ({ page }) => { + await signInAs(page, 'Author'); + await page.getByRole('button', { name: 'Create draft' }).click(); + await expect(page.getByLabel('Title')).toHaveValue('Untitled editorial brief'); + const title = `Conflict evidence ${Date.now()}`; + await page.getByLabel('Title').fill(title); + await page.getByLabel('Article body').fill('Local baseline.'); + await expect(page.getByText(/Revision \d+ saved\./)).toBeVisible({ timeout: 12_000 }); + await expect(page.getByRole('button', { name: new RegExp(title) })).toBeVisible({ timeout: 12_000 }); + const listResponse = await page.context().request.get('/api/workspaces/ws-demo/posts'); + const list = await listResponse.json() as { data: { id: string; title: string; version: number; excerpt: string }[] }; + const selected = list.data.find((post) => post.title === title); + if (!selected) throw new Error('Created conflict fixture was not listed.'); + const serverResponse = await page.context().request.patch(`/api/workspaces/ws-demo/posts/${selected.id}`, { + headers: { origin: 'http://127.0.0.1:3000' }, + data: { expectedVersion: selected.version, title, excerpt: selected.excerpt, content: 'Newer server content.' }, + }); + expect(serverResponse.status()).toBe(200); + await page.getByLabel('Article body').fill('Stale browser content.'); + await expect(page.getByText('Conflict', { exact: true })).toBeVisible({ timeout: 12_000 }); + await expect(page.getByText(/has not overwritten/)).toBeVisible(); + await page.getByRole('button', { name: 'Compare versions' }).click(); + await expect(page.getByText('Newer server content.')).toBeVisible(); +}); diff --git a/tests/e2e/media-ai.spec.ts b/tests/e2e/media-ai.spec.ts new file mode 100644 index 0000000..c58cc02 --- /dev/null +++ b/tests/e2e/media-ai.spec.ts @@ -0,0 +1,67 @@ +import { expect, test, type Page } from '@playwright/test'; + +async function signInAs(page: Page, role: 'Author' | 'Editor') { + await page.goto('/sign-in'); + await page.getByRole('button', { name: new RegExp(`^${role}`) }).click(); + await expect(page).toHaveURL(/\/workspace\/ws-demo$/, { timeout: 20_000 }); +} + +test('AI suggestion remains a preview until the author explicitly applies it', async ({ page }) => { + await signInAs(page, 'Author'); + await page.getByRole('button', { name: 'Create draft' }).click(); + await expect(page.getByLabel('Title')).toHaveValue('Untitled editorial brief'); + const title = `Explicit AI application ${Date.now()}`; + await page.getByLabel('Title').fill(title); + await page.getByLabel('Article body').fill('Human-authored baseline.'); + await expect(page.getByText(/Revision \d+ saved\./)).toBeVisible({ timeout: 12_000 }); + await page.getByRole('button', { name: 'Generate suggestion' }).click(); + await expect(page.getByText(/Mock demo suggestion ready/)).toBeVisible({ timeout: 20_000 }); + await expect(page.getByRole('article', { name: 'AI suggestion preview' })).toBeVisible(); + await expect(page.getByLabel('Article body')).toHaveValue('Human-authored baseline.'); + await page.getByRole('button', { name: 'Apply suggestion' }).click(); + await expect(page.getByLabel('Article body')).toHaveValue(/Editorial next step/u); + await expect(page.getByText(/Revision \d+ saved\./)).toBeVisible({ timeout: 12_000 }); +}); + +test('verified media can be safely replaced and deleted through authorized roles', async ({ page }) => { + test.setTimeout(60_000); + await signInAs(page, 'Author'); + await page.getByRole('button', { name: 'Create draft' }).click(); + await expect(page.getByLabel('Title')).toHaveValue('Untitled editorial brief'); + const title = `Media lifecycle ${Date.now()}`; + await page.getByLabel('Title').fill(title); + await page.getByLabel('Article body').fill('Media lifecycle fixture.'); + await expect(page.getByText(/Revision \d+ saved\./)).toBeVisible({ timeout: 12_000 }); + const file = page.getByLabel(/PNG, JPEG or WebP/); + await file.setInputFiles('app/icon.png'); + await page.getByLabel('Alternative text').fill('AutoBlog brand mark'); + await page.getByRole('button', { name: 'Upload verified image' }).click(); + await expect(page.getByText('Verified image activated.')).toBeVisible({ timeout: 12_000 }); + await expect(page.getByText(/image\/png/)).toBeVisible(); + await file.setInputFiles('app/icon.png'); + await page.getByRole('button', { name: 'Replace safely' }).click(); + await expect(page.getByText(/Replacement activated/)).toBeVisible({ timeout: 12_000 }); + await expect(page.getByRole('button', { name: 'Delete asset' })).toHaveCount(0); + + await page.getByRole('button', { name: 'Sign out' }).click(); + await signInAs(page, 'Editor'); + await page.getByRole('button', { name: new RegExp(title) }).click(); + await expect(page.getByRole('button', { name: 'Delete asset' })).toBeVisible({ timeout: 12_000 }); + await page.getByRole('button', { name: 'Delete asset' }).click(); + await expect(page.getByText(/cleanup queued/)).toBeVisible(); +}); + +test('anonymous AI and media mutations are rejected before provider access', async ({ playwright }) => { + const anonymous = await playwright.request.newContext({ baseURL: 'http://127.0.0.1:3000' }); + const ai = await anonymous.post('/api/workspaces/ws-demo/ai/suggest', { data: { postId: 'post-editorial-systems', title: 'Anonymous attempt', excerpt: '', content: '', instruction: 'Generate content.' } }); + expect(ai.status()).toBe(401); + expect((await ai.json() as { error: { code: string } }).error.code).toBe('UNAUTHENTICATED'); + const media = await anonymous.post('/api/workspaces/ws-demo/media', { multipart: { postId: 'post-editorial-systems', file: { name: 'fake.png', mimeType: 'image/png', buffer: Buffer.from('not-an-image') } } }); + expect(media.status()).toBe(401); + expect((await media.json() as { error: { code: string } }).error.code).toBe('UNAUTHENTICATED'); + const jobs = await anonymous.post('/api/jobs/run'); + expect(jobs.status()).toBe(401); + const scheduler = await anonymous.post('/api/jobs/run', { headers: { 'x-autoblog-cron-secret': 'e2e-only-distinct-cron-secret-0000000' } }); + expect(scheduler.status()).toBe(200); + await anonymous.dispose(); +}); diff --git a/tests/e2e/recruiter-entry.spec.ts b/tests/e2e/recruiter-entry.spec.ts new file mode 100644 index 0000000..120b350 --- /dev/null +++ b/tests/e2e/recruiter-entry.spec.ts @@ -0,0 +1,8 @@ +import { expect, test } from '@playwright/test'; + +test('recruiter can reach the bounded demo entry', async ({ page }) => { + await page.goto('/'); + await expect(page.getByRole('heading', { level: 1, name: 'Publish with evidence.' })).toBeVisible(); + await page.getByRole('link', { name: 'Enter guided demo' }).click(); + await expect(page).toHaveURL(/\/sign-in$/); +}); diff --git a/tests/integration/ai-governance.test.ts b/tests/integration/ai-governance.test.ts new file mode 100644 index 0000000..694adba --- /dev/null +++ b/tests/integration/ai-governance.test.ts @@ -0,0 +1,85 @@ +import { mkdir } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { and, eq } from 'drizzle-orm'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import type { AIAdapter } from '@/src/modules/ai/adapter'; +import { DrizzleAIRepository } from '@/src/modules/ai/drizzle-repository'; +import { MockAIAdapter } from '@/src/modules/ai/mock-adapter'; +import { AIService } from '@/src/modules/ai/service'; +import { DrizzleEditorialRepository } from '@/src/modules/editorial/drizzle-repository'; +import { DEMO_WORKSPACE_ID } from '@/src/modules/identity/demo'; +import type { MembershipContext } from '@/src/modules/identity/domain'; +import { createDatabase, type DatabaseContext } from '@/src/platform/db/client'; +import { aiQuotaWindows, aiUsage, auditEvents } from '@/src/platform/db/schema'; +import { migrateDatabase } from '@/src/platform/db/migrate'; +import { seedDemo } from '@/src/platform/db/seed'; + +let database: DatabaseContext; +let repository: DrizzleAIRepository; +let editorial: DrizzleEditorialRepository; + +const context = (workspaceId = DEMO_WORKSPACE_ID): MembershipContext => ({ + workspaceId, workspaceSlug: 'demo', workspaceName: 'AutoBlog Editorial Lab', userId: 'demo-author', + userName: 'Author', userEmail: 'author@demo.autoblog.local', role: 'Author', +}); +const input = { postId: 'post-editorial-systems', title: 'Editorial systems', excerpt: '', content: 'Source content.', instruction: 'Improve structure and clarity.' }; + +beforeEach(async () => { + await mkdir('data/tests', { recursive: true }); + database = createDatabase(`file:${join('data', 'tests', `${crypto.randomUUID()}.db`).replaceAll('\\', '/')}`); + await migrateDatabase(database.client); await seedDemo(database); + repository = new DrizzleAIRepository(database); editorial = new DrizzleEditorialRepository(database); +}); + +afterEach(() => database.client.close()); + +const createService = (adapter: AIAdapter = new MockAIAdapter(), quota = 1_000_000, timeout = 1000) => + new AIService(repository, adapter, quota, (workspaceId, postId) => editorial.find(workspaceId, postId), timeout); + +describe('AI governance', () => { + it('returns a labeled suggestion, records bounded metadata and never mutates the post', async () => { + const before = await editorial.find(DEMO_WORKSPACE_ID, input.postId); + const result = await createService().suggest(context(), input, 'ai-success'); + expect(result.mode).toBe('mock'); + expect(result.provider).toBe('deterministic-mock'); + expect(result.suggestion.content).not.toBe(before?.content); + expect(await editorial.find(DEMO_WORKSPACE_ID, input.postId)).toEqual(before); + const usage = await database.db.select().from(aiUsage); + expect(usage).toHaveLength(1); + expect(usage[0]).toMatchObject({ inputTokens: null, outputTokens: null, mode: 'mock' }); + const [audit] = await database.db.select().from(auditEvents).where(eq(auditEvents.action, 'ai.suggested')); + expect(JSON.stringify(audit?.metadata)).not.toContain(input.content); + }); + + it('enforces workspace quota before invoking the adapter', async () => { + let calls = 0; + const adapter: AIAdapter = { mode: 'mock', suggest: async () => { calls += 1; return new MockAIAdapter().suggest(input, new AbortController().signal); } }; + await expect(createService(adapter, 100).suggest(context(), input, 'quota')).rejects.toMatchObject({ code: 'QUOTA_EXCEEDED' }); + expect(calls).toBe(0); + }); + + it('enforces a durable per-user rate limit', async () => { + const service = createService(); + for (let index = 0; index < 5; index += 1) await service.suggest(context(), { ...input, instruction: `Improve structure ${index}` }, `rate-${index}`); + await expect(service.suggest(context(), input, 'rate-exceeded')).rejects.toMatchObject({ code: 'RATE_LIMITED' }); + expect(await database.db.select().from(aiUsage)).toHaveLength(5); + }); + + it('times out an adapter and releases its quota reservation', async () => { + const hanging: AIAdapter = { mode: 'mock', suggest: async () => new Promise(() => undefined) }; + await expect(createService(hanging, 1_000_000, 10).suggest(context(), input, 'timeout')).rejects.toMatchObject({ code: 'PROVIDER_UNAVAILABLE' }); + const [window] = await database.db.select().from(aiQuotaWindows).where(eq(aiQuotaWindows.workspaceId, DEMO_WORKSPACE_ID)); + expect(window?.reservedCharacters).toBe(0); + expect(await database.db.select().from(aiUsage)).toHaveLength(0); + }); + + it('rejects a cross-workspace post before quota or provider access', async () => { + let calls = 0; + const adapter: AIAdapter = { mode: 'mock', suggest: async () => { calls += 1; return new MockAIAdapter().suggest(input, new AbortController().signal); } }; + await expect(createService(adapter).suggest(context('different-workspace'), input, 'cross-workspace')).rejects.toMatchObject({ code: 'NOT_FOUND' }); + expect(calls).toBe(0); + expect(await database.db.select().from(aiQuotaWindows).where(and(eq(aiQuotaWindows.workspaceId, 'different-workspace'), eq(aiQuotaWindows.usedCharacters, 0)))).toHaveLength(0); + }); +}); diff --git a/tests/integration/authentication.test.ts b/tests/integration/authentication.test.ts new file mode 100644 index 0000000..4a3bada --- /dev/null +++ b/tests/integration/authentication.test.ts @@ -0,0 +1,47 @@ +import { mkdir } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { DEMO_PASSWORD } from '@/src/modules/identity/demo'; +import { createAuthentication } from '@/src/platform/auth/auth'; +import { createDatabase, type DatabaseContext } from '@/src/platform/db/client'; +import { migrateDatabase } from '@/src/platform/db/migrate'; +import { seedDemo } from '@/src/platform/db/seed'; + +let database: DatabaseContext; +let databasePath: string; + +beforeEach(async () => { + await mkdir('data/tests', { recursive: true }); + databasePath = join('data', 'tests', `${crypto.randomUUID()}.db`).replaceAll('\\', '/'); + database = createDatabase(`file:${databasePath}`); + await migrateDatabase(database.client); + await seedDemo(database); +}); + +afterEach(async () => { + database.client.close(); +}); + +describe('database-backed authentication', () => { + it('rejects arbitrary credentials and issues a revocable secure session for a seeded identity', async () => { + const testAuth = createAuthentication(database, { baseURL: 'http://localhost:3000', secret: 'integration-secret-at-least-32-characters' }); + await expect(testAuth.api.signInEmail({ body: { email: 'author@demo.autoblog.local', password: 'arbitrary-non-empty' } })).rejects.toBeDefined(); + + const response = await testAuth.api.signInEmail({ + body: { email: 'author@demo.autoblog.local', password: DEMO_PASSWORD }, + asResponse: true, + }); + expect(response.status).toBe(200); + const cookie = response.headers.get('set-cookie'); + expect(cookie).toContain('HttpOnly'); + expect(cookie).toContain('SameSite=Lax'); + + const session = await testAuth.api.getSession({ headers: new Headers({ cookie: cookie ?? '' }) }); + expect(session?.user.email).toBe('author@demo.autoblog.local'); + await testAuth.api.signOut({ headers: new Headers({ cookie: cookie ?? '' }) }); + const revoked = await testAuth.api.getSession({ headers: new Headers({ cookie: cookie ?? '' }) }); + expect(revoked).toBeNull(); + }); +}); diff --git a/tests/integration/demo-reset.test.ts b/tests/integration/demo-reset.test.ts new file mode 100644 index 0000000..50d995b --- /dev/null +++ b/tests/integration/demo-reset.test.ts @@ -0,0 +1,71 @@ +import { mkdir } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { eq } from 'drizzle-orm'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { DrizzleDemoRepository } from '@/src/modules/identity/drizzle-demo-repository'; +import { DemoService } from '@/src/modules/identity/demo-service'; +import { DEMO_WORKSPACE_ID } from '@/src/modules/identity/demo'; +import type { MembershipContext } from '@/src/modules/identity/domain'; +import { createDatabase, type DatabaseContext } from '@/src/platform/db/client'; +import { auditEvents, idempotencyRecords, mediaObjects, posts, workspaces } from '@/src/platform/db/schema'; +import { migrateDatabase } from '@/src/platform/db/migrate'; +import { seedDemo } from '@/src/platform/db/seed'; + +let database: DatabaseContext; +let service: DemoService; + +const context = (role: MembershipContext['role'], workspaceId = DEMO_WORKSPACE_ID): MembershipContext => ({ + workspaceId, workspaceSlug: workspaceId, workspaceName: 'Workspace', userId: `demo-${role.toLowerCase()}`, + userName: role, userEmail: `${role.toLowerCase()}@demo.autoblog.local`, role, +}); + +beforeEach(async () => { + await mkdir('data/tests', { recursive: true }); + database = createDatabase(`file:${join('data', 'tests', `${crypto.randomUUID()}.db`).replaceAll('\\', '/')}`); + await migrateDatabase(database.client); await seedDemo(database); + service = new DemoService(new DrizzleDemoRepository(database)); +}); + +afterEach(() => database.client.close()); + +describe('bounded demo reset', () => { + it('restores the fixture, removes only demo objects and preserves unrelated workspace data', async () => { + const now = new Date(); + await database.db.insert(workspaces).values({ id: 'ws-configured', slug: 'configured', name: 'Configured workspace', isDemo: false, createdAt: now, updatedAt: now }); + await database.db.insert(mediaObjects).values([ + { storageKey: 'ws-demo/orphan-object', data: Buffer.from('demo'), createdAt: now }, + { storageKey: 'ws-configured/object', data: Buffer.from('configured'), createdAt: now }, + ]); + await database.db.update(posts).set({ title: 'Mutated demo title' }).where(eq(posts.id, 'post-editorial-systems')); + const result = await service.reset(context('Owner'), { idempotencyKey: 'reset-fixture-once' }, 'reset-request'); + expect(result).toMatchObject({ fixtureVersion: 1, alreadyApplied: false }); + const [restored] = await database.db.select().from(posts).where(eq(posts.id, 'post-editorial-systems')); + expect(restored?.title).toBe('Designing the next editorial operating system'); + expect(await database.db.select().from(workspaces).where(eq(workspaces.id, 'ws-configured'))).toHaveLength(1); + expect(await database.db.select().from(mediaObjects).where(eq(mediaObjects.storageKey, 'ws-demo/orphan-object'))).toHaveLength(0); + expect(await database.db.select().from(mediaObjects).where(eq(mediaObjects.storageKey, 'ws-configured/object'))).toHaveLength(1); + }); + + it('returns the persisted result for a repeated idempotency key without another reset audit', async () => { + const first = await service.reset(context('Owner'), { idempotencyKey: 'repeatable-reset-key' }, 'reset-first'); + const second = await service.reset(context('Owner'), { idempotencyKey: 'repeatable-reset-key' }, 'reset-second'); + expect(second).toEqual({ ...first, alreadyApplied: true }); + expect(await database.db.select().from(idempotencyRecords)).toHaveLength(1); + expect(await database.db.select().from(auditEvents).where(eq(auditEvents.action, 'demo.reset'))).toHaveLength(1); + }); + + it('denies Reviewer and any non-demo workspace', async () => { + await expect(service.reset(context('Reviewer'), { idempotencyKey: 'reviewer-reset-key' }, 'reviewer')).rejects.toMatchObject({ code: 'FORBIDDEN' }); + const now = new Date(); + await database.db.insert(workspaces).values({ id: 'ws-real', slug: 'real', name: 'Real', isDemo: false, createdAt: now, updatedAt: now }); + await expect(service.reset(context('Owner', 'ws-real'), { idempotencyKey: 'real-workspace-key' }, 'real')).rejects.toMatchObject({ code: 'FORBIDDEN' }); + }); + + it('rate limits distinct reset operations while idempotent retries remain free', async () => { + for (let index = 0; index < 3; index += 1) await service.reset(context('Owner'), { idempotencyKey: `bounded-reset-${index}` }, `bounded-${index}`); + await expect(service.reset(context('Owner'), { idempotencyKey: 'bounded-reset-fourth' }, 'bounded-fourth')).rejects.toMatchObject({ code: 'RATE_LIMITED' }); + await expect(service.reset(context('Owner'), { idempotencyKey: 'bounded-reset-0' }, 'bounded-repeat')).resolves.toMatchObject({ alreadyApplied: true }); + }); +}); diff --git a/tests/integration/editorial-persistence.test.ts b/tests/integration/editorial-persistence.test.ts new file mode 100644 index 0000000..4a3f172 --- /dev/null +++ b/tests/integration/editorial-persistence.test.ts @@ -0,0 +1,81 @@ +import { mkdir } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { DrizzleEditorialRepository } from '@/src/modules/editorial/drizzle-repository'; +import { EditorialService } from '@/src/modules/editorial/service'; +import { DEMO_WORKSPACE_ID } from '@/src/modules/identity/demo'; +import type { MembershipContext } from '@/src/modules/identity/domain'; +import { createDatabase, type DatabaseContext } from '@/src/platform/db/client'; +import { migrateDatabase } from '@/src/platform/db/migrate'; +import { seedDemo } from '@/src/platform/db/seed'; +import { AppError } from '@/src/platform/observability/errors'; + +let database: DatabaseContext; +let databasePath: string; +let service: EditorialService; + +const context = (role: MembershipContext['role'], userId = `demo-${role.toLowerCase()}`): MembershipContext => ({ + workspaceId: DEMO_WORKSPACE_ID, + workspaceSlug: 'demo', + workspaceName: 'AutoBlog Editorial Lab', + userId, + userName: role, + userEmail: `${role.toLowerCase()}@demo.autoblog.local`, + role, +}); + +beforeEach(async () => { + await mkdir('data/tests', { recursive: true }); + databasePath = join('data', 'tests', `${crypto.randomUUID()}.db`).replaceAll('\\', '/'); + database = createDatabase(`file:${databasePath}`); + await migrateDatabase(database.client); + await seedDemo(database); + service = new EditorialService(new DrizzleEditorialRepository(database)); +}); + +afterEach(async () => { + database.client.close(); +}); + +describe('durable editorial repository', () => { + it('migrates from empty, creates a post, and persists it after reconnect', async () => { + const created = await service.create(context('Author'), { title: 'Durable product evidence', excerpt: 'A restart test.', content: 'Persist this revision.' }, 'request-create'); + expect(created.version).toBe(1); + database.client.close(); + + database = createDatabase(`file:${databasePath}`); + const reopened = new EditorialService(new DrizzleEditorialRepository(database)); + const found = await reopened.get(context('Author'), created.id); + expect(found.content).toBe('Persist this revision.'); + expect(await migrateDatabase(database.client)).toEqual([]); + }); + + it('creates immutable revisions and rejects a concurrent stale writer', async () => { + const author = context('Author'); + const initial = await service.get(author, 'post-editorial-systems'); + const saved = await service.save(author, initial.id, { expectedVersion: initial.version, title: initial.title, excerpt: initial.excerpt, content: `${initial.content}\n\nWriter A.` }, 'request-a'); + expect(saved.version).toBe(initial.version + 1); + + await expect(service.save(author, initial.id, { expectedVersion: initial.version, title: initial.title, excerpt: initial.excerpt, content: 'Writer B stale content.' }, 'request-b')) + .rejects.toMatchObject({ code: 'VERSION_CONFLICT', status: 409 }); + const current = await service.get(author, initial.id); + expect(current.content).toContain('Writer A.'); + expect(current.content).not.toContain('Writer B'); + }); + + it('denies Reviewer creation and Author edits to another author', async () => { + await expect(service.create(context('Reviewer'), { title: 'Forbidden draft', excerpt: '', content: '' }, 'request-denied')) + .rejects.toBeInstanceOf(AppError); + const editorPost = await service.create(context('Editor'), { title: 'Editor-owned post', excerpt: '', content: '' }, 'request-editor'); + await expect(service.save(context('Author'), editorPost.id, { expectedVersion: 1, title: editorPost.title, excerpt: '', content: 'Unauthorized.' }, 'request-author')) + .rejects.toMatchObject({ code: 'FORBIDDEN' }); + }); + + it('isolates workspace identifiers at the repository boundary', async () => { + const repository = new DrizzleEditorialRepository(database); + expect(await repository.find('ws-not-the-demo', 'post-editorial-systems')).toBeNull(); + expect(await repository.list('ws-not-the-demo')).toEqual([]); + }); +}); diff --git a/tests/integration/editorial-workflow.test.ts b/tests/integration/editorial-workflow.test.ts new file mode 100644 index 0000000..8755109 --- /dev/null +++ b/tests/integration/editorial-workflow.test.ts @@ -0,0 +1,136 @@ +import { mkdir } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { and, eq } from 'drizzle-orm'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { DrizzleEditorialRepository } from '@/src/modules/editorial/drizzle-repository'; +import { EditorialService } from '@/src/modules/editorial/service'; +import { DEMO_WORKSPACE_ID } from '@/src/modules/identity/demo'; +import type { MembershipContext } from '@/src/modules/identity/domain'; +import { createDatabase, type DatabaseContext } from '@/src/platform/db/client'; +import { auditEvents, jobs, publications, revisions } from '@/src/platform/db/schema'; +import { migrateDatabase } from '@/src/platform/db/migrate'; +import { seedDemo } from '@/src/platform/db/seed'; + +let database: DatabaseContext; +let repository: DrizzleEditorialRepository; +let service: EditorialService; + +const context = (role: MembershipContext['role']): MembershipContext => ({ + workspaceId: DEMO_WORKSPACE_ID, + workspaceSlug: 'demo', + workspaceName: 'AutoBlog Editorial Lab', + userId: `demo-${role.toLowerCase()}`, + userName: role, + userEmail: `${role.toLowerCase()}@demo.autoblog.local`, + role, +}); + +beforeEach(async () => { + await mkdir('data/tests', { recursive: true }); + const path = join('data', 'tests', `${crypto.randomUUID()}.db`).replaceAll('\\', '/'); + database = createDatabase(`file:${path}`); + await migrateDatabase(database.client); + await seedDemo(database); + repository = new DrizzleEditorialRepository(database); + service = new EditorialService(repository); +}); + +afterEach(() => database.client.close()); + +describe('editorial reliability', () => { + it('runs author-reviewer-editor publication while later edits preserve the published revision', async () => { + const author = context('Author'); + const reviewer = context('Reviewer'); + const editor = context('Editor'); + const draft = await service.get(author, 'post-editorial-systems'); + const submitted = await service.transition(author, draft.id, { expectedVersion: draft.version, action: 'submit' }, 'submit'); + expect(submitted.state).toBe('InReview'); + const approved = await service.transition(reviewer, draft.id, { expectedVersion: submitted.version, action: 'approve' }, 'approve'); + expect(approved.state).toBe('Approved'); + const published = await service.transition(editor, draft.id, { + expectedVersion: approved.version, action: 'publish', idempotencyKey: 'publish:flagship:1', + }, 'publish'); + expect(published.state).toBe('Published'); + + const publicBeforeEdit = await service.publicPost('demo', draft.slug); + const laterDraft = await service.save(editor, draft.id, { + expectedVersion: published.version, title: 'A later private draft', excerpt: published.excerpt, content: 'Not public yet.', + }, 'later-edit'); + expect(laterDraft.state).toBe('Draft'); + expect(laterDraft.publishedRevisionId).toBe(publicBeforeEdit.revisionId); + expect((await service.publicPost('demo', draft.slug)).content).toBe(publicBeforeEdit.content); + }); + + it('restores by appending a new immutable revision and rejects stale restore', async () => { + const author = context('Author'); + const current = await service.get(author, 'post-editorial-systems'); + const history = await service.revisions(author, current.id); + const oldest = history.at(-1); + if (!oldest) throw new Error('Seed history is empty.'); + const restored = await service.restore(author, current.id, { expectedVersion: current.version, revisionId: oldest.id }, 'restore'); + expect(restored.version).toBe(current.version + 1); + expect(restored.content).toBe(oldest.content); + const after = await service.revisions(author, current.id); + expect(after).toHaveLength(history.length + 1); + expect(after[0]?.restoredFromRevisionId).toBe(oldest.id); + expect(after.at(-1)).toEqual(oldest); + await expect(service.restore(author, current.id, { expectedVersion: current.version, revisionId: oldest.id }, 'stale-restore')) + .rejects.toMatchObject({ code: 'VERSION_CONFLICT' }); + }); + + it('leases and publishes a pinned scheduled revision exactly once', async () => { + const reviewer = context('Reviewer'); + const editor = context('Editor'); + const reviewed = await service.get(reviewer, 'post-ai-governance'); + const approved = await service.transition(reviewer, reviewed.id, { expectedVersion: reviewed.version, action: 'approve' }, 'approve-scheduled'); + const scheduledAt = new Date(Date.now() + 60_000); + const scheduled = await service.transition(editor, approved.id, { + expectedVersion: approved.version, action: 'schedule', scheduledFor: scheduledAt.toISOString(), idempotencyKey: 'schedule:ai-governance:1', + }, 'schedule'); + expect(scheduled.state).toBe('Scheduled'); + const repeated = await service.transition(editor, approved.id, { + expectedVersion: approved.version, action: 'schedule', scheduledFor: scheduledAt.toISOString(), idempotencyKey: 'schedule:ai-governance:1', + }, 'schedule-repeat'); + expect(repeated).toMatchObject({ id: scheduled.id, version: scheduled.version, state: 'Scheduled' }); + const first = await repository.runDuePublicationJobs(new Date(scheduledAt.getTime() + 1000)); + const second = await repository.runDuePublicationJobs(new Date(scheduledAt.getTime() + 2000)); + expect(first).toEqual({ claimed: 1, completed: 1, failed: 0 }); + expect(second).toEqual({ claimed: 0, completed: 0, failed: 0 }); + const final = await service.get(editor, approved.id); + expect(final).toMatchObject({ state: 'Published', publishedRevisionId: approved.draftRevisionId }); + const publicationRows = await database.db.select().from(publications).where(eq(publications.idempotencyKey, 'schedule:ai-governance:1')); + const auditRows = await database.db.select().from(auditEvents).where(and(eq(auditEvents.targetId, approved.id), eq(auditEvents.action, 'post.publish_scheduled'))); + expect(publicationRows).toHaveLength(1); + expect(auditRows).toHaveLength(1); + }); + + it('retries malformed durable jobs three times without changing a publication', async () => { + const now = new Date(); + await database.db.insert(jobs).values({ + id: 'job-malformed', workspaceId: DEMO_WORKSPACE_ID, type: 'publish', payload: { invalid: true }, + status: 'pending', runAt: now, idempotencyKey: 'job:malformed', createdAt: now, updatedAt: now, + }); + for (let attempt = 0; attempt < 3; attempt += 1) { + const result = await repository.runDuePublicationJobs(new Date(now.getTime() + 120_000 + attempt * 120_000)); + expect(result.failed).toBe(1); + } + expect(await repository.runDuePublicationJobs(new Date(now.getTime() + 600_000))).toEqual({ claimed: 0, completed: 0, failed: 0 }); + const [failed] = await database.db.select().from(jobs).where(eq(jobs.id, 'job-malformed')); + expect(failed).toMatchObject({ status: 'failed', attempts: 3, lastErrorCode: 'INTERNAL_FAILURE' }); + }); + + it('denies Author publication and rejects an illegal approval', async () => { + const author = context('Author'); + const draft = await service.get(author, 'post-editorial-systems'); + const submitted = await service.transition(author, draft.id, { expectedVersion: draft.version, action: 'submit' }, 'submit-denied'); + const approved = await service.transition(context('Reviewer'), draft.id, { expectedVersion: submitted.version, action: 'approve' }, 'approve-denied'); + await expect(service.transition(author, draft.id, { expectedVersion: approved.version, action: 'publish', idempotencyKey: 'forbidden:publish' }, 'forbidden')) + .rejects.toMatchObject({ code: 'FORBIDDEN' }); + await expect(service.transition(context('Reviewer'), draft.id, { expectedVersion: approved.version, action: 'approve' }, 'illegal')) + .rejects.toMatchObject({ code: 'ILLEGAL_TRANSITION' }); + const oldRevision = await database.db.select().from(revisions).where(eq(revisions.postId, draft.id)); + expect(oldRevision).toHaveLength(3); + }); +}); diff --git a/tests/integration/media-security.test.ts b/tests/integration/media-security.test.ts new file mode 100644 index 0000000..a48e2a8 --- /dev/null +++ b/tests/integration/media-security.test.ts @@ -0,0 +1,129 @@ +import { mkdir } from 'node:fs/promises'; +import { join } from 'node:path'; + +import sharp from 'sharp'; +import { beforeEach, afterEach, describe, expect, it } from 'vitest'; + +import { DatabaseMediaProvider } from '@/src/modules/media/database-provider'; +import { DrizzleMediaRepository } from '@/src/modules/media/drizzle-repository'; +import { MediaCleanupWorker } from '@/src/modules/media/cleanup-worker'; +import type { MediaProvider } from '@/src/modules/media/provider'; +import { MediaService } from '@/src/modules/media/service'; +import { DEMO_WORKSPACE_ID } from '@/src/modules/identity/demo'; +import type { MembershipContext } from '@/src/modules/identity/domain'; +import { createDatabase, type DatabaseContext } from '@/src/platform/db/client'; +import { migrateDatabase } from '@/src/platform/db/migrate'; +import { seedDemo } from '@/src/platform/db/seed'; + +class MemoryMediaProvider implements MediaProvider { + readonly objects = new Map<string, Buffer>(); + failPut = false; + failDelete = false; + + async put(key: string, data: Buffer, signal: AbortSignal) { + signal.throwIfAborted(); + if (this.failPut) throw new Error('injected provider failure'); + this.objects.set(key, data); + } + async get(key: string, signal: AbortSignal) { signal.throwIfAborted(); return this.objects.get(key) ?? null; } + async delete(key: string, signal: AbortSignal) { signal.throwIfAborted(); if (this.failDelete) throw new Error('injected cleanup failure'); this.objects.delete(key); } +} + +let database: DatabaseContext; +let repository: DrizzleMediaRepository; +let provider: MemoryMediaProvider; +let service: MediaService; +let png: Buffer; + +const context = (role: MembershipContext['role'], workspaceId = DEMO_WORKSPACE_ID): MembershipContext => ({ + workspaceId, workspaceSlug: 'demo', workspaceName: 'AutoBlog Editorial Lab', userId: `demo-${role.toLowerCase()}`, + userName: role, userEmail: `${role.toLowerCase()}@demo.autoblog.local`, role, +}); + +beforeEach(async () => { + await mkdir('data/tests', { recursive: true }); + database = createDatabase(`file:${join('data', 'tests', `${crypto.randomUUID()}.db`).replaceAll('\\', '/')}`); + await migrateDatabase(database.client); await seedDemo(database); + repository = new DrizzleMediaRepository(database); provider = new MemoryMediaProvider(); + service = new MediaService(repository, provider, 1_000_000); + png = await sharp({ create: { width: 80, height: 60, channels: 3, background: '#c8ff5a' } }).png().toBuffer(); +}); + +afterEach(() => database.client.close()); + +const upload = (replaceAssetId?: string) => service.upload(context('Author'), { + postId: 'post-editorial-systems', altText: 'Editorial cover', fileName: '../cover.png', + declaredMimeType: 'image/png', data: png, ...(replaceAssetId ? { replaceAssetId } : {}), +}, crypto.randomUUID()); + +describe('media security and compensation', () => { + it('preserves the active asset when replacement provider storage fails', async () => { + const active = await upload(); + provider.failPut = true; + await expect(upload(active.id)).rejects.toMatchObject({ code: 'PROVIDER_UNAVAILABLE' }); + expect(await service.list(context('Author'), 'post-editorial-systems')).toEqual([expect.objectContaining({ id: active.id, status: 'active' })]); + expect((await repository.find(DEMO_WORKSPACE_ID, active.id))?.status).toBe('active'); + }); + + it('activates a verified replacement before idempotent cleanup removes the old object', async () => { + const oldAsset = await upload(); + const oldStored = await repository.find(DEMO_WORKSPACE_ID, oldAsset.id); + const replacement = await upload(oldAsset.id); + expect(replacement.replacesAssetId).toBe(oldAsset.id); + expect((await repository.find(DEMO_WORKSPACE_ID, oldAsset.id))?.status).toBe('replaced'); + expect((await service.list(context('Author'), 'post-editorial-systems')).map((asset) => asset.id)).toEqual([replacement.id]); + const worker = new MediaCleanupWorker(database, provider); + provider.failDelete = true; + expect(await worker.run(new Date(Date.now() + 1000))).toEqual({ claimed: 1, completed: 0, failed: 1 }); + expect((await service.read(context('Author'), replacement.id)).data).toEqual(png); + provider.failDelete = false; + expect(await worker.run(new Date(Date.now() + 120_000))).toEqual({ claimed: 1, completed: 1, failed: 0 }); + expect(await worker.run(new Date(Date.now() + 121_000))).toEqual({ claimed: 0, completed: 0, failed: 0 }); + expect(provider.objects.has(oldStored?.storageKey ?? '')).toBe(false); + expect((await service.read(context('Author'), replacement.id)).data).toEqual(png); + }); + + it('compensates a metadata finalization failure without exposing an orphan', async () => { + class FailingRepository extends DrizzleMediaRepository { + override async finalize(): Promise<never> { throw new Error('injected metadata failure'); } + } + const failing = new MediaService(new FailingRepository(database), provider, 1_000_000); + await expect(failing.upload(context('Author'), { + postId: 'post-editorial-systems', altText: '', fileName: 'cover.png', declaredMimeType: 'image/png', data: png, + }, 'metadata-failure')).rejects.toThrow('injected metadata failure'); + expect(provider.objects.size).toBe(0); + }); + + it('rejects forged and oversized content before provider storage and isolates workspace reads', async () => { + await expect(service.upload(context('Author'), { + postId: 'post-editorial-systems', fileName: 'forged.jpg', declaredMimeType: 'image/jpeg', data: png, + }, 'forged')).rejects.toMatchObject({ code: 'VALIDATION_FAILED' }); + const tinyLimit = new MediaService(repository, provider, png.byteLength - 1); + await expect(tinyLimit.upload(context('Author'), { + postId: 'post-editorial-systems', fileName: 'large.png', declaredMimeType: 'image/png', data: png, + }, 'oversize')).rejects.toMatchObject({ code: 'VALIDATION_FAILED' }); + expect(provider.objects.size).toBe(0); + const asset = await upload(); + await expect(service.read(context('Author', 'different-workspace'), asset.id)).rejects.toMatchObject({ code: 'NOT_FOUND' }); + }); + + it('requires deletion policy and completes durable cleanup', async () => { + const asset = await upload(); + await expect(service.delete(context('Author'), asset.id, 'delete-denied')).rejects.toMatchObject({ code: 'FORBIDDEN' }); + await service.delete(context('Editor'), asset.id, 'delete-allowed'); + await expect(service.read(context('Editor'), asset.id)).rejects.toMatchObject({ code: 'NOT_FOUND' }); + const worker = new MediaCleanupWorker(database, provider); + expect((await worker.run(new Date(Date.now() + 1000))).completed).toBe(1); + expect((await repository.find(DEMO_WORKSPACE_ID, asset.id))?.status).toBe('deleted'); + }); + + it('database provider stores opaque data durably', async () => { + const durable = new DatabaseMediaProvider(database); + const key = `${DEMO_WORKSPACE_ID}/provider-contract`; + const signal = new AbortController().signal; + await durable.put(key, png, signal); + expect(await durable.get(key, signal)).toEqual(png); + await durable.delete(key, signal); + expect(await durable.get(key, signal)).toBeNull(); + }); +}); diff --git a/tests/lighthouse/reports.spec.ts b/tests/lighthouse/reports.spec.ts new file mode 100644 index 0000000..eee7936 --- /dev/null +++ b/tests/lighthouse/reports.spec.ts @@ -0,0 +1,104 @@ +import { mkdir, rm, writeFile } from 'node:fs/promises'; +import { chromium, expect, test } from '@playwright/test'; +import { launch } from 'chrome-launcher'; +import lighthouse from 'lighthouse'; + +const OUTPUT_DIRECTORY = '.lighthouse'; +const ROUTES = [ + { name: 'marketing', url: 'http://127.0.0.1:3100/' }, + { name: 'sign-in', url: 'http://127.0.0.1:3100/sign-in' }, +] as const; +const BUDGETS = { + performance: 0.8, + accessibility: 0.95, + bestPractices: 0.9, + seo: 0.9, + lcpMs: 2_500, + cls: 0.1, +} as const; + +type Metrics = Readonly<{ + performance: number; + accessibility: number; + bestPractices: number; + seo: number; + lcpMs: number; + cls: number; +}>; + +function required(value: number | null | undefined, metric: string): number { + if (value === null || value === undefined) throw new Error(`Lighthouse omitted ${metric}.`); + return value; +} + +function median(values: readonly number[]): number { + const ordered = [...values].sort((left, right) => left - right); + return required(ordered[Math.floor(ordered.length / 2)], 'median value'); +} + +function medians(runs: readonly Metrics[]): Metrics { + return { + performance: median(runs.map((run) => run.performance)), + accessibility: median(runs.map((run) => run.accessibility)), + bestPractices: median(runs.map((run) => run.bestPractices)), + seo: median(runs.map((run) => run.seo)), + lcpMs: median(runs.map((run) => run.lcpMs)), + cls: median(runs.map((run) => run.cls)), + }; +} + +test('six production Lighthouse reports satisfy the release budgets', async () => { + await rm(OUTPUT_DIRECTORY, { recursive: true, force: true }); + await mkdir(OUTPUT_DIRECTORY, { recursive: true }); + const chrome = await launch({ + chromePath: chromium.executablePath(), + chromeFlags: ['--headless=new', '--no-sandbox', '--disable-dev-shm-usage'], + }); + const summary: Record<string, Metrics> = {}; + + try { + for (const route of ROUTES) { + const runs: Metrics[] = []; + for (let index = 1; index <= 3; index += 1) { + const result = await lighthouse(route.url, { + port: chrome.port, + output: 'html', + logLevel: 'error', + onlyCategories: ['performance', 'accessibility', 'best-practices', 'seo'], + formFactor: 'desktop', + screenEmulation: { mobile: false, width: 1_350, height: 940, deviceScaleFactor: 1, disabled: false }, + }); + if (!result) throw new Error(`Lighthouse returned no result for ${route.name}.`); + const metrics = { + performance: required(result.lhr.categories.performance?.score, 'performance score'), + accessibility: required(result.lhr.categories.accessibility?.score, 'accessibility score'), + bestPractices: required(result.lhr.categories['best-practices']?.score, 'best-practices score'), + seo: required(result.lhr.categories.seo?.score, 'SEO score'), + lcpMs: required(result.lhr.audits['largest-contentful-paint']?.numericValue, 'LCP'), + cls: required(result.lhr.audits['cumulative-layout-shift']?.numericValue, 'CLS'), + } satisfies Metrics; + runs.push(metrics); + const report = Array.isArray(result.report) ? result.report[0] : result.report; + if (!report) throw new Error(`Lighthouse returned no HTML report for ${route.name}.`); + await writeFile(`${OUTPUT_DIRECTORY}/${route.name}-${index}.html`, report, 'utf8'); + } + summary[route.name] = medians(runs); + } + } finally { + await chrome.kill(); + } + + await writeFile(`${OUTPUT_DIRECTORY}/summary.json`, `${JSON.stringify(summary, null, 2)}\n`, 'utf8'); + process.stdout.write(`[lighthouse] ${JSON.stringify(summary)}\n`); + for (const route of ROUTES) { + const metrics = summary[route.name]; + expect(metrics, `${route.name} summary`).toBeDefined(); + if (!metrics) continue; + expect(metrics.performance).toBeGreaterThanOrEqual(BUDGETS.performance); + expect(metrics.accessibility).toBeGreaterThanOrEqual(BUDGETS.accessibility); + expect(metrics.bestPractices).toBeGreaterThanOrEqual(BUDGETS.bestPractices); + expect(metrics.seo).toBeGreaterThanOrEqual(BUDGETS.seo); + expect(metrics.lcpMs).toBeLessThanOrEqual(BUDGETS.lcpMs); + expect(metrics.cls).toBeLessThanOrEqual(BUDGETS.cls); + } +}); diff --git a/tests/performance/budgets.spec.ts b/tests/performance/budgets.spec.ts new file mode 100644 index 0000000..7b1e4a9 --- /dev/null +++ b/tests/performance/budgets.spec.ts @@ -0,0 +1,95 @@ +import { mkdir, rm, writeFile } from 'node:fs/promises'; +import { expect, test, type BrowserContext, type Page } from '@playwright/test'; + +const BUDGETS = { + lcpMs: 2_500, + inpProxyMs: 200, + workflowResponseMs: 500, + cls: 0.1, + landingJavaScriptBytes: 180 * 1024, + workspaceJavaScriptBytes: 320 * 1024, +} as const; + +type Metrics = Readonly<{ lcp: number; cls: number; eventDuration: number; javaScriptBytes: number }>; +const report: Record<string, unknown> = {}; + +test.beforeAll(async () => { + await rm('.performance', { recursive: true, force: true }); + await mkdir('.performance', { recursive: true }); +}); + +test.afterAll(async () => { + await writeFile('.performance/bundle-report.json', `${JSON.stringify(report, null, 2)}\n`, 'utf8'); +}); + +async function installObservers(context: BrowserContext) { + await context.addInitScript(() => { + const metrics = { lcp: 0, cls: 0, eventDuration: 0 }; + Object.defineProperty(window, '__autoblogMetrics', { value: metrics, configurable: true }); + new PerformanceObserver((list) => { + for (const entry of list.getEntries()) metrics.lcp = Math.max(metrics.lcp, entry.startTime); + }).observe({ type: 'largest-contentful-paint', buffered: true }); + new PerformanceObserver((list) => { + for (const entry of list.getEntries()) { + const shift = entry as PerformanceEntry & { hadRecentInput?: boolean; value?: number }; + if (!shift.hadRecentInput) metrics.cls += shift.value ?? 0; + } + }).observe({ type: 'layout-shift', buffered: true }); + try { + new PerformanceObserver((list) => { + for (const entry of list.getEntries()) metrics.eventDuration = Math.max(metrics.eventDuration, entry.duration); + }).observe({ type: 'event', buffered: true, durationThreshold: 16 } as PerformanceObserverInit & { durationThreshold: number }); + } catch { /* Event Timing is optional in older Chromium builds. */ } + }); +} + +async function readMetrics(page: Page): Promise<Metrics> { + await page.waitForTimeout(800); + return page.evaluate(() => { + const metrics = (window as unknown as { __autoblogMetrics: { lcp: number; cls: number; eventDuration: number } }).__autoblogMetrics; + const javaScriptBytes = performance.getEntriesByType('resource') + .map((entry) => entry as PerformanceResourceTiming) + .filter((entry) => entry.initiatorType === 'script') + .reduce((total, entry) => total + (entry.transferSize || entry.encodedBodySize), 0); + return { ...metrics, javaScriptBytes }; + }); +} + +test('marketing route stays inside production Web Vital and JavaScript budgets', async ({ browser }) => { + const context = await browser.newContext(); await installObservers(context); + const page = await context.newPage(); await page.goto('/'); + const metrics = await readMetrics(page); + report.marketing = metrics; + process.stdout.write(`[performance] landing ${JSON.stringify(metrics)}\n`); + expect(metrics.lcp).toBeGreaterThan(0); + expect(metrics.lcp).toBeLessThanOrEqual(BUDGETS.lcpMs); + expect(metrics.cls).toBeLessThanOrEqual(BUDGETS.cls); + expect(metrics.javaScriptBytes).toBeLessThanOrEqual(BUDGETS.landingJavaScriptBytes); + await context.close(); +}); + +test('cold authenticated workspace stays inside production interaction and bundle budgets', async ({ browser }) => { + const authentication = await browser.newContext(); + const signIn = await authentication.newPage(); await signIn.goto('/sign-in'); + await signIn.getByRole('button', { name: /^Author/u }).click(); + await expect(signIn).toHaveURL(/\/workspace\/ws-demo$/u, { timeout: 20_000 }); + const storageState = await authentication.storageState(); await authentication.close(); + + const context = await browser.newContext({ storageState }); await installObservers(context); + const page = await context.newPage(); await page.goto('/workspace/ws-demo'); + const before = await page.evaluate(() => performance.now()); + await page.getByRole('button', { name: 'Open revision history' }).click(); + await expect(page.getByRole('region', { name: 'Revision history' })).toBeVisible(); + const interactionMs = await page.evaluate((started) => performance.now() - started, before); + const metrics = await readMetrics(page); + const inpUpperBoundMs = metrics.eventDuration || 16; + report.workspace = { ...metrics, inpUpperBoundMs, workflowResponseMs: Math.round(interactionMs) }; + process.stdout.write(`[performance] workspace ${JSON.stringify({ ...metrics, inpUpperBoundMs, workflowResponseMs: Math.round(interactionMs) })}\n`); + expect(metrics.lcp).toBeGreaterThan(0); + expect(metrics.lcp).toBeLessThanOrEqual(BUDGETS.lcpMs); + expect(metrics.cls).toBeLessThanOrEqual(BUDGETS.cls); + expect(inpUpperBoundMs).toBeLessThanOrEqual(BUDGETS.inpProxyMs); + expect(interactionMs).toBeLessThanOrEqual(BUDGETS.workflowResponseMs); + expect(metrics.javaScriptBytes).toBeLessThanOrEqual(BUDGETS.workspaceJavaScriptBytes); + await context.close(); +}); diff --git a/tests/unit/ai-adapters.test.ts b/tests/unit/ai-adapters.test.ts new file mode 100644 index 0000000..674554a --- /dev/null +++ b/tests/unit/ai-adapters.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest'; + +import type { AIAdapter } from '@/src/modules/ai/adapter'; +import { GeminiAIAdapter } from '@/src/modules/ai/gemini-adapter'; +import { MockAIAdapter } from '@/src/modules/ai/mock-adapter'; + +const input = { postId: 'post-1', title: 'Editorial systems', excerpt: '', content: 'A source draft.', instruction: 'Improve the structure.' }; + +async function expectAdapterContract(adapter: AIAdapter) { + const result = await adapter.suggest(input, new AbortController().signal); + expect(result.suggestion.title.length).toBeGreaterThan(2); + expect(result.suggestion.title.length).toBeLessThanOrEqual(180); + expect(result.suggestion.content).toContain('source draft'); + expect(result.provider).toBeTruthy(); + expect(result.model).toBeTruthy(); +} + +describe('AI adapter contract', () => { + it('is shared by deterministic mock and configured Gemini transport', async () => { + await expectAdapterContract(new MockAIAdapter()); + await expectAdapterContract(new GeminiAIAdapter('test-key', 'test-model', async () => ({ + text: JSON.stringify({ title: 'Editorial systems, refined', excerpt: 'A concise angle.', content: 'A source draft.', rationale: 'Improved hierarchy.' }), + inputTokens: 12, outputTokens: 8, + }))); + }); + + it('rejects malformed or excessive provider output', async () => { + const malformed = new GeminiAIAdapter('test-key', 'test-model', async () => ({ text: '{invalid' })); + await expect(malformed.suggest(input, new AbortController().signal)).rejects.toMatchObject({ code: 'PROVIDER_UNAVAILABLE' }); + const excessive = new GeminiAIAdapter('test-key', 'test-model', async () => ({ text: JSON.stringify({ title: 'x'.repeat(181), excerpt: '', content: '', rationale: 'valid rationale' }) })); + await expect(excessive.suggest(input, new AbortController().signal)).rejects.toMatchObject({ code: 'PROVIDER_UNAVAILABLE' }); + }); + + it('honors cancellation in mock mode', async () => { + const controller = new AbortController(); controller.abort(); + await expect(new MockAIAdapter().suggest(input, controller.signal)).rejects.toThrow(); + }); +}); diff --git a/tests/unit/containment.test.ts b/tests/unit/containment.test.ts new file mode 100644 index 0000000..db49e39 --- /dev/null +++ b/tests/unit/containment.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, it } from 'vitest'; + +import { PRODUCT_LIMITS } from '@/src/platform/config/limits'; + +describe('containment limits', () => { + it('keeps costly public boundaries finite', () => { + expect(PRODUCT_LIMITS.mediaMaxBytes).toBe(5_242_880); + expect(PRODUCT_LIMITS.aiPromptMaxCharacters).toBeLessThanOrEqual(8_000); + expect(PRODUCT_LIMITS.jobMaxAttempts).toBeGreaterThan(0); + }); +}); diff --git a/tests/unit/documentation-contract.test.ts b/tests/unit/documentation-contract.test.ts new file mode 100644 index 0000000..06e2d60 --- /dev/null +++ b/tests/unit/documentation-contract.test.ts @@ -0,0 +1,52 @@ +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; + +const root = resolve(import.meta.dirname, '../..'); + +function read(relativePath: string): string { + return readFileSync(resolve(root, relativePath), 'utf8'); +} + +function localLinks(relativePath: string): string[] { + const document = read(relativePath); + return [...document.matchAll(/\]\((?!https?:|#)([^)]+)\)/gu)].flatMap((match) => { + const target = match[1]; + return target ? [target.replace(/#.*/u, '')] : []; + }); +} + +describe('release documentation contract', () => { + it('has no broken local evidence links', () => { + for (const relativePath of [ + 'README.md', + 'FEATURES.md', + 'SECURITY.md', + 'docs/data-model.md', + 'docs/deployment.md', + 'docs/releases/v2.0.0.md', + ]) { + const base = dirname(resolve(root, relativePath)); + for (const link of localLinks(relativePath)) { + expect(existsSync(resolve(base, link)), `Missing ${relativePath} link: ${link}`).toBe(true); + } + } + }); + + it('keeps every RFC problem visible and rejects superseded product claims', () => { + const ledger = read('docs/engineering/rfc-closure-ledger.md'); + for (let problem = 1; problem <= 18; problem += 1) { + expect(ledger).toContain(`P-${String(problem).padStart(2, '0')}`); + } + + const publicDocs = `${read('README.md')}\n${read('FEATURES.md')}`; + for (const supersededClaim of [ + 'WCAG AA Compliant', + '20+ example posts', + 'Session Storage: Browser-based state persistence', + 'Choose between Gemini 1.5 Flash', + ]) { + expect(publicDocs).not.toContain(supersededClaim); + } + }); +}); diff --git a/tests/unit/editorial-workflow.test.ts b/tests/unit/editorial-workflow.test.ts new file mode 100644 index 0000000..54f8630 --- /dev/null +++ b/tests/unit/editorial-workflow.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest'; + +import { resolveTransition, TRANSITION_RULES } from '@/src/modules/editorial/workflow'; + +describe('editorial workflow', () => { + it('defines the flagship state sequence and permissions', () => { + expect(resolveTransition('Draft', 'submit')).toMatchObject({ to: 'InReview', permission: 'post.submit' }); + expect(resolveTransition('InReview', 'approve')).toMatchObject({ to: 'Approved', permission: 'review.approve' }); + expect(resolveTransition('Approved', 'schedule')).toMatchObject({ to: 'Scheduled', permission: 'post.schedule' }); + expect(resolveTransition('Scheduled', 'publish')).toMatchObject({ to: 'Published', permission: 'post.publish' }); + }); + + it('rejects every action outside its declared source states', () => { + const states = ['Draft', 'InReview', 'ChangesRequested', 'Approved', 'Scheduled', 'Published', 'Archived'] as const; + for (const [action, rule] of Object.entries(TRANSITION_RULES)) { + for (const state of states.filter((candidate) => !rule.from.includes(candidate))) { + expect(() => resolveTransition(state, action as keyof typeof TRANSITION_RULES)).toThrowError(expect.objectContaining({ code: 'ILLEGAL_TRANSITION' })); + } + } + }); +}); diff --git a/tests/unit/media-validation.test.ts b/tests/unit/media-validation.test.ts new file mode 100644 index 0000000..6a5d65c --- /dev/null +++ b/tests/unit/media-validation.test.ts @@ -0,0 +1,27 @@ +import sharp from 'sharp'; +import { describe, expect, it } from 'vitest'; + +import { safeFileName, verifyImage } from '@/src/modules/media/domain'; + +describe('media boundary validation', () => { + it('decodes allowlisted image content and sanitizes the untrusted name', async () => { + const data = await sharp({ create: { width: 32, height: 24, channels: 3, background: '#c8ff5a' } }).png().toBuffer(); + const verified = await verifyImage({ data, fileName: '../../ unsafe résumé.png', declaredMimeType: 'image/png', maxBytes: 1_000_000 }); + expect(verified).toMatchObject({ mimeType: 'image/png', width: 32, height: 24 }); + expect(verified.fileName).toBe('unsafe-r-sum-.png'); + expect(verified.checksum).toMatch(/^[a-f0-9]{64}$/u); + }); + + it('rejects forged MIME, non-images, oversize and excessive dimensions', async () => { + const png = await sharp({ create: { width: 8, height: 8, channels: 3, background: '#000' } }).png().toBuffer(); + await expect(verifyImage({ data: png, fileName: 'forged.jpg', declaredMimeType: 'image/jpeg', maxBytes: 1_000_000 })).rejects.toMatchObject({ code: 'VALIDATION_FAILED' }); + await expect(verifyImage({ data: Buffer.from('not an image'), fileName: 'fake.png', declaredMimeType: 'image/png', maxBytes: 1_000_000 })).rejects.toMatchObject({ code: 'VALIDATION_FAILED' }); + await expect(verifyImage({ data: png, fileName: 'large.png', declaredMimeType: 'image/png', maxBytes: png.byteLength - 1 })).rejects.toMatchObject({ code: 'VALIDATION_FAILED' }); + const wide = await sharp({ create: { width: 4097, height: 1, channels: 3, background: '#000' } }).png().toBuffer(); + await expect(verifyImage({ data: wide, fileName: 'wide.png', declaredMimeType: 'image/png', maxBytes: 1_000_000 })).rejects.toMatchObject({ code: 'VALIDATION_FAILED' }); + }); + + it('removes path components and unsafe filename characters', () => { + expect(safeFileName('C:\\temp\\<script>.webp')).toBe('script-.webp'); + }); +}); diff --git a/tests/unit/rbac-policy.test.ts b/tests/unit/rbac-policy.test.ts new file mode 100644 index 0000000..a570111 --- /dev/null +++ b/tests/unit/rbac-policy.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest'; + +import { permissionSchema, roleSchema, type Permission, type Role } from '@/src/modules/identity/domain'; +import { can, PERMISSION_MATRIX } from '@/src/modules/identity/policy'; + +const expected: Readonly<Record<Role, readonly Permission[]>> = { + Owner: permissionSchema.options, + Admin: permissionSchema.options.filter((permission) => permission !== 'demo.reset'), + Editor: ['workspace.read', 'post.create', 'post.update', 'post.delete', 'post.submit', 'review.request_changes', 'review.approve', 'post.schedule', 'post.publish', 'post.archive', 'revision.restore', 'media.upload', 'media.delete', 'ai.suggest'], + Author: ['workspace.read', 'post.create', 'post.update', 'post.submit', 'revision.restore', 'media.upload', 'ai.suggest'], + Reviewer: ['workspace.read', 'review.request_changes', 'review.approve', 'ai.suggest'], +}; + +describe('RBAC permission matrix', () => { + it.each(roleSchema.options)('evaluates every command for %s', (role) => { + for (const permission of permissionSchema.options) { + expect(can(role, permission, { actorId: 'actor', ownerId: 'actor' }), `${role}:${permission}`).toBe(expected[role].includes(permission)); + expect(PERMISSION_MATRIX[permission].roles.includes(role), `declared:${role}:${permission}`).toBe(expected[role].includes(permission)); + } + }); + + it('denies author ownership-scoped commands against another author', () => { + for (const permission of ['post.update', 'post.submit', 'revision.restore', 'media.upload'] as const) { + expect(can('Author', permission, { actorId: 'author-a', ownerId: 'author-b' })).toBe(false); + } + }); +}); diff --git a/tests/unit/validation-errors.test.ts b/tests/unit/validation-errors.test.ts new file mode 100644 index 0000000..773e2f0 --- /dev/null +++ b/tests/unit/validation-errors.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest'; + +import { createPostSchema, savePostSchema } from '@/src/modules/editorial/domain'; +import { AppError, normalizeError } from '@/src/platform/observability/errors'; + +describe('external validation and public errors', () => { + it('rejects oversized or structurally invalid editorial input', () => { + expect(createPostSchema.safeParse({ title: 'x', content: '' }).success).toBe(false); + expect(savePostSchema.safeParse({ expectedVersion: 0, title: 'Valid title', excerpt: '', content: '' }).success).toBe(false); + }); + + it('maps unknown internal details to a stable non-sensitive error', () => { + const error = normalizeError(new Error('database-password=must-not-leak')); + expect(error).toBeInstanceOf(AppError); + expect(error.code).toBe('INTERNAL_FAILURE'); + expect(error.message).not.toContain('database-password'); + }); +}); diff --git a/tests/visual/surfaces.spec.ts b/tests/visual/surfaces.spec.ts new file mode 100644 index 0000000..8372877 --- /dev/null +++ b/tests/visual/surfaces.spec.ts @@ -0,0 +1,33 @@ +import { expect, test } from '@playwright/test'; + +test('marketing desktop visual', async ({ page }) => { + await page.goto('/'); + await expect(page).toHaveScreenshot('marketing-desktop.png', { fullPage: true, animations: 'disabled' }); +}); + +test('sign-in desktop visual', async ({ page }) => { + await page.goto('/sign-in'); + await expect(page).toHaveScreenshot('sign-in-desktop.png', { fullPage: true, animations: 'disabled' }); +}); + +test('workspace desktop visual', async ({ page }) => { + await page.goto('/sign-in'); + await page.getByRole('button', { name: /^Author/u }).click(); + await expect(page).toHaveURL(/\/workspace\/ws-demo$/u, { timeout: 20_000 }); + await expect(page).toHaveScreenshot('workspace-desktop.png', { fullPage: true, animations: 'disabled' }); +}); + +test('public preview desktop visual', async ({ page }) => { + await page.goto('/preview/demo/immutable-publishing'); + await expect(page).toHaveScreenshot('public-preview-desktop.png', { fullPage: true, animations: 'disabled' }); +}); + +test('marketing and workspace mobile visuals', async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }); + await page.goto('/'); + await expect(page).toHaveScreenshot('marketing-mobile.png', { fullPage: true, animations: 'disabled' }); + await page.goto('/sign-in'); + await page.getByRole('button', { name: /^Author/u }).click(); + await expect(page).toHaveURL(/\/workspace\/ws-demo$/u, { timeout: 20_000 }); + await expect(page).toHaveScreenshot('workspace-mobile.png', { fullPage: true, animations: 'disabled' }); +}); diff --git a/tests/visual/surfaces.spec.ts-snapshots/marketing-desktop.png b/tests/visual/surfaces.spec.ts-snapshots/marketing-desktop.png new file mode 100644 index 0000000..75dbcc0 Binary files /dev/null and b/tests/visual/surfaces.spec.ts-snapshots/marketing-desktop.png differ diff --git a/tests/visual/surfaces.spec.ts-snapshots/marketing-mobile.png b/tests/visual/surfaces.spec.ts-snapshots/marketing-mobile.png new file mode 100644 index 0000000..43e212a Binary files /dev/null and b/tests/visual/surfaces.spec.ts-snapshots/marketing-mobile.png differ diff --git a/tests/visual/surfaces.spec.ts-snapshots/public-preview-desktop.png b/tests/visual/surfaces.spec.ts-snapshots/public-preview-desktop.png new file mode 100644 index 0000000..be121a9 Binary files /dev/null and b/tests/visual/surfaces.spec.ts-snapshots/public-preview-desktop.png differ diff --git a/tests/visual/surfaces.spec.ts-snapshots/sign-in-desktop.png b/tests/visual/surfaces.spec.ts-snapshots/sign-in-desktop.png new file mode 100644 index 0000000..9a6a136 Binary files /dev/null and b/tests/visual/surfaces.spec.ts-snapshots/sign-in-desktop.png differ diff --git a/tests/visual/surfaces.spec.ts-snapshots/workspace-desktop.png b/tests/visual/surfaces.spec.ts-snapshots/workspace-desktop.png new file mode 100644 index 0000000..b1ca6d5 Binary files /dev/null and b/tests/visual/surfaces.spec.ts-snapshots/workspace-desktop.png differ diff --git a/tests/visual/surfaces.spec.ts-snapshots/workspace-mobile.png b/tests/visual/surfaces.spec.ts-snapshots/workspace-mobile.png new file mode 100644 index 0000000..a183b33 Binary files /dev/null and b/tests/visual/surfaces.spec.ts-snapshots/workspace-mobile.png differ diff --git a/tsconfig.json b/tsconfig.json index 30f9045..0040500 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,27 +1,56 @@ { - "compilerOptions": { - "target": "ES2017", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": false, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": ["./*"] - } - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "compilerOptions": { + "target": "ES2022", + "lib": [ + "dom", + "dom.iterable", + "es2023" + ], + "allowJs": false, + "skipLibCheck": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "types": [ + "bun", + "node" + ], + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "app/**/*.ts", + "app/**/*.tsx", + "src/**/*.ts", + "src/**/*.tsx", + "scripts/**/*.ts", + "tests/**/*.ts", + "tests/**/*.tsx", + "*.config.ts", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules", + ".next", + "test-results", + "playwright-report" + ] } diff --git a/types/group.d.ts b/types/group.d.ts deleted file mode 100644 index c3eacdb..0000000 --- a/types/group.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Post } from './post'; - -export type Group<T = Post> = { id: string; name: string; sub_groups: SubGroup<T>[] }; -export type SubGroup<T = Post> = { id: string; name: string; items: T[] }; diff --git a/types/post.d.ts b/types/post.d.ts deleted file mode 100644 index bd4dce5..0000000 --- a/types/post.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Types } from 'mongoose'; - -export type PostBase = { - _id: string; - name: string; - image: { public_id: string; display_name: string }; -}; - -export type Journal = PostBase & { - title: string; - description: string; - state?: string; - kind?: string; - date: string; -}; - -export type Event = PostBase & { - title: string; - description: string; - state?: string; - kind?: string; - date: { from: string; to: string }; -}; - -export type Post = { - _id: string; - group: { name: string; _id: string }; - sub_group: { name: string; _id: string }; -} & (Event | Journal); - -export type Post_serverOnly = { - _id: Types.ObjectId; - group: { name: string; _id: Types.ObjectId }; - sub_group: { name: string; _id: Types.ObjectId }; -} & (Event | Journal); diff --git a/types/sidebar.d.ts b/types/sidebar.d.ts deleted file mode 100644 index b0ba50e..0000000 --- a/types/sidebar.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type Group = { - id: string; - name: string; -}; diff --git a/types/user.d.ts b/types/user.d.ts deleted file mode 100644 index 62fca76..0000000 --- a/types/user.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -export type Role = { - id: string; - name: 'admin' | 'cooperator' | 'developer'; -}; -export type User = { - id: string; - username: string; - email: string; - password: string; - role: Role; -}; diff --git a/ui/alert-dialog.tsx b/ui/alert-dialog.tsx deleted file mode 100644 index 48a0ef8..0000000 --- a/ui/alert-dialog.tsx +++ /dev/null @@ -1,141 +0,0 @@ -"use client" - -import * as React from "react" -import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" - -import { cn } from "@/utils/shadcn" -import { buttonVariants } from "@/ui/button" - -const AlertDialog = AlertDialogPrimitive.Root - -const AlertDialogTrigger = AlertDialogPrimitive.Trigger - -const AlertDialogPortal = AlertDialogPrimitive.Portal - -const AlertDialogOverlay = React.forwardRef< - React.ElementRef<typeof AlertDialogPrimitive.Overlay>, - React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay> ->(({ className, ...props }, ref) => ( - <AlertDialogPrimitive.Overlay - className={cn( - "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", - className - )} - {...props} - ref={ref} - /> -)) -AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName - -const AlertDialogContent = React.forwardRef< - React.ElementRef<typeof AlertDialogPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content> ->(({ className, ...props }, ref) => ( - <AlertDialogPortal> - <AlertDialogOverlay /> - <AlertDialogPrimitive.Content - ref={ref} - className={cn( - "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", - className - )} - {...props} - /> - </AlertDialogPortal> -)) -AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName - -const AlertDialogHeader = ({ - className, - ...props -}: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn( - "flex flex-col space-y-2 text-center sm:text-left", - className - )} - {...props} - /> -) -AlertDialogHeader.displayName = "AlertDialogHeader" - -const AlertDialogFooter = ({ - className, - ...props -}: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn( - "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", - className - )} - {...props} - /> -) -AlertDialogFooter.displayName = "AlertDialogFooter" - -const AlertDialogTitle = React.forwardRef< - React.ElementRef<typeof AlertDialogPrimitive.Title>, - React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title> ->(({ className, ...props }, ref) => ( - <AlertDialogPrimitive.Title - ref={ref} - className={cn("text-lg font-semibold", className)} - {...props} - /> -)) -AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName - -const AlertDialogDescription = React.forwardRef< - React.ElementRef<typeof AlertDialogPrimitive.Description>, - React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description> ->(({ className, ...props }, ref) => ( - <AlertDialogPrimitive.Description - ref={ref} - className={cn("text-sm text-muted-foreground", className)} - {...props} - /> -)) -AlertDialogDescription.displayName = - AlertDialogPrimitive.Description.displayName - -const AlertDialogAction = React.forwardRef< - React.ElementRef<typeof AlertDialogPrimitive.Action>, - React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action> ->(({ className, ...props }, ref) => ( - <AlertDialogPrimitive.Action - ref={ref} - className={cn(buttonVariants(), className)} - {...props} - /> -)) -AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName - -const AlertDialogCancel = React.forwardRef< - React.ElementRef<typeof AlertDialogPrimitive.Cancel>, - React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel> ->(({ className, ...props }, ref) => ( - <AlertDialogPrimitive.Cancel - ref={ref} - className={cn( - buttonVariants({ variant: "outline" }), - "mt-2 sm:mt-0", - className - )} - {...props} - /> -)) -AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName - -export { - AlertDialog, - AlertDialogPortal, - AlertDialogOverlay, - AlertDialogTrigger, - AlertDialogContent, - AlertDialogHeader, - AlertDialogFooter, - AlertDialogTitle, - AlertDialogDescription, - AlertDialogAction, - AlertDialogCancel, -} diff --git a/ui/avatar.tsx b/ui/avatar.tsx deleted file mode 100644 index 166ad5f..0000000 --- a/ui/avatar.tsx +++ /dev/null @@ -1,50 +0,0 @@ -'use client'; - -import * as React from 'react'; -import * as AvatarPrimitive from '@radix-ui/react-avatar'; - -import { cn } from '@/utils/shadcn'; - -const Avatar = React.forwardRef< - React.ElementRef<typeof AvatarPrimitive.Root>, - React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> ->(({ className, ...props }, ref) => ( - <AvatarPrimitive.Root - ref={ref} - className={cn( - 'relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full', - className - )} - {...props} - /> -)); -Avatar.displayName = AvatarPrimitive.Root.displayName; - -const AvatarImage = React.forwardRef< - React.ElementRef<typeof AvatarPrimitive.Image>, - React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> ->(({ className, ...props }, ref) => ( - <AvatarPrimitive.Image - ref={ref} - className={cn('aspect-square h-full w-full', className)} - {...props} - /> -)); -AvatarImage.displayName = AvatarPrimitive.Image.displayName; - -const AvatarFallback = React.forwardRef< - React.ElementRef<typeof AvatarPrimitive.Fallback>, - React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> ->(({ className, ...props }, ref) => ( - <AvatarPrimitive.Fallback - ref={ref} - className={cn( - 'flex h-full w-full items-center justify-center rounded-full bg-muted', - className - )} - {...props} - /> -)); -AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; - -export { Avatar, AvatarImage, AvatarFallback }; diff --git a/ui/breadcrumb.tsx b/ui/breadcrumb.tsx deleted file mode 100644 index 57e2c86..0000000 --- a/ui/breadcrumb.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import * as React from 'react'; -import { Slot } from '@radix-ui/react-slot'; -import { ChevronRight, MoreHorizontal } from 'lucide-react'; - -import { cn } from '@/utils/shadcn'; - -const Breadcrumb = React.forwardRef< - HTMLElement, - React.ComponentPropsWithoutRef<'nav'> & { - separator?: React.ReactNode; - } ->(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />); -Breadcrumb.displayName = 'Breadcrumb'; - -const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<'ol'>>( - ({ className, ...props }, ref) => ( - <ol - ref={ref} - className={cn( - 'flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5', - className - )} - {...props} - /> - ) -); -BreadcrumbList.displayName = 'BreadcrumbList'; - -const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<'li'>>( - ({ className, ...props }, ref) => ( - <li - ref={ref} - className={cn('inline-flex items-center gap-1.5', className)} - {...props} - /> - ) -); -BreadcrumbItem.displayName = 'BreadcrumbItem'; - -const BreadcrumbLink = React.forwardRef< - HTMLAnchorElement, - React.ComponentPropsWithoutRef<'a'> & { - asChild?: boolean; - } ->(({ asChild, className, ...props }, ref) => { - const Comp = asChild ? Slot : 'a'; - - return ( - <Comp - ref={ref} - className={cn('transition-colors hover:text-foreground', className)} - {...props} - /> - ); -}); -BreadcrumbLink.displayName = 'BreadcrumbLink'; - -const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<'span'>>( - ({ className, ...props }, ref) => ( - <span - ref={ref} - role="link" - aria-disabled="true" - aria-current="page" - className={cn('font-normal text-foreground', className)} - {...props} - /> - ) -); -BreadcrumbPage.displayName = 'BreadcrumbPage'; - -const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<'li'>) => ( - <li - role="presentation" - aria-hidden="true" - className={cn('[&>svg]:w-3.5 [&>svg]:h-3.5', className)} - {...props}> - {children ?? <ChevronRight />} - </li> -); -BreadcrumbSeparator.displayName = 'BreadcrumbSeparator'; - -const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<'span'>) => ( - <span - role="presentation" - aria-hidden="true" - className={cn('flex h-9 w-9 items-center justify-center', className)} - {...props}> - <MoreHorizontal className="h-4 w-4" /> - <span className="sr-only">More</span> - </span> -); -BreadcrumbEllipsis.displayName = 'BreadcrumbElipssis'; - -export { - Breadcrumb, - BreadcrumbList, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbPage, - BreadcrumbSeparator, - BreadcrumbEllipsis, -}; diff --git a/ui/button.tsx b/ui/button.tsx deleted file mode 100644 index a7db540..0000000 --- a/ui/button.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import * as React from 'react'; -import { Slot } from '@radix-ui/react-slot'; -import { cva, type VariantProps } from 'class-variance-authority'; - -import { cn } from '@/utils/shadcn'; - -const buttonVariants = cva( - 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', - { - variants: { - variant: { - default: 'bg-primary text-primary-foreground hover:bg-primary/90', - destructive: - 'bg-destructive text-destructive-foreground hover:bg-destructive/90', - outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', - secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', - ghost: 'hover:bg-accent hover:text-accent-foreground', - link: 'text-primary underline-offset-4 hover:underline', - }, - size: { - default: 'h-10 px-4 py-2', - sm: 'h-9 rounded-md px-3', - lg: 'h-11 rounded-md px-8', - icon: 'h-10 w-10', - }, - }, - defaultVariants: { - variant: 'default', - size: 'default', - }, - } -); - -export interface ButtonProps - extends React.ButtonHTMLAttributes<HTMLButtonElement>, - VariantProps<typeof buttonVariants> { - asChild?: boolean; -} - -const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( - ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : 'button'; - return ( - <Comp - className={cn(buttonVariants({ variant, size, className }))} - ref={ref} - {...props} - /> - ); - } -); -Button.displayName = 'Button'; - -export { Button, buttonVariants }; diff --git a/ui/calendar.tsx b/ui/calendar.tsx deleted file mode 100644 index 3bfe0a9..0000000 --- a/ui/calendar.tsx +++ /dev/null @@ -1,70 +0,0 @@ -'use client'; - -import * as React from 'react'; -import { ChevronLeft, ChevronRight } from 'lucide-react'; -import { DayPicker } from 'react-day-picker'; - -import { cn } from '@/utils/shadcn'; -import { buttonVariants } from '@/ui/button'; - -export type CalendarProps = React.ComponentProps<typeof DayPicker>; - -function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) { - return ( - <DayPicker - showOutsideDays={showOutsideDays} - className={cn('p-3', className)} - classNames={{ - months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0', - month: 'space-y-4', - caption: 'flex justify-center pt-1 relative items-center', - caption_label: 'text-sm font-medium', - nav: 'space-x-1 flex items-center', - nav_button: cn( - buttonVariants({ variant: 'outline' }), - 'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100' - ), - nav_button_previous: 'absolute left-1', - nav_button_next: 'absolute right-1', - table: 'w-full border-collapse space-y-1', - head_row: 'flex', - head_cell: 'text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]', - row: 'flex w-full mt-2', - cell: 'h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20', - day: cn( - buttonVariants({ variant: 'ghost' }), - 'h-9 w-9 p-0 font-normal aria-selected:opacity-100' - ), - day_range_end: 'day-range-end', - day_selected: - 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground', - day_today: 'bg-accent text-accent-foreground', - day_outside: - 'day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground', - day_disabled: 'text-muted-foreground opacity-50', - day_range_middle: - 'aria-selected:bg-accent aria-selected:text-accent-foreground', - day_hidden: 'invisible', - ...classNames, - }} - components={{ - IconLeft: ({ className, ...props }) => ( - <ChevronLeft - className={cn('h-4 w-4', className)} - {...props} - /> - ), - IconRight: ({ className, ...props }) => ( - <ChevronRight - className={cn('h-4 w-4', className)} - {...props} - /> - ), - }} - {...props} - /> - ); -} -Calendar.displayName = 'Calendar'; - -export { Calendar }; diff --git a/ui/card.tsx b/ui/card.tsx deleted file mode 100644 index b979567..0000000 --- a/ui/card.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import * as React from 'react'; - -import { cn } from '@/utils/shadcn'; - -const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( - ({ className, ...props }, ref) => ( - <div - ref={ref} - className={cn( - 'rounded-xl border bg-card text-card-foreground shadow', - className - )} - {...props} - /> - ) -); -Card.displayName = 'Card'; - -const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( - ({ className, ...props }, ref) => ( - <div - ref={ref} - className={cn('flex flex-col space-y-1.5 p-6', className)} - {...props} - /> - ) -); -CardHeader.displayName = 'CardHeader'; - -const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( - ({ className, ...props }, ref) => ( - <div - ref={ref} - className={cn('font-semibold leading-none tracking-tight', className)} - {...props} - /> - ) -); -CardTitle.displayName = 'CardTitle'; - -const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( - ({ className, ...props }, ref) => ( - <div - ref={ref} - className={cn('text-sm text-muted-foreground', className)} - {...props} - /> - ) -); -CardDescription.displayName = 'CardDescription'; - -const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( - ({ className, ...props }, ref) => ( - <div ref={ref} className={cn('p-6 pt-0', className)} {...props} /> - ) -); -CardContent.displayName = 'CardContent'; - -const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( - ({ className, ...props }, ref) => ( - <div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} /> - ) -); -CardFooter.displayName = 'CardFooter'; - -export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; diff --git a/ui/checkbox.tsx b/ui/checkbox.tsx deleted file mode 100644 index f79f76e..0000000 --- a/ui/checkbox.tsx +++ /dev/null @@ -1,28 +0,0 @@ -'use client'; - -import * as React from 'react'; -import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; -import { Check } from 'lucide-react'; - -import { cn } from '@/utils/shadcn'; - -const Checkbox = React.forwardRef< - React.ElementRef<typeof CheckboxPrimitive.Root>, - React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> ->(({ className, ...props }, ref) => ( - <CheckboxPrimitive.Root - ref={ref} - className={cn( - 'peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground', - className - )} - {...props}> - <CheckboxPrimitive.Indicator - className={cn('flex items-center justify-center text-current')}> - <Check className="h-4 w-4" /> - </CheckboxPrimitive.Indicator> - </CheckboxPrimitive.Root> -)); -Checkbox.displayName = CheckboxPrimitive.Root.displayName; - -export { Checkbox }; diff --git a/ui/collapsible.tsx b/ui/collapsible.tsx deleted file mode 100644 index 9fa4894..0000000 --- a/ui/collapsible.tsx +++ /dev/null @@ -1,11 +0,0 @@ -"use client" - -import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" - -const Collapsible = CollapsiblePrimitive.Root - -const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger - -const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent - -export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/ui/command.tsx b/ui/command.tsx deleted file mode 100644 index 9c4e863..0000000 --- a/ui/command.tsx +++ /dev/null @@ -1,153 +0,0 @@ -"use client" - -import * as React from "react" -import { type DialogProps } from "@radix-ui/react-dialog" -import { Command as CommandPrimitive } from "cmdk" -import { Search } from "lucide-react" - -import { cn } from "@/utils/shadcn" -import { Dialog, DialogContent } from "@/ui/dialog" - -const Command = React.forwardRef< - React.ElementRef<typeof CommandPrimitive>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive> ->(({ className, ...props }, ref) => ( - <CommandPrimitive - ref={ref} - className={cn( - "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground", - className - )} - {...props} - /> -)) -Command.displayName = CommandPrimitive.displayName - -const CommandDialog = ({ children, ...props }: DialogProps) => { - return ( - <Dialog {...props}> - <DialogContent className="overflow-hidden p-0 shadow-lg"> - <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"> - {children} - </Command> - </DialogContent> - </Dialog> - ) -} - -const CommandInput = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Input>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> ->(({ className, ...props }, ref) => ( - <div className="flex items-center border-b px-3" cmdk-input-wrapper=""> - <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" /> - <CommandPrimitive.Input - ref={ref} - className={cn( - "flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50", - className - )} - {...props} - /> - </div> -)) - -CommandInput.displayName = CommandPrimitive.Input.displayName - -const CommandList = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.List>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.List> ->(({ className, ...props }, ref) => ( - <CommandPrimitive.List - ref={ref} - className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)} - {...props} - /> -)) - -CommandList.displayName = CommandPrimitive.List.displayName - -const CommandEmpty = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Empty>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty> ->((props, ref) => ( - <CommandPrimitive.Empty - ref={ref} - className="py-6 text-center text-sm" - {...props} - /> -)) - -CommandEmpty.displayName = CommandPrimitive.Empty.displayName - -const CommandGroup = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Group>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group> ->(({ className, ...props }, ref) => ( - <CommandPrimitive.Group - ref={ref} - className={cn( - "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground", - className - )} - {...props} - /> -)) - -CommandGroup.displayName = CommandPrimitive.Group.displayName - -const CommandSeparator = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Separator>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator> ->(({ className, ...props }, ref) => ( - <CommandPrimitive.Separator - ref={ref} - className={cn("-mx-1 h-px bg-border", className)} - {...props} - /> -)) -CommandSeparator.displayName = CommandPrimitive.Separator.displayName - -const CommandItem = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Item>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item> ->(({ className, ...props }, ref) => ( - <CommandPrimitive.Item - ref={ref} - className={cn( - "relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", - className - )} - {...props} - /> -)) - -CommandItem.displayName = CommandPrimitive.Item.displayName - -const CommandShortcut = ({ - className, - ...props -}: React.HTMLAttributes<HTMLSpanElement>) => { - return ( - <span - className={cn( - "ml-auto text-xs tracking-widest text-muted-foreground", - className - )} - {...props} - /> - ) -} -CommandShortcut.displayName = "CommandShortcut" - -export { - Command, - CommandDialog, - CommandInput, - CommandList, - CommandEmpty, - CommandGroup, - CommandItem, - CommandShortcut, - CommandSeparator, -} diff --git a/ui/context-menu.tsx b/ui/context-menu.tsx deleted file mode 100644 index d2e720b..0000000 --- a/ui/context-menu.tsx +++ /dev/null @@ -1,193 +0,0 @@ -'use client'; - -import * as React from 'react'; -import * as ContextMenuPrimitive from '@radix-ui/react-context-menu'; -import { Check, ChevronRight, Circle } from 'lucide-react'; - -import { cn } from '@/utils/shadcn'; - -const ContextMenu = ContextMenuPrimitive.Root; - -const ContextMenuTrigger = ContextMenuPrimitive.Trigger; - -const ContextMenuGroup = ContextMenuPrimitive.Group; - -const ContextMenuPortal = ContextMenuPrimitive.Portal; - -const ContextMenuSub = ContextMenuPrimitive.Sub; - -const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup; - -const ContextMenuSubTrigger = React.forwardRef< - React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>, - React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & { - inset?: boolean; - } ->(({ className, inset, children, ...props }, ref) => ( - <ContextMenuPrimitive.SubTrigger - ref={ref} - className={cn( - 'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground', - inset && 'pl-8', - className - )} - {...props}> - {children} - <ChevronRight className="ml-auto h-4 w-4" /> - </ContextMenuPrimitive.SubTrigger> -)); -ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName; - -const ContextMenuSubContent = React.forwardRef< - React.ElementRef<typeof ContextMenuPrimitive.SubContent>, - React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent> ->(({ className, ...props }, ref) => ( - <ContextMenuPrimitive.SubContent - ref={ref} - className={cn( - 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', - className - )} - {...props} - /> -)); -ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName; - -const ContextMenuContent = React.forwardRef< - React.ElementRef<typeof ContextMenuPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content> ->(({ className, ...props }, ref) => ( - <ContextMenuPrimitive.Portal> - <ContextMenuPrimitive.Content - ref={ref} - className={cn( - 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', - className - )} - {...props} - /> - </ContextMenuPrimitive.Portal> -)); -ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName; - -const ContextMenuItem = React.forwardRef< - React.ElementRef<typeof ContextMenuPrimitive.Item>, - React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & { - inset?: boolean; - } ->(({ className, inset, ...props }, ref) => ( - <ContextMenuPrimitive.Item - ref={ref} - className={cn( - 'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', - inset && 'pl-8', - className - )} - {...props} - /> -)); -ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName; - -const ContextMenuCheckboxItem = React.forwardRef< - React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>, - React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem> ->(({ className, children, checked, ...props }, ref) => ( - <ContextMenuPrimitive.CheckboxItem - ref={ref} - className={cn( - 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', - className - )} - checked={checked} - {...props}> - <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> - <ContextMenuPrimitive.ItemIndicator> - <Check className="h-4 w-4" /> - </ContextMenuPrimitive.ItemIndicator> - </span> - {children} - </ContextMenuPrimitive.CheckboxItem> -)); -ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName; - -const ContextMenuRadioItem = React.forwardRef< - React.ElementRef<typeof ContextMenuPrimitive.RadioItem>, - React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem> ->(({ className, children, ...props }, ref) => ( - <ContextMenuPrimitive.RadioItem - ref={ref} - className={cn( - 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', - className - )} - {...props}> - <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> - <ContextMenuPrimitive.ItemIndicator> - <Circle className="h-2 w-2 fill-current" /> - </ContextMenuPrimitive.ItemIndicator> - </span> - {children} - </ContextMenuPrimitive.RadioItem> -)); -ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName; - -const ContextMenuLabel = React.forwardRef< - React.ElementRef<typeof ContextMenuPrimitive.Label>, - React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & { - inset?: boolean; - } ->(({ className, inset, ...props }, ref) => ( - <ContextMenuPrimitive.Label - ref={ref} - className={cn( - 'px-2 py-1.5 text-sm font-semibold text-foreground', - inset && 'pl-8', - className - )} - {...props} - /> -)); -ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName; - -const ContextMenuSeparator = React.forwardRef< - React.ElementRef<typeof ContextMenuPrimitive.Separator>, - React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator> ->(({ className, ...props }, ref) => ( - <ContextMenuPrimitive.Separator - ref={ref} - className={cn('-mx-1 my-1 h-px bg-border', className)} - {...props} - /> -)); -ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName; - -const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => { - return ( - <span - className={cn( - 'ml-auto text-xs tracking-widest text-muted-foreground', - className - )} - {...props} - /> - ); -}; -ContextMenuShortcut.displayName = 'ContextMenuShortcut'; - -export { - ContextMenu, - ContextMenuTrigger, - ContextMenuContent, - ContextMenuItem, - ContextMenuCheckboxItem, - ContextMenuRadioItem, - ContextMenuLabel, - ContextMenuSeparator, - ContextMenuShortcut, - ContextMenuGroup, - ContextMenuPortal, - ContextMenuSub, - ContextMenuSubContent, - ContextMenuSubTrigger, - ContextMenuRadioGroup, -}; diff --git a/ui/dialog.tsx b/ui/dialog.tsx deleted file mode 100644 index 634af84..0000000 --- a/ui/dialog.tsx +++ /dev/null @@ -1,122 +0,0 @@ -"use client" - -import * as React from "react" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { X } from "lucide-react" - -import { cn } from "@/utils/shadcn" - -const Dialog = DialogPrimitive.Root - -const DialogTrigger = DialogPrimitive.Trigger - -const DialogPortal = DialogPrimitive.Portal - -const DialogClose = DialogPrimitive.Close - -const DialogOverlay = React.forwardRef< - React.ElementRef<typeof DialogPrimitive.Overlay>, - React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> ->(({ className, ...props }, ref) => ( - <DialogPrimitive.Overlay - ref={ref} - className={cn( - "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", - className - )} - {...props} - /> -)) -DialogOverlay.displayName = DialogPrimitive.Overlay.displayName - -const DialogContent = React.forwardRef< - React.ElementRef<typeof DialogPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> ->(({ className, children, ...props }, ref) => ( - <DialogPortal> - <DialogOverlay /> - <DialogPrimitive.Content - ref={ref} - className={cn( - "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", - className - )} - {...props} - > - {children} - <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> - <X className="h-4 w-4" /> - <span className="sr-only">Close</span> - </DialogPrimitive.Close> - </DialogPrimitive.Content> - </DialogPortal> -)) -DialogContent.displayName = DialogPrimitive.Content.displayName - -const DialogHeader = ({ - className, - ...props -}: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn( - "flex flex-col space-y-1.5 text-center sm:text-left", - className - )} - {...props} - /> -) -DialogHeader.displayName = "DialogHeader" - -const DialogFooter = ({ - className, - ...props -}: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn( - "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", - className - )} - {...props} - /> -) -DialogFooter.displayName = "DialogFooter" - -const DialogTitle = React.forwardRef< - React.ElementRef<typeof DialogPrimitive.Title>, - React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> ->(({ className, ...props }, ref) => ( - <DialogPrimitive.Title - ref={ref} - className={cn( - "text-lg font-semibold leading-none tracking-tight", - className - )} - {...props} - /> -)) -DialogTitle.displayName = DialogPrimitive.Title.displayName - -const DialogDescription = React.forwardRef< - React.ElementRef<typeof DialogPrimitive.Description>, - React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description> ->(({ className, ...props }, ref) => ( - <DialogPrimitive.Description - ref={ref} - className={cn("text-sm text-muted-foreground", className)} - {...props} - /> -)) -DialogDescription.displayName = DialogPrimitive.Description.displayName - -export { - Dialog, - DialogPortal, - DialogOverlay, - DialogClose, - DialogTrigger, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription, -} diff --git a/ui/drawer.tsx b/ui/drawer.tsx deleted file mode 100644 index 1f59d0f..0000000 --- a/ui/drawer.tsx +++ /dev/null @@ -1,99 +0,0 @@ -'use client'; - -import * as React from 'react'; -import { Drawer as DrawerPrimitive } from 'vaul'; - -import { cn } from '@/utils/shadcn'; - -const Drawer = ({ - shouldScaleBackground = true, - ...props -}: React.ComponentProps<typeof DrawerPrimitive.Root>) => ( - <DrawerPrimitive.Root shouldScaleBackground={shouldScaleBackground} {...props} /> -); -Drawer.displayName = 'Drawer'; - -const DrawerTrigger = DrawerPrimitive.Trigger; - -const DrawerPortal = DrawerPrimitive.Portal; - -const DrawerClose = DrawerPrimitive.Close; - -const DrawerOverlay = React.forwardRef< - React.ElementRef<typeof DrawerPrimitive.Overlay>, - React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay> ->(({ className, ...props }, ref) => ( - <DrawerPrimitive.Overlay - ref={ref} - className={cn('fixed inset-0 z-50 bg-black/80', className)} - {...props} - /> -)); -DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName; - -const DrawerContent = React.forwardRef< - React.ElementRef<typeof DrawerPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content> ->(({ className, children, ...props }, ref) => ( - <DrawerPortal> - <DrawerOverlay /> - <DrawerPrimitive.Content - ref={ref} - className={cn( - 'fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background', - className - )} - {...props}> - <div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" /> - {children} - </DrawerPrimitive.Content> - </DrawerPortal> -)); -DrawerContent.displayName = 'DrawerContent'; - -const DrawerHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( - <div className={cn('grid gap-1.5 p-4 text-center sm:text-left', className)} {...props} /> -); -DrawerHeader.displayName = 'DrawerHeader'; - -const DrawerFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( - <div className={cn('mt-auto flex flex-col gap-2 p-4', className)} {...props} /> -); -DrawerFooter.displayName = 'DrawerFooter'; - -const DrawerTitle = React.forwardRef< - React.ElementRef<typeof DrawerPrimitive.Title>, - React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title> ->(({ className, ...props }, ref) => ( - <DrawerPrimitive.Title - ref={ref} - className={cn('text-lg font-semibold leading-none tracking-tight', className)} - {...props} - /> -)); -DrawerTitle.displayName = DrawerPrimitive.Title.displayName; - -const DrawerDescription = React.forwardRef< - React.ElementRef<typeof DrawerPrimitive.Description>, - React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description> ->(({ className, ...props }, ref) => ( - <DrawerPrimitive.Description - ref={ref} - className={cn('text-sm text-muted-foreground', className)} - {...props} - /> -)); -DrawerDescription.displayName = DrawerPrimitive.Description.displayName; - -export { - Drawer, - DrawerPortal, - DrawerOverlay, - DrawerTrigger, - DrawerClose, - DrawerContent, - DrawerHeader, - DrawerFooter, - DrawerTitle, - DrawerDescription, -}; diff --git a/ui/dropdown-menu.tsx b/ui/dropdown-menu.tsx deleted file mode 100644 index 14f18ef..0000000 --- a/ui/dropdown-menu.tsx +++ /dev/null @@ -1,187 +0,0 @@ -'use client'; - -import * as React from 'react'; -import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; -import { Check, ChevronRight, Circle } from 'lucide-react'; - -import { cn } from '@/utils/shadcn'; - -const DropdownMenu = DropdownMenuPrimitive.Root; - -const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; - -const DropdownMenuGroup = DropdownMenuPrimitive.Group; - -const DropdownMenuPortal = DropdownMenuPrimitive.Portal; - -const DropdownMenuSub = DropdownMenuPrimitive.Sub; - -const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; - -const DropdownMenuSubTrigger = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { - inset?: boolean; - } ->(({ className, inset, children, ...props }, ref) => ( - <DropdownMenuPrimitive.SubTrigger - ref={ref} - className={cn( - 'flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', - inset && 'pl-8', - className - )} - {...props}> - {children} - <ChevronRight className="ml-auto" /> - </DropdownMenuPrimitive.SubTrigger> -)); -DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName; - -const DropdownMenuSubContent = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.SubContent>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent> ->(({ className, ...props }, ref) => ( - <DropdownMenuPrimitive.SubContent - ref={ref} - className={cn( - 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', - className - )} - {...props} - /> -)); -DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName; - -const DropdownMenuContent = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> ->(({ className, sideOffset = 4, ...props }, ref) => ( - <DropdownMenuPrimitive.Portal> - <DropdownMenuPrimitive.Content - ref={ref} - sideOffset={sideOffset} - className={cn( - 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', - className - )} - {...props} - /> - </DropdownMenuPrimitive.Portal> -)); -DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; - -const DropdownMenuItem = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.Item>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & { - inset?: boolean; - } ->(({ className, inset, ...props }, ref) => ( - <DropdownMenuPrimitive.Item - ref={ref} - className={cn( - 'relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', - inset && 'pl-8', - className - )} - {...props} - /> -)); -DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; - -const DropdownMenuCheckboxItem = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem> ->(({ className, children, checked, ...props }, ref) => ( - <DropdownMenuPrimitive.CheckboxItem - ref={ref} - className={cn( - 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', - className - )} - checked={checked} - {...props}> - <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> - <DropdownMenuPrimitive.ItemIndicator> - <Check className="h-4 w-4" /> - </DropdownMenuPrimitive.ItemIndicator> - </span> - {children} - </DropdownMenuPrimitive.CheckboxItem> -)); -DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName; - -const DropdownMenuRadioItem = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem> ->(({ className, children, ...props }, ref) => ( - <DropdownMenuPrimitive.RadioItem - ref={ref} - className={cn( - 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', - className - )} - {...props}> - <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> - <DropdownMenuPrimitive.ItemIndicator> - <Circle className="h-2 w-2 fill-current" /> - </DropdownMenuPrimitive.ItemIndicator> - </span> - {children} - </DropdownMenuPrimitive.RadioItem> -)); -DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; - -const DropdownMenuLabel = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.Label>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { - inset?: boolean; - } ->(({ className, inset, ...props }, ref) => ( - <DropdownMenuPrimitive.Label - ref={ref} - className={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)} - {...props} - /> -)); -DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; - -const DropdownMenuSeparator = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.Separator>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator> ->(({ className, ...props }, ref) => ( - <DropdownMenuPrimitive.Separator - ref={ref} - className={cn('-mx-1 my-1 h-px bg-muted', className)} - {...props} - /> -)); -DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; - -const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => { - return ( - <span - className={cn('ml-auto text-xs tracking-widest opacity-60', className)} - {...props} - /> - ); -}; -DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'; - -export { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuCheckboxItem, - DropdownMenuRadioItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuGroup, - DropdownMenuPortal, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, - DropdownMenuRadioGroup, -}; diff --git a/ui/input.tsx b/ui/input.tsx deleted file mode 100644 index 10920cf..0000000 --- a/ui/input.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as React from 'react'; - -import { cn } from '@/utils/shadcn'; - -const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>( - ({ className, type, ...props }, ref) => { - return ( - <input - type={type} - className={cn( - 'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm', - className - )} - ref={ref} - {...props} - /> - ); - } -); -Input.displayName = 'Input'; - -export { Input }; diff --git a/ui/label.tsx b/ui/label.tsx deleted file mode 100644 index 17b37b7..0000000 --- a/ui/label.tsx +++ /dev/null @@ -1,22 +0,0 @@ -'use client'; - -import * as React from 'react'; -import * as LabelPrimitive from '@radix-ui/react-label'; -import { cva, type VariantProps } from 'class-variance-authority'; - -import { cn } from '@/utils/shadcn'; - -const labelVariants = cva( - 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70' -); - -const Label = React.forwardRef< - React.ElementRef<typeof LabelPrimitive.Root>, - React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & - VariantProps<typeof labelVariants> ->(({ className, ...props }, ref) => ( - <LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} /> -)); -Label.displayName = LabelPrimitive.Root.displayName; - -export { Label }; diff --git a/ui/popover.tsx b/ui/popover.tsx deleted file mode 100644 index 4a6deaa..0000000 --- a/ui/popover.tsx +++ /dev/null @@ -1,31 +0,0 @@ -'use client'; - -import * as React from 'react'; -import * as PopoverPrimitive from '@radix-ui/react-popover'; - -import { cn } from '@/utils/shadcn'; - -const Popover = PopoverPrimitive.Root; - -const PopoverTrigger = PopoverPrimitive.Trigger; - -const PopoverContent = React.forwardRef< - React.ElementRef<typeof PopoverPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> ->(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( - <PopoverPrimitive.Portal> - <PopoverPrimitive.Content - ref={ref} - align={align} - sideOffset={sideOffset} - className={cn( - 'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', - className - )} - {...props} - /> - </PopoverPrimitive.Portal> -)); -PopoverContent.displayName = PopoverPrimitive.Content.displayName; - -export { Popover, PopoverTrigger, PopoverContent }; diff --git a/ui/radio-group.tsx b/ui/radio-group.tsx deleted file mode 100644 index 49c39a4..0000000 --- a/ui/radio-group.tsx +++ /dev/null @@ -1,44 +0,0 @@ -"use client" - -import * as React from "react" -import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" -import { Circle } from "lucide-react" - -import { cn } from "@/utils/shadcn" - -const RadioGroup = React.forwardRef< - React.ElementRef<typeof RadioGroupPrimitive.Root>, - React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root> ->(({ className, ...props }, ref) => { - return ( - <RadioGroupPrimitive.Root - className={cn("grid gap-2", className)} - {...props} - ref={ref} - /> - ) -}) -RadioGroup.displayName = RadioGroupPrimitive.Root.displayName - -const RadioGroupItem = React.forwardRef< - React.ElementRef<typeof RadioGroupPrimitive.Item>, - React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item> ->(({ className, ...props }, ref) => { - return ( - <RadioGroupPrimitive.Item - ref={ref} - className={cn( - "aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", - className - )} - {...props} - > - <RadioGroupPrimitive.Indicator className="flex items-center justify-center"> - <Circle className="h-2.5 w-2.5 fill-current text-current" /> - </RadioGroupPrimitive.Indicator> - </RadioGroupPrimitive.Item> - ) -}) -RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName - -export { RadioGroup, RadioGroupItem } diff --git a/ui/scroll-area.tsx b/ui/scroll-area.tsx deleted file mode 100644 index 755d2d4..0000000 --- a/ui/scroll-area.tsx +++ /dev/null @@ -1,48 +0,0 @@ -"use client" - -import * as React from "react" -import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" - -import { cn } from "@/utils/shadcn" - -const ScrollArea = React.forwardRef< - React.ElementRef<typeof ScrollAreaPrimitive.Root>, - React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> ->(({ className, children, ...props }, ref) => ( - <ScrollAreaPrimitive.Root - ref={ref} - className={cn("relative overflow-hidden", className)} - {...props} - > - <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]"> - {children} - </ScrollAreaPrimitive.Viewport> - <ScrollBar /> - <ScrollAreaPrimitive.Corner /> - </ScrollAreaPrimitive.Root> -)) -ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName - -const ScrollBar = React.forwardRef< - React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>, - React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar> ->(({ className, orientation = "vertical", ...props }, ref) => ( - <ScrollAreaPrimitive.ScrollAreaScrollbar - ref={ref} - orientation={orientation} - className={cn( - "flex touch-none select-none transition-colors", - orientation === "vertical" && - "h-full w-2.5 border-l border-l-transparent p-[1px]", - orientation === "horizontal" && - "h-2.5 flex-col border-t border-t-transparent p-[1px]", - className - )} - {...props} - > - <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" /> - </ScrollAreaPrimitive.ScrollAreaScrollbar> -)) -ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName - -export { ScrollArea, ScrollBar } diff --git a/ui/select.tsx b/ui/select.tsx deleted file mode 100644 index 4eea725..0000000 --- a/ui/select.tsx +++ /dev/null @@ -1,144 +0,0 @@ -'use client'; - -import * as React from 'react'; -import * as SelectPrimitive from '@radix-ui/react-select'; -import { Check, ChevronsUpDown, ChevronUp } from 'lucide-react'; - -import { cn } from '@/utils/shadcn'; - -const Select = SelectPrimitive.Root; - -const SelectGroup = SelectPrimitive.Group; - -const SelectValue = SelectPrimitive.Value; - -const SelectTrigger = React.forwardRef< - React.ElementRef<typeof SelectPrimitive.Trigger>, - React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> ->(({ className, children, ...props }, ref) => ( - <SelectPrimitive.Trigger - ref={ref} - className={cn( - 'relative flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1', - className - )} - {...props}> - {children} - <SelectPrimitive.Icon asChild> - <ChevronsUpDown className="absolute right-0 inset-y-auto h-4 w-4 opacity-50" /> - </SelectPrimitive.Icon> - </SelectPrimitive.Trigger> -)); -SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; - -const SelectScrollUpButton = React.forwardRef< - React.ElementRef<typeof SelectPrimitive.ScrollUpButton>, - React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton> ->(({ className, ...props }, ref) => ( - <SelectPrimitive.ScrollUpButton - ref={ref} - className={cn('flex cursor-default items-center justify-center py-1', className)} - {...props}> - <ChevronUp className="h-4 w-4" /> - </SelectPrimitive.ScrollUpButton> -)); -SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; - -const SelectScrollDownButton = React.forwardRef< - React.ElementRef<typeof SelectPrimitive.ScrollDownButton>, - React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton> ->(({ className, ...props }, ref) => ( - <SelectPrimitive.ScrollDownButton - ref={ref} - className={cn('flex cursor-default items-center justify-center py-1', className)} - {...props}> - <ChevronsUpDown className="h-4 w-4" /> - </SelectPrimitive.ScrollDownButton> -)); -SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName; - -const SelectContent = React.forwardRef< - React.ElementRef<typeof SelectPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> ->(({ className, children, position = 'popper', ...props }, ref) => ( - <SelectPrimitive.Portal> - <SelectPrimitive.Content - ref={ref} - className={cn( - 'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', - position === 'popper' && - 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1', - className - )} - position={position} - {...props}> - <SelectScrollUpButton /> - <SelectPrimitive.Viewport - className={cn( - position === 'popper' && - 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]' - )}> - {children} - </SelectPrimitive.Viewport> - <SelectScrollDownButton /> - </SelectPrimitive.Content> - </SelectPrimitive.Portal> -)); -SelectContent.displayName = SelectPrimitive.Content.displayName; - -const SelectLabel = React.forwardRef< - React.ElementRef<typeof SelectPrimitive.Label>, - React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label> ->(({ className, ...props }, ref) => ( - <SelectPrimitive.Label - ref={ref} - className={cn('px-2 py-1.5 text-sm font-semibold', className)} - {...props} - /> -)); -SelectLabel.displayName = SelectPrimitive.Label.displayName; - -const SelectItem = React.forwardRef< - React.ElementRef<typeof SelectPrimitive.Item>, - React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> ->(({ className, children, ...props }, ref) => ( - <SelectPrimitive.Item - ref={ref} - className={cn( - 'relative flex flex-row justify-between w-full cursor-default select-none items-center rounded-sm py-1.5 px-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', - className - )} - {...props}> - <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> - - <SelectPrimitive.ItemIndicator> - <Check className="size-4 text-green-300" /> - </SelectPrimitive.ItemIndicator> - </SelectPrimitive.Item> -)); -SelectItem.displayName = SelectPrimitive.Item.displayName; - -const SelectSeparator = React.forwardRef< - React.ElementRef<typeof SelectPrimitive.Separator>, - React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> ->(({ className, ...props }, ref) => ( - <SelectPrimitive.Separator - ref={ref} - className={cn('-mx-1 my-1 h-px bg-muted', className)} - {...props} - /> -)); -SelectSeparator.displayName = SelectPrimitive.Separator.displayName; - -export { - Select, - SelectGroup, - SelectValue, - SelectTrigger, - SelectContent, - SelectLabel, - SelectItem, - SelectSeparator, - SelectScrollUpButton, - SelectScrollDownButton, -}; diff --git a/ui/separator.tsx b/ui/separator.tsx deleted file mode 100644 index 0bc2d24..0000000 --- a/ui/separator.tsx +++ /dev/null @@ -1,26 +0,0 @@ -'use client'; - -import * as React from 'react'; -import * as SeparatorPrimitive from '@radix-ui/react-separator'; - -import { cn } from '@/utils/shadcn'; - -const Separator = React.forwardRef< - React.ElementRef<typeof SeparatorPrimitive.Root>, - React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> ->(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => ( - <SeparatorPrimitive.Root - ref={ref} - decorative={decorative} - orientation={orientation} - className={cn( - 'shrink-0 bg-border', - orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]', - className - )} - {...props} - /> -)); -Separator.displayName = SeparatorPrimitive.Root.displayName; - -export { Separator }; diff --git a/ui/sheet.tsx b/ui/sheet.tsx deleted file mode 100644 index 4ba80fe..0000000 --- a/ui/sheet.tsx +++ /dev/null @@ -1,124 +0,0 @@ -'use client'; - -import * as React from 'react'; -import * as SheetPrimitive from '@radix-ui/react-dialog'; -import { cva, type VariantProps } from 'class-variance-authority'; -import { X } from 'lucide-react'; - -import { cn } from '@/utils/shadcn'; - -const Sheet = SheetPrimitive.Root; - -const SheetTrigger = SheetPrimitive.Trigger; - -const SheetClose = SheetPrimitive.Close; - -const SheetPortal = SheetPrimitive.Portal; - -const SheetOverlay = React.forwardRef< - React.ElementRef<typeof SheetPrimitive.Overlay>, - React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay> ->(({ className, ...props }, ref) => ( - <SheetPrimitive.Overlay - className={cn( - 'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0', - className - )} - {...props} - ref={ref} - /> -)); -SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; - -const sheetVariants = cva( - 'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500', - { - variants: { - side: { - top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top', - bottom: 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom', - left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm', - right: 'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm', - }, - }, - defaultVariants: { - side: 'right', - }, - } -); - -interface SheetContentProps - extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>, - VariantProps<typeof sheetVariants> {} - -const SheetContent = React.forwardRef< - React.ElementRef<typeof SheetPrimitive.Content>, - SheetContentProps & { container?: Element | DocumentFragment } ->(({ side = 'right', className, children, container, ...props }, ref) => ( - <SheetPortal container={container}> - <SheetOverlay /> - <SheetPrimitive.Content - ref={ref} - className={cn(sheetVariants({ side }), className)} - {...props}> - {children} - </SheetPrimitive.Content> - </SheetPortal> -)); -SheetContent.displayName = SheetPrimitive.Content.displayName; - -const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn('flex flex-col space-y-2 text-center sm:text-left', className)} - {...props} - /> -); -SheetHeader.displayName = 'SheetHeader'; - -const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn( - 'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', - className - )} - {...props} - /> -); -SheetFooter.displayName = 'SheetFooter'; - -const SheetTitle = React.forwardRef< - React.ElementRef<typeof SheetPrimitive.Title>, - React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title> ->(({ className, ...props }, ref) => ( - <SheetPrimitive.Title - ref={ref} - className={cn('text-lg font-semibold text-foreground', className)} - {...props} - /> -)); -SheetTitle.displayName = SheetPrimitive.Title.displayName; - -const SheetDescription = React.forwardRef< - React.ElementRef<typeof SheetPrimitive.Description>, - React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description> ->(({ className, ...props }, ref) => ( - <SheetPrimitive.Description - ref={ref} - className={cn('text-sm text-muted-foreground', className)} - {...props} - /> -)); -SheetDescription.displayName = SheetPrimitive.Description.displayName; - -export { - Sheet, - SheetPortal, - SheetOverlay, - SheetTrigger, - SheetClose, - SheetContent, - SheetHeader, - SheetFooter, - SheetTitle, - SheetDescription, -}; diff --git a/ui/sidebar.tsx b/ui/sidebar.tsx deleted file mode 100644 index bdfdecf..0000000 --- a/ui/sidebar.tsx +++ /dev/null @@ -1,821 +0,0 @@ -'use client'; - -import * as React from 'react'; -import { Slot } from '@radix-ui/react-slot'; -import { VariantProps, cva } from 'class-variance-authority'; - -import { useIsMobile } from '@/hooks/use-mobile'; -import { cn } from '@/utils/shadcn'; -import { Button } from '@/ui/button'; -import { Input } from '@/ui/input'; -import { Separator } from '@/ui/separator'; -import { Sheet, SheetContent, SheetTrigger } from '@/ui/sheet'; -import { Skeleton } from '@/ui/skeleton'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/ui/tooltip'; -import { useIsTablet } from '@/hooks/use-tablet'; -import { Drawer, DrawerContent, DrawerTrigger } from './drawer'; -import { TbLayoutSidebarRightCollapseFilled } from 'react-icons/tb'; -import { TbLayoutSidebarRightExpandFilled } from 'react-icons/tb'; -import { LuPanelBottomOpen } from 'react-icons/lu'; - -const SIDEBAR_COOKIE_NAME = 'sidebar:state'; -const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; -const SIDEBAR_WIDTH = '16rem'; -const SIDEBAR_WIDTH_MOBILE = '18rem'; -const SIDEBAR_WIDTH_ICON = '5rem'; -const SIDEBAR_KEYBOARD_SHORTCUT = 'b'; - -type SidebarContext = { - state: 'expanded' | 'collapsed'; - open: boolean; - setOpen: (open: boolean) => void; - openMobile: boolean; - setOpenMobile: (open: boolean) => void; - isMobile: boolean; - openTablet: boolean; - setOpenTablet: (open: boolean) => void; - isTablet: boolean; - toggleSidebar: () => void; -}; - -const SidebarContext = React.createContext<SidebarContext | null>(null); - -function useSidebar() { - const context = React.useContext(SidebarContext); - if (!context) { - throw new Error('useSidebar must be used within a SidebarProvider.'); - } - - return context; -} - -const SidebarProvider = React.forwardRef< - HTMLDivElement, - React.ComponentProps<'div'> & { - defaultOpen?: boolean; - open?: boolean; - onOpenChange?: (open: boolean) => void; - } ->( - ( - { - defaultOpen = true, - open: openProp, - onOpenChange: setOpenProp, - className, - style, - children, - ...props - }, - ref - ) => { - const isMobile = useIsMobile(); - const isTablet = useIsTablet(); - const [openMobile, setOpenMobile] = React.useState(false); - const [openTablet, setOpenTablet] = React.useState(false); - - // This is the internal state of the sidebar. - // We use openProp and setOpenProp for control from outside the component. - const [_open, _setOpen] = React.useState(defaultOpen); - const open = openProp ?? _open; - const setOpen = React.useCallback( - (value: boolean | ((value: boolean) => boolean)) => { - const openState = typeof value === 'function' ? value(open) : value; - if (setOpenProp) { - setOpenProp(openState); - } else { - _setOpen(openState); - } - - // This sets the cookie to keep the sidebar state. - document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; - }, - [setOpenProp, open] - ); - - // Helper to toggle the sidebar. - const toggleSidebar = React.useCallback(() => { - if (isMobile) return setOpenMobile(open => !open); - if (isTablet) return setOpenTablet(open => !open); - return setOpen(open => !open); - }, [isMobile, isTablet, setOpen, setOpenMobile, setOpenTablet]); - - // Adds a keyboard shortcut to toggle the sidebar. - React.useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if ( - event.key === SIDEBAR_KEYBOARD_SHORTCUT && - (event.metaKey || event.ctrlKey) - ) { - event.preventDefault(); - toggleSidebar(); - } - }; - - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [toggleSidebar]); - - // We add a state so that we can do data-state="expanded" or "collapsed". - // This makes it easier to style the sidebar with Tailwind classes. - const state = open ? 'expanded' : 'collapsed'; - - const contextValue = React.useMemo<SidebarContext>( - () => ({ - state, - open, - setOpen, - isMobile, - openMobile, - setOpenMobile, - isTablet, - setOpenTablet, - openTablet, - toggleSidebar, - }), - [ - state, - open, - setOpen, - isMobile, - openMobile, - setOpenMobile, - isTablet, - setOpenTablet, - openTablet, - toggleSidebar, - ] - ); - - return ( - <SidebarContext.Provider value={contextValue}> - <TooltipProvider delayDuration={0}> - <div - style={ - { - '--sidebar-width': SIDEBAR_WIDTH, - '--sidebar-width-icon': - SIDEBAR_WIDTH_ICON, - ...style, - } as React.CSSProperties - } - className={cn( - 'group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar', - className - )} - ref={ref} - {...props}> - {children} - </div> - </TooltipProvider> - </SidebarContext.Provider> - ); - } -); -SidebarProvider.displayName = 'SidebarProvider'; - -const Sidebar = React.forwardRef< - HTMLDivElement, - React.ComponentProps<'div'> & { - side?: 'left' | 'right'; - variant?: 'sidebar' | 'floating' | 'inset'; - collapsible?: 'offcanvas' | 'icon' | 'none'; - } ->( - ( - { - side = 'left', - variant = 'sidebar', - collapsible = 'offcanvas', - className, - children, - ...props - }, - ref - ) => { - const { - isMobile, - isTablet, - openTablet, - setOpenTablet, - state, - openMobile, - setOpenMobile, - } = useSidebar(); - - if (collapsible === 'none') { - return ( - <div - className={cn( - 'flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground', - className - )} - ref={ref} - {...props}> - {children} - </div> - ); - } - - if (isMobile) - return ( - <Drawer open={openMobile} onOpenChange={setOpenMobile} {...props}> - <DrawerTrigger asChild> - <div className="z-50 fixed bottom-[--p] right-[--p] flex flex-row items-center gap-x-2.5 bg-foreground p-3 rounded-full"> - <div className="relative size-full flex justify-center items-center "> - <SidebarTrigger className="absolute inset-auto z-50 opacity-0" /> - <LuPanelBottomOpen className="size-5 bg-background rounded-xl overflow-hidden" /> - </div> - </div> - </DrawerTrigger> - <DrawerContent - data-sidebar="sidebar" - data-mobile="true" - className="w-screen bg-sidebar p-[--external-p] text-sidebar-foreground [&>button]:hidden min-h-96" - style={ - { - '--sidebar-width': - SIDEBAR_WIDTH_MOBILE, - } as React.CSSProperties - }> - <div className="flex h-full w-full flex-col gap-[--external-p]"> - {children} - </div> - </DrawerContent> - </Drawer> - ); - if (isTablet) - return ( - <Sheet open={openTablet} onOpenChange={setOpenTablet} {...props}> - <SheetTrigger asChild> - <div className="z-50 fixed bottom-[--p] left-[--p] flex flex-row items-center gap-x-2.5 bg-foreground p-3 rounded-full"> - <div className="relative size-full flex justify-center items-center "> - <SidebarTrigger className="absolute inset-auto z-50 opacity-0" /> - {!openTablet ? ( - <TbLayoutSidebarRightCollapseFilled className="bg-background size-8" /> - ) : ( - <TbLayoutSidebarRightExpandFilled className="bg-background size-8" /> - )} - </div> - </div> - </SheetTrigger> - <SheetContent - data-sidebar="sidebar" - data-mobile="true" - className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden" - style={ - { - '--sidebar-width': - SIDEBAR_WIDTH_MOBILE, - } as React.CSSProperties - } - side={side}> - <div className="flex h-full w-full flex-col"> - {children} - </div> - </SheetContent> - </Sheet> - ); - return ( - <div - ref={ref} - className="group peer hidden md:block text-sidebar-foreground" - data-state={state} - data-collapsible={state === 'collapsed' ? collapsible : ''} - data-variant={variant} - data-side={side}> - {/* This is what handles the sidebar gap on desktop */} - <div - className={cn( - 'duration-200 relative h-svh w-[--sidebar-width] bg-transparent transition-[width] ease-linear ', - 'group-data-[collapsible=offcanvas]:w-[--p]', - // "group-data-[collapsible=offcanvas]:w-0", - 'group-data-[side=right]:rotate-180', - variant === 'floating' || variant === 'inset' - ? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]' - : 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]' - )} - /> - <div - className={cn( - 'duration-200 fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] ease-linear md:flex', - side === 'left' - ? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]' - : 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]', - // Adjust the padding for floating and inset variants. - variant === 'floating' || variant === 'inset' - ? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]' - : 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l', - '!bg-transparent !border-none' - )} - {...props}> - <div - data-sidebar="sidebar" - className={cn( - 'flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow', - className - )}> - {children} - </div> - </div> - </div> - ); - } -); -Sidebar.displayName = 'Sidebar'; - -const SidebarTrigger = React.forwardRef< - React.ElementRef<typeof Button>, - React.ComponentProps<typeof Button> ->(({ className, onClick, children, ...props }, ref) => { - const { toggleSidebar, open } = useSidebar(); - - return ( - <Button - ref={ref} - data-sidebar="trigger" - variant="ghost" - size="icon" - className={cn('size-8 [&>svg]:size-6 !p-0', className)} - onClick={event => { - onClick?.(event); - toggleSidebar(); - }} - {...props}> - {children ?? - (!open ? ( - <TbLayoutSidebarRightCollapseFilled className="size-8" /> - ) : ( - <TbLayoutSidebarRightExpandFilled className="size-8" /> - ))} - <span className="sr-only">Toggle Sidebar</span> - </Button> - ); -}); -SidebarTrigger.displayName = 'SidebarTrigger'; - -const SidebarRail = React.forwardRef<HTMLButtonElement, React.ComponentProps<'button'>>( - ({ className, ...props }, ref) => { - const { toggleSidebar } = useSidebar(); - - return ( - <button - ref={ref} - data-sidebar="rail" - aria-label="Toggle Sidebar" - tabIndex={-1} - onClick={toggleSidebar} - title="Toggle Sidebar" - className={cn( - 'absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex', - '[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize', - '[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize', - 'group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar', - '[[data-side=left][data-collapsible=offcanvas]_&]:-right-2', - '[[data-side=right][data-collapsible=offcanvas]_&]:-left-2', - className - )} - {...props} - /> - ); - } -); -SidebarRail.displayName = 'SidebarRail'; - -const SidebarInset = React.forwardRef<HTMLDivElement, React.ComponentProps<'main'>>( - ({ className, ...props }, ref) => { - return ( - <div - ref={ref} - className={cn( - 'relative flex max-h-svh h-full flex-1 flex-col', - 'peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow', - className - )} - {...props} - /> - ); - } -); -SidebarInset.displayName = 'SidebarInset'; - -const SidebarInput = React.forwardRef< - React.ElementRef<typeof Input>, - React.ComponentProps<typeof Input> ->(({ className, ...props }, ref) => { - return ( - <Input - ref={ref} - data-sidebar="input" - className={cn( - 'h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring', - className - )} - {...props} - /> - ); -}); -SidebarInput.displayName = 'SidebarInput'; - -const SidebarHeader = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>( - ({ className, ...props }, ref) => { - return ( - <div - ref={ref} - data-sidebar="header" - className={cn('flex flex-col gap-2 p-2', className)} - {...props} - /> - ); - } -); -SidebarHeader.displayName = 'SidebarHeader'; - -const SidebarFooter = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>( - ({ className, ...props }, ref) => { - return ( - <div - ref={ref} - data-sidebar="footer" - className={cn('flex flex-col gap-2 p-2', className)} - {...props} - /> - ); - } -); -SidebarFooter.displayName = 'SidebarFooter'; - -const SidebarSeparator = React.forwardRef< - React.ElementRef<typeof Separator>, - React.ComponentProps<typeof Separator> ->(({ className, ...props }, ref) => { - return ( - <Separator - ref={ref} - data-sidebar="separator" - className={cn('mx-2 w-auto bg-sidebar-border', className)} - {...props} - /> - ); -}); -SidebarSeparator.displayName = 'SidebarSeparator'; - -const SidebarContent = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>( - ({ className, ...props }, ref) => { - return ( - <div - ref={ref} - data-sidebar="content" - className={cn( - 'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden', - className - )} - {...props} - /> - ); - } -); -SidebarContent.displayName = 'SidebarContent'; - -const SidebarGroup = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>( - ({ className, ...props }, ref) => { - return ( - <div - ref={ref} - data-sidebar="group" - className={cn('relative flex w-full min-w-0 flex-col', className)} - {...props} - /> - ); - } -); -SidebarGroup.displayName = 'SidebarGroup'; - -const SidebarGroupLabel = React.forwardRef< - HTMLDivElement, - React.ComponentProps<'div'> & { asChild?: boolean } ->(({ className, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : 'div'; - - return ( - <Comp - ref={ref} - data-sidebar="group-label" - className={cn( - 'duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', - 'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0', - className - )} - {...props} - /> - ); -}); -SidebarGroupLabel.displayName = 'SidebarGroupLabel'; - -const SidebarGroupAction = React.forwardRef< - HTMLButtonElement, - React.ComponentProps<'button'> & { asChild?: boolean } ->(({ className, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : 'button'; - - return ( - <Comp - ref={ref} - data-sidebar="group-action" - className={cn( - 'absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', - // Increases the hit area of the button on mobile. - 'after:absolute after:-inset-2 after:md:hidden', - 'group-data-[collapsible=icon]:hidden', - className - )} - {...props} - /> - ); -}); -SidebarGroupAction.displayName = 'SidebarGroupAction'; - -const SidebarGroupContent = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>( - ({ className, ...props }, ref) => ( - <div - ref={ref} - data-sidebar="group-content" - className={cn('w-full text-sm', className)} - {...props} - /> - ) -); -SidebarGroupContent.displayName = 'SidebarGroupContent'; - -const SidebarMenu = React.forwardRef<HTMLUListElement, React.ComponentProps<'ul'>>( - ({ className, ...props }, ref) => ( - <ul - ref={ref} - data-sidebar="menu" - className={cn('flex w-full min-w-0 flex-col gap-1', className)} - {...props} - /> - ) -); -SidebarMenu.displayName = 'SidebarMenu'; - -const SidebarMenuItem = React.forwardRef<HTMLLIElement, React.ComponentProps<'li'>>( - ({ className, ...props }, ref) => ( - <li - ref={ref} - data-sidebar="menu-item" - className={cn('group/menu-item relative', className)} - {...props} - /> - ) -); -SidebarMenuItem.displayName = 'SidebarMenuItem'; - -const sidebarMenuButtonVariants = cva( - 'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0', - { - variants: { - variant: { - default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground', - outline: 'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]', - }, - size: { - default: 'h-8 text-sm', - sm: 'h-7 text-xs', - lg: 'h-12 text-sm group-data-[collapsible=icon]:!p-0', - }, - }, - defaultVariants: { - variant: 'default', - size: 'default', - }, - } -); - -const SidebarMenuButton = React.forwardRef< - HTMLButtonElement, - React.ComponentProps<'button'> & { - asChild?: boolean; - isActive?: boolean; - tooltip?: string | React.ComponentProps<typeof TooltipContent>; - } & VariantProps<typeof sidebarMenuButtonVariants> ->( - ( - { - asChild = false, - isActive = false, - variant = 'default', - size = 'default', - tooltip, - className, - ...props - }, - ref - ) => { - const Comp = asChild ? Slot : 'button'; - const { isMobile, state } = useSidebar(); - - const button = ( - <Comp - ref={ref} - data-sidebar="menu-button" - data-size={size} - data-active={isActive} - className={cn( - sidebarMenuButtonVariants({ variant, size }), - className - )} - {...props} - /> - ); - - if (!tooltip) { - return button; - } - - if (typeof tooltip === 'string') { - tooltip = { - children: tooltip, - }; - } - - return ( - <Tooltip> - <TooltipTrigger asChild>{button}</TooltipTrigger> - <TooltipContent - side="right" - align="center" - hidden={state !== 'collapsed' || isMobile} - {...tooltip} - /> - </Tooltip> - ); - } -); -SidebarMenuButton.displayName = 'SidebarMenuButton'; - -const SidebarMenuAction = React.forwardRef< - HTMLButtonElement, - React.ComponentProps<'button'> & { - asChild?: boolean; - showOnHover?: boolean; - } ->(({ className, asChild = false, showOnHover = false, ...props }, ref) => { - const Comp = asChild ? Slot : 'button'; - - return ( - <Comp - ref={ref} - data-sidebar="menu-action" - className={cn( - 'absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0', - // Increases the hit area of the button on mobile. - 'after:absolute after:-inset-2 after:md:hidden', - 'peer-data-[size=sm]/menu-button:top-1', - 'peer-data-[size=default]/menu-button:top-1.5', - 'peer-data-[size=lg]/menu-button:top-2.5', - 'group-data-[collapsible=icon]:hidden', - showOnHover && - 'group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0', - className - )} - {...props} - /> - ); -}); -SidebarMenuAction.displayName = 'SidebarMenuAction'; - -const SidebarMenuBadge = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>( - ({ className, ...props }, ref) => ( - <div - ref={ref} - data-sidebar="menu-badge" - className={cn( - 'absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground select-none pointer-events-none', - 'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground', - 'peer-data-[size=sm]/menu-button:top-1', - 'peer-data-[size=default]/menu-button:top-1.5', - 'peer-data-[size=lg]/menu-button:top-2.5', - 'group-data-[collapsible=icon]:hidden', - className - )} - {...props} - /> - ) -); -SidebarMenuBadge.displayName = 'SidebarMenuBadge'; - -const SidebarMenuSkeleton = React.forwardRef< - HTMLDivElement, - React.ComponentProps<'div'> & { - showIcon?: boolean; - } ->(({ className, showIcon = false, ...props }, ref) => { - // Random width between 50 to 90%. - const width = React.useMemo(() => { - return `${Math.floor(Math.random() * 40) + 50}%`; - }, []); - - return ( - <div - ref={ref} - data-sidebar="menu-skeleton" - className={cn('rounded-md h-8 flex gap-2 px-2 items-center', className)} - {...props}> - {showIcon && ( - <Skeleton - className="size-4 rounded-md" - data-sidebar="menu-skeleton-icon" - /> - )} - <Skeleton - className="h-4 flex-1 max-w-[--skeleton-width]" - data-sidebar="menu-skeleton-text" - style={ - { - '--skeleton-width': width, - } as React.CSSProperties - } - /> - </div> - ); -}); -SidebarMenuSkeleton.displayName = 'SidebarMenuSkeleton'; - -const SidebarMenuSub = React.forwardRef<HTMLUListElement, React.ComponentProps<'ul'>>( - ({ className, ...props }, ref) => ( - <ul - ref={ref} - data-sidebar="menu-sub" - className={cn( - 'mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5', - 'group-data-[collapsible=icon]:hidden', - className - )} - {...props} - /> - ) -); -SidebarMenuSub.displayName = 'SidebarMenuSub'; - -const SidebarMenuSubItem = React.forwardRef<HTMLLIElement, React.ComponentProps<'li'>>( - ({ ...props }, ref) => <li ref={ref} {...props} /> -); -SidebarMenuSubItem.displayName = 'SidebarMenuSubItem'; - -const SidebarMenuSubButton = React.forwardRef< - HTMLAnchorElement, - React.ComponentProps<'a'> & { - asChild?: boolean; - size?: 'sm' | 'md'; - isActive?: boolean; - } ->(({ asChild = false, size = 'md', isActive, className, ...props }, ref) => { - const Comp = asChild ? Slot : 'a'; - - return ( - <Comp - ref={ref} - data-sidebar="menu-sub-button" - data-size={size} - data-active={isActive} - className={cn( - 'flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground', - 'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground', - size === 'sm' && 'text-xs', - size === 'md' && 'text-sm', - 'group-data-[collapsible=icon]:hidden', - className - )} - {...props} - /> - ); -}); -SidebarMenuSubButton.displayName = 'SidebarMenuSubButton'; - -export { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarGroup, - SidebarGroupAction, - SidebarGroupContent, - SidebarGroupLabel, - SidebarHeader, - SidebarInput, - SidebarInset, - SidebarMenu, - SidebarMenuAction, - SidebarMenuBadge, - SidebarMenuButton, - SidebarMenuItem, - SidebarMenuSkeleton, - SidebarMenuSub, - SidebarMenuSubButton, - SidebarMenuSubItem, - SidebarProvider, - SidebarRail, - SidebarSeparator, - SidebarTrigger, - useSidebar, -}; diff --git a/ui/skeleton.tsx b/ui/skeleton.tsx deleted file mode 100644 index bf76e6c..0000000 --- a/ui/skeleton.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { cn } from '@/utils/shadcn'; - -function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) { - return <div className={cn('animate-pulse rounded-md bg-muted', className)} {...props} />; -} - -export { Skeleton }; diff --git a/ui/switch.tsx b/ui/switch.tsx deleted file mode 100644 index e608b29..0000000 --- a/ui/switch.tsx +++ /dev/null @@ -1,29 +0,0 @@ -'use client'; - -import * as React from 'react'; -import * as SwitchPrimitives from '@radix-ui/react-switch'; - -import { cn } from '@/utils/shadcn'; - -const Switch = React.forwardRef< - React.ElementRef<typeof SwitchPrimitives.Root> & { icon: React.ReactNode }, - React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> ->(({ className, icon, ...props }, ref) => ( - <SwitchPrimitives.Root - className={cn( - 'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-zinc-800 data-[state=unchecked]:bg-input', - className - )} - {...props} - ref={ref}> - <SwitchPrimitives.Thumb - className={cn( - 'pointer-events-none flex items-center justify-center h-full aspect-square rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:bg-black data-[state=checked]:text-white data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0.5' - )}> - {icon} - </SwitchPrimitives.Thumb> - </SwitchPrimitives.Root> -)); -Switch.displayName = SwitchPrimitives.Root.displayName; - -export { Switch }; diff --git a/ui/tabs.tsx b/ui/tabs.tsx deleted file mode 100644 index 8b6a73c..0000000 --- a/ui/tabs.tsx +++ /dev/null @@ -1,55 +0,0 @@ -"use client" - -import * as React from "react" -import * as TabsPrimitive from "@radix-ui/react-tabs" - -import { cn } from "@/utils/shadcn" - -const Tabs = TabsPrimitive.Root - -const TabsList = React.forwardRef< - React.ElementRef<typeof TabsPrimitive.List>, - React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> ->(({ className, ...props }, ref) => ( - <TabsPrimitive.List - ref={ref} - className={cn( - "inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground", - className - )} - {...props} - /> -)) -TabsList.displayName = TabsPrimitive.List.displayName - -const TabsTrigger = React.forwardRef< - React.ElementRef<typeof TabsPrimitive.Trigger>, - React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> ->(({ className, ...props }, ref) => ( - <TabsPrimitive.Trigger - ref={ref} - className={cn( - "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm", - className - )} - {...props} - /> -)) -TabsTrigger.displayName = TabsPrimitive.Trigger.displayName - -const TabsContent = React.forwardRef< - React.ElementRef<typeof TabsPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content> ->(({ className, ...props }, ref) => ( - <TabsPrimitive.Content - ref={ref} - className={cn( - "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", - className - )} - {...props} - /> -)) -TabsContent.displayName = TabsPrimitive.Content.displayName - -export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/ui/toast.tsx b/ui/toast.tsx deleted file mode 100644 index 0a35c3f..0000000 --- a/ui/toast.tsx +++ /dev/null @@ -1,92 +0,0 @@ -'use client'; - -import * as React from 'react'; -import * as ToastPrimitives from '@radix-ui/react-toast'; -import { cva, type VariantProps } from 'class-variance-authority'; -import { X } from 'lucide-react'; - -import { cn } from '@/utils/shadcn'; - -const ToastProvider = ToastPrimitives.Provider; - -const ToastViewport = React.forwardRef< - React.ElementRef<typeof ToastPrimitives.Viewport>, - React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport> ->(({ className, ...props }, ref) => ( - <ToastPrimitives.Viewport - ref={ref} - className={cn('fixed bottom-[--p] right-[--p] top-auto z-[100] flex flex-col max-h-screen w-full p-0 max-w-[min(55vw,420px)]', className)} - {...props} - /> -)); -ToastViewport.displayName = ToastPrimitives.Viewport.displayName; - -const toastVariants = cva( - 'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-[--p] shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-right-full', - { - variants: { - variant: { - default: 'border bg-background text-foreground', - destructive: 'destructive group border-destructive bg-destructive text-destructive-foreground', - }, - }, - defaultVariants: { - variant: 'default', - }, - }, -); - -const Toast = React.forwardRef< - React.ElementRef<typeof ToastPrimitives.Root>, - React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & VariantProps<typeof toastVariants> ->(({ className, variant, ...props }, ref) => { - return <ToastPrimitives.Root ref={ref} className={cn(toastVariants({ variant }), className)} {...props} />; -}); -Toast.displayName = ToastPrimitives.Root.displayName; - -const ToastAction = React.forwardRef<React.ElementRef<typeof ToastPrimitives.Action>, React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>>( - ({ className, ...props }, ref) => ( - <ToastPrimitives.Action - ref={ref} - className={cn( - 'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive', - className, - )} - {...props} - /> - ), -); -ToastAction.displayName = ToastPrimitives.Action.displayName; - -const ToastClose = React.forwardRef<React.ElementRef<typeof ToastPrimitives.Close>, React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>>( - ({ className, ...props }, ref) => ( - <ToastPrimitives.Close - ref={ref} - className={cn( - 'absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600', - className, - )} - toast-close="" - {...props}> - <X className="h-4 w-4" /> - </ToastPrimitives.Close> - ), -); -ToastClose.displayName = ToastPrimitives.Close.displayName; - -const ToastTitle = React.forwardRef<React.ElementRef<typeof ToastPrimitives.Title>, React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>>( - ({ className, ...props }, ref) => <ToastPrimitives.Title ref={ref} className={cn('text-sm font-semibold', className)} {...props} />, -); -ToastTitle.displayName = ToastPrimitives.Title.displayName; - -const ToastDescription = React.forwardRef< - React.ElementRef<typeof ToastPrimitives.Description>, - React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description> ->(({ className, ...props }, ref) => <ToastPrimitives.Description ref={ref} className={cn('text-xs md:text-sm opacity-90', className)} {...props} />); -ToastDescription.displayName = ToastPrimitives.Description.displayName; - -type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>; - -type ToastActionElement = React.ReactElement<typeof ToastAction>; - -export { type ToastProps, type ToastActionElement, ToastProvider, ToastViewport, Toast, ToastTitle, ToastDescription, ToastClose, ToastAction }; diff --git a/ui/toaster.tsx b/ui/toaster.tsx deleted file mode 100644 index b1276d6..0000000 --- a/ui/toaster.tsx +++ /dev/null @@ -1,53 +0,0 @@ -'use client'; - -import CircularProgress from '@/components/check-progress'; -import Typewriter from '@/components/typewriter'; -import { useToast } from '@/hooks/use-toast'; -import { - Toast, - ToastClose, - ToastDescription, - ToastProvider, - ToastTitle, - ToastViewport, -} from '@/ui/toast'; - -export function Toaster() { - const { toasts } = useToast(); - - return ( - <ToastProvider> - {toasts.map(function ({ id, title, description, action, ...props }) { - return ( - <Toast key={id} {...props}> - <div className="grid gap-1"> - {title && ( - <ToastTitle className="flex flex-row items-center justify-start gap-x-3"> - <div className="size-5"> - <CircularProgress /> - </div> - <Typewriter - text={title} - speed={23} - /> - </ToastTitle> - )} - {description && ( - <ToastDescription> - <Typewriter - text={description} - delay={585} - speed={10} - /> - </ToastDescription> - )} - </div> - {action} - <ToastClose /> - </Toast> - ); - })} - <ToastViewport /> - </ToastProvider> - ); -} diff --git a/ui/toggle-group.tsx b/ui/toggle-group.tsx deleted file mode 100644 index 567faf3..0000000 --- a/ui/toggle-group.tsx +++ /dev/null @@ -1,61 +0,0 @@ -"use client" - -import * as React from "react" -import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group" -import { type VariantProps } from "class-variance-authority" - -import { cn } from "@/utils/shadcn" -import { toggleVariants } from "@/ui/toggle" - -const ToggleGroupContext = React.createContext< - VariantProps<typeof toggleVariants> ->({ - size: "default", - variant: "default", -}) - -const ToggleGroup = React.forwardRef< - React.ElementRef<typeof ToggleGroupPrimitive.Root>, - React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> & - VariantProps<typeof toggleVariants> ->(({ className, variant, size, children, ...props }, ref) => ( - <ToggleGroupPrimitive.Root - ref={ref} - className={cn("flex items-center justify-center gap-1", className)} - {...props} - > - <ToggleGroupContext.Provider value={{ variant, size }}> - {children} - </ToggleGroupContext.Provider> - </ToggleGroupPrimitive.Root> -)) - -ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName - -const ToggleGroupItem = React.forwardRef< - React.ElementRef<typeof ToggleGroupPrimitive.Item>, - React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> & - VariantProps<typeof toggleVariants> ->(({ className, children, variant, size, ...props }, ref) => { - const context = React.useContext(ToggleGroupContext) - - return ( - <ToggleGroupPrimitive.Item - ref={ref} - className={cn( - toggleVariants({ - variant: context.variant || variant, - size: context.size || size, - }), - className - )} - {...props} - > - {children} - </ToggleGroupPrimitive.Item> - ) -}) - -ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName - -export { ToggleGroup, ToggleGroupItem } diff --git a/ui/toggle.tsx b/ui/toggle.tsx deleted file mode 100644 index a5b2fe5..0000000 --- a/ui/toggle.tsx +++ /dev/null @@ -1,45 +0,0 @@ -"use client" - -import * as React from "react" -import * as TogglePrimitive from "@radix-ui/react-toggle" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/utils/shadcn" - -const toggleVariants = cva( - "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 gap-2", - { - variants: { - variant: { - default: "bg-transparent", - outline: - "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground", - }, - size: { - default: "h-10 px-3 min-w-10", - sm: "h-9 px-2.5 min-w-9", - lg: "h-11 px-5 min-w-11", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -) - -const Toggle = React.forwardRef< - React.ElementRef<typeof TogglePrimitive.Root>, - React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> & - VariantProps<typeof toggleVariants> ->(({ className, variant, size, ...props }, ref) => ( - <TogglePrimitive.Root - ref={ref} - className={cn(toggleVariants({ variant, size, className }))} - {...props} - /> -)) - -Toggle.displayName = TogglePrimitive.Root.displayName - -export { Toggle, toggleVariants } diff --git a/ui/tooltip.tsx b/ui/tooltip.tsx deleted file mode 100644 index ebf60d0..0000000 --- a/ui/tooltip.tsx +++ /dev/null @@ -1,30 +0,0 @@ -'use client'; - -import * as React from 'react'; -import * as TooltipPrimitive from '@radix-ui/react-tooltip'; - -import { cn } from '@/utils/shadcn'; - -const TooltipProvider = TooltipPrimitive.Provider; - -const Tooltip = TooltipPrimitive.Root; - -const TooltipTrigger = TooltipPrimitive.Trigger; - -const TooltipContent = React.forwardRef< - React.ElementRef<typeof TooltipPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> ->(({ className, sideOffset = 4, ...props }, ref) => ( - <TooltipPrimitive.Content - ref={ref} - sideOffset={sideOffset} - className={cn( - 'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', - className - )} - {...props} - /> -)); -TooltipContent.displayName = TooltipPrimitive.Content.displayName; - -export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/utils/_try.ts b/utils/_try.ts deleted file mode 100644 index 719c31d..0000000 --- a/utils/_try.ts +++ /dev/null @@ -1,20 +0,0 @@ -export default async function _try<R extends () => any>( - action: R, - on = { - success: (data: Awaited<ReturnType<R>>): any => Response.json({ success: 1, data }), - error: ({ cause, message, stack }: Error) => { - console.error( - `| Cause => ${cause}\n| Error message in MIDDLE handlers => ${message}\n`, - `\n| Stack => ${stack}` - ); - - return Response.json({ success: 0, message }); - }, - } -) { - try { - return on.success(await action()); - } catch (error) { - return on.error(error as Error); - } -} diff --git a/utils/api-fetch.ts b/utils/api-fetch.ts deleted file mode 100644 index d15d7c8..0000000 --- a/utils/api-fetch.ts +++ /dev/null @@ -1,595 +0,0 @@ -let idCounter = 1000; -const createId = () => `${Date.now()}-${++idCounter}`; - -const createDemoPost = (section: string, overrides: Record<string, any> = {}): Record<string, any> => { - const id = overrides.id ?? createId(); - return { - id, - _id: id, - name: overrides.name ?? `Demo ${section} entry`, - title: overrides.title ?? 'Demo Title', - description: overrides.description ?? 'Demo content for presentation purposes.', - state: overrides.state ?? 'Draft', - kind: overrides.kind ?? 'General', - date: overrides.date ?? new Date().toISOString(), - image: - overrides.image ?? - ({ - public_id: 'sample', - display_name: 'demo-image', - } as const), - group: overrides.group ?? ({ _id: 'group-demo', name: 'Demo Group' } as const), - sub_group: overrides.sub_group ?? ({ _id: 'subgroup-demo', name: 'Demo Subgroup' } as const), - ...overrides, - }; -}; - -const demoStore = { - sidebar: { - featured: [ - { - id: 'grp-featured-2025', - name: '2025 Highlights', - sub_groups: [ - { - id: 'sg-featured-january', - name: 'January', - items: [ - createDemoPost('featured', { - id: 'post-digital-revolution', - name: 'The Digital Revolution', - title: 'Embracing Digital Transformation in Modern Business', - description: - 'Explore how digital technologies are reshaping industries and creating new opportunities for growth and innovation.', - kind: 'Featured', - state: 'Published', - image: { public_id: '/demo/digital-innovation-01.webp', display_name: 'Digital Innovation' }, - date: new Date('2025-01-15').toISOString(), - }), - createDemoPost('featured', { - id: 'post-sustainable-design', - name: 'Sustainable Design', - title: 'Building a Sustainable Future Through Thoughtful Design', - description: - 'How designers and architects are integrating sustainability into their creative processes.', - kind: 'Featured', - state: 'Published', - image: { public_id: '/demo/architecture-design-01.jpg', display_name: 'Sustainable Architecture' }, - date: new Date('2025-01-10').toISOString(), - }), - ], - }, - { - id: 'sg-featured-february', - name: 'February', - items: [ - createDemoPost('featured', { - id: 'post-community-impact', - name: 'Community Impact', - title: 'Creating Meaningful Impact Through Community Engagement', - description: 'Case studies of organizations making real differences in their local communities.', - kind: 'Featured', - state: 'Published', - image: { public_id: '/demo/community-engagement-01.jpg', display_name: 'Community Engagement' }, - date: new Date('2025-02-05').toISOString(), - }), - ], - }, - ], - }, - ], - design: [ - { - id: 'grp-design-digital', - name: 'Digital Design', - sub_groups: [ - { - id: 'sg-design-ui-ux', - name: 'UI/UX Trends', - items: [ - createDemoPost('design', { - id: 'post-ux-2025', - name: 'UX Trends 2025', - title: 'User Experience Trends Defining Digital Design in 2025', - description: - 'From adaptive interfaces to AI-driven personalization, discover the UX innovations shaping the digital landscape.', - kind: 'Tutorial', - state: 'Published', - image: { public_id: '/demo/digital-interface-01.png', display_name: 'Digital Interface' }, - date: new Date('2025-01-20').toISOString(), - }), - createDemoPost('design', { - id: 'post-accessibility', - name: 'Accessible Design', - title: 'Building Inclusive Digital Experiences for Everyone', - description: - 'Technical guide on implementing WCAG standards and creating truly accessible interfaces.', - kind: 'Guide', - state: 'Published', - image: { public_id: '/demo/digital-interface-02.png', display_name: 'Accessibility Design' }, - date: new Date('2025-01-28').toISOString(), - }), - ], - }, - { - id: 'sg-design-visual', - name: 'Visual Systems', - items: [ - createDemoPost('design', { - id: 'post-color-psychology', - name: 'Color Psychology', - title: 'The Psychology Behind Color Choices in Design', - description: 'Understanding how colors influence user perception and behavior in digital products.', - kind: 'Analysis', - state: 'Published', - image: { public_id: '/demo/board-culture.jpg', display_name: 'Color Theory' }, - date: new Date('2025-02-01').toISOString(), - }), - createDemoPost('design', { - id: 'post-typography', - name: 'Typography Mastery', - title: 'Font Selection and Typography That Elevates Your Design', - description: - 'Deep dive into choosing and pairing typefaces for optimal readability and aesthetic impact.', - kind: 'Guide', - state: 'Draft', - image: { public_id: '/demo/digital-interface-01.png', display_name: 'Typography' }, - date: new Date('2025-02-10').toISOString(), - }), - ], - }, - ], - }, - { - id: 'grp-design-print', - name: 'Print & Branding', - sub_groups: [ - { - id: 'sg-design-branding', - name: 'Brand Strategy', - items: [ - createDemoPost('design', { - id: 'post-brand-identity', - name: 'Brand Identity Development', - title: 'Creating a Cohesive Brand Identity Across All Touchpoints', - description: 'Strategic framework for building recognizable and memorable brands.', - kind: 'Case Study', - state: 'Published', - image: { public_id: '/demo/fashion-exhibition-01.jpg', display_name: 'Brand Identity' }, - date: new Date('2025-01-25').toISOString(), - }), - ], - }, - ], - }, - ], - culture: [ - { - id: 'grp-culture-exhibitions', - name: 'Exhibitions & Events', - sub_groups: [ - { - id: 'sg-culture-current', - name: 'Current Shows', - items: [ - createDemoPost('culture', { - id: 'post-installation-art', - name: 'Installation Art Showcase', - title: 'Immersive Installations Pushing Artistic Boundaries', - description: - 'Curated collection of contemporary installations exploring space, light, and viewer interaction.', - kind: 'Exhibition', - state: 'Published', - image: { public_id: '/demo/installation-art-01.jpg', display_name: 'Installation Art' }, - date: new Date('2025-02-15').toISOString(), - }), - createDemoPost('culture', { - id: 'post-fashion-week', - name: 'Fashion Week Coverage', - title: "This Season's Fashion: Innovation Meets Tradition", - description: 'Comprehensive coverage of emerging designers and trendsetting collections.', - kind: 'Exhibition', - state: 'Published', - image: { public_id: '/demo/fashion-exhibition-01.jpg', display_name: 'Fashion Exhibition' }, - date: new Date('2025-02-12').toISOString(), - }), - ], - }, - { - id: 'sg-culture-past', - name: 'Past Exhibitions', - items: [ - createDemoPost('culture', { - id: 'post-retrospective', - name: 'Annual Retrospective 2024', - title: 'Reflecting on Cultural Highlights from 2024', - description: 'A look back at the most significant cultural moments and exhibitions of the past year.', - kind: 'Feature', - state: 'Published', - image: { public_id: '/demo/installation-art-01.jpg', display_name: 'Retrospective' }, - date: new Date('2025-01-05').toISOString(), - }), - ], - }, - ], - }, - { - id: 'grp-culture-interviews', - name: 'Interviews & Profiles', - sub_groups: [ - { - id: 'sg-culture-artists', - name: 'Artist Profiles', - items: [ - createDemoPost('culture', { - id: 'post-artist-interview', - name: 'In Conversation: Contemporary Artists', - title: 'Dialogue with Leading Voices in Contemporary Art', - description: 'Exclusive interviews with artists discussing their practice, inspiration, and vision.', - kind: 'Interview', - state: 'Published', - image: { public_id: '/demo/digital-innovation-01.webp', display_name: 'Artist Profile' }, - date: new Date('2025-02-08').toISOString(), - }), - ], - }, - ], - }, - ], - insights: [ - { - id: 'grp-insights-analysis', - name: 'Trend Analysis', - sub_groups: [ - { - id: 'sg-insights-industry', - name: 'Industry Reports', - items: [ - createDemoPost('insights', { - id: 'post-market-analysis', - name: 'Q1 2025 Market Analysis', - title: 'Key Insights: Market Trends and Economic Indicators', - description: - 'Detailed analysis of emerging market trends, consumer behavior shifts, and economic forecasts.', - kind: 'Analysis', - state: 'Published', - image: { public_id: '/demo/digital-interface-01.png', display_name: 'Market Analysis' }, - date: new Date('2025-02-03').toISOString(), - }), - createDemoPost('insights', { - id: 'post-tech-forecast', - name: 'Technology Forecast', - title: "What's Next: Technology Predictions for 2025", - description: - 'Expert perspective on AI, quantum computing, renewable energy, and emerging technologies.', - kind: 'Forecast', - state: 'Published', - image: { public_id: '/demo/digital-innovation-01.webp', display_name: 'Tech Forecast' }, - date: new Date('2025-01-30').toISOString(), - }), - ], - }, - { - id: 'sg-insights-thought', - name: 'Thought Leadership', - items: [ - createDemoPost('insights', { - id: 'post-future-work', - name: 'The Future of Work', - title: 'Reimagining Work: Remote, Hybrid, and Beyond', - description: - 'Exploring organizational transformation, employee wellbeing, and the evolving workplace.', - kind: 'Opinion', - state: 'Published', - image: { public_id: '/demo/digital-interface-02.png', display_name: 'Future of Work' }, - date: new Date('2025-02-06').toISOString(), - }), - ], - }, - ], - }, - ], - resources: [ - { - id: 'grp-resources-guides', - name: 'Guides & Tutorials', - sub_groups: [ - { - id: 'sg-resources-technical', - name: 'Technical Guides', - items: [ - createDemoPost('resources', { - id: 'post-web-performance', - name: 'Web Performance Optimization', - title: 'Complete Guide to Building Fast, Responsive Websites', - description: - 'Technical deep-dive on optimization techniques, Core Web Vitals, and performance metrics.', - kind: 'Technical', - state: 'Published', - image: { public_id: '/demo/digital-interface-01.png', display_name: 'Performance Guide' }, - date: new Date('2025-01-18').toISOString(), - }), - createDemoPost('resources', { - id: 'post-security-best', - name: 'Security Best Practices', - title: 'Securing Your Digital Assets: A Comprehensive Checklist', - description: 'Essential security practices for protecting data, systems, and user information.', - kind: 'Guide', - state: 'Published', - image: { public_id: '/demo/digital-interface-02.png', display_name: 'Security Guide' }, - date: new Date('2025-02-02').toISOString(), - }), - ], - }, - { - id: 'sg-resources-tools', - name: 'Tools & Resources', - items: [ - createDemoPost('resources', { - id: 'post-design-tools', - name: "Designer's Toolkit 2025", - title: 'Essential Tools and Resources for Modern Designers', - description: 'Curated collection of software, plugins, and assets to enhance your design workflow.', - kind: 'Resource', - state: 'Draft', - image: { public_id: '/demo/board-culture.jpg', display_name: 'Design Tools' }, - date: new Date('2025-02-11').toISOString(), - }), - ], - }, - ], - }, - ], - } as Record<string, any[]>, - home: [ - { section: 'hero', public_id: '/demo/digital-innovation-01.webp', display_name: 'Hero - Digital Innovation' }, - { section: 'featured', public_id: '/demo/architecture-design-01.jpg', display_name: 'Featured Stories' }, - { section: 'insights', public_id: '/demo/installation-art-01.jpg', display_name: 'Latest Insights' }, - { section: 'contact', public_id: '/demo/board-culture.jpg', display_name: 'Get In Touch' }, - ], - advisory: { - id: 'advisory-demo', - about: 'Our editorial advisory brings together industry leaders, researchers, and practitioners to guide content quality and ensure relevance.', - services: 'Content Strategy\nEditorial Services\nThought Leadership Program', - image: { public_id: '/demo/board-culture.jpg', display_name: 'Editorial Advisory Board' }, - }, -}; - -const clone = <T>(value: T): T => JSON.parse(JSON.stringify(value)); - -const findPostInSidebar = (id: string) => { - for (const groups of Object.values(demoStore.sidebar)) { - for (const group of groups) { - for (const subGroup of group.sub_groups) { - const post = subGroup.items.find((item: any) => item.id === id || item._id === id); - if (post) return post; - } - } - } - - return null; -}; - -export default async (path: string, options?: Omit<RequestInit, 'body'> & { body?: any }) => { - const method = (options?.method ?? 'GET').toUpperCase(); - const body = options?.body ?? {}; - - const sidebarGetMatch = path.match(/^\/sidebar\/([^/]+)$/); - if (method === 'GET' && sidebarGetMatch) { - const section = sidebarGetMatch[1]; - return { - success: 1, - data: clone(demoStore.sidebar[section] ?? []), - message: 'Demo mode: sidebar groups loaded.', - } as ApiRes; - } - - const sidebarCreateGroupMatch = path.match(/^\/sidebar\/([^/]+)\/group\/id$/); - if (method === 'POST' && sidebarCreateGroupMatch) { - const section = sidebarCreateGroupMatch[1]; - const group = { id: createId(), name: body.name ?? 'New Group', sub_groups: [] }; - demoStore.sidebar[section] ??= []; - demoStore.sidebar[section].push(group); - return { success: 1, data: clone(group), message: 'Demo mode: group created.' } as ApiRes; - } - - const sidebarCreateSubGroupMatch = path.match(/^\/sidebar\/([^/]+)\/subgroup\/id$/); - if (method === 'POST' && sidebarCreateSubGroupMatch) { - const section = sidebarCreateSubGroupMatch[1]; - const groups = demoStore.sidebar[section] ?? []; - const group = groups.find((g: any) => g.id === body.group?.id); - if (!group) return { success: 0, data: null, message: 'Group not found.' } as ApiRes; - const subGroup = { id: createId(), name: body.name ?? 'New Subgroup', items: [] }; - group.sub_groups.push(subGroup); - return { success: 1, data: clone(subGroup), message: 'Demo mode: subgroup created.' } as ApiRes; - } - - const sidebarCreatePostMatch = path.match(/^\/sidebar\/([^/]+)\/id$/); - if (method === 'POST' && sidebarCreatePostMatch) { - const section = sidebarCreatePostMatch[1]; - const groups = demoStore.sidebar[section] ?? []; - const group = groups.find((g: any) => g.id === body.group?.id); - const subGroup = group?.sub_groups.find((s: any) => s.id === body.sub_group?.id); - if (!subGroup) return { success: 0, data: null, message: 'Subgroup not found.' } as ApiRes; - const post = createDemoPost(section, { - name: body.name, - group: { _id: group.id, name: group.name }, - sub_group: { _id: subGroup.id, name: subGroup.name }, - }); - subGroup.items.push(post); - return { success: 1, data: clone(post), message: 'Demo mode: post created.' } as ApiRes; - } - - const sidebarRenameGroupMatch = path.match(/^\/sidebar\/([^/]+)\/group\/([^/]+)$/); - if (method === 'PATCH' && sidebarRenameGroupMatch) { - const section = sidebarRenameGroupMatch[1]; - const groupId = sidebarRenameGroupMatch[2]; - const group = (demoStore.sidebar[section] ?? []).find((g: any) => g.id === groupId); - if (group) group.name = body.name ?? group.name; - return { success: 1, data: clone(group), message: 'Demo mode: group updated.' } as ApiRes; - } - - const sidebarRenameSubGroupMatch = path.match(/^\/sidebar\/([^/]+)\/subgroup\/([^/]+)$/); - if (method === 'PATCH' && sidebarRenameSubGroupMatch) { - const section = sidebarRenameSubGroupMatch[1]; - const subGroupId = sidebarRenameSubGroupMatch[2]; - for (const group of demoStore.sidebar[section] ?? []) { - const subGroup = group.sub_groups.find((s: any) => s.id === subGroupId); - if (subGroup) { - subGroup.name = body.name ?? subGroup.name; - return { - success: 1, - data: clone(subGroup), - message: 'Demo mode: subgroup updated.', - } as ApiRes; - } - } - } - - const sidebarRenamePostMatch = path.match(/^\/sidebar\/([^/]+)\/([^/]+)$/); - if (method === 'PATCH' && sidebarRenamePostMatch) { - const postId = sidebarRenamePostMatch[2]; - const post = findPostInSidebar(postId); - if (post) post.name = body.name ?? post.name; - return { success: 1, data: clone(post), message: 'Demo mode: post updated.' } as ApiRes; - } - - const sidebarDeleteGroupMatch = path.match(/^\/sidebar\/([^/]+)\/group\/([^/]+)$/); - if (method === 'DELETE' && sidebarDeleteGroupMatch) { - const section = sidebarDeleteGroupMatch[1]; - const groupId = sidebarDeleteGroupMatch[2]; - demoStore.sidebar[section] = (demoStore.sidebar[section] ?? []).filter((group: any) => group.id !== groupId); - return { success: 1, data: null, message: 'Demo mode: group deleted.' } as ApiRes; - } - - const sidebarDeleteSubGroupMatch = path.match(/^\/sidebar\/([^/]+)\/subgroup\/([^/]+)$/); - if (method === 'DELETE' && sidebarDeleteSubGroupMatch) { - const section = sidebarDeleteSubGroupMatch[1]; - const subGroupId = sidebarDeleteSubGroupMatch[2]; - for (const group of demoStore.sidebar[section] ?? []) { - group.sub_groups = group.sub_groups.filter((subGroup: any) => subGroup.id !== subGroupId); - } - return { success: 1, data: null, message: 'Demo mode: subgroup deleted.' } as ApiRes; - } - - const sidebarDeletePostMatch = path.match(/^\/sidebar\/([^/]+)\/([^/]+)$/); - if (method === 'DELETE' && sidebarDeletePostMatch) { - const section = sidebarDeletePostMatch[1]; - const postId = sidebarDeletePostMatch[2]; - for (const group of demoStore.sidebar[section] ?? []) { - for (const subGroup of group.sub_groups) { - subGroup.items = subGroup.items.filter((item: any) => item.id !== postId && item._id !== postId); - } - } - return { success: 1, data: null, message: 'Demo mode: post deleted.' } as ApiRes; - } - - const insetGetPostMatch = path.match(/^\/inset\/([^/]+)\/([^/]+)$/); - if (method === 'GET' && insetGetPostMatch) { - const section = insetGetPostMatch[1]; - const postId = insetGetPostMatch[2]; - const existing = findPostInSidebar(postId); - const post = - existing ?? - createDemoPost(section, { - id: postId, - _id: postId, - name: `Demo ${section} post`, - }); - return { - success: 1, - data: clone(post), - message: 'Demo mode: post loaded.', - } as ApiRes; - } - - const insetUpdateImageMatch = path.match(/^\/inset\/([^/]+)\/([^/]+)\/image$/); - if (method === 'PATCH' && insetUpdateImageMatch) { - const postId = insetUpdateImageMatch[2]; - const post = findPostInSidebar(postId); - const updatedImage = { - public_id: `demo-image-${postId}`, - display_name: body.display_name ?? 'demo-image', - }; - if (post) post.image = updatedImage; - return { - success: 1, - data: clone(updatedImage), - message: 'Demo mode: image updated.', - } as ApiRes; - } - - const insetUpdatePostMatch = path.match(/^\/inset\/([^/]+)\/([^/]+)\/post$/); - if (method === 'PATCH' && insetUpdatePostMatch) { - const postId = insetUpdatePostMatch[2]; - const post = findPostInSidebar(postId); - if (post) Object.assign(post, body); - return { - success: 1, - data: clone(post ?? body), - message: 'Demo mode: post updated.', - } as ApiRes; - } - - if (method === 'GET' && path === '/inset/home') { - return { success: 1, data: clone(demoStore.home), message: 'Demo mode: home images loaded.' } as ApiRes; - } - - const homeImageMatch = path.match(/^\/inset\/home\/([^/]+)\/image$/); - if (method === 'PATCH' && homeImageMatch) { - const section = homeImageMatch[1]; - const current = demoStore.home.find(item => item.section === section); - const updated = { - section, - public_id: current?.public_id ?? 'sample', - display_name: body.display_name ?? current?.display_name ?? `${section}-image`, - }; - demoStore.home = demoStore.home.map(item => (item.section === section ? updated : item)); - return { success: 1, data: clone(updated), message: 'Demo mode: home image updated.' } as ApiRes; - } - - if (method === 'GET' && path === '/inset/advisory') { - return { - success: 1, - data: clone(demoStore.advisory), - message: 'Demo mode: advisory data loaded.', - } as ApiRes; - } - - if (method === 'PATCH' && path === '/inset/advisory') { - demoStore.advisory = { - ...demoStore.advisory, - ...body, - }; - return { success: 1, data: clone(demoStore.advisory), message: 'Demo mode: advisory updated.' } as ApiRes; - } - - if (method === 'PATCH' && path === '/inset/advisory/image') { - demoStore.advisory.image = { - public_id: body.public_id ?? demoStore.advisory.image.public_id, - display_name: body.display_name ?? demoStore.advisory.image.display_name, - }; - return { - success: 1, - data: clone(demoStore.advisory.image), - message: 'Demo mode: advisory image updated.', - } as ApiRes; - } - - if (method === 'POST' && path === '/auth/admin') { - const isValid = !!body?.username?.trim?.() && !!body?.secretToken?.trim?.(); - return { - success: isValid ? 1 : 0, - data: { authorized: isValid }, - message: isValid ? 'Demo mode: access granted.' : 'Demo mode: missing credentials.', - } as ApiRes; - } - - return { - success: 1, - data: clone(body ?? null), - message: `Demo mode: ${method} ${path} simulated locally.`, - } as ApiRes; -}; - -export type ApiRes<T = any> = { success: 0 | 1; data: T; message: string }; diff --git a/utils/objectId_check.ts b/utils/objectId_check.ts deleted file mode 100644 index 282d24f..0000000 --- a/utils/objectId_check.ts +++ /dev/null @@ -1,12 +0,0 @@ -'only server'; -import { Post, Post_serverOnly } from '@/types/post'; -import { Types } from 'mongoose'; - -export const post = (post: Post) => { - delete post._id; - return { - ...post, - group: { ...post.group, _id: new Types.ObjectId(post.group._id) }, - sub_group: { ...post.sub_group, _id: new Types.ObjectId(post.sub_group._id) }, - } as Post_serverOnly; -}; diff --git a/utils/revalidate.ts b/utils/revalidate.ts deleted file mode 100644 index 2d29088..0000000 --- a/utils/revalidate.ts +++ /dev/null @@ -1,12 +0,0 @@ -export default async ({ kind, tags, paths }: { kind: 'tags' | 'paths'; tags?: string[]; paths?: string[] }) => { - if (kind === 'tags' && (!tags || !tags.some(tag => tag.trim().length))) return; - if (kind === 'paths' && (!paths || !paths.some(path => path.trim().length))) return; - - const value = (kind === 'paths' ? paths : tags).filter(path => path.trim().length); - - return { - success: 1, - message: `Demo mode: revalidate ${kind} skipped.`, - data: { [kind]: value }, - }; -}; diff --git a/utils/shadcn.ts b/utils/shadcn.ts deleted file mode 100644 index bd0c391..0000000 --- a/utils/shadcn.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { clsx, type ClassValue } from "clsx" -import { twMerge } from "tailwind-merge" - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) -} diff --git a/utils/switch-theme.ts b/utils/switch-theme.ts deleted file mode 100644 index 3d70c28..0000000 --- a/utils/switch-theme.ts +++ /dev/null @@ -1,31 +0,0 @@ -export default (invert = true) => { - const dark = () => { - localStorage.setItem('theme', 'dark'); - document.querySelector('meta[name="theme-color"]').setAttribute('content', '#0a0a0a'); - document.querySelectorAll('[dark]').forEach(el => el.setAttribute('dark', '1')); - document.documentElement.classList.add('dark'); - document.documentElement.style.colorScheme = 'dark'; - }; - const light = () => { - localStorage.setItem('theme', 'light'); - document.querySelector('meta[name="theme-color"]').setAttribute('content', '#ffffff'); - document.querySelectorAll('[dark]').forEach(el => el.setAttribute('dark', '0')); - document.documentElement.classList.remove('dark'); - document.documentElement.style.colorScheme = 'light'; - }; - - switch (localStorage.theme) { - case invert ? 'light' : 'dark': { - dark(); - break; - } - case invert ? 'dark' : 'light': { - light(); - break; - } - default: - light(); - } - - return localStorage.theme; -}; diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..bc3574f --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,11 @@ +import { fileURLToPath } from 'node:url'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + resolve: { alias: { '@': fileURLToPath(new URL('.', import.meta.url)) } }, + test: { + environment: 'node', + include: ['tests/unit/**/*.test.ts', 'tests/contracts/**/*.test.ts'], + passWithNoTests: false, + }, +}); diff --git a/vitest.integration.config.ts b/vitest.integration.config.ts new file mode 100644 index 0000000..fab39fa --- /dev/null +++ b/vitest.integration.config.ts @@ -0,0 +1,12 @@ +import { fileURLToPath } from 'node:url'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + resolve: { alias: { '@': fileURLToPath(new URL('.', import.meta.url)) } }, + test: { + environment: 'node', + include: ['tests/integration/**/*.test.ts'], + fileParallelism: false, + passWithNoTests: true, + }, +});